Aklman · Library

03 / 第三章

工具调用 · Function Calling 完全指南.

工具是 Agent 的手脚 —— JSON Schema 定义能力边界,错误处理决定鲁棒性,并行调用决定速度。


研究助手现在只会搜索;这一章给它加第二个工具 —— fetch_page,把搜到的网页抓回来读。借它把工具的三件事讲透:schema 怎么定义能力、错误怎么回传决定鲁棒性、并行怎么调决定速度。模型够不到你的函数实现,只看得到你写的 schema 和描述。

I

工具的定义就是它的契约.

模型不读你的函数实现,只读你的 schema 和描述。这份契约写得好不好,直接决定它调得对不对。 一个工具就三块:name、description、入参 schema。description 不是文档,是 prompt —— 模型靠它决定「什么时候、用什么参数」调这个工具。1注 1Anthropic 文档 · Tool use —— 工具由 name / description / input_schema(JSON Schema)定义;strict: true 保证调用严格符合 schema;tool_choice 可选 auto / any / tool / none;tool_result 可带 is_error 把失败回传给模型。截至 2026-05。模型能只凭描述和少量示例就学会怎么调,靠的是 in-context learning —— GPT-3 那篇早已证明,大模型「无需梯度更新或微调」、仅凭提示里的示例就能改变行为。4注 4Brown et al. · 「Language Models are Few-Shot Learners」(GPT-3,2020,arXiv:2005.14165)—— in-context / few-shot learning:大模型「无需任何梯度更新或微调,仅凭文本里的少量示例」就能执行新任务。这是工具描述与示例为何能改变模型行为的根。同一个工具,两家的写法几乎一样:
{
    "name": "fetch_page",
    "description": "抓取一个 URL 的正文内容。要读 web_search 找到的网页时调用。",
    "input_schema": {
        "type": "object",
        "properties": {
            "url": {"type": "string", "description": "要抓取的页面 URL"},
        },
        "required": ["url"],
    },
}
{
    "type": "function",
    "function": {
        "name": "fetch_page",
        "description": "抓取一个 URL 的正文内容。要读 web_search 找到的网页时调用。",
        "parameters": {
            "type": "object",
            "properties": {
                "url": {"type": "string", "description": "要抓取的页面 URL"},
            },
            "required": ["url"],
        },
        "strict": True,
    },
}

研究助手的 fetch_page,两家 API 的定义

两边都支持 strict —— 强制模型的调用严格符合 schema,省掉一整类「参数缺失 / 类型不对」的脏活。1注 1Anthropic 文档 · Tool use —— 工具由 name / description / input_schema(JSON Schema)定义;strict: true 保证调用严格符合 schema;tool_choice 可选 auto / any / tool / none;tool_result 可带 is_error 把失败回传给模型。截至 2026-05。2注 2OpenAI 文档 · Function calling —— 工具用 {type: function, function: {name, description, parameters, strict}} 定义;响应在 message.tool_calls,结果以 {role: tool, tool_call_id, content} 回传。形态稳定,作对照。能开就开。 反过来,过度暴露能力是这一章最该警惕的反模式:一个 run_sql(query) 工具,等于把整个数据库的权限交给了模型。工具的边界就是权限的边界 —— schema 越窄越安全。3注 3JSON Schema —— 工具入参 schema 的规范来源(type / properties / required / enum 等)。模型读的是这份契约,不是你的函数实现。这条线第 9 章还会再画一次。

II

错误处理决定鲁棒性.

工具一定会失败 —— fetch_page 尤其会:404、超时、抓回一页渲染不出的 JS。agent 的鲁棒性不在 happy path,在你怎么把失败回传给模型。 关键一招:别让工具的异常炸掉整个循环,把错误当成一个普通的 tool_result 传回去(带 is_error)。模型看到错误,就能改参数重试,或者体面地告诉用户做不到。1注 1Anthropic 文档 · Tool use —— 工具由 name / description / input_schema(JSON Schema)定义;strict: true 保证调用严格符合 schema;tool_choice 可选 auto / any / tool / none;tool_result 可带 is_error 把失败回传给模型。截至 2026-05。
def call_tool(block):
    try:
        out = run_tool(block.name, block.input)
        return {"type": "tool_result", "tool_use_id": block.id, "content": out}
    except Exception as e:
        return {                       # 错误也是一种 observation
            "type": "tool_result", "tool_use_id": block.id,
            "content": f"error: {e}", "is_error": True,
        }

把失败当结果回传,而不是抛异常

但别让它无限重试。给每个工具一个重试上限,撞到上限还失败,就把失败如实交给用户。盲目重试既烧钱又卡死 —— 这笔账第 11 章细算。

III

并行调用决定速度.

研究助手常常想一次搜好几个角度、或一次抓好几个页面 —— 模型在一轮里给出多个工具调用。串行执行,是在浪费它的并行意图。 一个 assistant 轮次里可以有多个 tool_use 块。把它们并发执行,再把所有 tool_result 放进同一个 user 轮次回传 —— 每个结果带对自己的 tool_use_id 就行,顺序无所谓。1注 1Anthropic 文档 · Tool use —— 工具由 name / description / input_schema(JSON Schema)定义;strict: true 保证调用严格符合 schema;tool_choice 可选 auto / any / tool / none;tool_result 可带 is_error 把失败回传给模型。截至 2026-05。
import asyncio
 
async def run_calls(blocks):
    async def one(b):
        out = await run_tool_async(b.name, b.input)
        return {"type": "tool_result", "tool_use_id": b.id, "content": out}
    return await asyncio.gather(*[one(b) for b in blocks])   # 一起跑,不排队

并发执行一轮里的多个工具调用

但并行有前提:工具之间无依赖,且都能安全并发。读操作放心并行;有副作用的工具(写文件、下单、转账)要想清楚 —— 并发的副作用是最难复现的 bug。 动手 · 给你第二章那个工具做三件事:

工具的 schema 就是它的权限, 描述就是它的说明书。.

A tool's schema is its permission; its description is its manual..

Aklman Library

讨论

讨论.

评论区初始化中…