从 NestJS 基础到 LangChain 实战:构建可维护的 AI 应用架构

从 NestJS 基础到 LangChain 实战:构建可维护的 AI 应用架构

用最优雅的 Node.js 框架,开启你的 AI 应用开发之旅

写在前面

如果你正在学习 NestJS + LangChain,恭喜你,你走上了一条既能写出高质量后端代码,又能拥抱 AI 浪潮的正确道路。NestJS 提供了一套严谨的架构规范,而 LangChain 则让你轻松集成大语言模型能力。

但在深入 AI 之前,很多同学会发现:NestJS 的基础概念有点模糊了 ------ Controller、Service、Module 什么关系?管道和拦截器到底有什么区别?这篇指南就是为你准备的。

我将带你把 NestJS 的核心骨架重新搭起来,然后平滑地衔接到 LangChain 的 AI 世界中。


一、为什么是 NestJS?它和 Express 有什么不同?

开始写代码之前,先搞清楚一个根本问题:为什么业界选择 NestJS,而不是直接用 Express?

特性 Express.js NestJS
架构风格 极简、无固定模式 模块化、分层架构
TypeScript 支持 需要手动配置 原生支持,开箱即用
依赖注入 (DI) 需要手动管理依赖 内置强大的 DI 容器
代码组织 由开发者自由决定 通过模块系统强制组织结构
可维护性 小型项目灵活,大型项目易混乱 天然适合大型企业级应用

简单说:Express 给你自由,NestJS 给你规范。当你构建的是一个需要长期维护、多人协作的 AI 应用时,NestJS 的架构优势就会充分体现出来。


二、快速启动:搭建你的第一个 NestJS 项目

bash 复制代码
# 安装 NestJS CLI
npm i -g @nestjs/cli

# 创建项目(启用 TypeScript 严格模式)
nest new my-ai-app --strict

# 进入项目目录
cd my-ai-app

# 启动开发服务器
npm run start:dev

访问 http://localhost:3000,你会看到经典的 "Hello World!"

创建后的项目结构如下:

bash 复制代码
my-ai-app/
├── src/
│   ├── main.ts                 # 应用入口,负责启动服务
│   ├── app.module.ts           # 根模块,应用的"总指挥"
│   ├── app.controller.ts       # 根控制器,处理 HTTP 请求
│   └── app.service.ts          # 根服务,承载业务逻辑
├── test/                       # 测试文件
├── nest-cli.json               # Nest CLI 配置
├── package.json
└── tsconfig.json               # TypeScript 配置

三、三大核心概念:模块、控制器、服务

这是 NestJS 的基石,理解这三样,你的 NestJS 知识骨架就搭起来了。

3.1 模块 (Modules) ------ 应用的"部门"

模块使用 @Module() 装饰器来组织代码。每个应用至少有一个根模块 AppModule

typescript 复制代码
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],       // 导入其他模块(如数据库模块、AI 模块)
  controllers: [AppController], // 注册该模块的控制器
  providers: [AppService],      // 注册该模块的服务/提供者
  exports: [],       // 导出服务供其他模块使用
})
export class AppModule {}

模块是 NestJS 组织代码的基本单元 ,你可以按功能领域划分模块,比如 UserModuleOrderModuleAiModule

3.2 控制器 (Controllers) ------ 处理 HTTP 请求的"前台"

控制器负责接收特定路由的请求,并返回响应。它只做"路由转发",不写业务逻辑。

typescript 复制代码
// src/app.controller.ts
import { 
  Controller, Get, Post, Put, Delete, 
  Body, Param, Query, HttpCode, HttpStatus 
} from '@nestjs/common';

@Controller('users') // 路由前缀为 /users
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get() // 处理 GET /users
  getUsers() {
    return this.appService.getUsers();
  }

  @Get(':id') // 处理 GET /users/123
  getUserById(@Param('id') id: string) {
    return this.appService.getUserById(id);
  }

  @Post() // 处理 POST /users
  @HttpCode(HttpStatus.CREATED) // 返回 201 状态码
  createUser(@Body() createUserDto: any) {
    return this.appService.createUser(createUserDto);
  }

  @Get('search') // 处理 GET /users/search?keyword=nest
  searchUsers(@Query('keyword') keyword: string) {
    return this.appService.searchUsers(keyword);
  }

  @Put(':id')
  updateUser(@Param('id') id: string, @Body() updateUserDto: any) {
    return this.appService.updateUser(id, updateUserDto);
  }

  @Delete(':id')
  deleteUser(@Param('id') id: string) {
    return this.appService.deleteUser(id);
  }
}
参数装饰器速查表
装饰器 提取的数据位置 示例 使用场景
@Param() URL 路径变量 /users/123 提取 123 获取特定资源 ID
@Query() URL ? 后面的参数 ?page=1&size=10 分页、过滤、搜索
@Body() 请求(JSON/表单) { name: '小明' } 新增/修改资源

