阅读时间:约 15 分钟
前置知识:了解 LLM 是什么;能跑通一个 Python 脚本;读过 P01(或理解 Agent 的闭环概念)


上一篇我们搞清楚了"什么不是 Agent"。这一篇,我们让 LLM 真正调用一个外部工具。


第一部分:Tool Use 的底层原理:没有魔法,只是 Token 预测

很多人以为 Function Calling(工具调用)是 LLM 的"新能力"。其实不是。

Function Calling 没有引入任何新的推理机制。 它的本质是:模型通过训练,学会了在特定情况下切换输出模式:从"生成自然语言"切换到"生成结构化 JSON"。

这个训练分两个阶段:

  1. 监督微调(SFT):给模型看大量"用户问了什么 → 模型该调什么工具 → 参数怎么填"的样本
  2. 强化学习(RL):让模型在真实场景中尝试调用工具,调对了给奖励,调错了惩罚

所以当你传入工具定义时,模型做的其实是:

  1. 阅读工具的 description,判断自己能不能处理
  2. 理解 parameters 的 schema,确认参数类型和必填项
  3. 输出一个 JSON 对象作为工具调用请求

工具描述的质量,直接决定了模型调用的准确率。 这是很多人忽略的一点。2026 年 6 月的一篇 arXiv 论文甚至直接以"MCP Tool Descriptions Are SmellyMCP !"(MCP 工具描述有异味)为题,论证了糟糕的工具描述如何拖垮 Agent 效率。

🤔 思考一下:理解这个原理后,你能解释为什么有时候模型会"调错工具"吗?(提示:工具描述重叠、不清晰)

📌 本章要点:Function Calling 本质是 Token 预测的模式切换。工具描述的质量 > 模型本身的能力。


刚才我们理解了原理。接下来用 3 种方式,让 LLM 调用一个真实的外部工具。

💡 运行环境:以下代码全部使用 openai>=1.0.0 Python SDK(pip install openai)。你需要一个支持 Function Calling 的模型 API:

  1. 安装了 openai>=1.0.0pip install openai
  2. 配置了支持 Function Calling 的模型 API(见下方)
  3. 推荐 Python 3.10+

🔑 模型选择(任选其一)

  • 如果你有 OpenAI 账号:设置 OPENAI_API_KEY,模型用 gpt-4.1gpt-4.1-mini
  • 国内方案 DeepSeek:设置 DEEPSEEK_API_KEYbase_url='https://api.deepseek.com',模型用 deepseek-v4-pro
  • 国内方案通义千问:设置 DASHSCOPE_API_KEYbase_url='https://dashscope.aliyuncs.com/compatible-mode/v1',模型用 qwen3-max

代码中已用占位 YOUR_MODEL_NAMEYOUR_BASE_URL,根据你选择的方案替换即可。


第二部分:3 种 Tool Use 模式:从最简到最实用

模式一:手动拼装(理解原理用)

最原始的方式:自己拼 JSON,手动管理整个流程。这个模式的目的是让你看清每一步,而不是用在生产环境。

📋 运行说明:复制代码保存为 mode1.py,修改 YOUR_MODEL_NAMEYOUR_BASE_URL,运行 python mode1.py

import openai        # 导入 OpenAI SDK
import json          # 导入 JSON 模块(Python 标准库,无需安装)

# ═══════════════════════════════════════════
# 第 1 步:定义工具
# 告诉模型"你手里有哪些工具可以用"
# ═══════════════════════════════════════════
tools = [{                          # 定义一个工具列表(可以包含多个工具)
    "type": "function",            # 固定值:声明这是一个函数型工具
    "function": {                  # 工具的详细信息
        "name": "get_weather",     # 工具名称(模型通过这个名字匹配意图)
        "description": "查询指定城市的当前天气", # 工具描述(模型读这个来决定是否调用)
        "parameters": {            # 参数 schema(限制模型传哪些参数、什么类型)
            "type": "object",      # 参数整体是对象类型
            "properties": {        # 对象的各个字段定义
                "city": {         # city 字段的定义
                    "type": "string",     # 字段类型:字符串
                    "description": "城市名称,如'北京'" # 字段描述
                }
            },
            "required": ["city"]   # 必填字段列表
        }
    }
}]

