Skip to content

Output Parser

要点

  • Agent 的输出通常只有两条路:直接给用户看,或者交给程序继续处理。
  • 如果输出只是展示给用户,直接取最后一条消息的文本就够了。
  • 如果输出需要程序消费,就必须做结构化解析,常见做法是用 JsonOutputParserwithStructuredOutput()
  • JSON 解析只保证格式合法,不保证字段符合业务约束,必须再加一层 schema 校验。
  • 解析失败时,兜底要当作失败分支处理,而不是默认结果。

1. 背景:输出要往哪走

前面几篇一直在整理输入,这一篇开始看输出。在单个 Agent 应用里,输出通常只有两条路:

  • 直接给用户看。
  • 交给程序继续处理。

例如同一轮请求结束后,Agent 可能回了这样一段话:

text
这个接口的异常没有记录请求 ID,导致后续排查缺少上下文。建议先在入口统一注入 traceId,再补充错误日志。

如果只是显示在消息窗口里,这段话已经够用。但如果程序下一步还要:

  • 判断问题严重级别。
  • 决定要不要触发告警分支。
  • 写入结构化日志。
  • 给数据库存一条标准化记录。

那这段自然语言就不够用了。Output Parser 处理的就是第二条线:把输出收成程序能继续消费的结果。

2. 不是所有输出都需要解析

这里先把一个误区拎出来。不是。如果这一轮输出本来就是写给用户看的,自然语言往往就是最终形态。

typescript
const result = await agent.invoke({
  messages: [
    {
      role: "user",
      content: "帮我解释一下为什么这个接口会超时。",
    },
  ],
});

const finalText = result.messages.at(-1)?.text ?? "";
console.log(finalText);

这里最自然的处理方式就是直接拿最后一条消息的文本。不需要为了「用了 parser」而硬把它再过一遍解析器。后面只看一件事:什么时候直接拿文本,什么时候必须收成结构。

3. 给用户看的输出:文本就够了

如果当前 Agent 的结果只是继续展示给用户,最常见的做法就是直接取文本:

typescript
const result = await agent.invoke({
  messages: [
    {
      role: "user",
      content: "帮我总结一下这段日志的关键信息。",
    },
  ],
});

const finalText = result.messages.at(-1)?.text ?? "";
console.log(finalText);

这条线的重点是可读性,不是结构化。下面这些输出通常都属于这一类:

  • 聊天回复。
  • 解释概念。
  • 总结一段内容。
  • 给用户看的建议。

如果后面只是显示到页面上,或者交给下一个面向用户的节点继续改写,文本就够了。

4. 给程序读的输出:要做结构化节点

真正需要 Output Parser 的,通常不是 Agent 最后的自然语言回复,而是程序要拿去判断的那部分结果。比如希望系统在生成回复之前,先做一轮问题分类:

  • severity 是什么。
  • confidence 有多少。
  • 要不要走告警分支。

这时候更稳的做法不是去 parse Agent 最后的自然语言,而是专门做一个「结构化分析节点」。

先看最常见的第一步:JsonOutputParser

typescript
import { JsonOutputParser } from "@langchain/core/output_parsers";
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";

const parser = new JsonOutputParser();

const prompt = ChatPromptTemplate.fromMessages([
  [
    "system",
    [
      "你负责做问题分类。",
      "只返回 JSON,不要补充解释。",
      parser.getFormatInstructions(),
    ].join("\n"),
  ],
  ["user", "{input}"],
]);

const model = new ChatOpenAI({
  apiKey: process.env.MODEL_API_KEY,
  model: process.env.MODEL_NAME ?? "deepseek-chat",
  configuration: {
    baseURL: process.env.MODEL_BASE_URL ?? "https://api.deepseek.com/v1",
  },
});

const chain = prompt.pipe(model).pipe(parser);

const classifyResult = await chain.invoke({
  input: "支付接口返回 500,订单状态显示为未知,用户重复点击了三次。",
});

console.log(classifyResult);

这段代码虽然不是直接调用 Agent,但它在 Agent 应用里很常见。它通常扮演的是 Agent 旁边的「判定节点」。顺序很简单:

  1. 先用结构化节点做判断。
  2. 程序根据结果决定分支。
  3. 再把处理后的上下文交给 Agent 输出自然语言。

