← 目录
otter-coordinator · 对三个提议的线上验证 · 2026-07-25

CRM 归 CRM,
Agent 归 Agent。

你提的拆分是对的,而且比我原来的方案更好——它给出了一条我没想到的迁移路径。 但你问的三个问题,线上数据的答案跟直觉不一样。其中一个是「今天完全做不到」。

先确认我理解对了
  • 不基于 negotiation prompt 的具体内容立论——那块还在动,可能有失误。本文只用结构性与规模性事实。
  • Coordinator 彻底拆成两半:CRM(像 Salesmartly,纯沟通)+ Engagement Agent(milestone / proposal / escalation)。后者是重构主体,暂停 inbox 就能重构,完成后切回开启
  • 交付物看板保留,按合作活动 group。但要想清楚:这是否意味着以 creator 为记录主体?那样能否统一复购/复用?交易金额能否准确记录?会话怎么方便绑定?能不能用 Coordinator 自己的 Campaign 来做绑定(不再从 scheduler 同步)?
01
你的提议 · 我完全同意

这一刀切在正确的位置

我原来的方案是按「线 A 成本 / 线 B 安全」分的,那是按问题类型分。 你这一刀是按系统边界分,更好——因为它同时给出了迁移路径。

CRM · 沟通层
  • 渠道接入、消息收发、去重、投递回执
  • thread / conversation 归并
  • 联系人与渠道身份(邮箱 / 手机 / handle)
  • 收件箱、已读状态、@提及、内部备注
  • 消息规范化:剥追踪链接、剥 UI 状态、流量分类
规模:291,196 threads · 持续在线,不停机
Engagement Agent · 业务层
  • 事实账本(价格、条款、承诺)与出处
  • milestone / deliverable / obligation
  • proposal / escalation / 自动派发
  • decide() 判定与 Writer 表达
  • 对外投影(Scheduler N4、Bitable)
规模:4,740 deliverables · 可暂停、可重建、可切换
为什么这一刀是对的 · 两个数量级

CRM 侧是 291,196 条会话线程;Agent 侧是 4,740 条交付物记录。 差两个数量级。 一个是高吞吐、低语义、必须永远在线的管道; 一个是低吞吐、高语义、可以停下来重做的状态机。 它们对可用性、一致性、演进速度的要求完全不同,不该住在一个系统里。

「暂停 inbox」这个开关已经存在

coordinator_engagements 上已经有一列 suppress_received_channel_agent_runs db-schema/coordinator.ts。 也就是说:消息继续进来、继续入库、继续在收件箱里给人看,只是不再触发 agent 运行。 这正是你说的那个模式,而且是逐 engagement 粒度的——可以按活动、按批次灰度,不必全量停机。

阶段CRMAgent运营看到什么
今天在线在线(旧)正常
重构期照常在线,消息不丢暂停(灰度批次)收件箱正常,自动回复停,人工接管这批
切换后照常在线新版本,读同一份消息重建状态恢复自动,历史可回放核对

这解决了我原方案里最难的一环:不再需要「双写两套业务真相解释」的长期比对期。 新 Agent 从 CRM 已有的消息重建账本,和旧状态做一次性比对即可。

02
你的三个问题

线上数据怎么说

问题一

以 creator 为记录主体,能统一复购/复用吗?

能,但不免费

因为今天系统里根本没有「达人」这个身份。 得先做一次身份归并,而好消息是:数据支持归并。

先看今天 kol_id 的实际分布——它本该是达人身份:

51,175engagements
39,064distinct kol_id
39,053kol 只有 1 条 engagement
11kol 有 2 条以上
结论

kol_id 不是「人」,是「活动 × 人」。 同一个真人参与第二个活动时会拿到一个全新的 kol_id。 所以今天问「我们跟 @laura 一共合作过几次、付过多少钱」——系统结构上答不出来。 这不是复购流程不统一,是复购这个概念在数据层根本不存在

