前文已经介绍了消息和提示词模板。前面的示例向模型发送问题后,得到的主要还是一段自然语言。
自然语言适合人阅读,但程序更希望得到稳定的数据。例如,程序需要的天气信息可能是:
{
"city": "北京",
"condition": "晴天",
"temperature_c": 26
}
有了固定字段以后,代码可以直接读取 city、condition 和 temperature_c,不需要从一整段文字中猜测哪一部分是城市、天气或温度。
本文从最简单的 StrOutputParser 开始,依次介绍 JSON、Pydantic、列表、XML 和 with_structured_output(),最后再看 LangChain 如何解析工具调用格式中的结构化参数。
输出解析器和结构化输出都能得到程序可读取的数据,但它们介入模型调用的位置并不相同:

上半部分先让模型正常生成内容,再由解析器转换结果;下半部分在调用模型之前绑定 Schema,让模型服务按照指定协议返回结构化数据。后面的示例都围绕这两条路径展开。
1. 为什么需要输出解析
1.1 聊天模型默认返回 AIMessage
使用 ChatOpenAI.invoke() 调用聊天模型时,返回的不是普通字符串,而是一个 AIMessage:
message = llm.invoke("天空为什么是蓝色的?")
print(type(message).__name__)
print(message.content)
AIMessage 不只保存回答文本,还能保存令牌用量、结束原因、工具调用等信息,因此比普通字符串更完整。
但是,如果当前程序只需要显示或保存回答文本,就还要读取:
text = message.content
输出解析器就是用来完成这类转换的组件。
1.2 程序需要明确的数据类型
同一份模型回答,可以根据后续用途转换成不同类型:
- 普通回答可以转换成 str。
- 多个项目可以转换成 list。
- 固定字段可以转换成 dict。
- 需要类型校验时可以转换成 Pydantic 对象。
- XML 文本可以转换成嵌套字典。
这就是输出解析器最主要的作用:把模型返回的内容转换成程序更容易继续处理的数据。
2. 输出解析器和结构化输出不是一回事
这两个概念很容易混淆。
输出解析器的过程是:
提示词 -> 模型生成文本 -> 输出解析器转换文本
例如:
chain = prompt | llm | JsonOutputParser()
with_structured_output() 的过程则是:
先把 Schema 绑定到模型 -> 模型按照指定结构生成结果 -> LangChain 返回结构化对象
例如:
structured_llm = llm.with_structured_output(Weather)
两种方式都能得到结构化结果,但区别很重要:
- 输出解析器主要依靠提示词约束模型输出,然后在模型返回后进行解析。
- with_structured_output() 会使用模型接口提供的 JSON 或工具调用能力。
- 模型服务不支持某种结构化协议时,with_structured_output() 可能失败。
- 输出解析器更加通用,但提示词不清楚时也可能解析失败。
3. 运行环境
本文代码位于:
/path/to/llm-learning/langchain/p06_structured_output/
九个示例文件如下:
p06_structured_output/
├── 01_str_output_parser.py
├── 02_json_output_parser.py
├── 03_pydantic_output_parser.py
├── 04_list_and_xml_parsers.py
├── 05_with_structured_output_pydantic.py
├── 06_structured_output_schema_types.py
├── 07_include_raw_and_validation.py
├── 08_bind_tools_and_tool_parsers.py
└── 09_structured_output_methods.py
进入项目并激活 Python 3.12 虚拟环境:
cd /path/to/llm-learning
source .venv_langchain/bin/activate
python -m pip install -r requirements.txt
除了列表和 XML 示例,其余脚本都要连接本地 Qwen3。使用前面文章验证过的命令启动服务:
"$VIRTUAL_ENV/bin/python" -m mlx_lm server \
--model Qwen3-14B-AWQ-4bit-MLX \
--host 127.0.0.1 \
--port 18080 \
--chat-template-args '{"enable_thinking": false}'
另开一个终端检查服务是否已经就绪:
curl http://127.0.0.1:18080/v1/models
运行脚本时还可能看到:
PyTorch was not found. Models won't be available and only tokenizers, configuration and file/data utilities can be used.
本文通过 HTTP 连接独立运行的 MLX-LM Server,不使用 PyTorch 加载模型,因此这条提示不影响运行结果。
4. 解析器 1:StrOutputParser
StrOutputParser 是最简单的输出解析器。它会提取 AIMessage.content,返回普通 str。
代码文件为 01_str_output_parser.py:
# 这个文件演示如何把本地模型返回的 AIMessage 转换成普通字符串。
# 运行前需要先在项目根目录启动 mlx_lm.server。
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# 连接已经启动的本地 Qwen3 OpenAI 兼容接口。
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="Qwen3-14B-AWQ-4bit-MLX",
temperature=0,
max_tokens=64,
)
# 创建聊天提示词模板,问题会在运行时填入 question 变量。
prompt = ChatPromptTemplate.from_messages(
[
("system", "请用一句简短的话回答用户问题。"),
("human", "{question}"),
]
)
question = {"question": "天空为什么是蓝色的?"}
print("方式一:不使用输出解析器 chain = prompt | llm")
chain_without_parser = prompt | llm
message = chain_without_parser.invoke(question)
print("返回类型:", type(message).__name__) # AIMessage
print("返回对象:", message.content)
print("方式二:使用 StrOutputParser chain = prompt | llm | StrOutputParser()")
chain_with_parser = prompt | llm | StrOutputParser()
text = chain_with_parser.invoke(question)
print("返回类型:", type(text).__name__) # str
print("返回对象:", repr(text))
# 方式二也可以拆成两步理解:先拿 AIMessage,再 parser.invoke(message)
parser = StrOutputParser()
text_from_message = parser.invoke(message)
print("分步解析结果与管道结果一致:", text_from_message == text)
运行:
python langchain/p06_structured_output/01_str_output_parser.py
真实输出:
方式一:不使用输出解析器 chain = prompt | llm
返回类型: AIMessage
返回对象: 因为太阳光在大气中散射,蓝光散射最多,所以天空呈现蓝色。
方式二:使用 StrOutputParser chain = prompt | llm | StrOutputParser()
返回类型: str
返回对象: '因为太阳光在大气中散射,蓝光散射最多,所以天空呈现蓝色。'
分步解析结果与管道结果一致: True
定义解析器时要写括号:
parser = StrOutputParser()
下面这种写法得到的是类本身,不是解析器实例:
parser = StrOutputParser
把类本身放入管道时,LangChain 无法按照预期执行解析。
5. 解析器 2:JsonOutputParser
当程序需要固定字段时,JSON 是非常常见的中间格式。
使用 JsonOutputParser 时,提示词至少要说明三件事:
- 只返回合法 JSON。
- 不要返回解释文字。
- JSON 中需要哪些字段。
代码文件为 02_json_output_parser.py:
# 这个文件演示如何让本地模型输出 JSON,并解析成 Python 字典。
# 运行前需要先在项目根目录启动 mlx_lm.server。
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="Qwen3-14B-AWQ-4bit-MLX",
temperature=0,
max_tokens=128,
)
# 提示词必须明确要求模型只返回合法 JSON,并写清字段名称。
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"只返回合法 JSON,不要输出解释或 Markdown 代码块。"
"JSON 必须包含 city、condition、temperature_c,next 四个字段。 其中 next 表示继续向用户询问, 如是否想继续知道某个城市的天气?",
),
("human", "请整理下面的天气信息:{weather_text}"),
]
)
# JsonOutputParser 会把模型生成的 JSON 文本转换成 Python 字典。
chain = prompt | llm | JsonOutputParser()
result = chain.invoke({"weather_text": "北京今天晴天,气温 26 摄氏度。"})
print("返回类型:", type(result).__name__)
print("完整结果:", result)
print("--------------------------------")
print("城市:", result["city"])
print("天气:", result["condition"])
print("温度:", result["temperature_c"])
print("是否继续:", result["next"])
运行:
python langchain/p06_structured_output/02_json_output_parser.py
真实输出:
返回类型: dict
完整结果: {'city': '北京', 'condition': '晴天', 'temperature_c': 26, 'next': '是否想继续知道广州的天气?'}
--------------------------------
城市: 北京
天气: 晴天
温度: 26
是否继续: 是否想继续知道广州的天气?
模型生成的是 JSON 文本,JsonOutputParser 解析后得到 Python dict。因此可以使用:
result["city"]
直接读取字段。
如果模型在 JSON 前后增加解释、使用错误引号或生成不完整 JSON,解析就可能失败。即使解析器能够处理部分常见的 Markdown JSON 形式,也不应该依赖模型自由发挥,提示词中仍应明确要求只返回 JSON。
6. 解析器 3:PydanticOutputParser
字典可以保存固定字段,但下面的字典仍然存在问题:
{
"city": "上海",
"temperature_c": "很热"
}
程序希望 temperature_c 是整数,但字典本身不会主动检查。Pydantic 可以定义字段类型并在运行时校验数据。
代码文件为 03_pydantic_output_parser.py:
# 这个文件演示如何使用 PydanticOutputParser 返回经过校验的对象。
# 运行前需要先在项目根目录启动 mlx_lm.server。
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="Qwen3-14B-AWQ-4bit-MLX",
temperature=0,
max_tokens=128,
)
# Pydantic 模型定义程序希望得到的字段和字段类型。
class Weather(BaseModel):
"""一条天气信息。"""
city: str = Field(description="城市名称")
condition: str = Field(description="天气情况")
temperature_c: int = Field(description="摄氏温度,使用整数")
next: str = Field(description="是否继续向用户询问, 如是否想继续知道另一个城市的天气")
# 解析器会根据 Weather 生成格式说明,并校验最终结果。
parser = PydanticOutputParser[Weather](pydantic_object=Weather)
prompt = ChatPromptTemplate.from_messages(
[
("system", "根据格式要求整理信息。\n{format_instructions}"),
("human", "{weather_text}"),
]
).partial(format_instructions=parser.get_format_instructions())
chain = prompt | llm | parser
result = chain.invoke({"weather_text": "上海今天多云,气温 24 摄氏度。"})
print("返回类型:", type(result).__name__)
print("完整结果:", result)
print("城市:", result.city)
print("天气:", result.condition)
print("温度:", result.temperature_c)
print("是否继续:", result.next)
运行:
python langchain/p06_structured_output/03_pydantic_output_parser.py
真实输出:
返回类型: Weather
完整结果: city='上海' condition='多云' temperature_c=24 next='是否想继续知道北京的天气?'
城市: 上海
天气: 多云
温度: 24
是否继续: 是否想继续知道北京的天气?
6.1 Field 的作用
Field(description=…) 不只是给开发者看的注释。get_format_instructions() 会根据 Pydantic 模型生成 JSON Schema 格式说明,字段描述会帮助模型理解每个字段应该填写什么。
format_instructions = parser.get_format_instructions()
再通过 partial() 把格式说明预先填入提示词:
prompt = prompt.partial(format_instructions=format_instructions)
这样每次调用时只需要传入真正变化的天气文字。
6.2 PydanticOutputParser 与 JsonOutputParser 的区别
JsonOutputParser 返回字典:
{"city": "上海", "temperature_c": 24}
PydanticOutputParser 返回对象:
Weather(city="上海", temperature_c=24)
对象字段可以通过点号访问:
result.city
result.temperature_c
更重要的是,Pydantic 会检查必填字段和字段类型。
7. 解析器 4:列表解析器与 XML 解析器
不是所有结构化结果都需要 JSON 或 Pydantic。对于简单列表,可以使用专门的列表解析器。
代码文件为 04_list_and_xml_parsers.py:
# 这个文件演示列表解析器和 XML 解析器,不需要启动本地模型服务。
from langchain_core.output_parsers import (
CommaSeparatedListOutputParser, # 逗号解析器
MarkdownListOutputParser, # Markdown 解析器
NumberedListOutputParser, # 编号解析器
XMLOutputParser, # XML 解析器
)
# 逗号分隔列表会被转换成 Python list。
comma_parser = CommaSeparatedListOutputParser()
comma_result = comma_parser.invoke("苹果, 香蕉, 橙子")
print("逗号列表:", comma_result) # 逗号列表: ['苹果', '香蕉', '橙子']
# Markdown 无序列表会去掉每一行开头的短横线。
markdown_parser = MarkdownListOutputParser()
markdown_result = markdown_parser.invoke("- 苹果\n- 香蕉\n- 橙子")
print("Markdown 列表:", markdown_result) # Markdown 列表: ['苹果', '香蕉', '橙子']
# 编号列表会去掉数字编号,只保留每一项的内容。
numbered_parser = NumberedListOutputParser()
numbered_result = numbered_parser.invoke("1. 苹果\n2. 香蕉\n3. 橙子")
print("编号列表:", numbered_result) #编号列表: ['苹果', '香蕉', '橙子']
# XMLOutputParser 需要 defusedxml,用于安全地解析 XML 文本。
xml_parser = XMLOutputParser()
xml_text = "<weather><city>北京</city><condition>晴天</condition></weather>"
xml_result = xml_parser.invoke(xml_text)
print("XML 结果:", xml_result) # XML 结果: {'weather': [{'city': '北京'}, {'condition': '晴天'}]}
运行:
python langchain/p06_structured_output/04_list_and_xml_parsers.py
真实输出:
逗号列表: ['苹果', '香蕉', '橙子']
Markdown 列表: ['苹果', '香蕉', '橙子']
编号列表: ['苹果', '香蕉', '橙子']
XML 结果: {'weather': [{'city': '北京'}, {'condition': '晴天'}]}
三个列表解析器都返回 list[str],区别只在于输入文本的格式。
XMLOutputParser 需要额外安装 defusedxml:
python -m pip install defusedxml==0.7.1
如果没有安装,会看到类似错误:
ImportError: defusedxml is not installed.
8. 把 Schema 绑定到模型with_structured_output
前面的解析器都位于模型之后:
chain = prompt | llm | parser
with_structured_output() 则直接给模型绑定一个 Schema:
structured_llm = llm.with_structured_output(WeatherReport)
调用 structured_llm.invoke() 时,LangChain 会根据模型接口支持的方式传递 Schema,再把返回值转换成指定结构。
代码文件为 05_with_structured_output_pydantic.py:
# 这个文件演示使用 with_structured_output 和 Pydantic 获取结构化结果。
# 运行前需要先在项目根目录启动 mlx_lm.server。
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="Qwen3-14B-AWQ-4bit-MLX",
temperature=0,
max_tokens=256,
)
# 嵌套模型用于描述温度数值和单位。
class Temperature(BaseModel):
"""温度信息。"""
value: int = Field(description="温度数值")
unit: str = Field(description="温度单位")
# 主模型中包含普通字段、嵌套对象、列表和可选字段。
class WeatherReport(BaseModel):
"""从一段文字中提取天气信息。"""
city: str = Field(description="城市名称")
condition: str = Field(description="天气情况")
temperature: Temperature = Field(description="温度信息")
suggestions: list[str] = Field(description="出行建议列表")
warning: str | None = Field(description="预警信息,没有预警时填写 null")
next: str = Field(description="继续向用户询问的提示, 如是否想继续知道上海的天气? 但城市需要是一个指定城市但不能是上海")
# 本地 Qwen3 可以通过 function_calling 返回符合 Pydantic 的参数。
structured_llm = llm.with_structured_output(
WeatherReport,
method="function_calling",
)
result = structured_llm.invoke(
"广州今天小雨,气温 23 摄氏度。出门建议带伞并穿防滑鞋,没有天气预警。"
)
print("返回类型:", type(result).__name__)
print("完整结果:", result)
print("城市:", result.city)
print("温度:", result.temperature.value, result.temperature.unit)
print("出行建议:", result.suggestions)
print("预警信息:", result.warning)
print("是否继续:", result.next)
运行:
python langchain/p06_structured_output/05_with_structured_output_pydantic.py
真实输出:
返回类型: WeatherReport
完整结果: city='广州' condition='小雨' temperature=Temperature(value=23, unit='摄氏度') suggestions=['带伞', '穿防滑鞋'] warning=None next='是否想继续知道深圳的天气?'
城市: 广州
温度: 23 摄氏度
出行建议: ['带伞', '穿防滑鞋']
预警信息: None
是否继续: 是否想继续知道深圳的天气?
这个例子同时包含了四种常见字段:
- city , condition,next 是普通字符串。
- temperature 是嵌套的 Temperature 对象。
- suggestions 是字符串列表。
- warning 可以是字符串,也可以是 None。
Schema 类和字段都应该写清楚说明。对于本地模型,类说明可以帮助模型理解这次结构化输出的目的,字段说明可以减少字段值填错的情况。
9. 四种 Schema 定义方式
LangChain 模型接口中常见的 Schema 定义方式有四种:
- Pydantic。
- TypedDict。
- JSON Schema。
- dataclass。
它们都可以传给 with_structured_output(),但返回类型和校验能力不同。
代码文件为 06_structured_output_schema_types.py:
# 这个文件比较 Pydantic、TypedDict、JSON Schema 和 dataclass 四种 Schema。
# 运行前需要先在项目根目录启动 mlx_lm.server。
from dataclasses import dataclass
from typing import Annotated
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing_extensions import TypedDict
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="Qwen3-14B-AWQ-4bit-MLX",
temperature=0,
max_tokens=128,
)
# json_mode 要求提示词明确说明输出 JSON 以及需要的字段。
question = (
"请只输出 JSON,从‘深圳今天晴天’中提取 city 和 condition 两个字段,"
"不要输出解释或 Markdown 代码块。"
)
print("=" * 60)
print("方式一:Pydantic BaseModel")
print("=" * 60)
pydantic_llm = llm.with_structured_output(WeatherModel, method="json_mode")
pydantic_result = pydantic_llm.invoke(question)
print("返回类型:", type(pydantic_result).__name__)
print("返回结果:", pydantic_result)
print("点号访问:", pydantic_result.city, pydantic_result.condition)
print()
输出:
============================================================
方式一:Pydantic BaseModel
============================================================
返回类型: WeatherModel
返回结果: city='深圳' condition='晴天'
点号访问: 深圳 晴天
# ---------------------------------------------------------------------------
# 方式三:JSON Schema 字典
# - 不依赖 Pydantic/TypedDict,手写标准 JSON Schema,跨语言、跨工具最通用
# - 字段写在 properties / required 里,适合动态生成或从 OpenAPI 复制
# - with_structured_output 返回 dict,用 result["city"] 访问
# ---------------------------------------------------------------------------
weather_json_schema = {
"title": "WeatherJson",
"description": "天气信息",
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"condition": {"type": "string", "description": "天气情况"},
},
"required": ["city", "condition"],
}
输出:
============================================================
方式二:TypedDict
============================================================
返回类型: dict
返回结果: {'city': '深圳', 'condition': '晴天'}
下标访问: 深圳 晴天
print("=" * 60)
print("方式三:JSON Schema")
print("=" * 60)
json_schema_llm = llm.with_structured_output(weather_json_schema, method="json_mode")
json_schema_result = json_schema_llm.invoke(question)
print("返回类型:", type(json_schema_result).__name__)
print("返回结果:", json_schema_result)
print("下标访问:", json_schema_result["city"], json_schema_result["condition"])
print()
输出:
============================================================
方式三:JSON Schema
============================================================
返回类型: dict
返回结果: {'city': '深圳', 'condition': '晴天'}
下标访问: 深圳 晴天
# ---------------------------------------------------------------------------
# 方式四:dataclass
# - 标准库 dataclass,得到带属性的对象;这里用 Field 只是为了补充字段描述
# - 校验能力弱于 Pydantic,但比裸 dict 更易 result.city 点号访问
# - with_structured_output 返回 WeatherData 实例
# ---------------------------------------------------------------------------
@dataclass
class WeatherData:
"""dataclass 格式的天气信息。"""
city: str = Field(description="城市名称")
condition: str = Field(description="天气情况")
print("=" * 60)
print("方式四:dataclass")
print("=" * 60)
dataclass_llm = llm.with_structured_output(WeatherData, method="json_mode")
dataclass_result = dataclass_llm.invoke(question)
print("返回类型:", type(dataclass_result).__name__)
print("返回结果:", dataclass_result)
print("点号访问:", dataclass_result.city, dataclass_result.condition)
真实输出:
============================================================
方式四:dataclass
============================================================
返回类型: dict
返回结果: {'city': '深圳', 'condition': '晴天'}
下标访问: 深圳 晴天
手动转成 dataclass 后点号访问: 深圳 晴天
运行
python langchain/p06_structured_output/06_structured_output_schema_types.py
从实际结果可以看到:
- Pydantic Schema 返回 WeatherModel 对象,并执行 Pydantic 运行时校验。
- TypedDict 返回 dict。
- JSON Schema 返回 dict。
- dataclass 在当前调用方式下也返回 dict,不是 WeatherData 实例。
如果需要明确的 Python 对象、字段访问和运行时校验,Pydantic 更适合。如果只需要轻量字典,TypedDict 或 JSON Schema 更直接。
10. 三种结构化输出方法
ChatOpenAI.with_structured_output() 支持三种 method:
method="json_schema"
method="json_mode"
method="function_calling"
三种方法的目标相同,但 Schema 进入模型请求的方式不同,服务端需要支持的协议也不同:

