withStructuredOutput 一行封装的背后:method 三选一 + 方法总表

系列第 4 篇。第 3 篇手写 bindTools 拿到了 tool_calls.args,但你看------要自己建工具对象、自己判空、自己担心模型不配合。LangChain 把这些都封装进了一个方法:withStructuredOutput。

一行替代手写版

js 复制代码
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";

const model = new ChatOpenAI({ /* 照旧 */ });

const scientistSchema = z.object({
  name: z.string().describe("科学家的姓名"),
  birth_year: z.number().describe("出生年份"),
  nationality: z.string().describe("国籍"),
  fields: z.array(z.string()).describe("研究领域列表"),
  major_achievements: z.array(z.string()).describe("主要成就列表"),
});

const structuredModel = model.withStructuredOutput(scientistSchema, {
  method: "functionCalling",  // ⚠️ 见下文:这个参数很关键
  includeRaw: true,           // true → 返回 { raw, parsed }
});

const { parsed, raw } = await structuredModel.invoke("介绍一下爱因斯坦");

它的内部(源码可见)正好就是第 3 篇手写那套:bindTools 注册 schema 为工具 → invoke → 从 tool_calls 里按名字找到那个调用 → 取出 .args → 再按 schema 校验。你手写一遍,它封装一行,还额外带校验。

用 includeRaw: true 还能同时看到"校验好的对象"和"原始消息",数据来源一目了然:

js 复制代码
parsed = { name: "爱因斯坦", birth_year: 1879, nationality: "德国", ... }
raw.tool_calls[0] = { name: "extract", args: {...}, type: "tool_call", id: "call_xxx" }

parsed 就是框架从 raw.tool_calls[0].args 抽出来再校验的结果,两者同源 。没传工具名时默认叫 "extract"。

必知大坑:不传 method 直接 400

config.method 支持三个值(源码里 SUPPORTED_METHODS):

method 背后做了什么 硬在哪 门槛
functionCalling schema 当工具,答案走 tool_calls.args 工具参数语法强制 模型支持 tools(大多支持)
jsonMode response_format: {type:"json_object"} 平台保证 content 是合法 JSON 语法硬、schema 不管,本地再校验
jsonSchema response_format: {type:"json_schema",...} 原生结构化 解码端按 schema 强制 服务端要认这个参数

坑在这:库对不以 gpt-3/gpt-4 开头的模型名,不传 method 时默认返回 "jsonSchema"(源码逻辑:它假设非 gpt-3/4 = 现代模型都支持原生 json_schema)。

而你的 baseURL 背后若是 DeepSeek 这类 OpenAI 兼容服务,它大概率不认识 response_format: json_schema → 一调用就 400。

ini 复制代码
你:model.withStructuredOutput(schema)      // 没写 method
库:模型名不是 gpt-3/4 → 默认 method = "jsonSchema"
你 baseURL 服务:response_format json_schema?不认 → 400 ❌

对策:显式传 method: "functionCalling" (最普适,DeepSeek 支持 function calling,实测能跑通)。等确认服务端支持原生 json_schema 了,再换成 "jsonSchema" 升级到最硬档。

实测长这样

用 method: "functionCalling" 跑通后的真实返回:

js 复制代码
{
  name: "阿尔伯特·爱因斯坦",
  birth_year: 1879,               // number,不是字符串------schema 约束生效
  nationality: "德国",
  fields: ["理论物理学", "相对论", "量子力学"],
  major_achievements: ["提出狭义相对论", "提出广义相对论", ...]
}

注意 birth_year: 1879 是真数字、fields 是真数组------zod 的约束在两端都生效:既让模型按这个格式填表,又对结果做了逐项校验。

全景:确保拿到 JSON 对象的所有方法

把这一系列四篇的方法收进一张总表:

scss 复制代码
软 ←──────────────────────────────────────────────────→ 硬
prompt哄 → JSON.parse → JsonOutputParser → StructuredOutputParser
        → bindTools → jsonMode → json_schema(native) → withStructuredOutput
  靠模型自觉 ↑                          ↑ 靠协议层保证
   (可被打破)                (字段语义仍可能错 → 仍需校验兜底)
方法 数据落点 校验 一句话
prompt 哄 + JSON.parse content 无 模型裹围栏/夹废话就崩
JsonOutputParser content 无 剥围栏 + parse,宽容
StructuredOutputParser content zod 讲 schema + 严格验收(仍赌文本)
bindTools 手写 tool_calls.args 无(可自加) 通道级,但要自己抽 args
withStructuredOutput 工具槽 / response_format zod 一行封装 + method 可选

选型建议 :现代应用直接上 withStructuredOutput + method:"functionCalling";服务端支持原生再升 jsonSchema。纯流式逐字渲染场景才回头用 JsonOutputParser。模型太老不支持 tools 时,退回 StructuredOutputParser 文本派。

最后一句大实话

不存在"让模型保证输出 JSON"的单点魔法。 "确保"是个组合拳:① 选对通道(文本解析 or 结构槽)+ ② 本地 schema 校验 + ③ 失败重试。层级越高,越靠协议而不是靠运气;但哪怕最强的 jsonSchema,字段语义也可能错(年份填 3000 也是合法 number),所以兜底的本地校验永远值得留一层。


系列导航

相关推荐
狂潮lister3 小时前
第6篇 掀翻共识:注意力叠权才是一切,异源对撞破同源自噬|【狂潮元理论 2.1‑public】
人工智能·llm
先吃饱再说4 小时前
大模型总是不按格式输出?LLM 结构化输出的四种解法
人工智能·llm
吴凡wudianmei4 小时前
线上出问题不知道卡在哪?全链路追踪 + 三端统一部署
架构·langchain·llm
江屿风5 小时前
【Plain Language Large Model】【大语言模型底层原理全解析:从Token生成到智能体演进】流食般投喂
人工智能·笔记·语言模型·llm·json·token·聊天模板
databook5 小时前
让 LLM 输出不再失控:用 Pydantic 校验大模型输出
人工智能·python·llm
桃西西呀6 小时前
嘴替鸭小程序是怎么看懂一句话的:本地规则 + 大模型的混合引擎拆解
人工智能·llm·ai编程
xiezhr7 小时前
你的WorkBuddy不好用,大概率是这些设置没设好
llm·aigc·agent
MoonOut18 小时前
LLM | 大模型训练 / 部署语境下的常用显卡
llm
桃西西呀21 小时前
饭桌上又卡壳了?我用 TRAE 写了个嘴替鸭,把回话提前备好
人工智能·llm·ai编程
神也畏惧菠萝1 天前
Coze 工作流数据库 Day06:架构与高级循环控制的深度技术分析
数据库·架构·llm·coze·coze数据库·数据库流程结构