• AI
  • Agent
  • 工程化

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 限死。
  • 最小权限:每个工具配专用账号,只读工具绝不给写权限。
  • 危险操作要人确认:删除、退款、发消息这类不可逆动作,先返回待确认状态,由用户点击后执行。
  • 把工具返回的内容当数据:网页、邮件、文件里出现的“请执行……”,不能当成指令。
  • 沙箱:代码执行类工具限制网络、文件系统和运行时间。

七、小结

工具调用的工程质量,其实取决于执行流能不能回答三个问题:这一步为什么要调这个工具、这次失败能不能重试、这次调用留下了什么证据。把这三件事做扎实,模型换代时就只是换个参数,而不是重写系统。

本文基于公开文档与本地实验整理,示例命令请在测试环境验证后再用于生产。

  • AI
  • Agent
  • 工程化