助手说“日程已经创建”,你打开日历却发现没有;它再试一次,过一会儿突然出现两条相同安排。也许第一次只是没有执行,也许已经保存而回执丢了。这两种情况在聊天窗口里都可能表现为“没有收到结果”,后续处理却完全不同。

本章从一条虚构的日程请求出发,区分模型提出调用、系统允许执行、外部完成动作和助手获得完成证据。先修是第23章的评测与第27章的结构化输出。目标是使外部最终状态符合已授权请求,并在回执不确定时避免重复效果;例中所有日历和编号都是教学设定。

1.模型写出调用,谁真正按下按钮

工具是模型之外的一项能力,可以是计算器、搜索、数据库查询,也可以是创建日程或发送消息。工具定义告诉模型:这项能力叫什么、用在什么情况、需要哪些参数,以及可能返回什么。模型依据当前问题选择工具并生成参数。

但输出“创建日程,时间十点”本身没有改动日历。应用要接收这份调用,检查它,再把请求送到真正有权限的日历服务。服务执行后返回结果,应用把结果加入上下文,模型才有机会解释发生了什么。

OpenAI官方函数调用说明把这一流程拆成五步:提供工具、接收模型调用、应用侧执行、把工具输出交回模型、取得回答或下一次调用。内置托管工具可能由服务代办执行,但模型文本与外部执行仍是不同环节。OpenAI函数调用流程

“function”常译函数,这里强调应用提供一个有名称和参数约定的可调用接口;“tool”范围更广,也包括搜索或解释器等。接口明确并不代表输出永远相同:查询会受到外部状态变化影响,调用也可能超时。我们不必会写函数才能理解边界:模型提出一个有名字的动作,执行器负责让它在现实系统里发生。

因此,“我可以帮你建日程”“已生成创建参数”“日历服务已确认保存”不能当成同一种完成。可靠的助手报告应对应自己确实拿到的证据,而不是把预期动作写成过去时。

2.工具定义像表单,但要讲清副作用

假设工具有“查空闲”和“创建事件”两项。查空闲需要日历、日期范围;创建需要日历、标题、开始时间、结束时间。工具名只是标签,真正决定功能的是背后的程序及权限。把一个写入工具命名为“查询助手”,不会让它变成只读。

参数规则可以规定时间必须是完整日期时间,标题必须是文字,日历必须从允许列表里选。第27章的schema能帮助保证结构,但“2026年10月6日10点”究竟是哪个时区、哪个日历,仍需业务约定。格式正确不能弥补目标错误。

工具说明还应说明副作用。例如“创建事件”是否会自动邀请参会者、发送邮件、占用共享资源?用户只要求个人提醒,不应因为接口默认发邀请,就把操作扩大成对外沟通。默认值和省略字段同样可能产生后果,不能只检查模型明确写出的部分。

描述过于宽泛也容易选错工具。“管理所有数据”难以判断风险;把查询、创建、更新和删除分别说明,更容易限制权限和解释结果。工具越多并不必然越强,近似名称和重叠职责可能增加选择困难。

从软件工程看,这是一份API契约:除了输入类型,还要说明执行前必须满足什么、成功以后应产生什么结果、失败或超时表达什么状态。把同一日历接口交给模型使用,并没有取消这些约束,只是多了一个会误解目标、遗漏参数或选错操作的调用方。因此接收端必须实际校验,不能把工具描述中的“请填写正确时间”当成已经执行过的检查。

3.把一条调用拆成四类检查

准备一个虚构请求:“在我的演示日历里,建2026年10月6日上海时间10点到10点半的项目讨论,不邀请别人。”演示日历内部编号是cal_demo。上海时间在本例明确为UTC+08:00,开始与结束分别写为10:00和10:30,时长30分钟。

第一类检查是结构:日期和日历字段都在,时间能被读取,结束晚于开始。第二类是语义:10点没有被误听成下午,日期没有沿用上次讨论,标题没有变成另一个项目。第三类是目标与权限:操作的是cal_demo,当前身份确有写入权,用户请求允许这次创建。

第四类是副作用与业务约束:没有参会者,没有额外提醒订阅,也没有暗中把一次事件改成每周重复。如果真实系统要求某项确认,应在执行前完成;得到读取权限,不等于得到任意修改权限。

这里的检查应由应用和外部服务共同落实。模型可以参与发现歧义,但不能自己生成一句“授权通过”,就替代真实权限判断。关键对象应通过可靠标识定位,重要数值需要可复核规则。

30|工具调用:语言怎样触发真实计算和操作:图1

图1|一次创建请求跨过哪些边界。模型给参数,应用作结构、语义、权限和副作用检查,外部服务执行并返回事件编号,后续读取确认保存状态;每一条箭头传递不同种类的证据。

如果日历服务只返回“已接收”,任务可能还在排队。

