第 15 课: Output Parsers — 结构化输出解析

0 阅读7分钟

课程目标

掌握 LangChain.js 的输出解析器体系:BaseOutputParser 抽象基类、StringOutputParser 文本提取、JsonOutputParser JSON 解析、StructuredOutputParser Zod 校验、ListOutputParser 列表解析、流式输出解析的 transform() 机制,以及 withStructuredOutput() 的对比。


15.1 为什么需要 Output Parser?

LLM 返回的是非结构化的文本(字符串或 AIMessage),但程序需要的是结构化数据(JSON 对象、列表、特定格式)。Output Parser 的职责就是桥接这个 gap。

Output Parser 也是 Runnable,所以可以直接参与 pipe() 组合:

const chain = prompt.pipe(model).pipe(parser);
//                                      ↑ Output Parser

15.2 继承体系

源码位置: libs/langchain-core/src/output_parsers/

Runnable<string | BaseMessage, T>
  └── BaseLLMOutputParser<T>                    // 最底层抽象
        └── BaseOutputParser<T>                  // 标准抽象基类
              └── BaseTransformOutputParser<T>   // 支持流式 transform
                    ├── StringOutputParser       // 提取纯文本
                    ├── ListOutputParser         // 列表解析
                         ├── CommaSeparatedListOutputParser
                         ├── NumberedListOutputParser
                         └── MarkdownListOutputParser
                    ├── BytesOutputParser        // 转为字节
                    └── BaseCumulativeTransformOutputParser<T>  // 累积式流式
                          ├── JsonOutputParser   // JSON 解析
                          └── XMLOutputParser    // XML 解析
              └── StructuredOutputParser         // Zod 校验解析

15.3 BaseOutputParser — 核心抽象

源码位置: libs/langchain-core/src/output_parsers/base.ts

export abstract class BaseOutputParser<T = unknown> extends BaseLLMOutputParser<T> {
  // 核心:解析 LLM 输出文本
  abstract parse(text: string, callbacks?: Callbacks): Promise<T>;

  // 返回格式说明(可注入到 Prompt 中指导 LLM)
  abstract getFormatInstructions(options?: FormatInstructionsOptions): string;

  // 从 Generation 中提取 text 后调用 parse
  parseResult(generations: Generation[] | ChatGeneration[]): Promise<T> {
    return this.parse(generations[0].text);
  }
}

BaseLLMOutputParser.invoke() 方法(第 72-99 行)处理两种输入:

  • 字符串输入:直接调用 parseResult([{ text: input }])
  • BaseMessage 输入:提取文本后调用 parseResult([{ message: input, text: ... }])

15.4 StringOutputParser — 最简单的 Parser

源码位置: libs/langchain-core/src/output_parsers/string.ts

export class StringOutputParser extends BaseTransformOutputParser<string> {
  parse(text: string): Promise<string> {
    return Promise.resolve(text);  // 直接返回原文本
  }

  getFormatInstructions(): string {
    return "";  // 无格式要求
  }
}

看起来什么都没做?它的价值在于:

  1. AIMessagecontent 提取为纯字符串
  2. 支持流式 transform(),逐 chunk 提取文本
  3. 处理多模态内容:从 ContentBlock[] 中提取文本部分,忽略 reasoning/thinking 块
import { StringOutputParser } from "@langchain/core/output_parsers";

const chain = prompt.pipe(model).pipe(new StringOutputParser());

// invoke 返回 string 而非 AIMessage
const result: string = await chain.invoke({ question: "Hello" });

// stream 返回 string chunk 而非 AIMessageChunk
for await (const chunk of await chain.stream({ question: "Hello" })) {
  process.stdout.write(chunk); // 逐 token 输出纯文本
}

15.5 JsonOutputParser — JSON 解析

源码位置: libs/langchain-core/src/output_parsers/json.ts

export class JsonOutputParser<T extends Record<string, any>> 
  extends BaseCumulativeTransformOutputParser<T> {

  async parse(text: string): Promise<T> {
    return parseJsonMarkdown(text, JSON.parse) as T;
  }

  async parsePartialResult(generations: ChatGeneration[] | Generation[]): Promise<T | undefined> {
    return parseJsonMarkdown(generations[0].text) as T | undefined;
  }
}