3.3 服务 (Services) ------ 承载业务逻辑的"员工"

服务是提供者 的一种,使用 @Injectable() 装饰器。它包含了核心的业务逻辑,控制器通过依赖注入 (DI) 来使用服务。

typescript 复制代码
// src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable() // 标记为可被注入的提供者
export class AppService {
  private users = [{ id: '1', name: 'John' }];

  getUsers() {
    return this.users;
  }

  getUserById(id: string) {
    return this.users.find(user => user.id === id);
  }

  createUser(createUserDto: any) {
    const newUser = { id: String(this.users.length + 1), ...createUserDto };
    this.users.push(newUser);
    return newUser;
  }

  searchUsers(keyword: string) {
    return this.users.filter(user => user.name.includes(keyword));
  }

  updateUser(id: string, updateUserDto: any) {
    const user = this.getUserById(id);
    Object.assign(user, updateUserDto);
    return user;
  }

  deleteUser(id: string) {
    this.users = this.users.filter(user => user.id !== id);
    return { deleted: true };
  }
}

3.4 三者的关系:一张图说清楚

scss 复制代码
┌─────────────────────────────────────────────────────────┐
│                        模块 (Module)                    │
│  ┌─────────────────┐    ┌──────────────────────────┐  │
│  │   控制器 (Controller) │    │      服务 (Service)      │  │
│  │  - 接收 HTTP 请求    │───→│  - 核心业务逻辑          │  │
│  │  - 参数解析          │    │  - 数据校验              │  │
│  │  - 返回响应          │    │  - 数据库操作            │  │
│  └─────────────────┘    └──────────────────────────┘  │
│         │                           │                   │
│         │      依赖注入 (DI)        │                   │
│         └───────────────────────────┘                   │
└─────────────────────────────────────────────────────────┘

四、用 CLI 生成代码,提升开发效率

NestJS CLI 可以快速生成模块、控制器、服务等文件,极大地提升开发效率:

bash 复制代码
# 生成 users 模块(会创建 users.module.ts)
nest g module users

# 生成 users 控制器(会创建 users.controller.ts 和测试文件)
nest g controller users

# 生成 users 服务(会创建 users.service.ts 和测试文件)
nest g service users

# 一键生成完整的 CRUD 资源(模块+控制器+服务)
nest g resource products

💡 实战建议 :当你需要新增一个功能模块时,永远先用 nest g 命令生成骨架,而不是手动创建文件。这不仅能保证代码结构规范,还能自动更新模块的依赖注册。


五、管道 (Pipes):数据的"安检门"和"翻译官"

管道在控制器执行之前 工作,核心职能有两个:验证 (Validation)转换 (Transformation)

5.1 管道与 DTO 结合的标准三步走

第一步:安装依赖

bash 复制代码
npm install class-validator class-transformer

第二步:在 DTO 类里贴"规则标签"

typescript 复制代码
// src/users/dto/create-user.dto.ts
import { 
  IsString, IsEmail, IsInt, IsNotEmpty, 
  Min, MaxLength, IsOptional 
} from 'class-validator';
import { Transform } from 'class-transformer';

export class CreateUserDto {
  @IsString()
  @IsNotEmpty({ message: '姓名不能为空' })
  @MaxLength(50, { message: '姓名不能超过50个字符' })
  name: string;

  @IsEmail({}, { message: '请输入正确的邮箱格式' })
  @IsNotEmpty()
  email: string;

  @IsInt()
  @Min(18, { message: '年龄必须大于18岁' })
  @Transform(({ value }) => parseInt(value, 10)) // 自动将字符串 "18" 转为数字 18
  age: number;

  @IsOptional() // 可选字段
  @IsString()
  bio?: string;
}

第三步:在控制器中使用

typescript 复制代码
// src/users/users.controller.ts
import { Body, Post, UsePipes, ValidationPipe } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Post()
  // 方式一:局部启用(最常用,推荐)
  async createUser(@Body(ValidationPipe) createUserDto: CreateUserDto) {
    // 走到这里时,数据已经通过验证并且完成类型转换了
    return this.usersService.create(createUserDto);
  }
}

5.2 全局启用验证(推荐)

main.ts 中启用全局验证管道,所有接口自动生效:

typescript 复制代码
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 全局启用验证管道
  app.useGlobalPipes(new ValidationPipe({
    transform: true,           // 自动类型转换
    whitelist: true,           // 剔除 DTO 中未定义的字段
    forbidNonWhitelisted: true, // 存在未定义字段时抛出错误
    stopAtFirstError: true,    // 遇到第一个错误就停止校验
  }));
  
  await app.listen(3000);
}
bootstrap();

💡 对 LangChain 的实战意义:用户的提问一定不能为空,Token 数必须合法。用管道拦截掉非法请求,可以避免把你的 AI 额度浪费在无效调用上。


六、中间件、守卫、拦截器:请求链路上的三道关卡

6.1 执行顺序图(核心)

请求进来 → 中间件 (Middleware) → 守卫 (Guards) → 拦截器前置 (Interceptors pre) → 管道 (Pipes) → 控制器 (Controller) → 服务 (Service) → 拦截器后置 (Interceptors post) → 响应返回

6.2 中间件 (Middleware) ------ 最早的"门卫"

执行时机:在所有其他机制之前,最早接触到请求。

主要用途:记录日志、设置请求头、CORS 配置、解析原始 body。

typescript 复制代码
// src/common/middleware/logger.middleware.ts
export function loggerMiddleware(req, res, next) {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  console.log('请求头:', req.headers);
  next(); // 必须调用 next() 放行
}

// 在模块中注册
// src/app.module.ts
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(loggerMiddleware)
      .forRoutes('*'); // 对所有路由生效,也可以指定特定路径
  }
}

6.3 守卫 (Guards) ------ 权限"安检员"

执行时机:在中间件之后,拦截器/管道之前。

核心功能 :判断当前请求是否有权限 访问这个路由。返回 true 放行,返回 false 或抛出异常则拦截。

typescript 复制代码
// src/common/guards/api-key.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';

@Injectable()
export class ApiKeyGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const apiKey = request.headers['x-api-key'];
    
    if (apiKey && apiKey === process.env.API_SECRET_KEY) {
      return true;
    }
    throw new UnauthorizedException('无效的 API Key');
  }
}

// 在控制器中使用
@Post('ask')
@UseGuards(ApiKeyGuard) // 只有携带正确 Key 的请求才能进来
async askQuestion(@Body(ValidationPipe) dto: AskQuestionDto) {
  return this.aiService.ask(dto);
}

6.4 拦截器 (Interceptors) ------ 响应"包装大师"

执行时机 :在守卫之后执行前置逻辑 ;在控制器执行完后执行后置逻辑

核心功能

  • 前置:可以修改请求数据
  • 后置:统一格式化返回结果、记录耗时、缓存响应
typescript 复制代码
// src/common/interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // 前置逻辑(执行控制器之前)
    console.log('请求处理开始...');
    
    return next.handle().pipe(
      map(data => ({
        code: 200,
        success: true,
        timestamp: new Date().toISOString(),
        data: data, // 控制器的返回值被包装在这里
      }))
    );
  }
}

// 在 main.ts 全局注册
app.useGlobalInterceptors(new TransformInterceptor());

6.5 四者对比总结表

特性 中间件 守卫 管道 拦截器
执行顺序 第 1 名 第 2 名 第 4 名 第 3 名(包裹全过程)
核心职责 原始请求处理 权限/身份验证 数据验证 & 转换 响应映射 & 缓存
能否访问 DTO ❌ 只能拿到 raw body ❌ 通常只拿 headers/token 专管 DTO 对象 ⚠️ 能拿到最终结果
常用装饰器 configure() 注册 @UseGuards() @UsePipes() @UseInterceptors()

七、MVC 架构在 NestJS 中的落地

NestJS 虽然不完全遵循经典 MVC,但它的分层思想一脉相承。传统 MVC 中的 Model 在 NestJS 中被拆解为三个部分:

