Files
aiops/docs/agent/06-Agent核心引擎设计.md
T
attackerandClaude 92a2247390 feat: 新增 Agent CLI — Web 版 AI 运维 Agent(参考 opsxcli 移植)
后端引擎 backend/pkg/agentcli/(纯引擎库,零业务依赖,剥离 CLI/TUI 外壳):
- llm: OpenAI 兼容客户端,复用 model.LLMConfig
- tools: ssh_execute(复用 pkg/remoteexec)+ analyze_output + 风险评估黑名单
- safety: 三级安全模式 + 五级风险 + WebSocket 异步确认
- prompt: 五层 System Prompt
- core: ReAct 16 轮循环 + 自适应检查点 + 循环检测
- evolver: 四层记忆 + 进化循环,data/agentcli/ 文件存储

集成层:
- controller/agent_cli.go: WebSocket 控制器(事件流推流 + 高危确认 + 会话持久化)
- model/agent_cli.go: agent_cli_sessions/messages 表 + AutoMigrate
- 路由 /ws/agent-cli + /api/v1/aiops/agent-cli/sessions
- seed 权限 aiops:agent-cli / aiops:llm-config

前端:
- AgentCLI.tsx: 对话流 + 工具块 + 高危确认 + 四模块 tips 面板
- LLMConfig.tsx: 大模型设置独立页
- api/agentCli.ts + Sidebar/App 菜单路由

文档:
- docs/agent/ 11 篇(opsxcli 同名文档适配改写 + 00-README 映射总览)

其他:
- CLAUDE.md 补充 agent-cli 模块说明
- 修正 .gitignore 根级二进制规则(前导 /,避免误伤 frontend/src/components/aiops/)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-27 09:55:17 +08:00

416 lines
16 KiB
Markdown
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.
# 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