1. 为什么说AI应用必须拆微服务——先想清楚再动手
先说一个我最近的真实经历。上个月帮一个创业团队做架构评审,他们做了一个企业内部的知识库问答系统,技术栈很时髦:LangChain + FastAPI + PostgreSQL + OpenAI兼容接口,代码全在一个单体仓库里,部署也是单机Docker Compose。演示的时候一切正常,但一上生产就乱了套:早晨九点员工开始集中提问,模型推理把CPU和内存吃满,普通查询接口跟着超时;销售团队在群里反馈“系统转圈转了半分钟”;后来接入一个新的向量模型做灰度,负责人告诉我他们需要把整个服务重启一遍。那一刻我才意识到,很多人对“AI原生应用”的理解还停留在“调模型API”的层面,根本没意识到这类应用在架构上和传统CRUD系统有着本质差别。
所以这篇东西,我想认真聊聊“微服务架构下的AI原生应用开发”。这堆术语大家听得耳朵起茧,但真正能把它们落到工程上、并且把理论和实践对齐的人,其实不多。更重要的是,我会直接给出可以抄作业的架构拆解、技术选型和踩坑记录。
先说清楚一个概念边界。所谓的AI原生应用,不是指“用到了AI功能”的应用。你在后台接一个翻译API、加一个图像识别接口,那叫“有AI功能的应用”。真正的AI原生应用,是指大模型(或各类AI能力)作为整个系统的核心逻辑引擎,应用的流程编排、上下文管理、工具调用、动态决策,全部围绕模型能力来构建。典型例子包括:RAG知识库问答系统、Agent工作流应用、代码辅助助手、自动化数据分析助手。这种应用有一个共同特征——它的主流程不是人写死的if/else,而是模型在运行时动态决定的。这就给架构带来了完全不一样的挑战。
为什么这类应用特别需要微服务架构?我总结了四个致命问题,全是单体架构在AI场景下必然踩中的坑。
慢依赖阻塞一切。 大模型推理的延迟不是数据库查询的毫秒级,而是秒级甚至几十秒级。如果应用是单体,一次模型调用超时,占用的可能是Web服务器的整个工作线程,而该线程本来还要处理其他普通请求。一个慢动作拖垮整个系统的“队头阻塞”,在单体AI应用里几乎无法避免。
资源隔离缺失。 CPU密集的推理任务和IO密集的Web查询任务放在同一个进程里,调度器会在两者之间反复横跳。模型推理需要的是稳定的算力,普通查询需要的是快速响应。混部在一起,两边的SLO(服务等级目标)都保不住。更别提GPU资源了——你有GPU的推理服务和普通无状态API混在一起,调度、扩展、故障定位都是灾难。
迭代节奏冲突。 业务功能每两天发一次版本,模型服务每周要换一次prompt和推理参数。把两者揉在同一个服务里,发布一次就要全量回归、全量重启,模型热切换做不了,灰度也做不了。开发体验极其痛苦。
故障爆炸半径太大。 单体架构下,模型供应商的API抖动一次,整个系统的所有接口全部跟着超时;向量数据库慢查询几分钟,整个应用不可用。故障面完全失控。
听到这里肯定有人反问:那是不是所有AI应用都必须上微服务?我最讨厌的技术观点就是“无脑微服务”。一个给内部用的demo、一个百人规模的验证项目,单体完全够用,硬拆微服务只会增加负担。但如果你做的是面向生产环境的AI原生应用,尤其是要对接多个模型、要持续迭代prompt、要处理RAG检索、要支撑多端入口,那微服务拆分就不是选择题,而是迟早要过的坎。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心架构拆解:一个AI请求是怎么穿过六个服务的
我先把一套经过实战验证的AI原生应用微服务架构画出来(完整代码骨架后面给),然后带你深入理解每个模块存在的理由。技术栈我选了比较成熟的组合:Java生态的Spring AI做模型接入,Python生态的应用才用LangChain或直接自研编排,这取决于团队背景,但架构思想是通用的。
一套完整的AI原生应用,至少包含六个基础服务组件:
| 服务模块 | 核心职责 | 典型技术 |
|---|---|---|
| 接入网关 | 统一入口、鉴权、流式转发、限流 | Spring Cloud Gateway / APISIX |
| 编排服务 | 对话状态管理、工具调用、Agent逻辑 | Spring AI / LangChain4j / 自研 |
| 模型网关 | 多模型路由、API密钥管理、降级容错 | LiteLLM / Bifrost / 自研 |
| 推理服务 | 大模型推理、微调模型部署、GPU资源池化 | vLLM / SGLang / Triton |
| 记忆与向量服务 | 会话历史、语义检索、向量持久化 | Milvus / pgvector / Redis |
| RAG管线 | 文档解析、分块、索引、召回重排 | 自研 + LangChain |
这里有个很重要的认知:并不是每个服务都必须独立部署,但逻辑边界一定要清晰。小团队可以把编排服务和接入网关合在一起部署,模型网关和推理服务可以合并,但代码层面必须分成独立模块。这个习惯能让你在项目规模变大时,不用推倒重来。
2.1 接入网关:流式转发的第一道关
AI原生应用的网关和传统API网关最大的不同,在于它必须支持SSE(Server-Sent Events)流式转发。大模型的结果是一块一块“吐”出来的,用户体验上也要一个字一个字地显出来,这时候网关就不能做传统的“拿到完整响应再返回”,而要支持长连接、流式数据中转、半关闭连接等操作。
我在Spring Cloud Gateway里做过一个坑:默认情况下Gateway的Netty对响应缓冲有限制,模型返回流一旦超过阈值就被强制断开。后来在路由配置里显式关闭了响应缓冲,并且把超时时间从默认的5秒调整到适合流式场景的300秒。这块后面我会把完整配置贴出来。
网关还要承担路由降级职责。比如你同时接了GPT、Claude、国产模型三家,GPT超时了,网关层要能自动把流量切到Claude,而不是让底层报错一路抛给前端。
2.2 编排服务:AI大脑的“操作系统”
编排服务是整个架构中最像“大脑”的部分。它接收用户输入,维护整个对话的上下文窗口,判断当前该走哪个工具(比如查数据库还是搜知识库),循环调用模型直到得出最终答案。
这个模块的设计要点是:状态与执行分离。对话状态(历史消息、临时变量、工具调用栈)放在Redis或专门的StateStore里,编排服务本身保持无状态,这样才能水平扩展。否则你一旦部署多副本,用户的会话就不知道该路由到哪台机器,体验直接崩塌。
这个模块的API设计我建议遵循一个模式:每次请求带上conversation_id和user_id,编排服务先从存储层拉取历史上下文,组装成模型需要的prompt结构,调用模型网关拿到结果,再把结果写回状态存储。看起来简单,但做成微服务之后,这里涉及到的缓存失效、并发写冲突、上下文截断策略,全都是细节。
2.3 模型网关:统一所有模型调用的适配层
模型网关的价值很多人低估了,它本质上是一个“模型调用的路由器”。没有它,你的编排服务会直接和各个模型供应商耦合:OpenAI一个sdk、Claude一个sdk、国产模型一个sdk,prompt格式转换、错误重试、成本统计全部散落在业务代码里,维护起来痛不欲生。
做了模型网关之后,编排服务只需要对着一个标准接口发请求,由网关负责:
- 多模型统一适配(统一请求/响应格式)
- 路由策略(按用户、按成本、按优先级分发到不同模型)
- 自动重试与降级(模型A超时自动切模型B)
- Token用量统计与成本预算控制
- Prompt安全过滤与脱敏
这块我强烈建议不管技术栈多旧,都要独立成一个服务。它能把模型相关的“脏活累活”全部隔离掉,让你的编排服务只专注业务逻辑。
2.4 推理服务:最好独立部署,别和业务进程混在一起
模型推理服务是唯一一个“必须”有GPU资源的模块。如果用云服务商的托管API,这一层可以省掉;但如果你有私有化部署需求,或者用开源模型(Qwen、Llama等)做定制,就需要自己部署推理服务。
我见过很多团队一开始图省事,把模型直接加载在业务进程里。Tomcat里挂了个深度学习模型,结果每次GC(垃圾回收)都让推理响应延迟飙升两倍。后来规规矩矩拆出来,用vLLM部署推理服务,开了continuous batching,吞吐提升了一个数量级。这个教训很值钱:模型推理进程做GC时,整个推理都会卡住,这业务进程完全没办共享。
推理服务要支持的功能包括:模型热加载与热替换(灰度新模型不需要重启业务)、请求排队与优先级调度、批处理优化。目前vLLM在这块做得比较成熟,SGLang在长文本场景有优势,可按需选择。
2.5 记忆与向量服务:RAG的灵魂所在
AI原生应用和传统系统最大的差别,就是它需要“记忆”。聊到一半,用户说“我刚才问的那个问题”,你的系统要知道用户指的是什么。通常我们把短期记忆存在Redis(带TTL),长期记忆和文档知识沉淀到向量数据库里。
向量数据库选型,不用盲目追求“分布式大数据”。早期百万级向量规模,用pgvector配合PostgreSQL索引完全够用,省去运维一个大数据的成本;向量量级真到千万级以上,再上Milvus也不迟。这个演进路线,能帮你把技术债降到最低。
RAG的检索质量其实和你用哪个向量库关系不大,真正的瓶颈在文本分块、Embedding模型选择、重排策略。这三个点不做好,换个再牛的向量库也就那样。
2.6 完整链路走一遍
我们拿一个典型的“企业知识库问答系统”来走一遍完整流程(这正好也是很多团队第一个AI原生应用):
- 用户在Web端输入:“上季度的销售数据异常,帮我分析可能原因”
- 请求通过接入网关,网关鉴权通过,SSE流式连接建立
- 网关把请求转发给编排服务,带上
conversation_id - 编排服务从Redis拉取该用户最近10轮对话历史,拼入上下文
- 编排服务判断这个问题需要检索知识库,调用RAG管线的检索接口,拿到相关文档片段
- 编排服务把“历史对话 + 检索文档 + 用户问题”组装成prompt,发给模型网关
- 模型网关按路由策略将请求转发给GPT-4(或内部微调模型)
- 模型网关开始收到流式token,一边转发给编排服务,一边累计token用量
- 编排服务实时把token推给接入网关,网关转推给浏览器,实现打字机效果
- 生成完成,编排服务把完整回答写入Redis记忆存储,网关把SSE流关闭
这条链路看起来不长,但每个环节都是独立的服务,任何一环出问题都不会拖垮整体。这就是微服务架构带给AI应用的核心价值——隔离故障、独立扩展、独立迭代。
3. 从零开始落地:一整套可运行的微服务骨架
理论说得再多,不如跑起来一个能用的骨架。这一节给出我自己在项目中使用的技术选型和关键实现,你可以直接照着搭,也可以按团队偏好替换组件。
3.1 技术选型
我的标准是:团队上手成本低、生态成熟、社区资料多。以Java为业务主语言,因为大部分搞微服务架构的团队都熟。模型推理层用Python,因为模型生态绕不开Python。
| 层次 | 选型 | 理由 |
|---|---|---|
| 网关 | Spring Cloud Gateway | Java生态无缝集成,流式转发改造成本低 |
| 编排服务 | Spring Boot 3 + Spring AI | Spring AI统一了模型调用接口,支持流式 |
| 模型网关 | 自研(基于Spring WebFlux) | 轻量,避免引入重量级框架 |
| 推理服务 | vLLM(自托管开源模型) | 吞吐率高,处理流式输出原生友好 |
| 状态存储 | Redis | 会话记忆、临时状态、TTL管理都方便 |
| 向量存储 | pgvector(前期) | 一套PostgreSQL搞定结构化+向量,少维护一个组件 |
| 消息队列 | RabbitMQ / Kafka | 异步任务:文档解析、索引构建、事件通知 |
| 部署 | Docker Compose(开发)+ K8s(生产) | 简化开发,生产留弹性扩展能力 |
这组选型最大的好处是:一个会Java的团队能在两周内把骨架跑起来,后期扩容迁移也有清晰路径,不会一开始就背上过重的架构包袱。
3.2 项目模块划分
仓库结构上我建议用Maven多模块:
bash复制ai-platform/
├── gateway/ # 接入网关
├── orchestration/ # 编排服务
├── model-gateway/ # 模型网关
├── rag-service/ # RAG检索服务
├── vector-store/ # 向量存储初始化与迁移
├── common/ # 公共库:DTO、工具类、异常定义
└── docker-compose/ # 本地开发环境编排
这里有个容易踩的坑:公共库不要放太多逻辑,只放纯POJO、枚举、常量、工具函数。一旦你在common里塞了跟具体服务绑定的东西,后面每次改动都会引发全链路重新编译和部署,那个痛苦你真的不想体验。
3.3 编排服务关键实现
编排服务是整个系统的核心,我给出一个简化版的Agent调用逻辑,用的是Spring AI的ChatClient,并实现了和模型网关的流式对接。
java复制@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatClient chatClient;
private final ConversationMemory memory;
private final RagClient ragClient;
@PostMapping("/stream")
public Flux<String> streamChat(@RequestBody ChatRequest request) {
String conversationId = request.conversationId();
// 1. 拉取历史上下文
List<Message> history = memory.getHistory(conversationId);
// 2. RAG检索增强
List<Document> documents = ragClient.retrieve(request.question());
String augmentedPrompt = PromptBuilder.build(history, documents, request.question());
// 3. 调用模型网关,返回流式结果
return modelGateway.stream(conversationId, augmentedPrompt)
.doOnComplete(() -> memory.save(conversationId, request.question(),
lastAnswer));
}
}
这里有几个细节值得反复强调:
- 流式返回一定要用
Flux,不要自己开线程池去while循环读响应——背压处理会让你写到怀疑人生。 - 上下文管理千万不要一股脑把历史全塞进去。超过模型上下文窗口的部分要么截断,要么做摘要压缩。我通常做法是:保留最近3轮完整对话 + 把更早的对话做一次summary,既保语义又控长度。
- RAG检索的时机由编排规则决定。不是每个问题都需要检索知识库,先让模型判断“需不需要外部知识”,再决定是否调RAG。这一步能省大量token和时间。
3.4 模型网关实现要点
模型网关本质上就是一个WebFlux的转发服务,核心代码如下,关键是把上游模型服务的SSE字节流原样透传:
java复制@PostMapping(value = "/v1/chat/completions", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> proxy(@RequestBody ModelRequest request) {
return webClient.post()
.uri(modelRouter.route(request.model())) // 动态路由
.bodyValue(request)
.retrieve()
.bodyToFlux(String.class)
.map(data -> ServerSentEvent.builder(data).build())
.doOnComplete(() -> usageTracker.record(request, lastTokenCount));
}
模型网关的健壮性设计比功能还重要,我列四个核心项:
超时控制。 模型调用的P95延迟可能在10-30秒,超时不能设得太短,但也不能无限等。我的经验是:普通对话15秒超时,复杂推理任务60秒超时,超时后自动降级到备选模型。
自动重试。 重试要考虑幂等性。生成请求天然不具备幂等性(每次生成结果可能不同),所以重试只适合网络层错误(连接断开、5xx),不适合模型返回的业务级错误。
限流熔断。 模型供应商有配额限制,要客户端做滑动窗口限流;同时要做熔断,连续N次失败就打开断路器,直接切备选通道,不再等待超时。
Token预算管理。 每个请求设置max_tokens上限,每个用户每天有总预算,超了就拒绝调用或者降级到便宜模型。没有成本控制机制,AI原生应用上生产就是一个烧钱的无底洞。
3.5 本地部署与生产部署
开发环境用Docker Compose一键拉起所有依赖:
yaml复制version: '3.8'
services:
redis:
image: redis:7-alpine
ports: ["6379:6379"]
postgres:
image: pgvector/pgvector:pg16
ports: ["5432:5432"]
environment:
POSTGRES_USER: ai
POSTGRES_PASSWORD: ai123456
POSTGRES_DB: ai_platform
rabbitmq:
image: rabbitmq:3-management
ports: ["5672:5672", "15672:15672"]
生产环境建议部署到K8s,一个服务一个Deployment,每个服务单独配HPA(水平自动伸缩)。但如果你团队刚起步,K8s运维负担太重,先两台云主机用Docker Compose + 进程守护跑起来也是完全可以接受的。
4. 生产环境里最难啃的骨头:流式、冷启动与状态
架构搭起来不难,骨架跑通也快,但真正让你在生产环境里熬夜加班到怀疑人生的,永远是那几块硬骨头。我挑四个最常见的坑,每一个都是我真实踩过、并且陪别人一起踩过的。
4.1 流式响应在微服务链路的穿透
流式(Streaming)是AI原生应用体验的生命线。用户看到“一个字一个字蹦出来”的效果,会认为系统在“思考”;一旦是一个长久的空白等待,用户立刻会觉得系统死了。这块体验细节直接决定产品口碑。
说一个我在Gateway层踩过的深坑:Spring Cloud Gateway默认使用Netty,对响应做了缓冲。当模型网关返回一个长文本流,Gateway缓冲区的默认上限一满,连接就直接断了,前端看到的结果是“生成到一半突然消失”。排查了很久才发现要从两个方面解决:
- 关闭Gateway对路由的响应缓冲:
yaml复制spring:
cloud:
gateway:
httpclient:
response-timeout: 300s
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin
- 用WebFlux的
ServerSentEvent而不是传统的ResponseBodyEmitter,两种方式在背压处理上的行为有很大差异,WebFlux的背压语义更符合流式场景。
另外一个细节:每个微服务之间的调用都要透传“流式标记”,不要让中间层把整个流攒齐再往下传。业界管这个叫“字节流不落地”。模型网关收到一个token就立刻转发给编排服务,编排服务也立刻转发给接入网关,任何一环有缓冲,整条链路就回到了“等全部生成完再返回”的笨模式,体验瞬间回到解放前。
4.2 推理服务的冷启动与弹性伸缩
大模型的冷启动是K8s场景下的噩梦。一个7B参数的模型加载到GPU显存,光权重加载就要30秒到1分钟;一个更大规模的模型可能要几分钟。如果HPA检测到负载升高才扩容新副本,新副本启动到就绪的几分钟内,请求已经超时了一大批。
解决思路有两个:
保证最小常驻副本并预热。 永远保持至少1-2个推理副本预热在线上,永远不要让“扩出来的新副本”去承接瞬时峰值流量。启动时就加载好模型、建好connection pool,Ready探针做成真实的推理健康检查(发一个测试请求看是否返回),而不是简单的ping端口。
流量预测与提前扩容。 根据业务规律提前扩容,比如每天早晨9点前对推理服务扩容到平时的2倍;下午6点后再缩回来。这个朴素的办法在多数场景下比任何自适应算法都好使。
还有一个被很多人忽略的:推理服务要支持并发批处理。vLLM的continuous batching能把多个并发请求拼在一个batch里推理,GPU利用率大幅提升。用传统方式一个请求占用一个进程,GPU利用率可能不到20%,而vLLM可以轻松做到60%以上。
4.3 会话状态与幂等:微服务化后最容易翻车的点
单体应用时代,会话就是进程内的一个Map,随便放。拆微服务之后,会话状态必须外置(Redis/数据库),而且还要考虑分布式环境下的并发问题。
这里我踩过一个非常典型的坑:用户连续发送两条消息,编排服务的两个副本同时处理,都从Redis读出同一份历史上下文,各自组装prompt,然后各自把新消息写回Redis——后者覆盖了前者,用户的一条消息“凭空消失”了。
解决方法是使用Redis的乐观锁或原子操作来更新会话状态。给每个会话维护一个version字段,写回的时候带上版本号,CAS(Compare And Swap)校验失败就重读重试。这个机制看起来简单,但在多副本场景下是必须的。
幂等设计也要提前想清楚。用户在网络抖动时点了两次发送,你的系统可能会收到两条一样的请求。如果编排服务不做幂等控制,模型会被调用两次、产生两个回答、用户看到重复内容。建议每个请求生成一个request_id,在编排服务里用Redis做去重:相同request_id且相同内容的请求,直接返回第一次处理的结果或一个“处理中”的状态。
4.4 可观测性:没有prompt trace,排障就是大海捞针
传统微服务排查问题,看日志、看指标、看链路追踪就够了。AI原生应用多了一个维度:你得知道模型收到了什么prompt、返回了什么结果、消耗了多少token、模型调用花了多少时间。没有这些信息,用户说“答案不对”,你连从哪开始查都不知道。
我的做法是在编排服务埋点,把每次模型调用的事件单独记录:
- 请求ID、用户ID、会话ID
- 输入token数、输出token数、总延迟
- 最终prompt的完整内容(截断到一定长度,防止日志过大)
- 模型路由、是否命中缓存、是否降级
- 每次RAG检索命中的文档ID与得分
这些数据可以导出到Elasticsearch或ClickHouse里做分析。另外要时刻关注几个关键指标:
- 首token延迟(TTFT,Time To First Token):用户感知“系统有没有反应”的关键。理想值小于1秒。
- Token吞吐量:每秒生成的token数,决定流式体验是否流畅。
- 缓存命中率:如果语义缓存命中率低,说明缓存策略有问题,重复问题反复调用模型,浪费钱。
- 降级比率:模型A降级到模型B的比例。频繁降级说明主模型存在问题,需要检查供应商状态。
我见过很多团队上线AI应用后只盯着CPU、内存、接口QPS,完全不管这些AI专属指标。问题一出,两眼一抹黑,只能靠猜。这个习惯真的得早点改。
5. 从理论到实践的落地节奏:给不同阶段团队的迁移建议
最后说说落地节奏。网上很多微服务+AI的教程一上来就扔给你一张复杂的架构图,Nacos、Sentinel、SkyWalking、Istio一整套全上,搞得人不明觉厉,但真照着搭,十有八九死在半路上。我不推荐这么干。
我建议你按照以下节奏推进,每一步都有明确的“完成标准”:
5.1 第一阶段:先跑通单体,但要保持模块边界
不要一上来就拆微服务。先把AI应用的核心链路跑通,哪怕是一个单体项目,但代码结构上必须物理隔离模块:orchestration、rag、model-client、conversation-store。这就像装修前先拉好水电管线,后面拆墙不会弄得到处是线。这个阶段的完成标准是:一个端到端的AI应用能在本地跑通,并且你能清楚说出每个模块的职责边界。
5.2 第二阶段:把最容易“爆炸”的模块先拆出去
第一个要拆的,是模型调用相关逻辑——把它变成独立的模型网关服务。因为它变更是最频繁的(换模型、调路由、改prompt模板),同时故障率也是最高的(外部API不稳定)。拆出去之后,你立刻会感受到微服务化的第一个好处:模型供应商抖动,不再影响整个应用了。
第二个拆的是编排服务里的状态管理,把会话状态外置到Redis。这是为将来编排服务多副本部署做准备。完成标准:部署两个编排副本,用户连续对话,状态不丢失、不重复。
5.3 第三阶段:按需拆分,不要为了拆而拆
业务量上来之后,你自然会遇到瓶颈——RAG索引构建太耗时,就把它拆成独立的异步任务服务;推理吞吐不够,就部署独立的推理服务并用vLLM加速;多端接入场景复杂,再把网关层独立出来。每个服务的出现都应该有一个“为什么”的理由,而不是“因为微服务架构听起来高级”。
从经验来看,一个5-10人的小团队,在业务真正需要之前,最多拆到“1个网关 + 1个编排 + 1个模型网关 + 向量存储”就足够支撑到百万级用户了。再往下拆,就是在为组织规模和业务复杂度买单,而不是为技术上的虚荣心买单。
5.4 最后一个经验之谈
经历了从单体AI应用到微服务架构的完整过程,我想说一句实话:微服务解决的不是技术问题,而是组织协作和系统演进的问题。AI原生应用的模型能力迭代极其迅速,今天爆出一个新模型、明天多了一个Agent框架,如果你的架构不能让你在48小时内替换掉一个模型、独立升级一个模块,那你就会被技术更新的洪流拖垮。
架构没有标准答案,但有一条原则是通用的:AI原生应用的微服务架构,核心是让“变化的”(模型、prompt、编排逻辑)和“稳定的”(用户、数据、基础设施)解耦。牢牢抓住这个原则,你的每一步取舍都不会走偏。
