第06章:结构化输出 (上)

1、结构化输出概述

1.1 什么是结构化输出

LangChain的结构化输出(Structured Output) 指的是:

要求模型最终返回一个符合预定义结构的数据对象,例如固定字段的JSON、Pydantic 模型、 TypedDict,而不再是无格式的自然语言文本。

它的核心目标是把" 自然语言回答 "变成" 程序可以稳定消费的数据 "。

例如,不是让模型输出:

盗梦空间在2010年上映,导演是克里斯托弗·诺兰,评分9.3。

而是让它输出成类似这样的结构

复制代码
{
    "title": "盗梦空间",
    "year": 2010,
    "director": "克里斯托弗·诺兰",
    "rating": 9.3
}

这样做的价值主要有三点:

  • 更容易被代码处理:下游系统可以直接读字段,而不是再从自然语言里做解析。
  • 结果更稳定:减少"模型说法变了但意思差不多"导致的解析失败。
  • 更适合工程化:适用于表单抽取、分类、路由、调用工具参数生成、工作流状态传递等场景。

1.2 传统方式 vs 结构化输出

1、传统的几种方式(繁琐、不推荐)

复制代码
# 1. 提示词要求JSON
prompt = "以JSON格式返回:{name, age, occupation}"
response = model.invoke(prompt)

# 2. 手动解析
import json
data = json.loads(response.content)

# 3. 手动验证类型
if not isinstance(data['age'], int):
    raise ValueError("age must be int")

# 4. 手动创建对象
person = Person(**data)

2、结构化输出(简洁)

复制代码
# 一步到位
structured_llm = model.with_structured_output(Person)
person = structured_llm.invoke("张三是一名 30 岁的软件工程师")
# ✅ 自动解析、验证、创建对象

为什么第2种结构化输出机制这么受欢迎?

在没有 Pydantic 等结构化方案之前,开发者需要写大量的 Prompt 苦口婆心地求大模型"请返回 JSON, 不要带任何解释",然后自己写繁琐的 json.loads() 和 try...except 。

而有了 Pydantic 等结构化方案结合 .with_structured_output() 之后:

  • Prompt 变干净了: 字段的 description 直接充当了 Prompt 的一部分。
  • 类型安全: 编辑器能自动补全,代码运行前就能做类型检查。
  • 极其稳定: 依托大模型厂商底层的 JSON 模式,输出错误率降到了极低。

1.3 结构化输出模式

目前LangChain 1.x 支持多种Schema与结构化输出方式:

  • Pydantic(字段校验、描述、嵌套结构,功能最丰富)
  • TypedDict(轻量类型约束)
  • JSON Schema(与前后端/跨语言接口最通用)
  • dataclass

模型对象可以调用 with_structured_output() 绑定输出模式(schema)。

只有 Pydantic 返回的是Schema类实例,其余三种方式返回的都是 字典 ;也只有 Pydantic 在类型不 匹配时会抛出异常。

问题:现在所有模型都支持"本章要讲解的结构化输出方式"吗?

大部分现代模型支持(通过函数调用):

  • ✅ OpenAI (gpt-4, gpt-3.5-turbo)
  • ✅ Anthropic (claude-3)
  • ✅ Groq (llama-3)
  • ❌ 某些旧模型不支持 如果不支持,LangChain 会回退到提示词 + JSON 解析

2、四种模式的使用

2.1 模式1:Pydantic

它通过在运行时强制执行类型提示,确保数据的正确性和一致性,是 生产场景首选 。

2.1.1 基本使用

需要满足的几个要素:

  • 所有结构化输出的数据模型都必须继承 BaseModel
  • 使用 类型提示 。Pydantic 支持丰富的字段类型:str 、int、float、Listxxx、Optionalxxx
  • 使用 Field() 添加字段默认值和描述,帮助 LLM 理解字段含义

举例1:

1)大模型的初始化

复制代码
from  langchain_community.chat_models import ChatTongyi
from dotenv import load_dotenv


load_dotenv()

# api_key 与 api_base 会自动获取 .env下的 DASHSCOPE_API_KEY
model = ChatTongyi(
    model = 'qwen3.7-max',

)

2)定义 Pydantic 模型

使用清晰的字段描述;没有描述,LLM 可能格式错误

