深入学LangChain 官方文档(四):Structured Output 结构化输出

深入学LangChain官方文档(四):Structured Output 结构化输出

本篇对应的官方文档

本篇讲解范围

本篇集中讲清 Agent 如何接收 schema、选择或显式指定结构化输出策略、校验模型结果,并把业务对象写入最终 state 的 structured_response。完整 Agent loop 留给第 05 篇,通用工具注册与执行留给第 06 篇;模型级 with_structured_output() 只说明边界,不并列展开第二套教程。

前面的Model篇和Message 篇分别解决了两个基础问题:Model 是推理引擎,Message 是模型上下文的基本单位。模型现在能收到角色明确的输入,也能返回包含程序字段的消息,但业务系统仍然缺少一道关键保证:模型给出的内容,能不能直接交给下一段代码。

而本篇讨论的 Structured Output,就是这道从"模型回答"到"业务对象"的交接。它不是单纯让输出看起来像 JSON,而是先定义 schema,再让 Agent 按确定的策略生成、捕获和校验结果,最后把通过校验的数据放进 structured_response

客服工单很适合暴露这层差异。用户可能说:"订单 A1024 的耳机右侧没有声音,我明天出差,希望尽快换货。"客服页面可以直接展示这句话;路由程序却需要订单号、问题分类、紧急程度和下一步动作。如果字段名偶尔变化、优先级有时写"紧急"有时写"high",后续判断就会变成一串脆弱的字符串分支。

字段契约路径保留了自然语言输入,但交给业务路由的结果必须经过字段和类型校验。自由文本路径的问题不在文字写得差,而在它没有给下游一个稳定接口。

因此,本篇会沿着同一个客服输入走完一条完整路径:先看 JSON 文本为什么仍不可靠,再用 Pydantic schema 写出业务契约,随后解释 response_formatProviderStrategyToolStrategystructured_response 的职责,最后把校验失败与重试放回这条执行链。

读完后我们回学到结果不只是保留自然语言给人看;还要进入工单路由、数据库写入或接口调用。具体来说,就是先把字段契约说清楚,再选择结构化输出策略。

1. JSON 文本为什么还不是业务对象

结构化输出的第一次尝试,是在 prompt 末尾加一句"请以 JSON 返回"。模型通常会照做,甚至会给出看起来很规整的代码块:

json 复制代码
{
  "order_id": "A1024",
  "category": "hardware",
  "priority": "紧急",
  "next_action": "尽快处理"
}

这段内容能被 JSON 解析器读取,却不一定能被业务程序稳定使用。假设路由规则只接受 lowmediumhigh 三种优先级,那么"紧急"虽然语义正确,却已经越过接口边界。next_action 也过于宽泛,系统无法据此判断应该创建换货单、退款单还是人工复核任务。

有效 JSON 只证明括号、引号和逗号符合语法。业务对象还要回答四类问题:

  • 字段是否完整,例如没有订单号时能否使用 null,还是必须拒绝本次抽取;
  • 类型是否稳定,例如优先级必须是字符串,商品数量必须是整数;
  • 值域是否受控,例如问题分类只能来自预先定义的枚举;
  • 字段含义是否清楚,例如 next_action 表示给用户的回复,还是后台要执行的动作。

如果这些约束只写在自然语言 prompt 中,模型每次都要重新解释描述,应用也只能在返回后自行补校验。随着字段增加,prompt、解析器和业务判断会分别维护一份规则,三者很容易逐渐不一致。

业务程序依赖的不是一对大括号,而是一份稳定契约。 Structured Output 的核心价值,是把字段结构从"希望模型遵守的描述"提升为 Agent 可以执行、校验并返回的 schema。

2. schema 把字段、类型和值域写在同一个地方

LangChain 的 Agent 结构化输出支持 Pydantic model、dataclass、TypedDict 和 JSON Schema。它们都能描述输出形状,但运行时能力并不完全相同。Pydantic 可以把字段说明、类型、默认值和校验规则集中在一个 Python 类型里,并在成功时返回经过验证的模型实例,适合本篇的工单场景。

python 复制代码
from typing import Literal

from pydantic import BaseModel, Field


