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

+74
View File
@@ -0,0 +1,74 @@
# Agent CLI 设计文档(aiops)
> 本文由 opsxcli 同名设计文档集适配改写,结合 aiops Agent CLI 的 Web 实现(`backend/pkg/agentcli/`)。
本系列文档是 **aiops Agent CLI**(Web 版 AI 运维 Agent)的设计文档集。Agent CLI 把 opsxcli 项目的 AI Agent 能力(ReAct 推理、工具调用、自进化、安全审批)移植到 aiops 的 Gin 后端,剥离 CLI/TUI 外壳,改造成一个由 WebSocket 驱动、与 React 前端对话的「类似 Claude Code / Codex」的运维 Agent 库。
一句话定位:**用户在 Agent CLI 页面选择目标主机,用自然语言描述任务,后端 ReAct 引擎自主通过 SSH 在远程服务器上排障、修复,并通过 WebSocket 把思考、命令、输出实时回传给前端。**
---
## 文档目录
| 编号 | 文档 | 一句话简介 |
|------|------|-----------|
| 01 | [项目概述与设计哲学](01-项目概述与设计哲学.md) | Agent CLI 的定位、四大设计哲学、与 opsxcli 的关系 |
| 02 | [整体架构设计](02-整体架构设计.md) | 引擎五层结构(controller / model / pkg / frontend / evolver)与执行流程 |
| 03 | [Harness 工程化与反馈回路](03-Harness工程化与反馈回路.md) | Harness Engineering 理论在 aiops Agent CLI 中的落地(前馈 + 反馈 + Steering) |
| 04 | [插件架构设计](04-插件架构设计.md) | Tool 接口 + Registry 工具注册中心,Web 版精简为 2 个工具 |
| 05 | [Builtin 内置工具](05-Builtin内置命令.md) | ssh_execute 与 analyze_output 两个内置工具的设计 |
| 06 | [Agent 核心引擎设计](06-Agent核心引擎设计.md) | ReAct 16 轮主循环、自适应检查点、循环检测、Evolver 集成 |
| 07 | [Prompt 构建与记忆注入](07-Prompt构建与记忆注入.md) | 五层 System Prompt 与动态记忆注入 |
| 08 | [Evolver 自进化引擎](08-Evolver自进化引擎.md) | 四层记忆 + 精简进化循环(OBSERVE→…→PREDICT) |
| 09 | [安全系统与工具注册](09-安全系统与工具注册.md) | 三级安全模式、五级风险、黑名单、WebSocket 确认 |
| 10 | [会话管理](10-会话管理.md) | GORM 数据库表持久化会话与消息(多用户隔离) |
---
## opsxcli → aiops 映射总览
| opsxcli 模块 | opsxcli 路径 | aiops 对应 | 处理方式 |
|--------------|-------------|-----------|----------|
| Agent ReAct 核心 | `internal/agent/core/` | `backend/pkg/agentcli/core/agent.go` | **适配**:剥离 TUI,改 EventCallback 推 WebSocket;保留 16 轮循环 / 检查点 / 循环检测 |
| 工具注册中心 | `internal/agent/tools/registry.go` | `backend/pkg/agentcli/tools/registry.go` | **原样移植**:Tool 接口(5 方法)+ DynamicRiskTool + Registry(线程安全) |
| 内置工具集 | `local_bash / ssh_execute / scp_transfer / file_read / file_search / analyze_output`(6 个) | `ssh_execute` + `analyze_output`(2 个) | **精简剥离**:去掉 local_bash(不在后端服务器本地执行)、scp、file_read、file_search |
| 安全系统 | `internal/agent/safety/` | `backend/pkg/agentcli/safety/safety.go` + `tools/risk.go` | **适配**:三级模式 / 五级风险 / 黑名单保留;确认机制由 CLI 终端交互改为 WebSocket confirm 事件 + 前端弹框 |
| Prompt 构建 | `internal/agent/prompt/` | `backend/pkg/agentcli/prompt/prompt.go` | **适配**:保留五层架构,角色改写为「aiops Agent CLI 智能运维 Agent」 |
| 自进化引擎 | `internal/agent/evolver/`(13 步 + 3300 行) | `backend/pkg/agentcli/evolver/engine.go` + `memory.go`(~400 行) | **精简适配**:保留四层记忆 + 进化循环思想;步骤从 13 步精简为核心 6 步(OBSERVE→CLASSIFY→LEARN→STORE→MEMORIZE→PREDICT) |
| LLM 客户端 | `internal/llm/`(多 Provider 配置向导 + claude.go/gemini.go 等 15 Provider) | `backend/pkg/agentcli/llm/openai.go` + `types.go` | **精简剥离**:剥离 wizard 配置向导和多 Provider,统一走 OpenAI 兼容接口;配置来自 DB 表 `ai_llm_configs`(model.LLMConfig) |
| 会话管理 | `internal/agent/session/`(JSONL 文件存储) | `backend/model/agent_cli.go` + `controller/agent_cli.go`(GORM DB) | **改造**:多用户 Web 场景下从本地文件改为数据库表 `agent_cli_sessions` / `agent_cli_messages`,按 user_id 隔离 |
| 记忆存储 | `~/.opsxcli/agent/*.jsonl` | `data/agentcli/*.jsonl`(工作目录) | **适配**:从用户主目录改为应用工作目录 |
| LLM 配置 | `~/.opsxcli/providers/*.json` | DB 表 `ai_llm_configs` + 「大模型设置」页面 | **改造**:从本地文件改为 DB 表 + Web 配置页 |
| CLI 命令层 | `cmd/`(Cobra) | `backend/controller/agent_cli.go`(Gin WebSocket handler) | **改造**:从 Cobra 命令改为 WebSocket 控制器 |
| TUI 终端界面 | `internal/agent/tui/`(Bubble Tea) | `frontend/src/components/aiops/AgentCLI.tsx`(React) | **改造**:从 Bubble Tea TUI 改为 React 对话流组件 |
| 远程执行 | `internal/agent/tools/` 内 SSH 实现 | `pkg/remoteexec/`(Go-native SSH,aiops 既有) | **复用**:直接复用 aiops 已有的远程执行包 |
---
## 移植决策
### 剥离了什么
1. **CLI 外壳**:去掉 Cobra 命令定义、参数解析、帮助文本 —— 由 WebSocket 控制器替代。
2. **TUI 终端界面**:去掉 Bubble Tea + 全套终端渲染 —— 由 React 对话流组件替代。
3. **LLM 多 Provider 配置向导**:去掉 `wizard.go` 交互式向导、`claude.go` / `gemini.go` 等专有 Provider —— 统一走 OpenAI 兼容 `/chat/completions`,覆盖 deepseek / qwen / moonshot / ollama / openai / custom。
4. **本地执行类工具**:去掉 `local_bash`(Web 后端不在服务器本地执行命令)、`scp_transfer`、`file_read`、`file_search` —— Web 版聚焦远程运维。
5. **Evolver 复杂分析**:去掉 opsxcli 13 步循环中的反射评分、经验整合去重、Skill 自动提炼等重型子流程 —— 精简为核心学习链路。
### 适配了什么
1. **会话持久化**:从本地 JSONL 文件改为 GORM 数据库表,支持多用户并发与按用户隔离。
2. **记忆路径**:从 `~/.opsxcli/agent/` 改为应用工作目录 `data/agentcli/`,便于容器化部署。
3. **LLM 配置来源**:从 `~/.opsxcli/providers/*.json` 改为 DB 表 `ai_llm_configs`,通过「大模型设置」页面配置。
4. **确认机制**:从 CLI 终端 `bufio.Reader` 改为 WebSocket `confirm` 事件 + 前端 `useConfirm()` 弹框。
5. **SSH 凭据**:从 `~/.ssh/config` 解析改为关联 `model.Server` 凭据(凭证系统 v2),由 controller 注入引擎。
6. **角色定位**:从「opsxcli 智能运维超级助手」改写为「aiops Agent CLI 智能运维 Agent(Web 版,类似 Claude Code)」。
### 精简了什么
1. **工具集**:6 个 → 2 个(`ssh_execute` + `analyze_output`)。
2. **Evolver 步骤**:13 步 → 6 步(保留学习与注入能力,剥离重型分析)。
3. **LLM 层**:15 个 Provider 文件 → 1 个 OpenAI 兼容客户端。
4. **记忆层级**:保留四层(经验 / 环境 / 事实 / 程序),但程序层仅保留接口位、未实现自动 Skill 提炼。
> 作者:Logan.Li
@@ -0,0 +1,230 @@
# 01 - 项目概述与设计哲学
> 本文由 opsxcli 同名设计文档适配改写,结合 aiops Agent CLI 的 Web 实现(`backend/pkg/agentcli/`)。
> opsxcli 原文档把 Agent 描述为 CLI 工具(`opsxcli agent -q "..."`),aiops 改为 Web 页面 + WebSocket 事件流。涉及 CLI 特性的地方已注明 aiops 的对应实现。
> 建议阅读顺序:01 → 02 → 03 → ... → 10
## 项目定位
**aiops Agent CLI** —— aiops 平台内置的 Web 版智能运维 Agent。
类似 Claude Code / Codex,但运行在浏览器里:用户在「Agent CLI」页面选择一台目标主机,用自然语言描述运维任务(如「检查磁盘空间」「nginx 启动失败的原因」),后端 ReAct 引擎自主通过 SSH 在远程服务器上排查、修复,并通过 WebSocket 把每一步思考、执行的命令、输出与最终结果实时回传给前端。
这是把 opsxcli(基于 Cobra 的 CLI 运维 Agent)的 AI 能力,剥离 CLI/TUI 外壳、适配为 aiops 的 Gin 后端库后的产物。
## 设计目标
| 维度 | 目标 |
|------|------|
| 形态 | Web 对话流(WebSocket 双向通信),不是命令行 |
| 部署 | 嵌入 aiops 后端二进制(go:embed),无需独立服务 |
| 安全 | 默认 balanced;高危命令前端确认;危险命令硬拒 |
| 智能 | 16 轮 ReAct 主循环 + 自进化记忆注入 |
| 隔离 | 多用户并发,会话/消息按 user_id 隔离,持久化到 DB |
## 五层引擎结构
aiops Agent CLI 不是单一包,而是分散在 aiops 的标准 Gin 分层中:
```
┌─────────────────────────────────────────────────────┐
│ frontend/ (React 对话流 UI) │
│ components/aiops/AgentCLI.tsx │
│ - WebSocket 客户端,渲染 thought/tool_call/ │
│ tool_result/done/error 事件流 │
│ - 高危命令 useConfirm() 弹框确认 │
├─────────────────────────────────────────────────────┤
│ controller/ (HTTP/WS handler) │
│ controller/agent_cli.go │
│ - WebSocket 入口 /ws/agent-cli?id=<serverID> │
│ - 构建 Engine、注入 EventCallback + ConfirmFn │
│ - 事件落库 + 推 WS │
├─────────────────────────────────────────────────────┤
│ model/ (GORM 模型) │
│ model/agent_cli.go │
│ - AgentCLISession / AgentCLIMessage │
│ - LLMConfig (ai_llm_configs) │
├─────────────────────────────────────────────────────┤
│ pkg/agentcli/ (引擎库,零业务依赖) │
│ ├── agentcli.go Engine 顶层入口 │
│ ├── core/ ReAct 主循环 │
│ ├── tools/ 工具注册 + ssh_execute 等 │
│ ├── safety/ 安全控制器 │
│ ├── prompt/ 五层 System Prompt │
│ ├── evolver/ 自进化引擎(四层记忆) │
│ └── llm/ OpenAI 兼容客户端 │
├─────────────────────────────────────────────────────┤
│ pkg/remoteexec/ (复用 aiops 既有 Go-native SSH) │
└─────────────────────────────────────────────────────┘
```
**关键设计原则**:`pkg/agentcli/` 零业务依赖 —— 不 import `model` / `controller`,所有持久化与 WebSocket 由 controller 层负责。引擎只关心「思考-行动-观察」循环本身。
## Agent 引擎子模块全景
`backend/pkg/agentcli/` 包含 7 个子包:
```
backend/pkg/agentcli/
├── agentcli.go ── Engine 顶层入口:组装 LLM + 工具 + 安全 + Evolver + ReAct 核心
├── core/ ── ReAct 核心引擎:16 轮主循环 + 自适应检查点 + 循环检测
├── prompt/ ── Prompt 构建:五层 System Prompt + 动态记忆注入
├── evolver/ ── 自进化引擎:四层记忆 + 精简进化循环
├── safety/ ── 安全系统:三级模式 + 阈值确认(ConfirmFn 回调)
├── tools/ ── 工具注册中心 + 2 个内置工具 + 风险评估
└── llm/ ── LLM 抽象层(OpenAI 兼容,剥离多 Provider)
```
> 与 opsxcli 的差异:opsxcli 有 `tui/` 子包(Bubble Tea 终端界面),aiops 把它替换为前端 React 组件 `AgentCLI.tsx`,由 controller 序列化事件推 WS。
## 四大设计哲学
### 1. 安全第一 (Safety First)
> 运维工具的底线是「不搞砸」。
- 所有工具执行前必须通过安全评估(`safety.Controller.Check`)
- 三级安全模式:`strict` / `balanced`(默认)/ `permissive`
- 5 级风险等级:Safe → Low → Medium → High → Critical
- 危险命令黑名单硬拒(不可确认放行):`rm -rf /`、`mkfs`、`dd if=/dev/zero`、fork bomb 等
- 高危命令经 WebSocket `confirm` 事件 + 前端 `useConfirm()` 弹框由用户确认
- 会话与每条消息落库,事后可审计(`agent_cli_messages` 表)
### 2. 渐进式复杂度 (Progressive Complexity)
> 简单任务零配置,复杂任务有能力。
- 默认配置即可运行(`core.DefaultConfig()`)
- LLM 配置通过「大模型设置」页面填一次即生效(DB 表 `ai_llm_configs`)
- 引擎初始化时优雅降级:Evolver 失败设为 `nil`,不影响主 ReAct 循环
- 安全模式按需升级(balanced 默认 → strict 高敏生产环境)
### 3. 工具化思维 (Tool-Oriented)
> 万物皆 Tool,一切可注册。
- 统一的 `Tool` 接口(5 方法):`Name` / `Description` / `Parameters` / `RiskLevel` / `Execute`
- `DynamicRiskTool` 扩展接口:依据运行时参数动态评估风险(如 `ssh_execute` 看具体命令)
- `Registry` 线程安全注册中心
- 实现接口即可扩展,无需修改核心代码
- **Web 版内置工具精简为 2 个**(剥离 opsxcli 的 local_bash / scp / file_read / file_search):
- `ssh_execute` —— 在目标主机远程执行命令(复用 `pkg/remoteexec`)
- `analyze_output` —— 对冗长输出做摘要/错误提取,避免 token 爆炸
### 4. 自我进化 (Self-Evolution)
> 用得越多,越懂你。
- Evolver 引擎在任务完成后异步学习(30s 超时 + panic 恢复,绝不阻塞主循环)
- 四层记忆(`data/agentcli/`):
- **经验记忆**(`experience.jsonl`)—— 任务类型 → 工具序列 + 提示,按成功率优先注入
- **环境记忆**(`environment.json`)—— 已知主机列表
- **事实记忆**(`factual.jsonl`)—— LLM 提炼的持久事实
- **程序记忆**(`procedural.jsonl`)—— 程序性知识(接口位,未实现自动 Skill 提炼)
- 经验按任务类型分类(磁盘空间 / 网络 / 服务管理 / 软件安装 等 11 类)
- 下次任务开始时,Evolver 通过 `GetContextForPrompt()` 把相关历史经验注入 System Prompt
## Agent 核心工作流
```
用户在 Agent CLI 页面选择主机,输入 "内存使用率"
│
▼
┌──────────────────────┐
│ WebSocket 入口 │ ← controller/agent_cli.go: Run()
│ /ws/agent-cli?id=… │ 校验 JWT、加载 model.Server、解析 SSH 凭据
└────────┬─────────────┘
│
▼
┌──────────────────────┐
│ Engine 组装 │ ← pkg/agentcli/agentcli.go: NewEngine()
│ LLM(OpenAI兼容) │ 注入 EventCallback + ConfirmFn
│ + tools(ssh/analyze)│
│ + safety + Evolver │
└────────┬─────────────┘
│
▼
┌──────────────────────┐
│ Prompt Builder │ ← prompt/prompt.go: BuildSystemPrompt()
│ 五层 System Prompt │ Layer 1 角色 / 2 安全 / 3 工具 / 4 工作方式 / 5 记忆
│ + Evolver 记忆注入 │
└────────┬─────────────┘
│
▼
┌──────────────────────┐ ┌──────────────┐
│ LLM 推理 │────▶│ 工具调用 │
│ (ReAct 16 轮循环) │◀────│ ssh_execute │
└────────┬─────────────┘ └──────┬───────┘
│ │
│ ┌──────────────────────┘
│ │ 每步都 emit Event
│ ▼
│ 自适应检查点 │ ┌──────────────┐
│ (Round 4/8/12) │ │ 安全检查 │
│ 循环检测(>3次拒绝) │ │ (RiskLevel) │
│ 连续失败追踪 │ │ 高危→confirm│
└────────┬─────────────┘ └──────────────┘
│ EventCallback → controller
│ ├─ 推 WS: thought/tool_call/tool_result/done
│ └─ 落库: agent_cli_messages
▼ (16 轮迭代或 LLM 不再返回 tool_calls)
┌──────────────────────┐
│ Evolver 学习 │ ← 后台 goroutine,不影响响应速度
│ (经验 + 主机 + 事实) │
└──────────────────────┘
```
## LLM 客户端策略
> opsxcli 有 15 个 Provider(注册制工厂 + `claude.go` / `gemini.go` 等专有实现 + 配置向导 `wizard.go`)。
> aiops 精简为单一 OpenAI 兼容客户端,覆盖所有 OpenAI 协议兼容后端。
```go
// backend/pkg/agentcli/llm/openai.go
// 唯一客户端实现:走标准 /chat/completions + Bearer 鉴权
// 支持后端:openai / deepseek / qwen / moonshot / ollama / custom
func NewClient(cfg Config) Client
```
配置不再来自 `~/.opsxcli/providers/*.json`,而是 DB 表 `ai_llm_configs`(`model.LLMConfig`),由 controller 在握手阶段加载并注入 Engine。
## 文档阅读指南
| 编号 | 文档 | 核心内容 |
|------|------|----------|
| 01 | 本文档 | 项目概述、设计哲学、引擎全景 |
| 02 | [整体架构设计](02-整体架构设计.md) | 五层结构、执行流程、依赖关系 |
| 03 | [Harness 工程化与反馈回路](03-Harness工程化与反馈回路.md) | Harness 理论、前馈/反馈、Steering Loop |
| 04 | [插件架构设计](04-插件架构设计.md) | Tool 接口、Registry、扩展规范 |
| 05 | [Builtin 内置工具](05-Builtin内置命令.md) | ssh_execute 与 analyze_output 设计 |
| 06 | [Agent 核心引擎设计](06-Agent核心引擎设计.md) | ReAct 主循环、自适应检查点 |
| 07 | [Prompt 构建与记忆注入](07-Prompt构建与记忆注入.md) | 五层 Prompt、动态记忆层 |
| 08 | [Evolver 自进化引擎](08-Evolver自进化引擎.md) | 四层记忆、精简进化循环 |
| 09 | [安全系统与工具注册](09-安全系统与工具注册.md) | 风险评估、安全模式、Tool 接口 |
| 10 | [会话管理](10-会话管理.md) | GORM 持久化、按用户隔离 |
> 注:opsxcli 还有第 11 篇「TUI 终端界面设计」,aiops 已剥离 Bubble Tea TUI,由 React 组件 `AgentCLI.tsx` 替代,故本系列不含该篇。
## 快速开始
```bash
# 1. 在「大模型设置」页面配置 LLM(DB 表 ai_llm_configs,Enabled=true)
# 2. 在「服务器管理」确保目标主机凭据(凭证系统 v2)已配置
# 3. 进入「Agent CLI」页面,选择主机 → 输入任务 → 观察 WebSocket 推送的事件流
```
代码侧调用示例(controller):
```go
engine, _ := agentcli.NewEngine(agentcli.EngineConfig{
LLM: llm.Config{Endpoint: ..., APIKey: ..., Model: ...},
SSH: sshCfg, // 来自 model.Server 凭据
SafetyMode: safety.ModeBalanced,
})
engine.SetCallback(onEvent) // 推 WS + 落库
engine.SetConfirmFn(confirmFn) // 高危确认
output, _ := engine.Run(ctx, query, serverInfo)
```
> 作者:Logan.Li
+303
View File
@@ -0,0 +1,303 @@
# 02 - 整体架构设计
> 本文由 opsxcli 同名设计文档适配改写,结合 aiops Agent CLI 的 Web 实现(`backend/pkg/agentcli/`)。
> opsxcli 的「cmd/ + plugins/ + internal/」CLI 三层在 aiops 中改造为「controller/(HTTP/WS handler)+ model/(GORM)+ pkg/agentcli/(引擎库)+ frontend/(React UI)」四层。
## 项目结构(Agent CLI 相关部分)
```
aiops/
├── backend/
│ ├── controller/
│ │ └── agent_cli.go # WebSocket 控制器(HTTP/WS handler 层)
│ │ # - /ws/agent-cli?id=<serverID> 入口
│ │ # - 构建 Engine、注入回调、事件落库
│ │ # - ListSessions/GetSession/DeleteSession REST
│ ├── model/
│ │ ├── agent_cli.go # AgentCLISession / AgentCLIMessage(GORM 模型)
│ │ └── llm.go # LLMConfig(ai_llm_configs 表)
│ ├── pkg/
│ │ ├── agentcli/ # Agent 引擎库(零业务依赖,核心)
│ │ │ ├── agentcli.go # Engine 顶层入口
│ │ │ ├── core/ # ReAct 主循环 + EventCallback
│ │ │ ├── tools/ # Registry + ssh_execute + analyze_output + risk
│ │ │ ├── safety/ # 安全控制器(ConfirmFn 注入)
│ │ │ ├── prompt/ # 五层 System Prompt
│ │ │ ├── evolver/ # 四层记忆 + 精简进化循环
│ │ │ └── llm/ # OpenAI 兼容客户端 + 类型定义
│ │ └── remoteexec/ # 复用 aiops 既有 Go-native SSH
│ └── router/routes.go # 路由注册:WS + REST 会话管理
│
├── frontend/
│ ├── src/
│ │ ├── components/aiops/AgentCLI.tsx # React 对话流组件
│ │ ├── api/agentCli.ts # 会话历史 REST 客户端
│ │ └── components/ui/confirm-dialog.tsx# useConfirm() 高危确认
│ └── ...
│
└── data/agentcli/ # Evolver 记忆目录(运行时生成)
├── experience.jsonl # 经验记忆
├── factual.jsonl # 事实记忆
├── procedural.jsonl # 程序记忆(接口位)
└── environment.json # 环境记忆(已知主机)
```
## 架构设计原则
### 1. 分层 + 依赖注入
aiops Agent CLI 的核心约束是「引擎库零业务依赖」:
- **controller 层** —— 负责协议适配(WebSocket)、凭据构建、事件持久化、用户隔离
- **model 层** —— GORM 模型(会话、消息、LLM 配置、Server)
- **pkg/agentcli 层** —— 纯算法库,只依赖 `pkg/remoteexec` 和自己的子包,**不 import model/controller**
- **frontend 层** —— React 对话流 UI,纯事件消费 + 控制消息发送
依赖通过函数参数注入:
```go
// controller/agent_cli.go 关键注入点
engine.SetCallback(ac.onEvent) // EventCallback:推 WS + 落库
engine.SetConfirmFn(ac.confirmFn) // ConfirmFn:高危确认(阻塞等前端回传)
```
引擎因此可被任何调度方复用(不仅是 WebSocket,未来也可被 cron 或审批流调用)。
### 2. 工具化设计
每个工具都是独立的 struct,实现 `Tool` 接口即可注册:
```
pkg/agentcli/tools/
├── registry.go # Registry(线程安全) + Tool/DynamicRiskTool 接口 + RiskLevel
├── ssh_execute.go # 远程命令执行(DynamicRiskTool)
├── builtin.go # analyze_output(只读 Safe)
└── risk.go # 黑名单 + AssessBashRisk 动态评估
```
> 与 opsxcli 的差异:opsxcli 把风险评估放在 `safety/` 包,aiops 为避免 `tools` ↔ `safety` 循环依赖,把 `risk.go` 置于 `tools` 包,由 `ssh_execute` 直接调用。
### 3. 配置集中化(DB 驱动)
opsxcli 通过 `~/.opsxcli/config.yaml` 和环境变量加载配置;aiops 配置来自 DB:
| 配置项 | opsxcli | aiops |
|--------|---------|-------|
| LLM 配置 | `~/.opsxcli/providers/*.json` | DB 表 `ai_llm_configs`(model.LLMConfig) |
| LLM 配置入口 | CLI 向导 `wizard.go` | 「大模型设置」Web 页面 |
| SSH 凭据 | `~/.ssh/config` 解析 | model.Server 关联凭证系统 v2 |
| 引擎运行参数 | config.yaml + 环境变量 | `core.DefaultConfig()`(硬编码默认,必要时通过 EngineConfig 覆盖) |
| 记忆目录 | `~/.opsxcli/agent/` | 工作目录 `data/agentcli/`(容器化友好) |
### 4. 协议适配层(controller)
controller 是「Web 世界」与「引擎世界」的翻译器:
| Web 协议 | 引擎调用 | 引擎事件 → Web |
|----------|----------|----------------|
| `WS start {query}` | `engine.Run(ctx, query, serverInfo)` | emit `thought`/`tool_call`/`tool_result`/`progress`/`done`/`error` → 序列化为 `wsEvent` 推 WS |
| `WS confirm/deny {confirm_id}` | 解锁 `ConfirmFn` 的 chan | emit 前由 controller 推 `confirm` 事件 |
| `WS abort` | `cancel()` 取消 ctx | emit `error`(ctx.Err()) |
| `GET /sessions` | — | 直接查 `agent_cli_sessions` 表 |
---
## 核心流程图
### WebSocket 握手 + 任务执行完整流程
以用户在 Agent CLI 页面输入「检查 nginx 启动失败的原因」为例:
```
用户在 Agent CLI 页面选择主机、输入任务 "检查 nginx 启动失败的原因"
│
▼
┌──────────────────────────────────────────────────┐
│ 1. WebSocket 握手 (controller/agent_cli.go: Run) │
│ ├─ 校验 JWT (middleware.JWTAuth) │
│ ├─ 加载 model.Server (按 ?id=) │
│ ├─ buildSSHConfig: 解析凭证系统 v2 凭据 │
│ ├─ 加载 model.LLMConfig (Enabled=true) │
│ └─ upgrader.Upgrade 升级 WS │
└────────────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ 2. 推送 session 事件(主机上下文) │
│ wsEvent{type:"session", message:"已连接主机…"}│
└────────────────────┬─────────────────────────────┘
│
▼ (前端发来 {type:"start", query:"..."})
┌──────────────────────────────────────────────────┐
│ 3. startTask │
│ ├─ DB 创建 AgentCLISession (status=running) │
│ ├─ DB 存用户消息 (role=user) │
│ ├─ 构建 Engine (注入 LLM/SSH/SafetyMode) │
│ ├─ engine.SetCallback(ac.onEvent) │
│ ├─ engine.SetConfirmFn(ac.confirmFn) │
│ └─ go engine.Run(ctx, query, serverInfo) │
└────────────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ 4. Engine.Run → core.Agent.Run(ReAct 循环) │
│ │
│ ┌───────────────────────────────────┐ │
│ │ Prompt 构建(五层) │ │
│ │ L1 角色 / L2 安全 / L3 工具 │ │
│ │ L4 工作方式 / L5 Evolver 记忆 │ │
│ └──────────────┬────────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────┐ │
│ │ for iter := 0; iter < 16; iter++ { │ │
│ │ emit progress(round) │ │
│ │ 自适应检查点(iter 3/7/11) │ │
│ │ trimMessages(裁剪历史) │ │
│ │ resp = llm.Complete(...) │ │
│ │ emit thought(resp.Content) │ │
│ │ if 无 tool_calls: │ │
│ │ emit done → triggerEvolve │ │
│ │ return │ │
│ │ for tc in resp.ToolCalls: │ │
│ │ risk = toolRisk(...) │ │
│ │ emit tool_call(name,args,risk) │ │
│ │ approved = safetyCtl.Check(..)│ ←──┐ │
│ │ if !approved: 跳过 │ │ 高危 │
│ │ result = tool.Execute(ctx,args)│ │ 触发 │
│ │ emit tool_result(output,...) │ │ confirm │
│ │ records.append(..) │ │ 事件 │
│ │ } │ │ │
│ │ 循环检测 / 连续失败追踪 │ │ │
│ │ } │ │ │
│ └────────────────────────────────────┘ │ │
└──────────────────────────────────────────┘ │ │
│ │
┌───────────────────────────┐
│ confirmFn (controller) │
│ 推 wsEvent{type:"confirm"}│
│ ch := make(chan bool) │
│ select { │
│ case approved := <-ch: │
│ case <-5min超时: │
│ } │
└─────────┬─────────────────┘
│ 前端 useConfirm() 弹框
│ 发回 {type:"confirm"/"deny"}
▼
resolveConfirm(confirmID, approved)
│ ch <- approved
▼ 解锁 safety.Check
```
### 事件落库映射
`onEvent` 回调把每个事件同时推送 WS 与落库 `agent_cli_messages`:
| Event.Type | DB 字段 type | DB 字段 role | 备注 |
|------------|--------------|--------------|------|
| `thought` | thought | assistant | AI 思考文本 |
| `tool_call` | tool_call | tool | 含工具名、参数 JSON、风险等级 |
| `tool_result` | tool_result | tool | 含输出、成败、耗时 |
| `done` | done | assistant | 最终答案 + token 统计 |
| `error` | error | assistant | 错误信息(含达到最大轮次) |
| `progress` | — | — | 太频繁,不入库 |
### 系统数据流
```
┌────── controller 层(协议适配) ──────┐
│ agent_cli.go (WS handler) │
│ ├─ 加载 Server/LLMConfig │
│ ├─ DB 创建 Session/Message │
│ └─ 注入 Callback/ConfirmFn │
└─────────────────┬─────────────────────┘
│ 依赖注入
▼
┌────── pkg/agentcli 层(引擎库) ──────┐
│ agentcli.go (Engine) │
│ ├─ core.Agent (ReAct) │
│ │ ├─ llm.Client (OpenAI 兼容) │
│ │ ├─ tools.Registry │
│ │ │ ├─ ssh_execute │
│ │ │ └─ analyze_output │
│ │ ├─ safety.Controller │
│ │ ├─ prompt.BuildSystemPrompt │
│ │ └─ evolver.EvolverEngine │
│ └─ (零 model/controller 依赖) │
└─────────────────┬─────────────────────┘
│ 调用
▼
┌────── pkg/remoteexec(Go-native SSH)─┐
│ Exec(cfg, command, timeout) │
└───────────────────────────────────────┘
```
## 技术栈
| 层 | 技术 |
|----|------|
| 后端框架 | Gin + GORM + gorilla/websocket |
| LLM 协议 | OpenAI 兼容 `/chat/completions` + function calling |
| SSH | aiops 既有 `pkg/remoteexec`(Go-native,无外部依赖) |
| 前端 | React 19 + TypeScript + shadcn/ui(`useConfirm` 弹框) |
| 持久化 | MySQL(主库)/ SQLite(降级) —— 复用 aiops 既有 DB |
| 记忆存储 | 文件 `data/agentcli/*.jsonl`(与 opsxcli 原生一致,零 DB 依赖) |
> 为什么记忆用文件而会话用 DB?记忆是引擎库内部的产物(学习沉淀),与业务无关;会话需要多用户隔离、列表查询、删除等业务操作,必须走 DB。这是「引擎库零业务依赖」原则的延伸。
## 路由注册
`backend/router/routes.go`:
```go
// Agent CLI — AI 运维 Agent(WebSocket 推流 + 会话历史回放)
agentCLIHandler := controller.NewAgentCLIHandler(db)
r.GET("/ws/agent-cli", middleware.JWTAuth(&cfg.JWT, db), agentCLIHandler.Run)
agentCLIGroup := authorized.Group("/aiops/agent-cli")
{
agentCLIGroup.GET("/sessions", agentCLIHandler.ListSessions)
agentCLIGroup.GET("/sessions/:id", agentCLIHandler.GetSession)
agentCLIGroup.DELETE("/sessions/:id", agentCLIHandler.DeleteSession)
}
```
WebSocket 因无法设自定义 header,JWT 通过 `?token=` query param 由 `JWTAuth` 中间件兼容解析。
## 添加新模块示例
以「增加一个新工具 `ping_check`」为例:
1. **实现 Tool 接口**(`pkg/agentcli/tools/ping_check.go`):
```go
type PingCheckTool struct{ cfg remoteexec.SSHConfig }
func (t *PingCheckTool) Name() string { return "ping_check" }
func (t *PingCheckTool) Description() string { return "在目标主机 ping 指定地址" }
func (t *PingCheckTool) Parameters() map[string]interface{} { /* JSON Schema */ }
func (t *PingCheckTool) RiskLevel() tools.RiskLevel { return tools.RiskSafe }
func (t *PingCheckTool) Execute(ctx context.Context, args map[string]interface{}) (*tools.Result, error) {
// 调用 remoteexec.Exec 执行 ping
}
```
2. **在 Engine 中注册**(`pkg/agentcli/agentcli.go: NewEngine`):
```go
registry.Register(tools.NewSSHExecuteTool(cfg.SSH, coreCfg.ToolTimeout))
registry.Register(tools.NewAnalyzeOutputTool())
registry.Register(tools.NewPingCheckTool(cfg.SSH)) // 新增
```
3. 工具自动出现在 LLM 的 `tools` 参数中(`registry.ToLLMTools()`),Prompt Layer 3 工具描述自动生成(`buildToolDescriptions()`)。无需改 Prompt、无需改 controller、无需改前端。
## 设计目标
1. **Web 友好**:所有交互经 WebSocket,适配浏览器对话流
2. **零业务耦合**:引擎库可独立测试与复用
3. **复用 aiops 既有能力**:LLM 配置、SSH、凭证系统、DB
4. **易于扩展**:Tool 接口 + Registry,实现接口即扩展
5. **可审计**:每条事件落库,会话历史可回放
> 作者:Logan.Li
@@ -0,0 +1,276 @@
# 03 - Harness 工程化与反馈回路
> 本文由 opsxcli 同名设计文档适配改写,结合 aiops Agent CLI 的 Web 实现(`backend/pkg/agentcli/`)。
> Harness Engineering 是与具体语言/形态无关的理论框架,原文的思想性内容(控制论、Steering Loop、四类调节 Harness)几乎完全保留;落地实例部分从「opsxcli CLI + golangci-lint」改写为「aiops 后端库 + WebSocket 反馈回路 + Evolver 自进化」。
---
## 1. 核心定义:Agent = Model + Harness
### 1.1 什么是 Harness
> **Agent = Model + Harness** — LangChain
Harness 是 Agent 中除 Model 本身之外的一切。在大模型运维 Agent 的有界上下文中,Harness 分为两个层次:
- **内置 Harness(built-in harness)**:引擎作者构建的部分 —— System Prompt、工具注册机制、ReAct 编排循环
- **外部 Harness(outer harness)**:业务方为自身场景构建的部分 —— 安全模式、确认机制、记忆注入、审计落库
Model 是「大脑」,Harness 是「身体 + 环境 + 工具 + 规则」。大脑决定智能,但身体和环境决定智能能否被正确、可靠、持续地发挥。
### 1.2 两个目标
> 一个构建良好的 Harness 服务于两个目标:
> 1. **提高智能体首次就做对的概率**(feedforward)
> 2. **提供反馈回路,在问题到达人类前尽可能自我纠正**(feedback)
> — Martin Fowler / Birgitta Böckeler
这两个目标分别对应**前馈指导**和**反馈传感器**,是 Harness 的双引擎。
### 1.3 核心理念
> **Humans steer. Agents execute.** — OpenAI
人类的工作重心从「敲每一条命令」,转向**设计环境、明确意图和构建反馈回路**。在 aiops Agent CLI 中,运维人员的角色从「亲自 SSH 排障」变为「在 Web 上描述任务、审阅高危确认、监督 Agent 执行」。
---
## 2. 控制论基础:前馈与反馈
### 2.1 前馈指导 vs 反馈传感器
| 维度 | 前馈指导(Feedforward Guides) | 反馈传感器(Feedback Sensors) |
|------|-------------------------------|-------------------------------|
| **时机** | 智能体行动**之前** | 智能体行动**之后** |
| **目的** | 预期行为并加以引导 | 观察产出并帮助自我纠正 |
| **效果** | 提高首次做对的概率 | 在问题到达人类前自我纠正 |
| **aiops 实例** | 五层 System Prompt、工具描述、安全规则文本 | 安全 Check 拦截、循环检测、连续失败追踪、自适应检查点 |
> 仅有前馈,规则永远无法验证是否奏效;仅有反馈,智能体会重复同样的错误。两者缺一不可。
### 2.2 计算型 vs 推理型
| 类型 | 特点 | 例子 |
|------|------|------|
| **计算型(Computational)** | 确定性、快速、CPU 执行 | 黑名单匹配、命令风险分级、循环检测、token 估算 |
| **推理型(Inferential)** | 语义分析、AI 审查、LLM 调用 | Evolver 用 LLM 提炼经验、LLM 决定下一步工具 |
计算型传感器成本低、速度快,适合每次工具调用都运行(如 `safety.Controller.Check`、`AssessBashRisk`)。推理型传感器更强大但更贵、不确定,适合关键节点(如任务完成后的经验提炼)。
推理型传感器的特别威力:当反馈信号被优化为 LLM 可消费的格式时(如把「连续失败」翻译为「请换一种思路」的中文提示注入对话),形成一种**积极的 prompt injection** —— 这正是 aiops 自适应检查点的设计依据。
### 2.3 Steering Loop 掌舵循环
Steering Loop 是 Harness Engineering 的元过程 —— 通过迭代改进 Harness 来引导智能体。
```
┌──────────┐ Agent 工作 ┌──────────┐ 系统改进 ┌──────────┐
│ Harness │ ──────────► │ 观察问题 │ ──────────► │ 更新Harness│
│ (当前版本) │ │(多次发生) │ │ (新版本) │
└──────────┘ └──────────┘ └──────────┘
▲ │
└────────────────────────────────────────────────────┘
循环往复
```
在 aiops Agent CLI 中,Steering Loop 有两种实现:
1. **人工 Steering**:管理员在「大模型设置」调整 SystemPrompt、温度等 → 影响 Agent 下次行为
2. **自动 Steering**:Evolver 引擎观察任务执行 → 自动学习经验 → 注入下次 Prompt(详见第 08 篇)
后者是 opsxcli 设计思想的核心延伸 —— 用代码自动化 Steering Loop。
### 2.4 Keep Quality Left — 把质量左移
在 aiops Agent CLI 的执行链路中,反馈传感器按成本分布:
| 阶段 | 应运行的传感器 | 特点 |
|------|---------------|------|
| **工具执行前** | 黑名单硬拒、`AssessBashRisk` 分级、安全模式阈值 | 毫秒级,每次必跑 |
| **工具执行中** | context 超时(`ToolTimeout` 120s 兜底) | 防卡死 |
| **轮次间** | 自适应检查点(Round 4/8/12)、循环检测(>3 次拒绝)、连续失败追踪 | 防 LLM 跑偏 |
| **任务完成后** | Evolver 异步学习(30s 超时) | 较贵,异步 |
---
## 3. 三类调节 Harness
> Agent Harness 如同一个控制论调节器(governor),通过前馈和反馈将系统调节到期望状态。
### 3.1 可维护性 Harness(Maintainability)
- 计算型:文件大小红线(Go handler ≤400 行、组件 ≤700 行)、`go vet`、`tsc --noEmit`
- 推理型:代码审查(review)
- 在 Agent CLI 代码中体现为「引擎库零业务依赖」「单文件 ≤300 行」等约束
### 3.2 架构适应度 Harness(Architecture Fitness)
- 前馈:分层依赖约定(controller → model + pkg/agentcli,引擎库不反向 import)
- 反馈:编译失败即拦截(Go 强类型天然传感器)
### 3.3 行为 Harness(Behaviour)— 本项目的重点
如何引导和感知 Agent 是否按预期功能运行:
- 前馈:五层 System Prompt(角色 + 安全规则 + 工作方式 ReAct 范式)
- 反馈:
- 安全层硬拦截 + 高危确认(防破坏性操作)
- ReAct 循环中的自适应检查点(防 LLM 死循环或编造输出)
- WebSocket 事件流的可观测性(人类可随时观察思考与命令)
- Evolver 经验学习(成功的工具序列被强化,失败的被记录为「换法」提示)
---
## 4. Harnessability 与设计启示
### 4.1 可 Harness 性
aiops 后端是 Go 强类型 + 清晰的包边界,天然适合被 Harness:
- 强类型使 LLM 工具的 JSON Schema 可以严格约束参数
- 清晰的 controller / model / pkg 分层使「引擎库零业务依赖」可行
- 复用 aiops 既有的凭证系统、SSH 包、DB,大幅缩减 Harness 代码量
### 4.2 Ashby 必要多样性定律
> 调节器必须拥有至少与被调节系统相同的多样性。
aiops Agent CLI 通过两层多样性缩减:
1. **工具集精简**(6 → 2):只保留 `ssh_execute` + `analyze_output`,把「在远程主机做任何事」收敛为「执行 shell + 分析输出」,让 LLM 的决策空间可控
2. **Prompt 工作方式约束**:明确告诉 LLM「一次只做关键的、可验证的一步」「优先只读命令」「失败要换法重试」
这是把潜在的「无限可能的 Agent」收敛为「可控的运维 Agent」的关键策略。
### 4.3 Harness 模板
aiops Agent CLI 本身就是一个「Web 版运维 Agent Harness 模板」—— 它的前馈(五层 Prompt)、反馈(安全 + 检查点 + Evolver)、工具协议(Tool 接口)可以被视为同类产品的参考实现。
---
## 5. 反馈回路设计
### 5.1 多轮自我评审循环
aiops ReAct 循环本质就是一种多轮自我评审:
```
Round 1:LLM 思考 + 决定工具
│
Round 2:执行工具 → 观察输出(exit_code 是关键信号)
│
Round 3:根据输出判断 → 继续排障或修复
│
═══ 检查点 Round 4 ═══
├─ 若重复调用率 >50% 或连续失败 ≥3 → 注入「换思路」提示
│
Round N:… 直到 LLM 不再返回 tool_calls
│
═══ 检查点 Round 8 ═══ 强制中间总结
═══ 检查点 Round 12 ═══ 敦促收尾
│
最终答案 + Evolver 学习(后台)
```
### 5.2 黄金原则
从 OpenAI Codex 实践提炼的四条原则在 aiops Agent CLI 中的体现:
1. **优先使用共享抽象**:`pkg/remoteexec`(SSH)、`pkg/agentcli/tools`(Tool 接口)、`pkg/agentcli/llm`(Client 接口),将不变式集中管理
2. **不使用「YOLO 式」探测**:所有命令先经 `AssessBashRisk` 评估,黑名单硬拒、高危确认,绝不盲目执行
3. **统一错误处理**:工具返回 `Result{Success, Output, Error}`,错误信息以中文回到对话供 LLM 理解;Evolver 失败优雅降级不阻断主循环
4. **避免全局状态**:`Engine` 每次任务一个实例,`Registry` 通过 `sync.RWMutex` 保护;会话状态走 DB 不走全局变量
---
## 6. aiops Agent CLI 代码对照
### 6.1 前馈指导实例
| 理论概念 | aiops 实现 | 代码位置 |
|----------|-----------|----------|
| 角色与能力引导 | 五层 System Prompt | `pkg/agentcli/prompt/prompt.go` |
| 工具能力披露 | Registry 动态生成工具描述 | `core/agent.go: buildToolDescriptions` |
| 工作范式引导 | ReAct 范式文本(思考-行动-观察-循环-收尾) | `prompt.go` Layer 4 |
| 安全规则文本 | 黑名单 + 优先只读 + 高危确认 | `prompt.go` Layer 2 |
### 6.2 反馈传感器实例
| 理论概念 | aiops 实现 | 代码位置 |
|----------|-----------|----------|
| 计算型反馈(执行前) | 黑名单硬拒 + 风险分级 | `tools/risk.go` + `safety/safety.go: Check` |
| 计算型反馈(轮次间) | 循环检测(同参 >3 拒绝) | `core/agent.go: processToolCalls` |
| 计算型反馈(轮次间) | 连续失败追踪 | `core/agent.go: updateConsecutiveFailures` |
| 结构化反馈 | 自适应检查点(Round 4/8/12 注入提示) | `core/agent.go: checkAdaptiveCheckpoint` |
| 上下文保护 | 消息裁剪 + tool 消息完整性修复 | `core/agent.go: trimMessages` + `ensureToolMessageIntegrity` |
| 安全反馈 | 三级模式阈值 + 高危确认 | `safety/safety.go` |
### 6.3 Steering Loop 实例
aiops 的 **Evolver 自进化引擎**是自动 Steering Loop 的代码级实现:
```
EvolverEngine (pkg/agentcli/evolver/)
│
├── engine.go 精简进化循环(OBSERVE→CLASSIFY→LEARN→STORE→MEMORIZE→PREDICT)
└── memory.go 四层记忆(experience/factual/procedural/environment)
```
每次任务执行后,Evolver 异步学习并存储经验;下次任务开始时,`GetContextForPrompt()` 把相关历史经验注入 System Prompt 的 Layer 5 —— 这就是「观察问题 → 改进 Harness」的自动化实现。
### 6.4 品味不变式实例
| 类别 | 不变式 | 理由 |
|------|--------|------|
| 语言 | 注释和错误信息使用中文,署名 `// Logan.Li` | 项目规范 |
| 命名 | 导出函数 PascalCase,包名全小写 | Go 标准 |
| 错误 | `Result.Error` 携带中文错误,Evolver 失败降级不 panic | 保留错误链,不阻断主流程 |
| 依赖 | controller → model + pkg/agentcli(单向) | 引擎库零业务依赖 |
| 日志 | 引擎 emit Event,由 controller 决定落库还是推 WS | 引擎不直接依赖 DB/WS |
| 安全 | 黑名单硬拒不可被确认放行 | 安全红线 |
---
## 7. Web 可观测性
> opsxcli 是 CLI 工具,可观测性 = slog + 审计日志 + `--debug`。
> aiops Agent CLI 是 Web 服务,可观测性 = **WebSocket 事件流 + DB 持久化 + 会话历史回放**。
### 7.1 事件流(实时)
引擎通过 `EventCallback` 推送结构化事件,controller 序列化为 `wsEvent` 后推 WebSocket:
| Event.Type | 含义 | 前端渲染 |
|------------|------|----------|
| `progress` | 轮次 + token | 顶部进度条 |
| `thought` | AI 思考文本 | 思考气泡 |
| `tool_call` | 即将执行的工具 + 风险 | 工具调用卡片(含风险标签) |
| `tool_result` | 工具输出 + 成败 + 耗时 | 工具结果卡片 |
| `confirm` | 高危确认请求 | useConfirm 弹框 |
| `done` | 最终答案 | 完成气泡 |
| `error` | 错误(含达到最大轮次) | 错误气泡 |
### 7.2 持久化(事后审计)
每条事件(除 progress)落库 `agent_cli_messages`,前端「会话历史」页面可按 session_id 回放完整 ReAct 推理过程:
```go
// controller/agent_cli.go: persistMsg
// 每条消息记录:round / type / role / content / tool_name / tool_args /
// tool_output / risk / success / duration / tokens
```
> 与 opsxcli 的差异:opsxcli 把审计日志写到 `~/.opsxcli/audit/audit.log`(文件),aiops 直接走 DB(多用户、可查询、可关联用户与主机)。
---
## 8. 参考资源
| 资源 | 说明 |
|------|------|
| [Martin Fowler — Harness Engineering](https://martinfowler.com/articles/harness-engineering.html) | 理论框架,控制论视角 |
| [OpenAI — Harness Engineering](https://openai.com/zh-Hans-CN/index/harness-engineering) | 实践报告,Codex 实战经验 |
| [LangChain — Agent = Model + Harness](https://blog.langchain.com/the-anatomy-of-an-agent-harness/) | 术语定义 |
| [Anthropic — Effective Harnesses for Long-Running Agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents) | 长时间运行 Agent 的 Harness 设计 |
> 作者:Logan.Li
+177
View File
@@ -0,0 +1,177 @@
# 04 - 插件架构设计
> 本文由 opsxcli 同名设计文档适配改写,结合 aiops Agent CLI 的 Web 实现(`backend/pkg/agentcli/`)。
> opsxcli 的「plugins/」CLI 插件层(mysql/ssh/ping 等 46+ 命令)在 aiops 中不适用 —— Web 版聚焦远程运维,工具精简为 2 个并改由 `tools.Tool` 接口承载。
## 工具(Tool)= aiops 版的「插件」
opsxcli 有完整的 CLI 插件生态(46+ 命令插件);aiops Agent CLI 把「插件」概念收敛为更纯粹的 **Tool 接口**:
- 不再有 CLI 命令、参数解析、帮助文本
- 工具的唯一职责是:被 LLM 调用 → 在目标主机做事 → 返回结构化结果
- LLM 通过 function calling 协议感知工具的存在与用法
> 这是合理的精简:Web Agent 的能力边界由「Tool 接口 + LLM 推理」共同决定,不再需要 CLI 命令那一层。
## Tool 接口(5 方法)
```go
// backend/pkg/agentcli/tools/registry.go
type Tool interface {
Name() string // 工具名(LLM 调用时用)
Description() string // 给 LLM 看的描述
Parameters() map[string]interface{} // JSON Schema 参数定义
RiskLevel() RiskLevel // 静态风险等级
Execute(ctx context.Context, args map[string]interface{}) (*Result, error)
}
```
设计哲学:**接口最小化** —— 5 个方法覆盖工具的完整生命周期。
### DynamicRiskTool 扩展接口
```go
type DynamicRiskTool interface {
Tool
RiskLevelForArgs(args map[string]interface{}) RiskLevel
}
```
部分工具的「静态风险」无法反映真实风险(如 `ssh_execute` 静态是 High,但 `df -h` 是 Safe、`rm -rf /` 是 Critical)。这些工具额外实现 `DynamicRiskTool`,安全控制器 `Check` 时优先调用动态评估。
### Result 结构
```go
type Result struct {
Success bool `json:"success"`
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
```
## Registry 注册中心
```go
type Registry struct {
tools map[string]Tool
mu sync.RWMutex // 读写锁,支持并发注册和查询
}
```
核心方法:
| 方法 | 说明 |
|------|------|
| `NewRegistry()` | 创建空注册中心 |
| `Register(tool)` | 注册工具(线程安全) |
| `Get(name)` | 按名获取工具(LLM 调用时按名查找) |
| `List()` | 列出所有工具(构建 Prompt Layer 3 工具描述) |
| `ToLLMTools()` | 转换为 OpenAI function calling 格式(自动注入到 LLM `tools` 参数) |
`ToLLMTools()` 是 Registry 与 LLM 协议的桥梁:注册的工具自动出现在 LLM 的 function 列表中,无需手动维护。
## 已注册工具一览
> 与 opsxcli 的关键差异:opsxcli 有 6 个工具(local_bash / ssh_execute / scp_transfer / file_read / file_search / analyze_output),aiops 精简为 2 个。
| 工具 | 风险等级 | 功能 | 来源 |
|------|----------|------|------|
| `ssh_execute` | High(静态)/ Safe~Critical(动态) | 在目标主机远程执行 shell 命令 | aiops 既有 `pkg/remoteexec` |
| `analyze_output` | Safe | 对冗长输出做摘要/错误提取 | 内置 Go 实现 |
### 为什么精简?
| opsxcli 工具 | aiops 处理 | 原因 |
|--------------|-----------|------|
| `local_bash` | **剥离** | Web 后端不应在服务器本地执行命令(安全风险 + 多用户隔离) |
| `scp_transfer` | **剥离** | 文件传输走 aiops 既有的「文件管理」模块,不混入 Agent |
| `file_read` | **剥离** | 远程读文件用 `ssh_execute cat`,无需独立工具 |
| `file_search` | **剥离** | 远程搜文件用 `ssh_execute grep/find` |
| `ssh_execute` | **保留** | 核心远程运维能力 |
| `analyze_output` | **保留** | 防止长输出 token 爆炸,辅助 LLM 聚焦关键信息 |
工具越少,LLM 的决策空间越收敛(呼应 Ashby 定律),首次做对的概率越高。
## 工具分类标准(aiops 版)
### 放入内置工具集的条件(需同时满足)
1. 属于运维 Agent 高频需要的能力
2. 无法用现有工具简单组合完成(如 `analyze_output` 不能用 `ssh_execute` 替代 —— 它操作的是已有文本而非远程命令)
3. 实现简洁(单文件 < 300 行)
### 通过 `ssh_execute` 间接覆盖的能力
opsxcli 用独立插件实现的能力,在 aiops 中全部由 `ssh_execute` 执行远程命令覆盖:
| opsxcli 插件能力 | aiops 等效 |
|------------------|-----------|
| `mysql -e "..."` | `ssh_execute "mysql -e \"...\""` |
| `redis-cli ...` | `ssh_execute "redis-cli ..."` |
| `ping / traceroute` | `ssh_execute "ping ..."` |
| `cat / grep / find` | `ssh_execute "cat ..."` |
| `systemctl status nginx` | `ssh_execute "systemctl status nginx"` |
LLM 会根据任务自主决定执行什么命令,无需为每类命令提供独立工具。
## 混合架构的好处
1. **职责单一**:每个工具只做一件事,`ssh_execute` 执行、`analyze_output` 分析
2. **协议简单**:LLM 只需理解 2 个 function,决策快、错误少
3. **风险可控**:所有远程操作统一走 `ssh_execute` 的风险评估(`AssessBashRisk`),不会因为工具增多而出现安全盲区
4. **扩展灵活**:未来若需要新工具(如 `kubectl_apply`),实现 Tool 接口注册即可
## 扩展指南
### 添加新工具
1. 实现 `Tool` 接口(必要时实现 `DynamicRiskTool`):
```go
// pkg/agentcli/tools/kubectl_get.go
type KubectlGetTool struct{ cfg remoteexec.SSHConfig }
func (t *KubectlGetTool) Name() string { return "kubectl_get" }
func (t *KubectlGetTool) Description() string {
return "在目标主机执行 kubectl get 并返回结果"
}
func (t *KubectlGetTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"resource": map[string]interface{}{"type": "string", "description": "如 pods/deployments"},
},
"required": []string{"resource"},
}
}
func (t *KubectlGetTool) RiskLevel() tools.RiskLevel { return tools.RiskSafe }
func (t *KubectlGetTool) Execute(ctx context.Context, args map[string]interface{}) (*tools.Result, error) {
// 复用 remoteexec 执行 kubectl get
}
```
2. 在 `agentcli.NewEngine` 中注册:
```go
registry.Register(tools.NewSSHExecuteTool(cfg.SSH, coreCfg.ToolTimeout))
registry.Register(tools.NewAnalyzeOutputTool())
registry.Register(tools.NewKubectlGetTool(cfg.SSH)) // 新增
```
3. 工具自动出现在 LLM 的 function 列表与 Prompt Layer 3 工具描述中。无需改 Prompt、controller、前端。
## 常见陷阱
| 陷阱 | 解决方案 |
|------|----------|
| 忘记设置 `RiskLevel` 导致误判 | 默认按接口返回值,高危工具必须显式声明 |
| 高危工具未实现 `DynamicRiskTool` | 静态等级无法反映参数差异(如 `ssh_execute` 看 `df` 还是 `rm -rf`),必须实现动态评估 |
| 工具 Execute 无超时 | `Config.ToolTimeout=120s` 兜底(`context.WithTimeout`) |
| 工具输出过长导致 token 爆炸 | `OutputMaxLength=10000` 截断 + 引导 LLM 调用 `analyze_output` |
## 总结
aiops Agent CLI 的「插件架构」是 opsxcli 的精简版:从「46+ CLI 插件」收敛为「2 个 Tool 接口实现」。这不是能力的退化,而是**收敛决策空间、聚焦远程运维、降低 LLM 误判概率**的有意设计。详细工具实现见 [05 - Builtin 内置工具](05-Builtin内置命令.md)。
> 作者:Logan.Li
+248
View File
@@ -0,0 +1,248 @@
# 05 - Builtin 内置工具
> 本文由 opsxcli 同名设计文档适配改写,结合 aiops Agent CLI 的 Web 实现(`backend/pkg/agentcli/`)。
> opsxcli 原文讨论「builtin 基础命令(ls/cat/grep 等)的 Go 原生实现 vs 命令转发」抉择;aiops 版剥离了所有本地命令,仅保留 2 个工具:`ssh_execute`(远程执行)+ `analyze_output`(输出分析)。本文聚焦这 2 个工具的设计。
## 设计目标
为 aiops Agent CLI 提供**最小而充分**的工具集:
- `ssh_execute` 覆盖所有「在远程主机做事」的需求(排障、安装、配置、重启)
- `analyze_output` 解决「命令输出过长导致 LLM token 爆炸」的问题
工具数量从 opsxcli 的 6 个精简为 2 个,但能力边界通过 LLM 自主编排命令来扩展,而非通过堆工具。
## 工具 1:ssh_execute(远程命令执行)
### 定位
在目标主机上执行任意 shell 命令,返回合并的 stdout + stderr + exit_code。这是 Agent 真正「动手」的唯一通道。
### 实现
```go
// backend/pkg/agentcli/tools/ssh_execute.go
type SSHExecuteTool struct {
cfg remoteexec.SSHConfig // 目标主机连接配置(含凭据,由 controller 注入)
timeout time.Duration // 默认命令超时(60s)
}
func (t *SSHExecuteTool) Name() string { return "ssh_execute" }
func (t *SSHExecuteTool) Description() string {
return fmt.Sprintf(
"在远程服务器 %s(用户 %s)上执行 shell 命令并返回合并的 stdout+stderr 输出与退出码。"+
"用于排障(查看日志/进程/磁盘/网络)、安装与配置软件、重启服务等。"+
"高危命令(rm -rf /、mkfs、reboot 等)需用户确认。", host, user)
}
func (t *SSHExecuteTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"command": map[string]interface{}{
"type": "string",
"description": "要执行的 shell 命令,例如 df -h、systemctl status nginx、tail -n 100 /var/log/messages",
},
"timeout": map[string]interface{}{
"type": "integer",
"description": "命令超时秒数(默认 60)",
},
},
"required": []string{"command"},
}
}
```
### 风险评估:DynamicRiskTool
`ssh_execute` 是典型「静态等级无法反映真实风险」的工具,因此实现 `DynamicRiskTool`:
```go
// 静态等级:High(保守)
func (t *SSHExecuteTool) RiskLevel() RiskLevel { return RiskHigh }
// 动态等级:看具体命令
func (t *SSHExecuteTool) RiskLevelForArgs(args map[string]interface{}) RiskLevel {
return AssessBashRisk(StringParam(args, "command"))
}
```
| 命令示例 | 动态风险 | 安全模式 balanced 下的行为 |
|----------|----------|--------------------------|
| `df -h` / `ps aux` / `cat /etc/hosts` | Safe | 直接执行 |
| `mkdir /tmp/foo` / `touch x` | Low | 直接执行 |
| `chmod 644 /etc/cfg` / `cp a b` | Medium | strict 需确认,balanced 放行 |
| `systemctl restart nginx` / `kill 1234` | High | 弹框确认 |
| `rm -rf /var/log/*` | Critical | **硬拒**(不可确认放行) |
### Execute 流程
```go
func (t *SSHExecuteTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) {
command := strings.TrimSpace(StringParam(args, "command"))
if command == "" {
return &Result{Success: false, Error: "参数 command 不能为空"}, nil
}
// ... timeout 解析
res, err := remoteexec.Exec(t.cfg, command, timeout)
if err != nil {
return &Result{Success: false, Error: "SSH 连接失败: " + err.Error()}, nil
}
// 关键:附带 exit_code,让 LLM 判断命令是否成功(排障关键信号)
output := res.Output + fmt.Sprintf("[exit_code=%d]", res.ExitCode)
return &Result{Success: res.Success, Output: output, Error: res.Error}, nil
}
```
**设计要点**:
- **复用 aiops 既有 `pkg/remoteexec`**:Go-native SSH,无外部依赖,凭据由 controller 从 `model.Server` 关联凭证系统构建后注入
- **`exit_code` 是关键信号**:LLM 通过 `exit_code` 判断命令是否真正成功(避免 LLM 误以为「输出有内容」就是「执行成功」)
- **连接级失败与命令级失败分离**:SSH 连不上返回 `Error`,命令执行返回 `exit_code != 0`,让 LLM 区分排查方向
- **凭据不暴露给 LLM**:`SSHConfig` 是工具私有的,LLM 只看到 `command` 参数
## 工具 2:analyze_output(输出分析)
### 定位
对一段(可能很长的)命令输出做摘要:统计行数、提取疑似错误/告警行、必要时截断保留首尾。**纯文本处理,不执行任何命令**,因此 `RiskSafe`。
### 为什么需要它?
LLM 上下文窗口有限(即使 128k 模型,超长输出也会显著增加成本和延迟)。`ssh_execute` 单次输出截断在 10000 字符,但即便如此,日志类输出(如 `journalctl -n 1000`)仍可能让 LLM 难以聚焦。
`analyze_output` 让 LLM 自主决定「输出太长时先分析再推理」:
```
Round 3: ssh_execute "journalctl -u nginx -n 500"
→ 输出 500 行
Round 4: analyze_output(output, focus="error")
→ 提取 8 行疑似错误 + 截断保留首尾
Round 5: LLM 基于精炼后的信息推理
```
### 实现
```go
// backend/pkg/agentcli/tools/builtin.go
func (t *AnalyzeOutputTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) {
output := StringParam(args, "output")
focus := strings.ToLower(StringParam(args, "focus"))
// 1. 超长截断:保留前 40 + 后 40 行,中间用省略标注
// 2. 提取疑似错误/告警行(errorKeywords,含中英文)
// - 英文:error / fail / exception / traceback / denied / timeout / refused / critical / fatal
// - 中文:错误 / 失败 / 拒绝 / 超时 / 无法 / 异常
// - 若传 focus,把 focus 作为额外关键词优先匹配
// 3. 最多 20 条疑似错误行
// 4. 组装摘要:总行数 + 截断标注 + 错误行 + 输出片段
}
```
**错误关键词列表**:
```go
errorKeywords := []string{
"error", "fail", "exception", "traceback", "denied",
"permission", "timeout", "refused", "unavailable", "critical", "fatal",
"错误", "失败", "拒绝", "超时", "无法", "异常",
}
```
**输出格式示例**:
```
总行数: 500(已截断保留首尾)
疑似错误/告警行 (8):
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
...
输出片段:
... (前 40 行)
... [已省略 420 行] ...
... (后 40 行)
```
## 工具组合:典型 ReAct 序列
`ssh_execute` 与 `analyze_output` 的组合覆盖绝大多数运维场景:
### 场景 1:磁盘满排障
```
Round 1: ssh_execute "df -h" → 发现 /var 满
Round 2: ssh_execute "du -sh /var/*" → 定位 /var/log 最大
Round 3: ssh_execute "ls -lah /var/log | head -20" → 找到 access.log 巨大
Round 4: 给出清理建议(不直接 rm,让用户确认)
```
### 场景 2:服务启动失败
```
Round 1: ssh_execute "systemctl status nginx" → failed
Round 2: ssh_execute "journalctl -u nginx -n 500" → 500 行日志
Round 3: analyze_output(output, focus="error") → 提取 8 行错误,端口被占
Round 4: ssh_execute "ss -tlnp | grep :80" → 确认 80 端口被占用
Round 5: 给出根因 + 解决建议
```
### 场景 3:性能排查
```
Round 1: ssh_execute "top -bn1 | head -20" → CPU 100% 进程
Round 2: ssh_execute "ps aux --sort=-%cpu | head -10" → 定位 java 进程
Round 3: ssh_execute "jstack <pid>" → 线程栈
Round 4: analyze_output(jstack_output, focus="BLOCKED") → 死锁线索
Round 5: 给出根因(线程死锁)
```
## 工具与 Prompt 的协作
Prompt Layer 2 明确告诉 LLM:
> 排障时优先使用只读命令(df/ps/cat/grep/tail/systemctl status/journalctl)收集信息,确认根因后再动手修复。
Prompt Layer 3 自动列出工具描述(来自 `buildToolDescriptions()`):
```
【可用工具】
- ssh_execute: 在远程服务器 ... 上执行 shell 命令 ...
- analyze_output: 分析一段(可能很长的)命令输出文本 ...
```
工具的命名和描述本身就是给 LLM 的「使用说明书」,描述写得好,LLM 就能正确选用。
## 扩展指南
### 何时应该新增工具(而非用 ssh_execute)?
满足以下任一条件:
1. **命令协议复杂、LLM 容易写错**(如 `kubectl apply -f` 需要 YAML 参数,用 `ssh_execute` 难以表达)
2. **需要结构化输入/输出**(如数据库查询返回表结构)
3. **需要专门的风险评估逻辑**(如 `kubectl delete` 需要按资源类型分级)
如果只是「执行某条命令」,用 `ssh_execute` 即可,不需要新工具。
### 实现步骤
见 [04 - 插件架构设计](04-插件架构设计.md) 的「添加新工具」章节。
## 常见陷阱
| 陷阱 | 解决方案 |
|------|----------|
| `ssh_execute` 输出过长撑爆上下文 | `OutputMaxLength=10000` 截断 + 引导调用 `analyze_output` |
| LLM 把「输出有内容」误判为「命令成功」 | 输出末尾附 `[exit_code=N]`,Prompt 显式提醒 |
| `analyze_output` 关键词覆盖不全 | 扩展 `errorKeywords` 列表(含中英文) |
| 命令超时无反馈 | `context.WithTimeout(ctx, ToolTimeout)` + 失败信息回写 |
| 凭据泄漏给 LLM | `SSHConfig` 是工具私有字段,不在 `Parameters` 中暴露 |
## 总结
aiops Agent CLI 的工具集设计遵循「少即是多」:2 个工具 + LLM 自主编排,覆盖绝大多数远程运维场景。`ssh_execute` 是「手」(动手做事),`analyze_output` 是「放大镜」(聚焦关键信息),二者配合让 Agent 既有行动力又有判断力。
> 作者:Logan.Li
+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
@@ -0,0 +1,301 @@
# 07 - Prompt 构建与记忆注入
> 本文由 opsxcli 同名设计文档适配改写,结合 aiops Agent CLI 的 Web 实现(`backend/pkg/agentcli/prompt/prompt.go`)。
> 五层 System Prompt 架构保留;opsxcli 的「personaLayer / capabilityLayer / constraintLayer / fewShotLayer / memoryLayer」五常量在 aiops 中合并为单个 `BuildSystemPrompt()` 函数;角色从「opsxcli 智能运维超级助手」改写为「aiops Agent CLI 智能运维 Agent(Web 版,类似 Claude Code)」。
## 设计理念
Prompt 是 LLM Agent 的灵魂。aiops Agent CLI 沿用 opsxcli 的 **五层 System Prompt 架构**,将静态知识与动态记忆分层组装:
- **Layer 1-4 静态层**:编译期确定,不随运行时变化(角色、安全、工具、工作方式)
- **Layer 5 动态层**:运行时由 Evolver 注入历史经验,越用越聪明
## 五层 System Prompt 架构
```
┌──────────────────────────────────────────┐
│ Layer 1: 角色定位 │ ← 我是谁
│ 「aiops Agent CLI 智能运维 Agent」 │
├──────────────────────────────────────────┤
│ Layer 2: 安全规则 │ ← 我必须遵守什么
│ 黑名单 / 高危确认 / 优先只读 │
├──────────────────────────────────────────┤
│ Layer 3: 工具说明 │ ← 我能用什么
│ 由 Registry 动态生成(ssh_execute 等) │
├──────────────────────────────────────────┤
│ Layer 4: 工作方式(ReAct 范式) │ ← 我怎么工作
│ 思考 → 行动 → 观察 → 循环 → 收尾 │
├──────────────────────────────────────────┤
│ Layer 5: 记忆注入 │ ← 我学到了什么(动态)
│ Evolver 提供的历史经验 │
└──────────────────────────────────────────┘
```
## BuildSystemPrompt 函数
```go
// backend/pkg/agentcli/prompt/prompt.go
func BuildSystemPrompt(serverInfo, toolDescs, evolveContext string) string {
var sb strings.Builder
// Layer 1: 角色
sb.WriteString("你是 aiops 平台的 Agent CLI —— 一个企业级智能运维 Agent," +
"类似 Claude Code / Codex,但运行在 Web 页面上,通过 SSH 工具远程操作真实服务器。\n" +
"你的职责是:根据用户的自然语言任务描述,自主在目标服务器上排查故障、安装与配置软件、修复问题," +
"并通过 WebSocket 把每一步思考、执行的命令和输出实时反馈给用户。\n\n")
// 目标主机(动态片段,但不属于记忆层)
if strings.TrimSpace(serverInfo) != "" {
sb.WriteString("【目标主机】\n" + serverInfo + "\n\n")
}
// Layer 2: 安全规则
sb.WriteString("【安全规则】\n")
sb.WriteString("1. 破坏性命令(rm -rf /、mkfs、dd 覆写磁盘等)被严格禁止,绝不执行。\n")
sb.WriteString("2. 高危操作(reboot、systemctl restart、kill、卸载软件等)需要用户确认后才能执行。\n")
sb.WriteString("3. 排障时优先使用只读命令(df/ps/cat/grep/tail/systemctl status/journalctl)收集信息...\n")
sb.WriteString("4. 不要执行任何你不确定后果的命令;宁可多问用户,也不盲目操作。\n\n")
// Layer 3: 工具(动态生成)
if strings.TrimSpace(toolDescs) != "" {
sb.WriteString("【可用工具】\n" + toolDescs + "\n\n")
}
// Layer 4: 工作方式(ReAct)
sb.WriteString("【工作方式(ReAct 循环)】\n")
sb.WriteString("1. 思考:分析当前状态,决定下一步...\n")
sb.WriteString("2. 行动:调用 ssh_execute 执行命令(一次只做关键的、可验证的一步)。\n")
sb.WriteString("3. 观察:仔细阅读命令输出(含 exit_code,0 表示成功)。\n")
sb.WriteString("4. 循环:如果命令失败或问题未解决,分析错误原因,换一种方法重试,直到真正解决。\n")
sb.WriteString("5. 收尾:问题解决后,用清晰的中文总结:问题根因、你做了什么、最终结果、后续建议。\n\n")
sb.WriteString("【重要】务必真正解决问题,而不是假装解决。每一步都要基于实际命令输出判断," +
"禁止编造不存在的输出。\n\n")
// Layer 5: 记忆注入(动态)
if strings.TrimSpace(evolveContext) != "" {
sb.WriteString("【历史经验(来自自进化引擎)】\n" + evolveContext + "\n\n")
}
// 输出要求
sb.WriteString("【输出要求】使用简体中文,简洁专业。思考与命令穿插呈现,避免冗长空话。")
return sb.String()
}
```
> 与 opsxcli 的差异:
> - opsxcli 把五层拆为五个独立常量(`personaLayer` / `capabilityLayer` / `constraintLayer` / `fewShotLayer` / `memoryLayerPrefix`),aiops 合并为单个函数,更紧凑
> - opsxcli 有独立的 `BuilderV2` 结构和 `NewBuilderV2WithMemory` 工厂,aiops 直接用函数(Web 场景下不需要构建器状态)
> - aiops 移除了独立的 fewShot 示例层,把 ReAct 工作方式说明合并到 Layer 4
## Layer 1: 角色定位
> **关键改写**:opsxcli 的角色是「opsxcli 智能运维超级助手」(CLI 工具语境);aiops 改写为「aiops 平台的 Agent CLI —— 企业级智能运维 Agent,类似 Claude Code / Codex,但运行在 Web 页面上」。
**设计要点**:
- **明确类比**:用「类似 Claude Code / Codex」让用户和 LLM 都快速理解定位
- **强调 Web 语境**:「运行在 Web 页面上」「通过 SSH 远程操作」「通过 WebSocket 反馈」
- **明确职责边界**:排障、安装配置、修复 —— 不是「无所不能的 AI」
- **不包含版本号**(与 opsxcli 一致)
### 目标主机片段
`serverInfo` 由 controller 构建,注入到 Layer 1 之后:
```go
serverInfo := fmt.Sprintf("主机名: %s | IP: %s:%d | 用户: %s | OS: %s",
ac.server.Hostname, ac.server.IP, ac.server.Port, ac.server.Username, ac.server.OS)
```
让 LLM 知道当前操作的是哪台主机、什么系统、用什么用户。
## Layer 2: 安全规则
四条硬规则:
1. **破坏性命令严格禁止**(呼应代码层的黑名单硬拒)
2. **高危操作需用户确认**(呼应代码层的 ConfirmFn)
3. **排障优先只读命令**(引导 LLM 先收集信息再动手)
4. **不确定时不盲目操作**(宁可多问)
> **注意**:Layer 2 是 Prompt 级别的「软约束引导」,与 `safety.Controller.Check` 的代码级「硬拦截」是不同层面的机制。两者互补:Prompt 引导 LLM 主动避免危险操作,代码层兜底防止 LLM 失控。
## Layer 3: 工具说明
由 `core.Agent.buildToolDescriptions()` 动态生成:
```go
func (a *Agent) buildToolDescriptions() string {
var sb strings.Builder
for _, t := range a.registry.List() {
sb.WriteString(fmt.Sprintf("- %s: %s\n", t.Name(), t.Description()))
}
return sb.String()
}
```
**为什么动态生成?**
- 工具集可扩展(实现 Tool 接口注册即可),硬编码 Prompt 会与代码脱节
- 工具描述本身就是给 LLM 的「使用说明书」,由工具自己提供最准确
- `ssh_execute` 的描述会包含具体的目标主机和用户信息(动态拼接)
输出示例:
```
【可用工具】
- ssh_execute: 在远程服务器 192.168.1.100(用户 root)上执行 shell 命令并返回合并的 stdout+stderr 输出与退出码...
- analyze_output: 分析一段(可能很长的)命令输出文本:统计行数、提取疑似错误/告警行...
```
## Layer 4: 工作方式(ReAct 范式)
五步循环:
1. **思考** —— 分析状态,决定下一步(产出 `thought` 文本)
2. **行动** —— 调用 `ssh_execute` 执行命令(一次只做关键的、可验证的一步)
3. **观察** —— 仔细阅读输出(含 `exit_code`,0 表示成功)
4. **循环** —— 失败则换法重试,直到真正解决
5. **收尾** —— 清晰中文总结(根因、做了什么、结果、建议)
**关键约束**(红线):
> 务必真正解决问题,而不是假装解决。每一步都要基于实际命令输出判断,禁止编造不存在的输出。
这条约束非常重要 —— LLM 有「编造输出」的倾向,明确禁止 + 配合 `exit_code` 信号,能显著降低幻觉。
## Layer 5: 记忆注入(动态)
由 `evolver.EvolverEngine.GetContextForPrompt(query)` 提供:
```go
func (e *EvolverEngine) GetContextForPrompt(query string) string {
taskType := classifyTask(query, nil)
var sb strings.Builder
// 同类任务的历史经验(优先成功)
if exps := e.mem.FindExperiences(taskType, 3); len(exps) > 0 {
sb.WriteString(fmt.Sprintf(" 【%s 类历史经验】\n", taskType))
for _, exp := range exps {
mark := "成功"
if !exp.Success { mark = "未成功" }
sb.WriteString(fmt.Sprintf(" - (%s) %s\n", mark, exp.Hint))
}
}
// 相关事实
if facts := e.mem.RelevantFacts(query, 3); len(facts) > 0 {
sb.WriteString(" 【已知事实】\n")
for _, f := range facts {
sb.WriteString(fmt.Sprintf(" - %s\n", f.Fact))
}
}
// 已知主机
if hosts := e.mem.Hosts(); len(hosts) > 0 {
sb.WriteString(fmt.Sprintf(" 【已知主机】%s\n", strings.Join(hosts, ", ")))
}
return sb.String()
}
```
注入示例:
```
【历史经验(来自自进化引擎)】
【磁盘空间 类历史经验】
- (成功) 完成「磁盘空间」类任务时,工具序列 ssh_execute → analyze_output 最终成功,可作为参考。
- (成功) 对于磁盘满问题,先检查大日志文件再清理
【已知事实】
- 生产 nginx 日志目录是 /var/log/nginx
【已知主机】192.168.1.10, 192.168.1.11
```
**设计要点**:
- 按 `query` 分类后只注入**同类**经验,避免无关信息污染
- 优先注入**成功**经验(`FindExperiences` 内部排序)
- 限制条数(经验 3 条、事实 3 条),控制 token 开销
> 与 opsxcli 的差异:opsxcli 把记忆层细分为 5.0 事实 / 5.1 经验 / 5.1.5 技能 / 5.2 环境 / 5.3 偏好;aiops 精简为 3 段(经验 / 事实 / 已知主机),剥离了用户偏好(Web 场景下偏好由安全模式等显式配置承载)和技能层(未实现自动 Skill 提炼)。
## 注入流程
```
core.Agent.Run()
│
├─ evolveCtx = evolver.GetContextForPrompt(query) ← Layer 5 内容
│
├─ toolDescs = buildToolDescriptions() ← Layer 3 内容
│
├─ systemPrompt = prompt.BuildSystemPrompt(
│ serverInfo, ← Layer 1 目标主机片段
│ toolDescs, ← Layer 3
│ evolveCtx, ← Layer 5
│ )
│
└─ messages = [{system: systemPrompt}, {user: query}]
```
## 为什么 Layer 1-4 是静态的?
- 角色定位和工作方式在编译期确定
- 安全规则不应该被动态修改(防止 Evolver 污染安全约束)
- 静态内容可以利用 LLM Provider 的 Prompt Caching(减少 token 消耗,部分 Provider 如 Anthropic 支持)
- 只有事实/经验需要动态更新
## 输出要求
最后一段统一约束输出风格:
> 使用简体中文,简洁专业。思考与命令穿插呈现,避免冗长空话。
这与 opsxcli 一致 —— 中文运维语境的统一规范。
## 扩展指南
### 修改角色描述
直接修改 `BuildSystemPrompt` 中 Layer 1 的字符串。注意:
- 不要包含版本号
- 明确能力边界(不要吹成「无所不能」)
- 场景化描述优于抽象形容
### 增加新的 Prompt 层
1. 在 `BuildSystemPrompt` 中增加新的 `sb.WriteString(...)` 片段
2. 在本文档更新架构图
3. 若是动态层,需在 `Agent.Run` 中准备数据并传入
### 调整记忆注入策略
修改 `evolver.GetContextForPrompt`:
- 增加新的记忆段(如「最近失败的任务」)
- 调整条数限制
- 改进分类匹配逻辑
## 常见陷阱
| 陷阱 | 解决方案 |
|------|----------|
| Prompt 过长导致 token 超限 | Layer 1-4 精简 + Layer 5 按需注入 + 条数限制 |
| 记忆注入了无关经验 | `GetContextForPrompt` 按 `task_type` 匹配,不全量注入 |
| Layer 1-4 被 Evolver 污染 | 静态层硬编码在函数中,Evolver 只能影响 Layer 5 |
| LLM 编造命令输出 | Layer 4 显式禁止 + 输出附 `exit_code` 验证 |
| 角色描述含版本号 | 已清理,禁止再加版本号 |
| 工具描述与实际工具脱节 | Layer 3 由 Registry 动态生成,永远一致 |
## 与 opsxcli 的差异总结
| 维度 | opsxcli | aiops |
|------|---------|-------|
| 代码组织 | 5 个独立常量 + BuilderV2 结构 | 单个 `BuildSystemPrompt` 函数 |
| 角色 | opsxcli 智能运维超级助手(CLI) | aiops Agent CLI(Web 版,类比 Claude Code) |
| fewShot 层 | 独立 Layer 4(运维场景示例) | 合并进 Layer 4 工作方式 |
| 记忆子层 | 5 个(事实/经验/技能/环境/偏好) | 3 个(经验/事实/已知主机) |
| 静态/动态分离 | 严格(BuilderV2.enableMemory 开关) | 严格(函数内 if 判断) |
思想完全保留(五层架构 + 静态动态分离 + 记忆按需注入),实现更紧凑。
> 作者:Logan.Li
+515
View File
@@ -0,0 +1,515 @@
# 08 - Evolver 自进化引擎
> 本文由 opsxcli 同名设计文档适配改写,结合 aiops Agent CLI 的 Web 实现(`backend/pkg/agentcli/evolver/`)。
> **保留**:四层记忆架构(经验/环境/事实/程序)+ 进化循环思想(OBSERVE → ... → PREDICT)+ 异步非阻塞 + 优雅降级。
> **精简**:opsxcli 13 步循环 + 3300 行精简为 6 步核心循环 + ~400 行;剥离反射评分、经验整合去重、自动 Skill 提炼等重型子流程。这是有意的精简适配,保留学习能力和注入机制。
## 包位置
```
backend/pkg/agentcli/evolver/
├── engine.go # 进化引擎核心(精简进化循环)
└── memory.go # 四层记忆 + JSONL/JSON 文件存储
```
## 设计哲学
> **让 Agent 越用越聪明** —— 每次任务完成后自动学习经验,优化后续表现。
Evolver 借鉴 opsxcli 的设计理念:通过经验积累、LLM 反射分析和环境自适应,实现 Agent 的持续自我优化。用户使用越多,Agent 越了解服务器环境、最佳工具组合和已知事实。
**精简原则**:保留「学习 → 存储 → 注入」的核心闭环,剥离重型分析(评分、整合、Skill 提炼)。精简后的 Evolver 仍是真正的自学习引擎,而非装饰品 —— 它真的会把上次任务的经验存下来,并在下次同类任务时注入 Prompt。
## 核心数据结构
### EvolverEngine
```go
// backend/pkg/agentcli/evolver/engine.go
type EvolverEngine struct {
mem *Memories // 四层记忆集合
llm llm.Client // 可选,用于提炼经验/事实(nil 时退化为规则学习)
memDir string // 记忆目录(默认 data/agentcli)
}
```
> 与 opsxcli 的差异:剥离了 `minSteps` / `simpleThresh` / `moderateThresh` / `onProgress` 等配置字段;剥离 `reflectionClient` 独立接口(直接用 `llm.Client`)。
### TaskExecution(任务执行记录)
每次任务完成后由 `core.Agent.triggerEvolve` 构造:
```go
type TaskExecution struct {
Query string // 用户原始查询
ToolCalls []ToolCallRecord // 工具调用序列
TotalSteps int // 总迭代步数
TotalTokens int // 消耗的 token 数
Duration time.Duration // 执行耗时
Success bool // 是否成功
FinalAnswer string // 最终回答
Timestamp time.Time // 执行时间
ServerHost string // 目标主机(用于环境记忆)
}
```
### ToolCallRecord / EvolveResult
```go
type ToolCallRecord struct {
ToolName string
Args map[string]interface{}
Output string
Duration time.Duration
Success bool
RiskLevel int
}
type EvolveResult struct {
TaskType string // 自动分类的任务类型
LearnedHint string // 学到的经验提示
ToolSequence []string // 工具序列
ExperienceAdded bool // 是否新增经验
Consolidated bool // 是否触发整合(aiops 版保留字段,未实现)
}
```
## 精简进化循环(6 步)
opsxcli 是 13 步(OBSERVE → EXTRACT → REFLECT → SCORE → COMPARE → LEARN → STORE → CONSOLIDATE → PREDICT → ADAPT → MEMORIZE → DISTILL → FEEDBACK),aiops 精简为 6 步核心:
```
┌─────────────────────────────────────────────┐
│ Evolve 6 步核心循环 │
├─────────────────────────────────────────────┤
│ │
│ Step 1: OBSERVE ── 观察工具序列 │
│ ↓ 提取 ToolSequence(如 ssh → analyze) │
│ │
│ Step 2: CLASSIFY ── 任务类型分类 │
│ ↓ 关键词规则匹配(11 类,无需 LLM) │
│ │
│ Step 3: LEARN ── 生成经验提示 │
│ ↓ LLM 优先提炼一句经验,失败降级规则生成 │
│ │
│ Step 4: STORE ── 存入经验 + 环境记忆 │
│ ↓ experience.jsonl + environment.json │
│ │
│ Step 5: MEMORIZE ── LLM 提炼持久事实 │
│ ↓ 异步,从 FinalAnswer 提炼事实存 factual │
│ │
│ Step 6: PREDICT ── 注入下次 Prompt │
│ ↓ GetContextForPrompt(下次任务开始时) │
│ │
└─────────────────────────────────────────────┘
```
### 各步骤详解
#### Step 1: OBSERVE(观察)
提取工具序列:
```go
for _, tc := range exec.ToolCalls {
result.ToolSequence = append(result.ToolSequence, tc.ToolName)
}
```
输出如 `["ssh_execute", "analyze_output", "ssh_execute"]`。
#### Step 2: CLASSIFY(任务分类)
关键词规则匹配(不调 LLM,零成本):
```go
func classifyTask(query string, calls []ToolCallRecord) string {
text := strings.ToLower(query)
for _, c := range calls {
if cmd, ok := c.Args["command"].(string); ok {
text += " " + strings.ToLower(cmd)
}
}
// 11 类规则匹配
rules := []struct{ typ string; kws []string }{
{"磁盘空间", []string{"disk", "空间", "df", "du", "inode", "磁盘", "满了"}},
{"网络", []string{"network", "网络", "ping", "netstat", "ss ", "端口", "tcp", "dns"}},
{"服务管理", []string{"service", "服务", "systemctl", "nginx", "mysql", "redis", "restart"}},
{"软件安装", []string{"install", "安装", "yum", "apt", "docker", "部署", "deploy"}},
{"进程排查", []string{"process", "进程", "ps ", "kill", "cpu", "top", "卡死"}},
{"内存排查", []string{"memory", "内存", "mem", "free", "oom", "swap"}},
{"日志排查", []string{"log", "日志", "journalctl", "tail", "grep"}},
{"权限问题", []string{"permission", "权限", "chmod", "chown", "denied", "拒绝"}},
{"性能调优", []string{"performance", "性能", "负载", "load", "慢", "slow"}},
{"配置管理", []string{"config", "配置", "conf", "vim", "sed"}},
}
// ... 命中即返回,否则 "通用运维"
}
```
**设计要点**:把 query + 实际命令拼起来分类,比单看 query 更准(用户问「检查 nginx」,实际命令可能是 `systemctl status nginx`,分类为「服务管理」更合理)。
#### Step 3: LEARN(生成经验提示)
LLM 优先,失败降级规则:
```go
func (e *EvolverEngine) learnHint(ctx context.Context, exec *TaskExecution, taskType string) string {
tools := strings.Join(uniqueStrings(resultToolNames(exec.ToolCalls)), " → ")
// 尝试用 LLM 提炼一句经验
if e.llm != nil {
if hint := e.llmLearnHint(ctx, exec, taskType, tools, exec.Success); hint != "" {
return hint
}
}
// 降级:规则生成
if exec.Success {
return fmt.Sprintf("完成「%s」类任务时,工具序列 %s 最终成功,可作为参考。", taskType, tools)
}
return fmt.Sprintf("尝试「%s」类任务时,工具序列 %s 未成功,下次可换用不同命令或思路。", taskType, tools)
}
```
LLM 提炼的 prompt(中文):
```
你是运维经验提炼器。根据以下任务执行记录,用一句中文(不超过80字)提炼经验提示,
说明下次做同类任务的关键要点或推荐工具顺序。只返回经验文本,不要多余解释。
任务: <query>
工具序列: <tools>
结果: 成功/失败
```
**降级策略**:LLM 不可用或超时 → 用规则模板生成(仍然有价值,只是不够精炼)。
#### Step 4: STORE(存储经验 + 环境记忆)
```go
exp := ExperienceEntry{
TaskType: result.TaskType,
Query: truncate(exec.Query, 200),
Hint: result.LearnedHint,
ToolSequence: result.ToolSequence,
Success: exec.Success,
Timestamp: exec.Timestamp.Unix(),
}
e.mem.AddExperience(exp)
if exec.ServerHost != "" {
e.mem.AddHost(exec.ServerHost) // 环境记忆
}
```
写入 `data/agentcli/experience.jsonl`(追加)和 `environment.json`(去重合并)。
#### Step 5: MEMORIZE(LLM 提炼持久事实)
仅在任务成功 + 有 FinalAnswer + LLM 可用时触发,**异步执行不阻塞**:
```go
if e.llm != nil && exec.Success && exec.FinalAnswer != "" {
go e.memorizeFact(ctx, exec) // 异步
}
```
LLM 提炼 prompt:
```
从以下运维任务记录中,提炼 1 条值得长期记住的事实(如某主机的固定配置、已知问题、约定)。
如果没有明显可沉淀的事实,回复「无」。只返回事实本身,一句话。
任务: <query>
结论: <final_answer>
```
提炼出的事实存入 `factual.jsonl`。
> 与 opsxcli 的差异:opsxcli 的 MEMORIZE 从执行过程中提取结构化事实(key-value),aiops 简化为让 LLM 自由提炼一句事实。更简单,覆盖面更广。
#### Step 6: PREDICT(注入下次 Prompt)
`GetContextForPrompt(query)` 在下次任务开始时被 `core.Agent.Run` 调用:
```go
evolveCtx := a.evolver.GetContextForPrompt(query)
systemPrompt := prompt.BuildSystemPrompt(serverInfo, toolDescs, evolveCtx)
```
注入内容会被放入 Prompt Layer 5(详见 [07 - Prompt 构建](07-Prompt构建与记忆注入.md))。
## 四层记忆系统
aiops 保留 opsxcli 的四层记忆架构:
```
┌──────────────────────────────────────────────────────────────┐
│ 四层记忆架构 │
├──────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: ExperienceMemory(经验层) │
│ 任务完成后的经验,按 task_type 分类 │
│ 存储: data/agentcli/experience.jsonl │
│ │
│ Layer 2: EnvironmentMemory(环境层) │
│ 已知主机列表 │
│ 存储: data/agentcli/environment.json │
│ │
│ Layer 3: FactualMemory(事实层) │
│ LLM 提炼的持久事实 │
│ 存储: data/agentcli/factual.jsonl │
│ │
│ Layer 4: ProceduralMemory(程序层) │
│ 程序性知识(接口位,未实现自动 Skill 提炼) │
│ 存储: data/agentcli/procedural.jsonl │
│ │
└──────────────────────────────────────────────────────────────┘
```
### Memories 结构
```go
// backend/pkg/agentcli/evolver/memory.go
type Memories struct {
dir string
mu sync.Mutex
expPath string // experience.jsonl
facPath string // factual.jsonl
sklPath string // procedural.jsonl
envPath string // environment.json
}
```
### ExperienceEntry / FactEntry / SkillEntry
```go
type ExperienceEntry struct {
TaskType string `json:"task_type"`
Query string `json:"query"`
Hint string `json:"hint"`
ToolSequence []string `json:"tool_sequence"`
Success bool `json:"success"`
Timestamp int64 `json:"timestamp"`
}
type FactEntry struct {
Fact string `json:"fact"`
Source string `json:"source"`
Timestamp int64 `json:"timestamp"`
}
type SkillEntry struct {
Name string `json:"name"`
When string `json:"when"`
Steps []string `json:"steps"`
Timestamp int64 `json:"timestamp"`
}
```
> 与 opsxcli 的差异:
> - opsxcli 的 `Experience` 有 `SuccessRate` / `UsageCount` / `Tags`(用于整合去重),aiops 简化为 `Success` 布尔(剥离整合逻辑)
> - opsxcli 的 `FactEntry` 是 key-value(`Key` / `Value` / `Category`),aiops 简化为单字段 `Fact`(一句话)
> - opsxcli 的 `SkillEntry` 有完整的 ID/Version/Triggers/Pitfalls/SuccessRate,aiops 简化为 Name/When/Steps(接口位,未实际使用)
### 存储格式
#### experience.jsonl(追加写入)
```json
{"task_type":"磁盘空间","query":"检查磁盘空间","hint":"对于磁盘满问题,先检查大日志文件再清理","tool_sequence":["ssh_execute","analyze_output"],"success":true,"timestamp":1748000000}
```
#### environment.json(整体读写)
```json
{
"hosts": ["192.168.1.10", "192.168.1.11"]
}
```
#### factual.jsonl(追加写入)
```json
{"fact":"生产 nginx 日志目录是 /var/log/nginx","source":"检查 nginx 启动失败","timestamp":1748000000}
```
### 核心查询方法
| 方法 | 说明 |
|------|------|
| `AddExperience(e)` | 追加经验 |
| `FindExperiences(taskType, n)` | 按类型查找,**优先成功**,取最近 n 条 |
| `AddFact(f)` | 追加事实 |
| `RelevantFacts(query, n)` | 按 query 关键词匹配事实(双向 Contains) |
| `AddHost(host)` | 记录已知主机(去重) |
| `Hosts()` | 已知主机列表 |
| `CountStats()` | 各层条目数(供审计展示) |
**`FindExperiences` 的排序策略**:
```go
// 优先成功经验,按时间取最近 n 条
successFirst := ... // 过滤 success=true
others := ... // 过滤 success=false
out := append(pick(successFirst), pick(others)...)
```
让 LLM 优先看到成功的经验。
## 与 Agent 的集成
### 初始化与优雅降级
在 `agentcli.NewEngine` 中:
```go
ev, err := evolver.NewEvolverEngine(coreCfg.MemoryDir, client)
if err != nil {
ev = nil // 降级:无自学习,不影响主流程
}
agent := core.NewAgent(client, registry, coreCfg, sc, ev)
```
**降级场景**:
- 记忆目录无法创建(权限问题)→ `ev = nil`
- 此时 Agent 正常运行,只是 Layer 5 为空
### 异步进化流程
```
用户查询 → core.Agent.Run() ReAct 循环 → 任务完成
↓
triggerEvolve()(异步 goroutine)
↓
30s 超时上下文 + panic 恢复
↓
EvolverEngine.Evolve()
↓
存入 experience.jsonl / factual.jsonl
↓
下次 Run() → GetContextForPrompt() → 注入 Layer 5
```
**三层保护**:
1. **后台 goroutine**:不阻塞用户,答案立即返回
2. **30 秒超时**:`context.WithTimeout(ctx, 30*time.Second)`
3. **panic 恢复**:`defer func() { _ = recover() }()`
### 经验注入
下次任务开始时,`GetContextForPrompt(query)` 按 `task_type` 取相关经验注入 Layer 5:
```
【历史经验(来自自进化引擎)】
【磁盘空间 类历史经验】
- (成功) 对于磁盘满问题,先检查大日志文件再清理
- (成功) 完成「磁盘空间」类任务时,工具序列 ssh_execute → analyze_output 最终成功
【已知事实】
- 生产 nginx 日志目录是 /var/log/nginx
【已知主机】192.168.1.10, 192.168.1.11
```
## 任务分类系统(11 类)
`classifyTask` 把用户查询 + 实际命令分类为 11 类:
| 任务类型 | 关键词(部分) |
|----------|---------------|
| 磁盘空间 | disk / 空间 / df / du / inode / 磁盘 / 满了 |
| 网络 | network / 网络 / ping / netstat / ss / 端口 / tcp / dns |
| 服务管理 | service / 服务 / systemctl / nginx / mysql / redis / restart |
| 软件安装 | install / 安装 / yum / apt / docker / 部署 |
| 进程排查 | process / 进程 / ps / kill / cpu / top / 卡死 |
| 内存排查 | memory / 内存 / mem / free / oom / swap |
| 日志排查 | log / 日志 / journalctl / tail / grep |
| 权限问题 | permission / 权限 / chmod / chown / denied / 拒绝 |
| 性能调优 | performance / 性能 / 负载 / load / 慢 / slow |
| 配置管理 | config / 配置 / conf / vim / sed |
| 通用运维 | (兜底) |
## 创建方式
```go
// 基础引擎(无 LLM 提炼,纯规则学习)
engine, _ := evolver.NewEvolverEngine("data/agentcli", nil)
// 带 LLM 提炼的引擎(推荐)
engine, _ := evolver.NewEvolverEngine("data/agentcli", llmClient)
```
> 与 opsxcli 的差异:opsxcli 有 `NewEvolverEngine` 和 `NewEvolverEngineWithLLM` 两个工厂;aiops 合并为一个,通过 `client` 参数是否为 nil 区分。
## 统计与监控
```go
func (e *EvolverEngine) GetStats() map[string]interface{} {
return map[string]interface{}{
"enabled": true,
"memory": e.mem.CountStats(), // {experience: N, factual: N, skill: N, hosts: N}
}
}
```
由 `Engine.EvolveStats()` 暴露,供审计展示。
## 精简决策说明
### 剥离了什么
| opsxcli 步骤 | aiops 处理 | 原因 |
|--------------|-----------|------|
| Step 2.5 REFLECT(LLM 反射分析) | 合并进 Step 3 LEARN | 单次 LLM 调用同时做「分析 + 提炼」更高效 |
| Step 3 SCORE(效率评分) | 剥离 | 评分用于整合决策,aiops 不做整合 |
| Step 4 COMPARE(历史对比) | 简化为 `FindExperiences` 查询 | 不需要复杂相似度计算 |
| Step 7 CONSOLIDATE(经验整合) | 剥离 | JSONL 追加 + 查询时去重足够,不需要主动合并 |
| Step 9.7 DISTILL(自动 Skill 提炼) | 剥离 | Skill 提炼需要复杂条件判断 + 文件管理,价值/复杂比不高 |
| Step 10 FEEDBACK(用户反馈消息) | 剥离 | Web 场景下经验学习对用户透明,不需要弹消息 |
### 保留了什么
- **四层记忆架构** —— 思想完整保留,只是程序层未实际使用
- **OBSERVE → LEARN → STORE → MEMORIZE → PREDICT 闭环** —— 真正的学习与注入
- **LLM 提炼 + 规则降级** —— 学习质量有保障
- **异步非阻塞 + 优雅降级** —— 工程可靠性
- **按 task_type 分类注入** —— 避免无关污染
精简后的 Evolver 仍是「真正的自学习引擎」,而非装饰品。
## 设计亮点
1. **异步非阻塞**:进化在后台 goroutine,不增加用户等待
2. **优雅降级**:LLM 失败退化为规则学习,Evolver 初始化失败 Agent 正常运行
3. **四层记忆**:经验 + 环境 + 事实 + 程序,覆盖从瞬时到持久的认知链路
4. **会话级反馈**:上次任务的经验立即注入下次 Prompt
5. **线程安全**:`Memories.mu sync.Mutex` 保护文件读写
6. **零 DB 依赖**:纯文件存储,与引擎库「零业务依赖」原则一致
## 常见陷阱
| 陷阱 | 解决方案 |
|------|----------|
| 记忆目录权限不足 | `NewEvolverEngine` 返回 error,`NewEngine` 把 ev 设为 nil 降级 |
| LLM 提炼经验超时 | 30s 超时 + 降级为规则模板 |
| 经验注入污染上下文 | 按 `task_type` 匹配 + 条数限制(3 条) |
| 事实记忆累积过多无关项 | LLM prompt 明确「无明显事实回复『无』」+ 不写入 |
| 并发写入 JSONL 冲突 | `Memories.mu sync.Mutex` 串行化写入 |
| 程序层未实现却被引用 | 当前版本 `procedural.jsonl` 仅作为接口位,`GetContextForPrompt` 不读它 |
## 与 opsxcli 的差异总结
| 维度 | opsxcli | aiops |
|------|---------|-------|
| 进化步骤 | 13 步 | 6 步 |
| 代码规模 | ~3300 行 | ~400 行 |
| 记忆查询 | 复杂相似度 + 整合去重 | 简单 task_type 匹配 + 时间倒序 |
| 经验字段 | SuccessRate / UsageCount / Tags | Success(布尔) |
| Skill 提炼 | 自动(Step 9.7) | 未实现(接口位) |
| 反射分析 | 独立 Step + JSON 结构 | 合并进 LEARN |
| 用户反馈 | Step 10 弹消息 | 剥离(Web 透明) |
**核心思想保留度**:四层记忆 ✓ / 进化闭环 ✓ / 异步降级 ✓ / 经验注入 ✓。精简的是重型分析,不是学习能力本身。
> 作者:Logan.Li
@@ -0,0 +1,468 @@
# 09 - 安全系统与工具注册
> 本文由 opsxcli 同名设计文档适配改写,结合 aiops Agent CLI 的 Web 实现(`backend/pkg/agentcli/safety/safety.go` + `tools/registry.go` + `tools/risk.go`)。
> **保留**:三级安全模式 / 五级风险 / 危险命令黑名单 / AssessBashRisk 动态评估 / Tool 接口 + Registry。
> **适配**:确认机制从 CLI 终端 `bufio.Reader` 改为 WebSocket `confirm` 事件 + 前端 `useConfirm()` 弹框;审计日志从文件改为 DB 落库。
## 设计理念
安全系统是运维 Agent 的生命线。aiops Agent CLI 安全系统遵循两个核心原则:
1. **默认安全**:balanced 模式下,high+ 操作需要确认,覆盖 95% 日常运维
2. **可审计**:每次工具调用都通过 EventCallback 落库,事后可追溯
工具注册系统遵循**工具化思维**:万物皆 Tool,实现接口即可扩展。
---
## Part 1: 安全系统
### 架构总览
```
┌────────────────────────────────────────────────────┐
│ safety.Controller │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ risk.go │ │ controller.go │ │
│ │ (在 tools 包)│ │ (在 safety 包)│ │
│ │ 黑名单 + 分级 │ │ 阈值 + ConfirmFn│ │
│ └──────────────┘ └──────────────┘ │
│ │
│ Safety Modes: │
│ strict → medium+ 确认 │
│ balanced → high+ 确认(默认) │
│ permissive → critical 仅处理 │
└────────────────────────────────────────────────────┘
```
> 与 opsxcli 的差异:opsxcli 把风险评估和审计都放在 `safety/` 包;aiops 为避免 `tools` ↔ `safety` 循环依赖,把 `risk.go`(黑名单 + AssessBashRisk)置于 `tools` 包,由 `ssh_execute` 的 `DynamicRiskTool` 直接调用;`safety` 包只做「是否需确认」的策略。
### Controller 结构
```go
// backend/pkg/agentcli/safety/safety.go
type Controller struct {
mode Mode // 当前安全模式
confirmFn ConfirmFn // 确认回调(controller 注入:推 WS confirm + 阻塞等回传)
autoApprove bool // 自动批准(仅测试用,生产禁用)
}
```
> 与 opsxcli 的差异:剥离 `reader *bufio.Reader`(CLI 终端交互)和 `history []ExecutionRecord`(内存审计历史),改为通过 EventCallback 落库 DB;剥离 `auditLog *AuditLogWriter`(文件审计)。
### ConfirmFn —— Web 版的确认机制
```go
type ConfirmFn func(toolName string, args map[string]interface{}, risk tools.RiskLevel) (bool, error)
```
controller 实现的 `confirmFn`:
```go
// backend/controller/agent_cli.go
func (ac *agentConn) confirmFn(toolName string, args map[string]interface{}, risk tools.RiskLevel) (bool, error) {
confirmID := fmt.Sprintf("cf-%d", time.Now().UnixNano())
cmd, _ := args["command"].(string)
// 1. 推 WS confirm 事件(前端 useConfirm 弹框)
ac.write(wsEvent{
Type: "confirm", ConfirmID: confirmID,
ToolName: toolName, Command: cmd, Risk: risk.Display(), ToolArgs: args,
})
// 2. 注册 channel 等待前端回传
ch := make(chan bool, 1)
ac.cfMu.Lock()
ac.confirms[confirmID] = ch
ac.cfMu.Unlock()
// 3. 阻塞等待(5 分钟超时)
select {
case approved := <-ch:
return approved, nil
case <-time.After(5 * time.Minute):
ac.delConfirm(confirmID)
return false, fmt.Errorf("确认超时")
}
}
```
前端处理(`AgentCLI.tsx`):
```tsx
case 'confirm': {
const ok = await confirm({
title: '确认执行高危命令',
description: `即将执行:${ev.command}`,
confirmText: '确认执行',
});
wsRef.current?.send(JSON.stringify({
type: ok ? 'confirm' : 'deny',
confirm_id: ev.confirm_id,
}));
break;
}
```
这是「Web 版 CLI 终端确认」的完整替代 —— 用户在浏览器看到弹框,点确认/拒绝,结果通过 WebSocket 回传解锁引擎。
### 三级安全模式
```go
const (
ModeStrict Mode = "strict" // medium+ 需确认
ModeBalanced Mode = "balanced" // high+ 需确认(默认)
ModePermissive Mode = "permissive" // 仅 critical 处理
)
```
| 模式 | 需要确认的等级 | 适用场景 |
|------|---------------|----------|
| `strict` | medium 及以上 | 生产环境、关键主机、新手操作 |
| `balanced`(默认) | high 及以上 | 日常运维 |
| `permissive` | 仅 critical | 测试环境、自动化脚本 |
阈值映射:
```go
func (c *Controller) riskThreshold() tools.RiskLevel {
switch c.mode {
case ModeStrict: return tools.RiskMedium
case ModePermissive: return tools.RiskCritical
default: return tools.RiskHigh
}
}
```
### 五级风险评估
```go
// backend/pkg/agentcli/tools/registry.go
const (
RiskSafe RiskLevel = iota // 0 - 只读(cat, grep, df, ps)
RiskLow // 1 - 低风险(mkdir, touch)
RiskMedium // 2 - 中风险(chmod, 文件修改)
RiskHigh // 3 - 高风险(ssh_execute, 服务控制)
RiskCritical // 4 - 危险(rm -rf /, mkfs, dd, 数据删除)
)
```
每级都有中文显示(`Display()`):
```go
case RiskSafe: return "安全"
case RiskLow: return "低风险"
case RiskMedium: return "中风险"
case RiskHigh: return "高风险"
case RiskCritical: return "危险"
```
前端在 `tool_call` 事件中拿到 `risk` 字段,渲染为风险标签。
### 危险命令黑名单(硬红线)
```go
// backend/pkg/agentcli/tools/risk.go
var dangerousCommands = []string{
// 文件系统毁灭性操作
"rm -rf /", "rm -rf /*", "rm -rf /bin", "rm -rf /boot", "rm -rf /etc",
"rm -rf /home", "rm -rf /lib", "rm -rf /lib64", "rm -rf /proc", "rm -rf /root",
"rm -rf /sbin", "rm -rf /sys", "rm -rf /usr", "rm -rf /var",
// 磁盘格式化
"mkfs", "mkfs.ext", "mkfs.xfs", "mkfs.btrfs", "mkfs.vfat",
// 磁盘覆写
"dd if=/dev/zero", "dd if=/dev/random", "dd if=/dev/urandom",
// Fork bomb
":(){ :|:& };:", ":() { : | : & }; :",
// 设备重定向
"> /dev/sda", "> /dev/sdb", "> /dev/sdc", "> /dev/hda",
// 数据销毁
"mv / /dev/null", "mv /* /dev/null",
// 权限破坏
"chmod -R 000 /", "chmod -R 777 /",
// 服务自毁
"systemctl stop ssh", "systemctl disable ssh", "kill -9 1",
// 防火墙清空
"iptables -F", "iptables --flush",
}
```
**特殊检测**:`rm -rf` + 根目录路径(即使不在黑名单字面中):
```go
if strings.HasPrefix(lower, "rm") && strings.Contains(lower, "-rf") && containsRootPath(lower) {
return true, "rm -rf 包含根目录路径"
}
```
`containsRootPath` 检测 `/boot`、`/etc`、`/usr`、`/var` 等系统目录。
### Check 策略
```go
func (c *Controller) Check(tool tools.Tool, args map[string]interface{}) (bool, error) {
risk := assessToolRisk(tool, args)
// 1. 黑名单硬红线:绝不允许(不可确认放行)
if risk >= tools.RiskCritical {
return false, nil
}
// 2. 未达阈值或自动批准:直接放行
if c.autoApprove || risk < c.riskThreshold() {
return true, nil
}
// 3. 达阈值:请求用户确认
if c.confirmFn == nil {
return false, nil // 无确认回调时保守拒绝(Web 场景应始终注入 confirmFn)
}
return c.confirmFn(tool.Name(), args, risk)
}
```
**三层策略**:
1. **硬红线**(Critical):黑名单命令直接拒绝,**不可被确认放行**
2. **阈值放行**:低于模式阈值的操作直接执行
3. **用户确认**(达阈值):调 `confirmFn` 推 WS 弹框等用户决定
### AssessBashRisk 动态评估
```go
func AssessBashRisk(command string) RiskLevel {
// 1. 黑名单精确匹配 → Critical
if isDangerous, _ := IsDangerousCommand(command); isDangerous {
return RiskCritical
}
// 2. 纯只读命令 → Safe
if isReadOnlyCommand(lower) {
return RiskSafe
}
// 3. 组合命令(&& || | ;)→ 取子命令最高风险
if isCompound(lower) {
return assessCompoundCommand(lower)
}
// 4. 单命令分类
isSudo := strings.HasPrefix(lower, "sudo ")
return assessSingleCommand(strings.TrimPrefix(lower, "sudo "), isSudo)
}
```
### 命令分类表
| 类别 | 命令示例 | 风险等级 |
|------|---------|---------|
| 只读(Safe) | cat, grep, awk, sed, head, tail, ps, top, df, du, free, uptime, uname, hostname, ifconfig, ip addr, netstat, ss, ls, pwd, who, w, last, vmstat, iostat, lsof, find, which, file, stat, date, echo, id, whoami, dmesg, systemctl status, journalctl, lsblk, mount | Safe |
| 修改(Low/Medium) | cp, mv, mkdir, rmdir, touch, chmod, chown, chgrp, ln, tar, gzip, gunzip, zip, unzip, rsync, tee | Low(sudo 时 Medium) |
| 高危(Medium/High) | rm, dd, shutdown, reboot, halt, poweroff, init, systemctl restart/stop/start, kill, killall, pkill, userdel, groupdel, fdisk, parted, mkfs | Medium(sudo 时 High) |
| 危险(Critical) | rm -rf /, mkfs, dd if=/dev/zero, fork bomb, > /dev/sda, chmod -R 000 / | Critical(硬拒) |
### sudo 提升
`sudo` 会让风险等级 +1(最高到 Critical 前):
```go
// assessSingleCommand
if isSudo {
return RiskHigh // 高危命令 + sudo → High
}
return RiskMedium // 高危命令无 sudo → Medium
```
### 组合命令取最高
```go
// assessCompoundCommand: 拆分 && || | ; 后取子命令最高风险
// "df -h && rm /tmp/file" → max(Safe, Medium) = Medium
// "cat /etc/passwd; rm -rf /tmp" → max(Safe, Low) = Low
```
### 审计落库(替代文件审计)
> opsxcli 把审计写到 `~/.opsxcli/audit/audit.log`;aiops 直接走 DB(`agent_cli_messages` 表),多用户场景下可按 user_id / session_id 查询。
controller 的 `onEvent` 回调把每个 `tool_call` / `tool_result` 落库:
```go
case core.EventToolCall:
argsJSON, _ := json.Marshal(e.ToolArgs)
ac.persistMsg(sid, e.Round, "tool_call", "tool", "",
e.ToolName, string(argsJSON), "", e.Risk, false, "", 0)
case core.EventToolResult:
ac.persistMsg(sid, e.Round, "tool_result", "tool", e.Output,
e.ToolName, "", "", "", e.Success, e.Duration, 0)
```
每条记录包含:工具名、参数 JSON、风险等级、成败、耗时、轮次、会话 ID、用户 ID(通过 session 关联)。前端「会话历史」页面可完整回放。
---
## Part 2: 工具注册系统
### Registry 注册中心
```go
// backend/pkg/agentcli/tools/registry.go
type Registry struct {
tools map[string]Tool
mu sync.RWMutex
}
```
核心方法:
| 方法 | 说明 |
|------|------|
| `NewRegistry()` | 创建空注册中心 |
| `Register(tool)` | 注册工具(线程安全) |
| `Get(name)` | 按名获取工具(LLM 调用时按名查找) |
| `List()` | 列出所有工具 |
| `ToLLMTools()` | 转换为 OpenAI function calling 格式 |
### Tool 接口(5 方法)
```go
type Tool interface {
Name() string
Description() string
Parameters() map[string]interface{} // JSON Schema
RiskLevel() RiskLevel
Execute(ctx context.Context, args map[string]interface{}) (*Result, error)
}
```
### DynamicRiskTool 扩展接口
```go
type DynamicRiskTool interface {
Tool
RiskLevelForArgs(args map[string]interface{}) RiskLevel
}
```
安全控制器 `Check` 时优先调用动态评估:
```go
func assessToolRisk(tool tools.Tool, args map[string]interface{}) tools.RiskLevel {
if dt, ok := tool.(tools.DynamicRiskTool); ok {
return dt.RiskLevelForArgs(args) // 动态优先
}
return tool.RiskLevel()
}
```
### Result 结构
```go
type Result struct {
Success bool `json:"success"`
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
```
### 参数解析辅助
```go
func StringParam(args map[string]interface{}, key string) string
func IntParam(args map[string]interface{}, key string) int // 兼容 int/float64/int64
```
LLM 返回的 JSON 参数反序列化后,数字可能是 `float64`(JSON 原生),`IntParam` 统一处理。
### 已注册工具一览
| 工具 | 静态风险 | 动态风险 | 功能 |
|------|---------|---------|------|
| `ssh_execute` | High | Safe~Critical | 远程命令执行(DynamicRiskTool) |
| `analyze_output` | Safe | — | 输出分析(纯文本,只读) |
详细设计见 [05 - Builtin 内置工具](05-Builtin内置命令.md)。
> 与 opsxcli 的差异:剥离了 `local_bash` / `scp_transfer` / `file_read` / `file_search` 四个工具;剥离 SSH 连接池(aiops 直接用 `pkg/remoteexec`,无连接池需求);剥离 `Closer` 接口(Web 工具无资源需释放)。
## 扩展指南
### 添加新工具
```go
// 1. 实现 Tool 接口(必要时实现 DynamicRiskTool)
type MyTool struct{}
func (t *MyTool) Name() string { return "my_tool" }
func (t *MyTool) Description() string { return "我的工具" }
func (t *MyTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"target": map[string]interface{}{"type": "string", "description": "目标"},
},
"required": []string{"target"},
}
}
func (t *MyTool) RiskLevel() tools.RiskLevel { return tools.RiskMedium }
func (t *MyTool) Execute(ctx context.Context, args map[string]interface{}) (*tools.Result, error) {
// 实现
return &tools.Result{Success: true, Output: "done"}, nil
}
// 2. 在 agentcli.NewEngine 中注册
registry.Register(&MyTool{})
```
### 扩展危险命令黑名单
修改 `tools/risk.go` 的 `dangerousCommands` 切片:
```go
var dangerousCommands = []string{
// ... 现有
"your-critical-command",
}
```
### 自定义确认逻辑
controller 的 `confirmFn` 可对接企业审批系统(如先走工单再确认):
```go
engine.SetConfirmFn(func(toolName string, args map[string]interface{}, risk tools.RiskLevel) (bool, error) {
// 1. 推 WS 弹框让用户初步确认
// 2. 高危场景额外对接审批工单系统
// 3. 返回最终批准结果
})
```
## 常见陷阱
| 陷阱 | 解决方案 |
|------|----------|
| 忘记设置 RiskLevel 导致误判 | 默认按接口返回值,高危工具必须显式声明 |
| 高危工具未实现 DynamicRiskTool | 静态等级无法反映参数差异,必须实现动态评估 |
| 黑名单只看前缀匹配 | `IsDangerousCommand` 同时做精确匹配 + `rm -rf` 根路径特殊检测 |
| 工具 Execute 无超时 | `Config.ToolTimeout=120s` 兜底(`context.WithTimeout`) |
| 确认回调无超时 | controller 的 `confirmFn` 内置 5 分钟超时 |
| 黑名单可被确认放行 | Check 策略第一层 Critical 直接 `return false, nil`,不走 confirmFn |
| LLM 编造工具名 | `registry.Get` 找不到时返回 observation「工具未找到」,LLM 自动换法 |
| 参数 JSON 解析失败 | `json.Unmarshal` 失败时返回 observation「参数解析失败」,不 panic |
## 与 opsxcli 的差异总结
| 维度 | opsxcli | aiops |
|------|---------|-------|
| 确认机制 | `bufio.Reader` CLI 终端交互 | WebSocket `confirm` 事件 + 前端 `useConfirm()` 弹框 |
| 审计日志 | 文件 `~/.opsxcli/audit/audit.log` | DB 表 `agent_cli_messages` |
| 风险评估位置 | `safety/risk.go` | `tools/risk.go`(避免循环依赖) |
| 内存审计历史 | `history []ExecutionRecord`(上限 1000 FIFO) | 剥离(直接走 DB) |
| 工具集 | 6 个 | 2 个 |
| SSH 连接池 | 有(`ssh_pool.go`) | 剥离(用 `pkg/remoteexec`) |
| Closer 接口 | 有 | 剥离 |
安全核心(三级模式 / 五级风险 / 黑名单 / 动态评估)完全保留,差异仅在确认机制和审计载体。
> 作者:Logan.Li
+388
View File
@@ -0,0 +1,388 @@
# 10 - 会话管理
> 本文由 opsxcli 同名设计文档适配改写,结合 aiops Agent CLI 的 Web 实现(`backend/model/agent_cli.go` + `controller/agent_cli.go`)。
> **核心改造**:opsxcli 用本地 JSONL 文件存储会话(单用户 CLI 场景);aiops 用 **GORM 数据库表**存储会话与消息,支持多用户并发、按 user_id 隔离、REST 查询与历史回放。
## 设计理念
- **会话管理**:Agent 引擎本身无状态(每次任务独立),所有对话过程持久化到会话层,支持历史回溯、审计、断点排查
- **多用户隔离**:Web 场景下多个用户同时使用 Agent CLI,会话必须按 user_id 隔离
- **结构化查询**:会话需要列表、筛选、删除等业务操作,必须走 DB 而非文件
> 为什么改用 DB?opsxcli 是 CLI 工具,单用户、单机使用,JSONL 文件够用且零依赖;aiops 是 Web 平台,多用户并发、需要列表查询和按主机/用户筛选、需要与其他业务表(Server、User)关联 —— DB 是必然选择。
## 架构总览
```
┌────────────────────────────────────────────────────┐
│ REST + WebSocket │
│ /ws/agent-cli (WebSocket 推流) │
│ /aiops/agent-cli/* (REST 会话管理) │
├────────────────────────────────────────────────────┤
│ controller/agent_cli.go │
│ AgentCLIHandler │
│ ├─ Run (WebSocket) │
│ ├─ ListSessions / GetSession / DeleteSession │
│ └─ agentConn (单连接运行态) │
├────────────────────────────────────────────────────┤
│ model/agent_cli.go (GORM) │
│ AgentCLISession (agent_cli_sessions 表) │
│ AgentCLIMessage (agent_cli_messages 表) │
├────────────────────────────────────────────────────┤
│ MySQL / SQLite (aiops 既有 DB) │
└────────────────────────────────────────────────────┘
```
## 核心数据模型
### AgentCLISession(会话)
```go
// backend/model/agent_cli.go
type AgentCLISession struct {
ID uint `gorm:"primarykey" json:"id"`
UserID uint `gorm:"index" json:"user_id"` // 发起用户(隔离)
ServerID uint `gorm:"index" json:"server_id"` // 目标主机
ServerName string `gorm:"size:128" json:"server_name"` // 主机名快照
ServerHost string `gorm:"size:64" json:"server_host"` // 主机 IP 快照
Title string `gorm:"size:255" json:"title"` // 任务标题(query 截断)
Status string `gorm:"size:32;default:running" json:"status"` // running/done/aborted/error
Provider string `gorm:"size:50" json:"provider"` // LLM 提供商
Model string `gorm:"size:100" json:"model"` // 模型名
Rounds int `json:"rounds"` // ReAct 轮次
TotalTokens int `json:"total_tokens"` // token 消耗
Duration int64 `json:"duration"` // 耗时(毫秒)
Error string `gorm:"type:text" json:"error,omitempty"` // 错误信息
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (AgentCLISession) TableName() string { return "agent_cli_sessions" }
```
**设计要点**:
- **`UserID` 索引**:多用户隔离的核心,所有查询都按 user_id 过滤
- **`ServerID` + `ServerName` + `ServerHost`**:关联主机的同时保存快照(主机 later 被删/改名时历史仍可读)
- **`Status` 四态**:`running`(进行中)/ `done`(完成)/ `aborted`(用户中断)/ `error`(出错或达最大轮次)
- **`Title`**:query 截断到 60 字符,作为会话列表的显示标题
### AgentCLIMessage(消息)
```go
type AgentCLIMessage struct {
ID uint `gorm:"primarykey" json:"id"`
SessionID uint `gorm:"index;not null" json:"session_id"`
Round int `json:"round"` // ReAct 轮次
Type string `gorm:"size:32" json:"type"` // thought/tool_call/tool_result/done/error/session
Role string `gorm:"size:32" json:"role"` // user/assistant/tool
Content string `gorm:"type:text" json:"content"` // 思考文本/最终答案
ToolName string `gorm:"size:64" json:"tool_name"` // 工具名
ToolArgs string `gorm:"type:text" json:"tool_args"` // 工具参数 JSON
ToolOutput string `gorm:"type:text" json:"tool_output"` // 工具输出
Risk string `gorm:"size:32" json:"risk"` // 风险等级
Success bool `json:"success"`
Duration string `gorm:"size:32" json:"duration"` // 工具耗时
Tokens int `json:"tokens"`
CreatedAt time.Time `gorm:"index" json:"created_at"`
}
func (AgentCLIMessage) TableName() string { return "agent_cli_messages" }
```
**消息类型映射引擎事件**:
| Event.Type | DB Type | DB Role | Content | ToolName | ToolOutput |
|------------|---------|---------|---------|----------|------------|
| 用户输入 | (无 Type) | user | query | — | — |
| `thought` | thought | assistant | AI 思考文本 | — | — |
| `tool_call` | tool_call | tool | (空) | 工具名 | (空,Args 在 ToolArgs) |
| `tool_result` | tool_result | tool | 输出 | 工具名 | 输出(冗余便于查询) |
| `done` | done | assistant | 最终答案 | — | — |
| `error` | error | assistant | 错误信息 | — | — |
| `progress` | — | — | — | — | — |(不入库,太频繁)
## 会话生命周期
```
WebSocket start {query}
│
▼
startTask
├─ 创建 AgentCLISession (status=running)
├─ 存用户消息 (role=user, content=query)
├─ 构建 Engine、注入 Callback
└─ go engine.Run(ctx, query, serverInfo)
│
│ 每个事件触发 onEvent → 落库 agent_cli_messages
│ ├─ thought → persistMsg(type=thought, role=assistant)
│ ├─ tool_call → persistMsg(type=tool_call, role=tool, args, risk)
│ ├─ tool_result → persistMsg(type=tool_result, role=tool, output, success)
│ ├─ done → persistMsg(type=done, role=assistant, answer, tokens)
│ └─ error → persistMsg(type=error, role=assistant, message)
│
▼
finish (更新 session.status / rounds / duration / error)
├─ ctx.Err() != nil → status=aborted
├─ err != nil → status=error
└─ 否则 → status=done
```
## controller 实现
### startTask(创建会话)
```go
// backend/controller/agent_cli.go
func (ac *agentConn) startTask(sshCfg remoteexec.SSHConfig, llmCfg model.LLMConfig, query string) {
ac.abort() // 取消上一次任务
// 1. 创建 DB 会话
title := query
if r := []rune(title); len(r) > 60 {
title = string(r[:60]) + "..."
}
sess := &model.AgentCLISession{
UserID: ac.userID, ServerID: ac.server.ID,
ServerName: ac.server.Hostname, ServerHost: ac.server.IP,
Title: title, Status: "running",
Provider: llmCfg.Provider, Model: llmCfg.Model,
}
ac.db.Create(sess)
ac.session = sess
// 2. 存用户消息
ac.persistMsg(sess.ID, 0, "user", "user", query, "", "", "", "", false, "", 0)
// 3. 构建 Engine
engine, _ := agentcli.NewEngine(...)
engine.SetCallback(ac.onEvent)
engine.SetConfirmFn(ac.confirmFn)
// 4. 启动 ReAct 循环(goroutine)
go func() {
defer engine.Close()
_, err := engine.Run(ctx, query, serverInfo)
// ... finish
}()
}
```
### onEvent(事件落库 + 推 WS)
```go
func (ac *agentConn) onEvent(e core.Event) {
ev := wsEvent{...} // 序列化
if ac.session != nil {
ev.SessionID = ac.session.ID
}
ac.write(ev) // 推 WS
// 持久化(progress 不入库)
if ac.session == nil { return }
sid := ac.session.ID
switch e.Type {
case core.EventThought:
ac.persistMsg(sid, e.Round, "thought", "assistant", e.Thought, ...)
case core.EventToolCall:
argsJSON, _ := json.Marshal(e.ToolArgs)
ac.persistMsg(sid, e.Round, "tool_call", "tool", "", e.ToolName, string(argsJSON), "", e.Risk, ...)
case core.EventToolResult:
ac.persistMsg(sid, e.Round, "tool_result", "tool", e.Output, e.ToolName, "", "", "", e.Success, e.Duration, 0)
case core.EventDone:
ac.persistMsg(sid, e.Round, "done", "assistant", e.Answer, ..., true, "", e.Tokens)
case core.EventError:
ac.persistMsg(sid, e.Round, "error", "assistant", e.Message, ...)
}
}
```
### finish(结束会话)
```go
func (ac *agentConn) finish(sess *model.AgentCLISession, status, errMsg string, rounds int, dur time.Duration) {
// 推 WS 结束事件
ac.write(wsEvent{Type: status, SessionID: sess.ID, Message: errMsg, Round: rounds})
// 更新 DB
updates := map[string]interface{}{
"status": status, "rounds": rounds, "duration": dur.Milliseconds(),
}
if errMsg != "" {
updates["error"] = errMsg
}
ac.db.Model(sess).Updates(updates)
}
```
### REST 接口
```go
// backend/router/routes.go
agentCLIGroup := authorized.Group("/aiops/agent-cli")
{
agentCLIGroup.GET("/sessions", agentCLIHandler.ListSessions) // 列表(按 user_id 过滤)
agentCLIGroup.GET("/sessions/:id", agentCLIHandler.GetSession) // 详情(含全部消息)
agentCLIGroup.DELETE("/sessions/:id", agentCLIHandler.DeleteSession) // 删除(校验归属)
}
```
前端 `frontend/src/api/agentCli.ts` 调用这些接口实现「会话历史」页面:
- 列表展示用户的所有会话(标题、主机、状态、耗时、token)
- 点击某会话查看完整 ReAct 推理过程(thought / tool_call / tool_result 时间线)
- 删除会话(连带消息)
## WebSocket 协议
### 后端 → 前端(wsEvent)
```go
type wsEvent struct {
Type string `json:"type"` // session/thought/tool_call/tool_result/progress/done/error/confirm
Round int `json:"round,omitempty"`
SessionID uint `json:"session_id,omitempty"`
Thought string `json:"thought,omitempty"`
ToolName string `json:"tool_name,omitempty"`
ToolArgs map[string]interface{} `json:"tool_args,omitempty"`
Risk string `json:"risk,omitempty"`
Output string `json:"output,omitempty"`
Success bool `json:"success,omitempty"`
Duration string `json:"duration,omitempty"`
Tokens int `json:"tokens,omitempty"`
Answer string `json:"answer,omitempty"`
Message string `json:"message,omitempty"`
ConfirmID string `json:"confirm_id,omitempty"` // confirm 事件专属
Command string `json:"command,omitempty"` // confirm 事件专属
}
```
### 前端 → 后端(wsControl)
```go
type wsControl struct {
Type string `json:"type"` // start/confirm/deny/abort
Query string `json:"query,omitempty"` // start 时任务描述
ConfirmID string `json:"confirm_id,omitempty"` // confirm/deny 时
}
```
## 并发与隔离
### 单连接串行
每个 WebSocket 连接(`agentConn`)同一时刻只运行一个任务:
```go
func (ac *agentConn) startTask(...) {
ac.abort() // 取消上一次任务,再启动新的
}
```
### 多用户隔离
- 每个会话绑定 `UserID`(JWT 中间件注入)
- REST 查询自动按 `user_id` 过滤
- 删除会话时校验归属
### 线程安全
- `agentConn.writeMu sync.Mutex` —— 串行化 WebSocket 写入
- `agentConn.cfMu sync.Mutex` —— 保护 `confirms` map
- 引擎 `core.Agent.mu sync.Mutex` —— 保护 token 计数等内部状态
## 持久化策略
### 落库失败不阻断
```go
func (ac *agentConn) persistMsg(...) {
if err := ac.db.Create(msg).Error; err != nil {
_ = err // 持久化失败不阻断主流程
}
}
```
DB 写入失败时,WS 推送仍正常,用户体验不受影响(只是历史回放缺这条)。
### progress 不入库
`progress` 事件每个轮次都发,且只含 round 和 token,价值低、频率高,不入库(避免 DB 压力)。
## 历史回放
前端通过 `GET /sessions/:id` 拿到完整消息列表,按时间顺序渲染:
```
[会话 #42] 检查 nginx 启动失败的原因
├─ 用户:检查 nginx 启动失败的原因
├─ Round 1 [thought]:我先查看 nginx 服务状态...
├─ Round 1 [tool_call] ssh_execute "systemctl status nginx" (高风险)
├─ Round 1 [tool_result] failed (1.2s) [exit_code=3]
├─ Round 2 [thought]:服务 failed,查看日志...
├─ Round 2 [tool_call] ssh_execute "journalctl -u nginx -n 500" (安全)
├─ Round 2 [tool_result] 500 行 (0.8s)
├─ Round 3 [thought]:日志太长,先分析...
├─ Round 3 [tool_call] analyze_output(output, focus="error")
├─ Round 3 [tool_result] 8 行疑似错误 (0.05s)
├─ Round 4 [thought]:端口被占,确认一下...
├─ Round 4 [tool_call] ssh_execute "ss -tlnp | grep :80" (安全)
├─ Round 4 [tool_result] (0.3s)
└─ [done] 根因:80 端口被 apache 占用。建议:systemctl stop httpd && systemctl start nginx
```
这是 opsxcli「会话导出为 Markdown」的 Web 升级版 —— 不需要导出,直接在线回放。
## 扩展指南
### 添加会话标签 / 收藏
1. 在 `AgentCLISession` 增加 `Tags string` / `Starred bool` 字段
2. 增加 REST 接口 `PATCH /sessions/:id`
3. 前端列表增加标签筛选
### 会话统计
```go
// 例:用户本月使用的 token 总数
db.Model(&AgentCLIMessage{}).
Where("session_id IN (?) AND type = ?",
db.Model(&AgentCLISession{}).Select("id").Where("user_id = ?", uid),
"done").
Select("COALESCE(SUM(tokens), 0)").Scan(&totalTokens)
```
### 导出会话为 Markdown
参考 opsxcli 的 `ExportMarkdown`,在 controller 增加一个 REST 接口,遍历消息组装 Markdown 文本返回。
## 常见陷阱
| 陷阱 | 解决方案 |
|------|----------|
| 多用户看到别人的会话 | 所有查询按 `user_id` 过滤,删除时校验归属 |
| WebSocket 断开导致会话悬挂 | `ctx.Err()` 触发 finish(status=aborted);前端重连后可查看历史 |
| 消息表无限增长 | 定期清理老会话(运维任务);或按 `created_at` 分区 |
| 落库失败丢失历史 | 不阻断主流程,但应在日志记录;progress 不入库减少压力 |
| 长任务超过 WS 超时 | ReAct 循环每步检查 `ctx.Err()`,前端断开 → ctx cancel → finish(aborted) |
| 并发写 WS 错乱 | `writeMu sync.Mutex` 串行化写入 |
| ConfirmFn 无人响应 | 5 分钟超时自动拒绝 |
| `lastRound` 不准 | 从 DB 查 `MAX(round)` 而非内存计数(断线重连场景) |
## 与 opsxcli 的差异总结
| 维度 | opsxcli | aiops |
|------|---------|-------|
| 存储后端 | JSONL 文件(`~/.opsxcli/agent/sessions/`) | GORM DB(`agent_cli_sessions` / `agent_cli_messages` 表) |
| 多用户 | 不支持(单用户 CLI) | 原生支持(user_id 索引隔离) |
| 会话结构 | `{session-id}/meta.json + messages.jsonl` | 单表 sessions + 单表 messages(一对多) |
| 查询能力 | 只能遍历文件 | SQL 任意查询、JOIN、聚合 |
| 导出 | ExportMarkdown / ExportJSON / ExportToFile | 在线回放(前端时间线) |
| 消息字段 | role/content/tool_calls | type/role/content/tool_name/tool_args/tool_output/risk/success/duration/tokens |
| 接口 | Manager 接口(10 方法) | REST(3 方法)+ WebSocket 推流 |
存储载体变了,但「会话 = 元数据 + 消息流」的核心模型保留。DB 方案在多用户 Web 场景下是必然选择,也使会话与其他业务表(Server、User、LLMConfig)的关联变得自然。
> 作者:Logan.Li