开发 Agent 的坑

从一个 AIOps Agent 后端重写项目中总结的工程教训。我们参考了 claude code 的源码架构,用 Go + Eino ADK 重写了一套运维 Agent 系统。这篇文章不讲 prompt engineering 技巧,只讲在工程层面踩过的坑。

坑 1:Prompt 散写------最隐蔽的技术债

刚开始写 agent 的时候,prompt 是最容易"随手写"的东西。这里加一句角色描述,那里塞一段 tool 使用说明,审批流程的提示藏在某个 helper 函数里。

三个月后你会发现:没有人知道 agent 的完整 prompt 长什么样。改一个地方,另一个地方的行为莫名其妙变了。

我们的做法是把 prompt 强制分成四层,每层有明确的职责边界:

职责 禁止包含
System Prompt 环境与角色定义 具体 tool 的使用说明
Developer Instructions 运行约束 回答风格
Tool Prompt Set 能力描述(capability + constraints + result shape) 回答风格、策略约束
Runtime Policy Prompt 当前 mode 的策略约束 tool 描述

关键规则:所有 prompt 必须通过 PromptCompiler 统一编译输出。禁止在 agent loop 的中间环节、局部 helper 函数、或者 nudge 消息里注入 prompt 片段。

claude code 的做法更灵活------每个 Tool 自带 prompt() 方法,运行时动态生成。这在 tool 数量多的时候确实更好维护,但前提是你有一个清晰的 prompt 分层约束,否则每个 tool 各写各的,最终拼出来的 prompt 一样是一团乱麻。

坑 2:用 Prompt 补偿代码缺陷

这是最常见的反模式。

场景:agent 在 inspect 模式下不应该执行写操作,但你发现它偶尔会调用 file_write

错误做法:在 prompt 里加一句"在 inspect 模式下,你绝对不能调用 file_write 工具"。

为什么错:prompt 是建议,不是硬约束。模型可能忽略它,尤其是在上下文很长的时候。你加了这句话,测了几次发现"好了",但这只是概率性的改善,不是确定性的修复。

正确做法:在 PolicyEngine 的 ModePolicy 中添加硬约束------inspect 模式下 file_write 直接返回 deny。代码层面拦截,100% 生效。

go 复制代码
// 这是硬约束,不是建议
func (p *InspectModePolicy) CheckCapability(toolName string, toolKind string) PolicyDecision {
    if isMutationTool(toolName) {
        return PolicyDecision{Action: PolicyActionDeny, Reason: "inspect mode prohibits mutation"}
    }
    return PolicyDecision{Action: PolicyActionAllow}
}

原则:Prompt 告诉模型"应该怎么做",PolicyEngine 确保"不能怎么做"。两者互补,但安全边界必须由代码保证。

坑 3:Tool 注册的平行宇宙

项目初期,tool 的注册方式很随意:有的直接在 agent loop 里硬编码,有的通过配置文件加载,MCP tool 又走另一套路径。结果就是:

  • 没有人知道当前 agent 到底能用哪些 tool
  • 内置 tool 和 MCP tool 名称冲突时行为不确定
  • 新加一个 tool 需要改三个地方

我们的解决方案是统一注册中心 (Capability Registry)。所有 tool,不管是内置的、MCP 动态加载的、还是 extension 提供的,都必须通过同一个 Registry.Register() 注册。

claude code 的 assembleToolPool() 函数做了一件很聪明的事:合并内置 tool 和 MCP tool 时,内置 tool 在名称冲突时优先。这个优先级规则看起来简单,但如果没有统一的合并入口,你根本无法保证这个语义。

更进一步,claude code 还有 Deny Rules------在组装 tool pool 之前,先过滤掉被禁止的 tool。这比我们目前的实现更完善。如果你的 agent 需要支持多租户或者不同权限级别的用户,deny rules 是必须的。

坑 4:陷入局部字符串过滤的泥潭

这个坑非常容易踩,而且一旦踩进去就很难出来。

场景:某个 tool 的输出格式有问题,前端显示异常。

错误做法:在 Projection 层加一个 if toolName == "coroot.rca_report" { 特殊处理 }

一周后另一个 tool 也有类似问题,再加一个 if。一个月后你的 Projection 层里有 15 个针对特定 tool 名称的 switch-case。每次加新 tool 都要检查这些特殊逻辑是否需要更新。

正确做法:从源头修复 。让 tool 的 Display() 方法返回正确的结构化数据,而不是在下游做字符串修补。

同样的反模式还出现在:

  • 在通用逻辑中 strings.Contains(toolName, "coroot") 判断特定 tool
  • 用正则表达式从 assistant 文本中解析 tool 状态
  • 在 mode 判断中硬编码 if mode == "special_debug_mode"

这些都是信号:你的抽象层有缺陷。正确的做法是通过接口声明属性(IsReadOnly()IsDestructive()),通过 Visibility 控制可见性,通过 ModePolicy 定义边界。

坑 5:没有 Hooks 就没有可观测性

我们最初的系统没有 hook 机制。结果是:

  • 想加审计日志?改 RuntimeKernel 的 RunTurn
  • 想在 tool 执行前做安全检查?改 ToolDispatcher
  • 想在用户提交 prompt 时做内容过滤?改 API handler

每个需求都要改核心代码,每次改动都有引入 bug 的风险。

