LangChain 系列 6:输出解析器与结构化输出


前面已经学习了消息和提示词模板。前面的示例向模型发送问题后,得到的主要还是一段自然语言。

自然语言适合人阅读,但程序更希望得到稳定的数据。例如,程序需要的天气信息可能是:

{
  "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 时,提示词至少要说明三件事:

  1. 只返回合法 JSON。
  2. 不要返回解释文字。
  3. 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 定义方式有四种:

  1. Pydantic。
  2. TypedDict。
  3. JSON Schema。
  4. 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

文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录