别再让大模型当状态机:一次 Agent 协议减负实践

0 阅读12分钟

现在还有哪位前端不想做一个自己的 agent 呢?

现在买基金这么火,为了更深入的了解agent,为了投资理财争取早日退休,为了..我开始开发了一个研究我的基金持仓的 agent。于是开始了我一个星期烧 30 亿 token 的日子。

在这里我分享一次我的交学费的经历。

在一次重构 Agent 后,我确定了一件事:

运行时已经知道的,不要再让模型维护。

但这条原则必须和另一条一起使用:

运行时不知道的,不要因为能写规则,就假装它知道。

前者删除模型维护的状态镜像,

后者阻止运行时越界成为语义裁判。

本文讨论的不是让系统少存状态。

恰恰相反,主机仍应严格持有持久化状态。

要删除的是模型对这些客观系统事实的重复描述。

主线可以概括为:

运行时维护客观系统事实,模型负责语义理解和下一步提议。


一个严肃 Agent 里有两类复杂度

这里的场景是个人投资研究 Agent。

它可以读取授权范围内的组合、材料和公开资料,

形成研究结论,

但不能由模型直接执行投资操作。

一次简化流程是:

用户提出问题
→ 模型判断下一步需要什么信息
→ 运行时按权限执行工具调用
→ 模型根据工具结果继续研究或形成表达
→ 运行时校验客观边界并发布或结束

这类任务同时有两种复杂度。

第一种是语义复杂度:

用户真正关心什么
下一步最值得查什么
资料是否互相矛盾
是否需要澄清或补查
结论怎样解释

第二种是系统复杂度:

工具是否存在且获准
调用是否真正执行
结果是否真实返回并落库
引用是否仍在当前范围内
发布是否已经提交
本次运行是否已进入终态

两者不能因为都很复杂就交给同一方。

运行时(Runtime)是主机持有的执行与状态层。

它负责授权、工具执行、持久化、证据和数值权威、恢复、发布与终态。

模型(Model)负责理解、调查、比较、提出下一步和表达。

graph LR
用户问题 --> 大模型
大模型 --> Runtime
Runtime --> Tool
Tool --> 大模型
Runtime --> 回答

图中的箭头不表示模型拥有运行时状态。

模型看到的是当前允许它使用的工具和已返回的结果。

主机保存的是实际发生过的调用、结果和状态迁移。


AgentTurn 为什么会变成认知税

早期 Agent 往往从很轻的路径开始:

用户 → 模型 → 回答

接入工具、审计和恢复后,

系统自然会想知道模型这一轮计划做什么、缺什么、能否结束。

于是出现结构化的 AgentTurn

{
  "actions": ["search"],
  "gaps": ["还缺来源"],
  "completionClaim": false
}

它在早期并非错误。

运行时可以校验动作、记录回合并据此继续执行。

问题在于协议常常持续膨胀。

随着可靠性要求增加,

状态、缺口、权限、工具列表、发布状态和完成声明都会进入模型输出。

模型于是同时做两件事:

理解和研究
+
维护系统状态

后者往往只是状态镜像。

例如运行时已知某项能力是否获批、某次调用是否成功、发布是否提交、运行是否结束。

如果模型还在每一轮重复输出这些信息,

系统里就有两份状态:

模型描述的状态
运行时真实的状态

两者冲突时,最终只能以运行时为准。

因此模型那一份不是权威,

却仍需生成、解析、校验和纠错。

这就是认知税。

它至少带来三个问题:

  1. 镜像会漂移:运行时拒绝了权限,模型历史却仍认为工具可用。
  2. 模型注意力被协议一致性占用,而非下一步研究。
  3. 任一字段失配都可能把局部问题升级为整轮重试。

字段少不等于模型一定更聪明。

更准确的判断是:

只有承载模型独有语义信息的字段,才值得进入模型协议。

复杂字段还会提高组合输出正确的难度。

若把每个字段 95% 的正确率和字段间独立视为假设示例

二十个字段同时正确的概率是 0.95^20≈35.8%

