服务拆分与配置管理
难度:⭐⭐ | 高频指数:🔥🔥 | 应用岗相关度:★★★
面试回答
常见问法
- LLM 应用为什么不能一个进程跑到底,要拆服务?
- 你们项目按什么边界拆的?数据接入、检索、编排、模型调用怎么分?
- Prompt、模型版本、Feature Flag 这些”非代码配置”怎么管?
- 配置改了怎么生效,要不要重启?灰度怎么做?
- 多环境(dev / staging / prod)的密钥和配置怎么隔离?
- 跨服务的一次请求,怎么把链路串起来排查?
回答
LLM 应用和传统 Web 服务最大的区别是:链路里的每一段都有独立的变化频率和独立的故障模式。Prompt 一天改五次,模型一周换一版,向量库一个月扩容一次,编排逻辑可能两个月才动一次。如果都堆在一个进程里,任何一处改动都要重新部署整条链路,灰度根本做不细。
工程上,拆服务的目标不是微服务而微服务,而是让配置入口、变化频率、故障域三件事对齐。比方说数据接入层经常要适配新格式,就单独拆;模型调用层要做厂商切换和限流熔断,也单独拆;编排层是业务逻辑变化最快的,必须独立部署。检索和后处理同理。每一段服务对外只有清晰契约,内部可以自由演进。
配置管理上的关键认知是:Prompt、模型路由表、Feature Flag 是代码以外的”第二份代码”,要有版本、有回滚、有灰度、有审计。线上很多事故不是代码 Bug,而是 Prompt 改坏了或者 Feature Flag 推错了环境。面试时要表现出的判断力是:知道哪些东西必须放配置中心、哪些可以塞环境变量、哪些根本不该让运行时去改。
追问
- Prompt 应该放代码仓库还是配置中心?为什么?
- 配置热更新有哪些坑?怎么做到”改了就生效,但又不影响正在处理的请求”?
- 模型版本的灰度策略一般怎么设计?按用户、按流量比例还是按场景?
- 一个请求经过 5 个服务,怎么把日志串起来?trace ID 在哪一层注入?
- 密钥(OpenAI Key、数据库密码)放在哪?为什么不能放 Git?
- 单体 + 配置文件什么时候够用?什么时候必须上配置中心?
原理展开
1. 为什么 LLM 应用更需要拆服务
传统 Web 应用拆不拆服务,主要看团队规模和模块解耦诉求。LLM 应用多一个更硬的理由:链路里每一段的变化频率天差地别。
举个真实节奏:
| 层 | 典型变化频率 | 变化驱动者 |
|---|---|---|
| 数据接入(解析、清洗、入库) | 每周 | 数据团队 / 业务方新接源 |
| 向量索引 / 检索 | 每两周 | 算法调参 |
| Prompt / 编排逻辑 | 每天 | 算法 + 产品 |
| 模型调用层 | 每月切厂商或换版本 | 平台团队 |
| 后处理(格式化、审核、改写) | 每周 | 业务 + 合规 |
如果全在一个进程里,任何一处小改都要走完整发布流程,发布频次被最快变化的那一层拖死,每次发布的回滚半径还特别大。拆开之后,编排层可以一天发好几次,模型调用层稳稳压住不动。
另一个动因是故障隔离。模型厂商挂掉是常态,向量库慢查询是常态,外部工具超时也是常态。拆服务后这些故障的传染面被切断,否则一处卡住,整个进程的 worker 池都被堵死。
2. 典型拆分边界
工程上比较稳的拆法,是按”职责 + 变化频率 + 依赖方向”切:
[Gateway / BFF]
│
▼
[编排服务 (Orchestrator)]
│ │ │ │
▼ ▼ ▼ ▼
[检索] [模型调用] [工具调用] [后处理]
│ │
▼ ▼
[向量库] [LLM 厂商]
│
▼
[数据接入服务 (离线,独立部署)]
每一层的职责边界:
- Gateway / BFF:鉴权、限流、租户隔离、请求规范化
- 编排服务:业务流程,决定调用顺序、Prompt 选型、何时 Fallback
- 检索服务:query 改写、向量检索、重排、片段拼装
- 模型调用服务:屏蔽多厂商差异,统一重试、限流、缓存、token 统计
- 工具调用服务:Function calling、外部 API、内部接口
- 后处理服务:格式化、内容审核、敏感词、PII 脱敏
- 数据接入:离线的文档解析 / Embedding / 入库,独立调度
判断标准是:能不能独立部署、独立监控、独立扩容、独立 oncall。 切不动这四件事,就不算真的拆开。
3. 单体 vs 微服务的取舍
不是所有项目都该一上来就拆。判断维度:
| 维度 | 单体 + 配置文件 | 微服务 + 配置中心 |
|---|---|---|
| 团队规模 | < 5 人 | 多团队协作 |
| 变化频率 | 整条链路一起改 | 各层节奏差 5 倍以上 |
| 故障要求 | 允许重启恢复 | 需要分层熔断 |
| 灰度粒度 | 不需要 | 按场景 / 用户分流 |
| 运维成本 | 一台机器一个进程 | 有 K8s / Service Mesh |
| 调试链路 | 本地一把过 | 必须依赖 trace 系统 |
实战里我倾向于:第一个版本写成单体但模块清晰,每个模块的接口当作未来服务的契约来设计;到了第二三个迭代,业务清楚了再按需拆出独立服务。先拆模块边界,再拆物理服务,顺序反了会很痛。
4. 配置管理的分层
LLM 应用的配置远不止数据库连接串。要分清几类,放的地方不一样:
# 1. 基础设施配置(环境变量 / K8s ConfigMap)
DATABASE_URL: ...
REDIS_URL: ...
LOG_LEVEL: info
# 2. 密钥(密钥管理服务 / Vault / KMS,绝不进 Git)
OPENAI_API_KEY: ${secret:openai_key}
ANTHROPIC_API_KEY: ${secret:anthropic_key}
# 3. 模型路由表(配置中心,支持热更新)
routes:
- intent: code_generation
primary: claude-sonnet-4
fallback: [gpt-4o, deepseek-v3]
- intent: summarization
primary: gpt-4o-mini
# 4. Prompt 模板(配置中心 + 版本化)
prompts:
rag_answer:
version: v17
template: |
根据下面的资料回答问题...
# 5. Feature Flag(专门的 FF 平台)
features:
enable_speculative_decoding: { rollout: 10%, env: prod }
new_rerank_model: { whitelist: [team_alpha] }
这五类东西的生命周期完全不一样:基础设施配置发布时定型,密钥要严格审计,模型路由要能秒级生效,Prompt 要能回滚到上一版,Feature Flag 要能按用户灰度。混着放就是事故源。
5. Prompt 和模型版本怎么管
这是 LLM 项目特有的痛点。Prompt 是代码以外的”第二份代码”,绝大多数线上质量回归都和它有关。
实战上推荐的最小集:
- 版本化:每个 Prompt 有唯一 ID + 版本号,调用时同时记录到 trace
- 环境隔离:dev / staging / prod 各自一份,不能共享
- 审计 + diff:谁在什么时候改的,diff 留痕
- A/B + 灰度:新版本先 10% 流量,盯指标再放量
- 回滚一键:发现指标掉,能秒级切回上一版
模型版本同理。gpt-4o-2024-08-06 和 gpt-4o-2024-11-20 是两个东西,不要在代码里写死模型名,全部走路由表,方便切换和回滚。面试官真正想问的是:你有没有意识到这是配置而不是代码。
6. 配置热更新和”安全热更”
热更新听起来简单——拉新配置 + 替换内存里的旧值。坑在于:
正在处理的请求怎么办? 一个请求处理到一半,Prompt 突然变了,逻辑就乱了。正确做法是请求级快照:
def handle_request(req):
# 进入时取一次快照,整个请求生命周期都用这份
config = config_center.snapshot()
prompt = config.prompts["rag_answer"]
model = config.routes.pick(req.intent)
...
怎么避免推坏配置直接打爆全站? 配置中心要支持:
- 灰度发布:先 1% 实例生效,盯一会儿再全量
- 健康检查:实例拉到新配置后做一次自检,失败自动回退
- 强制回滚:上一版本永远保留,一键切回
Schema 校验:配置中心推之前要按 schema 校验,避免缺字段或类型错。简单一条 JSON Schema 能挡掉 80% 的低级错误。
7. 灰度发布与影子流量
LLM 应用的灰度比传统服务更难——指标不是 5xx 率而是质量。模型换了,接口都 200,但答非所问。
常用三种姿势:
- 流量比例灰度:新模型 / 新 Prompt 先吃 5%,盯人工标注 + 自动评估
- 按场景灰度:低风险场景(内部工具)先上,高风险(对客)后上
- 影子流量(shadow):新版本和旧版本同时调用,只用旧版本返回给用户,新版本结果落日志做离线对比
影子流量是 LLM 场景特别值钱的能力——不影响线上体验就能拿到新模型在真实流量下的表现。代价是 token 成本翻倍,所以一般只跑一小段时间或一小比例流量。
8. 跨服务追踪和可观测性
链路拆了之后,没有 trace 等于盲调。一个用户请求经过 Gateway → 编排 → 检索 → 模型调用,少则 3 段多则 8 段,任何一段慢了都得能定位到。
最小可观测三件套:
1. Trace ID
- 在 Gateway 注入
- 通过 HTTP header / gRPC metadata 在所有服务间透传
- 写到所有日志、所有 LLM 调用记录、所有指标
2. 结构化日志
- 每条日志带 trace_id / span_id / service_name
- 关键事件(LLM 调用、检索、Fallback)必须 log
- LLM 调用要 log 模型名、Prompt 版本、token 数、延迟
3. 指标
- 端到端延迟(P50 / P95 / P99)
- 每段服务的延迟
- LLM 成本(token × 单价)
- Fallback 触发率
- 质量指标(如果有自动评估)
LLM 调用必须额外记录的字段:模型名、Prompt 版本、输入 token、输出 token、是否走缓存、是否走 Fallback、单次成本。这些数据线上排查、做 cost attribution、做容量规划都要用到。
9. 一个真实事故的复盘视角
讲一个面试爱听的小故事框架——配置改坏导致全站答非所问:
- 算法同学在配置中心改了 RAG 的 Prompt 模板,把”根据下面资料回答”改成了一个实验版本
- 配置直接推 prod,没走灰度
- 用户反馈瞬间起飞,质量指标暴跌
- 回滚用了 8 分钟,因为没有”一键回滚到上一版”
复盘出来的改造:
- Prompt 配置必须走灰度(5% → 20% → 100%)
- 配置中心要有 diff 预览和审批流
- 上一版本永远保留,回滚一键
- Prompt 变更要进 CI,跑一遍离线评估集
这种”配置事故”在 LLM 项目里比代码 Bug 还常见,能讲出这种细节,面试官就知道你真的上过线。
对比总结
| 场景 | 推荐 |
|---|---|
| 第一版 demo / 内部小工具 | 单体 + 环境变量 + 一份 YAML |
| PoC 转生产,团队 5-10 人 | 模块清晰的单体 + 简易配置中心 |
| 多团队协作 / 多场景灰度 | 按职责拆服务 + 配置中心 + Feature Flag |
| 模型频繁切换 / 多厂商 | 独立的模型调用服务 + 路由表 |
| Prompt 一天改几次 | Prompt 配置中心 + 版本 + 灰度 |
| 强合规场景 | 密钥走 KMS / Vault,配置全审计 |
| 同时跑多套实验 | 影子流量 + 在线 A/B |
易错点
- 把”拆服务”当成 KPI——服务数量不是优势,能独立部署、独立监控才是
- 把 Prompt 写进代码仓库——改一次发一次,灰度做不了,回滚靠 revert
- 密钥直接进 Git 或写死在镜像里——这是 P0 事故的最常见来源
- 配置热更新没做请求级快照——同一个请求里读到两个版本的配置,逻辑直接乱
- 不区分基础设施配置、业务配置、Feature Flag——一锅炖之后改任何东西都得重启
- trace 没在 Gateway 注入——后面服务再补,链路永远拼不全
- 模型名写在代码里——切版本要发布,A/B 做不了,成本归因也做不了
记忆技巧
记三组锚点:
- 拆服务三对齐:配置入口对齐、变化频率对齐、故障域对齐
- 配置五分层:基础设施 / 密钥 / 路由表 / Prompt / Feature Flag
- 热更三件套:请求级快照、灰度发布、一键回滚
一句话总结:Prompt 和模型版本是代码以外的第二份代码,必须像代码一样有版本、有灰度、有回滚。
面试速答版
LLM 应用拆服务的核心理由是链路里每一段的变化频率和故障模式都不一样——Prompt 一天改几次,模型一个月换一版,向量库更稳定。混在一个进程里,发布频次被最快的那一层拖死,灰度也做不细。
工程上一般按职责拆成 Gateway、编排、检索、模型调用、工具调用、后处理几层,加上离线的数据接入。配置上要分清基础设施、密钥、路由表、Prompt、Feature Flag 五类,各自放对地方。Prompt 和模型版本必须配置化、版本化、灰度化,不能写死在代码里,这是 LLM 项目最容易踩的坑。
面试加分版
如果想多讲一层,可以补:
- 影子流量是 LLM 场景独有的红利:不影响线上体验就能拿到新模型在真实流量下的对比数据,代价只是 token 成本
- trace 必须记录模型名、Prompt 版本、token 数——这三个字段决定了你能不能做 cost attribution 和事后归因
- 配置事故比代码事故更常见:Prompt 推坏一次就是 P1,所以配置中心需要 diff 预览、审批流、灰度和一键回滚
- 不要追求”零配置代码”——所有逻辑都靠配置驱动会把调试链路拉长到无法忍受,配置只放那些真正需要运行时调整的部分