LOADING
2905 words
15 minutes
🤖 LangChain.js 从入门到实战:构建 AI 应用的完整指南

前言

当我第一次打开 LangChain 的文档时,说实话有点懵。RunnableLCELChainAgent……这些概念堆在一起,让人不知从何下手。

于是我决定——用实际项目来学

这篇文章是我从 v1.0 到 v8.0,一步步构建一个 AI 聊天后端的完整记录。希望能帮你少走一些弯路。


一、LangChain 是什么?为什么要学它?

简单说,LangChain 是一个构建 LLM 应用的框架。它帮你解决几个核心问题:

问题LangChain 的答案
每次调用都要拼 prompt?PromptTemplate 帮你管理
多轮对话上下文怎么维护?4 种 Memory 策略可选
多个 LLM 调用串起来?LCEL 链式调用
让 LLM 做多步推理?RunnableSequence 或 LangGraph
让 LLM 调用外部工具?Tool / Agent 机制

但记住:LangChain 不是魔法,它是 LLM API 的优雅封装。理解这一点,你就不会在抽象层里迷失。


二、从最基础开始:LLM 调用

一切从 ChatOpenAI 开始。虽然名字叫 OpenAI,但 LangChain 的 @langchain/openai 包兼容任何 OpenAI 格式的 API——我实际用的是 DeepSeek

import { ChatOpenAI } from "@langchain/openai";
const llm = new ChatOpenAI({
model: "deepseek-chat",
temperature: 0.7,
configuration: {
baseURL: "https://api.deepseek.com/v1",
},
});
const response = await llm.invoke("你好,请介绍一下自己");
console.log(response.content);

三种调用模式

LangChain 的 LLM 支持三种调用方式,适用于不同场景:

// 1. invoke - 标准调用,等待完整结果
const result = await llm.invoke(messages);
// 2. stream - 流式调用,逐 token 输出(打字机效果)
const stream = await llm.stream(messages);
for await (const chunk of stream) {
process.stdout.write(chunk.content?.toString() || "");
}
// 3. batch - 批量调用,并行处理多个输入
const results = await llm.batch([["用户问题1"], ["用户问题2"], ["用户问题3"]]);

💡 经验:流式调用对用户体验提升巨大——用户不需要盯着转圈等完整回复。但是后端实现更复杂,需要处理 WebSocket 逐帧推送。


三、消息模型:理解 BaseMessage 体系

LangChain 的消息体系是构建对话的基础。所有消息继承自 BaseMessage

import {
HumanMessage,
AIMessage,
SystemMessage,
} from "@langchain/core/messages";
// 系统消息 - 设定 AI 角色
const systemMsg = new SystemMessage("你是一个技术导师。");
// 用户消息
const userMsg = new HumanMessage("如何学好 TypeScript?");
// AI 回复
const aiMsg = new AIMessage("建议从基础类型开始,逐步深入泛型。");
// 组合后调用
const response = await llm.invoke([systemMsg, userMsg]);

关键点:LLM 是无状态的——每次调用你都得把”上下文”作为消息数组传回去。这就是记忆系统存在的原因。


四、四种记忆策略:从 Buffer 到向量检索

这是我在 v6.0 花最多精力研究的部分。多轮对话中,记忆管理直接决定了对话质量。

策略 1:Buffer Memory — 全部记录

最直接的策略:保留所有历史消息。

const history = new InMemoryChatMessageHistory();
// 每一轮对话都 push 新消息
history.addUserMessage("我叫小明");
history.addAIMessage("你好小明!");
// 获取全部历史传给 LLM
const allMessages = await history.getMessages();
const response = await llm.invoke(allMessages);

优点:信息无损,上下文完整 缺点:Token 消耗线性增长,长对话成本爆炸

运行 5 轮后,消息数从 1 -> 3 -> 5 -> 7 -> 9,每轮都在翻倍。

策略 2:Window Memory — 滑动窗口