但同一个 creator_handle 确实散在多个 kol_id 上,而且邮箱能把它们缝回来:

handle → 1 个 kol_id26,911 人
handle → 2 个3,582 人
handle → 3 个946 人
handle → 4–10 个464 人
handle 无 kol_id7,726 人

4,992 个 handle 被拆成了 2–10 个 kol_id,涉及 12,153 条 engagement。

邮箱是可用的身份脊柱

同一邮箱关联的 engagement 数邮箱数覆盖 engagement
123,54223,542
23,7187,436
31,0473,141
4–126953,443
可执行的结论

5,460 个邮箱已经可证明地对应 2 次以上合作,覆盖约 14,020 条 engagement。 这批复购关系今天就躺在库里,只是没有一个实体把它们表达出来。 做一张 creator + creator_identity(email/phone/handle), 回填一次就能拿到。

但要接受两个现实: ① 邮箱身份只覆盖 34,127 / 51,175 = 67% 的 engagement,剩下 33% 需要 handle/手机兜底或留作未归并; ② 归并会出错(共用邮箱、经纪人代收),所以 merge / unmerge 必须是运营可操作的动作,不能是纯自动的一次性脚本

问题二

实际发生交易的金额,能准确记录吗?

今天完全不能

而且以 creator 为记录主体不会自动修好它。 这是三个问题里最刺眼的一个。

4,740deliverable 总数
16填了 paid_amount_usd
0.3%
48状态标记为 paid
$17,261全库累计可查的
已付金额

对照谈判侧的数据,反差非常大:

价格条目(已谈)32,478
其中已接受2,036
标记已付48
有金额的已付16
这意味着什么

我们知道 2,036 次谈成了什么价,却只知道 16 次实际付了多少。 系统答不出「我们有没有按谈成的价格付款」这个问题。 任何按达人汇总的历史报价、可靠度、ROI,今天都建立在一个 0.3% 覆盖率的字段上—— 那不是数据稀疏,那是这条链路没有被写入过。

怎么修

creator-as-record 提供的是汇总的容器,不是数据本身。金额必须单独补一条带出处的记录:

payment(deliverable_id, creator_id, amount, currency,
        source_kind,      -- 'finance_webhook' | 'operator' | 'contract'
        source_ref,       -- ← 不是消息,是打款回执/财务事件
        paid_at, agreed_amount_ref  -- 指向当初谈成的那条价格事实)

注意最后一列:把「付了多少」和「当初谈的多少」用外键连起来, 差额本身就是一个可查询、可告警的指标。这一列今天不存在,所以这个问题今天连问都问不出来。

优先级建议

这条比看板本身更值得先做。没有金额,看板上「已发布 · 待打款」那一格永远是空的, 而那恰好是运营最想看的一格。

问题三

creator × N campaigns,那么多会话,能方便绑定吗?

人工绑定这条路已经被证伪了

手动绑定工具今天就存在,上线以来一共用了 310 次——0.45% 的 engagement。不是不好用,是量级根本不允许。

291,196chat thread 总数
6.1平均每个 engagement
的 thread 数
16.3%标记为
in_current_scope
310手动绑定累计使用次数
(campaign_thread_links)

thread 的归属状态分布:

当前范围内47,456 · 16.3%
范围外(有归属)186,710 · 64.1%
未分类57,030 · 19.6%
量级判决

29 万条线程、平均每个合作 6.1 条、长尾有 15 条以上的。 任何需要人逐条确认的绑定方案,在这个量级上都不成立。 绑定必须默认自动,只把真正歧义的少数抛给人——而且要立刻抛,不是静默排队。

好在歧义是少数:绝大多数入站消息都带着技术标识符(邮件的 In-Reply-To / References 指向我们自己发出的 Message-ID、WhatsApp 的引用消息 id)。真正需要人判断的, 是「这个手机号同时属于两个在跑的合作」这一类——而这时最快的办法往往是问达人本人

