【Langchain】8.聊天模型-结构化输出
8. 聊天模型-结构化输出
文章目录
大语言模型默认返回自然语言文本。但在实际开发中,我们往往需要结构化数据——对象、字典、JSON——以便程序直接解析和后续处理。聊天模型提供了
with_structured_output()方法,让模型按照我们定义的 Schema 返回结构化结果。
1. 为什么需要结构化输出
先看一个对比。让模型介绍《三体》中的角色"罗辑",自然语言返回的是大段文本,需要人工阅读和理解:
罗辑是《三体》系列中的主角之一,原本是一个玩世不恭的社会学家,
在面壁计划中被选为面壁者,最终成为执剑人...
而作为程序员或开发者,我们更希望得到这样的数据:
class Character(BaseModel):
name: str # 罗辑
age: int # 未知(虚构人物)
identity: str # 面壁者、执剑人
affiliations: list # ["人类社会", "面壁计划"]
结构化输出的核心价值体现在四个方面:结果可直接反序列化为对象或字典,无需额外的文本解析;Pydantic 验证确保字段类型正确;可与工具调用、Chain 等组件无缝衔接;输出结构固定,前端或下游系统可预定义处理逻辑。
2. with_structured_output() 方法概述
def with_structured_output(
self,
schema: dict | type,
*,
method: Literal["function_calling", "json_mode", "json_schema"] = "json_schema",
include_raw: bool = False,
strict: bool | None = None,
**kwargs: Any,
) -> Runnable[LanguageModelInput, dict[str, Any] | BaseModel]:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
schema | dict / type | — | 输出结构定义,支持 Pydantic / TypedDict / JSON Schema 三种方式(详见第 3 节) |
method | Literal | "json_schema" | 提取方式:json_schema(OpenAI 原生结构化输出)、function_calling(工具调用)、json_mode(JSON 模式) |
include_raw | bool | False | 是否同时返回原始 AIMessage(详见第 5 节) |
strict | bool | None | None | 是否强制严格模式(仅 OpenAI 支持,确保精确遵循 Schema) |
**kwargs | Any | {} | 透传到 self.bind(**kwargs),如 tool_choice、stop 等 |
该方法不会修改原模型,而是返回一个新的
Runnable实例。原模型仍可正常调用。
Info: 关于签名中的
*,
*,是 Python 的 keyword-only arguments separator(关键字参数分隔符),其后的参数只能以关键字形式传入,不能按位置传递。例如:# ✅ 正确——参数名写清楚,可读性好 model.with_structured_output(Joke, include_raw=True, method="json_mode") # ❌ TypeError——include_raw 和 method 不能按位置传 model.with_structured_output(Joke, True, "json_mode")这是 Python 常用的设计模式,目的是强制调用方写明参数名,避免因位置搞混参数。
2.1 基本用法
with_structured_output() 是聊天模型的方法,接收一个 Schema 定义,返回一个新的 Runnable 实例。需要注意的是:这个新实例的输出不再是 AIMessage,而是按 Schema 解析后的结构化对象:
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
model = ChatOpenAI(model="gpt-4o-mini")
class Joke(BaseModel):
"""给用户讲一个笑话。"""
setup: str = Field(description="这个笑话的开头")
punchline: str = Field(description="这个笑话的妙语")
# 绑定结构化输出,返回新的 Runnable
structured_model = model.with_structured_output(Joke)
result = structured_model.invoke("给我讲一个关于唱歌的笑话")
print(result)
# 输出: Joke(setup='为什么歌手总是喜欢在洗手间里唱歌?', punchline='因为那里有很好的回音和灵感!')
2.2 普通模型 vs 结构化模型对比
| 对比项 | 普通模型 | 结构化模型 |
|---|---|---|
| 返回类型 | AIMessage | Schema 指定类型(Pydantic / dict) |
| 输出内容 | .content 字符串 | 预定义字段 + 类型 |
| 调用方式 | model.invoke() | model.with_structured_output(Schema).invoke() |
with_structured_output()和bind_tools()一样,都是返回新的 Runnable 实例,不会修改原模型。
2.3 嵌套输出
结构化输出也支持嵌套结构——一个字段的类型可以是另一个 Pydantic 类:
from pydantic import BaseModel, Field
from typing import List, Optional
class Joke(BaseModel):
"""给用户讲一个笑话。"""
setup: str = Field(description="这个笑话的开头")
punchline: str = Field(description="这个笑话的妙语")
rating: Optional[int] = Field(default=None, description="从1到10分,给这个笑话评分")
class Data(BaseModel):
"""获取关于笑话的数据列表。"""
jokes: List[Joke]
structured_model = model.with_structured_output(Data)
result = structured_model.invoke("分别讲一个关于唱歌和跳舞的笑话")
print(result)
# 输出: Data(jokes=[
# Joke(setup='为什么唱歌的人总是很快乐?', punchline="因为他们总是'音'乐满满!", rating=8),
# Joke(setup='...', punchline='...', rating=7)
# ])
3. 三种定义方式
with_structured_output() 支持三种不同的 Schema 定义方式:
| 方式 | 返回类型 | 适用场景 |
|---|---|---|
| Pydantic | Pydantic 对象 | 推荐方式:类型安全、运行时校验、支持嵌套 |
| TypedDict | 字典 | 轻量级、不需要 Pydantic 依赖时 |
| JSON Schema | 字典 | 已有 JSON Schema 定义、跨语言兼容 |
这三种不同的 Schema 定义方式,区别:
- Pydantic 方式通过继承
BaseModel声明输出结构,返回类型安全的 Pydantic 对象,提供运行时校验、字段描述和嵌套支持,是官方推荐的方式; - TypedDict 方式用类型注解声明字典结构,返回普通字典,轻量且不需要 Pydantic 依赖,但校验只发生在开发期(依赖类型检查器),没有运行时保障;
- JSON Schema 方式直接传入 JSON Schema 字典,同样返回字典,但无类型提示也无运行时校验。
3.1 Pydantic 方式(推荐)
通过继承 pydantic.BaseModel 定义输出结构,是最推荐的方式——提供类型验证、字段描述和 IDE 自动补全:
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import Optional
model = ChatOpenAI(model="gpt-4o-mini")
class Joke(BaseModel):
"""给用户讲一个笑话。"""
setup: str = Field(description="这个笑话的开头")
punchline: str = Field(description="这个笑话的妙语")
rating: Optional[int] = Field(
default=None,
description="从1到10分,给这个笑话评分"
)
structured_model = model.with_structured_output(Joke)
result = structured_model.invoke("给我讲一个关于唱歌的笑话")
print(result)
# 输出: Joke(setup='为什么歌手总是带一把伞?', punchline='因为他们怕下雨时会错过音调!', rating=7)
3.2 TypedDict 方式
TypedDict 为字典提供精确的类型提示,定义输出的键名和值类型。配合 Annotated 注解提供字段描述:
from langchain_openai import ChatOpenAI
from typing import Optional
from typing_extensions import Annotated, TypedDict
model = ChatOpenAI(model="gpt-4o-mini")
class Joke(TypedDict):
"""给用户讲一个笑话。"""
setup: Annotated[str, ..., "这个笑话的开头"]
punchline: Annotated[str, ..., "这个笑话的妙语"]
rating: Annotated[Optional[int], None, "从1到10分,给这个笑话评分"]
structured_model = model.with_structured_output(Joke)
result = structured_model.invoke("讲一个关于跳舞的笑话")
print(result)
# 输出: {'setup': '跳舞的鱼总是快乐,因为它们在水里摇摆', 'punchline': '...', 'rating': 8}
3.3 JSON Schema 方式
直接传递一个 JSON Schema 字典,适合已有现成 Schema 定义或需要跨语言兼容的场景:
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o-mini")
json_schema = {
"title": "joke",
"description": "给用户讲一个笑话。",
"type": "object",
"properties": {
"setup": {
"type": "string",
"description": "这个笑话的开头",
},
"punchline": {
"type": "string",
"description": "这个笑话的妙语",
},
"rating": {
"type": "integer",
"description": "从1到10分,给这个笑话评分",
"default": None,
},
},
"required": ["setup", "punchline"],
}
structured_model = model.with_structured_output(json_schema)
result = structured_model.invoke("给我讲一个关于唱歌的笑话")
print(result)
# 输出(实际返回的是字典):
# {'setup': '为什么唱歌的人总是很开心?', 'punchline': '因为他们总是有很多音符可供选择!', 'rating': 7}
Note: JSON Schema 内部仍返回字典
虽然我们传入的是 JSON Schema 定义,但 LangChain 内部将其解析后,最终返回的仍然是 Python 字典对象,而非 JSON 字符串。三种方式的输出在程序中使用方式一致。
4. 选择输出结构(Union 类型)
4.1 问题场景
当模型的输出可能有两种或多种形态时,需要让 LLM 根据输入语义自行选择合适的结构。
例如,我们定义了 Joke 结构来让模型讲笑话。但如果用户问的是"你是谁",模型仍然会按照 Joke 格式返回——产生不合理的输出:
result = structured_model.invoke("你是谁?")
# ❌ 不合理的输出:Joke(setup='...', punchline='...', rating=...)
4.2 使用 Union 提供选择
通过定义父类包裹多个可选结构,用 Union 类型让 LLM 根据语义自动选择:
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import Optional, Union
model = ChatOpenAI(model="gpt-4o-mini")
class Joke(BaseModel):
"""给用户讲一个笑话。"""
setup: str = Field(description="这个笑话的开头")
punchline: str = Field(description="这个笑话的妙语")
rating: Optional[int] = Field(default=None, description="从1到10分,给这个笑话评分")
class ConversationalResponse(BaseModel):
"""以对话的方式回应。待人友善,乐于助人。"""
response: str = Field(description="对用户查询的会话响应")
class FinalResponse(BaseModel):
"""最终回复,选择合适的输出结构。"""
final_output: Union[Joke, ConversationalResponse]
structured_model = model.with_structured_output(FinalResponse)
# 用户要求讲笑话 → 输出 Joke
print(structured_model.invoke("给我讲一个关于唱歌的笑话"))
# ✅ final_output=Joke(setup='为什么歌手总是带着梯子?', punchline='因为他们想要达到更高的音调!', rating=7)
# 用户打招呼 → 输出 ConversationalResponse
print(structured_model.invoke("你好"))
# ✅ final_output=ConversationalResponse(response='你好!有什么我可以帮助你的吗?')
4.3 工作原理
核心思路是利用嵌套输出 +
Union类型,让 LLM 根据输入内容自行选择合适子结构。
Union的作用是定义一组候选输出结构,将选择权交给 LLM,由它根据输入语义自动匹配最合适的那个。一些典型应用场景:比如用户的不同意图需要不同的输出结构;当模型无法匹配任何已知结构时,可设计一个兜底类型(如通用回复),避免输出不合理数据;
5. include_raw 参数详解
5.1 参数作用
默认情况下,with_structured_output() 只返回解析后的结构化结果。设置 include_raw=True 会让模型同时返回原始 AIMessage,方便调试和追踪:
structured_model = model.with_structured_output(Joke, include_raw=True)
result = structured_model.invoke("给我讲一个关于唱歌的笑话")
print(result)
# {
# "raw": AIMessage( # 模型原始响应
# content='{"setup":"...","punchline":"...","rating":8}',
# response_metadata={
# "token_usage": { ... }, # token 用量统计
# "model_name": "gpt-4o-mini-...",
# "finish_reason": "stop",
# },
# ...
# ),
# "parsed": Joke( # 解析后的结构化结果
# setup="为什么歌手总是带着铅笔和纸?",
# punchline="...",
# rating=8,
# ),
# "parsing_error": None, # 解析错误(有值时表示解析失败)
# }
5.2 返回结构
当 include_raw=True 时,返回的是一个包含三个键的字典:
| 键 | 类型 | 说明 |
|---|---|---|
raw | AIMessage | 模型原始的完整响应(含 token 用量、finish_reason 等) |
parsed | Schema 类型 | 解析后的结构化结果(成功时) |
parsing_error | BaseException 或 None | 解析过程中的错误信息(失败时有值) |
5.3 典型用途
result = structured_model.invoke("给我讲一个关于唱歌的笑话")
# 查看原始 AIMessage(调试)
print(result['raw'].response_metadata['token_usage'])
# 输出: {'completion_tokens': 40, 'prompt_tokens': 173, 'total_tokens': 213}
# 使用解析后的结构化对象
print(result['parsed'].setup)
# 输出: 你知道为什么歌手总是喜欢在吃饭的时候唱歌吗?
# 检查解析是否出错
if result['parsing_error']:
print(f"解析失败:{result['parsing_error']}")
Tip: include_raw 的用途
默认include_raw=False只返回结构化对象,简洁易用。开启include_raw=True适合需要监控 token 消耗、调试解析错误或需要原始模型元数据的场景。
6. 实用场景
6.1 场景一:信息提取器
结构化输出最直接的用途是从非结构化文本中提取结构化信息:
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from pydantic import BaseModel, Field
from typing import Optional
model = ChatOpenAI(model="gpt-4o-mini")
class Person(BaseModel):
"""一个人的信息。"""
name: Optional[str] = Field(default=None, description="这个人的名字")
hair_color: Optional[str] = Field(default=None, description="如果知道这个人头发的颜色")
skin_color: Optional[str] = Field(default=None, description="如果知道这个人的肤色")
height_in_meters: Optional[str] = Field(default=None, description="以米为单位的高度")
structured_model = model.with_structured_output(schema=Person)
messages = [
SystemMessage(content="你是一个提取信息的专家,只从文本中提取相关信息。"
"如果您不知道要提取的属性的值,属性值返回null"),
HumanMessage(content="史密斯身高6英尺,金发。")
]
result = structured_model.invoke(messages)
print(result)
# 输出: Person(name='史密斯', hair_color='金发', skin_color=None, height_in_meters='1.83')
关键设计要点:
Optional[str]——字段可为空
name: Optional[str] = Field(default=None, description="这个人的名字")
# ↳ 等价于 str | None,取值可以是字符串,也可以是 None
# ↳ 默认值 None:文本中找不到时 LLM 输出 None,而不是编造
信息提取场景下,原始文本不一定包含所有字段。Optional[str] 告诉模型:这个字段不是必须填的
- 文本中有相关信息 → LLM 提取并填入字符串
- 文本中没有 → LLM 直接输出
None,避免瞎编
6.2 场景二:少样本提示增强信息提取
当提取任务较复杂时,可以在消息中提供几个示例(少样本提示),帮助 LLM 理解提取规则:
示例1:
文本:"张三,黑头发,黄皮肤,身高1.75米"
提取:{name: "张三", hair_color: "黑色", skin_color: "黄色", height: "1.75"}
示例2:
文本:"李四,白皮肤"
提取:{name: "李四", hair_color: null, skin_color: "白色", height: null}
新文本:"王五,红发,身高六英尺"
提取:...
关于少样本提示的代码实现,将在后续提示词模板章节详细讲解。此处仅了解该场景的存在。
6.3 场景三:与工具结合使用
结构化输出也可以与工具调用结合——先通过工具获取外部数据,再以结构化形式返回结果。
方式一(不推荐):with_structured_output 的 tools 参数
structured_search_model = model.with_structured_output(
SearchResult,
tools=[web_search], # 告知模型有哪些工具可用
strict=True,
include_raw=True,
)
这种方式只是让模型知道有哪些工具可以调用,但不会自动执行工具。返回的结果中只包含 AIMessage(工具调用信息),parsed 字段为 None。
方式二(推荐):先 bind_tools 再 with_structured_output
正确的做法是分两步走:先绑定工具完成调用,再对结果进行结构化输出。
Warning: 注意绑定顺序
先绑定工具,再绑定结构化输出——顺序不能颠倒。
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from pydantic import BaseModel, Field
model = ChatOpenAI(model="gpt-4o-mini")
# 1. 定义结构化输出对象
class SearchResult(BaseModel):
"""结构化搜索结果。"""
query: str = Field(description="搜索查询")
findings: str = Field(description="调查结果摘要")
# 2. 定义工具
@tool
def web_search(query: str) -> str:
"""在网上搜索信息。"""
return "西安今天多云转小雨,气温18-23度,东南风2级,空气质量良好"
# 3. 先绑定工具
model_with_search = model.bind_tools([web_search])
# 4. 准备消息,调用工具
messages = [HumanMessage("搜索当前最新的西安的天气")]
ai_msg = model_with_search.invoke(messages)
messages.append(ai_msg)
for tool_call in ai_msg.tool_calls:
tool_msg = web_search.invoke(tool_call)
messages.append(tool_msg)
# 5. 在绑定了工具的 model 基础上再绑定结构化输出
structured_search_model = model_with_search.with_structured_output(SearchResult)
result = structured_search_model.invoke(messages)
print(result)
# 输出: SearchResult(query='西安天气', findings='西安今天多云转小雨,气温18-23度,东南风2级,空气质量良好')
完整调用流程:
Note: 本质上是两次调用
上述流程中实际上调用了两次模型:第一次让模型决定调用工具,第二次让模型将工具结果解析为结构化对象。这种方式虽然步骤较多,但能确保结果的准确性和结构化。
7. 总结
| 知识点 | 说明 |
|---|---|
with_structured_output() | 聊天模型提供的方法,让输出按指定 Schema 返回结构化数据 |
| 三种定义方式 | Pydantic(推荐)/ TypedDict / JSON Schema,效果等效 |
| 嵌套输出 | 支持结构的嵌套组合(如 List[Joke] 作为字段类型) |
| Union 选择结构 | 通过 Union 类型让 LLM 根据语义自动选择输出结构 |
include_raw | 设为 True 可同时获取原始 AIMessage,便于调试和监控 |
| 信息提取 | 用 Optional 字段 + 清晰描述,从非结构化文本中提取结构化数据 |
| 与工具结合 | 先 bind_tools() 再 .with_structured_output(),顺序不可颠倒 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)