Skip to content

21 · 拆分与迁移实战

一句话点题:20 章 末段,那个 AI 客服已经长成一坨「改一处动一片」的大单体。这一章把 14 章 的手艺全用上——在它继续接客、继续退款、需求继续变的同时,不停机地把它一块块安全拆开、把零件一个个安全换掉。


🎯 实战篇第 4 章 · 本章只练一件事

演进的手艺(不是世界观)。承接 14 章:绞杀者、抽象分支、并行运行、零停机数据迁移、防腐层、模块化单体、适应度函数——全部案例化到这个 AI 客服上

读完你应该能本章靠什么练
判断该不该拆、先拆哪块第一节:找缝 + 模块化单体优先
用绞杀者无停机地抽出一个服务第二节:抽「检索服务」的分步剧本
给「换模型 / 换检索」兜底信心第四节:影子流量(AI 迁移杀手锏)
零停机换掉向量库第五节:expand-contract 五步

开场:飞行中的飞机,别想着落地重造

20 章 走到规模化末段,这个 AI 客服赚着钱、扛着大促、每天处理着真实退款——但它内部是这样的:

   一个部署单元里,什么都揉在一起:
   ┌──────────────────────────────────────────────┐
   │  编排  +  RAG 检索  +  工具/退款  +  计费计量    │
   │  ↑ 改检索算法,要整个重新发布、可能拖垮退款       │
   │  ↑ 检索团队和退款团队,卡在同一个代码库里排队上线  │
   └──────────────────────────────────────────────┘

这正是 演进触发信号 里「组织/效率」那类红灯:改一处牵动一片、多团队互相阻塞、发布越来越慢

新手的本能是:「这单体没救了,推倒,用微服务重写一版干净的。」14 章 第一节已经把这条路堵死了——旧系统不会停下等你(靶子在移动),它那些「难看」的代码里沉淀着踩坑换来的隐性知识(某个退款边界、某条注入防护)。重写 = 对着移动靶射击 + 把血泪经验清零 + 要求对手原地等你。

所以本章只有一条主路:不推倒,只渐进。让新旧并存,旧的一块块萎缩,新的一块块长出来,任何一步都能停、能回滚、系统始终在线。 下面六节,全是这条主路上的具体手艺。

AI 时代这条主线更要命:vibe coding 让你半天就能让 AI「生成一版更干净的检索服务」——但**「把它在飞行中安全换上去」AI 给不了你**,那要的是对「先拆哪、怎么兜底、何时切、出事怎么退」的连续判断。实现廉价化,掌舵越值钱。


一、先找缝,别急着拆(限界上下文 + 模块化单体优先)

拆之前先回答两个问题:沿哪条缝拆?先拆哪一块?

沿业务能力的自然边界找缝(DDD 限界上下文,14 章),不要按技术分层拆。这个 AI 客服天然有四条缝:

   ┌──────────┐  ┌──────────┐  ┌──────────────┐  ┌──────────┐
   │ 对话编排   │  │ 检索 RAG  │  │ 工具/退款     │  │ 计费计量  │
   │ (会话/路由)│  │ (向量/重排)│  │ (碰钱/状态机) │  │ (token账) │
   └──────────┘  └──────────┘  └──────────────┘  └──────────┘

哪块先抽? 不是「全拆」,而是问:哪块的「独立伸缩 / 独立发布 / 故障隔离」诉求最强?

候选独立诉求该不该先抽
检索 RAG要独立伸缩(检索 QPS 涨法和对话不同)、要高频迭代检索质量、可能被别的产品复用✅ 第一个抽
退款/工具可靠性 / 合规要求和对话完全不同、故障要隔离(检索挂不能连累退款)✅ 第二个抽
对话编排是中枢,改动频繁但不需独立伸缩留在主体
计费计量简单、稳定留在主体(模块化即可)

铁律(来自 14 章):先模块化单体,再按需拆服务。 别一上来拆四个微服务,把「函数调用」升级成「网络调用」、凭空背上分布式的全部苦头(10 章)。正确顺序:

一坨泥  ──▶  模块化单体(一个部署单元,内部强制边界)  ──▶  只把「检索/退款」抽成服务

