Skip to main content

Nest.js 在 AI 链路里

框架不决定 AI 能力。Nest.js 不会让你的检索更准,也不会让流式更快——它决定的是多人协作时代码会不会长歪。

所以这篇不讨论"用 Nest 能不能做 AI",而是讨论它在什么规模下值得,以及 AI 服务放在它的哪一层。

ControllerService编排AI Provider可替换实现DI 的真正收益把模型供应商做成可注入的实现,测试时换成假的,不用打真实 APIGuard / Interceptor鉴权与限流有统一位置,不用每个路由自己写一遍代价小服务用不上这套结构;冷启动比裸 Express/Fastify 重,serverless 不划算框架不决定 AI 能力,它决定多人协作时代码会不会长歪
图:Nest 的价值在结构,不在性能

1. 它和 Express / Fastify 不是同一类东西​

三个框架经常被放在一起比吞吐,然后得出"Nest 最慢"的结论。这个对比本身有问题:

  • Express 是路由 + 中间件,刻意做得薄
  • Fastify 是高性能 HTTP 服务器,schema 校验内置
  • Nest 是应用框架,跑在 Express 或 Fastify 之上,补的是它们不提供的东西

Nest 提供的是:依赖注入容器、模块系统、Guard(鉴权)、Interceptor(横切逻辑)、Pipe(校验)、异常过滤器、以及开箱可用的测试模块。

所以"Nest 慢"的实际含义是:它为你换来的一套架构,代价是一部分吞吐和更重的启动。这个交换值不值,取决于你的团队和代码规模。

2. AI 服务里,DI 的真正价值​

依赖注入在 AI 场景有一个特别具体的用处:把模型供应商做成可注入的实现。

// 定义接口
interface LlmProvider {
complete(req: CompletionRequest): Promise<CompletionResult>
stream(req: CompletionRequest): AsyncIterable<string>
}

// 生产实现
@Injectable()
class AnthropicProvider implements LlmProvider { /* ... */ }

// 测试实现
@Injectable()
class FakeProvider implements LlmProvider {
async complete() { return { text: '固定回复', usage: { in: 10, out: 5 } } }
}

带来的好处有三个,都很实际:

测试不打真实 API。 这是 AI 功能能做单元测试的前提——否则每个测试都在花钱且不稳定(具体位置见《测试分层比覆盖率数字重要》)。

换供应商不改业务代码。 配合《多模型接入与降级链》里的路由,切换只发生在 provider 实现层。

降级实现可以注入。 熔断时的兜底实现也可以是同一个接口的实现。

3. Guard 与 Interceptor 的用处​

AI 服务里有几件事是每个路由都需要的:鉴权、限流、预算检查、审计日志。

在 Express 里这些靠中间件链,但中间件是"扁平"的——哪个路由用了哪些中间件要靠人记住,且不好组合。

Nest 的 Guard(返回布尔,决定是否放行)和 Interceptor(包裹执行,能做前后处理)给了这些横切关注点一个明确的位置:

  • Guard:鉴权、权限范围校验、预算是否耗尽
  • Interceptor:请求日志、耗时统计、token 用量上报、trace 上下文注入

其中最后一项——trace 上下文注入——在 AI 服务里价值最高。见《给大模型链路装仪表》,你要把每次请求串到 trace 上,而 Interceptor 是做这件事最自然的位置。

4. 流式在 Nest 里要注意什么​

Nest 默认用 Express 适配器,而 Express 的 res 对象配合流式有一个经典陷阱:任何把它当普通响应处理的中间件都会破坏流式。

具体表现和《流式链路上的缓冲点》里说的一样:压缩中间件、日志中间件读一遍响应体,流式就变成非流式。

要点:

  • 流式路由直接操作原始响应对象或返回可读流,不走 JSON 序列化
  • 给流式路由排除压缩与 body 解析类的中间件
  • 设置 X-Accel-Buffering: no 与 Cache-Control: no-cache, no-transform
  • 把客户端断连信号传给上游:自己建 AbortController,监听 res.on('close') 并用 res.writableFinished 区分正常结束——req.signal 在 Node 的 IncomingMessage 上通常不存在,传 undefined 给 fetch 不会报错,只会让取消静默失效

Nest 其实内置了一个专门给 SSE 用的装饰器,很多人不知道:

@Sse('stream')
stream(@Query('q') q: string): Observable<MessageEvent> {
return from(this.llm.stream(q)).pipe(
map((chunk) => ({ data: chunk, type: 'delta' })),
)
}

@Sse() 会替你设好 Content-Type: text/event-stream,并把返回的 Observable 按事件帧格式写出去(data / id / type / retry 四个字段)。适合"事件结构规整"的场景;但它不处理背压,也拿不到最细的写控制,所以要么用它、要么走原始响应,别两套混着写。

顺带一句 Guard 里的限流:位置是对的,但 @nestjs/throttler 默认把计数放在进程内存里,多实例部署等于每个实例各限一份。要么换成 Redis 存储,要么统一收口到网关。

如果吞吐是真需求,可以切到 Fastify 适配器——Nest 官方支持,切换后仍保留 DI 和模块系统。这是"要 Nest 的结构 + 要 Fastify 的性能"的标准做法。

5. 什么时候不该用​

说清楚边界比说优点重要:

小服务不该用。 三个接口的服务上 Nest,结构是纯负担。三个 endpoint 不需要模块系统。

serverless 要谨慎。 DI 容器和模块初始化带来真实的冷启动开销。在 Lambda 这类按冷启动计费的环境里,这个代价会被放大。已有对比结论普遍认为:serverless 场景更适合 Express/Fastify/Hono 这类轻量方案。

BFF 通常是过度设计。 见《BFF 该聚合,不该写业务》,BFF 应该薄。Nest 的全套结构在 BFF 上通常没必要——除非你的 BFF 已经长成了一个有几十个接口的层。

原型阶段。 需要快速验证想法时,强制的结构会拖慢速度。

判断标准很朴素:团队人数 × 代码预期寿命。几个人、要活几年的服务,Nest 划算;一个人、三个接口,不划算。

6. 和 FastAPI 怎么选​

这一条其实就是《AI 后端选 Python 还是 Node》的延伸:

如果核心是检索与编排——Python 生态领先半年以上,Nest 补不回这个差距。

如果核心是给前端提供 AI 接口、且团队是 TypeScript——Nest 是 Node 侧最像"正经后端框架"的选择,尤其当服务会变大时。

两者都有的分层架构里——Nest 常常承担 BFF/网关那一层,而不是推理层。它的结构优势在"聚合多个下游、统一鉴权与限流"这件事上正好用得上。

7. AI 相关的模块怎么划​

一个实用的模块划分:

LlmModule        // provider 抽象与实现(可替换、可降级)
RetrievalModule // 检索服务(向量库、过滤、重排)
PromptModule // 提示词模板与版本
ChatModule // 会话编排(串起上面三个)
ObservabilityModule // trace、用量、审计(全局)

其中 PromptModule 值得单独拎出来。把提示词当代码管理(版本化、可测试、可 review)是 AI 服务里最重要的一条工程纪律之一,而模块化为它提供了一个自然的位置。

ObservabilityModule 应该是全局的,不要只在 ChatModule 里埋点——否则将来加了第二个 AI 功能,又要重做一遍。

8. 一个常见错误:把编排逻辑写进 Controller​

Nest 的结构容易让人把业务逻辑堆在 Controller 里(因为它就在那里,方便)。

AI 服务里这会特别痛:Controller 里出现"先检索再拼提示词再调模型再解析结果",意味着这段逻辑无法被单独测试、无法被第二个接口复用、也无法换实现。

正确的位置是 Service 层,Controller 只做参数校验与响应组装。

判断方法:Controller 里如果出现任何"顺序"(先做什么后做什么),那就是编排逻辑放错了地方。

9. 清单​

  • 确认规模值得:团队 ≥5 人 / 接口数会增长 / 要活几年
  • LLM provider 做成可注入接口,有测试实现
  • 鉴权、限流、预算放在 Guard
  • 日志、耗时、token 上报、trace 注入放在 Interceptor
  • 流式路由排除了会缓冲的中间件
  • 编排逻辑在 Service,不在 Controller
  • 提示词单独成模块,版本化
  • 吞吐是瓶颈时切 Fastify 适配器
  • serverless 场景重新评估是否值得

10. 一句话​

Nest.js 不解决 AI 的任何一个技术问题。它解决的是"五个工程师一起写这个服务,一年后代码还能不能看懂"。

这个问题在小项目里不存在,在真项目里迟早会出现。它的价值就在这里——不多,也不少。