微服务拆分实战:智能问答中模型、业务与知识库的边界划分

发布时间:2026/10/20 12:26:04
微服务拆分实战:智能问答中模型、业务与知识库的边界划分
1. 为什么非拆不可三个组件挤在一个进程里的慢性病我接手某个智能问答项目的时候系统还处于“一个进程装天下”的阶段。模型调用、业务逻辑、知识检索全部写在同一套代码里部署方式也简单——两台机器一个服务数据库和索引文件都在本地。上线头两个月确实省心但随着迭代深入问题开始一个接一个冒出来最后逼着我们不得不动手拆。先说最典型的三个“慢性病”如果你现在也正被这些问题困扰那基本上可以确定拆分这件事已经拖不起了。1.1 生命周期错位三类内容的更新节奏完全不同模型服务、业务服务、知识库这三者的迭代频率天然不一样。拿我们当时的情况举例模型版本平均一个月升一次业务功能每周都有小迭代知识库里的文档则是几乎每天都有更新。当它们全在同一个进程里时任何一方的变更都会影响全局。知识库里新增了一篇文档按理说这是个纯内容层面的变更却要走完整的发布流程——代码打包、测试回归、重启服务。模型升级就更痛苦因为模型服务跟业务代码耦合在一起升级模型意味着整个应用要重新发版业务功能哪怕一行代码没动也得跟着做全量回归。这种“牵一发动全身”的节奏时间一长必然拖垮交付效率。1.2 故障边界模糊一次模型超时就能拖垮整个业务第二个问题更致命就是故障隔离。当时我们用的是外部模型服务接口网络抖动和超时是家常便饭。但因为在单体架构里所有请求共用一个线程池模型调用的超时等待一旦一多线程池被占满接下来的业务请求全部排队接口响应时间从几百毫秒直接飙升到几十秒。印象最深的一次事故模型供应商那边出了一个线上故障接口大量超时我们的业务接口跟着全军覆没。用户端看到的不是“回答慢”而是“整个应用打不开”。这不是模型服务的问题却因为边界不清让整个业务系统背了锅。1.3 资源扩展维度完全不同CPU、GPU、存储混在一起互相拖累第三个问题是资源扩展的维度不一样。模型推理依赖GPU计算而GPU资源是有限的单卡跑不动就需要排队或单独管理业务接口吃的是CPU和内存靠水平扩展就能线性提升知识库则更偏重存储和检索引擎需要关注的是磁盘IO、向量索引的构建速度。三者放在同一个部署单元里你在扩容的时候只能按木桶原理来——为了让模型推理更快你得给整个服务加机器结果CPU和内存也跟着过量分配为了让业务支持更高并发你得扩容但模型部分并不会因此受益。浪费资源不说成本核算也混乱最后根本说不清钱花在了哪个环节。这三个问题叠加在一起结论其实已经很清晰拆是必须的问题只在于怎么拆。2. 模型服务的边界把推理能力当成独立产品来设计拆的第一步先动模型服务。我的建议是无论业务多复杂先把所有模型调用能力收敛成一个独立的服务对外提供统一的推理接口。这一步收益最大风险最小而且几乎是后续所有架构调整的基础。2.1 模型服务只回答“纯问题”不回答“业务问题”这是一个常被忽略但非常关键的设计原则模型服务的输入输出要跟业务语义彻底解耦。换句话说模型服务只接收“请根据以下资料回答xxx”这样的纯文本请求输出“答案文本引用片段置信度”这样的结构化结果它不应该感知用户身份、订单状态、权限等级这些业务概念。为什么非要这样我举一个反面例子。假设你在模型服务层直接设计了“带用户信息的对话接口”比如请求里传了user_id、order_id、权限标识等等。刚开始看着方便业务层调用也少写了一行代码。但接下来每一次业务变化比如新增了一个会员等级、改了一种订单状态模型服务必须跟着改接口——这就违背了拆分的初衷变成了把一个进程里的耦合拆成了跨网络的耦合反而更难受。正确做法是把模型服务当作一个“只会解题的工具人”。它接收标准格式的问题内部负责调用合适的模型、拼接Prompt、处理长文本然后返回答案。业务上那些“什么时候改用哪个模型”“要不要带历史对话”“答案能不能给这个用户看”全部留在业务服务里决策。2.2 无状态化与推理资源管理模型服务必须无状态化这是它能否水平扩展的前提。所谓无状态就是服务本身不保存任何对话历史、不持有用户上下文、不维护本地缓存每个请求都自带完整输入。这样做有两个直接好处第一扩容没有任何心理负担。流量上来了直接加副本就行不需要做会话粘滞也不用担心多副本之间状态不一致。第二模型推理的并发控制可以做得更纯粹。当时我们在模型服务里单独维护了一套排队机制核心逻辑很简单因为模型单次推理耗时比较高如果同时打进来的请求太多GPU资源一竞争每个请求都会变慢整体的吞吐量反而下降。我们通过信号量控制最大并发数多余的请求先排队而不是一股脑地涌进模型。这个控制逻辑在单体架构里不太方便做因为业务请求的频率模型和推理请求完全不一样混在一起很难设定一个合理的并发阈值。2.3 模型版本与灰度在服务内部做路由而不是让业务层感知模型升级是常态但“升级”这件事最好被关在模型服务内部。我们的做法是模型服务内部维护一个可配置的路由规则。比如线上跑着v1和v2两个版本默认流量全部走v1v2只接收5%的请求用来做效果对比。这个比例可以在配置中心动态调整不需要重新发布。等v2的效果验证稳定了把路由切到100%再慢慢下线v1。这样做的好处显而易见模型版本升级对业务服务完全透明业务方不需要修改任何代码就能享受到新模型的收益。如果你把版本管理放到业务层去做每个调用模型的业务方都要跟着适配一遍一个模型升级要联动五六个服务一起发版倒霉的又是运维和研发。模型服务拆完之后你会立刻感受到一个明显变化模型升级变得轻量了故障影响面收窄了扩容时也终于可以理直气壮地只给模型服务加GPU资源。下一步要处理的就是业务服务和知识库之间的边界。3. 业务服务与知识库的分界目录归我词典归你模型服务拆完之后剩下最难也最容易扯皮的部分就是业务服务和知识库怎么分。很多人觉得这有什么好分的知识库不就是一堆文档和索引吗做一个独立服务把检索能力暴露出来不就行了原则上是这样但实操中“哪些数据放知识库、哪些数据留业务库”“谁有权改索引”“缓存归谁管”这类问题没那么容易拍板。3.1 知识库的核心职责文档接入、清洗、切片、向量化、检索知识库服务到底该管哪些事我可以列一个我们实践下来比较成熟的边界清单文档接入支持不同渠道上传的文档PDF、Word、Markdown、网页抓取等统一转换成标准格式。内容清洗去噪、去重、格式归一化把无效的页眉页脚、导航信息处理掉。切片根据标题层级和段落语义把长文档切分成适合检索的片段记录每个片段对应的原文位置。向量化与索引构建把切片文本转换为向量写入向量索引库同时保留关键字索引做混合检索。检索接收查询文本返回最相关的Top-K个片段附带相似度分数和来源标识。一句话概括知识库负责“我知道什么东西在哪里”但不关心业务方拿这些东西去干什么。3.2 业务服务保留什么Prompt编排、会话状态、权限过滤那么业务服务还剩哪些活这些活有个共同特点——它们都带有强烈的业务属性或者需要感知当前用户的上下文。Prompt编排决定有哪些系统指令、参考哪些知识片段、要不要带历史对话。会话状态管理维护多轮对话的上下文处理会话超时和重置。权限过滤根据用户身份确定能看到哪些知识库内容、哪些文档的检索结果需要拦截。业务数据拼装把模型输出的答案与订单信息、用户信息、产品信息等结构化数据融合最终组装成用户看到的内容。这里有个很常见的争议点权限过滤到底该放业务层还是知识库层我的答案很明确——放业务层。原因很简单知识库是通用能力它不应该背某个具体业务的权限规则。同一个知识库可能服务多个业务线每条业务线的权限模型都不一样。你让知识库适配所有业务的权限规则结果就是知识库服务变得越来越重最后变成一个大泥球。正确姿势是知识库只负责按文档级别或者目录级别做粗粒度的可见性标识精细到字段级别、用户级别的过滤一律在业务服务里处理。3.3 判断数据归属的三个问题遇到具体数据不知道怎么分的时候我们内部用一个非常简单的判断框架问三个问题就能有答案第一这个数据的直接消费者是谁如果一份数据既被检索流程消费又被业务逻辑直接查询那它大概率需要双写——一份进知识库做索引一份留业务库做关联。比如一篇产品说明书全文内容给知识库用于检索它的产品编号和适用版本要留在业务库里跟订单体系做关联。第二这个数据的变更频率和触发方是谁如果数据的变更来自内容运营那归知识库管如果变更来自业务流程那归业务库管。内容运营不会天天改订单状态业务流程也不会去改文档全文。以变更触发方来判断归属基本不会出大错。第三这个数据挂了会影响什么范围知识库服务的故障不应影响核心交易链路业务服务的故障也不该阻断知识库的内容更新。两者的依赖关系要画成“弱依赖”不是“强依赖”。3.4 缓存和元数据的归属最后聊一下缓存和元数据。知识库的检索结果通常有较强的时效性但也存在一些高频不变的检索场景比如“公司介绍”“常见问题”这些内容。我们的做法是在业务服务侧做二级缓存缓存知识库的检索结果同时保留一个短TTL比如5分钟。元数据方面文档的标题、作者、上传时间、所属目录这些“关于文档的文档”由知识库统一维护。业务数据里如果引用了文档ID只需要存ID不冗余存文档信息。这样知识库更新文档信息时业务侧不需要同步改数据。边界理清楚之后接下来的问题是物理层面怎么连是业务服务直接调用知识库和模型服务还是中间加一层网关这就涉及到拆分后的网络拓扑设计。4. 三种拓扑方案直连、网关与事件驱动拆分完成并不等于架构落地你还要决定服务之间怎么通信。不同的拓扑方案直接决定了整个系统的复杂度上限和排查问题的难度。我按实际项目的演进路径把三种主流方案都分析一遍。4.1 直连模式小规模快速起步最简单的方案业务服务直接通过HTTP或RPC调用模型服务和知识库服务一个调用链路上只有三个节点没有多余的中间层。这种模式的优缺点都非常鲜明维度表现优点链路短、延迟低、排障直观调了谁一眼就能看到优点部署简单不需要额外维护网关或消息队列缺点业务服务要感知所有下游服务的地址和细节调用关系会越来越乱缺点公共逻辑认证、限流、熔断没有统一收口的地方我的建议是如果你的业务只有一条产品线调用方不超过两三个直连模式足够用了。不要一听“微服务”就觉得必须上个网关没必要。我们一开始也是直连大概持续到接入第二个产品线时才开始别扭起来。4.2 网关聚合模式企业级标配第二个方案是引入一层聚合网关。这层网关的主要职责不是网络转发而是做服务编排收到前端请求后由网关决定调用哪个业务服务、是否并行调用知识库、如何组装最终响应。这个模式和直连最本质的区别在于业务服务不再直接依赖知识库的具体接口而是通过网关把“检索知识库”作为一个步骤编排进去。这样做有几个好处统一收口认证、限流、超时控制、熔断策略全部沉淀在网关层不用每个服务各做一套。减少客户端感知前端只需要对接网关一个入口不用关心背后到底调了几个服务。接口更稳定网关层可以对下游做适配下游接口变了网关改改映射就行业务服务不用动。代价是多了了一跳网络开销以及聚合网关本身会成为新的复杂点。如果网关编排逻辑写得太重它会退化成单体时代的“service层”只是从代码耦合变成了配置耦合。4.3 事件驱动模式异步与解耦的代价第三种方案适用于对时效性要求不高、但需要深度解耦的场景——事件驱动。典型场景是知识库的内容更新。举一个实际例子某个产品线的运营同学更新了一份产品知识文档这份文档会影响很多线上正在进行的会话。如果走同步调用知识库更新完成后直接推给业务服务做缓存刷新链路很直接但耦合紧。如果用事件驱动知识库更新后只发一个“内容已变更”的事件业务服务订阅这个事件收到后再去拉取增量内容、刷新本地缓存。事件驱动的好处是业务服务和知识库之间完全断了直接依赖知识库挂了不影响业务服务启动和运行业务服务也不关心知识库内部怎么重建索引。坏处也直接排查问题困难一个操作触发了一串异步事件链路追踪需要额外工具才能串起来消息丢失和重复消费的处理也要提前设计。我给的判断标准是这样的同步能解决的事不要上异步异步方案只用来解决“不想让调用方等待”的事。一个知识库更新本来几十毫秒就能返回没必要为了解耦而解耦。拓扑定下来之后绕不开的就是接口层面和数据层面怎么协同。拆分后的服务之间不是“断开关系”而是“把关系通过更清晰的契约串起来”。5. 拆分后的协同接口契约、链路追踪与数据一致性5.1 接口契约版本、错误码、幂等设计拆分以后服务之间通过接口通信接口的设计质量就是系统的生命线。版本策略模型服务和知识库服务的接口必须从第一天就带上版本号比如/v1/search、/v2/generate。我们的策略是小改动向后兼容大改动直接升版本旧版本保留至少两个迭代周期再下线。这一步看着多余但实际帮我们避免过很多次“上游升级下游炸”的事故。错误码设计不要只返回“500”这种笼统的HTTP状态码内部还要有业务错误码。比如知识库服务可以定义1001表示索引不存在、1002表示文档未就绪、1003表示检索超时。这样业务服务才能根据错误码做出差异化处理而不是一律当作系统异常去报警。幂等设计模型调用和检索调用要支持同一个请求标识重复提交而不产生副作用。因为跨网络的调用超时重试是常态如果没有幂等重试就可能带来重复扣费、重复写入等麻烦。我们的做法很简单请求里带一个request_id服务端记录最近处理过的ID重复请求直接返回上一次的结果。5.2 链路追踪让一次请求跨越三个服务还能串起来拆分之前日志都在一个进程里有问题看一个日志文件就行。拆分之后一次用户请求会经历业务服务、模型服务、知识库服务出问题的时候三个服务的日志各写各的如果没有关联排障基本靠猜。我们的方案是在入口处生成一个全局唯一的trace_id通过HTTP Header透传到下游所有服务。每个服务打印日志时都带上这个字段日志系统里按trace_id一查三个阶段分别耗时多少、走了哪台机器、返回了什么结果一目了然。另外有个小细节容易被忽略调用模型服务和知识库服务的延时数据要用打点的方式独立记录。我们当时把模型服务的TP50、TP95、TP99延时单独做了面板运维同学每天扫一眼这两个指标比盯业务接口的健康状态更能提前发现模型侧的问题。5.3 知识库更新如何通知业务服务最后说一下数据一致性的问题。知识库的内容更新后业务服务侧如果做了缓存就会出现一段时间的“旧数据”。这个可以接受关键是多久能恢复。我们的做法分两层一层是前面提到的TTL过期兜底——缓存最多5分钟必须重新拉取另一层是事件通知——知识库发布更新事件业务服务收到后主动把相关缓存打失效。两者配合缓存不一致的时间窗口可以控制在几秒以内。如果连几秒的延迟都受不了那就要考虑把知识检索做成同步直连完全不做缓存用性能换一致性。取舍的依据还是业务场景——不太重要的推荐位内容缓存几秒没什么影响用户在线的法律咨询问答答案引用的法规必须是最新版本那就别过度缓存。拆分的思路和技术方案都讲完了最后说说实操落地中我们踩过的坑和一些经验判断这部分可能是对你最有用的。6. 实操中的几个坑从拆分顺序到过度设计6.1 最典型的翻车现场一拆到底结果联调成本爆炸我见过一个很典型的反面案例我们内部叫A项目团队雄心勃勃把模型服务、业务服务、知识库服务、再附带一个文件处理服务、一个用户画像服务一口气拆成了五个独立部署的微服务。架构图画出来非常漂亮但真正联调的时候直接陷入泥潭——改一个接口参数涉及四个服务同步改、同步发布本地开发环境要同时起五个服务一启动就是半个多小时。这就是典型的“过度拆分”。微服务拆分是有代价的每一个服务都带来了部署成本、运维成本、联调成本、监控成本。如果这些服务彼此之间是强同步调用关系实际上你只是把一个单体应用变成了五个需要同时发版才能工作的“分布式单体”复杂度只增不减。我后来总结了一条原则“按变更频率分不按功能模块分。”功能模块可以内聚在一个服务里但如果两个功能模块永远一起变更拆分它们毫无意义反过来如果两个模块的变更完全独立即使功能上能放在一起拆了也值得。6.2 推荐的拆分顺序先模型再知识库最后事件化如果让我给一个刚起步的团队做规划推荐的顺序是第一步先把模型服务独立出来。原因是它的资源模型最特殊GPU、变更频率高、故障影响最不可控。这一步做好收益立刻就能体现。第二步把知识库从业务服务中分离。判断标准是看业务里是否有多个场景需要检索同一批文档。当第二个业务方开始想用同一个知识库时就是独立的最佳时机。如果只有一条业务线在用知识库可以先不做成独立服务而是在代码层面通过Module/包的方式做逻辑隔离等需求明确了再物理拆分。第三步再考虑事件驱动改造。这个要谨慎异步是一把双刃剑。只有当“同步调用导致用户体验明显变差”或“服务间依赖明显影响可用性”时才值得引入消息队列。6.3 拆分之后运维视角必须补位拆分完成不算结束反而只是运维工作的开始。你需要为每个服务定义独立的健康检查、日志目录、监控面板和报警阈值。尤其是模型服务建议单独关注三个指标请求排队长度、单次推理耗时、GPU利用率。这三个指标能直接反映模型服务是“资源不够”还是“代码有bug”还是“上游请求过载”。另外要留意服务间的超时配置要分级。模型服务延迟高它的客户端超时时间要设置得比知识库宽松一些比如模型服务设置10秒超时知识库设置3秒超时。网关层再统一设置一个总超时时间比如12秒防止级联等待。拆了三次之后我最大的感受是架构拆分不是一步到位的终点而是一系列基于现实问题的渐进决策。每次拆分都要回答同一个问题——当前最痛的耦合点在哪里拆掉它能换来什么代价是什么。模型服务、业务服务、知识库这三者之间的边界本质上不是画在技术上的而是画在“变更频率、故障范围、资源类型”这三条线交汇处的。先理清这些底层约束再动手拆每一步就都会走得比较踏实。