面试知识库

M0 · 项目全景与架构串讲#

简历 Bullet Point: 设计并实现对话式全球购物 Agent(138 万商品 / 6 平台),采用 Think→Act→Observe→Reflect 有状态循环 + 同质 fork 并行 + Harness Hook Pipeline 治理框架 + 四层终止机制 + 意图驱动能力菜单,零 GPU 条件下通过 Rubric 评测驱动迭代,端到端延迟从 83s 压到 40s


开场钩子#

场景#

项目早期的一次端到端测试,用户 query 是”旅行三件套,预算 300”。主 loop 判断需要跨 5 个平台并行搜,调 parallel_dispatch_tool 派了 5 个子 Agent。

前三个子 Agent 正常——搜完一轮候选就返回了。但第四个子一直在循环:搜了一轮觉得不够,换个关键词再搜,搜完又觉得不好,再换……它在 item_search 上转了 7 圈。第五个子更离谱——它搜完候选之后,自己调了 shopping_summary,输出了一份格式完美的购物推荐:“根据您的需求,我为您精选了以下 3 款旅行用品……”。问题是这个子 Agent 的职责是返回原始候选给主 loop 合流,不是自己做推荐。主 loop 收到这份”候选”后傻了——它期望的是一个 JSON 列表,拿到的是一段自然语言文案。

5 分钟超时到,整个请求被杀掉,用户看到一个空白页面。

这次事故逼出了后来整个项目最核心的设计原则:对 LLM 的行为约束,prompt 是辅助,机制才是兜底。

面试官切入#

“你简历上写了’四层终止机制’和’Harness 治理框架’——能先介绍一下整个项目,然后讲讲这些是怎么来的?“


一、模块运作流程#

1.1 一句话定位#

ShoppingX 是一个对话式全球购物 Agent:用户用自然语言说购物意图,系统跨 6 个电商平台并行检索、比价、算关税运费、按偏好精挑,最后给出一份带购买理由的商品卡清单,并把用户偏好沉淀为跨会话长期记忆。

1.2 全景流程图#

1.3 分步详解#

第一层:主 AgentLoop

基于 LangGraph 的 create_agent 封装,Think→Act→Observe→Reflect 四阶段有状态循环。不写死步数,模型在 Reflect 阶段自判信息够了就调终结性工具(shopping_summarychat_fallback)收尾。TERMINAL_TOOLS = {"shopping_summary", "chat_fallback"},LangGraph 检测到这两个工具被调用后不再调度下一轮。

为什么不在调完终结工具后硬插 → END 跳出?因为还要留最后一轮让模型把结构化结果转成面向用户的收尾文案。自然终止比硬跳更稳,也不丢这段文案。

意图驱动能力菜单:早期版本 system prompt 写死工具调用顺序,每次请求不管意图都全链路跑一遍。重构后 planner 工具解析意图输出 tasks(如 ["category_insight", "chat_fallback"]["item_search", "price_compare", "shipping_summary"]),模型按 tasks 组合工具子集。品类行情走快捷路径两步出结果,完整购物才走全链路。

第二层:同质 fork

当需要跨平台并行搜索时,主 loop 通过 dispatch_tool 派出子 Agent。关键词是同质:子 Agent 是主 Agent 的完整克隆——同样的 13 个工具(9 大业务工具 + ask_user + forget_preference + dispatch_tool + parallel_dispatch_tool)、同样的 system prompt、同样的中间件栈。fork 三件事判断(满足任一即 fork):① 能并行 ② 要隔离 ③ 链够深。

子与父的四点不同:① LLM 档位——子用 get_fast_llm() 关闭 reasoning ② thread_id 独立——sub-{uuid}-d{depth} 隔离对话历史 ③ session_dir 继承父的——产物归同一会话目录 ④ 执行层权限——depth_gate 在 depth≥1 时硬拦聚合工具和上下文工具。能力同质但授权不同质。

第三层:fork 安全四层

① 深度闸:MAX_FORK_DEPTH=1,子想再 fork 孙直接拦掉(ContextVar 跟踪) ② 超时 + 迭代上限:子 Agent 90s 超时 + 最多 6 次迭代 ③ 结果截断:回传 JSON 超 4000 token 就截断 ④ 循环检测:滑动窗口 6 次调用里同一工具出现 4 次触发收尾催促。

