大模型应用上线后,最先暴露出来的往往不是模型能力,而是后端系统的承载方式:请求执行时间更长、单次推理成本更高、会话需要持续上下文,流式输出还会长时间占用连接。若仍按普通无状态 Web 接口处理,常见结果就是会话串线、重复执行、请求超时、队列堆积,甚至一次流量尖峰拖垮整个服务。
这类问题不能只靠增加 Web 实例解决。更稳妥的思路是把“接入、状态、调度、推理、结果”拆开,让每一层都明确自己的边界:请求先经过鉴权和限流,再读取状态与缓存;需要模型计算的任务进入队列,由 Worker 按可用算力消费;执行过程通过幂等键、确认机制和状态机保证可追踪,结果落库后再返回给用户。
先判断:你的 AI 请求属于哪一类
并不是所有请求都需要同一套架构。可以先按交互方式和资源占用做分类:
- 短交互请求:例如简单问答、分类或改写。通常要求较快返回,适合保留同步 API,但仍要设置超时、并发上限和取消机制。
- 流式对话:模型逐步返回内容,客户端体验较好,但连接存活时间更长。网关、负载均衡、应用服务器和客户端都要支持流式传输,不能只盯着模型接口的超时配置。
- 长任务:例如文档解析、批量摘要、知识库构建和离线生成。应采用“提交任务—返回 task_id—异步执行—查询或回调结果”的模式,避免占用用户请求连接。
- 高价值任务:例如订单生成、自动化操作和需要外部工具调用的 Agent。除了执行成功,还要记录输入、工具调用、重试次数和最终状态,方便审计与补偿。
分类的目的不是增加复杂度,而是避免把所有流量都直接推给模型服务。实时对话和批处理任务应该分开排队;可重试的模型调用和不可重复的外部副作用也应该分开处理。
一套可落地的分层架构
1. 接入层:先保护后端,再谈扩容
接入层负责鉴权、参数校验、租户配额、请求限流和路由。限流不应只按 QPS 设置,还要考虑并发中的 token 数、上下文长度、模型类型和预计执行时间。对于长任务,入口只负责校验并创建任务,不在 HTTP 请求中等待完整推理。
建议为每个请求生成全局唯一的 request_id,并让它贯穿网关日志、队列消息、模型调用和结果表。对于客户端可能重复提交的请求,再增加业务幂等键,例如 tenant_id + conversation_id + client_request_id。这样即使客户端重试,也不会无条件创建第二个任务。
2. 状态层:会话记录和任务状态分开
把完整对话只放在单个应用实例的内存中,是本地开发中很常见、线上却很脆弱的做法。多实例部署后,请求可能被分发到不同实例;实例重启也会使内存状态消失。
可以按访问频率和数据价值分层:
- 热状态:当前会话摘要、最近几轮消息、任务进度,可放在带 TTL 的分布式缓存中。
- 持久状态:完整消息、用户配置、审计记录和最终结果,写入数据库或对象存储。
- 派生状态:向量索引、统计信息和缓存结果,允许重建,但要有版本号标识来源数据。
并发更新时,不要简单地执行“读取—修改—写回”。至少应选择一种保护策略:按会话加分布式锁、使用数据库条件更新,或携带版本号进行乐观并发控制。更新失败时重新读取最新版本,而不是覆盖其他请求刚写入的上下文。
3. 队列层:用背压匹配推理能力
消息队列的价值是把入口流量与模型消费能力解耦,而不是让任务无限堆积。一个基本流程如下:
- API 校验请求并写入任务记录,状态为
queued。 - 发送带有
task_id、模型配置快照、输入引用和幂等键的消息。 - Worker 获取消息后,将任务原子地改为
running。 - 推理成功则保存结果并进入
succeeded;失败则记录错误并按策略重试。 - Worker 只有在结果可靠落地后才确认消息;超过重试上限的任务进入死信或人工处理队列。
Redis Streams 适合已经使用 Redis、希望减少基础设施数量的中小型系统,可以利用 consumer group 管理消费者和待确认消息;RabbitMQ 更适合需要明确路由、发布确认和消费确认的任务系统;Kafka 更适合高吞吐事件流、日志和可重放数据。选型时应看消息持久化、重试和顺序语义、运维能力以及已有技术栈,而不是只比较宣传中的吞吐数字。
队列必须配套背压:监控待处理任务数量、最老任务等待时间、消费速率和失败率。当积压超过业务可接受范围时,应降低入口速率、拒绝低优先级任务或提示用户稍后处理,而不是继续接收直到所有请求一起超时。
4. 缓存层:缓存确定性结果,不要盲目缓存对话
缓存可以减少重复推理,但生成式输出未必具有稳定、可复用的结果。设计缓存键时,应至少考虑模型版本、系统提示词版本、工具配置、输入内容和关键采样参数。模型或提示词升级后要改变版本标识,避免旧结果污染新逻辑。
比较实用的缓存分层包括:
- 本地短缓存:保存进程内的少量热点配置或只读数据,降低网络访问。
- 分布式缓存:保存会话摘要、模型配置、短期任务进度和幂等记录。
- 结果缓存:只用于可接受复用的确定性或近似确定性请求,并设置过期时间。
- 语义检索缓存:对相似问题复用结果时,必须增加相似度阈值、权限校验和内容版本校验。
缓存命中不能绕过权限和数据新鲜度检查。对带用户隐私、实时库存或外部系统状态的回答,宁可不缓存,也不要因为命中旧结果造成业务错误。
可靠投递的关键:至少一次并不等于只执行一次
分布式系统中,生产者发送成功、消费者执行成功、结果写入成功并不是同一个事件。网络故障可能让生产者不知道消息是否已被接收;消费者处理完成后若在确认前宕机,消息可能再次投递。因此工程上常见的是“至少一次投递”,应用必须接受重复消息的可能。
要把重复影响降到最低,可以采用以下组合:
- 每个任务使用不可变的
task_id,数据库对它建立唯一约束。 - 外部副作用调用使用幂等请求号,或先记录待执行意图,再执行补偿。
- 结果写入使用条件更新,例如只允许
running进入succeeded或failed。 - 重试采用指数退避,并区分可重试错误与参数错误、权限错误等永久失败。
- 记录原始错误、尝试次数、最后一次心跳和下一次重试时间。
Redis Streams 的待确认消息、RabbitMQ 的发布确认与消费确认,都只能解决消息链路的一部分问题;“业务结果已经持久化”仍需要由应用自身保证。不要把 ACK 当成业务事务的替代品。
用状态机管理长任务
当任务只有“成功/失败”两个字段时,排查中间异常会很困难。可以把状态控制为有限集合,例如:
created -> queued -> running -> succeeded
| -> retry_wait -> queued
| -> cancelled
-> failed状态迁移应由服务端统一执行,并记录时间、操作者或触发原因。需要特别处理三类边界:
- 超时:执行节点失联后,不能立刻认定任务未执行;应结合租约、心跳或执行凭证判断是否可以安全重试。
- 取消:取消请求只代表“不再继续”,不一定能立刻终止已经发给模型的调用,需要向 Worker 传播取消信号并清理后续结果。
- 迟到结果:旧任务完成后写回时,要检查任务版本和当前状态,避免覆盖新一轮请求的结果。
状态机的价值在于让每一种异常都有明确去向。它不负责提升模型质量,但能让业务知道任务在哪里、为什么失败、是否可以重试,以及谁可以进行人工补偿。
上线前的检查清单
先做最小闭环
个人项目或小团队不必一开始就部署多个复杂组件。可以先完成:一个 API 服务、一个分布式缓存、一个持久化数据库、一个队列和一组可水平扩展的 Worker。优先把幂等、超时、重试、状态迁移和日志链路做完整,再根据瓶颈增加向量库、独立推理服务或批量调度器。
重点观测这些指标
- 请求成功率、首 token 延迟和完整响应耗时。
- 当前运行任务数、队列深度、最老任务等待时间和重试率。
- 缓存命中率、缓存条目大小和过期删除情况。
- GPU 利用率、显存水位、推理错误类型和 Worker 心跳。
- 各状态停留时间,以及从
queued到succeeded的端到端耗时。
这些指标比单独统计接口 QPS 更能说明 AI 服务是否真正可用。尤其要同时观察队列延迟和用户体验:平均值正常并不代表长尾请求没有持续超时。
选择部署方式
如果只是验证产品想法,可以把 API、Worker、Redis 和数据库放在同一台云服务器或轻量云主机上,通过容器编排隔离进程,并为数据目录配置持久化盘和备份。业务增长后,再将数据库、缓存、队列和 GPU 推理服务拆分,按组件的资源特征独立扩容。
无论采用单机还是多节点,都应预留日志、监控、备份和灰度升级路径。AI 应用的复杂度往往来自长任务和外部依赖,部署规模小并不意味着可以省略恢复方案。
总结
大模型高并发架构的核心不是堆叠更多中间件,而是建立清晰的控制链:接入层限制流量,状态层保证上下文一致,缓存层减少重复计算,队列层把请求速率压到推理能力以内,Worker 负责执行,可靠投递和状态机负责让结果可追踪、可重试、可恢复。
对个人开发者和小团队,先从可观测的最小闭环开始;对需要多租户、批量推理或 Agent 工具调用的系统,再逐步引入优先级队列、租约、死信、语义缓存和独立推理集群。这样既能控制初期成本,也能为后续上云扩展保留清晰的演进路径。
参考实现机制:Redis Streams 消费者组与待确认消息、RabbitMQ 发布确认与消费确认。具体语义和配置应以所使用版本的官方文档为准。