那学期开学选课的第一天,我盯着监控面板上接近100%的CPU使用率,手边是学生群里的“机器人怎么转圈不回答”的反馈。这是我们维护的高等教育AI智能体第一次被流量教做人。在此之前,它还是一个典型的单体应用,所有业务逻辑、模型调用、定时任务都塞在同一个工程里,试点期只有几百个用户时一切都好,但等到真正面对全校上万名学生的并发咨询时,问题一个接一个冒出来。
后来我们花了将近一个学期,把整个智能体从单体逐步演进成微服务架构。整个过程没有传说中那么顺利,反而踩了大量文档里不会写的坑。这篇想完整记录这次架构转型的决策思考和落地细节,包括服务边界怎么划、会话状态怎么处理、异步任务如何幂等、流量怎么灰度切换。如果你也在做高校或其他ToB场景的AI智能体,而且单体架构已经开始让你难受,这篇内容应该能帮你少走不少弯路。
1. 高校AI智能体为什么非拆不可:单体架构的真实困境
1.1 所有模块挤在一个进程里,改哪里都像踩钢丝
先交代一下我们这个AI智能体的功能范围:招生问答、选课指导、教务通知查询、论文格式咨询、校园生活服务,还接了几个第三方的“智能体工作流”做多轮任务处理。从代码结构看,它并不算特别乱,有controller、service、dao的分层,也有统一返回体,但问题在于所有模块都跑在一个进程里。
意图识别、知识检索、会话管理、用户画像、教务系统适配、模型调用,这些功能共享同一个数据库连接池、同一个定时任务线程池、同一份配置文件。听起来是“高内聚”,实际是“一荣俱荣,一损俱损”。我印象最深的一次故障是某个定时同步学生数据的任务写死了一个表结构,结果凌晨同步失败后不停重试,把数据库连接池占满,第二天早上智能体整个不可用,连招生问答这种跟同步任务毫无关系的功能也一起挂了。
团队协作是另一个痛点。高中阶段我们只有三个开发,还勉强能靠口头沟通避免冲突。后来产品线扩大,分了算法、后端、前端三个小组,单体仓库里的代码合并冲突越来越频繁。算法组只要改动意图识别模块,后端组的检索模块就得跟着回归测试,因为两个模块共用同一个核心包和缓存key前缀。这种被迫绑定的部署周期,比技术债本身更消耗团队士气。
1.2 选课季和招生季的流量潮汐,让单体扩缩容很吃亏
高校业务有一个非常明显的特征:流量是潮汐式的。平时一天几千次请求,开学选课那几天、招生咨询那几周,QPS会瞬间翻几十倍。这种场景对弹性扩缩容的要求极高,而单体架构几乎无法优雅应对。
单体应用不是不能横向扩展,多部署几个实例做负载均衡就行了。但麻烦的是,所有模块会被一起放大。招生问答和选课指导的高峰期其实不完全重合,但在单体里你只能按“最热模块”的流量来扩容,哪怕知识检索模块只需要2个实例,模型推理模块需要6个实例,你也只能整体部署6套,剩下4套检索实例在高峰结束后白白浪费。
更麻烦的是AI智能体的负载不均衡。模型推理是CPU/GPU密集型的,业务API是IO密集型的,学生查询教务系统的时候主要在等外部接口,但大模型生成回答的时候又在疯狂吃CPU。这两个完全不同特征的负载挤在同一个进程里,互相抢资源,调度起来非常难受。我们做过一次压测,单实例在4000并发下,数据库连接先被拖垮,然后整个服务的响应时间从几百毫秒直接飙到几十秒,最后连健康检查都过不了。
1.3 不盲目拆分的判断信号:哪些情况才值得动手
微服务不是银弹。如果你现在只有一两个开发、几千个用户、一次部署能在一个小时之内完成,那强行拆微服务只会增加运维难度。这里我总结几个我认为值得动手的信号,判断时可以对照一下:
- 模块之间的部署周期已经被迫绑在一起,一个模块要上线,其他模块必须跟着回归。
- 某个非核心功能故障会直接拖垮核心功能,比如定时任务挂掉导致问答服务不可用。
- 按用户量和流量扩展时只能整体扩展,无法针对某个高负载链路独立扩容。
- 团队人数超过一定规模后,代码合并冲突已经频繁影响到日常开发。
- 数据模型出现明显的“一张大表被所有模块共用”的迹象。
用这个标准复盘,我们的系统至少中了四个。尤其是“非核心功能拖垮核心功能”这一点,直接推动了架构转型立项。但我也要强调,如果你的系统还没到这些信号,不要为了面试写简历或者追赶热点去拆微服务,模块化单体现在仍然是很多业务的最优解。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 服务边界怎么划?从业务语义到数据归属的拆解方法
2.1 先拆“业务能力”,不是拆代码分层
很多团队做微服务拆分时,习惯按照技术分层来拆:controller拆一个服务,service拆一个服务,dao拆一个服务。结果拆出来一堆贫血微服务,服务之间互相调用全靠RPC,事务被拉得稀碎,性能反而更差。
正确的做法是先梳理业务能力。对AI智能体来说,它的核心能力不是“接口”“方法”,而是“理解用户意图”“检索知识”“调用工具”“生成回答”“记住上下文”“对接外部系统”这些业务动作。我建议在动手前先画一张业务能力地图,把我们系统的能力列出来:
| 业务能力 | 职责说明 | 主要用户 |
|---|---|---|
| 对话接入 | 接收用户消息、维护会话连接、格式转换 | 学生、教师 |
| 意图识别与编排 | 判断用户想干什么,拆解任务步骤,调度其他服务 | 智能体内核 |
| 知识检索 | 从校园文档库中检索相关内容,做重排序 | 问答与推荐 |
| 模型推理 | 调用大模型生成回答,封装不同模型供应商 | 所有AI能力 |
| 会话记忆 | 保存多轮对话上下文、用户偏好、任务状态 | 对话场景 |
| 教务数据适配 | 对接选课、成绩、课表等校内系统 | 业务功能 |
| 反馈审计 | 记录模型输出、人工反馈、问题追踪 | 运营与管理员 |
拆分服务时,我的经验是按这个能力地图来,而不是按代码目录来。一个服务对应一组高内聚的业务能力,内部可以有多个模块,但对外提供的是稳定接口。比如“教务数据适配”服务内部可以同时对接教务系统、一卡通、图书馆系统,但它们对外都统一走适配层,不要让上层服务直接感知底层数据源的多样性。
2.2 数据归属决定服务边界:一张数据矩阵表解决争吵
服务边界讨论到最后,通常都会变成数据表归属的争吵。尤其是高校系统,学生数据、课程数据、选课记录、问答日志,分布在各个历史系统里,谁都觉得自己该管一部分数据。
我们当时用了一个很笨但有效的方法:画数据归属矩阵。把所有核心数据实体列出来,然后标注四个属性:谁负责写入、谁只读、谁通过接口获取、谁绝对不能直接访问。举个例子:
| 数据实体 | 写入方 | 读取方 | 获取方式 |
|---|---|---|---|
| 学生选课记录 | 教务系统 | 智能体编排服务 | 对接教务API |
| 校园文档知识库 | 知识检索服务 | 意图编排服务 | 内部RPC |
| 会话历史 | 会话记忆服务 | 编排服务、评估服务 | 内部API |
| 模型调用账单 | 模型网关 | 运维看板 | 数据库只读 |
| 用户反馈 | 反馈审计服务 | 运营后台 | 内部API |
这张表画完之后,很多争执自动就消失了。比如一开始有人提出来,知识检索服务和模型推理服务应该共享同一个向量数据库,因为模型服务也要对用户query做embedding。但我们在矩阵表里一标就发现,embedding操作属于模型网关的职责,检索数据属于知识服务的职责,两者可以通过接口联动,而不是共享数据库。这个决定帮我们避免了一次隐式耦合。
这里还要多说一句,最怕的就是“共享数据库反模式”。单体拆微服务时,为了省事,很多团队会让多个服务连同一个MySQL库,刚开始确实快,但很快会发现服务之间通过数据库表结构形成了隐式依赖,改一张表要通知所有服务。我们最终的底线是:每个服务至少拥有自己独立的schema,跨服务的数据需求一律走API或事件。
2.3 哪些模块必须独立成服务:按变更频率和资源消耗分类
业务能力地图画出来后,不是所有能力都要马上独立成服务。我们当时用两个维度做了优先级排序:变更频率和资源消耗特征。变更频率高说明团队需要频繁迭代,资源消耗波动大说明需要独立扩缩容,这两个维度综合起来就能判断优先级。
- 高变更 + 高波动:优先拆分。比如Agent编排服务,规则经常调整,选课季流量又高,不拆很难持续迭代。
- 高变更 + 低波动:建议拆分,但可以晚一点。比如反馈审计服务,逻辑不复杂,但要跟上业务新需求。
- 低变更 + 高波动:建议拆分,部署规模可以弹性调整。比如教务数据适配服务,接口稳定但调用量大。
- 低变更 + 低波动:可以暂时留在单体,或者作为公共服务接入。比如字典清洗、通用工具。
我们实际拆分顺序是:模型网关最先拆,因为它是AI能力的关键通道,且第三方模型供应商切换频繁;其次是知识检索服务,因为它的数据增长快、检索压力大;然后是会话记忆服务,因为它直接关系到多轮对话体验;最后是编排服务和教务数据适配。这个顺序让我们每走一步都能独立验证收益,而不是一次性把整个系统炸掉重来。
3. 核心链路重构:从一次调用打通到多服务协同
3.1 请求入口:意图识别与任务编排分离
单体时代,一个用户请求进来,代码从controller一路调用到mapper,逻辑很直观。微服务化之后,请求链路变成了“网关 -> 编排服务 -> 多个能力服务 -> 模型网关 -> 返回”,每一步都要考虑网络开销、超时、容错。
我们先做的不是把所有逻辑拆开,而是把入口拆干净。网关不再关心业务逻辑,只负责鉴权、限流、参数校验,然后把用户消息交给编排服务。编排服务负责两件事:意图识别和任务规划。它先判断用户是想查选课时间、问招生政策,还是想申请某项服务,然后生成一个执行计划,再按计划依次调用知识检索、教务适配、模型推理等服务。
这里的关键点是编排服务本身必须无状态。我们曾经想过把每个会话的执行上下文直接放在编排服务内存里,这样实现简单,但后来发现一旦编排服务多实例部署,同一个用户的连续请求可能被负载均衡到不同实例,上下文就丢了。所以最终我们把会话上下文和任务状态统一放到外部存储,编排服务只负责计算,不负责保存状态。
编排服务还会面临“任务步骤多、某一步失败怎么办”的问题。我们的做法是给每个执行计划定义一个状态机:pending、running、waiting_tool、succeeded、failed、timeout。每一步执行完都会更新状态,如果某一步失败,会根据策略决定是重试、跳过还是终止,而不是整个请求直接报错。这个状态机让运维同学在排查问题时能清晰看到任务卡在哪一步。
3.2 RAG知识库微服务化:高校数据源的接入与隔离
高校AI智能体绕不开RAG,几乎所有问答类能力都依赖知识检索。但高校的知识库数据源非常杂:招生简章是PDF,培养方案是Word,选课规则是Excel,校园通知是网页,还有一部分是历史系统导出的纯文本。单体时代,我们在项目启动时一次性加载所有文档到内存,然后做简单的关键词匹配,勉强能用,但文档一多、更新一频繁就完全撑不住。
拆成独立的知识检索服务后,我们重新设计了数据源接入方式。每个数据源对应一个adapter,负责把不同格式的内容解析成统一的结构化文档,然后再做分块、向量化、入库存。统一结构大致是:tenant_id、doc_id、title、content、chunks、metadata、updated_at。tenant_id用来区分不同学院、不同学期、不同业务域,避免一个学院上传的文档污染另一个学院的检索结果。
向量化是另一个独立步骤。我们最初贪省事,直接在业务代码里调用embedding接口,后来发现模型升级、数据量增长之后,重新向量化非常耗时,而且容易阻塞业务流程。拆服务后,知识检索服务里有专门的数据同步任务,文档入库、更新、删除都会触发增量向量化,对外不阻塞在线检索。在线检索过程分为两步:先用向量召回TopK候选片段,再用重排序模型精排,最后把最相关的几个片段返回给编排服务。
关于向量数据库的选型,我的建议是不要一上来就上分布式向量库。高校私有化部署场景,数据量在百万级以内,用PostgreSQL加上pgvector插件就够用,运维成本低,还能跟业务数据放一起管理。如果后续数据规模增长或者并发检索要求特别高,再考虑迁移到独立向量库。我们目前就是pgvector,实测百万级向量在单机上召回延迟在几十毫秒级别,足够用。
3.3 模型网关:把自研模型与第三方API统一收口
AI智能体项目最不可避免的一件事就是频繁切换模型。今天某个模型的推理速度快,明天另一个模型的效果好,后天学校采购了私有化模型服务,你要在它们之间做切换。单体时代,模型SDK调用散落在多个业务类里,换模型的时候到处改代码,还要担心改漏。
模型网关模式解决的就是这个问题。我们把所有模型调用统一收口到一个服务,对外只暴露一个“chat/completions”风格的接口,内部再路由到具体的模型提供方。这样做有几个直接好处:第一,业务服务不需要关心底层模型是自研的还是第三方的;第二,可以在网关层统一做超时控制、重试、熔断、成本统计;第三,方便做安全过滤和脱敏。
高校场景有一个很实际的需求:涉及个人隐私的数据不能出内网。所以我们把模型网关做成支持路由策略的,默认优先走校内私有化模型,只有私有模型不可用或者需要特定能力时才允许溢出到云端模型。路由配置放在配置中心,运维同学不用改代码就能切换。这个设计在招生季帮了我们大忙,因为校内GPU资源有限,高峰期可以把一些低敏感度、对时效要求不高的请求分流到云端,降低排队时间。
模型网关还要处理“流式输出”。大模型生成回答是逐字逐句的,如果同步等待整段生成完再返回,用户体验很差。单体时代我们用SSE自己硬扛,但拆成微服务后,网关到前端走SSE没问题,编排服务到模型网关之间也要支持流式,否则流式的效果会在中间层被截断。我们最终在编排服务里做了一个“透传”模式,当单模型直接回答时,编排服务只做流式转发,不引入额外buffer。
3.4 Agent工作流与状态管理:从程序化流程到可编排流程
AI智能体区别于普通问答系统的地方在于它能执行复杂任务。比如“帮我对比一下今年和去年的选课规则差异,然后生成一份总结,再帮我预约一个学业咨询”,这背后是多个步骤的组合,仅靠单次模型调用不可能完成。
微服务拆分后,工作流引擎的选择很关键。我们调研过直接用代码硬编流程、用DAG框架、用独立的工作流引擎三种方案。最初为了快速上线,我们用代码硬编,把流程写死在编排服务里,虽然简单,但每加一个新任务类型都要改代码重新部署,完全没有达到“快速迭代”的预期。后来我们引入了一种轻量的、基于状态的流程编排方式,把每个任务类型定义成步骤数组,每个步骤对应一个服务调用或一个工具调用,步骤之间的状态通过状态机来管理。
这种改造带来的最大变化是,新增一个业务场景不需要再改动编排服务的核心逻辑,而是新增一份流程定义,比如定义一个“课程咨询流程”:先查培养方案,再查选课时间,最后生成回答。流程定义可以存在数据库或配置中心,产品经理甚至能够通过后台配置调整流程顺序,开发只需要保证每个步骤对应的服务接口可用。对高校场景来说,这种灵活性很宝贵,因为学校业务人员经常会在学期中间提出新的流程需求。
状态管理的细节需要特别注意。工作流中间状态不能只放在内存里,否则编排服务一重启,所有正在执行的任务就断了。我们要把每个任务的状态持久化到一个task表,包含task_id、session_id、current_step、input、output、status、created_at、updated_at。恢复的时候,根据状态机的定义决定从哪个步骤继续,而不是让用户重新提一遍需求。
4. 微服务改造中最容易翻车的三个技术细节
4.1 数据一致性:选课、审批、积分这种强状态怎么处理
只要拆成微服务,就一定会遇到跨服务的数据更新问题。我举一个我们实际遇到的场景:智能体帮学生提交选课申请,同时需要更新会话状态、发送通知、记录操作日志。
在单体架构里,这些操作可以在一个本地事务里完成:更新选课表、插入日志、更新会话,要么都成功,要么都失败。拆成微服务后,这些数据分散在教务适配服务、会话记忆服务、反馈审计服务各自的数据存储里,本地事务已经管不住了。我们一开始也想直接用分布式事务方案,后来发现高校业务根本没有那么多强一致的场景,过度设计反而会让系统复杂、性能变差。
我们的原则是:把业务分成“强一致操作”和“最终一致操作”。强一致操作收敛到同一个服务内部处理,比如选课扣减名额和选课记录写入,必须保证在一个服务的一个事务里;最终一致操作通过事件驱动完成,比如发送站内信、更新操作日志、通知其他系统,哪个失败就从消息队列里重试。这样既避免了分布式事务的复杂性,又保证了核心业务不出错。
幂等控制是另一个容易被忽略的问题。学生端网络不稳定,用户点了一次“提交选课”,前端可能重试了三次;如果智能体没有做幂等,学生就会收到三条选课成功通知,甚至产生三次重复选课记录。我们的做法是在选课请求里带一个全局唯一的request_id,服务端用request_id + action做唯一约束,重复请求直接返回第一次处理的结果。这个逻辑虽然在单体时代也适用,但微服务化之后,因为服务间调用链路变长,重试和乱序的概率增大,幂等已经从“加分项”变成了“必选项”。
4.2 连续多轮对话的状态保持:别让上下文丢在服务边界上
多轮对话是AI智能体的灵魂,也是微服务改造中最容易翻车的点。单体时代,会话上下文可以放在一个全局Map里,或者直接存放在应用内存里,因为所有请求都在同一个进程,找起来方便。拆成微服务后,用户的每一轮请求可能被路由到编排服务的不同实例,如果上下文只存在实例内存里,下一轮请求过来就找不到了,对话体验会瞬间变成“小学生失忆现场”。
我们设计会话记忆服务,统一负责会话状态的读写。编排服务在处理每一轮请求时,先根据session_id从记忆服务拉取上下文,结合当前消息一起交给模型,模型生成回答后再把新的对话内容写回记忆服务。这样编排服务的实例可以任意横向扩缩容,用户感知不到背后的服务切换。
不过会话状态不是越多越好。把全部历史消息一股脑塞给大模型,既浪费token又拖慢响应速度,而且高校业务对话往往很长,比如一个学生断断续续咨询一周,累计几十轮消息。我们的方案是把记忆分成两层:短期记忆保存最近几轮的完整消息,长期记忆只保存关键信息,比如“学生是2024级计算机专业”、“想转专业到软件工程”、“已经咨询过两次转专业流程”。长期记忆由模型在每轮对话结束后自动抽取,再存到记忆服务里。这样每次请求实际传给模型的上下文控制在合理范围,多轮对话的连贯性也能保证。
这里还要提一个坑:会话状态的持久化。最开始我们图快,把短期记忆放在Redis里,Redis一重启就全没了。后来我们把会话快照定期持久化到数据库,Redis只做缓存加速,即使缓存丢失也能从数据库恢复。虽然多了一次IO开销,但换来的是用户数据不丢,在高校场景里很值得。
4.3 异步任务与重试幂等:AI推理超时不等于系统崩溃
大模型推理是出了名的“慢”,一次完整问答可能要几秒,复杂的任务型对话可能要几十秒。如果所有请求都同步等待,前端会频繁超时,服务端的线程池也会被占满。单体时代我们试过调大HTTP超时时间,结果只是把问题往后推,并没有真正解决。
拆成微服务后,我们用异步化把耗时操作和请求主链路解耦。对于即时问答类场景,采用SSE流式返回,让用户看到“正在生成”的过程;对于批量处理类场景,比如“帮我把这学期的所有课程大纲总结一遍”,客户端提交任务后立即得到一个task_id,后台由独立的异步worker执行,前端通过轮询或者WebSocket获取任务进度。
任务队列的重试机制同样要小心。调用第三方模型接口经常出现“网络抖动”,如果失败后立刻重试,大概率还是失败;如果一直重试,又可能造成重复下单、重复发通知。我们的做法是:重试指数退避,最多重试三次;重试时带相同的task_id,下游服务用task_id做幂等,避免同一条指令被执行多次。
为了避免“一个服务卡死导致连锁反应”,我们还在异步worker里做了舱壁隔离。不同的任务类型使用独立的线程池和队列,比如模型生成任务和教务数据同步任务分开。这样即使模型生成任务因为模型接口变慢而积压,也不会影响教务数据同步这种对系统稳定性至关重要的任务。这个思路在单体时代很难实现,因为所有代码共享同一个线程池,也是我们拆分后获得的最直接收益之一。
5. 网关、注册中心与部署落地:高教场景的基建选型
5.1 服务发现与配置中心:小团队怎么选不踩坑
微服务拆分后,服务实例的数量上来了,网络地址会动态变化,不能再靠写死在配置文件里的IP地址来调用了。这时候需要一个服务注册与发现组件。很多技术方案一上来就是Spring Cloud全家桶、Nacos、Sentinel一套组合,但对高校信息化团队来说,学习成本和运维成本都不低。
如果你的团队已经有Kubernetes,最轻量的方案是直接用Kubernetes的Service机制,让服务通过DNS名称互相访问,完全不需要额外部署注册中心。如果还没有容器化,或者团队习惯了Java生态,Nacos是一个不错的选择,它同时提供服务发现和配置中心,还支持多环境隔离。我们最终选择了Nacos,原因是团队Java技术栈比较统一,且Nacos在配置管理上比纯Kubernetes ConfigMap要直观,支持配置变更推送,改一个模型路由配置不用重启服务。
配置中心的一个核心原则是:密钥和敏感信息不能进代码仓库。我们曾经因为图方便,把模型API Key写在一个配置文件里提交到了Git仓库,后来发现服务商后台显示来自未知IP的调用量激增,才意识到泄露了。迁移到Nacos后,我们把密钥放在加密配置项里,并且设置了细粒度的权限控制,只有特定的服务账号才能读取。这件事在高校场景里尤其重要,因为涉及大量学生个人数据,一旦泄露就不是技术问题而是合规问题。
5.2 API网关和统一鉴权的边界:别把业务逻辑塞进网关
在微服务架构里,API网关是外部请求进入系统的第一道关卡,我们让网关承担了四件事:路由转发、限流熔断、统一鉴权、审计日志。网关对外暴露一个统一域名,内部分别路由到编排服务、反馈服务、管理后台服务等。前端不需要知道每个服务的具体地址,只需要跟网关打交道。
鉴权是网关最重要的职责之一。高校场景里需要对接的统一身份认证体系很多,有传统的CAS、有OAuth2、有企业微信、有的还要接微信服务号。我们的做法是网关统一处理这些认证协议,认证完成后把用户身份信息以请求头的方式注入到下游服务,比如X-User-Id、X-Tenant-Id、X-User-Roles。下游服务不再解析token,只信任网关注入的请求头。这样每个服务都能省掉一套认证逻辑,也避免了不同服务鉴权标准不一致的混乱。
但这里必须划清边界:网关只做通用横切逻辑,不能掺和具体业务判断。我们早期曾想把“判断学生是否选课高峰期”这种业务规则也放到网关里,方便做限流策略,后来发现业务规则变起来太快,网关一改就要重新发布,影响所有服务。正确的做法是网关只做基础限流,比如按用户ID或IP维度限制请求频率,业务级的优先级控制放在编排服务里。保持网关的“薄”,才能让网关稳定。
内部服务之间的调用也不能裸奔。服务A调用服务B时,至少要带上内部凭证,服务B校验凭证后放行。我们使用的是轻量级的内部Token机制,每个服务实例启动时从配置中心获取自己的身份凭证,调用其他服务时带到gRPC或HTTP的metadata里,对方校验通过后才处理请求。这套机制不需要很重,但能防止内部接口被直接暴露到公网后滥用。
5.3 部署策略与成本控制:在有限的机器上跑微服务
微服务拆分后最直观的问题是机器用量变多。单体应用两个实例就能跑,拆成八个服务后,如果每个服务都开两个实例,就是十六个实例,哪怕每个实例只占0.5核,总数也比之前高很多。高校信息化的预算普遍不是特别充裕,这个问题必须正面解决。
我们的做法有几个原则。第一,按资源消耗特征决定副本数:模型网关这类高消耗但无状态的服务,可以随流量动态扩缩;教务数据适配这类IO密集型但逻辑简单的服务,固定两个副本就够。第二,低流量服务不要单独一个进程,可以先合并部署,在同一个Pod或VM里跑多个模块,等流量确实上来了再拆开。第三,数据库不要每个服务独立一个MySQL实例,可以在同一个实例里建多个schema,通过账号权限隔离,既节约资源又保证一定的隔离性。
下面是我们在拆分后做的一次资源对比,可以参考:
| 服务 | 实例规格 | 副本数 | 高峰期策略 |
|---|---|---|---|
| API网关 | 2C4G | 2 | 固定 |
| 编排服务 | 4C8G | 2-4 | 按QPS伸缩 |
| 知识检索服务 | 4C8G | 2-6 | 流量驱动 |
| 模型网关 | 4C8G | 2-8 | 推理高峰扩容 |
| 会话记忆服务 | 2C4G | 2 | 固定 |
| 教务适配服务 | 2C4G | 2 | 固定 |
这套方案落地后,日常资源占用只比单体模式多了不到30%,但高峰期可以按需扩到原来的四倍,而且扩缩容互不影响。对比单体时期“所有服务一起扛”的窘境,成本和稳定性都有明显改善。
监控这块也不能缺。拆成微服务后,如果还靠登录服务器看日志,排查一个跨服务问题的成本会高到崩溃。我们上了Prometheus + Grafana做指标监控,用OpenTelemetry做链路追踪,每一次用户请求从网关到编排再到知识检索、模型网关,全链路都带同一个trace_id。排查问题的时候,在日志平台里一搜trace_id,所有服务节点上的日志按时间排序,问题定位时间从小时级降到了分钟级。
6. 迁移过程复盘:灰度切换与回滚的实操经验
6.1 单体保留还是整体重写?演进式拆分更适合高校
架构转型最忌讳“推倒重来”。我们也讨论过要不要用一个全新的技术栈直接重写整个系统,后来一致否决了。原因是高校业务里旧系统有大量历史积累:对话日志、知识库文档、复杂规则、管理层习惯,如果一次性重写,相当于在一个学期内把所有业务、数据、团队认知全部迁移过去,风险极大。
我们采用的是老系统逐步改造的演进式方案。具体来说,先让单体继续承担大部分请求,然后一刀一刀把能力模块剥离出去。整体顺序是:模型网关 -> 知识检索 -> 会话记忆 -> 编排 -> 教务数据适配。每个模块剥离后,单体就少一块职责,新服务逐渐接管相应的流量。这个顺序有一个潜在线索:先拆与业务数据耦合最低、又最容易被独立验证的模块,再拆那些牵连广、依赖复杂的模块。
模型网关是我们最早拆出来的,因为它不依赖任何业务表,只负责模型调用,而且所有AI能力都在用它,一旦改造成功,收益立竿见影。知识检索是第二个拆的,因为RAG是问答质量的基础,而且它的数据结构相对独立,不太会跟其他服务产生复杂的一致性问题。会话记忆拆得稍微晚一点,因为它与编排逻辑的交互很频繁,需要在接口设计上多花时间。教务数据适配放到了最后,因为它要对接的外部系统最多,需要逐一切流量验证。
6.2 从旁路验证到逐步切流量:不让学生感受到“架构变了”
系统拆分最大的风险不是代码写不出来,而是切换过程中业务不可用。我们的原则是:每一次服务切换都必须有完整的灰度方案,不允许直接全量上线。
以知识检索服务切换为例,我们分了三步走。第一步是旁路验证:单体仍然处理线上用户的真实请求,同时把请求的查询参数复制一份发送给新知识检索服务,但不使用新服务的返回结果,只记录新服务的返回内容和耗时,用来对比新旧检索结果的相关性和性能。这一步发现过几次新服务返回空结果的问题,都是因为文档解析规则和旧系统不一致,在旁路阶段就修掉了。
第二步是内部白名单放量:先让运维和产品团队使用新服务,自己体验一遍,确认识别质量没有明显下降,然后扩大到部分学院的学生。这个阶段要密切跟踪告警、用户反馈和人工客服工单。我们当时发现过一个问题:新服务在知识检索时对繁体字的处理方式跟旧系统不一致,导致港澳台学生问的问题检索不到结果。这种问题只有真实用户流量才能暴露出来。
第三步才是按比例切流量。我们从5%开始,观察半小时指标稳定后,再逐步提升到10%、30%、50%、100%。每一步都保留随时回滚的能力,因为新旧服务在网关里都有对应路由配置,只要改一下权重就能立刻切回。最后全量切换后,我们也把单体里的旧代码保留了一个版本,直到新系统稳定运行了两周才彻底下线相关模块。
6.3 我们踩过的坑,和一点更长期的建议
整个过程当然不是一帆风顺的,这里记录几个我们踩得比较深的坑,希望你能绕过。
第一个坑是共享数据库。拆分初期,知识检索服务还是直接连接原来单体的那个MySQL实例,导致检索服务的高负载查询经常拖慢其他模块的页签接口。后来我们花了整整一周,把知识相关的表迁到了独立实例,才彻底解耦。这件事让我明白,拆服务不拆库等于白拆,数据库的物理隔离必须跟服务拆分同步进行。
第二个坑是模型网关没有设置超时时间。我们上线后第三天,某个第三方模型API因为对方机房网络问题一直不返回,结果编排服务的HTTP连接池被全部占满,所有对话请求都排着队等模型网关释放连接。后来给模型网关加了三层保护:连接超时、读超时、服务熔断,超过阈值直接降级返回“当前咨询人数较多,请稍后再试”,这比无限等待要好得多。
第三个坑是异步任务重复执行。我们在做“批量总结课程大纲”功能时,worker处理完消息后还没来得及提交确认,消息队列又把同一条消息重新投递了一次,导致学生收到了两封一样的总结邮件。后来我们在数据库里加了task_id唯一索引,处理前先查重,才彻底解决。这个教训在AI智能体场景里很典型:只要涉及外部工具调用,就一定要考虑幂等,尤其是当模型输出会触发真实动作时。
最后一个建议是关于“微服务化程度”的。我们拆到今天,并不是每个模块都变成了独立服务,像反馈审计、字典管理这类低频低负载模块,至今还在一个合并服务里跑着。我觉得这是合理的,微服务的目的不是让服务数量最大化,而是让组织能更快响应业务、让系统更稳定地扛住流量。如果一个模块拆出去后既没有独立部署需求,也没有独立扩展需求,那它还留在合并部署状态反而是更优解。
回到最初的问题,高等教育AI智能体的架构转型,不是一个“从A到B”的一锤子买卖,而是一个持续判断的过程。我们今天拆了的服务,未来如果团队变小、业务变简单,完全可以再合回来。重要的是,每次架构调整都要有明确的问题驱动,而不是为了在技术汇报里多写一行“微服务”关键字。