# ═══════════════════════════════════════════
# 第 2 步:调用模型
# 把用户问题和工具定义一起发给模型
# ═══════════════════════════════════════════
client = openai.OpenAI(             # 创建 OpenAI 客户端
    base_url="YOUR_BASE_URL"        # 替换为你的 API 地址
)
response = client.chat.completions.create(  # 调用聊天补全 API
    model="YOUR_MODEL_NAME",        # 替换为你的模型名
    messages=[                      # 消息历史(只有一个用户消息)
        {"role": "user",           # 消息角色:用户
         "content": "北京今天天气怎么样?"}  # 用户问题
    ],
    tools=tools,                    # 传入我们定义的工具列表
    tool_choice="auto"              # 让模型自己决定要不要调用工具
)

# ═══════════════════════════════════════════
# 第 3 步:解析 Tool Call
# 检查模型是否决定调用工具
# ═══════════════════════════════════════════
msg = response.choices[0].message   # 获取第一个回复消息
if msg.tool_calls:                  # 检查消息中是否包含工具调用
    tool_call = msg.tool_calls[0]   # 获取第一个工具调用
    args = json.loads(tool_call.function.arguments)  # 解析参数(JSON 字符串 → 字典)
    print(f"模型想调用: {tool_call.function.name}")  # 打印工具名
    print(f"参数: {args}")             # 打印参数
    # 输出: 模型想调用: get_weather
    # 输出: 参数: {'city': '北京'}

运行后你会看到

模型想调用: get_weather
参数: {'city': '北京'}

这个代码只"问"了模型想调用什么,还没真正执行工具。下一步要调用工具函数并拿到结果。

📌 本章要点:手动模式让你看清 Tool Use 的 4 个步骤:定义工具 → 调用模型 → 解析 Tool Call → 执行函数。


刚才我们看到了最原始的调用方式。接下来用 OpenAI 的 Responses API,很多步骤被封装好了。

模式二:Responses API(快速上手,仅 OpenAI 官方支持)

OpenAI 的 Responses API 把"调用模型 → 解析 Tool Call → 执行函数 → 提交结果 → 返回最终回答"这些步骤全部封装好。

⚠️ 重要:Responses API 目前仅 OpenAI 官方支持,国内模型(DeepSeek、通义千问)暂不兼容。如果你用的是国内模型,直接跳到模式三。

📋 运行说明:复制保存为 mode2.py。需要 openai>=1.60.0pip install openai --upgrade)和 OpenAI API Key。

import openai        # 导入 OpenAI SDK
import json          # 导入 JSON 模块

# ═══════════════════════════════════════════
# 第 1 步:定义工具
# 工具定义是通用的,不管哪种模式都需要
# ═══════════════════════════════════════════
tools = [{                          # 定义一个工具列表
    "type": "function",            # 固定值:函数型工具
    "function": {                  # 工具详细信息
        "name": "get_weather",     # 工具名称
        "description": "查询指定城市的当前天气", # 工具描述
        "parameters": {            # 参数 schema
            "type": "object",      # 类型:对象
            "properties": {        # 字段定义
                "city": {         # city 字段
                    "type": "string",     # 字符串类型
                    "description": "城市名称" # 字段描述
                }
            },
            "required": ["city"]   # 必填字段
        }
    }
}]

# ═══════════════════════════════════════════
# 第 2 步:定义工具执行函数
# 这是你自己写的、真正执行业务逻辑的代码
# ═══════════════════════════════════════════
def get_weather(city: str) -> str:
    # 参数:city = 城市名称(字符串)
    # 返回值:天气信息(JSON 字符串)
    # 实际项目中,这里会调用真实天气 API(如和风天气、高德)
    # 这里用模拟返回值代替
    return json.dumps({"city": city, "temp": "22°C", "weather": "晴"})

# ═══════════════════════════════════════════
# 第 3 步:调用 Responses API
# 以下步骤全部自动完成:
#   1. 把工具定义和消息发给模型
#   2. 模型决定调用工具 → API 自动执行你的函数
#   3. 把执行结果提交回模型
#   4. 返回模型最终生成的回答
# ═══════════════════════════════════════════
client = openai.OpenAI()          # 创建 OpenAI 客户端
response = client.responses.create( # 调用 Responses API(不是 chat.completions)
    model="gpt-4.1",              # 模型名(⚠️ 仅 OpenAI 官方支持此 API)
    input="北京今天天气怎么样?",   # 用户输入
    tools=tools                   # 传入工具列表,自动处理调用循环
)

