Commit 939a2fb8 by luoqi

fix(引导)+docs: 撤掉「候选够不着 N 就不出这批多大」那道闸;四问补上深层理由

**① 代码:那道闸的理由只成立一半**(产品指出)。
  原判据 `candidateTotal > batchSize`,注释理由是「候选 312 / N 495 时往上调 target 还是 312,
  点了没反应,而 why 说人数会变 ⇒ 说谎」。️ **那只考虑了往上调** —— 主管同样可以往下调:
  把每天几通 15→5,N=165 < 312 ⇒ target 真的变成 165。他想少发一些,而这道闸把入口藏了。
  ⇒ 闸撤掉,改成**据实说明**:候选够不着 N 时 why 直接讲清方向
    「符合条件的只有 N 人,已经全在本批里了 —— 往上调不会更多,往下调可以少发一些」。
   别再拿"点了没反应"当不出的理由 —— 该修的是那句话,不是把节点藏起来。
  两条测试跟着反转:原来锁"不出",现在锁"两种局面都出且措辞据实";1296 全绿。

**② 文档:四问补上深层理由**(产品指出"分析过的没写上,缺说服力")。
  1 治疗 —— 诊所这季度的经营重点,医生排期/设备耗材/话术都围着它转,**只有他知道**,
    系统里没有任何数据能推出来
  2 多久没来 —— **他能估出回来多少**,而这个估算往下游一步就是诊所排班:
    要不要给医生和科室提前留位。**捞回来了却没人接诊,比没捞更伤客户** ⇒ 必须他拍
  3 高价值 —— **他答不了**:不看数据不知道这批里有多少高价值,也不知道切完还剩几个
    (可能一刀下去只剩三个人)
  4 团队负载 —— 捞太多打不完、到期回池等于白发一轮;而"还吃得下多少"的数在系统里,
    不在他脑子里
  ⇒ 「1、2 问他就有;3、4 问他他也说不出」—— 选人要拆两半的理由,到这里才立得住。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent d253f173
