一个 Bug 为什么要改五层:Agent 架构如何从跨层修补走向局部 Owner

10 阅读15分钟

做复杂 Agent 一段时间后,会遇到一种很典型的工程症状:

一个看起来很小的问题,最后却要同时修改:

  • Prompt
  • Runtime
  • Tool
  • Evidence
  • Recovery

比如用户问了一个需要查资料的问题,最后答案里把一个搜索摘要误认为已经读过的正文。

第一反应可能是改 Prompt:

«告诉模型,搜索摘要不等于正文。»

但很快会发现只改 Prompt 不够。

Tool 返回的结果要区分 search hit 和 read result;Evidence 要记录到底读过什么;Runtime 要知道哪些来源真的参与了当前回答;Recovery 不能在恢复时把已经失效的来源重新当成有效证据。

最后,一个“不要把摘要当正文”的问题,横跨了五六层。

这种现象很容易被理解成:

«Agent 系统本来就复杂,跨层修改很正常。»

但最近一轮架构收敛让我越来越觉得,事情并不完全是这样。

复杂系统需要很多层,但一个具体事实不应该同时拥有很多 owner。

架构成熟的一个重要标志,不是层越来越完善,而是:

«同一种问题发生时,修改半径越来越小。»


一、分层不等于职责已经清楚

Agent 架构通常很容易画出一张漂亮的图:

flowchart LR U["用户请求"] --> M["模型"] M --> T["工具"] T --> R["运行时"] R --> E["证据"] E --> M R --> C["恢复与发布"]

每一层似乎都有明确职责。

模型负责推理,工具负责能力,运行时负责执行,证据负责可信状态,恢复负责断点续跑。

但真正写代码时,问题经常不是“有没有分层”,而是:

«同一个事实到底由谁说了算?»

假设系统里有一个事实:

«“这个网页内容能不能支持当前结论?”»

它可能同时出现五种表达。

Prompt 里写:

只有真正读取过页面,才能用正文支持结论。

Tool 返回:

{ "sourceRef": "xxx", "status": "partial" }

Runtime 保存:

read completed

Evidence 再维护:

sourceVerified = true

Recovery 为了下一轮继续,又投影成:

{ "previousSources": [...] }

如果这五个地方都在重新解释“这个来源到底意味着什么”,系统虽然有五层,实际上却有五个半独立的 owner。

于是任何一个边界调整,都必须横跨整个系统修改。

这才是很多 Agent 架构开始变重的真正原因。


二、真正应该局部化的,不是代码,而是事实的权威

这里的 locality,不只是“相关代码放在同一个目录”。

更重要的是:

«某一类事实,只允许一个地方拥有定义权;其他层只能消费它的投影。»

比如一次 Tool 调用是否真的发生过。

模型可以说:

我想读取这个页面。

但模型不能定义:

这个页面已经读取成功。

Tool Host 才知道调用有没有执行。

同样:

模型可以判断:

这个证据可能足够回答问题。

但它不能定义:

这个 sourceRef 当前仍然拥有读取权限。

这是 Runtime / Host 才能证明的事实。

反过来也一样。

Runtime 可以证明:

三个来源都已经成功返回。

但它不能因此推出:

三个来源已经足够回答用户的问题。

后者仍然是语义判断。

所以真正稳定的结构不是简单的:

Model Runtime Tool Evidence Recovery

而是:

flowchart TD Q["用户问题"] --> M["模型:语义判断"] M --> T["工具:声明能力"] T --> H["Host:记录实际发生的调用"] H --> E["证据:保存可验证事实"] E --> M

H --> R["运行时:权限与生命周期"]
R --> X["恢复:重建已有事实"]
X --> M

这里最关键的不是箭头,而是每一层拥有不同的权威。

事实可以跨层传播。

定义权不能跟着事实一起复制。


三、一个问题为什么会横跨五层?

回头看那些需要同时修改 Prompt、Runtime、Tool、Evidence、Recovery 的问题,通常可以归结成三种原因。

  1. 同一个规则被复制了很多份

最常见的是 Prompt 和 Runtime 都在维护同一条规则。

例如:

基金比较需要注意产品关系、指数关系和持仓重叠。

如果所有 Research 请求无论是否研究基金,都把这套规则放进系统 Prompt,那么 Runtime 或 request builder 就开始承担一个额外责任:

«判断什么领域的指导应该被装进去。»

很快又需要知道:

  • 用户是不是在问基金;
  • 当前 Skill 是什么;
  • Tool 有没有相关能力;
  • 上一轮是不是已经读过基金数据;
  • Recovery 后这套指导还要不要保留。

一条领域 Prompt 最终开始影响 request builder、capability、history 和 recovery。

问题已经不是 Prompt 太长,而是:

«领域知识的 activation 没有明确 owner。»

最近我们把这一类规则改成:

通用 Research 请求 → 只有通用原则

激活 Fund Skill → 加载基金语义

实际完成 Portfolio read → 后续轮次保留 Portfolio 相关指导

工具只是“可用” → 不自动激活领域语义

也就是说,领域语义不再由几个地方分别猜。

它只来自两种有明确来源的事实:

Skill declaration 或 真实 Host participation

一次实际测量里,序列化请求大小分别出现:

  • 通用 Research:减少约 31%
  • Fund 场景:减少约 12%
  • Portfolio 场景:减少约 24%

这些数字只能证明请求确实变小了,不能直接证明 token、延迟、成本或回答质量改善。

但更重要的变化其实不是节省了多少字符。

而是:

«一个领域规则为什么出现,现在终于可以回答清楚了。»


四、把 Authority 压回真正拥有事实的地方

另一个典型问题是“来源能不能用”。

早期很容易出现这样的推导:

Tool 可见 → 来源可访问 → 来源可读取 → 来源已经被读取 → 来源可以支持回答

这条链里每一个箭头其实都不成立。

例如一个搜索结果返回:

{ "title": "某基金年度报告", "snippet": "...", "sourceRef": "source-123" }

它最多证明:

«搜索阶段发现了这个结果。»

它不能证明正文已经读取。

更不能证明恢复到下一轮后,这个 "sourceRef" 依然拥有当前用途的授权。

所以后来更合理的做法不是继续给模型加一句:

请谨慎使用搜索结果。

而是重新定义事实归属。

Search

负责发现:

我找到了这个候选来源。

Read

负责产生:

Host 实际返回了这一段内容。

Evidence

负责保存:

本次实际交付了哪些内容、 覆盖了哪些页、 对应什么来源和时间。

Runtime

负责判断:

当前调用是否还具有权限、 scope 是否有效、 source handle 是否仍可使用。

Model

最后才能判断:

这些已经返回的内容是否足够支撑某个结论。

这样一来,“来源真假”就不再是一个跨五层的模糊布尔值。

它被拆成几种完全不同的事实。

这也是 locality 一个很重要的表现:

«不要追求一个万能状态,而要让每个 owner 保存自己真正知道的事实。»


五、答案生成、用户可见和持久化,也不是同一个事实

另一个曾经非常容易跨层污染的问题,是回答交付。

直觉上我们很容易写成:

answer = success / failed

但对于流式 Agent,这个状态太粗了。

一次真实运行可能是:

模型已经生成正文 ↓ 正文已经发送给用户 ↓ 后台保存正文 ↓ 来源元数据补写 ↓ trace 最终结算

假设第三步之后,来源认证或后台诊断失败。

系统应该怎么办?

如果“回答成功”只有一个总状态,一个后置失败就可能反过来吞掉已经有效交付的正文。

于是一个本来属于:

source metadata

的问题,最后修改到了:

publication rendering runtime persistence recovery

更稳定的方式是把不同事实分开:

模型是否生成正文 用户是否已经看到正文 正文是否 durable 来源状态是否完整 整个 research 是否 complete

这些状态彼此相关,但没有谁可以冒充另外一个。

于是:

«来源补充失败,不应该自动删除已经允许展示的正文。»

同样:

«用户看到正文,也不能冒充整个 Research 已经完整完成。»

这看似只是状态模型变细了。

但它带来的真正架构价值还是 locality:

回答展示的问题回到 Delivery; 持久化的问题回到 Persistence; 来源的问题回到 Evidence; 运行完成的问题回到 Runtime。

以前一个 failure 可以在系统里四处传播。

现在更容易回答:

«到底是谁失败了?»


六、Recovery 最容易成为第二个“万能 owner”

