设计题 06:Agent 的失败排查与可观测性
权重:★★ 形式:45 分钟系统设计轮
素材来源:Own — QueWI 的网关日志与链路追踪、意图识别线上误判回流(「埋点字段要能对上训练需要的字段」这条约束)
相关知识点:06-agent-infra/observability.md、05-agent-evaluation/agent-eval.md、llm-as-judge.md、06-agent-infra/gateway-routing.md
1. 题干#
「我们做了一个多步 agent,demo 里跑得挺好,上线以后隔三差五就有用户说它做错了事、或者卡在那里不动。我们看日志也看不出什么,而且这些失败复现不了。你帮我们搞清楚它到底哪里坏了。」
注意这个题干有多模糊:没说多频繁、没说哪一步、没说「做错」具体是什么、没说任务有没有客观终态、也没说「复现不了」到底卡在哪里。这不是题干写得差,这是刻意的。 面试官在看你会不会把「失败」和「复现不了」这两个糊在一起的词拆开。
2. 第一步:澄清(这一轮最重要的动作)#
绝对不要听完就开始讲 trace 和 span。开场第一句应该是:
「在搭可观测性之前我想先确认几件事 —— 因为这几个答案会决定记什么、记多久、以及这套东西最后能不能真的定位问题。」
必问清单(按对方案的影响排序)#
| # | 问题 | 为什么它会改变方案 |
|---|---|---|
| 1 | 「失败」具体指什么? | 报错/超时(工程问题)、做完了但做错了(能力问题)、该拒绝时没拒绝(对齐问题)—— 三类要用三套完全不同的判定方式。混在一起统计,得到的数字没有意义 |
| 2 | 失败频率多少、集中在哪些步骤? | 有聚类 → 大概率是某一类共性原因(某个工具、某类输入形态、某个长度区间);完全随机 → 更可能是环境抖动、并发、上下文长度这类系统性因素。诊断方向完全不同 |
| 3 | 现在缺的是「复现」还是「归因」? | 两者解法不同,很多团队两样都缺但分不清。复现需要留全量输入 + 当时的工具与环境版本快照;归因需要 span 树 + 每步状态。先问清哪一样更缺,才知道先补什么 |
| 4 | 任务有客观终态吗?能在沙箱里重放吗? | 有终态(改了数据库、生成了文件、调了接口)→ 可以用环境终态断言自动判定成败,最可靠;没有终态(「帮我写封邮件」这种)→ 只能靠 judge + 人工审 |
| 5 | trace 里可以放用户内容吗?要留存多久? | 有合规限制 → 脱敏 / 引用式存储 / 分级采样 / 明确留存期限。香港客户还要问数据能不能出境、能不能交给第三方 SaaS |
| 6 | 现有的可观测基建是什么? | 已有 OTel / APM → 接入并扩展自定义字段;什么都没有 → 先用 SDK 埋点 + 一个能按 trace 查询的库,别先建平台 |
| 7 | 谁会看这些数据? | 工程师排障 / PM 看质量 / 训练数据回流 —— 三种消费者要的字段、聚合方式和界面都不同。第 3 种最容易被漏掉,而它恰恰决定了埋点字段设计 |
| 8 | 「复现不了」具体是哪里断了? | 拿不到当时的输入?环境已经变了(工具返回变了、外部 API 换了版本)?还是同一输入每次路径本来就不同(非确定性)?三种断法对应三种不同的补法 |
面试技巧:先问 1、2、3。第 3 问是这题的题眼 —— 用户说「复现不了」,但真正缺的往往是归因:就算让他复现一百次,他也不知道该看哪一步。
3. 参考架构#
假设澄清后得到:任务有客观终态、失败集中在工具调用、有 PII 合规要求、需要工程师排障 + 训练回流两个消费者。
┌─ 埋点 + 采集 ─────────────────────────────────────────┐
│ agent 循环里每一步自动开 span(不靠人手写),异步上报 │
│ LLM / 工具 / 检索 / 路由 / 判断 / 人工介入 六类 span │
│ PII 脱敏 · 分级采样(失败全记 / 成功按比例) │
│ 大 payload 截断或外链对象存储(trace 只留引用) │
└───────────────────────────────────────────────────────┘
↓
┌─ 存储 ────────────────────────────────────────────────┐
│ Session(多轮容器)→ Trace(一次运行)→ Span(一步) │
│ 版本 tag:模型 / prompt / 工具 schema / 代码版本 │
└───────────────────────────────────────────────────────┘
↓
┌─ 使用 ────────────────────────────────────────────────┐
│ ① 排障:按 session / user / 时间 查某一次运行 │
│ ② 归因:从终态往回找第一个偏离正常的分叉 │
│ ③ 聚合:单步成功率 / 步数 / token / 成本 / 无效调用率 │
│ ④ 回放:用历史输入重跑新版本 → 回归评测 │
│ ⑤ 回流:失败 case → 评测集 / 训练数据 │
└───────────────────────────────────────────────────────┘画图时要讲的三个点:
- ① 排障和⑤回流是同一套数据,但要在一开始就一起设计。 最常见的失败是「上线三个月后想捞路由误判 case,才发现 trace 里根本没记路由判定结果」。埋点设计要回头问一句「这些字段将来能用来干什么」 —— 没有这一问,数据存了也白存。
- ② 归因和聚合是两种读法,缺一不可。 单次运行要能整条拉出来给人看(纵向),全局要能按步骤聚合成功率(横向)。只有纵向的是调试工具,只有横向的是仪表盘。
- ④ 回放依赖「版本 tag」。 trace 里如果没有模型版本、prompt 版本、工具 schema 版本,回放就只能测出「环境变了」,测不出「我的改动是好是坏」。
4. 关键决策与权衡#
4.1 为什么日志不够(这题必须答得具体)#
| Agent 的特性 | 只打日志的问题 | trace 的解法 |
|---|---|---|
| 多步 | 一次运行产生几十上百行日志,散落在不同服务的日志文件里,时序难对齐 | 一次运行 = 一个 trace,每步 = 一个 span,父子结构天然表达调用关系 |
| 非确定性 | 同一个输入每次路径不同,没法靠复现定位 | 把那一次的完整路径与每步输入输出留档,可整段回放分析 |
| 错误传播 | 最后报的错往往不是根因(第 1 步就偏了,第 7 步才崩) | 顺着调用树往回找第一个偏离正常的分叉 |
| 成本分布不均 | 总成本无法归因到具体步骤 | 每个 span 记 token 与成本,能算出哪一步最贵 |
| 需要人审 | 没有可读的完整上下文,无法判断过程是否合理 | span 的输入输出就是评审所需的全部材料 |
一句话:日志回答「系统做了什么」,trace 回答「这一次为什么失败」。
4.2 每一步记什么#
按 span 类型分(面试要能当场列出来):
| span 类型 | 必须记录 |
|---|---|
| LLM 调用 | 模型名与版本、prompt(或引用)、输出、输入/输出 token 数、温度等参数、成本、延迟、结束原因(正常 / 超长 / 被截断)、错误与重试 |
| 工具调用 | 工具名与 schema 版本、完整参数、返回结果(或摘要)、成功/失败、错误类型、耗时、是否重试 |
| 检索(RAG) | 原始 query、改写后的 query、命中的 chunk id 与分数、rerank 前后排序、注入上下文的长度 |
| 路由决策 | 选中的模型、置信度/分数、判定为哪一档、备用路径 |
| 人工介入 / 拒绝 | 触发原因、被拦截的动作、人工选择 |
| 最终结果 | 用户可见输出、是否成功、是否达成环境终态、用户反馈(点赞点踩、是否追问、是否转人工) |
必须有的版本 tag:模型版本、prompt 版本、工具 schema 版本、代码版本。少了任何一个,「改完之后是变好还是变差」这个问题就无法回答。
要主动提出的工程约束(这些说出来就比「我会埋点」高一层):
- PII 与合规:prompt 里常有用户隐私 —— 「全都记下来」和「什么都不记」都是错的。
- 采样:分级 —— 失败与异常全记,成功按比例采样。
- 大 payload:长文档、大返回值要截断或外链对象存储,trace 里只留引用。
- 关联:trace 必须能关联到 user id、session id、版本号,否则无法做对比与排障。
- 开销:埋点本身要异步、批量,不能在关键路径上做重活。
4.3 怎么把失败归因到某一步(本题核心)#
第一步:没有参照就没有归因。 先定义「正常」,三条参照按可靠性排序:① 环境终态断言(最可靠)—— 跑完后直接检查环境状态(数据库那行改对了没、文件生成了没、接口返回对不对),它与路径无关,因此不会把「走了另一条合法路径」误判成失败;② 单步期望状态 —— 给定正确的前序状态,这一步的合理输出集合是什么(工具选对了吗?参数填对了吗?检索召回了该召回的 chunk 吗?);③ 与成功运行的共同路径对比,找出第一个分叉点(first divergence)。
第二步:从终态往回找第一个偏离正常的分叉。 为什么往回找 —— 因为错误会传播:第 1 步就偏了,第 7 步才崩,最后报的错通常不是根因。
第三步:按失败类型分层用不同手段(可靠性递减,能自动化的优先):
| 失败类型 | 例子 | 手段 |
|---|---|---|
| 硬失败 | 工具报错、schema 校验失败、调用不存在的工具、超时 | 规则可自动判定。这一档应该被压到接近 0 —— 它是纯粹的工程问题 |
| 语义失败 | 选错工具、参数值幻觉、检索没召回、计划方向错了 | 需要单步期望状态。规则只能覆盖一部分,剩下的用校准过的 judge,而 judge 做归因本身有噪声 |
| 质量失败 | 过程没错但结果不好 | 只能 judge + 人工。且要注意正确路径不唯一,不要做严格轨迹匹配 |
第四步:用单步粒度指标做 credit assignment(从 trace 里算出来,也是回流评测的依据):工具选择准确率、参数/字段正确率、无效调用率、冗余与循环调用、错误恢复率(工具报错后能否自行纠正)、该弃权/该提问时是否正确收手。
误差累积解释了为什么「长任务失败率高」不能归因到某一步:若每步独立地有概率 p 做对,n 步全对的概率量级是 p^n —— 单步 95% 在 10 步任务上约剩 60%,在 30 步任务上会崩到很低。所以正确的问法不是「哪一次崩在哪」,而是「哪一步的单步可靠性最低」。
必须主动说出的边界(这一节最能体现诚实):① 不是所有失败都能归因到单一步骤 —— 有些是多个近似正确的步骤组合出的偶发失败,没有单一根因;② 必须把「模型失败」和「环境失败」分开 —— 网络抖动、第三方 API 限流、工具本身有 bug 都会污染结论,否则你会去优化一个不存在的模型问题(工具本身的单测与契约测试应当先于 agent 评测);③ judge 做的语义归因是代理指标不是 ground truth,需要校准,且部分维度覆盖不到。
4.4 PII 与留存(FDE 必备意识)#
trace 里必然包含用户输入和工具返回值,也就必然包含 PII。做法:
- 脱敏或引用式存储:正文放受控存储,trace 里只留引用与结构化摘要。
- 分级采样:失败全记(高信息量、低频),成功按比例(高频、低信息量)。
- 留存期限与访问审计:明确谁能看、看多久。
- 跨境合规:香港客户要问清数据能不能出境、能不能交给第三方 SaaS —— 数据合规往往是一票否决项,优先于功能选型。 用词也要准确:区分「脱敏」(可逆)和「匿名化」(不可逆),不要声称做过匿名化除非确实不可逆。
4.5 什么不该建(资深信号)#
- 不要一开始自建全套平台(存储、查询、可视化)。先用现成 SDK + 简单的按 trace 查询,验证「真的能定位问题」再投入。
- 不要全量原始存储、也不要为了「零缺失」而不采样。 存储成本随流量线性增长,叠加合规风险 —— 成本和合规双输。
- 不要在关键路径上做同步埋点重活。 观测不能拖慢 agent,否则用户会为了可观测性付出体验代价。
- 不要把用户反馈率当质量指标。 反馈有严重的选择偏置(点踩的人更有表达欲,沉默的大多数不反馈)。
- 不要只记最终输出。 只记结果,等于放弃了归因、回放和评测三种用途。
- 但也不要「先随便记,以后再说」。 埋点字段与将来用途脱节是最常见的断点 —— 上线后才想要「路由误判 case」,却发现没记判定结果与置信度。
4.6 闭环:从生产失败回到评测与训练#
线上 trace 采集(每步输入输出 + 结果 + 反馈)
↓ 失败聚类与归因(路由 / 检索 / 工具 / 生成 / 用户输入本身)
↓ 人工标注 / 构造样本(评测样本 + 训练样本 + 偏好对)
↓ 改进 prompt / 工具描述 / 换模型 / SFT 补能力 / DPO 补偏好
↓ 回归评测(新版本在 golden set 上不能退化)
↓ 灰度上线 → 继续采集 trace这个闭环最容易断的四环:
- 埋点与训练需求脱节 —— 字段没记,case 捞不出来(见 4.5 最后一条)。
- 失败没有分类 —— 一堆 case 无从下手,必须先能按系统边界归类。
- 闭环不闭合 —— 只采集不回流(白存),或回流了但不做回归评测(等于拿线上做实验)。
- 合规与分布偏置 —— 生产数据用于评测/训练要过脱敏与授权;而且线上暴露的问题只是全部问题的一个子集,长尾样本少,直接当评测集会带来偏置。
5. 容量与成本直觉#
面试官常问「记这么多要多少存储」。不要求精算,但要有量级感觉:
- trace 体积 ≈ 每次运行的 span 数 × 每 span 的 payload。 其中 payload 是主项 —— 长的工具返回和检索上下文会让单条 trace 大得离谱,所以大 payload 必须外链,trace 里只留引用与摘要。
- 采样是必须的,不是优化选项。 失败全记 + 成功按比例,能砍掉绝大部分存储同时保住信号。存储还要分层:近期数据可全量可查,老数据降级为聚合或低比例采样。
- 回放的成本最容易被低估。 回放一次 agent 任务 = 十几次模型调用 + 十几次工具调用 + 一个可重置的沙箱,所以回放要抽样做,选有代表性的失败/成功 case 子集,不要全量重跑。
- 归因是持续的人工作业。 给失败 case 分类要花人;优先级按「该类失败的出现频率 × 是否可自动判定」排 —— 先做高频且能自动判定的那一类。
- 一句面试话术:「可观测性的目标不是存得最全,而是能定位、能回流。」
6. 面试官会怎么往下挖#
第一层:澄清与动机#
- Q:你会先问什么? ← 见第 2 节。核心是把「失败」和「复现不了」这两个模糊词拆开
- Q:日志为什么不够? ← 见 4.1,要说 agent 的三个具体特性,不要笼统说「日志不好用」
- Q:为什么不先把所有 prompt 全量存下来,以后慢慢分析? ← 成本与合规双输(见 4.5 与 5);正确做法是分级采样 + 预先设计字段。价值侧也承认完整数据最有分析余地,所以失败 case 全量保留
第二层:技术细节#
- Q:trace 里你会记哪些字段? ← 见 4.2 的表格,并且要主动带上版本 tag 与工程约束
- Q:怎么确定是哪一步坏的? ← 见 4.3:先有参照(终态断言 / 单步期望状态),再从终态往回找第一个偏离的分叉;然后用单步指标做 credit assignment
- Q:过程对不对怎么判? ← 优先环境终态断言(与路径无关);其次只比对关键约束(是否调用了必需工具、是否包含必需参数、是否触发危险动作);不要做完整轨迹的严格匹配 —— 正确路径不唯一,会产生大量假阴性
- Q:怎么区分「模型失败」和「环境失败」? ← 在 span 上打错误类型标签,把环境类单列统计,不让它污染模型结论。前提是工具本身先有单测与契约测试 —— 工具返回不稳定时 agent 评测毫无意义
第三层:工程与生产#
- Q:失败率百分之几算高? ← 信息不够。要分桶(高频路径上的失败远比长尾严重)、分类(哪一类失败)、看趋势而非绝对值。agent 还要看 pass^k 而不是单次成功率 —— 一个 pass@10 很高但 pass^10 很低的 agent,在用户手里就是不可用的
- Q:怎么知道新版本没有变差? ← 用历史 trace 回放做回归评测,按版本 tag 聚合线上指标做对比。注意回放的环境要与当时一致,否则测出来的是环境差异而不是模型差异
- Q:用户投诉了某一次对话,你怎么查? ← trace 必须能按 user / session / 时间检索到完整的那一次运行 —— 这是所有归因的前提,也是为什么埋点一开始就要关联会话与用户 id
- Q:怎么让埋点不影响性能? ← 异步、批量、采样、不在关键路径做重活;埋点 SDK 自己失败要能降级,不能因为观测挂了导致业务挂
第四层:反思与边界#
- Q:这套东西什么时候会不够用? ← ① 无法在沙箱重建的环境(真实世界副作用、外部系统状态已变);② 非确定性导致同一输入本来就不可复现;③ 「正确路径不唯一」让语义归因天然带噪声;④ 无法断言终态的主观任务只能靠 judge + 人工
- Q:客户说「任何用户内容都不允许落盘」,你怎么做可观测性? ← 保留结构化元数据(模型、token、耗时、工具名、成败、错误类型、路由决策),内容用引用式存储或即时匿名化;可以留即时抽样的匿名样本用于评测,但会牺牲一部分语义归因能力(看不到「它答错了什么」)。把这个代价明确说给客户,让他选 —— 这是 FDE 该做的
- Q:如果只能做一件事,你做哪件? ← 把失败 case 全量留档、并能按 session 查到完整 trace。 因为失败是稀缺且高信息量的信号,成功 case 可以采样甚至不记;而「能不能定位一次具体失败」是整套可观测性的价值兑现点
7. 自测#
1. 为什么 agent 的可观测性必须用 trace 而不是日志?
答案要点
因为 agent 有三个特性让日志失效:① 多步 —— 一次运行几十上百步,日志散落在不同服务里、时序难对齐;② 非确定性 —— 同一输入每次路径不同,没法靠复现定位,必须留存那一次的完整路径;③ 错误传播 —— 最后报的错通常不是根因(第一步就偏了,第七步才崩),需要顺着调用树往回找第一个异常分叉。此外还有成本分摊(每步 token 与成本)和人审可读性两个理由。trace 把一次运行组织成带父子关系的 span 树,每步的输入输出、耗时、成本、错误都在里面,既支持归因也支持回放。
2. 一次运行失败了,你怎么确定是哪一步坏的?
答案要点
先有参照才能归因:优先用环境终态断言(与路径无关,最客观),其次用单步期望状态,最后才是与成功运行的共同路径对比。然后从终态往回找第一个偏离正常的分叉 —— 因为错误会传播,最后报的错一般不是根因。再按失败类型分层:硬失败(工具报错、schema 失败)用规则自动判、应压到接近 0;语义失败(选错工具、参数幻觉、检索没召回)用单步期望状态 + 校准过的 judge;质量失败只能 judge + 人工。最后用单步粒度指标(工具选择准确率、参数正确率、错误恢复率等)做 credit assignment。主动说明边界:存在无法归因到单一步骤的失败,judge 归因有噪声,且必须把环境失败与模型失败分开。
3. trace 里哪些字段是「上线时想不到、以后一定会要」的?举两例并说明后果。
答案要点
两类最典型:① 版本 tag(模型版本、prompt 版本、工具 schema 版本、代码版本)—— 缺了就永远无法回答「改完之后是变好还是变差」,也无法做按版本的线上对比;② 路由/决策类字段(选中的模型、置信度、判定的档位)—— 缺了就捞不出误判 case,训练数据回流做不了,而这类需求通常是上线几个月后才出现的,届时数据已经丢了。其他如「结束原因」(是正常结束还是被截断/超长)、「是否重试」也属于这一类。核心论点:埋点设计要回头问一句「这些字段将来能用来干什么」,只采集不回流等于白存。
4. 客户要求「任何用户内容都不能落盘」,你怎么设计可观测性?牺牲了什么?
答案要点
保留结构化元数据:模型与版本、token 数、耗时、工具名与参数结构(值可脱敏)、成功/失败与错误类型、路由决策、终态断言结果、用户反馈信号。内容侧用引用式存储(正文留在受控系统,trace 只留引用)或即时匿名化后再落盘,并明确留存期限与访问审计;如果连匿名化样本都不允许,就只能留即时抽样的极少量、经合规流程批准的样本用于评测。牺牲的是语义归因能力 —— 看不到「它具体答错了什么」,只能知道「哪一步失败了」,因此语义类失败的根因分析会显著变弱,需要更多依赖终态断言与结构化单步指标。必须把这个代价明确告知客户并让他决策,而不是悄悄降级实现。
5. 怎么把线上失败变成评测集和训练数据?这个闭环最容易断在哪?
答案要点
闭环:线上 trace 采集(每步输入输出 + 结果 + 反馈)→ 失败聚类与归因(按系统边界分成路由 / 检索 / 工具 / 生成 / 输入本身)→ 人工标注或构造样本(评测样本、训练样本、偏好对)→ 改进 prompt / 工具描述 / 换模型 / SFT / DPO → 回归评测确保不回退 → 灰度上线 → 继续采集。最容易断的四环:① 埋点字段与训练需求脱节(没记路由判定结果就捞不出路由误判 case,这是最常见的断点);② 失败没有分类,一堆 case 无从下手;③ 闭环不闭合 —— 只采集不回流,或回流了但不做回归评测(等于拿线上当实验);④ 合规与分布偏置 —— 生产数据用于训练要过脱敏与授权,且线上暴露的只是问题的一个子集,直接当评测集会带偏。