1. 项目缘起当AI能力需要被“管理”时最近两年AI原生应用开发的热度居高不下。无论是大语言模型LLM的直接调用还是基于LangChain这类框架构建复杂的AI工作流开发者们都在积极探索如何将AI能力无缝集成到自己的产品中。然而一个普遍的现象是很多初期的AI应用项目其代码结构往往呈现出一种“胶水代码”的状态业务逻辑、AI调用、数据预处理、结果后处理、错误处理等全部混杂在一起。这种代码在原型验证阶段或许可行一旦进入迭代和维护阶段就会迅速变得难以管理成为技术债的重灾区。我自己在构建一个内部知识库问答系统时就深有体会。最初为了快速验证效果我直接在Express的路由处理函数里写满了对LangChain链Chain的调用、对向量数据库的查询、以及对返回结果的格式化。不到两个月这个文件就膨胀到了近千行任何一点小的需求变更比如更换Embedding模型、调整提示词模板、或者增加一个结果缓存层都变得异常痛苦牵一发而动全身。这时我才意识到AI应用不仅仅是“调用一个API”它同样需要严谨的、可维护的软件架构。这正是我选择将NestJS与LangChain结合的原因。NestJS以其清晰的分层架构、强大的依赖注入DI和模块化设计闻名是构建企业级Node.js后端应用的绝佳选择。而LangChain则提供了构建AI应用所需的一系列标准化抽象如模型、提示词、链、代理、记忆等。将两者结合本质上是用NestJS的工程化能力去“封装”和“管理”LangChain提供的AI原子能力从而构建出既强大又易于维护的AI应用架构。这不仅仅是技术栈的叠加更是一种架构思想的实践将易变的AI逻辑与稳定的业务基础设施解耦。2. 为什么是NestJS LangChain架构选型深度剖析面对琳琅满目的技术选型为什么偏偏是这对组合这需要从它们各自解决的痛点以及结合后产生的化学反应来分析。2.1 LangChain的强项与短板LangChain的核心价值在于它提供了一套面向AI应用的高层抽象。它将与大模型交互的复杂过程拆解成Models、Prompts、Indexes、Chains、Agents、Memory等组件。开发者可以像搭积木一样组合这些组件快速构建出检索增强生成RAG、智能代理Agent等复杂流程。这极大地提升了开发效率降低了入门门槛。但是LangChain本身主要关注于“AI工作流”的编排它并不是一个完整的后端应用框架。它缺乏清晰的项目结构规范一个LangChain项目应该怎么组织chains、tools、prompts的目录没有官方约定容易导致混乱。依赖管理与生命周期控制如何优雅地初始化一个昂贵的Embedding模型或LLM并在多个地方复用如何管理数据库连接、缓存客户端等外部依赖可测试性如何对一段包含LLM调用的业务逻辑进行单元测试或集成测试直接mock OpenAI的API吗配置管理如何根据不同环境开发、测试、生产切换模型API密钥、温度参数等可观测性如何统一地记录每一次LLM调用的耗时、Token使用量、输入输出这些正是构建一个健壮、可维护的生产级应用所必需的“非功能性需求”。2.2 NestJS如何补足短板NestJS恰恰擅长解决上述问题。它深受Angular启发采用模块化、分层式的设计。模块化Modules我们可以创建独立的AiModule专门负责所有LangChain相关组件的初始化和导出。例如EmbeddingService、ChatModelService、VectorStoreService都可以在这里面提供。依赖注入Dependency Injection这是NestJS的灵魂。通过DI我们可以将初始化好的LangChain组件如一个特定的LLM链作为一个“服务”Service注入到任何需要它的控制器Controller或其他服务中。这解决了依赖管理和复用的问题。配置管理NestJS内置了强大的nestjs/config模块可以轻松地从环境变量、配置文件读取配置并注入到服务中。我们可以用其管理所有AI模型的API Key、Base URL、超时时间等。可测试性由于依赖是注入的我们可以非常方便地在测试中替换真实的AI服务为模拟对象Mock从而实现真正的单元测试而无需每次测试都调用真实的、缓慢且收费的API。拦截器与过滤器我们可以利用NestJS的拦截器Interceptor来统一处理AI调用的可观测性需求例如自动记录日志、收集性能指标。利用异常过滤器Exception Filter可以统一处理LangChain调用中可能抛出的各种错误如API限额不足、网络超时并将其转化为友好的HTTP错误响应。2.3 结合后的架构视图结合后的架构清晰地将应用分为三层接口层Interface Layer由NestJS的控制器和DTO构成负责接收HTTP请求、验证参数、返回响应。这一层完全不知道LangChain的存在。应用层/领域层Application/Domain Layer这是核心业务逻辑所在。由NestJS的各种服务Service构成。这些服务会注入并使用由AiModule提供的、封装好的AI能力服务如QuestionAnsweringChainService。在这里我们编排业务规则并调用AI能力。基础设施层Infrastructure Layer由AiModule及其内部服务构成。它负责与LangChain库交互初始化模型、链、向量数据库等并将它们包装成干净、易用的服务接口暴露给上层。同时这一层也包含数据库、缓存、外部API等其它基础设施的封装。这样的分层使得代码职责清晰任何一层的变化都不会轻易波及另一层。例如从OpenAI的GPT-4切换到Anthropic的Claude你只需要修改基础设施层的模型配置和服务从简单的链升级到带工具的智能代理Agent你也主要是在应用层和基础设施层进行改动接口层可以保持不变。3. 从零搭建一个可维护的AI应用骨架理论说再多不如动手实践。让我们从一个最简单的例子开始构建一个基于NestJS和LangChain的问答服务。我们将遵循“基础设施层 - 应用层 - 接口层”的自底向上顺序来搭建。3.1 项目初始化与核心依赖安装首先使用NestJS CLI创建一个新项目并安装核心依赖。# 创建NestJS项目 nest new my-ai-app # 进入项目目录 cd my-ai-app # 安装LangChain及相关依赖这里以OpenAI和内存向量库为例 npm install langchain/core langchain/openai langchain # 安装NestJS配置模块 npm install nestjs/config3.2 构建基础设施层AiModule这是我们的核心。在src目录下创建ai文件夹并在其中创建模块、服务等。src/ ├── ai/ │ ├── ai.module.ts │ ├── services/ │ │ ├── llm.service.ts │ │ ├── embedding.service.ts │ │ └── vector-store.service.ts │ └── chains/ │ └── qa.chain.ts3.2.1 配置管理首先在根目录创建.env文件存放敏感配置OPENAI_API_KEYyour_openai_api_key_here OPENAI_BASE_URL可选如果你使用代理或兼容API EMBEDDING_MODELtext-embedding-3-small CHAT_MODELgpt-3.5-turbo然后我们创建一个配置服务。在ai文件夹下创建configs/ai.config.ts// src/ai/configs/ai.config.ts import { registerAs } from nestjs/config; export default registerAs(ai, () ({ openAIApiKey: process.env.OPENAI_API_KEY, openAIBaseURL: process.env.OPENAI_BASE_URL, embeddingModel: process.env.EMBEDDING_MODEL || text-embedding-3-small, chatModel: process.env.CHAT_MODEL || gpt-3.5-turbo, }));3.2.2 核心服务封装接下来我们创建封装了LangChain核心功能的Service。// src/ai/services/llm.service.ts import { Injectable, Inject } from nestjs/common; import { ConfigType } from nestjs/config; import { ChatOpenAI } from langchain/openai; import aiConfig from ../configs/ai.config; Injectable() export class LlmService { public readonly chatModel: ChatOpenAI; constructor( Inject(aiConfig.KEY) private config: ConfigTypetypeof aiConfig, ) { // 集中初始化Chat模型便于统一配置和管理 this.chatModel new ChatOpenAI({ openAIApiKey: this.config.openAIApiKey, modelName: this.config.chatModel, temperature: 0.7, // 可配置化 maxTokens: 1000, ...(this.config.openAIBaseURL { configuration: { baseURL: this.config.openAIBaseURL } }), }); } }// src/ai/services/embedding.service.ts import { Injectable, Inject } from nestjs/common; import { ConfigType } from nestjs/config; import { OpenAIEmbeddings } from langchain/openai; import aiConfig from ../configs/ai.config; Injectable() export class EmbeddingService { public readonly embeddings: OpenAIEmbeddings; constructor( Inject(aiConfig.KEY) private config: ConfigTypetypeof aiConfig, ) { // 集中初始化Embedding模型 this.embeddings new OpenAIEmbeddings({ openAIApiKey: this.config.openAIApiKey, model: this.config.embeddingModel, ...(this.config.openAIBaseURL { configuration: { baseURL: this.config.openAIBaseURL } }), }); } }注意这里将模型实例作为服务的公共属性暴露。在更复杂的场景下你可能希望将其封装在方法内部或者使用工厂模式来按需创建不同配置的模型实例。这里为了清晰起见采用了简单直接的方式。3.2.3 构建可复用的链Chain链是LangChain的核心抽象。我们将其也封装为可注入的“服务”尽管它可能不涉及外部资源但通过DI管理能极大提升可测试性和可配置性。// src/ai/chains/qa.chain.ts import { Injectable } from nestjs/common; import { StringOutputParser } from langchain/core/output_parsers; import { PromptTemplate } from langchain/core/prompts; import { RunnableSequence } from langchain/core/runnables; import { LlmService } from ../services/llm.service; Injectable() export class QaChain { private chain: RunnableSequence; constructor(private llmService: LlmService) { this.initializeChain(); } private initializeChain() { // 定义提示词模板 const promptTemplate PromptTemplate.fromTemplate( 你是一个专业的问答助手。 请根据以下上下文来回答问题。如果你不知道答案就诚实地回答你不知道。 上下文{context} 问题{question} 请给出你的答案 ); // 构建链模板 - 语言模型 - 输出解析器 this.chain RunnableSequence.from([ promptTemplate, this.llmService.chatModel, new StringOutputParser(), ]); } async invoke(question: string, context: string): Promisestring { return this.chain.invoke({ question, context, }); } }3.2.4 整合成AiModule最后将所有这些服务整合到一个模块中。// src/ai/ai.module.ts import { Module } from nestjs/common; import { ConfigModule } from nestjs/config; import { LlmService } from ./services/llm.service; import { EmbeddingService } from ./services/embedding.service; import { QaChain } from ./chains/qa.chain; import aiConfig from ./configs/ai.config; Module({ imports: [ConfigModule.forFeature(aiConfig)], // 加载AI相关配置 providers: [LlmService, EmbeddingService, QaChain], // 注册服务 exports: [LlmService, EmbeddingService, QaChain], // 导出供其他模块使用 }) export class AiModule {}至此我们的基础设施层就搭建完毕了。AiModule是一个功能完备的单元任何其他模块只需要导入AiModule就可以在其服务中注入并使用LlmService、EmbeddingService或QaChain。4. 应用层实践业务服务与AI能力的融合有了稳固的基础设施我们现在可以在应用层构建具体的业务服务了。假设我们正在构建一个文档问答系统我们需要一个服务来处理用户的提问。4.1 创建领域服务在src目录下创建documents文件夹代表“文档”领域模块。// src/documents/services/document-qa.service.ts import { Injectable, Logger } from nestjs/common; import { QaChain } from ../../ai/chains/qa.chain; // 假设我们有一个服务能根据问题从向量库检索相关上下文 import { VectorStoreService } from ../../ai/services/vector-store.service; Injectable() export class DocumentQaService { private readonly logger new Logger(DocumentQaService.name); constructor( private readonly qaChain: QaChain, private readonly vectorStoreService: VectorStoreService, ) {} async answerQuestion(question: string): Promise{ answer: string; sources?: string[] } { this.logger.log(Processing question: ${question}); // 1. 检索相关上下文这里简化了实际可能涉及更复杂的检索逻辑 const relevantContexts await this.vectorStoreService.similaritySearch(question, 5); // 检索最相关的5个片段 const context relevantContexts.map(doc doc.pageContent).join(\n\n); if (!context) { this.logger.warn(No relevant context found for question: ${question}); return { answer: 抱歉在现有知识库中未找到相关信息。 }; } // 2. 调用AI链生成答案 try { const answer await this.qaChain.invoke(question, context); this.logger.log(Successfully generated answer for question: ${question}); // 3. 可以在这里添加后处理比如格式化、敏感词过滤等 const processedAnswer this.postProcessAnswer(answer); return { answer: processedAnswer, sources: relevantContexts.map(doc doc.metadata.source).filter(Boolean), // 返回来源信息 }; } catch (error) { this.logger.error(Failed to generate answer for question: ${question}, error.stack); // 这里可以抛出自定义的业务异常由全局过滤器处理 throw new Error(AI服务处理失败请稍后重试。); } } private postProcessAnswer(answer: string): string { // 示例简单的后处理移除可能的多余空白和特定标记 return answer.trim().replace(/\n{3,}/g, \n\n); } }4.2 创建模块// src/documents/documents.module.ts import { Module } from nestjs/common; import { AiModule } from ../ai/ai.module; import { DocumentQaService } from ./services/document-qa.service; // 假设有控制器 // import { DocumentsController } from ./controllers/documents.controller; Module({ imports: [AiModule], // 导入AiModule从而可以使用QaChain等 providers: [DocumentQaService], // controllers: [DocumentsController], exports: [DocumentQaService], // 如果其他模块也需要这个服务 }) export class DocumentsModule {}这个DocumentQaService就是一个典型的应用层服务。它不关心LLM具体是OpenAI还是Azure OpenAI也不关心链是如何构建的它只依赖抽象的QaChain和VectorStoreService。它的职责是协调检索和生成这两个步骤并处理业务逻辑如无结果的反馈、日志记录、答案后处理。这种设计使得对AI组件的替换或升级变得非常容易。5. 接口层与全局增强控制器、异常处理与可观测性现在我们需要为用户提供一个访问入口并完善应用的非功能性需求。5.1 创建RESTful API控制器// src/documents/controllers/documents.controller.ts import { Controller, Post, Body, HttpCode, HttpStatus } from nestjs/common; import { DocumentQaService } from ../services/document-qa.service; import { AskQuestionDto } from ../dto/ask-question.dto; Controller(documents/qa) export class DocumentsController { constructor(private readonly documentQaService: DocumentQaService) {} Post() HttpCode(HttpStatus.OK) async askQuestion(Body() askQuestionDto: AskQuestionDto) { const { question } askQuestionDto; const result await this.documentQaService.answerQuestion(question); return { success: true, data: result, }; } }对应的DTO用于验证输入// src/documents/dto/ask-question.dto.ts import { IsString, MinLength, MaxLength } from class-validator; export class AskQuestionDto { IsString() MinLength(5, { message: 问题不能少于5个字符 }) MaxLength(500, { message: 问题不能超过500个字符 }) question: string; }5.2 实现全局AI调用拦截器可观测性为了监控每一次AI调用的性能我们可以创建一个拦截器。// src/common/interceptors/ai-logging.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from nestjs/common; import { Observable } from rxjs; import { tap } from rxjs/operators; Injectable() export class AiLoggingInterceptor implements NestInterceptor { private readonly logger new Logger(AiLoggingInterceptor.name); intercept(context: ExecutionContext, next: CallHandler): Observableany { const request context.switchToHttp().getRequest(); const handlerName context.getHandler().name; const className context.getClass().name; const startTime Date.now(); return next.handle().pipe( tap(() { const duration Date.now() - startTime; // 在实际项目中这里可以将日志发送到监控系统如Prometheus, Datadog this.logger.log([${className}.${handlerName}] AI调用完成耗时 ${duration}ms); // 更精细的监控可以在这里记录Token使用量需要从LangChain的回调中获取 }), ); } }然后你可以在控制器、方法或全局范围内使用这个拦截器。例如在DocumentsController的askQuestion方法上使用UseInterceptors(AiLoggingInterceptor)。5.3 处理AI相关异常LangChain调用可能失败网络错误、API限额、模型错误等。我们需要一个统一的异常过滤器来优雅地处理这些错误。// src/common/filters/ai-exception.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus, Logger } from nestjs/common; import { Response } from express; import { OpenAI } from openai; // 假设错误来自OpenAI SDK Catch() // 可以更精确地捕获特定错误如Error, OpenAI.APIError export class AiExceptionFilter implements ExceptionFilter { private readonly logger new Logger(AiExceptionFilter.name); catch(exception: any, host: ArgumentsHost) { const ctx host.switchToHttp(); const response ctx.getResponseResponse(); let status HttpStatus.INTERNAL_SERVER_ERROR; let message 服务器内部错误; // 判断错误类型 if (exception instanceof OpenAI.APIError) { this.logger.error(OpenAI API Error: ${exception.status} - ${exception.message}); status HttpStatus.BAD_GATEWAY; // 502 Bad Gateway 或 429 Too Many Requests message AI服务暂时不可用: ${exception.message}; } else if (exception.message?.includes(timeout)) { this.logger.error(AI请求超时: ${exception.message}); status HttpStatus.GATEWAY_TIMEOUT; // 504 message AI服务响应超时请稍后重试; } else { // 其他未知错误 this.logger.error(Unhandled AI Exception: ${exception.message}, exception.stack); } response.status(status).json({ success: false, timestamp: new Date().toISOString(), path: ctx.getRequest().url, message, }); } }在main.ts中全局注册这个过滤器app.useGlobalFilters(new AiExceptionFilter());。6. 进阶架构应对复杂AI工作流与Agent当你的AI应用从简单的问答升级到需要多步骤推理、工具使用如搜索、计算、执行代码的智能代理Agent时架构的清晰性变得更加重要。LangChain的Agent和LangGraph用于构建有状态的、循环的、多参与者的工作流是处理这类复杂场景的利器。如何在NestJS中优雅地集成它们6.1 封装Agent作为服务与封装Chain类似我们可以将Agent的初始化逻辑封装在一个服务中。// src/ai/agents/calculator.agent.ts import { Injectable } from nestjs/common; import { ChatOpenAI } from langchain/openai; import { Calculator } from langchain/community/tools/calculator; import { AgentExecutor, createOpenAIFunctionsAgent } from langchain/agents; import { pull } from langchain/hub; import { PromptTemplate } from langchain/core/prompts; Injectable() export class CalculatorAgentService { private agentExecutor: AgentExecutor; constructor(private llmService: LlmService) { this.initializeAgent(); } private async initializeAgent() { // 1. 定义工具 const tools [new Calculator()]; // 2. 从Hub拉取预定义的提示词或本地定义 const prompt await pullPromptTemplate(hwchase17/openai-functions-agent); // 3. 创建Agent const agent await createOpenAIFunctionsAgent({ llm: this.llmService.chatModel, tools, prompt, }); // 4. 创建执行器 this.agentExecutor new AgentExecutor({ agent, tools, verbose: process.env.NODE_ENV development, // 开发环境输出详细步骤 }); } async run(input: string): Promisestring { const result await this.agentExecutor.invoke({ input }); return result.output; } }然后在AiModule中提供这个服务。应用层的服务如一个MathTutorService就可以注入并使用这个CalculatorAgentService而无需关心Agent内部复杂的工具绑定和提示词工程。6.2 使用LangGraph编排复杂工作流对于更复杂的、有状态的、可能涉及循环或分支的工作流例如一个需要反复与用户确认需求的订餐代理LangGraph是比简单Chain或Agent更合适的选择。在NestJS中集成LangGraph的关键在于将整个图Graph的构建和运行也封装成一个服务。// src/ai/graphs/customer-service.graph.ts import { Injectable } from nestjs/common; import { StateGraph, END } from langchain/langgraph; import { BaseMessage } from langchain/core/messages; import { LlmService } from ../services/llm.service; // 定义图的状态结构 interface CustomerServiceState { messages: BaseMessage[]; userIntent: string | null; confirmedDetails: Recordstring, any; } Injectable() export class CustomerServiceGraph { private graph: any; // 实际类型应为StateGraph constructor(private llmService: LlmService) { this.buildGraph(); } private buildGraph() { // 1. 定义节点函数 const identifyIntentNode async (state: CustomerServiceState) { // 使用LLM分析用户消息识别意图 // ... 实现逻辑 return { userIntent: book_restaurant }; }; const collectDetailsNode async (state: CustomerServiceState) { // 根据意图询问用户缺失的详细信息人数、时间、偏好 // ... 实现逻辑 return { messages: [...state.messages, newAIMessage] }; }; const confirmNode async (state: CustomerServiceState) { // 向用户确认所有信息 // ... 实现逻辑 const userConfirmed true; // 模拟用户确认 return { confirmedDetails: userConfirmed ? state.confirmedDetails : null }; }; // 2. 构建图 const workflow new StateGraphCustomerServiceState({ channels: { /* 状态通道定义 */ } }) .addNode(identify_intent, identifyIntentNode) .addNode(collect_details, collectDetailsNode) .addNode(confirm, confirmNode) .addEdge(identify_intent, collect_details) .addConditionalEdges(collect_details, (state) state.allDetailsCollected ? confirm : collect_details) // 循环收集 .addEdge(confirm, END); this.graph workflow.compile(); } async invoke(initialMessage: string): PromiseCustomerServiceState { const initialState: CustomerServiceState { messages: [new HumanMessage(initialMessage)], userIntent: null, confirmedDetails: {}, }; const finalState await this.graph.invoke(initialState); return finalState; } }这个CustomerServiceGraph服务对外提供了一个简单的invoke方法但内部封装了一个可能非常复杂的、带状态循环的工作流。应用层的CustomerService只需要调用graph.invoke()并处理最终结果即可。这种封装将复杂的AI工作流逻辑隔离在了基础设施层保持了应用层代码的简洁和可维护性。7. 测试策略如何为AI应用编写可靠的测试可维护架构的另一个重要支柱是可测试性。测试AI应用有其特殊性因为LLM的输出具有非确定性。我们的目标是测试我们的代码逻辑而不是测试OpenAI的API。7.1 单元测试Mock一切外部依赖对于应用层的服务如DocumentQaService我们应该mock掉所有AI基础设施层的依赖。// src/documents/services/document-qa.service.spec.ts import { Test, TestingModule } from nestjs/testing; import { DocumentQaService } from ./document-qa.service; import { QaChain } from ../../ai/chains/qa.chain; import { VectorStoreService } from ../../ai/services/vector-store.service; describe(DocumentQaService, () { let service: DocumentQaService; let mockQaChain: jest.MockedQaChain; let mockVectorStoreService: jest.MockedVectorStoreService; beforeEach(async () { // 创建模拟对象 mockQaChain { invoke: jest.fn(), } as any; mockVectorStoreService { similaritySearch: jest.fn(), } as any; const module: TestingModule await Test.createTestingModule({ providers: [ DocumentQaService, { provide: QaChain, useValue: mockQaChain }, { provide: VectorStoreService, useValue: mockVectorStoreService }, ], }).compile(); service module.getDocumentQaService(DocumentQaService); }); it(should return answer when context is found, async () { // 1. 准备模拟数据 const mockContext NestJS是一个用于构建高效、可扩展的Node.js服务器端应用的框架。; const mockAnswer NestJS是一个用于构建Node.js服务器端应用的框架。; mockVectorStoreService.similaritySearch.mockResolvedValue([ { pageContent: mockContext, metadata: {} }, ]); mockQaChain.invoke.mockResolvedValue(mockAnswer); // 2. 执行测试 const result await service.answerQuestion(什么是NestJS); // 3. 断言 expect(mockVectorStoreService.similaritySearch).toHaveBeenCalledWith(什么是NestJS, 5); expect(mockQaChain.invoke).toHaveBeenCalledWith(什么是NestJS, mockContext); expect(result.answer).toBe(mockAnswer); expect(result.success).toBeUndefined(); // 确保返回的是service的格式不是controller的 }); it(should handle no context found, async () { mockVectorStoreService.similaritySearch.mockResolvedValue([]); const result await service.answerQuestion(一个未知的问题); expect(mockQaChain.invoke).not.toHaveBeenCalled(); expect(result.answer).toContain(未找到相关信息); }); });7.2 集成测试测试模块间的协作对于基础设施层本身或者想测试整个AiModule的集成可以进行集成测试。// src/ai/ai.module.spec.ts (集成测试示例) import { Test, TestingModule } from nestjs/testing; import { AiModule } from ./ai.module; import { LlmService } from ./services/llm.service; import { ConfigModule } from nestjs/config; describe(AiModule Integration, () { let llmService: LlmService; beforeEach(async () { // 使用真实模块但通过ConfigModule覆盖配置为测试值 const module: TestingModule await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ envFilePath: .env.test, // 使用测试环境变量 isGlobal: true, }), AiModule, ], }).compile(); llmService module.getLlmService(LlmService); }); it(should successfully create LlmService with mocked config, () { expect(llmService).toBeDefined(); expect(llmService.chatModel).toBeDefined(); // 注意这里不会真正调用API只是检查实例是否创建成功 }); });7.3 E2E测试模拟API调用使用supertest等库可以测试从HTTP请求到响应的完整流程。这时你可能需要在一个测试数据库中准备一些文档并使用一个测试专用的、可预测的Mock LLM。LangChain社区有一些工具如langchain/core/testing中的FakeChatModel可以帮助你。// test/app.e2e-spec.ts (简化版) import * as request from supertest; import { Test } from nestjs/testing; import { AppModule } from ../src/app.module; import { INestApplication } from nestjs/common; import { AiModule } from ../src/ai/ai.module; // 假设我们有一个用于测试的Mock LLM服务 import { MockLlmService } from ./mocks/mock-llm.service; describe(DocumentsController (e2e), () { let app: INestApplication; beforeAll(async () { const moduleFixture await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(LlmService) // 覆盖真实的LlmService .useClass(MockLlmService) // 使用模拟的、返回固定结果的Service .compile(); app moduleFixture.createNestApplication(); await app.init(); }); it(/POST documents/qa (成功), () { return request(app.getHttpServer()) .post(/documents/qa) .send({ question: 测试问题 }) .expect(200) .expect((res) { expect(res.body.success).toBe(true); expect(res.body.data.answer).toBe(这是一个来自Mock LLM的固定答案); }); }); });通过分层测试策略我们能够确保代码的每一层都按预期工作并且组合起来也能正确运行从而为AI应用的持续迭代提供了坚实的质量保障。