# 21 · 拆分与迁移实战 > 一句话点题:**[20 章](20-演进剧本MVP到规模化.md) 末段,那个 AI 客服已经长成一坨「改一处动一片」的大单体。这一章把 [14 章](14-演进与拆分大型系统.md) 的手艺全用上——在它继续接客、继续退款、需求继续变的同时,不停机地把它一块块安全拆开、把零件一个个安全换掉。** --- > **🎯 实战篇第 4 章 · 本章只练一件事** > > 练**演进的手艺**(不是世界观)。承接 [14 章](14-演进与拆分大型系统.md):绞杀者、抽象分支、并行运行、零停机数据迁移、防腐层、模块化单体、适应度函数——**全部案例化到这个 AI 客服上**。 > > | 读完你应该能 | 本章靠什么练 | > |---|---| > | 判断该不该拆、先拆哪块 | 第一节:找缝 + 模块化单体优先 | > | 用绞杀者无停机地抽出一个服务 | 第二节:抽「检索服务」的分步剧本 | > | 给「换模型 / 换检索」兜底信心 | 第四节:影子流量(AI 迁移杀手锏) | > | 零停机换掉向量库 | 第五节:expand-contract 五步 | --- ## 开场:飞行中的飞机,别想着落地重造 [20 章](20-演进剧本MVP到规模化.md) 走到规模化末段,这个 AI 客服赚着钱、扛着大促、每天处理着真实退款——但它内部是这样的: ``` 一个部署单元里,什么都揉在一起: ┌──────────────────────────────────────────────┐ │ 编排 + RAG 检索 + 工具/退款 + 计费计量 │ │ ↑ 改检索算法,要整个重新发布、可能拖垮退款 │ │ ↑ 检索团队和退款团队,卡在同一个代码库里排队上线 │ └──────────────────────────────────────────────┘ ``` 这正是 [演进触发信号](演进触发信号.md) 里「组织/效率」那类红灯:**改一处牵动一片、多团队互相阻塞、发布越来越慢**。 新手的本能是:「这单体没救了,推倒,用微服务重写一版干净的。」**[14 章](14-演进与拆分大型系统.md) 第一节已经把这条路堵死了**——旧系统不会停下等你(靶子在移动),它那些「难看」的代码里沉淀着踩坑换来的隐性知识(某个退款边界、某条注入防护)。**重写 = 对着移动靶射击 + 把血泪经验清零 + 要求对手原地等你。** > 所以本章只有一条主路:**不推倒,只渐进。让新旧并存,旧的一块块萎缩,新的一块块长出来,任何一步都能停、能回滚、系统始终在线。** 下面六节,全是这条主路上的具体手艺。 > > **AI 时代这条主线更要命**:vibe coding 让你半天就能让 AI「生成一版更干净的检索服务」——但**「把它在飞行中安全换上去」AI 给不了你**,那要的是对「先拆哪、怎么兜底、何时切、出事怎么退」的连续判断。实现廉价化,掌舵越值钱。 --- ## 一、先找缝,别急着拆(限界上下文 + 模块化单体优先) 拆之前先回答两个问题:**沿哪条缝拆?先拆哪一块?** **沿业务能力的自然边界找缝**(DDD 限界上下文,[14 章](14-演进与拆分大型系统.md)),不要按技术分层拆。这个 AI 客服天然有四条缝: ``` ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐ │ 对话编排 │ │ 检索 RAG │ │ 工具/退款 │ │ 计费计量 │ │ (会话/路由)│ │ (向量/重排)│ │ (碰钱/状态机) │ │ (token账) │ └──────────┘ └──────────┘ └──────────────┘ └──────────┘ ``` **哪块先抽?** 不是「全拆」,而是问:**哪块的「独立伸缩 / 独立发布 / 故障隔离」诉求最强?** | 候选 | 独立诉求 | 该不该先抽 | |---|---|---| | **检索 RAG** | 要独立伸缩(检索 QPS 涨法和对话不同)、要高频迭代检索质量、可能被别的产品复用 | **✅ 第一个抽** | | **退款/工具** | 可靠性 / 合规要求和对话完全不同、故障要隔离(检索挂不能连累退款) | **✅ 第二个抽** | | 对话编排 | 是中枢,改动频繁但不需独立伸缩 | 留在主体 | | 计费计量 | 简单、稳定 | 留在主体(模块化即可) | > **铁律(来自 [14 章](14-演进与拆分大型系统.md)):先模块化单体,再按需拆服务。** 别一上来拆四个微服务,把「函数调用」升级成「网络调用」、凭空背上分布式的全部苦头([10 章](10-分布式系统的硬道理.md))。正确顺序: > > ``` > 一坨泥 ──▶ 模块化单体(一个部署单元,内部强制边界) ──▶ 只把「检索/退款」抽成服务 > ``` > > 先在进程内把四条缝划清(改边界几乎零成本),等缝被真实业务捶打稳了,**只为「确实要独立伸缩/发布」的检索和退款**支付分布式代价。其余两块,留在模块化单体里就是好归宿。 --- ## 二、绞杀者:把「检索服务」无停机地抽出来 决定先抽检索,但**不能停服去搬**。用**绞杀者模式**([14 章](14-演进与拆分大型系统.md)):在旧单体外围加一层路由,把「检索」这一块的流量逐步引向新服务。 ``` 命门是这一层「门面/路由」——没有它,你连「悄悄切一小部分流量」的开关都没有: 编排层 ──▶ ┌─────────────────────────┐ │ 检索路由(开关 + 灰度比例) │ └──────┬──────────────┬─────┘ │ 90%(默认) │ 10%(灰度) ▼ ▼ ┌─────────────┐ ┌──────────────┐ │ 单体内旧检索 │ │ 新检索服务 │ │ (逐步萎缩) │ │ (逐步长大) │ └─────────────┘ └──────────────┘ ``` **分步迁移剧本(每一步都能停、能回滚):** 1. **立门面**:把单体里所有「检索」调用,收口到一个内部接口 `retrieve(query, tenant) → chunks`(这是抽象分支的第①步,见下节)。此刻行为完全不变。 2. **建新服务**:把检索逻辑复制 / 重写成独立的「检索服务」,自己的库、自己的部署。**先不接流量。** 3. **影子比对**(第四节细讲):新服务对真实查询「陪跑」,比对召回质量,**结果不返回用户**。 4. **灰度切流**:路由开关从 1% → 10% → 50% → 100%,每档观察召回质量、延迟、错误率。**任何一档不对,开关切回 0,旧检索一直在线。** 5. **绞杀完成**:100% 走新服务且稳定后,删掉单体内的旧检索代码。它「被绞杀」下线。 > **架构智慧**:绞杀者不是「快」,它常常要**新旧并存维护一段时间**——但它把「一次性赌上整个系统」拆成了「一次切一小档」的可控风险序列。**你不是在和旧单体决斗,而是在它身上长出新检索,直到旧的没流量、自然枯死。** --- ## 三、抽象分支:在「模型抽象层」背后换 provider [20 章](20-演进剧本MVP到规模化.md) 规模化要引入 **AI 网关**(多 provider 故障转移)。但「调用模型」这件事被编排层里几十处直接调用缠死了——你不可能在外面拦一层。这时用**抽象分支**([14 章](14-演进与拆分大型系统.md)):**不开长命分支,全程在主干,靠一层抽象让新旧实现并存。** ``` 抽象分支五步,全程主干可发布: ① 插入抽象层:所有地方都改成调 ModelClient 接口,不再直连某 provider SDK 编排层 ──▶ 【ModelClient 抽象】──▶ 直连 provider A(旧) ② 抽象层背后写新实现:接 AI 网关(多 provider + 容灾 + 语义缓存) 编排层 ──▶ 【ModelClient】─┬─▶ 直连 provider A(默认,开关控制) └─▶ AI 网关(新,先不放量) ③④ feature flag 逐步把流量切到网关,出问题随时切回 ⑤ 网关稳了,删掉「直连 provider」的旧实现 ``` > **架构智慧**:**「开个长命分支慢慢改、改完再合」是直觉,却是规模化重构最贵的反模式——你在制造一颗『合并核弹』,引信时间还由别人(也在改主干的同事)决定。** 抽象分支把它倒过来:先立一层「插座」(`ModelClient`),让新旧实现都能插上去,再在主干上不慌不忙地切。**多写一层抽象的成本,买的是「随时能停、能发、能回退」的安全感。** > > 顺带,这层 `ModelClient` 抽象**本身就是 AI 系统该有的接缝**——provider 涨价、限速、被封、出新模型……「换模型」在 AI 时代是高频事件,把它收在一个接口背后,是 [08 章](08-架构决策记录与演进.md)「在最可能变的地方留好接缝」的标准兑现。 --- ## 四、并行运行 / 影子流量:给「换模型、换检索」兜底信心(AI 迁移杀手锏) 第二、三节都给了你「切换开关」。但**切之前,你凭什么相信新检索 / 新模型是对的?** 跑通测试?真实流量永远比你的测试用例刁钻——**AI 系统更是如此,因为它本来就没有「正确答案」可断言。** 最硬核的办法是**并行运行 / 影子流量**([14 章](14-演进与拆分大型系统.md)):**新旧两套对同一批真实请求同时跑,旧的结果返回用户(用户无感),新的结果在背后和旧的悄悄比对。** ``` 场景 A:换检索算法(纯向量 → 混合+重排) ┌──▶ 旧检索 ──▶ chunks_A ─────────▶ 返回给用户 ✅ 真实查询 ──┬─┤ │ └──▶ 新检索 ──▶ chunks_B ──┐ │ ▼ │ 用 eval 给两边召回打分 │ 一致/更好 → 信心+1;更差 → 落日志排查 场景 B:换模型 / 接网关 ┌──▶ 旧模型 ──▶ 答案_A ──────────▶ 返回给用户 ✅ 真实问题 ──┬─┤ │ └──▶ 候选模型 ─▶ 答案_B ──┐ │ ▼ │ LLM-as-judge / 规则评分 比对答案质量 │ 质量不退化 → 才敢切;退化 → 别切,继续修 ``` 这正是 AI 迁移最该养成的肌肉记忆,也呼应 [14 章的 AI 视角](14-演进与拆分大型系统.md): > **让 AI 快,让比对慢;速度交给模型,信心交给数据。** AI 让你飞快产出「新检索 / 新实现」,但**对不对、退没退化,交给「真实流量 + eval」来审判,而不是交给你的自信。** 关键纪律(同 GitHub Scientist):旧实现的结果**永远**返回用户;候选实现在背后跑、**吞掉它抛的异常**(绝不让实验代码搞挂线上);等「不一致 / 质量退化率」降到够低,才真正切流。 > 📎 这套机制的活样板是 GitHub 的 **Scientist** 库,[14 章真实案例](14-演进与拆分大型系统.md) 有详解。AI 时代它的价值不降反升:它天生就是给「AI 改的 / AI 生成的代码」兜底信心的最佳搭档。 --- ## 五、零停机数据迁移:换掉向量库 规模化后,MVP 那个凑合的向量存储(比如 pgvector)扛不住了,要换成专用向量库(Milvus / Qdrant)。**数据最难改**——代码能蓝绿回滚,数据只有一份、改坏往往救不回。所以走 [14 章](14-演进与拆分大型系统.md) 的 **expand→contract** 五步,每步可回滚: ``` ① 双写:新文档入库时,同时写【旧向量库】和【新向量库】。检索仍走旧。 → 退路:停掉对新库的写即可,旧库一直是权威。 ② 回填:把存量文档批量灌进新库。⚠️ AI 特有的代价—— 这一步要把所有历史文档块「重新 embedding」,既花钱又花时间, 要分批限速、可中断重来。 → 退路:回填只写新库,随时可停。 ③ 影子读校验:检索两边都查,返回用户的仍是旧库结果; 比对「新库召回」vs「旧库召回」是否一致(就是第四节的影子流量!)。 → 退路:只比对不切流,召回不达标就继续修。 ④ 切读:一致率够高,把检索切到新向量库。此时仍在双写。 → 退路:读再切回旧库,旧库一直被双写、依然新鲜。 ⑤ 清理:观察稳定后,停掉对旧库的双写,下线旧库。 → 唯一不可逆的一步,放最后、留足观察期。 ``` > **架构智慧**:数据迁移的灾难几乎都源于「一刀切」——半夜停服、跑迁移脚本、上线、祈祷。正确姿势是拉成长链条,**让「旧库一直是权威」成为随时能跳的安全网**,直到最后一刻才剪断。AI 系统这里**唯一的特殊**,是第②步回填要「重新 embedding」——这是一笔实打实的算力账,要当成本来规划(呼应 [19 章](19-完整设计演练中等复杂度系统.md) 步骤 ②)。 --- ## 六、接旧系统别被污染 + 让架构不再腐化 **防腐层(ACL):** AI 客服要接企业那套用了十年的订单 / 支付系统——字段诡异、概念杂糅。别让它的脏模型渗进你干净的退款服务。架一道**防腐层**翻译隔离([14 章](14-演进与拆分大型系统.md)): ``` ┌──────────────┐ ┌────────┐ ┌────────────────────┐ │ 退款服务 │───▶│ 防腐层 │───▶│ 旧订单/支付系统 │ │ (干净的新模型) │◀───│ ACL │◀───│ (脏字段、诡异状态) │ └──────────────┘ └────────┘ └────────────────────┘ 新服务只跟「翻译干净的接口」打交道,旧系统的腐烂渗不进来。 ``` **适应度函数:** 费力拆出来的漂亮边界,怎么保证不被后续几百次提交又揉回一坨泥?把架构约束写成**会失败、能卡 CI 的自动化测试**([14 章](14-演进与拆分大型系统.md)),给架构装上免疫系统。这个 AI 客服该写的几条: | 你在乎的架构约束 | 写成适应度函数(进 CI) | |---|---| | 编排层不准直接 import 向量库内部类 | 依赖检查:扫到就 fail | | **所有模型调用必须经过 `ModelClient` 抽象**(不准散落直连 provider) | 依赖检查:直连 provider SDK 就 fail | | 退款服务的每个写操作必须带幂等键 | 静态检查 / 契约测试:缺 key 就 fail | | 检索服务 p99 < 200ms | 性能测试:超了就 fail | | 检索不准跨租户召回 | 集成测试:跨租户能查到就 fail(安全红线) | > **架构智慧**:架构不是画完图就定型的雕像,是要持续维护的活系统;活的东西没有免疫系统必然腐化。**适应度函数不阻止系统长大,它只阻止系统在长大时烂掉。** 没有它,你今天辛苦拆出的每一道边界,都只是在等一个赶时间的下午被人 `import` 穿。 --- ## 📌 真实案例 本章的手艺都来自 [14 章的真实案例](14-演进与拆分大型系统.md),拆迁路上最值得记的两条: - **GitHub Scientist**(并行运行的活样板):重构高危的权限判断时,旧逻辑结果照常返回用户、新逻辑在背后对真实流量比对,靠数据而非自信建立信心。**AI 时代,它就是给「换模型 / 换检索 / AI 改的代码」兜底的最佳搭档。** - **Segment《Goodbye Microservices》**(拆过头的警钟):为「故障隔离」把系统拆得过碎,结果改一个公共库要花一周,最后**回退到单体**。提醒你:**服务数量从不是目标——先模块化单体把边界划对,再按瓶颈按需抽服务。** --- ## 🎯 随堂检验 --- ## 本章小结 - **别推倒重写**:旧 AI 客服难看的代码里沉淀着踩坑经验;重写 = 追移动靶 + 清零血泪 + 要求对手等你。唯一主路是**渐进演进**。 - **先找缝、先模块化单体**:按业务能力(对话/检索/工具退款/计费)找限界上下文;先在进程内划清边界,只把「确实要独立伸缩/发布」的检索、退款抽成服务。 - **绞杀者**:在编排层和检索之间加路由开关,灰度把流量从旧检索切到新检索服务,旧的被绞杀下线。 - **抽象分支**:把「调用模型」收进 `ModelClient` 抽象,在它背后无停机地从「直连 provider」切到「AI 网关」——「换模型」是 AI 时代高频事件,这层接缝必留。 - **并行运行/影子流量是 AI 迁移杀手锏**:换检索、换模型都让新实现「陪跑」,用真实流量 + eval 比对质量。**让 AI 快、让比对慢;速度交给模型,信心交给数据。** - **零停机换向量库**:双写→回填(注意「重新 embedding」的算力账)→影子校验→切读→清理,让旧库一直当权威安全网。 - **防腐层**隔离旧订单系统的脏模型;**适应度函数**把架构约束(模型必经抽象层、退款必带幂等键、不准跨租户召回)写进 CI,给架构装免疫系统。 > **承上启下**:到这里,你已经把这个 AI 客服**读懂(18)、设计(19)、演进(20)、拆迁(21)** 走了一整圈——但它本质还是「对话 + 受控动作」。下一章 [22 · AI 原生系统设计](22-AI原生系统设计.md) 把自主性再往上推一档:设计一个**自主 Agent**——让它自己规划、调用工具、多步把整个工单处理到底。自主性越强,[17 章](17-大模型时代的架构判断.md) 那三个新约束咬得越狠,而这正好把我们引向最后的 **AI 协同设计篇**。 --- ## 相关链接 - 方法论本体:[14 · 演进与拆分大型系统](14-演进与拆分大型系统.md) —— 本章是它的完整案例化 - 同一系统:[19 · 完整设计](19-完整设计演练中等复杂度系统.md) · [20 · 演进剧本](20-演进剧本MVP到规模化.md) - 配套:[08 · ADR 与演进](08-架构决策记录与演进.md)(留好接缝)、[10 · 分布式硬道理](10-分布式系统的硬道理.md)(别过早拆服务)、[11 · 数据一致性](11-数据一致性工程.md)(双写/回填) - 实战对照:[AI 网关模板](../templates/ai-gateway/README.md) · [向量数据库模板](../templates/vector-database/README.md)