Agent 一旦支持长任务和恢复,Recovery 很容易迅速膨胀。

因为恢复似乎什么都要知道:

用户原始问题 模型之前的判断 工具调用结果 来源 预算 run id publication 状态 remaining actions research needs

于是最简单的实现往往是:

«把上一次 Runtime 状态尽量完整地重新塞回模型。»

这又创造了一份系统状态镜像。

模型下一轮不仅要继续思考,还需要重新理解:

上次运行到哪里 还有多少预算 哪些 publication 已经发生 哪些内部 ID 有什么意义 哪些恢复状态需要维护

最近我们做的一个重要调整,是把恢复上下文拆成两类。

模型真正需要的语义材料

例如:

用户原始问题 此前已经回答的内容 实际读取过的来源 日期和覆盖范围 尚未解决的澄清问题

Runtime 自己的运行账本

例如:

runId budget ledger publication bookkeeping internal recovery state

第二类信息不再因为“恢复需要”就自动进入模型上下文。

Recovery 的职责也因此变窄:

«恢复已有事实,而不是让模型重新接管 Runtime。»

这是我现在越来越重视的一条原则:

«恢复能力越强,越要警惕 Recovery 变成系统第二个大脑。»


七、Locality 不等于把所有规则都做成硬代码

这里还有一个很容易走到另一个极端的问题。

当我们开始强调:

事实有 owner 状态要局部化 Runtime 应该确定性

很自然会产生一个冲动:

«那是不是所有事情都应该由 Runtime 保证?»

不是。

例如 Research 过程中,我们希望模型持续维护当前研究问题:

还缺什么? 哪些问题已经解决? 新证据是否推翻了原来的判断? 是否需要重新打开某个问题?

这类 "research needs" 很重要。

但它仍然是模型的研究判断。

Runtime 可以:

保存 needs 提供 set_research_needs 恢复 needs 记录模型有没有更新

但 Runtime 不适合写一个机械规则:

每两轮必须更新 needs,否则禁止继续。

因为:

没更新

并不等于:

研究质量差。

反过来,频繁更新也不等于真的 adaptive。

所以最近我们的方向更接近:

Prompt / Skill → 明确要求模型主动复盘研究问题

Runtime → 接受并保存这些变化

Dogfood / Evaluation → 判断模型实际维护得好不好

这里再次体现了 locality:

«Runtime 对“有没有发生更新”有权威;模型对“该不该更新、怎么更新”负责语义;评测负责判断更新有没有价值。»

没有任何一层需要假装自己知道全部答案。


八、我现在更关注一个指标:修改半径

以前做 Agent 架构 review,我会重点看:

模块是否清晰 接口是否稳定 测试是否完整

现在我会额外问一个问题:

«一个普通缺陷出现时,需要修改多少个 owner?»

这个指标可以叫“修改半径”。

它不是正式的工程度量,只是一种很有用的架构直觉。

假设出现:

«“Web search 的摘要被当成正文。”»

如果修复需要:

改 Prompt 改 Tool schema 改 Runtime classifier 改 Evidence status 改 Recovery projection 改 Publication gate

我会高度怀疑:

这个概念没有真正的 owner。

理想情况下,我们应该能够追到一个更具体的问题,例如:

Search result 的 observation 类型表达错了

那么主要修复点应该就在:

Search / Evidence seam

其他层最多更新契约测试,而不是重新实现一遍判断。

再比如:

«“已经展示的正文因为后台 source metadata 失败而消失。”»

如果最终定位到:

Publication renderer 错误地把 source certification 当成 answer visibility 的前置条件

那修复就应该集中在 Delivery owner。

不是再去 Prompt 里告诉模型:

请尽量确保来源完整后再回答。


九、怎样判断一个问题是不是已经有真正的 owner?

现在我会连续问五个问题。

  1. 谁能证明这个事实?

不是谁“最方便判断”,而是谁拥有原始事实。

例如:

工具是否执行成功

应该问 Host。

不是问模型,也不是从聊天文本猜。


  1. 其他层拿到的是事实,还是又重新推理了一遍?

好的结构:

Host → observation → Runtime 消费 observation

危险的结构:

Host 返回一段 JSON → Runtime 猜状态 → Evidence 再猜一次 → Recovery 再从文本推断一次