这不是项目实测,

也不是对任何模型可靠性的结论。

它只说明:协议字段越多,越应谨慎要求模型承担彼此关联的系统状态。

谁拥有事实,谁维护事实

模型应该负责:

用户到底想解决什么
下一步最值得查什么
搜索词如何组织
资料是否冲突
是否值得继续调查
如何解释结论

运行时应该负责:

工具是否存在且获准
调用是否实际发生
工具结果是否返回并落库
发布是否实际提交
引用是否属于当前范围
预算和运行状态是否允许继续

前者需要开放语义判断。

后者是系统可以证明的客观事实。

所以原则是:

谁拥有事实,谁维护事实。

模型不是数据库,

也不是权限系统。


从完整回合描述到原生工具调用

旧路径是:

模型
→ AgentTurn JSON
→ 运行时解读整轮状态

更合适的路径是:

模型
→ 原生工具调用
→ 主机固化 ModelTurn
→ 运行时执行
→ 工具结果
→ 模型

变化不只是在接口层使用工具调用。

关键在于模型不再描述“系统现在完整是什么状态”。

它只提出下一步的语义动作。

例如模型可以先调用:

search(...)

读取结果后再调用:

read(...)

或者在满足发布条件时调用:

publish(...)

研究结束时提出:

finish()

协议从“请描述你的完整工作流状态”,

缩为“请做你认为合理的下一步”。

删除 AgentTurn 不等于删除 ModelTurn

删除的是模型生成并维护的整轮状态协议。

没有删除主机持有的、持久化的 ModelTurn 边界。

模型一次完整响应闭合后,

主机仍需记录:

本轮 ModelTurn 的内容
提出了哪些调用
调用的顺序
哪些调用已经执行
哪些结果已经落库
每次执行的回执

因此:

模型不维护状态
≠
系统不维护状态

系统状态反而应更严格。

恢复依赖的是实际发生过、已落库的事实,

不是模型上次声称发生过什么。


finish() 是结束提议,不是模型设置终态

旧协议中的:

completionClaim = true

很容易被误解为模型宣布任务完成。

更清晰的含义是让模型调用:

finish()

它只表示:

模型认为语义上的研究可以结束。

这是一项提议,

不是写入终态的权限。

运行时随后执行终态门控,

只检查它能客观验证的条件:

是否已有正式输出
当前引用是否仍有效
是否存在未结算调用
当前运行是否允许进入终态

运行时不应据此判断:

用户的问题是否回答得足够完整
风险是否讲得足够多
反例是否已经调查充分

这些是开放语义判断,

不是运行时持有的事实。

因此终态门控应当严格,

但不冒充全知的语义裁判。

它可以拒绝一个客观上不能安全结束的运行,

不能借“完成检查”替模型判断答案好不好。


权限、工具可见性与能力边界

动态能力是运行时职责最清楚的例子。

假设模型当前只能看到基础工具,

却在同一响应中提出:

1. 申请文档能力
2. 询问用户更多信息

运行时执行第一项后批准了文档能力。

下一轮可见工具可能新增:

search_document
read_document

此时第二项旧动作不应继续跨越边界执行。

原因不是运行时认为“询问用户”在语义上不合适。

而是一个可验证的客观事实:

旧工具契约 ≠ 新工具契约

能力获批意味着工具契约改变。

所以运行时应执行确定性的边界:

能力获批
→ 工具契约变化
→ 当前响应后续动作不再执行
→ 开启下一 ModelTurn
→ 模型在新工具集合下重新决定下一步

旧契约下提出的动作不得跨过新契约边界。

这保护的是调用产生时的权限与工具作用域,

不是预先规定模型下一步该搜索还是该追问。

工具可见性也应按同样方式分工。

运行时根据当前用户权限、任务限制、数据源状态和已批准能力,

派生本轮真实可见的工具集合。

模型无需维护一份 availableTools 状态镜像。

下一次请求直接得到真实的 tools[] 即可。

三个概念应明确分开:

权限 → 运行时持有
工具可见性 → 运行时派生
工具选择 → 模型决定

