Skip to content
LangChain.js 基本概念:模型、提示词与输出解析
本篇覆盖 LangChain.js 中三个基础组件的职责与协作方式:ChatModel(模型包装器)、PromptTemplate(提示词模板)与 OutputParser(输出解析器)。它们各自独立工作,但通过 .pipe() 串联后才能构成一条完整的“自然语言输入 → 模型调用 → 结构化数据输出”处理链。
ChatModel:模型包装器与消息返回
在 LangChain.js 里与语言模型交互的最基础单位是 ChatModel,它不是一个具体的模型实现,而是一层统一的调用接口。它接受一个消息列表,返回一个 AIMessage 对象——不是纯文本字符串。这一点和直接调用 OpenAI SDK 拿回 response.choices[0].message.content 的行为不太一样。
这种封装的意义不在“简化调用”,而在于后续可以在同一个接口上挂载统一的管道处理:解析、记忆、流式回调,全部通过同一套 invoke / stream / batch 接口运转。
LangChain.js 的 ChatModel 实例化需要依赖具体的模型提供商包,常用的是 @langchain/openai 里的 ChatOpenAI。
创建实例并调用 invoke
typescript
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
model: "gpt-4o",
temperature: 0.2,
maxTokens: 512,
apiKey: process.env.OPENAI_API_KEY,
});
const response = await model.invoke("你好,请用一句话介绍自己。");
console.log(response);运行后 response 的类型是 AIMessage,不是 string。它的结构大致如下:
json
{
"lc": 1,
"type": "constructor",
"id": ["langchain", "schema", "AIMessage"],
"kwargs": {
"content": "我是由 OpenAI 训练的语言模型,可以回答问题、提供建议和生成文本。",
"additional_kwargs": {}
}
}真正有用的文本在 response.content 里。但直接用 AIMessage 包裹的优势在于,它可以携带额外的元数据(例如工具调用的信息、推理内容等),下游的输出解析器可以统一处理这个结构,而不必让每个调用方自己去拆 content。
invoke 方法的签名大致是:
typescript
invoke(input: BaseLanguageModelInput, options?: Partial<BaseLanguageModelCallOptions>): Promise<AIMessage>input 可以是字符串,也可以是一个消息数组。传入字符串时,LangChain 内部会自动把它包装成 HumanMessage。
如果直接传入消息数组,就能控制对话历史或系统提示词:
typescript
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
const messages = [
new SystemMessage("你是一个言简意赅的技术文档翻译助手。"),
new HumanMessage("请将 'dependency injection' 译为中文"),
];
const response = await model.invoke(messages);
console.log(response.content); // 依赖注入关键结论:ChatModel 返回的是 AIMessage 对象,不是裸字符串。后续要接入的 PromptTemplate 和 OutputParser 也是围绕这个对象展开的。
PromptTemplate:模板与变量注入
直接用字符串拼提示词在逻辑简单时还行,一旦需要根据用户输入或上下文动态变化,这种硬拼方式很快就会变得难以维护。PromptTemplate 的职责是把“固定的提示结构”和“可变的输入数据”分开,通过占位符在调用时注入变量。
在 LangChain.js 里,最常用的创建方式是 PromptTemplate.fromTemplate。
fromTemplate 与占位符
typescript
import { PromptTemplate } from "@langchain/core/prompts";
const template = new PromptTemplate({
template: "将以下句子翻译为{target_lang}:{input}",
inputVariables: ["target_lang", "input"],
});
const prompt = await template.format({
target_lang: "日语",
input: "今天天气真不错",
});
console.log(prompt);
// 将以下句子翻译为日语:今天天气真不错format 方法返回一个字符串,可以当作 ChatModel 的输入。但如果要将模板与 ChatModel 串联,实际上不需要手动调用 format——后面会看到 .pipe() 会自动完成这个步骤。
fromTemplate 是静态语法糖,它会自动从花括号中提取变量名:
typescript
const template = PromptTemplate.fromTemplate("总结以下内容为一句话:{text}");
const prompt = await template.format({ text: "......" });模板里支持任意多个占位符,但占位符名称需要与传入对象的键严格匹配,否则会抛出异常。
这里的关键点在于:PromptTemplate 只负责生成字符串,它不关心这个字符串最终是发给哪个模型。这种解耦意味着同一个模板可以套在不同模型上做对比实验。
OutputParser:从文本到结构化数据
模型返回的 AIMessage.content 是自由文本,但对于需要进一步处理的下游逻辑来说,自由文本不可靠。OutputParser 负责把模型输出从文本转换为结构化的数据类型。
LangChain.js 提供了多种内置解析器,常用的有:
- StringOutputParser:直接提取
AIMessage.content并返回字符串。 - StructuredOutputParser:根据 Zod schema 将输出解析为 JavaScript 对象。
StringOutputParser 与 StructuredOutputParser
StringOutputParser 看起来只是简单取了 content,但它的价值在于统一返回类型。在没有解析器的链式调用里,下一个节点拿到的可能是 AIMessage 也可能是 string,而 StringOutputParser 让输出稳定变成字符串,避免了到处检查 .content 的习惯和因此而产生的状态污染问题。
typescript
import { StringOutputParser } from "@langchain/core/output_parsers";
const parser = new StringOutputParser();
const text = await parser.invoke(response); // response 是 AIMessage
console.log(typeof text); // stringStructuredOutputParser 则更进一步。你需要先用 Zod 定义一个期望的数据结构,解析器会要求模型输出符合该结构的 JSON,并自动校验和转换类型。
typescript
import { StructuredOutputParser } from "@langchain/core/output_parsers";
import { z } from "zod";
const schema = z.object({
name: z.string().describe("联系人姓名"),
phone: z.string().describe("联系电话"),
reason: z.string().describe("来电原因"),
});
const parser = StructuredOutputParser.fromZodSchema(schema);
const formatInstructions = parser.getFormatInstructions();
// 这会生成一段提示词,描述 JSON 格式要求formatInstructions 是解析器生成的一段指令文本,必须嵌入到提示词模板里,告诉模型按照该格式返回 JSON。如果不注入这段指令,模型可能返回纯文本,解析器就会抛异常。
typescript
const template = PromptTemplate.fromTemplate(`
从以下客户留言中提取信息:
{message}
{format_instructions}
`);
const prompt = await template.format({
message: "我叫张三,电话13800138000,想咨询退款事宜。",
format_instructions: formatInstructions,
});模型看到完整的提示词(包含格式说明)后,就会返回类似这样的 JSON:
json
{
"name": "张三",
"phone": "13800138000",
"reason": "退款咨询"
}然后解析器自动将它转成 JavaScript 对象。如果 JSON 不合法或者字段类型不符,解析阶段会直接报错,不会让脏数据流入后续逻辑。
用 .pipe() 组合三个组件
上面三个组件可以各自独立使用,但真正的威力来自于通过 pipe() 组合成一个Runnable 序列。
.pipe() 是 LangChain 表达式语言(LCEL)的核心操作符。a.pipe(b) 表示“先把 a 的输出传给 b 作为输入”。这样就能写出声明式的处理链。
定义管道并调用
将 PromptTemplate、ChatModel、OutputParser 串联:
typescript
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { StructuredOutputParser } from "@langchain/core/output_parsers";
import { z } from "zod";
// 1. 定义解析器与格式说明
const schema = z.object({
intent: z.string().describe("用户意图,如 预订、查询、投诉"),
entity: z.string().describe("涉及的实体,如 酒店、航班、账户"),
confidence: z.number().describe("置信度 0-1"),
});
const parser = StructuredOutputParser.fromZodSchema(schema);
// 2. 定义提示词模板,嵌入格式指令
const prompt = PromptTemplate.fromTemplate(`
分析用户输入,提取意图与实体。
{format_instructions}
用户输入: {input}
`);
// 3. 创建模型
const model = new ChatOpenAI({
model: "gpt-4o",
temperature: 0,
});
// 4. 用 pipe 串联
const chain = prompt.pipe(model).pipe(parser);此时 chain 就是一个 Runnable,它吸收了 prompt 需要的 {input} 变量,自动执行:先格式化提示词 → 调用模型 → 解析输出。
调用:
typescript
const result = await chain.invoke({
input: "我想订一张明天从北京到上海的机票",
format_instructions: parser.getFormatInstructions(),
});
console.log(result);
// { intent: '预订', entity: '机票', confidence: 0.98 }因为 format_instructions 也声明在了模板里,所以调用 invoke 时一定要把 parser.getFormatInstructions() 传进去,否则模型不知道要输出 JSON。
也可以把 format_instructions 作为部分变量预设进去,减少调用时的参数:
typescript
const promptWithInstructions = await prompt.partial({
format_instructions: parser.getFormatInstructions(),
});
const chain = promptWithInstructions.pipe(model).pipe(parser);
const result = await chain.invoke({ input: "退掉我最近的一笔订单" });
// { intent: '退款', entity: '订单', confidence: 0.95 }在这里,.pipe() 不仅连线了数据流,在 TypeScript 下还保证了类型传导:prompt.pipe(model).pipe(parser) 的输出类型会被自动推导为 Zod schema 定义的类型,避免了手动 as 断言。
示例:从自然语言中提取意图与实体
下面把完整的“输入自然语言 → 输出结构化 JSON”流程打包成一个函数:
typescript
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { StructuredOutputParser } from "@langchain/core/output_parsers";
import { z } from "zod";
const intentSchema = z.object({
intent: z.string().describe("意图分类,如:订单查询、退款、产品咨询、其他"),
entities: z.object({
product: z.string().optional().describe("涉及的产品名"),
orderId: z.string().optional().describe("订单编号"),
}).describe("提取到的实体"),
});
const parser = StructuredOutputParser.fromZodSchema(intentSchema);
const prompt = PromptTemplate.fromTemplate(`
根据用户输入识别意图和实体。
{format_instructions}
用户输入:{input}
`);
const model = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
const chain = prompt.pipe(model).pipe(parser);
async function analyzeUserInput(text: string) {
return chain.invoke({
input: text,
format_instructions: parser.getFormatInstructions(),
});
}
// 测试
(async () => {
const result1 = await analyzeUserInput("我的订单 12345 怎么还没发货?");
console.log(result1);
// { intent: '订单查询', entities: { orderId: '12345' } }
const result2 = await analyzeUserInput("你们有什么款的蓝牙耳机?");
console.log(result2);
// { intent: '产品咨询', entities: { product: '蓝牙耳机' } }
})();运行逻辑:传入文本,模型看到带格式约束的提示词,按要求生成 JSON,解析器做类型校验后返回对象。一旦 JSON 格式不符合 Zod schema,解析器会抛错,可以在外层 catch 做降级处理(例如用 fallback 规则)。
注意点与限制
格式指令是必需的:
StructuredOutputParser依赖getFormatInstructions()生成的描述文本,如果不把它嵌入提示词,模型几乎不会按期望的结构输出。即便用了withStructuredOutput()这类模型内置方法,本质上也是在内部接入了类似的指令。输出解析的脆弱性:模型可能返回合法的 JSON 但键名不符(比如多了一个空格),也可能返回一段包含 JSON 的 Markdown 代码块。内置解析器会尽力清洗,但不保证 100% 成功。对关键业务路径,建议加一层自定义的校验/重试逻辑。
StringOutputParser不处理格式:它的职责只是提取content属性,不会主动去掉前后的空白或换行。如果需要清理格式,要在它后面再串联一个自定义的 Runnable 函数。温度参数的影响:当使用
StructuredOutputParser时,通常将temperature设为 0 或接近 0 的值,提高输出的确定性。高温度下模型可能创造出看似合法但语义不合理的数据。不再推荐手动处理
AIMessage.content:在复杂的链或状态图里,如果某些节点返回AIMessage,另一些返回string,状态定义会变得很混乱。统一用解析器把输出转成纯字符串或对象,能有效避免这类问题。LangChain.js 的版本:本文基于最新 v1.x 系列(2025 年 10 月发布)。旧版
js.langchain.ac.cn教程站点已弃用,所有 API 请参照当前https://langchain.com/docs的 JavaScript 部分。
