面试知识库

ShoppingX 后端设计增强方案#

定位:在现有「纯 Agent 系统」之上,叠加能体现后端工程能力的基础设施模块。面试 / 作品集导向 —— 每块追求「痛点清晰 + 能画架构图 + 一个关键权衡 + before/after 可量化」,而非生产全量。

原则做透 1~2 块 > 铺一堆半成品。优先复用本仓库已有基建(Qdrant 召回、llm.py、FastAPI/WS、usage.py、Langfuse)。

本文整合自两份前期调研(BACKEND_HIGHLIGHTS.md 的现状核实 + 市面调研,ShoppingX后端设计亮点调研.md 的分模块落地设计),去掉代码细节,只保留设计思想、权衡与叙事


✅ 落地状态(截至 2026-06-30)#

P0 + P1 已全部落地并合入 main,按推荐执行顺序 C → B → A → D → E → F → I 逐块完成。每块走统一流程:关一(ruff/mypy/pytest)+ 关三(/code-review high effort,发现全部处置)+ 面试向开发文档(docs/milestones/ENH-*.md)+ 单块提交。

内容状态开发文档
C并发背压(任务槽 + fork 级 Semaphore + 429)✅ 已合并ENH-C-并发背压
B韧性工程(断路器 + 退避重试 + 降级链,对准 4 真实外呼)✅ 已合并ENH-B-韧性工程
A可观测性收缩版(Prometheus metrics + structlog)✅ 已合并ENH-A-可观测性
D事件回放(Redis Stream 断线重连补发)✅ 已合并ENH-D-事件回放
E语义缓存(仅 category_insight,精确 + 语义两级)✅ 已合并ENH-E-缓存
FFinOps 预算闸(token 从「测」到「控」)✅ 已合并ENH-F-FinOps预算闸
IJWT 鉴权(堵 user_id 越权读 Store)✅ 已合并ENH-I-JWT鉴权

未做(维持「讲得出为什么暂不做」的毕业线 / P2),详见 §三优先级矩阵与各模块说明:

  • I 块仅堵 user_id 维度:thread 维度资源(/api/history/api/files/ws/api/task/{tid}/cancel/api/upload)尚无属主校验,需 thread_id → owner(user_id) 归属表,列为下一增量(不做半成品授权)。
  • I 的 P2(限流 / 版本 / 幂等)、F 的跨 provider fallback(只有一家 provider key)、A 的 OTel system traceD 多副本事件总线/路由C 任务持久化/队列G GuardrailsH 数据管道J 依赖注入 均按计划暂不做。

下文「现状画像 / 模块清单 / 优先级矩阵」保留为立项时的分析与权衡(解释每块为什么做、怎么取舍),是上面落地状态的依据,不随完成而删。


〇、背景与定位#

