Function Calling 与结构化输出
🔧 Function Calling 让 LLM 从文本生成器变成行动执行者;结构化输出确保返回结果机器可解析。
先建立正确心智模型
Function Calling 是一份模型与应用之间的协议:模型提出“调用哪个工具、参数是什么”,应用负责校验、鉴权、执行和回传。模型不能绕过应用直接获得权限。
Structured Output 是另一份协议:它约束模型最终返回的数据形状。两者可以组合:工具参数用 Schema 约束,最终答复再使用另一个 Schema。
Function Calling 基础
工作流程
1. 定义工具(名称、描述、参数 Schema)
2. 用户提问 → LLM 决策是否调用工具
3. LLM 返回 tool_call(工具名 + 参数 JSON)
4. 应用层执行工具,获取结果
5. 将结果放回对话,LLM 生成最终回答
工具定义示例
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如北京、上海"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["city"]
}
}
}
结构化输出
方式一:JSON Mode
强制模型输出合法 JSON,但不保证字段结构:
response = client.chat.completions.create(
model="gpt-4o",
response_format={"type": "json_object"},
messages=[{"role": "user", "content": "..."}]
)
方式二:Structured Outputs(推荐)
提供 JSON Schema,模型严格遵守结构:
from pydantic import BaseModel
class NewsItem(BaseModel):
title: str
summary: str
tags: list[str]
sentiment: Literal["positive", "negative", "neutral"]
response = client.beta.chat.completions.parse(
model="gpt-4o",
response_format=NewsItem,
messages=[...]
)
news = response.choices[0].message.parsed
一个可靠的工具循环
MAX_STEPS = 8
items = initial_items
for step in range(MAX_STEPS):
response = model.generate(items=items, tools=tools)
calls = [x for x in response.output if x.type == "function_call"]
if not calls:
return validate_final(response)
outputs = []
for call in calls:
args = validate_schema(call.arguments)
authorize(user, call.name, args)
result = execute_with_timeout_and_idempotency(call.name, args)
outputs.append({
"type": "function_call_output",
"call_id": call.call_id,
"output": safe_serialize(result),
})
items += response.output + outputs
raise MaxStepsExceeded()
关键点:
- 同时限制步骤数、时间、token 和费用。
- 写操作使用幂等键,网络重试不会重复产生副作用。
- 参数通过 Schema 只是第一步,还要做用户、租户和资源级鉴权。
- 工具错误应结构化返回:可重试、参数错误、权限错误或不可恢复。
- 工具结果也是不可信数据,外部网页或邮件中的指令不能改变策略。
并行工具调用
一次对话可以同时调用多个工具:
# LLM 可能返回多个 tool_calls
for tool_call in response.choices[0].message.tool_calls:
result = execute_tool(tool_call.function.name,
json.loads(tool_call.function.arguments))
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
工具设计原则
| 原则 | 说明 |
|---|---|
| 单一职责 | 每个工具只做一件事 |
| 幂等性 | 相同输入多次调用结果一致 |
| 描述精确 | description 比 name 更影响调用决策 |
| 参数精简 | 参数越少,模型越准确 |
| 返回简洁 | 只返回模型需要的信息 |
常见问题排查
| 问题 | 原因 | 解决 |
|---|---|---|
| 模型不调用工具 | description 不够清晰 | 改进工具描述,加示例 |
| 参数填错 | 参数名歧义 | 用 description 详细说明每个参数 |
| 重复调用 | 工具结果没有放回对话 | 确保 tool result 正确追加 |
| JSON 解析失败 | 模型输出不合规 | 用 Structured Outputs 或加校验重试 |
常见误区
- Function Calling 不是自动执行,需要应用层手动调用并将结果传回
- tool_choice: "auto" 时模型可能选择不调用工具,需要在对话设计上引导
- 工具数量超过 15-20 个时,模型选择准确率下降,考虑分类或动态注入