LOADING
3385 words
17 minutes
LangChain 工程模块化(三)

为什么 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 — 配置集中管理

原则:所有可变配置都从环境变量读取,不要硬编码。

src/server/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 共享。

src/server/llm.ts
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 — 会话状态管理

原则:每个连接一个会话,状态与连接分离。

src/server/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 是业务资产,要集中管理,可复用,可版本化。

src/server/prompts.ts
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,新增模式不影响已有代码。

src/server/handlers/stream.ts
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深度思考(多轮推理链)
handleDeepLangGraphLangGraph Plan & Execute

为什么这么设计?

  • ✅ 每个 handler 不超过 60 行,容易理解和修改
  • ✅ 新增模式只需要加一个新文件,不会改坏已有的代码
  • ✅ 可以单独测试每个 handler
  • ✅ 代码复用:所有 handler 都用同一个 llm、session、prompts

模块 6:memory-strategies.ts — 策略模式的完美体现

四种记忆策略,对外只有一个接口 getContextMessages

src/server/memory-strategies.ts
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 — 系统保护

工程化的底线:你的服务必须能扛住恶意攻击或误操作。

src/server/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 件事:

src/server/ws-server.ts
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.tsLLM 实例封装单例
session.ts会话状态管理仓库模式
prompts.tsPrompt 模板库工厂模式
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)

LangChain 工程模块化(三)
/posts/2026-7-1/3-langchain-工程模块化/
Author
Atopos
Published at
2026-07-01
License
CC BY-NC-SA 4.0

Some information may be outdated