Chapter 04 · Engineering
工程落地:部署、配置与架构
03 章解决的是知识资产化——Skill 把经验沉淀为可复用组件;本章回到工程问题:这些组件如何在真实环境中部署、配置、切换与持续运行。
本章覆盖 4 道原题
- Q9 — 可插拔 RAG、一键配置切换,是不是热更新?(概念辨析)
- Q10 — 工厂模式的优点是什么?(设计模式)
- Q11 — 本地部署要考虑什么?配置管理放在哪里?(部署清单)
- Q13 — 第一个项目后台怎么搭建的?(项目经历)
Q9可插拔 RAG、一键配置切换,是不是热更新?
概念边界——能否区分配置热更新与代码热更新两个层级,并讲清"一键切换"背后的完整实现链路。
参考答案
结论先行:属于配置热更新(hot reload 的配置层级),不属于代码热更新。
热更新(hot reload)的严格定义是不重启进程让新代码或新配置生效,它分两级:
| 层级 | 含义 | 典型实现 | 一键切换 RAG 是否属于 |
|---|---|---|---|
| 配置热更新 | 运行时更换参数,或在已加载的实现集合内重新选择 | Nacos / Apollo 配置推送 + 监听回调 | 是 |
| 代码热更新 | 不重启进程替换实现逻辑本身 | Erlang hot swap、Java agent 字节码替换 | 否——新增策略类仍需发版重启 |
可插拔 RAG 的结构是策略模式 + 注册表 + 工厂:检索器、重排器、嵌入模型各自抽成接口,所有实现注册进注册表,配置决定实例化哪个。切换的对象正是 01 章讨论过的记忆介质——向量库、BM25 索引等检索后端。"一键配置切换"只是在已加载实现集合内重新选择,所以落在配置热更新一侧。
实现链路(以 Nacos 为例):
- 配置中心 watch:Nacos 1.x 用长轮询——客户端请求被服务端 hold 住 29.5 秒,期间用 MD5 比对配置是否变化;2.x 起改为 gRPC 长连接,配置变更由服务端直推
ConfigChangeNotify。 - 回调触发:客户端监听器收到变更,进入应用内回调。
- 工厂重建:工厂按新配置重新组装 RAG pipeline 实例。
- 原子替换引用:新实例就绪后一次性替换旧引用;旧实例处理完在途请求后释放,保证切换过程对调用方透明。
常见错误:模块级单例在启动时缓存了旧配置对象,配置中心推送之后实例并未重建,表现为"改了不生效"。这正对应 Spring 中 @RefreshScope 要解决的刷新边界问题——只有声明在刷新范围内的 Bean 才会随配置变更销毁重建。
「这套机制属于配置热更新:检索器、重排器抽成接口注册进注册表,配置中心通过 watch 推送变更——Nacos 1.x 是 29.5 秒长轮询加 MD5 比对,2.x 起是 gRPC 长连接直推——应用收到回调后由工厂按新配置重建 pipeline,原子替换引用,旧实例处理完在途请求再释放,全程不重启。但它不是代码热更新:切换只发生在已加载的实现集合内,要新增一种策略类,仍然要发版重启,这是边界。」
加分点
主动点破面试官考的是概念边界——"热更新"在不同语境下指代不同层级,先框定定义再回答,比直接回答"是/不是"高一个档次。
再补两个生产细节:灰度发布(配置中心按 IP 维度推送 beta 配置,先让一台机器吃到新配置)与配置历史回滚(推错配置一键回到上一版本),说明这套链路在生产环境如何被驯服。
追问预判
替换 pipeline 引用的瞬间,正在处理的请求怎么办?
应对思路
在途请求继续持有旧实例引用直至完成——引用替换是原子的,但旧实例不立即销毁,等引用计数归零或 drain 完成后再释放资源(关闭连接池、卸载索引)。这与负载均衡摘流量时的优雅下线是同一思路。
Nacos 长轮询为什么 hold 29.5 秒而不是 30 秒?
应对思路
客户端超时设为 30 秒,服务端提前 500ms 返回,留出网络传输余量,避免恰好在网关或客户端超时边界上被误判为超时失败。
如果要求新增一种检索策略也不重启,怎么做?
应对思路
那就进入代码热更新领域:JVM 系用 Java agent / Instrumentation 做字节码替换,Python 可动态 import 新模块,Erlang 原生支持 hot swap。但工程上要权衡:动态加载引入版本一致性、内存泄漏与可观测性问题,多数生产系统选择"新增走发版、切换走配置"的折中。
Q10工厂模式的优点是什么?
能否跳出教科书定义,用开闭原则和真实创建场景说明工厂模式到底解决什么问题。
参考答案
核心优点五条,逐条都要能落到场景:
- 解耦创建与使用:调用方只依赖抽象接口,不需要知道具体类名与构造细节。
- 开闭原则:新增一种产品 = 新增一个类并注册,不修改已有调用代码。
- 依赖倒置:高层模块依赖抽象而非具体实现,依赖方向从"调用方 → 具体类"反转为"双方 → 接口"。
- 创建逻辑集中收口:鉴权、重试、默认参数、连接池等横切逻辑统一写在工厂里,而不是散落在每个调用点。
- 可测试性:测试时向工厂注入 mock 实现即可替换真实依赖。
LLM 应用中的实例:provider 工厂按配置切换 OpenAI / Anthropic / 本地模型;Embedding 与 Retriever 工厂支撑 Q9 的可插拔 RAG。LangChain 的 init_chat_model(model, model_provider) 就是官方提供的工厂函数,其 configurable 模式还支持运行时按 config 选择模型——工厂模式在主流框架中是一等公民。
from abc import ABC, abstractmethod
class Retriever(ABC): # 抽象接口:调用方只认它
@abstractmethod
def retrieve(self, query: str, top_k: int = 5) -> list[str]: ...
_REGISTRY: dict[str, type[Retriever]] = {}
def register(name: str): # 注册表装饰器
def wrap(cls: type[Retriever]):
_REGISTRY[name] = cls
return cls
return wrap
@register("bm25")
class BM25Retriever(Retriever):
def retrieve(self, query, top_k=5): ...
@register("vector")
class VectorRetriever(Retriever):
def retrieve(self, query, top_k=5): ...
def create_retriever(conf: dict) -> Retriever:
cls = _REGISTRY[conf["type"]] # 配置决定实例化哪个实现
return cls(**conf.get("params", {}))
注册表新增检索策略 = 加一个带 @register 的类,工厂与调用方零改动——这就是开闭原则的具体形态,也是 Q9 可插拔 RAG 的最小实现。
「工厂模式把对象的创建和使用解耦:调用方只依赖抽象接口,新增产品只需加类注册、不改旧代码,符合开闭原则和依赖倒置;同时创建逻辑集中收口,鉴权、重试、默认参数统一处理,测试时也容易注入 mock。在 LLM 应用里最典型的就是 provider 工厂——按配置切换 OpenAI、Anthropic 或本地模型,LangChain 的 init_chat_model 就是官方工厂函数。」
加分点
精准辨析是强加分项:简单工厂不在 GoF 23 个模式之列,它只是把 if-else 收口到一处的踏脚石,新增产品仍要改工厂函数本身,依然违反开闭原则;工厂方法用继承 + 多态才真正消除了分支。能讲清这条分界,说明模式不是背的。
反向加分:没有真实变化点时不要预铺脚手架——只有一个实现却套三层抽象,是对模式的滥用。
追问预判
工厂模式和策略模式什么关系?
应对思路
职责不同:工厂管"创建谁",策略管"运行时用哪种行为"。两者常组合出现——Q9 的可插拔 RAG 就是策略模式定义可互换的检索行为、工厂按配置创建具体策略实例。
工厂方法和抽象工厂的区别?
应对思路
工厂方法面向单一产品等级,每个子类工厂创建一种产品;抽象工厂面向产品族,一个工厂同时创建一组配套对象——例如某 provider 工厂同时产出该厂商配套的 chat 模型、embedding 模型与 tokenizer,保证族内一致。
Q11本地部署要考虑什么?配置管理放在哪里?
工程全局观——从模型推理到密钥管理,候选人脑中是否有一张把系统部署进客户机房的完整清单。
参考答案
第一问:本地 / 私有化部署的六个考虑维度。
- ① 模型推理:选型与资源是核心。显存按"参数量 × 精度字节数 + KV cache"估算,显存不足时用 AWQ / GPTQ 量化压到 4bit。推理引擎对比见下表。
- ② 数据不出域:私有化部署的核心动机——文档、向量、对话记录全部留在客户内网,这一条决定了后面所有依赖都要自带。
- ③ 依赖服务自带:向量库、数据库、Redis 全部打成离线镜像随系统交付,不能假设客户机房有外网。
- ④ 监控与日志:私有化环境无法远程登录排查,指标采集、日志落盘与告警必须随包内置。
- ⑤ 模型 license:开源模型的商用条款各不相同,交付前必须核对授权范围。
- ⑥ 离线交付流程:镜像导出、依赖打包、升级与回滚方案,都要在没有外网的前提下成立。
| 方案 | 底层机制 | 并发模型 | 适用场景 |
|---|---|---|---|
| Ollama | llama.cpp 封装,GGUF 量化,CPU/GPU 皆可 | 请求基本串行 | 单机开发、原型验证 |
| vLLM | PagedAttention 分页管理 KV cache + continuous batching | 高并发批处理,吞吐数倍于前者 | 生产服务,需 NVIDIA GPU |
第二问:配置管理分层回答。
- 单机 / 小规模:
.env文件 + pydantic-settings 读入并做类型校验,启动即发现配置错误。 - 集群:配置中心(Nacos / Apollo / Consul)做统一下发与热更新(衔接 Q9 的链路);K8s 环境则用 ConfigMap 挂载。
- 敏感信息与普通配置分离:API key、数据库密码进 K8s Secret 或 Vault,绝不进 git;普通配置走配置中心,二者生命周期与权限模型完全不同。
「本地部署先抓核心动机:数据不出域。围绕它展开六块——模型推理选型(开发用 Ollama,生产用 vLLM,靠 PagedAttention 和 continuous batching 撑并发,显存不够上 AWQ/GPTQ 量化)、依赖服务全部离线镜像自带、监控日志内置、模型 license 核对、离线交付与回滚方案。配置管理分层答:单机 .env 加 pydantic-settings,集群上配置中心或 ConfigMap,API key 这类敏感信息单独进 Secret 或 Vault,绝不进 git。」
加分点
Nacos 3.0(2025 年 4 月 GA)起内置 MCP Registry API——配置中心正在演化为 AI 应用的基础设施,注册的不只是服务与配置,还有模型上下文协议的工具端点。这条动态与本面试的 AI 应用开发方向直接相关,提到它说明候选人在持续跟进基础设施层的演进。
追问预判
vLLM 的吞吐为什么比 Ollama 高?
应对思路
两个机制:PagedAttention 把 KV cache 按页管理,消除显存碎片,同样显存能容纳更多并发序列;continuous batching 允许新请求在 batch 运行中途动态插入,GPU 不必等整批跑完,利用率大幅提升。Ollama 底层是 llama.cpp,面向单机交互场景,请求基本串行。
7B 模型部署需要多少显存?
应对思路
FP16 下权重约 7B × 2 字节 ≈ 14GB,再加 KV cache 与激活值,单卡 24GB 起步;AWQ / GPTQ 量化到 4bit 后权重降到 4–5GB,消费级显卡即可承载,代价是少量精度损失。
API key 放进私有 git 仓库行不行?
应对思路
不行。git 历史不可抹除,仓库权限会随协作扩散,密钥一旦提交即视为泄露,只能轮换。正确做法:Secret / Vault 管理,运行时注入环境变量,并在 CI 中加密钥扫描防止误提交。
Q13第一个项目后台怎么搭建的?
项目经历的颗粒度——选型理由、分层结构与量化结果能否经得起逐层追问,而不是技术名词罗列。
参考答案
这是经历题,没有标准答案,但有标准结构:用 STAR 框架组织叙述,每一段都给面试官留出可追问的"钩子"。
- S(背景与规模):项目解决什么问题,服务多少用户、多大数据量——规模决定了后面所有选型的合理性基准。
- T(任务):候选人在后台中负责哪一块,边界说清。
- A(行动):技术选型 + 理由、分层架构、关键机制决策——这是面试官追问的主战场。
- R(结果):量化指标收尾——QPS、P95 延迟、首 token 时间、成本变化。
以下是一个可信的样板架构(A 部分的参考栈),用于校准叙述的颗粒度:
叙述中有两个必须讲对的机制点,面试官的追问通常围绕它们展开:
机制点一:SSE 选型理由。LLM 输出是单向流——服务端持续推 token,客户端无需上行。SSE 比 WebSocket 实现简单(纯 HTTP,无协议升级握手),过代理与网关更友好;流式的根基在于 ASGI 的 send 可多次调用,每生成一段 token 就 send 一次,客户端按 text/event-stream 逐段渲染。需要双向交互(如语音对话)才轮到 WebSocket。
在 async 路由中直接调用阻塞的同步 SDK(如老版 OpenAI 客户端、同步数据库驱动),会卡死整个事件循环——不只这一个请求慢,是所有并发请求一起停摆。修复方式:换全异步客户端(httpx / asyncpg),或把阻塞调用丢进 run_in_executor 的线程池。这是 async 后台的第一道分水岭问题。
回答此题的重心,是把前几章的设计决策融进自己的项目叙述:01 章的记忆介质选型(为什么 pgvector 而非独立向量库)、02 章的 Agent 分层(路由层与编排层的边界划在哪)、03 章的 Skill 化沉淀,都成为"A(行动)"段落里现成的深度素材。
「我负责整个对话后台。选 FastAPI 是因为 LLM 调用是 IO 密集型,async + ASGI 能用单进程扛高并发;输出走 SSE,因为 LLM 输出是单向流,比 WebSocket 简单且过代理友好。存储分三层:PostgreSQL 加 pgvector 管业务数据和向量,Redis 管会话和缓存,文档解析这类慢活丢给 Celery 异步跑,整套 Docker Compose 编排。上线后 P95 首 token 时间从 3 秒压到 800 毫秒。」
加分点
量化结果是经历题的硬通货:QPS、P95 延迟、首 token 时间、单位成本,至少给出一个改善前后的对比数字。另一个高阶动作是在叙述中主动暴露一次"修正过的决策"——例如先用了同步 SDK 导致事件循环阻塞、定位后切换异步客户端——比一帆风顺的叙述可信得多。
追问预判
为什么用 SSE 不用 WebSocket?
应对思路
需求是单向流:服务端推 token,客户端只收。SSE 是纯 HTTP,无协议升级,过 Nginx、网关、CDN 都不需要特殊配置,断线还有内建的自动重连语义;WebSocket 的双向能力在此场景是多付的复杂度。
async 接口里调了同步 SDK 会发生什么?怎么发现?
应对思路
事件循环被阻塞,所有协程停摆,表现为压测时并发数一上去延迟集体飙升。定位手段:压测对比单接口与并发场景、事件循环 lag 监控、或 Python 的 asyncio debug 模式报告慢回调。修复:全异步客户端,或 run_in_executor 隔离。
为什么选 pgvector 而不是独立向量库?
应对思路
数据量中等(百万级以内向量)时,pgvector 让向量与业务数据同库——少一个运维组件、天然事务一致、备份策略统一;规模上去或需要高级索引调度时再迁 Milvus。先答规模判断,再答迁移路径,体现选型是权衡而非偏好。
本章自测
-
"一键配置切换检索策略"属于哪一级热更新?它的能力边界在哪里?
查看答案
属于配置热更新:在已加载的实现集合内重新选择,不重启进程。边界:新增策略类或修改实现逻辑不在已加载集合内,配置推送无能为力,仍需发版重启——那是代码热更新(Erlang hot swap / Java agent 级别)的领域。
-
工厂方法相比简单工厂解决了什么问题?
查看答案
简单工厂只是把 if-else 分支收口到一处,新增产品仍要修改工厂函数本身,违反开闭原则,且不在 GoF 23 个模式之列;工厂方法用继承 + 多态消除分支——新增产品只需新增类(或注册表条目),已有代码零改动。
-
vLLM 相比 Ollama 吞吐高数倍,靠的是哪两个机制?
查看答案
PagedAttention:KV cache 按页管理,消除显存碎片,同样显存容纳更多并发序列;continuous batching:新请求在 batch 运行中途动态插入,GPU 不等整批结束,利用率大幅提升。Ollama 基于 llama.cpp,面向单机交互,请求基本串行。
-
在 FastAPI 的 async 路由中调用阻塞的同步 SDK,后果与修复方式分别是什么?
查看答案
后果:事件循环被阻塞,所有并发请求一起停摆,而不只是当前请求变慢。修复:改用全异步客户端(httpx / asyncpg),或将阻塞调用放入
run_in_executor的线程池中隔离执行。
延伸阅读
- LangChain · init_chat_model — 官方工厂函数的参数与 configurable 运行时选型模式。
- Nacos 3.0 Release Notes — 内置 MCP Registry API,配置中心向 AI 基础设施的演化节点。
- Red Hat · vLLM vs Ollama — 两类推理引擎的机制与场景对比。
- 本库教程:design-patterns — 工厂、策略等模式的系统展开。
- 本库教程:nacos 与 fastapi — Q9 配置链路与 Q13 后台框架的深入版本。