事实可以被投影。

不要被重复解释。


  1. 删除这一层,复杂度会扩散还是消失?

这是一个很好用的 deletion test。

如果删掉某个 adapter:

10 个调用方都必须重新实现复杂逻辑

说明这个 adapter 很可能有价值。

如果删掉之后消失的只是:

字段复制 schema mapping 状态翻译 alias 另一套测试

而真正的业务 owner 本来就在下游,那么它可能只是一个 shadow owner。


  1. 失败以后,能不能指出一个主要责任方?

成熟系统应该逐渐能说:

Provider transport failure Tool argument failure Source authorization failure Evidence coverage failure Publication failure Persistence failure Model semantic failure

而不是所有问题最终都落成:

Agent 回答失败。

故障名称越具体,往往说明 owner 越清楚。


  1. 修复这个问题时,是在增加规则,还是删除歧义?

Agent 工程特别容易通过:

再加一句 Prompt 再加一个 fallback 再加一个 runtime check

解决局部问题。

这些补丁短期可能有效,但也可能让同一概念多一个 owner。

真正好的重构经常不是新增能力,而是删除:

重复判断 重复状态 重复 mapping 重复语义

让剩下的那个 owner 变得更深。


十、好的架构不是没有跨层数据,而是没有跨层权威漂移

Agent 永远会是跨层系统。

模型必须消费 Tool result。

Runtime 必须知道 Tool execution。

Recovery 必须读取持久化状态。

Evidence 必须进入下一轮模型上下文。

所以目标绝对不是:

«每一层互相隔离,互不认识。»

真正应该消灭的是另一件事:

«同一事实经过每一层时,都被重新解释一次。»

我现在更喜欢这样的结构:

Owner 保存事实 ↓ 派生稳定 projection ↓ 其他层消费

而不是:

A 判断一次 ↓ B 再猜一次 ↓ C 再补一个规则 ↓ D 为恢复重新解释

这也是“深模块”在 Agent 系统里很具体的一种表现:

一个模块真正有价值,不是因为它包装了一层接口。

而是因为:

«大量复杂度进入它之后,可以在那里结束。»


十一、为什么这件事对 Agent 尤其重要

普通业务系统当然也会遇到职责漂移。

但 Agent 会把这个问题放大。

因为 Agent 天生同时包含:

自然语言语义 结构化协议 动态工具 外部数据 权限 长生命周期 恢复 流式交付 模型不确定性

当某个概念 owner 不清楚时,我们特别容易使用自然语言把缝补起来。

比如:

Prompt 提醒一下

是最便宜的。

然后发现不稳定:

Runtime 再兜一下

再发现恢复后失效:

Recovery 再补一下

最终每一层都有一点“聪明逻辑”。

系统没有明显错误。

但任何人想回答:

«这个行为到底是谁定义的?»

都需要同时打开五个模块。

我现在越来越觉得:

这是 Agent 架构真正危险的复杂度。

不是代码多。

不是 Tool 多。

甚至不是 Prompt 长。

而是:

«知识和权威没有 locality。»


结语

我们最早做 Agent 架构治理时,核心问题是:

«Runtime 已经知道的,不要再让模型维护。»

后来又进一步变成:

«不属于 Runtime 的能力,也不要因为 Runtime 需要治理,就顺手让 Runtime 成为它的 owner。»

现在我觉得还可以再往前走一步:

«一个事实,不仅要放在正确的层,还应该尽量只有一个真正的 owner。»

Prompt 可以指导模型。

Runtime 可以执行边界。

Tool 可以提供能力。

Evidence 可以保存观察事实。

Recovery 可以恢复已经发生的事情。

它们当然需要协作。

但协作不意味着共同拥有同一个事实。

当越来越多以前需要同时修改 Prompt、Runtime、Tool、Evidence、Recovery 的问题,能够被压缩成:

这是 Evidence 的问题。

或者:

这是 Delivery 的问题。

或者:

这是模型研究策略的问题,Runtime 不应该修。

系统实际上发生了一个比“测试更多”“协议更严”更重要的变化:

«复杂度开始有地方可以结束了。»

我现在会把这件事看成 Agent 架构逐渐成熟的一个重要信号:

不是系统没有复杂度,而是每种复杂度终于找到了自己应该待的地方。