工具契约、执行循环与副作用
小工具的返回值也有 Schema
工具名表达动作与资源,如 inventory_lookup,比万能 execute 清楚。参数只包含模型需要决定的业务字段;tenant_id、操作者与权限从可信会话注入,不让模型指定。返回值区分 found=false、count=0 和系统错误,避免模型把失败当成成功空结果。
工具循环的顺序是生成意图、完整解析、结构校验、授权、执行、关联结果、继续生成。所有调用都要返回可匹配结果;多工具可并行只说明没有依赖,不能忽略它们可能修改同一资源。流式参数必须收齐后校验,取消只取消尚未执行部分,不能撤销已经发生的外部转账。
离线实现一个明确的幂等边界
Python 3.12 保存 execute.py,无依赖运行。这个例子用内存表达原子操作顺序,只适用于单进程教学;生产需事务与持久化唯一键。
import json
orders={};stock={'A':3}
def reserve(principal,key,args):
if principal!='alice':raise PermissionError('forbidden')
if set(args)!={'sku','quantity'} or args['sku']!='A':raise ValueError('bad args')
n=args['quantity']
if type(n) is not int or n<=0:raise ValueError('bad quantity')
fingerprint=json.dumps(args,sort_keys=True)
cache_key=(principal,key)
if cache_key in orders:
old=orders[cache_key]
if old['fingerprint']!=fingerprint:raise ValueError('idempotency conflict')
return old['result']
if stock['A']<n:raise ValueError('insufficient stock')
stock['A']-=n
result={'reserved':n,'remaining':stock['A']}
orders[cache_key]={'fingerprint':fingerprint,'result':result}
return result
first=reserve('alice','order-1',{'sku':'A','quantity':2})
assert reserve('alice','order-1',{'sku':'A','quantity':2})==first
assert stock['A']==1
try:reserve('alice','order-1',{'sku':'A','quantity':1})
except ValueError as e:assert str(e)=='idempotency conflict'
else:raise AssertionError('conflicting replay accepted')
print('one reservation',stock)先鉴权再查幂等缓存,避免未经授权者读取旧结果。相同键与不同参数是冲突,不应返回旧成功结果假装新命令已完成。生产并发下读缓存、扣库存和写结果必须同事务;外部支付无法与本地数据库直接同事务时,使用支付方幂等键与可查询状态协调。
确认、沙箱与审计
确认记录绑定主体、规范化参数摘要、工具版本与有效期。确认后参数变化需要重新决策,不能把一个全局 approved=true 用于所有动作。查询、写入、删除、执行命令按副作用与可逆性采用不同策略;不是所有写操作都机械弹窗,产品可以预授权限定范围。
沙箱限制实际文件、网络和进程能力,工具描述不构成沙箱。日志至少记录业务键、调用 ID、授权结论和副作用结果;敏感参数保存经过访问控制的引用,不把密钥写进 trace。达到步骤、时间或费用上限时返回明确停止状态,不能继续调用直到模型满意。
练习
在正文实验后尝试 bob 重放 order-1,应抛 PermissionError 且 stock 仍为 1。再用 alice 的新键请求 2,应失败且不能扣成负数。
先记录自己的输出与解释,再核对本章答案。