三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

基于NestJS与Next.js构建企业级AI应用引擎:架构设计与工程实践

基于NestJS与Next.js构建企业级AI应用引擎:架构设计与工程实践

1. 项目概述:为什么需要“企业级 AI 应用引擎”?

最近和几个创业团队的技术负责人聊天,大家不约而同地提到了同一个痛点:AI 能力集成太“散”了。今天接个 OpenAI 的聊天接口,明天加个 Stable Diffusion 的绘图功能,后天又要处理文档的向量化检索。每个功能都是临时起意,用几段脚本糊在现有业务代码里。初期跑起来没问题,但随着用户量上来,问题就全暴露了:API 调用混乱没有熔断、提示词工程毫无管理、算力成本失控、前后端数据流像一团乱麻。这让我想起十多年前做 Web 2.0 项目时,大家也是把 jQuery 插件到处塞,直到前端工程化的出现才把我们从“屎山”里拯救出来。现在的 AI 应用开发,就处在这样一个“前工程化”的混沌期。

所以,“企业级 AI 应用引擎”这个概念,并不是要造一个多么玄乎的底层框架,它的核心目标非常务实:为频繁、多样且复杂的 AI 能力集成,提供一个统一、健壮、可观测的“插座”和“配电箱”。它应该能让业务开发者像调用本地服务一样调用 AI 能力,同时让架构师能清晰地掌控成本、性能和稳定性。这背后,需要一个深思熟虑的全栈架构来支撑。

我选择了 NestJS 和 Next.js 这套组合作为这次实践的基石。NestJS 以其清晰的分层架构、强大的依赖注入和对 TypeScript 的极致支持,成为了构建稳健后端服务的首选;而 Next.js,特别是其 App Router 和对 React Server Components 的成熟运用,让我们能构建出兼具高性能与良好开发体验的前端。更重要的是,两者在 TypeScript 上同源,共享类型定义变得异常顺畅,这是提升全栈开发效率的关键。

这次分享,我会从一个真实的“智能客服知识库问答”场景出发,带你一步步搭建这个引擎的核心骨架。我们会采用 Monorepo 来管理前后端代码,确保项目结构清晰且易于协作。整个系列会聚焦于架构设计、核心模式和实践中的“坑”,目标是交付一套能直接用于生产环境参考的蓝图。

2. 架构核心:Monorepo 设计与技术选型背后的逻辑

在动手写第一行代码之前,花在架构设计上的时间永远是最值得的。我们首先要回答:代码怎么组织?为什么是这些技术?

2.1 为什么是 Monorepo?不仅仅是代码放在一起

很多人把 Monorepo 简单理解为用一个仓库放多个项目。对于我们的 AI 应用引擎而言,它的价值远不止于此。

核心优势一:类型安全与共享代码的无缝衔接AI 应用前后端交互的数据结构往往复杂多变。一个对话请求,可能包含消息历史、系统指令、温度参数、流式输出标志等。在传统的多仓库模式下,你需要手动维护两份类型定义(后端 DTO/接口 和前端 TypeScript 类型),一旦一方修改,同步就是一场噩梦。在 Monorepo 中,我们可以创建一个共享的packages/typespackages/schemas包,使用 Zod 或 TypeScript 定义核心的数据契约。前后端都依赖这个共享包,类型定义天然一致,重构时 IDE 能提供跨项目的引用检查和自动更新,这是提升开发效率和减少 Bug 的利器。

核心优势二:统一的工具链与开发体验你可以为整个项目配置一致的代码格式化(Prettier)、代码检查(ESLint)、提交规范(Commitlint)和 Git Hook(Husky)。这意味着,无论是后端 NestJS 代码还是前端 Next.js 代码,都遵循同一套质量守则。同时,你可以利用 Turborepo 或 Nx 这样的构建系统,实现智能的任务编排和缓存。例如,运行turbo run dev可以并行启动后端和前端开发服务器,并且只构建发生变更的部分,极大提升了本地开发效率。

核心优势三:简化依赖管理与部署协调当你的 AI 引擎需要升级底层模型 SDK(比如从 OpenAI SDK v3 升级到 v4)时,在 Monorepo 中你只需要在一个地方更新依赖版本,然后所有使用它的服务(可能是多个后端微服务)会同步更新。这避免了在多仓库中逐个查找、更新可能导致的版本不一致问题。在部署时,你也可以通过 Turbo 的 Pipeline 配置,确保后端构建并部署完成后,再构建部署前端,保证上下游服务的版本一致性。

注意:Monorepo 不是银弹。随着项目膨胀,初始构建时间和仓库体积会增长。务必从一开始就规划好清晰的目录结构,并利用好.gitignore和 Turbo/Nx 的远程缓存功能。对于超大型团队,可能需要评估是否在后期拆分为更细粒度的 Multi-Repo。

2.2 NestJS + Next.js:全栈 TypeScript 的黄金搭档

后端:NestJS 作为 AI 服务的“调度中心”NestJS 的核心价值在于它强制性的架构约束。对于需要集成多种 AI 服务(OpenAI、Anthropic、本地部署的 Llama.cpp 等)的引擎来说,这种约束是福不是祸。

  • 模块化(Modules):我们可以将不同的 AI 能力抽象为独立的模块。例如,ChatModule负责对话,EmbeddingModule负责文本向量化,ImageGenerationModule负责文生图。每个模块内部封装了对应供应商的 SDK 调用、错误处理和提示词模板。业务层只需注入对应的 Service,无需关心底层实现。
  • 依赖注入(DI)与抽象:这是实现“可插拔”AI 供应商的关键。我们可以定义一个抽象的AIService接口,然后为 OpenAI、Azure OpenAI 等提供不同的实现。通过配置,可以轻松切换或同时使用多个供应商,实现降级和负载均衡。
  • 拦截器与守卫:这是实现企业级管控的利器。一个全局的LoggingInterceptor可以记录每一次 AI 调用的耗时、token 用量和成本;一个RateLimitGuard可以防止单个用户滥用 API;一个ValidationPipe确保输入数据的格式安全,防止提示词注入攻击。

前端:Next.js 作为 AI 交互的“智能终端”Next.js 的价值在于它统一了渲染范式,并提供了强大的服务端能力,这对于 AI 应用常见的流式响应和复杂状态管理至关重要。

  • App Router 与 Server Components:我们可以将大部分数据获取逻辑(如获取对话历史、知识库列表)放在 Server Component 中,直接调用后端服务,获得更好的安全性和首屏性能。页面是静态还是动态,缓存策略如何,都可以通过简单的配置声明。
  • API Routes 作为轻量级代理:虽然核心 AI 业务在后端,但前端有时也需要一些轻量的、与 UI 强相关的服务端逻辑。Next.js 的 API Routes 非常适合处理文件上传(如图片生成时的草图)、服务器端的事件流转发(SSE)等,避免将所有流量都导向后端主服务。
  • 流式渲染(Streaming):这是 AI 对话应用的标配。Next.js 可以很好地支持从后端流式接收 AI 回复,并通过 Suspense 边界逐步渲染到 UI 上,实现打字机效果,用户体验远优于等待整个响应完成。

2.3 初始项目结构搭建

理论说再多,不如一行命令。我们使用 Turborepo 来快速搭建项目骨架。

# 使用 Turborepo 官方模板创建项目 npx create-turbo@latest enterprise-ai-engine cd enterprise-ai-engine

创建完成后,清理模板文件,建立我们自己的目录结构:

enterprise-ai-engine/ ├── apps/ │ ├── backend/ # NestJS 后端应用 │ └── frontend/ # Next.js 前端应用 ├── packages/ │ ├── types/ # 共享的 TypeScript 类型定义 │ ├── config-eslint/ # 共享的 ESLint 配置 │ └── ui/ # 共享的 React UI 组件库(可选) ├── package.json ├── turbo.json # Turborepo 任务配置 └── tsconfig.json # 根级 TypeScript 配置

关键配置解析(turbo.json):

