差别
这里会显示出您选择的修订版和当前版本之间的差别。
| 两侧同时换到之前的修订记录 前一修订版 | |||
| 智能体二次开发:langchain:核心能力详解_structuredoutput [2026/05/20 19:02] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | 智能体二次开发:langchain:核心能力详解_structuredoutput [2026/05/20 19:02] (当前版本) – ↷ 页面langchain二次开发:核心能力详解_structuredoutput被移动至智能体二次开发:langchain:核心能力详解_structuredoutput 张叶安 | ||
|---|---|---|---|
| 行 1: | 行 1: | ||
| + | ====== 第八章:核心能力详解 - Structured Output ====== | ||
| + | |||
| + | 在真实业务中,我们很多时候并不希望模型“自由发挥”写一段话,而是希望它返回一个可以直接被程序消费的结构。例如: | ||
| + | * 工单分类结果; | ||
| + | * 会议纪要行动项; | ||
| + | * 风险评估结果; | ||
| + | * 审批建议; | ||
| + | * SQL 查询参数; | ||
| + | * 页面渲染 JSON。 | ||
| + | |||
| + | 这时就需要结构化输出(Structured Output)。它的意义不在于“让 JSON 更漂亮”,而在于:**让大模型输出进入可校验、可测试、可组合、可自动化处理的工程流程。** | ||
| + | |||
| + | 本章会系统讲解结构化输出的价值、Schema 设计、LangChain 中的 `response_format` 用法、校验与重试、联合类型、错误处理,以及多个可直接上手的代码案例。 | ||
| + | |||
| + | ===== 8.1 为什么结构化输出如此关键 ===== | ||
| + | |||
| + | ==== 8.1.1 自然语言不利于程序消费 ==== | ||
| + | |||
| + | 自然语言适合给人读,但对程序极不友好: | ||
| + | * 字段顺序不稳定; | ||
| + | * 容易漏项; | ||
| + | * 需要脆弱的字符串解析; | ||
| + | * 很难直接做数据校验。 | ||
| + | |||
| + | 例如你要做“工单分流”,程序真正需要的不是一段解释,而是这些字段: | ||
| + | * 工单类别; | ||
| + | * 优先级; | ||
| + | * 是否需要人工升级; | ||
| + | * 处理部门; | ||
| + | * 处理原因。 | ||
| + | |||
| + | ==== 8.1.2 结构化输出让链路更可控 ==== | ||
| + | |||
| + | 它带来几个直接收益: | ||
| + | * 可做字段级校验; | ||
| + | * 可直接落库、检索、统计; | ||
| + | * 可作为后续工具调用输入; | ||
| + | * 可作为工作流条件分支; | ||
| + | * 可记录失败率、缺失率、重试率。 | ||
| + | |||
| + | ===== 8.2 在 LangChain 中定义 Schema ===== | ||
| + | |||
| + | 根据 LangChain 官方文档,`response_format` 支持多种 Schema 表达方式,最常见的是 Pydantic 模型和 TypedDict。对于教程和企业项目来说,Pydantic 最直观。 | ||
| + | |||
| + | ==== 8.2.1 定义一个基础 Schema ==== | ||
| + | |||
| + | <code python> | ||
| + | from pydantic import BaseModel, Field | ||
| + | from typing import Literal | ||
| + | |||
| + | class TicketAnalysis(BaseModel): | ||
| + | category: str = Field(description=" | ||
| + | priority: Literal[" | ||
| + | summary: str = Field(description=" | ||
| + | need_human: bool = Field(description=" | ||
| + | </ | ||
| + | |||
| + | ==== 8.2.2 字段描述不是装饰品 ==== | ||
| + | |||
| + | 字段描述越清晰,模型越容易稳定生成。 | ||
| + | |||
| + | 例如: | ||
| + | * 坏字段:`priority: | ||
| + | * 好字段:`priority: | ||
| + | |||
| + | 描述中最好补充: | ||
| + | * 取值范围; | ||
| + | * 单位; | ||
| + | * 长度限制; | ||
| + | * 业务含义; | ||
| + | * 是否为最终用户展示字段。 | ||
| + | |||
| + | ===== 8.3 使用 `create_agent(..., | ||
| + | |||
| + | LangChain 官方结构化输出文档给出的现代方法之一,是在 Agent 中直接指定 `response_format`。 | ||
| + | |||
| + | ==== 8.3.1 最小示例 ==== | ||
| + | |||
| + | <code python> | ||
| + | from langchain.agents import create_agent | ||
| + | |||
| + | agent = create_agent( | ||
| + | model=" | ||
| + | tools=[], | ||
| + | response_format=TicketAnalysis, | ||
| + | system_prompt=" | ||
| + | ) | ||
| + | |||
| + | result = agent.invoke({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }] | ||
| + | }) | ||
| + | |||
| + | print(result[" | ||
| + | </ | ||
| + | |||
| + | 此时 `structured_response` 通常就是一个 `TicketAnalysis` 实例,而不再是一段自然语言文本。 | ||
| + | |||
| + | ==== 8.3.2 结构化响应的好处 ==== | ||
| + | |||
| + | 一旦你获得稳定的结构化结果,就可以直接做: | ||
| + | * 工单路由; | ||
| + | * 告警升级; | ||
| + | * 自动打标签; | ||
| + | * 指标统计; | ||
| + | * 与数据库 / 工单系统对接。 | ||
| + | |||
| + | ===== 8.4 ProviderStrategy 与 ToolStrategy ===== | ||
| + | |||
| + | 根据官方文档,LangChain 会根据模型能力选择合适的结构化输出策略: | ||
| + | * **ProviderStrategy**:如果模型提供商原生支持结构化输出,则优先使用 provider 原生能力; | ||
| + | * **ToolStrategy**:如果原生不支持,则通过工具调用机制实现结构化输出。 | ||
| + | |||
| + | 对使用者来说,大多数时候可以直接写: | ||
| + | |||
| + | <code python> | ||
| + | agent = create_agent( | ||
| + | model=" | ||
| + | tools=[], | ||
| + | response_format=TicketAnalysis | ||
| + | ) | ||
| + | </ | ||
| + | |||
| + | 让 LangChain 自动决定底层策略。 | ||
| + | |||
| + | ===== 8.5 联合类型与多种输出模式 ===== | ||
| + | |||
| + | 有些场景不是只有一种结构化输出,而是根据输入不同返回不同类型。结构化输出文档中也支持 `Union` 这类模式。 | ||
| + | |||
| + | ==== 8.5.1 示例:客服分流 ==== | ||
| + | |||
| + | <code python> | ||
| + | from typing import Union | ||
| + | from pydantic import BaseModel, Field | ||
| + | |||
| + | class RefundRequest(BaseModel): | ||
| + | type: str = Field(default=" | ||
| + | order_id: str | ||
| + | reason: str | ||
| + | |||
| + | class TechSupportRequest(BaseModel): | ||
| + | type: str = Field(default=" | ||
| + | issue: str | ||
| + | severity: str | ||
| + | |||
| + | RoutingSchema = Union[RefundRequest, | ||
| + | </ | ||
| + | |||
| + | 然后将其作为 `response_format`: | ||
| + | |||
| + | <code python> | ||
| + | router_agent = create_agent( | ||
| + | model=" | ||
| + | tools=[], | ||
| + | response_format=RoutingSchema, | ||
| + | system_prompt=" | ||
| + | ) | ||
| + | </ | ||
| + | |||
| + | 这类设计很适合: | ||
| + | * 多分支工作流路由; | ||
| + | * 不同工具链入口选择; | ||
| + | * Agent 内部任务拆分。 | ||
| + | |||
| + | ===== 8.6 结构化输出案例一:会议纪要行动项提取 ===== | ||
| + | |||
| + | ==== 8.6.1 定义 Schema ==== | ||
| + | |||
| + | <code python> | ||
| + | from pydantic import BaseModel, Field | ||
| + | from typing import List, Literal | ||
| + | |||
| + | class ActionItem(BaseModel): | ||
| + | owner: str = Field(description=" | ||
| + | task: str = Field(description=" | ||
| + | deadline: str = Field(description=" | ||
| + | priority: Literal[" | ||
| + | |||
| + | class MeetingSummary(BaseModel): | ||
| + | topic: str = Field(description=" | ||
| + | decisions: List[str] = Field(description=" | ||
| + | action_items: | ||
| + | </ | ||
| + | |||
| + | ==== 8.6.2 调用模型 ==== | ||
| + | |||
| + | <code python> | ||
| + | meeting_agent = create_agent( | ||
| + | model=" | ||
| + | tools=[], | ||
| + | response_format=MeetingSummary, | ||
| + | system_prompt=" | ||
| + | ) | ||
| + | |||
| + | result = meeting_agent.invoke({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 会议主题:四月产品发布准备 | ||
| + | 决定:本周完成官网上线文案终稿;演示视频由市场部统一剪辑。 | ||
| + | 待办: | ||
| + | - 张三在 4 月 8 日前完成官网首页文案定稿; | ||
| + | - 李四本周内确认演示视频脚本; | ||
| + | - 王五跟进展会物料制作,优先级高。 | ||
| + | """ | ||
| + | }] | ||
| + | }) | ||
| + | |||
| + | print(result[" | ||
| + | </ | ||
| + | |||
| + | ===== 8.7 结构化输出案例二:风险审核结果 ===== | ||
| + | |||
| + | 有些场景需要的不仅是“分类”,而是可直接驱动业务流程的审核结果。 | ||
| + | |||
| + | <code python> | ||
| + | from typing import Literal | ||
| + | |||
| + | class RiskReview(BaseModel): | ||
| + | risk_level: Literal[" | ||
| + | need_manager_approval: | ||
| + | blocked: bool | ||
| + | reasons: list[str] | ||
| + | recommendation: | ||
| + | </ | ||
| + | |||
| + | 对应调用: | ||
| + | |||
| + | <code python> | ||
| + | risk_agent = create_agent( | ||
| + | model=" | ||
| + | tools=[], | ||
| + | response_format=RiskReview, | ||
| + | system_prompt=" | ||
| + | ) | ||
| + | </ | ||
| + | |||
| + | 这类结构化输出非常适合接工作流: | ||
| + | * `blocked=True` → 直接结束并提示用户; | ||
| + | * `need_manager_approval=True` → 进入审批分支; | ||
| + | * `risk_level=low` → 自动流转。 | ||
| + | |||
| + | ===== 8.8 校验、重试与错误处理 ===== | ||
| + | |||
| + | 结构化输出不意味着永远零错误。即使启用了 Schema,仍可能出现: | ||
| + | * 字段缺失; | ||
| + | * 字段值不合法; | ||
| + | * 超长文本塞进短字段; | ||
| + | * 模型把解释性文字混进字段; | ||
| + | * 在复杂边界案例中分类不稳定。 | ||
| + | |||
| + | ==== 8.8.1 使用 `ToolStrategy(..., | ||
| + | |||
| + | 官方文档中提到,可以通过 `ToolStrategy` 设置错误处理行为,例如定制校验失败后如何反馈给模型。 | ||
| + | |||
| + | <code python> | ||
| + | from langchain.agents.structured_output import ToolStrategy | ||
| + | |||
| + | agent = create_agent( | ||
| + | model=" | ||
| + | tools=[], | ||
| + | response_format=ToolStrategy( | ||
| + | schema=TicketAnalysis, | ||
| + | handle_errors=" | ||
| + | ) | ||
| + | ) | ||
| + | </ | ||
| + | |||
| + | ==== 8.8.2 业务层二次校验 ==== | ||
| + | |||
| + | 即便模型已经返回结构化对象,业务层仍建议做二次校验: | ||
| + | |||
| + | <code python> | ||
| + | def validate_ticket(result: | ||
| + | if result.priority == " | ||
| + | raise ValueError(" | ||
| + | </ | ||
| + | |||
| + | 这类业务校验适合补足“Schema 能校验格式,但无法校验业务合理性”的缺口。 | ||
| + | |||
| + | ===== 8.9 结构化输出案例三:电商评论抽取 ===== | ||
| + | |||
| + | <code python> | ||
| + | class ReviewExtraction(BaseModel): | ||
| + | sentiment: Literal[" | ||
| + | pros: list[str] | ||
| + | cons: list[str] | ||
| + | need_followup: | ||
| + | followup_reason: | ||
| + | |||
| + | review_agent = create_agent( | ||
| + | model=" | ||
| + | tools=[], | ||
| + | response_format=ReviewExtraction, | ||
| + | system_prompt=" | ||
| + | ) | ||
| + | |||
| + | result = review_agent.invoke({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }] | ||
| + | }) | ||
| + | |||
| + | print(result[" | ||
| + | </ | ||
| + | |||
| + | 这种结果可以被后续系统直接用于: | ||
| + | * 评论标签统计; | ||
| + | * 差评预警; | ||
| + | * 自动客服跟进。 | ||
| + | |||
| + | ===== 8.10 结构化输出设计的实践建议 ===== | ||
| + | |||
| + | ==== 8.10.1 字段宁少勿乱 ==== | ||
| + | |||
| + | 初期不要试图一次抽取 30 个字段。建议从真正驱动业务流程的 3~8 个字段开始。 | ||
| + | |||
| + | ==== 8.10.2 尽量用枚举而不是自由文本 ==== | ||
| + | |||
| + | 例如: | ||
| + | * `priority`: `low/ | ||
| + | * `status`: `todo/ | ||
| + | * `sentiment`: | ||
| + | |||
| + | 这样更便于后续流程判断。 | ||
| + | |||
| + | ==== 8.10.3 将展示文案与业务字段拆开 ==== | ||
| + | |||
| + | 例如风险审核结果可以拆成: | ||
| + | * `risk_level`:供系统判断; | ||
| + | * `recommendation`:供界面展示; | ||
| + | * `reasons`:供审计记录。 | ||
| + | |||
| + | ===== 8.11 常见失败模式 ===== | ||
| + | |||
| + | * 让模型同时“写解释 + 输出 JSON”,导致结构污染; | ||
| + | * 字段过多、描述过弱,模型频繁漏值; | ||
| + | * 以为有 Schema 就不需要业务校验; | ||
| + | * 结构化输出后,下游系统仍然解析自然语言; | ||
| + | * 未记录失败样例,导致同类问题反复出现。 | ||
| + | |||
| + | ===== 8.12 本章小结 ===== | ||
| + | |||
| + | * 结构化输出是把模型接入业务流程的关键能力; | ||
| + | * `response_format` 是 LangChain 中最值得掌握的结构化输出入口之一; | ||
| + | * ProviderStrategy 与 ToolStrategy 让结构化输出具备更好的兼容性; | ||
| + | * 结构化输出必须搭配校验、重试和业务规则; | ||
| + | * 它特别适合分类、抽取、工作流路由和 Agent 中间状态管理。 | ||
| + | |||
| + | ===== 练习 ===== | ||
| + | |||
| + | 1. 定义一个“会议纪要行动项”Schema,至少包含负责人、任务、截止时间、优先级 4 个字段。 | ||
| + | |||
| + | 2. 用 `create_agent(..., | ||
| + | |||
| + | 3. 设计一个联合类型 `Union` Schema,用于区分“退款请求”和“技术支持请求”。 | ||
| + | |||
| + | 4. 写一个二次校验函数,检查结构化输出的字段是否满足你的业务规则。 | ||
| + | |||
| + | 5. 将一个自然语言分析流程改造成结构化输出流程,并说明这样做对后续系统有什么帮助。 | ||
| + | |||
| + | ===== 参考资源 ===== | ||
| + | |||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | |||