第 1 章面试 · 05 LLM Gateway(统一模型调用服务)
目标岗位:Agent Harness 研发/工程方向(参考
~/agent/jb/jd.md) 配套讲义:讲义 05 · LLM Gateway 用法:先自己口头答一遍,再对照"参考回答";重点看"考察点"和"坑"。 原则:所有回答结论先行 → 项目证据 → 原理 → 边界与改进,不要背概念。
0. 面试官视角:Gateway 题到底在考什么
Gateway 是"把分散的厂商调用收敛成平台能力"的典型系统设计题:
- 概念层:Gateway 解决什么问题(密钥、协议、可观测);
- 实践层:你的网关怎么分层、每个模块在哪、有没有测试;
- 判断层:为什么这么设计?并发和性能边界?新增厂商要改什么?——拉差距。
回答公式:一句话结论 → 你项目里的实现/例子 → 原理 → 边界与改进。
1. 按讲义章节的题目与参考回答
Q1:LLM Gateway 解决什么问题?为什么不直接让业务调厂商 SDK?
参考回答:四个问题——密钥分散(每个组件持 key,泄露面大)、协议差异 (每家格式不同)、不可观测(没有统一日志)、无法统一治理(限流/回退/成本)。 网关收敛成一层:密钥集中在 config 层、协议统一成 OpenAI Compatible、 每次调用留痕、限流回退统一做。我的网关
create_app组装了路由、限流、 模板、用量、provider 链,业务只发 HTTP。
Q2:密钥为什么集中?怎么防泄露?
参考回答:集中后业务组件不碰厂商密钥,泄露面从"每个组件"缩小到"网关 一个点"。密钥从环境变量读(
config.py,支持 DASHSCOPE_API_KEY 等厂商 命名空间),不进代码库。诚实边界:网关本身是新的攻击面——生产上密钥 应放密钥管理服务(KMS/Vault),网关进程还要做最小权限和审计。
Q3:为什么对外统一成 OpenAI Compatible,而不是自定义协议?
参考回答:OpenAI Compatible 是事实标准,DeepSeek、DashScope、本地 Ollama/vLLM 都兼容,生态里现成 SDK 和工具直接可用。对内用
ChatProvider接口 + adapter 屏蔽差异(讲义 01 §6),对外格式稳定、对内可演进。收益: 换厂商不改业务代码、OpenAI 官方 SDK 能直连网关。
Q4:流式代理为什么要"两段式"?坑在哪?
参考回答:
StreamingResponse只管"网关→客户端",上游"模型→网关"如果整体 缓冲,首字节照样等全部生成完——我之前就用httpx.Client.request踩过这个 坑,首页实测首字节≈总耗时。改成本仓的_stream_sse(client.stream逐行 转发)后首字节远小于总耗时。第二个坑是取消:客户端断开要一路关到上游连接, 否则 token 白付(讲义 02 §5)。第三个坑是代理缓冲:Cache-Control: no-cache
X-Accel-Buffering: no是标配。
Q5:结构化输出屏蔽底层差异是怎么做的?
参考回答:上层只传 JSON Schema,网关做两件事——按厂商转换 response_format (OpenAI 透传 / Gemini 映射 / Anthropic 隐藏工具),返回后统一校验 + 纠错 重试 + 降级(
structured.py)。业务层完全不感知模型协议,这是 Gateway “屏蔽差异"的代表性例子。
Q6:请求/响应校验"两层"各防什么?
参考回答:入口用 Pydantic(
server/schemas.py,FastAPI 自动校验)拦住 “请求格式错”——缺字段、类型错直接 422;出口在结构化路径用 jsonschema 拦住"模型输出脏”。两层拦截的意义:错误发生在哪一层就归哪一层,排查不用 猜。诚实边界:普通 chat 的响应是自由文本,不强制出口校验;只有结构化 路径是严格校验。
Q7:Prompt 模板为什么网关统一管理?
参考回答:模板是"系统行为"的一部分,散在业务代码里没法审计、没法回滚。 网关统一管理后:上层只传变量(
/v1/prompts/{name}/render)、变更走版本 历史可回滚、注入隔离声明集中维护(讲义 03)。这也是"平台化"的体现—— 业务不需要知道模板长什么样,只需要传变量。
Q8:限流、重试、fallback 三层怎么配合?
参考回答:网关入口令牌桶限流(429 + Retry-After)保护网关自己;单请求对 上游 429/5xx 指数退避重试(流式只在首字节前);重试耗尽由 FallbackProvider 换备用模型,连续失败还触发熔断。关键判断是错误分类——鉴权/内容过滤这类 错误重试没用,直接抛(
classify_provider_error)。三层顺序不能乱:先挡 外部洪峰,再消化瞬时抖动,最后处理端点故障。
加分点:能说出"每层解决不同故障类型,且错误分类决定是否值得重试"。
Q9:用量日志记录什么?怎么支撑成本治理?
参考回答:每次调用一条 UsageRecord:provider/model、输入输出 token、耗时、 估算成本、状态与错误,可选落 JSONL。
/v1/usage/summary聚合(请求数/ 错误率/成本/平均延迟),/v1/usage/recent给明细。成本治理 = 先有数据: 哪家贵、哪个接口慢、错误率高都能查。诚实边界:成本是估算(按模型名前缀 匹配价格表),正式计费以厂商账单为准。
Q10:网关的性能和并发边界?(判断层)
参考回答:诚实说当前是"演示级"性能:provider 是同步 httpx,流式用
asyncio.to_thread逐块转发(事件循环可取消但每块有线程切换开销); 长文本任务用后台线程。生产演进:provider 改 async httpx、流式用 worker 线程 + asyncio.Queue 单连接转发、限流换分布式令牌桶(Redis)、多实例下 用量/模板/任务存储换数据库。面试时我会主动说"当前优先正确性和可演示, 性能边界清楚,知道怎么演进"。
坑:别吹"高并发生产级"——项目测试都是单机、内存态。
Q11:新增一个厂商或新协议,要改多少代码?
参考回答:协议兼容的厂商(又一个 OpenAI 兼容服务)只需在
specs.py加 一行 ProviderSpec(默认端点、key 环境变量、模型关键词);协议不同的才写 新 adapter(实现ChatProvider.complete/stream两个方法 + 注册)。因为 厂商差异做成了"声明式数据 + 少量钩子",而不是散落的 if 分支。
Q12:网关的测试策略是什么?
参考回答:三层——provider 层用 httpx.MockTransport 模拟各家 HTTP 响应 (不碰真实网络);网关层用 FastAPI TestClient + stub provider 跑完整 HTTP 链路(流式 SSE、限流、错误块、断点任务、结构化输出);存储层测 JSONL 持久化与恢复。当前 95 个测试,
conda run -n llmops pytest一条命令全跑。
2. 通用回答技巧
2.1 用 PREP 结构
- Point:“网关把分散的厂商调用收敛成平台能力”;
- Reason:密钥、协议、可观测、治理四个问题都需要统一层;
- Example:OpenAI 兼容入口 + ChatProvider + 限流/回退/用量 + 首页演示;
- Point:边界——单机内存态、同步 httpx,性能演进路径清楚。
2.2 弹药库
- 首页五个面板(对话/模型/模板/结构化/长文本/用量)一条龙演示;
/docsSwagger;tests/95 个用例;- 限流 429、
demo:boom错误链路、结构化降级等"可触发演示"。
2.3 诚实边界清单
- 单机 + 内存态(模板/用量/任务),多实例需换存储;
- provider 同步 httpx,性能是演示级(有 async 演进路径);
- 用量成本是估算;
- 无鉴权(网关本身没有 API key 保护,属后续阶段)。
2.4 反问环节
- “贵司网关多实例部署后,限流和用量存储怎么做一致性?”
- “线上怎么监控各厂商的 P99 延迟和结构化输出失败率?”
3. 一分钟项目介绍(开场自述模板 · Gateway 版)
“我做的 Anna 项目第一阶段就是一个 LLM 统一模型调用服务:FastAPI 网关 + ChatProvider 抽象 + adapter 屏蔽 OpenAI/Anthropic/Gemini/Ollama 的协议差异, 对外 OpenAI Compatible,业务不碰密钥。网关统一做了五件事:流式两段代理、 结构化输出(Schema 校验 + 纠错重试 + 降级)、模板库(版本化 + 注入隔离)、 限流/重试/跨模型回退熔断、Token/成本/延迟日志。每个能力首页都能现场演示, 95 个测试覆盖。设计上我坚持"声明式数据 + 少量钩子"而不是散落的 if 分支, 所以新增一个 OpenAI 兼容厂商只加一行 spec。诚实说,当前是单机内存态、 provider 还是同步 httpx,性能是演示级;我的演进路径是 async 化 + 分布式 存储,这些边界我都清楚。”