详解用 JSON Schema 定义函数描述并让模型返回结构化调用结果的完整流程,区分「路由层」与「执行层」,消除解析幻觉。
语言模型在生成文本方面表现出色,但在需要每次都返回机器可读数据时,其行为就不那么可预测了。Function calling——有时被称为 tool use——通过让你以 JSON Schema 格式描述函数,然后让模型决定何时以及如何调用它们来解决这个问题。配合结构化输出约束,你得到的是确定的、可安全解析的响应,而不是指望模型自行将回复格式化正确。
这在生产环境中非常重要。如果模型偶尔返回 "price": "twelve dollars" 而不是 "price": 12.0,就会破坏下游解析逻辑,并需要脆弱的 regex 回退机制。Function calling 消除了这类 bug。
当你发送带有函数定义的请求时,模型并不会调用任何东西。它返回一个结构化的载荷,告诉你「我会用这些参数调用这个函数」。然后由你的代码执行具体的操作——数据库查询、API 调用、本地计算——并可选择将结果反馈给模型以获得最终的自然语言响应。
关键洞察:function calling 是你和模型之间关于输出格式的契约,而不是执行机制。模型是一个路由者,而不是执行者。
主流 provider 的 SDK 使用 JSON Schema 来定义函数。以下是一个最小的 Python 示例:
import json
import openai
client = openai.OpenAI() # uses OPENAI_API_KEY from env
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'Paris'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"default": "celsius"
}
},
"required": ["city"],
"additionalProperties": False
},
"strict": True
}
}
]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "What's the weather in Lyon?"}],
tools=tools,
tool_choice="auto"
)
message = response.choices[0].message
if message.tool_calls:
call = message.tool_calls[0]
args = json.loads(call.function.arguments)
print(f"Function: {call.function.name}")
print(f"Arguments: {args}")
# -> {'city': 'Lyon', 'unit': 'celsius'}
"strict": True 标志强制模型完全符合你的 schema——不允许多余键、不允许缺失必填字段。启用它。没有这个标志,模型可能会省略可选字段或添加意外属性,从而破坏你的反序列化代码。
在真正的 agent 中,你需要将结果反馈回去以继续对话。模式始终相同:执行工具、追加结果、再次调用 API,重复直到没有剩余的工具调用。
def call_tool(name: str, args: dict) -> str:
"""Execute the tool and return a string result."""
if name == "get_weather":
# Replace with a real weather API call
return json.dumps({"city": args["city"], "temp_c": 18, "condition": "partly cloudy"})
raise ValueError(f"Unknown tool: {name}")
def run_agent(user_message: str, max_iterations: int = 10) -> str:
messages = [{"role": "user", "content": user_message}]
iterations = 0
while iterations < max_iterations:
iterations += 1
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
finish_reason = response.choices[0].finish_reason
if finish_reason == "length":
raise RuntimeError("Response truncated — increase max_tokens or shorten schema")
messages.append(msg) # append assistant turn to history
if not msg.tool_calls:
return msg.content # done
for call in msg.tool_calls:
result = call_tool(
call.function.name,
json.loads(call.function.arguments)
)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result
})
raise RuntimeError(f"Agent did not terminate after {max_iterations} iterations")
print(run_agent("What's the weather in Lyon and should I bring a jacket?"))
两点值得注意:始终限制迭代次数,在解析参数之前始终检查 finish_reason。如果在 JSON 解析过程中响应被截断,没有这个保护就会静默失败。
Function calling 适用于「模型决定下一步做什么」的场景。如果你只是需要从非结构化文本中提取结构化数据,Structured Outputs 方式更简洁——传入一个 Pydantic model,得到一个类型化的、经过验证的对象:
from pydantic import BaseModel
from openai import OpenAI
client = OpenAI()
class Invoice(BaseModel):
vendor: str
total_eur: float
due_date: str # ISO 8601
line_items: list[str]
completion = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Extract invoice data from the text."},
{
"role": "user",
"content": (
"Invoice from Acme Corp, 1250 EUR due 2026-09-30. "
"Line items: consulting 10h, travel expenses."
)
}
],
response_format=Invoice,
)
invoice = completion.choices[0].message.parsed
print(invoice.vendor) # Acme Corp
print(invoice.total_eur) # 1250.0
print(invoice.due_date) # 2026-09-30
print(invoice.line_items) # ['consulting 10h', 'travel expenses']
模型要么返回一个有效的 Invoice,要么抛出 RefusalError。不需要 try/except 包裹 json.loads,不需要 regex 回退,不需要后处理。
决策规则很简单:用结构化输出做确定性提取,用 function calling 当模型需要决定采取哪个行动时。
Schema 过度设计。 尽量保持 schema 扁平。深度嵌套的 schema 配合大量可选字段会增加模型做出错误选择的概率。如果你发现自己在三层嵌套,考虑拆分成两个顺序调用。
不处理拒绝。 模型可能会拒绝填充某个字段——特别是对于敏感或模糊的内容。在访问 message.parsed 之前始终检查 message.refusal:
if completion.choices[0].message.refusal:
raise ValueError(f"Model refused: {completion.choices[0].message.refusal}")
暴露过多工具。 不要一次性暴露二十个函数然后期望模型正确选择。对于复杂的 agent,根据对话状态控制可用工具的范围。更多工具意味着更多歧义,意味着更多路由错误。
盲目信任参数。 模型根据用户输入构造工具参数。如果用户输入了对抗性内容,最终函数参数中可能会出现意外值。将每个参数视为不可信输入——在执行前验证、清理和检查授权。
Function calling 和结构化输出是互补的,而不是竞争关系。Tool use 处理代理决策:哪个行动、以什么顺序、用什么参数。结构化输出处理数据提取:每次给我一个类型化对象,不需要解析脆弱的文本。
对于任何涉及用户数据、访问控制或外部服务的操作,对模型可以调用的每个函数应用纵深防御。模型不了解你的业务逻辑——你了解。将 LLM 的函数参数视为你处理用户提供的 HTTP 查询参数一样:验证类型、强制边界、检查权限。在工具层应用扎实的安全加固检查表,可以在生产前阻止最常见的代理漏洞。