09_结构化输出_如何让LLM返回你想要的JSON
概述:为什么结构化输出这么重要?
很多 LLM Demo 最后都是这样拿结果:
response = model.invoke("请从文本中提取姓名、邮箱和手机号,并返回 JSON")
print(response.text())
模型可能返回:
{
"name": "John Doe",
"email": "john@example.com",
"phone": "555-123-4567"
}
看起来没问题。
但真实项目里,你很快会遇到这些输出:
当然可以,下面是提取结果:
{
"name": "John Doe",
"email": "john@example.com",
"phone": "555-123-4567"
}
或者:
```json
{
"name": "John Doe",
"email": "john@example.com",
"phone": "555-123-4567",
}
甚至:
```json
{
"name": "John Doe",
"email": null,
"phone": 5551234567,
"note": "The email was not provided"
}
这些对人类来说都能看懂,但对程序来说很麻烦。
结构化输出要解决的就是这个问题:
让模型输出符合你定义的 schema,并让应用拿到可直接使用的结构化对象,而不是靠猜、靠正则、靠手动清洗。
结构化输出的目标,是把 LLM 的自然语言结果变成可验证、可解析、可进入业务系统的数据结构。
反例:用正则解析模型输出为什么不靠谱?
很多人一开始会这样做:
import json
import re
text = response.text()
match = re.search(r"\{.*\}", text, re.S)
data = json.loads(match.group(0))
这个写法在 Demo 里经常能跑。
但它很脆弱:
- 模型可能输出 Markdown 代码块。
- JSON 里可能多一个尾随逗号。
- 模型可能输出两个 JSON。
- 模型可能在 JSON 前后加解释。
- 字段类型可能不对。
- 必填字段可能缺失。
- 模型可能返回数组而不是对象。
- 嵌套字段可能结构错误。
更严重的是,正则只能“抓一段看起来像 JSON 的文本”,它不能告诉你:
rating是否在 1 到 5 之间。sentiment是否只能是positive/negative/neutral。items是否一定是列表。email是否可以为空。- 输出是否多了业务不允许的字段。
这就是为什么结构化输出需要 schema 和校验。
正则只能抽文本,schema 才能定义结构和约束。
LangChain 结构化输出的两条主线
LangChain 当前结构化输出主要有两条主线:
| 使用场景 | 推荐方式 |
|---|---|
| 直接调用模型做抽取、分类、生成结构 | model.with_structured_output(schema) |
| 使用 Agent,并希望最终状态里有结构化结果 | create_agent(..., response_format=schema) |
可以这样理解:
普通模型调用:
model -> with_structured_output -> 结构化结果
Agent 调用:
create_agent -> response_format -> structured_response
两者都围绕 schema 工作。
schema 可以是:
- Pydantic model
- Dataclass
- TypedDict
- JSON Schema
其中,Pydantic 最适合需要运行时校验的业务场景。
先看最小例子:Pydantic + with_structured_output
如果你只是想从一段文本中提取结构化信息,不一定需要 Agent。
可以直接用模型的 with_structured_output。
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
load_dotenv()
class ContactInfo(BaseModel):
"""Contact information for a person."""
name: str = Field(description="The person's full name")
email: str = Field(description="The person's email address")
phone: str = Field(description="The person's phone number")
model = init_chat_model(
"openai:gpt-4o-mini",
temperature=0,
)
structured_model = model.with_structured_output(ContactInfo)
result = structured_model.invoke(
"Extract contact info: John Doe, john@example.com, 555-123-4567"
)
print(result)
print(result.name)
print(result.email)
print(result.phone)
返回结果不再是字符串,而是 ContactInfo 对象:
ContactInfo(
name="John Doe",
email="john@example.com",
phone="555-123-4567",
)
这比让模型“请返回 JSON”然后手动解析稳定得多。
为什么 Pydantic 很适合结构化输出?
因为它同时提供:
- 字段类型。
- 字段描述。
- 默认值。
- 可选字段。
- 枚举约束。
- 数值范围约束。
- 嵌套结构。
- 运行时校验。
模型负责生成结构,Pydantic 负责验证结构。
Pydantic Field:把字段语义告诉模型
不要只写字段名。
不推荐:
class ProductReview(BaseModel):
rating: int
sentiment: str
key_points: list[str]
推荐:
from typing import Literal
from pydantic import BaseModel, Field
class ProductReview(BaseModel):
"""Analysis of a product review."""
rating: int | None = Field(
description="Rating from 1 to 5. Use None if no explicit rating is present.",
ge=1,
le=5,
)
sentiment: Literal["positive", "negative", "neutral"] = Field(
description="Overall sentiment of the review."
)
key_points: list[str] = Field(
description="Key points mentioned in the review, each 1-5 words."
)
字段描述很重要。
模型会参考这些描述来决定:
- 字段含义是什么。
- 什么时候填
None。 - 允许哪些枚举值。
- 列表里应该放什么。
- 数字范围是多少。
schema 不只是给程序看的,也是给模型看的。
示例一:评论分析
下面做一个评论分析器。
from typing import Literal
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
load_dotenv()
class ProductReview(BaseModel):
"""Analysis of a product review."""
rating: int | None = Field(
description="Rating from 1 to 5. Use None if no explicit rating is present.",
ge=1,
le=5,
)
sentiment: Literal["positive", "negative", "neutral"] = Field(
description="Overall sentiment of the review."
)
key_points: list[str] = Field(
description="Key points mentioned in the review, each 1-5 words."
)
model = init_chat_model(
"openai:gpt-4o-mini",
temperature=0,
)
review_model = model.with_structured_output(ProductReview)
result = review_model.invoke(
"Analyze this review: Great product, 5 out of 5 stars. Fast shipping, but expensive."
)
print(result)
你期望得到:
ProductReview(
rating=5,
sentiment="positive",
key_points=["fast shipping", "expensive"],
)
这里有两个关键点:
rating有ge=1, le=5校验。sentiment只能是三个枚举值之一。
如果模型输出 rating=10,Pydantic 校验会失败。
这比手写 json.loads() 后再到处 if 判断更清楚。
示例二:信息抽取
信息抽取是结构化输出最常见的场景。
from pydantic import BaseModel, Field
class ResumeInfo(BaseModel):
"""Structured information extracted from a resume."""
name: str = Field(description="Candidate's name")
years_of_experience: int | None = Field(
description="Total years of work experience, None if not mentioned"
)
skills: list[str] = Field(description="Technical skills mentioned in the resume")
current_company: str | None = Field(description="Current company, None if unknown")
调用:
resume_model = model.with_structured_output(ResumeInfo)
result = resume_model.invoke(
"""
张三是一名后端工程师,拥有 6 年 Python 和 Go 开发经验。
目前就职于星河科技,熟悉 FastAPI、PostgreSQL、Redis 和 Kubernetes。
"""
)
print(result.name)
print(result.years_of_experience)
print(result.skills)
print(result.current_company)
业务代码可以直接使用字段:
if result.years_of_experience and result.years_of_experience >= 5:
print("资深候选人")
这就是结构化输出的价值:模型结果可以进入程序逻辑。
示例三:意图识别
智能客服里,经常要先判断用户意图。
from typing import Literal
from pydantic import BaseModel, Field
class UserIntent(BaseModel):
"""User intent classification result."""
intent: Literal[
"query_order",
"refund_request",
"technical_support",
"small_talk",
"unknown",
] = Field(description="The user's main intent")
confidence: float = Field(description="Confidence score from 0 to 1", ge=0, le=1)
reason: str = Field(description="Brief reason for the classification")
调用:
intent_model = model.with_structured_output(UserIntent)
result = intent_model.invoke("我的订单 A1001 怎么还没发货?")
print(result.intent)
print(result.confidence)
print(result.reason)
后续可以分流:
if result.intent == "query_order":
# 调订单工具
pass
elif result.intent == "refund_request":
# 进入退款流程
pass
else:
# 交给普通问答
pass
这比让模型返回一段“用户可能是在查询订单”的自然语言更适合工程系统。
TypedDict:更轻量的 schema
如果你不需要 Pydantic 的运行时校验,可以用 TypedDict。
from typing_extensions import Annotated, TypedDict
class MovieDict(TypedDict):
"""A movie with details."""
title: Annotated[str, ..., "The title of the movie"]
year: Annotated[int, ..., "The year the movie was released"]
director: Annotated[str, ..., "The director of the movie"]
rating: Annotated[float, ..., "The movie's rating out of 10"]
调用:
movie_model = model.with_structured_output(MovieDict)
result = movie_model.invoke("Provide details about the movie Inception")
print(result)
print(result["title"])
返回结果通常是字典:
{
"title": "Inception",
"year": 2010,
"director": "Christopher Nolan",
"rating": 8.8,
}
TypedDict 的优点:
- 写法轻量。
- 适合简单结构。
- 类型提示清晰。
缺点:
- 运行时校验不如 Pydantic 强。
- 复杂约束表达能力弱。
如果业务对数据质量要求高,优先用 Pydantic。
JSON Schema:适合跨语言和平台约束
如果你需要和前端、Java、Go、低代码平台或外部系统共享 schema,可以用 JSON Schema。
json_schema = {
"title": "Ticket",
"description": "A customer support ticket",
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Short title of the ticket",
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Ticket priority",
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "Ticket tags",
},
},
"required": ["title", "priority", "tags"],
}
ticket_model = model.with_structured_output(
json_schema,
method="json_schema",
)
result = ticket_model.invoke(
"用户说:系统登录不了,提示 500 错误,影响线上业务。"
)
print(result)
JSON Schema 的优点:
- 跨语言通用。
- 适合接口契约。
- 可以和现有 API schema 体系结合。
缺点:
- Python 里写起来比 Pydantic 啰嗦。
- 校验和业务对象映射通常要自己处理。
Agent 结构化输出:response_format
如果你使用 create_agent,推荐用 response_format。
LangChain 的 Agent 会把最终结构化结果放到返回 state 的 structured_response 字段里。
from langchain.agents import create_agent
from pydantic import BaseModel, Field
class ContactInfo(BaseModel):
"""Contact information for a person."""
name: str = Field(description="The name of the person")
email: str = Field(description="The email address of the person")
phone: str = Field(description="The phone number of the person")
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[],
response_format=ContactInfo,
)
result = agent.invoke({
"messages": [
{
"role": "user",
"content": "Extract contact info from: John Doe, john@example.com, 555-123-4567",
}
]
})
print(result["structured_response"])
你会拿到:
ContactInfo(
name="John Doe",
email="john@example.com",
phone="555-123-4567",
)
注意,这里不是从最后一条自然语言消息里解析 JSON。
结构化结果会被放在:
result["structured_response"]
Agent 场景下,不要从 messages[-1].content 里硬扒 JSON,要读 structured_response。
response_format 的三种策略
官方文档里,response_format 可以接收几类值:
| 写法 | 含义 |
|---|---|
response_format=MySchema |
直接传 schema,让 LangChain 自动选择策略 |
response_format=ProviderStrategy(MySchema) |
使用供应商原生结构化输出 |
response_format=ToolStrategy(MySchema) |
使用工具调用模拟结构化输出 |
response_format=None |
不显式要求结构化输出 |
最常用的是直接传 schema:
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[],
response_format=ContactInfo,
)
LangChain 会根据模型能力自动选择:
- 支持原生结构化输出时,使用
ProviderStrategy - 否则使用
ToolStrategy
这也是 v1.0 之后比较重要的设计:开发者先定义目标结构,策略选择尽量交给框架。
ProviderStrategy:供应商原生结构化输出
一些模型供应商原生支持结构化输出。
这时可以使用 ProviderStrategy。
from langchain.agents import create_agent
from langchain.agents.structured_output import ProviderStrategy
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[],
response_format=ProviderStrategy(ContactInfo),
)
Provider-native 的优势是:
- 由模型供应商 API 层约束结构。
- 通常比纯 Prompt 更稳定。
- 能更早发现 schema 不匹配。
- 适合对格式可靠性要求高的场景。
但它也有前提:
- 你选的模型和供应商要支持。
- 不同供应商支持的方法和严格程度可能不同。
- 如果同时使用 tools,还要确认模型是否支持工具和结构化输出同时使用。
官方文档也提到,当直接传 schema 且模型支持原生结构化输出时,LangChain 会自动使用 ProviderStrategy。
ToolStrategy:用工具调用实现结构化输出
如果模型不支持原生结构化输出,但支持工具调用,LangChain 可以用 ToolStrategy。
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[],
response_format=ToolStrategy(ProductReview),
)
它的思路是:
把 schema 变成一个“结构化输出工具”
模型通过 tool call 填参数
LangChain 校验参数并返回 structured_response
这和普通工具调用很像。
区别是:这个工具不是为了查天气、查订单,而是为了让模型用结构化参数表达最终答案。
自定义 tool message content
ToolStrategy 还支持 tool_message_content:
from langchain.agents.structured_output import ToolStrategy
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[],
response_format=ToolStrategy(
schema=ProductReview,
tool_message_content="结构化评论分析已生成。",
),
)
这会影响结构化输出工具调用后写入消息历史的内容。
一般业务代码主要关心:
result["structured_response"]
而不是这个中间 ToolMessage。
错误处理:模型填错 schema 怎么办?
结构化输出不是魔法。
模型仍然可能出错。
例如 schema 要求:
rating: int = Field(ge=1, le=5)
但模型输出:
rating = 10
Pydantic 校验会失败。
在 ToolStrategy 中,官方文档说明了 handle_errors 参数:
ToolStrategy(
schema=ProductReview,
handle_errors=True,
)
默认 True 会捕获结构化输出错误,把错误反馈给模型,让模型重试。
也可以自定义错误提示:
ToolStrategy(
schema=ProductReview,
handle_errors="请提供合法评分,rating 必须在 1 到 5 之间。",
)
或者只处理特定异常:
ToolStrategy(
schema=ProductReview,
handle_errors=ValueError,
)
如果你想让错误直接抛出:
ToolStrategy(
schema=ProductReview,
handle_errors=False,
)
生产环境怎么选?
- 用户输入复杂、模型偶发填错:可以开启自动重试。
- 你希望错误进入上游异常处理:设为
False。 - 你要控制模型重试提示:传自定义字符串或函数。
结构化输出失败不是罕见异常,要把校验失败和重试策略纳入设计。
多结构输出:Union 类型
有些场景下,输入可能对应多种结构。
例如用户输入可能是联系人,也可能是事件。
from typing import Union
from pydantic import BaseModel, Field
class ContactInfo(BaseModel):
"""Contact information."""
name: str = Field(description="Person's name")
email: str = Field(description="Email address")
class EventDetails(BaseModel):
"""Event details."""
event_name: str = Field(description="Name of the event")
date: str = Field(description="Event date")
使用:
from langchain.agents.structured_output import ToolStrategy
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[],
response_format=ToolStrategy(Union[ContactInfo, EventDetails]),
)
模型会根据上下文选择更合适的 schema。
但要注意:
- Union 分支不要过多。
- 分支之间要有清晰边界。
- 如果输入同时包含多个对象,模型可能错误返回多个结构化输出。
- 官方文档提到,出现 multiple structured outputs 时,Agent 会给模型错误反馈并要求重试。
如果业务真的需要抽取多个对象,应该把 schema 设计成列表字段,而不是让模型在多个 schema 之间摇摆。
嵌套结构:列表和子对象
真实业务中,输出经常是嵌套结构。
例如从会议纪要里抽取多个行动项。
from typing import Literal
from pydantic import BaseModel, Field
class ActionItem(BaseModel):
"""One action item from a meeting."""
task: str = Field(description="The task to be completed")
assignee: str | None = Field(description="Person responsible, None if unknown")
priority: Literal["low", "medium", "high"] = Field(description="Task priority")
class MeetingSummary(BaseModel):
"""Structured meeting summary."""
topic: str = Field(description="Meeting topic")
decisions: list[str] = Field(description="Decisions made in the meeting")
action_items: list[ActionItem] = Field(description="Action items")
调用:
summary_model = model.with_structured_output(MeetingSummary)
result = summary_model.invoke(
"""
会议讨论了 RAG 项目上线计划。
决定下周先灰度给研发部门。
李雷负责补充权限过滤,优先级 high。
韩梅梅负责整理评估集,优先级 medium。
"""
)
print(result.topic)
print(result.decisions)
print(result.action_items[0].task)
嵌套结构适合:
- 会议纪要。
- 简历解析。
- 合同条款抽取。
- 工单拆解。
- 商品信息抽取。
但嵌套越深,模型出错概率越高。
建议:
- 控制嵌套层级。
- 字段描述写清楚。
- 列表数量有限制时写进描述。
- 重要字段加校验。
include_raw:同时拿原始消息和解析结果
调试结构化输出时,有时你不仅想看解析后的对象,也想看模型原始返回。
可以使用 include_raw=True。
structured_model = model.with_structured_output(
ProductReview,
include_raw=True,
)
result = structured_model.invoke(
"Great product: 5 out of 5 stars. Fast shipping, but expensive."
)
print(result["raw"])
print(result["parsed"])
print(result["parsing_error"])
返回结构类似:
{
"raw": AIMessage(...),
"parsed": ProductReview(...),
"parsing_error": None,
}
这在排查问题时很有用:
- 模型到底生成了什么?
- 解析失败原因是什么?
- token usage 在 raw 里有没有?
- tool call 参数是什么?
生产日志里可以记录必要元数据,但注意不要把敏感用户信息完整打到日志里。
json_schema、function_calling、json_mode 的区别
官方模型文档里提到,结构化输出可能有不同 method:
| method | 含义 |
|---|---|
json_schema |
使用供应商原生结构化输出能力 |
function_calling |
通过工具调用 / 函数调用方式让模型填 schema |
json_mode |
要求模型输出合法 JSON,但 schema 仍需在 Prompt 中描述 |
可以这样理解:
json_schema:API 层强约束 schema
function_calling:把 schema 当作工具参数让模型填写
json_mode:只保证 JSON 形态,不一定保证业务 schema
一般可靠性排序:
Provider-native json_schema > function_calling > 只靠 Prompt / json_mode
但具体还要看模型和供应商支持情况。
不要只看名字,要实际用业务样本测试。
结构化输出和 Tool 调用有什么关系?
第 08 篇我们讲了 Tool。
结构化输出和 Tool 调用关系很近。
Tool 调用要求模型生成结构化参数:
{
"order_id": "A1001"
}
ToolStrategy 也是类似思路:把结构化输出 schema 变成一个特殊工具,让模型通过 tool call 填字段。
区别在于:
- 普通 Tool 是为了执行外部动作。
- ToolStrategy 是为了生成最终结构化结果。
所以如果你理解了工具调用,就更容易理解 ToolStrategy。
生产实践一:schema 要尽量小而清晰
不要一上来定义一个超级大 schema:
class Everything(BaseModel):
field_1: str
field_2: str
...
field_80: str
字段越多,模型越容易漏填、误填、填无关内容。
建议:
- 一个 schema 只服务一个明确任务。
- 字段数量尽量控制。
- 必填字段和可选字段分清楚。
- 枚举值要少而清晰。
- 复杂对象拆成多步抽取。
例如简历解析可以分两步:
- 抽取基本信息。
- 抽取项目经历列表。
不要一次性让模型抽完所有维度。
生产实践二:可选字段要明确
如果信息可能不存在,不要强行必填。
不推荐:
class ContactInfo(BaseModel):
name: str
email: str
phone: str
如果文本里没有手机号,模型可能会编一个。
更好:
class ContactInfo(BaseModel):
name: str | None = Field(description="Name if present, otherwise None")
email: str | None = Field(description="Email if present, otherwise None")
phone: str | None = Field(description="Phone number if present, otherwise None")
Prompt 或字段描述里也要强调:
Do not make up missing values. Use None if the value is not present.
中文业务可以写:
如果原文没有明确提到,不要编造,填 None。
生产实践三:枚举值优先用 Literal
分类任务不要让模型自由发挥字符串。
不推荐:
sentiment: str
模型可能输出:
正面
积极
positive
pos
偏正面
推荐:
from typing import Literal
sentiment: Literal["positive", "negative", "neutral"]
这样后续业务逻辑更稳定。
同理:
priority: Literal["low", "medium", "high"]
intent: Literal["query_order", "refund_request", "technical_support", "unknown"]
能枚举就不要让模型自由写字符串。
生产实践四:不要让模型决定权限字段
结构化输出经常用于业务流转。
例如:
class ActionDecision(BaseModel):
action: str
approved: bool
这类字段要谨慎。
不要让模型直接决定:
- 是否允许退款。
- 是否有权限查看订单。
- 是否可以删除数据。
- 是否通过审批。
模型可以输出建议:
class ActionSuggestion(BaseModel):
action: Literal["refund", "transfer_to_human", "answer"]
reason: str
但真正的权限判断要由后端规则系统完成。
结构化输出是数据接口,不是权限系统。
生产实践五:结构化输出也要评估
不要只测一个例子。
准备评估集:
| input | expected |
|---|---|
| “好评,物流很快,5 星” | rating=5, sentiment=positive |
| “没写评分,但说质量一般” | rating=None, sentiment=neutral |
| “太差了,客服也不回” | rating=None, sentiment=negative |
| “10/10,非常棒” | rating=5 或按规则处理 |
评估维度:
- 是否能解析。
- 字段是否完整。
- 类型是否正确。
- 枚举值是否正确。
- 缺失值是否没有编造。
- 数值范围是否合规。
- 错误输入是否能稳定失败或返回 None。
结构化输出不是“能跑一次就上线”的能力。
常见错误一:只在 Prompt 里写“返回 JSON”
错误:
请返回 JSON,不要输出其他内容。
这能提高概率,但不够可靠。
更好:
class Result(BaseModel):
name: str
email: str | None
structured_model = model.with_structured_output(Result)
Prompt 是软约束,schema 是硬得多的工程约束。
常见错误二:字段描述太空
错误:
class Review(BaseModel):
score: int = Field(description="score")
模型不知道分数范围,也不知道没有分数时怎么办。
改进:
score: int | None = Field(
description="Rating from 1 to 5. Use None if the review does not include a clear rating.",
ge=1,
le=5,
)
字段描述要写业务语义,不要重复字段名。
常见错误三:把最后一条 message 当结果
Agent 结构化输出里,错误写法:
result = agent.invoke(...)
data = json.loads(result["messages"][-1].content)
正确写法:
result = agent.invoke(...)
data = result["structured_response"]
如果用了 response_format,就应该读取 structured_response。
常见错误四:强制必填导致模型编造
如果 schema 里所有字段都是必填,模型为了满足 schema 可能会补一个不存在的值。
例如文本里没有手机号,但 schema 要求:
phone: str
模型可能填:
unknown
not provided
000-000-0000
如果业务允许缺失,写成:
phone: str | None
并在描述中说明没有就填 None。
常见错误五:结构化输出字段里放长篇自然语言
结构化输出适合承载数据,不适合承载一整篇文章。
不推荐:
class BlogPost(BaseModel):
title: str
content: str
summary: str
如果 content 是几千字长文,结构化输出的收益有限,还可能增加失败率。
更好的做法:
- 长文本生成走普通文本输出。
- 标题、摘要、标签、分类走结构化输出。
- 如果确实需要结构化长文,拆成章节列表。
完整 Demo:客服意图识别 + 结构化结果
下面给一个完整可运行示例。
from typing import Literal
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
load_dotenv()
class SupportIntent(BaseModel):
"""Structured user intent for customer support."""
intent: Literal[
"query_order",
"refund_request",
"technical_support",
"complaint",
"small_talk",
"unknown",
] = Field(description="The user's main intent")
confidence: float = Field(description="Confidence from 0 to 1", ge=0, le=1)
order_id: str | None = Field(
description="Order ID if explicitly mentioned, otherwise None"
)
should_transfer_to_human: bool = Field(
description="Whether this request should be transferred to a human agent"
)
reason: str = Field(description="Brief reason for the classification")
model = init_chat_model(
"openai:gpt-4o-mini",
temperature=0,
)
intent_model = model.with_structured_output(SupportIntent)
examples = [
"帮我查一下订单 A1001 怎么还没发货?",
"我要退款,你们这个产品太差了。",
"SDK 初始化一直报 401,怎么处理?",
"你好呀,今天心情怎么样?",
]
for text in examples:
result = intent_model.invoke(text)
print("输入:", text)
print("意图:", result.intent)
print("置信度:", result.confidence)
print("订单号:", result.order_id)
print("是否转人工:", result.should_transfer_to_human)
print("原因:", result.reason)
print("-" * 40)
这个 Demo 展示了结构化输出的典型用法:
- 用
Literal固定意图枚举。 - 用
float+ge/le控制置信度范围。 - 用
str | None表达可缺失字段。 - 用布尔字段进入业务流程。
- 用
reason保留可解释性。
业务层就可以基于结构做路由:
if result.intent == "query_order" and result.order_id:
# 调订单查询工具
pass
elif result.should_transfer_to_human:
# 转人工
pass
else:
# 普通回答
pass
完整 Demo:Agent response_format
如果你希望 Agent 最终返回结构化结果:
from typing import Literal
from langchain.agents import create_agent
from pydantic import BaseModel, Field
class Ticket(BaseModel):
"""Customer support ticket."""
title: str = Field(description="Short ticket title")
priority: Literal["low", "medium", "high"] = Field(description="Ticket priority")
category: Literal["order", "refund", "technical", "other"] = Field(
description="Ticket category"
)
summary: str = Field(description="Brief summary of the user issue")
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[],
response_format=Ticket,
system_prompt="你是一个客服工单助手,需要把用户问题整理成结构化工单。",
)
result = agent.invoke({
"messages": [
{
"role": "user",
"content": "我订单 A1001 还没发货,已经等了三天了,麻烦尽快处理。",
}
]
})
ticket = result["structured_response"]
print(ticket.title)
print(ticket.priority)
print(ticket.category)
print(ticket.summary)
Agent 场景的关键点仍然是:
ticket = result["structured_response"]
不要去解析最后一条 message。
总结
结构化输出的本质是什么?如果只记住一句话:
结构化输出是用 schema 约束模型输出,并通过校验把 LLM 结果变成业务代码可直接使用的数据对象。
再具体一点:
- 不要长期依赖正则和手写
json.loads()抢救模型输出。 - 直接模型调用可以用
model.with_structured_output(schema)。 - Agent 场景可以用
create_agent(..., response_format=schema)。 - Agent 的结构化结果在
result["structured_response"]。 - Pydantic 适合需要字段描述、运行时校验和复杂约束的场景。
- TypedDict 更轻量,但运行时校验能力弱。
- JSON Schema 适合跨语言、跨系统的 schema 契约。
- 直接传 schema 时,LangChain 会尽量自动选择 ProviderStrategy 或 ToolStrategy。
- ProviderStrategy 使用供应商原生结构化输出能力,通常更可靠。
- ToolStrategy 用工具调用实现结构化输出,并支持错误处理和重试。
include_raw=True可以同时拿原始消息、解析结果和解析错误。- 可选字段要用
None表达,避免模型编造。 - 枚举字段优先用
Literal,不要让模型自由发挥字符串。 - 结构化输出不是权限系统,高风险决策仍要由后端规则和审批控制。
更多推荐



所有评论(0)