LangChain 聊天模型 —— 结构化输出详解与实战

引入

常规调用聊天大模型时,模型返回原始文本字符串。这种自由文本对人类阅读十分友好,但不利于程序自动处理。当业务需要从模型结果中定向提取固定字段(如名称、数值、状态等),并存入数据库、对接下游逻辑时,只能依靠正则等方式手动解析文本,代码繁琐、容错差,极易出错。

我们以 DeepSeek 模型 + LangChain 普通调用方式直观感受这个问题:

复制代码
import os
from langchain_openai import ChatOpenAI

# 初始化 DeepSeek 聊天模型
model = ChatOpenAI(
    model="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1"
)

# 普通调用,获取字符串结果
response = model.invoke("讲一个关于唱歌的笑话")
print(response.content)

输出示例:

有一天,一个人去参加歌唱比赛,评委问他:"你准备唱什么歌?" 他自信满满地回答:"《青藏高原》!" 评委点点头,示意他开始。 他深吸一口气,张开嘴------ "哞------" 评委愣住了,问:"你在干什么?" 他一脸无辜:"我在唱'哞'啊,青藏高原上的牦牛不都是这么叫的吗?"

模型返回纯文本字符串,如果程序想要从中自动提取特定信息,只能手写解析逻辑,稳定性很难保障。

LangChain 为聊天模型提供了结构化输出能力,实现从自由文本到规范结构化数据的范式转变:开发者预先定义目标数据结构,指令模型严格按照指定格式返回结果,程序可直接获取可读写的结构化对象,省去复杂文本解析环节。

LangChain 通过聊天模型内置方法**with_structured_output()**实现该能力,基础使用流程分为三步:

  1. 定义预期输出结构;
  2. 将结构绑定至模型,生成具备结构化输出能力的 Runnable;
  3. 发起调用,直接获得匹配预设结构的数据。

伪代码示例展示基础调用范式:

复制代码
# 1. 定义输出结构
schema = {"foo": "bar"}
# 2. 绑定 schema,生成支持结构化输出的 Runnable
model_with_structure = model.with_structured_output(schema)
# 3. 发起调用,直接获取结构化数据
structured_output = model_with_structure.invoke("用户输入指令")

**该方法支持三种主流结构定义方式:TypedDict、JSON Schema、Pydantic。**三者返回结果存在区别:传入 TypedDict / JSON Schema 将得到字典;传入 Pydantic 类会直接返回 Pydantic 实例对象。

核心参数精简说明

  • schema:核心参数,定义输出规范,支持 JSON 结构、TypedDict、Pydantic 类;
  • method :底层实现方案
    • json_schema(默认):依托模型原生结构化输出 API;
    • function_calling:基于工具 / 函数调用实现结构化返回;
    • json_mode:基础 JSON 模式,需要额外在提示词约束输出格式;
  • include_raw:控制返回内容。默认仅返回解析后结构化数据;开启后可同时拿到原始模型消息、解析结果与解析异常信息,适合调试;
  • strict:严格模式开关,开启后强制校验模型输出完全符合定义的 Schema。

下文将分别演示 TypedDict、JSON Schema、Pydantic 三种结构定义方案,结合 DeepSeek 模型实战对比不同写法的用法、差异与适用场景。


返回 Pydantic 对象

单个 Pydantic 案例结构

with_structured_output() 中传入 Pydantic 模型类,调用后会直接返回 Pydantic 对象实例。

完整流程:大模型输出符合规范的 JSON → LangChain 自动提取 JSON 内容 → 使用 Pydantic 完成字段解析、类型校验 → 返回可直接访问属性的 Pydantic 对象,无需手动解析字典。

基础单层 Pydantic 示例

复制代码
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import Optional
import os

# 接入 DeepSeek 模型
model = ChatOpenAI(
    model="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1"
)

# 定义期望输出结构:Pydantic模型
class Joke(BaseModel):
    """给用户讲的一个笑话"""
    setup: str = Field(description="这个笑话的开头铺垫")
    punchline: str = Field(description="这个笑话的包袱/妙语")
    rating: Optional[int] = Field(default=None, description="从1-10分给这个笑话评分")

