课程目标
掌握 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 ""; // 无格式要求
}
}
看起来什么都没做?它的价值在于:
- 将
AIMessage的content提取为纯字符串 - 支持流式
transform(),逐 chunk 提取文本 - 处理多模态内容:从
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" |
MarkdownListOutputParser | Markdown 列表 | "- 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;
}
}
}
适用于 JsonOutputParser、XMLOutputParser 这种需要看到完整/部分文本才能解析的场景。
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() 是另一种获取结构化输出的方式。两者对比:
| 维度 | OutputParser | withStructuredOutput |
|---|---|---|
| 工作方式 | 后处理 LLM 文本输出 | 要求 LLM 原生输出结构化数据 |
| 可靠性 | 依赖 LLM 遵循格式指令 | 依赖 LLM 的 function calling 能力 |
| Provider 依赖 | 无,任何 LLM 都可以 | 需要 Provider 支持 |
| 流式支持 | 通过 transform | Provider 原生支持 |
| 适用场景 | 通用场景、旧模型 | 支持 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 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | output_parsers/base.ts | BaseOutputParser, OutputParserException, invoke() 双路输入处理 |
| P0 | output_parsers/string.ts | StringOutputParser, 多模态内容文本提取逻辑 |
| P0 | output_parsers/transform.ts | BaseTransformOutputParser._transform(), BaseCumulativeTransformOutputParser 累积逻辑 |
| P1 | output_parsers/json.ts | JsonOutputParser, parsePartialResult, parseJsonMarkdown |
| P1 | output_parsers/structured.ts | StructuredOutputParser, Zod 校验, getFormatInstructions() |
| P2 | output_parsers/list.ts | ListOutputParser 流式分片策略 |
| P2 | output_parsers/xml.ts | XMLOutputParser, SAX 解析器 |
| P2 | output_parsers/bytes.ts | BytesOutputParser |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 掌握 StringOutputParser 和 JsonOutputParser 的基本用法 |
| 🔵 中阶 | 理解 StructuredOutputParser 如何结合 Zod 进行严格校验和生成格式说明 |
| 🟡 高阶 | 掌握流式输出解析:BaseTransformOutputParser 逐 chunk vs BaseCumulativeTransformOutputParser 累积式 |
| 🟠 资深 | 分析 withStructuredOutput vs OutputParser 的选择策略;理解 OutputParserException 的错误恢复设计 |
| 🔴 架构 | 设计容错解析策略:auto-fix、retry with feedback、fallback parser 链 |
下一课预告
第 16 课是"第一条完整 Chain 组合实战"——将 Prompt + Model + Parser + Tools + History 全部组合起来,构建真实可用的应用。