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

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

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

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

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

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

9.8 KiB
Raw Blame History

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 真正「动手」的唯一通道。

实现

// 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:

// 静态等级: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 流程

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 基于精炼后的信息推理

实现

// 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. 组装摘要:总行数 + 截断标注 + 错误行 + 输出片段
}

错误关键词列表:

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 - 插件架构设计 的「添加新工具」章节。

常见陷阱

陷阱 解决方案
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