...@@ -25,7 +25,7 @@ flowchart TD ...@@ -25,7 +25,7 @@ flowchart TD
A2["① 选人 · 助手精选<br/>3 要不要只挑高价值的 —— 先摆这批人的全景<br/>4 团队还吃得下多少 —— 先摆在岗人手与在手量<br/>—— · —— · —— · —— · —— · ——<br/>不等他给全,先按这几条默认出一版:<br/>没联系过的排前面,其余按优先级<br/>每人每天 15 通 · 时效 1 天<br/>这批多大 = 在岗人数 × 15 × 时效"] A2["① 选人 · 助手精选<br/>3 要不要只挑高价值的 —— 先摆这批人的全景<br/>4 团队还吃得下多少 —— 先摆在岗人手与在手量<br/>—— · —— · —— · —— · —— · ——<br/>不等他给全,先按这几条默认出一版:<br/>没联系过的排前面,其余按优先级<br/>每人每天 15 通 · 时效 1 天<br/>这批多大 = 在岗人数 × 15 × 时效"]
A2 --> D1{"这批人对吗?"} A2 --> D1{"这批人对吗?"}
D1 --> G1["助手摆出的引导 —— 满足条件才出现<br/>① 能加个条件:这一格 ≥50 人 · 他还没加过 · 切完还剩 ≥10 人<br/>  消费高于本格平均 / 转介绍达人 / 权益身份 / 获客渠道<br/>② 这批多大:人数是系统估的 · 且候选 > 本批人数<br/>  可改 每人每天几通 · 时效几天"] D1 --> G1["助手摆出的引导 —— 满足条件才出现<br/>① 能加个条件:这一格 ≥50 人 · 他还没加过 · 切完还剩 ≥10 人<br/>  消费高于本格平均 / 转介绍达人 / 权益身份 / 获客渠道<br/>② 这批多大:人数是系统估的(他没自己指定过)<br/>  可改 每人每天几通 · 时效几天"]
G1 --> R1["重新选一版<br/>⚠️ 已经排好的分法一并作废"] G1 --> R1["重新选一版<br/>⚠️ 已经排好的分法一并作废"]
R1 --> A2 R1 --> A2
...@@ -69,24 +69,28 @@ flowchart TD ...@@ -69,24 +69,28 @@ flowchart TD
- **图里只画了引导节点。** 主管随口说的(「换成正畸」「改成 200 人」「只要今年来过的」)同样能触发重出 —— 那条路不走按钮,靠助手听懂 + 调工具,⛔ 不在这张图上。 - **图里只画了引导节点。** 主管随口说的(「换成正畸」「改成 200 人」「只要今年来过的」)同样能触发重出 —— 那条路不走按钮,靠助手听懂 + 调工具,⛔ 不在这张图上。
- **「换无专属客服的患者补上」挂在③下面,但它换的是人** —— 走重排(几秒),不是就地改。它是四个节点里唯一一个"长在分人环、代价却在选人环"的选项。 - **「换无专属客服的患者补上」挂在③下面,但它换的是人** —— 走重排(几秒),不是就地改。它是四个节点里唯一一个"长在分人环、代价却在选人环"的选项。
- **「改时效」有两条路,代价不一样**:从②「这批多大」改,会连人数一起重估(**重出**);从④「最忙的那位」改,只延长这一批的期限、人不变(**就地改**)。 - **「改时效」有两条路,代价不一样**:从②「这批多大」改,会连人数一起重估(**重出**);从④「最忙的那位」改,只延长这一批的期限、人不变(**就地改**)。
- **符合条件的人比这批还少时,②照样给他**:往上调确实不会更多,但**往下调可以少发一些** —— 那句话据实写清方向,⛔ 不把入口藏起来。
### 召回策略:一批召回要回答的四个问题 ### 召回策略:一批召回要回答的四个问题
**这一环在「选人」之前** —— 先把问题摆清楚,再看谁答得了。 **这一环在「选人」之前** —— 先把问题摆清楚,再看谁答得了。
| | 要回答的 | 谁答 | 凭什么 | | | 要回答的 | 谁答 | **为什么是他 / 为什么要助手先摆** |
|---|---|---|---| |---|---|---|---|
| **1** | 做哪一类还没启动的治疗(种植 / 正畸 / 拔牙…) | **主管** | 这个季度的经营重点 | | **1** | 做哪一类还没启动的治疗(种植 / 正畸 / 拔牙…) | **主管** | 这是诊所这个季度的经营重点 —— 医生排期、设备耗材、话术准备都围着它转。**这件事只有他知道**,系统里没有任何数据能推出来 |
| **2** | 找多久没来的人(三个月内 … 三年以上) | **主管** | 经验 —— 见下 | | **2** | 找多久没来的人(三个月内 … 三年以上) | **主管** | **他能从这个时间估出大概能回来多少** —— 见下,这一条是整套策略里最值钱的判断 |
| **3** | 要不要只挑高价值的(消费额、商保、意向) | 主管定,**助手先摆数据** | 这批人的画像构成、消费分布 | | **3** | 要不要只挑高价值的(消费额、商保、意向) | 主管定,**助手先摆数据** | **他答不了**:不看数据,他不知道这批里有多少是高价值的,也不知道切完还剩几个 —— 可能一刀下去只剩三个人 |
| **4** | 团队还吃得下多少、几天内打完 | 主管定,**助手先摆数据** | 在岗几人、各自手上压着多少 | | **4** | 团队还吃得下多少、几天内打完 | 主管定,**助手先摆数据** | **捞太多打不完,到期回池等于白发一轮**。而"还吃得下多少"要看在岗几人、各自手上压着多少 —— 这些数在系统里,不在他脑子里 |
**1 和 2 是运营意图,主管张口就来;3 和 4 他答不了** —— 不把全景摆在面前,「要不要只挑高价值的」根本没法回答:他不知道这批人里有多少是高价值的,也不知道切完还剩几个 **1、2 是运营意图,问他就有;3、4 问他他也说不出**,得先把全景摆到他面前。这就是为什么选人要拆成两半
<Callout type="info">**「多久没来」不只是一个筛选条件。** <Callout type="info">**「多久没来」不只是一个筛选条件 —— 它是这套策略里最值钱的那个判断。**
有经验的主管能从它估出这批大概能回来多少 —— 一两年没来的和三年以上的,回头率不是一个量级。
这个估算往下游走一步就是**诊所的排班**:预计能回来多少人、多少要做种植,医生和科室要不要提前留位。 有经验的主管能从它估出这批大概能回来多少:一两年没来的和三年以上的,回头率不是一个量级。
⇒ 所以这一项**必须由主管拍**,⛔ 系统不代劳、也不推荐。</Callout>
而这个估算往下游走一步,就是**诊所的排班**:预计能回来多少人、其中多少要做种植,医生和科室要不要提前留出位置。**捞回来了却没人接诊,比没捞更伤客户。**
⇒ 所以这一项必须由主管拍,⛔ 系统不代劳、也不推荐 —— 它算得出人数,算不出诊所那边接得住多少。</Callout>
### 选人 = 主管初选 + 助手精选 ### 选人 = 主管初选 + 助手精选
...@@ -145,7 +149,7 @@ flowchart TD ...@@ -145,7 +149,7 @@ flowchart TD
|---|---|---| |---|---|---|
| **专属排满了** | 这一版真有人排不进去 | 它是唯一「不处理就真的有人被漏掉」的一条 | | **专属排满了** | 这一版真有人排不进去 | 它是唯一「不处理就真的有人被漏掉」的一条 |
| **能加个条件** | 这一格 ≥ 50 人 · 他还没加过条件 · 切完至少还剩 10 人 | 人太少时「平均数」「最多的那一项」本身就是噪音;已经加过就不再层层套娃 | | **能加个条件** | 这一格 ≥ 50 人 · 他还没加过条件 · 切完至少还剩 10 人 | 人太少时「平均数」「最多的那一项」本身就是噪音;已经加过就不再层层套娃 |
| **这批多大** | 人数是系统估的(他没自己指定过)**且候选人数 > 这批人数** | 候选还够不着 N 时,改时效/改通数都不会有任何反应 —— 而这条的说明写着「人数会跟着变」,那就成了**说谎** | | **这批多大** | 人数是系统估的(他没自己指定过) | 他自己说过「就发 200 人」就不必再解释这个数哪来的 |
| **最忙的那位** | 最忙的人分完之后的量 > 每天通数 × 时效 | 没超就不提,⛔ 不制造无谓的告警 | | **最忙的那位** | 最忙的人分完之后的量 > 每天通数 × 时效 | 没超就不提,⛔ 不制造无谓的告警 |
### 后三步 ### 后三步
......
...@@ -442,12 +442,16 @@ export function computeSignals(p: AssignmentProposal, extra: Signal[] = []): Sig ...@@ -442,12 +442,16 @@ export function computeSignals(p: AssignmentProposal, extra: Signal[] = []): Sig
// 列 3/5/7 天必然漏掉他要的那个,而漏掉那次他只能去打字 → 走模型理解 → 慢且可能错。 // 列 3/5/7 天必然漏掉他要的那个,而漏掉那次他只能去打字 → 走模型理解 → 慢且可能错。
// ⚠️ 15(DAILY_CALLS_PER_AGENT)是**换算尺**,⛔ 不是容量上限、⛔ 不参与落人。 // ⚠️ 15(DAILY_CALLS_PER_AGENT)是**换算尺**,⛔ 不是容量上限、⛔ 不参与落人。
// //
// 🔴 **候选够不着 N 时不出**(2026-08-13):`target = min(batchSize, candidateTotal)`, // 🔴 **2026-08-16 撤掉「候选够不着 N 就不出」那道闸**(产品指出)。
// 候选 312 / N 495 时**瓶颈是候选不是 N** —— 而这两个按钮预填当前值、往上调: // 原判据是 `candidateTotal > batchSize`,理由写的是:候选 312 / N 495 时瓶颈是候选,
// 改时效 1→3 天 ⇒ N=1485,改每天几通 15→30 ⇒ N=990,target 都还是 312。 // 往上调(时效 1→3 天 ⇒ N=1485)`target` 还是 312,点了没反应,而 why 说"人数会跟着变"⇒ 说谎。
// 点了没有任何反应,而这条的 why 写着「这批人数都会跟着变」⇒ **它在说谎**。 // ⚠️ **那个理由只考虑了往上调。** 主管同样可以**往下调**:候选 312 / N 495 时把每天几通
// ⚠️ 判据要用严格 `>`:相等时候选同样是瓶颈(往上调仍然无效)。 // 从 15 改成 5,N=165 < 312 ⇒ target 真的变成 165。他想少发一些,而这道闸把入口藏了。
if (p.basis === 'default' && p.candidateTotal > p.batchSize) { // ⇒ 闸撤掉,改成**据实说明**:候选够不着 N 时,why 里直接讲清楚"往上不会更多、往下可以少发"。
// ⛔ 别再用"点了没反应"当不出的理由 —— 该修的是那句话,不是把节点藏起来。
if (p.basis === 'default') {
/// 候选就这么多人,N 已经够不着 —— 往上调无效、往下调有效,措辞必须分开
const capped = p.candidateTotal <= p.batchSize;
out.push({ out.push({
key: 'batch_size_basis', key: 'batch_size_basis',
severity: SEV.INFO, severity: SEV.INFO,
...@@ -458,7 +462,10 @@ export function computeSignals(p: AssignmentProposal, extra: Signal[] = []): Sig ...@@ -458,7 +462,10 @@ export function computeSignals(p: AssignmentProposal, extra: Signal[] = []): Sig
/// 2026-08-15 实测这里和 `modelFacts` 两处都印错过,沿革见 `rosterCount` 的字段注释。 /// 2026-08-15 实测这里和 `modelFacts` 两处都印错过,沿革见 `rosterCount` 的字段注释。
title: `本批 ${p.batchSize} = 在岗 ${p.rosterCount} × 每天 ${p.dailyCalls} × ${d} `, title: `本批 ${p.batchSize} = 在岗 ${p.rosterCount} × 每天 ${p.dailyCalls} × ${d} `,
// ⚠️ 只陈述事实。⛔ 不写「建议改成 X 天」—— 一批推多大是主管的判断。 // ⚠️ 只陈述事实。⛔ 不写「建议改成 X 天」—— 一批推多大是主管的判断。
why: `这是系统按这个式子估的,不是根据历史算的。改时效或改每天几通,这批人数都会跟着变。`, why: capped
? `这是系统按这个式子估的,不是根据历史算的。符合条件的只有 ${p.candidateTotal} 人,已经全在本批里了 —— ` +
`往上调不会更多,往下调可以少发一些。`
: `这是系统按这个式子估的,不是根据历史算的。改时效或改每天几通,这批人数都会跟着变。`,
defaultLabel: '不处理 = 就按这个数发', defaultLabel: '不处理 = 就按这个数发',
options: [ options: [
{ {
......
...@@ -212,11 +212,9 @@ describe('引导节点 · 判定', () => { ...@@ -212,11 +212,9 @@ describe('引导节点 · 判定', () => {
}); });
/** /**
* 🔴 `short_supply` 与 `batch_size_basis` **互斥**(2026-08-13 加判据之后): * ⚠️ 2026-08-16:`batch_size_basis` 的「候选够不着 N 就不出」那道闸**撤了**(产品指出:
* 前者要 `候选 < N`,后者要 `候选 > N` —— 同一份提案不可能两个都成立。 * 那个理由只考虑往上调,而主管同样可以往下调、少发一些)。⇒ 两种局面它都出,
* ⚠️ 所以"四个同时成立"这件事从此不存在,这条改成分两种局面各断一次。 * 差别在 `why` 的措辞。下面这条改成断"两种局面都有它"。
* ⚠️ 2026-08-14 `short_supply` 整条删掉之后,局面 A 就只剩派活那两条了 ——
* 「这批怎么来的」那一组在候选不够时**本来就没有要主管定的事**。
*/ */
test('🔴 ⛔ 不截断 —— 成立几条出几条,防噪音靠 tier 分层', () => { test('🔴 ⛔ 不截断 —— 成立几条出几条,防噪音靠 tier 分层', () => {
// 局面 A:候选不够(N 是够的,人不够) // 局面 A:候选不够(N 是够的,人不够)
...@@ -229,7 +227,16 @@ describe('引导节点 · 判定', () => { ...@@ -229,7 +227,16 @@ describe('引导节点 · 判定', () => {
byAgent: [agent('张悦', 90)], byAgent: [agent('张悦', 90)],
}), }),
); );
expect(short.map((x) => x.key)).toEqual(['pending', 'daily_overload']); expect(short.map((x) => x.key)).toEqual(['pending', 'daily_overload', 'batch_size_basis']);
/**
* 🔴 候选够不着 N 时,那句话必须**据实**说清方向 —— 往上调确实不会更多。
* ⛔ 不许再说「改时效或改每天几通,这批人数都会跟着变」:他往上调一次没反应,
* 下次就不信这张卡片上的任何一句话了。
*/
const capped = short.find((x) => x.key === 'batch_size_basis')!;
expect(capped.why).toContain('往上调不会更多');
expect(capped.why).toContain('往下调可以少发');
expect(capped.why).not.toContain('都会跟着变');
// 局面 B:候选管够(瓶颈是 N)—— 这时才轮到「本批多大是怎么估的」 // 局面 B:候选管够(瓶颈是 N)—— 这时才轮到「本批多大是怎么估的」
const ample = computeSignals( const ample = computeSignals(
...@@ -273,18 +280,33 @@ describe('引导节点 · 判定', () => { ...@@ -273,18 +280,33 @@ describe('引导节点 · 判定', () => {
); );
}); });
test('🔴 候选够不着 N → ⛔ 不出「本批多大」(改时效往上是空操作,说会变就是说谎)', () => { /**
// target = min(batchSize, candidateTotal):候选 312 / N 495 时改 N 往上,target 还是 312 * 🔴 2026-08-16 **反过来了**:原来这条锁的是「候选够不着 N 就不出」,
expect( * 理由是"改 N 往上是空操作,说会变就是说谎"。产品指出:**那只考虑了往上调** ——
keys(proposal({ basis: 'default', candidateTotal: 312, target: 312, batchSize: 495 })), * 候选 312 / N 495 时把每天几通 15→5,N=165 < 312 ⇒ target 真的变成 165。
).not.toContain('batch_size_basis'); * 他想少发一些,而那道闸把入口藏了。
// 相等时候选同样是瓶颈 —— 判据必须是严格 > * ⇒ 现在**两种局面都出**,差别在 `why` 的措辞:够不着时据实说"往上不会更多、往下可以少发"。
expect( * ⛔ 别再用"点了没反应"当不出的理由 —— 该修的是那句话,不是把节点藏起来。
keys(proposal({ basis: 'default', candidateTotal: 495, target: 495, batchSize: 495 })), */
).not.toContain('batch_size_basis'); test('🔴 候选够不着 N 时照样出「本批多大」—— 他可以往下调、少发一些', () => {
expect( for (const c of [
keys(proposal({ basis: 'default', candidateTotal: 5000, target: 495, batchSize: 495 })), { candidateTotal: 312, target: 312, batchSize: 495 }, // 候选 < N
).toContain('batch_size_basis'); { candidateTotal: 495, target: 495, batchSize: 495 }, // 候选 = N
]) {
const s = computeSignals(proposal({ basis: 'default', ...c })).find(
(x) => x.key === 'batch_size_basis',
);
expect({ 局面: c.candidateTotal, 出了吗: s != null }).toEqual({
局面: c.candidateTotal,
出了吗: true,
});
expect(s!.why).toContain('往下调可以少发');
}
// 候选管够时仍是原来那句(往上往下都真的会变)
const ample = computeSignals(
proposal({ basis: 'default', candidateTotal: 5000, target: 495, batchSize: 495 }),
).find((x) => x.key === 'batch_size_basis')!;
expect(ample.why).toContain('都会跟着变');
}); });
test('⛔ 模型拿不到 options 的技术细节 —— why 里不许出现 intent id', () => { test('⛔ 模型拿不到 options 的技术细节 —— why 里不许出现 intent id', () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment