打分 → 生成话术 · 这一段拆开看

云电脑自动 DM · 零星 / 小文账号 · 2026-06-15 实测 + PR #349 / #355 / #358 / #359 四次接力复盘
背景:在「云电脑 DM 实时监控页」上,每条评论的 8 节点旅程里有一段叫 打分 → 生成话术。 最近发现这一段忽长忽短:快的几秒、慢的好几分钟,看不出原因。 本页把这一段拆成 3 个子环节,先讲每段在干什么 → 用今天「零星」+「小文」账号实测定位瓶颈 → 再串讲今天一天 PR #349(节奏门收紧)→ #355(事件驱动 + 城市超时)→ #358(5s 软冷却)→ #359(perAccountMax 1→3)四次接力, 一步步把这一段从一周前估计的 ~75s 砍到 PR #355 实测 41s → 预期最终 ~18s(待明日实测复评)。 直接跳第 6 节看总结 ↓

1. 现在监控页看到的:只有 1 段

在监控页 / 客户旅程页里,目前这段长这样 ——
打分
?
生成话术
1.2s
最快
8s
健康
38s
偏慢
1.5min
偏慢
5.8min
长尾 · 06-12 复盘案例
同一段从 1 秒到 5 分钟都见过,因为这一段里实际上发生了好几件不同的事 ——「等」和「真的在算」混在了一起。

2. 拆开看:其实是 3 段

打分
LLM 算完意向
分写回数据库
A · 等派单
在排队等下一轮派单 cron 把这个客户分给我
客户被领走
claim RPC
抢到锁
B · 准备料
查客户底细 + 查抖音号 + 查城市
开始喊 LLM
材料备齐
调 OpenRouter
C · LLM 写话术
大模型生成开场白 + 自检不过会重试
话术写好
入派单队列
等无影发出
关键洞察:这 3 段的"性质"完全不一样。 A 是纯等(机器在排队、不在干活); B 是 IO 网络(查数据库 + 调外部接口); C 是 LLM 推理(OpenRouter 服务 + 模型自身耗时)。 时间忽长忽短,是因为这三种性质混在一起,A 和 C 的长尾各有各的原因,混看就看不出规律。

3. 每段具体在干什么