治理框架:Harness Hook Pipeline

6 个 Hook 点覆盖 Agent 全生命周期——on_session_startpre_thinkpre_tool_callpost_tool_callpost_reflecton_session_end。所有运行时管控(截断、熔断、断言、漂移检测、阶段门、终结纪律、预算闸、输出审核)都是独立函数通过 @harness_hook 装饰器注册。新增一道检查 = 写一个函数 + 一行装饰器,不碰主流程代码。当前注册了 15+ 个 Hook 模块。

阶段状态机

四阶段 PLANNING → SEARCHING → COMPARING → CONCLUDING,每阶段只放行对应的工具子集。模型在 PLANNING 阶段想调 item_search 会被执行层哨兵拒绝。为什么不直接摘掉工具?因为摘工具会改 system prompt 的内容,破坏 prompt cache 的前缀匹配——工具表恒定、执行层拦截,前缀稳定。

召回管道

BGE-M3 编码(query + 偏好词一起编码)→ Qdrant dense 向量搜索 + 多维 payload filter(平台 / 价格 / 评分 / 品牌排除)→ BGE-Reranker cross-encoder 精排。个性化走「偏好词并入检索词 + payload filter」(向量级 user 塔融合已删,长期零调用)。跨语言靠 BGE-M3 的多语言能力。

品类知识库——两段式检索

category_insight 采用 resolve_category(品类投票确定目标品类)+ fetch_cards(按 resolved 品类精确取卡)的两段式管道,避免一次性语义搜索把不相关品类的卡片混进来。

记忆体系

长期偏好通过会话结束后独立运行的记忆管家(curator)扫本轮对话提取 like/dislike 双极偏好沉淀到 Store,下次对话自动注入。会话级短期偏好 P_t 逐轮累积约束(预算/材质/颜色),由 item_picker 机制性强制执行——硬 dislike 走确定性过滤、预算走数值比较,不依赖模型转述。记忆 schema 用 domain/slug 派生 dedup_key(废弃手拼 key),P_t 扩展 current_intent/slots/rejected_options/open_questions/decisions_made 五个结构化字段。

可观测性

Langfuse 全链路 trace(每个工具调用 + 每次 LLM 请求),AG-UI 事件流 + WebSocket 实时推送(十余类事件),Prometheus metrics(工具 RT P95 / 成本 / 熔断),Rubric 评测分数回注 trace,工具 RT 告警(P95 滑动窗口 + 三态状态机)。

1.4 数据流 trace:一条 query 端到端走一遍#

“想买便宜又抗造的旅行三件套,预算 300,不要塑料的” 为例。

Step 1 → planner(意图拆解)

  • LLM 提取结构化字段 → PlanOutput
  • tasks: [recommend, price_compare, landed_cost]category: "旅行收纳"budget_amount: 300currency: "CNY"(系统规则回填)hard_constraints: ["不要塑料"]soft_preferences: ["便宜", "抗造"]

Step 2 → dispatch_tool(跨平台并行 fork)

  • 主 loop 调 parallel_dispatch_tool,fork 出 3 个同质子 Agent(amazon/shein/shopee)

Step 3 → item_search × 3(子 Agent 内执行)

  • BGE-M3 编码(query + 偏好词)→ Qdrant dense + payload filter(platform / price_usd_max=42.86 / min_rating)→ BGE-Reranker 精排
  • 每平台返回 ItemSearchOutput(~20 条候选)

Step 4 → price_compare(跨平台比价)

  • 汇率归一 to_base(price, currency, "USD"),按 price_usd 升序排

Step 5 → shipping_calc(到手价估算)

  • 收货国四层解析(用户明示 → 偏好 → 语言推断 → 默认 CN)
  • 查运费表 + 关税率 + 免征额 → landed_usd,国内单免税

Step 6 → item_picker(偏好精挑)

  • 硬过滤:dislike-hard “塑料” → 确定性淘汰
  • 预算过滤:landed_usd > 42.86 的淘汰
  • LLM 打分:按软偏好评分 + 写 pick_reason
  • rejected_options 机械灌入 P_t(不经 curator 猜测)

