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

16 KiB
Raw Blame History

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 从「盲目执行命令」进化为「有目的地探索和解决问题」。

核心结构

// 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 + 落库:

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 实现同时做两件事:

func (ac *agentConn) onEvent(e core.Event) {
    ac.write(wsEvent{...})  // 1. 推 WebSocket
    ac.persistMsg(...)      // 2. 落库 agent_cli_messages(progress 除外)
}

Config 配置

// 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 轮是能力与效率的平衡点,配合自适应检查点避免无意义循环。

主循环核心代码(精简版)

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()

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 工程化)。

循环检测

防止 LLM 陷入「用相同参数反复调用同一工具」的死循环:

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 次(合理调试),超过则强制换法。

连续失败追踪

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(粗略估算)

func estimateTokens(s string) int {
    return len([]rune(s))/3 + 1
}

中文约 1 字/token,英文约 1/4,折中 rune/3。够用即可,不需要精确 tokenizer。

trimMessages

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 修复这种孤立:

// 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(异步进化)

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 入口):

evolveCtx := ""
if a.evolver != nil {
    evolveCtx = a.evolver.GetContextForPrompt(query)  // 按 task_type 取历史经验
}
systemPrompt := prompt.BuildSystemPrompt(serverInfo, toolDescs, evolveCtx)

注入的内容会被放入 Prompt Layer 5(详见 07 - Prompt 构建 与 08 - Evolver)。

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