创见博客
大模型结构化输出:从提示词约定到 schema 约束
七崽爱吃小饼干2026/10/14阅读 0

在做一个能对接程序的大模型应用时,一个绕不开的需求是:让模型返回指定结构的数据——比如从一段话里抽出 name、age、city 三个字段,供后续代码使用。过去我们靠提示词「求」模型返回 JSON,再手动 JSON.parse;现在 LangChain 提供了一套更可靠的方案:结构化输出(Structured Output)。这篇文章讲清楚老办法的坑、新方案的思路,以及具体怎么用。

一、老办法为什么不可靠

要让模型返回 JSON,最直觉的做法是在提示词里写清楚:

js
const res = await model.invoke(
  "从这句话提取信息:张三,今年 30 岁,住在上海。请以 JSON 返回,字段为 name, age, city。"
);
const data = JSON.parse(res.content);

但实际跑一下会发现,模型经常这样返回:

```json
{
  "name": "张三",
  "age": 30,
  "city": "上海"
}
```

它用 Markdown 代码块把 JSON 包了起来,JSON.parse 直接报错。于是你不得不写正则去清理围栏:

js
const cleaned = String(res.content)
  .replace(/^```(?:json)?/i, "")
  .replace(/```$/, "")
  .trim();
const data = JSON.parse(cleaned);

即便这样能跑通,老办法仍然很脆,问题在于它只是**「请求」而非「强制」**:

  • 格式不稳定:可能带代码块围栏、前后夹着解释文字,需要各种正则兜底;
  • 字段不受控:模型可能把 name 写成 Name 或 姓名,把 age 返回成字符串 "30";
  • 没有校验:字段缺失、类型不对,代码要到运行时才炸,错误被推迟到很远的地方;
  • 没有类型:拿到的是一坨 any,TypeScript 给不了任何提示和补全。

归根结底:提示词是给模型的「建议」,而不是对输出结构的「保证」。

二、新方案的核心思路

新方案换了个思路:不再用提示词描述格式,而是把「你想要的形状」用 schema 表达出来,直接交给模型作为约束。

具体来说:

  1. 你用一个 schema(通常是 zod)定义输出的字段、类型和含义;
  2. LangChain 把它转成模型能理解的 JSON Schema;
  3. 模型据此产出结构化结果(底层要么借工具调用,要么用厂商原生的结构化输出能力);
  4. LangChain 自动把结果解析成对象,并用 schema 校验——不合法就报错,而不是悄悄给你脏数据。

最终你拿到的是一个类型安全的 JS 对象,可以直接 data.city 这样使用,不再有 JSON.parse 和正则清理。

三、用法一:withStructuredOutput

最简单的方式是给模型套一个 withStructuredOutput(schema),它会返回一个新的 Runnable,调用后直接得到对象:

js
import { z } from "zod";
import { initChatModel } from "langchain";

const ContactSchema = z.object({
  name: z.string().describe("人名"),
  age: z.number().describe("年龄(整数)"),
  city: z.string().describe("所在城市"),
});

const model = await initChatModel("deepseek:deepseek-chat", { apiKey });

const structured = model.withStructuredOutput(ContactSchema, {
  name: "extract_contact", // 底层函数名,可自定义
});

const result = await structured.invoke("从这句话提取信息:张三,今年 30 岁,住在上海。");
console.log(result);
// { name: '张三', age: 30, city: '上海' }
console.log(typeof result.age); // "number" —— 已经是 number,不是字符串

注意 .describe() 会写进 JSON Schema 的 description,帮助模型理解每个字段该怎么填。

includeRaw:同时拿到原始消息

有时你想看看底层到底发生了什么,可以用 includeRaw: true,它会返回 { raw, parsed }:

js
const structured = model.withStructuredOutput(ContactSchema, {
  name: "extract_contact",
  includeRaw: true,
});

const { raw, parsed } = await structured.invoke("从这句话提取信息:张三,今年 30 岁,住在上海。");
console.log(parsed);          // { name: '张三', age: 30, city: '上海' }
console.log(raw.content);     // ""   正文是空的
console.log(raw.tool_calls);  // [ { name: 'extract_contact', args: {...}, ... } ]

从 raw.tool_calls 能看出:默认方法其实是借一次工具调用,让模型把结果填进这个「工具」的参数里,再被解析出来。

三种底层方法(method)

withStructuredOutput 支持三种实现,按模型能力选择:

method原理特点
functionCalling(默认)借工具调用产出结构化结果兼容性最好,大多数模型都支持
jsonMode让模型输出 JSON 文本(response_format)依赖模型支持 JSON 模式
jsonSchema厂商原生的严格结构化输出约束最强,但大多只有 OpenAI 等支持
js
const byFC = model.withStructuredOutput(ContactSchema, { method: "functionCalling" });
const byJson = model.withStructuredOutput(ContactSchema, { method: "jsonMode" });

两个实战注意点:

  • jsonMode 可能要求提示词里出现「json」字样。比如 DeepSeek 的 json_object 模式,如果提示词里没有 json 就会报 400;而 functionCalling 没有这个限制,所以更通用。
  • jsonSchema / strict 只有部分厂商支持。DeepSeek 不支持原生严格模式,强行用会报错。不确定时直接用默认的 functionCalling 最稳妥。