{ "$schema": "https://turbo.build/schema.json", "globalDependencies": ["**/.env.*local"], // 环境变量变更时,使缓存失效 "pipeline": { "build": { "dependsOn": ["^build"], // 依赖的包先构建 "outputs": [".next/**", "dist/**"] }, "dev": { "cache": false // 开发模式不缓存 }, "lint": { "outputs": [] } } }

在根目录的package.json中,我们配置脚本,实现一键启动:

{ "scripts": { "dev": "turbo run dev", "build": "turbo run build", "lint": "turbo run lint", "format": "prettier --write \"**/*.{ts,tsx,md}\"" } }

现在,运行npm run dev,Turborepo 会并行启动后端和前端开发服务器。一个清晰、高效的全栈开发环境就准备就绪了。

3. 后端核心:使用 NestJS 构建健壮的 AI 服务层

后端是整个引擎的大脑,负责调度、编排和管控所有 AI 能力。我们以“智能对话”这个最普遍的场景为例,深入核心设计。

3.1 领域模型与模块划分

首先,在共享的packages/types中定义核心的对话类型,确保前后端语言一致。

// packages/types/src/chat.ts export interface ChatMessage { role: 'system' | 'user' | 'assistant'; content: string; } export interface ChatCompletionRequest { messages: ChatMessage[]; model?: string; // 如 'gpt-4', 'claude-3-sonnet' stream?: boolean; temperature?: number; maxTokens?: number; // ... 其他供应商特定参数可通过扩展传递 } export interface ChatCompletionResponse { id: string; choices: { message: ChatMessage; finishReason: string; }[]; usage: { promptTokens: number; completionTokens: number; }; }

后端apps/backend内,我们按照领域驱动设计(DDD)的轻量级思路划分模块:

apps/backend/src/ ├── modules/ │ ├── chat/ │ │ ├── chat.module.ts │ │ ├── chat.controller.ts │ │ ├── chat.service.ts │ │ ├── dto/ │ │ ├── interfaces/ │ │ └── providers/ # AI 供应商实现(openai.provider.ts, azure.provider.ts) │ ├── knowledge-base/ # 知识库模块(后续扩展) │ └── file/ # 文件上传与处理模块 ├── common/ │ ├── filters/ # 异常过滤器 │ ├── interceptors/ # 日志、转换拦截器 │ └── guards/ # 限流、权限守卫 └── main.ts

3.2 实现可插拔的 AI 供应商服务

这是架构的核心。我们不直接在ChatService里写死 OpenAI 的调用,而是通过抽象和依赖注入来解耦。

第一步:定义抽象接口

// apps/backend/src/modules/chat/interfaces/ai-provider.interface.ts import { ChatCompletionRequest, ChatCompletionResponse } from '@enterprise-ai-engine/types'; export interface IAiProvider { createChatCompletion( request: ChatCompletionRequest, options?: any, ): Promise<ChatCompletionResponse>; createChatCompletionStream( request: ChatCompletionRequest, options?: any, ): AsyncIterable<string>; // 返回流式数据 }

第二步:实现具体供应商(以 OpenAI 为例)

// apps/backend/src/modules/chat/providers/openai.provider.ts import { Injectable, Logger } from '@nestjs/common'; import OpenAI from 'openai'; import { IAiProvider, ChatCompletionRequest, ChatCompletionResponse } from '../interfaces'; import { Stream } from 'openai/streaming'; @Injectable() export class OpenAiProvider implements IAiProvider { private readonly openai: OpenAI; private readonly logger = new Logger(OpenAiProvider.name); constructor() { // 密钥应从配置模块动态注入,此处简化 this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); } async createChatCompletion(request: ChatCompletionRequest): Promise<ChatCompletionResponse> { try { const completion = await this.openai.chat.completions.create({ model: request.model || 'gpt-4-turbo-preview', messages: request.messages, temperature: request.temperature, max_tokens: request.maxTokens, }); // 将 OpenAI 的响应格式转换为我们定义的通用格式 return this.transformResponse(completion); } catch (error) { this.logger.error(`OpenAI API调用失败: ${error.message}`, error.stack); throw new Error(`AI服务暂时不可用: ${error.message}`); } } async *createChatCompletionStream(request: ChatCompletionRequest): AsyncIterable<string> { try { const stream = await this.openai.chat.completions.create({ model: request.model || 'gpt-4-turbo-preview', messages: request.messages, temperature: request.temperature, stream: true, }) as Stream<OpenAI.Chat.Completions.ChatCompletionChunk>; for await (const chunk of stream) { const content = chunk.choices[0]?.delta?.content || ''; if (content) { yield content; // 逐块返回文本内容 } } } catch (error) { this.logger.error(`OpenAI 流式调用失败: ${error.message}`); throw error; } } private transformResponse(openaiResponse: any): ChatCompletionResponse { // ... 实现格式转换逻辑 } }

第三步:在模块中动态提供实现

// apps/backend/src/modules/chat/chat.module.ts import { Module, Provider } from '@nestjs/common'; import { ChatService } from './chat.service'; import { ChatController } from './chat.controller'; import { OpenAiProvider } from './providers/openai.provider'; import { AzureOpenAiProvider } from './providers/azure.provider'; // 根据配置决定使用哪个 Provider const aiProvider: Provider = { provide: 'AI_PROVIDER', useClass: process.env.AI_PROVIDER === 'azure' ? AzureOpenAiProvider : OpenAiProvider, }; @Module({ controllers: [ChatController], providers: [ChatService, aiProvider], exports: [ChatService], }) export class ChatModule {}

第四步:在 Service 中注入并使用

// apps/backend/src/modules/chat/chat.service.ts import { Inject, Injectable } from '@nestjs/common'; import { IAiProvider } from './interfaces/ai-provider.interface'; @Injectable() export class ChatService { constructor( @Inject('AI_PROVIDER') private readonly aiProvider: IAiProvider, ) {} async createCompletion(request: ChatCompletionRequest) { // 这里可以添加业务逻辑,如对话历史管理、敏感词过滤、成本计算等 return this.aiProvider.createChatCompletion(request); } async createCompletionStream(request: ChatCompletionRequest) { return this.aiProvider.createChatCompletionStream(request); } }

通过这样的设计,更换 AI 供应商就像修改一个环境变量一样简单。未来增加新的供应商(如 Anthropic、Google Gemini),只需新增一个实现IAiProvider的类并在模块中配置即可,业务代码ChatService完全不用动。

3.3 企业级功能增强:全局拦截器与守卫

日志与监控拦截器我们需要记录每一次 AI 调用的详细信息,用于成本分析和性能监控。

// apps/backend/src/common/interceptors/logging.interceptor.ts import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common'; import { Observable, tap } from 'rxjs'; @Injectable() export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger(LoggingInterceptor.name); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const { method, url, body } = request; const now = Date.now(); return next.handle().pipe( tap((data) => { const response = context.switchToHttp().getResponse(); const delay = Date.now() - now; // 关键:记录 AI 调用相关的业务日志 if (url.includes('/chat/completions')) { this.logger.log({ type: 'AI_API_CALL', path: url, method, requestId: request.headers['x-request-id'], userId: request.user?.id, // 假设用户信息已注入 model: body?.model, promptTokens: data?.usage?.promptTokens, completionTokens: data?.usage?.completionTokens, totalTokens: data?.usage?.totalTokens, duration: `${delay}ms`, timestamp: new Date().toISOString(), }); } this.logger.log(`${method} ${url} ${response.statusCode} - ${delay}ms`); }), ); } }

速率限制守卫防止 API 被滥用,保护后端服务和 AI 账户预算。

// apps/backend/src/common/guards/rate-limit.guard.ts import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { Redis } from 'ioredis'; // 使用 Redis 存储计数 @Injectable() export class RateLimitGuard implements CanActivate { private redisClient: Redis; constructor(private reflector: Reflector) { this.redisClient = new Redis(process.env.REDIS_URL); } async canActivate(context: ExecutionContext): Promise<boolean> { // 可以从元数据获取针对不同端点的限流策略 const limit = this.reflector.get<number>('rateLimit', context.getHandler()) || 10; // 默认 10次/分钟 const request = context.switchToHttp().getRequest(); const key = `rate-limit:${request.user?.id || request.ip}:${request.path}`; const current = await this.redisClient.incr(key); if (current === 1) { await this.redisClient.expire(key, 60); // 设置过期时间为1分钟 } if (current > limit) { throw new ForbiddenException(`请求过于频繁,请稍后再试。限制: ${limit} 次/分钟`); } return true; } }

在 Controller 中使用:

// apps/backend/src/modules/chat/chat.controller.ts import { Controller, Post, Body, UseGuards, UseInterceptors, Sse } from '@nestjs/common'; import { ChatService } from './chat.service'; import { RateLimitGuard } from '../../common/guards/rate-limit.guard'; import { LoggingInterceptor } from '../../common/interceptors/logging.interceptor'; import { ChatCompletionRequest } from '@enterprise-ai-engine/types'; @Controller('chat') @UseInterceptors(LoggingInterceptor) // 应用日志拦截器 export class ChatController { constructor(private readonly chatService: ChatService) {} @Post('completions') @UseGuards(RateLimitGuard) // 应用限流守卫 async createCompletion(@Body() request: ChatCompletionRequest) { return this.chatService.createCompletion(request); } @Post('completions/stream') @UseGuards(RateLimitGuard) @Sse() // 使用 Server-Sent Events 返回流 async createCompletionStream(@Body() request: ChatCompletionRequest) { const stream = await this.chatService.createCompletionStream(request); // 将 AsyncIterable 转换为 Observable,NestJS @Sse 装饰器需要 return new Observable((subscriber) => { (async () => { for await (const chunk of stream) { subscriber.next({ data: { content: chunk } }); } subscriber.complete(); })(); }); } }

4. 前端核心:使用 Next.js 构建流式 AI 交互界面

前端是用户与 AI 引擎交互的窗口,核心挑战在于高效处理流式响应和复杂状态。

4.1 使用 React Server Components 获取数据

在 Next.js 的 App Router 中,我们优先使用 Server Component 来获取初始数据,保证安全性和性能。

// apps/frontend/app/chat/page.tsx import { getChatHistory } from '@/app/actions/chat-actions'; // 服务端 Action import ChatClient from './chat-client'; export default async function ChatPage({ searchParams }: { searchParams: { sessionId?: string } }) { // 在服务端直接获取数据,不会暴露 API 密钥 const initialHistory = await getChatHistory(searchParams.sessionId); return ( <div className="container mx-auto p-4"> <h1 className="text-2xl font-bold mb-4">AI 对话助手</h1> {/* 将初始数据传递给客户端组件 */} <ChatClient initialMessages={initialHistory} /> </div> ); }

4.2 实现流式对话客户端

客户端组件ChatClient负责处理用户输入和渲染流式响应。

// apps/frontend/app/chat/chat-client.tsx 'use client'; import { useState, useRef, useEffect } from 'react'; import { ChatMessage } from '@enterprise-ai-engine/types'; export default function ChatClient({ initialMessages = [] }: { initialMessages: ChatMessage[] }) { const [messages, setMessages] = useState<ChatMessage[]>(initialMessages); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const messagesEndRef = useRef<HTMLDivElement>(null); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!input.trim() || isLoading) return; const userMessage: ChatMessage = { role: 'user', content: input }; const newMessages = [...messages, userMessage]; setMessages(newMessages); setInput(''); setIsLoading(true); // 添加一个空的 assistant 消息占位符,用于流式填充 const assistantMessageId = Date.now().toString(); setMessages((prev) => [...prev, { role: 'assistant', content: '', id: assistantMessageId }]); try { const response = await fetch('/api/chat/stream', { // 调用 Next.js API Route method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: newMessages }), }); if (!response.ok || !response.body) { throw new Error('网络响应错误'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let accumulatedContent = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); // 假设后端返回的是纯文本流或简单的 SSE 格式数据 accumulatedContent += chunk; // 实时更新最后一条 assistant 消息的内容 setMessages((prev) => prev.map((msg) => msg.id === assistantMessageId ? { ...msg, content: accumulatedContent } : msg ) ); } } catch (error) { console.error('对话失败:', error); setMessages((prev) => prev.map((msg) => msg.id === assistantMessageId ? { ...msg, content: `抱歉,对话出现错误: ${error.message}` } : msg ) ); } finally { setIsLoading(false); } }; return ( <div className="flex flex-col h-[600px] border rounded-lg"> <div className="flex-1 overflow-y-auto p-4"> {messages.map((msg, idx) => ( <div key={idx} className={`mb-3 ${msg.role === 'user' ? 'text-right' : ''}`}> <div className={`inline-block px-4 py-2 rounded-lg ${msg.role === 'user' ? 'bg-blue-100' : 'bg-gray-100'}`}> {msg.content || (msg.role === 'assistant' && '思考中...')} </div> </div> ))} <div ref={messagesEndRef} /> </div> <form onSubmit={handleSubmit} className="border-t p-4"> <div className="flex"> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} className="flex-1 border rounded-l-lg p-2" placeholder="输入您的问题..." disabled={isLoading} /> <button type="submit" disabled={isLoading} className="bg-blue-500 text-white px-4 py-2 rounded-r-lg disabled:opacity-50" > {isLoading ? '发送中...' : '发送'} </button> </div> </form> </div> ); }

4.3 创建 Next.js API Route 作为代理

为了更好的控制和安全性,我们不直接从前端调用后端服务,而是通过 Next.js 的 API Route 进行代理。这样可以隐藏后端地址,并在服务端统一添加认证、日志等逻辑。

// apps/frontend/app/api/chat/stream/route.ts import { type NextRequest } from 'next/server'; export async function POST(request: NextRequest) { const body = await request.json(); // 1. 可选:在这里进行用户身份验证和请求验证 // const session = await getAuthSession(); // if (!session) { return new Response('Unauthorized', { status: 401 }); } // 2. 调用后端 NestJS 的流式端点 const backendResponse = await fetch(`${process.env.BACKEND_API_URL}/chat/completions/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', // 可以传递认证信息,如 API Key 'X-API-Key': process.env.INTERNAL_API_KEY || '', }, body: JSON.stringify(body), }); if (!backendResponse.ok || !backendResponse.body) { console.error('后端服务错误:', backendResponse.statusText); return new Response('后端服务异常', { status: 502 }); } // 3. 将后端的流式响应直接转发给前端 return new Response(backendResponse.body, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, }); }

实操心得:流式传输的坑:在转发流式响应时,确保不要对响应体进行任何缓冲或完整的await操作。直接使用backendResponse.body并创建新的Response对象是正确做法。此外,处理 SSE 时,前端需要正确解析data:前缀,我们的示例做了简化。在生产环境中,建议使用成熟的库如eventsource-parser来处理。

5. 环境配置、部署与监控要点

一个企业级应用,除了代码,还需要配套的运维能力。

5.1 多环境配置管理

使用dotenv和 NestJS 的ConfigModule来管理配置。

// apps/backend/src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ChatModule } from './modules/chat/chat.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // 全局可用 envFilePath: `.env.${process.env.NODE_ENV || 'development'}`, // 按环境加载 }), ChatModule, ], }) export class AppModule {}

环境文件示例 (.env.production):

NODE_ENV=production BACKEND_PORT=3001 OPENAI_API_KEY=sk-*** AI_PROVIDER=openai REDIS_URL=redis://redis-server:6379 LOG_LEVEL=info

5.2 使用 Docker 容器化部署

为每个应用编写Dockerfile,并使用docker-compose.yml编排。

# apps/backend/Dockerfile FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ COPY ../../package*.json ../../ RUN npm ci --only=production COPY . . RUN npm run build FROM node:18-alpine AS runner WORKDIR /app ENV NODE_ENV production COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./ EXPOSE 3001 CMD ["node", "dist/main.js"]
# docker-compose.prod.yml version: '3.8' services: redis: image: redis:alpine ports: - '6379:6379' volumes: - redis_data:/data backend: build: context: . dockerfile: apps/backend/Dockerfile ports: - '3001:3001' environment: - NODE_ENV=production - REDIS_URL=redis://redis:6379 depends_on: - redis restart: unless-stopped frontend: build: context: . dockerfile: apps/frontend/Dockerfile ports: - '3000:3000' environment: - NEXT_PUBLIC_BACKEND_URL=http://backend:3001 depends_on: - backend restart: unless-stopped volumes: redis_data:

5.3 基础监控与日志收集

企业级应用必须可观测。除了我们自定义的LoggingInterceptor,还应集成成熟的日志系统。

  • 结构化日志:使用winstonpino替代console.log,输出 JSON 格式的日志,便于 ELK(Elasticsearch, Logstash, Kibana)或 Loki 收集。
  • 应用性能监控(APM):集成 Sentry(错误跟踪)或 OpenTelemetry(分布式追踪),监控 API 延迟、错误率和 AI 调用的链式追踪。
  • 健康检查端点:在 NestJS 中暴露/health端点,供 Kubernetes 或负载均衡器进行存活性和就绪性探测。
// apps/backend/src/health/health.controller.ts import { Controller, Get } from '@nestjs/common'; import { HealthCheck, HealthCheckService, HttpHealthIndicator } from '@nestjs/terminus'; @Controller('health') export class HealthController { constructor( private health: HealthCheckService, private http: HttpHealthIndicator, ) {} @Get() @HealthCheck() check() { return this.health.check([ () => this.http.pingCheck('nestjs-docs', 'https://docs.nestjs.com'), // 可以添加数据库、Redis 等健康检查 ]); } }

6. 常见问题与排查技巧实录

在实际开发和运维中,你会遇到各种各样的问题。这里记录了几个最典型的“坑”及其解决方案。

6.1 流式响应中断或延迟高

  • 现象:前端接收流式响应时,经常中途断开,或者响应速度很慢。
  • 排查
    1. 检查超时设置:NestJS 默认没有全局超时,但反向代理(如 Nginx)或云服务商(如 AWS ALB)可能有。确保将代理的超时时间设置得足够长(例如 300 秒)。
    2. 检查网络连接:确保前端到 Next.js API Route,以及 Next.js 到后端 NestJS 服务之间的网络稳定,没有防火墙阻断长连接。
    3. 后端流生成阻塞:检查createChatCompletionStream方法,确保for await...of循环内没有执行同步的耗时操作(如复杂的数据库查询)。AI 响应的每个 chunk 应立即 yield。
  • 解决
    • 在 Nginx 配置中增加:proxy_read_timeout 300s;
    • 在 Next.js API Route 中,考虑设置request.socket.setTimeout(0)来禁用 Node.js socket 超时(需谨慎)。
    • 将后端的非必要逻辑(如最终对话记录保存)移到流式响应结束后异步执行。

6.2 类型在 Monorepo 中不共享或报错

  • 现象:在apps/frontend中导入@enterprise-ai-engine/types包时,VS Code 提示找不到模块或类型不对。
  • 排查
    1. 检查包是否已构建:在根目录运行npm run buildnpx turbo run build,确保共享包types被优先构建。
    2. 检查tsconfig.json路径别名:在apps/frontend/tsconfig.json中,确保正确配置了paths指向共享包。
  • 解决
    • 使用 Turborepo,它通常能自动处理依赖关系。如果不行,在根目录的tsconfig.json中设置"references"
    • 一个更简单粗暴但有效的方法:在共享包packages/typespackage.json中,设置"main": "./dist/index.js""types": "./dist/index.d.ts",并确保构建脚本"build": "tsc"生成了声明文件。

6.3 AI 供应商切换后行为不一致

  • 现象:从 OpenAI 切换到 Azure OpenAI 后,同样的提示词返回结果差异很大,或者流式接口不工作。
  • 排查
    1. 参数映射:不同供应商的 API 参数名称和取值范围可能不同。例如,OpenAI 的max_tokens在 Azure 上可能是maxTokens。仔细对照官方文档。
    2. 响应格式:流式响应的数据格式可能完全不同。OpenAI 返回的是data: [DONE]格式的 SSE,而 Azure OpenAI 可能返回纯 JSON 行。
  • 解决
    • 在具体的 Provider 实现类中,实现一个normalizeRequest方法,将通用请求参数转换为特定供应商的参数。
    • 实现一个adaptStream方法,将不同供应商的流式响应统一转换为前端期望的格式(如纯文本 chunk)。这是抽象接口IAiProvidercreateChatCompletionStream返回AsyncIterable<string>的原因。

6.4 部署后前端无法连接到后端

  • 现象:本地开发一切正常,部署到服务器后,前端页面报Failed to fetchNetwork Error
  • 排查
    1. 环境变量:检查 Next.js 构建时和运行时使用的NEXT_PUBLIC_BACKEND_URL是否正确。Docker 构建时和运行时的环境变量可能不同。
    2. CORS 问题:NestJS 后端默认不允许跨域。在生产环境中,需要正确配置 CORS 来源,或者通过反向代理(如 Nginx)将前后端请求代理到同一个域名下。
    3. 网络策略:在 Docker Compose 或 Kubernetes 中,确保frontend服务能通过服务名(如http://backend:3001)访问到backend服务。
  • 解决
    • 在 NestJS 的main.ts中根据环境配置 CORS:
      app.enableCors({ origin: process.env.FRONTEND_URL || 'http://localhost:3000', credentials: true, });
    • 更佳实践是使用反向代理。一个简单的 Nginx 配置可以将/api代理到后端,并直接提供前端静态文件。

踩过这些坑之后,我的体会是,构建企业级 AI 应用引擎,技术选型只是第一步,更重要的是在架构初期就为“变化”做好准备——AI 模型会变,供应商会变,业务需求也会变。通过清晰的抽象(如IAiProvider)、严格的边界(前后端分离、API 契约)和统一的运维手段(容器化、监控),我们构建的不仅仅是一个应用,而是一个能够持续演进、稳定支撑业务创新的“引擎”。这个系列的第一篇,我们搭好了骨架,打通了核心流程。下一篇,我们将深入引擎的“智能”部分:如何设计一个可扩展的提示词模板引擎,以及如何集成向量数据库实现基于私有知识的精准问答。

← 返回列表