复制代码
from pydantic import BaseModel, Field
class Person(BaseModel):
    """人物信息"""
    name: str = Field(description="姓名")
    age: int = Field(description="年龄")
    occupation: str = Field(description="职业")

LangChain 会要求 LLM 的输出必须能填充这些字段。

3)使用 with_structured_output 即可引导模型进行结构化输出:

复制代码
response = model.invoke("张三是一名30岁的软件工程师")
# 创建结构化输出的
structured_llm = model.with_structured_output(Person)
# 调用
result = structured_llm.invoke("张三是一名 30 岁的软件工程师")
print(result)
print(type(result))
# result 是 Person 实例
print(result.name) # "张三"
print(result.age) # 30
print(result.occupation) # "软件工程师"

说明:没有描述,LLM 可能格式错误。

复制代码
name='张三' age=30 occupation='软件工程师'
<class '__main__.Person'>
张三
30
软件工程师

举例2:

复制代码
from pydantic import BaseModel, Field


# 定义输出结构
class SentimentAnalysis(BaseModel):
    """情感分析结果"""

    sentiment: str = Field(
        description="情感倾向:positive/negative/neutral"
    )
    confidence: float = Field(
        description="置信度,0-1之间"
    )
    keywords: list[str] = Field(
        description="关键词列表"
    )


# 创建结构化输出模型
structured_model = model.with_structured_output(SentimentAnalysis)

# 待分析文本
text = "这个课程内容很实用,学到了很多知识,强烈推荐!"

# 调用模型
result = structured_model.invoke(
    f"分析以下文本的情感:\n{text}"
)

# 输出结果
print(f"类型: {type(result)}")
print(f"情感: {result.sentiment}")
print(f"置信度: {result.confidence}")
print(f"关键词: {result.keywords}")

类型: <class '__main__.SentimentAnalysis'>
情感: positive
置信度: 0.95
关键词: ['实用', '学到了很多', '强烈推荐', '课程', '知识']

2.1.2 高级特性

情况1:可选字段 「Optional」

问题:LLM 未填充某些字段怎么办?

使用 Optional 指定字段为可选的

举例:

复制代码
from typing import Optional
from pydantic import BaseModel, Field
class Person(BaseModel):
    """人物信息"""
    name: str = Field(description="姓名")
    age: Optional[str] = Field(description="年龄")
    occupation: str = Field(description="职业")

structured_llm = model.with_structured_output(Person)
structured_llm.invoke("张三是一名医生")

Person(name='张三', age='未知', occupation='医生')

情况2:默认值 「default」

LLM 未提供的信息会使用默认值。格式如下:

Field(default="默认值", description="描述")

注意:不同模型提供商对default字段的支持是不同的。

复制代码
from pydantic import BaseModel, Field

class Person(BaseModel):
    """人物信息"""
    name: str = Field(description="姓名")
    age: int = Field(1, description="年龄")
    occupation: str = Field(description="职业")

structured_llm = model.with_structured_output(Person)
structured_llm.invoke("张三是一名医生")

Person(name='张三', age=1, occupation='医生')

举例2:

复制代码
class Config(BaseModel):
    timeout: Optional[int] = Field(30,description="超时时间(单位秒)")
    retry: bool = Field(False,description="是否支持重试")
    max_attempts: int = Field(6,description="最大重试次数")
# 测试
structured_llm = model.with_structured_output(Config)
structured_llm.invoke("配置要求:支持重试,最多重试5次")

Config(timeout=30, retry=True, max_attempts=5)

举例3:

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

# 假设你已经定义了 model_with_openrouter
# from your_module import model_with_openrouter

class Product(BaseModel):
    """产品信息"""
    name: str = Field(description="产品名称")
    price: float = Field(description="价格")
    description: Optional[str] = Field(description="产品描述")
    stock: int = Field(default=100, description="库存")

# 测试
structured_llm = model.with_structured_output(Product)

print("\n场景1:完整信息")
result1 = structured_llm.invoke("iPhone 15 售价 5999 元,最新款智能手机,库存50台")
print(result1)

print("\n场景2:缺少描述和库存")
result2 = structured_llm.invoke("MacBook Pro 售价 12999 元")
print(result2)

场景1:完整信息

name='iPhone 15' price=5999.0 description='最新款智能手机' stock=50

场景2:缺少描述和库存

name='MacBook Pro' price=12999.0 description=None stock=100

情况3:枚举类型 「Literal」

问题:如何限制字段的可选值? 回答:使用枚举

