Prompt 中 Task 部分的待解决问题

Historical research / Superseded(2026-07-15):本文的问题发现(尤其 typeCategory ↔ closeResult drift、attempt 与 terminal Outcome 混淆)仍有参考价值,但字段、mapping 和方案不再具有规范效力。现行设计见 Task System Design

来源:逐句分析现有和更新的Prompt中task部分的逻辑, 并对照最新schema字段得出。


问题 1:unreachable / neglected定义中的次数阈值脱离时间窗,逻辑不通

contact-analyzer 把 12 个 leadStatus 按客户轨迹分成 3 个 Phase:

  • Phase 1(往前推进,7 个)new / attempted / connected / booked / showed / trialed / converted
  • Phase 2(交谈过但卡住,3 个)bad_timing / not_interested / lost_contact
  • Phase 3(从未接通,2 个)unreachable / neglected

Phase 3 用纯次数判定——neglected(<3 次)/ unreachable(≥3 次),没有任何时间约束。次数脱离时间没有约束力,举两例:

  • 3 通电话打在 10 分钟内 vs 摊在 一周里,都判 unreachable
  • 1 次尝试,对刚进来 1 小时的 lead 和进来 3 周的 lead,都判 neglected

两版 prompt 都有这个问题——pipeline-2(部署版 prompt-builder.ts)和 oliver-proposals 2026-05 提案版这段一字不差。不是 Oliver 引入的,部署版原来就这样

建议

unreachable / neglected的次数阈值必须带时间锚才有意义。最自然的做法是temperature 重新启用——"客户随时间衰减的冷热度"本来就是它的语义,让它真正承担"距上次尝试 > N 天"这种约束,Phase 3 / neglected 用它来界定时间。


问题 2:task status 命名漂移——openpending

现状

contact-analyzer prompt 原文(L552):

Tasks have binary status (pending or closed) at the database level.

数据库 schema、所有 prompt(oliver 2026-05 提案 / pipeline-2 部署版 / prompt-eval v1b-20260504 历史版本)、SQL 查询里全部用 pending / closed

但产品一直叫 "open"——这个漂移没有任何记录,也没经过产品确认**。

这不是 Oliver 2026-05 引入的

Schema 早期 V1 就把 status 限定为 pending / closed(见 archive 的 task-system-v1.mdtask-system-schema-investigation.md)。当前 tasks.ts 用 Drizzle 的 { enum: TASK_STATUS } 约束(TS 类型层——status 字段本身没有显式 DB CHECK,只有 typeCategory / closeResult 才加了 check())。后续所有 prompt / 代码都被迫跟随。Oliver 没动这个字段,只是按现有 schema 写的

状态

待团队对齐:到底是 pending 还是 open?


问题3:typeCategory ↔ closeResult 现状梳理

