# 06 - Agent 核心引擎设计 > 本文由 opsxcli 同名设计文档适配改写,结合 aiops Agent CLI 的 Web 实现(`backend/pkg/agentcli/core/agent.go`)。 > 核心算法(16 轮 ReAct 主循环、自适应检查点、循环检测、连续失败追踪、Evolver 集成)全部保留;TUI 回调(Bubble Tea)剥离,改为 `EventCallback` 推 WebSocket 事件流。 ## 设计理念 Agent 核心引擎遵循 **ReAct (Reasoning + Acting)** 范式: - **Reasoning**:LLM 推理决定下一步行动(产出 `thought` 文本 + 工具调用计划) - **Acting**:调用工具执行具体操作(`ssh_execute` / `analyze_output`) - **Observing**:观察工具结果(`exit_code`、输出、耗时),反馈到下一轮推理 通过 16 轮迭代循环,让 Agent 从「盲目执行命令」进化为「有目的地探索和解决问题」。 ## 核心结构 ```go // backend/pkg/agentcli/core/agent.go type Agent struct { llm llm.Client // LLM 客户端(OpenAI 兼容) registry *tools.Registry // 工具注册中心 config *Config // 运行配置 safetyCtl *safety.Controller // 安全控制器 evolver *evolver.EvolverEngine // 自进化引擎(可为 nil,降级运行) callback EventCallback // 事件回调(controller 注入,推 WS + 落库) totalTokens int // token 消耗统计 mu sync.Mutex } ``` > 与 opsxcli 的差异: > - 剥离 `tokenizer *TokenEstimator` 独立结构,改为内联的 `estimateTokens` 函数 > - 剥离 `lastEvolveResult` + `evolveResultMu`,改为 Evolver 内部维护 > - 剥离 `evolveWg sync.WaitGroup`,进化 goroutine 自带 `recover()`,不阻塞主循环 ### 依赖关系 ``` ┌─────────┐ │ Agent │ └────┬────┘ ┌───────┬───────┼───────┬──────────┐ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ┌────────┐┌──────┐┌──────┐┌──────┐┌────────┐ │Config ││Safety││Tools ││Evolver││Prompt │ │配置 ││安全 ││工具 ││进化 ││构建器 │ └────────┘└──────┘└──────┘└──────┘└────────┘ ``` ## EventCallback —— 替代 TUI 的事件流 opsxcli 通过 `ToolCallback` 驱动 Bubble Tea TUI 实时渲染;aiops 改为 `EventCallback`,由 controller 序列化后推 WebSocket + 落库: ```go type EventType string const ( EventThought EventType = "thought" // AI 思考文本 EventToolCall EventType = "tool_call" // 即将执行的工具(名/参数/风险) EventToolResult EventType = "tool_result" // 工具执行结果(输出/成败/耗时) EventProgress EventType = "progress" // 轮次 + token EventDone EventType = "done" // 任务完成(最终答案) EventError EventType = "error" // 错误(含达到最大轮次) ) type EventCallback func(Event) ``` controller 的 `onEvent` 实现同时做两件事: ```go func (ac *agentConn) onEvent(e core.Event) { ac.write(wsEvent{...}) // 1. 推 WebSocket ac.persistMsg(...) // 2. 落库 agent_cli_messages(progress 除外) } ``` ## Config 配置 ```go // backend/pkg/agentcli/core/agent.go type Config struct { MaxIterations int // 16 - 最大迭代次数 Temperature float64 // 0.3 - LLM 温度(偏低 = 更确定) ToolTimeout time.Duration // 120s - 工具执行超时 MaxTokens int // 4096 - 单次 LLM 最大输出 token SafetyMode safety.Mode // balanced OutputMaxLength int // 10000 - 输出截断长度 MaxContextTokens int // 8000 - 上下文窗口(裁剪阈值 90%) MemoryDir string // data/agentcli - Evolver 记忆目录 } ``` 默认值通过 `DefaultConfig()` 提供。controller 可通过 `EngineConfig.Core` 覆盖。 > 与 opsxcli 的差异:opsxcli 用 `LoadConfig()` 从环境变量加载(`OPSXCLI_AGENT_*`),aiops 改为代码默认值 + `EngineConfig` 显式覆盖(Web 场景下环境变量管理不直观)。 ## 16 轮 ReAct 主循环 ### 流程图 ``` 用户输入 │ ▼ ┌─────────────────────────────────────────────┐ │ ReAct Main Loop │ │ │ │ Round 1 ──→ LLM 推理 → 工具调用 → 观察 │ │ Round 2 ──→ LLM 推理 → 工具调用 → 观察 │ │ Round 3 ──→ LLM 推理 → 工具调用 → 观察 │ │ ═══════ CHECKPOINT 1 (Round 4) ═══════ │ │ Round 4 ──→ 启发式评估 + 纠偏提示 │ │ ... │ │ Round 7 ──→ LLM 推理 → 工具调用 → 观察 │ │ ═══════ CHECKPOINT 2 (Round 8) ═══════ │ │ Round 8 ──→ 强制中间总结 │ │ ... │ │ Round 11 ──→ LLM 推理 → 工具调用 → 观察 │ │ ═══════ CHECKPOINT 3 (Round 12) ═════ │ │ Round 12 ──→ 最终提醒,敦促收尾 │ │ ... │ │ Round 16 ──→ 最大迭代,强制结束 │ └──────────────┬──────────────────────────────┘ │ ▼ 最终回答 / 总结 │ ▼ (后台异步) ┌──────────────┐ │ Evolver 学习 │ └──────────────┘ ``` ### 为什么是 16 轮? | 迭代次数 | 优点 | 缺点 | |----------|------|------| | 8 轮 | 快速响应 | 复杂运维任务不够用 | | **16 轮** | **覆盖 90% 运维场景** | **偶尔多消耗 token** | | 32 轮 | 兜底极端场景 | 成本高、用户等待久 | 16 轮是**能力与效率的平衡点**,配合自适应检查点避免无意义循环。 ### 主循环核心代码(精简版) ```go for iter := 0; iter < a.config.MaxIterations; iter++ { if err := ctx.Err(); err != nil { return nil, err } // 中断检查 a.emit(Event{Type: EventProgress, Round: iter + 1}) // 自适应检查点 if hint := a.checkAdaptiveCheckpoint(iter, ...); hint != "" { messages = append(messages, llm.Message{Role: "user", Content: hint}) } trimmed := a.trimMessages(messages) // 裁剪历史 resp, err := a.llm.Complete(ctx, &llm.CompletionRequest{ Messages: trimmed, Tools: llmTools, Temperature: a.config.Temperature, MaxTokens: a.config.MaxTokens, }) // ... a.emit(Event{Type: EventThought, Round: iter + 1, Thought: resp.Message.Content}) if len(resp.Message.ToolCalls) == 0 { // 最终答案 a.emit(Event{Type: EventDone, Answer: resp.Message.Content, ...}) a.triggerEvolve(ctx, query, serverInfo, answer, toolRecords, true, start) return &tools.Result{Success: true, Output: answer}, nil } // 处理工具调用(含安全检查、循环检测、风险事件) observations, records, failInc := a.processToolCalls(ctx, resp.Message.ToolCalls, ...) messages = append(messages, observations...) } // 达到最大轮次 a.triggerEvolve(ctx, query, serverInfo, "", toolRecords, false, start) return nil, fmt.Errorf("达到最大迭代次数(%d)...", a.config.MaxIterations) ``` ## 自适应检查点机制 ### checkAdaptiveCheckpoint() ```go func (a *Agent) checkAdaptiveCheckpoint(iter int, history map[string]int, consecFail, totalCalls int) string { switch iter { case 3: // 第 4 轮(0-indexed 为 3) repeat := 0 for _, c := range history { if c > 1 { repeat += c - 1 } } if totalCalls > 0 && (float64(repeat)/float64(totalCalls) > 0.5 || consecFail >= 3) { return "【系统提示】检测到重复使用相同工具或连续失败,请尝试换一种思路或不同命令。" } case 7: // 第 8 轮 return "【系统提示】已执行到中段,请总结进展。信息足够就直接给出最终答案,否则说明剩余步骤。" case 11: // 第 12 轮 return fmt.Sprintf("【系统提示】仅剩 %d 步,请立即整理已有信息给出最终答案,不要再调用新工具。", a.config.MaxIterations-iter-1) } return "" } ``` | 检查点 | 轮次 | 策略 | 触发条件 | |--------|------|------|----------| | **Tier 1** | Round 4 | 启发式纠偏 | 重复工具调用率 >50% **或** 连续失败 ≥3 | | **Tier 2** | Round 8 | 强制中间总结 | 始终注入 | | **Tier 3** | Round 12 | 敦促收尾 | 始终注入 | ### 设计决策:为什么用消息注入而不是额外 LLM 调用? | 方案 | 优点 | 缺点 | |------|------|------| | ❌ 额外 LLM 调用评估状态 | 精确 | 每次 +1 API 调用,成本翻倍 | | ✅ **消息注入** | **零额外成本** | 依赖 LLM 自主理解提示 | 把引导消息以 `user` 角色注入对话,LLM 在下一轮推理时自然调整行为。这是「计算型反馈 + 积极的 prompt injection」的典型应用(见 [03 - Harness 工程化](03-Harness工程化与反馈回路.md))。 ## 循环检测 防止 LLM 陷入「用相同参数反复调用同一工具」的死循环: ```go key := tc.Function.Name + ":" + tc.Function.Arguments history[key]++ if history[key] > 3 { obs = append(obs, llm.Message{ Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name, Content: "错误: 检测到循环调用,相同参数已调用超过3次,请换一种方式", }) continue } ``` 阈值 >3 次:允许 LLM 重试 2~3 次(合理调试),超过则强制换法。 ## 连续失败追踪 ```go func (a *Agent) updateConsecutiveFailures(obs []llm.Message, current int) int { errs := 0 for _, o := range obs { c := o.Content if strings.HasPrefix(c, "错误") || strings.HasPrefix(c, "失败") || strings.HasPrefix(c, "执行错误") || strings.HasPrefix(c, "执行失败") { errs++ } } if errs == 0 { return 0 } // 有成功就重置 return current + errs } ``` 连续失败 ≥3 次会触发 Tier 1 检查点的纠偏提示。 ## Token 估算与消息裁剪 ### estimateTokens(粗略估算) ```go func estimateTokens(s string) int { return len([]rune(s))/3 + 1 } ``` 中文约 1 字/token,英文约 1/4,折中 rune/3。够用即可,不需要精确 tokenizer。 ### trimMessages ```go func (a *Agent) trimMessages(msgs []llm.Message) []llm.Message { maxT := a.config.MaxContextTokens * 9 / 10 // 留 10% 余量 sysT := estimateTokens(msgs[0].Content) kept := ... // 从末尾向前累加,超 maxT 停止 if len(kept) < 4 && len(msgs) > 4 { kept = msgs[len(msgs)-4:] // 最少保留最近 4 条 } result := append([]llm.Message{msgs[0]}, kept...) // system + 最近消息 return ensureToolMessageIntegrity(result) } ``` ### ensureToolMessageIntegrity(关键) LLM API 要求 `role: tool` 消息必须紧跟在带 `tool_calls` 的 `assistant` 消息之后,且 ID 匹配。裁剪后可能出现孤立 tool 消息,导致 API 报错: ``` Messages with role 'tool' must be a response to a preceding message with 'tool_calls' ``` `ensureToolMessageIntegrity` 修复这种孤立: ```go // 1. 收集所有 assistant.tool_calls 的 ID // 2. 遍历 tool 消息: // - 若 ID 不在 validIDs 中 → 删除 // - 若前一条不是 assistant 或 ID 不匹配 → 删除 ``` ## 工具调用处理(processToolCalls) 完整流程: ``` 对每个 tool_call: ├─ 循环检测(>3 次拒绝) ├─ registry.Get(name) 查找工具 ├─ json.Unmarshal 解析参数 ├─ risk = toolRisk(tool, args) ← 优先 DynamicRiskTool ├─ emit EventToolCall(name, args, risk) ├─ safetyCtl.Check(tool, args) ← 高危触发 ConfirmFn │ ├─ 黑名单(Critical)→ 拒绝 │ ├─ 阈值内 / AutoApprove → 放行 │ └─ 达阈值 → confirmFn → controller 推 WS confirm → 等前端回传 ├─ tool.Execute(ctx, args) ← context.WithTimeout(ToolTimeout) ├─ 输出截断(> OutputMaxLength) ├─ emit EventToolResult(output, success, duration) └─ 构建 observation 消息追加到 messages ``` ## Evolver 集成 ### triggerEvolve(异步进化) ```go func (a *Agent) triggerEvolve(ctx context.Context, query, serverHost, answer string, records []evolver.ToolCallRecord, success bool, start time.Time) { if a.evolver == nil { return } // 降级 exec := &evolver.TaskExecution{...} go func() { defer func() { _ = recover() }() // panic 恢复 eCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() _ = a.evolver.Evolve(eCtx, exec) // 错误忽略 }() } ``` **三层保护**: 1. **后台 goroutine**:不阻塞主循环,用户立即拿到答案 2. **30 秒超时**:防止进化过程无限运行 3. **panic 恢复**:捕获异常,确保 goroutine 崩溃不影响主流程 ### GetContextForPrompt(经验注入) 任务开始时(`Run` 入口): ```go evolveCtx := "" if a.evolver != nil { evolveCtx = a.evolver.GetContextForPrompt(query) // 按 task_type 取历史经验 } systemPrompt := prompt.BuildSystemPrompt(serverInfo, toolDescs, evolveCtx) ``` 注入的内容会被放入 Prompt Layer 5(详见 [07 - Prompt 构建](07-Prompt构建与记忆注入.md) 与 [08 - Evolver](08-Evolver自进化引擎.md))。 ## Agent 初始化流程 ``` NewAgent(client, registry, cfg, sc, ev) │ ├─ cfg == nil → DefaultConfig() ├─ sc == nil → safety.NewController(balanced) ├─ MaxContextTokens <= 0 → 强制设为 8000(防御性校验) └─ return Agent ``` **优雅降级设计**:Evolver 初始化失败时,`agentcli.NewEngine` 把 `ev` 设为 `nil`,Agent 继续正常工作,只是没有自学习能力。 ## 扩展指南 ### 增加新的迭代策略 1. 在 `Config` 中增加策略配置项(如新的检查点轮次) 2. 在 `checkAdaptiveCheckpoint()` 中添加新 case 3. 编写对应的中文提示文本 ### 自定义事件类型 1. 在 `EventType` 常量中新增(如 `EventWarning`) 2. 在 `Event` 结构中增加字段 3. 在 controller 的 `onEvent` 中增加 case 处理(推 WS + 落库) ## 常见陷阱 | 陷阱 | 解决方案 | |------|----------| | MaxIterations 设太大导致 token 失控 | 默认 16 + 自适应检查点 | | trimMessages 后 tool 消息孤立 | `ensureToolMessageIntegrity()` 修复 | | Evolver 失败导致 Agent 不可用 | 优雅降级,`evolver=nil` 时跳过进化 | | MaxContextTokens=0 导致上下文异常 | 防御性校验,<=0 时强制 8000 | | 高并发下 messages 切片竞争 | Agent 不是并发安全的,**每个任务用独立实例**(controller 已保证:每次 startTask 新建 Engine) | | LLM 不返回 tool_calls 也不返回 content | 视为完成,emit done(空答案) | ## 与 opsxcli 的差异总结 | 维度 | opsxcli | aiops | |------|---------|-------| | 回调机制 | `ToolCallback` 驱动 Bubble Tea TUI | `EventCallback` 推 WebSocket + 落库 | | Token 估算 | 独立 `TokenEstimator` 结构 | 内联 `estimateTokens` 函数 | | 配置加载 | 环境变量 `OPSXCLI_AGENT_*` | `DefaultConfig()` + `EngineConfig` 覆盖 | | 进化 goroutine 同步 | `evolveWg.Wait()` | 自带 recover + 30s 超时,不等 | | MaxContextTokens 默认 | 6000(为 8k 模型留余量) | 8000(适配现代 128k 模型) | 核心算法(16 轮循环 / 检查点 / 循环检测 / 连续失败追踪 / Evolver 集成)完全保留,差异仅在工程外壳。 > 作者:Logan.Li