先在进程内把四条缝划清(改边界几乎零成本),等缝被真实业务捶打稳了,只为「确实要独立伸缩/发布」的检索和退款支付分布式代价。其余两块,留在模块化单体里就是好归宿。


二、绞杀者:把「检索服务」无停机地抽出来

决定先抽检索,但不能停服去搬。用绞杀者模式(14 章):在旧单体外围加一层路由,把「检索」这一块的流量逐步引向新服务。

   命门是这一层「门面/路由」——没有它,你连「悄悄切一小部分流量」的开关都没有:

   编排层 ──▶ ┌─────────────────────────┐
              │  检索路由(开关 + 灰度比例)  │
              └──────┬──────────────┬─────┘
                     │ 90%(默认)    │ 10%(灰度)
                     ▼              ▼
              ┌─────────────┐  ┌──────────────┐
              │ 单体内旧检索  │  │ 新检索服务     │
              │ (逐步萎缩)   │  │ (逐步长大)    │
              └─────────────┘  └──────────────┘

分步迁移剧本(每一步都能停、能回滚):

  1. 立门面:把单体里所有「检索」调用,收口到一个内部接口 retrieve(query, tenant) → chunks(这是抽象分支的第①步,见下节)。此刻行为完全不变。
  2. 建新服务:把检索逻辑复制 / 重写成独立的「检索服务」,自己的库、自己的部署。先不接流量。
  3. 影子比对(第四节细讲):新服务对真实查询「陪跑」,比对召回质量,结果不返回用户
  4. 灰度切流:路由开关从 1% → 10% → 50% → 100%,每档观察召回质量、延迟、错误率。任何一档不对,开关切回 0,旧检索一直在线。
  5. 绞杀完成:100% 走新服务且稳定后,删掉单体内的旧检索代码。它「被绞杀」下线。

架构智慧:绞杀者不是「快」,它常常要新旧并存维护一段时间——但它把「一次性赌上整个系统」拆成了「一次切一小档」的可控风险序列。你不是在和旧单体决斗,而是在它身上长出新检索,直到旧的没流量、自然枯死。


三、抽象分支:在「模型抽象层」背后换 provider

20 章 规模化要引入 AI 网关(多 provider 故障转移)。但「调用模型」这件事被编排层里几十处直接调用缠死了——你不可能在外面拦一层。这时用抽象分支(14 章):不开长命分支,全程在主干,靠一层抽象让新旧实现并存。

   抽象分支五步,全程主干可发布:

   ① 插入抽象层:所有地方都改成调 ModelClient 接口,不再直连某 provider SDK
        编排层 ──▶ 【ModelClient 抽象】──▶ 直连 provider A(旧)

   ② 抽象层背后写新实现:接 AI 网关(多 provider + 容灾 + 语义缓存)
        编排层 ──▶ 【ModelClient】─┬─▶ 直连 provider A(默认,开关控制)
                                  └─▶ AI 网关(新,先不放量)

   ③④ feature flag 逐步把流量切到网关,出问题随时切回
   ⑤ 网关稳了,删掉「直连 provider」的旧实现

架构智慧:「开个长命分支慢慢改、改完再合」是直觉,却是规模化重构最贵的反模式——你在制造一颗『合并核弹』,引信时间还由别人(也在改主干的同事)决定。 抽象分支把它倒过来:先立一层「插座」(ModelClient),让新旧实现都能插上去,再在主干上不慌不忙地切。多写一层抽象的成本,买的是「随时能停、能发、能回退」的安全感。

顺带,这层 ModelClient 抽象本身就是 AI 系统该有的接缝——provider 涨价、限速、被封、出新模型……「换模型」在 AI 时代是高频事件,把它收在一个接口背后,是 08 章「在最可能变的地方留好接缝」的标准兑现。


四、并行运行 / 影子流量:给「换模型、换检索」兜底信心(AI 迁移杀手锏)

第二、三节都给了你「切换开关」。但切之前,你凭什么相信新检索 / 新模型是对的? 跑通测试?真实流量永远比你的测试用例刁钻——AI 系统更是如此,因为它本来就没有「正确答案」可断言。

