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


系列导航

相关推荐
CoderJia程序员甲2 小时前
GitHub 热榜项目 - 周榜(2026-09-26)
ai·大模型·llm·github
熊猫钓鱼>_>10 小时前
MetaAI深度研究研究报告
ai·meta·大模型·llm·agent·web·metaai
老A的AI实验室11 小时前
赛博月刊 #2026年9月
大数据·人工智能·深度学习·ai·llm
用户31346721435411 小时前
Agent相关-文档加载器 Document Loader
langchain·llm
海天一色y14 小时前
AI 大模型算法岗面试题库
llm·rmsnorm·flash-attention·decoder-only
吃饱了得干活1 天前
Agent 的核心组件:大脑、记忆、手脚与心跳
llm·agent
孟健1 天前
Gemini 4 Argon 对比 GPT-6 Astra:百万 Token 输出很诱人,但我劝你先别迁编程工作流
人工智能·llm·ai编程
柒和远方2 天前
LangSmith RAG 量化评估:从"感觉还行"到"数据说话"
langchain·llm·测试
银河技术2 天前
TB级文本去重实战:从单机 OOM 到 Spark / Ray 分布式架构的工程演进
分布式·微服务·重构·架构·spark·llm·rag
流浪0012 天前
大模型技术全景(十三):记忆管理 Memory
llm·memory·记忆