系列第 1 篇。你可能遇到过这个场景:让大模型"返回 JSON",它确实乖乖回了
{"name":"爱因斯坦",...}——但你在代码里result.name却得到undefined。问题出在哪?
先回答一个最底层的问题:大模型输出的到底是什么?
大模型是"打字机",不是"数据库"
大模型的本质是逐 token 预测:根据已有文本,按概率预测下一个 token,拼成完整回复。token 解码出来就是字符——也就是字符串。
无论你用的是 OpenAI、DeepSeek 还是 LangChain,接口里那个 message.content 的类型永远是 string:
// 你以为
response.content = { name: "爱因斯坦", birth_year: 1879 } // ❌ 不存在
// 实际
response.content = '{\n "name": "爱因斯坦",\n "birth_year": 1879\n}' // ✅ 字符串
模型内存里没有"对象"这个东西。所谓"让它返回 JSON",只是让它吐出一段"长得像 JSON 的文本"。至于这段文本是不是真的符合 JSON 语法、有没有裹 ```json、有没有在开头啰嗦两句——它都不保证。
那为什么必须变成对象?
因为程序不读文本,程序读结构。
一段字符串 '{"name":"爱因斯坦"}' 对代码来说是一个"整体":你不能 content.name,不能遍历 content.fields,不能直接塞给数据库。只有经过 JSON.parse 变成内存里的对象,字段才可访问、可迭代、可被校验、可传走。
const s = '{"name":"爱因斯坦"}';
s.name; // undefined —— 字符串上没有属性
JSON.parse(s).name; // "爱因斯坦" —— 对象上才有
"对象"是你和下游代码之间唯一可消费的形态。大模型的"回答"要变成你程序的"数据",中间必然要过一道 parse。
拦路虎:模型总爱把 JSON 裹进 markdown
你光会 JSON.parse 还不够。实际调大模型时,输出经常长这样:
好的,以下是爱因斯坦的信息:
```json
{"name": "爱因斯坦", "birth_year": 1879}
模型把 JSON 裹进了 markdown 代码块,还带一句开场白。直接 `JSON.parse` 会报错。于是最常见的处理是**先用正则把代码块里的 JSON 抠出来再 parse**:
```js
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/);
const jsonStr = jsonMatch ? jsonMatch[1] : content; // 剥围栏,没有围栏就用原文
const result = JSON.parse(jsonStr); // 再 parse
这套"剥 markdown → JSON.parse"几乎是每次调大模型的固定动作——所以 LangChain 把它封装成了解析器。
JsonOutputParser:把上面的套路封装好
import { JsonOutputParser } from "@langchain/core/output_parsers";
const parser = new JsonOutputParser();
const result = await parser.parse(response.content);
console.log(result.name); // "爱因斯坦" —— 直接可用
它内部做了什么?翻源码(@langchain/core 的 parseJsonMarkdown):
trim掉首尾空白;- 找到第一个
```,剥掉可选的json语言标注; - 截到第二个
```之间的内容; - 交给
JSON.parse。
和你手写的正则殊途同归,只是它更宽容:不认 json 标注的裸围栏也能剥、只有一个围栏也能处理。而你手写版只认 ```json 这一种。
一个容易踩的坑:它的格式说明是空的
很多教程会让你在 prompt 里拼上 parser.getFormatInstructions():
const prompt = `
请介绍爱因斯坦的信息,包含 name、birth_year 等字段。
${parser.getFormatInstructions()}
`;
但对 JsonOutputParser 来说,这一行什么都没加——它的 getFormatInstructions() 返回的是空字符串。源码注释说得很直白:JSON 太常见,不需要额外教模型。所以真正约束模型输出格式的,始终是你 prompt 里自己写的那句中文。这个解析器只负责"事后把模型交上来的东西宽容地解析",不负责"事前约束"。
小结
你写 prompt(文本)
↓
LLM = 打字机 → 永远输出 字符串(可能裹 markdown、夹废话)
↓
JsonOutputParser = 剥围栏 + JSON.parse → JS 对象
↓
result.name ✅
- 大模型天生只吐字符串,"JSON"只是内容长得像 JSON 的文本;
- 变成对象是你的 parse 完成的,不是模型送的;
JsonOutputParser= 剥围栏 +JSON.parse的封装,宽容但不校验。
下一篇我们会看到它的短板:它不校验字段。模型少给你一个字段、把 birth_year 写成字符串,它都一声不吭。要"讲清楚再验收",就该轮到 StructuredOutputParser 出场了。