让大模型从“会回答”走向真正调用业务系统

Function Calling 并不是让模型直接执行 Python 函数,而是建立一套结构化协议: 模型判断需要什么能力、生成参数,你的应用执行真实代码,再把结果交回模型。 本文用同一个订单查询场景,对照 OpenAI Responses API 与 Claude Messages API 的完整写法。

Function Calling 到底是什么

普通对话只要求模型返回文字。Tool Calling 则允许开发者在请求里附上一组工具说明, 例如 get_order_statuscreate_ticketsend_email。 模型读到用户需求后,可以不直接回答,而是返回一个结构化的"调用请求"。

模型不是业务系统的执行者,更像一个会理解自然语言的调度器。 它负责决定"调用哪个工具、传什么参数";你的代码负责权限校验、真实执行和结果回传。

这一区分很重要。模型可以生成 {"order_id":"A1024"}, 但它无法凭空访问你的订单数据库。只有应用收到调用请求后,主动执行数据库查询, 订单状态才真正被取回。

一次工具调用,本质上是五步对话

调用 ID 不是装饰字段

OpenAI 使用 call_id,Claude 使用 tool_use_id。 并行调用时,模型需要靠它判断每份结果属于哪一个请求。

Tool Schema 不是接口文档的缩写版

模型只看得到你提供的工具定义。函数名含糊、描述过短、字段含义不清, 都会直接降低选工具和填参数的准确率。一个可用的 Tool Schema 至少要回答四个问题: 这个工具做什么、什么时候用、每个参数表示什么、什么情况不该用。

复制代码
{
  "name": "get_order_status",
  "description": "查询已登录用户的一笔订单。仅用于读取订单状态;
不要用它取消订单或修改收货地址。order_id 来自用户提供的订单编号。",
  "parameters / input_schema": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "订单编号,例如 A1024;不要传入用户姓名。"
      }
    },
    "required": ["order_id"],
    "additionalProperties": false
  }
}

设计时更值得关注的几件事

名称要能区分语义。 与其写 query,不如写 orders_get_statusgithub_list_pull_requests

用 enum 限制合法状态。 比起让模型自由生成字符串,["pending","shipped","delivered"] 更稳定。

不要让模型填写应用已经知道的值。 当前用户 ID、租户 ID、权限范围应由服务端上下文注入,而不是交给模型猜。

返回高信号结果。 工具输出只保留模型完成下一步所需字段,不要把整张数据库记录原样塞回上下文。

OpenAI:处理 function_callfunction_call_output

在 Responses API 中,函数工具放在顶层 tools 数组里。 当模型需要查询订单时,response.output 中会出现 type: "function_call" 的 item。参数位于 arguments, 通常需要先做 JSON 解析。

1. 定义工具并发起第一次请求

python 复制代码
from openai import OpenAI
import json

client = OpenAI()

tools = [{
    "type": "function",
    "name": "get_order_status",
    "description": (
        "查询当前登录用户的一笔订单。只读取状态,不修改订单。"
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "订单编号,例如 A1024"
            }
        },
        "required": ["order_id"],
        "additionalProperties": False
    },
    "strict": True
}]

input_items = [{
    "role": "user",
    "content": "帮我查一下订单 A1024 到哪里了。"
}]

response = client.responses.create(
    model="gpt-5.5",
    tools=tools,
    input=input_items
)

2. 找出调用、执行函数并回传结果

python 复制代码
def get_order_status(order_id: str) -> dict:
    # 示例:真实项目中应查询数据库或内部服务
    fake_db = {
        "A1024": {
            "status": "shipped",
            "carrier": "SF Express",
            "eta": "2026-06-14"
        }
    }
    return fake_db.get(order_id, {"error": "order_not_found"})

# 保留模型本轮输出,下一轮需要完整上下文
input_items.extend(response.output)

for item in response.output:
    if item.type != "function_call":
        continue

    args = json.loads(item.arguments)

    if item.name == "get_order_status":
        result = get_order_status(args["order_id"])
    else:
        result = {"error": "unknown_tool"}

    input_items.append({
        "type": "function_call_output",
        "call_id": item.call_id,
        "output": json.dumps(result, ensure_ascii=False)
    })