四、用法二:在 Agent 里做结构化输出

进入 v1 的 Agent 世界后,结构化输出成了 createAgent 的一等参数:用 responseFormat 声明输出形状,结果放在 result.structuredResponse。

有两种策略:

toolStrategy(推荐,兼容性好)

借助工具调用来产出结构化结果:

js
import { createAgent, toolStrategy } from "langchain";
import { z } from "zod";

const ContactSchema = z.object({
  name: z.string().describe("人名"),
  age: z.number().describe("年龄(整数)"),
  city: z.string().describe("所在城市"),
});

const agent = createAgent({
  model: "deepseek:deepseek-chat",
  tools: [], // 本例只做抽取,不需要工具
  responseFormat: toolStrategy(ContactSchema),
});

const res = await agent.invoke({
  messages: [{ role: "user", content: "从这句话提取信息:张三,今年 30 岁,住在上海。" }],
});

console.log(res.structuredResponse);
// { name: '张三', age: 30, city: '上海' }

providerStrategy(依赖厂商原生能力)

如果你希望用厂商原生的严格结构化输出,可以用 providerStrategy:

js
import { providerStrategy } from "langchain";

const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [],
  responseFormat: providerStrategy(ContactSchema),
});
const res = await agent.invoke({ messages: [{ role: "user", content: "..." }] });
console.log(res.structuredResponse);

它会把 schema 直接交给模型厂商的原生结构化输出接口。代价是兼容性:如果厂商不支持(比如 DeepSeek),会直接报错 This response_format type is unavailable now。因此除非你确定目标模型支持,否则优先用 toolStrategy。

五、Python 中的实现

LangChain 在 Python 侧的能力和 JS 完全对齐,只是 schema 的载体换成了 Pydantic(对应 JS 里的 Zod)。上面讲的 with_structured_output 和 Agent 的 response_format 都有对应写法。

with_structured_output

用 Pydantic 的 BaseModel 定义字段,Field(description=...) 相当于 JS 里的 .describe():

python
from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model

class Contact(BaseModel):
    name: str = Field(description="人名")
    age: int = Field(description="年龄(整数)")
    city: str = Field(description="所在城市")

model = init_chat_model("deepseek:deepseek-chat", temperature=0)
structured = model.with_structured_output(Contact)

result = structured.invoke("从这句话提取信息:张三,今年 30 岁,住在上海。")
print(result)          # name='张三' age=30 city='上海'(Pydantic 实例,不是 dict)
print(result.age)      # 30

返回值是一个校验过的 Pydantic 实例,字段缺失或类型不对会直接抛 ValidationError——这就是客户端校验那一层。想要原始消息可以加 include_raw=True:

python
structured = model.with_structured_output(Contact, include_raw=True)
out = structured.invoke("...")
print(out["parsed"])   # Contact 实例
print(out["raw"])      # 原始 AIMessage

Agent 中的结构化输出

v1 的 create_agent 用 response_format 声明输出形状,结果放在 structured_response:

python
from langchain.agents import create_agent

agent = create_agent(
    model="deepseek:deepseek-chat",
    tools=[],
    response_format=Contact,          # 直接给 Pydantic 模型即可
)
res = agent.invoke({"messages": [{"role": "user", "content": "从这句话提取信息:张三,今年 30 岁,住在上海。"}]})
print(res["structured_response"])     # Contact(name='张三', age=30, city='上海')

四种 schema 载体

Python 侧 with_structured_output / response_format 接受多种 schema,和 JS 里的 Zod / JSON Schema 一一对应:

Python 载体运行时校验说明
Pydantic BaseModel✅首选,定义 + 校验 + 自动出 JSON Schema
TypedDict❌只有类型注解,返回普通 dict,不做校验
@dataclass❌(默认)结构清晰但不强制类型,可用 Pydantic 包装补齐校验
JSON Schema(dict)❌最底层,直接就是发给模型的「线格式」,可动态拼装

和 JS 的对应关系:Pydantic ↔ Zod,TypedDict ↔ TS interface(都只是编译/静态期类型),JSON Schema dict 两边一致,@dataclass 在 JS 里没有原生对应、实践中用 Zod 顶替。

六、小结与选型

老办法:提示词「求」模型给 JSON + 手动 JSON.parse,脆弱、无校验、无类型。 新办法:把 schema 交给模型作为约束,自动解析 + 校验,得到类型安全的对象。

按场景选:

  1. 单独调用模型 → model.withStructuredOutput(schema);
  2. Agent + 结构化输出 → createAgent({ ..., responseFormat: toolStrategy(schema) });
  3. 需要厂商原生严格约束 → method: "jsonSchema" 或 providerStrategy(schema)(需确认模型支持)。

一句话总结:从「让模型尽量按格式返回」,变成「用 schema 把格式固定下来,并自动校验」。 这既让代码不再依赖脆弱的正则清理,也让「模型输出」和「程序消费」之间有了可靠的契约。

评论
0/100