跳至正文
来两杯美式
返回

给生产 Agent 做体检(二):诊断之前,先定义人口——数据质检与风险筛选

By 来两杯美式
发布于

给生产 Agent 做诊断,最容易被忽略的环节不是 Prompt,也不是模型,而是诊断开始之前的两个决定:哪些对话算数,哪些对话值得花一次 LLM 调用。前者决定统计口径的分母,后者决定成本的上限。这两件事没定义清楚,后面的一切指标——问题数、覆盖率、严重级别分布——都没有可信的解释。

本篇讲我们在数据层做的三件事:数据质检(定义”有效对话”)、Session 化(把散落的行还原成用户旅程)、风险筛选(从有效对话里挑出真正需要 LLM 看的候选)。核心思路可以压缩成一句话:

不是每条对话都值得花一次 LLM 调用,但每条对话都要被规则看一眼。

这套设计带来的直接效果是:LLM 诊断调用量显著下降,而关键的失败证据——用户差评的轮次、多轮对话的收尾轮、所有命中确定性风险信号的行——一条都没丢。

数据质检与风险筛选总览:原始数据经质检、Session 化、规则诊断、风险筛选五个阶段,筛出候选对话送 LLM 诊断,非候选行直接进入报告统计

一、先看具体问题:直接全量诊断会发生什么

假设我们有一个电商客服 Agent,日志表里每天新增几万行对话记录,每个字段一行:用户问了什么、Agent 答了什么、耗时多少、用户是否点了差评。要做诊断,最朴素的做法是全量丢给 LLM:

for row in allRows:
  await llmDiagnose(row)

这条路在真实数据上会撞到三类问题。

第一类是脏数据。 日志表里有重复上报的行、有 query 为空的行(用户只点了个按钮触发)、有 answer 截断的行。这些行进入 LLM 诊断,产出的不是诊断结论,而是噪声。更糟的是它们会污染分母——“共诊断 50000 条对话,发现问题 3000 条”,看起来问题率是 6%,实际上其中一部分”对话”根本不是对话。

第二类是成本。 全量诊断意味着绝大多数一次问答就结束、用户没有任何不满的”平淡对话”也各花一次 LLM 调用。这些调用绝大多数的产出是”无问题”。花钱买确定性,但买的是不需要确定的地方的确定性。

第三类是口径漂移。 一旦后来为了省成本改成抽样,报告里”问题数”的含义就变了:之前是全量里的问题数,现在是样本里的问题数。如果报告不写清楚,读报告的人(通常是 PM)会拿两期数字直接对比,得出完全错误的趋势判断。

这三个问题的共同根源是:诊断管道没有显式定义”人口”。所以我们的方案不是优化 LLM 调用本身,而是在 LLM 之前插一层确定性的数据层,把人口定义清楚。

二、数据质检:定义”有效对话”

数据层的第一步是质检。我们把输入数据建模成一张模板化的宽表,分必填列和可选增强列:

列分类字段说明
必填query用户本轮输入,非空才可能成为有效对话
必填answerAgent 本轮回复,非空才可能成为有效对话
可选增强session_id会话标识,用于多轮还原;缺失时行自成单行会话
可选增强request_time请求时间,用于会话内排序
可选增强row_id行唯一 ID,时间相同时做稳定排序兜底
可选增强first_token_response_time首 token 耗时,规则诊断用
可选增强vote用户反馈(如点踩),规则诊断用
可选增强tool_calls本轮工具调用记录,作为诊断上下文

必填列的设计哲学是最小充分:只要 query 和 answer 都非空,这一轮交互在语义上就是完整的,LLM 就有东西可判断。其余列都是增强——缺失不阻断流程,只是让部分规则信号和上下文变弱。

质检做三件事:

interface QualityResult {
  validRows: Row[];
  duplicateCount: number;
  emptyRequiredCount: number; // query or answer empty
}

function runQualityChecks(rows: Row[]): QualityResult {
  const deduped = deduplicate(rows); // by content hash
  const valid = deduped.filter(
    r => isNonEmpty(r.query) && isNonEmpty(r.answer)
  );
  return {
    validRows: valid,
    duplicateCount: rows.length - deduped.length,
    emptyRequiredCount: deduped.length - valid.length,
  };
}
  1. 去重:重复上报在日志系统里几乎不可避免,同一行被消费两次就是重复计费一次。按内容哈希去重,而不是按时间戳——时间戳在重试场景下会变。
  2. 空 query 过滤:query 为空的行(纯按钮触发、心跳、系统事件)直接出局,它们不构成”对话”;answer 为空的行同样在此出局(截断、上报失败)。
  3. 有效对话判定:由此得到一个严格定义——