层级 文件位置 核心职责
Controller 层 *.controller.ts 接收请求、路由转发、返回响应
Service 层 *.service.ts 核心业务逻辑(计算、调用外部 API、编排流程)
DTO/Entity 层 *.dto.ts / *.entity.ts 数据结构定义 + 数据校验规则
Repository 层 由 ORM 提供 数据库 CRUD 操作
scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                       Controller 层                         │
│                    (负责路由和参数解析)                       │
└─────────────────────────┬───────────────────────────────────┘
                          │ 调用
                          ▼
┌─────────────────────────────────────────────────────────────┐
│                       Service 层                            │
│                   (负责核心业务逻辑)                          │
└─────────────────────────┬───────────────────────────────────┘
                          │ 调用
                          ▼
┌─────────────────────────────────────────────────────────────┐
│                    Repository / ORM 层                      │
│                  (负责数据库操作)                            │
└─────────────────────────────────────────────────────────────┘

八、模块之间的关系:不只是 1:1

初学者容易陷入"一个模块只能配一个控制器和一个服务"的误区。实际上:

typescript 复制代码
// ✅ 一个模块可以包含多个控制器
@Module({
  controllers: [UserController, OrderController, AdminController],
  providers: [UserService, OrderService, AdminService],
})
export class AppModule {}

// ✅ 一个控制器可以注入多个服务
@Controller('orders')
export class OrderController {
  constructor(
    private orderService: OrderService,
    private inventoryService: InventoryService,
    private pointsService: PointsService,
  ) {}
}

// ✅ 模块也可以没有控制器(纯工具模块)
@Module({
  providers: [DatabaseService, LoggerService],
  exports: [DatabaseService, LoggerService], // 导出供其他模块使用
})
export class CommonModule {}

💡 实战建议:按"功能领域(Domain)"划分模块,而不是死板地 1:1:

bash 复制代码
AiModule (AI 功能模块)
   ├── ChatController      → 处理 /ai/chat 对话
   ├── DocumentController  → 处理 /ai/document 文档解析
   ├── LangChainService    → 核心 LangChain 调用逻辑
   ├── VectorService       → 向量数据库操作
   └── PromptService       → Prompt 模板管理

九、与 LangChain 集成:让 AI 融入你的架构

现在,把前面的所有知识串联起来,构建一个真正的 AI 应用。

9.1 安装依赖

bash 复制代码
npm install nestjs-langchain langchain @langchain/openai

9.2 注册 LangChain 模块

typescript 复制代码
// src/app.module.ts
import { Module } from '@nestjs/common';
import { LangChainModule } from 'nestjs-langchain';
import { AiModule } from './ai/ai.module';

@Module({
  imports: [
    LangChainModule.register({
      model: {
        model: 'openai:gpt-3.5-turbo',
        apiKey: process.env.OPENAI_API_KEY,
      },
      systemPrompt: 'You are a helpful AI assistant built with NestJS.',
    }),
    AiModule,
  ],
})
export class AppModule {}

9.3 在服务中使用 LangChain

typescript 复制代码
// src/ai/ai.service.ts
import { Injectable } from '@nestjs/common';
import { LangChainService } from 'nestjs-langchain';

@Injectable()
export class AiService {
  constructor(private readonly langChainService: LangChainService) {}

  async askQuestion(question: string) {
    // 直接调用 AI 代理
    return await this.langChainService.run(question);
  }

  async askWithContext(question: string, context: string) {
    // 带上下文的问答(适用于 RAG 场景)
    const prompt = `基于以下上下文回答问题:\n\n上下文:${context}\n\n问题:${question}`;
    return await this.langChainService.run(prompt);
  }
}

9.4 将 Service 方法定义为 AI 工具 ------ 最强大的特性

这是 nestjs-langchain 最惊艳的功能:通过 @Tool() 装饰器,你可以将任何 NestJS 服务的方法变成一个 AI 可以调用的工具。

typescript 复制代码
// src/math/math.service.ts
import { Injectable } from '@nestjs/common';
import { Tool, ToolParam } from 'nestjs-langchain';

@Injectable()
export class MathService {
  @Tool({
    description: '将两个数字相加。当用户需要进行加法计算时使用此工具。'
  })
  add(
    @ToolParam({ name: 'a', description: '第一个加数', type: 'number' })
    a: number,
    @ToolParam({ name: 'b', description: '第二个加数', type: 'number' })
    b: number,
  ): number {
    return a + b;
  }

  @Tool({
    description: '获取当前天气信息,支持城市名称查询。'
  })
  async getWeather(
    @ToolParam({ name: 'city', description: '城市名称', type: 'string' })
    city: string,
  ): Promise<string> {
    // 这里可以调用真实的天气 API
    return `${city}的天气是晴天,25°C`;
  }
}

