从 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 组织代码的基本单元 ,你可以按功能领域划分模块,比如 UserModule、OrderModule、AiModule。
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 的核心概念:
- 三大核心:模块(组织)、控制器(路由)、服务(业务逻辑)
- 参数装饰器 :
@Param()、@Query()、@Body()的区分与使用 - 管道与 DTO:数据验证和转换的标准实践
- 请求链路:中间件 → 守卫 → 拦截器 → 管道 → 控制器的完整执行顺序
- 模块关系:不局限于 1:1,按功能领域灵活组织
- LangChain 集成:将 NestJS 服务方法定义为 AI 工具
这趟旅程的终点,是你能用 NestJS 构建出结构清晰、易于维护、安全可靠的 AI 应用。
下一步学习方向
- 深入 LangChain:理解 Chain、Agent、Retriever、Vector Store 等概念
- RAG 实战:结合向量数据库(Chroma、Pinecone)实现知识库问答
- 流式响应:使用 SSE 或 WebSocket 实现 AI 流式输出
- 单元测试:用 NestJS 的测试工具测试你的 AI 服务
📌 核心代码示例已上传 ,欢迎在实际项目中参考使用。如果遇到问题,建议查阅 NestJS 官方文档 和 LangChain JS 文档。
Happy Coding! 🚀