A 段等派单 · "排队"
大白话:打分完了,但派单 cron 不是分秒触发的,它一分钟才"睁眼"一次。 就算它睁眼了,还要过 4 道门才会真的把这个客户分给当前账号。任何一道门不开,就再等一分钟。
  1. 等下一次 cron tick:每 1 分钟轮一次 → 平均等 30s,最坏 60s (PR #355 事件驱动后已绕过)
  2. 过积压门:这账号上一条还没发完?再等
  3. 过节奏门:距上次发送不足 30s + 0~5s 抖动?再等 (PR #349 已从 60s+30s 收紧到 30s+5s)
  4. 过小时门:近 1 小时已发 10 条?停一段时间
  5. 过日限门:今天已发到上限(零星 = 18)?停到明天 0 点
  6. claim_leads_batch RPC:从全池里挑出这个客户 + 锁 4h,本身 100ms 级
健康 < 90s 偏慢 1.5 – 5min 超标 > 5min(基本是节奏门/小时门压住)
📌 A 段今天做过的四次接力优化:
PR #349(13:09 部署):节奏门 base 60s → 30s、jitter 30s → 5s。把节奏门从限速器位置「踢出去」—— 之前节奏门要求 60~120s 比 cron tick 60s 更高、会让你等到下一班车(最坏多等 60s);现在节奏门 30~35s < cron tick 60s,cron tick 反而成了天花板。详见姊妹页《四道闸门·派单风控解读》
PR #355(15:33 部署):打分 cron 末尾直接调 runDispatch 事件驱动,绕过 cron tick 天花板。
PR #358(16:37 部署):事件驱动路径专用 5s 软冷却(dispatch lib 节奏门 base 30s → 5s 仅本路径),绕过节奏门对事件驱动 trigger 的拦截(实测发现 PR #355 后 trigger 仍被 paced_skip)。
PR #359(18:22 部署):perAccountMax 1 → 3真正的最后一公里—— 实测发现小文 A=39s 仍是 cron tick 主导,原因是每账号每轮 trigger 最多入队 1 条,余下 mid/high 等下一轮(事件驱动 2min / cron tick 1min)。改 3 后一次最多入队 9 条,把"等下一轮"那一档也消除
四次是接力关系:踢出节奏门 → 绕过 cron tick → 绕过节奏门对事件驱动的拦 → 绕过 cron tick 的批次限制。
B 段准备料 · "查数据"
大白话:客户分给我了,但要写出一句"懂你"的开场白,得先把这个人的底细查清楚 —— 他的评论原文、他的家装阶段(已经在装哪一步)、他的抖音号、他的城市……都从数据库或缓存里捞一下。
  1. 读评论详情(loadCommentDetails)—— DB 查询,几十 ms
  2. 读客户的家装阶段画像(loadLeadProfiles)—— DB 查询,几十 ms
  3. 查抖音号缓存(loadDouyinNumbers)—— DB 查询,几十 ms
  4. 反查城市(enrichCommentCity)—— 这步可能慢,需要调外部 worker 接口,正常 200ms~1s,但接口抽风时能拖到几秒甚至超时
健康 < 2s 偏慢 2 – 10s 超标 > 10s(基本是城市反查卡住)
C 段LLM 写话术 · "真的在算"
大白话:材料齐了,喊大模型写开场白。这一段是真的在花算力,但里面其实也不止一次调用,而且可能重试
  1. 解析称呼(resolveAddressFormSmart)—— 跑 V4 Flash 小模型决定怎么称呼对方(哥/姐/老板),约 0.8~2s
  2. 选线索(pickClues)—— 本地规则,毫秒级
  3. 主 LLM 调用(chatReply)—— qwen3-235b 写开场白,典型 3~8s,模型偶尔卡 / OpenRouter 限流时能到 20s+
  4. 自检(sanitize + 各种 hasXxx 检查)—— 本地规则,毫秒级,但如果不过会触发重试整个第 3 步(最多 3 次)
健康 < 6s 偏慢 6 – 15s 超标 > 30s(一般是自检失败重试 2~3 次,或模型卡)

3.5 实测:今天 10 个成功 case 拆开看

脚本 scripts/_breakdown-lingxing-score-to-opener.ts 拉「零星」账号 2026-06-15 北京时间今天所有 dispatch_queue.status=sent 的前 10 条 ——

#客户 (评论片段) A 等派单B 准备料C LLM 写合计 3 段分布重试
1四方田 (亳州可以做吗)29s11s3s43s
29s
11s
3s
1
2柿柿如意的人生 (那里的)13s14s5s33s
13s
14s
5s
1
3老鼠爱大米 (顶楼今年想装修…)17s13s4s34s
17s
13s
4s
1
4桐桐妈 (老板是在那个城市)4s9s5s18s
4s
9s
5s
1
5下一站 (4楼总9层落地窗…)23s10s4s37s
23s
10s
4s
1
6暖暖1331 (给我一份)1m4s12s5s1m21s
64s
12s
5s
1
7爆炸妹儿 (需要一份辟雷手册…)35s11s5s51s
35s
11s
5s
1
8用户人生如梦 (包柜门拉篮一套…)48s11s5s1m4s
48s
11s
5s
1
9雪花 (1,给我来一份)21s20s17s58s
21s
20s
17s
1
10玻璃1 (厂在哪里)41s17s5s1m3s
41s
17s
5s
1
中位 / 最大26s / 1m4s12s / 20s5s / 17s47s / 1m21s
三段时间占比 · 跟纸上估算差很多
A 等派单
61.3%
中位 26s
B 准备料
26.6%
中位 12s
C LLM 写话术
12.1%
中位 5s
三个反直觉的发现:
  • A 段占六成以上 —— 派单 cron tick 60s 是头号嫌疑,平均白等 30s 跟数据中位 26s 吻合得非常好。
  • B 段意外地大(中位 12s,远超纸上估算的 <2s 健康水位)—— 4 个 DB 读 + 城市反查串行执行,每条都稳定花 10s+,这是之前没意识到的大头
  • C 段反而最稳 —— 10 条全部 attempts=1(首轮自检全过),没有重试。说明 prompt 工程已经做得不错;C 段优化收益最低。

优化后预期效果

当前中位优化项优化后中位节省
A 等派单 26s cron tick 60s → 30s(白等时间砍半) ~13s −13s
B 准备料 12s 城市反查 2s 超时 + IP 兜底 + 4 个 DB 读改并发 / 合 1 个 RPC ~2s −10s
C LLM 写话术 5s 不动(已经很稳,attempts=1 全场,没低垂果实) 5s 0
合计中位 47s → 优化后 ~20s −27s(−57%)
结论 / 哪个最该优化: 在「打分→生成话术」这一段,真正卡时间的是 A 段(等派单)和 B 段(准备料),合计 88%。 只做两件事就够 —— ① cron 改 30s tick(改一行 vercel.json)+ ② 城市反查加超时 + 4 个 DB 读改并发, 总工程量不到 1 小时,就能把这一段中位从 47s 砍到 ~20s(−57%)。 C 段(LLM 写话术)目前 5s 是物理下限,不动。
注意 case #6 暖暖1331 的 A 段 1m4s:10 条里 A 段最长的,正好等于一个完整的 cron 周期。 很可能是打分完成 → 派单 cron 检查时撞上了 4 道门里某一道(节奏门 / 小时门),等了下一轮才放行。 cron 改 30s 后这种 case 最坏 1m4s → 30s 左右。
注意 case #9 雪花的 C 段 17s:10 条里唯一的 C 段异常(attempts 仍是 1,但模型推理特别慢), 可能是 OpenRouter 单次抽风。这种偶发长尾要靠换模型 / 换 provider 应对,跟改 cron tick 是两件事。 非长尾下,C 段几乎没法再压。

4. 完整优化清单(含次优先级)

下面 7 个机会,前 3 个就是上面"优化后效果"表里那些;后 4 个是边际收益小的二期项,做完前 3 个再看 ——

① cron tick → 事件驱动 ✓ 已完成 PR #355 高收益 实际省 ~24s 中位
原打算:Vercel cron 改 30s。但 Vercel cron 是 minute-granularity,* * * * * 已经是它支持的最高频,不能改成 30s。 改方案:事件驱动 ——/api/cron/analyze 打分完末尾同步调 runDispatch(enforcePacing=true),新评论直接被派、不等下个 cron tick。1min cron 保留兜底(老 backlog)。 已部署:2026-06-15 15:33 CST · commit 14f8c51
● A 段 · 实现:src/app/api/cron/analyze/route.ts + 加 60s 预算保险
② 城市反查超时兜底 ✓ 已完成 PR #355 高收益 实际省 ~10s 中位
enrichCommentCity 调外部接口反查城市,接口抽风时整条派单被它卡死。 已落地方案 A:fetchUserProfileFromWorkeropts.timeoutMs,默认仍是 8s(peer gate 不变,宁慢勿漏同行),仅 enrichCommentProfile 内部传 2000ms。 超时即 city=null,dispatch 已 fail-open 降级到 comments.ip_location(IP 库 → 省/市,精度够开场白用)。 已部署:2026-06-15 15:33 CST · commit 14f8c51(含 self-review fix 279efc0)。
● B 段 · 实现:src/lib/enrich-user-city.ts
③ 称呼解析的 V4 Flash 调用并行化 中收益 可省 1~2s
现在 C 段是串行:先调 V4 Flash 解析称呼(1~2s),再调主 LLM 写话术(3~8s)。 两次调用之间没有依赖(称呼只影响最终 prompt 拼装),可以并行发起,等两个都回来再合。 或者更狠:称呼解析改成纯规则(现在已经有 resolveAddressForm 兜底),省掉这次 LLM 调用 → 直接砍 1~2s。
● C 段 · 实现:generateOpener 内部改 Promise.all / 删掉 smart 走规则
④ 节奏门 base 60s → 30s、jitter 30s → 5s ✓ 已完成 PR #349 中收益 已让节奏门退出限速器位置
2026-06-15 13:09 部署(commit 37b3541)。节奏门 base 60s → 30s + jitter 30s → 5s + 三个云电脑账号 accounts.min_send_interval_seconds DB UPDATE。 效果:节奏门要求从 60~120s 砍到 30~35s < cron tick 60s。节奏门不再是 A 段的限速器,cron tick 接管成为天花板(直到 PR #355 把这个天花板也绕过)。 继续砍到 15s 没意义 —— 实际等待已经是 cron tick / dispatch 自身耗时决定,不是节奏门。详见姊妹页《四道闸门》复评日 2026-06-22,无 7911 触发即固化。
● A 段 · 已落地
④' 事件驱动专用 5s 软冷却 ✓ 已完成 PR #358 高收益 绕过 30s 节奏门对事件驱动 trigger 的拦截
2026-06-15 16:37 部署(commit 59a1ca4)。PR #355 部署后实测 A 中位 26s → 25s 几乎没动,诊断发现 dispatch lib 的 shouldPaceSkip 仍按 30s 拦事件驱动 trigger,paced_skip 后等下一 cron tick。 改动:DispatchOptions 加 softCooldownSec?enrichCommentProfile 路径传 2000ms,仅 analyze 末尾 trigger 时节奏门 base 用 5s(cron 路径仍 30s 不变)。 硬门(积压/小时/日限/滚动 24h)全保留,总量风控不变。
● A 段 · 已落地
④'' perAccountMax 1 → 3(绕过 cron tick 批次天花板)✓ 已完成 PR #359 高收益 A 段最后一公里,预期 39s → ~5s
2026-06-15 18:22 部署(commit c717986)。PR #358 后实测小文 A=39s(n=7,case "我的我" A=1m45s 连撞两轮 cron tick),诊断:小文相邻发送 3~20min >> 30s 节奏门,节奏门 / 软冷却根本没机会咬 —— 真正的瓶颈是 perAccountMax=1 让每次 trigger 最多入队 1 条/账号,余下 mid/high 要等下一轮 trigger。 改动:两条路径(事件驱动 + 1min cron)都改成 3,一次最多入队 9 条。节奏门继续约束实际发送间隔,总量上限仍由小时门/日限门锁死。 预期:A 中位 39s → ~5s、合计 56s → ~18s。
● A 段 · 已落地,待明日数据复评
⑤ 主 LLM 改用更小但够用的模型 / 加缓存 中收益 可省 2~4s
qwen3-235b 写开场白质量稳,但 235B 推理本身就慢。 方案 A:A/B 测试 qwen3-30b-a3b 类小模型,质量可接受就切(推理时间 ~3s → ~1.5s)。 方案 B:开 OpenRouter 的 provider routing 偏好速度,让请求自动落到延迟最低的 provider。 不推荐方案:本地缓存——每条评论的 prompt 都不一样(含评论原文/姓名/城市),命中率几乎为零。
● C 段 · 实现:bake-off 评测后切 LLM_CHAT_MODEL
⑥ 自检失败的"傻重试"改成"前置过滤" 中收益 极端 case 可省 6~16s
现在 LLM 写完话术后做一堆自检(地理是否对得上、是否捏造经历、是否触发禁词…), 不过则整个重调 LLM,最多 3 次。一次重试 = 多花一整个 C 段时间。 可以在 prompt 里就把这些规则写得更死(few-shot + 明确禁止)让首轮通过率提升, 或者把"已经检测出问题"的失败原因更精细地反馈给下一轮(已部分实现 previousFailureReason)。 收益看现在自检失败率有多高 —— 需要先拉 Langfuse 看真实分布。
● C 段 · 实现:prompt 工程 + 看 Langfuse 重试率决定 ROI
⑦ 4 个数据库读合并成 1 个 RPC 低收益 可省 100~200ms
B 段现在串行做 4 次 DB 读(评论详情 / 家装画像 / 抖音号缓存 / 城市),每次 50ms 左右。 改成一个 Postgres RPC 一次拿全能省一点点。但绝对值小(<200ms),相对 A/C 段那种分钟级长尾,意义不大。 仅作为如果 ① ② ③ 都做完了的"再雕一刀"。
● B 段 · 实现:写一个新的 load_dispatch_context RPC

5. 推荐落地顺序

动作影响段预计省时实现成本 / 风险
✓ 1 事件驱动派单(cron tick 改不了 30s,改了别的) A ~24s 已完成 PR #355 · 2026-06-15 15:33
✓ 2 城市反查超时 8s → 2s(仅富化路径) B ~10s 已完成 PR #355 · 2026-06-15 15:33
3 城市富化提前到 analyze 阶段 B 中位 ~500ms /api/cron/analyze 多一步写回;风险:analyze 慢一点但 270s 预算够
4 称呼解析改回规则 / 并行化 C ~1.5s generateOpener;风险:低,已有规则兜底,最坏退化为旧行为
✓ 5 节奏门 base 60s → 30s、jitter 30s → 5s A 节奏门退出限速器 已完成 PR #349 · 2026-06-15 13:09(复评日 6/22)
✓ 5' 事件驱动专用 5s 软冷却 A 绕过 30s 节奏门拦截事件驱动 已完成 PR #358 · 2026-06-15 16:37
✓ 5'' perAccountMax 1 → 3(绕过 cron tick 批次天花板) A A 中位 39s → ~5s(预期) 已完成 PR #359 · 2026-06-15 18:22
6 主 LLM 模型 bake-off C ~2~4s 需要走 eval harness,建议 AKKE_USE_EVAL_KEY=1 烧 OpenRouter eval key 跑对比
7 4 个 DB 读合 1 个 RPC B ~150ms 收益小,不优先
建议先做的事(不需要拍板就能动): 在动任何代码之前,先把 3 段拆分落进监控页(API 加 claimedFromPoolAt + openerFinishedAt 两个字段), 跑一周收集真实分布数据,再用真实占比决定优先级 —— 比如如果 A 段中位是 50s、B 段中位是 0.5s、C 段中位是 5s,那城市反查的优化就完全不该排第 2。 现在的优先级是基于"理论上哪段最长尾",需要数据校正。

6. 全部改动累计的时效提升

把今天落地的 4 个 PR 串起来看,从"一周前的状态"到"今天 18:22 第四次接力部署完",这一段中位的演进 ——

时间点关键改动 A 段B 段C 段合计中位瓶颈在哪
~2026-06-07
PR #315 之前
节奏门 base 120s/60s + jitter 0~min_interval 推 50~90s ~12s ~5s ~75s+ 节奏门要求 60~240s > cron tick,常被顶到下一班车
2026-06-08 之后
PR #315
jitter 上限 30s + 三账号统一 60s base 推 40~60s ~12s ~5s ~57s 节奏门 60~90s 仍可能顶到下一班,cron tick 60s 也常咬
2026-06-15 13:09
PR #349 后(实测)
节奏门 30s + 5s · 「踢出限速器位置」 26s 12s 5s 47s 节奏门退场 · cron tick 60s 接管 + 城市反查长尾 12s
2026-06-15 16:13
PR #355 后(实测,零星)
事件驱动 trigger + 城市反查 2s 超时 25s 11s 5s 41s 效果远低于预期 · 节奏门拦截事件驱动
2026-06-15 17:41
PR #358 后(实测,小文 n=7)
事件驱动专用 5s 软冷却 39s 9s 4s 56s 节奏门虽绕过,但 perAccountMax=1 + cron tick 仍主导
2026-06-15 18:22
PR #359 后(预期,待明日实测)
perAccountMax 1 → 3(绕过 cron tick 批次天花板) ~5s ~9s ~4s ~18s 一次 trigger 入队最多 9 条,余下不用等下一轮 cron tick
从 6/7 前到 6/15 末:合计中位 75s+ → ~18s · −76%(最终待实测复评) 四次接力归零 cron 限制
⚠️ 实测跟预期差很多 —— 为什么?
部署后 40 分钟拉的新 10 条 sent,A 中位 26s → 25s,几乎没动。事件驱动确实在干活(case 明哥 A=17s 是事件驱动绕过 cron tick 的样本),但大多数 case 撞上节奏门被 paced_skip,必须等下一个 1min cron tick 才重试。 B 段最大值从 20s → 14s(−6s 长尾被砍),但中位 12s → 11s 只动 1s —— 中位本来就是 DB 读 + worker 反查 < 2s 的范围,2s 超时只砍长尾不动中位。

"地板和天花板"层层揭开,四次接力推到底

每解决一层,下一层就浮出来当瓶颈。本以为两层就够,实际四层才到 dispatch lib 自身:

最终预期:A 段瓶颈降到 dispatch lib 本身耗时(DB 读 + claim RPC + insert)≈ 2-5s。 总量风控(小时门 10/h + 日限 25/d + 滚动 24h × 2)一直没动,7911 风险无增量

诚实结论 —— 今天两次改动合起来,「打分 → 生成话术」中位时延从一周前估计的 75s+ 砍到 41s,提升 −45%(不是原本以为的 −87%)。 一天 4 次接力,把 A 段从"节奏门 → cron tick → dispatch lib 内节奏门 → perAccountMax 批次天花板"四层瓶颈一层层揭开 + 干掉。 总量风控(小时门 10/h + 日限 25/d + 滚动 24h × 2)一直没动,7911 风险无增量。 PR #359 实测要等明日累积 ≥10 条 sent 后跑 _breakdown 复评 —— 预期 A 中位 39s → ~5s、合计 56s → ~18s(−76% vs 一周前 75s)。 复评日跟 PR #349 一起 2026-06-22,观察一周无 7911 + 无举报即固化。

附:本页提到的关键术语

数据源:/api/public/cloud-pc-dm-live(监控页 API)· src/lib/cloud-pc-dispatch.ts(派单主流程)· src/lib/llm.ts generateOpener(话术生成)· supabase/migrations/20260519115500_llm_observability_foundation.sql(latency_ms 字段)· scripts/_breakdown-lingxing-score-to-opener.ts(账号 8af08f10 零星·北京 2026-06-15 当天 dispatch_queue.status=sent 前 10 条实测)。
部署 anchor:PR #349 = 37b3541(节奏门,13:09)· PR #355 = 14f8c51(事件驱动 + 城市超时,15:33)· PR #358 = 59a1ca4(5s 软冷却,16:37)· PR #359 = c717986(perAccountMax 1→3,18:22)。 第 6 节"累计时效提升"中 PR #349 之前的数据是用 dispatch-four-gates-2026-06-15.html 里 6/12-6/14 间隔反推估算;之后为实测;PR #359 后为预期,待明日复评。 生成于 2026-06-15 · 四次接力陆续部署后更新。