class SupportTicket(BaseModel):
    """从用户投诉中抽取的客服工单。"""

    order_id: str | None = Field(
        default=None,
        description="订单号;原文没有提供时返回 null",
    )
    category: Literal["product", "shipping", "billing", "service"] = Field(
        description="问题分类",
    )
    priority: Literal["low", "medium", "high"] = Field(
        description="处理优先级",
    )
    next_action: str = Field(
        description="客服系统下一步应执行的动作",
    )

这里真正重要的不是 BaseModel 这个类名,而是业务规则有了唯一落点。order_id 明确允许缺失,分类和优先级被限制在有限集合中,next_action 也说明了它属于后台动作,而不是面向用户的安抚话术。

字段描述仍然会提供给模型,用来解释每个字段应表达什么;Python 类型和值域则让校验器判断结果能否进入下游。自然语言说明和程序约束因此不再散落在不同文件中。

选择其他 schema 形式时要知道自己放弃或获得了什么。dataclass 和 TypedDict 更轻量,JSON Schema 便于跨语言共享;如果需要 Python 侧的字段验证、嵌套对象和清晰的错误信息,Pydantic 通常更直接。本篇不把四种写法逐一改写成相同示例,因为真正需要掌握的是 schema 的职责,而不是记住四套语法。

四种载体真正拉开差距的,不是定义时 少写几行代码,而是运行时校验、Python 类型语义和跨语言交换由谁承担。把这三条能力轴放在一起,schema 的选择就从语法偏好变成了工程边界。

Pydantic 同时保留 Python 类型语义并提供直接的运行时验证,适合在应用内部尽早拦住不合格对象;JSON Schema 更强调语言无关的契约交换,通常要结合具体执行方或校验器落地。dataclass 与 TypedDict 位于两者之间,更适合表达 Python 内部结构,但不能因为类型写得清楚,就默认所有运行时数据已经完成同等强度的验证。

schema 也不能替业务做全部判断。它可以约束 priority 只能取三个值,却不能凭空知道某家公司的 SLA 如何定义"high"。值域与字段说明仍然必须来自真实业务规则,Structured Output 负责执行这些规则,不负责发明规则。

3. response_format 是契约进入 Agent 的入口

定义好 SupportTicket 后,下一步不是把它拼进 prompt,而是通过 create_agentresponse_format 交给 Agent。官方接口允许这里接收四类值:ToolStrategyProviderStrategy、一个 schema type,或者 None

这四种写法表达的是控制选择:

  • None 表示没有显式请求结构化输出;
  • 直接传 SupportTicket,由 LangChain 根据模型能力选择策略;
  • ProviderStrategy(SupportTicket) 明确要求走供应商原生结构化输出路径;
  • ToolStrategy(SupportTicket) 明确借助 tool calling 生成并校验结构化结果。

直接传 schema 和显式传入策略对象的区别,最终落在"由框架判断路径"还是"由工程配置固定路径"。如果只把它们理解为三种等价写法,部署环境变化时就很难解释实际走了哪条链路。

直接传 schema 会把策略选择交给 LangChain 的能力识别,换来更短的配置;显式构造 ProviderStrategyToolStrategy,则把执行路径写进应用配置。三条路径使用的仍是同一份 schema 契约,差别只在约束由谁执行、能力判断由谁承担。

直接传 schema 的写法最短,但"自动"不代表不需要理解边界。LangChain 在当前版本中会读取模型 profile:模型与供应商支持原生结构化输出时选择 ProviderStrategy,其他情况选择 ToolStrategy。如果 profile 信息不可用,或者应用希望不同环境始终走相同路径,就应显式指定策略。

这也是本篇代码显式使用 ToolStrategy 的原因。Alibaba Cloud 官方能力表确认 qwen3.7-plus 支持 Function Calling,因此工具调用策略有明确依据;至于某个 OpenAI-compatible 连接能否被 LangChain profile 自动识别为供应商原生结构化输出,不在正文中擅自假设。

response_format 保存的是"怎样取得结构化结果"的配置,不是结构化结果本身。 它在 Agent 创建时进入执行图,真正的数据要等一次运行完成后,从最终 state 中读取。

response_format 位于执行链的输入侧,负责把业务 schema 交给 Agent;structured_response 位于输出侧,只在模型结果经过策略处理和 schema 校验后出现。把这两个字段分开,后面的策略差异才不会和结果读取混在一起。

