为什么 LangChain 代码总是越写越乱?
很多人学习 LangChain 的路径是这样的:
| 阶段 | 状态 | 问题 |
|---|---|---|
| 第 1 天 | 写了一个 100 行的 RAG 脚本,能跑起来 | ✅ 很开心 |
| 第 3 天 | 加了流式输出、对话历史、两种检索模式 | ⚠️ 300 行,有点乱 |
| 第 1 周 | 加了 3 种记忆策略、5 种 Prompt 模板、批处理能力 | ❌ 800 行,一个文件,不敢改 |
| 第 2 周 | 要加 Agent 工具调用、多轮推理…… | 💥 重构吧,不然没法维护了 |
核心问题:你把「业务逻辑」和「LangChain 框架代码」混在了一起。
💡 架构原则:框架是工具,不是架构。你的业务架构要独立于框架而存在。
一、我的架构演进路径
阶段 1:单文件脚本(100 行)
// rag-chain.ts — 能跑,但只有你能看懂import { ChatOpenAI } from "@langchain/openai";import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";import { PromptTemplate } from "@langchain/core/prompts";
const llm = new ChatOpenAI({...});const vectorStore = await MemoryVectorStore.fromDocuments(docs, embeddings);const retriever = vectorStore.asRetriever();const ragPrompt = PromptTemplate.fromTemplate(`...`);
const chain = RunnablePassthrough.assign({ context: (input) => retriever.invoke(input.question)}).pipe(ragPrompt).pipe(llm).pipe(parser);
const result = await chain.invoke({ question: "..." });问题:
- ❌ LLM 配置硬编码
- ❌ 没有会话管理
- ❌ 没有日志
- ❌ 没有错误处理
- ❌ 所有逻辑揉在一起
阶段 2:模块化拆分(分层架构)
我把项目拆成了 7 个模块,每个模块只做一件事:
src/server/├── config.ts # 配置集中管理├── logger.ts # 日志封装├── llm.ts # LLM 实例封装(单例)├── session.ts # 会话管理 + 状态存储├── prompts.ts # Prompt 模板库├── memory-strategies.ts # 四种记忆策略(策略模式)├── rate-limiter.ts # 限流保护└── handlers/ # 业务处理器(策略模式) ├── stream.ts # 流式调用 ├── invoke.ts # 非流式调用 ├── batch.ts # 批处理 ├── structured.ts # 结构化输出(JSON) ├── deep.ts # 深度思考(链式推理) └── deep-langgraph.ts # LangGraph Plan & Execute架构分层图:
┌──────────────────────────────────────────────────────────┐│ WebSocket 接入层 ││ ws-server.ts — 连接管理、消息路由、命令解析 │└──────────────────────┬───────────────────────────────────┘ │ ┌──────────────────┼──────────────────┐ │ │ │┌───▼────┐ ┌───────▼───────┐ ┌──────▼──────┐│ handlers│ │ memory-strategies│ │ prompts │ ← 业务能力层└───┬────┘ └─────────┬───────┘ └─────────────┘ │ │┌───▼────┐ ┌───────▼───────┐ ┌─────────────┐│ session│ │ llm │ │ rate-limiter│ ← 状态/能力层└───┬────┘ └───────────────┘ └─────────────┘ │┌───▼────┐ ┌───────────────┐│ config │ │ logger │ ← 横切关注点└────────┘ └───────────────┘二、核心模块设计详解
模块 1:config.ts — 配置集中管理
原则:所有可变配置都从环境变量读取,不要硬编码。
import dotenv from "dotenv";dotenv.config();
export const CONFIG = { port: parseInt(process.env.WS_PORT || "8080", 10),
llm: { model: "GLM-4.7", apiKey: process.env.ARK_API_KEY, // 敏感信息从环境变量读 temperature: 0.7, timeout: 30000, maxRetries: 2, baseURL: "https://ark.cn-beijing.volces.com/api/coding/v3", },
rateLimit: { windowMs: 60_000, // 时间窗口:1 分钟 maxRequests: 20, // 最多 20 次请求 },
systemPrompt: "你是一个智能助手,请用友好、自然的方式回答用户的问题。",};为什么这么设计?
- ✅ 开发/生产环境切换只需要改
.env - ✅ 敏感信息不会提交到 Git
- ✅ 配置改了不用重新编译
- ✅ 全局只有一份配置,避免重复定义
模块 2:llm.ts — LLM 实例单例封装
原则:LLM 实例只创建一次,所有 handler 共享。
import { ChatOpenAI } from "@langchain/openai";import { CONFIG } from "./config";
/** LLM 实例:纯流式/非流式调用,不绑定工具 */export const llm = new ChatOpenAI({ model: CONFIG.llm.model, apiKey: CONFIG.llm.apiKey, temperature: CONFIG.llm.temperature, streamUsage: false, timeout: CONFIG.llm.timeout, maxRetries: CONFIG.llm.maxRetries, configuration: { baseURL: CONFIG.llm.baseURL, },});使用方式:
// 任何 handler 都可以直接 import 使用import { llm } from "../llm";
const response = await llm.invoke(messages);const stream = await llm.stream(messages);为什么这么设计?
- ✅ 避免重复创建 LLM 实例(浪费资源)
- ✅ 配置统一,不会出现「这里 temperature 是 0.3,那里是 0.7」的混乱
- ✅ 以后换模型只需要改这一个文件
模块 3:session.ts — 会话状态管理
原则:每个连接一个会话,状态与连接分离。
import { SystemMessage, HumanMessage, AIMessage } from "@langchain/core/messages";import { CONFIG } from "./config";
export type MemoryStrategy = "buffer" | "window" | "summary" | "vector";
export interface Session { id: string; messages: BaseMessage[]; createdAt: number; messageCount: number; // 各种配置都挂在 session 上 memoryStrategy: MemoryStrategy; windowSize: number; summaryThreshold: number; recentToKeep: number; vectorEntries: VectorEntry[]; currentTemplate?: string;}
// 内存存储会话(生产环境可以换成 Redis)const sessions = new Map<string, Session>();
export function createSession(id: string): Session { /* ... */ }export function getSession(id: string): Session | undefined { /* ... */ }export function deleteSession(id: string): void { /* ... */ }export function addUserMessage(session: Session, content: string): void { /* ... */ }export function addAIMessage(session: Session, content: string): void { /* ... */ }export function clearHistory(session: Session): void { /* ... */ }为什么这么设计?
- ✅ 会话与 WebSocket 连接解耦(连接断开可以恢复会话)
- ✅ 每个会话的配置独立(A 用户用 buffer 策略,B 用户用 vector 策略)
- ✅ 状态变更有统一入口,不会乱改 session.messages
模块 4:prompts.ts — Prompt 模板库
原则:Prompt 是业务资产,要集中管理,可复用,可版本化。
import { PromptTemplate } from "@langchain/core/prompts";
export const promptTemplates: Record<string, PromptTemplate> = { tech: PromptTemplate.fromTemplate(`你是一位专业的技术导师。请用简洁易懂的语言回答问题。用户问题:{question}`), life: PromptTemplate.fromTemplate(`...`), english: PromptTemplate.fromTemplate(`...`), interview: PromptTemplate.fromTemplate(`...`), cot: PromptTemplate.fromTemplate(`...`), json: PromptTemplate.fromTemplate(`...`),};
export const DEFAULT_TEMPLATE = "tech";
export function getPromptTemplate(key?: string): PromptTemplate { return promptTemplates[key ?? DEFAULT_TEMPLATE] ?? promptTemplates[DEFAULT_TEMPLATE];}使用方式:
import { getPromptTemplate } from "../prompts";
const template = getPromptTemplate(templateKey);const formatted = await template.format({ question: content });为什么这么设计?
- ✅ 所有 Prompt 在一起,方便对比和优化
- ✅ 新增模板只需要在这里加,不用改业务代码
- ✅ 可以写单元测试验证每个模板的输出效果
模块 5:handlers/ — 业务处理器(策略模式)
原则:每种调用模式是一个独立的 handler,新增模式不影响已有代码。
import { llm } from "../llm";import { addAIMessage, addUserMessage } from "../session";import { getContextMessages } from "../memory-strategies";import { getPromptTemplate } from "../prompts";
export async function handleStream( ws: WebSocket, session: Session, content: string, templateKey: string = "tech",): Promise<void> { // 1. 参数校验 if (!content.trim()) { ws.send(JSON.stringify({ type: "error", content: "消息不能为空" })); return; }
// 2. 用 Prompt 模板包装 const template = getPromptTemplate(templateKey); const formatted = await template.format({ question: content }); addUserMessage(session, formatted);
try { // 3. 根据记忆策略构建上下文 const contextMessages = await getContextMessages(session, content);
// 4. 流式调用 LLM const stream = await llm.stream(contextMessages); let collected = "";
for await (const chunk of stream) { const text = typeof chunk.content === "string" ? chunk.content : ""; if (!text) continue; collected += text; // 逐字符发送 → 打字机效果 for (const char of text) { ws.send(JSON.stringify({ type: "chunk", content: char })); } }
const reply = collected || "(空回复)"; addAIMessage(session, reply);
// 5. 向量策略:将对话存入向量库 if (session.memoryStrategy === "vector") { addToVectorStore(session, content, reply); }
ws.send(JSON.stringify({ type: "done", mode: "stream", content: reply })); } catch (error: any) { logger.error(`[${session.id}] stream 调用失败`, error.message); ws.send(JSON.stringify({ type: "error", content: `请求失败: ${error.message}` })); }}其他 handler 的结构完全一致:
| Handler | 职责 |
|---|---|
handleStream | 流式调用(打字机效果) |
handleInvoke | 非流式调用(完整返回) |
handleBatch | 批处理(多个问题一起问) |
handleStructured | 结构化输出(JSON 解析) |
handleDeep | 深度思考(多轮推理链) |
handleDeepLangGraph | LangGraph Plan & Execute |
为什么这么设计?
- ✅ 每个 handler 不超过 60 行,容易理解和修改
- ✅ 新增模式只需要加一个新文件,不会改坏已有的代码
- ✅ 可以单独测试每个 handler
- ✅ 代码复用:所有 handler 都用同一个 llm、session、prompts
模块 6:memory-strategies.ts — 策略模式的完美体现
四种记忆策略,对外只有一个接口 getContextMessages:
export async function getContextMessages( session: Session, currentInput: string): Promise<BaseMessage[]> { switch (session.memoryStrategy) { case "buffer": return bufferContext(session); case "window": return windowContext(session); case "summary": return await summaryContext(session); case "vector": return await vectorContext(session, currentInput); default: return bufferContext(session); }}
// ---------- 四种策略的具体实现 ----------
function bufferContext(session: Session): BaseMessage[] { // 全量保留所有消息 return session.messages;}
function windowContext(session: Session): BaseMessage[] { // 只保留最近 N 轮对话 const system = session.messages.find(m => m._getType() === "system"); const recent = session.messages.slice(-session.windowSize * 2); return system ? [system, ...recent] : recent;}
async function summaryContext(session: Session): Promise<BaseMessage[]> { // 超过阈值触发压缩,旧消息变摘要,只保留最近几条 if (nonSystem.length >= session.summaryThreshold) { session.summary = await compressToSummary(...); } return buildContextFromSummary(session);}
async function vectorContext(session: Session, input: string): Promise<BaseMessage[]> { // 向量检索相关历史 + 最近对话 const related = findRelatedHistory(session.vectorEntries, input); const recent = session.messages.slice(-4); return [...related, ...recent];}使用方式(对 handler 完全透明):
// handler 里只需要这一行,不用关心用的是哪种策略const contextMessages = await getContextMessages(session, content);为什么这是最好的设计?
- ✅ 开闭原则:新增记忆策略只需要加一个 case,不需要改任何 handler
- ✅ 单一职责:每种策略的实现是独立的
- ✅ 可测试:可以单独测试每种策略的效果
- ✅ 对调用方透明:handler 完全不需要知道底层用的是什么策略
模块 7:rate-limiter.ts — 系统保护
工程化的底线:你的服务必须能扛住恶意攻击或误操作。
import { CONFIG } from "./config";
interface RateLimitEntry { count: number; resetTime: number;}
const rateLimitMap = new Map<string, RateLimitEntry>();
export function checkRateLimit(sessionId: string): boolean { const now = Date.now(); const entry = rateLimitMap.get(sessionId);
// 窗口过期,重置计数 if (!entry || now >= entry.resetTime) { rateLimitMap.set(sessionId, { count: 1, resetTime: now + CONFIG.rateLimit.windowMs, }); return true; }
// 在窗口内,检查是否超限 if (entry.count >= CONFIG.rateLimit.maxRequests) { return false; }
entry.count++; return true;}
export function clearRateLimit(sessionId: string): void { rateLimitMap.delete(sessionId);}在 ws-server.ts 中使用:
ws.on("message", async (raw) => { // 限流检查 if (data.type === "message" && !checkRateLimit(sessionId)) { ws.send(JSON.stringify({ type: "error", content: "请求过于频繁,请稍后再试" })); return; } // ... 正常处理});为什么必须有限流?
- ✅ 防止用户狂刷消息把 API 额度用光
- ✅ 防止前端 bug 导致死循环发请求
- ✅ 防止恶意攻击(DDoS 虽然防不住,但至少能防脚本刷)
三、接入层:ws-server.ts 的路由设计
WebSocket 服务器是整个系统的「门面」,它只做 5 件事:
export function createServer(): WebSocketServer { const wss = new WebSocketServer({ port: CONFIG.port });
wss.on("connection", (ws) => { const sessionId = randomUUID().slice(0, 8); const session = createSession(sessionId);
ws.send(JSON.stringify({ type: "welcome", sessionId, content: "欢迎连接到 AI 对话服务器!", }));
ws.on("message", async (raw) => { // Step 1:消息解析 + 错误处理 let data: ClientMessage; try { data = JSON.parse(raw.toString()); } catch { ws.send(JSON.stringify({ type: "error", content: "无效的 JSON 格式" })); return; }
// Step 2:会话获取 const sess = getSession(sessionId); if (!sess) return;
// Step 3:限流检查 if (data.type === "message" && !checkRateLimit(sessionId)) { ws.send(JSON.stringify({ type: "error", content: "请求过于频繁" })); return; }
// Step 4:命令处理(/help /clear /memory 等) if (content.startsWith("/")) { const handled = handleCommand(ws, sessionId, content, sess); if (handled) break; }
// Step 5:业务路由 → 分发给对应的 handler const mode = data.mode || "stream";
if (mode === "stream") { await handleStream(ws, sess, content, templateKey); } else if (mode === "invoke") { await handleInvoke(ws, sess, content, templateKey); } else if (mode === "batch") { await handleBatch(ws, sess, content, templateKey); } else if (mode === "structured") { await handleStructured(ws, sess, content, templateKey); } else if (mode === "deep") { await handleDeep(ws, sess, content); } else if (mode === "deep-langgraph") { await handleDeepLangGraph(ws, sess, content); } }); });}ws-server.ts 的设计原则:
- ✅ 它不做业务逻辑,只做路由分发
- ✅ 它不认识 LangChain,只认识 handler 接口
- ✅ 横切关注点在这里处理:连接、会话、限流、命令
四、架构设计的 7 条黄金原则
原则 1:单一职责原则(SRP)
每个模块只做一件事。
config.ts→ 只存配置llm.ts→ 只封装 LLM 实例session.ts→ 只管理会话状态prompts.ts→ 只管理 Prompt 模板- 每个 handler → 只处理一种调用模式
反模式:一个文件 1000 行,什么都做。
原则 2:开闭原则(OCP)
对扩展开放,对修改关闭。
好设计:新增记忆策略 → 加一个函数,加一个 case 坏设计:新增记忆策略 → 改 10 个地方的代码
好设计:新增调用模式 → 加一个 handler 文件 坏设计:新增调用模式 → 在主文件里堆 100 行 if-else
原则 3:依赖倒置原则(DIP)
依赖抽象,不依赖具体实现。
我的设计:handler 依赖 getContextMessages() 这个抽象接口,不依赖具体的 buffer/window/summary/vector 实现。
好处:切换记忆策略,handler 一行代码都不用改。
原则 4:不要重复自己(DRY)
相同的逻辑只写一次。
反模式:每个 handler 里都写一遍 new ChatOpenAI({...})
好设计:LLM 实例只创建一次,全局共享。
原则 5:配置与代码分离
所有可变的东西都不应该硬编码。
- 端口号 → 环境变量
- API Key → 环境变量
- Temperature → 配置文件
- 限流阈值 → 配置文件
原则 6:错误处理是一等公民
不要假设一切都会成功。
每个 handler 都必须有 try-catch:
- ✅ 捕获 LLM 调用失败
- ✅ 记录错误日志
- ✅ 返回友好的错误信息给客户端
- ✅ 不要让服务崩溃
原则 7:日志是你的眼睛
生产环境没有 debugger,你能依赖的只有日志。
关键节点必须打日志:
- 客户端连接 / 断开
- 收到的消息(前 50 个字符)
- 选择的模式和模板
- 实际发给 AI 的 Prompt
- 记忆策略和上下文消息数
- 调用耗时
- 错误信息
五、全文总结
架构演进路径
| 阶段 | 架构 | 可维护性 | 团队协作 |
|---|---|---|---|
| 单文件脚本 | 所有逻辑揉在一起 | ❌ 差 | ❌ 没法协作 |
| 模块化拆分 | 分层架构 + 策略模式 | ✅ 好 | ✅ 可以协作 |
| 微服务 | LangChain 服务独立部署 | ✅ 很好 | ✅ 大规模团队 |
我的模块清单
| 模块 | 职责 | 设计模式 |
|---|---|---|
config.ts | 配置集中管理 | 单例 |
llm.ts | LLM 实例封装 | 单例 |
session.ts | 会话状态管理 | 仓库模式 |
prompts.ts | Prompt 模板库 | 工厂模式 |
memory-strategies.ts | 四种记忆策略 | 策略模式 |
handlers/* | 各种调用模式 | 策略模式 |
rate-limiter.ts | 限流保护 | 滑动窗口 |
ws-server.ts | 接入层 + 路由 | 门面模式 |
一句话总结
LangChain 是工具,不是架构。好的架构是让业务逻辑独立于框架存在,框架只是实现业务的手段。
项目源码:learn-langchain.js
上一篇:[Prompt Engineering:把 LLM 当 API 用,而不是陪聊](Prompt Engineering(二).md) 下一篇预告:[LangGraph 多步推理:从「直线执行」到「有思考的 Agent」](LangGraph 多步推理(四).md)
Some information may be outdated