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),所以兜底的本地校验永远值得留一层。


系列导航

相关推荐
武子康1 小时前
小智发出 abort 后,旧声音为什么还可能继续?
人工智能·llm·agent
七夜zippoe2 小时前
LLM 选型指南:Agent 场景下大模型的核心能力评估框架
ai·大模型·llm·agent·核心 能力
XLYcmy2 小时前
ReasoningBank: Scaling Agent Self-Evolving with Reasoning Memory论文阅读
论文阅读·google·llm·agent·记忆·harness·自进化
武子康2 小时前
SGLang 和 vLLM,拿自己的请求测一轮再选
人工智能·llm·agent
桃西西呀5 小时前
买房怕买贵?我把真实成交价丢进决策树,看它到底按什么给房子定价
人工智能·机器学习·llm
今日无bug6 小时前
为什么大模型回答总是一个字一个字蹦出来?聊聊 SSE 流式输出与 BFF 层的那些事
前端·llm
桃西西呀6 小时前
苹果刚发布 iPhone Duo,可 3104 款手机参数一聚类,参数分了档,价格却不匹配
人工智能·机器学习·llm
程序猿编码7 小时前
LLM 技术迁移音频领域:基于 GGML 搭建 Roformer,端侧音乐人声分离实现详解
c++·llm·音视频·transformer·模型推理·ggml
熊猫钓鱼>_>19 小时前
免费域名的隐秘陷阱与网站稳定部署实战:火山引擎到底行不行?
人工智能·ai·llm·域名·火山引擎·引擎·火山