Files
CyberStrikeAI/internal/agent/agent.go
T
2026-04-19 04:43:45 +08:00

1875 lines
61 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agent
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"cyberstrike-ai/internal/config"
"cyberstrike-ai/internal/mcp"
"cyberstrike-ai/internal/mcp/builtin"
"cyberstrike-ai/internal/openai"
"cyberstrike-ai/internal/security"
"cyberstrike-ai/internal/storage"
"go.uber.org/zap"
)
// Agent AI代理
type Agent struct {
openAIClient *openai.Client
config *config.OpenAIConfig
agentConfig *config.AgentConfig
memoryCompressor *MemoryCompressor
mcpServer *mcp.Server
externalMCPMgr *mcp.ExternalMCPManager // 外部MCP管理器
logger *zap.Logger
maxIterations int
resultStorage ResultStorage // 结果存储
largeResultThreshold int // 大结果阈值(字节)
mu sync.RWMutex // 添加互斥锁以支持并发更新
toolNameMapping map[string]string // 工具名称映射:OpenAI格式 -> 原始格式(用于外部MCP工具)
currentConversationID string // 当前对话ID(用于自动传递给工具)
promptBaseDir string // 解析 system_prompt_path 时相对路径的基准目录(通常为 config.yaml 所在目录)
}
// ResultStorage 结果存储接口(直接使用 storage 包的类型)
type ResultStorage interface {
SaveResult(executionID string, toolName string, result string) error
GetResult(executionID string) (string, error)
GetResultPage(executionID string, page int, limit int) (*storage.ResultPage, error)
SearchResult(executionID string, keyword string, useRegex bool) ([]string, error)
FilterResult(executionID string, filter string, useRegex bool) ([]string, error)
GetResultMetadata(executionID string) (*storage.ResultMetadata, error)
GetResultPath(executionID string) string
DeleteResult(executionID string) error
}
// NewAgent 创建新的Agent
func NewAgent(cfg *config.OpenAIConfig, agentCfg *config.AgentConfig, mcpServer *mcp.Server, externalMCPMgr *mcp.ExternalMCPManager, logger *zap.Logger, maxIterations int) *Agent {
// 如果 maxIterations 为 0 或负数,使用默认值 30
if maxIterations <= 0 {
maxIterations = 30
}
// 设置大结果阈值,默认50KB
largeResultThreshold := 50 * 1024
if agentCfg != nil && agentCfg.LargeResultThreshold > 0 {
largeResultThreshold = agentCfg.LargeResultThreshold
}
// 设置结果存储目录,默认tmp
resultStorageDir := "tmp"
if agentCfg != nil && agentCfg.ResultStorageDir != "" {
resultStorageDir = agentCfg.ResultStorageDir
}
// 初始化结果存储
var resultStorage ResultStorage
if resultStorageDir != "" {
// 导入storage包(避免循环依赖,使用接口)
// 这里需要在实际使用时初始化
// 暂时设为nil,在需要时初始化
}
// 配置HTTP Transport,优化连接管理和超时设置
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 300 * time.Second,
KeepAlive: 300 * time.Second,
}).DialContext,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 30 * time.Second,
ResponseHeaderTimeout: 60 * time.Minute, // 响应头超时:增加到15分钟,应对大响应
DisableKeepAlives: false, // 启用连接复用
}
// 增加超时时间到30分钟,以支持长时间运行的AI推理
// 特别是当使用流式响应或处理复杂任务时
httpClient := &http.Client{
Timeout: 30 * time.Minute, // 从5分钟增加到30分钟
Transport: transport,
}
llmClient := openai.NewClient(cfg, httpClient, logger)
var memoryCompressor *MemoryCompressor
if cfg != nil {
mc, err := NewMemoryCompressor(MemoryCompressorConfig{
MaxTotalTokens: cfg.MaxTotalTokens,
OpenAIConfig: cfg,
HTTPClient: httpClient,
Logger: logger,
})
if err != nil {
logger.Warn("初始化MemoryCompressor失败,将跳过上下文压缩", zap.Error(err))
} else {
memoryCompressor = mc
}
} else {
logger.Warn("OpenAI配置为空,无法初始化MemoryCompressor")
}
return &Agent{
openAIClient: llmClient,
config: cfg,
agentConfig: agentCfg,
memoryCompressor: memoryCompressor,
mcpServer: mcpServer,
externalMCPMgr: externalMCPMgr,
logger: logger,
maxIterations: maxIterations,
resultStorage: resultStorage,
largeResultThreshold: largeResultThreshold,
toolNameMapping: make(map[string]string), // 初始化工具名称映射
}
}
// SetResultStorage 设置结果存储(用于避免循环依赖)
func (a *Agent) SetResultStorage(storage ResultStorage) {
a.mu.Lock()
defer a.mu.Unlock()
a.resultStorage = storage
}
// SetPromptBaseDir 设置单代理 system_prompt_path 相对路径的基准目录(一般为 config.yaml 所在目录)。
func (a *Agent) SetPromptBaseDir(dir string) {
a.mu.Lock()
defer a.mu.Unlock()
a.promptBaseDir = strings.TrimSpace(dir)
}
// ChatMessage 聊天消息
type ChatMessage struct {
Role string `json:"role"`
Content string `json:"content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}
// MarshalJSON 自定义JSON序列化,将tool_calls中的arguments转换为JSON字符串
func (cm ChatMessage) MarshalJSON() ([]byte, error) {
// 构建序列化结构
aux := map[string]interface{}{
"role": cm.Role,
}
// 添加content(如果存在)
if cm.Content != "" {
aux["content"] = cm.Content
}
// 添加tool_call_id(如果存在)
if cm.ToolCallID != "" {
aux["tool_call_id"] = cm.ToolCallID
}
// 转换tool_calls,将arguments转换为JSON字符串
if len(cm.ToolCalls) > 0 {
toolCallsJSON := make([]map[string]interface{}, len(cm.ToolCalls))
for i, tc := range cm.ToolCalls {
// 将arguments转换为JSON字符串
argsJSON := ""
if tc.Function.Arguments != nil {
argsBytes, err := json.Marshal(tc.Function.Arguments)
if err != nil {
return nil, err
}
argsJSON = string(argsBytes)
}
toolCallsJSON[i] = map[string]interface{}{
"id": tc.ID,
"type": tc.Type,
"function": map[string]interface{}{
"name": tc.Function.Name,
"arguments": argsJSON,
},
}
}
aux["tool_calls"] = toolCallsJSON
}
return json.Marshal(aux)
}
// OpenAIRequest OpenAI API请求
type OpenAIRequest struct {
Model string `json:"model"`
Messages []ChatMessage `json:"messages"`
Tools []Tool `json:"tools,omitempty"`
Stream bool `json:"stream,omitempty"`
}
// OpenAIResponse OpenAI API响应
type OpenAIResponse struct {
ID string `json:"id"`
Choices []Choice `json:"choices"`
Error *Error `json:"error,omitempty"`
}
// Choice 选择
type Choice struct {
Message MessageWithTools `json:"message"`
FinishReason string `json:"finish_reason"`
}
// MessageWithTools 带工具调用的消息
type MessageWithTools struct {
Role string `json:"role"`
Content string `json:"content"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
}
// Tool OpenAI工具定义
type Tool struct {
Type string `json:"type"`
Function FunctionDefinition `json:"function"`
}
// FunctionDefinition 函数定义
type FunctionDefinition struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
}
// Error OpenAI错误
type Error struct {
Message string `json:"message"`
Type string `json:"type"`
}
// ToolCall 工具调用
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function FunctionCall `json:"function"`
}
// FunctionCall 函数调用
type FunctionCall struct {
Name string `json:"name"`
Arguments map[string]interface{} `json:"arguments"`
}
// UnmarshalJSON 自定义JSON解析,处理arguments可能是字符串或对象的情况
func (fc *FunctionCall) UnmarshalJSON(data []byte) error {
type Alias FunctionCall
aux := &struct {
Name string `json:"name"`
Arguments interface{} `json:"arguments"`
*Alias
}{
Alias: (*Alias)(fc),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
fc.Name = aux.Name
// 处理arguments可能是字符串或对象的情况
switch v := aux.Arguments.(type) {
case map[string]interface{}:
fc.Arguments = v
case string:
// 如果是字符串,尝试解析为JSON
if err := json.Unmarshal([]byte(v), &fc.Arguments); err != nil {
// 如果解析失败,创建一个包含原始字符串的map
fc.Arguments = map[string]interface{}{
"raw": v,
}
}
case nil:
fc.Arguments = make(map[string]interface{})
default:
// 其他类型,尝试转换为map
fc.Arguments = map[string]interface{}{
"value": v,
}
}
return nil
}
// AgentLoopResult Agent Loop执行结果
type AgentLoopResult struct {
Response string
MCPExecutionIDs []string
LastReActInput string // 最后一轮ReAct的输入(压缩后的messagesJSON格式)
LastReActOutput string // 最终大模型的输出
}
// ProgressCallback 进度回调函数类型
type ProgressCallback func(eventType, message string, data interface{})
// AgentLoop 执行Agent循环
func (a *Agent) AgentLoop(ctx context.Context, userInput string, historyMessages []ChatMessage) (*AgentLoopResult, error) {
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, "", nil, nil, nil)
}
// AgentLoopWithConversationID 执行Agent循环(带对话ID
func (a *Agent) AgentLoopWithConversationID(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string) (*AgentLoopResult, error) {
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, conversationID, nil, nil, nil)
}
// EinoSingleAgentSystemInstruction 供 Eino adk.ChatModelAgent.Instruction 使用,与 AgentLoopWithProgress 首条 system 对齐(含 system_prompt_path 与 Skills 提示)。
func (a *Agent) EinoSingleAgentSystemInstruction(roleSkills []string) string {
systemPrompt := DefaultSingleAgentSystemPrompt()
if a.agentConfig != nil {
if p := strings.TrimSpace(a.agentConfig.SystemPromptPath); p != "" {
path := p
a.mu.RLock()
base := a.promptBaseDir
a.mu.RUnlock()
if !filepath.IsAbs(path) && base != "" {
path = filepath.Join(base, path)
}
if b, err := os.ReadFile(path); err != nil {
a.logger.Warn("读取单代理 system_prompt_path 失败,使用内置提示", zap.String("path", path), zap.Error(err))
} else if s := strings.TrimSpace(string(b)); s != "" {
systemPrompt = s
}
}
}
if len(roleSkills) > 0 {
var skillsHint strings.Builder
skillsHint.WriteString("\n\n本角色推荐使用的Skills\n")
for i, skillName := range roleSkills {
if i > 0 {
skillsHint.WriteString("、")
}
skillsHint.WriteString("`")
skillsHint.WriteString(skillName)
skillsHint.WriteString("`")
}
skillsHint.WriteString("\n- 这些名称与 skills/ 下 SKILL.md 的 `name` 一致。")
skillsHint.WriteString("\n- 若当前会话已启用 Eino 内置 `skill` 工具,请按需加载;否则以 MCP 与文本工作流完成。")
skillsHint.WriteString("\n- 例如传入 skill 参数为 `")
skillsHint.WriteString(roleSkills[0])
skillsHint.WriteString("`")
systemPrompt += skillsHint.String()
}
return systemPrompt
}
// AgentLoopWithProgress 执行Agent循环(带进度回调和对话ID)
// roleSkills: 角色配置的skills列表(用于在系统提示词中提示AI,但不硬编码内容)
func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string, callback ProgressCallback, roleTools []string, roleSkills []string) (*AgentLoopResult, error) {
// 设置当前对话ID
a.mu.Lock()
a.currentConversationID = conversationID
a.mu.Unlock()
// 发送进度更新
sendProgress := func(eventType, message string, data interface{}) {
if callback != nil {
callback(eventType, message, data)
}
}
systemPrompt := DefaultSingleAgentSystemPrompt()
if a.agentConfig != nil {
if p := strings.TrimSpace(a.agentConfig.SystemPromptPath); p != "" {
path := p
a.mu.RLock()
base := a.promptBaseDir
a.mu.RUnlock()
if !filepath.IsAbs(path) && base != "" {
path = filepath.Join(base, path)
}
if b, err := os.ReadFile(path); err != nil {
a.logger.Warn("读取单代理 system_prompt_path 失败,使用内置提示", zap.String("path", path), zap.Error(err))
} else if s := strings.TrimSpace(string(b)); s != "" {
systemPrompt = s
}
}
}
// 如果角色配置了skills,在系统提示词中提示AI(但不硬编码内容)
if len(roleSkills) > 0 {
var skillsHint strings.Builder
skillsHint.WriteString("\n\n本角色推荐使用的Skills\n")
for i, skillName := range roleSkills {
if i > 0 {
skillsHint.WriteString("、")
}
skillsHint.WriteString("`")
skillsHint.WriteString(skillName)
skillsHint.WriteString("`")
}
skillsHint.WriteString("\n- 这些名称与 skills/ 下 SKILL.md 的 `name` 一致;在 **Eino 多代理** 会话中请用内置 `skill` 工具按需加载全文")
skillsHint.WriteString("\n- 例如:在支持 Eino skill 工具时传入 skill 参数为 `")
skillsHint.WriteString(roleSkills[0])
skillsHint.WriteString("`")
skillsHint.WriteString("\n- 单代理 MCP 模式不会注入 skill 工具;需要时请使用多代理(DeepAgent")
systemPrompt += skillsHint.String()
}
messages := []ChatMessage{
{
Role: "system",
Content: systemPrompt,
},
}
// 添加历史消息(保留所有字段,包括ToolCalls和ToolCallID
a.logger.Info("处理历史消息",
zap.Int("count", len(historyMessages)),
)
addedCount := 0
for i, msg := range historyMessages {
// 对于tool消息,即使content为空也要添加(因为tool消息可能只有ToolCallID
// 对于其他消息,只添加有内容的消息
if msg.Role == "tool" || msg.Content != "" {
messages = append(messages, ChatMessage{
Role: msg.Role,
Content: msg.Content,
ToolCalls: msg.ToolCalls,
ToolCallID: msg.ToolCallID,
})
addedCount++
contentPreview := msg.Content
if len(contentPreview) > 50 {
contentPreview = contentPreview[:50] + "..."
}
a.logger.Info("添加历史消息到上下文",
zap.Int("index", i),
zap.String("role", msg.Role),
zap.String("content", contentPreview),
zap.Int("toolCalls", len(msg.ToolCalls)),
zap.String("toolCallID", msg.ToolCallID),
)
}
}
a.logger.Info("构建消息数组",
zap.Int("historyMessages", len(historyMessages)),
zap.Int("addedMessages", addedCount),
zap.Int("totalMessages", len(messages)),
)
// 在添加当前用户消息之前,先修复可能存在的失配tool消息
// 这可以防止在继续对话时出现"messages with role 'tool' must be a response to a preceeding message with 'tool_calls'"错误
if len(messages) > 0 {
if fixed := a.repairOrphanToolMessages(&messages); fixed {
a.logger.Info("修复了历史消息中的失配tool消息")
}
}
// 添加当前用户消息
messages = append(messages, ChatMessage{
Role: "user",
Content: userInput,
})
result := &AgentLoopResult{
MCPExecutionIDs: make([]string, 0),
}
// 用于保存当前的messages,以便在异常情况下也能保存ReAct输入
var currentReActInput string
maxIterations := a.maxIterations
thinkingStreamSeq := 0
for i := 0; i < maxIterations; i++ {
// 先获取本轮可用工具并统计 tools token,再压缩,以便压缩时预留 tools 占用的空间
tools := a.getAvailableTools(roleTools)
toolsTokens := a.countToolsTokens(tools)
messages = a.applyMemoryCompression(ctx, messages, toolsTokens)
// 检查是否是最后一次迭代
isLastIteration := (i == maxIterations-1)
// 每次迭代都保存压缩后的messages,以便在异常中断(取消、错误等)时也能保存最新的ReAct输入
// 保存压缩后的数据,这样后续使用时就不需要再考虑压缩了
messagesJSON, err := json.Marshal(messages)
if err != nil {
a.logger.Warn("序列化ReAct输入失败", zap.Error(err))
} else {
currentReActInput = string(messagesJSON)
// 更新result中的值,确保始终保存最新的ReAct输入(压缩后的)
result.LastReActInput = currentReActInput
}
// 检查上下文是否已取消
select {
case <-ctx.Done():
// 上下文被取消(可能是用户主动暂停或其他原因)
a.logger.Info("检测到上下文取消,保存当前ReAct数据", zap.Error(ctx.Err()))
result.LastReActInput = currentReActInput
if ctx.Err() == context.Canceled {
result.Response = "任务已被取消。"
} else {
result.Response = fmt.Sprintf("任务执行中断: %v", ctx.Err())
}
result.LastReActOutput = result.Response
return result, ctx.Err()
default:
}
// 记录当前上下文的 Token 用量(messages + tools),展示压缩器运行状态
if a.memoryCompressor != nil {
messagesTokens, systemCount, regularCount := a.memoryCompressor.totalTokensFor(messages)
totalTokens := messagesTokens + toolsTokens
a.logger.Info("memory compressor context stats",
zap.Int("iteration", i+1),
zap.Int("messagesCount", len(messages)),
zap.Int("systemMessages", systemCount),
zap.Int("regularMessages", regularCount),
zap.Int("messagesTokens", messagesTokens),
zap.Int("toolsTokens", toolsTokens),
zap.Int("totalTokens", totalTokens),
zap.Int("maxTotalTokens", a.memoryCompressor.maxTotalTokens),
)
}
// 发送迭代开始事件
if i == 0 {
sendProgress("iteration", "开始分析请求并制定测试策略", map[string]interface{}{
"iteration": i + 1,
"total": maxIterations,
})
} else if isLastIteration {
sendProgress("iteration", fmt.Sprintf("第 %d 轮迭代(最后一次)", i+1), map[string]interface{}{
"iteration": i + 1,
"total": maxIterations,
"isLast": true,
})
} else {
sendProgress("iteration", fmt.Sprintf("第 %d 轮迭代", i+1), map[string]interface{}{
"iteration": i + 1,
"total": maxIterations,
})
}
// 记录每次调用OpenAI
if i == 0 {
a.logger.Info("调用OpenAI",
zap.Int("iteration", i+1),
zap.Int("messagesCount", len(messages)),
)
// 记录前几条消息的内容(用于调试)
for j, msg := range messages {
if j >= 5 { // 只记录前5条
break
}
contentPreview := msg.Content
if len(contentPreview) > 100 {
contentPreview = contentPreview[:100] + "..."
}
a.logger.Debug("消息内容",
zap.Int("index", j),
zap.String("role", msg.Role),
zap.String("content", contentPreview),
)
}
} else {
a.logger.Info("调用OpenAI",
zap.Int("iteration", i+1),
zap.Int("messagesCount", len(messages)),
)
}
// 调用OpenAI
sendProgress("progress", "正在调用AI模型...", nil)
thinkingStreamSeq++
thinkingStreamId := fmt.Sprintf("thinking-stream-%s-%d-%d", conversationID, i+1, thinkingStreamSeq)
thinkingStreamStarted := false
response, err := a.callOpenAIStreamWithToolCalls(ctx, messages, tools, func(delta string) error {
if delta == "" {
return nil
}
if !thinkingStreamStarted {
thinkingStreamStarted = true
sendProgress("thinking_stream_start", " ", map[string]interface{}{
"streamId": thinkingStreamId,
"iteration": i + 1,
"toolStream": false,
})
}
sendProgress("thinking_stream_delta", delta, map[string]interface{}{
"streamId": thinkingStreamId,
"iteration": i + 1,
})
return nil
})
if err != nil {
// API调用失败,保存当前的ReAct输入和错误信息作为输出
result.LastReActInput = currentReActInput
errorMsg := fmt.Sprintf("调用OpenAI失败: %v", err)
result.Response = errorMsg
result.LastReActOutput = errorMsg
a.logger.Warn("OpenAI调用失败,已保存ReAct数据", zap.Error(err))
return result, fmt.Errorf("调用OpenAI失败: %w", err)
}
if response.Error != nil {
if handled, toolName := a.handleMissingToolError(response.Error.Message, &messages); handled {
sendProgress("warning", fmt.Sprintf("模型尝试调用不存在的工具:%s,已提示其改用可用工具。", toolName), map[string]interface{}{
"toolName": toolName,
})
a.logger.Warn("模型调用了不存在的工具,将重试",
zap.String("tool", toolName),
zap.String("error", response.Error.Message),
)
continue
}
if a.handleToolRoleError(response.Error.Message, &messages) {
sendProgress("warning", "检测到未配对的工具结果,已自动修复上下文并重试。", map[string]interface{}{
"error": response.Error.Message,
})
a.logger.Warn("检测到未配对的工具消息,已修复并重试",
zap.String("error", response.Error.Message),
)
continue
}
// OpenAI返回错误,保存当前的ReAct输入和错误信息作为输出
result.LastReActInput = currentReActInput
errorMsg := fmt.Sprintf("OpenAI错误: %s", response.Error.Message)
result.Response = errorMsg
result.LastReActOutput = errorMsg
return result, fmt.Errorf("OpenAI错误: %s", response.Error.Message)
}
if len(response.Choices) == 0 {
// 没有收到响应,保存当前的ReAct输入和错误信息作为输出
result.LastReActInput = currentReActInput
errorMsg := "没有收到响应"
result.Response = errorMsg
result.LastReActOutput = errorMsg
return result, fmt.Errorf("没有收到响应")
}
choice := response.Choices[0]
// 检查是否有工具调用
if len(choice.Message.ToolCalls) > 0 {
// 思考内容:如果本轮启用了思考流式增量(thinking_stream_*),前端会去重;
// 同时也需要在该“思考阶段结束”时补一条可落库的 thinking(用于刷新后持久化展示)。
if choice.Message.Content != "" {
sendProgress("thinking", choice.Message.Content, map[string]interface{}{
"iteration": i + 1,
"streamId": thinkingStreamId,
})
}
// 添加assistant消息(包含工具调用)
messages = append(messages, ChatMessage{
Role: "assistant",
Content: choice.Message.Content,
ToolCalls: choice.Message.ToolCalls,
})
// 发送工具调用进度
sendProgress("tool_calls_detected", fmt.Sprintf("检测到 %d 个工具调用", len(choice.Message.ToolCalls)), map[string]interface{}{
"count": len(choice.Message.ToolCalls),
"iteration": i + 1,
})
// 执行所有工具调用
for idx, toolCall := range choice.Message.ToolCalls {
// 发送工具调用开始事件
toolArgsJSON, _ := json.Marshal(toolCall.Function.Arguments)
sendProgress("tool_call", fmt.Sprintf("正在调用工具: %s", toolCall.Function.Name), map[string]interface{}{
"toolName": toolCall.Function.Name,
"arguments": string(toolArgsJSON),
"argumentsObj": toolCall.Function.Arguments,
"toolCallId": toolCall.ID,
"index": idx + 1,
"total": len(choice.Message.ToolCalls),
"iteration": i + 1,
})
// 执行工具
toolCtx := context.WithValue(ctx, security.ToolOutputCallbackCtxKey, security.ToolOutputCallback(func(chunk string) {
if strings.TrimSpace(chunk) == "" {
return
}
sendProgress("tool_result_delta", chunk, map[string]interface{}{
"toolName": toolCall.Function.Name,
"toolCallId": toolCall.ID,
"index": idx + 1,
"total": len(choice.Message.ToolCalls),
"iteration": i + 1,
// success 在最终 tool_result 事件里会以 success/isError 标记为准
})
}))
execResult, err := a.executeToolViaMCP(toolCtx, toolCall.Function.Name, toolCall.Function.Arguments)
if err != nil {
// 构建详细的错误信息,帮助AI理解问题并做出决策
errorMsg := a.formatToolError(toolCall.Function.Name, toolCall.Function.Arguments, err)
messages = append(messages, ChatMessage{
Role: "tool",
ToolCallID: toolCall.ID,
Content: errorMsg,
})
// 发送工具执行失败事件
sendProgress("tool_result", fmt.Sprintf("工具 %s 执行失败", toolCall.Function.Name), map[string]interface{}{
"toolName": toolCall.Function.Name,
"success": false,
"isError": true,
"error": err.Error(),
"toolCallId": toolCall.ID,
"index": idx + 1,
"total": len(choice.Message.ToolCalls),
"iteration": i + 1,
})
a.logger.Warn("工具执行失败,已返回详细错误信息",
zap.String("tool", toolCall.Function.Name),
zap.Error(err),
)
} else {
// 即使工具返回了错误结果(IsError=true),也继续处理,让AI决定下一步
messages = append(messages, ChatMessage{
Role: "tool",
ToolCallID: toolCall.ID,
Content: execResult.Result,
})
// 收集执行ID
if execResult.ExecutionID != "" {
result.MCPExecutionIDs = append(result.MCPExecutionIDs, execResult.ExecutionID)
}
// 发送工具执行成功事件
resultPreview := execResult.Result
if len(resultPreview) > 200 {
resultPreview = resultPreview[:200] + "..."
}
sendProgress("tool_result", fmt.Sprintf("工具 %s 执行完成", toolCall.Function.Name), map[string]interface{}{
"toolName": toolCall.Function.Name,
"success": !execResult.IsError,
"isError": execResult.IsError,
"result": execResult.Result, // 完整结果
"resultPreview": resultPreview, // 预览结果
"executionId": execResult.ExecutionID,
"toolCallId": toolCall.ID,
"index": idx + 1,
"total": len(choice.Message.ToolCalls),
"iteration": i + 1,
})
// 如果工具返回了错误,记录日志但不中断流程
if execResult.IsError {
a.logger.Warn("工具返回错误结果,但继续处理",
zap.String("tool", toolCall.Function.Name),
zap.String("result", execResult.Result),
)
}
}
}
// 如果是最后一次迭代,执行完工具后要求AI进行总结
if isLastIteration {
sendProgress("progress", "最后一次迭代:正在生成总结和下一步计划...", nil)
// 添加用户消息,要求AI进行总结
messages = append(messages, ChatMessage{
Role: "user",
Content: "这是最后一次迭代。请总结到目前为止的所有测试结果、发现的问题和已完成的工作。如果需要继续测试,请提供详细的下一步执行计划。请直接回复,不要调用工具。",
})
messages = a.applyMemoryCompression(ctx, messages, 0) // 总结时不带 tools,不预留
// 流式调用OpenAI获取总结(不提供工具,强制AI直接回复)
sendProgress("response_start", "", map[string]interface{}{
"conversationId": conversationID,
"mcpExecutionIds": result.MCPExecutionIDs,
"messageGeneratedBy": "summary",
})
streamText, _ := a.callOpenAIStreamText(ctx, messages, []Tool{}, func(delta string) error {
sendProgress("response_delta", delta, map[string]interface{}{
"conversationId": conversationID,
})
return nil
})
if strings.TrimSpace(streamText) != "" {
result.Response = streamText
result.LastReActOutput = result.Response
sendProgress("progress", "总结生成完成", nil)
return result, nil
}
// 如果获取总结失败,跳出循环,让后续逻辑处理
break
}
continue
}
// 添加assistant响应
messages = append(messages, ChatMessage{
Role: "assistant",
Content: choice.Message.Content,
})
// 发送AI思考内容(如果没有工具调用)
if choice.Message.Content != "" && !thinkingStreamStarted {
sendProgress("thinking", choice.Message.Content, map[string]interface{}{
"iteration": i + 1,
})
}
// 如果是最后一次迭代,无论finish_reason是什么,都要求AI进行总结
if isLastIteration {
sendProgress("progress", "最后一次迭代:正在生成总结和下一步计划...", nil)
// 添加用户消息,要求AI进行总结
messages = append(messages, ChatMessage{
Role: "user",
Content: "这是最后一次迭代。请总结到目前为止的所有测试结果、发现的问题和已完成的工作。如果需要继续测试,请提供详细的下一步执行计划。请直接回复,不要调用工具。",
})
messages = a.applyMemoryCompression(ctx, messages, 0) // 总结时不带 tools,不预留
// 流式调用OpenAI获取总结(不提供工具,强制AI直接回复)
sendProgress("response_start", "", map[string]interface{}{
"conversationId": conversationID,
"mcpExecutionIds": result.MCPExecutionIDs,
"messageGeneratedBy": "summary",
})
streamText, _ := a.callOpenAIStreamText(ctx, messages, []Tool{}, func(delta string) error {
sendProgress("response_delta", delta, map[string]interface{}{
"conversationId": conversationID,
})
return nil
})
if strings.TrimSpace(streamText) != "" {
result.Response = streamText
result.LastReActOutput = result.Response
sendProgress("progress", "总结生成完成", nil)
return result, nil
}
// 如果获取总结失败,使用当前回复作为结果
if choice.Message.Content != "" {
result.Response = choice.Message.Content
result.LastReActOutput = result.Response
return result, nil
}
// 如果都没有内容,跳出循环,让后续逻辑处理
break
}
// 如果完成,返回结果
if choice.FinishReason == "stop" {
sendProgress("progress", "正在生成最终回复...", nil)
result.Response = choice.Message.Content
result.LastReActOutput = result.Response
return result, nil
}
}
// 如果循环结束仍未返回,说明达到了最大迭代次数
// 尝试最后一次调用AI获取总结
sendProgress("progress", "达到最大迭代次数,正在生成总结...", nil)
finalSummaryPrompt := ChatMessage{
Role: "user",
Content: fmt.Sprintf("已达到最大迭代次数(%d轮)。请总结到目前为止的所有测试结果、发现的问题和已完成的工作。如果需要继续测试,请提供详细的下一步执行计划。请直接回复,不要调用工具。", a.maxIterations),
}
messages = append(messages, finalSummaryPrompt)
messages = a.applyMemoryCompression(ctx, messages, 0) // 总结时不带 tools,不预留
// 流式调用OpenAI获取总结(不提供工具,强制AI直接回复)
sendProgress("response_start", "", map[string]interface{}{
"conversationId": conversationID,
"mcpExecutionIds": result.MCPExecutionIDs,
"messageGeneratedBy": "max_iter_summary",
})
streamText, _ := a.callOpenAIStreamText(ctx, messages, []Tool{}, func(delta string) error {
sendProgress("response_delta", delta, map[string]interface{}{
"conversationId": conversationID,
})
return nil
})
if strings.TrimSpace(streamText) != "" {
result.Response = streamText
result.LastReActOutput = result.Response
sendProgress("progress", "总结生成完成", nil)
return result, nil
}
// 如果无法生成总结,返回友好的提示
result.Response = fmt.Sprintf("已达到最大迭代次数(%d轮)。系统已执行了多轮测试,但由于达到迭代上限,无法继续自动执行。建议您查看已执行的工具结果,或提出新的测试请求以继续测试。", a.maxIterations)
result.LastReActOutput = result.Response
return result, nil
}
// getAvailableTools 获取可用工具
// 从MCP服务器动态获取工具列表,使用简短描述以减少token消耗
// roleTools: 角色配置的工具列表(toolKey格式),如果为空或nil,则使用所有工具(默认角色)
func (a *Agent) getAvailableTools(roleTools []string) []Tool {
// 构建角色工具集合(用于快速查找)
roleToolSet := make(map[string]bool)
if len(roleTools) > 0 {
for _, toolKey := range roleTools {
roleToolSet[toolKey] = true
}
}
// 从MCP服务器获取所有已注册的内部工具
mcpTools := a.mcpServer.GetAllTools()
// 转换为OpenAI格式的工具定义
tools := make([]Tool, 0, len(mcpTools))
for _, mcpTool := range mcpTools {
// 如果指定了角色工具列表,只添加在列表中的工具
if len(roleToolSet) > 0 {
toolKey := mcpTool.Name // 内置工具使用工具名称作为key
if !roleToolSet[toolKey] {
continue // 不在角色工具列表中,跳过
}
}
// 使用简短描述(如果存在),否则使用详细描述
description := mcpTool.ShortDescription
if description == "" {
description = mcpTool.Description
}
// 转换schema中的类型为OpenAI标准类型
convertedSchema := a.convertSchemaTypes(mcpTool.InputSchema)
tools = append(tools, Tool{
Type: "function",
Function: FunctionDefinition{
Name: mcpTool.Name,
Description: description, // 使用简短描述减少token消耗
Parameters: convertedSchema,
},
})
}
// 获取外部MCP工具
if a.externalMCPMgr != nil {
// 增加超时时间到30秒,因为通过代理连接远程服务器可能需要更长时间
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
externalTools, err := a.externalMCPMgr.GetAllTools(ctx)
if err != nil {
a.logger.Warn("获取外部MCP工具失败", zap.Error(err))
} else {
// 获取外部MCP配置,用于检查工具启用状态
externalMCPConfigs := a.externalMCPMgr.GetConfigs()
// 清空并重建工具名称映射
a.mu.Lock()
a.toolNameMapping = make(map[string]string)
a.mu.Unlock()
// 将外部MCP工具添加到工具列表(只添加启用的工具)
for _, externalTool := range externalTools {
// 外部工具使用 "mcpName::toolName" 作为toolKey
externalToolKey := externalTool.Name
// 如果指定了角色工具列表,只添加在列表中的工具
if len(roleToolSet) > 0 {
if !roleToolSet[externalToolKey] {
continue // 不在角色工具列表中,跳过
}
}
// 解析工具名称:mcpName::toolName
var mcpName, actualToolName string
if idx := strings.Index(externalTool.Name, "::"); idx > 0 {
mcpName = externalTool.Name[:idx]
actualToolName = externalTool.Name[idx+2:]
} else {
continue // 跳过格式不正确的工具
}
// 检查工具是否启用
enabled := false
if cfg, exists := externalMCPConfigs[mcpName]; exists {
// 首先检查外部MCP是否启用
if !cfg.ExternalMCPEnable && !(cfg.Enabled && !cfg.Disabled) {
enabled = false // MCP未启用,所有工具都禁用
} else {
// MCP已启用,检查单个工具的启用状态
// 如果ToolEnabled为空或未设置该工具,默认为启用(向后兼容)
if cfg.ToolEnabled == nil {
enabled = true // 未设置工具状态,默认为启用
} else if toolEnabled, exists := cfg.ToolEnabled[actualToolName]; exists {
enabled = toolEnabled // 使用配置的工具状态
} else {
enabled = true // 工具未在配置中,默认为启用
}
}
}
// 只添加启用的工具
if !enabled {
continue
}
// 使用简短描述(如果存在),否则使用详细描述
description := externalTool.ShortDescription
if description == "" {
description = externalTool.Description
}
// 转换schema中的类型为OpenAI标准类型
convertedSchema := a.convertSchemaTypes(externalTool.InputSchema)
// 将工具名称中的 "::" 替换为 "__" 以符合OpenAI命名规范
// OpenAI要求工具名称只能包含 [a-zA-Z0-9_-]
openAIName := strings.ReplaceAll(externalTool.Name, "::", "__")
// 保存名称映射关系(OpenAI格式 -> 原始格式)
a.mu.Lock()
a.toolNameMapping[openAIName] = externalTool.Name
a.mu.Unlock()
tools = append(tools, Tool{
Type: "function",
Function: FunctionDefinition{
Name: openAIName, // 使用符合OpenAI规范的名称
Description: description,
Parameters: convertedSchema,
},
})
}
}
}
a.logger.Debug("获取可用工具列表",
zap.Int("internalTools", len(mcpTools)),
zap.Int("totalTools", len(tools)),
)
return tools
}
// convertSchemaTypes 递归转换schema中的类型为OpenAI标准类型
func (a *Agent) convertSchemaTypes(schema map[string]interface{}) map[string]interface{} {
if schema == nil {
return schema
}
// 创建新的schema副本
converted := make(map[string]interface{})
for k, v := range schema {
converted[k] = v
}
// 转换properties中的类型
if properties, ok := converted["properties"].(map[string]interface{}); ok {
convertedProperties := make(map[string]interface{})
for propName, propValue := range properties {
if prop, ok := propValue.(map[string]interface{}); ok {
convertedProp := make(map[string]interface{})
for pk, pv := range prop {
if pk == "type" {
// 转换类型
if typeStr, ok := pv.(string); ok {
convertedProp[pk] = a.convertToOpenAIType(typeStr)
} else {
convertedProp[pk] = pv
}
} else {
convertedProp[pk] = pv
}
}
convertedProperties[propName] = convertedProp
} else {
convertedProperties[propName] = propValue
}
}
converted["properties"] = convertedProperties
}
return converted
}
// convertToOpenAIType 将配置中的类型转换为OpenAI/JSON Schema标准类型
func (a *Agent) convertToOpenAIType(configType string) string {
switch configType {
case "bool":
return "boolean"
case "int", "integer":
return "number"
case "float", "double":
return "number"
case "string", "array", "object":
return configType
default:
// 默认返回原类型
return configType
}
}
// isRetryableError 判断错误是否可重试
func (a *Agent) isRetryableError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
// 网络相关错误,可以重试
retryableErrors := []string{
"connection reset",
"connection reset by peer",
"connection refused",
"timeout",
"i/o timeout",
"context deadline exceeded",
"no such host",
"network is unreachable",
"broken pipe",
"EOF",
"read tcp",
"write tcp",
"dial tcp",
}
for _, retryable := range retryableErrors {
if strings.Contains(strings.ToLower(errStr), retryable) {
return true
}
}
return false
}
// callOpenAI 调用OpenAI API(带重试机制)
func (a *Agent) callOpenAI(ctx context.Context, messages []ChatMessage, tools []Tool) (*OpenAIResponse, error) {
maxRetries := 3
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
response, err := a.callOpenAISingle(ctx, messages, tools)
if err == nil {
if attempt > 0 {
a.logger.Info("OpenAI API调用重试成功",
zap.Int("attempt", attempt+1),
zap.Int("maxRetries", maxRetries),
)
}
return response, nil
}
lastErr = err
// 如果不是可重试的错误,直接返回
if !a.isRetryableError(err) {
return nil, err
}
// 如果不是最后一次重试,等待后重试
if attempt < maxRetries-1 {
// 指数退避:2s, 4s, 8s...
backoff := time.Duration(1<<uint(attempt+1)) * time.Second
if backoff > 30*time.Second {
backoff = 30 * time.Second // 最大30秒
}
a.logger.Warn("OpenAI API调用失败,准备重试",
zap.Error(err),
zap.Int("attempt", attempt+1),
zap.Int("maxRetries", maxRetries),
zap.Duration("backoff", backoff),
)
// 检查上下文是否已取消
select {
case <-ctx.Done():
return nil, fmt.Errorf("上下文已取消: %w", ctx.Err())
case <-time.After(backoff):
// 继续重试
}
}
}
return nil, fmt.Errorf("重试%d次后仍然失败: %w", maxRetries, lastErr)
}
// callOpenAISingle 单次调用OpenAI API(不包含重试逻辑)
func (a *Agent) callOpenAISingle(ctx context.Context, messages []ChatMessage, tools []Tool) (*OpenAIResponse, error) {
reqBody := OpenAIRequest{
Model: a.config.Model,
Messages: messages,
}
if len(tools) > 0 {
reqBody.Tools = tools
}
a.logger.Debug("准备发送OpenAI请求",
zap.Int("messagesCount", len(messages)),
zap.Int("toolsCount", len(tools)),
)
var response OpenAIResponse
if a.openAIClient == nil {
return nil, fmt.Errorf("OpenAI客户端未初始化")
}
if err := a.openAIClient.ChatCompletion(ctx, reqBody, &response); err != nil {
return nil, err
}
return &response, nil
}
// callOpenAISingleStreamText 单次调用OpenAI的流式模式,只用于“不会调用工具”的纯文本输出(tools 为空时最佳)。
// onDelta 每收到一段 content delta,就回调一次;如果 callback 返回错误,会终止读取并返回错误。
func (a *Agent) callOpenAISingleStreamText(ctx context.Context, messages []ChatMessage, tools []Tool, onDelta func(delta string) error) (string, error) {
reqBody := OpenAIRequest{
Model: a.config.Model,
Messages: messages,
Stream: true,
}
if len(tools) > 0 {
reqBody.Tools = tools
}
if a.openAIClient == nil {
return "", fmt.Errorf("OpenAI客户端未初始化")
}
return a.openAIClient.ChatCompletionStream(ctx, reqBody, onDelta)
}
// callOpenAIStreamText 调用OpenAI流式模式(带重试),仅在“未输出任何 delta”时才允许重试,避免重复发送已下发的内容。
func (a *Agent) callOpenAIStreamText(ctx context.Context, messages []ChatMessage, tools []Tool, onDelta func(delta string) error) (string, error) {
maxRetries := 3
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
var deltasSent bool
full, err := a.callOpenAISingleStreamText(ctx, messages, tools, func(delta string) error {
deltasSent = true
return onDelta(delta)
})
if err == nil {
if attempt > 0 {
a.logger.Info("OpenAI stream 调用重试成功",
zap.Int("attempt", attempt+1),
zap.Int("maxRetries", maxRetries),
)
}
return full, nil
}
lastErr = err
// 已经开始输出了 delta,避免重复内容:直接失败让上层处理。
if deltasSent {
return "", err
}
if !a.isRetryableError(err) {
return "", err
}
if attempt < maxRetries-1 {
backoff := time.Duration(1<<uint(attempt+1)) * time.Second
if backoff > 30*time.Second {
backoff = 30 * time.Second
}
a.logger.Warn("OpenAI stream 调用失败,准备重试",
zap.Error(err),
zap.Int("attempt", attempt+1),
zap.Int("maxRetries", maxRetries),
zap.Duration("backoff", backoff),
)
select {
case <-ctx.Done():
return "", fmt.Errorf("上下文已取消: %w", ctx.Err())
case <-time.After(backoff):
}
}
}
return "", fmt.Errorf("重试%d次后仍然失败: %w", maxRetries, lastErr)
}
// callOpenAISingleStreamWithToolCalls 单次调用OpenAI流式模式(带工具调用解析),不包含重试逻辑。
func (a *Agent) callOpenAISingleStreamWithToolCalls(
ctx context.Context,
messages []ChatMessage,
tools []Tool,
onContentDelta func(delta string) error,
) (*OpenAIResponse, error) {
reqBody := OpenAIRequest{
Model: a.config.Model,
Messages: messages,
Stream: true,
}
if len(tools) > 0 {
reqBody.Tools = tools
}
if a.openAIClient == nil {
return nil, fmt.Errorf("OpenAI客户端未初始化")
}
content, streamToolCalls, finishReason, err := a.openAIClient.ChatCompletionStreamWithToolCalls(ctx, reqBody, onContentDelta)
if err != nil {
return nil, err
}
toolCalls := make([]ToolCall, 0, len(streamToolCalls))
for _, stc := range streamToolCalls {
fnArgsStr := stc.FunctionArgsStr
args := make(map[string]interface{})
if strings.TrimSpace(fnArgsStr) != "" {
if err := json.Unmarshal([]byte(fnArgsStr), &args); err != nil {
// 兼容:arguments 不一定是严格 JSON
args = map[string]interface{}{"raw": fnArgsStr}
}
}
typ := stc.Type
if strings.TrimSpace(typ) == "" {
typ = "function"
}
toolCalls = append(toolCalls, ToolCall{
ID: stc.ID,
Type: typ,
Function: FunctionCall{
Name: stc.FunctionName,
Arguments: args,
},
})
}
response := &OpenAIResponse{
ID: "",
Choices: []Choice{
{
Message: MessageWithTools{
Role: "assistant",
Content: content,
ToolCalls: toolCalls,
},
FinishReason: finishReason,
},
},
}
return response, nil
}
// callOpenAIStreamWithToolCalls 调用OpenAI流式模式(带重试),仅当还没有输出任何 content delta 时才允许重试。
func (a *Agent) callOpenAIStreamWithToolCalls(
ctx context.Context,
messages []ChatMessage,
tools []Tool,
onContentDelta func(delta string) error,
) (*OpenAIResponse, error) {
maxRetries := 3
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
deltasSent := false
resp, err := a.callOpenAISingleStreamWithToolCalls(ctx, messages, tools, func(delta string) error {
deltasSent = true
if onContentDelta != nil {
return onContentDelta(delta)
}
return nil
})
if err == nil {
if attempt > 0 {
a.logger.Info("OpenAI stream 调用重试成功",
zap.Int("attempt", attempt+1),
zap.Int("maxRetries", maxRetries),
)
}
return resp, nil
}
lastErr = err
if deltasSent {
// 已经开始输出了 delta:避免重复发送
return nil, err
}
if !a.isRetryableError(err) {
return nil, err
}
if attempt < maxRetries-1 {
backoff := time.Duration(1<<uint(attempt+1)) * time.Second
if backoff > 30*time.Second {
backoff = 30 * time.Second
}
a.logger.Warn("OpenAI stream 调用失败,准备重试",
zap.Error(err),
zap.Int("attempt", attempt+1),
zap.Int("maxRetries", maxRetries),
zap.Duration("backoff", backoff),
)
select {
case <-ctx.Done():
return nil, fmt.Errorf("上下文已取消: %w", ctx.Err())
case <-time.After(backoff):
}
}
}
return nil, fmt.Errorf("重试%d次后仍然失败: %w", maxRetries, lastErr)
}
// ToolExecutionResult 工具执行结果
type ToolExecutionResult struct {
Result string
ExecutionID string
IsError bool // 标记是否为错误结果
}
// executeToolViaMCP 通过MCP执行工具
// 即使工具执行失败,也返回结果而不是错误,让AI能够处理错误情况
func (a *Agent) executeToolViaMCP(ctx context.Context, toolName string, args map[string]interface{}) (*ToolExecutionResult, error) {
a.logger.Info("通过MCP执行工具",
zap.String("tool", toolName),
zap.Any("args", args),
)
// 如果是record_vulnerability工具,自动添加conversation_id
if toolName == builtin.ToolRecordVulnerability {
a.mu.RLock()
conversationID := a.currentConversationID
a.mu.RUnlock()
if conversationID != "" {
args["conversation_id"] = conversationID
a.logger.Debug("自动添加conversation_id到record_vulnerability工具",
zap.String("conversation_id", conversationID),
)
} else {
a.logger.Warn("record_vulnerability工具调用时conversation_id为空")
}
}
var result *mcp.ToolResult
var executionID string
var err error
// 单次工具执行超时:防止单个工具长时间挂起(如 30 分钟仍显示执行中)
toolCtx := ctx
var toolCancel context.CancelFunc
if a.agentConfig != nil && a.agentConfig.ToolTimeoutMinutes > 0 {
toolCtx, toolCancel = context.WithTimeout(ctx, time.Duration(a.agentConfig.ToolTimeoutMinutes)*time.Minute)
defer func() {
if toolCancel != nil {
toolCancel()
}
}()
}
// 检查是否是外部MCP工具(通过工具名称映射)
a.mu.RLock()
originalToolName, isExternalTool := a.toolNameMapping[toolName]
a.mu.RUnlock()
if isExternalTool && a.externalMCPMgr != nil {
// 使用原始工具名称调用外部MCP工具
a.logger.Debug("调用外部MCP工具",
zap.String("openAIName", toolName),
zap.String("originalName", originalToolName),
)
result, executionID, err = a.externalMCPMgr.CallTool(toolCtx, originalToolName, args)
} else {
// 调用内部MCP工具
result, executionID, err = a.mcpServer.CallTool(toolCtx, toolName, args)
}
// 如果调用失败(如工具不存在、超时),返回友好的错误信息而不是抛出异常
if err != nil {
detail := err.Error()
if errors.Is(err, context.DeadlineExceeded) {
min := 10
if a.agentConfig != nil && a.agentConfig.ToolTimeoutMinutes > 0 {
min = a.agentConfig.ToolTimeoutMinutes
}
detail = fmt.Sprintf("工具执行超过 %d 分钟被自动终止(可在 config.yaml 的 agent.tool_timeout_minutes 中调整)", min)
}
errorMsg := fmt.Sprintf(`工具调用失败
工具名称: %s
错误类型: 系统错误
错误详情: %s
可能的原因:
- 工具 "%s" 不存在或未启用
- 单次执行超时(agent.tool_timeout_minutes
- 系统配置问题
- 网络或权限问题
建议:
- 检查工具名称是否正确
- 若需更长执行时间,可适当增大 agent.tool_timeout_minutes
- 尝试使用其他替代工具
- 如果这是必需的工具,请向用户说明情况`, toolName, detail, toolName)
return &ToolExecutionResult{
Result: errorMsg,
ExecutionID: executionID,
IsError: true,
}, nil // 返回 nil 错误,让调用者处理结果
}
// 格式化结果
var resultText strings.Builder
for _, content := range result.Content {
resultText.WriteString(content.Text)
resultText.WriteString("\n")
}
resultStr := resultText.String()
resultSize := len(resultStr)
// 检测大结果并保存
a.mu.RLock()
threshold := a.largeResultThreshold
storage := a.resultStorage
a.mu.RUnlock()
if resultSize > threshold && storage != nil {
// 异步保存大结果
go func() {
if err := storage.SaveResult(executionID, toolName, resultStr); err != nil {
a.logger.Warn("保存大结果失败",
zap.String("executionID", executionID),
zap.String("toolName", toolName),
zap.Error(err),
)
} else {
a.logger.Info("大结果已保存",
zap.String("executionID", executionID),
zap.String("toolName", toolName),
zap.Int("size", resultSize),
)
}
}()
// 返回最小化通知
lines := strings.Split(resultStr, "\n")
filePath := ""
if storage != nil {
filePath = storage.GetResultPath(executionID)
}
notification := a.formatMinimalNotification(executionID, toolName, resultSize, len(lines), filePath)
return &ToolExecutionResult{
Result: notification,
ExecutionID: executionID,
IsError: result != nil && result.IsError,
}, nil
}
return &ToolExecutionResult{
Result: resultStr,
ExecutionID: executionID,
IsError: result != nil && result.IsError,
}, nil
}
// formatMinimalNotification 格式化最小化通知
func (a *Agent) formatMinimalNotification(executionID string, toolName string, size int, lineCount int, filePath string) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("工具执行完成。结果已保存(ID: %s)。\n\n", executionID))
sb.WriteString("结果信息:\n")
sb.WriteString(fmt.Sprintf(" - 工具: %s\n", toolName))
sb.WriteString(fmt.Sprintf(" - 大小: %d 字节 (%.2f KB)\n", size, float64(size)/1024))
sb.WriteString(fmt.Sprintf(" - 行数: %d 行\n", lineCount))
if filePath != "" {
sb.WriteString(fmt.Sprintf(" - 文件路径: %s\n", filePath))
}
sb.WriteString("\n")
sb.WriteString("推荐使用 query_execution_result 工具查询完整结果:\n")
sb.WriteString(fmt.Sprintf(" - 查询第一页: query_execution_result(execution_id=\"%s\", page=1, limit=100)\n", executionID))
sb.WriteString(fmt.Sprintf(" - 搜索关键词: query_execution_result(execution_id=\"%s\", search=\"关键词\")\n", executionID))
sb.WriteString(fmt.Sprintf(" - 过滤条件: query_execution_result(execution_id=\"%s\", filter=\"error\")\n", executionID))
sb.WriteString(fmt.Sprintf(" - 正则匹配: query_execution_result(execution_id=\"%s\", search=\"\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+\", use_regex=true)\n", executionID))
sb.WriteString("\n")
if filePath != "" {
sb.WriteString("如果 query_execution_result 工具不满足需求,也可以使用其他工具处理文件:\n")
sb.WriteString("\n")
sb.WriteString("**分段读取示例:**\n")
sb.WriteString(fmt.Sprintf(" - 查看前100行: exec(command=\"head\", args=[\"-n\", \"100\", \"%s\"])\n", filePath))
sb.WriteString(fmt.Sprintf(" - 查看后100行: exec(command=\"tail\", args=[\"-n\", \"100\", \"%s\"])\n", filePath))
sb.WriteString(fmt.Sprintf(" - 查看第50-150行: exec(command=\"sed\", args=[\"-n\", \"50,150p\", \"%s\"])\n", filePath))
sb.WriteString("\n")
sb.WriteString("**搜索和正则匹配示例:**\n")
sb.WriteString(fmt.Sprintf(" - 搜索关键词: exec(command=\"grep\", args=[\"关键词\", \"%s\"])\n", filePath))
sb.WriteString(fmt.Sprintf(" - 正则匹配IP地址: exec(command=\"grep\", args=[\"-E\", \"\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+\", \"%s\"])\n", filePath))
sb.WriteString(fmt.Sprintf(" - 不区分大小写搜索: exec(command=\"grep\", args=[\"-i\", \"关键词\", \"%s\"])\n", filePath))
sb.WriteString(fmt.Sprintf(" - 显示匹配行号: exec(command=\"grep\", args=[\"-n\", \"关键词\", \"%s\"])\n", filePath))
sb.WriteString("\n")
sb.WriteString("**过滤和统计示例:**\n")
sb.WriteString(fmt.Sprintf(" - 统计总行数: exec(command=\"wc\", args=[\"-l\", \"%s\"])\n", filePath))
sb.WriteString(fmt.Sprintf(" - 过滤包含error的行: exec(command=\"grep\", args=[\"error\", \"%s\"])\n", filePath))
sb.WriteString(fmt.Sprintf(" - 排除空行: exec(command=\"grep\", args=[\"-v\", \"^$\", \"%s\"])\n", filePath))
sb.WriteString("\n")
sb.WriteString("**完整读取(不推荐大文件):**\n")
sb.WriteString(fmt.Sprintf(" - 使用 cat 工具: cat(file=\"%s\")\n", filePath))
sb.WriteString(fmt.Sprintf(" - 使用 exec 工具: exec(command=\"cat\", args=[\"%s\"])\n", filePath))
sb.WriteString("\n")
sb.WriteString("**注意:**\n")
sb.WriteString(" - 直接读取大文件可能会再次触发大结果保存机制\n")
sb.WriteString(" - 建议优先使用分段读取和搜索功能,避免一次性加载整个文件\n")
sb.WriteString(" - 正则表达式语法遵循标准 POSIX 正则表达式规范\n")
}
return sb.String()
}
// UpdateConfig 更新OpenAI配置
func (a *Agent) UpdateConfig(cfg *config.OpenAIConfig) {
a.mu.Lock()
defer a.mu.Unlock()
a.config = cfg
// 同时更新MemoryCompressor的配置(如果存在)
if a.memoryCompressor != nil {
a.memoryCompressor.UpdateConfig(cfg)
}
a.logger.Info("Agent配置已更新",
zap.String("base_url", cfg.BaseURL),
zap.String("model", cfg.Model),
)
}
// UpdateMaxIterations 更新最大迭代次数
func (a *Agent) UpdateMaxIterations(maxIterations int) {
a.mu.Lock()
defer a.mu.Unlock()
if maxIterations > 0 {
a.maxIterations = maxIterations
a.logger.Info("Agent最大迭代次数已更新", zap.Int("max_iterations", maxIterations))
}
}
// formatToolError 格式化工具错误信息,提供更友好的错误描述
func (a *Agent) formatToolError(toolName string, args map[string]interface{}, err error) string {
errorMsg := fmt.Sprintf(`工具执行失败
工具名称: %s
调用参数: %v
错误信息: %v
请分析错误原因并采取以下行动之一:
1. 如果参数错误,请修正参数后重试
2. 如果工具不可用,请尝试使用替代工具
3. 如果这是系统问题,请向用户说明情况并提供建议
4. 如果错误信息中包含有用信息,可以基于这些信息继续分析`, toolName, args, err)
return errorMsg
}
// applyMemoryCompression 在调用LLM前对消息进行压缩,避免超过 token 限制。reservedTokens 为预留给 tools 的 token 数,传 0 表示不预留。
func (a *Agent) applyMemoryCompression(ctx context.Context, messages []ChatMessage, reservedTokens int) []ChatMessage {
if a.memoryCompressor == nil {
return messages
}
compressed, changed, err := a.memoryCompressor.CompressHistory(ctx, messages, reservedTokens)
if err != nil {
a.logger.Warn("上下文压缩失败,将使用原始消息继续", zap.Error(err))
return messages
}
if changed {
a.logger.Info("历史上下文已压缩",
zap.Int("originalMessages", len(messages)),
zap.Int("compressedMessages", len(compressed)),
)
return compressed
}
return messages
}
// countToolsTokens 统计 tools 序列化后的 token 数,用于日志与压缩时预留空间。mc 为 nil 时返回 0。
func (a *Agent) countToolsTokens(tools []Tool) int {
if len(tools) == 0 || a.memoryCompressor == nil {
return 0
}
data, err := json.Marshal(tools)
if err != nil {
return 0
}
return a.memoryCompressor.CountTextTokens(string(data))
}
// handleMissingToolError 当LLM调用不存在的工具时,向其追加提示消息并允许继续迭代
func (a *Agent) handleMissingToolError(errMsg string, messages *[]ChatMessage) (bool, string) {
lowerMsg := strings.ToLower(errMsg)
if !(strings.Contains(lowerMsg, "non-exist tool") || strings.Contains(lowerMsg, "non exist tool")) {
return false, ""
}
toolName := extractQuotedToolName(errMsg)
if toolName == "" {
toolName = "unknown_tool"
}
notice := fmt.Sprintf("System notice: the previous call failed with error: %s. Please verify tool availability and proceed using existing tools or pure reasoning.", errMsg)
*messages = append(*messages, ChatMessage{
Role: "user",
Content: notice,
})
return true, toolName
}
// handleToolRoleError 自动修复因缺失tool_calls导致的OpenAI错误
func (a *Agent) handleToolRoleError(errMsg string, messages *[]ChatMessage) bool {
if messages == nil {
return false
}
lowerMsg := strings.ToLower(errMsg)
if !(strings.Contains(lowerMsg, "role 'tool'") && strings.Contains(lowerMsg, "tool_calls")) {
return false
}
fixed := a.repairOrphanToolMessages(messages)
if !fixed {
return false
}
notice := "System notice: the previous call failed because some tool outputs lost their corresponding assistant tool_calls context. The history has been repaired. Please continue."
*messages = append(*messages, ChatMessage{
Role: "user",
Content: notice,
})
return true
}
// RepairOrphanToolMessages 清理失去配对的tool消息和未完成的tool_calls,避免OpenAI报错
// 同时确保历史消息中的tool_calls只作为上下文记忆,不会触发重新执行
// 这是一个公开方法,可以在恢复历史消息时调用
func (a *Agent) RepairOrphanToolMessages(messages *[]ChatMessage) bool {
return a.repairOrphanToolMessages(messages)
}
// repairOrphanToolMessages 清理失去配对的tool消息和未完成的tool_calls,避免OpenAI报错
// 同时确保历史消息中的tool_calls只作为上下文记忆,不会触发重新执行
func (a *Agent) repairOrphanToolMessages(messages *[]ChatMessage) bool {
if messages == nil {
return false
}
msgs := *messages
if len(msgs) == 0 {
return false
}
pending := make(map[string]int)
cleaned := make([]ChatMessage, 0, len(msgs))
removed := false
for _, msg := range msgs {
switch strings.ToLower(msg.Role) {
case "assistant":
if len(msg.ToolCalls) > 0 {
// 记录所有tool_call IDs
for _, tc := range msg.ToolCalls {
if tc.ID != "" {
pending[tc.ID]++
}
}
}
cleaned = append(cleaned, msg)
case "tool":
callID := msg.ToolCallID
if callID == "" {
removed = true
continue
}
if count, exists := pending[callID]; exists && count > 0 {
if count == 1 {
delete(pending, callID)
} else {
pending[callID] = count - 1
}
cleaned = append(cleaned, msg)
} else {
removed = true
continue
}
default:
cleaned = append(cleaned, msg)
}
}
// 如果还有未匹配的tool_calls(即assistant消息有tool_calls但没有对应的tool响应)
// 需要从最后的assistant消息中移除这些tool_calls,避免AI重新执行它们
if len(pending) > 0 {
// 从后往前查找最后一个assistant消息
for i := len(cleaned) - 1; i >= 0; i-- {
if strings.ToLower(cleaned[i].Role) == "assistant" && len(cleaned[i].ToolCalls) > 0 {
// 移除未匹配的tool_calls
originalCount := len(cleaned[i].ToolCalls)
validToolCalls := make([]ToolCall, 0)
for _, tc := range cleaned[i].ToolCalls {
if tc.ID != "" && pending[tc.ID] > 0 {
// 这个tool_call没有对应的tool响应,移除它
removed = true
delete(pending, tc.ID)
} else {
validToolCalls = append(validToolCalls, tc)
}
}
// 更新消息的ToolCalls
if len(validToolCalls) != originalCount {
cleaned[i].ToolCalls = validToolCalls
a.logger.Info("移除了未完成的tool_calls,避免重新执行",
zap.Int("removed_count", originalCount-len(validToolCalls)),
)
}
break
}
}
}
if removed {
a.logger.Warn("修复了对话历史中的tool消息和tool_calls",
zap.Int("original_messages", len(msgs)),
zap.Int("cleaned_messages", len(cleaned)),
)
*messages = cleaned
}
return removed
}
// ToolsForRole 返回与单 Agent 循环一致的工具定义(OpenAI function 格式),供 Eino DeepAgent 等编排层绑定 MCP 工具。
func (a *Agent) ToolsForRole(roleTools []string) []Tool {
return a.getAvailableTools(roleTools)
}
// ExecuteMCPToolForConversation 在指定会话上下文中执行 MCP 工具(行为与主 Agent 循环中的工具调用一致,如自动注入 conversation_id)。
func (a *Agent) ExecuteMCPToolForConversation(ctx context.Context, conversationID, toolName string, args map[string]interface{}) (*ToolExecutionResult, error) {
a.mu.Lock()
prev := a.currentConversationID
a.currentConversationID = conversationID
a.mu.Unlock()
defer func() {
a.mu.Lock()
a.currentConversationID = prev
a.mu.Unlock()
}()
return a.executeToolViaMCP(ctx, toolName, args)
}
// extractQuotedToolName 尝试从错误信息中提取被引用的工具名称
func extractQuotedToolName(errMsg string) string {
start := strings.Index(errMsg, "\"")
if start == -1 {
return ""
}
rest := errMsg[start+1:]
end := strings.Index(rest, "\"")
if end == -1 {
return ""
}
return rest[:end]
}