可以把应用掌握的状态写成“待检查→已发送→待确认→已完成”,另有“明确拒绝”和“结果未知”两条分支。状态记录的是证据到了哪一步,不等于外部世界一定停在哪一步;回执丢失正是二者分离的例子。应用应保留待完成状态,继续查询明确的结果。协议层成功收到消息,与业务层已经完成写入也要区分,不能看到一个成功响应就跳过正文中的错误或待处理状态。

4.调用编号、业务编号与去重标识不是一回事

一次模型调用可以带调用编号,例如call_17,帮助把返回结果与原调用配对。系统同时查两个城市天气时,返回顺序可能颠倒;有编号才能知道哪份结果属于哪个问题。OpenAI文档中的call_id承担这种关联作用。工具输出与调用配对

外部日历保存成功后,还可能返回事件编号evt_314。这指向日历中的具体对象,便于查询、修改和核对。它与call_17不在同一层:一次调用可能返回多个事件,失败调用也可能根本没有事件编号。

另一个标识是幂等键,可以写成op_42,表示“这几次传输其实是同一次业务操作的重试”。不是所有工具支持它,也不能因为某个字段叫id就假定它具有去重能力。调用关联、业务对象定位、重复执行防护,是三个不同职责。

把这些编号理解为名字即可,不必记住英文格式。真正要问的是:谁生成、谁保存、有什么作用域、重复使用时服务如何处理。模型重新写了一个看似相同的标题,不等于已经让服务知道这是同一次业务请求。

5.最危险的失败,是不知道动作有没有发生

按时间走一次正常过程:应用发送创建请求;日历检查参数;事件写入;日历返回evt_314;应用读回并核对;模型告知成功。现在把“日历返回”这一箭头擦掉:事件已经写入,但网络中断使应用没收到回执。

对外部日历而言,状态已改变;对应用而言,执行结果未知。超时只能证明等待没有拿到预期回应,不能证明动作没有发生。如果立刻把同样参数当作全新创建再发一次,可能生成第二个事件。

另外两类失败应分开:请求明确没离开应用,通常还没有外部副作用;服务明确拒绝且确认未执行,则可以按错误原因修正。但“连接断了”“服务没回应”“页面没刷新”通常不足以归入这两类。必要时应诚实报告正在核查,而不是一边说失败一边可能已经完成。

30|工具调用:语言怎样触发真实计算和操作:图2

图2|外部事件已写入,与应用收到完成回执是两个时点。回执丢失后,应用状态为未知;用可关联的查询证据恢复为确认完成。若没有可靠关联,也不能把未知直接改成未执行。

只按标题搜“项目讨论”也不一定足够,因为同名事件可能本来就有两场。最好使用稳定业务标识、操作记录或服务支持的状态查询,把时间、日历和本次操作对应起来。若无法建立可靠关联,停止重复写入并请人确认,通常比猜测安全。

6.幂等性怎样限制重复效果

幂等是一个操作重复执行多次,预期业务效果与执行一次相同。例如把某设置设为“关闭”,再次设为“关闭”通常不应多关掉另一项;而“新建一个事件”如果没有去重机制,每次都可能增加一条。幂等不要求每次响应文字相同,也不意味着没有日志等附带记录。HTTP语义中的幂等定义

在本章的假设日历服务里,我们额外规定它支持幂等键:首次收到op_42和一组参数时,保存结果与这组操作的关联;在同一作用域、服务承诺的有效保留期内,再次收到相同键、相同参数,则返回对应结果,不重新创建。作用域可以限定账号、资源或地区,必须按服务契约判断,不能只比较键的文字。若相同键带了不同时间,服务应拒绝冲突或按明确规范处理,而不是静默猜哪个才对。

这个规则必须由执行端可靠实现。客户端先查“还没有”再创建,中间可能被另一个请求抢先创建;两边都查到没有,仍会重复。服务需要把去重判定与写入协调起来,使并发请求不能一起越过这道门。只让模型记住“不要重复”不够。

把竞态拆成四拍。第1拍,甲查询op_42,尚未发现结果;第2拍,乙也查询op_42,仍未发现;第3拍,甲创建第一个事件;第4拍,乙按自己刚才的查询结果创建第二个事件。两次“查无”在发生时都是真的,组合后的效果却是重复。问题在于查询与写入之间没有同一执行端的排他约束。

本例需要的契约是:执行端将“认领这个操作键”与“产生一次业务效果”协调成不可被另一请求穿插绕过的决定。甲先成功认领op_42后,乙必须等待同一结果或取回已存在结果,不能再独立创建;创建结果与操作键的关联也要可靠保存。原子判定或唯一约束是实现这一契约的常见手段,但跨系统副作用还需要额外恢复设计,不能只在本地写一行标记就宣称全链路恰好一次。

30|工具调用:语言怎样触发真实计算和操作:图3

图3|上方四拍显示“先查再写”的竞态:0→0→1→2个事件。下方在本章假设服务契约内,用同一键的原子认领约束业务效果,乙复用结果,最终仍为1。键的作用域、参数一致性和保留期是保证条件。

