AI应用分层架构与扩展-从单机到企业级
一个 AI 应用从本地运行到支撑真实用户,不是简单地把代码搬上服务器。它面临的问题和传统 Web 应用有根本性的不同:LLM 调用平均需要 5-15 秒,每次调用花费真金白银,输出结果具有不确定性。这三个特点决定了 AI 应用的架构设计必须从一开始就考虑分层——不是为了炫耀复杂度,而是为了生存。
AI应用分层架构与扩展:从单机到企业级
一个 AI 应用从本地运行到支撑真实用户,不是简单地把代码搬上服务器。它面临的问题和传统 Web 应用有根本性的不同:LLM 调用平均需要 5-15 秒,每次调用花费真金白银,输出结果具有不确定性。这三个特点决定了 AI 应用的架构设计必须从一开始就考虑分层——不是为了炫耀复杂度,而是为了生存。
1.1 为什么 AI 应用需要分层架构
AI应用架构演进——从单机、微服务、分布式到企业级的四阶段架构演进对比
1.1.1 和传统 Web 应用的本质区别
传统 Web 应用的请求响应链条大致是:收到请求 → 查数据库 → 返回结果。整个过程 50-200ms,数据库查询失败可以立刻知道,结果是确定的。
AI 应用的请求链条是:收到请求 → 调用 LLM(等 5-15 秒)→ 解析模型输出(可能格式不对)→ 可能需要再调用 LLM 补充信息 → 返回结果。这中间有三个关键差异:
延迟高: 一次 GPT-4o 调用平均耗时 5-20 秒,而一次数据库查询平均 5-50 毫秒。前者是后者的 100-1000 倍。如果用传统同步阻塞模型处理这种调用,10 个并发请求就会把整个服务打满。
成本高: LLM API 按 Token 计费。每次调用都有真实成本,重复调用会线性累积费用。一个查询被重复处理 100 次,成本就乘以 100。缓存不是优化,而是生存必需品。
输出不确定: 同样的 Prompt,两次调用返回的格式可能不同。生产系统必须能处理这种不确定性,而不是假设模型总是按预期返回结构化 JSON。
这三个特点都指向同一个结论:AI 应用需要把不同职责明确分开,每一层只做自己擅长的事。
1.1.2 不分层的代价
假设你有一个 RAG 问答服务,用户问一个问题,服务的流程是:查向量数据库 → 调用 LLM 生成回答 → 返回结果。这个流程在单机、单线程下工作得很好。
现在来了 20 个并发用户,每个请求都在等待 LLM 响应(平均 12 秒)。因为没有异步处理,第 20 个用户的请求要等前 19 个都完成。他实际等待时间是 12 × 19 = 228 秒,将近 4 分钟。这种情况在生产环境中不是偶发问题,而是日常状态。
更严重的是:如果其中一次 LLM 调用超时(30 秒),持有这个连接的线程就被堵住 30 秒。如果同时有 10 个这样的超时请求,服务的所有处理能力都被耗尽,其余正常请求也无法被处理。这就是不分层的系统在 AI 应用场景下失控的典型模式。
1.2 AI 应用的四层架构模型
AI 应用的生产架构可以清晰地划分为四层,每层职责独立,可以单独扩展。
1.2.1 接入层:流量的入口和守卫
职责: 接收外部请求,分发流量,防止系统被打垮。
典型组件: Nginx 或云厂商的 ALB(应用负载均衡)做流量分发,Rate Limiting 中间件(如 SlowAPI)控制每个用户的请求频率,HTTPS 终止和证书管理。
与 Java 微服务的对应: 相当于 Spring Cloud Gateway 或 Nginx + Spring Security 的组合。Java 开发者通常熟悉这层,概念完全一致,只是工具名称不同。
AI 应用的特殊要求: 因为 LLM 响应时间长,接入层必须支持 SSE(Server-Sent Events,服务器推送事件)或 WebSocket,允许长连接保持。普通的 HTTP 30 秒超时配置会把正常的 LLM 调用当作故障切断。
1.2.2 服务层:业务逻辑的居所
职责: 处理业务逻辑,编排 Agent 流程,调用 AI 能力层。
典型组件: FastAPI 应用实例(异步处理),LangGraph / LangChain 做 Agent 编排,Pydantic 做输入输出验证。
与 Java 微服务的对应: 相当于 Spring Boot 的 Controller + Service 层。FastAPI 的依赖注入(Depends)和 Spring 的 @Autowired 解决的是同一个问题。
AI 应用的特殊要求: 服务层本身不应该有长时间阻塞操作。LLM 调用必须用 async/await 异步处理,长时间运行的 Agent 任务(超过 30 秒)必须推到消息队列(数据层)异步执行。
1.2.3 AI 能力层:智能能力的集中管理
职责: 统一管理所有 AI 相关调用,包括重试、限流、成本监控。
典型组件: LLM 调用客户端(OpenAI SDK / Anthropic SDK)配合重试和熔断逻辑,Embedding 服务(可以是调用 API 或本地部署的模型),向量检索引擎。
与 Java 微服务的对应: 相当于一个专门的"外部服务调用层",类似 Feign Client 的封装,但带有 AI 特有的成本和延迟管理。
为什么要单独成层: LLM 调用的逻辑(重试、限流感知、模型降级、成本统计)如果散落在业务逻辑里,改起来会牵一发动全身。单独抽出来,服务层只需要调用 llm_pool.call(prompt),不需要关心底层细节。
1.2.4 数据层:状态和异步任务的基础
职责: 持久化数据,提供缓存,管理异步任务队列。
典型组件:
- 向量数据库(Chroma、Milvus、Pinecone):存储和检索 Embedding
- 关系数据库(PostgreSQL):存储用户数据、对话历史、系统配置
- Redis:语义缓存、会话状态、任务状态跟踪
- 消息队列(Celery + Redis 或 RabbitMQ):处理耗时 Agent 任务
与 Java 微服务的对应: 关系数据库和 Redis 的概念完全一致。向量数据库是 AI 应用新增的,可以理解为一种特殊的存储,专门用于"相似度查询"而非精确匹配查询。
1.3 从 10 到 10000:扩展的三个阶段
AI 应用的扩展不需要一步到位。正确的做法是从简单开始,在实际的瓶颈出现时才升级架构。
1.3.1 阶段一:10-100 并发,单机就够
适用阶段: 早期产品、内部工具、MVP 验证期。
架构: 单台服务器,单个 FastAPI 进程(多 worker),asyncio 异步处理并发,SQLite 或单机 PostgreSQL,Chroma 作为本地向量数据库。
为什么够用: asyncio 的事件循环可以同时等待数百个 IO 操作(包括 LLM 调用)。100 个并发请求同时在等 LLM 响应,对于 asyncio 来说只是 100 个 await,不是 100 个线程。一台 4 核 16GB 的服务器,配合合理的 asyncio 设置,可以轻松支撑 100 个并发 LLM 请求。
触发升级的信号:
- 服务器 CPU 持续超过 80%
- P99 延迟(99% 请求的最大延迟)超过用户可接受范围(通常 30 秒对话型应用)
- LLM API 账号频繁触发 429 限流,但业务量还有增长空间
- 服务重启导致用户会话状态丢失,开始影响用户体验
成本参考: 单台 4 核 16GB 云服务器,大约 $50-100/月。LLM API 费用按实际用量计。
1.3.2 阶段二:100-1000 并发,水平扩展
适用阶段: 产品上线、用户增长期、需要高可用保障。
架构演进:
- FastAPI 应用水平扩展为 2-5 个实例(Docker + K8s 或 Docker Compose)
- Redis 从"可选优化"变成"必须有":作为多实例间的共享缓存和会话状态存储
- Celery 异步任务队列处理耗时超过 30 秒的 Agent 任务
- PostgreSQL 升级为主从复制,读写分离
- 向量数据库从本地 Chroma 迁移到 Milvus 或托管服务(Pinecone)
关键改动的原因: 多个 FastAPI 实例运行时,每个实例有自己的内存。如果用户第一次请求落在实例 1,第二次落在实例 2,没有共享存储,对话历史就丢失了。Redis 作为共享状态存储解决了这个问题。这和 Java 微服务从单节点扩展到集群时引入 Redis Session 的原因完全一样。
触发升级的信号:
- 单台服务器内存超过 80%,无法再加更多 FastAPI worker
- LLM 长任务(如文档分析、批量处理)导致 HTTP 超时投诉
- 数据库连接数达到上限,开始报 "too many connections"
成本变化: 从 $50-100/月增加到 $300-800/月(多台服务器 + 托管 Redis + 向量数据库服务)。
1.3.3 阶段三:1000+ 并发,专项优化
适用阶段: 大规模商业产品,需要精细化成本控制。
架构演进:
- LLM 调用池:统一管理多个 API Key,实现跨账号的请求分发,突破单账号的速率限制
- 向量数据库集群:Milvus 集群或多副本 Pinecone,支持数亿级向量的检索
- CDN 缓存 Embedding:把高频查询的 Embedding 结果缓存到 CDN 边缘节点,降低计算成本
- 语义缓存(Semantic Cache):相似问题复用相同回答,命中率达到 30-50% 时成本大幅下降
- 模型分级:简单问题用便宜的 GPT-4o-mini,复杂问题才用 GPT-4o,成本差 10 倍
关键收益: LLM API 的速率限制(Rate Limit)是单账号级别的,一个 OpenAI 账号每分钟可能只有 60 个请求。用多个账号的调用池,理论上可以线性叠加吞吐量。Semantic Cache 命中一次节省的成本是一次完整 LLM 调用的费用,在高并发下这个数字相当可观。
1.4 AI 应用特有的扩展瓶颈
传统 Web 应用的扩展瓶颈通常是 CPU、内存、数据库连接数。AI 应用有额外的、更难绕过的瓶颈。
1.4.1 LLM API 的速率限制:不是你的服务器慢
OpenAI、Anthropic 等 LLM 提供商对每个账号都有严格的速率限制,通常以 RPM(每分钟请求数)和 TPM(每分钟 Token 数)衡量。
一个典型的 OpenAI Tier-2 账号:RPM 上限 5000,TPM 上限 2,000,000。看起来很多,但如果你的应用每次对话要调用 3 次 LLM,每次平均输出 500 Token,那么 2000 个并发用户就会消耗掉全部 TPM 配额。
这个瓶颈和你的服务器没有关系,加机器也没用。解决方案是:多账号轮询分发请求、在请求层面排队控制流量、积极使用 Semantic Cache 减少实际 API 调用次数。
1.4.2 向量检索的内存瓶颈:索引必须在内存里
向量数据库的检索性能高度依赖内存——向量索引(HNSW、IVF 等索引结构)必须完整加载到内存才能达到毫秒级检索速度。如果内存不足,系统会频繁进行磁盘 IO,检索延迟从 10ms 可能劣化到 1-5 秒。
1 亿条 768 维 float32 向量的存储大小约为 288GB(1亿 × 768 × 4 字节)。这意味着向量数据库的机器需要大内存,无法像普通应用一样随意选择实例规格。
监控指标:观察向量数据库节点的内存使用率,以及检索操作的 P99 延迟。如果发现内存使用率超过 70% 且延迟开始劣化,需要提前扩容,不能等到 OOM。
1.4.3 Embedding 的计算成本:能缓存的要缓存
每次用户输入都需要先转换为向量(Embedding)才能进行相似度检索。如果用 OpenAI 的 text-embedding-3-small,每 1000 Token 收费 $0.02(以官方文档为准,实际价格请查阅最新定价)。
对于搜索类应用,同一个问题可能被不同用户反复问起。如果每次都重新计算 Embedding,成本线性累积。正确做法是把 Embedding 结果缓存到 Redis,以原始文本为 key,缓存期可以设置较长(数天到数周)——同样的文本 Embedding 结果不会变化。
Embedding 缓存命中率是一个值得监控的指标。对于知识库检索类应用,查询分布通常遵循长尾规律:20% 的问题贡献 80% 的查询量,这 20% 的问题是最值得缓存的。
1.4.4 上下文窗口的内存占用:长对话的内存管理
多轮对话的每一轮都需要携带完整的历史上下文发给 LLM。10 轮对话,每轮平均 1000 Token,累积的上下文就是 10,000 Token。随着对话继续,Token 数量线性增长,LLM 调用成本也线性增长。
更严重的是内存问题:如果把对话历史存在服务实例内存里,每个活跃会话都占用几十 KB 到几 MB 不等的内存,大量并发用户会耗尽服务实例的内存。
生产系统的正确做法是:对话历史存入 Redis(设置 TTL 自动清理过期会话),服务层每次请求时按需读取,超过一定长度的对话历史进行截断或摘要压缩。
1.5 与 Java 微服务架构的对比
| 维度 | Java 微服务 | AI 应用 |
|---|---|---|
| 服务间通信 | gRPC/REST,延迟 1-10ms | LLM API 调用(HTTP),延迟 2000-20000ms,慢 100-2000 倍 |
| 状态管理 | 数据库/Session(确定性) | 对话历史/Agent 状态(有时效性,需主动清理) |
| 扩展瓶颈 | CPU / 内存 / 数据库连接数 | LLM API 速率限制 / Token 成本 / 向量索引内存 |
| 故障隔离 | 熔断器 / 服务降级(Hystrix/Resilience4j) | 模型降级(主模型 → 备用模型)/ 缓存兜底 / 本地模型备用 |
| 性能优化 | 连接池 / SQL 优化 / JVM 调优 | Semantic Cache / Token 压缩 / 模型分级调用 |
| 成本模型 | 固定基础设施成本,随规模变化平缓 | LLM API 成本随用量线性增长,架构决策直接影响账单 |
| 正确性判断 | 单元测试覆盖(结果确定) | 评估框架(结果概率性,需 LLM-as-Judge) |
Java 开发者最需要适应的一个心智转换:Java 微服务的性能优化主要是让代码跑得更快(减少 CPU 开销、优化 SQL);AI 应用的性能优化主要是让 LLM 调用更少(缓存、复用、模型降级)。优化的目标从"减少计算时间"变成了"减少 API 调用次数"。
1.6 架构决策的三个原则
1.6.1 原则一:不要过早优化
路径 A(应用开发路径)学完后,单机部署的 FastAPI + asyncio 可以支撑 10-100 个并发用户。对于大多数 AI 产品的早期阶段,这已经够了。
过早引入 Kubernetes、分布式向量数据库、消息队列,会带来巨大的运维成本和复杂度。不要在还没有 100 个活跃用户的产品上搭建 1000 并发的架构。
判断标准: 当你有真实的数据(用户数、并发数、延迟指标)表明当前架构是瓶颈时,才做升级。
1.6.2 原则二:瓶颈在哪里扩展哪里
AI 应用的瓶颈位置和传统应用不同,不能凭直觉判断。必须先建立监控,再根据监控数据决定扩展方向。
常见的错误判断:以为服务慢是因为服务器不够强,加了 CPU 发现没用——真正的瓶颈是 LLM API 的速率限制。或者以为内存不够,加了内存发现没用——真正的瓶颈是 LLM 调用延迟,不是内存。
本章第 11 篇(监控告警)提供了建立 AI 特有监控指标的方法,在扩展决策之前先把监控建立起来。
1.6.3 原则三:AI 应用的成本是线性的
传统应用的基础设施成本通常是阶梯式的(买固定规格的服务器),弹性空间有限。AI 应用的 LLM API 成本是严格线性的:调用量翻倍,账单翻倍,没有阶梯折扣(超大规模账户可以谈合同价)。
这意味着架构设计直接影响成本。一个没有 Semantic Cache 的系统,和一个命中率 40% 的 Semantic Cache 系统,在相同用户量下,成本可以相差 1.67 倍。一个不做 Token 压缩的多轮对话系统,随着对话历史积累,每次调用的成本会持续增长。
架构师在 AI 应用里不只是技术决策者,也是成本控制者。
1.7 水平扩展 vs 垂直扩展:怎么做决策
这是一个 AI 应用扩展中经常被问到的问题。
垂直扩展(Scale Up):换更大的机器,加 CPU、内存、更快的 GPU。简单,不需要改代码,但有上限(单机性能有天花板),费用线性增长。
水平扩展(Scale Out):加更多台机器,跑更多实例。更灵活,理论上无上限,但需要代码支持无状态(多实例共享同一个请求)。
对于 AI 应用,决策框架是这样的:
| 瓶颈来源 | 推荐方案 | 原因 |
|---|---|---|
| CPU 计算(本地模型推理) | 先垂直(换更好的 GPU),再水平 | 推理任务 GPU 利用率更重要 |
| 并发请求数 | 水平扩展 FastAPI 实例 | asyncio 已经充分利用单机,加机器最有效 |
| 内存(向量索引) | 先垂直(加内存到索引能装下),再考虑分片 | 向量索引必须完整在内存,分片会降低检索质量 |
| LLM API 速率限制 | 多账号轮询 + Semantic Cache | 加机器不解决 API 速率问题 |
| 长任务(Agent 执行) | Celery 异步队列 + Worker 水平扩展 | 长任务应该从 HTTP 请求中分离出来 |
一个简单的优先级原则:先把单机利用率提高到 70-80%(异步、缓存、连接池),再考虑加机器;先加机器(水平扩展),再换更贵的机器(垂直扩展)。多数情况下,水平扩展的性价比更高。
1.8 与本章其他文章的关系
本文提供了 AI 应用生产化架构的全局视图,是本章各篇文章的概念框架。
接入层对应的文章: 第 2 篇(SSE 流式输出)解决接入层的长连接问题;第 6 篇(错误处理与重试)解决接入层到服务层的韧性问题。
AI 能力层对应的文章: 第 5 篇(Token 成本控制)和第 6 篇(错误处理与重试)专门处理 LLM 调用池的成本和稳定性;第 3 篇(LangSmith/Langfuse)提供 AI 能力层的可观测性。
数据层对应的文章: 第 9 篇(Celery 异步任务队列)详细展开消息队列的实现;第 10 篇(Redis 缓存策略)详细展开语义缓存和 Embedding 缓存的实现。
整体可观测性: 第 11 篇(监控告警)是验证本文扩展决策的工具——只有建立了完善的监控,才能准确判断瓶颈在哪一层、什么时候需要升级哪一层。
部署工具: 第 7 篇(Docker 部署)和第 12 篇(Kubernetes 部署)是实现阶段一到阶段三扩展的基础设施工具,对应本文"三个扩展阶段"的具体落地方式。
理解了分层模型,再去看每一篇具体实现,就能清楚地知道自己在解决哪一层的问题,而不是把所有技术细节堆在脑子里找不到落点。