最硬核的办法是并行运行 / 影子流量(14 章):新旧两套对同一批真实请求同时跑,旧的结果返回用户(用户无感),新的结果在背后和旧的悄悄比对。

   场景 A:换检索算法(纯向量 → 混合+重排)

                ┌──▶ 旧检索 ──▶ chunks_A ─────────▶ 返回给用户 ✅
   真实查询 ──┬─┤
              │ └──▶ 新检索 ──▶ chunks_B ──┐
              │                            ▼
              │                  用 eval 给两边召回打分
              │                  一致/更好 → 信心+1;更差 → 落日志排查

   场景 B:换模型 / 接网关
                ┌──▶ 旧模型 ──▶ 答案_A ──────────▶ 返回给用户 ✅
   真实问题 ──┬─┤
              │ └──▶ 候选模型 ─▶ 答案_B ──┐
              │                          ▼
              │            LLM-as-judge / 规则评分 比对答案质量
              │            质量不退化 → 才敢切;退化 → 别切,继续修

这正是 AI 迁移最该养成的肌肉记忆,也呼应 14 章的 AI 视角:

让 AI 快,让比对慢;速度交给模型,信心交给数据。

AI 让你飞快产出「新检索 / 新实现」,但对不对、退没退化,交给「真实流量 + eval」来审判,而不是交给你的自信。 关键纪律(同 GitHub Scientist):旧实现的结果永远返回用户;候选实现在背后跑、吞掉它抛的异常(绝不让实验代码搞挂线上);等「不一致 / 质量退化率」降到够低,才真正切流。

📎 这套机制的活样板是 GitHub 的 Scientist 库,14 章真实案例 有详解。AI 时代它的价值不降反升:它天生就是给「AI 改的 / AI 生成的代码」兜底信心的最佳搭档。


五、零停机数据迁移:换掉向量库

规模化后,MVP 那个凑合的向量存储(比如 pgvector)扛不住了,要换成专用向量库(Milvus / Qdrant)。数据最难改——代码能蓝绿回滚,数据只有一份、改坏往往救不回。所以走 14 章expand→contract 五步,每步可回滚:

   ① 双写:新文档入库时,同时写【旧向量库】和【新向量库】。检索仍走旧。
        → 退路:停掉对新库的写即可,旧库一直是权威。

   ② 回填:把存量文档批量灌进新库。⚠️ AI 特有的代价——
        这一步要把所有历史文档块「重新 embedding」,既花钱又花时间,
        要分批限速、可中断重来。
        → 退路:回填只写新库,随时可停。

   ③ 影子读校验:检索两边都查,返回用户的仍是旧库结果;
        比对「新库召回」vs「旧库召回」是否一致(就是第四节的影子流量!)。
        → 退路:只比对不切流,召回不达标就继续修。

   ④ 切读:一致率够高,把检索切到新向量库。此时仍在双写。
        → 退路:读再切回旧库,旧库一直被双写、依然新鲜。

   ⑤ 清理:观察稳定后,停掉对旧库的双写,下线旧库。
        → 唯一不可逆的一步,放最后、留足观察期。

架构智慧:数据迁移的灾难几乎都源于「一刀切」——半夜停服、跑迁移脚本、上线、祈祷。正确姿势是拉成长链条,让「旧库一直是权威」成为随时能跳的安全网,直到最后一刻才剪断。AI 系统这里唯一的特殊,是第②步回填要「重新 embedding」——这是一笔实打实的算力账,要当成本来规划(呼应 19 章 步骤 ②)。


六、接旧系统别被污染 + 让架构不再腐化

防腐层(ACL): AI 客服要接企业那套用了十年的订单 / 支付系统——字段诡异、概念杂糅。别让它的脏模型渗进你干净的退款服务。架一道防腐层翻译隔离(14 章):

   ┌──────────────┐    ┌────────┐    ┌────────────────────┐
   │ 退款服务       │───▶│ 防腐层  │───▶│ 旧订单/支付系统       │
   │ (干净的新模型)  │◀───│ ACL   │◀───│ (脏字段、诡异状态)    │
   └──────────────┘    └────────┘    └────────────────────┘
        新服务只跟「翻译干净的接口」打交道,旧系统的腐烂渗不进来。

适应度函数: 费力拆出来的漂亮边界,怎么保证不被后续几百次提交又揉回一坨泥?把架构约束写成会失败、能卡 CI 的自动化测试(14 章),给架构装上免疫系统。这个 AI 客服该写的几条:

你在乎的架构约束写成适应度函数(进 CI)
编排层不准直接 import 向量库内部类依赖检查:扫到就 fail
所有模型调用必须经过 ModelClient 抽象(不准散落直连 provider)依赖检查:直连 provider SDK 就 fail
退款服务的每个写操作必须带幂等键静态检查 / 契约测试:缺 key 就 fail
检索服务 p99 < 200ms性能测试:超了就 fail
检索不准跨租户召回集成测试:跨租户能查到就 fail(安全红线)

架构智慧:架构不是画完图就定型的雕像,是要持续维护的活系统;活的东西没有免疫系统必然腐化。适应度函数不阻止系统长大,它只阻止系统在长大时烂掉。 没有它,你今天辛苦拆出的每一道边界,都只是在等一个赶时间的下午被人 import 穿。


📌 真实案例

本章的手艺都来自 14 章的真实案例,拆迁路上最值得记的两条:

  • GitHub Scientist(并行运行的活样板):重构高危的权限判断时,旧逻辑结果照常返回用户、新逻辑在背后对真实流量比对,靠数据而非自信建立信心。AI 时代,它就是给「换模型 / 换检索 / AI 改的代码」兜底的最佳搭档。
  • Segment《Goodbye Microservices》(拆过头的警钟):为「故障隔离」把系统拆得过碎,结果改一个公共库要花一周,最后回退到单体。提醒你:服务数量从不是目标——先模块化单体把边界划对,再按瓶颈按需抽服务。

🎯 随堂检验

🤔要把 AI 客服的检索从「纯向量」换成「混合检索+重排」,上线前最稳妥的做法是?
  • A跑通离线评测就直接全量切到新检索,旧的删掉
  • B用影子流量:旧检索结果照常返回用户,新检索对同一批真实查询在背后跑,用 eval 比对召回质量,等不退化/更好再灰度切流
  • C开个长命分支把新检索彻底改好,一次性合并上线
🤔把单体 AI 客服改造成更好的结构,第一步最该做的是?
  • A立刻拆成「编排/检索/工具/计费」四个独立部署的微服务
  • B先在一个部署单元内把四条业务边界(限界上下文)划清成模块化单体,只把「确实需要独立伸缩/发布」的检索、退款按需抽成服务
  • C推倒重写一版干净的微服务架构

本章小结

  • 别推倒重写:旧 AI 客服难看的代码里沉淀着踩坑经验;重写 = 追移动靶 + 清零血泪 + 要求对手等你。唯一主路是渐进演进
  • 先找缝、先模块化单体:按业务能力(对话/检索/工具退款/计费)找限界上下文;先在进程内划清边界,只把「确实要独立伸缩/发布」的检索、退款抽成服务。
  • 绞杀者:在编排层和检索之间加路由开关,灰度把流量从旧检索切到新检索服务,旧的被绞杀下线。
  • 抽象分支:把「调用模型」收进 ModelClient 抽象,在它背后无停机地从「直连 provider」切到「AI 网关」——「换模型」是 AI 时代高频事件,这层接缝必留。
  • 并行运行/影子流量是 AI 迁移杀手锏:换检索、换模型都让新实现「陪跑」,用真实流量 + eval 比对质量。让 AI 快、让比对慢;速度交给模型,信心交给数据。
  • 零停机换向量库:双写→回填(注意「重新 embedding」的算力账)→影子校验→切读→清理,让旧库一直当权威安全网。
  • 防腐层隔离旧订单系统的脏模型;适应度函数把架构约束(模型必经抽象层、退款必带幂等键、不准跨租户召回)写进 CI,给架构装免疫系统。

承上启下:到这里,你已经把这个 AI 客服读懂(18)、设计(19)、演进(20)、拆迁(21) 走了一整圈——但它本质还是「对话 + 受控动作」。下一章 22 · AI 原生系统设计 把自主性再往上推一档:设计一个自主 Agent——让它自己规划、调用工具、多步把整个工单处理到底。自主性越强,17 章 那三个新约束咬得越狠,而这正好把我们引向最后的 AI 协同设计篇


相关链接

💬 评论