注册工具:

typescript 复制代码
// src/app.module.ts
@Module({
  imports: [
    LangChainModule.register({
      model: { model: 'openai:gpt-3.5-turbo', apiKey: process.env.OPENAI_API_KEY },
      systemPrompt: '你是一个智能助手,可以使用工具来帮助用户。',
      tools: [MathModule], // 注册 MathModule 中的所有 @Tool() 方法
    }),
    MathModule,
  ],
})
export class AppModule {}

现在,当用户问"帮我算一下 123 加 456 等于多少",AI 会自动识别并调用 MathService.add() 方法。


十、给 AI 应用的实战建议

基于以上所有知识,构建 AI 应用时,建议遵循以下最佳实践:

1. 用守卫保护你的 AI 接口

typescript 复制代码
@Post('ask')
@UseGuards(ApiKeyGuard, RateLimitGuard) // API Key + 限流双重保护
async ask(@Body(ValidationPipe) dto: AskDto) {
  return this.aiService.ask(dto);
}

2. 用管道验证用户输入

typescript 复制代码
export class AskDto {
  @IsString()
  @IsNotEmpty({ message: '问题不能为空' })
  @MaxLength(2000, { message: '问题不能超过2000字符' })
  question: string;

  @IsOptional()
  @IsInt()
  @Min(1)
  @Max(10)
  temperature?: number; // AI 的温度参数
}

3. 用拦截器统一响应格式

typescript 复制代码
// 所有 AI 接口返回统一格式
{
  code: 200,
  success: true,
  timestamp: '2026-08-10T10:00:00.000Z',
  data: { answer: '...', tokensUsed: 150 }
}

4. 分层清晰,各司其职

复制代码
Controller 层:只负责路由和参数校验
Service 层:负责 Prompt 构建、LangChain 调用、结果处理
Repository 层:负责对话历史、用户数据的持久化

十一、总结

通过这篇文章,我们从零开始,完整复习了 NestJS 的核心概念:

  1. 三大核心:模块(组织)、控制器(路由)、服务(业务逻辑)
  2. 参数装饰器@Param()@Query()@Body() 的区分与使用
  3. 管道与 DTO:数据验证和转换的标准实践
  4. 请求链路:中间件 → 守卫 → 拦截器 → 管道 → 控制器的完整执行顺序
  5. 模块关系:不局限于 1:1,按功能领域灵活组织
  6. LangChain 集成:将 NestJS 服务方法定义为 AI 工具

这趟旅程的终点,是你能用 NestJS 构建出结构清晰、易于维护、安全可靠的 AI 应用。

下一步学习方向

  • 深入 LangChain:理解 Chain、Agent、Retriever、Vector Store 等概念
  • RAG 实战:结合向量数据库(Chroma、Pinecone)实现知识库问答
  • 流式响应:使用 SSE 或 WebSocket 实现 AI 流式输出
  • 单元测试:用 NestJS 的测试工具测试你的 AI 服务

📌 核心代码示例已上传 ,欢迎在实际项目中参考使用。如果遇到问题,建议查阅 NestJS 官方文档LangChain JS 文档

Happy Coding! 🚀

相关推荐
circuitsosk3 小时前
Prompt Engineering进阶:面向复杂业务场景的模板化管理与动态注入策略
python·langchain·prompt·跨境电商·rag·上下文管理·动态注入
JaydenAI21 小时前
[基于OpenEvals的自动化评估-07]评估Agent输出文本的质量[下篇]
ai·langchain·agent·evaluation·openevals
淼澄研学1 天前
基于LangChain与AutoGen构建AI Agent的5个实操场景
人工智能·langchain
JaydenAI1 天前
[基于OpenEvals的自动化评估-08]对Agent的输出和输入进行安全性评估
ai·langchain·agent·evaluation·openevals
weixin_471383031 天前
07 LangGraph 集成 RAG
python·langchain·agent·langgraph
VipSoft1 天前
LangChain — RAG 知识库(实操)
langchain·rag
文艺理科生Owen1 天前
3 年,8 种方案,1 次重构:LangChain 记忆方案如何从混乱走向清晰
重构·langchain
浮生望1 天前
把《天龙八部》装进向量数据库:EPUB加载、文本分块与RAG问答全链路实战
langchain