为什么 Agent 协议要把 Runtime 推到台前

先看一个很普通的请求:

帮我把登录页报错修掉,跑一下测试,没问题就发到线上。

人看这句话,觉得很简单。换成 Agent 来做,里面至少有十几件事:

一个聊天式 Agent 会怎么做?

它会让模型先想一步,然后调用工具;工具返回一堆结果,再把结果塞回模型;模型再想下一步,再调用工具。这个循环在短任务里很好用,比如读一个文件、查一个函数、跑一次命令。

任务一长,麻烦就来了。

模型要记住自己刚才为什么查这个文件,要从 200 行日志里找出 3 行有用信息,还要判断测试失败是否由刚才的改动引起。它还要知道哪些动作可以自动做,哪些动作需要先问用户。比如发布线上版本,不能因为模型觉得“没问题”就直接发。

这就是我想设计 Agent 协议的起点。

协议的目的不在于写一套更漂亮的格式。它要解决一个更日常的问题:当一个任务变长、动作变多、风险变高时,系统怎么保持清楚、有序、可追踪。

第一步:把一句话变成任务单

自然语言适合沟通,不适合直接执行。

用户说“修好登录页”,模型能理解大概方向,但 Runtime 不能只靠“大概”。Runtime 要知道这句话接下来会变成哪些可处理的任务:

这很像你去维修店修车。

你不会站在师傅旁边一秒钟指挥一次:“先拿扳手,拧左边那颗螺丝,停一下,换另一个工具,再听我下一步。”你通常会说清楚问题、预算、边界和交付要求:车打不着火,今天要用,超过多少钱先联系我,换下来的零件留着给我看。

Agent 协议要做的就是这件事:把一句自然语言请求,变成 Runtime 能接住的任务单。

模型负责理解用户要什么,Runtime 负责把任务单落到执行环境里。这样一来,后面的执行不需要每一步都回到模型那里重新“猜一次”。

第二步:少让模型直接摸底层工具

模型很适合做判断。

比如它看到错误信息,可以推测“可能和 token 刷新有关”;看到测试失败,可以判断“这个失败和刚才改动有关”;看到用户需求,可以拆出“先定位、再修复、再验证”的顺序。

但底层工具怎么调,模型未必最适合决定。

Runtime 比模型更了解当前系统:文件在哪,哪些命令能跑,哪些工具有权限,哪些缓存结果可以复用,哪些操作会产生副作用。模型每次都亲自指定底层工具,反而容易把任务切得太碎。

以代码搜索为例,模型可能会连续做这些事:

  1. login
  2. auth
  3. refreshToken
  4. 打开三个文件
  5. 再看路由配置
  6. 再看请求封装

每一步都变成一次 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,系统会越来越像一个很聪明但没有工作台、没有记录本、没有权限边界的助手。它能做事,但你很难放心把长任务交给它。

协议要补上的,就是这张工作台、这本记录本,以及那些该停下来的红线。

关于协议本身的结构和完整内容,可以看这里:

Agent Protocol DSL:LLM 与 Runtime 的交互协议