从零构建企业级 AI 应用引擎:NestJS + Next.js 全栈架构设计与实践

摘要:本文详细介绍了一个企业级 AI 应用引擎平台从 0 到 1 的完整构建过程。涵盖 Monorepo 架构设计、工作流 DAG 调度引擎、变量管理系统、测试策略等核心技术实践。项目最终完成 21,285 行代码,444 个测试用例 100% 通过,为类似项目提供可参考的架构方案。


一、项目背景与目标

1.1 业务需求

随着大语言模型(LLM)的普及,企业需要一个平台来:

  • 快速创建 AI 对话应用
  • 可视化编排工作流(类似 Coze/Dify)
  • 统一管理多个 LLM 提供商(Ollama、阿里云等)
  • 提供可扩展的工具系统

1.2 技术挑战

挑战 说明
架构复杂度 需要支持工作流 DAG 调度、多租户、API 认证
响应性能 流式 SSE 响应,P95 < 500ms
测试覆盖 核心模块需要高覆盖率保障质量
可维护性 Monorepo 架构,多包依赖管理

1.3 功能清单(MVP)

  • 应用管理:创建应用、API Key 认证
  • 对话系统:支持流式/非流式响应
  • 工作流编排:6 种节点类型(start/llm/http/condition/tool/end)
  • 工具系统:HTTP、代码执行、时间工具
  • 模型管理:Ollama 本地模型、阿里云百炼

二、技术选型与架构设计

2.1 完整技术栈

vbscript 复制代码
后端:NestJS 10 + TypeScript + Prisma ORM
前端:Next.js 14 + React + shadcn/ui + TailwindCSS
数据库:PostgreSQL 16 + pgvector
缓存:Redis 7
AI 服务:Ollama 本地模型 + 阿里云百炼(可选)
包管理:pnpm workspace (Monorepo)
测试:Vitest + Supertest

2.2 Monorepo 架构设计

bash 复制代码
ai-engine/
├── apps/
│   ├── server/              # NestJS 后端 (端口 3000)
│   │   ├── prisma/          # Prisma ORM
│   │   └── src/
│   │       ├── modules/     # 业务模块
│   │       ├── auth/        # 认证模块
│   │       └── main.ts
│   └── web/                 # Next.js 前端 (端口 3001)
│       └── src/
│           ├── app/         # 页面路由
│           ├── components/  # UI 组件
│           └── hooks/       # React Hooks
├── packages/
│   ├── core/                # 核心引擎(工作流执行器)
│   ├── providers/           # LLM 提供商抽象
│   └── shared/              # 共享类型和工具
└── docs/                    # 项目文档

包依赖关系

bash 复制代码
apps/server → packages/core → packages/providers
           → packages/shared
apps/web    → packages/shared

为什么选择 pnpm?

  • 磁盘空间优化(硬链接机制)
  • workspace 支持优秀
  • 依赖提升策略避免幽灵依赖

2.3 模块划分

模块 职责 核心文件
App Module 应用 CRUD、API Key 管理 app.service.ts
Chat Module 对话管理、流式响应 chat.service.ts
Workflow Module 工作流 CRUD、执行 workflow.service.ts
Tool Module 工具注册、执行 tool.service.ts
Model Module 模型配置、切换 model.service.ts

三、核心模块实现

3.1 工作流执行引擎(DAG 调度)

核心流程

typescript 复制代码
// packages/core/src/workflow-executor.ts
export class WorkflowExecutor {
  async execute(
    workflow: WorkflowDefinition, 
    variables: Variables
  ): Promise<ExecutionResult> {
    // 1. 拓扑排序(DAG 调度)
    const sortedNodes = this.topologicalSort(
      workflow.nodes, 
      workflow.edges
    );
    
    // 2. 依次执行节点
    for (const node of sortedNodes) {
      const executor = this.getExecutor(node.type);
      const result = await executor.execute(node, variables);
      variables.set(`nodes.${node.id}.output`, result);
    }
    
    // 3. 返回最终结果
    return this.collectOutput(variables);
  }
  
  private topologicalSort(
    nodes: Node[], 
    edges: Edge[]
  ): Node[] {
    const visited = new Set<string>();
    const result: Node[] = [];
    
    function visit(nodeId: string) {
      if (visited.has(nodeId)) return;
      visited.add(nodeId);
      
      // 先访问所有前置节点
      edges
        .filter(e => e.target === nodeId)
        .forEach(e => visit(e.source));
      
      result.push(nodes.find(n => n.id === nodeId));
    }
    
    nodes.forEach(n => visit(n.id));
    return result;
  }
}

节点类型与执行器

typescript 复制代码
interface NodeExecutor {
  execute(node: Node, variables: Variables): Promise<any>;
}

// 策略模式实现
class LLMNodeExecutor implements NodeExecutor { /* LLM 调用 */ }
class HTTPNodeExecutor implements NodeExecutor { /* HTTP 请求 */ }
class ConditionNodeExecutor implements NodeExecutor { /* 条件判断 */ }
class ToolNodeExecutor implements NodeExecutor { /* 工具调用 */ }
class StartNodeExecutor implements NodeExecutor { /* 变量初始化 */ }
class EndNodeExecutor implements NodeExecutor { /* 结果收集 */ }

3.2 变量管理系统(三层作用域)

typescript 复制代码
// packages/core/src/variable-manager.ts
export class VariableManager {
  private global: Variables = {};           // 全局变量
  private nodeScoped: Map<string, Variables> = new Map(); // 节点变量
  private tempScoped: Variables = {};       // 临时变量
  
  // 设置变量
  set(path: string, value: any, scope: Scope = 'global') {
    switch (scope) {
      case 'global':
        this.global[path] = value;
        break;
      case 'node':
        // 节点作用域逻辑
        break;
      case 'temp':
        this.tempScoped[path] = value;
        break;
    }
  }
  
  // 模板解析:{{ nodes.llm_1.output }}
  resolve(template: string): string {
    return template.replace(/\{\{ (.+?) \}\}/g, (_, path) => {
      return this.getVariable(path) || '';
    });
  }
  
  private getVariable(path: string): any {
    const parts = path.split('.');
    let current: any = this.global;
    
    for (const part of parts) {
      if (current && typeof current === 'object') {
        current = current[part];
      } else {
        return undefined;
      }
    }
    
    return current;
  }
}

模板语法

bash 复制代码
{{ global_var }}              # 全局变量
{{ nodes.llm_1.output }}      # 节点输出
{{ temp.result }}             # 临时变量
{{ format_date(now, 'YYYY') }} # 函数调用

3.3 LLM Provider 抽象(策略模式)

typescript 复制代码
// packages/providers/src/provider-factory.ts
export interface LLMProvider {
  chat(messages: Message[], options: ChatOptions): Promise<ChatResponse>;
  stream(messages: Message[], options: ChatOptions): AsyncIterableIterator<ChatChunk>;
}

export function getProviderFactory(provider: string): LLMProvider {
  switch (provider) {
    case 'ollama':
      return new OllamaProvider();
    case 'aliyun':
      return new AliyunProvider();
    default:
      throw new Error(`Unknown provider: ${provider}`);
  }
}

// packages/providers/src/ollama-provider.ts
export class OllamaProvider implements LLMProvider {
  async chat(messages: Message[], options: ChatOptions): Promise<ChatResponse> {
    const response = await fetch('http://localhost:11434/api/chat', {
      method: 'POST',
      body: JSON.stringify({
        model: options.model,
        messages: messages,
        stream: false,
      }),
    });
    
    return response.json();
  }
  
  async *stream(messages: Message[], options: ChatOptions): AsyncIterableIterator<ChatChunk> {
    const response = await fetch('http://localhost:11434/api/chat', {
      method: 'POST',
      body: JSON.stringify({
        model: options.model,
        messages: messages,
        stream: true,
      }),
    });
    
    const reader = response.body?.getReader();
    while (true) {
      const { done, value } = await reader!.read();
      if (done) break;
      yield JSON.parse(new TextDecoder().decode(value));
    }
  }
}

3.4 API Key 认证机制

typescript 复制代码
// apps/server/src/auth/api-key.guard.ts
@Injectable()
export class ApiKeyGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private appService: AppService,
  ) {}
  
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 检查是否跳过认证
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;
    
    const request = context.switchToHttp().getRequest();
    const apiKey = request.headers['x-api-key'];
    
    if (!apiKey) {
      throw new UnauthorizedException('Missing API Key');
    }
    
    // 验证 API Key
    const app = await this.appService.findByApiKey(apiKey);
    if (!app) {
      throw new ForbiddenException('Invalid API Key');
    }
    
    request.app = app;
    return true;
  }
}

// 使用方式
@Controller('apps')
@UseGuards(ApiKeyGuard)
export class AppController {
  @Get()
  findAll(@Request() req) {
    // 已通过认证
    return this.appService.findAll(req.app.id);
  }
  
  @Get('models')
  @Public()  // 跳过认证
  findAllModels() {
    return this.modelService.findAll();
  }
}

四、技术难点与解决方案

4.1 流式 SSE 响应实现

问题 :NestJS 的 @Sse() 装饰器需要返回 Observable,但实际实现返回 AsyncIterableIterator

解决方案:改用普通 POST + 流式 Response

typescript 复制代码
// apps/server/src/modules/chat/chat.controller.ts
@Post('completions/stream')
async streamCompletion(
  @Body() dto: ChatCompletionDto,
  @Res() res: Response,
) {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  
  const stream = await this.chatService.streamResponse(dto);
  
  for await (const chunk of stream) {
    res.write(`data: ${JSON.stringify(chunk)}\n\n`);
  }
  
  res.end();
}

前端 SSE 客户端

typescript 复制代码
// apps/web/src/lib/sse-client.ts
export async function fetchStream(
  url: string, 
  data: any,
  onChunk: (chunk: any) => void
) {
  const response = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
  
  const reader = response.body?.getReader();
  const decoder = new TextDecoder();
  
  while (true) {
    const { done, value } = await reader!.read();
    if (done) break;
    
    const text = decoder.decode(value);
    const chunks = text.split('\n\n').filter(Boolean);
    
    for (const chunk of chunks) {
      if (chunk.startsWith('data: ')) {
        const data = JSON.parse(chunk.slice(6));
        onChunk(data);
      }
    }
  }
}

4.2 测试环境 Mock 策略

问题:E2E 测试中 ApiKeyGuard 拦截请求导致 404

解决方案:创建测试专用 Module,不导入 AuthModule

typescript 复制代码
// apps/server/test/app-test.module.ts
@Module({
  controllers: [AppTestController],
  providers: [
    { 
      provide: MOCK_APP_SERVICE, 
      useClass: MockAppService, 
    },
  ],
})
export class AppTestModule {}

// 测试文件
beforeAll(async () => {
  const moduleFixture = await Test.createTestingModule({
    imports: [AppTestModule], // 使用 Mock Module
  }).compile();

  app = moduleFixture.createNestApplication();
  app.setGlobalPrefix('/api'); // 手动设置路由前缀
  await app.init();
});

4.3 跨域与路由前缀问题

问题 :测试环境变量未生效,路由前缀 /api 未应用

解决方案:多层配置确保优先级

typescript 复制代码
// apps/server/src/main.ts
const apiPrefix = 
  configService.get('API_PREFIX') || 
  process.env.API_PREFIX || 
  '/api';
app.setGlobalPrefix(apiPrefix);
typescript 复制代码
// apps/server/test/global-setup.ts
export default async function setup() {
  process.env.API_PREFIX = '/api';
  process.env.NODE_ENV = 'test';
  console.log('✅ E2E test environment setup complete');
}

五、测试策略与实践

5.1 测试金字塔设计

scss 复制代码
        ┌─────────────────┐
        │   E2E 测试       │  81 用例 (18.2%)
        │  (完整流程)     │
       ├─────────────────┤
      │   服务层测试      │  70 用例 (15.8%)
      │  (业务逻辑)      │
     ├───────────────────┤
    │    单元测试         │ 363 用例 (81.8%)
    │   (工具函数)       │
   └─────────────────────┘
   
   总计:444 用例,100% 通过,执行时间 < 7 秒

5.2 单元测试:直接实例化 + Mock

typescript 复制代码
// apps/server/src/modules/chat/chat.service.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';

describe('ChatService', () => {
  let service: ChatService;
  const mockPrisma = { /* Mock 实现 */ };
  const mockConversation = { /* Mock 实现 */ };
  const mockMessage = { /* Mock 实现 */ };
  
  beforeEach(() => {
    // 直接实例化,不依赖 TestingModule
    service = new ChatService(mockPrisma, mockConversation, mockMessage);
    vi.clearAllMocks();
  });
  
  it('should send message and save to database', async () => {
    mockConversation.create.mockResolvedValue({ id: 'conv-1' });
    
    const result = await service.sendMessage({ 
      appId: 'app-1', 
      message: 'Hello' 
    });
    
    expect(result.conversationId).toBe('conv-1');
    expect(mockConversation.create).toHaveBeenCalledWith({
      data: { appId: 'app-1' },
    });
  });
});

5.3 E2E 测试:TestModule + Mock Services

typescript 复制代码
// apps/server/test/integration/apps.e2e-spec.ts
import { AppTestModule, MOCK_APP_SERVICE } from '../app-test.module';

describe('Apps API E2E', () => {
  let app: INestApplication;
  let mockService: MockAppService;

  beforeAll(async () => {
    const moduleFixture = await Test.createTestingModule({
      imports: [AppTestModule],
    }).compile();

    mockService = moduleFixture.get<MockAppService>(MOCK_APP_SERVICE);
    app = moduleFixture.createNestApplication();
    app.setGlobalPrefix('/api');
    await app.init();
  });

  it('should create an app', async () => {
    const response = await request(app.getHttpServer())
      .post('/api/apps')
      .send({ name: 'Test App' });
    
    expect(response.status).toBe(201);
    expect(response.body.id).toBeDefined();
  });
});