Step 7 → shopping_summary(终结,出清单)

  • items_preview 事件先推商品卡(先出货、后出文案)
  • LLM 生成 summary 文案 + 前 3 件 reason 重写
  • summary_delta 流式下发 → 前端逐字渲染

Step 8 → 收尾后处理

  • 落产物:summary.md + result.json + turns.json
  • report_task_result → WebSocket → 前端
  • curator 扫本轮 → 提取”不要塑料” → 沉淀长期 Store

1.5 技术选型决策表#

组件选了什么为什么选它什么情况下换
Agent 框架LangGraph create_agentLangChain 生态统一;原生支持 middleware 和 recursion_limit需要更细粒度图控制(条件分支/审批节点)→ StateGraph
向量库Qdrant原生 payload filter(价格/平台/评分);单节点开箱、分布式原生超千万 → Milvus;纯内存无 filter → Faiss
RAG 知识库OpenSearchHybrid Query(KNN+BM25);english 分词已有 ES → 直接 ES;轻量 → pgvector
EmbeddingBGE-M3(SiliconFlow)多语言;1024 维性价比高;API 兼容 OpenAI更高精度 → text-embedding-3-large
RerankerBGE-Reranker-v2-m3多语言对齐;API 无需本地 GPU低延迟 → 本地模型
LLMDeepSeek / Qwen(OpenAI 兼容 API)中文能力强;支持 reasoning 开关更强推理 → GPT-4o / Claude
偏好存储SQLite(默认)零依赖;接口与 Redis 同构多实例 → 切 Redis
账户库SQLAlchemy + AlembicORM + 可追踪迁移分库分表 → 换中间件

1.6 口述脚本#

面试官:“介绍一下你的项目。”

30 秒 · 项目是什么

“ShoppingX 是一个对话式全球购物 Agent。用户用自然语言说需求,系统跨 6 个平台并行检索、比价、算关税运费、按偏好精挑,给出带理由的商品卡清单。数据 138 万条商品,技术栈是 FastAPI + LangGraph + Qdrant + OpenSearch + BGE-M3/Reranker + Langfuse 可观测 + WebSocket 实时推送。已公网部署,支持注册和在线体验。”

1 分钟 · 为什么是 Agent 不是 Workflow

“早期版本是固定流水线。问题是用户说’最近什么旅行包比较火’只需查品类知识库就够,但 Workflow 全链路跑一遍。改成 Agent 后 Think 阶段自己判断调什么工具。后来进一步重构成意图驱动:planner 解析意图输出 tasks,模型按 tasks 组合工具子集。一句话:工具次序没有一行代码强制。”

1 分钟 · 三个技术亮点各 20 秒

“亮点一:机制兜底不靠 Prompt 祈祷。每个行为问题都有代码级硬约束——四层 fork 安全、阶段状态机、检索预算、token 预算闸。”

“亮点二:Harness 统一治理框架。修 bad case 不是修 prompt 是修 Harness。6 个 Hook 点,新增检查 = 加一个文件 + 一行装饰器。”

“亮点三:延迟减法。端到端从 83s 压到 40s,主 loop 从 9 轮压到 5 轮。核心洞察:96% 延迟是 LLM 解码——子 Agent 关 reasoning、planner 确定性预置省一轮、收尾文案不逐件重写理由。”

30 秒 · 诚实边界

“数据来自 Kaggle 公开数据集,没有真实平台 OAuth/支付/物流。没有 GPU 训练/微调。评测是离线跑。进程内状态,多副本需迁 Redis。“


二、踩坑实录#

坑 1:Agent vs Workflow 不是”自由度高就好”#

  • 现象:早期 Workflow 版本 system prompt 写死工具调用顺序,品类行情查询也走全链路。改 Agent 后又太自由,简单问题也调一堆工具。
  • 修法:引入 planner 做意图驱动——planner 解析意图输出 tasks,模型在 tasks 范围内组合工具。约束下的自主决策
  • 教训:Agent 核心价值不是”自由”,是”按意图灵活组合能力”。

坑 2:Prompt 级规则对弱模型约等于”祈祷”#

  • 现象:强模型很听话,换 Flash 系列子 Agent 后无视终止规则——不停重搜或自行调 shopping_summary
  • 修法:行为约束从 prompt 级提升到代码级机制——四层 fork 安全、阶段状态机、检索预算。
  • 教训:凡是”模型不遵守就会出事”的规则,必须有代码级兜底。机制打底,prompt 辅助

