Go语言AI开发避坑指南:LangChainGo连接OpenAI和Qwen的3个常见错误
最近在几个Go语言AI项目中,我频繁看到开发者在使用LangChainGo集成OpenAI和通义千问(Qwen)时,反复踩进同样的几个坑。这些错误往往不会导致程序直接崩溃,而是表现为一些难以捉摸的行为:比如流式响应突然中断、模型返回的结果牛头不对马嘴,或者干脆就是一片沉默。如果你已经掌握了LangChainGo的基础用法,但在实际调试中遇到了各种“玄学”问题,这篇文章或许能帮你快速定位到症结所在。
LangChainGo作为Go生态中最重要的AI应用框架之一,其设计理念确实很优雅——通过统一的接口抽象,让开发者能够无缝切换不同的AI模型。但正是这种抽象,在某些细节处理上埋下了隐患。特别是当你同时使用OpenAI官方接口和阿里云百炼平台(兼容OpenAI接口)时,配置上的细微差异就可能引发连锁反应。
这篇文章不会重复那些基础的安装和配置步骤,而是聚焦于三个最容易被忽视、调试起来最头疼的实际问题。我会结合社区中的真实案例,拆解错误日志背后的含义,并提供经过验证的解决方案。无论你是正在构建智能客服系统、文档分析工具,还是任何基于大语言模型的Go应用,这些经验都能帮你节省大量调试时间。
1. 环境变量配置的“隐藏陷阱”与多模型兼容方案
很多教程会告诉你,在.env文件中设置OPENAI_API_KEY和OPENAI_BASE_URL就能搞定一切。但在实际的多模型混合部署场景中,这种简单配置往往会出问题。最常见的情况是:你的应用需要同时调用OpenAI的GPT-4和阿里云百炼的Qwen模型,而这两个服务对请求头、URL路径甚至超时设置的要求并不完全相同。
1.1 基础配置错误:为什么.env文件有时“不生效”
先看一个典型的错误配置:
# .env 文件
OPENAI_API_KEY=sk-your-openai-key
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
这段配置的问题在于,它试图用同一个基础URL来服务两个不同的API端点。当你的代码尝试调用OpenAI官方接口时,请求会被错误地发送到阿里云的服务器,结果自然是各种认证失败或404错误。
更隐蔽的问题是环境变量的加载时机。很多开发者习惯在main()函数开始时调用godotenv.Load(),但如果你的模型客户端初始化代码在包级别的init()函数中执行,环境变量可能还没有被正确加载。
正确的多模型配置方案应该是这样的:
# .env 文件
# OpenAI官方服务
OPENAI_API_KEY=sk-your-openai-key-here
# 注意:OpenAI官方服务不需要显式设置BASE_URL,除非你使用代理
# 阿里云百炼服务(兼容OpenAI接口)
QWEN_API_KEY=sk-your-qwen-key-here
QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
# 可选:本地Ollama服务
OLLAMA_BASE_URL=http://localhost:11434
对应的Go代码中,你需要为不同的模型创建独立的客户端:
package llmclient
import (
"context"
"log"
"os"
"github.com/joho/godotenv"
"github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/llms/openai"
"github.com/tmc/langchaingo/llms/ollama"
)
// 初始化OpenAI官方客户端
func NewOpenAIClient(model string) (*openai.LLM, error) {
// 显式传递API Key,而不是依赖环境变量
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("OPENAI_API_KEY not set")
}
return openai.New(
openai.WithToken(apiKey),
openai.WithModel(model),
// 重要:显式设置超时,避免默认值不适用
openai.WithTimeout(30*time.Second),
)
}
// 初始化Qwen客户端(通过阿里云百炼)
func NewQwenClient(model string) (*openai.LLM, error) {
apiKey := os.Getenv("QWEN_API_KEY")
baseURL := os.Getenv("QWEN_BASE_URL")
if apiKey == "" || baseURL == "" {
return nil, fmt.Errorf("QWEN_API_KEY or QWEN_BASE_URL not set")
}
return openai.New(
openai.WithToken(apiKey),
openai.WithModel(model),
openai.WithBaseURL(baseURL),
// Qwen服务可能需要不同的超时设置
openai.WithTimeout(60*time.Second),
)
}
// 初始化本地Ollama客户端
func NewOllamaClient(model string) (*ollama.LLM, error) {
baseURL := os.Getenv("OLLAMA_BASE_URL")
if baseURL == "" {
baseURL = "http://localhost:11434"
}
return ollama.New(
ollama.WithModel(model),
ollama.WithServerURL(baseURL),
// 本地模型可以设置更长的超时
ollama.WithTimeout(120*time.Second),
)
}
1.2 环境变量加载的最佳实践
为了避免环境变量加载时机的问题,我推荐采用以下模式:
package config
import (
"sync"
"github.com/joho/godotenv"
)
var (
once sync.Once
configLoaded bool
)
// LoadConfig 确保配置只加载一次
func LoadConfig() error {
var err error
once.Do(func() {
// 尝试从多个位置加载.env文件
_ = godotenv.Load(".env.local")
_ = godotenv.Load(".env")
// 验证必要环境变量
requiredVars := []string{"OPENAI_API_KEY"}
for _, v := range requiredVars {
if os.Getenv(v) == "" {
err = fmt.Errorf("required environment variable %s is not set", v)
return
}
}
configLoaded = true
})
return err
}
// 在其他包的init函数中调用
func init() {
if err := config.LoadConfig(); err != nil {
log.Fatalf("Failed to load config: %v", err)
}
}
1.3 配置验证工具函数
为了在早期发现问题,可以添加一个配置验证函数:
func ValidateModelConfig(modelType string, modelName string) error {
switch modelType {
case "openai":
// 验证OpenAI模型名称
validModels := map[string]bool{
"gpt-4": true, "gpt-4-turbo": true,
"gpt-3.5-turbo": true, "gpt-4o": true,
}
if !validModels[modelName] {
return fmt.Errorf("invalid OpenAI model: %s", modelName)
}
case "qwen":
// 验证Qwen模型名称
validModels := map[string]bool{
"qwen-max": true, "qwen-plus": true,
"qwen-turbo": true, "qwen-7b-chat": true,
}
if !validModels[modelName] {
return fmt.Errorf("invalid Qwen model: %s", modelName)
}
case "ollama":
// Ollama模型名称验证较宽松
if modelName == "" {
return fmt.Errorf("Ollama model name cannot be empty")
}
default:
return fmt.Errorf("unknown model type: %s", modelType)
}
return nil
}
这个验证函数可以在客户端初始化前调用,提前捕获配置错误,而不是等到API调用失败时才发现问题。
2. 流式响应处理的“断流”问题与稳健性设计
流式响应(Streaming)是提升AI应用用户体验的关键特性,但也是问题最多的部分。LangChainGo提供了WithStreamingFunc选项来处理流式输出,但很多开发者在实现时忽略了错误处理和资源清理。
2.1 典型的流式响应处理错误
先看一个有问题的实现:
// 有问题的流式处理代码
func streamResponse(ctx context.Context, prompt string) error {
llm, err := openai.New()
if err != nil {
return err
}
completion, err := llms.GenerateFromSinglePrompt(
ctx,
llm,
prompt,
llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
// 问题1:没有检查上下文取消
// 问题2:直接输出,没有缓冲处理
fmt.Print(string(chunk))
return nil
}),
)
// 问题3:忽略了流式处理过程中的错误
_ = completion
return err
}
这段代码在简单场景下可能工作正常,但在生产环境中会遇到各种问题:
- 用户中途取消请求时,流可能不会立即停止
- 网络波动导致连接中断时,没有重试机制
- 高并发场景下,直接
fmt.Print可能导致输出混乱
2.2 健壮的流式响应处理器
下面是一个更加健壮的实现:
package streaming
import (
"context"
"fmt"
"io"
"sync"
"time"
"github.com/tmc/langchaingo/llms"
)
// StreamProcessor 封装流式处理逻辑
type StreamProcessor struct {
buffer strings.Builder
bufferLock sync.RWMutex
chunks chan []byte
errors chan error
done chan struct{}
}
// NewStreamProcessor 创建新的流处理器
func NewStreamProcessor() *StreamProcessor {
return &StreamProcessor{
chunks: make(chan []byte, 100), // 缓冲通道
errors: make(chan error, 1),
done: make(chan struct{}),
}
}
// StreamingFunc 返回符合LangChainGo要求的流式处理函数
func (p *StreamProcessor) StreamingFunc() llms.StreamingFunc {
return func(ctx context.Context, chunk []byte) error {
select {
case <-ctx.Done():
// 上下文被取消,停止处理
return ctx.Err()
case p.chunks <- chunk:
// 成功写入通道
return nil
case <-time.After(5 * time.Second):
// 写入超时,避免阻塞
return fmt.Errorf("timeout writing chunk to channel")
}
}
}
// Start 启动流处理器
func (p *StreamProcessor) Start(ctx context.Context) {
go p.processChunks(ctx)
}
// processChunks 处理接收到的数据块
func (p *StreamProcessor) processChunks(ctx context.Context) {
defer close(p.done)
for {
select {
case <-ctx.Done():
p.errors <- ctx.Err()
return
case chunk, ok := <-p.chunks:
if !ok {
// 通道关闭,处理完成
return
}
// 线程安全地更新缓冲区
p.bufferLock.Lock()
p.buffer.Write(chunk)
p.bufferLock.Unlock()
// 这里可以添加自定义处理逻辑
// 例如:实时推送到WebSocket、更新UI等
p.handleChunk(chunk)
}
}
}
// handleChunk 处理单个数据块
func (p *StreamProcessor) handleChunk(chunk []byte) {
// 示例:输出到控制台,实际项目中可能推送到WebSocket
fmt.Print(string(chunk))
// 可以在这里添加更多处理逻辑:
// 1. 实时计算token使用量
// 2. 检测敏感内容
// 3. 格式化输出(如Markdown解析)
}
// GetContent 获取完整的响应内容
func (p *StreamProcessor) GetContent() string {
p.bufferLock.RLock()
defer p.bufferLock.RUnlock()
return p.buffer.String()
}
// Wait 等待流处理完成
func (p *StreamProcessor) Wait() error {
<-p.done
select {
case err := <-p.errors:
return err
default:
return nil
}
}
// Close 清理资源
func (p *StreamProcessor) Close() {
close(p.chunks)
}
2.3 在实际项目中使用流处理器
func generateWithStreaming(ctx context.Context, llm llms.Model, prompt string) (string, error) {
processor := NewStreamProcessor()
processor.Start(ctx)
// 设置超时上下文
ctxWithTimeout, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
// 生成响应
_, err := llms.GenerateFromSinglePrompt(
ctxWithTimeout,
llm,
prompt,
llms.WithStreamingFunc(processor.StreamingFunc()),
llms.WithTemperature(0.7),
llms.WithMaxTokens(1000),
)
if err != nil {
processor.Close()
return "", fmt.Errorf("generation failed: %w", err)
}
// 等待所有数据块处理完成
if err := processor.Wait(); err != nil {
return "", fmt.Errorf("stream processing failed: %w", err)
}
content := processor.GetContent()
processor.Close()
return content, nil
}
2.4 错误处理与重试机制
对于生产环境,还需要添加重试机制:
type RetryConfig struct {
MaxRetries int
BaseDelay time.Duration
MaxDelay time.Duration
RetryableErrors []string // 可重试的错误类型
}
func generateWithRetry(ctx context.Context, llm llms.Model, prompt string, config RetryConfig) (string, error) {
var lastErr error
for i := 0; i <= config.MaxRetries; i++ {
if i > 0 {
// 指数退避延迟
delay := config.BaseDelay * time.Duration(1<<(i-1))
if delay > config.MaxDelay {
delay = config.MaxDelay
}
select {
case <-time.After(delay):
// 继续重试
case <-ctx.Done():
return "", ctx.Err()
}
}
content, err := generateWithStreaming(ctx, llm, prompt)
if err == nil {
return content, nil
}
lastErr = err
// 检查是否可重试的错误
if !isRetryableError(err, config.RetryableErrors) {
break
}
log.Printf("Generation attempt %d failed: %v", i+1, err)
}
return "", fmt.Errorf("failed after %d retries: %w", config.MaxRetries, lastErr)
}
func isRetryableError(err error, retryableErrors []string) bool {
errStr := err.Error()
for _, retryableErr := range retryableErrors {
if strings.Contains(errStr, retryableErr) {
return true
}
}
return false
}
3. 模型参数不兼容与API差异的深度解析
OpenAI API和阿里云百炼的兼容接口在大部分情况下可以互换使用,但某些高级参数和功能存在细微差异。这些差异不会导致请求失败,但会影响生成结果的质量和稳定性。
3.1 温度(Temperature)和Top-p参数的差异
虽然两个平台都支持temperature和top_p参数,但它们的默认值和有效范围可能不同:
| 参数 | OpenAI默认值 | OpenAI有效范围 | 百炼/Qwen默认值 | 百炼/Qwen有效范围 | 注意事项 |
|---|---|---|---|---|---|
| temperature | 1.0 | [0, 2] | 0.85 | [0, 2] | Qwen对极端值更敏感 |
| top_p | 1.0 | [0, 1] | 0.8 | [0, 1] | 两者同时设置时行为不同 |
| frequency_penalty | 0.0 | [-2, 2] | 0.0 | [0, 2] | Qwen只支持非负值 |
| presence_penalty | 0.0 | [-2, 2] | 0.0 | [0, 2] | Qwen只支持非负值 |
在实际使用中,我发现Qwen模型对temperature参数更加敏感。当temperature接近2.0时,OpenAI的模型还能产生相对连贯的文本,但Qwen可能会输出完全随机的字符。
推荐的参数设置策略:
type ModelParams struct {
Temperature float64
TopP float64
MaxTokens int
FrequencyPenalty float64
PresencePenalty float64
}
// GetOptimalParams 根据模型类型返回最优参数
func GetOptimalParams(modelType, modelName string) ModelParams {
switch modelType {
case "openai":
switch {
case strings.Contains(modelName, "gpt-4"):
return ModelParams{
Temperature: 0.7,
TopP: 0.9,
MaxTokens: 2000,
FrequencyPenalty: 0.1,
PresencePenalty: 0.1,
}
case strings.Contains(modelName, "gpt-3.5"):
return ModelParams{
Temperature: 0.8,
TopP: 0.95,
MaxTokens: 1500,
FrequencyPenalty: 0.0,
PresencePenalty: 0.0,
}
}
case "qwen":
switch {
case strings.Contains(modelName, "qwen-max"):
return ModelParams{
Temperature: 0.85,
TopP: 0.8,
MaxTokens: 2000,
FrequencyPenalty: 0.0, // Qwen建议设为0
PresencePenalty: 0.0,
}
case strings.Contains(modelName, "qwen-turbo"):
return ModelParams{
Temperature: 0.9,
TopP: 0.85,
MaxTokens: 1000,
FrequencyPenalty: 0.0,
PresencePenalty: 0.0,
}
}
}
// 默认参数
return ModelParams{
Temperature: 0.8,
TopP: 0.9,
MaxTokens: 1000,
FrequencyPenalty: 0.0,
PresencePenalty: 0.0,
}
}
3.2 函数调用(Function Calling)的兼容性问题
函数调用是构建AI应用的重要功能,但OpenAI和百炼平台的实现存在差异:
// OpenAI风格的函数定义
openaiFunctions := []llms.FunctionDefinition{
{
Name: "get_weather",
Description: "获取指定城市的天气信息",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"location": map[string]interface{}{
"type": "string",
"description": "城市名称,如:北京、上海",
},
"unit": map[string]interface{}{
"type": "string",
"enum": []string{"celsius", "fahrenheit"},
"description": "温度单位",
},
},
"required": []string{"location"},
},
},
}
// 百炼/Qwen可能需要不同的参数格式
// 注意:百炼平台对JSON Schema的某些特性支持不完全
qwenFunctions := []llms.FunctionDefinition{
{
Name: "get_weather",
Description: "获取指定城市的天气信息",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"location": map[string]interface{}{
"type": "string",
"description": "城市名称",
},
},
// 百炼平台对required字段的处理可能不同
"required": []interface{}{"location"},
},
},
}
函数调用兼容性封装:
package functioncalling
import (
"encoding/json"
"fmt"
"github.com/tmc/langchaingo/llms"
)
// FunctionCallAdapter 适配不同平台的函数调用
type FunctionCallAdapter struct {
platform string // "openai" 或 "qwen"
}
// NewFunctionCallAdapter 创建适配器
func NewFunctionCallAdapter(platform string) *FunctionCallAdapter {
return &FunctionCallAdapter{platform: platform}
}
// AdaptFunctions 适配函数定义
func (a *FunctionCallAdapter) AdaptFunctions(functions []llms.FunctionDefinition) []llms.FunctionDefinition {
if a.platform != "qwen" {
return functions
}
// 为Qwen平台适配函数定义
adapted := make([]llms.FunctionDefinition, len(functions))
for i, fn := range functions {
adapted[i] = a.adaptFunction(fn)
}
return adapted
}
// adaptFunction 适配单个函数
func (a *FunctionCallAdapter) adaptFunction(fn llms.FunctionDefinition) llms.FunctionDefinition {
if params, ok := fn.Parameters.(map[string]interface{}); ok {
// 确保required字段是[]interface{}类型
if required, ok := params["required"].([]string); ok {
interfaceRequired := make([]interface{}, len(required))
for i, r := range required {
interfaceRequired[i] = r
}
params["required"] = interfaceRequired
}
// 简化复杂的JSON Schema特性
a.simplifySchema(params)
}
return fn
}
// simplifySchema 简化JSON Schema以适应百炼平台
func (a *FunctionCallAdapter) simplifySchema(schema map[string]interface{}) {
if properties, ok := schema["properties"].(map[string]interface{}); ok {
for _, prop := range properties {
if propMap, ok := prop.(map[string]interface{}); ok {
// 移除百炼可能不支持的字段
delete(propMap, "examples")
delete(propMap, "const")
delete(propMap, "pattern")
// 简化enum的表示
if enum, ok := propMap["enum"].([]interface{}); ok {
// 确保enum值是字符串
for i, val := range enum {
if strVal, ok := val.(string); ok {
enum[i] = strVal
}
}
}
}
}
}
}
// ParseFunctionCall 解析函数调用响应
func (a *FunctionCallAdapter) ParseFunctionCall(response *llms.GenerateResponse) (string, map[string]interface{}, error) {
// 这里需要根据实际响应结构进行解析
// OpenAI和百炼的函数调用响应格式可能不同
// 示例解析逻辑
if response.FunctionCall != nil {
return response.FunctionCall.Name, response.FunctionCall.Arguments, nil
}
// 尝试从消息内容中解析
var result map[string]interface{}
if err := json.Unmarshal([]byte(response.Text), &result); err == nil {
if name, ok := result["name"].(string); ok {
if args, ok := result["arguments"].(map[string]interface{}); ok {
return name, args, nil
}
}
}
return "", nil, fmt.Errorf("no function call found in response")
}
3.3 嵌入模型(Embedding)的向量维度差异
当你在RAG(检索增强生成)应用中使用嵌入模型时,需要特别注意不同模型的向量维度:
| 模型 | 平台 | 向量维度 | 最大token长度 | 备注 |
|---|---|---|---|---|
| text-embedding-3-small | OpenAI | 1536 | 8191 | 最新推荐 |
| text-embedding-ada-002 | OpenAI | 1536 | 8191 | 旧版但仍可用 |
| text-embedding-v1 | 百炼/Qwen | 1536 | 2048 | 注意token限制 |
| text-embedding-v2 | 百炼/Qwen | 1024 | 2048 | 维度不同 |
维度兼容性处理:
package embeddings
import (
"fmt"
"github.com/tmc/langchaingo/embeddings"
"github.com/tmc/langchaingo/llms/openai"
)
// EmbeddingManager 管理不同平台的嵌入模型
type EmbeddingManager struct {
embedders map[string]embeddings.Embedder
dimensions map[string]int
}
// NewEmbeddingManager 创建嵌入管理器
func NewEmbeddingManager() *EmbeddingManager {
return &EmbeddingManager{
embedders: make(map[string]embeddings.Embedder),
dimensions: map[string]int{
"text-embedding-3-small": 1536,
"text-embedding-ada-002": 1536,
"text-embedding-v1": 1536,
"text-embedding-v2": 1024,
},
}
}
// GetEmbedder 获取或创建嵌入器
func (m *EmbeddingManager) GetEmbedder(modelName string) (embeddings.Embedder, error) {
if embedder, exists := m.embedders[modelName]; exists {
return embedder, nil
}
var embedder embeddings.Embedder
var err error
switch {
case strings.Contains(modelName, "text-embedding-3") ||
strings.Contains(modelName, "text-embedding-ada"):
// OpenAI嵌入模型
llm, err := openai.New()
if err != nil {
return nil, err
}
embedder, err = embeddings.NewEmbedder(llm)
case strings.Contains(modelName, "text-embedding-v"):
// 百炼嵌入模型
// 注意:这里需要使用兼容OpenAI接口的方式
llm, err := openai.New(
openai.WithBaseURL("https://dashscope.aliyuncs.com/compatible-mode/v1"),
openai.WithModel(modelName),
)
if err != nil {
return nil, err
}
embedder, err = embeddings.NewEmbedder(llm)
default:
return nil, fmt.Errorf("unsupported embedding model: %s", modelName)
}
if err != nil {
return nil, err
}
m.embedders[modelName] = embedder
return embedder, nil
}
// GetDimension 获取模型维度
func (m *EmbeddingManager) GetDimension(modelName string) (int, error) {
if dim, exists := m.dimensions[modelName]; exists {
return dim, nil
}
// 尝试从模型名称推断
if strings.Contains(modelName, "1536") {
return 1536, nil
} else if strings.Contains(modelName, "1024") {
return 1024, nil
} else if strings.Contains(modelName, "768") {
return 768, nil
}
return 0, fmt.Errorf("unknown dimension for model: %s", modelName)
}
// NormalizeDimensions 标准化向量维度
func (m *EmbeddingManager) NormalizeDimensions(vectors [][]float32, targetDim int) ([][]float32, error) {
if len(vectors) == 0 {
return vectors, nil
}
sourceDim := len(vectors[0])
if sourceDim == targetDim {
return vectors, nil
}
// 简单的维度调整策略
// 实际项目中可能需要更复杂的算法
result := make([][]float32, len(vectors))
for i, vec := range vectors {
if len(vec) != sourceDim {
return nil, fmt.Errorf("inconsistent vector dimensions")
}
result[i] = m.resizeVector(vec, sourceDim, targetDim)
}
return result, nil
}
// resizeVector 调整向量维度
func (m *EmbeddingManager) resizeVector(vec []float32, sourceDim, targetDim int) []float32 {
if targetDim > sourceDim {
// 升维:用零填充
result := make([]float32, targetDim)
copy(result, vec)
return result
} else {
// 降维:截断
return vec[:targetDim]
}
}
4. 实战案例:构建跨模型兼容的AI对话系统
现在让我们把这些知识点整合起来,构建一个真正能在生产环境中运行的、支持多模型的AI对话系统。这个系统需要能够:
- 根据配置自动选择最优模型
- 处理不同模型的参数差异
- 实现健壮的流式响应
- 提供统一的错误处理
4.1 系统架构设计
package aichat
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"time"
"github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/memory"
"github.com/tmc/langchaingo/chains"
)
// ChatSystem 核心聊天系统
type ChatSystem struct {
modelManager *ModelManager
memoryManager *memory.ConversationBuffer
streamProcessor *StreamProcessor
config *SystemConfig
}
// SystemConfig 系统配置
type SystemConfig struct {
DefaultModel string
FallbackModels []string
MaxRetries int
TimeoutSeconds int
EnableStreaming bool
EnableMemory bool
LogLevel string
}
// NewChatSystem 创建聊天系统
func NewChatSystem(config *SystemConfig) (*ChatSystem, error) {
modelManager, err := NewModelManager()
if err != nil {
return nil, fmt.Errorf("failed to create model manager: %w", err)
}
system := &ChatSystem{
modelManager: modelManager,
memoryManager: memory.NewConversationBuffer(),
config: config,
}
if config.EnableStreaming {
system.streamProcessor = NewStreamProcessor()
}
return system, nil
}
// Chat 处理聊天请求
func (s *ChatSystem) Chat(ctx context.Context, userInput string, options ...ChatOption) (*ChatResponse, error) {
// 应用选项
opts := &chatOptions{
model: s.config.DefaultModel,
temperature: 0.8,
maxTokens: 1000,
}
for _, opt := range options {
opt(opts)
}
// 选择模型
llm, err := s.selectModel(ctx, opts.model)
if err != nil {
return nil, fmt.Errorf("model selection failed: %w", err)
}
// 构建提示词
prompt, err := s.buildPrompt(ctx, userInput)
if err != nil {
return nil, fmt.Errorf("prompt building failed: %w", err)
}
// 生成响应
var response *ChatResponse
var genErr error
for attempt := 0; attempt <= s.config.MaxRetries; attempt++ {
if attempt > 0 {
log.Printf("Retry attempt %d for model %s", attempt, opts.model)
time.Sleep(time.Duration(attempt) * time.Second)
}
response, genErr = s.generateResponse(ctx, llm, prompt, opts)
if genErr == nil {
break
}
// 检查是否应该切换到备用模型
if attempt == s.config.MaxRetries/2 && len(s.config.FallbackModels) > 0 {
fallbackModel := s.config.FallbackModels[0]
log.Printf("Switching to fallback model: %s", fallbackModel)
llm, err = s.selectModel(ctx, fallbackModel)
if err != nil {
return nil, fmt.Errorf("failed to switch to fallback model: %w", err)
}
opts.model = fallbackModel
}
}
if genErr != nil {
return nil, fmt.Errorf("generation failed after %d attempts: %w",
s.config.MaxRetries, genErr)
}
// 更新记忆
if s.config.EnableMemory {
s.updateMemory(userInput, response.Content)
}
return response, nil
}
// generateResponse 生成响应
func (s *ChatSystem) generateResponse(ctx context.Context, llm llms.Model, prompt string, opts *chatOptions) (*ChatResponse, error) {
// 设置超时
ctxWithTimeout, cancel := context.WithTimeout(ctx,
time.Duration(s.config.TimeoutSeconds)*time.Second)
defer cancel()
var generateOpts []llms.CallOption
// 添加模型参数
generateOpts = append(generateOpts,
llms.WithTemperature(opts.temperature),
llms.WithMaxTokens(opts.maxTokens),
)
// 添加流式处理
if s.config.EnableStreaming && s.streamProcessor != nil {
s.streamProcessor.Start(ctxWithTimeout)
generateOpts = append(generateOpts,
llms.WithStreamingFunc(s.streamProcessor.StreamingFunc()),
)
}
// 执行生成
startTime := time.Now()
completion, err := llms.GenerateFromSinglePrompt(
ctxWithTimeout,
llm,
prompt,
generateOpts...,
)
elapsed := time.Since(startTime)
response := &ChatResponse{
Model: opts.model,
Latency: elapsed,
Timestamp: time.Now(),
}
if s.config.EnableStreaming && s.streamProcessor != nil {
// 等待流式处理完成
if err := s.streamProcessor.Wait(); err != nil {
return nil, fmt.Errorf("stream processing failed: %w", err)
}
response.Content = s.streamProcessor.GetContent()
response.IsStreaming = true
} else {
response.Content = completion
}
// 记录性能指标
s.recordMetrics(opts.model, elapsed, len(response.Content))
return response, err
}
// recordMetrics 记录性能指标
func (s *ChatSystem) recordMetrics(model string, latency time.Duration, outputLength int) {
// 这里可以集成监控系统,如Prometheus
log.Printf("Model: %s, Latency: %v, Output length: %d",
model, latency, outputLength)
// 示例:简单的性能跟踪
metrics := map[string]interface{}{
"model": model,
"latency_ms": latency.Milliseconds(),
"output_tokens": outputLength / 4, // 粗略估计
"timestamp": time.Now().Unix(),
}
if s.config.LogLevel == "debug" {
metricsJSON, _ := json.Marshal(metrics)
log.Printf("Performance metrics: %s", string(metricsJSON))
}
}
4.2 模型管理器实现
package aichat
import (
"context"
"fmt"
"sync"
"github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/llms/openai"
"github.com/tmc/langchaingo/llms/ollama"
)
// ModelManager 管理多个模型实例
type ModelManager struct {
models map[string]llms.Model
modelLock sync.RWMutex
config *ModelConfig
}
// ModelConfig 模型配置
type ModelConfig struct {
OpenAIAPIKey string
QwenAPIKey string
QwenBaseURL string
OllamaBaseURL string
ModelTimeout int // 秒
}
// NewModelManager 创建模型管理器
func NewModelManager() (*ModelManager, error) {
config := &ModelConfig{
ModelTimeout: 30,
}
// 从环境变量加载配置
// 实际项目中应该从配置文件加载
return &ModelManager{
models: make(map[string]llms.Model),
config: config,
}, nil
}
// GetModel 获取模型实例
func (m *ModelManager) GetModel(ctx context.Context, modelID string) (llms.Model, error) {
// 首先检查缓存
m.modelLock.RLock()
if model, exists := m.models[modelID]; exists {
m.modelLock.RUnlock()
return model, nil
}
m.modelLock.RUnlock()
// 创建新模型实例
m.modelLock.Lock()
defer m.modelLock.Unlock()
// 双重检查
if model, exists := m.models[modelID]; exists {
return model, nil
}
model, err := m.createModel(ctx, modelID)
if err != nil {
return nil, err
}
m.models[modelID] = model
return model, nil
}
// createModel 创建模型实例
func (m *ModelManager) createModel(ctx context.Context, modelID string) (llms.Model, error) {
switch {
case strings.HasPrefix(modelID, "gpt-"):
return m.createOpenAIModel(modelID)
case strings.HasPrefix(modelID, "qwen-"):
return m.createQwenModel(modelID)
case strings.HasPrefix(modelID, "llama-") ||
strings.HasPrefix(modelID, "mistral-") ||
strings.HasPrefix(modelID, "gemma-"):
return m.createOllamaModel(modelID)
default:
return nil, fmt.Errorf("unsupported model: %s", modelID)
}
}
// createOpenAIModel 创建OpenAI模型
func (m *ModelManager) createOpenAIModel(modelID string) (llms.Model, error) {
if m.config.OpenAIAPIKey == "" {
return nil, fmt.Errorf("OpenAI API key not configured")
}
opts := []openai.Option{
openai.WithToken(m.config.OpenAIAPIKey),
openai.WithModel(modelID),
openai.WithTimeout(time.Duration(m.config.ModelTimeout) * time.Second),
}
// 根据模型类型添加特定选项
if strings.Contains(modelID, "gpt-4") {
opts = append(opts, openai.WithMaxTokens(4000))
}
return openai.New(opts...)
}
// createQwenModel 创建Qwen模型
func (m *ModelManager) createQwenModel(modelID string) (llms.Model, error) {
if m.config.QwenAPIKey == "" || m.config.QwenBaseURL == "" {
return nil, fmt.Errorf("Qwen API configuration incomplete")
}
opts := []openai.Option{
openai.WithToken(m.config.QwenAPIKey),
openai.WithModel(modelID),
openai.WithBaseURL(m.config.QwenBaseURL),
openai.WithTimeout(time.Duration(m.config.ModelTimeout+30) * time.Second),
}
// Qwen特定配置
if strings.Contains(modelID, "qwen-max") {
opts = append(opts, openai.WithMaxTokens(2000))
}
return openai.New(opts...)
}
// createOllamaModel 创建Ollama模型
func (m *ModelManager) createOllamaModel(modelID string) (llms.Model, error) {
baseURL := m.config.OllamaBaseURL
if baseURL == "" {
baseURL = "http://localhost:11434"
}
opts := []ollama.Option{
ollama.WithModel(modelID),
ollama.WithServerURL(baseURL),
ollama.WithTimeout(time.Duration(m.config.ModelTimeout+60) * time.Second),
}
return ollama.New(opts...)
}
// HealthCheck 检查所有模型健康状态
func (m *ModelManager) HealthCheck(ctx context.Context) map[string]bool {
results := make(map[string]bool)
m.modelLock.RLock()
defer m.modelLock.RUnlock()
for modelID, model := range m.models {
// 简单的健康检查:尝试生成一个token
_, err := llms.GenerateFromSinglePrompt(
ctx,
model,
"ping",
llms.WithMaxTokens(1),
llms.WithTemperature(0),
)
results[modelID] = err == nil
}
return results
}
4.3 使用示例和最佳实践
package main
import (
"context"
"fmt"
"log"
"time"
"yourproject/aichat"
)
func main() {
// 配置聊天系统
config := &aichat.SystemConfig{
DefaultModel: "gpt-4-turbo",
FallbackModels: []string{"qwen-max", "gpt-3.5-turbo"},
MaxRetries: 3,
TimeoutSeconds: 60,
EnableStreaming: true,
EnableMemory: true,
LogLevel: "info",
}
// 创建聊天系统
chatSystem, err := aichat.NewChatSystem(config)
if err != nil {
log.Fatalf("Failed to create chat system: %v", err)
}
// 示例对话
ctx := context.Background()
messages := []string{
"你好,请介绍一下Go语言在AI开发中的优势",
"能给出一个具体的代码示例吗?",
"这个示例中如何处理错误?",
}
for i, message := range messages {
fmt.Printf("\n[用户] %s\n", message)
// 发送消息,启用流式输出
startTime := time.Now()
response, err := chatSystem.Chat(ctx, message,
aichat.WithTemperature(0.7),
aichat.WithMaxTokens(500),
)
if err != nil {
log.Printf("Chat error: %v", err)
continue
}
fmt.Printf("[AI - %s] %s\n", response.Model, response.Content)
fmt.Printf("响应时间: %v\n", response.Latency)
// 添加一点延迟,模拟真实对话
if i < len(messages)-1 {
time.Sleep(1 * time.Second)
}
}
// 健康检查
fmt.Println("\n=== 模型健康检查 ===")
healthStatus := chatSystem.HealthCheck(ctx)
for model, healthy := range healthStatus {
status := "✓ 健康"
if !healthy {
status = "✗ 异常"
}
fmt.Printf("%s: %s\n", model, status)
}
}
这个完整的实现展示了如何在实际项目中避免本文提到的各种陷阱。关键点包括:
- 环境隔离:每个模型类型有独立的配置,避免冲突
- 错误恢复:自动重试和故障转移机制
- 性能监控:记录响应时间和token使用情况
- 资源管理:合理的超时设置和连接池管理
- 可观测性:详细的日志和健康检查
在实际部署时,你还需要考虑:
- 速率限制:为每个API设置适当的请求频率限制
- 成本控制:监控token使用量,设置预算警报
- 缓存策略:对常见查询结果进行缓存
- A/B测试:同时运行多个模型,比较效果和成本
这些实践来自我在多个生产项目中的经验总结,希望能帮助你在Go语言AI开发中少走弯路。每个项目都有其特殊性,最重要的是理解原理,然后根据实际需求进行调整。

73

被折叠的 条评论
为什么被折叠?



