尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

NestJS 架构下 LangChain 集成:模块化、依赖注入与生产级 AI 应用实践

NestJS 架构下 LangChain 集成:模块化、依赖注入与生产级 AI 应用实践 1. 从单体到模块化为什么选择 NestJS 作为 AI 应用的后端基石如果你正在用 Node.js 写后端并且项目规模稍微大一点你大概率已经受够了 Express 或 Koa 那种“自由散漫”的风格。一个app.js文件里塞满了路由、中间件、数据库连接和业务逻辑初期开发是快但三个月后当你想加个新功能或者让新同事接手时面对一团乱麻的代码那种感觉就像在考古。而当我们把 AI 能力特别是像 LangChain 这样复杂的框架集成进来时这种混乱会被指数级放大。LangChain 本身提供了强大的抽象但如果没有一个良好的应用架构来承载它你的 AI 功能很快就会变成一堆难以维护的“黑盒”脚本散落在项目的各个角落。这就是为什么在构建一个严肃的、需要长期维护的 AI 应用时我强烈建议从 NestJS 开始。NestJS 不是一个新轮子它是对 Node.js 后端开发范式的一次“工业化”升级。它默认采用了控制反转IoC和依赖注入DI的设计模式这听起来有点学术但用大白话讲就是它强制你把代码组织成一个个职责单一的“模块”Module每个模块里包含提供服务的“提供者”Provider、处理请求的“控制器”Controller等等。这些组件之间的依赖关系不是硬编码的而是由框架的“容器”在运行时帮你组装好。这么做的好处对于 AI 应用来说简直是雪中送炭。想象一下你的 LangChain 链Chain需要访问向量数据库Vector Store、大模型LLM和记忆Memory。在传统写法里你可能会在一个路由处理函数里手动初始化这一切导致这个函数又长又复杂而且很难做单元测试。在 NestJS 里你可以把VectorStoreService、LLMService、MemoryService都定义成独立的 Provider然后在你的LangChainService里通过构造函数注入它们。代码清晰依赖关系一目了然更重要的是你可以轻松地为LLMService写一个模拟Mock版本在测试时替换掉真实的 OpenAI 调用这能极大提升测试的稳定性和速度。另一个关键点是 NestJS 的模块化系统。你可以创建一个独立的AiModule专门用来封装所有与 LangChain 相关的逻辑——模型配置、链的构建、工具Tool的定义等等。这个模块对外只暴露几个干净的接口比如一个AiService。应用的其他部分比如处理用户管理的UserModule或处理支付业务的BillingModule完全不需要知道 LangChain 的存在它们只需要依赖AiModule提供的服务即可。这种关注点分离Separation of Concerns是构建可维护系统的基石它能有效防止 AI 相关的代码“污染”整个代码库。注意很多从 Express 转过来的开发者最初会觉得 NestJS 的“规矩”太多有点束手束脚。但请相信我在涉及像 AI 这样复杂且变化快的领域时这些“规矩”所提供的结构和一致性其长期收益远远超过初期的那点学习成本。它迫使你思考架构而这恰恰是项目能否活过第一年的关键。2. LangChain 的核心抽象在 NestJS 中如何优雅地组织链、代理与工具LangChain 的设计哲学是通过一系列“抽象”来简化与大模型交互的复杂性。理解这些抽象并知道如何在 NestJS 的架构下安置它们是构建健壮 AI 应用的核心。我们主要关注三个核心概念链Chain、代理Agent和工具Tool。链Chain可以理解为一系列预定义好的调用序列。比如一个典型的“检索增强生成RAG”链可能包含将用户问题转换为向量 - 从向量库检索相关文档 - 将文档和问题组合成提示词 - 调用大模型生成答案。在 NestJS 中链不应该被写成一次性脚本。我的做法是为每一种类型的链创建一个独立的 Provider。例如创建一个RagChainService// rag-chain.service.ts import { Injectable } from nestjs/common; import { LLMChain } from langchain/chains; import { PromptTemplate } from langchain/prompts; import { ChatOpenAI } from langchain/chat_models/openai; import { VectorStoreService } from ../vector-store/vector-store.service; Injectable() export class RagChainService { private chain: LLMChain; constructor(private vectorStoreService: VectorStoreService) { const llm new ChatOpenAI({ temperature: 0 }); const prompt PromptTemplate.fromTemplate( 基于以下上下文回答问题 {context} 问题{question} 答案 ); this.chain new LLMChain({ llm, prompt }); } async ask(question: string): Promisestring { // 1. 从向量库检索上下文 const context await this.vectorStoreService.similaritySearch(question, 4); const contextText context.map(doc doc.pageContent).join(\n); // 2. 调用链 const result await this.chain.call({ context: contextText, question, }); return result.text; } }这样链的构建逻辑被封装在服务内部外部只需调用ask方法。你可以在AiModule中提供这个服务并在需要的地方注入使用。代理Agent比链更高级它让大模型自己决定调用哪个工具以及调用的顺序适合处理开放性的复杂任务。LangChain 提供了多种代理类型如 ReAct、OpenAI Functions。在 NestJS 中集成代理关键在于管理好“工具”Tool。工具是代理可以调用的函数比如搜索网络、查询数据库、执行计算等。我的建议是为每一个工具创建一个独立的 Provider。这听起来有点繁琐但好处巨大。例如一个计算器工具// tools/calculator.service.ts import { Injectable } from nestjs/common; import { Tool } from langchain/tools; import { z } from zod; Injectable() export class CalculatorService extends Tool { name calculator; description 用于执行数学计算。输入应该是一个数学表达式例如 2 2 或 sqrt(16)。; schema z.object({ expression: z.string().describe(一个数学表达式) }); constructor() { super(); } async _call(input: { expression: string }): Promisestring { try { // 警告在生产环境中直接使用eval是极其危险的这里仅为示例。 // 实际应使用安全的数学表达式解析库如 math.js const result eval(input.expression); return 计算结果为${result}; } catch (error) { return 计算失败${error.message}; } } }然后在一个AgentService中注入所有需要的工具并初始化代理// agent.service.ts import { Injectable } from nestjs/common; import { initializeAgentExecutorWithOptions } from langchain/agents; import { ChatOpenAI } from langchain/chat_models/openai; import { CalculatorService } from ./tools/calculator.service; import { SearchService } from ./tools/search.service; // 假设另一个工具 Injectable() export class AgentService { private executor: any; // 实际类型为 AgentExecutor constructor( private calculatorTool: CalculatorService, private searchTool: SearchService, ) {} async onModuleInit() { // 在模块初始化时构建代理避免每次请求都重建 const model new ChatOpenAI({ temperature: 0 }); const tools [this.calculatorTool, this.searchTool]; this.executor await initializeAgentExecutorWithOptions( tools, model, { agentType: openai-functions, verbose: true, // 开发时可开启查看代理的思考过程 } ); } async runAgent(input: string): Promisestring { const result await this.executor.invoke({ input }); return result.output; } }这种组织方式的好处是高内聚每个工具的代码都在自己的文件里修改和测试都很方便。低耦合AgentService只依赖于抽象的 Tool 接口具体用什么工具可以灵活配置。易于测试你可以单独测试每个工具也可以给AgentService注入模拟工具进行集成测试。可维护性当需要增加或删除工具时只需在AiModule的providers数组和AgentService的构造函数中调整即可不会影响其他代码。提示代理的verbose模式在开发调试时非常有用它能将大模型的思考过程Reasoning和工具调用记录到控制台。但在生产环境务必关闭以避免日志泄露敏感信息或产生过多噪音。3. 状态、记忆与持久化让 AI 应用拥有“上下文”能力一个只会回答单次问题的 AI 应用是单薄的。真正的对话式体验需要应用能够记住之前的交互内容这就是“记忆”Memory。LangChain 提供了多种记忆机制如ConversationBufferMemory简单缓存、ConversationSummaryMemory总结式记忆等。在 NestJS 应用中集成记忆我们需要解决两个核心问题记忆的生命周期和记忆的持久化。记忆的生命周期指的是这段记忆属于谁、存在多久。是绑定到当前 HTTP 请求还是绑定到某个登录用户的长时会话在 NestJS 中这通常由你选择的 Provider 作用域Scope来决定。请求作用域REQUEST如果记忆只存在于一次 API 调用内例如处理一个包含多轮对话历史的请求体你可以创建一个请求作用域的 Memory Service。但这种情况较少。默认作用域DEFAULT更常见的场景是记忆需要跨越多次请求关联到一个特定的会话Session或用户。这时记忆服务本身可以是单例的但它内部需要根据一个会话 ID 来获取和存储不同的记忆数据。记忆的持久化是指将会话记忆保存到数据库如 Redis、PostgreSQL中以便服务器重启后不丢失。LangChain 的 Memory 类通常提供了保存和加载状态的方法。下面是一个结合了 NestJS 会话管理和 Redis 进行持久化的记忆服务示例// memory.service.ts import { Injectable, Scope, Inject } from nestjs/common; import { REQUEST } from nestjs/core; import { ConversationSummaryMemory } from langchain/memory; import { ChatOpenAI } from langchain/chat_models/openai; import { RedisService } from ../redis/redis.service; // 假设你有一个封装好的 Redis 服务 Injectable({ scope: Scope.REQUEST }) // 每个请求一个实例 export class MemoryService { private memory: ConversationSummaryMemory; private sessionId: string; constructor( Inject(REQUEST) private request: Request, // 注入请求对象以获取会话ID private redisService: RedisService, ) { // 从请求的 Cookie 或 Header 中提取会话ID this.sessionId this.extractSessionId(request); this.initializeMemory(); } private extractSessionId(req: Request): string { // 简化示例实际应从认证信息或会话中间件中获取 return req.headers[x-session-id] as string || default-session; } private async initializeMemory() { const llm new ChatOpenAI({ temperature: 0 }); this.memory new ConversationSummaryMemory({ llm, memoryKey: chat_history, inputKey: input, }); // 尝试从 Redis 加载历史记忆 const savedMemory await this.redisService.get(memory:${this.sessionId}); if (savedMemory) { await this.memory.loadMemoryVariables(JSON.parse(savedMemory)); } } async getMemory() { return this.memory; } async saveContext(input: string, output: string) { await this.memory.saveContext({ input }, { output }); // 将当前记忆状态持久化到 Redis const memoryState await this.memory.loadMemoryVariables({}); await this.redisService.set( memory:${this.sessionId}, JSON.stringify(memoryState), 60 * 60 * 24, // 设置 TTL例如 24 小时 ); } async clearMemory() { await this.memory.clear(); await this.redisService.del(memory:${this.sessionId}); } }然后在你的链或代理服务中注入这个MemoryService在调用前获取记忆在调用后保存上下文// 在某个服务中 constructor(private memoryService: MemoryService) {} async handleChat(userInput: string) { const memory await this.memoryService.getMemory(); // 将记忆变量加入到链或代理的输入中 const result await this.chain.call({ input: userInput, chat_history: memory.chat_history, // 假设记忆中有这个键 }); // 保存本次交互到记忆 await this.memoryService.saveContext(userInput, result.text); return result.text; }这种设计实现了记忆的会话隔离和跨请求持久化。使用 Redis 是因为它读写速度快且支持设置过期时间TTL非常适合存储临时会话数据。对于更复杂的记忆结构如知识图谱你可能需要用到专门的图数据库。4. 配置管理与环境隔离安全地处理 API 密钥与模型参数AI 应用严重依赖外部服务OpenAI、 Anthropic、向量数据库等每个服务都需要 API 密钥和配置参数。把这些敏感信息硬编码在代码里或者散落在各个服务中是安全和运维的噩梦。NestJS 提供了一个优雅的解决方案配置模块nestjs/config它基于流行的dotenv库。首先安装依赖npm i nestjs/config。然后在根模块AppModule中导入ConfigModule// app.module.ts import { Module } from nestjs/common; import { ConfigModule } from nestjs/config; Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // 使配置全局可用无需在每个模块中重复导入 envFilePath: .env.${process.env.NODE_ENV || development}, // 根据环境加载不同的 .env 文件 }), // ... 其他模块 ], }) export class AppModule {}接下来创建不同环境的.env文件.env.development: 用于本地开发.env.production: 用于生产环境.env.test: 用于测试在这些文件中定义你的配置# .env.development OPENAI_API_KEYsk-your-dev-key-here OPENAI_BASE_URLhttps://api.openai.com/v1 MODEL_NAMEgpt-4o-mini EMBEDDING_MODELtext-embedding-3-small REDIS_URLredis://localhost:6379现在你可以在任何服务中通过ConfigService注入来获取配置// llm.service.ts import { Injectable } from nestjs/common; import { ConfigService } from nestjs/config; import { ChatOpenAI } from langchain/chat_models/openai; Injectable() export class LlmService { private chatModel: ChatOpenAI; constructor(private configService: ConfigService) { const apiKey this.configService.getstring(OPENAI_API_KEY); const baseURL this.configService.getstring(OPENAI_BASE_URL); const modelName this.configService.getstring(MODEL_NAME); // 安全地创建模型实例 this.chatModel new ChatOpenAI({ openAIApiKey: apiKey, configuration: { baseURL }, modelName, temperature: 0.7, // ... 其他参数也可以从配置中读取 }, { // 可选传递自定义的 fetch 实现或设置超时 baseURL, }); } async generate(prompt: string): Promisestring { const response await this.chatModel.invoke(prompt); return response.content as string; } }进阶技巧配置验证与结构化。直接使用configService.get容易因拼写错误导致运行时错误。更好的做法是使用 NestJS 的nestjs/config配合class-validator进行验证和类型化。首先定义一个配置类// config/ai.config.ts import { IsString, IsOptional, IsNumber } from class-validator; export class AiConfig { IsString() OPENAI_API_KEY: string; IsOptional() IsString() OPENAI_BASE_URL?: string https://api.openai.com/v1; IsString() MODEL_NAME: string gpt-4o-mini; IsNumber() IsOptional() MAX_TOKENS?: number 2000; }然后在模块中注册并验证// ai.module.ts import { Module } from nestjs/common; import { ConfigModule, ConfigService } from nestjs/config; import { validate } from ./config/validation; import aiConfig from ./config/ai.config; Module({ imports: [ ConfigModule.forFeature(aiConfig), // 加载特定命名空间的配置 ], providers: [LlmService], exports: [LlmService], }) export class AiModule {} // config/validation.ts import { plainToInstance } from class-transformer; import { validateSync } from class-validator; import { AiConfig } from ./ai.config; export function validate(config: Recordstring, unknown) { const validatedConfig plainToInstance(AiConfig, config, { enableImplicitConversion: true, }); const errors validateSync(validatedConfig, { skipMissingProperties: false, }); if (errors.length 0) { throw new Error(配置验证失败: ${errors.toString()}); } return validatedConfig; }这样应用启动时就会验证环境变量如果OPENAI_API_KEY缺失或MAX_TOKENS不是数字就会立刻报错而不是在运行时才崩溃。这为你的 AI 应用提供了第一道安全防线。5. 错误处理、日志与监控构建生产级 AI 应用的韧性AI 应用在生产环境中会面临独特的挑战第三方 API 不稳定、模型响应不可预测、用户输入千奇百怪。一个健壮的应用必须能优雅地处理这些情况并留下清晰的线索供排查。NestJS 的异常过滤器Exception Filter、拦截器Interceptor和日志Logger系统是构建这种韧性的利器。全局异常处理LangChain 调用可能抛出各种错误网络超时、API 限额、模型错误等。我们不应该让这些原生错误直接暴露给客户端。可以创建一个全局过滤器来捕获并转换它们// filters/ai-exception.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus, Logger } from nestjs/common; import { Request, Response } from express; import { OpenAIError } from openai; // 假设使用 OpenAI SDK Catch() // 捕获所有异常 export class AllExceptionsFilter implements ExceptionFilter { private readonly logger new Logger(AllExceptionsFilter.name); catch(exception: unknown, host: ArgumentsHost) { const ctx host.switchToHttp(); const response ctx.getResponseResponse(); const request ctx.getRequestRequest(); let status HttpStatus.INTERNAL_SERVER_ERROR; let message Internal server error; let details: any null; // 处理特定的 AI 相关错误 if (exception instanceof OpenAIError) { status HttpStatus.BAD_GATEWAY; // 502 Bad Gateway 表示上游服务问题 message AI service temporarily unavailable; details { type: exception.constructor.name, message: exception.message, // 注意不要暴露 stack trace 给客户端 }; this.logger.error(OpenAI Error on ${request.url}: ${exception.message}, exception.stack); } else if (exception instanceof Error) { // 处理其他通用错误 message exception.message; this.logger.error(Unhandled Error on ${request.url}: ${exception.message}, exception.stack); } // 生产环境返回简化的错误信息开发环境可以包含更多细节 const isProduction process.env.NODE_ENV production; const responseBody: any { statusCode: status, timestamp: new Date().toISOString(), path: request.url, message, }; if (!isProduction details) { responseBody.details details; } response.status(status).json(responseBody); } }在main.ts中全局应用这个过滤器app.useGlobalFilters(new AllExceptionsFilter());结构化日志与链路追踪当用户报告“AI 回答不对”时你需要能快速定位是哪个环节出了问题。简单的console.log远远不够。你需要结构化日志并包含请求 ID 来实现链路追踪。NestJS 内置了基于winston或pino的日志集成。这里以winston为例// utils/logger.ts import { createLogger, format, transports } from winston; import * as winston from winston; export const logger createLogger({ level: process.env.LOG_LEVEL || info, format: format.combine( format.timestamp(), format.errors({ stack: true }), format.json(), // 输出 JSON 格式便于日志收集系统如 ELK处理 ), defaultMeta: { service: ai-backend }, transports: [ new transports.Console(), new transports.File({ filename: logs/error.log, level: error }), new transports.File({ filename: logs/combined.log }), ], }); // 在你的服务中使用 import { Injectable, LoggerService } from nestjs/common; import { logger } from ../utils/logger; Injectable() export class LangChainService { private readonly logger logger; async invokeChain(input: string, requestId: string) { // 将请求ID加入日志上下文 const childLogger this.logger.child({ requestId }); childLogger.info(开始调用 LangChain 链, { input }); try { // ... 链调用逻辑 const result await this.chain.call({ input }); childLogger.info(链调用成功, { outputLength: result.text.length }); return result; } catch (error) { childLogger.error(链调用失败, { error: error.message, stack: error.stack }); throw error; } } }关键指标监控除了日志你还需要监控关键业务和技术指标。例如延迟每个 LangChain 调用、每个工具调用的耗时。成功率API 调用的成功/失败率。Token 消耗每次请求消耗的 Prompt Token 和 Completion Token 数量这直接关联成本。你可以使用拦截器Interceptor来统一收集这些指标// interceptors/metrics.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from nestjs/common; import { Observable } from rxjs; import { tap } from rxjs/operators; import { logger } from ../utils/logger; import * as PromClient from prom-client; // 假设使用 Prometheus const llmCallDuration new PromClient.Histogram({ name: llm_call_duration_seconds, help: Duration of LLM calls in seconds, labelNames: [chain_name, status], buckets: [0.1, 0.5, 1, 2, 5, 10], }); Injectable() export class MetricsInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observableany { const request context.switchToHttp().getRequest(); const startTime Date.now(); const chainName request.path; // 或从请求中提取更具体的标识 return next.handle().pipe( tap({ next: () { const duration (Date.now() - startTime) / 1000; llmCallDuration.labels(chainName, success).observe(duration); logger.debug([Metrics] ${chainName} 成功耗时 ${duration.toFixed(2)}s); }, error: (err) { const duration (Date.now() - startTime) / 1000; llmCallDuration.labels(chainName, error).observe(duration); logger.error([Metrics] ${chainName} 失败耗时 ${duration.toFixed(2)}s, { error: err.message }); }, }), ); } }将这些监控数据导出到 Prometheus 或发送到 Datadog、New Relic 等 APM 工具你就能清晰地看到应用的性能瓶颈和错误模式为容量规划和故障排查提供数据支持。6. 测试策略如何为复杂的 LangChain 逻辑编写可靠测试测试 AI 应用颇具挑战性因为它的核心——大模型——是一个非确定性的“黑盒”。我们无法像测试一个加法函数那样断言其固定输出。因此AI 应用的测试重点应该放在流程控制、工具调用和错误处理上并对模型输出进行模糊断言。NestJS 的依赖注入体系为测试提供了极大的便利。我们可以轻松地用模拟对象Mock替换掉外部依赖如 OpenAI API、向量数据库。单元测试Unit Test测试单个服务或工具的逻辑。使用jest或其它测试框架。// tools/calculator.service.spec.ts import { Test, TestingModule } from nestjs/testing; import { CalculatorService } from ./calculator.service; describe(CalculatorService, () { let service: CalculatorService; beforeEach(async () { const module: TestingModule await Test.createTestingModule({ providers: [CalculatorService], }).compile(); service module.getCalculatorService(CalculatorService); }); it(should be defined, () { expect(service).toBeDefined(); }); describe(_call, () { it(should correctly add two numbers, async () { // 注意实际生产代码中应使用安全的数学库而非 eval const result await service._call({ expression: 2 3 }); expect(result).toBe(计算结果为5); }); it(should return error message for invalid expression, async () { const result await service._call({ expression: 2 }); expect(result).toContain(计算失败); }); }); });集成测试Integration Test测试多个服务协同工作特别是 LangChain 链的组装和流程。这里的关键是模拟Mock所有外部 API 调用。假设我们要测试一个RagChainService它依赖VectorStoreService和LLMService。// rag-chain.service.integration-spec.ts import { Test, TestingModule } from nestjs/testing; import { RagChainService } from ./rag-chain.service; import { VectorStoreService } from ../vector-store/vector-store.service; import { LLMService } from ../llm/llm.service; // 创建模拟对象 const mockVectorStoreService { similaritySearch: jest.fn(), }; const mockLlmService { generate: jest.fn(), }; describe(RagChainService (Integration), () { let service: RagChainService; beforeEach(async () { const module: TestingModule await Test.createTestingModule({ providers: [ RagChainService, { provide: VectorStoreService, useValue: mockVectorStoreService }, { provide: LLMService, useValue: mockLlmService }, ], }).compile(); service module.getRagChainService(RagChainService); // 重置模拟函数的调用记录 jest.clearAllMocks(); }); it(should retrieve context and generate answer, async () { // 1. 准备模拟数据 const mockContext [ { pageContent: 文档A内容NestJS 是一个框架。 }, { pageContent: 文档B内容LangChain 用于构建AI应用。 }, ]; const mockAnswer 根据文档NestJS 是框架LangChain 用于构建AI应用。; // 2. 设置模拟行为 mockVectorStoreService.similaritySearch.mockResolvedValue(mockContext); mockLlmService.generate.mockResolvedValue(mockAnswer); // 3. 执行测试 const question NestJS 和 LangChain 是什么; const result await service.ask(question); // 4. 验证交互 expect(mockVectorStoreService.similaritySearch).toHaveBeenCalledWith(question, 4); expect(mockLlmService.generate).toHaveBeenCalledWith( expect.stringContaining(文档A内容) expect.stringContaining(文档B内容) expect.stringContaining(question) ); // 5. 验证结果 expect(result).toBe(mockAnswer); }); it(should handle empty context gracefully, async () { mockVectorStoreService.similaritySearch.mockResolvedValue([]); mockLlmService.generate.mockResolvedValue(我找不到相关信息。); const result await service.ask(一个冷门问题); expect(mockLlmService.generate).toHaveBeenCalledWith( expect.stringContaining(基于以下上下文回答问题) // 验证提示词中上下文为空 ); expect(result).toBe(我找不到相关信息。); }); });端到端测试E2E Test测试完整的 API 接口。NestJS 提供了supertest来模拟 HTTP 请求。对于 AI 应用E2E 测试的目标不是验证模型输出是否“正确”而是验证整个请求-响应流程是否通畅以及错误处理是否得当。// app.e2e-spec.ts import { Test, TestingModule } from nestjs/testing; import { INestApplication } from nestjs/common; import * as request from supertest; import { AppModule } from ./../src/app.module; import { LLMService } from ../src/llm/llm.service; describe(AppController (e2e), () { let app: INestApplication; const mockLlmService { generate: jest.fn() }; beforeAll(async () { const moduleFixture: TestingModule await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(LLMService) // 覆盖真实的 LLM 服务 .useValue(mockLlmService) .compile(); app moduleFixture.createNestApplication(); await app.init(); }); it(/chat (POST) - success, () { mockLlmService.generate.mockResolvedValue(这是一个模拟回答。); return request(app.getHttpServer()) .post(/chat) .send({ message: 你好 }) .expect(200) .expect((res) { expect(res.body.reply).toBe(这是一个模拟回答。); }); }); it(/chat (POST) - service error, () { mockLlmService.generate.mockRejectedValue(new Error(API 调用失败)); return request(app.getHttpServer()) .post(/chat) .send({ message: 你好 }) .expect(502) // 根据我们的全局过滤器应返回 502 .expect((res) { expect(res.body.message).toContain(AI service temporarily unavailable); }); }); afterAll(async () { await app.close(); }); });模糊断言与评估Fuzzy Assertion Evaluation对于某些场景你可能想测试模型输出是否“大致符合预期”。这可以通过评估器Evaluator来实现例如检查输出是否包含某个关键词或者使用另一个轻量级模型如gpt-3.5-turbo来评估输出与期望的相关性。但这通常更适用于 CI/CD 中的回归测试或效果评估而非日常的单元/集成测试。核心心得测试 AI 应用时要把大模型当作一个不确定的外部服务来对待。你的测试应该验证的是给定确定的输入和模拟的外部服务响应你的业务逻辑链的组装、工具的选择、错误的处理是否按预期执行。不要试图去断言大模型生成的每一个字。
返回列表