ShoppingX 现有骨架已覆盖 Agent 侧 的设计深度:

  • Agent 调度层:AgentLoop(Think→Act→Observe→Reflect)+ 同质子 Agent fork
  • 检索层:Qdrant 单栈 dense + payload filter,BGE-M3 召回 + BGE-Reranker-v2-m3 精排
  • 压缩层:Cache Breakpoint 上下文压缩
  • 记忆层:规则偏好 Store
  • 事件协议:AGUI 事件 + WebSocket
  • 评测闭环:Rubric 离线评测 + 上下文级飞轮(评测 → 定位 bad case → 改 prompt/工具/沉淀 few-shot → 再评测;零 GPU、不做 SFT/RL 训练

但从「后端工程功底展示」角度看,几块经典的后端基础设施能力当前是空白或只有占位。补上它们,能让项目在面试 / showcase 时显著区别于「demo 级 Agent 项目」。

下面先用代码核实的现状画像摆清家底(哪些已经做到同类项目偏上水平、哪些是真缺口),再给出增强模块清单优先级取舍


一、现状画像(对着代码核实,不是文档推断)#

逐条翻源码核实「已对上 vs 真缺」,每条标出代码出处。 图例:✅ 已对上 | 🟡 部分(有雏形 / 缺关键一环)| ❌ 缺

#业界经验点现状(代码证据)判定真实缺口 / 下一步
1Agentic loop 必装熔断 / 护栏、防 token 烧光fork 安全四层全齐:深度闸 MAX_FORK_DEPTH=1fork_guard.py)、循环检测 LoopDetector(窗口 6 / 阈值 4)、结果截断 4000 token、超时 + 迭代上限(dispatch_tool)。另有耗尽即收敛——搜满/fork 后/收尾后在执行层回哨兵拦下工具(middleware.py不摘工具以保 prompt cache 前缀,见 M6.1 / Mperf.1)比多数博客讲得更狠,面试主推亮点,当文档主线
2「别停在 Agent 那层」:要有完整后端层FastAPI 6+ 接口、WS 事件流、文件接口、偏好 / 历史读口、health;lifespan 预热分词器、safe_join 防穿越、上传限额(server.py骨架在了,但 3–6 是骨架里的空洞
3任务持久化 / 可恢复active_tasks: dict[str, asyncio.Task] 纯内存 + asyncio.create_task。有取消、同 thread 防重、按身份摘除防串台🟡进程内生命周期干净,但进程重启即全丢;无持久状态机 / 重试 / 死信 / 优先级。未挂 checkpointer(CLAUDE.md 主动划界)
4WS 多副本路由 + 事件回放ConnectionManager 进程内 dict + asyncio.Lock、身份比较防误删、推送失败摘连接;connect-first 协议避免早期事件丢失🟡单进程绑死,2 副本即坏(任务在 A、连接在 B 推不到)。无 Redis Pub/Sub 事件总线;无 last_event_id 回放
5外部依赖弹性:超时 / 重试 / 熔断 / 限流LLM timeout=60s + max_retries=2;HTTP 外呼均有超时(reranker 10s、towers 30s、web_search 20s);reranker 远程挂了优雅降级本地打分🟡超时齐、LLM 有重试、有降级;但 HTTP 依赖无退避重试、无熔断器、无全局并发闸 / 限流。fork 并行高并发缺背压
6成本 / Token 预算治理(FinOps)检索预算强:全树计数按 session 聚合(retrieval_budget.py)、ForkBudget。token 用量已测量:carried / peak / cache_read / 命中率聚合发 Langfuse(usage.py🟡「检索次数」是闸、「token 成本」只测不控。按 token/成本的预算闸、per-user/session 成本上限与超限熔断
7可观测性不可省 + OTel traceLangfuse 接主链路,多 fork 归并同一条 trace、session=thread_id、安静降级(tracing.py);AGUI 8 类事件(monitor.py);health 报 active_tasks🟡LLM 链路追踪完整;但缺系统级 metrics(Prometheus QPS / 延迟分位 / 错误率)、无贯穿 API→任务→外呼的 OTel system trace、health 过简
8ASGI 调优(uvloop / httptools 5–10x)uvicorn[standard] —— standard extra 已自动带 uvloop + httptools✅(默认吃到)已默认享受,但无显式 worker / 并发配置、无压测数据佐证。补一条 benchmark 即可讲
9Guardrails:不可信内容隔离 / 输入校验深度权限闸(子 Agent 无收尾 / 上下文工具授权,middleware.py)、web_search 兜底门、CORS、safe_join、上传限额🟡Agent 内部权限边界好;但 RAG/web_search 拉回内容当不可信源的注入隔离、工具入参校验、鉴权 / RBAC / 多租户

结论(真实画像)#

「Agent 运行时的护栏 / 权限 / 压缩 / 追踪做到同类项目偏上水平(#1/#2/#7/#9 的 Agent 侧、压缩、tracing),但『服务底盘』还是单机内存态 demo(#3/#4),弹性与成本是『测了 / 拦了一半』(#5/#6)。」

  • #1 fork 安全四层 + 耗尽夺权:被低估的硬亮点,代码比博客扎实,面试主推
  • 真缺口集中在 #3、#4:任务持久化 + WS 多副本——「内存态单机」硬伤,改造前后对比最鲜明。
  • 性价比快赢在 #5、#6、#7:HTTP 依赖加退避重试 + 熔断 + 并发闸(#5)、token 预算从「测」升级到「控」(#6,已有 usage.py 雏形)、补 Prometheus(#7)——投入小,把「部分」补成「完整」。

二、增强模块清单#

每块统一给出:解决什么问题 → 落地思路 → 关键权衡 → 设计亮点 / 面试叙事。按主题整合,不含代码

A. 可观测性补全:Metrics + 结构化日志(OTel system trace 降级到毕业线)#

解决什么问题。 一次 query 涉及主 AgentLoop → 多个 fork 子 Agent → 每个子 Agent 内部 2–3 次工具调用 → Qdrant 检索 → rerank → 合流。延迟从哪来、哪一步卡了、哪个平台拖后腿——没有系统级可观测性就是黑盒(之前定位 295s 延迟回归就是在黑盒里摸)。现状是 Langfuse 已覆盖 LLM 链路 trace,真正缺的是系统级 metrics(QPS / 延迟分位 / 错误率)与结构化日志

先厘清「trace 已有 vs 真缺」(关系到下面「做什么 / 不做什么」):

  • 现有 tracing.pyLangfuse v4 本身就基于 OpenTelemetry,且已把多 fork 归并同一条 trace、session=thread_id(tracing.py)。调用树级追踪已经在了
  • 因此不再自建 opentelemetry-sdk 完整栈——那会和 Langfuse 重复造轮子(两套 span 上下文、两份导出)。要补几条非 LLM 的 span(如 Qdrant 检索段),直接用 Langfuse 的 span API 挂在已有 trace 上即可。
  • 本块真正要补的就两样:Prometheus metrics + structlog

落地思路。

  • Prometheus Metrics(本块核心):暴露 /metrics,关键指标 —— active_tasks gauge、tool_duration_seconds histogram(按 tool_name 分桶)、fork_depth histogram、qdrant_latency_seconds histogram、rerank_short_circuit_rate counter、cache_hit/miss_total。系统级 QPS / 延迟分位 / 错误率从此可看板化。
  • 结构化日志:用 structlog 替代 print/logging,每条日志自动带 thread_id / fork_depth / user_id
  • (可选)补非 LLM span:若想让 Qdrant 检索 / rerank 段也进调用树,用 Langfuse 已有 trace 的 span API 加几条 child span,而非另起 OTel 栈。

关键权衡。 既不上 Jaeger 全套分布式 trace 存储,也不引 opentelemetry-sdk 自建 system trace——Langfuse 已提供调用树追踪,再叠一套 OTel 是性价比最低的一档。完整 OTel system trace 挪到毕业线:真要把 API→任务→外呼的 trace 脱离 Langfuse、对接 Jaeger/Tempo 时再做。

设计亮点 / 叙事。 复用已有的 ContextVar 体系让结构化日志自动携带上下文:set_thread_context 时用 structlog 的 bound logger bind 一次 thread_id / fork_depth / user_id,后续日志自动携带,工具实现者无感。核心叙事:「ContextVar 既做 thread 隔离,又做日志上下文传播——一个机制解决两个问题;trace 这一层不重复造轮子,直接吃 Langfuse 已经基于 OTel 归并好的调用树。」


B. 韧性工程:Circuit Breaker + Retry + 降级链#

解决什么问题。 系统不是在所有依赖健康时能跑就行,而是在部分依赖挂掉时仍能提供有意义的结果。现状超时齐、LLM 有重试、reranker 有本地降级,但 HTTP 依赖无退避重试、无熔断、无全局并发闸

落地思路。

断路器对准「真实外呼」,不对虚构对象。 现状平台商品数据是本地 CSV / Qdrant,并无真实多平台 API 外呼,所以「每个平台 API 一个断路器」目前没有保护对象。真正会抖动 / 挂掉、值得上断路器的外呼只有 4 个:reranker(siliconflow)、towers(embedding API)、web_search(tavily)、LLM。断路器只对这 4 个建。

  • 工具级 Circuit Breaker:对上述 4 个真实外部依赖各建一个断路器,状态机 CLOSED → OPEN → HALF_OPEN,滑动窗口(最近 N 次失败率 > 阈值则 OPEN)。OPEN 时直接走降级路径而非干等超时。

  • 降级链(核心架构思考):给依赖这些外呼的工具定义 fallback 链——

    工具 / 外呼正常路径降级路径 1降级路径 2
    item_search(towers embedding)向量编码 + Qdrant 检索embedding 外呼断路 → 退化为 BM25 纯文本搜索返回空 + 低置信度标记
    category_insight(reranker)Hybrid + Reranker 精排reranker 断路 → 跳过精排用粗排 Top-K(已有本地降级短路逻辑WebSearch 兜底
    web_search(tavily)Tavily 外呼断路 → 返回空 + degraded 标记,交还 Reflect——
    shipping_calc(汇率,如接实时 API)实时汇率 API本地缓存汇率表(TTL=24h)返回估算值 + 标记 estimated=True
  • 指数退避 + Jitter 重试:区分可重试异常(timeout、5xx)与不可重试异常(4xx、数据格式错误),只对前者退避重试(tenacity)。

关键权衡。 熔断阈值 / 恢复窗口是经验值,过激会误伤抖动、过缓失去保护意义——配合 metrics(A 块)观测后调参,不拍脑袋。reranker 已有本地兜底降级,是这套降级链最现成的样板,其余三个外呼照此补齐。

设计亮点 / 叙事。 降级结果不是静默吞异常,而是返回带 confidence / degraded / degradation_reason 的结构化结果。主 AgentLoop 在 Reflect 阶段看到 degraded 标记可以决定换策略(如某平台断路了就不再 fork 子 Agent 去搜它)。核心叙事:「降级信息参与决策——比 try-catch 高出一个层次。」


C. 任务持久化 + 背压控制#

解决什么问题。 现状 active_tasks: dict + asyncio.create_task 单实例能跑,但有工程硬伤:进程重启任务全丢、无并发上限、无优先级、无重试

先厘清两件正交、易混的事(关系到下面「做什么 / 不做什么」):

  • 页面刷新后任务继续 = 抗浏览器断线。现状已扛住——/api/task 是后台 asyncio.create_task 跑、与 WS 连接解耦(server.py),刷新不杀任务。断开窗口的事件会丢,由 D 块事件回放(Redis Stream) 补齐;而整页刷新 / 切换对话这种「冷启动」(前端 JS 上下文全没、连不上原来的重连逻辑)下要重新订阅并重建「正在跑的那一轮」,是后续增强加的一个探口 + 「进入对话即续看」路径补齐的(顺带把「切对话隐式取消任务」改成绝不隐式打断),详见 ENH-D §5
  • 任务持久化(durability) = 抗服务端进程重启/崩溃。进程一死,内存里的 asyncio 任务 + dict 一起没,无外部态可恢复。本块「不做」的就是这一件,与刷新无关。

落地思路(单机定稿)。

  • ✅ 做 · 并发背压(asyncio.Semaphore):任务级 semaphore 限全局并发;fork 级 semaphore(session 内)限单任务同时 fork 的子 Agent 数——用标准并发原语机械执行 retrieval budget,而非靠 prompt 约束模型遵守。零新依赖、立刻见效。
  • ✅ 做 · 背压信号回前端:semaphore 满时 /api/task 返回 429 Too Many Requests + Retry-After,可附排队位置。
  • ❌ 不做(写进毕业线)· 任务持久化 / 队列:单机、无「必须干完」语义(购物 query 中断重提交即可),且恢复半跑的多步 LLM Agent 流本身很难,ROI 为负。毕业线:出现多 worker / 必须干完 → Redis Stream 持久化队列(XREADGROUP/XACK、状态机持久化、幂等键、死信、超时回收)或 arq;要「节点内部跑一半也能 replay 续跑」→ Temporal(checkpointer 只存节点之间,存不了节点内循环,这是要 Temporal 的理由)。

关键权衡。 CLAUDE.md §2.2 明确把 checkpointer / 持久化划在范围外(理由:一请求一进程内 async task,无跨进程恢复需求)——本块维持这条边界。要做 Durable Execution 就是主动推翻它,把「当初为何不做 / 现在为何值得做」讲成权衡故事,而非无脑上 Temporal 引重依赖。落地次序:Semaphore 背压(现在做,零依赖)→ Redis Stream(毕业线)→ Temporal(仅确有可恢复长任务时)

设计亮点 / 叙事。 核心叙事:「用标准并发原语(Semaphore)而非 prompt 约束做 retrieval budget 控制——机械执行不依赖模型遵守指令。」(与 #1 fork 安全四层是同一套「机制兜底优于提示词」哲学。)


D. WebSocket 多副本路由 + 事件总线 / 回放#

解决什么问题。 两个层次,单机定稿里拆开处理

  • 事件回放(现在就做):现状 connect-first 协议只解决「任务起步早于首次连接」,不解决「中途断线重连」——用户刷新页面 / 短暂断网,断开窗口里发出的 AGUI 事件就丢了。任务本身在后台继续跑(见 C 块),但用户看到的事件流有个洞。
  • 多副本路由(毕业线,不做)ConnectionManager 是进程内 thread_id→WebSocket,部署 2 副本后任务在 A、连接在 B → 推不出去。单机不触发,写进毕业线。

落地思路(单机定稿)。

  • ✅ 做 · 事件回放(Redis Stream):每个 thread_id 一条 Redis Stream,事件 XADD 写入并 MAXLEN 自动裁剪(如保最近 200 条);stream ID 天然就是 last_event_id,前端重连时带上它,服务端补发缺口事件再转直播。选 Redis Stream 而非进程内 ring buffer 的理由:Streams 本就是为「带位置追踪的事件日志」设计,且同一套 Stream 即下面多副本事件总线的底座——一个决定两处收益
  • ❌ 不做(毕业线)· 多副本事件总线:部署 ≥2 副本时,用 Redis Pub/Sub 做跨实例广播(任意 worker publish(thread_id, event),持连实例 subscribe 转发,WS 层无状态化)——与上面的回放 Stream 共用一套 Redis 基础设施。
  • 备选:SSE / token streaming + 背压做单向事件流(很多团队用 SSE 替代 WS 做单向流),不改 Redis 这一侧的决策。

关键权衡。 现状刻意用 connect-first 协议换掉了事件缓冲——单进程「起步」够用,但「重连」不够,正是事件回放要补的洞。回放选 Redis(已在跑)是因为它本质是「跨重连 / 将来跨进程」的持久事件日志,恰是 Stream 的甜区;多副本路由维持「讲得出为什么暂不做」。

设计亮点 / 叙事。 核心叙事:**「无状态化 + 跨实例消息路由 + 最终一致事件流——把『2 副本即坏』的硬伤改成可水平扩展。」**改造前后对比最鲜明(对应取舍表「次选」档)。


E. 多级缓存 + 语义缓存(本项目独特优势)#

解决什么问题。 已有 Cache Breakpoint 处理 token 成本,但数据层缓存还没系统设计。重复的品类查询、汇率转换、Qdrant 检索可通过缓存显著降低延迟和外部调用。

落地思路。

铁律:强时效数据(价格 / 库存)一律不进缓存。 购物场景缓存的最大事故源是「返回了旧价格 / 已下架商品」。因此 item_search 的商品候选(带价格 / 库存)不缓存;只缓存弱时效对象:品类知识(category_insight)、汇率 / 税率表(本身按 TTL 刷新)。

缓存层技术缓存对象TTL失效策略
L1 本地cachetools.TTLCachecategory_insight 热品类结果(弱时效1h自动过期
L1 本地cachetools.TTLCache汇率表、税率表24h定时刷新(不用 lru_cache,无 TTL)
L2 RedisRedis String + TTLQdrant 商品检索结果——不做:含价格 / 库存,强时效,缓存=返回旧价风险
L2 Redis(毕业线)Redis String + TTL弱时效对象的跨进程共享——部署 ≥2 副本要共享热区时再上(见 §二·五 D)
L3 广播失效(毕业线)Redis Pub/Sub知识库灌库后失效通知Subscriber 清对应 key,不全量 flush
  • 语义缓存(高杠杆,但限定弱时效域):非精确匹配——query 向量化,近似命中直接返回历史结果。业界报告减 30–60% LLM 调用、高重复场景 ~73% 降本,命中毫秒级 vs 几秒推理。本项目已有整套向量召回基建(Qdrant + embedding),做语义缓存几乎零额外依赖——把召回层复用成缓存层。
    • 只对 category_insight 做语义缓存(知识库内容弱时效、重复查询多、命中即省一整条 Hybrid+Reranker 链)。
    • item_search 绝不做语义缓存:商品候选带价格 / 库存,「近似命中」返回别的 query 的旧结果在购物场景是硬伤。

关键权衡。 语义缓存的相似度阈值是双刃剑:太松会返回「貌似相关其实不对」的结果,太紧命中率低。需配缓存命中率 metric(A 块)持续校准。把适用域收窄到 category_insight 这一弱时效对象,正是为了在拿到降本收益的同时绕开价格 / 库存的时效陷阱——先把安全的那块做透、用数据验证命中率与正确性,再谈是否扩域。

设计亮点 / 叙事。 缓存命中率本身是 Prometheus metric(cache_hit_total / cache_miss_total),接入 A 块可观测性体系。核心叙事:category_insight 缓存命中率稳定在 30%+,省了 30% 的 reranker 调用——用数据说话。」


F. 轻量 LLM Gateway + 成本 / 用量治理(FinOps)#

解决什么问题。llm.pyinit_chat_model 裸封装,主 + 子 Agent 共享。Agent 是「成本放大器」(fork + 多轮 + 工具链,token 易失控);现状 token 用量只测不控usage.py 是雏形)。

落地思路(本体 = 成本治理;跨 provider fallback 留毕业线)。

先认清现状再分主次。 llm.py 现在是单一 OpenAI 兼容 endpoint(一个 base_url / 一份 key)。所谓「跨 provider fallback」前提是至少配两家 provider 的 key——现状没有第二家,硬写就是叙事倒地。因此本块本体是成本治理(用量归集 + 预算闸),fallback 视是否真有第二家 provider 决定

  • ✅ 本体 · 成本治理(FinOps)
    • token metering:按 user / session / 工具维度归集用量与成本、可归因(升级现有 usage.py 雏形——它已测 carried / peak / cache_read / 命中率,缺的是「按维度归集 + 落预算」)。
    • 预算闸:per-session / per-user token 预算,超了熔断(把 fork 侧「检索次数预算」升级成全链路 token / 成本预算——次数闸已有,成本闸是新增)。
    • 模型能力匹配任务难度:planner 便宜模型 / judge 强模型(get_llm / get_fast_llm / get_judge_llm 已是雏形,做实成本最优路由)。
  • 🟡 视条件做 · 轻量 LLM Gateway(自研薄层,参考 LiteLLM / Portkey 思想,不引重组件)
    • 路由策略:优先级 / 最低延迟 / 成本优化(简单任务走便宜模型)/ 语义路由(按 query 内容选模型)——单 provider 下也成立,因为本质是「在同一家的多个模型间选」,可先做。
    • 跨 provider fallback(毕业线):等真接入第二家 provider 的 key 再做(血泪教训:fallback 全指向同一家 = 没做,要跨 provider才有韧性意义)。在那之前 llm.py 已有的 max_retries=2 是同 provider 内的兜底。
    • provider cooldown / tpm/rpm 限额:单进程先用进程内令牌桶(见 §二·五 B),多 worker 撞 provider 限额再上 Redis(毕业线)。

关键权衡。 Gateway 自研 vs 引 LiteLLM:自研轻量层(路由 + 用量归集 + 预算闸)与 fork 高频外呼天然契合、可控;引 LiteLLM 省事但多一层重代理、且其核心价值(跨 provider 路由)在只有一家 key 时用不上。本项目规模建议自研轻量层,把「为什么不直接上 LiteLLM」「为什么 fallback 暂留毕业线」一起讲成权衡。

设计亮点 / 叙事。 核心叙事:**「token 预算从『测』升级到『控』——per-session 成本上限 + 超限熔断,FinOps 不是事后看账单而是事前设闸。」**复用 usage.py 现成雏形、几乎不引重依赖,降本数据好量化——当前能立刻落地、性价比最高的一块(跨 provider 那条等第二家 key 到位再点亮)。


G. Guardrails 安全护栏#

解决什么问题。 生产级远不止内容审核。现状 Agent 内部权限边界好(深度权限闸),但缺不可信内容隔离工具入参校验。本项目有 web_search / category_insight 外部内容入口,做这层很自然。

落地思路。 输入校验(工具 schema / 参数范围)、工具级 RBAC、prompt injection 防御(把 RAG / web_search 拉回的内容当不可信源隔离)、输出约束(JSON schema / 引用)、高危动作人工审批、预算与工具调用次数上限。

关键权衡。 单拎出来后端味略淡,适合顺手做——与 B 块的结构化 ToolResult、F 块的预算闸、I 块的入参校验天然耦合,不必独立成大模块。

设计亮点 / 叙事。 「外部拉回内容默认不可信」是 Agent 普遍短板,补上即与多数 demo 拉开差距。


H. 数据管道 + Schema Evolution#

解决什么问题。 数据侧目前是「Kaggle CSV + 手动灌库」。加一层正式 ETL 展示数据工程功底,也为多平台数据接入(AliExpress 900k、eBay PromptCloud、Shopee)打基础。

落地思路。

  • 四阶段标准化管线RawProduct →(统一 schema + 清洗)NormalizedProduct →(附加向量)EmbeddedProduct →(写入 Qdrant)IndexedProduct,每阶段用 Pydantic model 做 schema 校验,字段缺失 / 类型错误在入库前拦住。
  • 入库门禁(Quality Gate):标题长度、价格合理区间、品类路径非空等规则,脏数据入库前拦截。
  • Schema Migration(Qdrant 版):Qdrant 无 Alembic 式工具,自建版本化迁移脚本(如「为存量记录用小模型补 material 字段并回写 payload」),记 migration_version
  • 数据血缘(Lineage):每条入库商品带 source / ingested_at / pipeline_version / source_file,出问题可追溯来源。

关键权衡。 投入产出比定位在 P1——不是当前痛点,但为「多平台扩展」铺路,且 schema 校验 / 门禁是低成本高收益的源头防线。

设计亮点 / 叙事。 核心叙事:「数据质量决定召回质量,入库门禁是投入产出比最高的一层防线——从源头拦住脏数据,比事后在 prompt 里打补丁强一个数量级。」


I. API 层生产化加固#

解决什么问题。 当前 FastAPI 层接口在了,但无鉴权、限流、版本管理、幂等。这些是后端工程师的「基本功」。

落地思路。

  • 🔴 JWT 鉴权中间件(P1,先做)user_id 从 token 解析而非前端传入——堵住伪造 user_id 读取他人 Store 的真实安全漏洞。现状 user_id 由前端传入、无任何校验(context.py_user_id_var 但无 token 验证),任意改 user_id 即可读他人长期偏好 Store,这是已存在的越权读漏洞,不是「完整度打磨」。与 CLAUDE.md「鉴权 / RBAC / 多租户留作业」呼应,但越权读这一条应优先堵上。
  • 请求级限流(Sliding Window):基于 Redis ZSET / INCR+EXPIRE 的滑动 / 定窗限流,超限 429 + Retry-After
  • API 版本管理/api/v1 /api/v2 路由前缀并存,平滑演进事件格式。
  • 请求 / 响应 Pydantic ModelTaskResponsestatus / thread_id / estimated_seconds / position_in_queue
  • 幂等性idempotency_key 命中直接返回上次结果,防重复提交。

关键权衡。 本块拆成两档JWT 鉴权提到 P1(堵越权读,是安全洞不是打磨),其余(限流 / 版本 / 幂等)维持 P2 收尾。鉴权这一条与 G 块安全护栏、C/F 块 user 维度预算可共享 user_id 主体——一处解析、多处复用。

设计亮点 / 叙事。 核心叙事:「API 层不是 Agent 的附属品,而是独立的后端服务——鉴权、限流、幂等、版本管理一个都不少。」


J. 依赖注入 + 可测试性#

解决什么问题。 当前 store / monitor / get_llm() 都是模块级单例,单元测试很痛——无法 mock 掉 Qdrant / LLM 去测工具逻辑

落地思路。

  • FastAPI 原生 Depends 做依赖注入:get_store / get_llm 作为可注入依赖。
  • 测试时用 app.dependency_overrides 替换为 MockStore / AsyncMock LLM。
  • 工具实现从「内部直接 import client」改为「从 ContextVar / 参数获取 client」,测试时可替换。

关键权衡。 定位 P2,投入约 1 人天,收益是「可测试性从零到一」。改造面广(触及工具内部 client 获取方式),建议在骨架稳定后做,避免边改边返工。

设计亮点 / 叙事。 核心叙事:「能写出 mock 掉向量库和 LLM 的集成测试,在 demo 项目里几乎看不到——但这是后端工程师的基本素养。」


二·五、单机选型定稿(逐项拍板 + 毕业线)#

在「单机优先」哲学下逐项拍定。口径不是「能不能少碰 Redis」,而是该 Redis 的地方就 Redis、该进程内的地方就进程内,判据是这件事的本质。Redis / Qdrant / OpenSearch 均已在跑,本清单零新增基础设施

选型三判据:

  1. 进程内原语在单机下严格更优的(并发闸、L1 缓存、session 内预算)→ 进程内;上 Redis 反而多一次网络往返 + 一个 SPOF。
  2. **本质是「跨请求 / 跨会话累计或持久」**的(per-user 预算、限流、事件日志)→ Redis 甜区。
  3. 本质是向量检索的(语义缓存)→ Qdrant;纯版 Redis 无向量能力,这是能力约束不是偏好。

A. 用 Redis(复用已在跑实例,零新基础设施)#

关注点选型为什么是 Redis 的活
长期记忆 StoreRedis hash(现状)跨会话持久,已在用
per-user 跨会话预算Redis INCRBY+EXPIRE(日/月配额)跨请求累计、要持久
API 限流Redis INCR+EXPIRE 定窗跨请求计数、跨重启不重置
事件回放Redis StreamXADD+MAXLEN,stream ID 即 last_event_id,重连补发)Streams 专为事件日志设计;同一套 Stream 即 D 多副本事件总线的底座(一个决定两处收益)

B. 进程内原语(单机下严格优于 Redis,非「躲 Redis」)#

关注点选型为什么进程内更优
任务背压(全局)asyncio.Semaphore 单例单进程并发闸,Redis 多一跳
fork 背压(session 内)asyncio.SemaphoreForkBudget复用现有 retrieval_budget
任务编排asyncio.create_task + active_tasks dict(现状)后台跑、与 WS 解耦,刷新已扛住
per-session 预算闸ContextVar(复用 ForkBudgetsession 天然单请求内
L1 缓存(热品类)cachetools.TTLCache(1h)微秒级、无网络往返
L1 缓存(汇率/税率)cachetools.TTLCache(24h)同上;不用 lru_cache(无 TTL)
provider cooldown / 限额进程内令牌桶单进程够;毕业线再上 Redis

C. 走 Qdrant(能力约束)#

关注点选型为什么不是 Redis
语义缓存Qdrant 独立 collection(复用 embedding)纯版 Redis 无向量检索;并强化「召回基建复用成缓存」叙事

D. 暂不做(写进毕业线,讲得出为什么)#

关注点决定触发毕业的信号 → 下一档
任务持久化(抗进程重启不做出现「必须干完」语义 / 多 worker → arq;可恢复长流 → Temporal
L2 跨进程缓存不做部署 ≥2 副本、要共享热区 → Redis String+TTL
WS 多副本路由不做(进程内 dict)部署 ≥2 副本 → Redis Pub/Sub(+ 事件回放的 Stream)
provider 限额跨进程协调进程内令牌桶多 worker 并发外呼撞 provider 限额 → Redis

横切能力(纯 Python 库,无新基础设施)#

降级链 + 结构化 degraded 回 Reflect(自研,B 块真亮点,对准 reranker/towers/web_search/LLM 4 个真实外呼)、断路器(自研或 pybreaker)、退避重试(tenacity,仅 timeout/5xx)、LLM Gateway 路由(纯 Python 薄层,单 provider 下做同家多模型路由;跨 provider fallback 留毕业线)、用量归集 + 预算闸(升级 usage.py)、结构化日志(structlog,ContextVar bind 上下文)、metrics(prometheus-client/metrics)、鉴权(JWT,user_id 从 token 解析)、依赖注入(FastAPI Depends + dependency_overrides)。系统级 trace 不自建 OTel 栈——Langfuse v4 已基于 OTel 归并调用树;完整 opentelemetry-sdk/Jaeger 对接留毕业线。

新增依赖一览#

cachetools          # L1 缓存
tenacity            # 退避重试(仅 timeout/5xx)
structlog           # 结构化日志
prometheus-client   # metrics(/metrics)
pyjwt / python-jose # 鉴权(JWT,user_id 从 token 解析)
pybreaker(可选)   # 断路器,也可自研省掉
# opentelemetry-sdk —— 不引:Langfuse v4 已基于 OTel 归并调用树,自建是重复造轮子(毕业线再说)
plaintext

基础设施零新增:Redis / Qdrant / OpenSearch 均已在 docker-compose 里跑。


三、取舍与优先级#

3.1 按与现有基建契合度分档#

模块为什么适合本项目
首选(复用现有基建,故事独特,降本数据漂亮)FinOps 预算闸(F 本体)、语义缓存(E,仅 category_insight 弱时效域)直接复用 usage.py 雏形与 Qdrant 召回层,几乎不引新重依赖;F 的跨 provider fallback 等第二家 key 再点亮
次选(填真实缺口,改造前后对比鲜明)事件回放(D,= #4 进阶)、任务持久化 / Durable Execution(C 毕业线,= #3 进阶)现架构真实单点 / 边界,「内存态单机」硬伤改造对比强烈
快赢(把「部分」补成「完整」,投入小)背压(C,#3 现做档)、韧性工程(B,#5)、成本预算闸(F,#6)、可观测性收缩版(A,#7)Semaphore 并发闸、4 个真实外呼加退避 + 熔断、token 从测到控、补 Prometheus + structlog(不引 OTel 栈)
加分(补短板,顺手做)Guardrails(G)、数据管道(H)、API 加固(I)、依赖注入(J)补 Agent 安全 / 数据 / 服务完整度,单拎后端味略淡,适合收尾

3.2 优先级矩阵(含投入估算)#

「优先级」列的 ✅ = 已落地合入 main(见顶部落地状态表);无标记 = 暂未做。

优先级模块投入(人天)产出解决的已知问题
P0 ✅C 背压(Semaphore)1并发控制 + retrieval budget leakfork 失控 / 无并发上限
P0 ✅B 韧性工程(断路器对准 4 个真实外呼)2–3可用性从「全挂」到「优雅降级」reranker/towers/web_search/LLM 不稳定
P0 ✅A 可观测性补全(收缩版:Metrics + structlog)1–2系统级延迟/错误率从零到一295s 延迟回归定位
P1 ✅F FinOps 预算闸(用量归集 + 成本熔断)1–2token 从「测」到「控」成本失控
P1 ✅E 多级 + 语义缓存(语义缓存仅 category_insight1–2减少重复调用、降延迟category_insight 重复查询
P1 ✅D 事件回放(Redis Stream)1刷新/断线重连无缝续看断开窗口事件丢失
P1 ✅I-鉴权(JWT,从 I 块拆出)0.5–1堵越权读伪造 user_id 读他人 Store(真实安全洞)
P1H 数据管道2–3多平台数据接入基础单数据集 / 无 schema 校验
P2G Guardrails1不可信内容隔离 + 入参校验注入风险
P2I-其余 API 加固(限流 / 幂等 / 版本)1–1.5服务完整度无限流 / 重复提交
P2J 依赖注入1可测试性从零到一无法 mock 测试
毕业线A OTel system trace(对接 Jaeger/Tempo)1–2trace 脱离 Langfuse现 Langfuse 已够,规模上来才换
毕业线F 跨 provider fallback0.5单 provider 风险需接入第二家 provider key 才触发
毕业线D WS 多副本路由(Redis Pub/Sub)1–2从「2 副本即坏」到可水平扩展部署单点(≥2 副本才触发)
毕业线C 任务持久化(Redis Stream / Temporal)2–3抗进程重启续跑「必须干完」语义 / 多 worker

建议执行顺序。 P0 先做C 背压 → B 韧性 → A 收缩版可观测性,直接解决已知痛点,每块都有明确代码产出可 demo)→ P1 在骨架稳定后加入F 预算闸 / E 缓存复用现成基建、性价比最高,先做;D 事件回放补刷新/断线 UX 缺口;I-JWT 鉴权堵越权读这一真实安全洞)→ P2 收尾打磨毕业线档(A 的 OTel system trace、F 的跨 provider fallback、D 多副本路由、C 任务持久化)维持「讲得出为什么暂不做」,触发信号见 §二·五 D 表与各模块说明。整体最「独一份」的优势是已有一整套向量召回基建 + usage.py/Langfuse 现成观测雏形FinOps 预算闸 + 语义缓存(仅弱时效域,走 Qdrant) 最能复用、降本数据最漂亮、不引重型外部依赖,构成完整的「高可用 + 低成本 LLM 后端服务」故事。


四、整体架构增强后的分层视图#


五、面试叙事建议#

对每一块增强,用 「问题 → 方案 → 取舍 → 数据」 四段式讲述(以可观测性为例):

  1. 问题:「我们的 Agent 一次 query 会 fork 多个子 Agent 并发搜多个平台,定位延迟瓶颈时缺系统级 metrics——调用树 Langfuse 能看,但 QPS / 延迟分位 / 错误率没有看板。」
  2. 方案:「我没有再叠一套 OpenTelemetry——因为 Langfuse v4 本身基于 OTel、已经把多 fork 归并成一棵调用树。我补的是 prometheus-client 暴露 /metricstool_duration_seconds 按工具分桶等)+ structlog复用已有的 ContextVar 机制让每条日志自动带 thread_id / fork_depth。」
  3. 取舍:「不自建 OTel system trace、不上 Jaeger——当前 Langfuse 的调用树够用,重复造一套 span 上下文是性价比最低的一档;真要把 trace 脱离 Langfuse 对接 Jaeger/Tempo 再说(毕业线)。」
  4. 数据:「补上 metrics 后看板定位到某外呼 P99 是其他的 3 倍,把它从同步等待改成 timeout + 降级(B 块)后,端到端 P95 从 12s 降到 6s。」

这种叙事展示的不只是「会用工具」,而是工程判断力(包括「什么时候不该造轮子」)。贯穿全文的两条主线值得反复强调:

  • 机制兜底优于提示词:fork 安全四层(#1)、Semaphore 背压(C)、降级信息参与决策(B)——都是「用机制保证,不靠模型遵守指令」。
  • 一套机制解决多个问题 / 复用现有基建:ContextVar 同时做隔离 + 日志上下文传播(A);Qdrant 召回层复用成语义缓存(E,限弱时效域);usage.py 升级成预算闸(F);Redis Stream 同时做事件回放 + (毕业线)多副本事件总线(D);trace 直接吃 Langfuse 已基于 OTel 归并好的调用树,不重复造轮子(A)。

参考来源#