只保留最近 N 轮对话:

const WINDOW_SIZE = 3; // 保留最近 3 轮
const maxMessages = WINDOW_SIZE * 2;
function getRecentMessages(allMessages: BaseMessage[]): BaseMessage[] {
if (allMessages.length <= maxMessages) return allMessages;
return allMessages.slice(allMessages.length - maxMessages);
}

优点:Token 消耗可控,实现简单 缺点:早期信息永久丢失。如果你在第 1 轮说了名字,第 5 轮再问”我叫什么”,它已经忘了。

策略 3:Summary Memory — 智能摘要

当消息超过阈值时,用 LLM 自动生成摘要,用摘要替代早期消息:

const SUMMARIZE_THRESHOLD = 6;
const RECENT_MESSAGES_TO_KEEP = 4;
const summarizerPrompt = PromptTemplate.fromTemplate(
`请用中文总结以下对话的核心信息,保留所有关键事实:
{conversation}
摘要:`,
);
if (allMessages.length >= SUMMARIZE_THRESHOLD) {
// 用 LLM 生成摘要
const summary = await summarizerPrompt
.pipe(llm)
.pipe(new StringOutputParser())
.invoke({ conversation: oldMessages });
// 清除后重建:摘要 + 最近 N 条消息
await history.clear();
await history.addMessages([
new SystemMessage(`对话摘要:${summary}`),
...recentMessages,
]);
}

优点:长对话中保留关键信息,Token 可控 缺点:依赖 LLM 摘要质量,有额外 API 调用开销

策略 4:Vector Memory — 语义检索

这是我个人最喜欢的一个。把每条消息向量化存储,检索时找语义最相关的:

import { OpenAIEmbeddings } from "@langchain/openai";
const embeddings = new OpenAIEmbeddings({
configuration: { baseURL: "https://dashscope.aliyuncs.com/compat-mode/v1" },
});
// 存储结构
interface VectorEntry {
text: string;
vector: number[];
metadata: { timestamp: number; role: string };
}
// 检索最相关记忆
async function retrieveRelevantMemories(query: string, topK = 2) {
const queryVector = await embeddings.embedQuery(query);
const scored = vectorStore.map((entry) => ({
...entry,
score: cosineSimilarity(queryVector, entry.vector),
}));
return scored.sort((a, b) => b.score - a.score).slice(0, topK);
}
// 余弦相似度计算
function cosineSimilarity(a: number[], b: number[]): number {
const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
const normA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
const normB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
return dotProduct / (normA * normB);
}

优点:语义检索,能跨话题关联,接近人类记忆方式 缺点:需要 embedding 模型,实现更复杂

📊 策略对比总结

策略Token 消耗信息保留实现复杂度适用场景
Buffer🔴 高✅ 完整⭐ 极简短对话,演示
Window🟢 可控⚠️ 有限⭐ 简单客服对话
Summary🟡 中等✅ 关键信息⭐⭐ 中等长对话
Vector🟢 可控✅ 语义关联⭐⭐⭐ 较复杂知识型 AI

五、LCEL:LangChain 表达式语言

LCEL 是 LangChain 最优雅的设计之一。用 |(或 .pipe())把组件串成管道:

import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
const chain = PromptTemplate.fromTemplate(
"用{style}的风格写一段关于{topic}的文字",
)
.pipe(llm)
.pipe(new StringOutputParser());
const result = await chain.invoke({
style: "武侠小说",
topic: "量子力学",
});

RunnableSequence:更复杂的多步链

当需要多步推理时,用 RunnableSequence

import { RunnableSequence } from "@langchain/core/runnables";
const deepChain = RunnableSequence.from([
// 第一步:分析问题
{
analysis: async (input: { question: string }) => {
const analysis = await analysisPrompt.pipe(llm).invoke(input);
return { ...input, analysis: analysis.content };
},
},
// 第二步:深入研究
{
research: async (input) => {
const research = await researchPrompt.pipe(llm).invoke(input);
return { ...input, research: research.content };
},
},
// 第三步:综合回答
async (input) => {
const conclusion = await synthesizePrompt.pipe(llm).invoke(input);
return conclusion.content;
},
]);