# DeepSeek 完美支持 function_calling 方式实现结构化输出
model_with_structured = model.with_structured_output(Joke, method="function_calling")
result = model_with_structured.invoke("讲一个关于唱歌的笑话")

print(result)

运行输出:

setup='为什么唱歌的人从来不会迷路?' punchline='因为他们总会跟着调(调子/调头)走!' rating=7
💡补充说明: 若使用原生 OpenAI 接口,可省略 method="function_calling",直接简写:

复制代码
model_with_structured = model.with_structured_output(Joke)

由于我们当前对接 DeepSeek 模型,显式指定 method="function_calling" 兼容性更好。

拿到结果后,还可以直接通过 .属性名 访问字段:

复制代码
# 单独读取字段
print("笑话开头:", result.setup)
print("笑点:", result.punchline)
print("评分:", result.rating)

运行结果:

笑话开头: 为什么唱歌的人从来不会迷路?

笑点: 因为他们总会跟着调(调子/调头)走!

评分: 7

嵌套 Pydantic 结构(列表、多级对象)

结构化输出同样支持嵌套模型、数组列表,适合需要一次性返回多条数据的场景。

代码如下:

复制代码
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import Optional
import os

model = ChatOpenAI(
    model="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1"
)

class Joke(BaseModel):
    """给用户讲的一个笑话"""
    setup: str = Field(description="这个笑话的开头铺垫")
    punchline: str = Field(description="这个笑话的包袱/妙语")
    rating: Optional[int] = Field(default=None, description="从1-10分给这个笑话评分")

# 外层模型,内部包含 Joke 对象列表
class Data(BaseModel):
    """多条笑话数据集"""
    jokes: list[Joke]


model_with_structured = model.with_structured_output(Data, method="function_calling")
# Prompt 增加约束,强制模型填充评分字段
result = model_with_structured.invoke("分别讲一个关于唱歌和跳舞的笑话,每条笑话都必须给出1到10之间的数字评分")

print(result)

运行输出:

jokes=Joke(setup='为什么唱歌的人总是站在麦克风前面?', punchline='因为他们怕唱跑了调,麦克风能帮他们拉回来!', rating=7), Joke(setup='为什么跳舞的人从来不迷路?', punchline='因为他们总是跟着节奏走!', rating=6)

注意事项

  1. 评分字段缺失问题 rating 设置 default=None 为可空字段时,模型有时会忽略不返回评分。两种解决方案:

    • 在 Prompt 中显式强制要求输出评分;
    • 移除 Optional 和默认值,强制模型必须返回该字段。
  2. DeepSeek 适配要点 对接 DeepSeek 接口时,显式传入 method="function_calling" 能有效规避结构化解析异常,兼容性优于默认参数。

  3. 自带校验能力 Pydantic 会自动做类型校验:如果模型返回字符串数字、超出范围的分值,会直接抛出解析异常,提前拦截脏数据。

小结:Pydantic 方案优势明显,自带数据校验、支持嵌套结构、属性访问简洁,适合项目正式开发使用。下一节我们再对比 TypedDict、JSON Schema 两种定义方式。


返回 TypedDict

先了解 TypedDict:它用于为字典对象提供精确、结构化的类型提示。我们可以预先定义字典内存在哪些键,以及每个键对应的数据类型。 Python 3.8+ 支持以类的形式清晰定义 TypedDict,示例如下:

复制代码
from typing import TypedDict

class User(TypedDict):
    name: str
    age: int
    email: str
    is_active: bool = True  # 设置默认值

TypedDict 最重要的价值:借助类型检查工具,提前捕获键名拼写错误与值类型错误。

复制代码
user1: User = {
    "name": "Bob",
    "age": 25,
    "email": "bob@example.com"
}

# 类型检查器可以识别出两处错误
bad_user: User = {
    "name": "Dave",
    "age": "forty",      # 错误:age应当为int,传入字符串
    "emial": "dave@example.com" # 错误:键名拼写错误
}

在 LangChain 结构化输出中,我们同样可以将 TypedDict 传入 with_structured_output()。和 Pydantic 不同:调用结束后返回原生字典,而非对象实例。

方式 1:OpenAI 原生简洁写法(GPT 系列模型)

OpenAI 模型可以直接传入 TypedDict,一行代码实现结构化调用:

复制代码
from langchain_openai import ChatOpenAI
from typing import TypedDict, Annotated
import os

model = ChatOpenAI(
    model="gpt-4o-mini",
    api_key=os.getenv("OPENAI_API_KEY")
)

class Joke(TypedDict):
    """给用户讲的一个笑话"""
    setup: Annotated[str, "笑话开头"]
    punchline: Annotated[str, "笑话包袱"]
    rating: Annotated[int, "1-10评分"]

structured_model = model.with_structured_output(Joke)
result = structured_model.invoke("给我讲一个关于唱歌的笑话")
print(result)

拓展:开启 include_raw=True(OpenAI 可用)

OpenAI 支持配置 include_raw=True,调用后会返回一个封装好的字典,同时包含原始消息、解析结果、解析异常,非常便于调试。

复制代码
structured_model = model.with_structured_output(Joke, include_raw=True)
result = structured_model.invoke("给我讲一个关于唱歌的笑话")
print(result)

输出结构示例:

复制代码
{
    'raw': AIMessage(content='{"setup":"你知道为什么歌手总是喜欢在吃饭的时候唱歌吗?","punchline":"因为他们想要增加自己的'调味'!","rating":7}', ...),
    'parsed': {'setup': '你知道为什么歌手总是喜欢在吃饭的时候唱歌吗?', 'punchline': '因为他们想要增加自己的'调味'!', 'rating': 7},
    'parsing_error': None
}

字段说明:

  • raw:大模型返回完整原始 AIMessage;
  • parsed:根据 TypedDict 解析完成的结构化字典;
  • parsing_error:解析异常对象,解析正常时值为 None

方式 2:DeepSeek 适配方案

⚠️ 重要适配说明:

  1. DeepSeek 不支持直接使用 include_raw=True,开启会触发 400 接口报错;
  2. 推荐显式指定 method="function_calling" 保证兼容性;
  3. include_raw=True 属于 OpenAI 接口专属特性,无法在 DeepSeek 环境中一步同时获取 raw、parsed、parsing_error。

DeepSeek 标准结构化调用(TypedDict + function_calling)

复制代码
from langchain_openai import ChatOpenAI
from typing import TypedDict, Annotated
import os

# 接入DeepSeek
model = ChatOpenAI(
    model="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1"
)

class Joke(TypedDict):
    """给用户讲的一个笑话"""
    setup: Annotated[str, "笑话开头"]
    punchline: Annotated[str, "笑话包袱"]
    rating: Annotated[int, "1-10评分"]

# DeepSeek 推荐写法,指定function_calling
model_struct = model.with_structured_output(Joke, method="function_calling")
result = model_struct.invoke("讲一个关于唱歌的笑话")
print(result)

如果需要查看原始 AIMessage,只能分开两次调用

无法像 OpenAI 一样一步拿到全部信息,需要分开调用:

复制代码
# 1. 普通调用,只获取原始 AIMessage(对应OpenAI结果中的raw)
raw_ai_msg = model.invoke("讲一个关于跳舞的笑话")
print(raw_ai_msg)

# 2. 结构化调用,只获取解析完成后的字典(对应OpenAI结果中的parsed)
parsed_data = model_struct.invoke("讲一个关于跳舞的笑话")
print(parsed_data)

补充:不使用 with_structured_output,原生 JSON 模式手动解析(备选方案)

很多场景开发者会选择直接使用 response_format={"type": "json_object"},手动解析 JSON,作为结构化输出备选方案。但缺点是需要自行处理解析异常、提示词必须严格约束输出格式:

复制代码
from langchain_openai import ChatOpenAI
from typing import TypedDict, Annotated
import json
import os

model = ChatOpenAI(
    model="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1"
)

class Joke(TypedDict):
    """给用户讲的一个笑话"""
    setup: Annotated[str, "笑话开头"]
    punchline: Annotated[str, "笑话包袱"]
    rating: Annotated[int, "1-10评分"]

prompt = """生成1个唱歌相关笑话,严格输出JSON数组,数组内仅包含1个对象,字段:setup(笑话开头)、punchline(包袱)、rating(1-10整数评分),禁止任何额外文字、解释。"""
res = model.invoke(prompt, response_format={"type": "json_object"})
data = json.loads(res.content)
joke_list = [Joke(**item) for item in data]
print(joke_list)