可以先用一张表理解它们的核心区别:
| method | Schema 如何进入请求 | 模型原始结果通常位于 | 主要约束 |
|---|---|---|---|
| json_schema | 作为原生 Structured Outputs 的 JSON Schema 传给服务端 | raw.content | 服务端按照 Schema 生成并校验结果 |
| json_mode | 只要求服务端返回 JSON 对象 | raw.content | 服务端保证 JSON 格式,字段要求主要依靠提示词 |
| function_calling | Schema 被转换成工具定义 | raw.tool_calls | 模型按照工具参数格式填写字段 |
下面使用同一个 Pydantic 模型比较三种方法。公共代码如下:
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
class Weather(BaseModel):
"""从一段文字中提取天气信息。"""
city: str = Field(description="城市名称")
condition: str = Field(description="天气情况")
temperature_c: int = Field(description="摄氏温度,使用整数")
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="Qwen3-14B-AWQ-4bit-MLX",
temperature=0,
max_tokens=128,
)
weather_text = "苏州今天多云,气温 25 摄氏度。"
10.1 json_schema
json_schema 使用模型提供商的原生 Structured Outputs API。LangChain 会把 Weather 转换成 JSON Schema,并通过请求的 response_format 发送给服务端。服务端不仅要返回合法 JSON,还要按照 Schema 生成字段、类型和必填项。
json_schema_llm = llm.with_structured_output(
Weather,
method="json_schema",
strict=True,
)
result = json_schema_llm.invoke(
f"从下面文字中提取天气信息:{weather_text}"
)
print(type(result).__name__)
print(result)
print(result.city)
这里的几个参数分别表示:
- Weather 决定最终需要的字段和类型。成功时返回经过校验的 Weather 对象。
- method=”json_schema” 明确使用服务端的原生结构化输出协议。
- strict=True 要求服务端严格遵循 Schema。使用严格模式时,应先确认模型提供商支持该参数以及所使用的 Schema 特性。
json_schema 的优点是字段约束由服务端执行,不必在用户提示词中再次罗列 city、condition 和 temperature_c。它适合明确支持原生 Structured Outputs 的模型服务。
不过,接口提供 /v1/chat/completions 路径,并不代表它实现了原生 Structured Outputs 的全部行为。当前 MLX-LM OpenAI 兼容服务不能正确完成该模式:实测时服务端返回了普通天气说明,客户端尝试把它解析成 Weather 时产生校验错误:
ValidationError: Invalid JSON: expected value at line 1 column 1
为了让三种方法的对照脚本在这里失败后还能继续运行,可以捕获 Pydantic 的 ValidationError:
from pydantic import ValidationError
try:
result = json_schema_llm.invoke(
f"从下面文字中提取天气信息:{weather_text}"
)
print(result)
except ValidationError as error:
print("调用结果:失败")
print("错误类型:", type(error).__name__)
print("错误原因:", error.errors()[0]["msg"])
这次失败发生在客户端处理服务端响应的阶段。即使设置 include_raw=True,也可能在 LangChain 组装包含 raw、parsed 和 parsing_error 的返回值之前抛出异常,所以不能把 include_raw 当成所有服务端兼容问题的异常捕获器。
10.2 json_mode
json_mode 会在请求中开启 JSON 对象模式,要求模型返回可解析的 JSON。与 json_schema 不同,它不要求服务端按照完整 Schema 约束每一个字段,因此提示词必须同时说明:
- 只能返回 JSON。
- 必须包含哪些字段。
- 不要添加解释或 Markdown 代码块。
json_mode_llm = llm.with_structured_output(
Weather,
method="json_mode",
include_raw=True,
)
result = json_mode_llm.invoke(
"只返回合法 JSON,不要输出解释或 Markdown 代码块。"
"JSON 必须包含 city、condition、temperature_c 三个字段,"
f"从下面文字中提取天气信息:{weather_text}"
)
print("raw.content =", repr(result["raw"].content))
print("parsed =", result["parsed"])
print("parsing_error =", result["parsing_error"])
实际输出为:
raw.content = '{\n "city": "苏州",\n "condition": "多云",\n "temperature_c": 25\n}'
parsed = city='苏州' condition='多云' temperature_c=25
parsing_error = None
这个过程包含两层处理:
- 模型服务保证 raw.content 是 JSON 对象。
- LangChain 再用 Weather 解析该 JSON,得到 Pydantic 对象。
因此,JSON 语法正确不等于字段一定正确。如果模型遗漏 temperature_c,或者返回的值无法转换成整数,第二层 Pydantic 校验仍然可能失败。
json_mode 的优势是协议简单,许多兼容服务都能支持;缺点是字段约束主要依赖提示词。Schema 越复杂,提示词越容易遗漏约束。本文第 9 章比较四种 Schema 时使用的就是这种方法。
10.3 function_calling
function_calling 会把 Weather 转换成一个工具定义,让模型不再把天气数据写进普通回答,而是把它填写成一次工具调用的参数。LangChain 从 tool_calls 中读取参数,再用 Weather 完成校验和对象转换。
这里并没有执行查询天气之类的真实函数。工具定义只是结构化数据的载体。
function_llm = llm.with_structured_output(
Weather,
method="function_calling",
include_raw=True,
)
result = function_llm.invoke(
f"从下面文字中提取天气信息:{weather_text}"
)
print("raw.content =", repr(result["raw"].content))
print("raw.tool_calls =", result["raw"].tool_calls)
print("parsed =", result["parsed"])
print("parsing_error =", result["parsing_error"])
实际输出为:
raw.content = ''
raw.tool_calls = [{'name': 'Weather', 'args': {'city': '苏州', 'condition': '多云', 'temperature_c': 25}, 'id': None, 'type': 'tool_call'}]
parsed = city='苏州' condition='多云' temperature_c=25
parsing_error = None
可以看到,raw.content 是空字符串,真正的数据在 raw.tool_calls[0][“args”] 中。平时直接使用 result[“parsed”] 即可;查看 raw.tool_calls 主要用于理解协议或排查问题。
function_calling 适合已经支持工具调用、但不支持原生 json_schema 的模型服务。本文第 8 章的 Pydantic 示例使用的也是这种方法,当前 Qwen3 与 MLX-LM Server 可以成功返回该格式。
10.4 应该选择哪一种方法
三种方法没有一种能够适用于所有模型服务,可以按下面的顺序选择:
- 模型提供商明确支持原生 Structured Outputs 时,优先使用 json_schema,它对字段和类型的约束最强。
- 服务只支持 JSON 对象模式,而且 Schema 较简单时,使用 json_mode,并在提示词中完整写出字段要求。
- 模型的工具调用能力较稳定,但原生 Structured Outputs 不可用时,使用 function_calling。
在本地或第三方 OpenAI 兼容服务中,最好不要省略 method 并依赖默认值,因为不同 LangChain 版本的默认选择和不同服务端的协议实现可能并不一致。可以先使用一个只有两三个字段的 Schema 做最小测试,再把验证成功的方法用于正式业务。
本文三种方法的完整对照代码位于 09_structured_output_methods.py,运行:
python langchain/p06_structured_output/09_structured_output_methods.py
11. 同时查看原始消息和解析结果include_raw
默认情况下,with_structured_output() 只返回解析后的对象。遇到解析失败时,我们可能还需要查看模型原始返回了什么。
设置:
include_raw=True
以后,结果中会包含三个字段:
- raw:原始 AIMessage。
- parsed:解析后的对象。
- parsing_error:解析异常,没有异常时为 None。
代码文件为 07_include_raw_and_validation.py:
# 这个文件演示 include_raw 的返回内容,以及 Pydantic 解析失败时的异常。
# 运行前需要先在项目根目录启动 mlx_lm.server。
from langchain_core.exceptions import OutputParserException
from langchain_core.output_parsers import PydanticOutputParser
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
class Weather(BaseModel):
"""从文字中提取天气信息。"""
city: str = Field(description="城市名称")
temperature_c: int = Field(description="摄氏温度,使用整数")
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="Qwen3-14B-AWQ-4bit-MLX",
temperature=0,
max_tokens=128,
)
# include_raw=True 会同时保留原始 AIMessage、解析结果和解析异常。
structured_llm = llm.with_structured_output(
Weather,
method="function_calling",
include_raw=True,
)
result = structured_llm.invoke("从‘杭州今天 28 摄氏度’中提取天气信息。")
print("最外层返回类型:", type(result).__name__)
print("包含的字段:", list(result.keys()))
print("raw 类型:", type(result["raw"]).__name__)
tool_call = result["raw"].tool_calls[0]
print("raw 工具名称:", tool_call["name"])
print("raw 工具参数:", tool_call["args"])
print("parsed:", result["parsed"])
print("parsing_error:", result["parsing_error"])
# 下面故意把温度写成无法转换为整数的文字,观察 Pydantic 校验错误。
parser = PydanticOutputParser(pydantic_object=Weather)
invalid_json = '{"city": "杭州", "temperature_c": "很热"}'
try:
parser.invoke(invalid_json)
except OutputParserException as error:
print("\n错误类型:", type(error).__name__)
print("错误原因:temperature_c 不能转换为整数")
运行:
python langchain/p06_structured_output/07_include_raw_and_validation.py
真实输出:
最外层返回类型: dict
包含的字段: ['raw', 'parsed', 'parsing_error']
raw 类型: AIMessage
raw 工具名称: Weather
raw 工具参数: {'city': '杭州', 'temperature_c': 28}
parsed: city='杭州' temperature_c=28
parsing_error: None
错误类型: OutputParserException
错误原因: Failed to parse Weather from completion {"city": "\u676d\u5dde", "temperature_c": "\u5f88\u70ed"}. Got: 1 validation error for Weather
temperature_c
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='很热', input_type=str]
For further information visit https://errors.pydantic.dev/2.11/v/int_parsing
For troubleshooting, visit: https://docs.langchain.com/oss/python/langchain/errors/OUTPUT_PARSING_FAILURE
include_raw=True 很适合调试。解析成功时可以同时看到原始工具参数和 Pydantic 对象;解析失败时,也能通过 raw 检查模型到底返回了什么。
第二部分没有调用模型,而是故意把字符串 “很热” 交给整数类型字段。PydanticOutputParser 捕获 Pydantic 校验错误后,将它包装成 LangChain 的 OutputParserException。
12. 工具调用解析器与bind_tools
结构化输出还可以复用工具调用格式。这里暂时不创建和执行真正的工具,只观察模型如何按照 Weather Schema 生成参数。
模型把结构化参数写入 AIMessage.tool_calls 后,三个解析器会从同一条消息中提取不同粒度的结果:

代码文件为 08_bind_tools_and_tool_parsers.py:
# 这个文件演示 bind_tools 返回工具调用参数,再用三种解析器读取参数。
# 本例只解析工具调用格式,不会执行任何工具。
from langchain_core.output_parsers import (
JsonOutputKeyToolsParser,
JsonOutputToolsParser,
PydanticToolsParser,
)
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="Qwen3-14B-AWQ-4bit-MLX",
temperature=0,
max_tokens=128,
)
class Weather(BaseModel):
"""从文字中提取天气信息。"""
city: str = Field(description="城市名称")
condition: str = Field(description="天气情况")
# tool_choice 指定本次必须使用 Weather 结构生成参数。
llm_with_tool = llm.bind_tools([Weather], tool_choice="Weather")
message = llm_with_tool.invoke("从‘成都今天阴天’中提取天气信息。")
print("AIMessage.content:", repr(message.content))
tool_call = message.tool_calls[0]
print("工具名称:", tool_call["name"])
print("工具参数:", tool_call["args"])
print("--------------------------------")
# 返回工具名称和 args 参数。
json_tools_parser = JsonOutputToolsParser(first_tool_only=True)
json_tools_result = json_tools_parser.invoke(message)
print("\nJsonOutputToolsParser:", json_tools_result)
print("--------------------------------")
# 只返回名称为 Weather 的 args 参数。
json_key_parser = JsonOutputKeyToolsParser(
key_name="Weather",
first_tool_only=True,
)
json_key_result = json_key_parser.invoke(message)
print("JsonOutputKeyToolsParser:", json_key_result)
print("--------------------------------")
# 把 args 参数校验并转换成 Weather 对象。
pydantic_tools_parser = PydanticToolsParser(
tools=[Weather],
first_tool_only=True,
)
pydantic_tools_result = pydantic_tools_parser.invoke(message)
print("PydanticToolsParser:", pydantic_tools_result)
print("--------------------------------")
运行:
python langchain/p06_structured_output/08_bind_tools_and_tool_parsers.py
真实输出:
AIMessage.content: ''
工具名称: Weather
工具参数: {'city': '成都', 'condition': '阴天'}
--------------------------------
JsonOutputToolsParser: {'args': {'city': '成都', 'condition': '阴天'}, 'type': 'Weather'}
--------------------------------
JsonOutputKeyToolsParser: {'city': '成都', 'condition': '阴天'}
--------------------------------
PydanticToolsParser: city='成都' condition='阴天'
--------------------------------
这次模型没有把结果放在 AIMessage.content 中,而是放在 AIMessage.tool_calls 中。
三个解析器读取的是同一条消息,但返回结果不同:
- JsonOutputToolsParser 保留工具名称和 args。
- JsonOutputKeyToolsParser 只返回指定工具的参数字典。
- PydanticToolsParser 把参数转换成 Pydantic 对象。
这里完成的只是“生成并解析工具参数”。真正执行工具、把工具结果交回模型以及 Agent 调度属于后面的内容,本文不展开。
13. 常见问题
13.1 本地请求返回 502
/v1/models 可以访问,只能证明 HTTP 服务已经启动并能返回模型列表,不能证明聊天推理一定成功。如果 /v1/chat/completions 返回 502,应该先查看启动 MLX-LM Server 的终端日志,检查模型加载、推理异常、启动虚拟环境和模型路径。
然后使用 curl 直接发送一条最小聊天请求:
curl http://127.0.0.1:18080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "Qwen3-14B-AWQ-4bit-MLX",
"messages": [{"role": "user", "content": "你好"}],
"max_tokens": 16
}'
如果这条请求也失败,问题在模型服务端;如果它成功而 Python 请求失败,再检查客户端参数和请求内容。
13.2 默认 with_structured_output 解析失败
当 with_structured_output() 通过 json_schema 执行时,MLX-LM OpenAI 兼容服务没有完整实现该协议。应根据实际用途明确指定:
method="json_mode"
或者:
method="function_calling"
不能因为 /v1/chat/completions 可以调用,就假设所有 OpenAI 扩展能力都可用。
13.3 JSON 解析失败
优先检查模型原始输出,并在提示词中明确:
只返回合法 JSON,不要输出解释或 Markdown 代码块。
同时写清楚字段名称和字段含义。
13.4 Pydantic 校验失败
常见原因包括:
- 必填字段缺失。
- 字段名称与 Schema 不一致。
- 字段类型无法转换。
- 模型返回了 Schema 之外的复杂结构。
可以先设置 include_raw=True 查看 raw 和 parsing_error。
13.5 XML 解析器提示缺少依赖
安装:
python -m pip install defusedxml==0.7.1
再重新运行 XML 示例。
14. 现代输出解析器
下面的清单只统计 langchain_core.output_parsers 中的现代解析器,不混入 langchain_classic 兼容层。
顶层模块的 all 一共导出 17 个名称。其中 SimpleJsonOutputParser 是 JsonOutputParser 的别名,不是另一套解析逻辑。
| 分类 | 解析器 | 作用 |
|---|---|---|
| 文本 | StrOutputParser | 从模型消息中提取普通字符串 |
| JSON | JsonOutputParser | 把 JSON 文本解析成字典 |
| JSON | SimpleJsonOutputParser | JsonOutputParser 的别名 |
| Pydantic | PydanticOutputParser | 把结果校验并转换成 Pydantic 对象 |
| XML | XMLOutputParser | 把 XML 文本解析成嵌套字典 |
| 列表 | CommaSeparatedListOutputParser | 解析逗号分隔列表 |
| 列表 | MarkdownListOutputParser | 解析 Markdown 无序列表 |
| 列表 | NumberedListOutputParser | 解析编号列表 |
| 列表抽象类 | ListOutputParser | 列表解析器的抽象基础类 |
| Tools | JsonOutputToolsParser | 解析 OpenAI tools 格式并保留工具信息 |
| Tools | JsonOutputKeyToolsParser | 只返回指定工具的参数 |
| Tools | PydanticToolsParser | 把工具参数转换成 Pydantic 对象 |
| 抽象基类 | BaseLLMOutputParser | 模型输出解析器的底层抽象类 |
| 抽象基类 | BaseGenerationOutputParser | 以 Generation 为输入的抽象类 |
| 抽象基类 | BaseOutputParser | 常规输出解析器的基础抽象类 |
| 流式抽象类 | BaseTransformOutputParser | 支持转换流式输入的抽象类 |
| 流式抽象类 | BaseCumulativeTransformOutputParser | 支持累计解析流式结果的抽象类 |
langchain_core.output_parsers.openai_functions 子模块还提供 5 个旧 OpenAI function-call 格式解析器:
| 解析器 | 作用 |
|---|---|
| OutputFunctionsParser | 读取旧 function_call 格式的函数调用结果 |
| JsonOutputFunctionsParser | 把函数参数解析成 JSON 对象 |
| JsonKeyOutputFunctionsParser | 从函数参数 JSON 中提取指定字段 |
| PydanticOutputFunctionsParser | 把函数参数转换成 Pydantic 对象 |
| PydanticAttrOutputFunctionsParser | 从 Pydantic 解析结果中提取指定属性 |
这些类处理的是较早的 function_call 消息格式。当前示例使用的是 tools / tool_calls 格式,因此对应使用 JsonOutputToolsParser、JsonOutputKeyToolsParser 和 PydanticToolsParser。
抽象基类主要用于自行开发解析器,入门时不需要直接实例化。常规使用时,先掌握 StrOutputParser、JsonOutputParser、PydanticOutputParser 和三个 Tools 解析器即可。
15. 小结
输出解析器解决的是“模型已经返回内容以后,程序怎样读取”的问题;with_structured_output() 解决的是“调用模型时,怎样要求模型按照 Schema 返回”的问题。
在本文的本地 Qwen3 环境中:
- 普通文本、JSON、Pydantic、列表和 XML 解析器都可以正常使用。
- with_structured_output(method=”json_mode”) 可以返回 JSON 结构。
- with_structured_output(method=”function_calling”) 可以返回 Pydantic 对象。
- 默认 json_schema 与当前 MLX-LM Server 不兼容,需要明确切换方法。
- bind_tools() 可以得到标准 tool_calls,并由三个 Tools 解析器转换成不同结果。
最终要点汇总如下:
| 功能 | 核心写法 | 返回类型 | 适合场景 |
|---|---|---|---|
| 提取文本 | StrOutputParser() | str | 只需要模型回答文字 |
| 解析 JSON | JsonOutputParser() | dict | 需要固定字段,但不需要对象校验 |
| 解析 Pydantic | PydanticOutputParser(…) | Pydantic 对象 | 需要字段类型校验 |
| 解析简单列表 | 三种 List Parser | list[str] | 模型返回项目列表 |
| 解析 XML | XMLOutputParser() | dict | 模型或接口返回 XML |
| 模型结构化输出 | with_structured_output(…) | 对象或 dict | 调用模型时直接绑定 Schema |
| 查看原始结果 | include_raw=True | 包含 raw、parsed、parsing_error 的字典 | 调试结构化输出 |
| 解析工具参数 | 三种 Tools Parser | 字典或 Pydantic 对象 | 读取 AIMessage.tool_calls |