news 2026/7/19 3:53:13

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入学LangChain 官方文档(四):Structured Output 结构化输出

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

本篇对应的官方文档

  • LangChain Structured Output:支撑 Agent 场景中的response_format、两种执行策略、schema 校验、structured_response和错误处理主线。
  • Models - Structured Output:用于划清模型级with_structured_output()与本篇 Agent 结构化输出的调用边界。
  • Alibaba Cloud Model Studio - Function Calling:支撑qwen3.7-plus使用ToolStrategy所依赖的 Function Calling 能力。

本篇讲解范围

本篇集中讲清 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 返回”。模型通常会照做,甚至会给出看起来很规整的代码块:

{"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 类型里,并在成功时返回经过验证的模型实例,适合本篇的工单场景。

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

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

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读取。

importosfromtypingimportLiteralfromlangchain.agentsimportcreate_agentfromlangchain.agents.structured_outputimportToolStrategyfromlangchain_openaiimportChatOpenAIfrompydanticimportBaseModel,FieldclassSupportTicket(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 核心循环,继续追踪模型、消息和工具如何在一次运行中反复交接,直到满足终止条件。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/19 3:52:20

JavaScript 函数 this 全解

文章目录 一、this 是什么?二、四条铁律:this 绑定规则1. 默认绑定 —— 独立函数调用2. 隐式绑定 —— 作为对象方法调用3. 显式绑定 —— call、apply、bind4. new 绑定 —— 构造函数调用 三、箭头函数的 this四、this 绑定优先级五、常见场景实战1. …

作者头像 李华
网站建设 2026/7/19 3:51:26

Airflow云原生ELT实战:MWAA+Redshift+dbt架构落地指南

1. 项目概述:当Airflow离开本地服务器,真正扎根云原生环境“Airflow is on the Cloud”不是一句口号,而是过去三年我亲手推动的五个核心数据平台迁移项目中,最常被客户在立项会上反复确认的一句话。它背后藏着一个现实困境&#x…

作者头像 李华
网站建设 2026/7/19 3:49:49

Python hashlib模块:安全哈希与消息摘要详解

1. Python hashlib模块概述 hashlib是Python标准库中用于安全哈希和消息摘要的模块。它提供了多种加密哈希算法的通用接口,包括SHA系列、MD5以及更现代的BLAKE2算法。这个模块在数据完整性验证、密码存储、数字签名等场景中扮演着关键角色。 我第一次接触hashlib是…

作者头像 李华
网站建设 2026/7/19 3:49:44

Flask用户登录系统:密码安全与会话管理实践

1. 用户登录系统概述在Web应用开发中,用户登录系统是最基础也是最重要的功能模块之一。一个完整的用户登录系统需要处理用户认证、会话管理、密码安全等多个关键环节。使用Flask框架构建用户登录系统时,通常会结合Flask-Login、Werkzeug等扩展来实现这些…

作者头像 李华
网站建设 2026/7/19 3:45:30

技术类Newsletter设计方法论:从内容分发到认知操作系统

1. 项目概述:一份AI社区 Newsletter的底层逻辑与实操价值“Learn AI Together — Towards AI Community Newsletter #10”不是一封普通邮件,它是一份经过精密设计的知识流动协议,是AI领域里少有的、把“人”真正放在技术中心的社区运营样本。…

作者头像 李华