运行结果:

{'setup': '为什么唱歌的人总是在洗澡时唱得最好?', 'punchline': '因为浴室里的回音让他们误以为自己唱得不错。', 'rating': 7}

关键差异总结

  1. OpenAI + include_raw=True:单次调用,自动封装 raw / parsed / parsing_error,调试极其方便;
  2. DeepSeek 限制:不支持 include_raw=True,只能分开两次调用分别获取原始消息与结构化结果;不存在自动封装的外层字典;
  3. TypedDict 返回类型:字典,不像 Pydantic 拥有内置数据校验;适合轻量场景,只想做类型提示、不需要复杂校验的业务。

对比回顾:上面 Pydantic 调用返回模型对象, TypedDict 返回普通字典。下面我们继续介绍 JSON Schema 定义结构化输出的实现方式。


返回 JSON Schema

Pydantic、TypedDict 都依赖 Python 类型语法,适合在代码内部定义数据结构。如果希望脱离 Python 语言体系、实现跨语言通用的数据规范定义,则可以选择 JSON Schema,下面我们演示 JSON Schema 在结构化输出中的使用。

通过 with_structured_output() 传入 JSON Schema 对象,可以约束模型按照约定的 JSON 规范返回内容。需要注意:模型底层输出 JSON,经过 LangChain 解析后,程序拿到的最终结果是 Python 字典对象,而非原始 JSON 字符串。

方式 1:OpenAI 原生简洁写法(GPT 系列模型)

OpenAI 原生对 json_schema 方式支持完善,直接传入 Schema 字典即可,无需额外指定 method。代码如下:

复制代码
from langchain_openai import ChatOpenAI

# 初始化大模型
model = ChatOpenAI(model="gpt-4o-mini")

# 定义JSON Schema规范
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}

方式 2:DeepSeek 适配方案

⚠️ 适配重点说明:

**DeepSeek 对 method="json_schema" 原生支持有限,实践中推荐使用 method="function_calling" 模拟结构化输出。**依靠工具调用实现约束,约束强度弱于 OpenAI 原生结构化接口,因此建议搭配额外规则强化输出格式。

同时注意:

即便我们传入 JSON Schema,程序最终得到的依然是 Python 字典,不是原始 JSON 字符串。

代码如下:

复制代码
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage
import os

# 定义 DeepSeek 模型
model = ChatOpenAI(
    model="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1"
)

# 定义JSON Schema
json_schema = {
    "title": "joke",
    "description": "给用户讲一个笑话。",
    "type": "object",
    "properties": {
        "setup": {
            "type": "string",
            "description": "这个笑话的开头,单独一句话",
        },
        "punchline": {
            "type": "string",
            "description": "这个笑话的妙语包袱,单独一句话",
        },
        "rating": {
            "type": "integer",
            "description": "从1到10分,必须给出整数评分,不能为空",
        },
    },
    "required": ["setup", "punchline", "rating"],  # 将rating设置为必填字段
}

这里提供两种工程上可用的实现方式:

方式一:在用户提问中追加格式约束

直接在 Prompt 里增加输出规则。缺点是需求语句与格式规则混杂在一起,后期维护、修改约束条件不方便。

复制代码
structured_model = model.with_structured_output(json_schema, method="function_calling")
# 在提问中强制约束输出结构
result = structured_model.invoke("讲1个关于唱歌的笑话,严格按照setup、punchline、rating三个字段返回,rating必须是1-10的数字")
print(result)

方式二:通过系统消息统一放置格式约束(推荐)

把通用格式规则抽离到系统消息中,用户 Prompt 只保留业务需求,规则集中存放,便于统一调整与复用。

复制代码
# 全局系统约束,统一存放格式要求
sys_msg = SystemMessage(content="你必须严格按照指定json结构输出内容,拆分setup、punchline、rating三个独立字段,rating为1-10数字,禁止输出大段连贯文字。")
structured_model = model.with_structured_output(json_schema, method="function_calling")
# 用户输入仅描述业务需求,不需要重复书写格式规则
res = structured_model.invoke([sys_msg, "讲一个唱歌的笑话"])
print(res)

