跳到主要内容

创建日期: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);
消息类型角色说明
SystemMessagesystem设定身份/规则(一般放第一条)
HumanMessageuser用户输入
AIMessageassistant模型回复(可能含 tool_calls
ToolMessagetool工具执行结果(回填给模型,等价你手写循环里的 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
要 JSONwithStructuredOutput(zodSchema)(推理模型改 bindTools
给模型工具model.bindTools([tool], { tool_choice: 'auto' })

动手

  1. 用提示模板 + LCEL 写一条「中文翻译成英文」的链;
  2. City schema 加一个 province 字段,先看推理模型报错、再换 deepseek-chat 成功;
  3. chain.batch 一次跑 3 个问题,对比耗时。

自测

  1. 四种消息类型分别什么时候用?
  2. LCEL 的四个统一方法是什么?
  3. withStructuredOutput 为什么会报 Thinking mode does not support this tool_choice
  4. 推理模型上要拿结构化数据,两种替代方案是什么?
  5. batch 适合什么场景?

下一篇:LangGraph 入门:状态图、条件边与记忆