复制代码
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field

# 定义你的优先级枚举类
class Priority(str, Enum):
    LOW = "低"
    MEDIUM = "中"
    HIGH = "高"

class CustomerInfo(BaseModel):
    """客户信息"""
    name: str = Field(description="客户姓名")
    phone: str = Field(description="电话号码")
    email: Optional[str] = Field(description="邮箱")
    issue: str = Field(description="问题描述")
    urgency: Priority = Field(description="紧急程度")

# 测试
structured_llm = model.with_structured_output(CustomerInfo)

conversation = """
客服: 您好,请问有什么可以帮助您?
客户: 我是王小明,电话 138-1234-5678,我的订单一直没发货,很着急!
客服: 好的,我帮您查一下
"""

result = structured_llm.invoke(f"从以下客服对话中提取客户信息:\n{conversation}")
print(result)
print("\n提取结果:")
print(f" 客户: {result.name}")
print(f" 电话: {result.phone}")
print(f" 邮箱: {result.email or '未提供'}")
print(f" 问题: {result.issue}")
print(f" 紧急程度: {result.urgency.value}")

如果嫌单独定义一个 Enum 类太麻烦,也可以直接导入 typing 中的 Literal ,直接在字段里把允许 的值写死。

复制代码
from typing import Optional,Literal
from pydantic import BaseModel, Field


class CustomerInfo(BaseModel):
    """客户信息"""
    name: str = Field(description="客户姓名")
    phone: str = Field(description="电话号码")
    email: Optional[str] = Field(description="邮箱")
    issue: str = Field(description="问题描述")
    urgency: Literal["低","中","高"] = Field(description="紧急程度")

# 测试
structured_llm = model.with_structured_output(CustomerInfo)

conversation = """
客服: 您好,请问有什么可以帮助您?
客户: 我是王小明,电话 138-1234-5678,我的订单一直没发货,很着急!
客服: 好的,我帮您查一下
"""

result = structured_llm.invoke(f"从以下客服对话中提取客户信息:\n{conversation}")
print(result)
print("\n提取结果:")
print(f" 客户: {result.name}")
print(f" 电话: {result.phone}")
print(f" 邮箱: {result.email or '未提供'}")
print(f" 问题: {result.issue}")
print(f" 紧急程度: {result.urgency}")

应用场景:

  • 自动填充 CRM 系统
  • 工单自动分类
  • 客服辅助
情况4:列表提取 「List」
复制代码
from typing import List
from pydantic import BaseModel, Field
class Person(BaseModel):
    """人物信息"""
    name:str = Field(description="姓名")
    age:int =Field(description="年龄")

class PersonList(Person):
    people:List[Person]

structured_llm = model.with_structured_output(PersonList)
result = structured_llm.invoke("张三 30岁,李四 25岁")
print(result)

name='张三' age=30 people=Person(name='张三', age=30), Person(name='李四', age=25)

举例2:产品评论分析

复制代码
class Review(BaseModel):
    """产品评论"""
    product: str
    rating: int = Field(description="评分 1-5")
    pros: List[str] = Field(description="优点列表")
    cons: List[str] = Field(description="缺点列表")
structured_llm = model.with_structured_output(Review)
review = structured_llm.invoke("""
    iPhone 17 很棒!摄像头强大,手感好。但是价格贵,没有充电器。4分。
    """)
print(review)

应用场景: 批量处理用户评论, 自动生成分析报告 ,发现产品改进点

举例3:文档信息提取

复制代码
from typing import List
from pydantic import BaseModel, Field


class Invoice(BaseModel):
    """发票信息"""

    invoice_number: str = Field(description="发票号")
    date: str = Field(description="日期")
    total_amount: float = Field(description="总金额")
    items: List[str] = Field(description="商品")


# 测试
structured_llm = model.with_structured_output(Invoice)

invoice_text = """
发票号: INV-2024-001
日期: 2024-01-15
总金额: 1299.00
商品: MacBook Pro, AppleCare+
"""

invoice = structured_llm.invoke(f"提取发票信息:{invoice_text}")
print(invoice)

invoice_number='INV-2024-001' date='2024-01-15' total_amount=1299.0 items='MacBook Pro', 'AppleCare+'

应用场景: 自动化财务处理, OCR 后结构化, 数据录入

情况5:嵌套结构
复制代码
from pydantic import BaseModel, Field

