11918 字
60 分钟
LangChain Core 源码解剖:OutputParser 与结构化输出

LangChain Core 源码学习路线:第 3 篇 OutputParser 与结构化输出源码解剖#

核心问题: 模型输出如何从自然语言或 ToolCall 转换为业务代码可安全消费的结构化对象?

源码主线: AIMessage / Generation → OutputParser → JSON → Pydantic → structured_response

前置文章: 第 1 篇《Runnable 源码解剖》、第 2 篇《Prompt、Message 与 ChatModel 源码解剖》

依赖基线: langchain-core==1.4.8langchain==1.3.11

源码基线: https://github.com/langchain-ai/langchain/tree/83e824922ac9bf3bf882347fecabd09614bccc37 ,commit 83e824922ac9bf3bf882347fecabd09614bccc37

阅读边界: 本篇覆盖 Parser、Schema 和 structured output 策略,不展开真实 Tool 的本地执行。


0. 本篇在源码学习主线中的位置#

前两篇已经建立了两个底层认知,下一篇还会继续补上工具调用链路:

第 1 篇 Runnable:
所有组件都可以被 invoke / stream / batch / compose。
第 2 篇 Prompt / Message / ChatModel:
Prompt 不只是字符串,而是结构化消息构造器;
ChatModel 输入是 PromptValue / Messages,输出是 AIMessage。
第 4 篇 Tool Calling(后续):
模型不直接执行函数,而是输出 ToolCall;
本地执行器执行工具,再用 ToolMessage 回填。

这一篇要补上 Agent 工程中同样关键的一环:

模型输出如何从“自然语言文本”变成“业务代码可以安全消费的结构化对象”。

在真实 Agent 系统里,很多核心节点都不能只返回一段自然语言:

意图识别节点
不能只返回:“用户想旅行”
应返回:IntentResult(intent, slots, need_clarification, confidence)
槽位抽取节点
不能只返回:“用户想去东京 5 天”
应返回:destination="东京", days=5, preferences=[...]
计划生成节点
不能只返回:“第一天去浅草寺,第二天去...”
应返回:TravelPlan(days=[DayPlan(...)])
搜索 Query 生成节点
不能只返回:“搜索东京美食”
应返回:SearchQuery(query, filters, top_k, need_web)
反思节点
不能只返回:“计划还不错”
应返回:ReflectionResult(passed, issues, revised_plan, risk_level)
最终回复节点
不能只返回:“这是你的旅行计划”
应返回:FinalResponse(answer, citations, follow_up_questions, trace_summary)

所以结构化输出不是锦上添花,而是把 LLM 接入工程系统的基础能力。


1. 本篇问题、学习目标与能力边界#

本篇目标是掌握:

模型输出从自然语言字符串变成结构化对象的过程,以及这个过程在 LangChain 源码中的实现方式。

重点理解:

  • StrOutputParser
  • JsonOutputParser
  • PydanticOutputParser
  • BaseOutputParser
  • BaseGenerationOutputParser
  • BaseTransformOutputParser
  • BaseCumulativeTransformOutputParser
  • OutputParserException
  • with_structured_output
  • Pydantic schema
  • JSON Schema
  • schema validation
  • parser failure
  • retry / repair
  • include_raw
  • ProviderStrategy
  • ToolStrategy
  • JsonOutputKeyToolsParser
  • PydanticToolsParser

完成本篇后,你应该能回答:

  1. StrOutputParser 为什么可以接在 prompt | model 后面?
  2. JsonOutputParser 如何从模型输出中提取 JSON?
  3. PydanticOutputParser 如何在 JSON 解析后再做字段校验?
  4. Parser 抛出的异常是什么?异常在哪里抛出?
  5. with_structured_output() 是否等价于 PydanticOutputParser
  6. with_structured_output() 底层如何绑定 Schema?
  7. Pydantic Schema 如何转换成 JSON Schema?
  8. 结构化输出和 Tool Calling 有什么关系?
  9. Provider-native structured output 和 tool calling structured output 有什么区别?
  10. Parser 应该放在 Chain 里,还是依赖模型原生 structured output?
  11. 解析失败时应该 retry、repair、fallback,还是直接失败?
  12. 在 LangGraph 节点里,结构化输出结果应该如何写入 Agent State?

2. 核心概念与最小心智模型#

结构化输出在 Agent 范式中的位置#

结构化输出对应的不是单一 Agent 范式,而是横跨多个核心模块:

意图识别 / Intent Recognition
槽位抽取 / Slot Filling
计划生成 / Planning
工具参数生成 / Tool Argument Generation
规则判断 / Policy Decision
责任归因 / Responsibility Attribution
反思评估 / Reflection
安全判断 / Guardrail Decision
最终响应 / Response Packaging

这些节点的共同点是:

模型输出需要被业务代码消费
而不是只给用户阅读

因此范式映射为:

意图识别 / 槽位抽取 / 计划生成 / 责任判断
结构化输出
Pydantic Model / TypedDict / JSON Schema
Python 对象 / dict
Agent State

普通 LLM Chain → OutputParser#

传统链路:

User Input
Prompt
Model
Raw AIMessage / text
OutputParser
Python object

框架表达:

chain = prompt | model | parser

这对应第 1 篇 Runnable 的主线:

prompt | model | parser
RunnableSequence

其中:

Prompt
是 Runnable
ChatModel
是 Runnable
OutputParser
也是 Runnable

所以 parser 能接在 chain 后面,不是因为它是特殊语法,而是因为它实现了 Runnable 协议。

现代结构化输出 → with_structured_output#

现代链路:

Pydantic schema
model.with_structured_output(schema)
Runnable wrapper
模型原生 structured output
或 tool calling structured output
解析与校验
Pydantic object / dict

框架表达:

structured_model = model.with_structured_output(TravelIntent)
result = structured_model.invoke("我想去东京玩 5 天")

它表面上看起来像一个模型,实际上是一个封装后的 Runnable:

BaseChatModel
with_structured_output
Runnable[LanguageModelInput, PydanticObject | dict]

Agent 级结构化输出 → response_format#

Agent 级链路:

create_agent(
model=...,
tools=...,
response_format=Schema
)

结果会进入:

agent final state["structured_response"]

这比普通 parser 更接近生产级 Agent,因为它让结构化输出成为 Agent Runtime 的状态字段,而不是某个链路末尾的临时解析结果。


3. 完整执行链路#

最小示例与执行过程合并,直接观察文本、JSON、Pydantic 对象和 structured_response 的流转。

最小代码#

使用 with_structured_output#

from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model
class TravelIntent(BaseModel):
destination: str | None = Field(description="目的地")
days: int | None = Field(description="旅行天数")
preferences: list[str] = Field(description="用户偏好")
need_clarification: bool = Field(description="是否需要追问")
model = init_chat_model("openai:gpt-4.1-mini")
structured_model = model.with_structured_output(TravelIntent)
result = structured_model.invoke(
"我想去东京玩 5 天,喜欢美食和城市漫步"
)
print(result)

预期结果类型:

TravelIntent(
destination="东京",
days=5,
preferences=["美食", "城市漫步"],
need_clarification=False,
)

关键点:

result 不再是 AIMessage;
也不是字符串;
而是 Pydantic 对象。

对比普通模型调用#

raw_result = model.invoke(
"我想去东京玩 5 天,喜欢美食和城市漫步"
)
print(type(raw_result))
print(raw_result.content)

普通模型返回:

AIMessage

结构化模型返回:

TravelIntent

这说明:

with_structured_output
并不是改变模型本体;
而是返回了一个新的 Runnable wrapper。

使用 StrOutputParser#

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "你是旅行规划助手。"),
("user", "请为 {destination} 规划 {days} 天行程。")
])
chain = prompt | model | StrOutputParser()
result = chain.invoke({
"destination": "东京",
"days": 5,
})
print(type(result))
print(result)

输出:

str

这条链路只做:

AIMessage
提取文本 content
str

使用 JsonOutputParser#

from langchain_core.output_parsers import JsonOutputParser
parser = JsonOutputParser()
chain = prompt | model | parser
result = chain.invoke({
"destination": "东京",
"days": 5,
})
print(type(result))
print(result)

输出:

dict 或 list

前提是模型输出是可解析 JSON。

使用 PydanticOutputParser#

from langchain_core.output_parsers import PydanticOutputParser
parser = PydanticOutputParser(
pydantic_object=TravelIntent
)
format_instructions = parser.get_format_instructions()
prompt = ChatPromptTemplate.from_messages([
("system", "你是旅行需求解析器。请严格按格式输出。"),
("user", "{input}\n\n{format_instructions}")
])
chain = prompt | model | parser
result = chain.invoke({
"input": "我想去东京玩 5 天,喜欢美食和城市漫步",
"format_instructions": format_instructions,
})
print(type(result))
print(result)

这条链路是传统结构化输出方式:

Prompt 中塞格式要求
模型输出 JSON 文本
JsonOutputParser 解析 JSON
PydanticOutputParser 校验字段
TravelIntent 对象

