package chat import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strings" "github.com/Tencent/WeKnora/internal/logger" "github.com/Tencent/WeKnora/internal/types" "github.com/sashabaranov/go-openai" ) // RemoteAPIChat 实现了基于的聊天 type RemoteAPIChat struct { modelName string client *openai.Client modelID string baseURL string apiKey string } // QwenChatCompletionRequest 用于 qwen 模型的自定义请求结构体 type QwenChatCompletionRequest struct { openai.ChatCompletionRequest EnableThinking *bool `json:"enable_thinking,omitempty"` // qwen 模型专用字段 } // NewRemoteAPIChat 调用远程API 聊天实例 func NewRemoteAPIChat(chatConfig *ChatConfig) (*RemoteAPIChat, error) { apiKey := chatConfig.APIKey config := openai.DefaultConfig(apiKey) if baseURL := chatConfig.BaseURL; baseURL != "" { config.BaseURL = baseURL } return &RemoteAPIChat{ modelName: chatConfig.ModelName, client: openai.NewClientWithConfig(config), modelID: chatConfig.ModelID, baseURL: chatConfig.BaseURL, apiKey: apiKey, }, nil } // convertMessages 转换消息格式为OpenAI格式 func (c *RemoteAPIChat) convertMessages(messages []Message) []openai.ChatCompletionMessage { openaiMessages := make([]openai.ChatCompletionMessage, 0, len(messages)) for _, msg := range messages { openaiMsg := openai.ChatCompletionMessage{ Role: msg.Role, } // 处理内容:对于 assistant 角色,内容可能为空(当有 tool_calls 时) if msg.Content != "" { openaiMsg.Content = msg.Content } // 处理 tool calls(assistant 角色) if len(msg.ToolCalls) > 0 { openaiMsg.ToolCalls = make([]openai.ToolCall, 0, len(msg.ToolCalls)) for _, tc := range msg.ToolCalls { toolType := openai.ToolType(tc.Type) openaiMsg.ToolCalls = append(openaiMsg.ToolCalls, openai.ToolCall{ ID: tc.ID, Type: toolType, Function: openai.FunctionCall{ Name: tc.Function.Name, Arguments: tc.Function.Arguments, }, }) } } // 处理 tool 角色消息(工具返回结果) if msg.Role == "tool" { openaiMsg.ToolCallID = msg.ToolCallID openaiMsg.Name = msg.Name } openaiMessages = append(openaiMessages, openaiMsg) } return openaiMessages } // isQwenModel 检查是否为 qwen 模型 func (c *RemoteAPIChat) isAliyunQwen3Model() bool { return strings.HasPrefix(c.modelName, "qwen3-") && c.baseURL == "https://dashscope.aliyuncs.com/compatible-mode/v1" } // isDeepSeekModel 检查是否为 DeepSeek 模型 func (c *RemoteAPIChat) isDeepSeekModel() bool { return strings.Contains(strings.ToLower(c.modelName), "deepseek") } // buildQwenChatCompletionRequest 构建 qwen 模型的聊天请求参数 func (c *RemoteAPIChat) buildQwenChatCompletionRequest(messages []Message, opts *ChatOptions, isStream bool, ) QwenChatCompletionRequest { req := QwenChatCompletionRequest{ ChatCompletionRequest: c.buildChatCompletionRequest(messages, opts, isStream), } // 对于 qwen 模型,在非流式调用中强制设置 enable_thinking: false if !isStream { enableThinking := false req.EnableThinking = &enableThinking } return req } // buildChatCompletionRequest 构建聊天请求参数 func (c *RemoteAPIChat) buildChatCompletionRequest(messages []Message, opts *ChatOptions, isStream bool, ) openai.ChatCompletionRequest { req := openai.ChatCompletionRequest{ Model: c.modelName, Messages: c.convertMessages(messages), Stream: isStream, } thinking := false // 添加可选参数 if opts != nil { if opts.Temperature > 0 { req.Temperature = float32(opts.Temperature) } if opts.TopP > 0 { req.TopP = float32(opts.TopP) } if opts.MaxTokens > 0 { req.MaxTokens = opts.MaxTokens } if opts.MaxCompletionTokens > 0 { req.MaxCompletionTokens = opts.MaxCompletionTokens } if opts.FrequencyPenalty > 0 { req.FrequencyPenalty = float32(opts.FrequencyPenalty) } if opts.PresencePenalty > 0 { req.PresencePenalty = float32(opts.PresencePenalty) } if opts.Thinking != nil { thinking = *opts.Thinking } // 处理 Tools(函数定义) if len(opts.Tools) > 0 { req.Tools = make([]openai.Tool, 0, len(opts.Tools)) for _, tool := range opts.Tools { toolType := openai.ToolType(tool.Type) openaiTool := openai.Tool{ Type: toolType, Function: &openai.FunctionDefinition{ Name: tool.Function.Name, Description: tool.Function.Description, }, } // 转换 Parameters (map[string]interface{} -> JSON Schema) if tool.Function.Parameters != nil { // Parameters 已经是 JSON Schema 格式的 map,直接使用 openaiTool.Function.Parameters = tool.Function.Parameters } req.Tools = append(req.Tools, openaiTool) } } // 处理 ToolChoice // ToolChoice 可以是字符串或 ToolChoice 对象 // 对于 "auto", "none", "required" 直接使用字符串 // 对于特定工具名称,使用 ToolChoice 对象 // 注意:某些模型(如 DeepSeek)不支持 tool_choice,需要跳过设置 if opts.ToolChoice != "" { // DeepSeek 模型不支持 tool_choice,跳过设置(默认行为会自动使用工具) if c.isDeepSeekModel() { // 对于 DeepSeek,不设置 tool_choice,让 API 使用默认行为 // 如果有 tools,DeepSeek 会自动使用 logger.Infof(context.Background(), "deepseek model, skip tool_choice") } else { switch opts.ToolChoice { case "none", "required", "auto": // 直接使用字符串 req.ToolChoice = opts.ToolChoice default: // 特定工具名称,使用 ToolChoice 对象 req.ToolChoice = openai.ToolChoice{ Type: "function", Function: openai.ToolFunction{ Name: opts.ToolChoice, }, } } } } } req.ChatTemplateKwargs = map[string]interface{}{ "enable_thinking": thinking, } // print req // jsonData, err := json.Marshal(req) // if err != nil { // logger.Error(context.Background(), "marshal request: %w", err) // } // logger.Infof(context.Background(), "llm request: %s", string(jsonData)) return req } // Chat 进行非流式聊天 func (c *RemoteAPIChat) Chat(ctx context.Context, messages []Message, opts *ChatOptions) (*types.ChatResponse, error) { // 如果是 qwen 模型,使用自定义请求 if c.isAliyunQwen3Model() { return c.chatWithQwen(ctx, messages, opts) } // 构建请求参数 req := c.buildChatCompletionRequest(messages, opts, false) // 发送请求 resp, err := c.client.CreateChatCompletion(ctx, req) if err != nil { return nil, fmt.Errorf("create chat completion: %w", err) } if len(resp.Choices) == 0 { return nil, fmt.Errorf("no response from OpenAI") } choice := resp.Choices[0] response := &types.ChatResponse{ Content: choice.Message.Content, FinishReason: string(choice.FinishReason), Usage: struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` }{ PromptTokens: resp.Usage.PromptTokens, CompletionTokens: resp.Usage.CompletionTokens, TotalTokens: resp.Usage.TotalTokens, }, } // 转换 Tool Calls if len(choice.Message.ToolCalls) > 0 { response.ToolCalls = make([]types.LLMToolCall, 0, len(choice.Message.ToolCalls)) for _, tc := range choice.Message.ToolCalls { response.ToolCalls = append(response.ToolCalls, types.LLMToolCall{ ID: tc.ID, Type: string(tc.Type), Function: types.FunctionCall{ Name: tc.Function.Name, Arguments: tc.Function.Arguments, }, }) } } return response, nil } // chatWithQwen 使用自定义请求处理 qwen 模型 func (c *RemoteAPIChat) chatWithQwen( ctx context.Context, messages []Message, opts *ChatOptions, ) (*types.ChatResponse, error) { // 构建 qwen 请求参数 req := c.buildQwenChatCompletionRequest(messages, opts, false) // 序列化请求 jsonData, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } // 构建 URL endpoint := c.baseURL + "/chat/completions" // 创建 HTTP 请求 httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("create request: %w", err) } // 设置请求头 httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+c.apiKey) // 发送请求 client := &http.Client{} resp, err := client.Do(httpReq) if err != nil { return nil, fmt.Errorf("send request: %w", err) } defer resp.Body.Close() // 检查响应状态 if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API request failed with status: %d", resp.StatusCode) } // 解析响应 var chatResp openai.ChatCompletionResponse if err := json.NewDecoder(resp.Body).Decode(&chatResp); err != nil { return nil, fmt.Errorf("decode response: %w", err) } if len(chatResp.Choices) == 0 { return nil, fmt.Errorf("no response from API") } choice := chatResp.Choices[0] response := &types.ChatResponse{ Content: choice.Message.Content, FinishReason: string(choice.FinishReason), Usage: struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` }{ PromptTokens: chatResp.Usage.PromptTokens, CompletionTokens: chatResp.Usage.CompletionTokens, TotalTokens: chatResp.Usage.TotalTokens, }, } // 转换 Tool Calls if len(choice.Message.ToolCalls) > 0 { response.ToolCalls = make([]types.LLMToolCall, 0, len(choice.Message.ToolCalls)) for _, tc := range choice.Message.ToolCalls { response.ToolCalls = append(response.ToolCalls, types.LLMToolCall{ ID: tc.ID, Type: string(tc.Type), Function: types.FunctionCall{ Name: tc.Function.Name, Arguments: tc.Function.Arguments, }, }) } } return response, nil } // ChatStream 进行流式聊天 func (c *RemoteAPIChat) ChatStream(ctx context.Context, messages []Message, opts *ChatOptions, ) (<-chan types.StreamResponse, error) { // 构建请求参数 req := c.buildChatCompletionRequest(messages, opts, true) // 创建流式响应通道 streamChan := make(chan types.StreamResponse) // 启动流式请求 stream, err := c.client.CreateChatCompletionStream(ctx, req) if err != nil { close(streamChan) return nil, fmt.Errorf("create chat completion stream: %w", err) } // 在后台处理流式响应 go func() { defer close(streamChan) defer stream.Close() toolCallMap := make(map[int]*types.LLMToolCall) lastFunctionName := make(map[int]string) nameNotified := make(map[int]bool) buildOrderedToolCalls := func() []types.LLMToolCall { if len(toolCallMap) == 0 { return nil } result := make([]types.LLMToolCall, 0, len(toolCallMap)) for i := 0; i < len(toolCallMap); i++ { if tc, ok := toolCallMap[i]; ok && tc != nil { result = append(result, *tc) } } if len(result) == 0 { return nil } return result } for { response, err := stream.Recv() if err != nil { // 发送最后一个响应,包含收集到的 tool calls streamChan <- types.StreamResponse{ ResponseType: types.ResponseTypeAnswer, Content: "", Done: true, ToolCalls: buildOrderedToolCalls(), } return } if len(response.Choices) > 0 { delta := response.Choices[0].Delta isDone := string(response.Choices[0].FinishReason) != "" // 收集 tool calls(流式响应中 tool calls 可能分多次返回) if len(delta.ToolCalls) > 0 { for _, tc := range delta.ToolCalls { // 检查是否已经存在该 tool call(通过 index) var toolCallIndex int if tc.Index != nil { toolCallIndex = *tc.Index } toolCallEntry, exists := toolCallMap[toolCallIndex] if !exists || toolCallEntry == nil { toolCallEntry = &types.LLMToolCall{ Type: string(tc.Type), Function: types.FunctionCall{ Name: "", Arguments: "", }, } toolCallMap[toolCallIndex] = toolCallEntry } // 更新 ID、类型 if tc.ID != "" { toolCallEntry.ID = tc.ID } if tc.Type != "" { toolCallEntry.Type = string(tc.Type) } // 累积函数名称(可能分多次返回) if tc.Function.Name != "" { toolCallEntry.Function.Name += tc.Function.Name } // 累积参数(可能为部分 JSON) argsUpdated := false if tc.Function.Arguments != "" { toolCallEntry.Function.Arguments += tc.Function.Arguments argsUpdated = true } currName := toolCallEntry.Function.Name if currName != "" && currName == lastFunctionName[toolCallIndex] && argsUpdated && !nameNotified[toolCallIndex] && toolCallEntry.ID != "" { streamChan <- types.StreamResponse{ ResponseType: types.ResponseTypeToolCall, Content: "", Done: false, Data: map[string]interface{}{ "tool_name": currName, "tool_call_id": toolCallEntry.ID, }, } nameNotified[toolCallIndex] = true } lastFunctionName[toolCallIndex] = currName } } // 发送内容块 if delta.Content != "" { streamChan <- types.StreamResponse{ ResponseType: types.ResponseTypeAnswer, Content: delta.Content, Done: isDone, ToolCalls: buildOrderedToolCalls(), } } // 如果是最后一次响应,确保发送包含所有 tool calls 的响应 if isDone && len(toolCallMap) > 0 { streamChan <- types.StreamResponse{ ResponseType: types.ResponseTypeAnswer, Content: "", Done: true, ToolCalls: buildOrderedToolCalls(), } } } } }() return streamChan, nil } // GetModelName 获取模型名称 func (c *RemoteAPIChat) GetModelName() string { return c.modelName } // GetModelID 获取模型ID func (c *RemoteAPIChat) GetModelID() string { return c.modelID }