通用工程基础面试题
这篇不是传统后端八股的堆砌,而是把工程基础放回 AI 应用的真实链路:一次请求会调用模型、检索服务、工具 API、数据库和对象存储,可能还要跨队列运行很久。回答时要同时说明接口语义、并发边界、失败后的状态,以及如何观测和恢复。
使用说明
一个流式 AI API 像餐厅叫号:SSE 负责把进度送到前台,队列和任务表负责厨房继续做事;顾客关掉页面不等于订单取消。把连接、任务、重试和副作用分开,系统才不会因为一次断网重复下单。
每道题按“考察点 -> 回答脉络 -> 详细参考答案 -> 常见追问及答案 -> 扣分点”组织。答题时先给结论,再结合一次 AI 请求说明数据流,最后补充边界、指标和故障处理。不要只说“加缓存、加重试、上消息队列”,要说明它们保护的资源、可能引入的新问题和验收方法。
一、异步与并发
Q1:async/await、协程、线程和进程分别解决什么问题?
考察点
是否理解并发模型和阻塞边界,而不是把 async 当作自动提速开关。
回答脉络
先区分并发与并行,再说明事件循环、线程和进程的适用负载,最后联系模型请求和文档解析。
详细参考答案
async/await 是协作式异步编程语法。协程在遇到真正的异步 I/O 时主动让出控制权,事件循环再运行其他就绪任务,因此一个进程可以用较少线程同时等待多个模型、检索和工具请求。它主要提高 I/O 等待期间的利用率,不会让 CPU 密集计算自动并行。
线程适合复用阻塞 I/O SDK、文件操作或少量后台任务,但线程之间共享地址空间,需要锁和线程安全的数据结构;在 CPython 中,GIL 使纯 Python CPU 计算通常不能靠多线程获得线性加速。进程有独立地址空间,适合 CPU 密集的 OCR、压缩或复杂解析,但进程间传输数据有序列化成本,启动和内存开销也更高。
例如 RAG 请求可以在事件循环中并行等待向量检索和权限服务;PDF 解析若是 CPU 密集,则交给进程池或独立 Worker;只有同步的供应商 SDK 时,才把它放进有限大小的线程池,避免阻塞事件循环。
常见追问及答案
- 追问:
asyncio.gather会自动限制并发吗? 不会。它会同时调度传入的任务,必须用Semaphore、连接池或队列显式限制并发,否则容易打满下游配额。 - 追问:协程是否等于线程? 不是。协程由用户态调度,通常运行在线程上的事件循环里;协程切换便宜,但只要某个协程执行阻塞代码,所在事件循环线程仍会被卡住。
扣分点
- 说
async可以解决 CPU 密集问题。 - 忽略同步 SDK 阻塞事件循环的风险。
- 只会背线程池,不能解释资源上限。
Q2:在异步 Agent 服务中接入阻塞式 SDK,应该怎么做?
考察点
是否能识别隐藏阻塞,并设计可控的适配层。
回答脉络
先定位阻塞调用,再说明线程池隔离、超时取消和资源上限,最后讲替换为原生异步客户端的演进。
详细参考答案
我不会直接在 async def 中调用阻塞 SDK,而是先确认 SDK 是否在 DNS、连接、读取响应或 JSON 解析阶段同步阻塞。短期可以通过 asyncio.to_thread 或专用线程池隔离,但线程池大小必须按供应商并发配额、连接池大小和本机内存设置;不同下游最好使用不同的信号量,避免一个慢服务占满所有线程。
线程任务需要绑定请求 deadline。调用方取消时,要停止等待并尽量让 SDK 的底层请求也取消;如果 SDK 无法取消,至少限制线程池和结果回收,不能把“客户端已断开”当成服务端已经停止。长期应换用原生异步 SDK,或把供应商调用放到独立 Worker 服务,由队列、租约和状态查询管理。
例如模型供应商只有同步客户端,我会为它设置 model_sdk_pool、每租户并发额度和总 deadline。线程池中发生超时后记录 provider_request_id,恢复时先查询供应商状态,避免因为本地等待超时而重复触发带副作用的请求。
常见追问及答案
- 追问:把线程池调到很大能解决吞吐吗? 通常不能。线程越多会增加上下文切换、连接争用和下游压力,甚至让 P99 更差;应先确认瓶颈并做压测。
- 追问:超时后线程一定停止吗? 不一定。取消 Python Future 只会停止等待,底层阻塞调用可能继续运行,所以要有供应商请求 ID、状态查询和进程级隔离。
扣分点
- 在事件循环里直接调用同步 HTTP 客户端。
- 只讲线程池,不讲取消和超时后的未知结果。
- 用无限线程掩盖下游限额。
Q3:如何限制模型、工具和租户的并发?
考察点
是否能从全局、租户、下游和资源类型多个维度控制并发。
回答脉络
先区分请求并发、连接并发和 token 并发,再说明信号量、队列和配额组合,最后讲公平性。
详细参考答案
我会把并发拆成多个维度:入口请求数、模型生成中的请求数、每个下游的连接数、单租户任务数、每个 Agent 的工具调用数,以及 GPU 或 CPU 的实际容量。单个 Semaphore 只能保护一个瓶颈,不能用它替代全局配额。
在线请求可以在网关按租户和优先级限额,进入模型 Worker 后再按模型、区域和 token 预算限额;工具调用用独立信号量,避免一个慢工具拖住模型槽位。超过短期并发上限时,简单问答可以快速返回 429,允许排队的长任务则持久化到队列并返回任务 ID。公平调度可以使用每租户令牌桶加带权队列,防止大租户占满所有容量。
并发值不能拍脑袋。应结合下游 QPS、连接池、平均服务时间、内存、模型 token 吞吐和 P95 延迟压测。还要留出故障余量,避免把所有资源打满后没有空间处理重试、健康检查和优雅退出。
常见追问及答案
- 追问:并发限制和限流有什么区别? 并发限制控制“同时在执行多少个”,限流控制“一段时间内允许进入多少个”;两者通常都需要。
- 追问:为什么只按请求数限流不够? 一个长上下文生成请求和一个短请求消耗的 token、GPU 时间和内存差异很大,还要按 token、成本或资源权重限制。
扣分点
- 只用进程内全局变量,没有分布式或租户边界。
- 没有说明排队、拒绝和公平性。
- 忽略重试和健康检查占用容量。
Q4:取消一个流式 Agent 请求时,哪些东西必须一起取消?
考察点
是否区分客户端停止等待、服务端停止执行和外部副作用已经发生。
回答脉络
先定义取消语义,再沿请求树传播信号,最后处理不可取消的外部调用和资源回收。
详细参考答案
取消至少有三层含义:客户端断开连接;网关停止向客户端写数据;服务端停止模型生成、检索、工具调用和后续编排。服务端应为一次请求建立 cancellation token 或 deadline,并把它传播给子任务、HTTP 客户端、队列 Worker 和流式生成器。每个步骤都要在 I/O 返回、重试前和副作用提交前检查取消状态。
但取消不是回滚。外部邮件、支付或工单请求一旦发出,客户端断开不能证明它没有执行。对这类动作要记录动作状态和幂等键,取消时标记“取消等待”或“待核验”,由状态查询、补偿或人工队列收口。模型流可以停止生成,检索任务可以取消,已经提交的业务动作只能停止后续步骤。
资源回收包括关闭响应流、释放信号量、归还连接、删除临时文件和终止子进程。取消指标要区分用户取消、deadline 超时、服务保护性取消和下游取消,否则无法判断用户体验还是容量不足导致的中断。
常见追问及答案
- 追问:客户端断开后,服务端还要继续跑吗? 对无副作用的短请求通常应尽快停止;对已经进入不可逆动作的任务要继续做状态确认或补偿,不能简单杀掉并遗失事实。
- 追问:
asyncio.CancelledError捕获后能否忽略? 不应无条件吞掉。清理资源后要继续传播取消,除非明确把操作转入后台任务并持久化状态。
扣分点
- 把取消说成事务回滚。
- 只关闭浏览器连接,不停止下游工作。
- 忽略副作用未知状态。
二、接口与协议
Q5:HTTP、SSE、WebSocket 和 gRPC 在 AI 应用中怎么选?
考察点
是否能按交互方向、连接生命周期、代理兼容性和类型契约选协议,而不是按流行程度选型。
回答脉络
先描述通信模式,再结合聊天流、任务事件、内部服务和双向协作给出组合方案。
详细参考答案
HTTP/JSON 适合普通请求、创建任务和查询快照,代理、鉴权和调试工具最成熟。SSE 是服务端单向推送,浏览器接收模型 token、任务进度和审计事件时简单且容易经过网关;要配合事件 ID、重连游标和心跳。WebSocket 适合双方都需要持续发送的交互,例如实时语音、协作编辑或需要客户端频繁回传音频帧,但连接状态、扩缩容、断线恢复和代理支持更复杂。gRPC 基于 HTTP/2 和 protobuf,适合内部服务间强类型调用、流式传输和高吞吐,不一定适合作为浏览器的直接公开接口。
典型组合是:公网 REST 创建 Agent 任务,SSE 订阅事件;网关到编排器使用 HTTP 或 gRPC;实时语音单独用 WebSocket 或 WebRTC。协议选择还要看是否需要双向流、消息顺序、断点恢复、浏览器兼容、负载均衡和版本演进。
常见追问及答案
- 追问:SSE 断线后怎么补事件? 服务端为事件分配单调序列或事件 ID,客户端带
Last-Event-ID或游标重连,服务端先补发未确认事件,再继续实时推送。 - 追问:为什么不所有场景都用 WebSocket? 长连接会增加连接管理、代理和扩缩容成本;单向流场景用 SSE 更简单,普通请求更不需要保持连接。
扣分点
- 只按“性能更高”做协议选择。
- 流式接口没有重连、心跳和补发设计。
- 忽略浏览器、网关和负载均衡约束。
Q6:如何设计一个可演进的 AI API?
考察点
是否理解请求契约、版本兼容、错误语义和异步任务接口。
回答脉络
先区分同步与异步,再讲版本、幂等、错误和流式协议,最后说明向后兼容。
详细参考答案
同步接口适合短请求,超过软超时就返回任务 ID 转异步。创建任务的请求应包含 idempotency_key、客户端版本、租户上下文和能力选项;响应返回 task_id、状态、版本和查询地址,而不是让客户端长时间等待。任务查询返回权威快照,事件流返回可补发的增量,二者不能只靠前端拼接。
接口采用显式 schema 版本,新增字段默认可选,枚举扩展要考虑旧客户端,删除字段要经过弃用周期。错误至少区分参数错误、未认证、无权限、资源不存在、版本冲突、限流、下游暂时不可用和未知执行结果,并提供机器可读的 code、重试建议和关联 ID。流式响应中途不能随意改变事件类型;若发生错误,要标记流终态并让客户端可查询最终任务状态。
常见追问及答案
- 追问:HTTP 200 但模型失败怎么办? HTTP 状态只表达请求层是否受理,业务响应还要有明确的任务或生成状态;不能用 200 加一段模糊错误。
- 追问:怎么避免旧客户端覆盖新任务状态? 更新接口使用版本号、
ETag或If-Match,服务端按状态机拒绝过期写入。
扣分点
- 只有一个“聊天接口”,没有任务查询和错误 schema。
- 用文本错误消息替代机器可读错误。
- 没有幂等键和版本兼容策略。
三、超时、重试与幂等
Q7:一次 RAG/Agent 请求如何分配超时?
考察点
是否理解 deadline 传播、连接和读取超时的区别,以及尾延迟预算。
回答脉络
先给总 deadline,再拆连接、首字节、读取和子调用预算,最后讲超时后的状态处理。
详细参考答案
入口先建立绝对 deadline,例如请求剩余 8 秒,而不是每一层都重新设置 8 秒。网关、编排器、检索、权限、模型和工具都从同一个 deadline 计算自己的预算,避免层层叠加导致总耗时失控。HTTP 客户端通常要分别设置连接超时、TLS/握手超时、首字节超时和读取超时;流式模型的读取超时还要区分“长时间没有新 token”和“连接仍有正常心跳”。
预算应按依赖关系分配:认证和权限先完成,Dense/Sparse 检索可并行,reranker 只拿剩余时间,模型生成保留用户可接受的主体时间。超过软 deadline 可以返回部分答案或转异步;超过硬 deadline 必须停止新调用。任何超时都要记录调用阶段、剩余 deadline、是否已发出请求和供应商 request ID,后续才能判断是否安全重试。
常见追问及答案
- 追问:读取超时就一定没执行吗? 不一定。请求可能已在下游完成,只是响应没有回来;写操作要先查询状态或使用幂等键。
- 追问:为什么不能每个服务都设同样的 timeout? 会导致总预算膨胀,外层已经超时,内层还在继续消耗资源;应传播绝对截止时间。
扣分点
- 只配置一个全局整数 timeout。
- 不区分连接超时、读取超时和业务 deadline。
- 超时后无条件重试。
Q8:什么错误适合重试?如何避免重试风暴?
考察点
是否能分类暂时错误、永久错误和未知执行结果,并设计退避、预算和熔断。
回答脉络
先按错误类型决定是否可重试,再讲指数退避加抖动、次数/时间预算和下游保护。
详细参考答案
网络连接重置、部分 5xx、明确的 429 和短暂服务不可用通常可以重试;参数错误、权限拒绝、资源不存在、schema 不兼容通常不应原样重试。模型输出格式错误可以进行有限次数的结构化修复,但不能无限让模型“再想一次”。对写操作,超时和连接断开属于未知结果,必须先查询幂等键或业务状态。
退避使用指数或基于 deadline 的退避,并加入随机抖动,避免大量实例同一时刻再次请求。重试次数、总等待时间、token 成本和最大尝试次数都应计入预算;只有临时错误才进入重试。网关还要按下游和租户设置并发上限、熔断和重试放大系数,必要时快速失败或降级。监控 attempt_count、重试成功率、重试流量占比和最终 P99,判断重试是否真的提升可用性。
常见追问及答案
- 追问:429 应该立即重试吗? 先读取
Retry-After或供应商提示,并结合本地配额和 deadline;没有等待预算时直接返回受限,不要忙等。 - 追问:重试三次是行业标准吗? 不是。次数应由错误类型、总 deadline、操作幂等性和成本决定,三次只是常见起点。
扣分点
- 对所有异常统一重试。
- 没有抖动、预算和熔断。
- 忽略重试会重复副作用。
Q9:幂等键应该怎样设计?
考察点
是否能让重复请求返回同一业务结果,而不只是让客户端“记得不要重复”。
回答脉络
说明键的业务范围、请求指纹、状态表和并发竞争,再处理过期和未知状态。
详细参考答案
幂等键必须由服务端认可,并绑定租户、调用主体、动作类型和业务对象,不能只用全局随机字符串而失去归属。例如创建工单可以使用 tenant_id + user_id + client_request_id,外部支付还要绑定金额和收款对象的请求摘要。服务端建立唯一约束的动作表,原子记录 PENDING;重复请求若参数摘要不同就返回冲突,相同则返回已完成结果、处理中状态或明确失败。
超时后不能删除 PENDING 记录再重试,否则并发请求可能同时执行。应通过租约、状态查询和 fencing 防止旧 Worker 继续写入。键的保留时间要覆盖客户端重试窗口和对账周期;对于长期任务,任务 ID 与动作 ID 分开,不能用一个键覆盖所有步骤。
常见追问及答案
- 追问:查询接口需要幂等键吗? 读取通常天然幂等,但仍要考虑权限、时间窗口和分页游标;创建查询任务或导出任务则需要幂等。
- 追问:外部系统不支持幂等怎么办? 本地保存动作 ledger、请求 ID 和结果,对超时先查询;仍无法确认时进入人工核验,并降低自动重试范围。
扣分点
- 只在客户端去重。
- 幂等键不绑定请求参数,导致同键不同动作被错误复用。
- 没有处理并发、PENDING 和未知结果。
四、缓存与数据存储
Q10:AI 应用通常有哪些缓存?如何避免缓存错和越权?
考察点
是否能区分缓存对象、稳定性、权限范围和失效策略。
回答脉络
按 tokenizer/prompt、检索、业务数据和结果四层说明,最后讲 key、TTL 和一致性。
详细参考答案
缓存可以分为模型 prompt/prefix cache、embedding cache、检索结果 cache、权限或 schema cache、业务只读数据 cache,以及最终答案 cache。每一层的可共享范围不同:embedding 只由文本和模型版本决定;检索结果还要绑定索引版本、过滤条件和权限范围;最终答案必须绑定用户/租户、数据版本、工具能力和时间敏感参数,敏感任务通常不做跨用户共享。
缓存 key 应显式包含 schema/config/model/index 版本和必要的授权 scope,不能只用用户问题。失效可以按 TTL、版本切换、事件通知或主动 purge 组合;写入成功后要更新或删除相关缓存。缓存命中率不是唯一目标,还要监控陈旧命中、跨租户命中、回源比例和缓存导致的错误率。缓存不可用时应回源或降级,而不是把缓存当成唯一事实来源。
常见追问及答案
- 追问:为什么同一句问题不能共享答案? 用户权限、知识库版本、时间范围和上下文可能不同,共享会泄露数据或返回过期结论。
- 追问:缓存击穿怎么办? 对热点 key 做 single-flight/请求合并,设置随机 TTL 和预热;锁要有超时,不能因为缓存锁让所有请求永久等待。
扣分点
- 缓存 key 只有 prompt 文本。
- 忽略权限和知识库版本。
- 只谈命中率,不谈陈旧和泄露。
Q11:数据库、向量库和对象存储分别放什么?
考察点
是否能按访问模式、事务性和数据大小选存储,而不是把所有内容塞进一个数据库。
回答脉络
先区分权威元数据、检索索引和大对象,再说明一致性和引用关系。
详细参考答案
关系数据库适合任务状态、租户、权限、幂等动作、版本和需要事务/唯一约束的元数据;向量库或带向量能力的数据库适合 embedding、过滤字段和相似度检索,但不应成为唯一的业务事实源;对象存储适合原始 PDF、图片、音频、模型输出和大型 checkpoint,表中保存 URI、hash、版本、大小和加密信息。
文档入库时先把原文件写入不可变对象路径,再在数据库登记 document_id、版本、ACL 和处理状态,向量索引完成后通过版本发布。删除或权限撤销要有索引失效和对象生命周期策略。跨存储操作不能假装一个本地事务覆盖全部系统,使用状态机、outbox、校验任务和可重放流水线保证最终一致性。
常见追问及答案
- 追问:为什么不把 PDF 二进制放数据库? 大对象会影响备份、事务和查询性能;对象存储更适合分片、生命周期和 CDN,但必须做好权限和引用完整性。
- 追问:向量索引更新成功就代表文档可见吗? 不一定。要等 ACL、元数据和索引版本一起通过发布门槛,查询按已发布版本读取。
扣分点
- 用向量相似度代替权限过滤。
- 只讲存储类型,不讲权威来源和版本。
- 认为跨数据库写入天然原子。
Q12:数据库连接池、HTTP 连接池和模型并发之间有什么关系?
考察点
是否理解池大小、并发上限和排队之间的相互约束。
回答脉络
说明连接池保护连接资源,信号量保护业务并发,两者要按最小瓶颈校准。
详细参考答案
连接池限制的是同时打开或复用的连接数,业务信号量限制的是同时执行的请求数。一个 Agent 请求可能同时占用模型连接、数据库连接和工具连接,池太小会在池内排队,池太大又会压垮下游。配置时要考虑实例数:每实例数据库连接池 20 个,部署 10 个实例就可能产生 200 个连接,必须小于数据库可承受上限。
模型请求还受 token 吞吐、上下文长度和 GPU 内存影响,不能看到 HTTP 连接池有空闲就继续放量。应测量池等待时间、连接建立时间、下游处理时间和应用排队时间,并让上层 deadline 覆盖池等待。发生数据库连接耗尽时,优先减少新任务、拒绝低优先级请求或降级,而不是无限增加池大小。
常见追问及答案
- 追问:连接池大是不是吞吐就高? 只有在下游和本机资源仍有余量时才可能提高吞吐;过大通常增加上下文切换、锁争用和数据库压力。
- 追问:池等待需要计入 timeout 吗? 必须计入,否则请求在池里耗尽全部预算,拿到连接后已经没有时间完成业务。
扣分点
- 混淆连接数和业务并发。
- 忽略多实例总连接数。
- 不监控池等待和 deadline。
五、队列、限流与背压
Q13:什么时候应该使用队列?消息至少一次和恰好一次怎么回答?
考察点
是否能说明队列解决解耦和削峰,而不是把同步接口都改成异步。
回答脉络
先说适用场景,再说明投递语义、幂等消费、重试和死信。
详细参考答案
队列适合长耗时、可异步、需要削峰或需要在服务不可用时暂存的工作,例如文档解析、embedding、批量评估和长 Agent 任务。用户需要即时结果的简单问答不应无理由排队,应返回同步结果或明确的 202 任务状态。
工程上通常按至少一次投递设计:消息可能重复或乱序,消费者用 event_id、业务版本和唯一约束实现幂等;成功处理后再确认消息。失败按可重试/不可重试分类,指数退避后进入死信队列,并保留原始 payload、错误、尝试次数和 schema 版本。所谓“恰好一次”往往只能在特定事务边界内实现,跨队列和外部系统仍要靠幂等和对账。
常见追问及答案
- 追问:消息确认前 Worker 崩溃怎么办? 消息重新可见并由其他 Worker 处理,因此消费逻辑必须幂等,租约和可见性超时要避免两个 Worker 长时间同时执行。
- 追问:死信队列是不是错误终点? 不是。需要告警、人工或修复后重放流程,并防止重放未经权限和版本检查。
扣分点
- 宣称队列保证恰好一次而不解释边界。
- 消费失败无限重试。
- 没有死信、重放和幂等。
Q14:限流、熔断、降级和背压有什么区别?
考察点
是否能把四种保护机制放在正确的时间和层级。
回答脉络
分别定义准入、快速失败、能力缩减和消费速度控制,再讲组合策略。
详细参考答案
限流是入口准入控制,例如按租户、API key、模型和 token 速率限制请求;熔断是在检测到下游连续失败或延迟异常时暂时停止调用,给下游恢复空间;降级是主动减少能力,例如关闭 reranker、换小模型、只返回检索证据或转人工;背压是下游处理不过来时让上游减速、排队、拒绝或降低生产速度,防止内存和队列无限增长。
一个模型供应商变慢时,入口先按剩余容量限流,路由器停止把新请求发给异常供应商,简单问题走缓存或小模型,长任务进入持久队列;流式结果如果客户端消费慢,生产端要限制缓冲并暂停读取/生成,而不是无限积累 token。保护策略要记录触发原因和恢复条件,避免静默改变用户体验。
常见追问及答案
- 追问:熔断和限流哪个先做? 通常入口先限流保护自身,下游异常时再由熔断快速阻断;具体顺序按调用层级设计。
- 追问:降级能跳过权限检查吗? 不能。降级只能减少能力,身份、权限、数据隔离和高风险确认仍必须执行。
扣分点
- 把四者当同义词。
- 降级返回过时或越权数据。
- 没有背压导致内存和队列无界增长。
Q15:流式输出遇到慢客户端时如何做背压?
考察点
是否能处理生产者(模型)快于消费者(浏览器/下游)的情况。
回答脉络
先说明缓冲风险,再给有界队列、暂停读取、丢弃策略和断开回收。
详细参考答案
模型 token 生产速度可能高于客户端网络消费速度。如果服务端无界缓存,就会把大量 token 堆在内存里,最终拖垮整个 Worker。我会在模型读取器和响应写入器之间使用有界队列,队列满时暂停读取模型流;如果供应商 SDK 无法暂停,则限制读取窗口、取消生成或把流转为异步任务并只保留可恢复事件。
不能随意丢弃最终答案 token。进度类事件可以按策略合并,日志类事件可以采样,但文本内容要保证顺序和完整性。客户端断开时停止生成、关闭下游连接、释放队列和并发许可;如果已经生成的结果需要保留,则写入任务事件供查询,不要依赖断开的 HTTP 连接。
常见追问及答案
- 追问:把队列调大可以吗? 只能延缓问题,不能解决生产和消费速率不匹配;队列必须有上限,并配合暂停、取消或异步化。
- 追问:流式断开后还能恢复吗? 通过事件序号和任务快照可以恢复;单纯的 token 连接没有游标就无法可靠续传。
扣分点
- 只说加大缓冲区。
- 丢弃文本内容导致答案损坏。
- 断开后模型仍无限生成。
六、容器与运行时
Q16:如何为 AI API 构建可靠的 Docker 镜像?
考察点
是否掌握镜像可复现、最小权限、启动进程和模型/依赖缓存的实践。
回答脉络
从依赖锁定、多阶段构建、运行用户、启动方式和供应链安全展开。
详细参考答案
镜像构建应使用锁定的依赖版本和明确的基础镜像 digest,分离构建阶段与运行阶段,只复制运行所需文件,避免把源码仓库、测试数据和密钥带进镜像。Python 服务可以先构建 wheel 或虚拟环境,再复制到精简运行镜像;需要系统库的 OCR/音视频依赖要显式声明并做漏洞扫描。
容器以非 root 用户运行,文件系统尽量只读,临时目录使用受控卷,禁止把 API key 写入镜像层。PID 1 要能接收 SIGTERM,使用 exec 启动真实服务而不是让 shell 吞掉信号。模型权重和 embedding 模型按启动策略挂载或预热,不能每次请求临时下载。镜像标签、配置版本和 Git SHA 要写入运行信息,便于回滚和审计。
常见追问及答案
- 追问:为什么不在容器启动时
pip install? 会导致启动慢、版本漂移和外网依赖故障;依赖应在构建阶段固定,运行时只拉取明确版本的受控模型资产。 - 追问:镜像小就一定安全吗? 不一定,还要看依赖漏洞、运行权限、网络出口、密钥和供应链签名。
扣分点
- 镜像内硬编码 API key。
- 使用
latest且无法复现。 - 进程由 shell 包裹,无法优雅退出。
Q17:liveness、readiness 和 startup 检查有什么区别?
考察点
是否理解探针失败会触发什么动作,以及模型服务冷启动的特殊性。
回答脉络
区分“进程活着”“能接流量”“还在启动”,再说明检查深度和依赖边界。
详细参考答案
liveness 判断进程是否陷入无法自愈的死锁或崩溃,失败通常会重启容器;不应因为某个外部依赖暂时不可用就让所有实例不断重启。readiness 判断实例是否可以接收新流量,可以包含关键配置、连接池和模型 Worker 是否就绪,失败时从负载均衡摘除但不一定重启。startup 给慢启动服务准备时间,避免模型加载或缓存预热期间被 liveness 误杀。
探针要轻量、有超时、有限重试,不能每次探测都调用昂贵模型或第三方 API。外部依赖可以在 readiness 或业务层用更细的能力状态表示;例如模型主服务就绪但某个工具不可用,路由器应只关闭该工具能力,而不是让整个 Pod 不健康。
常见追问及答案
- 追问:数据库挂了,readiness 应该失败吗? 如果服务所有路径都依赖数据库,可以摘流量;若仍支持缓存或只读降级,要按能力拆分,避免全局不可用。
- 追问:liveness 失败会发生什么? 编排系统通常重启容器;因此检查必须只判断本进程是否能自愈,不能把普通业务错误当成 liveness 失败。
扣分点
- 三种探针混为一谈。
- 探针调用真实模型造成雪崩。
- 启动慢就不断重启。
Q18:服务滚动发布和优雅退出如何保证流式请求不被粗暴中断?
考察点
是否能处理停止接收、排空连接、任务转移和超时强杀。
回答脉络
先讲退出阶段,再讲短请求、流式请求和长任务的不同处理。
详细参考答案
收到 SIGTERM 后,实例先把 readiness 设为 false,停止接收新请求,但保留健康检查和管理接口;随后停止从队列领取新任务,等待正在运行的短请求和流式连接在 drain deadline 内完成。流式请求若无法在窗口内完成,应发送明确的终止事件和可恢复的任务 ID,客户端通过事件游标继续查询,而不是静默断开。
长任务不能依赖进程内内存。Worker 退出前释放租约或让租约过期,checkpoint 和动作状态已持久化,新的 Worker 可以恢复。到达硬退出时间后终止进程,启动恢复扫描处理未完成租约。滚动发布要限制同时不可用的实例数,先验证新版本 readiness,再逐步迁移流量,并保留快速禁用新配置的开关。
常见追问及答案
- 追问:为什么不能直接 kill 容器? 可能丢失流、重复执行动作和未写入的 checkpoint;优雅退出减少未知状态。
- 追问:连接一直不结束怎么办? 设置最大 drain 时间,先取消可取消请求,无法取消的任务转入持久化状态,最后强制退出并在恢复流程核对。
扣分点
- 只讲 Kubernetes 滚动更新,不讲请求和任务。
- 没有 drain deadline 和恢复机制。
- 把优雅退出当成回滚外部副作用。
七、可观测性与发布
Q19:AI 应用的日志、指标和 Trace 应该如何关联?
考察点
是否能建立从用户请求到模型、检索和工具的端到端证据链,同时控制敏感数据。
回答脉络
先统一关联 ID,再讲日志/指标/Trace 各自记录什么,最后讲脱敏和采样。
详细参考答案
入口生成或继承 trace_id,一次用户请求有 request_id,长任务有 task_id,每次模型调用、检索和工具调用有 span_id/tool_call_id。上下文传播到 HTTP header、队列消息和异步回调,不能只在日志文本里手工拼接。Trace 记录时间线和依赖关系;指标记录可聚合的延迟、错误、队列、token、成本、重试和业务成功率;日志记录结构化事件、状态变化和关键参数摘要。
日志不应默认保存完整 prompt、文档原文、密钥或工具结果。使用脱敏、hash、字段白名单和受控回放;Trace 可以保留输入输出引用而不是明文。高基数的用户 ID、问题文本不应直接作为 Prometheus label。采样要保证错误、超时、权限拒绝、未知副作用和高风险工具调用保留,正常低风险请求可以降采样。
常见追问及答案
- 追问:只看 LLM 的 token 和延迟够吗? 不够。还要看到检索召回、权限过滤、工具副作用、队列等待、重试、用户取消和最终业务结果。
- 追问:如何排查 P95 突然升高? 按阶段拆分 trace,先看入口排队、连接池、检索、模型 TTFT/生成、工具和重试,再按模型、租户、版本和区域分桶。
扣分点
- 只有字符串日志,没有关联 ID。
- 把敏感 prompt 全量写日志。
- 指标只看 QPS 和平均延迟。
Q20:配置、密钥和模型/Prompt 版本如何治理?
考察点
是否理解配置发布也是生产变更,能做到可审计、可回滚和不泄露秘密。
回答脉络
区分业务配置、密钥和版本化资产,再讲灰度、校验和回滚。
详细参考答案
超时、限流、模型路由、Prompt、工具 schema、检索索引和评估阈值都应有版本和生效范围,不能依赖某个实例的本地环境变量偷偷漂移。启动时校验必填配置和 schema,运行时对可热更新配置使用原子替换并记录 config_version。密钥放在 Secret Manager 或受控环境变量中,短期令牌最小权限、可轮换,日志和错误信息不回显。
发布采用 manifest 固定一组互相兼容的模型、Prompt、工具和索引版本,先在离线 golden/eval 和 sandbox 验证,再按租户或流量灰度。监控质量、安全、成本、延迟和错误;发现回归时回滚 manifest 或关闭高风险能力,而不是篡改已发生的业务记录。每次请求的 trace 记录配置版本,才能复现线上行为。
常见追问及答案
- 追问:Prompt 改一行也需要版本吗? 需要。它可能改变工具选择、成本和安全边界,至少要有可比较、可回滚的版本。
- 追问:配置热更新会不会带来半更新状态? 通过版本化快照和原子切换,单次请求固定读取一个版本;不要让同一请求混用两份策略。
扣分点
- 把密钥提交到仓库或写入镜像。
- 修改 Prompt 没有灰度和回滚。
- 线上行为无法追溯到配置版本。
Q21:如何排查“接口成功率正常,但用户觉得系统很慢且答案变差”?
考察点
是否能跳出 HTTP 200,结合体验、质量和长尾数据定位复合回归。
回答脉络
分别检查延迟分解、质量证据和版本/分桶,最后给出止损和验证方案。
详细参考答案
先按 trace_id 分解端到端时间:排队、鉴权、检索、rerank、模型 TTFT、生成、工具和流式缓冲,比较 P50/P95/P99,而不是只看平均值。用户“慢”可能是首 token 变慢,也可能是 token 之间间隔变大、SSE 被网关缓冲,或者答案完成前等待了一个不必要的工具。
答案变差要检查检索命中、权限过滤、引用版本、Prompt/model/config/index 版本和降级比例。按租户、问题类型、语言、文档版本和模型路由分桶,排除少数长文请求掩盖的回归。若确认新 reranker 或模型导致问题,先停止灰度、恢复上一个 manifest 或关闭复杂路径,再用固定评测集和线上抽样验证修复。
常见追问及答案
- 追问:成功率 99% 还需要处理吗? 需要。成功率可能只表示 HTTP 返回 2xx,不能说明引用正确、安全、延迟和用户任务是否完成。
- 追问:如何判断是网关缓冲还是模型慢? 比较模型生成时间、首 token 时间、下游发送时间和客户端收到事件时间;在各 hop 记录带序号的时间戳。
扣分点
- 只重启服务或扩大机器。
- 只看平均延迟和 HTTP 状态码。
- 没有按版本和任务类型分桶。
Q22:如何为 AI 应用设计一次故障注入和上线验收?
考察点
是否能把可靠性要求转成可执行测试,而不是只做 happy path。
回答脉络
从契约、故障、压力、安全和发布门禁五层回答。
详细参考答案
契约测试覆盖 API schema、模型结构化输出、工具响应、队列事件和回调版本;集成测试覆盖数据库事务、缓存失效、索引发布和对象引用。故障注入要模拟 DNS/连接失败、429/5xx、响应丢失、流式断开、模型超时、消息重复乱序、Worker 崩溃、数据库只读、对象存储不可用和外部 202 未知结果。对每种故障断言系统不会越权、不会重复副作用、会进入可观察的状态并最终恢复或人工收口。
压力测试同时改变 QPS、输入长度、输出长度、租户比例和工具成功率,观察队列、池等待、内存、TTFT、P99 和成本。发布门禁包括安全硬失败率、业务成功率、引用/工具正确性、P95/P99、重试放大、队列积压和资源余量。上线先灰度,保留停止调度、禁用工具、切换模型和回滚 manifest 的开关;测试产生的临时服务和数据在完成后清理。
常见追问及答案
- 追问:为什么只做单元测试不够? 很多问题发生在数据库、队列、网络和供应商边界,单元 mock 会掩盖超时、重复和协议不兼容。
- 追问:复杂页面测试是否必须全量自动化? 先确保服务端契约、事件补发和关键用户路径;页面长时间压力测试应单独评估成本和收益,不能以截图替代后端可靠性验证。
扣分点
- 只有正常路径和静态 lint。
- 故障注入不检查外部副作用次数。
- 没有上线门禁、灰度和止损开关。
八、综合设计题
Q23:设计一个支持流式回答、工具调用和长任务转异步的 AI API。
考察点
是否能把同步接口、流式事件、状态机、工具安全和恢复串成完整系统。
回答脉络
先定义请求分类,再讲模块和数据流,最后讲故障、观测和扩展。
详细参考答案
入口负责认证、租户限流、幂等创建和输入校验。短请求进入编排器,检索和权限并行执行,模型输出 token 或结构化工具意图;工具网关负责 schema、权限、确认、超时和幂等。SSE 层把 token、工具状态和终态事件发送给客户端,并保存事件序号。超过同步软 deadline 的请求转成持久任务,返回 task_id,Worker 从队列领取并把状态、checkpoint、动作 ledger 和结果引用写入数据库/对象存储。
客户端断开时,短请求取消生成;长任务继续或按策略暂停,不能丢失外部动作状态。事件流重连时按游标补发,任务查询返回权威快照。模型、工具或队列故障按错误类型重试或降级,副作用先查询状态。Trace 贯穿 request/task/tool,指标包括 TTFT、P95、任务成功率、队列等待、重试、未知动作和成本。发布时固定模型、Prompt、工具和索引版本,支持按租户灰度和快速禁用高风险工具。
常见追问及答案
- 追问:为什么需要任务快照和事件两种数据? 快照便于快速查询和恢复,事件保留完整历史、补发和审计;只存其中一种都会让某类场景困难。
- 追问:工具调用的结果丢了怎么办? 用
tool_call_id/幂等键查询动作 ledger;已成功则复用结果,处理中则等待,未知则暂停核验,不能凭模型上下文猜测。
扣分点
- 只有“前端 -> LLM -> 返回文本”的框图。
- 流式断线无法恢复。
- 没有长任务状态、幂等和权限边界。
Q24:如果让你在今天上线一个企业 Agent,你会如何确定第一版工程范围?
考察点
是否具备控制复杂度、识别高风险和渐进交付的能力。
回答脉络
先缩小任务和工具范围,再列不可缺少的可靠性底座,最后讲后续演进。
详细参考答案
第一版选择只读、边界清晰且可验证的任务,例如带权限过滤和引用的知识问答,或生成工单草稿而不是直接提交。保留一个模型路由、少量窄工具、明确的同步/异步边界和固定的评测集。工程底座不能省:认证与对象级授权、请求/任务 ID、总 deadline、有限重试、工具幂等、结构化日志和基本 trace、状态查询、人工接管以及禁用工具的开关。
暂不引入没有实际需求的多 Agent、复杂 WebSocket、跨区域一致性和全自动高风险动作。上线前用真实脱敏样本验证引用、权限、无答案、超时、重复请求和客户端断开;灰度观察成功率、P95、成本和安全事件。等数据证明瓶颈后再增加 reranker、模型 fallback、队列分区或更复杂的缓存。第一版的目标是可解释、可停止、可恢复,而不是功能数量最多。
常见追问及答案
- 追问:为什么不一开始就支持所有工具? 工具数量会放大提示词、权限、评估和副作用复杂度;窄接口更容易验证和审计。
- 追问:哪些能力绝不能为了赶进度省略? 服务端授权、敏感数据保护、超时/取消、幂等与未知结果处理、可观测性和事故止损开关。
扣分点
- 把“先上线”理解成先忽略安全和恢复。
- 只列技术名词,没有明确业务边界。
- 用模型提示词代替服务端工程约束。
结语
AI 应用的工程基础不是把传统后端名词换成“Agent”前缀,而是面对更长的调用链、更高的不确定性和更昂贵的失败。一个成熟回答应同时说明:并发怎样受控,接口怎样演进,超时后状态是否确定,副作用怎样幂等,数据怎样隔离,故障怎样观测和恢复,以及发布后如何快速止损。能把这些约束落到一次具体的模型、检索和工具调用链上,才算真正掌握了通用工程基础。