这既避免镜像漂移,

也使权限变化成为清晰的工具契约边界。


局部错误只应产生局部结果

重型回合协议常见的问题是:

一个字段无效
→ 整个 AgentTurn 无效
→ 修复
→ 重新生成完整协议

但工具调用往往可以分别判断。

例如:

调用 A 正确
调用 B 参数错误

更合理的执行方式是:

A 正常执行并持久化结果
B 返回安全、可归属的错误结果
开启下一 ModelTurn
模型自行决定是否修正 B

这里仍允许修复,

但修复是模型面对工具结果后的普通推理。

它不是运行时隐藏的第二套整轮协议。

局部错误隔离避免为了修一个参数,

重新生成整份世界状态。


历史压缩也不能改写主机事实

同样的原则也适用于历史压缩。模型可以摘要过去的回答,但用户原始约束不应被摘要模型改写后再当作系统事实。Runtime 已经保存了原始输入,就应该决定保留或省略,而不是把事实所有权转交给另一个模型。


四阶段演进

阶段形态得失
提示式 Agent用户 → 模型 → 回答自然,但工程控制弱
结构化 Agent模型结构化输出 → 运行时 → 工具获得校验、审计与恢复
重协议 Agent状态、权限和完成声明持续进入模型协议系统完整,但状态镜像变重
原生工具调用 Agent模型提议调用,运行时持有事实保留工程边界,删除模型状态镜像

最后一个阶段不是退回提示式 Agent。

它保留了持久化、审计、权限、恢复和终态门控,

只是不再要求模型维护这些系统事实。


运行时不应把一切写成规则

所有权重划后,很容易滑向另一个极端:

既然运行时可靠,是否应把所有判断都规则化?

答案是否定的。

以下问题即使可以被编码,

也不因此成为运行时客观知道的事实:

下一步最值得查什么
资料冲突时应追查哪一边
用户更关心收益还是风险
当前信息是否足以形成建议

代码能稳定执行,

不等于系统拥有这些语义判断的权威。

因此运行时可以强制执行显式权限、作用域、持久化、调用顺序和终态条件。

运行时不能根据自由文本的措辞,

伪装成客观规则去猜测意图、完整性、安全性或答案质量。

当语义不确定时,

应让模型表达不确定、提议澄清或继续调查,

而不是让运行时用未经授权的语义判断替代它。

这正是“严格但不全知”的含义。


外硬内软

一个更合适的 Agent 形态是外硬内软。

外层由运行时严格维护:

权限、工具契约、作用域、持久化、执行回执、恢复、发布与终态

内层交给模型处理:

理解、调查、比较、怀疑、补查与解释

严格的执行边界让系统诚实,

开放的语义空间让模型真正承担它擅长的工作。


代码评审的五个问题

新增模型字段或运行时状态时,可以先问:

  1. 这个信息是谁真正拥有的,运行时能否唯一确定?
  2. 如果运行时已经知道,为什么还要让模型输出状态镜像?
  3. 该字段表达模型独有的语义判断,还是系统已经发生的事实?
  4. finish() 是语义结束提议,还是在偷偷写入系统终态?
  5. 恢复依据真实落库事实和执行回执,还是模型上次自报的状态?

若这些问题没有清楚答案,

协议通常已经开始变重或越界。


这次升级没有证明什么

架构边界更清楚,

不等于模型质量、成本、延迟或投资研究能力已经被证明。

协议字段减少也不能直接推出回答更聪明、研究更深或消耗更低。

这些结论需要真实样本和可比较评测。

这次改变直接说明的只有:

哪些状态不再要求模型维护
哪些事实改由主机持有
哪些恢复不再依赖模型自报状态
哪些权限和发布边界可以客观执行

协议设计与产品效果必须分别证明。


最后

两条原则足以约束这类协议设计:

运行时已经知道的,不要让模型再维护。

运行时不知道的,不要假装它知道。

好的 Agent 架构不是消灭复杂度,

而是让运行时持有客观事实,让模型处理开放语义并提出下一步。