LangChain 03:结构化输出

前言

直接问模型,得到的是一段自然语言;而下游 API、表单或数据库写入要的是固定字段。
靠正则或关键词从回复里抠字段非常脆弱,模型换个措辞解析就失败。
结构化输出让模型按预定义 schema 返回数据,字段名、类型与含义都由代码约定。
LangChain 1.x 提供两条主要路径:Agent 场景把 Pydantic 模型传给 create_agent 的 response_format;单次调用场景用模型的 with_structured_output。
本文由同系列《LangChain 03:1.x Agent 教程》的结构化输出一节展开,配置与其保持一致:通过 OpenAI 兼容协议对接 火山方舟 Coding Plan。
环境安装与 init_chat_model 入门见同系列《LangChain 入门教程》,本文不再重复。
下文需要 Python 3.12+,依赖用 uv 管理;每个 Python 示例都是完整可运行脚本,复制到项目根目录执行 uv run python xxx.py 即可。

依赖

在项目目录声明 1.x 主包与 OpenAI 兼容集成(版本请按项目实际调整)。

1
uv add "langchain>=1.0,<2.0" langchain-openai langgraph python-dotenv rich

rich 用于在终端里更清楚地查看结构化结果:彩色高亮、自动缩进嵌套 dict / list / 模型对象,比内置 print 更适合调试。

在项目根目录创建 .env,写入 Coding Plan 的 API Key 与专用 Base URL,不要把 .env 提交进 Git。

1
2
OPENAI_API_KEY=你的火山方舟 API Key
OPENAI_BASE_URL=https://ark.cn-beijing.volces.com/api/coding/v3

示例脚本开头用 load_dotenv() 加载该文件。
langchain-openai 仍读取 OPENAI_* 环境变量;值必须来自 Coding Plan,请勿使用普通方舟 .../api/v3。
下文模型统一写 openai:ark-code-latest,也可在控制台改成具体模型名后同步替换代码。

响应模型

结构化输出的核心是先用 Pydantic 定义一个「响应模型」:继承 BaseModel,每个字段带类型注解,并用 Field(description=...) 说明含义。
字段描述会进入模型可见的 schema,写清楚「是什么、什么格式」,模型才能填得准。

基本字段

最常见的响应模型就是几个标量字段,下面以天气摘要为例。

1
2
3
4
5
6
from pydantic import BaseModel, Field

class WeatherBrief(BaseModel):
city: str = Field(description="城市名")
summary: str = Field(description="一句话天气摘要")
celsius: int = Field(description="气温(摄氏度)")

int 类型能顺带约束模型不要输出「25 度左右」这类模糊表述。
需要枚举取值时,用 Literal 把字段限制在固定选项内。

1
2
3
4
5
6
7
from typing import Literal

from pydantic import BaseModel, Field

class Intent(BaseModel):
action: Literal["weather", "math", "chat"] = Field(description="用户意图分类")
reason: str = Field(description="一句话判断依据")

嵌套与列表

真实业务的字段常有层级,Pydantic 模型可以直接嵌套,列表字段写 list[...]。

1
2
3
4
5
6
7
8
9
10
from pydantic import BaseModel, Field

class DailyForecast(BaseModel):
date: str = Field(description="日期,格式 YYYY-MM-DD")
celsius: int = Field(description="当日最高气温(摄氏度)")
condition: str = Field(description="天气现象,如晴、多云、小雨")

class WeekBrief(BaseModel):
city: str = Field(description="城市名")
days: list[DailyForecast] = Field(description="逐日预报列表")

解析后仍是类型化对象,brief.days[0].celsius 直接可读,不需要手工转换。

Agent 输出

response_format

create_agent 接受 response_format 参数,传入响应模型后,Agent 会在结束前把最终答案落成该结构的实例。
下面示例沿用天气工具:模型先调工具拿数据,再填进 WeatherBrief。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import os

from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from pydantic import BaseModel, Field
from rich import print as rprint

load_dotenv()

api_key = os.environ["OPENAI_API_KEY"]
base_url = os.environ["OPENAI_BASE_URL"]

class WeatherBrief(BaseModel):
city: str = Field(description="城市名")
summary: str = Field(description="一句话天气摘要")
celsius: int = Field(description="气温(摄氏度)")