claude code 的 hook 系统设计得很好:定义几个生命周期事件(PreToolUse、PostToolUse、PromptSubmit、SessionStart),允许注册回调函数。回调可以 block 操作、注入额外上下文、或者修改输入。

关键设计点:

  • Hook 可以返回 Block: true 来阻止操作(权限拦截)
  • Hook 可以返回 AdditionalCtx 来注入额外上下文到 prompt
  • Hook 支持条件匹配(if: "Bash(git *)"),避免对所有 tool 调用都触发
  • Hook 支持异步执行(async: true),不阻塞主流程

如果你的 agent 系统没有 hook 机制,趁早加。否则每个横切关注点都会变成对核心代码的侵入式修改。

坑 6:小范围优化循环

这是一个心态问题,但它会严重拖慢开发进度。

场景:agent 在某个特定场景下选错了 tool。你调了一下 prompt,好了。第二天另一个场景又出问题了,再调。第三天第一个场景又回退了。

你陷入了一个循环:针对具体 case 反复调整 if-else 分支或正则表达式,每次修复一个问题就引入另一个问题。

规则:如果一个问题需要超过 2 次针对性修补,说明设计有缺陷

正确做法:

  1. 先写一个 eval case 复现问题
  2. 分析是 prompt / tool / policy / model 哪个维度的问题
  3. 在对应维度的注册接口层面修复(而不是在 agent loop 里加特殊逻辑)
  4. 用 eval case 验证修复效果,同时确保其他 case 没有回退

这需要一个 eval 框架。我们的做法是定义 YAML 格式的测试用例,包含输入、期望的 tool 选择、输出关键词、策略合规性检查。每次调优只改一个维度,跑 eval 对比成功率。

坑 7:Extension 反客为主

Extension(插件)机制的初衷是让非核心能力(监控集成、沙盒管理、代码生成)以可插拔的方式挂载。但如果没有严格的隔离约束,extension 会逐渐侵入核心架构。

我们见过的反模式:

  • Extension 直接修改 RuntimeKernel 的行为("我需要在 turn 开始时做一个特殊检查")
  • Extension 绕过 PolicyEngine 自己做权限判断
  • Extension 直接读写 Store 中的 session 数据

规则:Extension 只能通过 Capability Registry 注册能力,通过 Projection 发射事件。它不能反向主导 RuntimeKernel、PromptCompiler 或 PolicyEngine 的设计。

claude code 的 plugin 系统更复杂(支持注册 tools/hooks/skills/commands/mcpServers),但核心约束是一样的:plugin 通过 PluginRegistry 接口与宿主系统交互,不能直接访问内部状态。

坑 8:没有 Feature Flag 就不敢上线

Agent 系统的行为是概率性的。你改了一个 prompt,在测试环境跑了 100 次都没问题,上线后在某个边缘 case 上翻车了。

没有 feature flag,你的选择是:全量回滚。

claude code 大量使用 feature() 函数控制功能开关。新 tool、新 prompt 规则、新的权限模式,都可以通过 flag 灰度发布。出问题了关掉 flag,不需要回滚代码。

这在 agent 系统中尤其重要,因为 agent 的行为空间远大于传统 CRUD 应用。你无法在测试环境覆盖所有可能的输入组合。

坑 9:绕过注册机制的"快速修复"

赶 deadline 的时候,最诱人的做法是直接在 RunTurn 里加一段特殊处理逻辑。"先这样,后面再重构。"

后面永远不会来。

三个月后你的 RunTurn 函数有 500 行,其中 200 行是各种特殊 case 的处理。每次改动都要小心翼翼地避开这些地雷。

规则:如果现有接口无法满足需求,停下来,重新审视接口设计。宁可花半天扩展接口,也不要花 5 分钟加一个 hack。

总结:Agent 工程的核心原则

  1. 注册制优先:所有模块通过统一接口注册,不允许平行能力池或硬编码旁路
  2. Prompt 是建议,代码是约束:安全边界必须由 PolicyEngine 保证,不能依赖 prompt 自觉
  3. 从源头修复:不要在下游做字符串修补,让每个模块对自己的输出负责
  4. 一次只改一个维度:prompt / tool / policy / model 分开调优,用 eval 验证
  5. 超过 2 次修补就重新设计:局部优化循环是设计缺陷的信号
  6. 横切关注点用 Hooks:审计、安全检查、内容过滤不应该侵入核心代码
  7. 灰度发布用 Feature Flags:agent 的行为空间太大,不能全量上线赌运气
  8. Extension 不能反客为主:插件通过注册接口与系统交互,不能直接操作内部状态
相关推荐
UIUV1 小时前
Go语言入门到精通学习笔记
后端·go·编程语言
段小二1 小时前
Agent 自动把机票改错了,推理完全正确——这才是真正的风险
java·后端
itjinyin2 小时前
ShardingSphere-jdbc 5.5.0 + spring boot 基础配置 - 实战篇
java·spring boot·后端
Victor3562 小时前
MongoDB(91)如何在MongoDB中使用TTL索引?
后端
老王以为2 小时前
前端重生之 - 前端视角下的 Python
前端·后端·python
Victor3562 小时前
MongoDB(92)什么是变更流(Change Streams)?
后端
云边有个稻草人2 小时前
Docker部署KingbaseES数据库操作指南
后端
NineData3 小时前
NineData亮相香港国际创科展InnoEX 2026,以AI加速布局全球市场
运维·后端
码农BookSea3 小时前
Hermes 深度解析:自我进化的 AI 智能体新范式
后端·ai编程