概述:为什么结构化输出这么重要?

很多 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"],
)

这里有两个关键点:

  • ratingge=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 只服务一个明确任务。
  • 字段数量尽量控制。
  • 必填字段和可选字段分清楚。
  • 枚举值要少而清晰。
  • 复杂对象拆成多步抽取。

例如简历解析可以分两步:

  1. 抽取基本信息。
  2. 抽取项目经历列表。

不要一次性让模型抽完所有维度。

生产实践二:可选字段要明确

如果信息可能不存在,不要强行必填。

不推荐:

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,不要让模型自由发挥字符串。
  • 结构化输出不是权限系统,高风险决策仍要由后端规则和审批控制。
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