LangChain 之 Agent 结构化输出

简介: 本文详解LangChain中Agent结构化输出机制:通过`response_format`参数(支持Provider/Tool两种策略),将模型“散文式”自然语言输出,强制约束为可直接`save()`的Pydantic等结构化对象,避免正则解析、JSON清洗等脆弱逻辑;强调Schema声明、校验重试与提示词协同,实现从文本到数据库记录的端到端数据管道。

一、散文输出的代价

上一篇把工具比作 Agent 的"手脚",讲的是怎么让它做事。但做事之后还有一环常被忽略:结果怎么交回给程序。

默认情况下,Agent 返回的是一段自然语言。"张三的消费等级是 VIP,最近一次购买在 1 月 15 日,累计消费一万五千美元,建议发送感谢邮件。"——人读着挺舒服,程序读着很痛苦。

于是就有了各种补救手段:正则、json.loads 套一层 try、让模型"请只返回 JSON"、再写一堆 if "```json" in text 的清洗逻辑。这些代码的共同结局是:在 demo 里跑得通,在上线后天天修。模型偶尔多写一句"好的,以下是结果:",正则就崩;字段顺序一变,解析就错;类型从数字变字符串,入库就报错。

LangChain 的答案是 response_format:把"要什么结构"声明出来,让框架去保证输出符合这个结构。Agent 的产出从一段散文,变成一个可以直接 save() 的对象。

对 Java 工程师,这套东西并不陌生:它相当于把 Jackson 反序列化、Bean Validation 校验、失败重试三件事,打包成一次声明。你写 DTO,框架负责让模型按 DTO 说话。

二、一个参数,四种取值

create_agent 的 response_format 支持四种配置:

取值 含义 适用
ProviderStrategy(Schema) 用模型厂商的原生结构化输出 OpenAI / Claude / Grok 等(中转 key 多不支持)
ToolStrategy(Schema) 动态创建一个"虚拟工具"接住数据 任何支持工具调用的模型,兼容性最好
Schema(直接传类) 按模型能力自动选上面两者 LangChain 1.2 仍可用,1.0 起官方建议显式指定
None 默认,自然语言回复 闲聊、解释类场景

无论选哪条路,结果都落在同一个地方:result["structured_response"]。

result = agent.invoke({
   "messages": [{
   "role": "user", "content": "分析客户张三"}]})
analysis = result["structured_response"]   # 已经是 CustomerAnalysis 实例,不是字符串
print(analysis.customer_tier)              # VIP客户

这一行 result["structured_response"] 是本文的关键。它是 Agent 状态里的一个独立键,和 messages 并列——结构化结果不进对话历史,只进状态。想清楚这一点,后面很多坑就通了。

顺带澄清一个常见混淆:很多模型提供 response_format={"type": "json_object"} 这样的 JSON mode,它只保证"输出是合法 JSON",不保证"字段名、类型、枚举值符合你的约定"。JSON mode 解决的是语法问题,response_format=ToolStrategy(Schema) 解决的是语义与结构问题。前者你还要自己校验,后者框架替你校验并重试。这是两件事,别混着用。

三、两条技术路线:约束解码 与 虚拟工具

ProviderStrategy 和 ToolStrategy 的差别,不是"配置写法不同",而是约束发生在哪一层。

在这里插入图片描述

ProviderStrategy 走模型侧。 厂商 API 支持把 JSON Schema 作为解码约束传进去,模型在生成每个 token 时就被限制在合法取值内——customer_tier 只可能吐出枚举里的四个值之一,根本"说不出来"别的。这是最硬的保证,代价是强依赖厂商能力,中转 key、私有化部署的模型基本用不了。

ToolStrategy 走框架侧。 它动态造一个工具,名字随便取,参数 schema 就是你要的结构。模型想交付结果,就必须"调用"这个工具并填参数——而工具调用本来就是结构化 JSON。框架接住这次调用,用 schema 校验,通过就写入 structured_response。

两条路殊途同归,但容错空间不同:Provider 策略下模型几乎不会产出非法结构;Tool 策略下模型仍可能填错类型、漏字段、或者一次返回多个结果——所以 ToolStrategy 才需要 handle_errors 这套重试机制,而 ProviderStrategy 基本用不上。

官方的推荐也很务实:能用 Provider 就用 Provider,用不了就 ToolStrategy,而后者覆盖了绝大多数国产模型和自建模型——这也是本文后续的重点。

四、ToolStrategy 的三个参数与四种 Schema

ToolStrategy 的配置只有三个参数,但每一个都值得说清:

ToolStrategy(
    schema=CustomerAnalysis,          # 必需:结构定义
    tool_message_content="分析完成",   # 可选:写进对话历史的提示语
    handle_errors=True,               # 可选:校验失败怎么办,默认 True
)

schema 支持四种写法,这是最容易选错的地方。

4.1 Pydantic:默认首选

from typing import Literal
from pydantic import BaseModel, Field, field_validator
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy

class CustomerAnalysis(BaseModel):
    """客户分析报告"""
    customer_name: str = Field(description="客户姓名")
    customer_tier: Literal["潜在客户", "普通客户", "VIP客户", "流失风险"] = Field(
        description="客户等级,只能是这四个值之一"
    )
    recent_activity: str = Field(default="", description="最近一次活动,如 2024-01-15 购买")
    spending_level: Literal["低", "中", "高"] = Field(description="消费水平")
    need_follow_up: bool = Field(default=False, description="是否需要跟进")

    @field_validator("spending_level")
    def check_level(cls, v: str) -> str:
        if v not in ("低", "中", "高"):
            raise ValueError("消费水平必须是 低/中/高")
        return v

agent = create_agent(
    model=deepseek_llm,
    tools=[search_customer_database, send_email],
    system_prompt="""分析指定客户:
1. 先搜索客户数据库获取最新记录
2. 若为 VIP 客户,发送感谢邮件
3. 基于结果输出结构化分析报告
4. 若查不到该客户,返回空对象,不要反复尝试""",
    response_format=ToolStrategy(CustomerAnalysis),
)

result = agent.invoke({
   "messages": [{
   "role": "user", "content": "请分析客户张三"}]})
if "structured_response" in result:
    print(result["structured_response"].model_dump_json(indent=2))

Pydantic 的优势在校验:Literal 把取值钉死,ge/le 能约束数值范围,field_validator 能做业务规则。校验失败时抛的是 StructuredOutputValidationError,会被 handle_errors 接住并回给模型重试——这条回路下一节展开。

4.2 dataclass / TypedDict:轻量但无校验

from dataclasses import dataclass, field
from typing import Optional, Literal

@dataclass
class CustomerAnalysis:
    """客户分析报告"""
    customer_name: Optional[str] = field(default=None, metadata={
   "description": "客户姓名"})
    customer_tier: Literal["潜在客户", "普通客户", "VIP客户", "流失风险"] = field(
        default="潜在客户", metadata={
   "description": "客户等级"}
    )
    spending_level: Optional[Literal["低", "中", "高"]] = field(
        default=None, metadata={
   "description": "消费水平"}
    )

TypedDict 用 Annotated[类型, 默认值, "描述"] 表达同样的东西。两者的共同短板是没有运行时校验:字段类型不对、枚举越界,框架不会拦,直接进你的业务代码。省下的依赖,要用下游的防御代码补回来。

4.3 JSON Schema:跨语言的公共契约

CUSTOMER_SCHEMA = {
   
    "title": "CustomerAnalysis",
    "type": "object",
    "description": "客户分析报告",
    "properties": {
   
        "customer_name": {
   "type": "string", "description": "客户姓名"},
        "customer_tier": {
   
            "type": "string",
            "enum": ["潜在客户", "普通客户", "VIP客户", "流失风险"],
            "description": "客户等级",
        },
        "spending_level": {
   "type": "string", "enum": ["低", "中", "高"], "description": "消费水平"},
    },
    "required": ["customer_name", "customer_tier", "spending_level"],
}

agent = create_agent(model=deepseek_llm, tools=[], response_format=ToolStrategy(CUSTOMER_SCHEMA))

适合 schema 由配置下发、或要和 Java 侧共用一份契约文件的场景。注意 required 要老实填——写成空数组又指望模型别漏字段,是靠运气。

4.4 四种写法怎么选

Schema 类型 运行时校验 额外依赖 推荐场景
Pydantic 完整(类型/枚举/自定义规则) 需装 pydantic 生产默认首选
dataclass 无 标准库 内部脚本、结构极简
TypedDict 无 typing_extensions 只需类型提示的快速原型
JSON Schema 按关键字部分生效 无 配置化、跨语言共享契约

一句话:只要数据要入库、要传给下游,就用 Pydantic。校验不是负担,是把错误挡在业务代码之外。

4.5 tool_message_content:顺手省一笔 token

默认情况下,虚拟工具调用成功后,那条 ToolMessage 里装的是完整的数据块:

ToolMessage(content="{'customer_name': '张三', 'customer_tier': 'VIP客户', ...}")

它会原样留在对话历史里。数据小无所谓,字段一多、列表一长,每轮推理都要重读一遍。tool_message_content 允许你换成一句短提示:

