从“大语言模型聊天机器人”转向“由大语言模型驱动、能够实际执行工作的系统”,发生在结构化输出和函数调用这一层。在这里,大语言模型不再只是生成文字,而是开始生成数据、决定行动,并与基础设施的其他部分集成。
在生产环境中,“结构化输出”并不意味着“我测试时JSON模式成功过一次”。它意味着一条稳健的流水线,能够处理模型差异、格式错误的输出、部分故障、Schema演进,以及大语言模型并不总是遵循指令这一复杂现实。
本文介绍在生产环境中真正经得住考验的模式。我们假定你已经了解基础知识(用过OpenAI的tool_choice、Anthropic的工具使用功能和JSON Schema约束)。我们将进一步探讨如何让这些系统可靠运行。
两种模式
这里有两种相关但不同的能力:
**结构化输出:**大语言模型生成符合某个Schema(通常是JSON)的输出。当你需要以可编程处理的格式获得大语言模型响应时使用。
**函数/工具调用:**向大语言模型提供一组可调用的函数,由模型决定调用哪个函数(如果需要),并生成调用参数。宿主系统执行该函数并返回结果。大语言模型可以继续调用其他函数,也可以生成最终响应。
模型API通常通过以下方式提供这些能力:
- 对于普通结构化输出,使用接收JSON Schema的
response_format(或同等)参数,例如OpenAI的“Structured Outputs”、Google Gemini的responseSchema等。 - 使用描述可用函数的
tools数组,并在调用时返回工具调用响应。Anthropic的工具使用API同时也是将输出限制为特定Schema的方式:定义一个采用所需Schema的工具,并强制模型调用它。
两种方式都可行,而且彼此相关。“函数调用”本质上就是一种结构化输出,其Schema即函数签名。
模式1:严格、明确的模式定义
提高可靠性最有效的单项措施,就是改进模式定义。
宽松的Schema:
{
"type": "object",
"properties": {
"category": { "type": "string" },
"priority": { "type": "string" }
}
}
严格的Schema:
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "feature_request", "complaint"],
"description": "工单类别。产品缺陷使用 'technical',登录/密码问题使用 'account'。"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "urgent"],
"description": "仅在服务中断或对业务有关键影响时使用 'urgent'。重要客户遇到阻塞性问题时使用 'high'。一般影响使用 'medium'。锦上添花的需求使用 'low'。"
}
},
"required": ["category", "priority"],
"additionalProperties": false
}
严格版本:
- 将值限制为已知枚举(不会出现自由文本漂移)。
- 包含充当内联提示的描述(模型会使用它们)。
- 要求必填字段(避免得到不完整的响应)。
- 禁止额外字段(不会出现随机幻觉生成的键)。
在生产环境中,每个Schema字段都应该有描述。每个枚举都应该明确定义。每个必填字段都应该标明。这就是“以Schema作为提示”——你的模式定义正在承担提示工程的工作。
模式2:约束生成
主流提供商现在都支持约束生成,即在解码层面限制模型只能生成有效输出。
- OpenAI:
response_format: { type: "json_schema", json_schema: { ..., strict: true } } - Anthropic:使用严格模式定义的工具。
- 开源方案:
outlines、lm-format-enforcer、jsonformer、vLLM基于语法的解码。
请始终使用这些能力。它们可以消除一整类故障(JSON格式错误、幻觉字段、缺失必填字段),而性能开销可以忽略不计。
如果无法使用约束生成(某些开放模型或某些配置),可以用验证加重试作为后备方案(参见模式4)。
模式3:Schema版本控制
Schema会不断演进。你会添加字段、弃用字段、更改枚举。
Schema变更就是代码变更。它应该:
- 有版本。为每个Schema标记版本号。
- 经过测试。部署前使用新Schema运行评估套件。
- 得到通知。让下游输出使用方了解变更。
- 尽可能向后兼容。添加新的可选字段,不要删除必填字段。
一种行之有效的做法是:将Schema保存为TypeScript类型或Pydantic模型,在源代码管理中进行版本控制,并从中生成JSON Schema。这些类型同时服务于模型API和应用程序代码。
class TicketClassificationV2(BaseModel):
category: Literal["billing", "technical", "account", "feature_request", "complaint"]
priority: Literal["low", "medium", "high", "urgent"]
confidence: float = Field(ge=0, le=1, description="此次分类的置信度,0-1")
needs_human_review: bool = Field(description="如果任何字段置信度较低或存在异常信号,则为True")
reasoning: str = Field(description="简要说明分类理由,尤其要说明不明显的情况")
一个Pydantic模型既定义Schema、验证输出,也充当Python代码中的类型。只保留一个事实来源。
模式4:验证与重试
即使使用约束生成,也要在使用输出前进行验证:
from pydantic import ValidationError
def call_with_validation(prompt, schema, max_retries=2):
for attempt in range(max_retries + 1):
response = llm_call(prompt, response_format=schema)
try:
parsed = schema.model_validate_json(response.content)
return parsed
except ValidationError as e:
if attempt < max_retries:
prompt = build_retry_prompt(prompt, response.content, e)
continue
raise
重试提示应包含原始指令、模型之前的输出,以及对问题的具体说明:
你之前的响应存在验证错误:
{error message}
你之前的输出:
{previous output}
请修正问题并生成有效响应。
重试的效果出奇地好——通常一次重试就能从模型错误中恢复。
限制:不要无限重试(最多2–3次)。不要因非验证错误(速率限制、内容过滤等)而重试。记录重试,以便监控重试率(重试率上升意味着模型漂移或提示存在问题)。
模式5:结果反思
对于高风险函数调用,让模型先反思工具结果,再使用它们。
基础循环:
1. 调用大语言模型,并提供可用工具。
2. 模型决定调用工具X。
3. 执行X。
4. 将结果传回模型。
5. 模型生成最终响应。
反思循环:
1. 调用大语言模型,并提供可用工具。
2. 模型决定调用工具X。
3. 执行X。
4. 将结果传回模型。
5. 模型评估:此结果是否符合预期?我应该据此采取行动吗?
6. 如果是,模型生成最终响应;如果不是,模型调用其他工具或请求澄清。
这可以捕获以下情况:
- 工具返回0条结果,但本应返回数据 → 模型识别出空结果情况。
- 工具返回错误 → 模型明确处理,而不是忽略错误。
- 工具返回意外数据 → 模型发现问题并作出调整。
实现方式:通过提示让模型明确评估工具结果,也可以采用结构化的“先评估、后行动”模式。
这会增加延迟和令牌用量。对于发送电子邮件、处理付款、修改记录等高风险操作,值得付出这些成本。对于低风险的信息检索,则可以跳过。
模式6:幂等性
大语言模型有时会重复调用同一工具,或者重试已经成功的调用。如果没有幂等性,就会产生重复操作:退款两次、发送两封邮件、创建两条记录。
实现幂等性的模式:
**幂等键。**每次工具调用都获得一个唯一键(由客户端生成并包含在调用中)。下游API或工具包装器使用该键检测重复调用,并返回已有结果。
**获取或创建语义。**创建记录的工具先进行查询。“使用电子邮箱X创建客户”会先检查该邮箱对应的客户是否存在;如果存在,就返回现有客户,而不是创建重复记录。
**操作日志。**工具记录每次操作。包装器执行前检查日志;如果操作已经完成,就返回缓存结果。
**保守的工具设计。**执行重大操作的工具在设计上要求明确确认或人工批准。大语言模型无法因意外进入紧密循环而触发这些操作。
任何有副作用的工具都应按幂等方式设计。忽视这一点是生产缺陷的主要来源。
模式7:工具调用可观测性
你需要知道工具调用中发生了什么。对于每次调用,记录:
- 时间戳。
- 工具名称和参数。
- 结果(或错误)。
- 持续时间。
- 所属用户/会话。
- 本轮中的调用链(此次工具调用是否属于更长的调用链?)。
基于这些数据构建仪表板。常见视图包括:
- 按工具划分的工具调用量。
- 按工具划分的错误率。
- 按工具划分的平均持续时间。
- 工具调用序列模式(“哪些工具往往一起调用?”)。
- 幻觉工具调用(大语言模型试图调用不存在的工具)。
这些信息可以揭示系统的故障点和高成本环节。
模式8:幻觉参数
大语言模型有时会为工具参数编造值。它们可能调用search_customers(email="..."),但其中的电子邮箱与用户的实际问题不符;也可能调用book_meeting(date="..."),但使用了用户从未提到的日期。
缓解措施:
带描述的严格Schema。“user_id必须是之前在对话中提到的ID。不要编造ID。”
**在工具包装器中验证。**如果值不合理(例如user_id不存在、日期已经过去),工具会返回结构化错误,模型再重新考虑。
反思。“调用此工具前,请确认你使用的值有对话内容作为依据。”
**受限的工具描述。**对特定实体执行操作的工具只公开此前在对话中检索到的实体ID。不要开放原始搜索。
**审计日志。**捕获幻觉参数的模式,并调整提示/模式定义。
模式9:优雅降级
工具会失败。API会中断。会触发速率限制。正确的响应很少是“告诉用户一切都无法工作”。
相关模式:
**缓存或过期数据。**如果实时数据源不可用,则返回缓存数据,并注明数据可能已过期。
**部分完成。**如果5个子任务中有3个成功,说明哪些已完成、哪些未完成。
**后备路径。**如果主要工具失败,模型知道还有后备方案。例如,如果 “search_documents” 失败,则回退到 “search_web”,并给出适当的注意事项。
**用户可见的错误状态。**如果工具确实无法完成任务,模型应向用户生成清晰的错误消息,而不是虚构成功。
模型需要了解这些模式。将它们写入系统提示:
如果工具返回错误:
- 如果存在替代工具,请尝试使用。
- 如果用户已经提供信息,请清楚报告部分结果。
- 工具返回错误时,绝不能声称操作成功。
模式10:流式结构化输出
从用户体验角度看,流式传输部分结构化输出非常理想——用户可以实时看到结果逐步形成,而不必一直等待。
实现方式:
- 大多数现代模型API都会逐令牌流式传输JSON输出。
- 增量解析不完整的JSON(使用
partial-json-parser等库,或编写一个小型流式解析器)。 - 随字段到达而更新界面。
这尤其适合包含多个部分的输出。较长的产品说明、包含多项洞见的分析、列出多个发现的代码审查——采用流式传输后,响应体验会明显更好。
注意:不要依据部分输出作出决策。流式传输仅用于展示;对结构化结果采取行动前,应等待输出完成。
模式11:函数调用与显式“决策”调用
原生函数调用很方便——模型可以“决定”何时调用工具。但对于某些工作流,显式的“决策”调用更可靠。
示例:在客户支持工作流中,模型需要从多种操作中作出选择。
**原生函数调用方法:**为模型提供5个工具(refund、send_article、escalate_to_human、ask_clarifying_question、close_ticket),让它自行决定。
**显式决策调用方法:**首先调用模型,并只提供一个工具decide_action,该工具只接收一个参数:要执行的操作。然后根据决策再次调用模型,此次只提供相关工具。
显式方法更慢、更冗长,但可靠性更高。模型在每一步都能更加专注,宿主系统对工作流也有更多控制权。
对于高风险工作流,显式方法往往更胜一筹。对于探索性或简单工作流,原生函数调用就足够了。
模式12:工具结果格式
如何返回工具结果很重要。模型需要读取结果,因此格式也很重要。
较差:
{"id": "cus_123", "n": "John", "p": "12345"}
较好:
{
"customer_id": "cus_123",
"name": "John Doe",
"phone": "+1-555-0123",
"tier": "premium",
"open_tickets": 0
}
最佳(适用于某些情况):
找到客户:
- ID:cus_123
- 姓名:John Doe
- 等级:Premium
- 电话:+1-555-0123
- 未结工单:0
该客户属于premium等级,且没有未结工单。
“最佳”形式便于人类阅读、包含上下文,也更便于模型在后续生成中使用。“较好”形式结构更清晰,也更适合机器读取。请根据下游任务选择模型处理效果最好的格式(需要实际测试)。
对于某些工具,同时返回结构化内容和叙述内容(“结果如下:[叙述内容]。原始数据:[JSON]”)效果很好。
模式13:Schema感知重试
有些验证错误无法恢复(模型从根本上误解了任务),另一些则很容易修正。
一种实用模式是:对错误分类,并据此作出响应。
def handle_validation_error(error):
if "missing required field" in str(error):
return retry_with_message("你遗漏了必填字段X。请补充该字段。")
elif "value not in enum" in str(error):
return retry_with_message("值X不在允许集合中。请从以下值中选择:...")
elif "type mismatch" in str(error):
return retry_with_message("字段X必须是数字,而不是字符串。")
else:
# 未知错误——仅进行一次通用重试
return retry_with_message("你的响应存在错误。请重试。")
有针对性的重试比通用重试更容易成功。
模式14:可组合性
工具应该能够组合。模型可以将功能单一、目标明确的小型工具组合成复杂工作流。
包办一切的单体工具process_customer_request(query)是一个黑盒。模型无法观察或引导其内部逻辑。
一组目标明确的工具——search_customer(email)、get_recent_orders(customer_id)、check_subscription_status(customer_id)、escalate_to_human(reason)——则可以由模型组合,为每种情况构建恰当的流程。
应以适当粒度设计工具。每个工具只做一件事,再由多个工具组合成工作流。
模式15:“我不知道”的模式定义
还有一个不易察觉的模式:在模式定义中明确表示不确定性。
class CustomerInfo(BaseModel):
name: str
name_confidence: Literal["high", "medium", "low"]
needs_clarification: bool
clarification_question: Optional[str] = None
模型可以返回“低置信度”和一个澄清问题,而不是凭空捏造。
这远优于让模型始终自信地填写字段,因为后者有时会填入幻觉数据。
完整示例:发票处理
为了把这些模式整合起来,下面是一个生产级发票处理系统。
**输入:**电子邮件所附的PDF发票。 **目标:**提取结构化数据,并传送到会计系统。
Schema:
class LineItem(BaseModel):
description: str
quantity: float
unit_price: float
total: float
confidence: Literal["high", "medium", "low"]
class Invoice(BaseModel):
vendor_name: str
vendor_id: Optional[str] = None # 如果在我们的记录中未找到,则为null
invoice_number: str
invoice_date: date
due_date: Optional[date] = None
line_items: List[LineItem]
subtotal: float
tax: float
total: float
currency: str # ISO 4217
confidence: Literal["high", "medium", "low"]
needs_review: bool
review_reasons: List[str] # 需要审查的具体原因
工作流:
- **OCR步骤:**视觉模型从PDF中提取文本。
- **提取步骤:**使用上述Schema调用大语言模型,并启用约束生成。
- 验证步骤: Pydantic进行验证。验证错误会触发一次包含错误反馈的重试。
- **交叉核对步骤:**调用工具,在记录中查找供应商。如果
vendor_name与已知供应商匹配,则附加vendor_id。否则设置needs_review=true。 - **数学检查步骤:**验证
sum(line_items.total) ≈ subtotal和subtotal + tax ≈ total。如果不相等,则设置needs_review=true。 - **置信度检查步骤:**如果
confidence为low,或任何明细项的置信度为low,则设置needs_review=true。 - **路由:**如果
needs_review=true,则发送到人工审核队列;否则传送到会计系统。 - **日志记录:**记录每一步的输入、输出、持续时间和错误。
已处理的故障模式:
- JSON格式错误:约束生成可以预防;重试可处理边缘情况。
- 幻觉字段:Schema定义很严格。
- 数学错误:经过验证。
- 未知供应商:予以标记。
- 低置信度:予以标记。
- 工具错误:明确处理。
**生产性能:**约95%的发票能够直通处理;5%被标记为需要审查。在自动处理的发票中,错误率低于0.5%(完全处于可接受范围内)。在审查队列中,约80%经确认无误,20%需要修正。
这才是生产级结构化输出。不只是“JSON模式成功过一次”,而是一条能够处理现实故障模式的流水线。
常见错误
我们反复看到以下几种模式:
**错误1:不验证。**无论用Pydantic、zod还是其他工具,都要验证。不要信任模型。
错误2:描述含糊。“category: string”对模型没有帮助。“category:billing、technical、account_access之一,其中billing涵盖……”才有帮助。
**错误3:工具过多。**提供30个工具时,模型会选错。每次调用应筛选到少于10个相关工具。
**错误4:验证失败后不重试。**一次格式错误的输出就让整个流程失败。带反馈重试一次。
**错误5:缺乏可观测性。**生产环境中的工具调用失败时,如果没有跟踪记录,你将无法诊断。
**错误6:有副作用的工具不具备幂等性。**重复退款、重复发送邮件。这是完全可以预见的缺陷。
**错误7:不经验证便信任大语言模型决定的参数。**幻觉用户ID、幻觉日期。执行前验证工具参数。
**错误8:跳过Schema版本控制。**Schema变更会破坏下游使用方。请对其进行版本控制。
从演示走向生产系统
结构化输出和函数调用是从“会说话的大语言模型”走向“会做事的大语言模型”的桥梁。做好了,它们能释放生产AI的潜力;做不好,则会以各种意想不到且代价高昂的方式出错。
真正重要的模式包括:严格的模式定义、约束生成、验证与重试、工具结果反思、幂等性、优雅降级、Schema感知错误处理,以及端到端可观测性。
这些模式中的每一项都在区分演示与生产系统。请从一开始就将它们纳入设计。