class Address(BaseModel):
    """地点描述"""
    city:str = Field(description="城市")
    district:str =Field(description="区域")

class Company(BaseModel):
    name:str =Field(description="公司名称")
    address:Address = Field(description="公司所在地")

structured_model = model.with_structured_output(Company)
response= structured_model.invoke("阿里巴巴在杭州的滨江区")
print(response)

name='阿里巴巴' address=Address(city='杭州', district='滨江区')

举例2:

复制代码
from typing import List
from pydantic import BaseModel, Field

# 1. 定义嵌套的 Pydantic 模型
class Actor(BaseModel):
    """演员信息"""

    name: str = Field(description="演员姓名")
    role: str = Field(description="饰演的角色")


class Movie(BaseModel):
    """电影信息"""

    title: str = Field(description="电影标题")
    year: int = Field(description="上映年份")
    director: str = Field(description="导演")
    cast: List[Actor] = Field(description="演员列表")
    rating: float = Field(description="评分")


# 2. 初始化模型并绑定输出结构
structured_model = model.with_structured_output(Movie)

# 3. 调用模型,直接获取 Movie 实例
response = structured_model.invoke("请介绍电影《盗梦空间》")

# 4. 访问结构化数据
print(f"电影名: {response.title}")
print(f"上映年份: {response.year}")
print(f"导演: {response.director}")
print(f"演员列表: {response.cast}")
print(f"评分: {response.rating}")

上代码输出结果如下

电影名: 盗梦空间

上映年份: 2010

导演: 克里斯托弗·诺兰

演员列表: Actor(name='莱昂纳多·迪卡普里奥', role='柯布 (Cobb)'), Actor(name='约瑟夫·高登-莱维特', role='亚瑟 (Arthur)'), Actor(name='艾伦·佩吉', role='阿丽阿德妮 (Ariadne)'), Actor(name='汤姆·哈迪', role='伊姆斯 (Eames)'), Actor(name='渡边谦', role='斋藤 (Saito)'), Actor(name='玛丽昂·歌迪亚', role='梅尔 (Mal)')

评分: 9.3

说明:LLM 能力有限,复杂嵌套结构可能会出错。所以建议:

  • 嵌套层级 ≤ 3 层
  • 使用清晰的 description
  • 必要时拆分成多个调用

举例3:

复制代码
from typing import List
from pydantic import BaseModel, Field


# 评论维度
class Aspect(BaseModel):
    """评论维度"""

    name: str = Field(description="维度名称,如:质量、价格、服务")
    score: int = Field(description="评分,1-5")
    comment: str = Field(description="具体评价")


# 产品评论分析
class ProductReview(BaseModel):
    """产品评论分析"""

    overall_sentiment: str = Field(
        description="整体情感:positive / negative / neutral"
    )
    overall_score: int = Field(description="综合评分,1-5")
    aspects: List[Aspect] = Field(description="各维度评价")
    summary: str = Field(description="一句话总结")


# 创建结构化模型
structured_model = model.with_structured_output(ProductReview)

# 测试评论
review_text = """
这款笔记本电脑性能非常强大,运行大型软件毫无压力。
屏幕色彩鲜艳,看视频很舒服。
不过价格有点贵,而且风扇噪音较大。
客服态度很好,物流也快。
总体来说还是值得购买的。
"""

# 调用模型
result = structured_model.invoke(
    f"分析以下产品评论:\n{review_text}"
)

# 输出结果
print(f"整体情感: {result.overall_sentiment}")
print(f"综合评分: {result.overall_score}/5")

print("\n各维度评价:")
for aspect in result.aspects:
    print(f" - {aspect.name}: {aspect.score}/5 - {aspect.comment}")

print(f"\n总结:{result.summary}")

整体情感: positive

综合评分: 4/5

各维度评价:

  • 性能: 5/5 - 性能非常强大,运行大型软件毫无压力

  • 屏幕: 5/5 - 色彩鲜艳,看视频很舒服

  • 价格: 3/5 - 价格有点贵,性价比一般

  • 噪音: 2/5 - 风扇噪音较大,影响使用体验

  • 服务: 5/5 - 客服态度很好,令人满意

  • 物流: 5/5 - 物流速度快,配送及时

总结:性能和屏幕表现出色,服务与物流令人满意,但价格偏高且风扇噪音较大,总体仍值得购买。

