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 实例。
  • 原子替换引用:新实例就绪后一次性替换旧引用;旧实例处理完在途请求后释放,保证切换过程对调用方透明。
配置中心 Nacos / Apollo watch 推送 应用内回调 Listener 新配置 工厂重建 RAG pipeline 实例 swap 原子替换引用 1.x 长轮询 hold 29.5s · MD5 比对 2.x gRPC 直推 ConfigChangeNotify 注册表:bm25 / vector / hybrid(已加载) 旧实例处理完在途请求后释放 代码热更新边界(这条链路做不到的部分) 新增策略类 / 替换实现逻辑 → 不在已加载集合内 配置推送无能为力,仍需发版重启
图 4-1可插拔 RAG 的配置热更新链路:配置中心推送变更,工厂在已加载实现集合内重建 pipeline 并原子替换;虚线框标出代码热更新的边界。

常见错误:模块级单例在启动时缓存了旧配置对象,配置中心推送之后实例并未重建,表现为"改了不生效"。这正对应 Spring 中 @RefreshScope 要解决的刷新边界问题——只有声明在刷新范围内的 Bean 才会随配置变更销毁重建。

60 秒口头版本

「这套机制属于配置热更新:检索器、重排器抽成接口注册进注册表,配置中心通过 watch 推送变更——Nacos 1.x 是 29.5 秒长轮询加 MD5 比对,2.x 起是 gRPC 长连接直推——应用收到回调后由工厂按新配置重建 pipeline,原子替换引用,旧实例处理完在途请求再释放,全程不重启。但它不是代码热更新:切换只发生在已加载的实现集合内,要新增一种策略类,仍然要发版重启,这是边界。」

加分点

Insight

主动点破面试官考的是概念边界——"热更新"在不同语境下指代不同层级,先框定定义再回答,比直接回答"是/不是"高一个档次。

再补两个生产细节:灰度发布(配置中心按 IP 维度推送 beta 配置,先让一台机器吃到新配置)与配置历史回滚(推错配置一键回到上一版本),说明这套链路在生产环境如何被驯服。

追问预判

追问 1

替换 pipeline 引用的瞬间,正在处理的请求怎么办?

应对思路

在途请求继续持有旧实例引用直至完成——引用替换是原子的,但旧实例不立即销毁,等引用计数归零或 drain 完成后再释放资源(关闭连接池、卸载索引)。这与负载均衡摘流量时的优雅下线是同一思路。

追问 2

Nacos 长轮询为什么 hold 29.5 秒而不是 30 秒?

应对思路

客户端超时设为 30 秒,服务端提前 500ms 返回,留出网络传输余量,避免恰好在网关或客户端超时边界上被误判为超时失败。

追问 3

如果要求新增一种检索策略也不重启,怎么做?

应对思路

那就进入代码热更新领域: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 选择模型——工厂模式在主流框架中是一等公民。

retriever_factory.py python
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 的最小实现。

60 秒口头版本

「工厂模式把对象的创建和使用解耦:调用方只依赖抽象接口,新增产品只需加类注册、不改旧代码,符合开闭原则和依赖倒置;同时创建逻辑集中收口,鉴权、重试、默认参数统一处理,测试时也容易注入 mock。在 LLM 应用里最典型的就是 provider 工厂——按配置切换 OpenAI、Anthropic 或本地模型,LangChain 的 init_chat_model 就是官方工厂函数。」

加分点

Insight

精准辨析是强加分项:简单工厂不在 GoF 23 个模式之列,它只是把 if-else 收口到一处的踏脚石,新增产品仍要改工厂函数本身,依然违反开闭原则;工厂方法用继承 + 多态才真正消除了分支。能讲清这条分界,说明模式不是背的。

反向加分:没有真实变化点时不要预铺脚手架——只有一个实现却套三层抽象,是对模式的滥用。

追问预判

追问 1

工厂模式和策略模式什么关系?

应对思路

职责不同:工厂管"创建谁",策略管"运行时用哪种行为"。两者常组合出现——Q9 的可插拔 RAG 就是策略模式定义可互换的检索行为、工厂按配置创建具体策略实例。

追问 2

工厂方法和抽象工厂的区别?

应对思路