03
你的第四个提议

用 Coordinator 自己的 Campaign 做绑定?——对一半

对的一半:停止从 scheduler 同步 campaign

数据完全支持。今天同步过来的 455 条 campaign,结构化信息几乎为零:

字段实际情况可用性
platform455/455 都是 "multi"1 个取值 = 零信息
target_influencer_profile0 条填充永远是空字符串
requirements454 条有值整个 brief 压成一条自由文本,无结构化渠道/数量
budget / pricingMode / region455 / 455 / 39 种这三个是真有用的
所以

同步换来的是 label + budget + pricingMode + region,外加一坨没结构的文本。 为这点东西绑一个跨服务的强依赖不划算,而且它让「新建一个活动」必须绕道 Java 服务—— 这正是 custom:<uuid>fork:<origin>[n] 这类伪造身份的来源。 Coordinator 本地拥有 Campaign,我赞成。

不对的一半:Campaign 不能就是绑定键

绑定问题的原话是:「这条会话,属于这个达人 8 个在跑的活动里的哪一个?」 把 Campaign 的所有权从 Java 挪到 Coordinator,没有回答这个问题—— 它换了容器的主人,没有提供选择的依据。

两件事必须分开:

问题本地拥有 Campaign 能解决吗需要什么
创建成本
建一个分组要绕 Java 服务
✓ 完全解决本地 Campaign 表,运营直接建
归属选择
这条 thread 归哪个合作
✗ 完全不解决一条独立的 thread ↔ collaboration 边,带证据与时间区间
-- Campaign 是分组容器,本地拥有,创建零成本
campaign(id, label, budget, pricing_mode, region,
         external_ref nullable  -- 想同步就同步,不同步也能用)

-- 绑定是一条独立的、可修订的边 —— 这才是回答「哪一个」的地方
thread_binding(thread_id, collaboration_id,
               evidence_kind,  -- in_reply_to | quoted_msg | operator | asked_creator
               evidence_ref,
               valid_from, valid_to)
一句话

拥有 Campaign 让「绑定目标」变便宜,不让「选哪个目标」变自动。 前者是你说的那个改动,值得做;后者需要另外一条边。两个都要,但别指望一个解决另一个。

04
综合

修正后的模型

creator              ← 新增。持久的人。复购/历史报价/可靠度住这里creator_identity  ← 新增。email / phone / handle,可 merge / unmerge
        │
        │  (邮箱已能缝合 5,460 人 / ~14,020 条 engagement)collaboration        ← 今天的 engagement。一次 creator × campaign × 轮次
  ├ campaign_id      本地拥有,可空(临时对话/实验不需要活动)deliverable[]     各自独立推进
  │    ├ obligation    ← 新增。球权 + 缺口 + 时限,按交付物
  │    └ payment       ← 新增。实付金额 + 出处 + 指向当初谈成的价格fact[]           ← 新增。价格/条款/承诺,每条带消息出处

thread  ←→  thread_binding  ←→  collaboration
  CRM 拥有 thread;Agent 拥有 collaboration;binding 是两者之间唯一的边

注意 thread_binding 这条边的位置:它正好落在 CRM 与 Agent 的分界线上。 CRM 负责把消息收好、归到 thread;Agent 负责业务状态; binding 是唯一需要两边协商的东西,也因此是唯一需要谨慎设计的接口。

复购在这个模型里长什么样

不需要 renewal_round,不需要 parent_engagement_id (今天分别只用了 13 次和 134 次)。复购就是同一个 creator 下的第 N 条 collaboration——一个 COUNT(*)。 历史报价是这个人名下所有 payment 和已接受价格事实的集合, 带币种、交付物类型、日期,作为历史给人看,不作为当前条款自动带入

05
看板 · 按你说的 group by 活动

但要先接受一个现实

数据现状检查