这种”分析 → 研究 → 综合”的三步模式,比直接提问的质量高很多——这其实就是 思维链(Chain-of-Thought) 的工程化实现。


六、结构化输出:让 LLM 说”人话”

LLM 返回的是字符串。当你需要 JSON 格式的结果时,要用 JsonOutputParser

import { JsonOutputParser } from "@langchain/core/output_parsers";
const jsonChain = PromptTemplate.fromTemplate(
`
将以下中文信息转换为 JSON 格式:
输入:{input}
参考格式(输出纯 JSON,不要 markdown 代码块):
{format}
`,
)
.pipe(llm)
.pipe(new JsonOutputParser());
const result = await jsonChain.invoke({
input: "张三,28岁,是一名软件工程师",
format: JSON.stringify({ name: "string", age: "number", job: "string" }),
});
// 输出: { name: "张三", age: 28, job: "软件工程师" }

⚠️ 注意事项:LLM 输出的 JSON 偶尔会格式错误。JsonOutputParser 有部分容错能力,但生产环境建议加 try-catch。

另一个坑:LLM 经常在 JSON 外面包 markdown 代码块(```json … ```),.pipe() 链里的输出解析器通常能处理,但如果在流式场景中自己拼字符串,要注意 strip 掉。


七、LangGraph:状态图驱动的工作流

v8.0 我学习到了 LangGraph。它与 LangChain 的关系:

  • LangChain = 构建块(LLM、Prompt、Memory、Chain)
  • LangGraph = 构建块之上的状态机框架

StateGraph 实现”规划→执行→评估”的 Plan-Execute 模式:

import { StateGraph, Annotation } from "@langchain/langgraph";
// 定义状态
const PlanExecuteState = Annotation.Root({
task: Annotation<string>(),
plan: Annotation<string[]>(),
completed: Annotation<string[]>(),
currentStepIndex: Annotation<number>(),
result: Annotation<string>(),
});
// 构建图
const workflow = new StateGraph(PlanExecuteState)
.addNode("planner", plannerNode) // 规划节点:拆解任务
.addNode("executor", executorNode) // 执行节点:执行一步
.addNode("replanner", replannerNode) // 重规划节点:评估完成度
// 条件路由 - LangGraph 的核心能力
.addConditionalEdges("planner", shouldContinue)
.addEdge("executor", "replanner")
.addConditionalEdges("replanner", afterReplan)
.setEntryPoint("planner");
const app = workflow.compile();
const result = await app.invoke({ task: "如何学习量子计算?" });

条件路由的决策函数:

// 从规划器出发:直接执行第一步
function shouldContinue(state: typeof PlanExecuteState.State) {
return "executor";
}
// 从重规划器出发:判断是否完成
function afterReplan(state: typeof PlanExecuteState.State) {
if (state.completed.length >= state.plan.length) {
return "__end__"; // 全部完成 → 结束
}
return "executor"; // 还有未完成步骤 → 继续执行
}

LangGraph 的优势:它不是线性链,而是一个有向图——你可以有循环(评估不通过就重做)、条件分支(不同问题走不同路径)、并行节点。


八、完整架构:WebSocket + React 前端

前面的都是独立脚本,生产环境需要一个真正的服务。这是我的整体架构:

┌─────────────────────────────┐
│ React 前端 (Vite) │
│ useWebSocket Hook │
│ │ │
│ ▼ WebSocket │
├─────────────────────────────┤
│ Node.js 后端 │
│ │
│ ┌──── 会话管理 ────┐ │
│ │ InMemorySession │ │
│ └──────────────────┘ │
│ │
│ ┌──── 路由分发 ────┐ │
│ │ stream / invoke │ │
│ │ batch / struct │ │
│ │ deep / langgraph│ │
│ └──────────────────┘ │
│ │
│ ┌──── 基础设施 ────┐ │
│ │ 限流器 / 日志 │ │
│ │ 心跳 / 优雅关闭 │ │
│ └──────────────────┘ │
└─────────────────────────────┘