工厂方法面向单一产品等级,每个子类工厂创建一种产品;抽象工厂面向产品族,一个工厂同时创建一组配套对象——例如某 provider 工厂同时产出该厂商配套的 chat 模型、embedding 模型与 tokenizer,保证族内一致。

Q11本地部署要考虑什么?配置管理放在哪里?

面试官在考什么

工程全局观——从模型推理到密钥管理,候选人脑中是否有一张把系统部署进客户机房的完整清单。

参考答案

第一问:本地 / 私有化部署的六个考虑维度。

  • ① 模型推理:选型与资源是核心。显存按"参数量 × 精度字节数 + KV cache"估算,显存不足时用 AWQ / GPTQ 量化压到 4bit。推理引擎对比见下表。
  • ② 数据不出域:私有化部署的核心动机——文档、向量、对话记录全部留在客户内网,这一条决定了后面所有依赖都要自带。
  • ③ 依赖服务自带:向量库、数据库、Redis 全部打成离线镜像随系统交付,不能假设客户机房有外网。
  • ④ 监控与日志:私有化环境无法远程登录排查,指标采集、日志落盘与告警必须随包内置。
  • ⑤ 模型 license:开源模型的商用条款各不相同,交付前必须核对授权范围。
  • ⑥ 离线交付流程:镜像导出、依赖打包、升级与回滚方案,都要在没有外网的前提下成立。
推理引擎选型:Ollama vs vLLM
方案底层机制并发模型适用场景
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;普通配置走配置中心,二者生命周期与权限模型完全不同。
60 秒口头版本

「本地部署先抓核心动机:数据不出域。围绕它展开六块——模型推理选型(开发用 Ollama,生产用 vLLM,靠 PagedAttention 和 continuous batching 撑并发,显存不够上 AWQ/GPTQ 量化)、依赖服务全部离线镜像自带、监控日志内置、模型 license 核对、离线交付与回滚方案。配置管理分层答:单机 .env 加 pydantic-settings,集群上配置中心或 ConfigMap,API key 这类敏感信息单独进 Secret 或 Vault,绝不进 git。」

加分点

Insight

Nacos 3.0(2025 年 4 月 GA)起内置 MCP Registry API——配置中心正在演化为 AI 应用的基础设施,注册的不只是服务与配置,还有模型上下文协议的工具端点。这条动态与本面试的 AI 应用开发方向直接相关,提到它说明候选人在持续跟进基础设施层的演进。

追问预判

追问 1

vLLM 的吞吐为什么比 Ollama 高?

应对思路

两个机制:PagedAttention 把 KV cache 按页管理,消除显存碎片,同样显存能容纳更多并发序列;continuous batching 允许新请求在 batch 运行中途动态插入,GPU 不必等整批跑完,利用率大幅提升。Ollama 底层是 llama.cpp,面向单机交互场景,请求基本串行。

追问 2

7B 模型部署需要多少显存?

应对思路

FP16 下权重约 7B × 2 字节 ≈ 14GB,再加 KV cache 与激活值,单卡 24GB 起步;AWQ / GPTQ 量化到 4bit 后权重降到 4–5GB,消费级显卡即可承载,代价是少量精度损失。

追问 3

API key 放进私有 git 仓库行不行?

应对思路

不行。git 历史不可抹除,仓库权限会随协作扩散,密钥一旦提交即视为泄露,只能轮换。正确做法:Secret / Vault 管理,运行时注入环境变量,并在 CI 中加密钥扫描防止误提交。

Q13第一个项目后台怎么搭建的?

面试官在考什么

项目经历的颗粒度——选型理由、分层结构与量化结果能否经得起逐层追问,而不是技术名词罗列。

参考答案

这是经历题,没有标准答案,但有标准结构:用 STAR 框架组织叙述,每一段都给面试官留出可追问的"钩子"。

  • S(背景与规模):项目解决什么问题,服务多少用户、多大数据量——规模决定了后面所有选型的合理性基准。
  • T(任务):候选人在后台中负责哪一块,边界说清。
  • A(行动):技术选型 + 理由、分层架构、关键机制决策——这是面试官追问的主战场。
  • R(结果):量化指标收尾——QPS、P95 延迟、首 token 时间、成本变化。

