后端引擎 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>
416 lines
16 KiB
Markdown
416 lines
16 KiB
Markdown
# 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
|