执行流程#

with_structured_output 执行流程#

Pydantic schema
转换为模型可理解的结构化输出约束
绑定到 ChatModel wrapper
模型生成结构化结果
框架解析并校验
返回 Pydantic 对象或 dict

细化为源码心智模型:

TravelIntent(BaseModel)
model.with_structured_output(TravelIntent)
判断 schema 类型
is_pydantic_schema = True
选择结构化输出策略
├─ provider-native structured output
└─ tool calling structured output
构造 llm Runnable
构造 output_parser
返回 llm | output_parser
或 RunnableMap(raw=llm) | parser_with_fallback

PydanticOutputParser 执行流程#

AIMessage / Generation
取出 text
JsonOutputParser.parse_result
parse_json_markdown
得到 dict
PydanticOutputParser._parse_obj
TravelIntent.model_validate(dict)
返回 TravelIntent

JsonOutputParser 执行流程#

模型输出文本
去除 Markdown 代码块
定位 JSON 对象
json.loads / parse_partial_json
返回 dict/list
失败则抛 OutputParserException

StrOutputParser 执行流程#

AIMessage
ChatGeneration
Generation.text / message.content
parse(text)
返回 text 本身

StrOutputParser 是最薄的一层 parser,基本不做结构化校验。


4. 源码地图、关键文件与阅读顺序#

核心目录:

langchain_core/output_parsers/
langchain_core/language_models/chat_models.py
langchain_core/utils/function_calling.py
langchain_core/output_parsers/openai_tools.py
langchain/agents/structured_output.py

重点文件:

libs/core/langchain_core/output_parsers/base.py
libs/core/langchain_core/output_parsers/string.py
libs/core/langchain_core/output_parsers/json.py
libs/core/langchain_core/output_parsers/pydantic.py
libs/core/langchain_core/output_parsers/openai_tools.py
libs/core/langchain_core/language_models/chat_models.py
libs/core/langchain_core/utils/function_calling.py
libs/langchain/langchain/agents/structured_output.py

重点类与函数:

BaseLLMOutputParser
BaseGenerationOutputParser
BaseOutputParser
BaseTransformOutputParser
BaseCumulativeTransformOutputParser
StrOutputParser
JsonOutputParser
PydanticOutputParser
OutputParserException
BaseChatModel.with_structured_output
BaseChatModel.bind_tools
JsonOutputToolsParser
JsonOutputKeyToolsParser
PydanticToolsParser
convert_to_json_schema
convert_to_openai_tool
convert_to_openai_function
ProviderStrategy
ToolStrategy

推荐阅读顺序:

1. output_parsers/base.py
2. output_parsers/string.py
3. output_parsers/json.py
4. output_parsers/pydantic.py
5. output_parsers/openai_tools.py
6. language_models/chat_models.py::BaseChatModel.with_structured_output
7. utils/function_calling.py
8. agents/structured_output.py

5. 对象模型、继承关系与协议边界#

OutputParser 对象模型#

Parser 也是 Runnable#

OutputParser 能放进:

prompt | model | parser

是因为它实现了 Runnable 协议。

对象关系可以理解为:

BaseOutputParser
BaseTransformOutputParser
RunnableSerializable

因此 parser 拥有:

parser.invoke(...)
parser.ainvoke(...)
parser.batch(...)
parser.stream(...)
parser.with_retry(...)
parser.with_config(...)

这也是为什么结构化输出错误可以接入 Runnable 级别的:

with_retry
with_fallbacks
config
callbacks
tags
metadata

BaseLLMOutputParser#

这一层更接近“从 LLM 生成结果解析”:

class BaseLLMOutputParser(Generic[T], ABC):
@abstractmethod
def parse_result(
self,
result: list[Generation],
*,
partial: bool = False,
) -> T:
...

它面对的是:

list[Generation]

而不是直接面对字符串。

为什么是 list?

因为 LLM 调用可能返回多个候选 generation:

n=1
n=3
n=5

Parser 通常只取第一个候选:

result[0]

BaseGenerationOutputParser#

这一层把 parser 接入 Runnable 调用。

伪代码:

class BaseGenerationOutputParser(
BaseLLMOutputParser[T],
RunnableSerializable[LanguageModelOutput, T],
):
def invoke(self, input, config=None, **kwargs):
if isinstance(input, BaseMessage):
generation = ChatGeneration(message=input)
else:
generation = Generation(text=input)
return self._call_with_config(
lambda inner_input: self.parse_result(
[generation]
),
input,
config,
run_type="parser",
)

也就是说:

parser.invoke(AIMessage)
包装成 ChatGeneration
parse_result([ChatGeneration])
parser.invoke("raw text")
包装成 Generation
parse_result([Generation])

这就是 parser 可以同时处理:

AIMessage
str
Generation

的原因。

BaseOutputParser#

BaseOutputParser 进一步定义了:

def parse(self, text: str) -> T:
...

以及:

def parse_result(self, result, partial=False):
return self.parse(result[0].text)

简化理解:

BaseGenerationOutputParser
面向 Generation
BaseOutputParser
面向 text
具体 parser
实现 parse(text)

BaseTransformOutputParser#

这一层负责 streaming 场景。

普通非流式:

完整 AIMessage
parse_result

流式场景:

AIMessageChunk
chunk 累积
parse_result(partial=True)
不断产出中间解析结果

不是所有 parser 都能高质量支持 streaming。

例如:

StrOutputParser
可以天然流式输出文本
JsonOutputParser
可以在 partial=True 时输出部分 JSON
PydanticOutputParser
通常需要完整对象后才能稳定校验

核心对象关系#

BaseLLMOutputParser
BaseGenerationOutputParser
BaseOutputParser
├─ StrOutputParser
├─ JsonOutputParser
└─ PydanticOutputParser

协议边界#

Parser 负责把模型结果转换为目标对象。Schema 负责描述和校验结构。模型原生 structured output 负责约束生成协议。三者可以协作,但不能互相替代。


6. 源码阅读策略与证据标准#

阅读顺序#

公开入口
输入输出类型
构建期对象
运行时主链
分支、异常与停止条件
扩展接口

证据标准#

标记使用条件
源码事实当前正式版源码可以直接证明
官方契约官方文档或 API Reference 明确承诺
简化伪代码压缩真实控制流,且明确不是逐字源码
作者推断根据调用关系得出,必须标注为推断
工程建议说明适用条件,不写成框架保证

7. 构建期源码解剖#

本章解释 Parser 对象、Schema 转换与 structured output 策略如何被构建。

with_structured_output() 源码解剖#

它解决什么问题#

传统 parser 方式依赖 Prompt:

请严格输出 JSON
不要输出解释
字段必须是...

但模型仍可能输出:

当然可以,下面是 JSON:
{
...
}
希望对你有帮助!

或者输出字段不全、类型错误、JSON 不闭合。

with_structured_output() 的目标是:

尽量利用模型供应商的原生结构化输出或工具调用机制,让结构化约束不只停留在 Prompt 文本里。

方法签名心智模型#

不同供应商集成包的签名略有差异,但常见形态是:

def with_structured_output(
self,
schema: dict | type | None = None,
*,
method: Literal[
"function_calling",
"json_mode",
"json_schema",
] = "function_calling",
include_raw: bool = False,
strict: bool | None = None,
**kwargs: Any,
) -> Runnable[LanguageModelInput, dict | BaseModel]:
...

核心参数:

参数含义
schemaPydantic / TypedDict / JSON Schema / tool schema
method使用 tool calling、JSON mode 还是 JSON schema
include_raw是否同时返回原始 AIMessage
strict是否启用供应商支持的严格 Schema 约束
kwargs供应商扩展参数

注意:

BaseChatModel 定义抽象协议;
具体行为通常由供应商模型类重写。

例如 OpenAI、Anthropic、Gemini、Ollama、Mistral、xAI 等集成包可能各自支持不同 method。

结构化输出的三条实现路线#

路线一:Provider-native structured output#
Pydantic / JSON Schema
供应商原生 response_format / json_schema / structured output
供应商 API 约束输出
LangChain 转为 Pydantic / dict

优点:

  • 稳定性高;
  • 供应商端强约束;
  • JSON 格式错误更少;
  • 适合生产核心节点。

缺点:

  • 依赖模型能力;
  • 不同供应商 API 差异大;
  • 不是所有模型都支持;
  • 某些情况下与工具调用同时使用有限制。
路线二:Tool calling structured output#
Pydantic Schema
转换为一个“虚拟工具”
模型调用这个工具
AIMessage.tool_calls
PydanticToolsParser / JsonOutputKeyToolsParser
Pydantic object / dict

核心思想:

让模型通过工具调用参数来表达结构化结果。

这与第 4 篇 Tool Calling 强关联。

路线三:Prompt + OutputParser#
PydanticOutputParser.get_format_instructions()
塞进 Prompt
模型输出 JSON 文本
Parser 解析与校验