特点

  • 自动处理 Markdown 代码块包裹的 JSON(如 ```json ... ```
  • 支持流式累积解析:通过 parsePartialResult 在 JSON 尚未完整时尝试解析
  • 支持 diff 模式:返回 JSON Patch 格式的增量变化

15.5.1 基本用法

import { JsonOutputParser } from "@langchain/core/output_parsers";

const parser = new JsonOutputParser<{ name: string; age: number }>();

const chain = prompt.pipe(model).pipe(parser);
const result = await chain.invoke({ query: "Tell me about yourself" });
// result 类型: { name: string; age: number }

15.5.2 流式 JSON 解析

for await (const chunk of await chain.stream({ query: "Tell me about yourself" })) {
  console.log(chunk);
  // 第 1 次: { name: "A" }             ← 部分 JSON
  // 第 2 次: { name: "Alice" }          ← 更新
  // 第 3 次: { name: "Alice", age: 30 } ← 完整
}

15.6 StructuredOutputParser — Zod 校验

源码位置: libs/langchain-core/src/output_parsers/structured.ts

JsonOutputParser 不同,StructuredOutputParser 使用 Zod schema 做严格校验

import { StructuredOutputParser } from "@langchain/core/output_parsers";
import { z } from "zod";

const parser = StructuredOutputParser.fromZodSchema(
  z.object({
    answer: z.string().describe("The answer to the question"),
    confidence: z.number().min(0).max(1).describe("Confidence score"),
    sources: z.array(z.string()).describe("Source references"),
  })
);

// 获取格式说明(可注入到 Prompt 中)
const instructions = parser.getFormatInstructions();
// 返回 JSON Schema 的描述文本,指导 LLM 按格式输出

15.6.1 getFormatInstructions

StructuredOutputParser.getFormatInstructions() 生成一段结构化的指令文本(第 84-99 行),包含:

  • JSON Schema 的说明和示例
  • 要求 LLM 输出符合 schema 的 JSON
  • 使用 toJsonSchema() 将 Zod 转为 JSON Schema

15.6.2 parse 方法

// 源码简化(structured.ts 第 107-133 行)
async parse(text: string): Promise<InferInteropZodOutput<T>> {
  try {
    const trimmedText = text.trim();
    // 提取 markdown 代码块中的 JSON
    const json = trimmedText.match(/^```(?:json)?\s*([\s\S]*?)```/)?.[1]
      || trimmedText.match(/```json\s*([\s\S]*?)```/)?.[1]
      || trimmedText;

    // 处理转义字符
    const escapedJson = json.replace(/"([^"\\]*(\\.[^"\\]*)*)"/g, ...);

    // 用 Zod 校验
    return await interopParseAsync(this.schema, JSON.parse(escapedJson));
  } catch (e) {
    throw new OutputParserException(`Failed to parse. Text: "${text}". Error: ${e}`, text);
  }
}

15.6.3 fromNamesAndDescriptions 便捷方法

无需手写 Zod schema:

const parser = StructuredOutputParser.fromNamesAndDescriptions({
  answer: "The answer to the question",
  source: "The source URL",
});
// 自动创建 z.object({ answer: z.string(), source: z.string() })

15.7 ListOutputParser — 列表解析

源码位置: libs/langchain-core/src/output_parsers/list.ts

解析方式格式说明
CommaSeparatedListOutputParser逗号分隔"foo, bar, baz"
NumberedListOutputParser数字序号"1. foo\n2. bar"
MarkdownListOutputParserMarkdown 列表"- foo\n- bar"
CustomListOutputParser自定义分隔符和长度可配置
import { CommaSeparatedListOutputParser } from "@langchain/core/output_parsers";

const parser = new CommaSeparatedListOutputParser();
const result = await parser.parse("apple, banana, cherry");
// ["apple", "banana", "cherry"]

ListOutputParser 的流式实现(第 14-57 行)很巧妙:逐步累积文本,发现新的完整项时立即 yield,最后一项在流结束后 yield。


15.8 其他 Parser

15.8.1 BytesOutputParser

将文本转为 Uint8Array,适用于二进制流场景:

import { BytesOutputParser } from "@langchain/core/output_parsers";

const parser = new BytesOutputParser();
const result = await parser.parse("Hello");
// Uint8Array

15.8.2 XMLOutputParser

解析 XML 格式输出:

import { XMLOutputParser } from "@langchain/core/output_parsers";

const parser = new XMLOutputParser({ tags: ["response", "answer", "sources"] });
const result = await parser.parse("<response><answer>42</answer></response>");
// { response: [{ answer: "42" }] }

15.9 流式解析的 transform 机制

源码位置: libs/langchain-core/src/output_parsers/transform.ts

Output Parser 支持流式处理的关键在于 transform() 方法。继承关系决定了两种流式策略:

15.9.1 BaseTransformOutputParser — 逐 chunk 解析

// 源码(transform.ts 第 23-38 行)
async *_transform(inputGenerator: AsyncGenerator<string | BaseMessage>): AsyncGenerator<T> {
  for await (const chunk of inputGenerator) {
    if (typeof chunk === "string") {
      yield this.parseResult([{ text: chunk }]);
    } else {
      yield this.parseResult([{ message: chunk, text: ... }]);
    }
  }
}

每个 chunk 独立解析,适用于 StringOutputParser 这种不需要累积上下文的场景。

15.9.2 BaseCumulativeTransformOutputParser — 累积解析

// 源码简化(transform.ts 第 86-141 行)
async *_transform(inputGenerator: AsyncGenerator<string | BaseMessage>): AsyncGenerator<T> {
  let prevParsed: T | undefined;
  let accGen: GenerationChunk | undefined;

  for await (const chunk of inputGenerator) {
    // 累积 chunk
    accGen = accGen ? accGen.concat(chunkGen) : chunkGen;

    // 尝试解析累积的文本
    const parsed = await this.parsePartialResult([accGen]);

    // 只在结果变化时 yield
    if (parsed !== undefined && !deepCompareStrict(parsed, prevParsed)) {
      if (this.diff) {
        yield this._diff(prevParsed, parsed);  // diff 模式:返回增量
      } else {
        yield parsed;                            // 正常模式:返回完整结果
      }
      prevParsed = parsed;
    }
  }
}

适用于 JsonOutputParserXMLOutputParser 这种需要看到完整/部分文本才能解析的场景。


15.10 OutputParserException

源码位置: libs/langchain-core/src/output_parsers/base.ts 第 170-198 行

export class OutputParserException extends Error {
  llmOutput?: string;      // 原始 LLM 输出
  observation?: string;    // 错误描述(可反馈给 LLM)
  sendToLLM: boolean;      // 是否将错误发回 LLM 重试
}

与普通 Error 的区别:携带结构化信息,Agent 可以识别并让 LLM 修正输出后重试。


15.11 withStructuredOutput vs OutputParser

BaseChatModel.withStructuredOutput() 是另一种获取结构化输出的方式。两者对比:

维度OutputParserwithStructuredOutput
工作方式后处理 LLM 文本输出要求 LLM 原生输出结构化数据
可靠性依赖 LLM 遵循格式指令依赖 LLM 的 function calling 能力
Provider 依赖无,任何 LLM 都可以需要 Provider 支持
流式支持通过 transformProvider 原生支持
适用场景通用场景、旧模型支持 tool calling 的现代模型
// withStructuredOutput 方式(推荐,如果 Provider 支持)
const modelWithSchema = model.withStructuredOutput(z.object({
  answer: z.string(),
  confidence: z.number(),
}));
const result = await modelWithSchema.invoke("What is 1+1?");
// 直接得到 { answer: "2", confidence: 0.99 }

// OutputParser 方式(通用)
const parser = StructuredOutputParser.fromZodSchema(z.object({
  answer: z.string(),
  confidence: z.number(),
}));
const chain = prompt.pipe(model).pipe(parser);

15.12 实战练习

练习 1:Prompt + Model + JsonOutputParser 链

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

interface MovieReview {
  title: string;
  rating: number;
  summary: string;
  pros: string[];
  cons: string[];
}

const parser = new JsonOutputParser<MovieReview>();

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "You are a movie critic. Always respond in valid JSON with fields: title, rating (1-10), summary, pros (array), cons (array)."],
  ["human", "Review the movie: {movie}"],
]);