有效对话(valid instance):通过去重、且 queryanswer 均非空的行。

这个定义看起来平淡,但它是整个系统里最重要的一个口径。后面所有的 totalInstances、问题率、覆盖率的分母,都锚定在这里。定义一旦上线就不要轻易改——改了等于换掉了统计人口,历史数据立刻不可比。

三、Session 化:把行还原成旅程

质检解决”哪些行算数”,Session 化解决”这些行如何组织”。用户和一个客服 Agent 的真实交互是多轮的:“我的订单怎么还没到” → “订单号是 xxx” → “好的我等等” ——三行记录,一个旅程。诊断时如果把它们当成三条独立对话,会同时丢失两个信息:前文的语境(第三行的”好的”单独看毫无意义)和旅程的结局(问题到底解决没有)。

Session 化的规则刻意做得简单:

排序兜底那个 row_id 值得多说一句:request_time 通常是毫秒甚至秒级精度,并发场景下同一会话的两轮可能拿到相同时间戳。没有 row_id 兜底时,排序结果取决于数据库返回顺序,同一份数据两次诊断可能产出不同的”最后一轮”,整个筛选结果就不可复现了。确定性管道里的每一步排序都要有全序的 tiebreaker。

至此,数据层的核心术语固定下来,后文全部沿用:

术语英文定义
有效对话valid instance通过质检的非重复行,queryanswer 非空
会话session共享同一非空 session_id 的有序行集合;无 session_id 的行自成单行会话
候选对话candidate instance被选中送 LLM 诊断的有效行
已筛查对话screened instance经过确定性风险筛选逻辑处理的有效行(无论是否成为候选)

注意 candidate 和 screened 的区分:所有有效行都被筛查过,但只有一部分成为候选。这个区分后面会直接体现在报告口径上。

四、确定性规则诊断:规则信号先于模型

在筛选候选之前,先跑一轮确定性的规则诊断——用代码给行打标签,不花一次 LLM 调用:

规则信号判定方式示例
首 token 超时first_token_response_time > threshold阈值可配置,如 5s
用户差评vote 为点踩(downvote)显式负反馈
空洞回复answer 命中模板黑名单如只回复”抱歉,请换个问法”
工具失败tool_calls 中存在 error 状态如物流查询接口超时

这里的设计原则是:能用确定性规则打出的标签,绝不交给 LLM。首 token 耗时是不是超阈值,比大小就够了,LLM 不会比大小更准,只会更贵、更不稳定。用户点没点差评,是一个布尔字段,LLM 的介入只会引入误判的概率。

规则标签有两重身份。第一重是诊断结论本身——“这轮首 token 耗时 8 秒”已经是可报告的问题证据,不需要 LLM 再确认。第二重更重要:它是下一节风险筛选的输入,命中规则标签的行会被强制选为 LLM 诊断候选。换句话说,规则负责”看得见”,模型负责”看得懂”,规则信号永远先于模型。

五、风险筛选:候选选择的四条规则(本篇核心)

现在到整条管道最关键的决策:哪些行值得花一次 LLM 调用?我们的候选选择算法是四条确定性规则,在数据质检和规则诊断之后执行:

function selectCandidates(validRows: Row[]): Set<RowId> {
  const candidates = new Set<RowId>();

  // Rule 1: any rule label forces candidacy
  for (const row of validRows) {
    if (row.ruleLabels.length > 0) candidates.add(row.id);
  }

  // Rule 2 & 3: session-level selection
  for (const session of groupBySession(validRows)) {
    if (session.rows.length >= 2) {
      candidates.add(lastOrderedRow(session).id); // Rule 2
    }
    // Rule 3: one-row sessions contribute only via Rule 1
  }

  // Rule 4: multiple hits still yield one candidate (Set semantics)
  return candidates;
}

逐条解释背后的取舍。

规则一:有规则标签的行必入选。 用户点了差评的轮次、首 token 超时的轮次,是已经暴露的风险证据。LLM 在这里的任务不是”判断有没有问题”,而是补全规则给不出的语义层:问题具体出在哪、严重程度如何、属于哪一类(知识缺失、工具误用还是意图理解偏差)。规则发现症状,模型诊断病因。

规则二:多行会话的最后一轮必入选,即使没有任何规则标签。 这是四条里最有设计感的一条。判断一个多轮用户旅程是否闭环——用户的问题到底解决了没有——最好的证据就是收尾轮。用户最后说的是”好的谢谢”还是”算了不用了”,这一个信号几乎概括了整个旅程的质量。而”旅程是否闭环”恰好是规则永远打不出来的标签:它是语义判断,只有 LLM 能做。所以我们把 LLM 的调用精准地花在这个判断价值最高的位置上。一个用户磨了七轮才得到答案,中间六轮可能都没有任何显式差评,但最后一轮的疲惫语气骗不了模型。

