从零构建企业级 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...

相关推荐
进击的野人2 小时前
新手入门:如何接入AI大模型?从零开始的实用指南
人工智能·agent·ai编程
小时前端2 小时前
怎么让 Claude 在你睡觉的时候干活
人工智能
AEIC学术交流中心2 小时前
【快速EI检索】2026年人工智能与电气工程国际学术会议(AI-EE 2026)
人工智能
Yan-英杰2 小时前
远程控制软件哪个安全?2026 ToDesk/向日葵/RayLink加密、隐私与防护全面对比评测
网络·人工智能·网络协议·tcp/ip·http
恋猫de小郭2 小时前
让你的 OpenClaw 带你学习,清华开源 AI 私人导师 OpenMAIC
前端·人工智能·ai编程
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章37-区域截图
图像处理·人工智能·opencv·算法·计算机视觉
CCPC不拿奖不改名2 小时前
RAG基础:评测系统RAGAS的四大指标
linux·服务器·人工智能·计算机视觉·前端框架·知识库搭建
搬砖者(视觉算法工程师)2 小时前
通俗易懂的 Transformer 入门文章(第三部分):多头注意力深度剖析
人工智能
happyprince2 小时前
2026年03月18日全球AI前沿动态
人工智能