优点:

  • 通用;
  • 适合不支持工具调用/原生结构化输出的模型;
  • 可完全自定义 prompt。

缺点:

  • 稳定性最低;
  • 需要 repair / retry;
  • 容易受模型输出风格影响。

tool calling 模式伪代码#

很多供应商 with_structured_output() 在 function_calling / tool_calling 模式下可以抽象为:

def with_structured_output(
self,
schema,
*,
include_raw=False,
**kwargs,
):
if kwargs:
raise ValueError(
"Received unsupported arguments"
)
is_pydantic_schema = is_basemodel_subclass(schema)
llm = self.bind_tools(
[schema],
tool_choice="any_or_schema_name",
structured_output_format={
"kwargs": {...},
"schema": schema,
},
)
if is_pydantic_schema:
output_parser = PydanticToolsParser(
tools=[schema],
first_tool_only=True,
)
else:
key_name = convert_to_openai_tool(schema)[
"function"
]["name"]
output_parser = JsonOutputKeyToolsParser(
key_name=key_name,
first_tool_only=True,
)
if include_raw:
return _include_raw_wrapper(
llm,
output_parser,
)
return llm | output_parser

逐层解释:

第一步:识别 schema 类型#
is_pydantic_schema = is_basemodel_subclass(schema)

如果是 Pydantic:

最终返回 Pydantic 对象

如果是 JSON Schema / TypedDict:

最终返回 dict
第二步:绑定 Schema 为工具#
llm = self.bind_tools([schema], ...)

这一步不是执行工具,而是把 Schema 转成模型可调用的“结构化输出工具”。

第三步:选择 parser#

Pydantic Schema:

PydanticToolsParser(
tools=[schema],
first_tool_only=True,
)

非 Pydantic Schema:

JsonOutputKeyToolsParser(
key_name=schema_name,
first_tool_only=True,
)
第四步:返回 RunnableSequence#
return llm | output_parser

所以结构化模型本质仍是:

RunnableSequence

只是中间模型输出不是自然语言 JSON 文本,而是 tool_calls。

include_raw=True 伪代码#

include_raw=True 时,返回值通常变成:

{
"raw": AIMessage,
"parsed": TravelIntent | dict | None,
"parsing_error": BaseException | None,
}

伪代码:

def _include_raw_wrapper(llm, output_parser):
parser_assign = RunnablePassthrough.assign(
parsed=itemgetter("raw") | output_parser,
parsing_error=lambda _: None,
)
parser_none = RunnablePassthrough.assign(
parsed=lambda _: None
)
parser_with_fallback = (
parser_assign.with_fallbacks(
[parser_none],
exception_key="parsing_error",
)
)
return RunnableMap(raw=llm) | parser_with_fallback

逐层解释:

第一步:保留 raw#
RunnableMap(raw=llm)

输出:

{"raw": AIMessage(...)}
第二步:尝试解析 raw#
parsed = itemgetter("raw") | output_parser

表示:

从 {"raw": AIMessage} 中取 raw
交给 output_parser
得到 parsed
第三步:解析成功#
{
"raw": AIMessage(...),
"parsed": TravelIntent(...),
"parsing_error": None,
}
第四步:解析失败#

fallback 触发:

{
"raw": AIMessage(...),
"parsed": None,
"parsing_error": OutputParserException(...),
}

这对调试极其重要:

不用丢失原始模型输出;
不用让整个链直接失败;
可以把错误写入 trace;
可以进入 repair 节点。

provider-native 模式伪代码#

不同供应商差异较大,但心智模型可以抽象为:

def with_structured_output_provider_native(
self,
schema,
*,
include_raw=False,
strict=None,
):
json_schema = convert_to_json_schema(schema)
llm = self.bind(
response_format={
"type": "json_schema",
"json_schema": json_schema,
"strict": strict,
},
structured_output_format={
"kwargs": {
"method": "json_schema",
"strict": strict,
},
"schema": schema,
},
)
if is_pydantic_schema(schema):
parser = PydanticOutputParser(
pydantic_object=schema
)
else:
parser = JsonOutputParser()
if include_raw:
return RunnableMap(raw=llm) | parser_with_fallback
return llm | parser

区别在于:

tool calling 模式
输出在 AIMessage.tool_calls 中
provider-native 模式
输出通常在 message content 或供应商标准字段中

具体取决于集成包适配。

为什么 with_structured_output 与 tool calling 有关系#

因为很多模型没有原生 structured output,但支持 tool calling。

此时 LangChain 会把:

class TravelIntent(BaseModel):
...

转成一个“虚拟工具”:

{
"type": "function",
"function": {
"name": "TravelIntent",
"description": "...",
"parameters": {
"type": "object",
"properties": {
"destination": {"type": "string"},
"days": {"type": "integer"}
}
}
}
}

模型返回:

AIMessage(
content="",
tool_calls=[
{
"name": "TravelIntent",
"args": {
"destination": "东京",
"days": 5,
},
"id": "call_...",
"type": "tool_call",
}
],
)

然后 parser 读取 tool call args,构造:

TravelIntent(...)

这就是:

结构化输出
=
工具调用协议的一种特殊用法

注意:这里的“工具”不是业务工具,不会执行真实 Python 函数。

它只是把结构化对象伪装成模型要调用的 schema。


Pydantic Schema 如何转 JSON Schema#

Pydantic Model#

from pydantic import BaseModel, Field
class TravelIntent(BaseModel):
destination: str | None = Field(
description="目的地"
)
days: int | None = Field(
description="旅行天数",
ge=1,
le=30,
)
preferences: list[str] = Field(
description="用户偏好"
)
need_clarification: bool = Field(
description="是否需要追问"
)

JSON Schema#

调用:

TravelIntent.model_json_schema()

近似得到:

{
"title": "TravelIntent",
"type": "object",
"properties": {
"destination": {
"anyOf": [
{"type": "string"},
{"type": "null"}
],
"description": "目的地"
},
"days": {
"anyOf": [
{
"type": "integer",
"minimum": 1,
"maximum": 30
},
{"type": "null"}
],
"description": "旅行天数"
},
"preferences": {
"type": "array",
"items": {"type": "string"},
"description": "用户偏好"
},
"need_clarification": {
"type": "boolean",
"description": "是否需要追问"
}
},
"required": [
"destination",
"days",
"preferences",
"need_clarification"
]
}

convert_to_json_schema#

LangChain 会使用工具函数把不同 schema 类型统一为 JSON Schema:

Pydantic BaseModel
TypedDict
Dataclass
JSON Schema dict
OpenAI tool schema
Python callable

统一成:

模型供应商可理解的 schema

这让 with_structured_output 支持多种输入类型。

Optional 不等于字段可省略#

这里非常容易踩坑。

destination: str | None

表示字段值可以是:

str
None

但如果没有默认值,Pydantic v2 中它仍然可能是必填字段。

如果你希望字段可以省略,应该写:

destination: str | None = None

在 Agent 结构化输出中,建议明确默认值:

class TravelIntent(BaseModel):
destination: str | None = Field(
default=None,
description="目的地,未知时为 None"
)

用 Field 写业务约束#

days: int | None = Field(
default=None,
ge=1,
le=30,
description="旅行天数,必须在 1 到 30 之间"
)
preferences: list[str] = Field(
default_factory=list,
max_length=10,
description="用户偏好列表"
)

这些约束不仅进入 JSON Schema,也会在本地 Pydantic 校验时生效。

用 Literal 写枚举#

from typing import Literal
class ReflectionResult(BaseModel):
risk_level: Literal["low", "medium", "high"]
passed: bool
issues: list[str]

这能避免模型输出:

"风险不大"
"一般"
"OK"

而应该输出:

low / medium / high

用 model_validator 做跨字段校验#

from pydantic import model_validator
class TravelIntent(BaseModel):
destination: str | None = None
days: int | None = None
need_clarification: bool
@model_validator(mode="after")
def check_clarification(self):
if self.need_clarification:
return self
if not self.destination or not self.days:
raise ValueError(
"不需要追问时,destination 和 days 必须存在"
)
return self

这类业务规则无法仅靠 Prompt 保证,应进入 Schema 层。


构建期总结、伪代码与源码证据#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def build_structured_model(model, schema, method):
json_schema = convert_to_json_schema(schema)
bound_model = bind_schema(model, json_schema, method=method)
parser = select_parser(schema, method)
return bound_model | parser

源码证据:


8. 运行时主链源码解剖#

本章沿 Str、JSON、Pydantic 与 with_structured_output 的主路径追踪结果转换。

StrOutputParser 源码解剖#

用途#

StrOutputParser 的职责非常简单:

从模型输出中提取文本,返回字符串。

它适合:

最终自然语言回复
普通摘要
改写任务
非结构化解释
不需要字段校验的输出

不适合:

意图识别
槽位抽取
计划步骤
工具参数
责任判断
安全决策

因为这些场景需要可校验结构。

源码伪代码#