final_response = client.responses.create(
    model="gpt-5.5",
    tools=tools,
    input=input_items
)

print(final_response.output_text)

不要假设一次只会返回一个调用

response.output 中可能有零个、一个或多个 function_call。 路由代码应该遍历全部 item,而不是只取第一个。

Reasoning model 的上下文要完整保留

当模型输出中包含与工具调用相关的 reasoning items 时,也需要随工具结果一起传回。 最稳妥的写法就是把上一轮 response.output 整体加入下一轮输入。

Claude:处理 tool_usetool_result

Claude 的 client tool 同样由你的应用执行,但数据结构不同。 工具参数字段叫 input_schema;模型发起调用时, 响应 content 中出现 tool_use block, 整体 stop_reason 通常为 tool_use

python 复制代码
import anthropic
import json

client = anthropic.Anthropic()

tools = [{
    "name": "get_order_status",
    "description": (
        "查询当前登录用户的一笔订单。仅用于读取物流和订单状态,"
        "不能取消订单、退款或修改地址。"
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "订单编号,例如 A1024"
            }
        },
        "required": ["order_id"],
        "additionalProperties": False
    },
    "strict": True
}]

messages = [{
    "role": "user",
    "content": "帮我查一下订单 A1024 到哪里了。"
}]

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    tools=tools,
    messages=messages
)

2. 执行工具,把结果作为 user message 回传

python 复制代码
# 先把 Claude 的完整 assistant content 放入历史
messages.append({
    "role": "assistant",
    "content": response.content
})

tool_results = []

for block in response.content:
    if block.type != "tool_use":
        continue

    try:
        if block.name == "get_order_status":
            result = get_order_status(block.input["order_id"])
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps(result, ensure_ascii=False)
            })
        else:
            raise ValueError("unknown_tool")
    except Exception as exc:
        tool_results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": str(exc),
            "is_error": True
        })

# tool_result blocks 必须放在 content 数组前面
messages.append({
    "role": "user",
    "content": tool_results
})

final_response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    tools=tools,
    messages=messages
)

final_text = "".join(
    block.text
    for block in final_response.content
    if block.type == "text"
)
print(final_text)

Client tools 与 Server tools

Claude 文档明确区分工具在哪一侧执行。自定义函数通常属于 client tools: Claude 只生成 tool_use,你的应用负责执行。像 web_searchweb_fetchcode_execution 等 server tools,则由 Anthropic 基础设施执行,应用不需要手动运行对应代码。

OpenAI 与 Claude:概念一致,协议不同

环节 OpenAI Responses API Claude Messages API
工具定义位置 顶层 tools 顶层 tools
Schema 字段 parameters input_schema
函数工具标识 type: function 自定义 client tool 通常直接写 name / description
模型发起调用 function_call output item tool_use content block
调用参数 arguments,JSON 字符串 input,对象
调用关联 ID call_id id,回传时写入 tool_use_id
回传结果 function_call_output tool_result
错误标记 通常在 output 中返回结构化错误 可显式设置 is_error: true
强制调用 required 或指定 function any 或指定 tool
禁止调用 none none
关闭并行 parallel_tool_calls: false disable_parallel_tool_use: true

所以迁移时不要只做字段名替换。最容易漏掉的是消息历史组织方式: OpenAI 需要把输出 item 和 function_call_output 继续送回; Claude 需要保留 assistant 的 content blocks,再用紧随其后的 user message 发送 tool_result

并行调用、tool_choice 与 Strict Mode

并行调用:同一轮可能有多个工具请求

用户问"查 A1024 和 B2048 两个订单",模型可能在同一轮生成两个调用。 如果它们互不依赖,可以并发执行;如果后一个动作依赖前一个结果, 更合理的方式是让模型分两轮调用。

OpenAI 可通过 parallel_tool_calls=false 把一轮限制为零或一个函数调用。 Claude 可通过 disable_parallel_tool_use=true 关闭并行。 Claude 同一 assistant turn 中的多个调用在语义上是无序的,不应默认后一个依赖前一个。

tool_choice:控制模型是否必须使用工具

OpenAI

auto:模型自行决定;required:至少调用一个;指定 function:强制调用某个函数;none:禁止工具。还可以用 allowed tools 限定本轮可选子集。

