结构化输出与 Function Calling¶
⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。
✅ 核验说明(2026-03-26):本章已按 OpenAI 官方 Structured Outputs / Function Calling 文档复核。示例默认模型统一更新为
gpt-5.4;多工具调用能力描述改为当前更稳妥的通用口径。学习时间: 6-8 小时 难度级别: ⭐⭐⭐ 中级 前置知识: Prompt 工程(第 2 章)、 Agent 开发基础(第 7 章) 学习目标: 掌握 JSON Mode 、结构化输出、 Function Calling 与 Tool Use 的原理与工程实践
📖 章节导读¶
结构化输出和 Function Calling 是大模型从"聊天工具"进化为"可编程引擎"的关键能力。掌握这些技术,能让 LLM 可靠地产出程序可解析的数据,并与外部系统进行精确交互。
1. 结构化输出(Structured Output)¶
1.1 为什么需要结构化输出¶
传统 LLM 输出自由文本,导致以下问题: - 解析不可靠:正则/字符串匹配容易失败 - 类型不确定:数字可能返回为字符串 - 格式不一致:同一 Prompt 多次调用返回格式不同
结构化输出通过约束解码(Constrained Decoding)保证输出严格遵循指定 Schema 。这意味着 LLM 在生成每个 token 时,会自动屏蔽掉不符合 JSON Schema 的 token,从而在推理阶段就保证输出的合法性,而非生成后再做校验——这是 JSON Mode 和 Structured Output 的本质区别。
1.2 JSON Mode vs Structured Output¶
| 特性 | JSON Mode | Structured Output |
|---|---|---|
| 保证合法 JSON | ✅ | ✅ |
| 保证符合 Schema | ❌ | ✅ |
| 字段名保证 | ❌ | ✅ |
| 类型保证 | ❌ | ✅ |
| 支持嵌套对象 | 部分 | ✅ |
| 主要提供商 | OpenAI/Anthropic/Gemini | OpenAI(2024.08+) |
1.3 OpenAI Structured Output 实战¶
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
# 定义输出Schema(使用Pydantic)
class ResearchPaper(BaseModel): # Pydantic BaseModel:自动数据验证和序列化
title: str
authors: list[str]
year: int
abstract_summary: str
key_contributions: list[str]
methodology: str
dataset: str | None = None
metrics: dict[str, float]
# 使用Structured Output
response = client.chat.completions.parse(
model="gpt-5.4",
messages=[
{"role": "system", "content": "你是论文分析专家,请提取论文的结构化信息。"},
{"role": "user", "content": """
分析这篇论文:
Attention Is All You Need (2017)
By Vaswani et al.
提出了Transformer架构...
"""}
],
response_format=ResearchPaper, # 直接传入Pydantic模型
)
paper = response.choices[0].message.parsed
print(f"标题: {paper.title}")
print(f"年份: {paper.year}")
print(f"贡献: {paper.key_contributions}")
1.4 Anthropic/Gemini 的 JSON 模式¶
# Anthropic Claude - 使用tool_use技巧实现结构化输出
import anthropic
client = anthropic.Anthropic()
# Claude通过Tool定义实现结构化输出
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[{
"name": "extract_info",
"description": "提取结构化信息",
"input_schema": {
"type": "object",
"properties": {
"sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
"keywords": {"type": "array", "items": {"type": "string"}}
},
"required": ["sentiment", "confidence", "keywords"]
}
}],
tool_choice={"type": "tool", "name": "extract_info"},
messages=[{"role": "user", "content": "分析这段评论的情感: '这个产品太棒了!'"}]
)
1.5 受限解码原理¶
结构化输出的底层机制:
常规生成: logits → softmax → 采样(所有token)
受限解码: logits → mask非法token → softmax → 采样(仅合法token)
示例 - 生成JSON {"name": 后:
合法token: [a-z, A-Z, 0-9, ", ', 空格...]
非法token: [}, ], 换行, EOF...] ← 被mask为-inf
💡 为什么受限解码比后处理校验更可靠? 后处理方案(生成 JSON →
json.loads()→ 失败则重试)存在两个问题:一是重试浪费 token 和延迟,二是 LLM 可能反复犯同样的格式错误。受限解码在生成阶段就杜绝了非法 token,一次生成即合法,生产环境中可显著降低重试率和尾部延迟。
框架实现: - Outlines: 基于正则表达式的约束采样,适合本地部署的开源模型 - Guidance: 微软的受限生成框架,支持交替生成文本和代码 - Instructor: 基于 Pydantic 的 LLM 结构化输出库,通过打补丁方式为 OpenAI/Anthropic 等多 provider 添加结构化输出支持
2. Function Calling / Tool Use¶
2.1 核心概念¶
Function Calling 让 LLM 从"只能输出文本"进化为"能驱动外部系统"。具体来说,它让 LLM 能够: 1. 识别意图:判断用户请求是否需要调用外部函数(而非仅靠文本回答) 2. 生成参数:根据函数的 JSON Schema 描述,生成类型安全的调用参数 3. 多轮调用:支持串行/并行多函数调用,构建复杂工作流
⚠️ 关键认知:LLM 本身不执行函数,它只输出"我想调用哪个函数、传什么参数"。应用层负责实际执行,并将结果以
tool消息返回给 LLM。这种设计让 LLM 保持在无副作用的"决策者"角色,安全可控。
用户: "北京今天天气怎么样?"
↓ LLM判断需要调用天气API
模型: function_call: get_weather(city="北京", date="2026-03-25")
↓ 应用层执行函数
结果: {"temp": 32, "condition": "晴"}
↓ 将结果返回给LLM
模型: "北京今天32°C,晴天。"
2.2 OpenAI Function Calling 实战¶
import json
from openai import OpenAI
client = OpenAI()
# 定义工具
tools = [
{
"type": "function",
"function": {
"name": "search_papers",
"description": "搜索学术论文",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词"
},
"year_from": {
"type": "integer",
"description": "起始年份"
},
"max_results": {
"type": "integer",
"description": "最大结果数",
"default": 10
}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate_metrics",
"description": "计算模型评估指标",
"parameters": {
"type": "object",
"properties": {
"predictions": {
"type": "array",
"items": {"type": "number"},
"description": "模型预测值"
},
"ground_truth": {
"type": "array",
"items": {"type": "number"},
"description": "真实标签"
},
"metric": {
"type": "string",
"enum": ["accuracy", "f1", "precision", "recall"],
"description": "评估指标类型"
}
},
"required": ["predictions", "ground_truth", "metric"]
}
}
}
]
# 对话循环(含工具调用处理)
messages = [
{"role": "system", "content": "你是AI研究助手,可以搜索论文和计算指标。"},
{"role": "user", "content": "帮我找2023年以来关于RLHF的论文"}
]
response = client.chat.completions.create(
model="gpt-5.4",
messages=messages,
tools=tools,
tool_choice="auto"
)
# 处理工具调用
message = response.choices[0].message
if message.tool_calls:
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments) # json.loads将JSON字符串→Python对象
# 实际执行函数
if func_name == "search_papers":
result = search_papers(**func_args) # 你的实现
elif func_name == "calculate_metrics":
result = calculate_metrics(**func_args)
# 将结果返回给LLM
messages.append(message)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False) # json.dumps将Python对象→JSON字符串
})
# 获取最终回复
final_response = client.chat.completions.create(
model="gpt-5.4",
messages=messages,
tools=tools
)
print(final_response.choices[0].message.content)
2.3 并行函数调用(Parallel Tool Calls)¶
# 当前主流 OpenAI 工具调用模型支持一次返回多个 tool_calls
# 用户: "查北京和上海的天气"
# 模型会同时返回:
# tool_call_1: get_weather(city="北京")
# tool_call_2: get_weather(city="上海")
# 处理并行调用
if message.tool_calls:
messages.append(message)
# 并行执行所有函数(可用asyncio加速)
import asyncio # Python标准异步库
async def execute_tools(tool_calls): # async def定义协程函数
tasks = []
for tc in tool_calls:
args = json.loads(tc.function.arguments)
tasks.append(execute_function(tc.function.name, args))
return await asyncio.gather(*tasks) # 并发执行多个协程任务
results = asyncio.run(execute_tools(message.tool_calls)) # 创建事件循环运行顶层协程
for tool_call, result in zip(message.tool_calls, results): # zip按位置配对多个可迭代对象
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False)
})
2.4 Anthropic Tool Use¶
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[
{
"name": "get_stock_price",
"description": "获取股票实时价格",
"input_schema": {
"type": "object",
"properties": {
"symbol": {"type": "string", "description": "股票代码"},
"market": {"type": "string", "enum": ["US", "CN", "HK"]}
},
"required": ["symbol"]
}
}
],
messages=[{"role": "user", "content": "查一下苹果股票价格"}]
)
# Claude通过content blocks返回tool_use
for block in response.content:
if block.type == "tool_use":
print(f"调用: {block.name}({block.input})")
3. OpenAI Agents SDK (2025)¶
3.1 SDK 概述¶
OpenAI 在 2025 年 3 月发布的 Agents SDK (原 Swarm 的生产版),为构建多 Agent 系统提供了开箱即用的基础设施,让你无需从零实现 Agent 编排循环、工具调用分发和安全检查:
- Agent 定义:声明式 Agent + 工具 + 指令,替代手写
while循环 +if/else分发 - Handoff 机制: Agent 间任务委托,实现"分诊→专家"的多 Agent 协作模式
- Guardrails:输入/输出安全护栏,在 Agent 执行前后自动触发安全检查
- Tracing:内置可观测性,每次运行自动记录完整的 Agent 决策链和工具调用轨迹
3.2 基础 Agent 定义¶
from agents import Agent, Runner, function_tool
# 定义工具
@function_tool
def search_database(query: str, limit: int = 5) -> str:
"""搜索知识库"""
# 实际实现
results = db.search(query, limit=limit)
return json.dumps(results)
@function_tool
def send_email(to: str, subject: str, body: str) -> str:
"""发送邮件"""
# 实际实现
return f"邮件已发送至 {to}"
# 定义Agent
research_agent = Agent(
name="研究助手",
instructions="""你是一个AI研究助手。
- 用户询问学术问题时,先搜索知识库
- 找到结果后整理成易读的格式
- 如果用户要求发送摘要,使用邮件工具""",
tools=[search_database, send_email],
model="gpt-5.4"
)
# 运行Agent
result = Runner.run_sync(
research_agent,
messages=[{"role": "user", "content": "帮我搜索最新的GRPO论文"}]
)
print(result.final_output)
3.3 多 Agent Handoff¶
from agents import Agent, Runner
# 注意:handoffs 必须传入 Agent 对象引用,不能用字符串
# 因此需要先定义被转交的 Agent,再定义分诊 Agent
# 代码专家
code_agent = Agent(
name="代码专家",
instructions="你是编程专家,帮助用户编写和调试代码。",
tools=[run_code, search_docs]
)
# 论文专家
paper_agent = Agent(
name="论文专家",
instructions="你是论文阅读专家,帮助用户理解和总结论文。",
tools=[search_papers, extract_info]
)
# 部署专家
deploy_agent = Agent(
name="部署专家",
instructions="你是MLOps专家,帮助用户部署和优化模型服务。",
tools=[check_gpu, deploy_model]
)
# 分诊Agent(必须在目标Agent定义之后创建)
triage_agent = Agent(
name="分诊助手",
instructions="根据用户问题类型,转交给对应的专家Agent。",
handoffs=[code_agent, paper_agent, deploy_agent] # Agent对象,非字符串
)
# 运行多Agent系统
result = Runner.run_sync(
triage_agent,
messages=[{"role": "user", "content": "帮我优化这段CUDA代码的推理速度"}]
)
# triage_agent → 判断是代码问题 → handoff到code_agent → 返回结果
3.4 Guardrails (安全护栏)¶
from agents import Agent, InputGuardrail, OutputGuardrail, GuardrailFunctionOutput
# 输入护栏:检查是否为合法请求
@InputGuardrail
async def check_safety(ctx, agent, input_data):
# 使用另一个LLM进行安全检查
result = await Runner.run( # await等待异步操作完成
safety_checker_agent,
messages=input_data
)
is_safe = "safe" in result.final_output.lower()
return GuardrailFunctionOutput(
output_info={"safe": is_safe},
tripwire_triggered=not is_safe
)
# 输出护栏:确保不泄露敏感信息
@OutputGuardrail
async def check_pii(ctx, agent, output):
has_pii = detect_pii(output) # 自定义PII检测
return GuardrailFunctionOutput(
output_info={"has_pii": has_pii},
tripwire_triggered=has_pii
)
secure_agent = Agent(
name="安全Agent",
instructions="...",
input_guardrails=[check_safety],
output_guardrails=[check_pii]
)
4. 实战:构建结构化数据提取 Pipeline¶
4.1 场景:简历信息提取¶
from pydantic import BaseModel, Field
from openai import OpenAI
class Education(BaseModel):
school: str = Field(description="学校名称")
degree: str = Field(description="学位: 本科/硕士/博士")
major: str = Field(description="专业")
gpa: float | None = Field(None, description="GPA")
year: str = Field(description="毕业年份")
class WorkExperience(BaseModel):
company: str
title: str
duration: str
highlights: list[str] = Field(description="工作亮点,3-5条")
tech_stack: list[str] = Field(description="使用的技术栈")
class ResumeInfo(BaseModel):
name: str
email: str | None = None
phone: str | None = None
education: list[Education]
experience: list[WorkExperience]
skills: list[str]
publications: list[str] | None = None
target_position: str | None = Field(None, description="目标岗位")
def extract_resume(text: str) -> ResumeInfo:
client = OpenAI()
response = client.chat.completions.parse(
model="gpt-5.4",
messages=[
{"role": "system", "content": "你是简历解析专家,准确提取简历中的所有结构化信息。"},
{"role": "user", "content": f"请提取以下简历信息:\n\n{text}"}
],
response_format=ResumeInfo,
)
return response.choices[0].message.parsed
# 使用
resume = extract_resume(resume_text)
print(f"候选人: {resume.name}")
print(f"技能: {resume.skills}")
for exp in resume.experience:
print(f" {exp.company} - {exp.title}: {exp.highlights}")
5. 面试高频题¶
Q1: JSON Mode 和 Structured Output 有什么区别¶
答: JSON Mode 只保证输出是合法 JSON ,但不保证符合特定 Schema ; Structured Output 通过受限解码(Constrained Decoding),在 token 生成时 mask 非法 token ,保证 100%符合指定的 JSON Schema ,包括字段名、类型、嵌套结构等。
Q2: Function Calling 的工作原理¶
答:模型在训练时被微调以理解函数定义 Schema 。推理时,模型根据用户意图决定是否调用函数、选择哪个函数、生成参数 JSON 。模型本身不执行函数,而是返回tool_calls给应用层,应用层执行后将结果以tool角色消息返回,模型再基于结果生成最终回复。
Q3: 如何处理 Function Calling 的错误¶
答: 1. 参数验证:在执行前用 Pydantic/JSON Schema 验证参数 2. 错误返回:将错误信息通过 tool message 返回给 LLM ,让它自行修正 3. 重试机制:设置最大重试次数,避免无限循环 4. 降级策略:工具不可用时退回到纯文本回答
Q4: OpenAI Agents SDK vs LangChain/LangGraph¶
答: Agents SDK 更轻量,内置 Handoff 、 Guardrails 、 Tracing ; LangChain 生态更丰富,支持更多 LLM/工具/向量 DB 。 Agents SDK 适合 OpenAI 生态内的快速开发, LangChain/LangGraph 适合多 provider 复杂编排。
✏️ 练习与面试¶
练习 1 : Structured Output 实践(⭐)¶
使用 OpenAI Structured Output API 实现一个"产品信息提取器":从用户评论文本中提取产品名称、评分(1-5)、情感(正面/负面/中性)、优缺点列表。
💡 参考答案
# 练习1:Structured Output 产品信息提取参考实现
from openai import OpenAI
from pydantic import BaseModel, Field
from enum import Enum
client = OpenAI()
class Sentiment(str, Enum):
positive = "正面"
negative = "负面"
neutral = "中性"
class ProductReview(BaseModel):
product_name: str = Field(description="产品名称")
rating: int = Field(ge=1, le=5, description="评分(1-5)")
sentiment: Sentiment = Field(description="情感倾向")
pros: list[str] = Field(description="优点列表")
cons: list[str] = Field(description="缺点列表")
summary: str = Field(description="一句话总结")
# 使用 Structured Output
review_text = """
我上个月买了一台 MacBook Pro M4,用了两周感觉非常好。
优点:性能超强,编译代码飞快,屏幕素质一流,电池续航也很给力。
缺点:价格确实贵,只有两个 USB-C 接口不太够用。
总体来说非常满意,给 5 分!
"""
response = client.responses.parse(
model="gpt-5-mini",
input=[
{"role": "system", "content": "从用户评论中提取产品信息。"},
{"role": "user", "content": review_text},
],
text_format=ProductReview,
)
result = response.output_parsed
print(f"产品: {result.product_name}")
print(f"评分: {result.rating}/5")
print(f"情感: {result.sentiment.value}")
print(f"优点: {', '.join(result.pros)}")
print(f"缺点: {', '.join(result.cons)}")
print(f"总结: {result.summary}")
练习 2 : Function Calling 工具集成(⭐⭐)¶
实现一个天气查询助手,使用 Function Calling 调用模拟天气 API ,支持多轮对话和工具选择。
💡 参考答案
# 练习2:Function Calling 天气助手参考实现
from openai import OpenAI
import json
client = OpenAI()
# 1. 定义工具
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度单位"},
},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "get_forecast",
"description": "获取未来几天的天气预报",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"days": {"type": "integer", "description": "预报天数(1-7)", "minimum": 1, "maximum": 7},
},
"required": ["city", "days"],
},
},
},
]
# 2. 模拟工具实现
def get_weather(city: str, unit: str = "celsius") -> str:
mock_data = {
"北京": {"temp": 22, "condition": "晴", "humidity": 45},
"上海": {"temp": 26, "condition": "多云", "humidity": 65},
"深圳": {"temp": 30, "condition": "阵雨", "humidity": 80},
}
data = mock_data.get(city, {"temp": 20, "condition": "未知", "humidity": 50})
if unit == "fahrenheit":
data["temp"] = data["temp"] * 9/5 + 32
return json.dumps({"city": city, **data}, ensure_ascii=False)
def get_forecast(city: str, days: int) -> str:
import random
conditions = ["晴", "多云", "阴", "小雨", "大雨"]
forecast = []
for i in range(days):
forecast.append({"day": i+1, "temp": random.randint(15, 35), "condition": random.choice(conditions)})
return json.dumps({"city": city, "forecast": forecast}, ensure_ascii=False)
tool_map = {"get_weather": get_weather, "get_forecast": get_forecast}
# 3. 多轮对话循环
messages = [{"role": "system", "content": "你是一个天气助手,帮助用户查询天气信息。"}]
print("天气助手已启动!(输入 'quit' 退出)\n")
while True:
user_input = input("你: ")
if user_input.lower() == "quit":
break
messages.append({"role": "user", "content": user_input})
# 循环处理工具调用
while True:
response = client.chat.completions.create(
model="gpt-5-mini",
messages=messages,
tools=tools,
tool_choice="auto",
)
msg = response.choices[0].message
messages.append(msg)
# 没有工具调用,输出最终回答
if not msg.tool_calls:
print(f"助手: {msg.content}\n")
break
# 执行工具调用
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f" [调用工具] {func_name}({func_args})")
result = tool_map[func_name](**func_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
练习 3 : OpenAI Agents SDK 多工具 Agent(⭐⭐⭐)¶
使用 OpenAI Agents SDK 构建一个研究助手 Agent ,集成搜索、代码执行、文件读写三个工具,支持 Handoff 到专业子 Agent 。
💡 参考答案
# 练习3:OpenAI Agents SDK 多工具 Agent 参考实现
# pip install openai-agents
from agents import Agent, Runner, function_tool
from pydantic import BaseModel
# 1. 定义工具
@function_tool
def search_web(query: str) -> str:
"""搜索互联网获取信息"""
# 实际使用时接入搜索 API
return f"搜索结果: 关于 '{query}' 的最新信息..."
@function_tool
def execute_code(code: str, language: str = "python") -> str:
"""执行代码并返回结果"""
try:
# 实际使用时接入沙箱执行环境
exec_globals = {}
exec(code, exec_globals)
return str(exec_globals.get("result", "代码执行成功"))
except Exception as e:
return f"执行错误: {e}"
@function_tool
def read_file(filepath: str) -> str:
"""读取文件内容"""
try:
with open(filepath, "r", encoding="utf-8") as f:
return f.read()[:2000] # 限制长度
except FileNotFoundError:
return f"文件不存在: {filepath}"
# 2. 定义专业子 Agent
code_agent = Agent(
name="代码专家",
instructions="你是一个代码专家。帮助用户编写、调试和优化代码。使用 execute_code 工具验证代码。",
tools=[execute_code],
model="gpt-5-mini",
)
research_agent = Agent(
name="研究助手",
instructions="你是一个研究助手。帮助用户搜索和分析信息。使用 search_web 工具获取最新资料。",
tools=[search_web],
model="gpt-5-mini",
)
# 3. 主 Agent(带 Handoff)
main_agent = Agent(
name="研究助手",
instructions="""你是一个全能研究助手。根据用户需求:
- 需要搜索信息时,转交给研究助手
- 需要编写/执行代码时,转交给代码专家
- 简单问题直接回答
你可以直接使用 read_file 工具读取文件。""",
tools=[read_file],
handoffs=[research_agent, code_agent],
model="gpt-5-mini",
)
# 4. 运行
async def main():
result = await Runner.run(
main_agent,
input="帮我搜索一下 2026 年最新的 RAG 技术,然后写一段代码实现简单的向量检索。",
)
print(f"最终回答:\n{result.final_output}")
import asyncio
asyncio.run(main())
📚 参考资源¶
- OpenAI Structured Outputs 文档
- OpenAI Function Calling 文档
- OpenAI Agents SDK GitHub
- Anthropic Tool Use 文档
- Instructor 库 - Pydantic 驱动的结构化输出
- Outlines - 受限解码框架
📎 交叉引用: - Prompt 工程基础 → LLM 应用/Prompt 工程 - Agent 开发框架 → AI Agent 开发实战 - MCP 工具生态 → AI Agent 开发实战/MCP 与工具生态
最后更新日期: 2026-04-21 适用版本: LLM 应用指南 v2026