做复杂 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
rust
H --> R["运行时:权限与生命周期"]
R --> X["恢复:重建已有事实"]
X --> M
这里最关键的不是箭头,而是每一层拥有不同的权威。
事实可以跨层传播。
定义权不能跟着事实一起复制。
三、一个问题为什么会横跨五层?
回头看那些需要同时修改 Prompt、Runtime、Tool、Evidence、Recovery 的问题,通常可以归结成三种原因。
- 同一个规则被复制了很多份
最常见的是 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?
现在我会连续问五个问题。
- 谁能证明这个事实?
不是谁"最方便判断",而是谁拥有原始事实。
例如:
工具是否执行成功
应该问 Host。
不是问模型,也不是从聊天文本猜。
- 其他层拿到的是事实,还是又重新推理了一遍?
好的结构:
Host → observation → Runtime 消费 observation
危险的结构:
Host 返回一段 JSON → Runtime 猜状态 → Evidence 再猜一次 → Recovery 再从文本推断一次
事实可以被投影。
不要被重复解释。
- 删除这一层,复杂度会扩散还是消失?
这是一个很好用的 deletion test。
如果删掉某个 adapter:
10 个调用方都必须重新实现复杂逻辑
说明这个 adapter 很可能有价值。
如果删掉之后消失的只是:
字段复制 schema mapping 状态翻译 alias 另一套测试
而真正的业务 owner 本来就在下游,那么它可能只是一个 shadow owner。
- 失败以后,能不能指出一个主要责任方?
成熟系统应该逐渐能说:
Provider transport failure Tool argument failure Source authorization failure Evidence coverage failure Publication failure Persistence failure Model semantic failure
而不是所有问题最终都落成:
Agent 回答失败。
故障名称越具体,往往说明 owner 越清楚。
- 修复这个问题时,是在增加规则,还是删除歧义?
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 架构逐渐成熟的一个重要信号:
不是系统没有复杂度,而是每种复杂度终于找到了自己应该待的地方。