5.4 测试结果

包名 测试文件 测试用例 通过 跳过 执行时间
apps/server 14 151 151 0 3.95s
packages/core 9 139 139 0 2.01s
packages/providers 4 93 81 12 794ms
packages/shared 1 61 61 0 438ms
总计 28 444 432 12 ~7s

六、性能优化与最佳实践

6.1 代码组织规范

typescript 复制代码
// 导入顺序:外部包 → 内部包 → 相对导入
import { Module, Injectable } from '@nestjs/common';
import { getProviderFactory } from '@ai-engine/providers';
import { ChatMessage } from '@ai-engine/shared';
import { PrismaService } from '../../prisma/prisma.service';

// 命名规范
// 文件:kebab-case (chat.service.ts)
// 类:PascalCase (ChatService)
// 变量/函数:camelCase (sendMessage)
// 常量:UPPER_SNAKE_CASE (API_PREFIX)

6.2 错误处理模式

typescript 复制代码
// 统一错误处理
private readonly logger = new Logger(ChatService.name);

try {
  await this.riskyOperation();
} catch (error) {
  this.logger.error(`Operation failed: ${error instanceof Error ? error.message : 'Unknown'}`);
  
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    throw new BadRequestException('Database error');
  }
  
  throw error;
}

6.3 日志与监控

typescript 复制代码
// 结构化日志
this.logger.log('Chat request received', {
  requestId: 'req-123',
  appId: 'app-456',
  messageCount: messages.length,
});

this.logger.error('LLM API call failed', {
  provider: 'ollama',
  model: 'qwen3.5:9b',
  error: error.message,
});

七、总结与展望

7.1 技术收获

  1. Monorepo 架构:代码复用更方便,测试隔离更清晰
  2. DAG 调度算法:拓扑排序在工作流编排中的应用
  3. 策略模式:LLM Provider、Node Executor 的抽象设计
  4. 测试驱动:高覆盖率带来的重构信心

7.2 待优化项

  • 工作流可视化编排(React Flow 集成)
  • Test Containers 真实数据库测试
  • CI/CD 集成(GitHub Actions)
  • 前端 E2E 测试(Playwright)

7.3 下一步计划

阶段 内容 预计时间
短期 Test Containers + CI/CD 1-2 周
中期 前端 E2E + 性能优化 1-2 月
长期 全链路压测 + 生产部署 3-6 月

附录:项目资源

运行项目

bash 复制代码
# 克隆项目
git clone https://github.com/lxx741/ai-engine
cd ai-engine

# 安装依赖
pnpm install

# 启动数据库
cd docker && docker-compose up -d

# 启动后端
pnpm dev:server  # http://localhost:3000

# 启动前端
pnpm dev:web     # http://localhost:3001

# 运行测试
pnpm test
pnpm --filter @ai-engine/server test:e2e

作者 :未完待续
完成时间 :2026-03-17
代码行数 :21,285 行
测试覆盖 :444 个用例,100% 通过
GitHubgithub.com/lxx741/ai-e...

相关推荐
枫叶林FYL1 小时前
【机器学习与智慧医疗】T2DM-EWS: 2型糖尿病早期预警系统(多参数集成分类模型)完整实现
人工智能·机器学习·分类
南屹川1 小时前
【缓存技术】Redis实战:从缓存策略到分布式锁
人工智能
Li emily7 小时前
解决了加密货币api多币种订阅时的数据乱序问题
人工智能·python·api·fastapi
山川绿水7 小时前
bugku——PWN——overflow2
人工智能·web安全·网络安全
程序员cxuan7 小时前
微信读书官方发了 skills,把我给秀麻了。
人工智能·后端·程序员
fake_ss1988 小时前
AI时代学习全栈项目开发的新范式
java·人工智能·学习·架构·个人开发·学习方法
nassi_8 小时前
对AI工程问题的一些思考
大数据·人工智能·hadoop
AI技术控8 小时前
《Transformers are Inherently Succinct》论文解读:从“能表达什么”到“多紧凑地表达”
人工智能·python·深度学习·机器学习·自然语言处理
蔡俊锋8 小时前
AI记忆压缩术:从305GB到7.4GB的魔法
人工智能·ai·ai 记忆
Upsy-Daisy8 小时前
AI Agent 项目学习笔记(二):Spring AI 与 ChatClient 主链路解析
人工智能·笔记·学习