前言
当我第一次打开 LangChain 的文档时,说实话有点懵。Runnable、LCEL、Chain、Agent……这些概念堆在一起,让人不知从何下手。
于是我决定——用实际项目来学。
这篇文章是我从 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("你好小明!");
// 获取全部历史传给 LLMconst 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 管理
把提示词集中管理,不要散落在各个文件里:
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/batchv2.0 🔗 链式调用 → LCEL, .pipe(), RunnableSequencev3.0 📝 提示工程 → PromptTemplate, Few-shot, CoTv4.0 🧠 深度思考 → 多步推理链v5.0 🔧 重构与集成 → WebSocket 服务化v6.0 💾 记忆系统 → Buffer → Window → Summary → Vectorv7.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
文中所有代码均来自实际可运行的项目,不是伪代码。
Some information may be outdated