8,586 个进行中的交付合作,只有 4,125 个有 deliverable 记录——52% 是空的。 而有记录的里面,3,837 个只有 1 条交付物,只有 288 个是多交付物。

所以看板上线第一天,一半以上的合作会是空泳道。 「把交付物建出来」本身要成为流程的第一步,不能假设它已经存在。 好消息:合同签署时按谈成的价格条目自动播种交付物,这条链路代码里已经有了。

@laura_makes · 3 个合作活动 · 累计 $4,200
Temu US Promotion 进行中 谈成 $1,000 · 已付 $1,000
视频 1 · TT已付 发布证明已收 ↳ 7/21 达人消息 $1,000
视频 2 · TT球在我们 脚本 v2 等我们审 · 达人 7/23 已交 超时 2 天
视频 3 · TT球在达人 30 秒开场钩子改写 ↳ 我们 7/12 的原话 13 天
CapCut July Promotion 谈判中 对方要价 $1,800 · 我方 $1,200
— 未建交付物待合同签署后播种 今天 52% 的交付合作处于这个状态
Pippit US April 已结 2026-04 · 已付 $3,200
历史2 条已交付 按时交付 · 0 次返工 —— 这一行就是复购依据 $3,200

这张图里有三件今天做不到的事,每一件对应上面一个问题: 顶部「@laura · 3 个合作活动」需要 creator 身份(问题一); 每一行右侧的金额需要 payment 记录(问题二); 每条消息引用能点开原文,需要事实带出处。

06
落地

按你的拆分重排的路径

归属动作为什么是这个顺序
1CRM消息规范化:入库时剥追踪链接、UI 已读状态、恒空字段;加流量分类(群发/自动回复)零 schema 变更,不动业务;Agent 重构前就该做完,否则新 Agent 也吃脏数据
2CRMthread_binding 独立成边,自动绑定 + 歧义立刻抛给人(含「问达人」这一级)29 万线程,人工绑定已证伪
3Agentpayment 记录 + 指回谈成价格0.3% 覆盖率是最大的数据空洞,而且看板最想看的一格靠它
4Agentcreator + creator_identity,按邮箱回填一次5,460 人的复购关系今天就躺在库里
5Agent暂停 inbox(灰度批次)→ 重建 fact 账本 + obligation → 与旧状态比对 → 切回开启你提的迁移路径。用 suppress_received_channel_agent_runs 逐批做
6AgentCampaign 本地化,campaign_id 改可空,合成身份退役依赖 2,不依赖 5
7Agent看板上线依赖 3+4,否则一半泳道是空的
和上一版的关键差别

上一版我把「双写比对期」当成必须承受的成本。 你的 pause-inbox 方案把它消掉了——CRM 不停,消息不丢,Agent 可以整个换掉再从消息重建。 这是这次讨论里最有价值的一个改动。

07
最后

三个我还需要你拍板的点

  1. 身份归并的口径。 邮箱只覆盖 67% 的 engagement。剩下 33%(主要是只有 handle 的)——是先不归并、留作「未确认」,还是用 handle+platform 做二级归并并接受一定误合并率?这会决定复购数字的可信度。
  2. payment 的数据源。 实付金额从哪来?财务系统回调、运营手填、还是合同签署额?这决定它是自动事实还是人工事实,也决定「有没有按谈成的价格付」这个告警能不能自动跑。
  3. Campaign 本地化后,存量 455 条怎么办? 一次性快照复制过来断开同步,还是保留 external_ref 做单向只读同步?我倾向前者,但这取决于 Scheduler 那边还有没有别的消费方在依赖这层关系。

本文数据均取自生产库直查(2026-07-25),未使用任何 negotiation prompt 内容作为论据。
上一版整体架构:Research/coordinator-vnext/04-vnext-architecture.md
实测明细:evidence/prod-token-economics.md · evidence/code-audit-facts.md