4. ProviderStrategyToolStrategy 约束输出的位置不同

两种策略服务同一个 schema,但它们把约束放在不同位置。

ProviderStrategy 使用模型供应商 API 提供的原生结构化输出能力。schema 会进入供应商支持的结构化参数,由供应商在生成层执行约束。官方文档把它视为可用时更可靠的方式,因为数据形状由模型服务直接保证,而不是在普通文本生成后再猜测。

这条路径仍然有边界。具体模型是否支持原生结构化输出、是否支持 strict、能否同时使用普通工具和结构化输出,都取决于供应商与模型能力。代码不能因为某个供应商的另一款模型支持,就把当前模型也写成必然支持。

ToolStrategy 则把 schema 表达成一次结构化输出工具调用。模型按照 tool schema 生成参数,LangChain 捕获参数并转换为目标对象。它适用于支持 tool calling、但没有原生结构化输出接口,或者应用希望显式固定工具调用路径的模型。

这里容易出现一个误解:结构化输出工具不是普通业务工具。天气工具会真的查询天气,库存工具会访问数据库;ToolStrategy 里的"工具"主要承担输出协议,让模型把结果放进一组受约束的参数。完整的工具注册、执行和结果回传会在第 06 篇展开。

两种策略最终都要回答同一个问题:生成结果是否符合 schema。差别在于约束由供应商原生接口执行,还是由 tool calling 与 LangChain 校验链完成。工程选择应该依据实际模型能力、错误处理需要和跨环境一致性,而不是根据类名长短决定。

两条路径不会在一次运行中并行执行。ProviderStrategy 把约束交给供应商原生能力,ToolStrategy 把 schema 转成 Function Calling 路径并由 LangChain 校验;选中的路径不同,最终都要把合格对象写进 structured_response

如果代码同时给 Agent 配置普通工具,官方文档还要求模型支持工具与结构化输出同时使用。结构化抽取单独能运行,不代表加入搜索、数据库等工具后仍能沿用完全相同的供应商能力组合;这类组合必须按具体模型验证。

5. structured_response 才是业务代码的读取位置

Agent 的输入仍然是熟悉的 messages state。客服投诉作为 user message 进入运行,模型和策略完成结构化输出后,invoke 返回最终 state。这个 state 不只包含最后一句回答,还包含过程消息以及结构化结果。

官方文档把通过捕获与校验的数据放在:

python 复制代码
ticket = result["structured_response"]

如果 schema 使用 Pydantic,ticket 是对应的 Pydantic 实例;业务代码可以访问 ticket.order_idticket.categoryticket.priority,也可以通过 model_dump() 转成字典。它不需要从 Markdown 代码块中截取 JSON,也不需要在多个候选字段名之间做兼容。

result["messages"] 仍然有价值。它保存用户输入、模型调用以及 ToolStrategy 可能产生的工具消息,适合排查模型为什么给出某个结果。structured_response 则面向下游业务,适合路由、持久化或接口调用。两者一个保存过程,一个提供结果,不能因为都在最终 state 中就混成同一用途。

Agent state 同时保留两类信息:messages 支撑调试、追踪和继续对话,structured_response 支撑工单路由等业务动作。下游代码先判断自己需要过程还是结果,再选择读取字段,而不是默认取消息列表的最后一项。

这个区分还会改变错误排查顺序。页面上没有自然语言回答,不一定表示调用失败;如果本次任务的目标是抽取工单,首先应该检查最终 state 是否存在 structured_response。反过来,字段结果不对时也不应只打印最后一条消息,而要检查 schema、策略和校验反馈。

结构化输出的完成标志不是"模型说完了",而是业务对象通过校验并进入最终 state。 这让 Agent 的输出可以成为程序接口,而不仅是聊天内容。

这里的"通过校验"仍然只是结构层结论。order_id="A1024" 符合字符串类型,不代表数据库里一定存在这个订单;next_action="create_replacement" 形状正确,也不代表当前用户拥有换货权限。结构化结果进入业务代码后,仍要继续执行订单查询、身份校验、权限判断和幂等控制。schema 负责拦住字段层错误,业务服务负责判断事实与动作是否合法,两层不能互相替代。

