1. 常驻 Agent 翻车实录没有分层之前我经历了什么做了快两年常驻 Agent我最大的感受是它不像是写一个程序更像是在养一个会自主行动的员工。普通程序崩了会告诉你在哪一行常驻 Agent 崩了往往是因为它在某个凌晨三点自己决定调了一个你没写过的工具。我最早的项目就是一句话的 while True 循环加 prompt模型记不住上次做了什么工具返回格式变了直接带崩整个会话栈prompt 改完上线想回滚连之前那版写的是什么都找不到。折腾了几轮之后我开始怀疑问题不在模型智障而在运行底座。后来我把常驻 Agent 拆成了四层——声明层、校验层、版本层、预演层并且用 72 条命令把每一层到底给什么保证全部实测了一遍。这篇文章就是那份实测记录以及踩坑之后我才想明白的分层逻辑。如果你正在做 AI Agent 开发尤其是 Agent 要常驻运行、对接外部工具、自动执行任务的项目这篇文章应该能帮你少走不少弯路。1.1 三个真实的翻车点第一个翻车点不可回滚。有一次我在线上顺手改了一版 prompt改完之后效果确实变好了但三天后出现了一个诡异的行为——Agent 开始频繁给同一个用户发相似内容。我第一反应是把 prompt 改回去结果发现我根本没留备份。我甚至想不起来上一版 prompt 写了什么因为它是直接写在代码字符串里的提交代码的时候也没有单独标记。那一次我被迫停服半天靠 git 历史倒推才找回改动。常驻 Agent 的可变资产实在太多了prompt、工具描述、技能包、知识库快照任何一个都可能是行为突变的根源不版本化等于裸奔。第二个翻车点输出不稳定。我的 Agent 依赖一个第三方工单系统它的 API 接口那天突然多返回了一个字段。模型收到这个陌生字段之后开始胡言乱语把工单优先级全部标记成低。更麻烦的是这个错误结果没有触发任何异常因为格式上它仍然是合法的 JSON。大家总说 AI 输出不稳定但常驻场景里真正不稳定的不只是模型输出还有工具返回、缓存数据、记忆状态。这些数据一旦进了 Agent 的状态机就会像病毒一样扩散。第三个翻车点越权调用。某天深夜Agent 在某个分支里调用了一个我根本没打算让它用的内部 API。它不是被攻击了而是被 prompt 拐带了。当时的 prompt 里有一句为了完成任务你可以使用一切可用的接口模型就真的去探测并调用了那个内部接口。常驻 Agent 的运行时间是小时级别、天级别随机性会被时间无限放大。一个偶发的 prompt 解释偏差在普通对话里只是答非所问在常驻 Agent 里就是一次真实的线上事故。1.2 四层不是功能拆分是保证拆分踩完这些坑之后我想明白了一件事分层的本质不是把代码拆成几个模块而是把运行保证拆开。声明层保证能力可审计校验层保证数据不出格版本层保证改动可回滚预演层保证变化可验证。这四个保证分别对应了我前面四个翻车点越权调用归声明层管输出不稳定归校验层管不可回滚归版本层管而预演层是用来防止新改动引入回归的。很多人讨论 harness 和 agent 的区别我觉得最直白的理解是agent 负责自由发挥harness 负责给这四类保证兜底。你的 Agent 可以很聪明但承载它的轨道必须是确定的。1.3 72 条命令是怎么实测的为了让结论可复现我改造了一个早期的 Agent 原型给每一层封装了命令入口。整套验证共 72 条命令声明层 12 条、校验层 16 条、版本层 22 条、预演层 18 条外加 4 条横切冒烟命令。每一条命令都指向一个具体的失败场景不是随机冒烟。后面每一章我会给出代表性命令、实测结果以及这一层真正能保证什么、不能保证什么。2. 声明层给什么保证能力边界和权限变得可审计2.1 声明层在声明什么声明层的核心是把 Agent 的能力边界从代码里抽出来变成一份显式的、可阅读、可比较的声明文件。声明内容包括四个部分能力集合这个 Agent 能调用哪些工具不能调用哪些工具。权限集合每个工具允许操作的资源范围比如只读还是可写、限定哪些项目、哪些用户。行为约束哪些动作是明确禁止的比如禁止删除、禁止发送消息、禁止访问外部网络。知识边界数据来源范围包括知识库白名单、可读取的文档目录。早期我图省事直接在代码里写tools [search, send_email, ...]然后让 Agent 自己去翻工具列表。这种做法最大的问题是工具的可见性和可调用性完全绑在代码里你没法在运行前回答这个 Agent 到底能干什么。我现在的做法是把声明写成一个 YAML 文件Agent 启动时只从这个文件加载工具和权限配置。文件长这样agent: name: customer-support description: 自动处理客户工单仅支持查询和标准回复 tools: - name: ticket.query permissions: [read] scope: project_id:demo - name: ticket.reply permissions: [write] scope: project_id:demo - name: internal.api.invoke enabled: false constraints: forbidden_actions: - ticket.delete - user.impersonate这段声明的意思很明确这个 Agent 只能读工单、写工单回复而且只限定在 demo 项目里。internal.api.invoke 被显式设为禁用ticket.delete 出现在禁止动作清单里。只要运行时严格遵守这份声明越权调用这类事故就能从根上掐掉。2.2 12 条命令实测声明层到底保证了什么我给声明层封装的命令路径是agentctl declare12 条验证命令里比较有代表性的几条是这样# 1. 导出当前 Agent 的完整能力声明 agentctl declare export --format json current.json # 2. 校验声明内引用的工具是否真实存在 agentctl declare verify --strict # 3. 对比两个版本声明的差异用于排查这个 Agent 能做什么变了没有 agentctl declare diff --from v1.2.0 --to v1.3.0 # 4. 尝试调用一个声明之外的接口验证是否被拒绝 agentctl declare probe --tool internal.api.invoke # 5. 查看当前声明的策略模式是默认拒绝还是默认允许 agentctl declare policy --mode实测的结论让我挺意外很多问题不是出在 Agent 不遵守声明而是出在声明本身有漏洞。比如工具列表里声明了一个搜索工具但声明里没有限制搜索范围Agent 就能用它去搜内网资源。所以verify --strict非常重要它会检查每个工具是否带上了 scope 限制。12 条命令跑下来的结果是所有声明之外调用的探测请求都被拒掉了声明层的保证是有效的。它给出的核心保证可以总结为能力边界可审计、权限变更可 diff。你可以随时问一个常驻 Agent你会做什么、你不会做什么答案不是模型自己想的而是声明文件说了算。2.3 声明文件写得好没用运行时必须强制我踩过这个坑。早期声明文件写得很漂亮但运行时 Agent 真正调用的工具是在代码里动态注册的声明形同虚设。后来我把 Agent 的工具加载机制改成只从声明配置加载代码里不允许出现任何额外的add_tool调用。谁想在运行时加工具就必须先改声明文件再走发布流程。这一点其实就是 harness 的典型职责把模型的能力面约束在轨道之内。声明层能保证你没声明就干不了但它不能保证你声明了也不会干错。一个工具合法但不该在这种场景下调用声明层管不住那是校验层和预演层的事。3. 校验层给什么保证模型输出和工具结果不再是无底洞3.1 常驻 Agent 的三个数据信任边界校验层解决的是数据可信问题。在常驻 Agent 里外部数据源有三个每一个都不能默认信任第一是用户输入。这个大家都会做防止 prompt 注入、防止用户传超长文本常规做法都有。第二是模型输出。这个是最容易被忽略的。你以为模型会严格按 JSON 格式输出但模型在 token 概率上自由发挥的空间太大了。少一个逗号、多一个字段、枚举值拼错都会让下游状态机走进死胡同。第三是工具返回结果。第三方 API 不会因为你是 AI Agent 就保证返回结构稳定。字段多一个、少一个、类型从字符串变成数字、直接返回 null这些都是常态。三个信任边界中工具返回和模型输出是大多数 Agent 项目校验做得最弱的地方。因为用户输入有现成的框架而模型输出和工具返回的校验你得自己设计 schema。3.2 16 条命令实测校验层扛得住什么我给校验层封装了一套注入测试命令大概长这样# 注入一个缺字段的模型输出观察下游是否崩溃 agentctl validate inject --phase output --schema ticket.schema --payload missing_field.json # 工具返回 null 时校验层是否把错误变成结构化异常 agentctl validate inject --phase tool_result --payload null_result.json # 工具返回超大对象时是否触发最大体积保护 agentctl validate inject --phase tool_result --payload oversize_10mb.json # 模型输出了非法枚举值是否被显式拦截 agentctl validate inject --phase output --schema priority.schema --payload invalid_enum.json # 某个工具调用超过 5 秒未返回是否触发超时与取消 agentctl validate inject --phase tool_call --timeout 5s16 条命令跑下来的结论是校验层能把所有非法数据变成显式的错误而不是让错误静默扩散。但这里有个关键认知显式错误不等于正确处理校验层的价值是让系统不崩溃、让错误可以被定位、可以走重试或降级逻辑而不是凭空把错误数据变正确。3.3 校验层最容易踩的两个坑坑一只校验用户输入不校验模型输出。很多 Agent 项目把大量精力花在用户输入的 prompt 注入防护上却对模型输出的合法性放水。其实对常驻 Agent 来说模型输出才是最大的不可控源因为它每天都在变。我见过一个 Agent 因为模型偶尔把status字段从 open 输出成 OPEN导致整个工单流程卡死整整跑了一天才被发现。校验输出的 schema 应该做到和校验用户输入同等严格。坑二把校验做成重试风暴。给工具调用做校验和超时是好事但如果不区分错误的可重试性就会出大问题。比如工具返回字段类型错误这通常是服务端接口变更了你重试一万次也没用。正确的做法是给错误打上标记retryable: true或retryable: false只有可重试的错误才允许触发重试并且重试次数要封顶。常驻 Agent 是 7x24 小时跑的一个无限重试的循环会把你下游服务打到熔断。4. 版本层给什么保证prompt、技能、配置都能回到过去4.1 版本层管哪些资产普通后端服务要版本化的是代码和数据库 schema常驻 Agent 要版本化的资产就多得多。我现在的目录结构是这样的agent-assets/ prompts/ triage.md summarize.md tools/ ticket.query.yaml ticket.reply.yaml skills/ email-drafting.md knowledge-lookup.md configs/ agent.yaml memory-schema.jsonprompt 文件、工具定义、技能包、声明配置、记忆库的 schema、甚至知识库的快照全部进版本仓库。一行 prompt 的改动就是一次发布必须可追溯、可回滚、可比较。4.2 22 条命令实测回滚真的能落吗版本层最重要的验证是坏发布之后能不能真正回到过去。我做了这样一次测试先发布一版故意写坏的 prompt——让 Agent 把所有工单都标记为紧急然后立刻执行回滚命令。实测过程中我最深的体会是回滚远没有想象中简单。最开始我以为只要 git 恢复文件就行结果发现回滚代码并不代表回滚 prompt因为 Agent 进程还跑在内存里旧 prompt 早就被加载进去了。这就是版本漂移代码回滚了运行态没有变线上行为还是坏的。我后来加了版本指纹来解决这个问题。Agent 每次启动时会把当前所有资产版本的 hash 写入运行态# 查看当前运行的 Agent 实际加载的所有资产版本 agentctl version fingerprint # 回滚某个资产到指定版本 agentctl rollback --asset prompts --to v1.0.3 # 回滚后强制刷新运行态并校验指纹是否匹配 agentctl rollback --asset prompts --to v1.0.3 --reload --verify-fingerprint22 条命令跑下来结论是版本层真正能保证的是所有资产可追溯、可回滚且运行态和版本指纹强绑定。没有指纹校验的回滚等于没回滚。4.3 回滚不是复制旧文件是恢复整个运行态常驻 Agent 比普通服务多了一个麻烦它是有记忆的。prompt 回滚了但记忆库里可能还存着坏版本运行期间写入的数据。如果旧版 prompt 不认识新版记忆结构Agent 一上线就会表现异常。我的做法是给记忆 schema 也纳入版本管理回滚 prompt 时同时校验记忆 schema 的兼容性。不兼容就拒绝回滚宁可停机处理也不默默降级。这里也牵涉到 Agent 记忆设计的一个原则记忆结构必须显式版本化不要让它变成隐式约定否则你总有一天会在回滚时被它咬一口。5. 预演层给什么保证干跑一遍压掉线上回归事故5.1 预演层不是测试框架是切换运行副作用预演层的核心思想很简单把 Agent 的副作用层替换成模拟实现保留推理和工具调用决策但不产生真实副作用。我给工具执行器加了一个 mode 参数agentctl dryrun --mode dryrun --prompt-file prompts/triage.md --input 工单: 用户投诉登录失败 agentctl dryrun --mode shadow --prompt-file prompts/triage.md --input 工单: 用户投诉登录失败dryrun 模式下Agent 会完整走一遍工具选择的流程但工具调用返回的是预置好的模拟结果不会真的发消息、改数据库、调外部 API。shadow 模式则更进一步真实请求会同时发给新老版本新版本的结果只记录不落库用来观察行为差异。很多人觉得预演层就是写单元测试其实不是。单测测的是函数逻辑预演层测的是 Agent 在完整上下文里的决策行为——它看到这段输入后会选哪个工具、会生成什么结构化动作。这个层面的回归问题是单测发现不了的。5.2 18 条命令实测预演能压掉哪些事故我用 18 条命令做了四组回归测试7 条正常业务回归、3 条恶意输入、4 条外部服务超时、4 条回滚验证。统计下来预演层发现了两类之前线上踩过的坑一类是提示词漂移。新版 prompt 在所有正常用例上表现良好但在用户一次性提两个问题的边缘用例上Agent 只处理了第一个问题。这种问题通过代码 review 很难发现但干跑一遍立刻暴露。另一类是工具选择的回归。换了工具描述之后Agent 在某些场景下不再选择最合适的工具而是退回到了一个泛化工具。这类问题在没有真实副作用的环境里更容易观察因为你可以放慢速度看它的决策过程。但预演层也有明确的边界。它抓不住三类问题真实并发下的资源竞争、下游服务的真实脏数据、以及长时间运行才出现的内存泄漏。干跑通过是必要不充分条件这个认知很重要不然你会把预演层当成免死金牌。5.3 影子模式和回放模式的取舍预演层有两种落地形态。回放模式成本低用历史真实数据喂给新版本观察输出和动作是否合理适合日常每次发布前跑一遍。影子模式成本高把线上真实请求同时复制给新版本实例只在后台观察不落库适合大版本升级前的灰度验证。我的建议是先从回放开始把历史上出过事的那批请求沉淀成 regression set每次改 prompt、改工具、改配置都先回放一遍。等 Agent 到了要动外部写操作的阶段再上影子模式。影子模式最大的好处是能暴露真实流量分布下的行为变化但要做好状态隔离不然新版本和旧版本共享同一份记忆影子就变成干扰了。6. 四层的分工关系与我的落地顺序6.1 一条请求经过四层时发生了什么把这四层放在一条真实的 Agent 请求链路里看会更清楚它们各自的角色。假设我的客户支持 Agent 收到一条工单消息声明层先检查这条消息涉及的客户、项目、工单操作是否在当前声明允许的范围内。超出范围直接拒绝执行。校验层接着检查模型解析出的工单意图、目标、参数是否符合 schema。工具返回的工单数据结构是否正确字段类型、枚举值、体积是否合法。版本层全程记录这次运行的 Agent 代码版本、prompt 版本、工具定义版本、记忆 schema 版本全部写进指纹。出问题的时候能精确回答这次行为是哪一版资产产生的。预演层在重大动作前介入如果 Agent 决定执行一个写操作比如给客户发送一封回复邮件在真实发送前可以要求先走一遍干跑确认工具选择、参数构造和预期输出都正常。这一套走下来Agent 的自由发挥空间被压缩在了一个可控的范围内模型可以自由推理但它的行动边界、数据格式、版本归属、变更验证都被底座锁死了。6.2 用一张表看四层给的保证与边界层回答的核心问题给的保证最常见失败模式实测验证入口声明层它能做什么、不能做什么能力可审计、权限可 diff声明文件与运行时不一致agentctl declare校验层数据格式是否正确非法输入输出被显式拦截只校验用户输入忽略模型输出agentctl validate版本层当前跑的是哪个版本能否回去所有资产可追溯可回滚版本漂移、回滚不彻底agentctl rollback预演层改动在真实副作用发生前是否安全低成本发现回归以为干跑通过等于线上通过agentctl dryrun这张表是我做实测时贴在最前面的。每次改 Agent 之前我都会看一眼这次改动会影响哪一层的保证声明层要改声明文件校验层要补 schema版本层要加资产版本预演层要加回归用例。搞清楚这个就不会出现只改了 prompt 但忘了补校验这种半吊子发布。6.3 我的落地顺序先说一句我不想再裸奔四层全部做完当然理想但如果你是第一次给常驻 Agent 搭底座我建议按这个顺序来第一步先做声明层和校验层。声明层解决它能干什么的边界问题校验层解决它收到的数据可不可信的问题。这两层加在一起两周内就能完成收益却是立竿见影的——你至少不会再因为越权调用和脏数据而半夜爬起来修 bug。第二步在第一次被用户发现线上行为变了之前尽快把版本层补上。版本层的价值是在出问题那一刻体现的不是平时。没有版本层你连这版 prompt 到底改了啥都答不上来。第三步当 Agent 要触达外部写操作时再上预演层。只读查询可以缓一缓但一旦 Agent 会发邮件、会改库、会调生产接口预演层就不再是可选项了。如果你在做多 Agent 架构这四个层更要先搭好。因为多个 Agent 之间互相调用的复杂度会让单 Agent 时代的偶发问题变成必然问题。每个子 Agent 都先过一遍同样的底座再谈协作编排。6.4 最后给一条实操建议72 条命令的最后一组是 4 条横切冒烟命令用来确认四层之间没有互相破坏。很多 Agent 项目死在层与层的假设冲突上声明层允许的工具校验层的 schema 里没定义版本层回滚的资产预演层的回归集没覆盖。所以我建议你每做完一层改动都跑一遍完整的 72 条命令而不是只跑受影响的那个子集。我现在每次发布前都会跑完整套大概需要十几分钟。这套命令跑完我才能放心说一句这次改动我可以让它上线。分层做下来我最大的感受是Agent 依然是那个会偶尔自由发挥的 Agent但我不再害怕它自由发挥了因为我能回答四个问题你允许做什么你这次做对了吗你用的是哪一版你打算实际动手之前先试一遍吗如果你的 Agent 也到了需要常驻运行、需要对接外部工具的阶段强烈建议从这四层里挑一层先做起来。这 72 条命令不是为了凑数字每条背后都对应一种具体的失望——我对旧版 Agent 的失望。