为什么 Agent 协议要把 Runtime 推到台前
先看一个很普通的请求:
帮我把登录页报错修掉,跑一下测试,没问题就发到线上。
人看这句话,觉得很简单。换成 Agent 来做,里面至少有十几件事:
- 先看报错是什么。
- 找到登录页代码。
- 找到接口调用位置。
- 判断是前端校验、接口返回、token 过期,还是路由跳转问题。
- 改代码。
- 跑测试。
- 如果测试失败,继续定位。
- 如果测试通过,看一下 diff。
- 判断这次改动会不会影响别的页面。
- 如果要发布,还要确认分支、环境、权限和发布步骤。
一个聊天式 Agent 会怎么做?
它会让模型先想一步,然后调用工具;工具返回一堆结果,再把结果塞回模型;模型再想下一步,再调用工具。这个循环在短任务里很好用,比如读一个文件、查一个函数、跑一次命令。
任务一长,麻烦就来了。
模型要记住自己刚才为什么查这个文件,要从 200 行日志里找出 3 行有用信息,还要判断测试失败是否由刚才的改动引起。它还要知道哪些动作可以自动做,哪些动作需要先问用户。比如发布线上版本,不能因为模型觉得“没问题”就直接发。
这就是我想设计 Agent 协议的起点。
协议的目的不在于写一套更漂亮的格式。它要解决一个更日常的问题:当一个任务变长、动作变多、风险变高时,系统怎么保持清楚、有序、可追踪。
第一步:把一句话变成任务单
自然语言适合沟通,不适合直接执行。
用户说“修好登录页”,模型能理解大概方向,但 Runtime 不能只靠“大概”。Runtime 要知道这句话接下来会变成哪些可处理的任务:
- 要收集哪些证据。
- 要检查哪些文件。
- 要不要改代码。
- 改完要跑哪些验证。
- 哪些结果需要回给模型继续判断。
- 哪些动作要先停下来问人。
这很像你去维修店修车。
你不会站在师傅旁边一秒钟指挥一次:“先拿扳手,拧左边那颗螺丝,停一下,换另一个工具,再听我下一步。”你通常会说清楚问题、预算、边界和交付要求:车打不着火,今天要用,超过多少钱先联系我,换下来的零件留着给我看。
Agent 协议要做的就是这件事:把一句自然语言请求,变成 Runtime 能接住的任务单。
模型负责理解用户要什么,Runtime 负责把任务单落到执行环境里。这样一来,后面的执行不需要每一步都回到模型那里重新“猜一次”。
第二步:少让模型直接摸底层工具
模型很适合做判断。
比如它看到错误信息,可以推测“可能和 token 刷新有关”;看到测试失败,可以判断“这个失败和刚才改动有关”;看到用户需求,可以拆出“先定位、再修复、再验证”的顺序。
但底层工具怎么调,模型未必最适合决定。
Runtime 比模型更了解当前系统:文件在哪,哪些命令能跑,哪些工具有权限,哪些缓存结果可以复用,哪些操作会产生副作用。模型每次都亲自指定底层工具,反而容易把任务切得太碎。
以代码搜索为例,模型可能会连续做这些事:
- 搜
login - 搜
auth - 搜
refreshToken - 打开三个文件
- 再看路由配置
- 再看请求封装
每一步都变成一次 toolCall,结果又全塞回上下文。模型看起来很忙,Runtime 其实只是搬运工。
更好的做法是让模型说清楚“围绕登录失败收集证据”,Runtime 自己去安排搜索、读取、去重和整理。模型最后看到的是一份能判断下一步的结果,避开一堆工具原始输出。
所以协议的设计方向很自然:让模型少下达底层动作,多表达任务意图。
第三步:让 Runtime 管住状态
长任务最容易乱在状态上。
继续看登录页修复这个例子。第一次测试失败,原因是 mock 没更新;第二次测试失败,原因是环境变量缺失;第三次测试才暴露出实际的代码问题。
如果所有内容都放进聊天上下文,模型会看到三轮失败日志、几次搜索结果、两种错误猜测、几段过期计划。它要自己判断哪些信息还有效,哪些已经过时。
这对模型来说很浪费注意力。
Runtime 可以做得更直接:
- 完整日志保存起来。
- 当前有效结论单独整理。
- 已经过期的猜测标记掉。
- 失败原因按轮次记录。
- 下一轮只给模型看“现在还需要判断什么”。
模型不需要每次重新翻仓库。它需要一张干净的工作台。
这也是协议要把“结果怎么返回给模型”纳入设计的原因。工具原始输出可以留在系统里,给模型看的内容要更像任务状态:做了什么,结果是什么,证据在哪,哪里还不确定。
日志用来复查,工作记忆用来推理。两者混在一起,模型就会越跑越累。
第四步:把风险动作交给规则拦住
Agent 系统里有些动作影响很小,比如读文件、搜索代码、运行只读检查。
有些动作影响很大,比如删除文件、改数据库、提交代码、发布线上版本、发邮件、下订单。
这些动作不能只靠一句提示词约束。
比如模型在修复登录页时说:“测试通过了,我准备发布。”这句话可以作为建议,但 Runtime 还要检查几件事:
- 当前分支是否允许发布。
- 有没有未提交的无关文件。
- 测试是否刚刚跑过。
- 发布环境是否生产环境。
- 这一步是否需要用户确认。
协议在这里起到一个翻译作用:模型可以提出意图,Runtime 按规则决定能不能执行。
这样做会让系统慢一点吗?会。关键动作慢一点是好事。用户需要一个知道什么时候该停下来的 Agent,胆子大不够。
第五步:为什么协议要这样设计
推到这里,协议的形状就比较清楚了。
如果只把协议做成 toolCall 的新包装,换个字段名、套一层 JSON,解决不了长任务里的混乱。
它也不适合做成让模型写程序的语言。模型如果开始负责所有执行细节,Runtime 又会退回搬运工的位置。
我希望这套协议保持几个特点:
第一,模型表达目标和理由,少表达底层操作。比如“检查登录失败的相关证据”,比“调用搜索工具查 A、B、C 三个关键词”更适合交给 Runtime。
第二,Runtime 接管执行和整理。它可以自己选择工具、保存完整证据、去掉重复信息,把结果整理成模型能继续判断的观察内容。
第三,权限和确认放在 Runtime。模型可以提出“发布”,但 Runtime 要根据规则判断是否允许,必要时把按钮交回给人。
第四,过程要能回看。用户后来问“你为什么改这里”“测试到底跑没跑”“发布前谁确认过”,系统应该拿得出记录,不能只剩一段模型总结。
第五,模型看到的信息要干净。它不需要每一行 stdout,也不需要每一次失败尝试都原样出现。它需要足够做下一步判断的材料,以及可以追到完整证据的引用。
这几个点连在一起,就形成了这套协议的设计思路:模型负责理解和判断,Runtime 负责执行和秩序,人保留关键选择权。
最后回到那个登录页例子
有了协议以后,“帮我修登录页并发布”不会再是一条在聊天里来回漂的自然语言。
它会先被模型理解成一个任务目标。Runtime 接住这个目标后,开始收集证据、组织执行、保存日志、整理结果。
模型在需要判断的地方出现:错误可能在哪里,修复方案是否合理,结果怎么解释给用户。
Runtime 在需要纪律的地方出现:文件怎么读,命令怎么跑,状态怎么记,证据怎么留,发布怎么拦。
人在需要承担后果的地方出现:是否接受改动,是否发布,是否允许高风险操作。
这就是我设计协议时最关心的事:让 Agent 系统从“一边聊天一边临场发挥”,变成“有人下目标,模型做判断,Runtime 管执行”。
如果继续把所有事都塞进 toolCall loop,系统会越来越像一个很聪明但没有工作台、没有记录本、没有权限边界的助手。它能做事,但你很难放心把长任务交给它。
协议要补上的,就是这张工作台、这本记录本,以及那些该停下来的红线。
关于协议本身的结构和完整内容,可以看这里: