前面已经学习了消息和提示词模板。前面的示例向模型发送问题后,得到的主要还是一段自然语言。
自然语言适合人阅读,但程序更希望得到稳定的数据。例如,程序需要的天气信息可能是:
{
"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. 运行环境
本章代码位于:
source/_posts/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
进入项目并激活 Python 3.12 虚拟环境:
cd ~/Documents/git/blog/source/_posts/llm_learning
source .venv/bin/activate
python -m pip install -r requirements.txt
本文实测使用的主要版本为:
langchain==1.0.3
langchain-core==1.0.2
langchain-openai==1.0.1
pydantic==2.11.10
defusedxml==0.7.1
除了列表和 XML 示例,其余脚本都要连接本地 Qwen3。使用前面文章验证过的命令启动服务:
"$VIRTUAL_ENV/bin/python" -m mlx_lm server \
--model model \
--host 127.0.0.1 \
--port 18080 \
--chat-template-args '{"enable_thinking": false}'
另开一个终端检查服务是否已经就绪:
curl --noproxy '*' 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. StrOutputParser:返回普通字符串
StrOutputParser 是最简单的输出解析器。它会提取 AIMessage.content,返回普通 str。
代码文件为 01_str_output_parser.py:
# 这个文件演示如何把本地模型返回的 AIMessage 转换成普通字符串。
# 运行前需要先在项目根目录启动 mlx_lm.server。
import httpx
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# 创建聊天提示词模板,问题会在运行时填入 question 变量。
prompt = ChatPromptTemplate.from_messages(
[
("system", "请用一句简短的话回答用户问题。"),
("human", "{question}"),
]
)
# 连接已经启动的本地 Qwen3 OpenAI 兼容接口。
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="default_model",
temperature=0,
max_tokens=64,
http_client=httpx.Client(trust_env=False),
)
# 不使用输出解析器时,模型返回 AIMessage。
message = (prompt | llm).invoke({"question": "天空为什么是蓝色的?"})
print("模型原始返回类型:", type(message).__name__)
print("模型原始内容:", message.content)
# StrOutputParser 会提取 AIMessage.content,返回普通字符串。
parser = StrOutputParser()
text = parser.invoke(message)
print("\n解析后的返回类型:", type(text).__name__)
print("解析后的内容:", text)
# 实际使用时可以用管道符把提示词、模型和解析器直接连接起来。
# 前面的 model 调用和 parser 调用,等价于执行下面这条完整链。
chain = prompt | llm | StrOutputParser()
print("\n完整链类型:", type(chain).__name__)
运行:
python langchain/p06_structured_output/01_str_output_parser.py
真实输出:
模型原始返回类型: AIMessage
模型原始内容: 天空呈现蓝色是因为空气分子对太阳光的散射作用,其中蓝光波长较短,更容易被散射到各个方向。
解析后的返回类型: str
解析后的内容: 天空呈现蓝色是因为空气分子对太阳光的散射作用,其中蓝光波长较短,更容易被散射到各个方向。
完整链类型: RunnableSequence
定义解析器时要写括号:
parser = StrOutputParser()
下面这种写法得到的是类本身,不是解析器实例:
parser = StrOutputParser
把类本身放入管道时,LangChain 无法按照预期执行解析。
5. JsonOutputParser:返回 Python 字典
当程序需要固定字段时,JSON 是非常常见的中间格式。
使用 JsonOutputParser 时,提示词至少要说明三件事:
- 只返回合法 JSON。
- 不要返回解释文字。
- JSON 中需要哪些字段。
代码文件为 02_json_output_parser.py:
# 这个文件演示如何让本地模型输出 JSON,并解析成 Python 字典。
# 运行前需要先在项目根目录启动 mlx_lm.server。
import httpx
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# 提示词必须明确要求模型只返回合法 JSON,并写清字段名称。
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"只返回合法 JSON,不要输出解释或 Markdown 代码块。"
"JSON 必须包含 city、condition、temperature_c 三个字段。",
),
("human", "请整理下面的天气信息:{weather_text}"),
]
)
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="default_model",
temperature=0,
max_tokens=128,
http_client=httpx.Client(trust_env=False),
)
# JsonOutputParser 会把模型生成的 JSON 文本转换成 Python 字典。
chain = prompt | llm | JsonOutputParser()
result = chain.invoke({"weather_text": "北京今天晴天,气温 26 摄氏度。"})
print("返回类型:", type(result).__name__)
print("完整结果:", result)
print("城市:", result["city"])
print("天气:", result["condition"])
print("温度:", result["temperature_c"])
运行:
python langchain/p06_structured_output/02_json_output_parser.py
真实输出:
返回类型: dict
完整结果: {'city': '北京', 'condition': '晴天', 'temperature_c': 26}
城市: 北京
天气: 晴天
温度: 26
模型生成的是 JSON 文本,JsonOutputParser 解析后得到 Python dict。因此可以使用:
result["city"]
直接读取字段。
如果模型在 JSON 前后增加解释、使用错误引号或生成不完整 JSON,解析就可能失败。即使解析器能够处理部分常见的 Markdown JSON 形式,也不应该依赖模型自由发挥,提示词中仍应明确要求只返回 JSON。
6. PydanticOutputParser:返回经过校验的对象
字典可以保存固定字段,但下面的字典仍然存在问题:
{
"city": "上海",
"temperature_c": "很热"
}
程序希望 temperature_c 是整数,但字典本身不会主动检查。Pydantic 可以定义字段类型并在运行时校验数据。
代码文件为 03_pydantic_output_parser.py:
# 这个文件演示如何使用 PydanticOutputParser 返回经过校验的对象。
# 运行前需要先在项目根目录启动 mlx_lm.server。
import httpx
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
# Pydantic 模型定义程序希望得到的字段和字段类型。
class Weather(BaseModel):
"""一条天气信息。"""
city: str = Field(description="城市名称")
condition: str = Field(description="天气情况")
temperature_c: int = Field(description="摄氏温度,使用整数")
# 解析器会根据 Weather 生成格式说明,并校验最终结果。
parser = PydanticOutputParser(pydantic_object=Weather)
prompt = ChatPromptTemplate.from_messages(
[
("system", "根据格式要求整理信息。\n{format_instructions}"),
("human", "{weather_text}"),
]
).partial(format_instructions=parser.get_format_instructions())
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="default_model",
temperature=0,
max_tokens=128,
http_client=httpx.Client(trust_env=False),
)
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)
运行:
python langchain/p06_structured_output/03_pydantic_output_parser.py
真实输出:
返回类型: Weather
完整结果: city='上海' condition='多云' temperature_c=24
城市: 上海
天气: 多云
温度: 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. 列表解析器和 XML 解析器
不是所有结构化结果都需要 JSON 或 Pydantic。对于简单列表,可以使用专门的列表解析器。
代码文件为 04_list_and_xml_parsers.py:
# 这个文件演示列表解析器和 XML 解析器,不需要启动本地模型服务。
from langchain_core.output_parsers import (
CommaSeparatedListOutputParser,
MarkdownListOutputParser,
NumberedListOutputParser,
XMLOutputParser,
)
# 逗号分隔列表会被转换成 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)
# 编号列表会去掉数字编号,只保留每一项的内容。
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)
运行:
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. with_structured_output:把 Schema 绑定到模型
前面的解析器都位于模型之后:
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。
import httpx
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
# 嵌套模型用于描述温度数值和单位。
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")
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="default_model",
temperature=0,
max_tokens=256,
http_client=httpx.Client(trust_env=False),
)
# 本地 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)
运行:
python langchain/p06_structured_output/05_with_structured_output_pydantic.py
真实输出:
返回类型: WeatherReport
完整结果: city='广州' condition='小雨' temperature=Temperature(value=23, unit='摄氏度') suggestions=['带伞', '穿防滑鞋'] warning=None
城市: 广州
温度: 23 摄氏度
出行建议: ['带伞', '穿防滑鞋']
预警信息: None
这个例子同时包含了四种常见字段:
- city 和 condition 是普通字符串。
- 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
import httpx
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing_extensions import TypedDict
class WeatherModel(BaseModel):
"""Pydantic 格式的天气信息。"""
city: str = Field(description="城市名称")
condition: str = Field(description="天气情况")
class WeatherDict(TypedDict):
"""TypedDict 格式的天气信息。"""
city: Annotated[str, "城市名称"]
condition: Annotated[str, "天气情况"]
# JSON Schema 使用普通字典描述字段和类型。
weather_json_schema = {
"title": "WeatherJson",
"description": "天气信息",
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"condition": {"type": "string", "description": "天气情况"},
},
"required": ["city", "condition"],
}
@dataclass
class WeatherData:
"""dataclass 格式的天气信息。"""
city: str = Field(description="城市名称")
condition: str = Field(description="天气情况")
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="default_model",
temperature=0,
max_tokens=128,
http_client=httpx.Client(trust_env=False),
)
# json_mode 要求提示词明确说明输出 JSON 以及需要的字段。
question = (
"请只输出 JSON,从‘深圳今天晴天’中提取 city 和 condition 两个字段,"
"不要输出解释或 Markdown 代码块。"
)
schemas = [
("Pydantic", WeatherModel),
("TypedDict", WeatherDict),
("JSON Schema", weather_json_schema),
("dataclass", WeatherData),
]
for schema_name, schema in schemas:
# 四种 Schema 使用相同的模型、方法和问题,便于比较返回类型。
structured_llm = llm.with_structured_output(schema, method="json_mode")
result = structured_llm.invoke(question)
print(f"{schema_name} 返回类型:{type(result).__name__}")
print(f"{schema_name} 返回结果:{result}\n")
运行:
python langchain/p06_structured_output/06_structured_output_schema_types.py
真实输出:
Pydantic 返回类型:WeatherModel
Pydantic 返回结果:city='深圳' condition='晴天'
TypedDict 返回类型:dict
TypedDict 返回结果:{'city': '深圳', 'condition': '晴天'}
JSON Schema 返回类型:dict
JSON Schema 返回结果:{'city': '深圳', 'condition': '晴天'}
dataclass 返回类型:dict
dataclass 返回结果:{'city': '深圳', 'condition': '晴天'}
从实际结果可以看到:
- Pydantic Schema 返回 WeatherModel 对象,并执行 Pydantic 运行时校验。
- TypedDict 返回 dict。
- JSON Schema 返回 dict。
- dataclass 在当前调用方式下也返回 dict,不是 WeatherData 实例。
如果需要明确的 Python 对象、字段访问和运行时校验,Pydantic 更适合。如果只需要轻量字典,TypedDict 或 JSON Schema 更直接。
10. 三种结构化输出方法
当前 langchain-openai==1.0.1 的 ChatOpenAI.with_structured_output() 支持三种 method:
method="json_schema"
method="json_mode"
method="function_calling"
三种方法的目标相同,但 Schema 进入模型请求的方式不同,服务端需要支持的协议也不同:

10.1 json_schema
json_schema 使用模型提供商的原生 Structured Output API。它不仅要求输出是 JSON,还要求模型服务真正理解并执行 JSON Schema 协议。
本文使用的 langchain-openai==1.0.1 默认值是:
method="json_schema"
但是,本文使用的 MLX-LM OpenAI 兼容服务不能正确完成该模式。实测时模型返回了普通文本,LangChain 随后产生 Pydantic JSON 校验错误:
ValidationError: Invalid JSON: expected value at line 1 column 1
这说明“接口路径兼容 OpenAI”不等于“支持 OpenAI 的全部结构化输出能力”。
10.2 json_mode
json_mode 要求模型返回合法 JSON,但不会自动把完整字段要求变成提示词。因此必须在问题中写清楚:
请只输出 JSON,包含 city 和 condition 两个字段,不要输出解释或 Markdown 代码块。
本文比较四种 Schema 时使用的就是:
structured_llm = llm.with_structured_output(
schema,
method="json_mode",
)
10.3 function_calling
function_calling 会把 Schema 转换成工具定义,让模型通过工具调用格式生成参数。LangChain 再从 tool_calls 中读取参数并转换成目标对象。
本文的 Pydantic 结构化输出使用:
structured_llm = llm.with_structured_output(
WeatherReport,
method="function_calling",
)
当前 Qwen3 模型和 MLX-LM Server 可以成功返回这种格式。
在本地兼容服务中,不建议省略 method 并假设默认模式一定可用。应根据服务端实际能力明确选择,并用一个最小 Schema 先验证。
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。
import httpx
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="default_model",
temperature=0,
max_tokens=128,
http_client=httpx.Client(trust_env=False),
)
# 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
错误原因:temperature_c 不能转换为整数
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 返回工具调用参数,再用三种解析器读取参数。
# 本例只解析工具调用格式,不会执行任何工具。
import httpx
from langchain_core.output_parsers import (
JsonOutputKeyToolsParser,
JsonOutputToolsParser,
PydanticToolsParser,
)
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
class Weather(BaseModel):
"""从文字中提取天气信息。"""
city: str = Field(description="城市名称")
condition: str = Field(description="天气情况")
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
model="default_model",
temperature=0,
max_tokens=128,
http_client=httpx.Client(trust_env=False),
)
# 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"])
# 返回工具名称和 args 参数。
json_tools_parser = JsonOutputToolsParser(first_tool_only=True)
json_tools_result = json_tools_parser.invoke(message)
print("\nJsonOutputToolsParser:", json_tools_result)
# 只返回名称为 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)
# 把 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)
运行:
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 --noproxy '*' http://127.0.0.1:18080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "default_model",
"messages": [{"role": "user", "content": "你好"}],
"max_tokens": 16
}'
如果这条请求也失败,问题在模型服务端;如果它成功而 Python 请求失败,再检查 Python HTTP 客户端是否读取了系统代理或 VPN 配置。
本文在 ChatOpenAI 中显式设置:
http_client=httpx.Client(trust_env=False)
它表示本地 HTTP 请求不读取系统代理环境变量,但它不能修复模型加载或推理过程中的服务端错误。
13.2 默认 with_structured_output 解析失败
本文使用的 langchain-openai==1.0.1 默认使用 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 1.0.2 的现代输出解析器
下面的清单以本机 langchain-core==1.0.2 为准,只统计现代 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 |