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]:
参数类型默认值说明
schemadict / type输出结构定义,支持 Pydantic / TypedDict / JSON Schema 三种方式(详见第 3 节)
methodLiteral"json_schema"提取方式:json_schema(OpenAI 原生结构化输出)、function_calling(工具调用)、json_mode(JSON 模式)
include_rawboolFalse是否同时返回原始 AIMessage(详见第 5 节)
strictbool | NoneNone是否强制严格模式(仅 OpenAI 支持,确保精确遵循 Schema)
**kwargsAny{}透传到 self.bind(**kwargs),如 tool_choicestop

该方法不会修改原模型,而是返回一个新的 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 结构化模型对比

普通模型

model.invoke

AIMessage

.content → 字符串

结构化模型

structured_model.invoke

直接返回结构化对象

Pydantic 对象 / 字典

对比项普通模型结构化模型
返回类型AIMessageSchema 指定类型(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 定义方式:

方式返回类型适用场景
PydanticPydantic 对象推荐方式:类型安全、运行时校验、支持嵌套
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 工作原理

请求与笑话相关

请求与对话相关

用户输入

FinalResponse

LLM 判断语义

Joke

ConversationalResponse

返回 final_output

核心思路是利用嵌套输出 + 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 时,返回的是一个包含三个键的字典:

类型说明
rawAIMessage模型原始的完整响应(含 token 用量、finish_reason 等)
parsedSchema 类型解析后的结构化结果(成功时)
parsing_errorBaseExceptionNone解析过程中的错误信息(失败时有值)

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级,空气质量良好')

完整调用流程:

结构化输出 搜索工具 聊天模型 用户 结构化输出 搜索工具 聊天模型 用户 搜索西安天气 调用 web_search 返回天气文本 解析为 SearchResult 返回结构化对象

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(),顺序不可颠倒
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