WebSocket 消息流

// 服务端:流式逐帧推送
ws.send(
JSON.stringify({
type: "chunk",
content: "正在",
mode: "stream",
}),
);
// 客户端:逐帧累积
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "chunk") {
setContent((prev) => prev + msg.content);
} else if (msg.type === "done") {
setIsComplete(true);
}
};

九、实用技巧与踩坑记录

1. Prompt 管理

把提示词集中管理,不要散落在各个文件里:

prompts.ts
export const TECH_TUTOR_PROMPT = PromptTemplate.fromTemplate(`
你是一个资深技术导师,擅长用通俗易懂的方式解释技术概念。
当前主题:{topic}
用户水平:{level}
请用类比和实例帮助用户理解。`);

2. 限流的重要性

不加限制的 API 调用会让你在调试时浪费大量 Token:

// 简单的每会话每分钟限流
const now = Date.now();
const windowStart = now - 60_000;
const recentRequests = session.timestamps.filter((t) => t > windowStart);
if (recentRequests.length >= MAX_REQUESTS_PER_MINUTE) {
ws.send(JSON.stringify({ type: "error", content: "请求过于频繁" }));
return;
}

3. 优雅关闭

生产服务不能直接 kill,要等正在处理的请求完成:

process.on("SIGTERM", async () => {
console.log("收到 SIGTERM,正在关闭服务...");
wss.close(() => {
console.log("WebSocket 服务已关闭");
process.exit(0);
});
});

4. Buffer vs Stream 的选择

场景推荐方式原因
简短回答invoke实现简单,延迟可接受
长文生成/写作stream打字机效果,用户体验好
多次无关联查询batch并行处理,吞吐量高
多步骤推理deep / langgraph分步质量优于单一调用

十、学习路线图总结

这是我从 v1.0 到 v8.0 的学习路径,也推荐给你:

v1.0 🔤 基础调用 → ChatOpenAI, invoke/stream/batch
v2.0 🔗 链式调用 → LCEL, .pipe(), RunnableSequence
v3.0 📝 提示工程 → PromptTemplate, Few-shot, CoT
v4.0 🧠 深度思考 → 多步推理链
v5.0 🔧 重构与集成 → WebSocket 服务化
v6.0 💾 记忆系统 → Buffer → Window → Summary → Vector
v7.0 📚 RAG → 检索增强生成(进行中)
v8.0 🔄 LangGraph → 状态图驱动工作流

写在最后

学了这么久,最大的感受是:LangChain 不是全能,但它是个好工具箱

  • 简单的 LLM 调用,直接调 API 就好,不需要框架
  • 多轮对话,Memory 策略很有用
  • 复杂的多步推理,LangGraph 的图结构比硬编码的状态机优雅很多
  • 但是…… 如果场景简单,不要为了用框架而用框架

用 npm 包的依赖情况来总结就是:

@langchain/core — 必须,核心类型和接口
@langchain/openai — 如果你用 OpenAI / DeepSeek / 兼容 API
@langchain/langgraph — 需要复杂工作流时
langchain — 完整套件,按需引入

希望这篇文章对你有帮助。如果你也在学 LangChain,欢迎交流👋


项目地址learn-langchain.js

技术栈:TypeScript + LangChain.js + DeepSeek + WebSocket + React

文中所有代码均来自实际可运行的项目,不是伪代码。

🤖 LangChain.js 从入门到实战:构建 AI 应用的完整指南
/posts/langchain/
Author
Atopos
Published at
2026-06-13
License
CC BY-NC-SA 4.0

Some information may be outdated