response_format=ToolStrategy(
    CustomerAnalysis,
    tool_message_content="客户分析完成",   # 历史里只留这一句
)

两个收益:对话流对最终用户更干净(不会在聊天窗口里蹦出一坨原始 JSON),上下文也省下来了。而无论这里填什么,结构化数据都会完整写进 structured_response——这句话的意思是:tool_message_content 只影响历史里那一条记录,不影响你要的数据。这是个很实用的解耦。

五、校验失败之后:handle_errors 五档

模型不是编译器。它可能把 rating 填成 10(你限的是 1–5)、把枚举值写成"复杂"、或者在 Union 场景下一次返回两个结构。handle_errors 决定这些情况怎么处理:

取值 行为 适用
True(默认) 捕获所有异常,用内置错误模板提示模型重试 绝大多数场景
False 不捕获,直接抛异常中断 希望快速失败、暴露问题
"固定字符串" 捕获所有异常,但用你写的提示语回给模型 需要统一话术
ExceptionType 只重试指定异常,其他照抛 精准控制
callable 自定义函数,按异常类型给不同提示 复杂业务

它的工作回路和上一篇的工具错误处理同源:失败 → 生成一条 ToolMessage → 模型读到错误 → 修正参数重来。

在这里插入图片描述

自定义 handler 能拿到具体异常类型,从而给出差异化提示:

from langchain.agents.structured_output import (
    ToolStrategy, StructuredOutputValidationError, MultipleStructuredOutputsError,
)

def custom_error_handler(error: Exception) -> str:
    if isinstance(error, StructuredOutputValidationError):
        return "输出不符合 Schema:rating 必须是 1-5 的整数,sentiment 只能是 正面/负面/中性。请重新分析。"
    if isinstance(error, MultipleStructuredOutputsError):
        return "一次只能返回一个结构,请选择最匹配的那个 Schema 重新输出。"
    return f"结构化输出失败:{error}。请检查字段后重试。"

agent = create_agent(
    model=deepseek_llm,
    tools=[],
    response_format=ToolStrategy(ProductEvaluation, handle_errors=custom_error_handler),
)

MultipleStructuredOutputsError 这个类型值得单独记一笔:它只在 Union[SchemaA, SchemaB] 场景出现——模型同时返回了两种结构,而框架只接受一个。默认的 True 恰好能处理它,一旦你设成 False,这里就会直接崩。

Union 的用法本身很有价值:一次调用同时完成"分类 + 提取"。

from typing import Union

agent = create_agent(
    model=deepseek_llm,
    tools=[],
    response_format=ToolStrategy(
        Union[ContactInfo, EventDetails],
        tool_message_content="提取完成",
        handle_errors=True,
    ),
)

给一段混杂文本,模型自己判断这是"联系人"还是"活动",再按对应结构输出。相当于把路由逻辑塞进了 schema 层,省掉一次分类调用。

但重试是有代价的,这点文档里不会写。每一次校验失败都意味着多一轮完整的模型调用——多付一份 token、多几百毫秒延迟。如果你的 Schema 写得太苛刻(比如要求模型从一段百字文本里提取三个必填字段,还都要精确格式),很容易看到"重试 3 次才成功"的日志。

几条经验:Schema 里必填字段越少越好,能设默认值就别设 required;枚举值给中文标签("已解决")比给英文码("RESOLVED")更容易命中;真遇到反复失败的字段,先改提示词把源数据说清楚,而不是加大重试次数。重试是安全网,不是主力方案。

六、两个上线才会遇到的坑

坑一:提示词里把"输出结构化结果"写太前,工具就不调了。

模型的注意力是有限的。如果 system prompt 开头就强调"请输出结构化报告",它很可能跳过搜索、跳过发邮件,直接凭想象填一份报告——格式完美,数据全是编的。正确做法是把结构化输出的要求放在提示词最后,前面留给流程步骤。

坑二:查不到数据时,Agent 会陷入工具死循环。

用户问"分析客户王五",数据库里没有这个人。模型不甘心,换个关键词再查、再查、再查,直到撞上递归上限。解决办法是在提示词里显式给出兜底路径:

如果查不到该客户信息,直接返回空对象,不要重复调用工具。

配上 required 字段留空、字段给 default="",这条路径才能真正走通。

这两个坑的共同点:它们都不是 API 层面的问题,而是提示词与 Schema 的配合问题。结构化输出降低了"解析"的难度,但没有降低"让模型按流程办事"的难度。