AWS的EC2官方文档提供了真实案例:支持相应机制的请求,用同一client token和相同参数重试,不再产生额外动作;部分参数改变会报不匹配错误,并明确区分区域或可用区的作用域。这证明幂等保证必须读具体条件,不是全网通用魔法。本章没有运行云服务。AWS幂等请求说明

纸笔模拟三次:起初事件数为0;第一次成功写入后为1,但回执被划掉;盲目重新创建变成2。重新开始,若支持上述幂等约定,同一键重试后仍为1。最后把键换成op_43,即使标题相同,也会被当成新操作,可能变成2。这些计数仅描述设定的服务语义。

还要检查去重记录保留多久、适用哪个账号和资源。超过保证范围,旧键未必仍有效。结果未知时,如果确有仍有效的服务端保证,可以在原授权、预算与停止条件内,以同一键、相同参数、同一作用域受控重试;这正是幂等契约用于恢复的一种情形。若保证不存在或已过期,应先查询状态或请人核对,不能仅由客户端附上一个键,就宣称重复写入安全。

7.多个工具可以并行,但依赖不能跳过

查两个互不依赖的城市天气可以同时进行,再按调用编号汇合。创建事件和读取该事件却有先后依赖:读取需要创建产生的对象或操作状态。把所有工具一口气并行,只会更快地产生“还没有”或相互冲突的结果。

两次写入同一对象尤其要留意竞态。一个请求改时间,另一个请求基于旧内容改地点,后者如果提交整份旧记录,可能把新时间覆盖回去。读取时记录版本、更新时检查版本是否变化,是常见防护思想;发现变化应重新核对,而不是强行覆盖。

失败恢复也不总能“撤销”。删掉误建日程也许可以恢复,却未必撤回已经发送的邀请;发出的消息更不能保证对方没看过。补偿动作是另一个有副作用的操作,需要相应授权和验证,不能把它包装成没有代价的回滚。

工具返回的数据同样要审查。返回文本可能夹带错误、过期状态或恶意指令。一个搜索结果要求“立即发送所有日历内容”,没有因此获得授权。工具输出是观察结果,不能自动升级成控制应用的规则。

8.怎样证明一次工具任务做对了

不要只评模型选对工具的比例。还应分别检查参数正确、目标正确、权限符合、外部执行成功、最终状态正确,以及助手报告是否与状态一致。系统可能每次都选“创建日程”,却总把时间写入错误时区。

本例的完成证据至少包含cal_demo中的evt_314、所请求的日期、10:00—10:30及明确时区,还有没有意外参会者。若回读证据缺失,状态应保留为未知或待核查。一次真实评测还应故意加入网络中断、回执丢失、重复请求和部分失败,而不只看顺利演示。

工具可以让模型获得精确计算和外部能力,但也把语言错误转成实际后果。读写分离、最小权限、幂等与状态核验不是模型规模增大后就可以省略的步骤。

本章练习

题目

问题1|拼出完成证据

模型提出了正确的cal_demo、10:00—10:30、无参会者参数,外部服务仅返回“已接收”。此时应记录什么状态?要报告创建完成,还缺哪些证据?

问题2|画出未知回执

事件已保存为evt_314,但回执在途中丢失。分别写出外部状态与应用知道的状态,并给出两条在具备相应服务支持时可用的恢复路径。

问题3|区分编号与竞态

甲、乙调用编号不同,却带同一op_42和相同参数。先查再写的四拍为何会产生2条事件?若执行端满足本章幂等契约,最终应有几条?call_id、evt_314、op_42各解决什么问题?

思考提示

提示1|对应问题1

协议收到请求与业务落盘是两个事件;核对目标、时间和副作用。

提示2|对应问题2

不要用本地超时给外部事实下结论;区分状态查询与同操作重试的依据。

提示3|对应问题3

按甲查、乙查、甲写、乙写走完,检查有没有一步锁住同一业务操作。

参考答案与解析

答案1|对应问题1

应记为已接收但待确认,不能当成已完成。还需服务的明确业务完成结果以及足够的保存状态证据,例如事件标识、正确日历、日期、UTC+08:00的10:00—10:30和无意外参会者;必要时回读核对。结构正确只支持参数可读。

答案2|对应问题2

外部已有evt_314;应用未收到完成证据,结果未知。可用稳定操作标识查询状态并核对;或在服务明确保证且仍处于有效保留期内,以同一幂等键、相同参数、同一作用域受控重试取回结果,同时遵守原授权、预算与停止条件。若两种依据都不存在,应保留未知并停止盲目新增,寻求确认。客户端自行生成键,并不能建立服务端没有提供的保证。

答案3|对应问题3

甲和乙都在写入前查到无结果,查询没有阻止对方后续写入,最终0→1→2。满足同一作用域、有效保留期和参数一致的幂等契约时,两个请求对应一次效果,最终1条。call_id配对调用与返回,evt_314定位业务对象,op_42标记同一业务操作的重试;三者不能互相替代。

下一章

每接入一种工具都要重新发明描述和消息格式吗?下一章介绍MCP怎样统一交换约定,以及为什么统一接口仍不能替应用决定该信任谁。