StrOutputParser 可以近似理解为:

class StrOutputParser(BaseTransformOutputParser[str]):
@property
def OutputType(self) -> type[str]:
return str
def parse(self, text: str) -> str:
return text

它几乎不改变模型输出。

真正的提取文本逻辑发生在 parser 基类处理 Generation 时:

AIMessage
ChatGeneration
.text
StrOutputParser.parse(text)
text

为什么不是直接返回 AIMessage.content#

因为 LCEL 中 parser 接收的是标准化后的 LLM output,而不是总是假设输入为 AIMessage

底层需要统一支持:

Generation(text="...")
ChatGeneration(message=AIMessage(...))
AIMessage
AIMessageChunk
str

所以源码不是简单:

return ai_message.content

而是:

输入 → Generation 包装 → parse_result → parse(text)

运行链路#

chain = prompt | model | StrOutputParser()

执行:

chain.invoke(input)
prompt.invoke(input)
ChatPromptValue
model.invoke(ChatPromptValue)
AIMessage
StrOutputParser.invoke(AIMessage)
ChatGeneration(message=AIMessage)
parse_result([ChatGeneration])
parse(generation.text)
str

常见误区#

误区:StrOutputParser 是为了“修复格式”#

不是。它不修复,不校验,不提取 JSON。

它只是:

模型输出 → 字符串
误区:最终回复都必须加 StrOutputParser#

不一定。

如果你的下游希望拿到:

AIMessage.content
AIMessage.response_metadata
AIMessage.usage_metadata
AIMessage.tool_calls

则不要加 StrOutputParser,因为它会丢掉消息对象上的元数据。


JsonOutputParser 源码解剖#

用途#

JsonOutputParser 的职责是:

把模型输出文本解析为 JSON 对象。

适合:

返回 dict
返回 list
简单结构化输出
不需要 Pydantic 强校验
需要 streaming partial JSON

不适合:

字段类型强约束
业务规则校验
枚举 / 范围 / 嵌套模型强约束
高风险业务决策

对象继承#

可以近似理解为:

JsonOutputParser
BaseCumulativeTransformOutputParser
BaseTransformOutputParser
BaseOutputParser

为什么是 cumulative?

因为 JSON 流式解析需要累积前面的 chunk:

{"destination": "东
{"destination": "东京", "days":
{"destination": "东京", "days": 5}

Parser 需要在流式过程中维护累计文本,并尝试解析部分 JSON。

parse_result() 伪代码#

核心逻辑可以拆为:

def parse_result(
self,
result: list[Generation],
*,
partial: bool = False,
) -> Any:
text = result[0].text
text = text.strip()
if partial:
try:
return parse_json_markdown(text)
except JSONDecodeError:
return None
try:
return parse_json_markdown(text)
except JSONDecodeError as error:
msg = f"Invalid json output: {text}"
raise OutputParserException(
msg,
llm_output=text,
) from error

逐层解释:

第一步:取第一个 generation#
text = result[0].text

多候选输出默认只解析第一个。

第二步:清理前后空白#
text = text.strip()
第三步:解析 JSON Markdown#

模型经常输出 Markdown 代码块:

```json
{
"destination": "东京",
"days": 5
}
```

parse_json_markdown 会尝试从 Markdown 代码块里提取 JSON。

第四步:partial 模式#

如果是流式部分解析:

partial=True

失败时不立刻抛错,而是返回:

None

因为流式 JSON 当前还没闭合是正常现象。

第五步:非 partial 模式#

完整输出解析失败时抛出:

OutputParserException

而不是裸 JSONDecodeError

这让上层 Runnable / retry / fallback 能捕获 LangChain 统一异常。

parse() 伪代码#

def parse(self, text: str) -> Any:
return self.parse_result(
[Generation(text=text)]
)

也就是说:

parse(text)
是 parse_result([Generation(text)]) 的简化入口。

get_format_instructions()#

JsonOutputParser 可以根据 JSON Schema 生成格式指令。

伪代码:

def get_format_instructions(self):
if self.pydantic_object is None:
return "Return a JSON object."
schema = self.pydantic_object.model_json_schema()
reduced_schema = remove_title_and_type(schema)
return JSON_FORMAT_INSTRUCTIONS.format(
schema=json.dumps(
reduced_schema,
ensure_ascii=False
)
)

核心作用:

把 Schema 文本塞进 Prompt
告诉模型应该输出什么 JSON

但要注意:

格式指令只是 prompt 约束;
并不等于模型一定严格遵守。

streaming 下的 partial JSON#

JsonOutputParser 在 streaming 模式中有特殊价值。

chain = prompt | model | JsonOutputParser()
for chunk in chain.stream(input):
print(chunk)

可能输出:

{"destination": "东京"}
{"destination": "东京", "days": 5}
{"destination": "东京", "days": 5, "preferences": ["美食"]}

如果启用 diff=True,可以输出 JSONPatch 差异。

这对前端很有用:

边生成
边渲染结构化表单
边更新 UI 局部字段

JsonOutputParser 的边界#

它只能保证:

输出是合法 JSON

不能保证:

字段一定存在
类型一定符合业务期望
days 一定大于 0
risk_level 一定属于枚举
preferences 不为空

这些应该交给:

PydanticOutputParser
with_structured_output(PydanticModel)
业务层 validator

PydanticOutputParser 源码解剖#

用途#

PydanticOutputParser 的职责是:

在 JSON 解析之后,用 Pydantic Model 做字段级、类型级、业务级校验,并返回 Pydantic 对象。

适合:

意图识别
槽位抽取
计划生成
反思结果
安全判定
最终结构化响应

尤其适合你在旅行规划助手中使用的节点:

IntentResult
TravelPlan
SearchQuery
ReflectionResult
FinalResponse

对象继承#

PydanticOutputParser
JsonOutputParser

这说明它不是从零解析文本,而是在 JSON parser 基础上再加一层 Pydantic 校验。

核心流程:

text
JsonOutputParser.parse_result
dict
PydanticOutputParser._parse_obj
BaseModel.model_validate
Pydantic object

_parse_obj() 伪代码#

def _parse_obj(self, obj: dict) -> TBaseModel:
try:
if issubclass(
self.pydantic_object,
pydantic.BaseModel,
):
return self.pydantic_object.model_validate(obj)
if issubclass(
self.pydantic_object,
pydantic.v1.BaseModel,
):
return self.pydantic_object.parse_obj(obj)
raise OutputParserException(
"Unsupported model version"
)
except (ValidationError, pydantic.v1.ValidationError) as error:
raise self._parser_exception(
error,
obj,
) from error

逐层解释:

第一步:判断 Pydantic 版本#

LangChain 需要兼容:

Pydantic v2 BaseModel
Pydantic v1 BaseModel

所以源码中会分别调用:

model_validate(obj)

或:

parse_obj(obj)
第二步:执行字段校验#

例如:

class TravelIntent(BaseModel):
days: int | None = Field(ge=1, le=30)

若模型输出:

{"days": -3}

Pydantic 会抛出:

ValidationError
第三步:包装为 OutputParserException#

LangChain 不直接向上抛裸 Pydantic 错误,而是转为:

OutputParserException

并附带原始 JSON 文本或对象。

_parser_exception() 伪代码#

def _parser_exception(
self,
error: Exception,
json_object: dict,
) -> OutputParserException:
json_string = json.dumps(
json_object,
ensure_ascii=False,
)
name = self.pydantic_object.__name__
msg = (
f"Failed to parse {name} from completion "
f"{json_string}. Got: {error}"
)
return OutputParserException(
msg,
llm_output=json_string,
)

这个异常信息很重要,后续 retry / repair 需要知道:

模型原始输出是什么?
哪里不符合 Schema?
目标 Pydantic 类是什么?

parse_result() 伪代码#

def parse_result(
self,
result: list[Generation],
*,
partial: bool = False,
) -> TBaseModel | None:
try:
json_object = super().parse_result(
result,
partial=partial,
)
return self._parse_obj(json_object)
except OutputParserException:
if partial:
return None
raise

关键点:

partial=True 时,解析失败返回 None;
partial=False 时,解析失败抛异常。

这与 streaming 场景有关。

get_format_instructions() 伪代码#

def get_format_instructions(self) -> str:
schema = self.pydantic_object.model_json_schema()
# 删除不必要字段,减少 token 干扰
reduced_schema = schema.copy()
reduced_schema.pop("title", None)
reduced_schema.pop("type", None)
schema_str = json.dumps(
reduced_schema,
ensure_ascii=False,
)
return _PYDANTIC_FORMAT_INSTRUCTIONS.format(
schema=schema_str
)

这会生成类似提示:

The output should be formatted as a JSON instance that conforms to the JSON schema below.

但重点是:

Prompt 里的 format instructions 是软约束;Pydantic 校验是硬校验。

PydanticOutputParser 的完整链路#

chain.invoke(input)
prompt.invoke(input)
model.invoke(messages)
AIMessage(content='{"destination":"东京",...}')
PydanticOutputParser.invoke(AIMessage)
ChatGeneration(message=AIMessage)
parse_result([ChatGeneration])
JsonOutputParser.parse_result
dict
PydanticOutputParser._parse_obj
TravelIntent.model_validate(dict)
TravelIntent

with_structured_output() 源码解剖#

它解决什么问题#

传统 parser 方式依赖 Prompt:

请严格输出 JSON
不要输出解释
字段必须是...

但模型仍可能输出:

当然可以,下面是 JSON:
{
...
}
希望对你有帮助!

或者输出字段不全、类型错误、JSON 不闭合。

with_structured_output() 的目标是:

尽量利用模型供应商的原生结构化输出或工具调用机制,让结构化约束不只停留在 Prompt 文本里。

方法签名心智模型#

不同供应商集成包的签名略有差异,但常见形态是:

def with_structured_output(
self,
schema: dict | type | None = None,
*,
method: Literal[
"function_calling",
"json_mode",
"json_schema",
] = "function_calling",
include_raw: bool = False,
strict: bool | None = None,
**kwargs: Any,
) -> Runnable[LanguageModelInput, dict | BaseModel]:
...

核心参数:

参数含义
schemaPydantic / TypedDict / JSON Schema / tool schema
method使用 tool calling、JSON mode 还是 JSON schema
include_raw是否同时返回原始 AIMessage
strict是否启用供应商支持的严格 Schema 约束
kwargs供应商扩展参数

注意:

BaseChatModel 定义抽象协议;
具体行为通常由供应商模型类重写。

例如 OpenAI、Anthropic、Gemini、Ollama、Mistral、xAI 等集成包可能各自支持不同 method。

结构化输出的三条实现路线#

路线一:Provider-native structured output#
Pydantic / JSON Schema
供应商原生 response_format / json_schema / structured output
供应商 API 约束输出
LangChain 转为 Pydantic / dict

优点:

  • 稳定性高;
  • 供应商端强约束;
  • JSON 格式错误更少;
  • 适合生产核心节点。

缺点:

  • 依赖模型能力;
  • 不同供应商 API 差异大;
  • 不是所有模型都支持;
  • 某些情况下与工具调用同时使用有限制。
路线二:Tool calling structured output#
Pydantic Schema
转换为一个“虚拟工具”
模型调用这个工具
AIMessage.tool_calls
PydanticToolsParser / JsonOutputKeyToolsParser
Pydantic object / dict

核心思想:

让模型通过工具调用参数来表达结构化结果。

这与第 4 篇 Tool Calling 强关联。

路线三:Prompt + OutputParser#
PydanticOutputParser.get_format_instructions()
塞进 Prompt
模型输出 JSON 文本
Parser 解析与校验

优点:

  • 通用;
  • 适合不支持工具调用/原生结构化输出的模型;
  • 可完全自定义 prompt。

缺点:

  • 稳定性最低;
  • 需要 repair / retry;
  • 容易受模型输出风格影响。

tool calling 模式伪代码#

很多供应商 with_structured_output() 在 function_calling / tool_calling 模式下可以抽象为:

def with_structured_output(
self,
schema,
*,
include_raw=False,
**kwargs,
):
if kwargs:
raise ValueError(
"Received unsupported arguments"
)
is_pydantic_schema = is_basemodel_subclass(schema)
llm = self.bind_tools(
[schema],
tool_choice="any_or_schema_name",
structured_output_format={
"kwargs": {...},
"schema": schema,
},
)
if is_pydantic_schema:
output_parser = PydanticToolsParser(
tools=[schema],
first_tool_only=True,
)
else:
key_name = convert_to_openai_tool(schema)[
"function"
]["name"]
output_parser = JsonOutputKeyToolsParser(
key_name=key_name,
first_tool_only=True,
)
if include_raw:
return _include_raw_wrapper(
llm,
output_parser,
)
return llm | output_parser

逐层解释:

第一步:识别 schema 类型#
is_pydantic_schema = is_basemodel_subclass(schema)

如果是 Pydantic:

最终返回 Pydantic 对象

如果是 JSON Schema / TypedDict:

最终返回 dict
第二步:绑定 Schema 为工具#
llm = self.bind_tools([schema], ...)

这一步不是执行工具,而是把 Schema 转成模型可调用的“结构化输出工具”。

第三步:选择 parser#

Pydantic Schema:

PydanticToolsParser(
tools=[schema],
first_tool_only=True,
)

非 Pydantic Schema:

JsonOutputKeyToolsParser(
key_name=schema_name,
first_tool_only=True,
)
第四步:返回 RunnableSequence#
return llm | output_parser

所以结构化模型本质仍是:

RunnableSequence

只是中间模型输出不是自然语言 JSON 文本,而是 tool_calls。

include_raw=True 伪代码#

include_raw=True 时,返回值通常变成:

{
"raw": AIMessage,
"parsed": TravelIntent | dict | None,
"parsing_error": BaseException | None,
}

伪代码:

def _include_raw_wrapper(llm, output_parser):
parser_assign = RunnablePassthrough.assign(
parsed=itemgetter("raw") | output_parser,
parsing_error=lambda _: None,
)
parser_none = RunnablePassthrough.assign(
parsed=lambda _: None
)
parser_with_fallback = (
parser_assign.with_fallbacks(
[parser_none],
exception_key="parsing_error",
)
)
return RunnableMap(raw=llm) | parser_with_fallback

逐层解释:

第一步:保留 raw#
RunnableMap(raw=llm)

输出:

{"raw": AIMessage(...)}
第二步:尝试解析 raw#
parsed = itemgetter("raw") | output_parser

表示:

从 {"raw": AIMessage} 中取 raw
交给 output_parser
得到 parsed
第三步:解析成功#
{
"raw": AIMessage(...),
"parsed": TravelIntent(...),
"parsing_error": None,
}
第四步:解析失败#

fallback 触发:

{
"raw": AIMessage(...),
"parsed": None,
"parsing_error": OutputParserException(...),
}

这对调试极其重要:

不用丢失原始模型输出;
不用让整个链直接失败;
可以把错误写入 trace;
可以进入 repair 节点。

provider-native 模式伪代码#

不同供应商差异较大,但心智模型可以抽象为:

def with_structured_output_provider_native(
self,
schema,
*,
include_raw=False,
strict=None,
):
json_schema = convert_to_json_schema(schema)
llm = self.bind(
response_format={
"type": "json_schema",
"json_schema": json_schema,
"strict": strict,
},
structured_output_format={
"kwargs": {
"method": "json_schema",
"strict": strict,
},
"schema": schema,
},
)
if is_pydantic_schema(schema):
parser = PydanticOutputParser(
pydantic_object=schema
)
else:
parser = JsonOutputParser()
if include_raw:
return RunnableMap(raw=llm) | parser_with_fallback
return llm | parser

区别在于:

tool calling 模式
输出在 AIMessage.tool_calls 中
provider-native 模式
输出通常在 message content 或供应商标准字段中

具体取决于集成包适配。

为什么 with_structured_output 与 tool calling 有关系#

因为很多模型没有原生 structured output,但支持 tool calling。

此时 LangChain 会把:

class TravelIntent(BaseModel):
...

转成一个“虚拟工具”:

{
"type": "function",
"function": {
"name": "TravelIntent",
"description": "...",
"parameters": {
"type": "object",
"properties": {
"destination": {"type": "string"},
"days": {"type": "integer"}
}
}
}
}

模型返回:

AIMessage(
content="",
tool_calls=[
{
"name": "TravelIntent",
"args": {
"destination": "东京",
"days": 5,
},
"id": "call_...",
"type": "tool_call",
}
],
)

然后 parser 读取 tool call args,构造:

TravelIntent(...)

这就是:

结构化输出
=
工具调用协议的一种特殊用法

注意:这里的“工具”不是业务工具,不会执行真实 Python 函数。

它只是把结构化对象伪装成模型要调用的 schema。


运行时总结、伪代码与源码证据#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def parse_generation(generation, schema=None):
text = generation.text.strip()
parsed = parse_json_markdown(text)
return validate_with_schema(parsed, schema) if schema else parsed

源码证据:


9. 关键分支、异常与边界#

本章解释 partial JSON、校验失败、retry、repair 与策略选型边界。

解析失败:异常在哪里抛出#

JSON 格式错误#

模型输出:

{
"destination": "东京",
"days": 5,

缺少右括号。

链路:

JsonOutputParser.parse_result
parse_json_markdown
JSONDecodeError
OutputParserException

最终抛出:

OutputParserException("Invalid json output: ...")

Pydantic 校验错误#

模型输出:

{
"destination": "东京",
"days": -5,
"preferences": "美食",
"need_clarification": false
}

问题:

days 小于最小值
preferences 应该是 list[str],不是 str

链路:

PydanticOutputParser.parse_result
JsonOutputParser.parse_result
dict
TravelIntent.model_validate(dict)
ValidationError
OutputParserException

tool calling structured output 解析失败#

模型返回 tool call:

{
"name": "TravelIntent",
"args": {
"days": "five"
},
"id": "call_001",
"type": "tool_call",
}

链路:

AIMessage.tool_calls
PydanticToolsParser.parse_result
TravelIntent.model_validate(args)
ValidationError
OutputParserException / ValidationError 包装

include_raw 捕获错误#

如果:

structured_model = model.with_structured_output(
TravelIntent,
include_raw=True,
)

解析失败不会直接丢掉 raw。

返回形态:

{
"raw": AIMessage(...),
"parsed": None,
"parsing_error": OutputParserException(...),
}

这非常适合 Agent 节点:

parsed is not None
写入 Agent State
parsed is None
进入 repair node / retry node / human fallback

retry / repair 机制#

三种修复思路#

结构化输出失败后,常见策略有三种:

Retry
重新调用同一个链路,让模型重新生成。
Repair
把坏输出 + 错误信息交给模型,让它修复为合法结构。
Fallback
换模型、换策略或转人工。

Runnable 级 with_retry#

structured_model = (
model.with_structured_output(TravelIntent)
.with_retry(stop_after_attempt=3)
)

心智模型:

invoke
模型调用或 parser 抛异常
Runnable retry 捕获
重新执行整个 Runnable

优点:

  • 简单;
  • 适合偶发格式错误;
  • 能重跑模型。

缺点:

  • 可能重复消耗 token;
  • 如果 prompt / schema 本身有问题,重试也没用;
  • 对有副作用的链路要谨慎。

结构化输出节点通常是纯模型调用,可以重试。

工具写入节点不应该随意重试。

OutputFixingParser#

传统方式:

from langchain_classic.output_parsers import OutputFixingParser
fixing_parser = OutputFixingParser.from_llm(
parser=parser,
llm=model,
)

心智模型:

parser.parse(bad_output)
失败
把 bad_output + format_instructions 交给修复 LLM
得到 fixed_output
再次 parser.parse(fixed_output)

适合:

JSON 少了逗号
多了 Markdown 包装
字段名略有偏差
输出带了额外解释

不适合:

事实错误
业务判断错误
缺少关键信息
模型不知道该填什么

RetryOutputParser / RetryWithErrorOutputParser#

传统方式:

RetryOutputParser
使用原 prompt + 坏输出,让模型重新生成。
RetryWithErrorOutputParser
额外把 parser error 也传给模型。

区别:

OutputFixingParser
更像“修补已有输出”。
RetryOutputParser
更像“基于原始任务重新回答”。
RetryWithErrorOutputParser
更像“带错误反馈重新回答”。

include_raw + 自定义 repair node#

在 LangGraph 中更推荐显式建节点:

extract_intent_node
parsed 成功?
├─ 是:进入 planner_node
└─ 否:进入 repair_intent_node

示例状态:

class TravelState(TypedDict):
user_request: str
intent_result: IntentResult | None
raw_intent_output: Any | None
parser_error: str | None
retry_count: int

节点伪代码:

def extract_intent_node(state):
result = structured_model.invoke(
state["user_request"]
)
if result["parsed"] is not None:
return {
"intent_result": result["parsed"],
"raw_intent_output": result["raw"],
"parser_error": None,
}
return {
"intent_result": None,
"raw_intent_output": result["raw"],
"parser_error": str(result["parsing_error"]),
}

路由:

def route_after_intent(state):
if state["intent_result"] is not None:
return "planner"
if state["retry_count"] < 2:
return "repair_intent"
return "human_handoff"

这比隐藏在 parser 里的自动修复更利于:

  • trace;
  • debugging;
  • 失败样本沉淀;
  • 回归测试;
  • 线上问题定位。

Parser 放在 Chain 里,还是用模型原生 structured output?#

选择优先级#

推荐优先级:

1. 供应商原生 structured output
2. with_structured_output + tool calling
3. PydanticOutputParser
4. JsonOutputParser
5. StrOutputParser + 手写解析

什么时候用 with_structured_output#

适合:

意图识别
槽位抽取
计划生成
分类判断
安全判断
责任归因
需要强类型对象
生产核心链路

理由:

  • Schema 约束更靠近模型调用;
  • 返回对象直接可用;
  • 可配合 provider-native 或 tool calling;
  • 不必手动写格式指令;
  • 更适合与 Agent Runtime 集成。

什么时候用 PydanticOutputParser#

适合:

模型不支持 structured output
需要完全控制 prompt
需要自定义格式指令
兼容旧代码
离线或本地模型能力弱

也适合学习源码,因为它能清楚展示:

文本 → JSON → Pydantic 对象

什么时候用 JsonOutputParser#

适合:

临时结构化输出
结构较简单
需要 streaming partial JSON
不想定义 Pydantic Model
前端逐步渲染 JSON

什么时候用 StrOutputParser#

适合:

最终自然语言回复
摘要
改写
翻译
解释
不需要结构化字段的任务

不适合:

Agent 内部控制节点
高风险决策节点
工具参数生成
状态更新

为什么不要过度依赖 Parser 修复#

如果一个节点频繁解析失败,通常说明:

Schema 设计太复杂
字段描述不清楚
Prompt 任务不聚焦
模型能力不够
输入上下文太乱
输出类型不适合一次生成

不要只靠 retry 堆成功率,应回到:

拆节点
简化 Schema
增加字段描述
减少上下文噪声
改用 provider-native structured output
增加评估集

分支语义总结、伪代码与源码证据#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def parse_with_recovery(result, partial=False):
try:
return parser.parse_result(result, partial=partial)
except OutputParserException as error:
return retry_or_repair(result, error)

源码证据:


10. 扩展机制与框架协作#

本章解释 structured output 与 Tool Calling、Agent State、节点模板和调试入口的协作。

与 Tool Calling 的关系#

相同点#

两者都依赖:

Schema
JSON Schema
Pydantic
模型生成结构化参数
本地校验

不同点#

维度Tool CallingStructured Output
目的请求外部能力返回结构化结果
模型输出AIMessage.tool_callsPydantic / dict
是否执行函数通常执行真实工具通常不执行业务工具
是否需要 ToolMessagetool strategy 下可能内部生成
典型对象BaseToolStructuredToolPydanticOutputParserPydanticToolsParser
下游工具执行器业务状态 / API 返回
风险工具误用、副作用解析失败、字段错误

tool calling structured output 是“虚拟工具调用”#

with_structured_output(TravelIntent) 在 tool calling 策略下近似等价于:

定义一个名为 TravelIntent 的虚拟工具
参数就是 TravelIntent 的字段
强制模型调用这个工具
解析工具参数
返回 Pydantic 对象

但不会执行:

def TravelIntent(...):
...

它只借用了工具调用的结构化参数能力。

为什么这比自然语言 JSON 稳定#

因为模型在 tool calling 模式下通常会被供应商 API 约束为:

必须以 tool_call 格式返回参数

而不是自由生成一段文本。

这减少了:

多余解释
Markdown 包装
JSON 拼写错误
字段外文本

但仍不能完全消除:

字段语义错误
枚举选错
数字不合理
缺少业务条件

所以仍需要 Pydantic 和业务校验。


Agent State 中如何使用结构化输出#

不要把 raw text 当作内部状态#

不推荐:

state["intent"] = "用户想去东京玩 5 天,喜欢美食"

推荐:

state["intent_result"] = IntentResult(
destination="东京",
days=5,
preferences=["美食", "城市漫步"],
need_clarification=False,
)

原因:

  • 后续节点可直接读取字段;
  • 可测试;
  • 可序列化;
  • 可回放;
  • 可验证;
  • 可做 failure taxonomy。

State 字段建议#

class TravelState(TypedDict):
user_request: str
intent_result: IntentResult | None
travel_plan: TravelPlan | None
search_query: SearchQuery | None
reflection_result: ReflectionResult | None
final_response: FinalResponse | None
raw_outputs: dict[str, Any]
parser_errors: dict[str, str]
retry_count: dict[str, int]

节点输出建议#

每个结构化节点返回 partial state update:

def intent_node(state: TravelState):
result = intent_model.invoke(
state["user_request"]
)
return {
"intent_result": result,
}

若使用 include_raw=True

def intent_node(state: TravelState):
result = intent_model.invoke(
state["user_request"]
)
return {
"intent_result": result["parsed"],
"raw_outputs": {
**state.get("raw_outputs", {}),
"intent": result["raw"],
},
"parser_errors": {
**state.get("parser_errors", {}),
"intent": (
str(result["parsing_error"])
if result["parsing_error"]
else ""
),
},
}

结构化输出节点的测试方式#

每个节点至少测试:

正常输入
缺少目的地
缺少天数
多偏好
冲突偏好
极端天数
非旅行请求
中英文混合
模型输出字段缺失
模型输出类型错误

不要只看端到端 demo。


旅行规划助手结构化输出设计#

IntentResult#

Schema#
from typing import Literal
from pydantic import BaseModel, Field, model_validator
class IntentResult(BaseModel):
intent: Literal[
"travel_planning",
"budget_planning",
"attraction_search",
"route_optimization",
"weather_query",
"general_question",
"unsupported",
] = Field(description="用户主要意图")
destination: str | None = Field(
default=None,
description="目的地,未知时为 None"
)
days: int | None = Field(
default=None,
ge=1,
le=30,
description="旅行天数,未知时为 None"
)
preferences: list[str] = Field(
default_factory=list,
description="用户偏好,如美食、城市漫步、亲子、历史"
)
constraints: list[str] = Field(
default_factory=list,
description="用户约束,如预算、老人同行、不能太累"
)
need_clarification: bool = Field(
description="是否需要追问"
)
clarification_questions: list[str] = Field(
default_factory=list,
description="需要追问的问题"
)
confidence: float = Field(
ge=0,
le=1,
description="意图识别置信度"
)
@model_validator(mode="after")
def validate_clarification(self):
if self.need_clarification and not self.clarification_questions:
raise ValueError(
"need_clarification=True 时必须给出 clarification_questions"
)
return self
字段说明#
字段含义是否必填校验规则
intent用户主意图Literal 枚举
destination目的地未知为 None
days天数1—30
preferences偏好list[str]
constraints约束list[str]
need_clarification是否追问bool
clarification_questions追问问题need_clarification=True 时不能为空
confidence置信度0—1
失败样例#
{
"intent": "旅行",
"destination": "东京",
"days": "五天",
"preferences": "美食",
"need_clarification": false,
"confidence": 1.2
}

问题:

intent 不在枚举里
days 应是 int
preferences 应是 list[str]
confidence 超过 1
使用场景#
Router 节点
槽位补全
是否追问用户
是否进入 Planner
是否进入 Agent State#

TravelPlan#

Schema#
from typing import Literal
from pydantic import BaseModel, Field
class Activity(BaseModel):
name: str = Field(description="活动名称")
type: Literal[
"food",
"attraction",
"shopping",
"transport",
"rest",
"hotel",
"other",
] = Field(description="活动类型")
location: str | None = Field(
default=None,
description="地点"
)
estimated_duration_hours: float = Field(
ge=0.25,
le=12,
description="预计耗时,单位小时"
)
reason: str = Field(description="推荐理由")
class DayPlan(BaseModel):
day_index: int = Field(ge=1, description="第几天")
theme: str = Field(description="当天主题")
activities: list[Activity] = Field(
min_length=1,
description="当天活动列表"
)
notes: list[str] = Field(
default_factory=list,
description="当天注意事项"
)
class TravelPlan(BaseModel):
destination: str = Field(description="目的地")
days: int = Field(ge=1, le=30, description="总天数")
plan: list[DayPlan] = Field(description="每日计划")
assumptions: list[str] = Field(
default_factory=list,
description="规划假设"
)
missing_information: list[str] = Field(
default_factory=list,
description="缺失信息"
)
字段说明#
字段含义是否必填校验规则
destination目的地str
days总天数1—30
plan每日计划list[DayPlan]
assumptions规划假设list[str]
missing_information缺失信息list[str]
失败样例#
{
"destination": "东京",
"days": 5,
"plan": "第一天浅草寺,第二天涩谷"
}

问题:

plan 应该是 list[DayPlan],不是字符串
进一步业务校验#

可以增加:

len(plan) == days
day_index 连续
每天活动总时长不超过阈值
交通活动之间地理上合理

这类校验可以写在:

model_validator
独立 verifier node
使用场景#
Planner 节点
路线优化节点
最终回复节点
是否进入 Agent State#

SearchQuery#

Schema#
from typing import Literal
from pydantic import BaseModel, Field
class SearchQuery(BaseModel):
query: str = Field(description="检索查询语句")
city: str | None = Field(
default=None,
description="限定城市"
)
category: Literal[
"attraction",
"food",
"weather",
"transport",
"hotel",
"policy",
"general",
] = Field(description="检索类别")
top_k: int = Field(
default=5,
ge=1,
le=20,
description="召回数量"
)
filters: dict[str, str | int | float | bool] = Field(
default_factory=dict,
description="结构化过滤条件"
)
need_realtime: bool = Field(
description="是否需要实时信息"
)
字段说明#
字段含义是否必填校验规则
query检索语句非空
city城市str / None
category类别Literal
top_k召回数量1—20
filters过滤条件dict
need_realtime是否实时bool
失败样例#
{
"query": "",
"category": "玩",
"top_k": 100,
"need_realtime": "是"
}

问题:

query 为空
category 不在枚举
top_k 超出范围
need_realtime 应为 bool
使用场景#
Tool-use 节点
RAG Query Rewrite 节点
搜索工具参数生成
是否进入 Agent State#

ReflectionResult#

Schema#
from typing import Literal
from pydantic import BaseModel, Field
class ReflectionIssue(BaseModel):
issue_type: Literal[
"missing_slot",
"unreasonable_route",
"budget_conflict",
"time_conflict",
"preference_mismatch",
"safety_risk",
"format_error",
"other",
] = Field(description="问题类型")
description: str = Field(description="问题描述")
severity: Literal["low", "medium", "high"] = Field(
description="严重程度"
)
suggested_fix: str = Field(description="修复建议")
class ReflectionResult(BaseModel):
passed: bool = Field(description="是否通过检查")
risk_level: Literal["low", "medium", "high"] = Field(
description="整体风险等级"
)
issues: list[ReflectionIssue] = Field(
default_factory=list,
description="发现的问题"
)
should_replan: bool = Field(description="是否需要重新规划")
should_ask_user: bool = Field(description="是否需要追问用户")
字段说明#
字段含义是否必填校验规则
passed是否通过bool
risk_level风险等级low/medium/high
issues问题列表list[ReflectionIssue]
should_replan是否重规划bool
should_ask_user是否追问bool
失败样例#
{
"passed": "yes",
"risk_level": "还行",
"issues": [
{
"issue_type": "路线不合理",
"severity": "严重"
}
]
}

问题:

passed 应为 bool
risk_level 不在枚举
issue_type 不在枚举
issue 缺少 description 和 suggested_fix
severity 不在枚举
使用场景#
Reflection 节点
Verifier 节点
是否进入 revise / replan / ask_user 路由
是否进入 Agent State#

FinalResponse#

Schema#
from pydantic import BaseModel, Field
class FinalResponse(BaseModel):
answer: str = Field(description="最终给用户的自然语言回复")
summary: str = Field(description="简短摘要")
follow_up_questions: list[str] = Field(
default_factory=list,
description="可选追问"
)
used_sources: list[str] = Field(
default_factory=list,
description="使用的信息来源或工具名称"
)
warnings: list[str] = Field(
default_factory=list,
description="风险提示或限制说明"
)
字段说明#
字段含义是否必填校验规则
answer用户可读答案str
summary摘要str
follow_up_questions后续追问list[str]
used_sources来源 / 工具list[str]
warnings风险提示list[str]
失败样例#
{
"answer": ["第一天去浅草寺"],
"summary": null
}

问题:

answer 应为 str
summary 应为 str
缺少默认字段时需定义 default_factory
使用场景#
最终回复节点
API Response
前端渲染
日志记录
是否进入 Agent State#
通常不必须

说明:

FinalResponse 可以作为接口返回;
若需要会话回放或质量评估,也可以写入 State / Trace。

结构化输出节点模板#

节点通用写法#

def structured_node(
state: TravelState,
model,
schema,
input_builder,
state_key: str,
):
structured_model = model.with_structured_output(
schema,
include_raw=True,
)
result = structured_model.invoke(
input_builder(state)
)
parsed = result["parsed"]
raw = result["raw"]
error = result["parsing_error"]
if parsed is None:
return {
state_key: None,
"parser_errors": {
**state.get("parser_errors", {}),
state_key: str(error),
},
"raw_outputs": {
**state.get("raw_outputs", {}),
state_key: raw,
},
}
return {
state_key: parsed,
"parser_errors": {
**state.get("parser_errors", {}),
state_key: "",
},
"raw_outputs": {
**state.get("raw_outputs", {}),
state_key: raw,
},
}

Router 节点示例#

def intent_node(state: TravelState):
result = intent_model.invoke(
state["user_request"]
)
return {
"intent_result": result,
}

条件路由#

def route_after_intent(state: TravelState):
intent = state["intent_result"]
if intent is None:
return "repair_intent"
if intent.need_clarification:
return "ask_user"
if intent.intent == "travel_planning":
return "planner"
if intent.intent == "attraction_search":
return "search"
return "unsupported"

这体现了结构化输出的工程价值:

LLM 输出对象
代码稳定读取字段
条件边可测试
Agent 控制流稳定

源码级调试断点#

Parser 调试#

langchain_core/output_parsers/base.py
- BaseGenerationOutputParser.invoke
- BaseOutputParser.parse_result
langchain_core/output_parsers/string.py
- StrOutputParser.parse
langchain_core/output_parsers/json.py
- JsonOutputParser.parse_result
- JsonOutputParser.parse
- JsonOutputParser.get_format_instructions
langchain_core/output_parsers/pydantic.py
- PydanticOutputParser._parse_obj
- PydanticOutputParser.parse_result
- PydanticOutputParser.get_format_instructions

观察变量:

input
result
result[0].text
partial
text
json_object
pydantic_object
ValidationError
OutputParserException

with_structured_output 调试#

langchain_core/language_models/chat_models.py
- BaseChatModel.with_structured_output
具体供应商包:
- langchain_openai/chat_models/base.py
- langchain_anthropic/chat_models.py
- langchain_google_genai/chat_models.py
- langchain_ollama/chat_models.py
- langchain_mistralai/chat_models.py

观察变量:

schema
method
include_raw
strict
is_pydantic_schema
llm
output_parser
tool_choice
structured_output_format

Tool Parser 调试#

langchain_core/output_parsers/openai_tools.py
- JsonOutputToolsParser
- JsonOutputKeyToolsParser
- PydanticToolsParser

观察:

AIMessage.tool_calls
tool_call["name"]
tool_call["args"]
first_tool_only
key_name
Pydantic model_validate

Function Calling Schema 调试#

langchain_core/utils/function_calling.py
- convert_to_json_schema
- convert_to_openai_tool
- convert_to_openai_function

观察:

Pydantic schema
TypedDict schema
JSON schema dict
OpenAI tool schema
strict

使用 inspect 查看源码#

import inspect
from langchain_core.output_parsers import (
StrOutputParser,
JsonOutputParser,
PydanticOutputParser,
)
print(inspect.getsource(StrOutputParser))
print(inspect.getsource(JsonOutputParser))
print(inspect.getsource(PydanticOutputParser))

查看包位置:

import langchain_core.output_parsers.json
print(langchain_core.output_parsers.json.__file__)

查看版本:

from importlib.metadata import version
print(version("langchain-core"))
print(version("langchain"))

扩展协作总结、伪代码与源码证据#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def structured_node(state):
parsed = structured_model.invoke(state["messages"])
return {"structured_response": parsed}

源码证据:


11. 工程决策与适用场景#

Schema 名称要语义清晰#

推荐:

IntentResult
TravelPlan
SearchQuery
ReflectionResult
FinalResponse

不推荐:

Result
Output
Data
Info
ModelResponse

模型会看到 Schema 名称,名称本身会影响输出。

Field description 要写给模型看#

弱描述:

days: int

强描述:

days: int | None = Field(
default=None,
ge=1,
le=30,
description="旅行天数;如果用户没有明确说明,则返回 None"
)

用枚举替代自由文本#

不推荐:

risk_level: str

推荐:

risk_level: Literal["low", "medium", "high"]

用 default_factory 避免可变默认值#

preferences: list[str] = Field(
default_factory=list
)

不要写:

preferences: list[str] = []

Optional 字段明确默认值#

推荐:

destination: str | None = None

或:

destination: str | None = Field(default=None)

在 State 里保存 parsed,不要只保存 raw#

state["intent_result"] = parsed

而不是:

state["intent_raw"] = raw.content

高风险节点加 Verifier#

例如:

责任判断
退款决策
取消订单
费用估算
安全判断

即使结构化输出通过 Pydantic,也应该加业务校验。

给结构化输出写回归测试#

每个 Schema 至少准备:

10 条正常样本
10 条边界样本
10 条错误样本
5 条 adversarial 样本

评估:

parse success rate
field accuracy
business validation pass rate
retry count
latency
cost

12. 常见误区与源码纠正#

误区:Prompt 写清楚就能保证 JSON 正确#

纠正:

Prompt 是软约束;
Parser / Pydantic 是硬校验;
Provider-native structured output 是更强约束。

误区:JsonOutputParser 能保证字段正确#

纠正:

JsonOutputParser 只保证能解析 JSON;
不保证字段存在、类型正确、业务有效。

误区:PydanticOutputParser 能保证模型理解正确#

纠正:

Pydantic 只能验证结构;
不能保证语义判断一定正确。

例如:

用户说“不要太累”
模型仍可能规划每天 10 个景点。

这需要:

Reflection / Verifier / 业务规则

误区:with_structured_output 一定使用原生结构化输出#

纠正:

取决于模型供应商和 method;
可能是 provider-native;
也可能是 tool calling;
也可能由集成包 fallback。

误区:结构化输出和 Tool Calling 是两件完全无关的事#

纠正:

tool calling structured output
本质上借用了工具调用参数 schema 来生成结构化对象。

误区:所有 parser failure 都应该自动修复#

纠正:

解析失败可能是模型格式问题;
也可能是输入缺失;
也可能是 Schema 设计错误;
也可能是业务约束冲突。

应分类处理。

误区:最终用户回复也必须 Pydantic#

纠正:

最终回复如果只是自然语言,StrOutputParser 或 AIMessage 即可;
但如果前端需要结构化卡片,可以用 FinalResponse。

误区:结构化输出越复杂越好#

纠正:

复杂 Schema 会增加:

  • 模型负担;
  • 解析失败率;
  • token 成本;
  • 调试难度;
  • 版本兼容成本。

建议:

一个节点只做一个明确结构化任务。

13. 最终心智模型与掌握检查#

OutputParser 链路#

AIMessage / text
OutputParser.invoke
Generation / ChatGeneration
parse_result
parse(text)
str / dict / Pydantic object

JsonOutputParser 链路#

text
parse_json_markdown
dict / list
失败则 OutputParserException

PydanticOutputParser 链路#

text
JsonOutputParser
dict
Pydantic model_validate
Pydantic object

with_structured_output 链路#

Schema
model.with_structured_output
provider-native 或 tool calling
output parser
Pydantic object / dict

Agent 节点链路#

用户输入 / 当前 State
结构化输出模型
Pydantic Result
写入 Agent State
Conditional Edge 读取字段路由

一句话总结:

OutputParser 是把模型输出接入工程系统的“结构化边界层”;with_structured_output 则把这个边界进一步前移到模型调用协议中。生产级 Agent 不应该依赖自然语言字符串驱动控制流,而应该让关键节点输出可校验、可测试、可回放的结构化对象。


14. 参考资料与下一篇衔接#

官方资料#

官方文档#

  1. LangChain Structured Output https://docs.langchain.com/oss/python/langchain/structured-output

  2. LangChain Models:Structured Output https://docs.langchain.com/oss/python/langchain/models

  3. LangChain Messages https://docs.langchain.com/oss/python/langchain/messages

  4. LangChain Tools https://docs.langchain.com/oss/python/langchain/tools

API Reference#

  1. Output Parsers https://reference.langchain.com/python/langchain-core/output_parsers

  2. StrOutputParser https://reference.langchain.com/python/langchain-core/output_parsers/string/StrOutputParser

  3. JsonOutputParser https://reference.langchain.com/python/langchain-core/output_parsers/json/JsonOutputParser

  4. PydanticOutputParser https://reference.langchain.com/python/langchain-core/output_parsers/pydantic/PydanticOutputParser

  5. BaseChatModel.with_structured_output https://reference.langchain.com/python/langchain-core/language_models/chat_models/BaseChatModel/with_structured_output

  6. OutputFixingParser https://reference.langchain.com/python/langchain-classic/output_parsers/fix/OutputFixingParser

  7. RetryOutputParser https://reference.langchain.com/python/langchain-classic/output_parsers/retry/RetryOutputParser

官方源码#

  1. output_parsers/base.py https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/base.py

  2. output_parsers/string.py https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/string.py

  3. output_parsers/json.py https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/json.py

  4. output_parsers/pydantic.py https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/pydantic.py

  5. output_parsers/openai_tools.py https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/openai_tools.py

  6. language_models/chat_models.py https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/language_models/chat_models.py

  7. utils/function_calling.py https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/utils/function_calling.py

  8. agents/structured_output.py https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/langchain/langchain/agents/structured_output.py


下一篇衔接#

完成本篇后,你已经理解:

模型输出
字符串
JSON
Pydantic 对象
Agent State

下一篇可以继续进入:

第 4 篇:Tool Calling 源码解剖

重点回答:

@tool 如何把 Python 函数包装成 BaseTool?
函数签名如何变成 args_schema?
model.bind_tools 如何把工具描述绑定到 ChatModel?
模型为什么只生成 ToolCall,而不直接执行函数?
BaseTool.invoke 如何校验参数并调用真实函数?
ToolMessage 如何通过 tool_call_id 回填模型上下文?
LangChain Core 源码解剖:OutputParser 与结构化输出
https://jupiter-ws.cn/posts/agent-frameworks/03_langchain_core_structured_output_source_deep_dive/
作者
Jupiter
发布于
2026-03-07
许可协议
CC BY-NC-SA 4.0