const chain = prompt.pipe(new ChatOpenAI({ model: "gpt-4o-mini" })).pipe(parser);

// 正常情况
const review = await chain.invoke({ movie: "Inception" });
// { title: "Inception", rating: 9, summary: "...", pros: [...], cons: [...] }

// 流式情况
for await (const partial of await chain.stream({ movie: "The Matrix" })) {
  console.log(partial); // 逐步构建的 JSON 对象
}

练习 2:错误处理

import { OutputParserException } from "@langchain/core/output_parsers";
import { StructuredOutputParser } from "@langchain/core/output_parsers";
import { z } from "zod";

const parser = StructuredOutputParser.fromZodSchema(
  z.object({ answer: z.number() })
);

try {
  // 模拟 LLM 返回了非 JSON 文本
  await parser.parse("I think the answer is forty-two");
} catch (e) {
  if (e instanceof OutputParserException) {
    console.log("Parse failed, LLM output:", e.llmOutput);
    // 可以将错误反馈给 LLM 重试
  }
}

15.13 源码精读路线

优先级文件关注点
P0output_parsers/base.tsBaseOutputParser, OutputParserException, invoke() 双路输入处理
P0output_parsers/string.tsStringOutputParser, 多模态内容文本提取逻辑
P0output_parsers/transform.tsBaseTransformOutputParser._transform(), BaseCumulativeTransformOutputParser 累积逻辑
P1output_parsers/json.tsJsonOutputParser, parsePartialResult, parseJsonMarkdown
P1output_parsers/structured.tsStructuredOutputParser, Zod 校验, getFormatInstructions()
P2output_parsers/list.tsListOutputParser 流式分片策略
P2output_parsers/xml.tsXMLOutputParser, SAX 解析器
P2output_parsers/bytes.tsBytesOutputParser

本课收获总结

级别你应该掌握的
🟢 基础掌握 StringOutputParserJsonOutputParser 的基本用法
🔵 中阶理解 StructuredOutputParser 如何结合 Zod 进行严格校验和生成格式说明
🟡 高阶掌握流式输出解析:BaseTransformOutputParser 逐 chunk vs BaseCumulativeTransformOutputParser 累积式
🟠 资深分析 withStructuredOutput vs OutputParser 的选择策略;理解 OutputParserException 的错误恢复设计
🔴 架构设计容错解析策略:auto-fix、retry with feedback、fallback parser 链

下一课预告

第 16 课是"第一条完整 Chain 组合实战"——将 Prompt + Model + Parser + Tools + History 全部组合起来,构建真实可用的应用。