工具调用与结构化输出
工具调用和结构化输出解决的是两类不同问题:
| 能力 | 用途 | 典型场景 |
|---|---|---|
| 工具调用 | 让模型请求应用执行某个函数 | 查询天气、读取订单、搜索数据库、调用业务接口 |
| 结构化输出 | 让模型的最终回答符合指定 JSON 结构 | 信息提取、表单生成、分类和程序间数据交换 |
这两项能力都依赖具体模型和上游渠道。使用前,请在模型广场查看模型能力;如果请求被拒绝,应先移除相关高级参数验证基础对话是否正常。
本页示例使用 Python 的 requests 库:
python -m pip install requests
工具调用的工作流程
模型不会替应用真正执行函数。完整流程由应用和模型共同完成:
- 应用把可用工具及其参数结构发送给模型。
- 模型决定是否调用工具,并返回工具名称和参数。
- 应用校验参数并执行本地函数或业务接口。
- 应用把执行结果和对应的工具调用 ID 发回模型。
- 模型根据工具结果生成最终回答。
用户问题 → 模型返回 tool_calls → 应用执行函数
← 模型生成最终回答 ← 应用回传工具结果
模型返回的工具名称和参数不能直接视为可信结果。是否执行、如何执行以及执行权限都由你的应用决定。
定义工具
下面定义一个 get_weather 函数工具。parameters 使用 JSON Schema 描述模型应生成的参数:
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如杭州",
}
},
"required": ["city"],
"additionalProperties": False,
},
"strict": True,
},
}
]
主要字段:
| 字段 | 说明 |
|---|---|
type | 函数工具固定为 function |
function.name | 应用中用于识别工具的稳定名称 |
function.description | 说明何时以及如何使用该工具 |
function.parameters | 函数参数的 JSON Schema |
function.strict | 请求模型严格遵循参数结构;需要模型和渠道支持 |
工具描述应明确、简短,并避免一次提供大量功能相近的工具。工具定义也会占用模型上下文和输入 Token。
完整工具调用示例
下面的脚本演示一次完整循环。示例中的天气数据由本地函数模拟,实际项目可以替换为自己的业务接口。
import json
import requests
API_URL = "https://www.bita-api.com/v1/chat/completions"
HEADERS = {"Authorization": "Bearer YOUR_BITA_API_KEY"}
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如杭州",
}
},
"required": ["city"],
"additionalProperties": False,
},
"strict": True,
},
}
]
def get_weather(city):
return {
"city": city,
"condition": "晴",
"temperature": 26,
"unit": "celsius",
}
messages = [
{
"role": "user",
"content": "杭州现在的天气怎么样?",
}
]
response = requests.post(
API_URL,
headers=HEADERS,
json={
"model": "YOUR_MODEL_ID",
"messages": messages,
"tools": tools,
"tool_choice": "auto",
},
timeout=120,
)
response.raise_for_status()
assistant_message = response.json()["choices"][0]["message"]
messages.append(assistant_message)
tool_calls = assistant_message.get("tool_calls", [])
if not tool_calls:
print(assistant_message.get("content", ""))
raise SystemExit
for tool_call in tool_calls:
function = tool_call["function"]
arguments = json.loads(function["arguments"])
if function["name"] == "get_weather":
tool_result = get_weather(arguments["city"])
else:
tool_result = {"error": "unknown_tool"}
messages.append(
{
"role": "tool",
"tool_call_id": tool_call["id"],
"content": json.dumps(tool_result, ensure_ascii=False),
}
)
final_response = requests.post(
API_URL,
headers=HEADERS,
json={
"model": "YOUR_MODEL_ID",
"messages": messages,
"tools": tools,
},
timeout=120,
)
final_response.raise_for_status()
result = final_response.json()
print(result["choices"][0]["message"]["content"])
模型可能返回零个、一个或多个 tool_calls,因此示例使用循环处理。每个工具结果必须通过原始 tool_call_id 与对应调用关联。
Claude / Anthropic Messages 示例
Claude 原生格式同样支持完整的工具调用闭环,但字段结构与 OpenAI 不同:
- 工具参数结构使用
input_schema。 - 模型通过
content中的tool_use内容块请求调用工具。 - 工具参数位于
tool_use.input,已经是 JSON 对象。 - 应用使用
tool_result内容块回传结果,并通过tool_use_id关联调用。
import json
import requests
API_URL = "https://www.bita-api.com/v1/messages"
HEADERS = {
"x-api-key": "YOUR_BITA_API_KEY",
"anthropic-version": "2023-06-01",
}
tools = [
{
"name": "get_weather",
"description": "查询指定城市的当前天气",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如杭州",
}
},
"required": ["city"],
},
}
]
def get_weather(city):
return {
"city": city,
"condition": "晴",
"temperature": 26,
"unit": "celsius",
}
messages = [
{
"role": "user",
"content": "杭州现在的天气怎么样?",
}
]
response = requests.post(
API_URL,
headers=HEADERS,
json={
"model": "YOUR_MODEL_ID",
"max_tokens": 1024,
"messages": messages,
"tools": tools,
},
timeout=120,
)
response.raise_for_status()
assistant_message = response.json()
messages.append(
{
"role": "assistant",
"content": assistant_message["content"],
}
)
tool_uses = [
block
for block in assistant_message["content"]
if block.get("type") == "tool_use"
]
if not tool_uses:
for block in assistant_message["content"]:
if block.get("type") == "text":
print(block.get("text", ""))
raise SystemExit
tool_results = []
for tool_use in tool_uses:
if tool_use["name"] == "get_weather":
result = get_weather(tool_use["input"]["city"])
else:
result = {"error": "unknown_tool"}
tool_results.append(
{
"type": "tool_result",
"tool_use_id": tool_use["id"],
"content": json.dumps(result, ensure_ascii=False),
}
)
messages.append(
{
"role": "user",
"content": tool_results,
}
)
final_response = requests.post(
API_URL,
headers=HEADERS,
json={
"model": "YOUR_MODEL_ID",
"max_tokens": 1024,
"messages": messages,
"tools": tools,
},
timeout=120,
)
final_response.raise_for_status()
for block in final_response.json()["content"]:
if block.get("type") == "text":
print(block.get("text", ""))
Claude 请求工具时,响应的 stop_reason 通常为 tool_use。一条响应也可能包含多个 tool_use 内容块,因此示例会收集并回传全部结果。
包含 tool_result 的用户消息必须紧跟在产生 tool_use 的助手消息之后。不要在两者之间插入其他消息,并确保每个 tool_result 都使用对应的 tool_use_id。
tool_choice(OpenAI Chat Completions)
tool_choice 用于控制模型如何选择工具:
| 值 | 作用 |
|---|---|
"auto" | 由模型决定直接回答还是调用工具 |
"none" | 禁止调用工具 |
| 指定函数 | 强制或限制使用某个函数;具体结构以接口兼容能力为准 |
默认情况下可以省略 tool_choice。如果模型频繁误调用工具,应先改善函数名称、描述和系统指令,而不是始终强制调用。
执行工具前的校验
应用收到工具调用后,至少应完成以下检查:
- 工具名称必须存在于应用允许的工具列表中。
- 使用 JSON Schema 或业务代码再次验证参数类型和取值范围。
- 从登录会话或服务端上下文获取用户身份,不要让模型自行填写权限信息。
- 删除、付款、发送消息等有外部影响的操作应在执行前获得明确确认。
- 为可能重复提交的操作设置幂等机制。
- 不要把模型生成的内容直接拼接为 Shell 命令、SQL 或代码执行。
使用 JSON Schema 约束输出
如果只需要模型返回固定结构的数据,不需要执行外部函数,可以使用 Structured Outputs。
下面要求 Chat Completions 返回符合指定结构的城市信息:
import json
import requests
response = requests.post(
"https://www.bita-api.com/v1/chat/completions",
headers={"Authorization": "Bearer YOUR_BITA_API_KEY"},
json={
"model": "YOUR_MODEL_ID",
"messages": [
{
"role": "user",
"content": "提取城市和国家:中国浙江省杭州市。",
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "location",
"strict": True,
"schema": {
"type": "object",
"properties": {
"city": {"type": "string"},
"country": {"type": "string"},
},
"required": ["city", "country"],
"additionalProperties": False,
},
},
},
},
timeout=120,
)
response.raise_for_status()
content = response.json()["choices"][0]["message"]["content"]
result = json.loads(content)
print(result["city"])
print(result["country"])
严格模式下,对象通常需要:
- 在
required中列出需要生成的字段。 - 设置
additionalProperties: false,避免产生未定义字段。 - 只使用当前模型支持的 JSON Schema 子集。
即使启用了结构化输出,应用仍应处理拒绝回答、内容截断、网络失败和上游异常等情况。
Chat Completions 与 Responses 的字段区别
两种接口都可以使用结构化输出,但配置位置不同:
Chat Completions
{
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "result",
"strict": true,
"schema": {}
}
}
}
Responses
{
"text": {
"format": {
"type": "json_schema",
"name": "result",
"strict": true,
"schema": {}
}
}
}
不要把 Chat Completions 的 response_format 直接放入 Responses 请求,也不要把 Responses 的 text.format 用于 Chat Completions。
JSON Mode
模型不支持 JSON Schema 时,可以尝试较基础的 JSON Mode:
{
"response_format": {
"type": "json_object"
}
}
JSON Mode 只保证返回内容是有效 JSON,不保证字段符合特定结构。使用时还需要在提示词中明确要求输出 JSON,并由应用自行校验字段和重试。
其他兼容协议
不同协议的工具调用字段并不相同:
| 接口格式 | 工具定义 | 模型请求调用 | 回传工具结果 |
|---|---|---|---|
| OpenAI Chat Completions | tools[].function | message.tool_calls[] | role: "tool" 和 tool_call_id |
| OpenAI Responses | tools[] | output[] 中的 function_call | function_call_output 和 call_id |
| Anthropic Messages | tools[] | content[] 中的 tool_use | tool_result 内容块 |
| Gemini | tools[].functionDeclarations | functionCall | functionResponse |
第三方工具指定协议时,应使用对应协议的完整调用闭环,不能只修改接口地址后继续使用另一种协议的消息结构。
常见问题
| 现象 | 建议检查 |
|---|---|
| 模型直接回答,没有调用工具 | 工具描述是否清晰,问题是否确实需要工具,tool_choice 是否允许调用 |
function.arguments 无法解析 | 模型是否支持工具调用或严格模式;应用是否捕获 JSON 解析错误 |
| 回传结果后接口报错 | 是否保留原始助手消息,并使用正确的 tool_call_id |
| 一次返回多个工具调用 | 遍历全部 tool_calls,分别执行并回传结果 |
response_format 不受支持 | 模型或渠道可能不支持 Structured Outputs,尝试 JSON Mode 或由应用校验普通 JSON |
| JSON 符合语法但字段错误 | JSON Mode 不保证 Schema;应使用 JSON Schema 或业务校验 |
下一步:图片、音频与多模态。