运行结果示例:

{'setup': '为什么唱歌的时候不能吃蛋糕?', 'punchline': '因为怕唱到高音时,突然来一个"蛋"裂!', 'rating': 7}

小结

JSON Schema 最大优势是跨语言通用,前端、Java、Go 等各类语言都可以复用同一套规范;缺点是书写冗长,缺少代码层面的类型提示。 至此,三种结构化定义方案(Pydantic、TypedDict、JSON Schema)全部介绍完毕,我们可以横向对比三者的适用场景:

  1. Pydantic:返回对象实例,自带强大数据校验,适合 Python 后端正式项目;
  2. TypedDict:返回普通字典,仅提供静态类型提示,轻量无运行时校验;
  3. JSON Schema:跨语言通用规范,返回字典,适合多语言协作场景。

三种结构化输出方案对比表

方案 定义形式 返回结果 运行时校验 跨语言 优势 劣势 适用场景
Pydantic Python 类(BaseModel) Pydantic 对象 ✅ 内置自动校验 ❌ Python 专属 支持嵌套结构、属性直接访问、字段描述清晰、强大的数据校验 只能在 Python 代码中定义,其他语言无法复用规范 Python 后端正式项目、需要复杂数据校验、多层嵌套结构
TypedDict Python 类型定义 Python 字典 ❌ 仅静态类型提示,无运行校验 ❌ Python 专属 写法轻量、结果是原生字典、上手简单 缺少运行时数据校验,嵌套书写繁琐 简单业务、仅需要 IDE 类型提示、追求轻量化实现
JSON Schema JSON 字典规范 Python 字典 ❌ LangChain 仅基础解析校验 ✅ 跨语言通用 前端 / Java/Go 等多语言均可共用一套规范 书写冗长,没有 IDE 原生类型提示 多团队多语言协作、规范需要对外共享、标准化接口场景

补充关键实操提醒(适配文中 DeepSeek 适配经验)

  1. OpenAI 原生对三种方案兼容性最佳,json_schema 模式可直接使用默认参数;
  2. 使用 DeepSeek 模型时,推荐统一显式配置 method="function_calling" 提升稳定性;
  3. DeepSeek 不支持 include_raw=True,无法一步同时获取原始消息、解析结果与异常信息;
  4. 若需要字段强制填充,除了 Schema 配置required,建议配合系统提示词加强约束,进一步降低模型输出不规范风险。

综合对比可以看出,三种结构化输出没有绝对优劣,可以根据项目开发语言、团队协作模式、是否需要运行时校验灵活选择。单一 Python 项目优先选择 Pydantic;轻量简单脚本可选 TypedDict;多语言协作、规范需要跨端共享时使用 JSON Schema。


选择输出格式

在实际开发中,经常存在动态分支结构化需求:根据用户提问意图,让模型返回不同形式的数据结构。例如用户请求生成笑话时,返回笑话专属结构体;普通日常问答,则返回通用对话响应结构体。

联合类型 Union 实现方案(OpenAI 环境)

采用 Pydantic 联合类型 Union 实现多分支输出。该方案依托 OpenAI 原生结构化输出能力(method="json_schema"),在 GPT 系列模型中能够稳定运行。

示例代码:

复制代码
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import Optional, Union

# OpenAI GPT模型初始化
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)
result = structured_model.invoke("给我讲一个关于唱歌的笑话")
print(result)
result = structured_model.invoke("你好")
print(result)

输出结果:

final_output=Joke(setup='为什么歌手总是带着梯子?', punchline='因为他们想要达到更高的音调!', rating=7)

final_output=ConversationalResponse(response='你好!有什么我可以帮助你的吗?')

兼容多模型通用方案:类型标识外层封装

跨模型适配局限性分析

该顶层 Union 写法存在较强的环境依赖,无法直接迁移到 DeepSeek 等第三方大模型:

  1. OpenAI 模型默认启用 method="json_schema" 原生结构化输出接口,原生支持联合子类型识别,能够稳定区分两种结构体;
  2. DeepSeek 模型暂不支持 json_schema 模式,只能依靠 method="function_calling"(工具调用)实现结构化输出。工具调用的 Schema 规范对顶层联合类型兼容性差,模型难以自主区分多种嵌套结构,极易输出不符合规范的 JSON,导致解析失败。