情况6:限制条件

举例1:

复制代码
from pydantic import BaseModel, Field
class User(BaseModel):
    name:str = Field(description="姓名",min_length=2,max_length=50)
    age:int =Field(description='年龄',le=150)
    email:str=Field(description="邮箱")


try:
    # user1=User(name="张三", age=20, email="张三@qq.com")
    user2=User(name="张三", age=200, email="张三@qq.com")
    # print(user1)
    print(user2)
except ValueError as e:
    print(f"[FAIL] 验证失败(符合预期): {e.errors()[0]['msg']}")

FAIL 验证失败(符合预期): Input should be less than or equal to 150

举例2:

复制代码
from pydantic import BaseModel, Field


class Product(BaseModel):
    """产品信息(严格验证)"""

    name: str = Field(
        description="产品名称(字符串类型)",
        min_length=2,
    )
    price: float = Field(
        description="价格,数字类型",
        gt=0,  # 必须大于 0
    )
    stock: int = Field(
        description="库存,整数类型",
        ge=0,  # 必须大于等于 0
    )


# 创建结构化模型
structured_llm = model.with_structured_output(Product)

# 正常数据
# response = structured_llm.invoke(
#     "华为 Mate 80 Pro Max,价格是7999,当前库存100"
# )

# 非法数据
response = structured_llm.invoke(
    "华为 Mate 80 Pro Max,价格是-7999,当前库存-100"
)

print(response)

None

2.1.3 工作流程图解

第1步:定义结构

比如:

复制代码
from pydantic import BaseModel, Field
class Person(BaseModel):
    """人物信息"""
    name: str = Field(description="姓名")
    age: int = Field(description="年龄")
    occupation: str = Field(description="职业")
第2步:协议转换 LangChain

内部会调用 Pydantic 的底层方法(如 model_json_schema() ),将你写的 Python 代码自 动转换成标准的 JSON Schema。

这个 JSON Schema 是一段严格的 JSON 文本,详细描述了有哪些字段、字段类型是什么( string , array 等)以及字段的描述( description )。

第3步:模型交互与强约束

LangChain 会将这个 JSON Schema 包装进给大模型的 API 请求中。

现代方法( .with_structured_output ): 现代大模型(如 OpenAI、Anthropic、Gemini 等)普 遍支持"函数调用/工具调用(Function/Tool Calling)"或"JSON Mode"。LangChain 会把 JSON Schema 作为 Tools 传入。

大模型侧的约束: 像 OpenAI 的 strict=True 参数,会启动模型的语法采样约束(Grammarbased sampling)。大模型在解码生成 token 时,不是瞎猜,而是严格按照 JSON Schema 的语 法树进行选择,从而在模型底层级保证了输出格式绝不走样。

第4步:自动解析与验证

当大模型返回符合 JSON 规范的字符串后,LangChain 的 PydanticStructuredOutputParser (解析 器)会接管工作:

  1. 解析(Parsing): 将字符串解析为 Python 字典。

  2. 验证(Validation): 将字典喂给你的 Pydantic 模型。Pydantic 会自动检查数据类型是否正确。 如果模型漏掉了必填字段,或者类型错误,这里会直接抛出验证错误(或者触发 LangChain 的重 试机制)。

  3. 返回(Return): 如果通过验证,你拿到的不再是冷冰冰的字符串,而是一个直接可以点出属性 的 Python Pydantic 对象(例如 result.title )。

相关推荐
北斗落凡尘1 小时前
React面试题
前端
念何架构之路1 小时前
restartmanager-重启管理子系统
java·开发语言
Java内核笔记1 小时前
告别第三方库!Spring Boot 4 原生 API 版本控制全解析:4 种策略 + 实战案例
java·后端
一棵白菜1 小时前
mac 部署 n8n
前端
学高数就犯困1 小时前
React:常见的性能优化手段
前端·react.js
天天码行空1 小时前
vkeyboardhand:零依赖虚拟键盘指法组件
前端·javascript·vue.js
东方小月1 小时前
从零开发一个 Coding Agent(五):使用 TypeBox 校验工具参数
前端·人工智能·后端
我要两颗404西柚1 小时前
Stage four:VUE项目上线
前端·javascript·vue.js
小小洋洋1 小时前
OpenWrt 从U盘迁移到内置 eMMC,并完成扩容与 Docker 安装
java·docker·eureka