大模型只吐字符串,凭什么你能拿到 JSON 对象?

2 阅读4分钟

系列第 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/coreparseJsonMarkdown):

  1. trim 掉首尾空白;
  2. 找到第一个 ```,剥掉可选的 json 语言标注;
  3. 截到第二个 ``` 之间的内容;
  4. 交给 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 出场了。