# 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