以下是一个可信的样板架构(A 部分的参考栈),用于校准叙述的颗粒度:

客户端 Web · App HTTP · SSE 流式 FastAPI 应用层(ASGI) SSE 流式输出 · async 路由 · 会话鉴权 流式补全 向量检索 异步任务 LLM 服务 vLLM · API 向量库 pgvector / Milvus PostgreSQL 业务数据 Redis 会话 · 缓存 任务队列 Celery / ARQ Docker Compose 统一编排 · 离线可交付
图 4-2典型 AI 应用后台分层架构:FastAPI 经 SSE 向客户端流式输出,下游分别对接推理、检索、存储与异步任务。

叙述中有两个必须讲对的机制点,面试官的追问通常围绕它们展开:

机制点一: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(行动)"段落里现成的深度素材。

60 秒口头版本(候选人引语示例)

「我负责整个对话后台。选 FastAPI 是因为 LLM 调用是 IO 密集型,async + ASGI 能用单进程扛高并发;输出走 SSE,因为 LLM 输出是单向流,比 WebSocket 简单且过代理友好。存储分三层:PostgreSQL 加 pgvector 管业务数据和向量,Redis 管会话和缓存,文档解析这类慢活丢给 Celery 异步跑,整套 Docker Compose 编排。上线后 P95 首 token 时间从 3 秒压到 800 毫秒。」

加分点

Insight

量化结果是经历题的硬通货:QPS、P95 延迟、首 token 时间、单位成本,至少给出一个改善前后的对比数字。另一个高阶动作是在叙述中主动暴露一次"修正过的决策"——例如先用了同步 SDK 导致事件循环阻塞、定位后切换异步客户端——比一帆风顺的叙述可信得多。

追问预判

追问 1

为什么用 SSE 不用 WebSocket?

应对思路

需求是单向流:服务端推 token,客户端只收。SSE 是纯 HTTP,无协议升级,过 Nginx、网关、CDN 都不需要特殊配置,断线还有内建的自动重连语义;WebSocket 的双向能力在此场景是多付的复杂度。

追问 2

async 接口里调了同步 SDK 会发生什么?怎么发现?

应对思路

事件循环被阻塞,所有协程停摆,表现为压测时并发数一上去延迟集体飙升。定位手段:压测对比单接口与并发场景、事件循环 lag 监控、或 Python 的 asyncio debug 模式报告慢回调。修复:全异步客户端,或 run_in_executor 隔离。

追问 3

为什么选 pgvector 而不是独立向量库?

应对思路

数据量中等(百万级以内向量)时,pgvector 让向量与业务数据同库——少一个运维组件、天然事务一致、备份策略统一;规模上去或需要高级索引调度时再迁 Milvus。先答规模判断,再答迁移路径,体现选型是权衡而非偏好。

本章自测

  1. "一键配置切换检索策略"属于哪一级热更新?它的能力边界在哪里?
    查看答案

    属于配置热更新:在已加载的实现集合内重新选择,不重启进程。边界:新增策略类或修改实现逻辑不在已加载集合内,配置推送无能为力,仍需发版重启——那是代码热更新(Erlang hot swap / Java agent 级别)的领域。

  2. 工厂方法相比简单工厂解决了什么问题?
    查看答案

    简单工厂只是把 if-else 分支收口到一处,新增产品仍要修改工厂函数本身,违反开闭原则,且不在 GoF 23 个模式之列;工厂方法用继承 + 多态消除分支——新增产品只需新增类(或注册表条目),已有代码零改动。

  3. vLLM 相比 Ollama 吞吐高数倍,靠的是哪两个机制?
    查看答案

    PagedAttention:KV cache 按页管理,消除显存碎片,同样显存容纳更多并发序列;continuous batching:新请求在 batch 运行中途动态插入,GPU 不等整批结束,利用率大幅提升。Ollama 基于 llama.cpp,面向单机交互,请求基本串行。

  4. 在 FastAPI 的 async 路由中调用阻塞的同步 SDK,后果与修复方式分别是什么?
    查看答案

    后果:事件循环被阻塞,所有并发请求一起停摆,而不只是当前请求变慢。修复:改用全异步客户端(httpx / asyncpg),或将阻塞调用放入 run_in_executor 的线程池中隔离执行。

延伸阅读