规则三:单行会话只看规则标签。 一问一答就结束的会话,没有”旅程闭环”可言,最后一轮就是唯一一轮。如果它干干净净——没有差评、没有超时、没有空洞回复——那么一次普通的问答顺利结束,LLM 再看一遍大概率产出”无问题”。这类对话通常是总量的大头,砍掉它们的 LLM 调用,是成本下降的主要来源。

规则四:多条件命中仍只算一个候选。 一个既被差评又是会话末轮的行,只进一次 LLM。用 Set 语义天然保证。这条不是优化,是正确性——否则诊断计数会膨胀,问题数会重复累计。

这套算法为什么能在大幅降本的同时不丢关键证据?因为它不是随机抽样,而是按证据密度定向筛选。三类高价值证据——显式负反馈(规则一)、性能异常(规则一)、旅程结局(规则二)——全部保留;被放弃的只有”单行、无任何风险信号”的对话,而这类对话恰恰是 LLM 最可能回答”无问题”的部分。放弃的是模型最没话可说的输入,留下的是模型最有话可说的输入。

还有两个容易被忽略的工程细节:

六、会话感知的执行分组:候选怎么送进 LLM

选完候选,下一个问题是执行编排。我们的原则:单次 LLM 请求绝不混入无关会话

如果为了凑满批次把两个不相干会话的候选拼进同一次请求,模型会看到”用户 A 问物流、用户 B 问退货”的缝合上下文。轻则上下文互相稀释,重则模型把 A 会话的订单号”接”到 B 会话的追问里,产出跨会话的幻觉诊断。诊断管道里,上下文污染不是质量问题,是正确性问题。

所以执行单位是会话分组(session group):一个分组只包含来自同一会话的候选轮次。不同分组之间可以并发,分组内部保持会话顺序。每个候选在请求里仍是独立的诊断条目,携带三样东西:当前行、已有的规则标签、有界的前置历史上下文——即同一会话中排在它之前的若干轮。历史设上限是为了防止超长会话撑爆上下文窗口,也避免无关的远古轮次稀释模型注意力。

最后一个边界情况:单个会话的候选数超过请求批次上限怎么办?答案是按序切批——从同一会话里按顺序切出多个批次,而不是把溢出的候选拼到别的会话的批次里去。宁可多一次请求,不跨会话混装。伪代码示意:

function buildRequestGroups(candidates: Row[]): Row[][] {
  const bySession = groupBy(candidates, r => r.sessionId);
  const groups: Row[][] = [];
  for (const [, rows] of bySession) {
    const ordered = sortByRequestTimeThenRowId(rows);
    for (const batch of chunk(ordered, MAX_BATCH_SIZE)) {
      groups.push(batch); // batches from one session, in order
    }
  }
  return groups;
}

与之配套的可靠性边界是:重试与 stale 任务恢复只作用于已持久化的候选诊断。任务中断后重启,恢复的对象是那些”已成为候选、已落库、未完成”的诊断,不会重新跑筛选、更不会给非候选行补诊断。候选集合在任务创建时冻结,之后的执行阶段只是一个有界的状态机。

七、覆盖率口径:两个分母不能混

筛选之后,报告遇到一个新问题:诊断覆盖了多少?这里必须严格区分两个分母:

diagnosed / candidates   ✅ 正确的覆盖率口径
diagnosed / total        ❌ 会误导的口径

diagnosed / total(有效对话里被 LLM 诊断过的比例)在筛选式管道里天然偏低,而且这个比例反映的是筛选率而非执行质量——筛选做得越好,这个数字反而越低。真正的执行质量指标是 diagnosed / candidates:候选里有多少被成功诊断了。这个数字接近 100% 才说明管道执行健康。

我们在任务和报告里显式区分这几个指标:

指标含义分母
totalInstances有效对话总数(筛查人口)
candidateInstancesLLM 候选数(LLM 人口)
diagnosedInstances已完成诊断的候选数candidateInstances
failedInstances诊断失败的候选数candidateInstances
coverageRate候选执行覆盖率 = diagnosed / candidatescandidateInstances

报告正文中的问题计数,口径是”已完成候选中的问题数”,并在摘要里明示筛选范围——让读者不会误以为所有有效对话都被 LLM 逐条诊断过。这一行说明文字看起来不起眼,但它挡住了最常见的误读:拿筛选管道的问题数和全量管道的问题数直接对比。

