基于ESM模块化构建DeepSeek LLM调用框架与轻量NLP任务实践

发布时间:2026/8/26 7:33:09
基于ESM模块化构建DeepSeek LLM调用框架与轻量NLP任务实践 1. 从“脚本小子”到“模块化工程”为什么我们需要 ESM 来调用 LLM几年前当我刚开始接触大语言模型LLM的 API 调用时我的代码通常长这样一个巨大的index.js文件开头是require(axios)中间夹杂着硬编码的 API Key 和几百行的 prompt 字符串最后用一个async function main()把所有逻辑塞进去。每次想加个新功能比如换个模型或者加个日志都感觉像是在拆一个随时会爆炸的毛线团。这种“脚本小子”式的开发在快速验证想法时还行一旦项目稍微复杂或者需要团队协作维护成本就会指数级上升。直到我开始用 ES 模块ESM来重构这些调用逻辑整个开发体验才豁然开朗。ESM 不是银弹但它提供了一种清晰、可预测的代码组织方式。想象一下你把 API 客户端、配置管理、工具函数、Prompt 模板分别放在不同的.mjs或.js文件里通过import/export像搭积木一样组合它们。这不仅让代码结构一目了然更重要的是它天然支持“关注点分离”——配置归配置业务逻辑归业务逻辑工具函数各司其职。当你需要调用像 DeepSeek 这样的 LLM 服务时这种模块化带来的好处是实实在在的你可以轻松地切换不同的模型配置、复用精心设计的 Prompt 模板、对 API 调用进行统一的错误处理和日志记录甚至可以把一些简单的 NLP 任务比如情感分析、关键词提取抽象成一个个独立的“Prompt 函数”。这就是本文想和你分享的核心如何用 ESM 模块化的工程思维来搭建一个健壮、可维护的 DeepSeek LLM 调用框架并顺带探索如何用精心设计的 Prompt将一些轻量级 NLP 任务从复杂的专用库中解放出来。我们不止步于“跑通一个 API 调用”而是着眼于构建一个可以随着需求灵活扩展的小型“工具箱”。无论你是想为自己的小项目添加智能对话能力还是希望系统化地管理多个 AI 服务这套思路都能让你事半功倍。2. 环境奠基配置一个纯净、现代的 Node.js ESM 项目在开始写第一行调用代码之前搭建一个正确的开发环境至关重要。这能避免后续遇到一堆诸如“requireis not defined”或“无法导入模块”的诡异问题。2.1 初始化项目与 package.json 的关键设置首先创建一个全新的项目目录并初始化mkdir deepseek-esm-project cd deepseek-esm-project npm init -y接下来打开生成的package.json文件我们需要进行几处关键修改以声明这是一个 ESM 项目{ name: deepseek-esm-project, version: 1.0.0, description: A modular DeepSeek LLM caller with ESM, type: module, main: index.js, scripts: { start: node index.js, dev: node --watch index.js }, keywords: [], author: , license: ISC }核心设置解析type: module这是最关键的一行。它告诉 Node.js这个项目中的所有.js文件都应该被解析为 ES 模块。没有它Node.js 会默认使用 CommonJS。设置之后你就可以在代码中使用import和export语法了。main: index.js指定项目的入口文件。scripts我们添加了一个dev脚本使用node --watch命令。这是 Node.js 18 自带的功能可以实现文件更改后自动重启对于开发调试非常方便无需额外安装nodemon。2.2 依赖安装选择轻量且兼容 ESM 的 HTTP 客户端我们将使用axios来发起 HTTP 请求因为它功能强大、使用广泛且对 Promise 支持良好。同时我们会安装dotenv来管理环境变量如 API Key这是一个最佳实践。npm install axios dotenv为什么是 Axios在 ESM 环境下你也可以使用原生的fetchNode.js 18 稳定支持。但axios提供了更便捷的拦截器、请求/响应数据自动转换如 JSON、以及更完善的错误处理机制对于生产级应用更友好。dotenv则能让我们将敏感的 API Key 从代码中剥离通过.env文件加载避免意外提交到代码仓库。安装完成后创建项目根目录下的.env文件并填入你的 DeepSeek API Key你需要在 DeepSeek 官网申请DEEPSEEK_API_KEYyour_api_key_here DEEPSEEK_API_BASEhttps://api.deepseek.com重要安全提示务必在.gitignore文件中添加.env确保这个包含密钥的文件不会被提交到 Git。2.3 项目结构设计模块化的雏形在写代码前先规划一下目录结构。一个清晰的目录是模块化成功的一半。deepseek-esm-project/ ├── node_modules/ ├── src/ │ ├── config/ # 配置模块 │ │ └── index.js │ ├── core/ # 核心调用模块 │ │ └── deepseekClient.js │ ├── prompts/ # Prompt 模板模块 │ │ ├── nlpTasks.js │ │ └── index.js │ └── utils/ # 工具函数模块 │ └── logger.js ├── .env # 环境变量本地 ├── .env.example # 环境变量示例 ├── .gitignore ├── package.json └── index.js # 项目主入口这个结构将不同的功能划分到不同的目录和文件中。src/config负责所有配置的集中管理src/core/deepseekClient.js是封装了底层 API 调用的客户端src/prompts/存放各种任务的 Prompt 模板src/utils/放一些通用的工具比如日志记录器。index.js作为入口负责将这些模块组合起来执行业务逻辑。3. 核心模块拆解构建健壮的 DeepSeek API 客户端现在我们来构建最核心的部分——一个封装了 DeepSeek Chat Completions API 的客户端模块。我们的目标是让它易于使用、易于配置并且具备良好的错误处理能力。3.1 配置管理模块集中化的环境与参数管理首先创建src/config/index.js// src/config/index.js import dotenv from dotenv; // 加载 .env 文件中的环境变量 dotenv.config(); // 验证必要的环境变量是否存在 const requiredEnvVars [DEEPSEEK_API_KEY]; for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { throw new Error(缺少必需的环境变量: ${envVar}); } } // 导出配置对象 export const config { apiKey: process.env.DEEPSEEK_API_KEY, apiBase: process.env.DEEPSEEK_API_BASE || https://api.deepseek.com, defaultModel: deepseek-chat, // DeepSeek 的主要对话模型 timeout: 30000, // 请求超时时间单位毫秒 }; // 导出默认的 API 请求头 export const getDefaultHeaders () ({ Authorization: Bearer ${config.apiKey}, Content-Type: application/json, });设计思路与避坑点集中管理所有配置项都从这个模块导出避免了在代码中散落着process.env.XXX。如果需要更换 API 服务商或调整超时时间只需修改这一个文件。启动时验证在应用启动伊始就检查关键环境变量是否存在可以尽早发现问题避免在运行时才因缺少 API Key 而报错提升调试效率。默认值为apiBase和timeout提供了合理的默认值即使.env文件未配置也能有基本的工作能力。3.2 客户端模块封装请求与响应处理接下来是重头戏src/core/deepseekClient.js// src/core/deepseekClient.js import axios from axios; import { config, getDefaultHeaders } from ../config/index.js; import { logger } from ../utils/logger.js; /** * DeepSeek API 客户端 */ class DeepSeekClient { constructor() { this.client axios.create({ baseURL: config.apiBase, timeout: config.timeout, headers: getDefaultHeaders(), }); // 可选添加请求拦截器用于日志、统一修改请求 this.client.interceptors.request.use( (requestConfig) { logger.info(发起请求: ${requestConfig.method?.toUpperCase()} ${requestConfig.baseURL}${requestConfig.url}); // 注意切勿在日志中打印完整的请求体以免泄露敏感信息如完整的 prompt return requestConfig; }, (error) { logger.error(请求拦截器错误:, error); return Promise.reject(error); } ); // 可选添加响应拦截器用于统一处理响应和错误 this.client.interceptors.response.use( (response) { logger.info(请求成功状态码: ${response.status}); return response; }, (error) { // 网络错误、超时、API 返回错误状态码如 4xx, 5xx都会进入这里 if (error.response) { // 请求已发出服务器响应了错误状态码 logger.error(API 错误响应: ${error.response.status}, { data: error.response.data, }); // 可以在此处根据不同的状态码抛出更友好的业务错误 const apiError new Error(DeepSeek API 错误: ${error.response.status}); apiError.status error.response.status; apiError.data error.response.data; throw apiError; } else if (error.request) { // 请求已发出但没有收到响应网络问题、超时 logger.error(网络错误或请求超时未收到响应:, error.message); throw new Error(网络连接失败: ${error.message}); } else { // 在设置请求时发生了错误 logger.error(请求配置错误:, error.message); throw error; } } ); } /** * 调用 DeepSeek Chat Completions API * param {Array} messages - 对话消息数组格式见 OpenAI API 规范 * param {Object} options - 其他可选参数如 model, temperature, max_tokens 等 * returns {PromiseString} - 模型生成的回复内容 */ async chat(messages, options {}) { const requestBody { model: options.model || config.defaultModel, messages: messages, temperature: options.temperature ?? 0.7, // 使用空值合并运算符提供默认值 max_tokens: options.max_tokens ?? 2048, stream: false, // 默认为非流式简化处理 ...options, // 允许调用者覆盖或添加其他任何官方支持的参数 }; try { logger.debug(发送请求体:, { ...requestBody, messages: [...] }); // 避免日志打印过长的 messages const response await this.client.post(/chat/completions, requestBody); // 提取并返回模型的回复文本 const content response.data.choices[0]?.message?.content; if (!content) { throw new Error(API 响应格式异常未找到回复内容); } return content.trim(); } catch (error) { // 拦截器已经处理了大部分错误并重新抛出这里主要处理业务逻辑错误 logger.error(调用 DeepSeek chat 方法失败:, error.message); throw error; // 将错误继续向上抛由调用方决定如何处理 } } } // 导出单例实例通常一个应用只需要一个客户端实例 export const deepSeekClient new DeepSeekClient();深度解析与实战经验Axios 实例化使用axios.create创建一个预配置的实例比每次调用都传递baseURL和headers更简洁、更高效。拦截器的威力这是将通用逻辑日志、错误格式化与业务逻辑分离的关键。请求拦截器适合添加签名、日志响应拦截器是统一处理 API 错误的最佳位置。注意在请求拦截器中记录日志时要避免打印完整的messages或包含敏感信息的请求体。健壮的chat方法参数设计messages参数遵循 OpenAI 的格式这保证了与主流生态的兼容性。options对象使用展开运算符...options使得方法能灵活接受未来 API 新增的任何参数。默认值策略使用空值合并运算符??来设置temperature和max_tokens的默认值它只在值为null或undefined时使用默认值比逻辑或||更安全因为0对于temperature是有效值。响应解析直接从响应数据中提取content并做了简单的存在性检查。在生产环境中你可能需要更严格的校验。错误处理方法内部的try-catch主要捕获业务逻辑错误如响应格式不对。HTTP 错误和网络错误已在拦截器中转换为更易读的错误类型并抛出。单例模式我们导出了一个已实例化的deepSeekClient。对于大多数应用一个全局的、配置好的客户端实例足够了。如果你需要连接多个不同配置的 DeepSeek 端点例如不同区域可以考虑导出DeepSeekClient类让调用方自行实例化。3.3 工具模块一个简单的日志器为了配合客户端我们实现一个简单的日志工具src/utils/logger.js// src/utils/logger.js /** * 简易日志工具可根据环境调整输出级别 * 在实际项目中建议使用 winston、pino 等专业日志库 */ const logLevel process.env.LOG_LEVEL || info; // 可从环境变量控制级别 const log (level, message, ...args) { const timestamp new Date().toISOString(); const logMessage [${timestamp}] [${level.toUpperCase()}] ${message}; // 简单的级别过滤 const levelPriority { error: 0, warn: 1, info: 2, debug: 3 }; if (levelPriority[level] levelPriority[logLevel]) { if (level error) { console.error(logMessage, ...args); } else { console.log(logMessage, ...args); } } }; export const logger { error: (msg, ...args) log(error, msg, ...args), warn: (msg, ...args) log(warn, msg, ...args), info: (msg, ...args) log(info, msg, ...args), debug: (msg, ...args) log(debug, msg, ...args), };这个日志器虽然简单但引入了“日志级别”的概念通过LOG_LEVEL环境变量可以控制输出量例如开发时设为debug生产环境设为warn避免生产环境日志泛滥。4. Prompt 即函数将轻量 NLP 任务模块化有了稳定的客户端我们就可以向上构建业务层。许多轻量级 NLP 任务如情感分析、摘要生成、关键词提取、实体识别、文本分类等并不总是需要引入NLTK、spaCy这样的重型库。对于精度要求不是极端苛刻的场景一个精心设计的 Prompt 配合强大的 LLM往往能快速得到一个不错的结果而且灵活性极高。4.1 设计可复用的 Prompt 模板我们在src/prompts/nlpTasks.js中定义一系列 Prompt 函数// src/prompts/nlpTasks.js /** * 情感分析 Prompt * param {string} text - 待分析的文本 * returns {Array} - 符合 Chat Completions API 格式的 messages 数组 */ export function createSentimentAnalysisPrompt(text) { return [ { role: system, content: 你是一个情感分析专家。请严格分析用户输入文本的情感倾向并仅返回以下三种标签之一positive积极、negative消极、neutral中性。不要添加任何解释性文字。 }, { role: user, content: 请分析以下文本的情感倾向\n\n${text}\n } ]; } /** * 文本摘要 Prompt * param {string} text - 待摘要的文本 * param {number} maxLength - 摘要最大长度字符数 * returns {Array} - messages 数组 */ export function createSummarizationPrompt(text, maxLength 200) { return [ { role: system, content: 你是一个文本摘要专家。请用简洁、连贯的中文概括以下文本的核心内容摘要长度不要超过 ${maxLength} 个字符。直接返回摘要文本不要添加“摘要”等前缀。 }, { role: user, content: 请为以下文本生成摘要\n\n${text}\n } ]; } /** * 关键词提取 Prompt * param {string} text - 待提取的文本 * param {number} topK - 提取关键词的数量 * returns {Array} - messages 数组 */ export function createKeywordExtractionPrompt(text, topK 5) { return [ { role: system, content: 你是一个关键词提取专家。请从用户提供的文本中提取出 ${topK} 个最核心的关键词或关键短语。请用中文逗号“”将关键词连接成一个字符串返回不要编号不要添加任何其他说明。 }, { role: user, content: 请从以下文本中提取关键词\n\n${text}\n } ]; } /** * 通用文本分类 Prompt多类别 * param {string} text - 待分类的文本 * param {Arraystring} categories - 候选类别数组如 [科技, 体育, 娱乐, 财经] * returns {Array} - messages 数组 */ export function createTextClassificationPrompt(text, categories) { const categoryList categories.join(、); return [ { role: system, content: 你是一个文本分类专家。请将用户输入的文本分类到以下类别之一${categoryList}。请仅返回类别名称不要添加任何解释。如果无法明确分类请返回你认为最接近的类别。 }, { role: user, content: 请对以下文本进行分类\n\n${text}\n } ]; }Prompt 设计心法明确的系统指令System Role这是引导模型行为的关键。指令必须清晰、具体、无歧义。例如在情感分析中我们明确限定了输出只能是三个标签之一并强调“不要添加任何解释”。这能极大提高输出格式的稳定性。结构化输出尽可能让模型输出结构化的简单格式如单个标签、逗号分隔的字符串。这比让模型输出一段自由文本更容易被后续代码解析和处理。参数化将变量如文本text、摘要长度maxLength、类别列表categories作为函数参数传入使得同一个 Prompt 模板可以处理不同的输入提高了复用性。示例的威力Few-Shot Prompting对于更复杂的任务可以在system或user消息中加入一两个输入输出的例子能显著提升模型的表现。本文为简洁未展示但在实际中是高级技巧。4.2 创建统一的 Prompt 模块出口为了方便导入我们在src/prompts/index.js中统一导出// src/prompts/index.js export * from ./nlpTasks.js; // 未来可以导出其他类型的 Prompt 模块如 export * from ./creativeWriting.js;这样在其他文件中就可以通过import { createSentimentAnalysisPrompt } from ../prompts/index.js;来引入所需的 Prompt 工厂函数。5. 实战演练组装模块完成 NLP 任务流水线现在所有零件都已备齐。让我们在项目入口index.js中将它们组装起来实现一个完整的、可交互的示例。// index.js import { deepSeekClient } from ./src/core/deepseekClient.js; import * as prompts from ./src/prompts/index.js; import { logger } from ./src/utils/logger.js; import readline from readline/promises; // 用于创建命令行交互界面 import { stdin as input, stdout as output } from process; // 创建命令行交互接口 const rl readline.createInterface({ input, output }); /** * 主运行函数 */ async function main() { logger.info(DeepSeek ESM 模块化调用示例启动); try { // 示例1情感分析 const sampleText1 这个产品的用户体验太棒了界面流畅功能强大彻底解决了我的痛点; logger.info(\n 示例1情感分析 ); logger.info(输入文本${sampleText1}); const sentimentMessages prompts.createSentimentAnalysisPrompt(sampleText1); const sentimentResult await deepSeekClient.chat(sentimentMessages, { temperature: 0.1 }); // 低 temperature 使输出更确定 logger.info(情感分析结果${sentimentResult}); // 示例2文本摘要 const sampleText2 在人工智能领域大语言模型近年来取得了突破性进展。这些模型通过在海量文本数据上进行训练学会了生成人类般的文本、翻译语言、编写代码等多种任务。技术的进步使得像聊天机器人、智能助手这样的应用越来越普及但也引发了关于数据隐私、算法偏见和就业影响的广泛讨论。; logger.info(\n 示例2文本摘要 ); logger.info(输入文本前100字符${sampleText2.substring(0, 100)}...); const summaryMessages prompts.createSummarizationPrompt(sampleText2, 150); const summaryResult await deepSeekClient.chat(summaryMessages); logger.info(摘要结果${summaryResult}); // 示例3关键词提取 logger.info(\n 示例3关键词提取 ); const keywordMessages prompts.createKeywordExtractionPrompt(sampleText2, 3); const keywordResult await deepSeekClient.chat(keywordMessages); logger.info(关键词提取结果${keywordResult}); // 示例4交互式文本分类 logger.info(\n 示例4交互式文本分类 ); const categories [科技, 体育, 娱乐, 财经, 健康, 其他]; const userInput await rl.question(请输入一段文本我将把它分类到 [${categories.join(, )}] 中\n ); if (userInput.trim()) { const classificationMessages prompts.createTextClassificationPrompt(userInput.trim(), categories); const classificationResult await deepSeekClient.chat(classificationMessages); logger.info(分类结果${classificationResult}); } else { logger.warn(输入为空跳过分类示例。); } } catch (error) { logger.error(程序运行过程中发生错误, error); // 可以根据 error.status 或 error.message 进行更精细的错误处理例如重试、降级等 } finally { rl.close(); // 关闭命令行接口 logger.info(程序运行结束。); } } // 执行主函数 main();运行与观察在终端执行npm run dev或node index.js。你将看到程序依次执行三个预设的 NLP 任务示例并最终等待你输入一段文本来进行交互式分类。观察控制台输出你不仅能看到任务结果还能看到通过logger记录的请求和响应日志取决于LOG_LEVEL设置。这个流程清晰地展示了模块化的优势index.js应用层只关心业务逻辑和流程控制——“做什么”。deepseekClient.js基础设施层负责以可靠的方式与 API 通信——“怎么做通信”。nlpTasks.js业务能力层负责将业务意图情感分析转化为机器可理解的指令Prompt——“怎么做转换”。config/index.js和logger.js支撑层提供配置和工具支持。每一层职责单一通过清晰的import接口连接修改或替换其中任何一部分对其他部分的影响都最小。6. 进阶优化与生产环境考量上面的示例是一个完整的起点但要用于实际生产或更复杂的项目还需要考虑以下几个方面6.1 性能优化实现请求缓存与批处理频繁调用 API 会产生费用和延迟。对于某些不那么需要实时性的任务如静态文章的关键词提取引入缓存机制能极大提升性能并降低成本。// src/utils/cache.js import NodeCache from node-cache; // 创建一个 TTL生存时间为 1 小时的缓存实例 const responseCache new NodeCache({ stdTTL: 3600 }); /** * 带缓存的 LLM 调用包装器 * param {Function} callFn - 实际的 LLM 调用函数需返回 Promise * param {String} cacheKey - 基于输入参数生成的唯一缓存键 * returns {Promiseany} */ export async function withCache(callFn, cacheKey) { const cachedResult responseCache.get(cacheKey); if (cachedResult) { logger.debug(缓存命中: ${cacheKey}); return cachedResult; } logger.debug(缓存未命中执行调用: ${cacheKey}); const freshResult await callFn(); responseCache.set(cacheKey, freshResult); return freshResult; } // 使用示例在调用客户端前生成一个基于 Prompt 内容的缓存键 // const cacheKey chat:${model}:${JSON.stringify(messages)}; // 简单示例实际可用哈希 // const result await withCache(() deepSeekClient.chat(messages, options), cacheKey);注意事项缓存键cacheKey的设计至关重要。它必须能唯一标识一次请求。一个简单的方法是将model和序列化后的messages数组拼接起来。但要注意如果messages很大直接序列化可能效率低可以考虑使用哈希函数如crypto.createHash(md5)生成一个固定长度的键。同时要清楚缓存的应用场景对于对话等上下文相关的请求缓存可能不适用。6.2 稳定性保障重试机制与熔断器网络和服务不稳定是常态。为关键请求添加重试逻辑可以提升整体成功率。// src/utils/retry.js /** * 带指数退避的简单重试函数 * param {Function} fn - 要重试的异步函数 * param {Object} options - 配置项 * param {number} options.maxRetries - 最大重试次数不含首次尝试 * param {number} options.baseDelay - 基础延迟毫秒数 * param {Function} options.shouldRetry - 判断错误是否应该重试的函数 */ export async function retryWithBackoff(fn, options {}) { const { maxRetries 3, baseDelay 1000, shouldRetry (error) true } options; let lastError; for (let attempt 0; attempt maxRetries; attempt) { try { return await fn(); } catch (error) { lastError error; if (attempt maxRetries || !shouldRetry(error)) { break; } // 计算指数退避延迟并加上一点随机抖动Jitter避免惊群 const delay baseDelay * Math.pow(2, attempt) Math.random() * 100; logger.warn(请求失败第 ${attempt 1} 次重试将在 ${delay.toFixed(0)}ms 后执行。错误:, error.message); await new Promise(resolve setTimeout(resolve, delay)); } } throw lastError; // 重试耗尽抛出最后的错误 } // 在客户端或调用处使用 // try { // const result await retryWithBackoff( // () deepSeekClient.chat(messages, options), // { // maxRetries: 2, // shouldRetry: (error) error.status 500 || error.message.includes(timeout) // 只对服务器错误和超时重试 // } // ); // } catch (error) { ... }重试策略选择并非所有错误都值得重试。4xx错误如认证失败、参数错误是客户端问题重试无用5xx错误服务器内部错误和网络超时则是重试的主要目标。shouldRetry函数让你可以精细控制重试条件。6.3 输出规范化与后处理LLM 的输出是自由文本即使有严格的 Prompt 指令也可能出现格式偏差。增加一个后处理层来清洗和验证输出是必要的。// src/utils/postProcessors.js /** * 处理情感分析结果确保返回预定义的标签 * param {string} llmOutput - 模型的原始输出 * returns {string} - 规范化后的标签 */ export function normalizeSentimentOutput(llmOutput) { const cleaned llmOutput.trim().toLowerCase(); if (cleaned.includes(positive) || cleaned.includes(积极)) return positive; if (cleaned.includes(negative) || cleaned.includes(消极)) return negative; if (cleaned.includes(neutral) || cleaned.includes(中性)) return neutral; // 如果无法识别返回一个默认值或抛出错误 logger.warn(无法规范化的情感分析输出: ${llmOutput} 返回默认值 neutral); return neutral; } /** * 处理关键词提取结果分割字符串并去重 * param {string} llmOutput - 模型返回的用逗号分隔的关键词字符串 * returns {Arraystring} - 关键词数组 */ export function parseKeywordsOutput(llmOutput) { return llmOutput .split(/[,]/) // 支持中英文逗号 .map(kw kw.trim()) .filter(kw kw.length 0); } // 在业务逻辑中使用 // const rawSentiment await deepSeekClient.chat(sentimentMessages); // const finalSentiment normalizeSentimentOutput(rawSentiment);后处理模块是保证下游系统稳定性的重要防线。它可以将 LLM 的“软”输出转化为程序可可靠消费的“硬”数据。7. 总结与扩展方向通过这一套基于 ESM 的模块化设计我们构建的不仅仅是一个能调用 DeepSeek API 的脚本而是一个可维护、可测试、易扩展的小型框架。回顾一下关键收益清晰度代码按功能分治结构一目了然新人上手快。可维护性修改配置、更换模型、增加新的 NLP 任务都只需要改动局部不会牵一发而动全身。可测试性每个模块都可以被独立地单元测试。例如你可以 mockaxios来测试DeepSeekClient的错误处理逻辑而无需真正调用 API。复用性deepSeekClient和各类Prompt函数可以在项目的任何地方被导入使用。你可以在此基础上继续扩展流式响应Streaming修改deepseekClient.chat方法支持stream: true参数并处理服务器发送事件Server-Sent Events实现打字机效果。多模型支持抽象一个通用的LLMClient基类然后派生出DeepSeekClient、OpenAIClient等并在配置层决定使用哪一个轻松实现模型切换或降级。更复杂的 Prompt 管理将 Prompt 模板移至 JSON 文件或数据库并开发一个简单的管理界面实现 Prompt 的版本管理和 A/B 测试。任务队列与并发控制对于需要批量处理大量文本的场景可以引入bull或p-queue库来管理任务队列控制并发请求数避免触发 API 的速率限制。从我自己的实践经验来看初期多花一点时间在结构设计上后期会节省大量的调试和重构时间。当你的 AI 功能从一个简单的“小玩具”成长为核心业务组件时这套模块化架构的价值会更加凸显。

相关新闻