我们在为「重读」付钱,
不是在为「思考」付钱。
每来一条达人消息,系统就把整条聊天记录重新推导一遍业务状态。真正的 agent 只花掉 coordinator 预算的 2.7%,其余 95% 花在它周围的重复推导上。 这份文档基于线上实测,不是基于代码阅读的推测。
(13 条入站消息)
换来 89 token 输出
已算好的确定性动作
数据来源:otter-lens 线上 LLM 调用日志 + 生产库直查,7 天窗口。 每一条论断在文档里都带一个证据编号 E4, 可以回到 Research/coordinator-vnext/evidence/ 复查。
四个案例,说明问题不在「模型不够聪明」
案例 A · 一条 635 KB 的 prompt,98.6% 是噪音
我们抓到线上一次真实调用:任务是「找出达人此前报过的价格」。输入 635 KB / 296,224 token, 输出 89 token。把这条 prompt 按块拆开,比例是这样的 E4:
prompt 组成 · 按真实字符数等比绘制
635,216 chars把那 98.6% 再拆开,里面装的是:
| 内容 | 占整条 prompt | 它是什么 |
|---|---|---|
| base64 点击追踪链接 × 67 | 42.9% | 邮件营销平台的跳转 URL |
| threadUnreadCount / threadLastReadAt / threadReadReceipts … | 27.3% | 界面已读状态——被喂给一个做价格抽取的模型 |
| htmlContent:null、attachments:null 等 10 个恒空字段 | 含在上一行 | 每条消息重复一次 |
| 不可见填充符 ͏ × 1,128 | — | 邮件预览占位符 |
| 真正的价格信号 | 0% | — |
追踪链接和已读回执不该在入库时就存在于可读文本里。 把 SELECT * 直接递给一个语言模型,不是上下文窗口不够用,是缺一个序列化器。
案例 B · 模型答对了,系统重跑 60 次去推翻它
上面那条 prompt 里的 29 条消息,全部来自同一个营销邮件列表 (emil@iphonephotographyschool.com,标题「Thank You For Contacting Us」×17)。 没有达人,没有价格。模型的完整回答是:
{"priceUpdates": [],
"summary": "No inbound creator-owned price quote for the current
YouTube long-form video package is present in the scoped history."}
它是对的。 然后系统在同一份 296k token 输入上重跑了 60 次,花掉 $26.46 E3。 原因在代码里:校验器把「这个事实本来就不存在」和「抽取失败」当成同一种状态, 于是触发了三级修复阶梯 activities.ts:2240-2400—— 整历史重抽 → 再抽 → 内部再循环 5 次。
(昂贵的重新推导) × (重试阶梯) —— 最差的组合。 而这条链路的起因是:我们群发冷邮件到了一个公司的客服邮箱,自动回复被当成了达人的谈判回复。
案例 C · 答案早算好了,却花 2 万 token 请模型复述一遍
后端有一段 9,290 字符的确定性 TypeScript,已经把该做什么、出多少钱都算出来了 prompts.ts:2197-2392。它长这样:
required_next_action:
type: send_channel_message
requirement:
recommended_offer_usd: 1000
comparison_basis: total_package
deliverable_count: 3
guardrail: "Use recommended_offer_usd as the exact JO total package offer…"
然后它被塞进一个 19,395 token 的 prompt——其中静态指令占 54.3%,这个决策只占 2.6% E11。模型的全部产出是:把这句话用英文重说一遍,交给下一个 LLM 去写句子。
抽样 40 次真实调用 E12:
(65 : 1)
或空手而归
模型仍不执行
50% 的不服从率,正是代码里要补一段「模型没发消息就替它发」兜底的原因 main-agent.ts:3185-3248。 用散文去约束一个类型化契约,是无效的。
案例 D · 叫「campaign_scoped_history」,里面装了 8 个 campaign
系统提示词里白纸黑字写着 “Every engagement is scoped to exactly one creator and one campaign; never mix facts from other campaigns” prompts.ts:71。 而线上那一个「已限定范围」的历史块里,实际含有 E5:
| 数量 | 维度 | 说明 |
|---|---|---|
| 8 | 不同的 campaignName | Pippit US April、CapCut Web YouTube、Temu North America、Pippit MX… |
| 9 | 不同的 referenceId | 本该唯一标识「活动 × 达人」 |
| 4 | 不同的 engagement | 四个独立合作的消息混在一起 |
| 29 / 29 | 标记为 referenceInScope: "true" | 范围是被断言的,不是被证明的 |
这不是偶然的 WHERE 写错。历史是按联系渠道聚拢的, 却按活动贴标签——而 19% 的达人(38,299 中的 7,295)同时在 ≥2 个活动上, 最多的一个在 20 个上 E6。这两个集合本来就不相等,没有任何 WHERE 子句能修好。
里程碑没坏,不要重写它
之前的设计稿主张把里程碑整体改造成「证据推导的投影」。数据不支持这个方案 E10:
(466 次)
——这是运营的产物
值基本是对的,而且主要是运营在用,不是 agent 在用。整体重写是一笔糟糕的交易。 真正的缺陷只有两条:粒度错了,以及没有出处。
里程碑是按交付物的(3 条视频各有独立进度),但提醒表有一条 「每个 engagement 最多一条活跃提醒」的唯一索引 coordinator.ts:564-569,而 isMilestoneActive() 查询不带交付物条件 milestone-reminders.ts:127-149。
后果:3 条视频的包,第 1 条发布后,第 2、3 条的催稿被直接掐掉。 这个不用等重构,是一个独立的小 PR。
成本问题和安全问题,不许互相抵账
我们把这份材料交给了三个独立评审(只给测量数据,不给结论)。其中一个被明确要求不许读代码库, 它的开场是整轮评审最有价值的一句话:
「实测数据里没有一分钱是被实体建模缺陷烧掉的。每个成本数字都能追到 prompt 构造。 实体建模的缺陷是安全与正确性问题,而且都不大。 把这两类混在一起,就会变成——用一个序列化器省下的钱,去支付一次重架构。」
| 问题 | 收益 | 风险 | 周期 | |
|---|---|---|---|---|
| 线 A 成本 |
prompt 构造、重新推导、重试阶梯、无闸门 | coordinator ~$250/周;同样的改法用在 ai.reply.* 上是 ~$2,281/周 | 低,多为加法、可回滚 | 天级 |
| 线 B 安全 |
合成身份、沙箱写生产、路由基数、无出处 | 不省钱;防止把错的价格发给错的人 | 中,但可做成纯加法 | 周级 |
线 A 先做,做完重新测量。 线 B 里真正紧急的只有两件——沙箱出站隔离、提醒基数 bug——其余等线 A 的数据再定。
最终落到 schema 上是 6 个新增对象 + 3 个可空列,全是加法, 可以挂在现有路由路径背后灰度。这不是一次重架构。
三条原则,其余都是推论
原则一 · 一条消息,一生只被 LLM 读一次
消息到达时,一次有界的抽取调用(新消息 + 当前事实卡 → 类型化 delta), 不给历史。此后这条消息的正文不再进入任何 prompt,除非人或 agent 显式引用它。
这不是压缩或检索策略,是结构上让单轮成本与对话长度无关。 「上下文不随聊天记录线性增长」这个要求,由此在构造上成立,而不是靠预算钳制。
「那个 296k token 的调用之所以存在,只是因为这个数字在当初很便宜就能记下来的时候被丢掉了。 60 次重试,是系统在为一次它拒绝执行的写入反复付钱。」
原则二 · 判定用代码,表达用模型
decide(state, trigger) → NoAction | Send(requirement) | Undetermined(q) | NeedsHuman(r)
decide 是纯函数,就是今天的 buildRequiredNextAction——它已经写好了。 改的是它的地位:从 prompt 里的一段建议,变成控制流本身。
| 返回 | 走向 | 今天的占比 | LLM 调用 |
|---|---|---|---|
| NoAction | 直接结束 | 72% 的运行 | 0 |
| Send | 直连 Writer,不经通用 agent | 40% 的消息运行 | 1 |
| Undetermined | 通用 agent —— 它唯一该待的地方 | 其余 60% | 2–3 |
| NeedsHuman | 升级给运营 | 少量 | 0 |
数据说清楚了:这是 40% 的优化,不是 100%。 谁说能把 agent 删光,谁就是在吹。
原则三 · 没有出处就不是事实
每一条会话性事实必须能定位到 (message_id, segment, char_start, char_end, span_sha256)。 非会话性事实(打款回调、目录数据)带它自己的来源事件——否则就是在伪造出处。
摘要永远不能成为事实,靠权限边界保证,不靠在 prompt 里写「不要编造」:
- 摘要不能提供证据引用
- 摘要不能更新投影、不能授权业务命令
- 任何基于摘要的检索,必须先把被引用的原文取回来再用
一条入站消息的完整链路
每一级都可能在花钱之前就终止
→ 右侧为该级的 LLM 成本三个模型角色,取代今天的十类调用
| 角色 | 输入 | 预算 | 工具 |
|---|---|---|---|
| Extractor | 新消息 + 当前事实卡 | 2.5k in / 300 out | 无 |
| Planner 仅 Undetermined | 有界上下文包 | 8k in / 800 out | 4–6 |
| Writer | requirement + 风格窗口 + 允许事实 | 3k in / 400 out | 无 |
删除:批评者(金额/语言/披露校验本来就是确定性的)· 语言检测 + 语言选择 + 翻译(3 次调用折进 Writer 契约)· 轮次分类 · 品牌意图分类 · 以及全部「重新推导」调用。
预期效果
| 今天 | vNext | |
|---|---|---|
| 正常一轮 | ~90k token / 6+ 次调用 | ~6k token / 2 次调用 |
| 案例 B 那类(营销自动回复) | 453 次调用 / $39.40 | 0 次调用 / $0 |
| 72% 的空转运行 | 每次仍构建 context | 0 次 LLM |
| 单轮成本随历史增长 | O(n) · 整段 O(n²) | O(1) |
我不打算把这写成「降本 15 倍」。诚实的说法是:正常路径大约一个数量级,病态路径接近全部消除, 而且 O(n²) 变成 O(n) 是结构性的,不会随时间劣化。
上下文预算 · 每一块都有主人
| # | 块 | 预算 | 负责方 |
|---|---|---|---|
| 1 | 系统策略(按 turn-type 拆,不是一份 30k 的巨石) | 600 | 平台 |
| 2 | 本轮合法的工具(3–6 个) | 700 | 能力代理 |
| 3 | 身份 + 触发器 | 200 | 路由 |
| 4 | 确定性状态卡 | 900 | 投影器 |
| 5 | 未决义务与待决问题 | 400 | obligation |
| 6 | 相关的带引用事实卡 | 1,200 | 账本 |
| 7 | 达人级持久偏好(白名单谓词) | 250 | 账本 |
| 8 | 最近 6–10 条有业务含义的消息(已清洗) | 2,000 | 消息表 |
| 9 | 检索到的原文证据(仅当 agent 主动要) | 1,200 | 检索 |
| 10 | 工具结果预留 | 800 | 循环 |
| 合计目标(硬上限 12k,maxOutputTokens 800) | 8,250 |
今天全包没有任何一处设 maxOutputTokens 代码审计 §A4。
数据模型 · 一张带出处的事实账本
今天状态分散在五种并存的持久化范式里:打分笔记表、价格/条款行表、 jsonb 数组、「时间线最新节点胜出」、以及重建式投影。事实账本一次性取代它们—— 这是简化,不是新增。
create table fact ( id uuid primary key, workspace_id uuid not null, -- 沙箱隔离靠这个,不靠 flag subject_type text not null, -- 'creator' | 'engagement' | 'deliverable' subject_id uuid not null, predicate text not null, -- 来自版本化注册表,不是模型自由生成 value jsonb not null, qualifiers jsonb not null, -- currency / channel / usage_rights / market status text not null, -- asserted | superseded | disputed | retracted source_kind text not null, -- message | system_event | operator source_ref uuid not null, -- ← 出处是必填,不是可选 supersedes uuid references fact(id) ); create unique index fact_current on fact (subject_type, subject_id, predicate) where status = 'asserted';
- 当前值 = status='asserted' 的那行。 更正 = 插新行 + 回指,旧行转 superseded。历史与审计免费。
- 冲突不靠置信度裁决。 两条互斥断言且无显式更正关系 → disputed,进人工队列。按置信度选一个是在制造静默错误。
- subject_type='creator' 的行跨活动可见;engagement / deliverable 的行结构上不可能污染兄弟活动。
「campaign 是唯一容器」这件事怎么破
今天沙箱、fork、临时对话、实验全靠伪造身份挤进 campaign 命名空间 (custom:<uuid>、fork:<origin>[n])。 正确做法是把挤在「哪个 campaign」里的三个正交维度拆开:
| 维度 | 字段 | 含义 |
|---|---|---|
| 出资 | campaign_id 可空 | 有 → 走预算校验 + 外部投影;无 → 临时,不投影 |
| 分组 | program_id 必填 | 创建时不需要碰遗留 Java 服务。kind ∈ {campaign, oneoff, adhoc, experiment, reengagement} |
| 环境 | env ∈ {live, sim} | creator / channel / deal / message 上都是真实列,在发送适配器强制 |
运营能不能在不碰遗留 Java 服务的情况下,创建一个 program?
能 → 没人会再去伪造 campaign。不能 → 你只是用新名词重建了同一个陷阱。
之后长什么样
- 一个聊天收件箱 + 一排里程碑勾选框
- 「在等谁」是从位掩码推断出来的,运营看不到到底卡在哪
- 路由不出来的消息静默重排队 24 小时——达人这期间会凉
- 达人说「我同意了独家条款」在哪儿?翻聊天记录
- 3 条视频的包只有一条提醒线,第 1 条发布后另外两条静默停摆
- 一块球权看板:每个交付物一条泳道
- 每条泳道随时显示:球在谁手上 · 缺的到底是什么 · 卡了多久
- 路由歧义立刻浮到运营面前,一次点击绑定
- 每张事实卡可点开到原文那一句——争议当场结束
- 交付物各自独立推进,互不掐断
球权看板
关键差别不是好看,是「缺的到底是什么」被写成了一个字段,而不是要人去猜。 催办因此可以指名道姓,而不是「有进展吗?」
agent 提议,人确认 —— 但只在该确认的地方
decide() 返回 Send 的 40% 直接走,不打扰人。 只有 Undetermined 和 NeedsHuman 才会出现在运营面前, 并且带着它的依据:
系统不猜。可以选一个,或者——
今天的系统拒绝猜,同时也拒绝问。对方是唯一确切知道答案的人, 发一句「想确认下你说的是哪个合作?」是完全正常的人类行为。这一级在证据阶梯里本来就该有。
签完之后,怎么保证片子快速做出来
线上有 8,586 个进行中的交付合作。签约到成片的链路是一串交接: 选品 → 寄样 → 脚本 → 改稿 → 拍摄 → 改片 → 发布 → 打款。 每一环的延迟都来自「不知道在等谁、等什么」。
把「等待」变成一个有主人、有缺口、有时限的对象
create table obligation ( deliverable_id uuid, -- ← 按交付物,所以 3 条视频并行 kind text, -- await_script_draft | await_our_review | await_publish … owner text, -- creator | us | brand ← 球权,最关键的字段 blocked_on text, -- 能让它完成的那一条 fact 谓词 due_at, sla_hours, cadence );
四个加速器
一 · 球权永远唯一且可见
任何时刻,每个交付物有恰好一个 owner 和一个具名的缺口。 今天「在等谁」是从位掩码推断的,推断不出来就只能发一封「有进展吗」。 有了 owner 字段,球在我们这边的时候会自动升级到内部,而不是去催达人—— 上面的看板里视频 2 就是这种情况,而这在今天是完全隐形的。
二 · 按交付物并行,不再互相掐断
这是前面那条线上正在发生的 bug 的正解:提醒挂在 (engagement_id, deliverable_id, kind) 上, 第 1 条视频发布不会再让第 2、3 条的催稿静默消失。
三 · 催办指名道姓,而不是按节奏骚扰
因为 blocked_on 知道缺的是哪一条事实,催办可以写成:
「Hi Laura, 想跟进一下视频进度,有更新吗?」
「Hi Laura, 视频 3 就差 7/12 提的那个 30 秒开场钩子改写了,其余都通过了。改完这一处就能进拍摄。」
四 · 素材是有版本的对象,不是聊天里的一条链接
这是那份「充足沙箱与对象存储」预算真正该花的地方—— 按交付物建存储前缀,而不是按达人:
artifact(deliverable_id, kind, version, uri, review_state, source_message_id)
kind ∈ { brief, script, cut, final, publish_proof }
最大的一类返工是「我们说的是哪一版」。脚本 v1/v2/v3 各自带审阅状态之后, 「已改的地方不会被重复要求,没改的地方不会被漏掉」。 再配合事实账本的引用能力,系统不会再去要一个达人已经给过的东西—— 而今天它会,因为它每轮都在重新推导,推导失败就再问一遍。
达人不是有效的隔离边界——19% 的达人同时在多个活动上, 按达人分容器等于把跨活动污染做成基础设施。而且容器解决不了任何一个实测问题: 成因是重新推导、重试阶梯、缺入库规范化,没有一条是「缺算力」。
这份预算我建议这么花:① env 做成真实列并在发送适配器强制, 让 sim 合作只能解析到沙箱地址(今天沙箱直接写生产表,唯一防线是一句 assertDevelopment()); ② 让容器「扮演达人」接收 sim 出站,做端到端演练; ③ 合同生成、媒体处理、脚本 diff 这类明确作业。 不要用在谈判轮次上。
迁移路径 · 阶段 0 今晚就能发
| 阶段 | 线 | 内容 | 形态 |
|---|---|---|---|
| 0a 今晚 | A | 序列化器。 给 LLM 之前剥掉:追踪 URL 参数、null 字段、界面已读状态、邮件填充符、重复引用链。 零 schema 变更、零迁移、对 33k 进行中谈判零风险。 | 1 个 PR |
| 0b 本周 | A | 重试策略分类(校验失败 ≠ 可重试)· 每轮调用预算硬上限 · 全局 maxOutputTokens · 工具裁剪成 3–4 个稳定 bundle · 接上 run 级 token 记录 | 4 个 PR |
| 0c 本周 | B | 沙箱出站隔离 · 提醒基数 bug | 2 个 PR |
| ⟵ 停下来重新测量。 如果干净的 prompt 之下实体模型仍然疼,那时你有证据,而且要改的系统更小了。 | |||
| 1 | A/B | 入库规范化 + 群发闸门 + 原始报文进对象存储。只做这一件,案例 B 类事故归零 | 加法 |
| 2 | B | thread + message_binding,与现有逻辑影子比对 | 加法 |
| 3 | A | fact + 到达即抽取。用现有重新推导路径当正确性预言机,一致后关掉它 | 需比对期 |
| 4 | A | decide() 提升为控制流,拿下 72% 空转 | 加法 |
| 5 | B | campaign_id 改可空 + program_id + env;合成身份退役 | 加法 |
| 6 | B | obligation 取代里程碑提醒;里程碑改只读投影,agent 停写 | 比对期 |
如果只做一件事,做 0a。 如果只做一周,做完 0a/0b/0c 然后重新测量,再回来看后面。
我可能错在哪
这一节是被独立评审逼出来的,我认为它比设计本身更重要。
案例 A 的 token 数我第一次量错了——把 lens 外层的 originalBytes: 103,930 / truncated: false 当成了 prompt 全长。 是独立评审质疑「98,416 字符不可能等于 296,224 token」,回查嵌套层的 requestMessages.originalBytes = 635,216 / truncated: true 才发现。 真实 prompt 是 635 KB。结论比原来更强,但过程说明: 任何来自可观测系统的数字,先确认它的截断与采样语义。 (案例 C 的 40 条样本已复核,12/12 未截断。)
- 「prompt 越大错误率越高」我说得太满。 38k 那一档的错误率反而低于 26k 档。上线前需按 model / purpose / 错误码 / 首次-重试 拆开重测。
- 一个病态合作证明了「可能发生」,没证明「普遍发生」。 必须测:多大比例的上下文里含有外来活动、自动化流量、零条真实达人消息。
- 过滤器的召回率是真风险。 traffic_class 会不会误杀转发、经纪人代收、带真实排期的自动回复、用群发平台发信的达人?必须有隔离区 + 召回率指标,不能直接丢。
- 编译式上下文会漏细节。 一句很久以前的让步可能进不了本轮上下文,导致「自信地答错」。缓解靠引用可追溯 + 运营可查看「本轮省略了什么」,而不是退回全量倾倒。
- 类型化状态机会拒绝合法但新颖的商业安排,运营可能为了推进流程而乱填状态。必须留显式的人工裁决出口。
- 33,169 个「进行中的谈判」里有多少是僵尸? 状态模型过载可能在虚报在办量。
完整设计:Research/coordinator-vnext/04-vnext-architecture.md
线上实测 E1–E12:evidence/prod-token-economics.md ·
源码审计 A–D:evidence/code-audit-facts.md
三份独立评审(只给数据、不给结论)+ 各自提问:evidence/consultations/
我读评审前写下的独立判断(留档对撞用):evidence/my-position-before-consultation.md