@tool
def get_weather(city: str) -> str:
"""查询指定城市的天气(示例数据)。"""
return f"{city} 今天晴,25°C。"

model = init_chat_model(
"openai:ark-code-latest",
temperature=0,
api_key=api_key,
base_url=base_url,
)

agent = create_agent(
model=model,
tools=[get_weather],
response_format=WeatherBrief,
system_prompt="根据工具结果填写结构化天气摘要。",
)

result = agent.invoke({
"messages": [{"role": "user", "content": "给我上海的天气摘要"}]
})
brief: WeatherBrief = result["structured_response"]
rprint(brief.model_dump())

读取结果

结构化结果不在 messages 里,而是挂在返回值的 structured_response 键上,类型就是传入的响应模型。
brief.model_dump() 转成 dict 后可直接序列化;也可以直接访问属性,比如 brief.celsius。
下游 API、表单或数据库写入应优先读 structured_response,而不是再解析自然语言。

校验与重试

模型返回的数据会先过一遍 Pydantic 校验,类型不符或缺少必填字段时不会静默通过。
校验失败时框架会按策略重试,成功结果才写入 structured_response。
降低失败率的关键仍在响应模型本身:字段描述写清楚,取值范围用类型与约束表达。
例如气温字段可以加上范围约束,超出范围直接判为不合法。

1
2
3
4
5
6
from pydantic import BaseModel, Field

class WeatherBrief(BaseModel):
city: str = Field(description="城市名")
summary: str = Field(description="一句话天气摘要")
celsius: int = Field(ge=-50, le=55, description="气温(摄氏度)")

ge / le 是 Pydantic 的标准约束参数,分别表示大于等于与小于等于;阈值请按业务实际调整。

模型直调

with_structured_output

不是所有场景都需要 Agent 循环,单纯做分类、抽取时直接在模型上调用 with_structured_output 更轻。
它返回一个包装后的模型,invoke 的结果直接就是响应模型实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import os
from typing import Literal

from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
from rich import print as rprint

load_dotenv()

api_key = os.environ["OPENAI_API_KEY"]
base_url = os.environ["OPENAI_BASE_URL"]

class Sentiment(BaseModel):
label: Literal["positive", "negative", "neutral"] = Field(description="情感倾向")
reason: str = Field(description="一句话判断依据")

model = init_chat_model(
"openai:ark-code-latest",
temperature=0,
api_key=api_key,
base_url=base_url,
)

structured_llm = model.with_structured_output(Sentiment)

result = structured_llm.invoke("这耳机音质不错,但佩戴一小时耳朵就疼。")
rprint(result)
rprint(result.label)

场景选择

两条路径如何选,看任务是否需要工具与多步推理。

  1. 单次分类、抽取、打标签:用 with_structured_output,一次模型调用直接拿结果。
  2. 需要查数据、调接口再汇总:用 Agent + response_format,工具结果由模型整理进固定字段。
  3. 在 LangGraph 图内做路由或规划:同样用 with_structured_output 包一层,输出交给条件边分发,见同系列《LangGraph 15:工作流模式》。

验证

按下面顺序确认示例可用。

  1. 执行 uv run python -c "import importlib.metadata as m; print(m.version('langchain'))",主版本应为 1.x。
  2. 确认 .env 中 OPENAI_BASE_URL 为 https://ark.cn-beijing.volces.com/api/coding/v3。
  3. 跑「response_format」脚本,打印的 dict 应含 city / summary / celsius 字段。
  4. 把 rprint(brief.model_dump()) 改成 rprint(brief.celsius),应直接输出整数。
  5. 跑「with_structured_output」脚本,result.label 应是三个枚举值之一。
  6. 临时删掉 response_format 参数再运行 Agent 示例,返回值将不再含 structured_response 键,可对照体会该参数的作用。

总结

  1. 先用 Pydantic 定义响应模型,字段类型与 Field(description=...) 描述共同构成模型可见的 schema。
  2. Agent 场景把响应模型传给 create_agent 的 response_format,结果从 result["structured_response"] 读取。
  3. 校验失败时框架按策略重试;字段描述写清楚、必要时加范围约束,能降低失败率。
  4. 单次分类或抽取用 with_structured_output 更轻,不必为拿固定字段起一个完整 Agent。
  5. 下游系统一律消费结构化结果,不要回头解析自然语言。