隐含 3 个问题(可能后续抽出独立 issue)

  • closeResult 是统一池,没按 typeCategory 隔离 —— prompt 让 AI 自己选,schema 不强制 "lead_follow_up 不能用 cancel_saved" 这种业务约束。AI 配错,代码层没硬挡。
  • 没有 typeCategory ↔ closeResult 的合法映射表 —— 现在只能从 prompt 规则反推,没有 schema 层的 mapping enforcement。
  • booked / cancelled 已于 2026-05-23 加入 DB enum(callytics-infrastructure #980),这是 Task V2 enum 演进历史;当前 V3 selected Target 的语义与迁移见 Task V3 Product / Domain Contract §16.2

typeCategory 有 9 种

谁建typeCategory干啥
lead-tracking 系统(非 AI,SLA 驱动)lead_outreachLead 进系统时首次 5 min SLA 联系
contact-analyzer(AI)lead_follow_up推新 lead 预约/转化
booked_not_converted推已预约 lead 完成转化
cancellation_risk挽留要取消的会员
retention解决投诉/服务质量问题
upgrade推会员升级
renewal恢复冻结/付款
referral推荐活动/特别促销
win_back挽回前会员

closeResult 目标态:20 个(12 原有 + ★ 8 新增)

先按"现有 / 建议新增"分,性质(🟢正向 / 🔴负向 / ⚪中性)降为表内一列。

现有 12 个(backend TASK_CLOSE_RESULT 已支持)

closeResult性质含义
attempted🟢 正向lead_outreach 的正向结果, 已发起联系
converted🟢 正向lead_follow_up / booked_not_converted 的正向结果, lead 转化成会员
cancel_saved🟢 正向cancellation_risk 的正向结果, 挽留成功
issue_resolved🟢 正向retention的正向结果, 投诉/问题解决
upgraded🟢 正向upgrade的正向结果, 升级成功
renewed🟢 正向renewal的正向结果, 续费/付款恢复
referral_obtained🟢 正向referral的正向结果, 推荐达成
win_back🟢 正向win_back的正向结果, 前会员回归
already_member🔴 负向lead_outreach / lead_follow_up / booked_not_converted / win_back 通用负向——发现对方已是会员,task 目标不成立
do_not_contact🔴 负向DNC / STOP(所有 typeCategory 通用负向)
wrong_number🔴 负向号码不通 / 联系不上(所有 typeCategory 通用负向)
other⚪ 中性无明确类别(通用中性;closeResult=other 时 closeNote 必填)

建议新增 8 个(★ backend enum 还未支持)

closeResult性质含义来源
unable_to_reach🔴 负向达到最大尝试次数仍联系不上,lead_outreach 的负向结果[Final schema §2.3 line 161](依赖问题 1 修复时间约束后才可靠触发)
booked🟢 正向lead 订了 intro 课,是 lead_follow_up 的正向结果Oliver 已提
lead_declined🔴 负向客户明确拒绝预约/转化(leadStatus 变 paused / terminal),lead_follow_up 的负向结果全新提案(英文名待定)
cancelled🔴 负向取消已发生,cancellation_risk 的负向结果Oliver 已提
upgrade_declined🔴 负向客户明确拒绝升级,upgrade 的负向结果全新提案(英文名待定,备选 not_upgraded
referral_declined🔴 负向客户拒绝推荐,referral 的负向结果全新提案(英文名待定,备选 no_referral
renewal_declined🔴 负向客户明确表示不再续费 / 不恢复付款,renewal 的负向结果全新提案(英文名待定,备选 not_renewed
win_back_declined🔴 负向前会员明确表示不再回归,win_back 的负向结果全新提案(英文名待定,备选 not_won_back

基于以上, 建议建立typeCategory ↔ closeResult 映射

下表只列 typeCategory-specific 的正负向。do_not_contact / wrong_number(通用负向)、other(通用中性)对所有 typeCategory 都适用,不重复列。

typeCategory🟢 正向🔴 负向
lead_outreachattempted(完成首次外联)unable_to_reach ★ / already_member
lead_follow_upbookedlead_declined ★ / already_member
booked_not_convertedconvertedalready_member
cancellation_riskcancel_savedcancelled
retentionissue_resolved
upgradeupgradedupgrade_declined
renewalrenewedrenewal_declined
referralreferral_obtainedreferral_declined
win_backwin_backwin_back_declined ★ / already_member

★ = 新增,backend TASK_CLOSE_RESULT enum 还未支持。

注 1:lead_declined 的底层触发 = contact 的 lifecycleState 变成 paused / terminal(当前 lead_follow_up 随之关闭);其中 paused 牵扯问题 7。

注 2:unable_to_reach 触发条件依赖 unreachable leadStatus(≥N 次真实尝试后仍未接通)。但 unreachable 目前只看次数不看时间窗(见问题 1),导致 unable_to_reach 可能被过早触发——问题 1 不修,这个负向结果就不可靠。

设计说明

attempted 的语义 —— 专门给 lead_outreach

attempted 表示员工完成了首次 SLA 外联任务(打了那通电话)—— 这是 lead_outreach 的"正向 = 任务完成"。

不给其他 typeCategory 用。Oliver 在两处对 attempted 的用法对这个词的语义理解有偏差:

  • [SECTION 7(line 529-530)]:禁止把 cancellation / complaint / manager-callback / upgrade / referral / win-back task 在"一次未接通后"关成 attempted——这里 attempted 不适用于cancellation / complaint / manager-callback / upgrade / referral / win-back task, 这段描述没必要.
  • [SECTION 4 Billing/payment recovery 模板(line 359-360)]:推荐在"付款链接已发出但付款未确认"时 close as attempted——这里 attempted 被当作"完成了一次有意义的行动(发出链接)"

这进一步支持映射表的设计:attempted 专属 lead_outreach(首次外联完成的正向结果),其他 typeCategory 各有专属的负向 closeResult,不再用 attempted 兜底。

如果 attempted 这词容易引起歧义("尝试"听起来像负向),可以考虑改名为 lead_outreached —— 直接表达"首次外联已完成"。详见问题 4。

为啥 1 个 typeCategory 没有对应的负向结果?

设计原则:如果一个任务无法界定负向结果, 但是在没有收到明确否定信号的情况下就需要继续跟下去的话, 那么这个任务需要员工手动用other来关闭, 并填入具体关闭原因, 或者有通用的负向结果出现, 也可以关闭.

具体到 1 个没有 typeCategory-specific 负向的:

typeCategory为啥不需要明确负向
retention投诉 / 问题不解决就一直 open,除非客户"明确放弃投诉"

注:booked_not_converted 虽然没有"客户拒绝"的明确负向,但有 already_member 兜底(发现对方已是会员,task 目标不成立)。

8 个新增 closeResult 待 backend 同步

closeResult适用 typeCategory状态
unable_to_reachlead_outreach全新提案;依赖问题 1 修复时间约束后才可靠触发
bookedlead_follow_upOliver 2026-05 已写入 prompt 规则,等 backend enum 加上
lead_declinedlead_follow_up全新提案,英文命名待团队拍板
cancelledcancellation_risk全新提案,英文命名待团队拍板
upgrade_declinedupgrade全新提案,英文命名待团队拍板(备选 not_upgraded
referral_declinedreferral全新提案,英文命名待团队拍板(备选 no_referral
renewal_declinedrenewal全新提案,英文命名待团队拍板(备选 not_renewed
win_back_declinedwin_back全新提案,英文命名待团队拍板(备选 not_won_back

加完后 closeResult enum 从 11 → 20


问题 4:attempted 不应删除,建议改名

attempted 专属于 lead_outreach正向关闭结果,语义是"首次外联已完成"(staff 完成了 SLA 任务,打出了那通电话)。

Final schema 提议删掉 attempted,是因为误解了其语义(以为它是进展事件而非 task 完成结果)。实际上 lead_outreach task 的正向结束就是"首次外联已打出",没有这个 closeResult,lead_outreach task 就没有正向出口。

attempted(正向)和 unable_to_reach(负向)是并列关系,不是替换关系:

closeResult性质触发条件
attempted(建议改名 lead_outreached🟢 正向首次外联已完成,不论接没接通
unable_to_reach🔴 负向达到最大尝试次数,从未接通,放弃

建议:保留语义,改名为 lead_outreached,明确表达"首次外联已完成",避免被误解成"只是尝试过但没有结论"。


问题 5:Oliver 引入 in-place UPDATE 现状梳理

现状:目前是 close + create 循环,不是真 UPDATE

现行 spec(task-lifecycle.md)第 6.1 节写得很清楚:

Contact Analysis 每次运行时,先 auto-close 该客户的旧 task,再根据最新分析决定是否创建新 task

也就是说:AI 不修改老 task,而是关掉老的、建新的

Oliver 的 UPDATE 改的是 task 的哪些字段

从 Oliver prompt 的 OUTPUT JSON SCHEMA 看:

{
  "action": "update",
  "taskId": "uuid",           // 引用已有 task
  "typeCategory": "...",      // 必填但不能改,只是回填确认
  "priority": "high | medium | low (可选)",
  "suggestedActions": "[...] (可选)",
  "reason": "≤500 字"
}

只能改 2 个字段(至少给 1 个):

字段改了的连锁影响
priority间接影响 dueAt——系统按 priority 推算时间窗(high=4h, medium=24h, low=72h)
suggestedActions给员工的具体话术指引换了

不能改: typeCategory(那是 task 身份)、taskIdcreatedAt

但 Oliver 这个 UPDATE 的字段集不全——补全后应该是这样

直接 mutate 现有列(5 个)

字段谁改怎么改
updatedAtORM 自动Drizzle $onUpdate 自动刷新
priorityOliver schema 允许high / medium / low
suggestedActionsOliver schema 允许完整替换 jsonb
dueAtderive 自动算priority 改了就重算(high=4h / medium=24h / low=72h)。前提:员工没手动 Adjust 过
actionNeededReasonOliver 漏了,语义上必须改suggestedActions 变了,"为啥要跟进"的理由也得跟着变,不然两者对不上

task_events 事件(用现有 audit log 表,不加新列)

AI update / staff 编辑 task 应该沿用这套:

event_type啥时候发payload(进 metadata jsonb)
task.updated_by_aicontact-analyzer 跑出 action: update{ fromPriority, toPriority, fromActions, toActions, fromActionNeededReason, toActionNeededReason, reason, runId } + actor_staff_name='ai-contact-analyzer'
task.updated_by_staff员工编辑 priority / suggestedActions 时{ fromPriority, toPriority, fromActions, toActions } + actor_user_id / actor_staff_name
task.due_at_changed员工 Adjust dueAt 时目前在 contact_timeline——和 task_events 的归属不一致,是 alignment 缺口(见下)

为啥不加 lastAiUpdateAt / updatedBy / lastAiUpdateRunId 列?

  • update 可发生 N 次 → 加专用列每次 overwrite,丢历史;task_events 天然保留
  • close 加专用列因为是终态(每 task 1 次,写完不变);update 是可重复事件,不该比照 close
  • "上次 AI 更新时间" / "谁更新" / "哪次 run" 全部从 task_events 查:
SELECT * FROM task_events 
WHERE task_id = ? AND event_type LIKE 'task.updated_by_%'
ORDER BY created_at DESC

前端用 computed API field 算"未读 AI 更新数"(自上次员工查看以来的事件数),渲染卡片角标。

📝 alignment 缺口:task.due_at_changed 当前在 contact_timeline,不在 task_events

两张表设计上的语义分工(已对照 schema task-events.ts / contact-timeline.ts):

设计意图的范围
task_events单个 task 的 audit log(task.created / task.reopened / task.note_edited 等;event_type 无 DB 约束,由 studio-api 定义)
contact_timeline跨实体的客户活动流(call / sms / lead / task 等都进;event_type 是 enum 约束)

⚠️ 但实际 schema 里两表的 task 事件有重叠,分工并不干净task.created 两张表都有,contact_timeline 还另有 task.status_changed / task.updated。所以下面"该搬"的方向成立,但别把它当成"两表本来泾渭分明、只有 due_at_changed 跑偏"——task 事件本来就两边都落。

task.due_at_changed 只在 contact_timeline 的 enum 里(task_events 没有这个值),进 contact_timeline 是历史决策。严格按"单 task 审计"语义它更该在 task_events,但要不要补 alignment PR 搬过来,是另一个待决策项。

⚠️ 冲突边界:不该 AI 直接覆盖的

字段冲突情况
dueAt员工手动 Adjust 过(如"客户说下周二再打")→ AI 重算 priority 时不该覆盖员工安排。需要 dueAt_manually_adjusted=true 标记
suggestedActions员工在卡片上编辑过 / 增删过 → AI 不该直接覆盖。可能需要 actions_locked_by_staff 标记

task-lifecycle.md §五 已经在讲员工手动 Adjust 时留过"AI 重建覆盖员工调整"的 caveat,UPDATE 机制也得处理同一类问题。

❌ 不该在 UPDATE 里改的(11 个)

字段为啥不该改
actionNeeded(bool)改 true → false 等于关 task,应该走 CLOSE action
status同上,改状态走 CLOSE
typeCategory / taskType / sourceTypetask 身份,改了就不是同一件事
所有 close 字段(closedAt / closedByStaffName / closedByStaffId / closeType / closeResult / closeNotetask 还 open,这些字段就该是空
系统标识(taskId / contactPhone / franchiseId / accountId / storePhone / storeId / sourceLeadId / createdAt强不变(注:schema 里是 accountId,不是 siteId;store 隔离键是 storePhone / storeId

总结

类别数量列表
直接改5updatedAt(自动)、prioritysuggestedActionsdueAt(derive)、actionNeededReason(Oliver 漏了)
timeline 事件3task.updated_by_ai / task.updated_by_staff / task.due_at_changed(现有,不变)
冲突防护标志2dueAt_manually_adjustedactions_locked_by_staff
绝对不该改11身份字段 + 状态字段 + close 字段
不加新列0updatedBy / lastAiUpdateAt / lastAiUpdateRunId(沿用 dueAt 模式,全走 timeline)

问题 6:suggestedActions 模板清单混入"建不建任务"的判断——字段职责错位

拆分后状态(2026-06-03 核查)

Oliver 将 contact 和 task 拆成两个 prompt 后:

  • [04-contact-profile] ✅ 已处理干净:suggestedActions 只作为 contact 级信号输出,无"建不建 task"判断,无 no human action 渠道选项。
  • [05-task-decision] ❌ 问题仍然存在:SECTION 4 的 OTF 模板清单原封不动保留了以下问题(见下方现状分析)。

需要修改的是 05-task-decision SECTION 4,不是 04-contact-profile。

现状(05-task-decision SECTION 4)

[SECTION 4(line 287-379)]的 OTF 模板清单列了 8 个场景,但其中 2 个放错了位置。

机制分三层:

  • suggestedActions(task_issues 表 jsonb 列)= 做什么 action
  • actionNeeded(task_issues 表 boolean 列)= 要不要跟进
  • 建 / 关 / 改 task = taskDecisions[] 里的 action 字段——不是数据库列

三者职责分开,但 SECTION 4 把"建不建任务"混进了 suggestedActions 模板:

模板场景位置它实际在说本该归谁管
Already booked[line 335-338]"不要建或保留人工任务"taskDecisions[] 不输出 create(或输出 close);与 suggestedActions 无关
Do not contact / no action[line 375-378]"不要建跟进建议,关掉相关任务"doNotContact 触发,走 taskDecisions[] 输出 close;与 suggestedActions 无关

剩下 6 个场景(新 lead、问价、取消风险、账单、投诉、课后跟进)才是 suggestedActions 真正该管的内容。

no human action 被列为必填渠道

[SECTION 4 line 298]要求每条 suggestedActions 元素必须包含 channel(可选值为 call / SMS / manager callback / no human action),选项包含 no human action。但"不需要人工"和"这条建议存在"本身冲突——应该走 actionNeeded=false + suggestedActions=[],根本不会有这条建议。

内容重复

"Already booked 不建 task"(SECTION 4 line 337)和 SECTION 5 CREATE RULES(line 404)重复写了同一条规则。

建议(修改 05-task-decision SECTION 4)

  1. 从模板清单删掉 "Already booked" 和 "Do not contact / no action" 两个场景,只留 6 个真正的 suggestedActions 场景
  2. channel 选项从 call / SMS / manager callback / no human action 改为 call / SMS / manager callback
  3. SECTION 4 删掉的场景由 SECTION 5 的 DO NOT create 规则覆盖,不重复

问题 7:paused 缺少和 terminal 对等的 task 停止联动,且与状态定义自相矛盾

设计意图

lead_follow_up类 task应该一直跟,直到对应 contact 的 contacts.lifecycleState 变成 pausedterminal 才停。

现状:terminal 写全了,paused 没写

prompt 对 terminal 有三重保险:

位置规则
prompt L402lifecycleState = terminalactionNeeded 必须 false
prompt L610lead_follow_up 创建条件明确排除 terminal(unreachable / lost_contact / not_interested

paused 只有状态定义里一句泛泛的"paused = stop proactive outreach, wait for reactivation"(prompt L98)。上面三条硬规则(actionNeeded RULE / DO NOT create / lead_follow_up 创建条件)全都只写 terminal,没写 paused

两个具体后果

  1. 自相矛盾:定义说 paused 要"停止主动外联",但 actionNeeded 的 RULE 只拦 terminal——prompt 没有任何规则阻止 paused 状态下 actionNeeded=true。定义说停、规则没拦。
  2. 已存在 task 无人管:唯一映射到 paused 的 leadStatus 是 bad_timingbad_timing 不在 lead_follow_up 创建条件里,所以不会新建;但"已经存在的 lead_follow_up task,客户变成 bad_timing(paused) 之后要关闭task"——prompt 完全没提及

建议(含一个待决策)

paused 补进 actionNeeded RULE 和 task 停止逻辑。


问题 8:一个 contact 可以有多个 task?

背景

当前设计(schema + Oliver prompt)允许一个 contact 同时有多个 open task,每个 typeCategory 最多 1 个。例如一个 member 可以同时有 retention(投诉未解决)和 upgrade(想升级)两个 task。

Oliver SECTION 2 Constraints 原文([line 111-113]):

Max 1 pending task per typeCategory per contact. If the same typeCategory is already pending, update it, close it, or leave it unchanged. Never create a duplicate.

即:同一个 typeCategory 不能重复建,但不同 typeCategory 可以并存——Oliver 认为一个 contact 同时有 retention + upgrade 两个 pending task 完全合法。

产品提案

一个 contact 最多有 1 个 open task(task = 一次通话的容器)。有多个需求不新建第二个 task,而是在这个 task 里新增一个 issue。每个 issue 对应一个 typeCategory,各自独立管理状态和 closeResult。

业务理由:给同一个客户打两个电话是浪费,一通电话应该把所有事情一起说清楚。

Oliver 那条去重规则平移到新结构:同一个 task 里,同一个 typeCategory 最多 1 个 open issue。原文:

Max 1 pending task per typeCategory per contact. If the same typeCategory is already pending, update it, close it, or leave it unchanged. Never create a duplicate.

新结构下有两层约束:

  • 1 个 contact 最多 1 个 open task(新增)
  • 1 个 task 里同一个 typeCategory 最多 1 个 open issue(Oliver 原规则平移)

数据结构设计(关系型,对照当前 schema)

下表基于 2026-05 当时的 tasks snapshot,不能当作当前 schema 或批准后的 Target schema。当前 V3 contract 中的 source snapshot 与 Target delta 分别见 §9.1§9.3;引用前仍须核对 live code。

tasks 表(联系人维度,1 task per contact)

保留联系人身份、整体生命周期、关闭元数据:

字段类型说明
taskIduuid PK不变
contactPhonetext NOT NULL唯一约束改为 (contactPhone, status='pending') — 每个 contact 最多 1 个 pending task
franchiseIdtext NOT NULL审计列,不变
accountIdtext NOT NULL审计列,不变
storePhonetext NULL最近通话的店端电话,不变
storeIdtext NULL不变
statustext NOT NULLpending | closed(沿用当前命名);由所有 issues 推导:所有 issues closed → task 自动 closed
prioritytext NULL派生自所有 pending issues 中最高的那个
dueAttimestamptz NULL派生自所有 pending issues 中最早的那个
closedAttimestamptz NULL整体关闭时间
closeTypetext NULLauto_closed | manual_closed,整体关闭方式
closedByStaffNametext NULL手动关整个 task 的员工姓名
closedByStaffIduuid NULL手动关整个 task 的员工 ID
notetext NULLtask 级自由备注,不变
createdAttimestamptz NOT NULL不变
updatedAttimestamptz NOT NULL不变

从 tasks 表移出、下沉到 task_issues 的字段taskType / typeCategory / actionNeeded / actionNeededReason / suggestedActions / sourceType / sourceLeadId / contactAnalysisRunId / closeResult / closeNote / closedAt(issue 级)/ closedByStaffName(issue 级)/ closedByStaffId(issue 级)/ closeType(issue 级)

task_issues 表(类别维度,N issues per task)

字段类型说明
issueIduuid PK主键
taskIduuid FK → tasks所属 task
contactPhonetext NOT NULL冗余,避免 JOIN
taskTypetext NOT NULLlead_outreach | follow_up,从 typeCategory 派生,随 typeCategory 下沉
typeCategorytext NOT NULL单值,每条 issue 一个类别(8 值,不含 lead_outreach
statustext NOT NULLpending | closed,每个 issue 独立
prioritytext NULLhigh | medium | low,每个 issue 独立
suggestedActionsjsonb NULL{action, reason, priority, priorityReason}[],每个 issue 独立
actionNeededboolean NULL每个 issue 独立
actionNeededReasontext NULL每个 issue 独立
sourceTypetext NOT NULLlead | contact_analysis | manual,每个 issue 独立创建入口
sourceLeadIdtext NULL仅 sourceType=lead 时填,每个 issue 独立
contactAnalysisRunIduuid NULLAI 批次 ID,每个 issue 独立
dueAttimestamptz NULL每个 issue 独立截止时间
closeResulttext NULL每个 issue 独立关闭结果(见问题 3 映射表)
closeNotetext NULLcloseResult=other 时必填
closedAttimestamptz NULL每个 issue 独立关闭时间
closeTypetext NULLauto_closed | manual_closed,每个 issue 独立
closedByStaffNametext NULL每个 issue 独立
closedByStaffIduuid NULL每个 issue 独立(V1 为 NULL)
createdAttimestamptz NOT NULL不变
updatedAttimestamptz NOT NULL不变

唯一约束(taskId, typeCategory, status='pending') — 同一个 task 里同一个 typeCategory 最多 1 个 pending issue。

task 关闭逻辑

所有 task_issues.status = 'closed'  →  tasks.status 自动变 'closed'
员工手动关整个 task                  →  未 closed 的 issues 批量标 closeResult='other',closeNote 必填

影响面(给 teammate 评估)

维度影响
Schema 迁移新增 task_issues 表;tasks 表移除 typeCategory / suggestedActions / actionNeeded / actionNeededReason / closeResult / closeNote(下沉到 issues);唯一约束从 (contactPhone, typeCategory) 改为 (contactPhone, status='pending')
存量数据现有每条 task 需迁移为 1 个 task + 1 个 issue
Prompt 改动taskDecisions 的 action 粒度变成 issue 级;Oliver SECTION 3 的每类规则对应 issue 操作,结构可复用
问题 3 映射表typeCategory ↔ closeResult 映射不变,closeResult 写在 issue 上而非 task 上
analyticstask_issues 是标准关系表,typeCategory 可直接作为分析维度

状态

待 teammate 评估 schema 迁移成本。


问题 9:taskDecisions action 字段缺 3 个值,prompt 需补全

现状

actiontaskDecisions[] 每个元素里的一个字段,值决定后端对 tasks 表做什么操作。

Oliver SECTION 10(line 650-699)目前只定义了 3 种 action shape:create / close / update

2026-05 Final proposal 当时列出 6 种 Task V2 action;当前 V3 command surface 见 Task V3 Product / Domain Contract §10.2

6 种 action

action含义
create_open新建一个 pending task/issue
create_closed新建并立即关闭(当场办成,留记录)
record_progress记录一次进展事件,不关 task
update更新任务信息(优先级 / 建议变了)
close关闭现有 pending task/issue
no_op明确表示"看过了,不需要动"

需要做的事

prompt 补全 3 个缺失的 action(create_closed / record_progress / no_op)之前,需要结合 schema 和后端实现规格,为全部 6 种 action 正式定义:

  1. 写哪张表(tasks / task_issues / task_progress_events / 不写库)
  2. 必须带哪些字段
  3. 不能带哪些字段

不能只靠 prompt 推断——字段规格必须先在 schema / backend 层拍板,再同步进 prompt。

其中 record_progress vs update 的边界需要特别说清楚:两者都针对已有 task,但 record_progress 是"发生了一件事(打电话没接、留了语音),task 本身不变",update 是"task 的信息需要调整(优先级变了、建议换了)"——这个区别必须写进 prompt,否则 AI 会混用。


问题 10:record_progress 启用时 progressType 枚举待统一

现状

Oliver 的正式输出([SECTION 10 line 650-699]没有 record_progress。他在 [SECTION 11 line 703-724]写了一段未来提案,标明"Recommended future backend support / Use this only after backend support exists",里面设想了 record_progress 的 JSON shape 及其 progressType 枚举(8 值)。

目前 record_progress 两边都没有落地,但将来启用时,Oliver 提案的 8 值和 Final schema 的 6 值需要统一:

来源
Oliver 提案[SECTION 11 line 703-724]called / texted / left_voicemail / no_answer / payment_link_sent / form_sent / manager_callback_promised / other(8 值)
2026-05 Final proposalHistorical;当前 V3 contract 见 Task V3 Product / Domain Contract §6.4no_answer / left_voicemail / text_sent / callback_requested / follow_up_scheduled / customer_considering(Task V2 6 values)

两套值名字不一样、数量不一样

两套逻辑分析

Oliver 的 8 值——"staff 做了什么"(动作日志):记录员工执行了哪个动作,纯行为记录,不管结果。

Final schema 的 6 值——"事情进展到什么状态"(状态日志):记录这次互动后客户侧/进展处于什么状态。

关键发现:record_progress要和update区别开来, 但是两套都有值会和 update 交联

record_progressupdate 的边界必须清晰——record_progress 只记录"发生了什么",task 本身不变;update 改变 task 的 priority / suggestedActions / dueAt。两者不能因同一个理由同时触发。

以下是两套全部值的逐一分析(Oliver 8 值 + schema 6 值,去重后 11 个独立概念):

来源是否和 update 交联分析
no_answer两套都有✅ 安全打了没接,task 不变,纯记录尝试次数
left_voicemail两套都有✅ 安全留了语音,task 不变,纯记录
texted / text_sentOliver / schema(同一概念)✅ 安全发了短信,task 不变,纯记录
calledOliver only✅ 安全打了电话(有人接但无实质进展),task 不变,纯记录
payment_link_sentOliver only⚠️ 边界模糊发了付款链接后,suggestedActions 可能要更新为"跟进是否已付"→ 可能需要同时 update
form_sentOliver only⚠️ 边界模糊发了表单(如取消表单)后,task 可能应该直接 close(cancellation proceeding)而不是 record_progress
callback_requestedschema only⚠️ 边界模糊客户要求回拨,dueAt 和 suggestedActions 可能需要跟着变 → 可能同时需要 update
customer_consideringschema only⚠️ 边界模糊客户在考虑中,priority 和 suggestedActions 可能要调整 → 边界不清
manager_callback_promisedOliver only❌ 和 update 交联承诺了经理回电 → priority 必须升 high,suggestedActions 必须更新 → 该走 update,不是记录
follow_up_scheduledschema only❌ 和 update 交联安排了跟进时间 → dueAt 必须跟着变 → 该走 update
otherOliver only兜底值,语义依赖备注,本身不引发 task 变化,但使用场景不明确

结论

  • 安全用于 record_progress(4 个)no_answer / left_voicemail / text_sent / called
  • 应该走 updateclose,不该进 record_progress(2 个)manager_callback_promised / follow_up_scheduled
  • 边界模糊,需要讨论拍板(4 个)payment_link_sent / form_sent / callback_requested / customer_considering

待拍板

progressType 最终枚举应只保留不引发任务信息变化的事件,从两套合并后剔除交联值,建议从 4 个安全值出发,再决定边界模糊的 4 个是否纳入(以及纳入后如何防止和 update 混用)。

状态

待团队拍板 progressType 最终枚举,再同步进 prompt 和 schema。


问题 11:SECTION 9 优先级判断混入"要不要建 task"的规则

现状

[SECTION 9 PRIORITY JUDGMENT(line 630-642)] 在描述 low 优先级时写了两类内容:

第一类(正确)low 的使用条件——"有明确但不紧的后续动作",这是优先级判断应该做的事。

第二类(放错位置):"以下情况不要建 low 优先级 task":

  • 例行预约确认
  • 确认短信
  • intake / waiver 表单提醒
  • 到店提示
  • no-answer / 满信箱 / 无意义语音(无高意向内容)

问题所在

第二类规则实际上不是在说"优先级怎么定",而是在说"这些场景根本不该建 task"——这是 CREATE 决策,属于 [SECTION 5 CREATE RULES(line 382-410)]的职责范围。

两个 section 的职责边界:

Section应该回答的问题
SECTION 5 CREATE RULES什么情况下建 task / 不建 task
SECTION 9 PRIORITY JUDGMENT已经决定要建 task 时,优先级怎么定

把"不建 task"的规则写进优先级章节,会让 AI 在两个地方找到关于"建不建"的判断,逻辑分散,容易漏读。

与问题 6 的关系

问题 6 是 suggestedActions 模板区混入了"建不建 task"判断;本问题是 SECTION 9 优先级区混入了同类判断。根因相同:把"不建 task"的规则分散写在多个 section,而不是集中在 SECTION 5 CREATE RULES 一处。

建议

把 SECTION 9 里"Do not create low-priority tasks for..."的列表移到 SECTION 5 CREATE RULES 的"Do not create a task when"列表下,SECTION 9 只保留优先级判断逻辑。这样 AI 找"建不建 task"只看一个地方。


问题 12:"configured outreach threshold" 概念与已有设计前后矛盾

出现位置

[05-task-decision] 中两处使用了 "threshold" 概念:

  • SECTION 4 New lead 模板(line 326-327):"close the task when the customer books or the configured outreach threshold is exhausted"
  • SECTION 3 renewal 规则(line 255):"keep/update until threshold or manual close"

问题

产品决策是所有阈值写死为常量,当前没有自定义阈值的功能。[04-contact-profile]里已全部硬编码为 3:

  • unreachable = 至少尝试 3 次从未接通
  • neglected = 少于 3
  • Lead attempt threshold = 3

05-task-decision 用"configured outreach threshold"——"configured" 暗示这是个可配置参数,但这个功能根本不存在。同一套 prompt 里一边写死 3,一边用"configured threshold",前后矛盾,会让人误以为阈值可以调整。

建议

删掉"configured outreach threshold"这个概念,统一改为直接写明"3 次尝试后仍无回应",或改用 unable_to_reach closeResult(见问题 4)作为触发条件,和 04-contact-profile 保持一致。


问题 13:员工行动指导中涉及关闭/更新的逻辑必须与 AI 规则一致

设计原则

suggestedActions 是写给员工看的行动指引,但部分模板里包含了"什么时候关 task"或"什么时候更新 task"的描述。这些描述必须与 AI 的对应规则完全一致:

  • 关闭条件 → 必须与 [SECTION 7 CLOSE RULES]对应
  • 更新条件 → 必须与 [SECTION 6 UPDATE RULES]对应

如果 suggestedActions 里的关闭/更新条件与 SECTION 6/7 不一致,会导致员工行为和 AI 判断相互矛盾——员工以为可以关,AI 却不认这个理由;或者员工按 AI 的规则操作,却在 suggestedActions 里找不到对应指引。

已发现的不一致

模板suggestedActions 里的描述SECTION 7 对应规则是否一致
New lead(SECTION 4 line 326-327)"close when customer books or outreach threshold exhausted"customer books → booked ✅;threshold exhausted → 无对应规则❌ 缺口
Billing/payment recovery(SECTION 4 line 359-360)"close as attempted when policy allows"renewal 的 close 规则用 renewed / renewal_declined,无 attempted❌ 不一致(见问题 3)

建议

审查 SECTION 4 所有 8 个 OTF 模板,逐一核对其中提到的关闭/更新条件与 SECTION 6 / SECTION 7 的对应关系。不一致的地方二选一:

  1. 修改 suggestedActions 模板,改用 SECTION 7 里正确的 closeResult
  2. 在 SECTION 7 里补充对应的关闭规则

问题 14:DNC 时 task 关闭由谁执行

两版冲突

原版(合并版):DNC 时 AI 输出 taskDecisions=[](空数组),不枚举任何 close 指令。原因是当时采用 close + create 循环——每次分析都先批量关掉旧 task,再建新的,DNC 就是"不建新的",所以 AI 什么都不用做,系统处理。

Oliver 新版([SECTION 7 line 514-516]):改为 AI 输出 close 指令,引用每个 pending task 的 taskId,用 closeResult=do_not_contact 逐个关闭。原因是新版不再批量关,AI 需要明确指定要关哪个 task。

Oliver 的逻辑(AI 枚举 taskId)在技术上可行——输入里的 PENDING TASKS 本来就是按 contactPhone 查出来的,AI 能看到该 contact 所有的 pending task。但这增加了 prompt 的复杂度:AI 需要识别所有 pending task 并逐个输出 close 指令,而不是简单地让系统处理。

但在"1 contact 最多 1 task"的设计下(见问题 8),这个问题自动消解

  • 一个 contact 最多 1 个 pending task
  • DNC 时系统只需要用 contactPhone 就能找到并关闭这个唯一的 task
  • AI 不需要枚举 taskId,只需要输出 doNotContact=true
  • 不存在"漏关某个 task"的风险,因为最多只有 1 个

建议

采用系统负责关闭的方式:AI 只需设置 doNotContact=true,系统根据 contactPhone 自动关闭该 contact 的 pending task,taskDecisions 不需要输出 close 指令。这与"1 contact 最多 1 task"设计配合,是最简洁且无漏关风险的方案。