第一道闸门只回答"数据形状能否被程序接收":字段是否完整、类型是否正确、枚举是否落在允许值域。第二道闸门才回答"这个动作能否在当前业务中执行":订单是否存在、调用者是否有权限、状态是否允许继续流转;任何一层失败,都应停在自己的职责边界处理,而不是把问题重新丢给模型猜测。

接下来聚焦第一道闸门内部:即使都属于结构化输出失败,字段没有通过 schema 与一次返回多个候选结果,触发的修复路径也不相同。

6. 校验失败不是一种错误,而是两条不同路径

模型仍可能犯错。Structured Output 提供的不是"模型永远不会输出错",而是让错误在进入业务代码之前暴露,并为修复建立明确路径。

第一类是 schema validation error。假设工单 schema 只允许 lowmediumhigh,模型却生成 urgent,Pydantic 校验会拒绝这个值。使用 ToolStrategy 且保持默认 handle_errors=True 时,LangChain 会把具体错误反馈给模型,请它重新生成符合约束的参数。

第二类是 multiple structured outputs error。某些场景会用 Union schema 允许模型在多个结构类型中选择一个;如果模型一次返回多个结构化输出,而当前运行只期望一个结果,问题不在字段值,而在返回数量。LangChain 同样可以把错误送回模型,要求它只保留最合适的结构。

把两者都叫"JSON 解析失败"会丢掉真正的定位信息:

  • JSON 解析关注语法是否有效;
  • schema validation 关注字段、类型和值域是否符合契约;
  • multiple structured outputs 关注一次运行返回了多少个互斥结果。

ToolStrategy.handle_errors 决定错误怎样离开校验点。默认 True 会捕获错误、生成反馈并重试;传入字符串可以给所有错误使用固定反馈;传入异常类型或处理函数可以缩小捕获范围;设为 False 则不重试,让异常直接交给调用方。

handle_errors 控制的是校验失败后的流向:启用处理时,错误反馈会进入消息链并推动模型重新生成;关闭处理时,失败不会绕过 schema 进入结果字段,而是作为异常交给调用方。重试由配置建立,不是模型自行决定"再试一次"。

重试也不是越多越好。如果 schema 本身互相矛盾,或者字段描述缺少必要业务信息,模型反复生成只会增加延迟与成本。看到同一字段连续失败时,应该回头检查 schema 和输入是否足够,而不是把所有问题都归因于模型"不听话"。

生产系统还需要决定失败后的业务动作:返回人工审核、保存原始文本等待补录,还是终止整个请求。Structured Output 能把错误定位到契约层,但无法替应用决定风险策略。

7. 用 qwen3.7-plus 跑通客服工单抽取

现在把前面的对象接成一次完整调用。代码重点只有四处:Pydantic schema 定义业务契约;ToolStrategy 固定执行路径;response_format 把策略交给 Agent;业务结果从 structured_response 读取。

python 复制代码
import os
from typing import Literal

from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field


class SupportTicket(BaseModel):
    """从用户投诉中抽取的客服工单。"""

    order_id: str | None = Field(
        default=None,
        description="订单号;原文没有提供时返回 null",
    )
    category: Literal["product", "shipping", "billing", "service"] = Field(
        description="问题分类",
    )
    priority: Literal["low", "medium", "high"] = Field(
        description="处理优先级",
    )
    next_action: str = Field(
        description="客服系统下一步应执行的动作",
    )


model = ChatOpenAI(
    model="qwen3.7-plus",
    api_key=os.environ["DASHSCOPE_API_KEY"],
    base_url=os.environ["DASHSCOPE_BASE_URL"],
)

agent = create_agent(
    model=model,
    tools=[],
    response_format=ToolStrategy(
        schema=SupportTicket,
        handle_errors=(
            "请严格按照工单 schema 返回一个结果;"
            "category 和 priority 必须使用允许的枚举值。"
        ),
    ),
)

result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": (
                    "订单 A1024 的耳机右侧没有声音。"
                    "我明天要出差,希望尽快换货。"
                ),
            }
        ]
    }
)

ticket: SupportTicket = result["structured_response"]
print(ticket.model_dump())

输入从 messages 进入 Agent,而不是直接传给 Pydantic。ToolStrategySupportTicket 转成模型可使用的结构化输出约束;模型生成参数后,LangChain 负责捕获并交给 Pydantic 校验。只有通过校验的对象才会成为 result["structured_response"]