实操现象:将课件代码直接迁移至 DeepSeek 环境运行,经常出现解析结果为 None。即便切换为带类型标识的外层封装方案,嵌套 Schema 依然存在偶发解析失败问题。

兼容多模型通用方案

为解决跨模型兼容问题,行业主流实践为:使用统一外层结构体,新增类型标识字段引导模型按需填充对应的子结构。通过显式标识降低模型理解成本,提升输出稳定性。

实现代码:

复制代码
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import Optional
import os
from langchain_core.messages import SystemMessage

# DeepSeek模型初始化
model = ChatOpenAI(
    model="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1"
)

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):
    """统一外层输出结构,依靠标识字段区分内容类型"""
    output_type: str = Field(description="可选枚举值:joke / chat")
    joke: Optional[Joke] = Field(default=None, description="output_type为joke时填充")
    chat: Optional[ConversationalResponse] = Field(default=None, description="output_type为chat时填充")

structured_model = model.with_structured_output(FinalResponse, method="function_calling")

# 增加系统提示,强制约束模型输出格式
sys_msg = SystemMessage(content="你必须严格按照FinalResponse结构输出。output_type只能填写joke或chat。当output_type='joke',填充joke对象,chat设为null;当output_type='chat',填充chat对象,joke设为null;禁止输出任何额外文字。")

res1 = structured_model.invoke([sys_msg, "给我讲一个关于唱歌的笑话"])
res2 = structured_model.invoke([sys_msg, "你好"])
print(res1)
print(res2)

拓展:运行中仍然出现 None 的原因与调试方案

在 DeepSeek function_calling 模式下,即便采用该封装方案,依旧存在概率返回None根本原因:DeepSeek 对多层嵌套 Pydantic Schema 的约束遵守能力有限,偶尔生成的 JSON 字段缺失、层级错位,LangChain 解析校验失败,最终返回空值。

推荐分层调试与优化手段:

1. 强化系统提示词:明确写明不同类型对应的字段填充规则,约束禁止输出额外自然语言;

2. 开启include_raw=True调试:捕获原始模型消息、解析结果与异常栈,定位 JSON 不合法的具体原因:

复制代码
structured_model = model.with_structured_output(
    FinalResponse,
    method="function_calling",
    include_raw=True
)

返回字典包含 raw(原始消息)、parsed(解析结果)、parsing_error(异常信息),便于排查模型输出问题。

  1. 兜底优化:扁平化 Schema :若嵌套结构持续不稳定,可以消除子模型嵌套,将所有字段平铺至外层结构,大幅降低模型输出难度,兼容性最强;

  2. 高可靠业务方案:意图识别分流 :先单独调用模型识别用户意图(需求是讲笑话还是普通对话),根据识别结果动态选择两套独立的结构化 Runnable,从根源规避单 Schema 承载多套结构带来的不稳定性。

总结

  1. 可使用 Union 联合类型写法,理解多分支结构化输出思路,仅在 OpenAI 模型环境运行;
  2. DeepSeek 等第三方模型开发场景:优先采用「类型标识外层封装方案」,同时配置强约束系统提示词;如果稳定性达不到业务要求,选择扁平化结构或意图分流方案;
  3. 底层核心差异:OpenAI json_schema 原生结构化输出与通用 function_calling 工具调用,二者对复杂联合、嵌套 Schema 的支持能力存在明显区别。

结构化输出实用场景

接下来结合实际开发场景,介绍结构化输出最常用、最核心的三类落地用法,分别为:信息结构化提取、少样本提示增强提取、与工具调用结合实现结构化检索输出。

场景一:作为通用信息提取器(最常用)

结构化输出最核心、最高频的落地场景就是非结构化文本信息提取。日常自然语言文本杂乱、无固定格式,无法直接用于程序入库、数据分析、字段匹配。通过结构化输出,我们可以预先定义需要提取的字段结构,让大模型自动从原始文本中筛选、清洗、补全信息,最终输出规范的结构化对象,无需手动正则匹配,极大降低开发成本。

