结构化输出与函数调用:生产环境中的实践模式
高级13 分钟阅读企业AI

结构化输出与函数调用:生产环境中的实践模式

结构化输出和函数调用是从“生成文本的大语言模型(LLM)”走向“实际执行工作的系统”的桥梁。在生产环境中,真正重要的模式围绕模式定义、错误处理、幂等性和优雅降级展开,而不只是JSON模式。

您应该能够做到的事情

生产级结构化输出和函数调用所需的不只是JSON模式。关键模式包括:严格的模式定义、明确的错误语义、幂等性、优雅的故障处理,以及能在模型错误扩散前将其捕获的工具结果反思循环。

AI Expert Team发布日期: 2026年5月15日
仅在此浏览器中保存。
本文内容

从“大语言模型聊天机器人”转向“由大语言模型驱动、能够实际执行工作的系统”,发生在结构化输出和函数调用这一层。在这里,大语言模型不再只是生成文字,而是开始生成数据、决定行动,并与基础设施的其他部分集成。

在生产环境中,“结构化输出”并不意味着“我测试时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:使用严格模式定义的工具。
  • 开源方案:outlineslm-format-enforcerjsonformer、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]  # 需要审查的具体原因

工作流:

  1. **OCR步骤:**视觉模型从PDF中提取文本。
  2. **提取步骤:**使用上述Schema调用大语言模型,并启用约束生成。
  3. 验证步骤: Pydantic进行验证。验证错误会触发一次包含错误反馈的重试。
  4. **交叉核对步骤:**调用工具,在记录中查找供应商。如果vendor_name与已知供应商匹配,则附加vendor_id。否则设置needs_review=true
  5. **数学检查步骤:**验证sum(line_items.total) ≈ subtotalsubtotal + tax ≈ total。如果不相等,则设置needs_review=true
  6. **置信度检查步骤:**如果confidence为low,或任何明细项的置信度为low,则设置needs_review=true
  7. **路由:**如果needs_review=true,则发送到人工审核队列;否则传送到会计系统。
  8. **日志记录:**记录每一步的输入、输出、持续时间和错误。

已处理的故障模式:

  • 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感知错误处理,以及端到端可观测性。

这些模式中的每一项都在区分演示与生产系统。请从一开始就将它们纳入设计。

继续阅读

通过下一篇文章继续沿着相同的学习路径进行学习。