这段代码没有把 tools=[] 理解成"完全没有工具机制"。它表示没有额外的业务工具;ToolStrategy 仍会使用结构化输出所需的 tool calling 路径。以后加入订单查询或库存检查工具时,才需要进一步验证模型能否同时完成普通工具调用和结构化输出。

handle_errors 中的文字只负责告诉模型怎样修正格式,不应该承载全部业务规则。允许的分类和值域已经写在 schema 中,错误反馈只强调如何回到契约。这样修改字段时不必同时维护一份长错误 prompt。

示例没有写死区域地址,而是从 DASHSCOPE_BASE_URL 读取。Alibaba Cloud 不同区域使用不同 endpoint 和 API key,二者必须属于同一部署范围;这属于模型连接前提,不是 Structured Output 机制本身。

如果运行后没有得到预期对象,可以按执行链从前向后排查:

  1. 模型调用是否成功,认证、endpoint 和模型名是否属于同一区域;
  2. qwen3.7-plus 是否实际收到结构化输出工具 schema;
  3. 模型返回的是一个还是多个结构化输出调用;
  4. Pydantic 具体拒绝了哪个字段、类型或值域;
  5. handle_errors 是反馈重试,还是已经关闭并向外抛出异常;
  6. 业务代码是否从 structured_response 读取,而不是继续解析最后一条消息。

六个检查点对应六个不同的故障层:能力不支持时改 prompt 没有用,策略选错时重写 schema 也解决不了问题。只有当前一层确认通过,排查才进入下一层;一旦失败,就在模型能力、策略、契约、错误反馈、结果读取或业务规则中定位对应对象。

这个顺序比反复修改 prompt 更可靠,因为每一步都对应一个明确对象:连接、策略、模型调用、schema 校验、错误处理和结果读取。

总结:把模型回答变成业务接口

Structured Output 解决的不是输出排版,而是模型结果能否跨过程序边界。客服工单从一段自然语言开始,schema 先定义字段、类型和值域,response_format 把契约交给 Agent,策略决定约束在哪一层执行,校验拦住不合格结果,最终对象再进入 structured_response

选择时可以抓住两条线:

  • 只需要给人展示一段回答,自然语言通常更合适;后续程序依赖固定字段时,再引入结构化输出。
  • 模型供应商原生支持且能力边界明确时可以使用 ProviderStrategy;需要依靠 Function Calling 或希望固定执行路径时使用 ToolStrategy

结构化输出也没有消除模型的不确定性。它做的是把不确定性限制在可检查的位置:字段不合规时产生 validation error,返回多个结果时产生数量错误,业务代码只接收通过契约的对象。

到这里,Model、Message 和 Structured Output 已经分别回答了"谁负责推理""上下文怎样传递""结果怎样成为程序对象"。下一篇进入 Agent 核心循环,继续追踪模型、消息和工具如何在一次运行中反复交接,直到满足终止条件。

相关推荐
qetfw11 小时前
CentOS 7 搭建 MariaDB 数据库服务
linux·数据库·centos·mariadb
光影62711 小时前
MySQL 进阶篇 —— 事务 / 外键 / 索引 / 高级查询
数据库·笔记·sql·学习·mysql
爬也要爬着前进11 小时前
redis哨兵模式搭建
数据库·redis·bootstrap
小月土星11 小时前
(实战篇)RAG-demo 深度解析:162行代码中的检索增强生成全景 (含Top-k法)
javascript·数据库·llm
10岁的博客13 小时前
DevEco Code 的 Plan+Build 模式:审方案再执行的技术深度解析
大数据·数据库·人工智能
buhuizhiyuci13 小时前
【算法篇】位运算 —— 基础篇
java·数据库·算法
fenglllle14 小时前
langchain简单对话demo
人工智能·langchain
youtootech14 小时前
HarmonyOS《柚兔学伴》项目实战08-关系型数据库入门——待办事项持久化
jvm·数据库·华为·harmonyos
微三云 - 廖会灵 (私域系统开发)14 小时前
电商系统国际化架构设计:多语言、多币种、多时区、多税制的全链路实现
java·前端·数据库
周杰伦的稻香14 小时前
PostgreSQL中的METHOD(认证方法)
数据库·postgresql