Claude

auto:自行决定;any:必须选一个工具;tool:指定工具;none:禁止工具。部分 thinking 配置对强制模式有额外限制。

Strict Mode:保证"格式正确",不是保证"业务正确"

两家都支持在工具定义中设置 strict: true,目的是让模型输出符合 JSON Schema。 OpenAI 的 strict schema 要求 object 设置 additionalProperties:false, 并把 properties 中的字段全部列入 required;可选字段通常通过允许 null 表达。 Claude 则使用 grammar-constrained sampling 将输出限制在支持的 Schema 子集内。

Strict 不等于安全

Schema 能保证 amount 是 number,却不能判断用户有没有退款权限,也不能判断 100000 是否超过业务上限。身份、授权、额度、资源归属和审计仍必须由服务端完成。

工具失败后,不要伪装成成功

调用外部系统必然会遇到超时、限流、资源不存在和权限不足。 错误应该以模型可理解、程序可记录的形式回传。模型拿到错误后,可以解释失败原因、 请求用户补充信息,或者调整参数再次调用。

复制代码
{
  "ok": false,
  "error": {
    "code": "ORDER_NOT_FOUND",
    "message": "当前账户下不存在该订单",
    "retryable": false
  }
}

Claude 的 tool_result 还可以设置 is_error:true。 OpenAI 一般把错误对象序列化到 function_call_output.output 中。 无论使用哪一家,都不要把异常堆栈、数据库连接串或内部路径直接暴露给模型。

第二次请求不保证一定返回自然语言。模型可能继续调用另一个工具,例如先查订单, 再根据物流异常创建工单。因此应用通常需要一个循环:只要还有 tool calls 就继续执行, 直到模型返回最终文本、达到最大步数,或触发安全终止条件。

必须设置循环上限

建议限制最大工具轮数、单轮调用数、总耗时和总成本。否则参数错误或提示冲突可能造成重复调用。

生产环境真正需要防的,不是 JSON 解析失败

工具名使用 allowlist 路由。 不要根据模型生成的任意字符串动态导入模块或拼接命令。

对参数再次做服务端校验。 即使开启 strict,也要验证资源归属、长度、范围和业务约束。

写操作默认要求确认。 退款、转账、删除、发信、发布内容等动作应分成"预览/确认/执行"阶段。

使用幂等键。 网络重试或模型重复调用时,避免同一笔退款、同一封邮件被执行两次。

工具只返回必要字段。 控制上下文成本,也减少把敏感数据意外带入后续生成的风险。

记录完整 trace。 至少记录 request ID、tool call ID、工具名、耗时、结果状态和脱敏后的参数。

把用户输入与工具输出都视为不可信数据。 外部网页、邮件或数据库文本可能包含 prompt injection。

先做小工具集,再逐步扩展。 可选工具过多会增加选择歧义和 token 成本;大型系统应考虑 tool search 或分层路由。

相关推荐
IvorySQL1 小时前
PostgreSQL 技术日报 (6月11日)|规划器扩展优化,POSETTE 大会倒计时
数据库·postgresql
胡小禾1 小时前
Redis哨兵模式下主从同步的偏差
数据库·redis·缓存
zzqssliu2 小时前
Taocarts接口限流实操:基于Redis实现API防刷与流量管控
数据库·redis·缓存
啦啦啦啦啦zzzz2 小时前
redis的持久化操作和主从复制与集群的关系及其应用
数据库·redis
IT策士2 小时前
Redis 从入门到精通:分片之道 —— Redis Cluster
数据库·redis·缓存
AOwhisky3 小时前
学习自测与解析:Redis系列第一期与第二期核心知识点详解
运维·数据库·redis·学习·云计算
kishu_iOS&AI3 小时前
LLM —— Milvmus向量数据库
数据库·人工智能·milvus
名不经传的养虾人3 小时前
从0到1:企业级AI项目迭代日记 Vol.46|三个检索源、缓存限流、深度整合——联网检索一日冲刺
数据库·人工智能·agent·ai编程·ai工作流·企业ai
BugShare3 小时前
Mac 上原生开发的开源免费、尽享丝滑数据库工具
数据库·macos·开源