下面基于 DeepSeek 模型实现通用人物信息提取案例,适配 DeepSeek 必须开启 method="function_calling" 保证结构化稳定性。所有字段设置为 Optional 可选类型,文本中无对应信息时自动返回 None,避免解析报错。

复制代码
from langchain_openai import ChatOpenAI
from typing import Optional
from pydantic import BaseModel, Field
from langchain_core.messages import HumanMessage, SystemMessage
import os

# 适配DeepSeek模型
model = ChatOpenAI(
    model="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1"
)

# 定义需要提取的结构化字段
class Person(BaseModel):
    """人物信息提取结构体"""
    name: Optional[str] = Field(default=None, description="人物姓名")
    hair_color: Optional[str] = Field(default=None, description="人物头发颜色,未知返回None")
    skin_color: Optional[str] = Field(default=None, description="人物肤色,未知返回None")
    height_in_meters: Optional[float] = Field(default=None, description="人物身高,单位米,未知返回None")

# DeepSeek必须指定function_calling保证结构化解析稳定
structured_model = model.with_structured_output(schema=Person, method="function_calling")

# 系统提示约束:严格提取、未知置空
messages = [
    SystemMessage(content="你是专业的文本信息提取专家,仅从用户文本中提取对应字段信息,无对应信息则返回null,禁止编造数据"),
    HumanMessage(content="史密斯身高6英尺,金发。")
]

result = structured_model.invoke(messages)
print(result)

运行结果:

name='史密斯' hair_color='金发' skin_color=None height_in_meters=1.8288

可以看到,模型自动完成了信息筛选、单位转换、空字段填充,输出完全符合预设结构,程序可直接读取对应属性完成后续业务逻辑。

场景二:少样本提示增强信息提取(拓展)

在复杂文本、模糊语义、字段歧义较多的场景下,仅依靠结构体约束,大模型偶尔会出现提取不准、字段遗漏、随意编造内容的问题。此时可以配合少样本提示(Few-shot),在提示词中传入多组真实样例,引导模型严格按照样例格式、提取逻辑输出结果,大幅提升结构化提取精度。

由于少样本提示属于独立知识点,本文暂不展开代码实现,后续在「提示词工程-少样本提示」章节结合结构化输出统一演示落地案例。

场景三:结构化输出与工具调用结合(重点踩坑)

在 Agent 开发场景中,经常需要:调用工具获取外部数据 → 对工具返回结果进行结构化整理。很多初学者会直接将 with_structured_output 与工具绑定使用,但该写法存在明显缺陷:结构化输出不会自动执行工具,仅能识别工具参数,无法自动整合工具返回结果

这段基于 OpenAI 模型演示完整踩坑过程与最终正确写法,帮助理解工具调用与结构化输出的执行顺序。

错误写法:直接绑定工具与结构化参数

通过 tools 参数绑定工具后,模型只会生成工具调用参数,不会执行工具、不会整理结构化结果,最终 parsed 解析结果为空。

复制代码
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage

# 初始化OpenAI模型
model = ChatOpenAI(model="gpt-4o-mini")

# 定义输出结构
class SearchResult(BaseModel):
    """结构化搜索结果"""
    query: str = Field(description="用户搜索指令")
    findings: str = Field(description="搜索结果摘要")

# 定义模拟搜索工具
@tool
def web_search(query: str) -> str:
    """联网搜索工具,模拟获取天气信息"""
    return "西安今天多云转小雨,气温18-23度,东南风2级,空气质量良好"

# 直接绑定工具+结构化输出
structured_search_model = model.with_structured_output(
    SearchResult,
    tools=[web_search],
    strict=True,
    include_raw=True,
)

result = structured_search_model.invoke("搜索当前最新的西安的天气")
print(result)

运行现象:模型仅返回工具调用的原始消息,parsed 结构化结果为 None,无法得到整理后的天气结构化数据。

进阶错误写法:链式绑定工具与结构化

部分开发者会使用 bind_tools 链式绑定结构化输出,但模型会直接凭借自身知识库回答问题,完全跳过工具调用流程,无法获取实时/外部数据。

复制代码
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage

model = ChatOpenAI(model="gpt-4o-mini")

class SearchResult(BaseModel):
    """结构化搜索结果"""
    query: str = Field(description="用户搜索指令")
    findings: str = Field(description="搜索结果摘要")