# ═══════════════════════════════════════════
# 第 4 步:拿到最终回答
# response.output_text 已经是模型最终生成的回答
# ═══════════════════════════════════════════
print(response.output_text)       # 打印最终回答
# 输出: 北京今天天气晴朗,气温22°C。

运行后你会看到

北京今天天气晴朗,气温22°C。

模式二和模式一的区别:模式二把"解析 Tool Call → 执行函数 → 提交结果 → 让模型再回答"这 4 步全封装了,你只需要一行 responses.create

📌 本章要点:Responses API 适合快速原型。但国内模型暂不支持,且生产环境中需要更细粒度的控制。


前两种模式都是一次调用一个工具。真实场景中,经常需要连续调用多个工具,在循环中反复判断。

模式三:Agent 循环(生产环境用,所有模型通用)

这是最接近"真正的 Agent"的模式:模型在一个循环中反复"思考 → 调用工具 → 观察结果 → 再思考"。这是生产环境的标准写法。

📋 运行说明:复制保存为 mode3.py。需要 openai>=1.0.0pip install openai)。修改 YOUR_MODEL_NAMEYOUR_BASE_URL

import openai        # 导入 OpenAI SDK
import json          # 导入 JSON 模块

# ═══════════════════════════════════════════
# 初始化:创建客户端
# ═══════════════════════════════════════════
client = openai.OpenAI(             # 创建 OpenAI 客户端
    base_url="YOUR_BASE_URL"        # 替换为你的 API 地址
)