错误处理也沿着这条口径走:LLM 调用失败只会降低候选覆盖率,不会把非候选行变成失败。判定任务进入终态的阈值,作用于候选覆盖率(存在候选时)。

八、边界情况:零候选与空报告

有一个边界情况值得单独说:零候选任务。如果一天的数据里,所有对话都是单行、干净、无规则标签——筛选结果为空——这个任务怎么收尾?

直觉的做法是标记”数据不足”,提示用户补充数据。但这是错的:数据明明是充足的,只是没有风险信号。一次全绿的体检应该产出一份”未发现风险信号”的空报告,而不是一个错误状态。用户看到空报告,得到的信息是”筛查人口 N 条,零候选,未触发任何风险规则”——这本身就是一个有价值的结论。把它标成”数据不足”,等于把”没病”和”没查”混为一谈,用户下次看到这个状态还是要来问一句。

所以规则是:零候选任务不发起任何 LLM 调用,正常完成,产出空诊断报告,附一条显式的筛选范围说明(selection-scope warning)。覆盖率此时不适用——diagnosed / candidates 的分母为零,报告里显式标注”零候选、不适用”,而不是显示一个误导性的 0%。降级、告警、重试这些为失败准备的动作,一个都不触发。

九、踩坑清单

最后是这条管道落地过程中踩过的坑,按出现频率排序:

十、局限与适用边界

这套设计不是万能的,有几个明确的边界:

十一、总结

回看这一整层设计,它其实反复在做同一件事:在模型之前,用应用代码把协议边界画清楚。什么是有效对话、什么算一个会话、哪个分母对应哪个指标、空候选产出什么状态——这些都是确定性判断,写代码比写 Prompt 可靠得多。而留给 LLM 的,是它真正擅长的那部分:理解一轮对话的语义、判断一个旅程是否闭环、给问题归类定级。

这也是整个系列的底层信条:模型负责语义判断,应用负责协议边界。本篇把人口和边界定义完了,下一篇进入模型一侧——如何让 LLM 把诊断结论以严格的结构化 JSON 稳定吐出来,而不是自由发挥一段谁也没法聚合分析的文本。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
查看系列全部文章
  1. 01.把存量系统交给 Agent,先想清楚 API、Skill、MCP 各是什么
  2. 02.存量系统 Agent 化,物理上到底怎么「接」
  3. 03.单点跑通之后,怎么把 Agent 能力沉淀成全公司可复用
  4. 04.DeepSeek Harness:把 Agent 宿主变成可组合的基础设施,企业能拿它做什么
  5. 05.一个系统,两种访客:给浏览器和 Agent 设计同一套身份体系
  6. 06.定时是 Agent 平台的另一半:Trigger/Action Registry 与调度原语分层
  7. 07.Agent 刚才干了什么:审计、调用日志与敏感数据豁免
  8. 08.提示词安全攻防全解析:越狱、注入与信息泄露
  9. 09.给生产 Agent 做体检(一):只用对话文本的黑盒诊断工作流
  10. 10.给生产 Agent 做体检(二):诊断之前,先定义人口——数据质检与风险筛选
  11. 11.给生产 Agent 做体检(三):让 LLM 稳定输出结构化诊断——严格 JSON Schema 实践
  12. 12.给生产 Agent 做体检(四):LLM 输出不完美怎么办——归一化、一次修复与失败隔离
  13. 13.给生产 Agent 做体检(五):不要消息队列——基于数据库的任务编排与可靠性
  14. 14.给生产 Agent 做体检(六):从诊断明细到 PM 能用的报告——评估结果的产品化
  15. 15.AI Agent:从工具到同事,中间隔着一层「自主性」
  16. 16.Agent 评测工程化(一):为什么不能只看平均分
  17. 17.Agent 评测工程化(二):Rubric 不是 Prompt,而是可执行的质量协议
  18. 18.Agent 评测工程化(三):从低分样本到问题簇
  19. 19.Agent 评测工程化(四):红线、一票否决与 1-5 分
  20. 20.Agent 评测工程化(五):让 LLM-as-judge 稳定输出结构化结果
  21. 21.Agent 评测工程化(六):评测前,先把 Agent 输出拆开
  22. 22.Agent 评测工程化(七):从评分报表到 PM 决策工作台
  23. 23.Agent 评测工程化(八):不用消息队列,也能跑长任务
  24. 24.Agent 评测工程化(九):把一次性评测变成持续优化体系

上一篇
给生产 Agent 做体检(三):让 LLM 稳定输出结构化诊断——严格 JSON Schema 实践
下一篇
给生产 Agent 做体检(一):只用对话文本的黑盒诊断工作流