TECH NOTE
1975 字 10 分钟 系列 15 / 2215 附录:重复请求占比怎么统计(一次调用耗时、平均 token 好量,这个得先定义)
ON THIS PAGE / 目录10 节
一句话讲透:它不是一个能从数据库里直接取出来的字段,而是一个”要先定义清楚”的派生指标。它唯一要服务的决策是——有状态模式(Handoffs / Skills)到底能替你省掉多少次”决策调用”。所以定义必须对着这个目的来:别拿”用户消息文本一样”当判据(追问、换说法、补充信息全都会漏判)。
关联:
03场景二与结尾的”量三件事” |05Handoffs |06Skills | 抓包方法见13附录
一、先定义:什么叫”一次重复请求”
对着决策来定义:同一会话里,这一轮能不能沿用上一轮已经”进入”的那个域/步骤——能,就是一次重复。
thread A: 轮1 [咖啡域] 轮2 [咖啡域] 轮3 [咖啡域] 轮4 [机票域] └─ 粘住 ─┘ └─ 粘住 ─┘ └─ 切换 ─┘相邻轮次对 3 个,其中粘住 2 个 → sticky_rate = 2 / 3 ≈ 66.7%公式:
sticky_rate = Σ(i≥2) [ d(i) == d(i-1) 且 gap(i) ≤ 空闲阈值 ] / Σ(i≥2) 1
d(i) = 第 i 轮**真正被使用**的域/步骤标签(落到了哪个子智能体 / 技能 / 路由分支) gap(i) = 与同会话上一轮的时间间隔(超过阈值就算"重新开始",不算粘住)为什么是这个量:有状态模式省掉的那一次调用,正是”重新进入这个域”的开销——Handoffs 省的是交接,Skills 省的是加载技能,Router 若做成有状态省的是分诊。所以”能沿用上一轮”的比例,就是它们省钱比例的上界。
二、口径先选一个(三种 d(i) 取法,别混着比)
| 口径 | d(i) 取什么 | 优点 | 代价 |
|---|---|---|---|
| 业务主键(推荐优先) | 这一轮操作的业务对象:订单号 / 工单号 / 设备号 / SKU | 最客观、SQL 一句 join 就能算、不用模型 | 通用闲聊类请求没有主键,算不出来 |
| 域 / 专家标签 | 本轮落到哪个子智能体、哪个技能、哪条路由分支 | 直接对应省下来的那次调用 | 需要标签(要从 trace/checkpoint 里取或补埋点) |
| 意图分类 | 离线用分类提示词打出的意图 | 通用、无业务假设 | 要花一次离线模型调用;分类口径本身会漂 |
🧭 建议:先试业务主键——多数 ToB 场景(客服工单、订单、设备)天然有主键,“同一工单连续追问”就是最典型的重复请求。没有主键的通用助手,再退到域标签或离线分类。
三、三条取数路线
路线 A:从 trace / checkpoint / 日志派生(推荐,半天能出第一个数)
每轮只需要四个字段:thread_id、turn_no、ts、domain_label。
标签从哪来:
- LangGraph checkpoint:状态里就有历史 messages。每轮的
AIMessage.tool_calls告诉你调了哪个工具/子智能体;按 tool → 域 的映射表就能得到domain_label。 - Deep Agents:更省事——它会给子智能体运行打上
lc_agent_name元数据(见deepagents/01的”同一个工具在多个智能体间共用”),在工具里runtime.config["metadata"]["lc_agent_name"]就能读到”这轮是谁在干活”。 - 纯日志:按
request_id/thread_id聚合,取日志里”本轮调用了哪个工具/子智能体”的字段。
如果现在什么都没有:加一行埋点,就四个字段,成本远低于事后靠猜。
路线 B:业务主键 join(SQL 模板)
-- 模板:表名/列名换成你自己的;核心是 LAG 取上一轮,再比主键与时间间隔WITH t AS ( SELECT thread_id, turn_no, biz_key, -- 订单号/工单号/设备号… created_at, LAG(biz_key) OVER (PARTITION BY thread_id ORDER BY turn_no) AS prev_key, LAG(created_at) OVER (PARTITION BY thread_id ORDER BY turn_no) AS prev_ts FROM agent_turn_log WHERE created_at >= NOW() - INTERVAL 14 DAY AND thread_id IS NOT NULL)SELECT COUNT(*) AS pairs, -- 分母:相邻轮次对 SUM(prev_key IS NOT NULL AND biz_key = prev_key AND TIMESTAMPDIFF(MINUTE, prev_ts, created_at) <= 30) AS sticky_pairs, -- 分子 ROUND(100 * SUM(prev_key IS NOT NULL AND biz_key = prev_key AND TIMESTAMPDIFF(MINUTE, prev_ts, created_at) <= 30) / COUNT(*), 1) AS sticky_rate_pctFROM tWHERE prev_key IS NOT NULL;PostgreSQL:把
TIMESTAMPDIFF(MINUTE, prev_ts, created_at)换成EXTRACT(EPOCH FROM (created_at - prev_ts)) / 60。 顺手再加一列GROUP BY维度(业务线 / 域 / 用户类型),分域看——重复率在域之间的差异通常比整体均值有用得多。
路线 C:离线分类(兜底)
没有主键、也没有埋点标签时:用 07 Router 的分类提示词,抽样 1–2 周会话,给每轮打一个域标签,再按第一节的公式算。
务必只抽样——为了统计而全量跑一遍模型,本身就违反这次优化的初衷。
四、该报告的五个数(一张 measurement card)
| # | 指标 | 怎么算 | 拿来回答什么 |
|---|---|---|---|
| 1 | 会话轮数分布 | 单轮会话占比、轮数 P50/P90 | 单轮会话对”重复”没贡献;占比高说明这个指标影响面小 |
| 2 | sticky_rate | 第一节公式 | 主指标:有状态模式能省的比例上界 |
| 3 | 平均连续同域长度(run length) | 按 thread 切段,统计同域连续段长度均值 | 直接换算”每个会话能省几次调用”≈ 粘性轮数 |
| 4 | 域切换频率 | 相邻轮次 d(i) != d(i-1) 的比例 | 切换太频繁 → Handoffs 的交接开销会把省下的吃回去 |
| 5 | 空闲阈值敏感性 | 阈值取 5 / 30 / 120 分钟各算一次 | 检验结论稳不稳;差异大说明”会话边界”定得不合理 |
五、把这个比例换成钱和延迟
可省调用数/天 ≈ 日轮次 × sticky_rate省下的 token/天 ≈ 可省调用数/天 × 单次调用平均 token省下的钱/天 ≈ 省下的 token/天 × 单价省下的延迟 ≈ 可省调用数/天 × 单次调用耗时 (串行时直接叠加)反向判据:如果 sticky_rate 只有 10%–15%,有状态模式带来的那点节省,通常撑不起它引入的”粘性复杂度”(要管状态、要兜底切回入口、要解释”为什么这个会话里一直是同一个专家”)——这时候 Subagents 的”恒定成本 + 强隔离”更划算。
六、六个坑(踩了会让数字虚高或虚低)
- 分母口径:按相邻轮次对,不是按会话数——你要省的是调用,不是会话。
- **别用”用户消息文本相同”**判重:
"再买一杯"和"帮我买杯咖啡"是同一件事;而连着两次"帮我查下订单"问的是两个订单,反而是两件事。 - 必须设空闲阈值:隔天再来的一句”再买一杯”,状态语义上已经不算”粘住”了;不设阈值,sticky_rate 会明显虚高。
- 新 thread 不算粘:状态不跨会话保留(除非你另做长期记忆),跨会话的”同样问题”是新请求,不是重复请求。
- 这是上界,不是承诺:上下文压缩可能把粘性打断——比如 Skills 的手册被摘要/裁掉后,下一轮其实得重新加载。真实节省要再打一个折(可用 A/B 或影子流量验证)。
- 别跨用户拼会话:一定按
thread_id分组后再取LAG,否则会把两个人的对话接成一条”连续追问”。
七、一页速览
- 定义:同一会话里”这一轮能沿用上一轮的域/步骤且间隔不超阈值” = 一次重复。
- 公式:
sticky_rate = 粘住的相邻轮次对 / 全部相邻轮次对。 - 取数:业务主键(首选)→ 域标签(trace/checkpoint/埋点)→ 离线分类(抽样兜底)。
- 报告:sticky_rate + 平均连续同域长度 + 域切换率 + 会话轮数分布 + 阈值敏感性。
- 结论用法:sticky_rate 高 → 值得上有状态模式(Handoffs / Skills);很低 → 老实用 Subagents,把精力放在上下文工程上。