创建日期:2026-09-14 | 最近更新:2026-09-14 本机真实运行(LangChain.js 1.5.11 + DeepSeek Anthropic 兼容端点);下方输出均为实测。
LangChain.js 核心:消息、提示、LCEL 与结构化输出
这篇讲 LangChain.js 的三件核心事:消息(Message) 是跟模型对话的载体,提示模板(PromptTemplate) 把变量塞进消息,LCEL(管道) 把「模板 → 模型 → 解析」串成一条链。最后讲结构化输出——包括我在推理模型上真实撞到的两个坑。
1. 消息:四种角色,一个数组
LangChain 用「消息对象」而不是裸字符串来描述对话:
import { SystemMessage, HumanMessage, AIMessage, ToolMessage } from '@langchain/core/messages';
const messages = [
new SystemMessage('你是前端专家,回答不超过 20 字。'),
new HumanMessage('什么是闭包?'),
];
const r = await model.invoke(messages);
console.log(r.text);
| 消息类型 | 角色 | 说明 |
|---|---|---|
SystemMessage | system | 设定身份/规则(一般放第一条) |
HumanMessage | user | 用户输入 |
AIMessage | assistant | 模型回复(可能含 tool_calls) |
ToolMessage | tool | 工具执行结果(回填给模型,等价你手写循环里的 tool_result) |
回看 frontend-agent 篇 2:你手写的
{role:'user', content:[{type:'tool_result',...}]}—— 在 LangChain 里就是ToolMessage。
2. 提示模板:把变量塞进消息
别用字符串拼 prompt(难维护、容易注入)。用 ChatPromptTemplate:
import { ChatPromptTemplate } from '@langchain/core/prompts';
const prompt = ChatPromptTemplate.fromMessages([
['system', '你是{role},回答不超过 20 字。'],
['human', '{question}'],
]);
模板里的 {role} {question} 是占位符;invoke 时传值即可。它还负责校验缺变量、控制消息结构——比手拼稳。
3. LCEL:用 .pipe() 把一切串起来
LCEL(LangChain Expression Language) 的核心就一个动作:.pipe()。凡是实现了 Runnable 接口的东西(提示模板、模型、解析器)都能被 pipe 串成一条链:
import { StringOutputParser } from '@langchain/core/output_parsers';
const chain = prompt.pipe(model).pipe(new StringOutputParser());
这条链一次把「填空 → 调模型 → 取出纯文本」做完。统一的 Runnable 接口让你在任何一环都用同样的四个方法:
| 方法 | 作用 |
|---|---|
invoke(input) | 跑一次,返回结果 |
batch([...]) | 批量并行跑多条输入 |
stream(input) | 流式产出(逐块回调) |
pipe(next) | 接到下一个环节 |
实测(真实输出):
console.log(await chain.invoke({ role: '前端专家', question: '解释一下闭包' }));
// → "函数记住并访问定义时的词法作用域。"
const outs = await chain.batch([
{ role: '前端专家', question: '什么是虚拟 DOM?' },
{ role: '前端专家', question: '什么是事件循环?' },
]);
// → batch 结果数: 2 | 第2条: "JS 单线程处理异步任务的调度机制。"
batch 一次发多条、并行拿结果——做评测/对比提示词时非常省事。
4. 结构化输出:让它直接吐 JSON(含两个真实的坑)
普通回复是文本,程序要用就得解析。LangChain 的 withStructuredOutput(zodSchema) 让你声明目标结构,模型直接返回符合 schema 的对象:
import { z } from 'zod';
const City = z.object({
name: z.string().describe('城市名'),
populationWan: z.number().describe('人口(万人)'),
famous: z.array(z.string()).describe('两个著名地标'),
});
const structured = model.withStructuredOutput(City, { name: 'city_info' });
const out = await structured.invoke('介绍一下杭州:人口多少万,列两个著名地标。');
⚠️ 坑 1:推理模型会拒绝「强制工具调用」
我用的 deepseek-v4-flash 是推理模型,直接跑上面的代码报 400:
[functionCalling] 失败: 400 {"error":{"message":"Thinking mode does not support this tool_choice", ...}}
原因:withStructuredOutput 默认用「强制模型调用某个工具」来实现结构化输出,而推理模型不支持强制 tool_choice(它要先思考)。
⚠️ 坑 2:切到 jsonSchema 模式,端点不强制
换成 { method: 'jsonSchema' }:
[jsonSchema] 失败: Failed to parse. Text: "杭州常住人口约 **1262.4 万人**(2024年末)…"。Error: SyntaxError: Unexpected token '杭'
端点没把「必须输出 JSON Schema」当硬约束,模型回了散文,解析直接失败。
✅ 两个能跑的方案
方案 A:非推理模型 + withStructuredOutput 直接成功(换成 deepseek-chat):
[deepseek-chat + structured] -> {"name":"杭州","populationWan":1262.4}
方案 B:推理模型 + bindTools(自动选择)+ 自己取 tool_calls:
const m2 = model.bindTools([cityTool], { tool_choice: 'auto' });
const r = await m2.invoke('用 city_info 工具给出杭州的人口(万人)与两个地标。');
console.log(r.tool_calls[0].args);
实测输出:
[bindTools] tool_calls = {"name":"杭州","populationWan":1252.2,"famous":["西湖","灵隐寺"]}
[bindTools] stop_reason = tool_use
结论(值得记住):「结构化输出」在不同模型上的实现方式不同——遇到 Thinking mode does not support this tool_choice,就切非推理模型,或改成 bindTools + tool_choice:'auto' 手动取参数。
5. 一页速查
| 你要做的事 | LangChain.js 写法 |
|---|---|
| 设定身份 | new SystemMessage('…') |
| 拼提示 | ChatPromptTemplate.fromMessages([...]) |
| 串链路 | prompt.pipe(model).pipe(parser) |
| 批量跑 | chain.batch([...]) |
| 取纯文本 | new StringOutputParser() 或 msg.text |
| 要 JSON | withStructuredOutput(zodSchema)(推理模型改 bindTools) |
| 给模型工具 | model.bindTools([tool], { tool_choice: 'auto' }) |
动手
- 用提示模板 + LCEL 写一条「中文翻译成英文」的链;
- 给
Cityschema 加一个province字段,先看推理模型报错、再换deepseek-chat成功; - 用
chain.batch一次跑 3 个问题,对比耗时。
自测
- 四种消息类型分别什么时候用?
- LCEL 的四个统一方法是什么?
withStructuredOutput为什么会报Thinking mode does not support this tool_choice?- 推理模型上要拿结构化数据,两种替代方案是什么?
batch适合什么场景?