parser.getFormatInstructions() 的作用是自动生成一段给模型的指令文本,告诉模型必须以什么样的 JSON 格式输出结果。模型如果不加约束,可能在返回 JSON 的同时加上一些废话,例如:

text
好的,这是你要的 JSON 结果: {...}

getFormatInstructions() 会生成一段标准化的英文指令,例如:

text
The output should be formatted as a JSON instance that conforms to the JSON schema below. ... Return nothing but the JSON.

这是为了在提示词层面强制要求模型遵循 JSON 语法规范,从而提高解析成功率。

5. JSON 还不够,还要加 schema 校验

只解析成 JSON 还不够,因为 JSON 只保证「像对象」,不保证「对象一定符合业务约束」。例如下面两段都算合法 JSON:

json
{ "severity": "high", "confidence": 0.82 }
json
{ "severity": 123, "confidence": "high" }

对解析器来说,这两段都能过;对业务来说,第二段完全不能用。所以真正落到程序逻辑里,还要再加一层 schema 校验。

typescript
import { z } from "zod";

const classifySchema = z.object({
  severity: z.enum(["low", "medium", "high", "critical"]),
  confidence: z.number().min(0).max(1),
  summary: z.string().min(1),
});

这层校验的价值很直接:

  • severity 只能是约定枚举。
  • confidence 必须落在 0 到 1 之间。
  • summary 不能为空。

这样程序拿到的结果,才真的能继续往下流。

6. JsonOutputParser 与 withStructuredOutput() 是两条路

这两个东西经常被混在一起。直接分开记会更清楚:

  • JsonOutputParser:先让模型输出文本,再把文本解析成 JSON。
  • withStructuredOutput():让模型直接按 schema 产出结构化结果。

后者不是「另一个 parser」,而是更靠前的一种结构化输出方式。如果当前 provider 和 model 支持结构化输出,可以直接把 schema 绑到模型上:

typescript
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { z } from "zod";

const classifySchema = z.object({
  severity: z.enum(["low", "medium", "high", "critical"]),
  confidence: z.number().min(0).max(1),
  summary: z.string(),
});

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  apiKey: process.env.OPENAI_API_KEY,
});

const structuredModel = model.withStructuredOutput(classifySchema);

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "判断问题严重级别,并给出简短摘要。"],
  ["user", "{input}"],
]);

const chain = prompt.pipe(structuredModel);

const result = await chain.invoke({
  input: "支付接口返回 500,订单状态显示为未知,用户重复点击了三次。",
});

console.log(result);

这里用 OpenAI 只是为了给一个常见示例。不是说前面用的 DeepSeek 就不能继续用,而是 withStructuredOutput() 的可用性要看具体 provider 和 model 的支持情况。

在 Agent 系统里,可以把它理解成:

  • 如果模型支持,就用结构化输出直接产出程序结果。
  • 如果不支持,就退回到「文本 JSON + parser + schema 校验」。

7. 解析失败时的兜底策略

输出解析最怕的不是模型答错,而是「看起来差不多,但格式不完全对」。例如:

  • JSON 前面多了一句解释。
  • 某个字段拼错。
  • confidence 返回成字符串。
  • 枚举值超出约定范围。

这时候最好把问题分层看:

层级常见问题
模型生成输出根本不是 JSON。
解析阶段JSON 语法不合法。
校验阶段字段类型不对,schema 不通过。
业务阶段字段合法,但不满足业务规则。

如果要兜底,也要把它当成失败分支,不要把它当成正常结果。

typescript
import { z } from "zod";

const classifySchema = z.object({
  severity: z.enum(["low", "medium", "high", "critical"]),
  confidence: z.number().min(0).max(1),
});

function safeParseClassify(input: unknown) {
  const result = classifySchema.safeParse(input);
  if (result.success) {
    return result.data;
  }
  return {
    severity: "low" as const,
    confidence: 0.5,
    fallback: true,
  };
}

这个默认值只是演示失败路径,不是通用默认策略。有些节点适合兜底,有些节点更适合直接重试或中断。选择哪种策略,取决于当前节点在链路中的位置:下游是用户可见的,还是内部决策用的。

基于 MIT 协议开源