LangChain Core 源码学习路线:第 3 篇 OutputParser 与结构化输出源码解剖
核心问题: 模型输出如何从自然语言或 ToolCall 转换为业务代码可安全消费的结构化对象?
源码主线:
AIMessage / Generation → OutputParser → JSON → Pydantic → structured_response前置文章: 第 1 篇《Runnable 源码解剖》、第 2 篇《Prompt、Message 与 ChatModel 源码解剖》
依赖基线:
langchain-core==1.4.8、langchain==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 源码中的实现方式。
重点理解:
StrOutputParserJsonOutputParserPydanticOutputParserBaseOutputParserBaseGenerationOutputParserBaseTransformOutputParserBaseCumulativeTransformOutputParserOutputParserExceptionwith_structured_outputPydantic schemaJSON Schemaschema validationparser failureretry / repairinclude_rawProviderStrategyToolStrategyJsonOutputKeyToolsParserPydanticToolsParser
完成本篇后,你应该能回答:
StrOutputParser为什么可以接在prompt | model后面?JsonOutputParser如何从模型输出中提取 JSON?PydanticOutputParser如何在 JSON 解析后再做字段校验?- Parser 抛出的异常是什么?异常在哪里抛出?
with_structured_output()是否等价于PydanticOutputParser?with_structured_output()底层如何绑定 Schema?- Pydantic Schema 如何转换成 JSON Schema?
- 结构化输出和 Tool Calling 有什么关系?
- Provider-native structured output 和 tool calling structured output 有什么区别?
- Parser 应该放在 Chain 里,还是依赖模型原生 structured output?
- 解析失败时应该 retry、repair、fallback,还是直接失败?
- 在 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, Fieldfrom 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 ChatPromptTemplatefrom 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_fallbackPydanticOutputParser 执行流程
AIMessage / Generation ↓取出 text ↓JsonOutputParser.parse_result ↓parse_json_markdown ↓得到 dict ↓PydanticOutputParser._parse_obj ↓TravelIntent.model_validate(dict) ↓返回 TravelIntentJsonOutputParser 执行流程
模型输出文本 ↓去除 Markdown 代码块 ↓定位 JSON 对象 ↓json.loads / parse_partial_json ↓返回 dict/list ↓失败则抛 OutputParserExceptionStrOutputParser 执行流程
AIMessage ↓ChatGeneration ↓Generation.text / message.content ↓parse(text) ↓返回 text 本身StrOutputParser 是最薄的一层 parser,基本不做结构化校验。
4. 源码地图、关键文件与阅读顺序
核心目录:
langchain_core/output_parsers/langchain_core/language_models/chat_models.pylangchain_core/utils/function_calling.pylangchain_core/output_parsers/openai_tools.pylangchain/agents/structured_output.py重点文件:
libs/core/langchain_core/output_parsers/base.pylibs/core/langchain_core/output_parsers/string.pylibs/core/langchain_core/output_parsers/json.pylibs/core/langchain_core/output_parsers/pydantic.pylibs/core/langchain_core/output_parsers/openai_tools.pylibs/core/langchain_core/language_models/chat_models.pylibs/core/langchain_core/utils/function_calling.pylibs/langchain/langchain/agents/structured_output.py重点类与函数:
BaseLLMOutputParserBaseGenerationOutputParserBaseOutputParserBaseTransformOutputParserBaseCumulativeTransformOutputParser
StrOutputParserJsonOutputParserPydanticOutputParser
OutputParserException
BaseChatModel.with_structured_outputBaseChatModel.bind_tools
JsonOutputToolsParserJsonOutputKeyToolsParserPydanticToolsParser
convert_to_json_schemaconvert_to_openai_toolconvert_to_openai_function
ProviderStrategyToolStrategy推荐阅读顺序:
1. output_parsers/base.py2. output_parsers/string.py3. output_parsers/json.py4. output_parsers/pydantic.py5. output_parsers/openai_tools.py6. language_models/chat_models.py::BaseChatModel.with_structured_output7. utils/function_calling.py8. agents/structured_output.py5. 对象模型、继承关系与协议边界
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_retrywith_fallbacksconfigcallbackstagsmetadataBaseLLMOutputParser
这一层更接近“从 LLM 生成结果解析”:
class BaseLLMOutputParser(Generic[T], ABC):
@abstractmethod def parse_result( self, result: list[Generation], *, partial: bool = False, ) -> T: ...它面对的是:
list[Generation]而不是直接面对字符串。
为什么是 list?
因为 LLM 调用可能返回多个候选 generation:
n=1n=3n=5Parser 通常只取第一个候选:
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 可以同时处理:
AIMessagestrGeneration的原因。
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]: ...核心参数:
| 参数 | 含义 |
|---|---|
schema | Pydantic / 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 BaseModelTypedDictDataclassJSON Schema dictOpenAI tool schemaPython 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源码证据:
- https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/language_models/chat_models.py
- https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/utils/function_calling.py
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(...))AIMessageAIMessageChunkstr所以源码不是简单:
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.contentAIMessage.response_metadataAIMessage.usage_metadataAIMessage.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 一定大于 0risk_level 一定属于枚举preferences 不为空这些应该交给:
PydanticOutputParser或with_structured_output(PydanticModel)或业务层 validatorPydanticOutputParser 源码解剖
用途
PydanticOutputParser 的职责是:
在 JSON 解析之后,用 Pydantic Model 做字段级、类型级、业务级校验,并返回 Pydantic 对象。
适合:
意图识别槽位抽取计划生成反思结果安全判定最终结构化响应尤其适合你在旅行规划助手中使用的节点:
IntentResultTravelPlanSearchQueryReflectionResultFinalResponse对象继承
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 BaseModelPydantic 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) ↓TravelIntentwith_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]: ...核心参数:
| 参数 | 含义 |
|---|---|
schema | Pydantic / 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源码证据:
- https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/json.py
- https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/pydantic.py
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 ↓OutputParserExceptiontool 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 fallbackretry / 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 output2. with_structured_output + tool calling3. PydanticOutputParser4. JsonOutputParser5. 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)源码证据:
- https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/base.py
- https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/langchain/langchain/output_parsers/retry.py
10. 扩展机制与框架协作
本章解释 structured output 与 Tool Calling、Agent State、节点模板和调试入口的协作。
与 Tool Calling 的关系
相同点
两者都依赖:
SchemaJSON SchemaPydantic模型生成结构化参数本地校验不同点
| 维度 | Tool Calling | Structured Output |
|---|---|---|
| 目的 | 请求外部能力 | 返回结构化结果 |
| 模型输出 | AIMessage.tool_calls | Pydantic / dict |
| 是否执行函数 | 通常执行真实工具 | 通常不执行业务工具 |
| 是否需要 ToolMessage | 是 | tool strategy 下可能内部生成 |
| 典型对象 | BaseTool、StructuredTool | PydanticOutputParser、PydanticToolsParser |
| 下游 | 工具执行器 | 业务状态 / 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 Literalfrom 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 应是 intpreferences 应是 list[str]confidence 超过 1使用场景
Router 节点槽位补全是否追问用户是否进入 Planner是否进入 Agent State
是TravelPlan
Schema
from typing import Literalfrom 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) == daysday_index 连续每天活动总时长不超过阈值交通活动之间地理上合理这类校验可以写在:
model_validator或独立 verifier node使用场景
Planner 节点路线优化节点最终回复节点是否进入 Agent State
是SearchQuery
Schema
from typing import Literalfrom 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 Literalfrom 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 应为 boolrisk_level 不在枚举issue_type 不在枚举issue 缺少 description 和 suggested_fixseverity 不在枚举使用场景
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 应为 strsummary 应为 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观察变量:
inputresultresult[0].textpartialtextjson_objectpydantic_objectValidationErrorOutputParserExceptionwith_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观察变量:
schemamethodinclude_rawstrictis_pydantic_schemallmoutput_parsertool_choicestructured_output_formatTool Parser 调试
langchain_core/output_parsers/openai_tools.py - JsonOutputToolsParser - JsonOutputKeyToolsParser - PydanticToolsParser观察:
AIMessage.tool_callstool_call["name"]tool_call["args"]first_tool_onlykey_namePydantic model_validateFunction Calling Schema 调试
langchain_core/utils/function_calling.py - convert_to_json_schema - convert_to_openai_tool - convert_to_openai_function观察:
Pydantic schemaTypedDict schemaJSON schema dictOpenAI tool schemastrict使用 inspect 查看源码
import inspectfrom 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}源码证据:
- https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/langchain/langchain/agents/structured_output.py
- https://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/openai_tools.py
11. 工程决策与适用场景
Schema 名称要语义清晰
推荐:
IntentResultTravelPlanSearchQueryReflectionResultFinalResponse不推荐:
ResultOutputDataInfoModelResponse模型会看到 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 ratefield accuracybusiness validation pass rateretry countlatencycost12. 常见误区与源码纠正
误区: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 objectJsonOutputParser 链路
text ↓parse_json_markdown ↓dict / list ↓失败则 OutputParserExceptionPydanticOutputParser 链路
text ↓JsonOutputParser ↓dict ↓Pydantic model_validate ↓Pydantic objectwith_structured_output 链路
Schema ↓model.with_structured_output ↓provider-native 或 tool calling ↓output parser ↓Pydantic object / dictAgent 节点链路
用户输入 / 当前 State ↓结构化输出模型 ↓Pydantic Result ↓写入 Agent State ↓Conditional Edge 读取字段路由一句话总结:
OutputParser 是把模型输出接入工程系统的“结构化边界层”;
with_structured_output则把这个边界进一步前移到模型调用协议中。生产级 Agent 不应该依赖自然语言字符串驱动控制流,而应该让关键节点输出可校验、可测试、可回放的结构化对象。
14. 参考资料与下一篇衔接
官方资料
官方文档
-
LangChain Structured Output https://docs.langchain.com/oss/python/langchain/structured-output
-
LangChain Models:Structured Output https://docs.langchain.com/oss/python/langchain/models
-
LangChain Messages https://docs.langchain.com/oss/python/langchain/messages
-
LangChain Tools https://docs.langchain.com/oss/python/langchain/tools
API Reference
-
Output Parsers https://reference.langchain.com/python/langchain-core/output_parsers
-
StrOutputParser https://reference.langchain.com/python/langchain-core/output_parsers/string/StrOutputParser
-
JsonOutputParser https://reference.langchain.com/python/langchain-core/output_parsers/json/JsonOutputParser
-
PydanticOutputParser https://reference.langchain.com/python/langchain-core/output_parsers/pydantic/PydanticOutputParser
-
BaseChatModel.with_structured_output https://reference.langchain.com/python/langchain-core/language_models/chat_models/BaseChatModel/with_structured_output
-
OutputFixingParser https://reference.langchain.com/python/langchain-classic/output_parsers/fix/OutputFixingParser
-
RetryOutputParser https://reference.langchain.com/python/langchain-classic/output_parsers/retry/RetryOutputParser
官方源码
-
output_parsers/base.pyhttps://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/base.py -
output_parsers/string.pyhttps://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/string.py -
output_parsers/json.pyhttps://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/json.py -
output_parsers/pydantic.pyhttps://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/pydantic.py -
output_parsers/openai_tools.pyhttps://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/output_parsers/openai_tools.py -
language_models/chat_models.pyhttps://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/language_models/chat_models.py -
utils/function_calling.pyhttps://github.com/langchain-ai/langchain/blob/83e824922ac9bf3bf882347fecabd09614bccc37/libs/core/langchain_core/utils/function_calling.py -
agents/structured_output.pyhttps://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 回填模型上下文?