坑 3:摘工具会破坏 prompt cache 的前缀匹配#

  • 现象:阶段状态机最初动态从工具表里移除被禁工具,prompt cache 命中率断崖下跌。
  • 修法:工具表恒定 + 执行层拦截。模型看到完整 13 个工具,但越权调用被哨兵拒绝返回 sentinel。
  • 教训:任何会改变 system prompt 内容的动态行为都要评估对 prompt cache 的影响。

坑 4:ContextVar 的 set 是单向的——子 set 了不会回写给父#

  • 现象:检索预算计数用 ContextVar,子 Agent fork 出去后各持快照副本,父看不到子的消耗。
  • 修法:预算计数改 module-level dict(以 session_dir 为 key),session_dir 通过 ContextVar 只读传播。
  • 教训隔离用 ContextVar,共享用 module dict + namespace key

坑 5:延迟减法——96% 是 LLM 解码不是后端代码#

  • 现象:端到端 83.2s,直觉以为向量检索或网络慢,trace 一看 ~96% 是 LLM 解码(reasoning/thinking token)。
  • 修法:四刀——① 子 Agent 关 reasoning(50s→5s)② planner 确定性预置省一轮 ③ 收尾文案不逐件重写 ④ 追问轮 picker 不抄 id。结果:83.2s→39.7s,9 轮→5 轮。
  • 教训:优化前先量——瓶颈在 LLM 解码,后端代码优化收益约等于零。

坑 6:KB 两段式检索——一次性语义搜索会混品类#

  • 现象category_insight 一次性语义搜索”旅行包”,混进”旅行杯""旅行枕”等无关品类的卡片。
  • 修法:拆成两段——resolve_category 先做品类投票确定目标品类,fetch_cards 按 resolved 品类精确取卡。
  • 教训:RAG 检索要先确定”在哪个域里找”再找,不能一步到位。

三、验收与量化#

端到端 Rubric 评测#

三级评分:P0 业务红线(一票否决)/ P1 执行规范(扣分)/ P2 质量打分(1-5)。聚合公式:total = (p2_avg / 5) × 100 - penalty × len(p1_violations),P0 任一 fail 则 total=0

种子 query 集覆盖多场景桶(多约束精挑 / 红线-预算 / 跨平台比价 / 品类洞察 / 闲聊兜底 / 跨语言召回 / 到手价 / 记忆注入 / 澄清 / 外部事实),动态生成评分细则 + judge 两刀打分。

关键洞察:先校准 judge 再改 Agent——首轮基线中多条 P0 fail 是 judge 假阳性(轨迹泄露当信息安全 fail、凭空造预算红线),校准后大幅涨分,一行 Agent 代码不动。

延迟治理#

瓶颈:~96% 是 LLM 解码时间。治理手段:

  • 子 Agent 关 reasoning:单子 Agent ~50s→~5s
  • planner 确定性预置:省掉”模型花一轮只为说出 planner 这个词”
  • 收尾文案不逐件重写:确定性理由补强 + LLM 只重写前 3 件
  • 追问轮 picker 不抄 id + reuse 轮不开 reasoning
  • 结果:端到端 83.2s→39.7s,主 loop 9→5 轮

各模块验证方式#

模块验证方式
Rubric 评测三级评分 + judge 校准 + 迭代对照
召回层Recall@K / MRR / NDCG(Recall@10=0.983)
fork 安全回归测试(递归拦/超时/截断/循环检测)
Harness回归测试 + 端到端 Rubric 间接反映
安全护栏白名单/脱敏/过滤测试 + P0 泄露消除
偏好记忆跨会话持久化 + 注入验证 + curator 提取

四、面试问答#

Q1: Agent 主循环怎么设计的?#

Think→Act→Observe→Reflect 四阶段循环,基于 LangGraph create_agent。和 ReAct 区别:不写死步数,模型自判终止;四层机制兜底弱模型。主循环 30 次迭代 / 300 秒超时,子循环 6 次 / 90 秒。recursion_limit = MAX_ITERATIONS * 2 + 1(LangGraph 一轮 = 两个 super-step + 最终响应 1)。

Q2: 为什么做成 Agent 不做 Workflow?#

