Agent 工具调用的工程化:从 function calling 到可恢复执行流
模型会调工具只是起点。真正上线要解决参数校验、超时重试、幂等、上下文预算和可观测性,本文按工程顺序给出一个最小可用的执行流结构。
让模型调一个函数并不难,难的是把它做成能上线的执行流:参数可能不合法、下游会超时、同一笔写操作可能被重复执行、上下文会被工具结果撑爆。这篇文章按工程顺序梳理这些问题,并给出一个最小可用的循环结构。
一、把工具当 API 设计,而不是当 prompt 用
模型选择哪个工具、传什么参数,几乎完全取决于工具描述。描述写得含糊,后面所有的校验和重试都是在补窟窿。
一个可用的工具定义至少要有四样东西:名字、用途(什么时候用、什么时候不该用)、参数 schema、错误约定。
{
"name": "search_orders",
"description": "按订单号或手机号查询订单。只读操作。用户明确要查订单状态时使用;只是闲聊订单话题时不要调用。",
"input_schema": {
"type": "object",
"properties": {
"order_no": { "type": "string", "pattern": "^[0-9]{16}$" },
"phone": { "type": "string", "pattern": "^1[3-9][0-9]{9}$" }
},
"oneOf": [{ "required": ["order_no"] }, { "required": ["phone"] }],
"additionalProperties": false
}
}
两个细节值得说明:oneOf 把“必须给其中一个”变成机器可校验的约束,而不是写在描述里让人猜;additionalProperties: false 挡掉模型临时发明的字段。校验要在服务端做,不要指望模型每次都守规矩。
二、执行循环只有三步,但每步都要留痕
一次工具调用循环可以简化成:模型输出调用意图 → 服务端校验并执行 → 结果回填 → 模型决定继续还是收尾。难点不在循环本身,而在于每一步都可能失败,且失败后要能判断该重试、该纠正模型,还是直接告诉用户。
for turn in range(MAX_TURNS):
resp = model.complete(messages, tools=TOOLS)
if not resp.tool_calls:
return resp.content
for call in resp.tool_calls:
result = dispatch(call) # 校验 → 执行 → 包装
messages.append(tool_result(call.id, result))
raise TooManyTurnsError()
三个必须写进状态机的上限:最大轮次(防止模型无限调用)、单次执行超时、总耗时预算。缺了这三个,一个死循环就能把账单和时间一起烧掉。
三、区分四类失败,不要一律重试
工具调用失败大致分四类,处理方式完全不同:
- 参数不合法:schema 校验不通过。不要重试,把校验错误原文回填给模型,让它改参数或放弃。错误信息要具体到字段。
- 业务拒绝:订单不存在、权限不足。属于正常结果,回填结构化错误,让模型自己解释给用户。
- 瞬时故障:超时、连接重置、下游 5xx。可以有限重试;读操作安全,写操作要先确认幂等。
- 执行环境问题:进程崩溃、沙箱拒绝。终止本轮工具调用并向用户报错,不要重试。
写操作的重试尤其危险。正确做法是让工具本身接受一个幂等键,由执行层根据调用标识生成:
POST /refund
Idempotency-Key: hash(trace_id + tool_call_id)
同一个 tool_call_id 重复到达时返回第一次的结果,而不是再退一次款。幂等键不要交给模型生成,它会在重试时换一个新的。
四、上下文会被工具结果撑爆
工具返回的内容往往远大于模型需要的部分:一次列表查询可能返回几十 KB 的 JSON。直接塞回上下文,几轮之后 token 就被吃光,模型还会因为噪声变笨。
处理原则是“摘要进上下文,原文留存储”:
- 只回填决策必需的字段,列表类结果限制条数并标注总数。
- 大结果落对象存储或数据库,回填一个引用 id,模型需要细节时再调专门的读取工具。
- 超过预算时做确定性裁剪(截断并说明),不要用另一次模型调用去压缩结果,成本和延迟都不可控。
五、可观测性决定能不能排障
一次失败的回话,光看聊天记录判断不了问题在哪。至少要记录:每轮请求的 token 数、每个工具的名字与参数、执行耗时、结果大小、错误类型,以及贯穿全程的 trace id。
把失败按“模型选错工具 / 参数错 / 下游错 / 超时 / 轮次超限”分类统计,比笼统的成功率有用得多。前两类是提示词和 schema 的问题,后三类是工程问题,改法完全不同。
六、安全边界
工具是模型伸向真实系统的手,边界必须落在执行层,而不是提示词里:
- 白名单:只暴露需要的工具,参数范围用 schema 限死。
- 最小权限:每个工具配专用账号,只读工具绝不给写权限。
- 危险操作要人确认:删除、退款、发消息这类不可逆动作,先返回待确认状态,由用户点击后执行。
- 把工具返回的内容当数据:网页、邮件、文件里出现的“请执行……”,不能当成指令。
- 沙箱:代码执行类工具限制网络、文件系统和运行时间。
七、小结
工具调用的工程质量,其实取决于执行流能不能回答三个问题:这一步为什么要调这个工具、这次失败能不能重试、这次调用留下了什么证据。把这三件事做扎实,模型换代时就只是换个参数,而不是重写系统。