再补一个排查清单:拿到结果发现 "structured_response" not in result,通常是三种原因。一是提示词没有要求"输出结构化报告",模型直接用自然语言答完了,压根没调那个虚拟工具;二是流程里工具调用失败或陷入循环,被 recursion_limit 截断,没走到输出环节;三是 handle_errors=False 且校验失败,异常在 invoke 时就抛了,你根本没拿到 result。三种情况的日志特征完全不同,别一上来就怀疑 Schema 写错了。

七、落地:从一段文本到一条数据

把视角拉远一点。结构化输出真正的价值,不在"少写几行解析代码",而在于它让 Agent 的产出可以直接进入数据管道。

在这里插入图片描述

左边是旧路径:自然语言 → 正则/字符串切割 → 字段缺失、类型漂移 → 人工兜底。每一步都在赌模型的措辞习惯。

右边是新路径:声明 Schema → 框架校验 → Pydantic 对象 → model_dump() 成 dict → 写 Kudu/Hive/MySQL、发 Kafka、调下游 API。对大数据工程师,这意味着 Agent 可以直接产出带 schema 的宽表记录;对 Java 工程师,这意味着 Agent 的返回能被 @RequestBody 直接接住。

analysis = result["structured_response"]
record = analysis.model_dump()          # {'customer_name': '张三', 'customer_tier': 'VIP客户', ...}

对应到服务端,就是这样一个 DTO——字段名和类型与 Python 侧一一对应,中间不需要任何字符串解析:

public record CustomerAnalysis(
        String customerName,        // 与 customer_name 对齐(Jackson 命名策略)
        String customerTier,
        String recentActivity,
        String spendingLevel,
        boolean needFollowUp) {
   }

@PostMapping("/agent/customer-analysis")
public CustomerAnalysis analyze(@RequestBody AnalyzeRequest req) {
   
    return agentClient.invoke(req);   // Python 侧返回的 JSON 直接反序列化
}

Java 侧再叠一层 Bean Validation(@NotNull、@Pattern),就形成了双端校验:Python 保证模型输出合规,Java 保证调用方传参合规。这套组合在生产里很稳。

上线前的检查清单:

  • Schema 用 Pydantic,枚举用 Literal,数值用 ge/le,业务规则用 field_validator;
  • 字段描述写全,Field(description=...) 是模型填参的唯一依据;
  • handle_errors 保持 True,除非你确实想让异常炸出来;
  • 提示词里给兜底:查不到、无关、超范围时输出什么,必须写明;
  • 结构化要求放最后,流程步骤放前面;
  • 读取时判空:result.get("structured_response") 可能为 None(模型没走结构化路径),别直接下标取值。
目录
相关文章
|
14天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
6141 45
|
2天前
|
云安全 人工智能 安全
|
15天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
5099 60
|
9天前
|
Linux 开发工具 iOS开发
2026最新codex下载安装保姆级使用教程(下载+安装+登录+使用,含所有历史版本)
本文详解OpenAI Codex国内快速上手指南:提供Win/macOS/Linux全平台免安装包、npm/Homebrew等多方式安装、账号/API Key双登录、简体中文切换,覆盖下载、安装、汉化、使用及高频避坑(如微软商店失败、npm超时等),10分钟即可跑通。
2442 6
|
1天前
|
JSON 前端开发 API
阿里云最新Qwen4-Max大模型介绍:模型定位、特征、能力、适用场景解析
2026 年 11 月 11 日,阿里发布第四代旗舰模型Qwen4-Max预览版,首发千问 AI 平台 Token Plan,并同步接入千问办公、Qoder 等入口。该版本聚焦知识理解、世界感知、编程与 Agentic 四项升级,主打复杂编程、专业办公、前端设计与专业软件操作。预览期至 11 月 25 日 24:00,到期后模型标识下线;限时 2 折、新用户再享 5 折,最低 1 折可体验。
|
7天前
|
人工智能 安全 API
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(dsh)是DeepSeek AI于2026年开源的轻量级Agent运行框架,基于Cordis内核,践行“一切皆插件”理念。支持Web/桌面双端,开箱即用;内置多模型接入、丰富插件生态(超3.5万),适配编程、办公、创作等场景。(239字)
1535 1
|
2天前
|
人工智能 前端开发 API
阿里云Qwen4-Max 大模型详解:定位、特征、能力、适用场景与相对Qwen3-Max差异解析
2026年11月11日,阿里发布第四代旗舰模型Qwen4-Max预览版,首发千问AI平台Token Plan并接入千问办公、Qoder等入口。该版本聚焦知识理解、世界感知、编程与Agentic四项升级,主打复杂编程、专业办公、前端设计与专业软件操作等真实任务。预览期为11月11日10:00至11月25日24:00,仅支持Token Plan调用,到期后模型标识下线;限时2折、新用户再享5折,最低1折可体验。

热门文章

最新文章