Workflow 一刀切全链路,品类行情也走检索+比价。Agent Think 阶段按意图选工具,但纯自由又太松。最终方案:意图驱动 planner 输出 tasks,模型在 tasks 范围内组合——约束下的自主决策。

Q3: 同质 fork “同质”是什么意思?#

子 Agent 和主共享同一份 FULL_TOOL_SET(13 工具)、同 prompt、同中间件栈。不做专用化裁剪:简化维护 + 教学架构展示可组合性。子也能调 shopping_summary 但被 depth_gate 执行层拦截。

Q4: 重新设计会做哪些不同?#

① 候选缓存从 module-level dict 改 ContextVar 防多会话串扰 ② 工具可见性裁剪需量化 cache 收益 vs 浪费 token ③ 事件回放需完整 event sourcing 支持历史审计

Q5: 最大技术风险?#

LLM 服务单点依赖(只配一个 provider)。进程内状态(排队/熔断/去重在内存),多副本需迁 Redis。

Q6: 150 万商品、零 GPU、离线评测——生产能用吗?#

不能直接用,但每个缺失能力有升级路径。数据需真实平台 OAuth + 反爬 + 库存一致性。零 GPU 的 embedding/reranker 走 API 需本地部署。离线评测需加埋点 + 线上指标 + 灰度。项目价值在于 Agent 架构设计、治理框架、质量保障体系这些可复用的工程能力

Q7: 支持 1000 QPS 怎么改?#

① 多 Uvicorn + LB,ConnectionManager 改 Redis Pub/Sub ② LLM 网关排队 + failover ③ Qdrant 分布式 + read replica ④ Agent 执行改消息队列。当前瓶颈在 LLM 响应时间,默认 slot=10 约 0.33 QPS。


五、前沿概念#

5.1 Agent 范式对比#

范式适用场景不适用
Workflow/DAG步骤固定、需确定性意图多样、步数不确定
ReAct单工具简单场景多工具组合、需收敛判断
AgentLoop意图多样、灵活工具组合确定性要求极高(金融交易)
Plan-and-Execute可提前规划、步骤弱依赖动态场景
Multi-Agent 同质 fork子任务同质、需并行隔离子任务差异大

5.2 机制优于 Prompt#

代码级机制的优势是确定性,prompt 的优势是灵活性。两者互补:prompt 覆盖正常路径(80%),机制兜底异常路径(20%)。

行为约束prompt 层机制层
终止”信息充分时调终结工具”迭代上限+超时+循环检测
工具顺序”先 planner 再 item_search”阶段状态机+哨兵
偏好遵守”不推荐黑名单属性”item_picker 硬过滤
递归控制”子不要再 fork”深度闸 MAX_FORK_DEPTH=1
成本控制”不要过度检索”检索预算+token 预算闸

5.3 Harness Hook Pipeline#

类似 Web 框架中间件或 Git pre-commit hook。核心好处是开闭原则——对扩展开放(加文件+装饰器),对修改关闭(不碰 main_agent.py)。与 Web middleware 的区别:粒度更细(6 个 Hook 点 vs request/response 两个)、context 更丰富(能看到工具参数/返回值/历史轨迹/阶段状态)。

5.4 零 GPU 迭代闭环#

Rubric 评测 → 定位 bad case → 改 prompt/工具/Harness 规则 → 再评测对照。不改权重只改上下文。天花板取决于模型的上下文学习能力,实测比预期高。真正需要 GPU 的场景是模型基础能力不够——弱模型连 schema 都解析不准就不是上下文能修的。


六、诚实边界#

模块做了没做什么时候该做
数据138 万条 Kaggle 公开数据集无实时爬虫/OAuth/库存一致性接真实平台时
模型全走 API不做 SFT/RL/微调(无 GPU)拿到 GPU + 标注数据时
评测离线 Rubric + 种子 query 集无线上 A/B / 埋点上线真实用户时
存储进程内状态多副本需迁 Redis需水平扩展时
隔离ContextVar + 独立实例无 checkpointer需可恢复任务时
鉴权JWT 验证 + Alembic 迁移未做 thread→owner 资源级鉴权多用户共实例时
前端React + Vite 全功能非专业前端、未移动端适配面向终端用户交付时
降级三层降级链本地 fallback 语义质量低永远——保底不是替代