package models import ( "context" "database/sql" "encoding/json" "fmt" "net/http" "strings" "time" "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/errors" seclib "yunion.io/x/pkg/utils" "yunion.io/x/sqlchemy" api "yunion.io/x/onecloud/pkg/apis/llm" "yunion.io/x/onecloud/pkg/appsrv" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/policy" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/llm/options" "yunion.io/x/onecloud/pkg/llm/utils" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/util/stringutils2" ) func init() { GetMCPAgentManager() } var mcpAgentManager *SMCPAgentManager var mcpAgentWorkerMan *appsrv.SWorkerManager func GetMCPAgentWorkerManager() *appsrv.SWorkerManager { return mcpAgentWorkerMan } func GetMCPAgentManager() *SMCPAgentManager { if mcpAgentManager != nil { return mcpAgentManager } mcpAgentManager = &SMCPAgentManager{ SSharableVirtualResourceBaseManager: db.NewSharableVirtualResourceBaseManager( SMCPAgent{}, "mcp_agents_tbl", "mcp_agent", "mcp_agents", ), } mcpAgentManager.SetVirtualObject(mcpAgentManager) return mcpAgentManager } type SMCPAgentManager struct { db.SSharableVirtualResourceBaseManager } // unsetOtherDefaultAgents 将除 excludeId 外所有条目的 default_agent 置为 false,保证全局唯一 func (man *SMCPAgentManager) unsetOtherDefaultAgents(ctx context.Context, excludeId string) error { q := man.Query().IsTrue("default_agent") if len(excludeId) > 0 { q = q.NotEquals("id", excludeId) } agents := make([]SMCPAgent, 0) err := db.FetchModelObjects(man, q, &agents) if err != nil { return errors.Wrap(err, "FetchModelObjects") } for i := range agents { _, err := db.Update(&agents[i], func() error { agents[i].DefaultAgent = false return nil }) if err != nil { return errors.Wrapf(err, "Update agent %s", agents[i].Id) } } return nil } // GetDefaultAgent 返回当前用户可见的、default_agent=true 的那条 MCP Agent(仅一条) func (man *SMCPAgentManager) GetDefaultAgent(ctx context.Context, userCred mcclient.TokenCredential) (*SMCPAgent, error) { query := jsonutils.NewDict() query.Set("default_agent", jsonutils.JSONTrue) ownerId, scope, err, _ := db.FetchCheckQueryOwnerScope(ctx, userCred, query, man, policy.PolicyActionList, true) if err != nil { return nil, errors.Wrap(err, "FetchCheckQueryOwnerScope") } q := man.Query() q = man.FilterByOwner(ctx, q, man, userCred, ownerId, scope) q = q.IsTrue("default_agent") var agent SMCPAgent err = q.First(&agent) if err != nil { if errors.Cause(err) == sql.ErrNoRows { return nil, nil } return nil, errors.Wrap(err, "First default agent") } return &agent, nil } // GetDefaultMcpServerTools 返回默认 MCP 服务器(options.Options.MCPServerURL)的 tools,不依赖任何 mcp_agent 记录 func (man *SMCPAgentManager) GetDefaultMcpServerTools(ctx context.Context, userCred mcclient.TokenCredential) (jsonutils.JSONObject, error) { timeout := time.Duration(options.Options.MCPAgentTimeout) * time.Second mcpClient := utils.NewMCPClient(options.Options.MCPServerURL, timeout, userCred) defer mcpClient.Close() tools, err := mcpClient.ListTools(ctx) if err != nil { return nil, errors.Wrap(err, "list default MCP tools") } return jsonutils.Marshal(tools), nil } type SMCPAgent struct { db.SSharableVirtualResourceBase // LLMId 关联的 LLM 实例 ID LLMId string `width:"128" charset:"ascii" nullable:"true" list:"user" create:"optional" update:"user"` // LLMUrl 对应后端大模型的 base 请求地址 LLMUrl string `width:"512" charset:"utf8" nullable:"false" list:"user" create:"required" update:"user"` // LLMDriver 对应使用的大模型驱动(llm_client),现在可以被设置为 ollama 或 openai LLMDriver string `width:"64" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"` // Model 使用的模型名称 Model string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"` // ApiKey 即在 llm_driver 中需要用到的认证 ApiKey string `width:"512" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"` // McpServer 即 mcp 服务器的后端地址 McpServer string `width:"512" charset:"utf8" nullable:"false" list:"user" create:"optional" update:"user"` // DefaultAgent 是否为默认 Agent,全局仅允许一条为 true DefaultAgent bool `default:"false" list:"user" create:"optional" update:"user"` } func (mcp *SMCPAgent) BeforeInsert() { if len(mcp.Id) == 0 { mcp.Id = db.DefaultUUIDGenerator() } if len(mcp.ApiKey) > 0 { sec, err := seclib.EncryptAESBase64(mcp.Id, mcp.ApiKey) if err != nil { log.Errorf("EncryptAESBase64 fail %s", err) } else { mcp.ApiKey = sec } } mcp.SSharableVirtualResourceBase.BeforeInsert() } func (mcp *SMCPAgent) BeforeUpdate() { if len(mcp.ApiKey) > 0 { // heuristic to check if it is plaintext _, err := seclib.DescryptAESBase64(mcp.Id, mcp.ApiKey) if err != nil { sec, err := seclib.EncryptAESBase64(mcp.Id, mcp.ApiKey) if err != nil { log.Errorf("EncryptAESBase64 fail %s", err) } else { mcp.ApiKey = sec } } } } func (mcp *SMCPAgent) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) { mcp.SSharableVirtualResourceBase.PostCreate(ctx, userCred, ownerId, query, data) if mcp.DefaultAgent { if err := GetMCPAgentManager().unsetOtherDefaultAgents(ctx, mcp.Id); err != nil { log.Errorf("unsetOtherDefaultAgents after create: %v", err) } } } func (mcp *SMCPAgent) PostUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) { mcp.SSharableVirtualResourceBase.PostUpdate(ctx, userCred, query, data) if mcp.DefaultAgent { if err := GetMCPAgentManager().unsetOtherDefaultAgents(ctx, mcp.Id); err != nil { log.Errorf("unsetOtherDefaultAgents after update: %v", err) } } } func (mcp *SMCPAgent) GetApiKey() (string, error) { if len(mcp.ApiKey) == 0 { return "", nil } // try decrypt key, err := seclib.DescryptAESBase64(mcp.Id, mcp.ApiKey) if err == nil { return key, nil } return mcp.ApiKey, nil } func (man *SMCPAgentManager) CustomizeHandlerInfo(info *appsrv.SHandlerInfo) { man.SSharableVirtualResourceBaseManager.CustomizeHandlerInfo(info) // log.Infoln("query name of handler info", info.GetName(nil)) switch info.GetName(nil) { case "get_specific": info.SetProcessTimeout(time.Hour * 4).SetWorkerManager(mcpAgentWorkerMan) } } func (man *SMCPAgentManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input *api.MCPAgentCreateInput) (*api.MCPAgentCreateInput, error) { var err error input.SharableVirtualResourceCreateInput, err = man.SSharableVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.SharableVirtualResourceCreateInput) if err != nil { return input, errors.Wrap(err, "validate SharableVirtualResourceCreateInput") } // 如果提供了 llm_id,则通过 LLM 获取 llm_url 和 model if len(input.LLMId) > 0 { llmObj, err := GetLLMManager().FetchByIdOrName(ctx, userCred, input.LLMId) if err != nil { return input, errors.Wrapf(err, "fetch LLM by id %s", input.LLMId) } llm := llmObj.(*SLLM) input.LLMId = llm.Id llmUrl, err := llm.GetLLMAccessUrlInfo(ctx, userCred, query) if err != nil { return input, errors.Wrapf(err, "get LLM URL from LLM %s", input.LLMId) } input.LLMUrl = llmUrl.LoginUrl if len(input.Model) == 0 { mdlInfos, err := llm.getProbedInstantModelsExt(ctx, userCred) if err != nil { return input, errors.Wrap(err, "get probed models from LLM instance") } if len(mdlInfos) == 0 { return input, httperrors.NewBadRequestError("no available models found in LLM instance %s", input.LLMId) } var firstModel api.LLMInternalInstantMdlInfo for _, mdlInfo := range mdlInfos { firstModel = mdlInfo break } input.Model = fmt.Sprintf("%s:%s", firstModel.Name, firstModel.Tag) } } // 验证 llm_url 不为空 if len(input.LLMUrl) == 0 { return input, errors.Wrap(httperrors.ErrInputParameter, "llm_url is required (or provide llm_id to auto-fetch)") } // 验证 llm_driver 必须是 ollama 或 openai input.LLMDriver = strings.ToLower(strings.TrimSpace(input.LLMDriver)) if !api.IsLLMClientType(input.LLMDriver) { return input, errors.Wrapf(httperrors.ErrInputParameter, "llm_driver must be one of: %s, got: %s", api.LLM_CLIENT_TYPES.List(), input.LLMDriver) } // 验证 model 不为空 if len(input.Model) == 0 { return input, errors.Wrap(httperrors.ErrInputParameter, "model is required") } // 验证 mcp_server 不为空 if len(input.McpServer) == 0 { input.McpServer = options.Options.MCPServerURL } // 对于 openai 驱动,api_key 是必需的 if input.LLMDriver == string(api.LLM_CLIENT_OPENAI) && len(input.ApiKey) == 0 { return input, errors.Wrap(httperrors.ErrInputParameter, "api_key is required when llm_driver is openai") } input.Status = api.STATUS_READY return input, nil } func (man *SMCPAgentManager) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input *api.MCPAgentUpdateInput) (*api.MCPAgentUpdateInput, error) { var err error input.SharableVirtualResourceCreateInput, err = man.SSharableVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.SharableVirtualResourceCreateInput) if err != nil { return input, errors.Wrap(err, "validate SharableVirtualResourceCreateInput") } // 如果提供了 llm_id,则通过 LLM 获取 llm_url 和 model if input.LLMId != nil && len(*input.LLMId) > 0 { llmObj, err := GetLLMManager().FetchByIdOrName(ctx, userCred, *input.LLMId) if err != nil { return input, errors.Wrapf(err, "fetch LLM by id %s", *input.LLMId) } llm := llmObj.(*SLLM) llmUrl, err := llm.GetLLMAccessUrlInfo(ctx, userCred, query) if err != nil { return input, errors.Wrapf(err, "get LLM URL from LLM %s", *input.LLMId) } input.LLMUrl = &llmUrl.LoginUrl if input.Model == nil || len(*input.Model) == 0 { mdlInfos, err := llm.getProbedInstantModelsExt(ctx, userCred) if err != nil { return input, errors.Wrap(err, "get probed models from LLM instance") } if len(mdlInfos) == 0 { return input, httperrors.NewBadRequestError("no available models found in LLM instance %s", *input.LLMId) } var firstModel api.LLMInternalInstantMdlInfo for _, mdlInfo := range mdlInfos { firstModel = mdlInfo break } modelStr := fmt.Sprintf("%s:%s", firstModel.Name, firstModel.Tag) input.Model = &modelStr } } // 如果更新 llm_driver,验证其值 if input.LLMDriver != nil { *input.LLMDriver = strings.ToLower(strings.TrimSpace(*input.LLMDriver)) if !api.IsLLMClientType(*input.LLMDriver) { return input, errors.Wrapf(httperrors.ErrInputParameter, "llm_driver must be one of: %s, got: %s", api.LLM_CLIENT_TYPES.List(), *input.LLMDriver) } } return input, nil } func (man *SMCPAgentManager) ListItemFilter( ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.MCPAgentListInput, ) (*sqlchemy.SQuery, error) { q, err := man.SSharableVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, input.SharableVirtualResourceListInput) if err != nil { return nil, errors.Wrapf(err, "SSharableVirtualResourceBaseManager.ListItemFilter") } if len(input.LLMDriver) > 0 { q = q.Equals("llm_driver", strings.ToLower(strings.TrimSpace(input.LLMDriver))) } if input.DefaultAgent != nil && *input.DefaultAgent { q = q.IsTrue("default_agent") } return q, nil } func (manager *SMCPAgentManager) FetchCustomizeColumns( ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool, ) []api.MCPAgentDetails { rows := make([]api.MCPAgentDetails, len(objs)) vrows := manager.SSharableVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) agents := []SMCPAgent{} jsonutils.Update(&agents, objs) llmIds := make([]string, 0) for i := range agents { if len(agents[i].LLMId) > 0 { llmIds = append(llmIds, agents[i].LLMId) } } var llmIdNameMap map[string]string if len(llmIds) > 0 { var err error llmIdNameMap, err = db.FetchIdNameMap2(GetLLMManager(), llmIds) if err != nil { log.Errorf("FetchIdNameMap2 for LLMs failed: %v", err) } } for i := range rows { rows[i].SharableVirtualResourceDetails = vrows[i] if i < len(agents) { rows[i].LLMId = agents[i].LLMId if name, ok := llmIdNameMap[agents[i].LLMId]; ok { rows[i].LLMName = name } rows[i].DefaultAgent = agents[i].DefaultAgent } } return rows } func (mcp *SMCPAgent) GetLLMClientDriver() ILLMClient { return GetLLMClientDriver(api.LLMClientType(mcp.LLMDriver)) } func (mcp *SMCPAgent) GetMcpServerUrl(ctx context.Context, userCred mcclient.TokenCredential) (string, error) { if len(mcp.McpServer) > 0 { return mcp.McpServer, nil } return options.Options.MCPServerURL, nil } func (mcp *SMCPAgent) GetDetailsMcpTools(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) { // 创建 MCP 客户端 timeout := time.Duration(options.Options.MCPAgentTimeout) * time.Second mcpServerUrl, err := mcp.GetMcpServerUrl(ctx, userCred) if err != nil { return nil, errors.Wrap(err, "GetMcpServerUrl") } mcpClient := utils.NewMCPClient(mcpServerUrl, timeout, userCred) // 获取工具列表 tools, err := mcpClient.ListTools(ctx) if err != nil { return nil, errors.Wrap(err, "list MCP tools") } return jsonutils.Marshal(tools), nil } func (mcp *SMCPAgent) GetDetailsToolRequest( ctx context.Context, userCred mcclient.TokenCredential, input api.LLMToolRequestInput, ) (jsonutils.JSONObject, error) { // 创建 MCP 客户端 timeout := time.Duration(options.Options.MCPAgentTimeout) * time.Second mcpServerUrl, err := mcp.GetMcpServerUrl(ctx, userCred) if err != nil { return nil, errors.Wrap(err, "GetMcpServerUrl") } mcpClient := utils.NewMCPClient(mcpServerUrl, timeout, userCred) defer mcpClient.Close() // 调用工具 result, err := mcpClient.CallTool(ctx, input.ToolName, input.Arguments) if err != nil { return nil, errors.Wrapf(err, "call tool %s", input.ToolName) } return jsonutils.Marshal(result), nil } // func (mcp *SMCPAgent) GetDetailsChatTest( // ctx context.Context, // userCred mcclient.TokenCredential, // input api.LLMChatTestInput, // ) (jsonutils.JSONObject, error) { // llmClient := mcp.GetLLMClientDriver() // if llmClient == nil { // return nil, errors.Error("failed to get LLM client driver") // } // message := llmClient.NewUserMessage(input.Message) // result, err := llmClient.Chat(ctx, mcp, []ILLMChatMessage{message}, nil) // if err != nil { // return nil, errors.Wrap(err, "chat with LLM") // } // return jsonutils.Marshal(result), nil // } func (mcp *SMCPAgent) PerformChatStream( ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.LLMMCPAgentRequestInput, ) (jsonutils.JSONObject, error) { appParams := appsrv.AppContextGetParams(ctx) if appParams == nil { return nil, errors.Error("failed to get app params") } w := appParams.Response w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") if f, ok := w.(http.Flusher); ok { f.Flush() } else { return nil, errors.Error("Streaming unsupported!") } _, err := mcp.process(ctx, userCred, &input, func(content string) error { if len(content) > 0 { for line := range strings.SplitSeq(content, "\n") { fmt.Fprintf(w, "data: %s\n", line) } fmt.Fprintf(w, "\n") if f, ok := w.(http.Flusher); ok { f.Flush() } } return nil }) if err != nil { fmt.Fprintf(w, "data: Error: %v\n\n", err) } return nil, nil } // process 处理用户请求 func (mcp *SMCPAgent) process(ctx context.Context, userCred mcclient.TokenCredential, req *api.LLMMCPAgentRequestInput, onStream func(string) error) (*api.MCPAgentResponse, error) { // 获取 MCP Server 的工具列表 mcpServerUrl, err := mcp.GetMcpServerUrl(ctx, userCred) if err != nil { return nil, errors.Wrap(err, "GetMcpServerUrl") } mcpClient := utils.NewMCPClient(mcpServerUrl, 10*time.Minute, userCred) defer mcpClient.Close() mcpTools, err := mcpClient.ListTools(ctx) if err != nil { return nil, errors.Wrap(err, "list MCP tools") } log.Infof("Got %d tools from MCP Server", len(mcpTools)) // get llmClient llmClient := mcp.GetLLMClientDriver() if llmClient == nil { return nil, errors.Error("failed to get LLM client driver") } tools := llmClient.ConvertMCPTools(mcpTools) // 构建系统提示词 systemPrompt := buildSystemPrompt() // 初始化消息历史 messages := make([]ILLMChatMessage, 0) messages = append(messages, llmClient.NewSystemMessage(systemPrompt)) // 处理历史消息 if len(req.History) > 0 { historyMessages := processHistoryMessages( req.History, llmClient, options.Options.MCPAgentUserCharLimit, options.Options.MCPAgentAssistantCharLimit, ) messages = append(messages, historyMessages...) } messages = append(messages, llmClient.NewUserMessage(req.Message)) // 记录工具调用 var toolCallRecords []api.MCPAgentToolCallRecord log.Infof("Phase 1: Thinking & Acting...") // 处理流式的工具调用参数 type accumToolCall struct { Id string Name string RawArguments strings.Builder } accToolCalls := make(map[int]*accumToolCall) var accumulatedContent strings.Builder var accumulatedReasoning strings.Builder hasToolCalls := false err = llmClient.ChatStream(ctx, mcp, messages, tools, func(chunk ILLMChatResponse) error { if chunk.HasToolCalls() { hasToolCalls = true for _, tc := range chunk.GetToolCalls() { idx := tc.GetIndex() if _, exists := accToolCalls[idx]; !exists { accToolCalls[idx] = &accumToolCall{ Id: tc.GetId(), } } atc := accToolCalls[idx] if id := tc.GetId(); id != "" { atc.Id = id } if name := tc.GetFunction().GetName(); name != "" { atc.Name = name } if args := tc.GetFunction().GetRawArguments(); args != "" { atc.RawArguments.WriteString(args) } } } if r := chunk.GetReasoningContent(); len(r) > 0 { accumulatedReasoning.WriteString(r) } content := chunk.GetContent() if len(content) > 0 { accumulatedContent.WriteString(content) if onStream != nil { if err := onStream(content); err != nil { return err } } } return nil }) if err != nil { return nil, errors.Wrap(err, "phase 1 chat stream error") } // 检查是否有工具调用 if !hasToolCalls { // 如果阶段一没有调用工具,直接返回结果 return &api.MCPAgentResponse{ Success: true, Answer: accumulatedContent.String(), ToolCalls: toolCallRecords, }, nil } // Convert accumulated tool calls to ILLMToolCall var toolCalls []ILLMToolCall // Find max index maxIdx := -1 for idx := range accToolCalls { if idx > maxIdx { maxIdx = idx } } for i := 0; i <= maxIdx; i++ { if atc, ok := accToolCalls[i]; ok { var args map[string]interface{} rawArgs := atc.RawArguments.String() if len(rawArgs) > 0 { if err := json.Unmarshal([]byte(rawArgs), &args); err != nil { log.Errorf("Failed to unmarshal arguments for tool %s: %v. Raw: %s", atc.Name, err, rawArgs) args = make(map[string]interface{}) } } else { args = make(map[string]interface{}) } toolCalls = append(toolCalls, &SLLMToolCall{ Id: atc.Id, Function: SLLMFunctionCall{ Name: atc.Name, Arguments: args, }, }) } } log.Infof("Got %d tool calls from Phase 1", len(toolCalls)) toolCallRecords, toolMessages, err := processToolCalls(ctx, toolCalls, accumulatedReasoning.String(), accumulatedContent.String(), mcpClient, llmClient) if err != nil { return nil, errors.Wrap(err, "process tool calls") } // 将工具调用相关的消息加入历史 messages = append(messages, toolMessages...) log.Infof("Phase 2: Streaming Response...") var finalAnswer strings.Builder err = llmClient.ChatStream(ctx, mcp, messages, tools, func(chunk ILLMChatResponse) error { content := chunk.GetContent() if len(content) > 0 { // 聚合最终答案 finalAnswer.WriteString(content) // 实时流式输出 if onStream != nil { if err := onStream(content); err != nil { return err } } } return nil }) if err != nil { return nil, errors.Wrap(err, "phase 2 stream error") } return &api.MCPAgentResponse{ Success: true, Answer: finalAnswer.String(), ToolCalls: toolCallRecords, }, nil } // buildSystemPrompt 构建系统提示词 func buildSystemPrompt() string { return api.MCP_AGENT_SYSTEM_PROMPT } func processHistoryMessages( history []api.MCPAgentChatMessage, llmClient ILLMClient, maxUserChars int, maxAssistantChars int, ) []ILLMChatMessage { if len(history) == 0 { return []ILLMChatMessage{} } var userChars, assistantChars int processedMessages := make([]ILLMChatMessage, 0) // 从最新的消息开始遍历,保留最新消息,丢弃最旧消息 for i := len(history) - 1; i >= 0; i-- { msg := history[i] msgChars := len(msg.Content) switch msg.Role { case "user": if userChars+msgChars > maxUserChars { break } userChars += msgChars processedMessages = append(processedMessages, llmClient.NewUserMessage(msg.Content)) case "assistant": if assistantChars+msgChars > maxAssistantChars { break } assistantChars += msgChars if len(msg.Content) > 0 { processedMessages = append(processedMessages, llmClient.NewAssistantMessage(msg.Content)) } } } for i, j := 0, len(processedMessages)-1; i < j; i, j = i+1, j-1 { processedMessages[i], processedMessages[j] = processedMessages[j], processedMessages[i] } return processedMessages } // processToolCalls 处理工具调用 func processToolCalls( ctx context.Context, toolCalls []ILLMToolCall, reasoningContent, content string, mcpClient *utils.MCPClient, llmClient ILLMClient, ) ([]api.MCPAgentToolCallRecord, []ILLMChatMessage, error) { toolCallRecords := make([]api.MCPAgentToolCallRecord, 0) messagesToAdd := make([]ILLMChatMessage, 0) // 使用带 reasoning_content 的 assistant 消息,满足 DeepSeek thinking mode + tool calls 要求 messagesToAdd = append(messagesToAdd, llmClient.NewAssistantMessageWithToolCallsAndReasoning(reasoningContent, content, toolCalls)) // 执行每个工具调用 for _, tc := range toolCalls { fc := tc.GetFunction() toolName := fc.GetName() arguments := fc.GetArguments() if arguments == nil { arguments = make(map[string]interface{}) } log.Infof("Calling tool: %s with arguments: %v", toolName, arguments) // 调用 MCP 工具 result, err := mcpClient.CallTool(ctx, toolName, arguments) resultText := utils.FormatToolResult(toolName, result, err) log.Infoln("Get result from mcp query", resultText) toolCallRecords = append(toolCallRecords, api.MCPAgentToolCallRecord{ Id: tc.GetId(), ToolName: toolName, Arguments: arguments, Result: resultText, }) // 将工具执行结果加入历史 messagesToAdd = append(messagesToAdd, llmClient.NewToolMessage(tc.GetId(), toolName, resultText)) } return toolCallRecords, messagesToAdd, nil }