@tool
def web_search(query: str) -> str:
    return "西安今天多云转小雨,气温18-23度,东南风2级,空气质量良好"

# 链式绑定工具与结构化输出
model_with_tools = model.bind_tools([web_search]).with_structured_output(SearchResult)
messages = [HumanMessage("搜索当前最新的西安的天气")]
result = model_with_tools.invoke(messages)
print(result)

运行结果:模型直接输出知识库中的天气数据,未触发工具调用,无法满足实时检索需求。

最终正确写法:分步执行工具调用 + 二次结构化解析

正确流程必须严格遵循三步:模型生成工具调用参数 → 手动执行工具、拼接工具返回结果 → 二次调用模型完成结构化整理。该流程可以保证工具正常调用、结果完整结构化。

复制代码
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage

model = ChatOpenAI(model="gpt-4o-mini")

# 定义结构化输出
class SearchResult(BaseModel):
    """结构化搜索结果"""
    query: str = Field(description="用户搜索指令")
    findings: str = Field(description="搜索结果摘要")

# 定义搜索工具
@tool
def web_search(query: str) -> str:
    return "西安今天多云转小雨,气温18-23度,东南风2级,空气质量良好"

# 1、绑定工具,让模型生成工具调用指令
model_with_search = model.bind_tools([web_search])
messages = [HumanMessage("搜索当前最新的西安的天气")]

# 2、第一次模型调用:获取工具调用参数
ai_msg = model_with_search.invoke(messages)
messages.append(ai_msg)

# 3、手动遍历并执行所有工具,拼接工具返回结果到消息列表
for tool_call in ai_msg.tool_calls:
    tool_msg = web_search.invoke(tool_call)
    messages.append(tool_msg)

# 4、第二次模型调用:基于工具结果生成结构化数据
structured_search_model = model_with_search.with_structured_output(SearchResult)
result = structured_search_model.invoke(messages)
print(result)

最终正确运行结果:

query='西安天气' findings='西安今天多云转小雨,气温18-23度,东南风2级,空气质量良好。'

总结

1、结构化输出与原生工具组合使用原生体验较差、代码繁琐,需要手动控制两轮模型调用、手动执行工具,开发效率低且容易出错;

2、手动分步执行仅适合原理学习、简单演示场景,不适合正式 Agent 项目开发;

3、工业级、自动化的「工具调用 + 结构化输出 + 多轮对话」能力,后续将通过 LangGraph Agent 框架实现,框架会自动完成工具调度、消息拼接、结果结构化整理,无需手动编写复杂流程。


本文主要讲解了 LangChain 聊天模型的结构化输出功能,详细介绍了 TypedDict、JSON Schema、Pydantic 三种结构化定义方式,对比了不同写法的优缺点,同时适配了 OpenAI 与 DeepSeek 模型的兼容差异,并结合实际场景演示了信息提取、工具协同等落地用法。结构化输出实现了大模型自由文本到标准化数据的转换,是工程化 AI 开发的重要基础。本篇为个人学习整理,内容若有疏漏或错误,欢迎大家批评指正,感谢观看!

相关推荐
花千树_0103 小时前
插件加载方式选不好,迟早被同事吐槽——四种姿势全解析
langchain·agent
fenglllle4 小时前
python3t pip3安装langchain失败心得
langchain
大模型真好玩6 小时前
LangChain DeepAgents 速通指南(十一)—— DeepAgents Code 记忆与状态管理
人工智能·langchain·agent
lhxcc_fly6 小时前
LangGraph基础知识点
python·langchain·llm·langgraph
BraveWang9 小时前
【LangChain 1.x】05、结构化输出|三种结构定义模式与两种结果获取方式
langchain
俊俊谢9 小时前
从零搭建:本地LangChain Agent调用远程LLaMA-Factory模型服务
langchain·llama
怕浪猫9 小时前
第15章 安全、伦理与合规
langchain·openai·agent
向日的葵00621 小时前
langchain的Tools教程(三)
python·langchain·tools
马里马里奥-1 天前
大模型应用开发笔记04:LangChain 工具(Tool)与中间件(Middleware)详解
笔记·中间件·langchain