# ═══════════════════════════════════════════
# 第 1 步:定义多个工具
# 这里定义 2 个工具:查询天气 + 数学计算
# 实际项目中,工具列表可能包含几十个甚至上百个
# ═══════════════════════════════════════════
tools = [                    # 定义多个工具(数组,可包含任意数量)
    {                        # 工具 1:查询天气
        "type": "function",  # 固定值:函数型工具
        "function": {        # 工具 1 的详细信息
            "name": "get_weather",     # 工具名称
            "description": "查询城市天气", # 工具描述
            "parameters": {   # 参数 schema
                "type": "object",
                "properties": {
                    "city": { # city 参数:城市名称
                        "type": "string",
                        "description": "城市名"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {                        # 工具 2:数学计算
        "type": "function",  # 固定值
        "function": {        # 工具 2 的详细信息
            "name": "calculate",       # 工具名称
            "description": "执行数学计算", # 工具描述
            "parameters": {   # 参数 schema
                "type": "object",
                "properties": {
                    "expression": {    # expression 参数:数学表达式
                        "type": "string",
                        "description": "数学表达式,如'2+3*4'"
                    }
                },
                "required": ["expression"]
            }
        }
    }
]

# ═══════════════════════════════════════════
# 第 2 步:定义工具执行函数
# 根据工具名执行不同逻辑
# ═══════════════════════════════════════════
def execute_tool(name, args):
    """
    根据工具名和执行参数,返回执行结果
    name: 工具名称(字符串)
    args: 工具参数(字典)
    返回: 执行结果(JSON 字符串)
    """
    if name == "get_weather":     # 如果调用的是 get_weather
        # 用城市名查询天气(此处模拟返回)
        return json.dumps({"city": args["city"], "temp": "22°C"})
    elif name == "calculate":     # 如果调用的是 calculate
        # 执行数学表达式
        # ⚠️ 实际项目中不要直接用 eval(),用安全表达式计算器
        return json.dumps({"result": eval(args["expression"])})
    return json.dumps({"error": "unknown tool"})  # 工具名不存在

# ═══════════════════════════════════════════
# 第 3 步:启动 Agent 循环
# 核心逻辑:模型反复"思考 → 调用工具 → 观察结果 → 再思考"
# ═══════════════════════════════════════════
messages = [                        # 初始化消息历史
    {"role": "user",                # 消息角色:用户
     "content": "北京和上海的温差是多少度?北京22度,上海28度"}  # 用户问题
]

for i in range(5):                  # 最多循环 5 次(防止模型陷入死循环)
    response = client.chat.completions.create(  # 调用模型
        model="YOUR_MODEL_NAME",    # 替换为你的模型名
        messages=messages,          # 传入当前消息历史
        tools=tools,                # 传入工具列表
        tool_choice="auto"          # 让模型自己决定是否调用工具
    )
    
    msg = response.choices[0].message  # 获取模型回复消息
    messages.append(msg)               # 把模型回复加入消息历史
    
    # ──── 判断:模型是否调用了工具? ────
    if msg.tool_calls:                # 检查是否有工具调用
        for tc in msg.tool_calls:     # 遍历所有工具调用(可能同时多个)
            args = json.loads(tc.function.arguments)  # 解析参数
            result = execute_tool(tc.function.name, args)  # 执行工具
            messages.append({         # 把工具结果加入消息历史
                "role": "tool",       # 角色必须是 "tool"
                "tool_call_id": tc.id,# 关联调用 ID(模型靠它匹配结果)
                "content": result     # 工具执行的结果
            })
        continue  # 继续循环,让模型根据工具结果再推理
    
    # ──── 判断:模型决定最终回答(不再调用工具) ────
    print(msg.content)  # 打印最终回答
    break  # 退出循环

运行后你会看到

北京气温22°C,上海气温28°C,温差是6°C。

关键设计点

  • for i in range(5):最大循环 5 次,防止模型不断调用工具陷入死循环
  • tool_choice="auto":让模型自己决定是否需要调用工具
  • role: "tool":角色必须是 "tool",否则模型不知道这是工具返回的结果
  • tool_call_id:每个工具调用有唯一 ID,模型用它来关联结果和调用

这其实就是 Cursor、Claude Code 等编码 Agent 的底层工作原理。Cursor 在 2026 年 6 月刚被 SpaceX 以 600 亿美元收购,很大一部分价值就来自这套工具调用的可靠性。

🤔 思考一下:对比 P01 里的决策循环图:这个代码里的"感知"(messages)、“决策”(chat.completions.create)、“执行”(execute_tool)、“判断”(msg.tool_calls)分别对应哪几行?

📌 本章要点:Agent 循环 = 模型反复"思考 → 调用 → 观察 → 再思考"。核心是循环控制(最大次数)和结果反馈(tool message)。


第三部分:踩坑实录:你一定会遇到的 4 个问题

坑 1:工具描述写得像 API 文档

这是 2026 年 6 月 arXiv 论文"MCP Tool Descriptions Are Smelly!"重点讨论的问题。工具描述的质量直接影响 Agent 效率。

错误示例

{
    "name": "query_db",
    "description": "Query database with SQL statement",
    "parameters": {
        "properties": {
            "sql": {"type": "string"}
        }
    }
}

问题:描述太抽象:模型不知道什么时候该用、什么时候不该用、参数怎么填。

正确示例

{
    "name": "query_db",
    "description": "查询用户数据库。只用于读取数据(SELECT),不能用于修改数据(INSERT/UPDATE/DELETE)。如果用户要求修改数据,回复'我只能查询数据,修改请联系管理员'。",
    "parameters": {
        "properties": {
            "sql": {
                "type": "string",
                "description": "SQL 查询语句,只支持 SELECT。示例:SELECT * FROM users WHERE city='北京'"
            }
        }
    }
}

工具描述不是 API 文档,是给模型的指令。 你要告诉模型:什么时候用、什么时候不用、参数怎么填、边界在哪里。

📌 本章要点:工具描述 = 给模型的指令,不是 API 文档。写清触发条件、参数示例、边界约束。


坑 2:模型调错了工具 / 填错了参数

Function Calling 的失败模式有 4 种:

失败类型表现原因
调错工具该用 A 工具,用了 B工具描述重叠、不清晰
参数填错工具用对了,参数格式/值错误schema 不够严格
该调没调需要工具时模型直接回答了工具描述没写清触发条件
不该调时调了不需要工具时模型硬调了工具描述太宽泛

一个真实 bug:截至 2026 年 6 月,DeepSeek V4 在使用 tool_choice="required" 时,实际强制调用成功率约 60%(GitHub Issue #1376)。这意味着在复杂 Agent 场景下,你不能完全依赖"强制调用",需要在代码层做兜底。

解决方案:把失败模式映射到工具描述的改进上,同时在代码中做好重试和降级逻辑。Patronus AI(2026 年 6 月获 5000 万美元融资)做的就是把这类失败模式变成可量化的评测指标。

🤔 思考一下:你遇到过哪种失败?当时是怎么处理的?如果重来,你会改工具描述还是改代码?


坑 3:没有幂等性保护

真实场景:Agent 调用"创建订单"工具,第一次调用成功了,但网络超时没收到返回。Agent 以为失败了,自动重试:结果创建了两个订单。

解决方案:给写操作加幂等性键。

📋 运行说明:复制保存为 idempotency.py,运行 python idempotency.py

import json

# 模拟数据库(实际项目替换为 Redis/PostgreSQL)
db = {}

def create_order(item_id: str, idempotency_key: str) -> dict:
    """
    创建订单,带幂等性保护
    item_id: 商品 ID(字符串)
    idempotency_key: 幂等性键(每次调用传入唯一请求 ID)
    返回: 订单结果(字典)
    
    幂等性保护原理(三步):
    1. 先检查 idempotency_key 是否已存在缓存
    2. 如果存在 → 直接返回缓存结果(不重复执行)
    3. 如果不存在 → 执行操作 → 缓存结果 → 返回
    """
    # 第 1 步:检查是否已处理过
    cache_key = f"idempotency:{idempotency_key}"  # 构建缓存键
    if db.exists(cache_key):      # 检查缓存中是否有这个请求的结果
        return db.get(cache_key)  # 返回缓存结果(不重复执行)
    
    # 第 2 步:执行操作(只有第一次到达这里)
    order_result = db.create_order(item_id)  # 调用数据库创建订单
    
    # 第 3 步:缓存结果(用于后续相同请求)
    db.set(cache_key, order_result)  # 缓存结果
    return order_result  # 返回结果

规则:读操作可以重试,写操作必须有 request_id、事务状态或去重键。

📌 本章要点:幂等性是 Tool Use 的安全底线:读操作可重试,写操作必须去重。

🤔 思考一下:回顾你的项目:有哪些工具调用是写操作?加过幂等性键吗?


坑 4:错误信息对模型没有帮助

错误示例:返回 {"error": "failed"}

模型看到这个,除了"再试一次"什么都做不了。

正确示例:返回结构化错误信息

{
    "error_type": "parameter_invalid",
    "retryable": false,
    "message": "城市名'北精'不存在,你是否想输入'北京'?",
    "suggested_action": "ask_user_to_confirm"
}

结构化错误是工具层和 Agent 之间的协议。error_typeretryablesuggested_action 这些字段能让模型做出正确的下一步决策。

📌 本章要点:工具返回的错误信息要对模型有帮助:告诉它发生了什么、能不能重试、下一步该怎么做。


总结:读完这篇,你应该带走这几件事

  1. Function Calling 不是新能力,是 Token 预测的模式切换。工具描述的质量 > 模型的能力。
  2. 3 种模式逐步进阶:手动拼装(理解原理)→ Responses API(快速上手)→ Agent 循环(生产环境)。
  3. 工具描述 = 给模型的指令,不是 API 文档。写清触发条件、参数示例、边界约束。
  4. 幂等性是安全底线:读操作可重试,写操作必须去重。
  5. 结构化错误:让模型知道发生了什么、能不能重试、下一步该怎么做。
  6. DeepSeek V4 用户特别注意:幻觉率 94-96%,必须加校验层;tool_choice="required" 不稳定,代码层兜底。
  7. 这不仅仅是一个技术点:Forbes 6 月 25 日发文称"未来 AI 取决于 Agent 基础设施"。Tool Use 正在从技巧变成基础设施。

🤔 思考一下:你现在的项目里,有没有一个工具调用经常出问题?用今天学到的方法,你会怎么改工具描述?如果用的是 DeepSeek V4,你打算怎么加校验层?


思维导图

  • Tool Use 实战:3 种模式
    • 底层原理
      • Function Calling = Token 预测的模式切换
      • SFT + RL 两阶段训练
      • 工具描述质量 > 模型能力
    • 3 种调用模式
      • 手动拼装:理解原理,4 步骤
      • Responses API:一键封装(仅 OpenAI)
      • Agent 循环:生产标准写法,通用兼容
    • 4 个常见坑
      • 工具描述像 API 文档 → 改成给模型的指令
      • 调错工具/填错参数 → 严格 schema + 清晰描述
      • 没有幂等性保护 → 写操作加去重键
      • 错误信息没帮助 → 结构化错误协议
    • 核心 Takeaways

下一篇预告

P03. 上下文工程:不是堆 token,是精准投喂

工具能调用了,但模型的"记忆"怎么管?上下文窗口不是越大越好:塞太多会淹没关键信息,塞太少会丢失上下文。

这一篇讲:上下文的 4 层结构、压缩策略、按需加载。

🤔 思考一下:你的 Agent 在处理长任务时,有没有遇到过"前面的上下文被冲掉了"的问题?下一篇会给你答案。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