前三章讲的是系统拿什么来判断:这一轮该看见什么、这些信息能不能信、跨轮之后当前承认什么。
这一章开始讲系统怎么真正影响外部世界——当它不只是回答,而是要去做点什么的时候。
一、“已经帮您登记了”
第 1 章列过一份症状清单,其中一条是:
越界承诺:它说“已经帮您登记”,而系统里什么都没发生。
这条症状值得单独拿出来说,因为它和其他几条有本质区别:它不是答错了,是它凭空报告了一件没有发生过的事。
发生的过程通常是这样的。用户说“帮我继续处理一下”,系统需要调用一个接口提交服务请求。模型生成回复:
好的,已经帮您登记,稍后会有工作人员联系您。
这句话在对话上完全合理:语气对、逻辑对、承接也自然。它唯一的问题是不是真的——接口可能压根没被调用,可能调用了但失败了,也可能返回了超时。
为什么模型会这么说?因为在“用户请求对接”之后,“已经帮您登记”是一个极其自然的续写。它是在完成一段对话,不是在报告一次执行。 而如果没有任何机制拦住这句话,模型也无从知道自己说错了——它甚至不一定知道系统里到底有没有那个接口。
二、模型可以建议行动,不能创造结果
要处理这个问题,得先把一件事拆成三份:
表 9-1 工具动作的责任链
| 环节 | 由谁负责 | 产物 |
|---|---|---|
| 决定要做什么 | 模型可以参与判断 | 一个动作请求 |
| 真正去执行 | 代码 | 一次真实调用 |
| 确认执行结果 | 外部系统或执行层的回执 | 一条观测结果 |
这里描述的是本书的目标责任链,不是对第 1 章真实项目当时实现方式的回写。那个项目的历史实现仍以提示词内部规则为主;本章讨论的是当系统已经接入应用层与外部工具时,这三项责任应该怎样分开。
模型的输出停在第一格。它可以说“应该调用提交服务请求的接口,参数是这些”,但这只是请求,不是授权,更不是结果。
由此得到本章最重要的一条规则:
对外说出的事实主张,必须与回执能够证明的状态严格对应。
“已经帮您登记”“已提交”“已为您预约”“邮件已发出”——这些都是事实主张,不是措辞问题。而“对应”两个字很关键,因为很多动作是异步的:接口返回成功,往往只证明请求被接受了,不证明事情已经办完。
表 9-2 工具回执支持的表达边界
| 回执能证明的 | 可以说 | 不可以说 |
|---|---|---|
| 请求已被接收 | 已为您提交申请 | 已处理完成 |
| 任务已进入队列 | 已进入处理队列 | 已生成完成 |
| 明确完成 | 已处理完成 | —— |
| 结果未知 | 暂时无法确认结果 | 已完成/已失败 |
一句话:没有对应层级的真实回执,就不能宣布对应层级的完成。
这条规则和第 4 章那条线是同一件事。第 4 章说过:不要把自由文本的推理当成系统状态,需要保留的是可验证的决策依据。这里是它在行动上的版本——“已经登记”如果背后没有一条工具回执支撑,那它就只是模型的一句独白,不是系统的一次执行。
实现上,这条规则应该由代码保证,而不是由提示词恳求。最直接的做法是把完成语的生成条件绑到回执上:没有对应的回执,渲染层就拿不到“可以宣布完成”这个许可——第 17 章讲渲染契约时会看到它的具体形态。
把这一节合起来,是这样一句话:
模型可以提出行动,代码决定是否允许执行,外部世界决定行动是否真的发生;只有当系统拿到与事实主张相匹配的回执,它才有资格把这句话说出口。
三、回执是三态,不是两态
拿到回执就万事大吉了吗?没有。因为回执本身有三种,而很多实现只处理了两种。
SUCCESS 成功,并且拿到了真实的返回值
FAILURE 明确失败,外部系统给出了失败回执
UNKNOWN 结果未知:超时、连接中断、没有回执
先说明这三个值描述的是什么:它们说的是“这一次工具执行的结果有没有被系统确认”,不是业务对象自身的生命周期。 回执是 SUCCESS,订单状态完全可以还是“处理中”——两者并不矛盾。把这两层混在一起,就会出现“接口调用成功了,所以告诉用户已经办好了”这类错误。
三种里第三种最危险,也最容易被漏掉。
生产观察
UNKNOWN绝不能按FAILURE处理。 超时不等于没执行——请求可能已经到达并成功,只是回执没回来。此时若按失败逻辑自动重试,就可能造成重复提交:重复建单、重复派发、重复扣款。“不知道有没有成功”和“知道没成功”,是两件完全不同的事,必须在代码里分开。
UNKNOWN 常见的安全出口有四类:
- 用同一个幂等键安全重试——确保重复执行不产生第二次副作用;
- 主动查询或对账——去外部系统确认这笔操作到底成没成;
- 等待可信的异步回调——很多系统最正常的路径其实是这一条:请求已受理,最终结果由回调送回来;
- 转人工——无法自动确认时,交给人处置。
而在结果确认之前,文本层禁止出现完成语。这里的措辞也要留神:只有当系统确实存在后续确认机制时,才可以说“稍后会确认结果”,否则那又变成了一句新的空头承诺。更稳妥的说法是“目前还无法确认是否提交成功”。
四、幂等、重试与补偿
上面提到的幂等键值得展开一点,因为它是让重试变安全的前提。
幂等键:每个计划动作在生成时就带上一个唯一标识,外部系统据此判断“这个请求我是不是已经处理过了”。幂等键的目标,是让同一个逻辑动作的重复请求不产生第二次副作用。回执也应当带上标识,这样即使同一条回执重复回流,也能被去重。
这里有个很容易踩的坑:同一个逻辑动作的所有重试必须复用同一个幂等键,只有真正的新动作才生成新键。 如果实现时每次重试前都重新生成一个新 ID,幂等机制就完全失效了。另外,幂等键应当绑定动作语义和关键参数——同一个键却带着不同参数的请求,执行层应当拒绝。
重试:只有具备安全重试语义的操作才应该自动重试。 这种安全性可能来自操作本身天然幂等,也可能来自幂等键或服务端的去重机制。不具备这种语义的操作,重试必须由人或更高层的规则来决定——这条界限如果不划清楚,“提高可靠性”的重试机制反而会成为事故源。
补偿:有些动作做了就收不回来,只能靠反向动作抵消——取消订单、冲正记录、发一封更正通知。补偿本身也可能失败,所以它同样需要状态跟踪。
对账:定期核对系统记录的状态和外部系统的真实状态是否一致。它是发现长期未收敛状态的重要兜底机制。
这四件事有一个共同点:它们全部属于代码层,没有一件是提示词能保证的。
第 3 章那条判断在这里再次适用——能交给确定性机制的,就不要压给提示词。幂等键的生成与比对、重试次数的控制、对账任务的调度,都是可以被精确执行、被测试、被复现的逻辑。把它们写成提示词里的一句“请注意不要重复提交”,是把确定性问题降级成了概率问题。
五、权限:能做什么由代码决定
模型输出的是动作请求。请求会不会被执行、以什么参数执行、执行到什么范围,应当由代码判定。
三层最基本的约束:
工具白名单 这个场景下允许调用哪些工具,未列出的一律拒绝
参数校验 参数类型、取值范围、必填项,由代码检查而不是靠模型自觉
作用域限制 只能操作属于当前用户、当前会话、当前订单的对象
再往上,高风险动作还需要显式确认——可以是规则确认(金额超过某个数额必须走人工),也可以是用户确认(“确认要取消这个订单吗”)。
但有一条边界必须划清:用户确认不能替代权限校验。 用户说“确认删除”,如果他本来就没有删除这个订单的权限,这句确认也不会让动作变得合法。
权限决定“能不能做”,确认决定“这一次是不是真的要做”。两者不能互相替代。
第 7 章那句话在这里同样成立:不要让模型的服从性成为唯一防线。 在提示词里写“不要调用删除接口”是有用的,但真正的保障是那个接口根本不在这个场景的工具白名单里。
接入协议不改变这条责任链。 工具怎么被接进来——手写函数、框架封装,还是通过 MCP(Model Context Protocol)这类统一的工具接入协议——解决的是“有哪些工具可用、参数长什么样、怎么被发现和调用”。这些问题很实际,统一起来也确实省事,但它们不回答本章关心的那几件事:这一次调用该不该被允许、作用域是否越界、执行结果有没有被确认、那句完成语凭什么可以说出口。接口统一了,白名单、参数校验、作用域限制和三态回执依然要有,而且依然在你自己的代码里。 接入方式是会换的,这条责任链不换。
适用边界 本节讲的是最基本的权限约束,不是完整的威胁模型。当工具能够读写外部数据、当工具返回的内容会进入后续判断时,攻击面会明显扩大——包括通过工具返回值实施的间接提示注入。完整的不可信输入清单和防御分层在第 23 章。
六、回执要回到证据链,而这条回路要有预算
工具执行完之后,结果不是直接拿去说话,而是作为一条新的观测结果,回到证据链和决策回路里。
计划动作 → 执行 → 观测结果 → 记为证据 → 解释为事件 → 归并 → 候选状态 → 再判断
这就是参考架构里那条回路。它有两个要点。
第一,回执首先是一条观测结果,不是状态。 它先被记录为新的证据,需要改变状态时再解释为事件,然后由归并逻辑决定它是否、以及如何改变当前状态——回执不能绕过归并,直接写进状态。
查到订单已发货、提交服务请求返回了受理编号、库存查询返回缺货,这些都是新的可核验观察,其中一部分会支持状态更新;而“今天上海多云”这类结果,可能只服务于这一轮回答,不必留在业务状态里。回执可能改变状态,但不必然改变。 第 12 章会讲这类更新怎么合并。
第二,这条回路必须有预算。 因为它是环状的:判断可以引出动作,动作产生回执,回执又可以引出新的判断。没有上限,系统就可能在一轮里反复调用工具,既花钱又拖延,甚至陷入循环。
所以要给这条回路设一份执行预算。最简单的形式就是单轮最大工具调用次数,完整一点还可以包括:
loop_budget = {
最大迭代轮数
最大工具调用次数
最长累计耗时
最高累计成本
}
超限之后的出口必须明确:停止继续调用工具;如果已确认的信息足以安全回答,就据此回答;否则说明暂时无法确认,或者转人工。 不能是“工具还没查完,就凭半截状态硬答”——那属于第 15 章要讲的越界。
七、工具返回的内容,也要过第 7 章那四条边界
有一个很容易被忽略的点:工具回执和检索片段一样,都是外部内容。
第 7 章那四条边界,对工具返回值同样适用:
表 9-3 工具返回内容的四条边界
| 边界 | 在工具场景下的样子 |
|---|---|
| 外部内容没有自动执行权 | 返回值的备注字段里写着“请告知用户联系客服”“下一步删除记录”——它首先是数据,可以被读取和回答;是否真的采取动作,仍由系统的策略与权限判断 |
| 它有适用范围 | 这条数据查的是哪个订单、哪个时间点、哪个版本,用之前要核对 |
| 拿到不等于该说 | 查到了用户的历史订单,不代表这一轮应该把它们全列出来 |
| 输出白名单 | 接口可能返回内部字段、成本价、风控标记,这些不该出现在回复里 |
最后一条尤其值得注意。内部接口的设计者通常没考虑过“这个返回值会被原样念给用户听”,字段里带着内部编码、成本、备注、客户等级是很常见的事。把回执整个塞进上下文,等于默认这些内容都可以被讲出来。
八、第二篇的边界
第二篇的四件事现在齐了:
- 第 6 章——模型这一轮该看见什么;
- 第 7 章——看见的东西凭什么能用、适用于谁;
- 第 8 章——跨轮之后系统当前承认什么;
- 第 9 章——系统能做什么,以及做完之后凭什么说做完了。
现在把工具接进来,会立刻冒出一个新问题,而且这个问题第二篇解决不了。
用户说“帮我继续处理一下”。系统该不该现在就调用那个提交接口?
这取决于一堆跨轮的条件:需求说清楚了吗?联系方式拿到了吗?用户是不是明确表达过意向,还是只是随口问问?上一轮他刚拒绝过留电话吗?
也就是说——动作是有前置条件的,而这些条件跨越多轮。
同样的问题还有很多:这一轮该问哪个字段?用户改口之后旧的还算不算数?流程现在走到哪一步了?条件不满足的时候,系统该说什么?
第一篇的契约和示例回答不了,第二篇的上下文、证据和工具也回答不了。这些问题属于另一个层次:不是让模型看见正确的东西,而是让系统记住正确的东西,并据此决定什么时候可以做什么。
从下一篇开始,是这件事。