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>
This commit is contained in:
attackerandClaude committed 2026-06-27 09:55:17 +08:00
1 parent f2069fe3f0
commit 92a2247390
37 files changed
+6449 -6

No files matched your search

+415
View File
@@ -0,0 +1,415 @@
# 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