29-dream-design.md 18.2 KB

29-DREAM · 经验汇流(dream)—— 设计草案

所属: 28 Agent 注册表统一 §5.6b 锁住的那条约束的下游设计;未确认项来源 28-followups §1.1。 日期: 2026-08-20 状态: 📝 四条取舍已由用户拍板(2026-08-20,见 §三),实现未开始。 标 ✅ 的是已生效的约束或有实测证据的事实;标 🔒 的是本轮拍板的决定。


〇、一句话

dream 是设想中定时跑的一套逻辑:整理所有 agent 在所有公司的 history 与 memory, 显式提炼后沉淀到 skill memory 上,让经验跨公司汇流,而不泄漏任何一家公司的具体信息。


一、开工前先看清原料:它现在是脏的

这一节全是实查 2026-08-20 的仓库状态得到的,不是推测。 结论很直接:在原料变干净之前,做提炼是没有意义的——垃圾进,垃圾出。

1.1 现在写进 memory 的不是「事实」,是截断的日志行

memory-service.appendToMemoryMd() 的全部逻辑是:

const taskOneLine   = (task   || '').split('\n')[0].substring(0, 80);
const resultOneLine = (result || '').split('\n')[0].substring(0, 100);
const entry = `- [${timestamp}] ${taskOneLine} → ${resultOneLine}`;

取第一行、砍到 80/100 字符。这不是提炼,是搬运 + 截断。 cto/_global/memory/memory.md 的实际内容:

## 关键事实
- [2026-08-20] [TASK from ceo] → ..._global/` 下不新增 `callback-*` 目录。反向验证过:临时把 `isTestEnv` 改成 `() => false`,该用例 FAIL。
- [2026-08-20] [SYSTEM HEARTBEAT] → 🔧 [TOOL_CALL] Bash (ID: toolu_01RsQ3gh5niCFyUZMFZj2hxF)
- [2026-08-20] [SYSTEM HEARTBEAT] → 🔧 [TOOL_CALL] Bash (ID: toolu_01MG6RqDgqcRCi5ageGk84Kt)

后三条是工具调用 id。它们作为「关键事实」被写下来、被注入下一轮提示词、 并且会成为 dream 的原料。第一条更糟:它是一句话的中段(开头被截掉了), 读起来像事实,其实缺主语。

§5.6b 说的错误版本是「每轮对话自动把原文写进共享 SKILL.md」。 现在这套已经是那个错误版本的单公司版了 —— 只是还没跨公司共享,所以还没泄漏。 dream 如果直接消费它,等于把这个错误放大到跨公司。

1.2 长期记忆是一个 3000 字符的滚动窗口

MAX_MEMORY_MD_SIZE = 3000。超了就截。所以「长期记忆」实际是最近若干条截断日志, 没有任何按重要性保留的机制。

1.3 「公司态目录」里有不是公司的东西

system/agents/{aid}/{cid}/ 是约定,不是校验。实查 96 个二级目录里混着:

形态 例子 是什么
真公司 e2e-demo / mycompany / idearun ✅ dream 的原料
保留段 _global 全局 agent 的落点,算不算「一家公司」需要定 ❓
ArenaSK 时代的运行态 train/ workspace/ ❌ 不是公司,是训练 / 工作目录
测试残留 tmp-w7-bootstrap-1786986291739-9n2xv、qroute-co-13/14、zz-* ❌ 夹具,且带时间戳,会无限增长

dream 的第一步「枚举所有公司」现在没有可靠答案。 可靠的答案在库里:company_agents 表(28 批次 4 起「这家公司雇了谁」的唯一真相源)。 目录只能用来定位文件,不能用来枚举公司。

1.4 绝大多数 memory 是空模板

75 份 memory.md 里,只有 7 份有非模板内容(innovator-evolver / master / org-innovator / innovator-digest / innovator-hunt / coo / qa-judge), 其余全是 _(刚入职,尚无记忆)_。

这条对设计有直接影响:dream 现在跑起来几乎没有原料。 它的价值随「真跑的量」增长,不是上线即有收益的东西。


二、已经生效的约束(不随设计变化)

✅ 公司态目录是 dream 的唯一原料,不许被任何清理策略当垃圾扫掉(§5.6b)。 任务态({aid}/{cid}/{taskId}/)可清,公司态({aid}/{cid}/memory/ 与 conversations/)不可清。

✅ 沉淀必须是提炼后的抽象,不是自动搬运原文。 错误版本真的发生过(28 号第一轮验收挖出的最严重 blocker):直接灌原始对话 = 把 A 公司的机密喂给 B 公司。

✅ 目录布局本身就是 dream 的主查询索引: 某个 agent 在所有公司的全部记忆 == glob: system/agents/{aid}/*/memory/,一个 glob 就够。 (前提是 1.3 那个「二级目录不都是公司」的问题被解决。)


三、需要定的取舍

🔒 决策 A —— 去敏的判据:独立判定 agent + 写作要求(2026-08-20 拍板)

定案 = A3 + A1 组合:提炼 agent 按 A1 的写作要求产出,产出后由另一个只负责判 「这段话能不能给别家公司看」的 agent 过闸门,判不准就拒绝沉淀。

理由(也是本仓库反复验证过的一条):两种错的代价不对称时,判据要偏向贵的那一侧 (P3B §八 关于 FAILING_VERDICTS 的那段)。「少沉淀一条经验」与「泄漏一次机密」显然不对称。 失效方向因此是「少沉淀」,不是「泄漏」。

下面三种形态的对比留档,说明为什么选它。

决策 A 的备选(留档)

这是安全边界,也是整个设计里唯一「做错了会造成真实损害」的一条。三种可选形态:

方案 判据 代价
A1 白名单式:只允许提炼成「不含任何专有名词的方法论」 产出里不许出现公司 id、人名、产品名、URL、文件路径 最安全;但也最容易把有用的经验一起滤掉(「给 X 公司做官网时发现静态站点要先定验收脚本」→ 只剩「要先定验收脚本」)
A2 黑名单式:过滤已知敏感模式后放行 正则 / LLM 扫一遍公司 id、密钥形态、路径 保留信息量更多;漏一个模式就是一次泄漏,而且是静默的
A3 二次判定:提炼 agent 产出后,由另一个 agent 只负责判「这段话能不能给别家公司看」 判定是一次独立的 agent 调用,双方不共享上下文 成本翻倍;但与本仓库既有的 validator / triager 模式同构,且失效方向可控(判不准就拒绝沉淀,宁可少沉淀)

🔒 决策 B —— 沉淀落点:四个都要,按「改动的可逆性」分层(2026-08-20 拍板)

用户定的是:system/agents/{aid}/memory/ 和 SKILL.md 都要写, 必要时还要迭代 soul.md / agent.md。

这比原来三选一的范围大得多,因为它引入了「agent 改写自己的定义」这件事。 下面这一节是这个决定的直接后果,不是可选的补充说明。

B.1 四个落点不是一回事,风险差三个数量级

落点 是什么 谁受影响 可逆性
{aid}/memory/(本体根) 经验积累,跨公司那一份 只影响这个 agent 读到的上下文 删掉即可,无副作用
SKILL.md 能力定义:这个 agent 会什么 同上,但会进 Skill 市场的语义 覆盖前留旧版即可回滚
soul.md 人格:它怎么说话、怎么取舍 所有雇了这个本体的公司 改坏了表现为「行为漂了」,不报错
agent.md 行为契约:它必须做什么、禁止做什么 所有雇了这个本体的公司 改坏了可能让整条链路静默失效

28 号的核心裁决是「本体只有一份」 —— 所以写 agent.md / soul.md 的影响面是 所有雇了它的公司,而不是提炼出这条经验的那一家。这恰恰是 §5.6b 担心的那种扩散, 只不过现在是有意为之。因此它必须比前两个落点多两道东西:

B.2 写定义文件的硬约束(这几条不是建议)

  1. ✅ agent.md 有一处 2000 字符的截断,但作用点要说准: graph-engine.js:915 给规划 agent 看的「角色定义摘录」只取 agent.md 的前 2000 字符(readFileSync(f).slice(0, 2000)), 超出部分静默丢弃。agent 自己那条装配链(prompt-builder)不截断,读全文。 → 后果是:dream 往 agent.md 末尾追加内容,规划侧读到的画像不会变; 但如果追加把重要约束顶到 2000 之后(例如重写时调整了顺序),规划侧会无声地少看到那一段。 → 判据:写 agent.md 时保证前 2000 字符仍然自足(职能说清楚), 新增内容往后放;改动后应当有一条检查盯住这个窗口。
  2. ✅ 这四个文件里有三个是 git 跟踪的定义(agent.md / soul.md / SKILL.md), 不是运行态。dream 改它们 = 改仓库里的文件 → 会被边界守卫看见,也会进 git status。 → 需要明确:dream 的产出是直接落盘还是提案 + 人工合并。
  3. ✅ 毕业闸门读这些文件(missingBodyDefinition / promoteDraftIfComplete)。 把 agent.md 写空 / 写坏会让本体退回「定义没填满」,雇不进任何公司。
  4. ✅ 必须可回滚且能分别回滚:经验沉淀写坏了只影响上下文,agent.md 写坏了影响所有公司。 两者混在同一次提交里,回滚粒度就没了。

B.3 🔒 定案:直接落盘,但写之前必须确认这份文件已经在 git 历史里(2026-08-20 拍板)

用户定的是直接写,不走提案;前置条件是「别导致直接修改而没有 git 历史」。

已实现:server/services/definition-write-gate.js,两条判据都满足才放行: ① 文件被 git 跟踪;② 该文件没有未提交改动。判不出来(不在仓库 / git 不可用)一律拒绝。

为什么是「拒绝并报告」,不是「脏就先替它提交一把」: 后者看着更顺手,但那正是本仓库记过的一类事故 —— 多会话共用同一个 worktree (27-P3C §15.4 / §17.2)。一份未提交的改动很可能是另一个会话正在写的东西, 替它提交等于把别人的半成品写进 main,还署错了名。所以把「要不要提交」留给人。

守卫 definition-write-gate.test.js 5 例,含「拒绝时必须给出下一步」那条 —— 撞上闸门的人(或 agent)要能照着做,否则只能猜。

决策 B 的备选(留档)

「skill memory」目前没有对应的实体。候选落点:

落点 形态 问题
B1 system/agents/{aid}/memory/(本体根,跨公司那一份) 与公司态并列,一个 agent 一份跨公司经验 目前只有 zz-scaffold-readonly-fresh 有这个目录,等于新建一层;但它天然与 §5.6b 的 glob 对齐,读的时候本体根 + 各公司态一起加载
B2 本体的 SKILL.md 复用现有概念(5 个本体已有 SKILL.md) 这正是 §5.6b 点名的错误版本的落点;SKILL.md 是「能力定义」,不是「经验积累」,混在一起之后没法分别回滚
B3 <repo>/../data/skills/ 那 27,059 条 Skill 仓库 与 ArenaSK 的 Skill 市场打通 那是面向分发的仓库,写进去等于对外发布;且跨了两个 data 根

(拍板结果见上:B1 与 B2 都要,另加 soul.md / agent.md 两个定义落点。)

🔒 决策 C —— 触发时机:定时,每日一次(2026-08-20 拍板)

备选留档如下。

⚠️ 与 §1.4 的冲突要知道:实查 75 份 memory.md 里只有 7 份有非模板内容, 所以每日跑起来绝大多数 agent 会是空转。这不是反对定时,是要求实现时做到: 没有新原料就不起 agent 调用(判据:上次 dream 之后有没有新的 session 摘要), 否则每天会有几十次「读完发现没东西」的 LLM 调用在烧钱,而且日志里全是噪声。

方案 说明
C1 定时(如每日一次) 简单;但没有真跑的日子会空转,有大量真跑的日子又攒得太多
C2 按量(某 agent 新增 N 条 session 摘要后触发) 与实际产出量对齐;需要一个计数落点
C3 手动(控制台一个按钮 + API) 先手动跑一段时间看提炼质量,稳定了再自动

❓ 决策 D —— 提炼质量怎么评 / 失败怎么办

这一条可以先不定,但要知道它没定:

  • 没有评价手段时,dream 的产出无法验证对错——它和 FDE 那边 「LLM 自己汇报分数」的问题是同一个(见 architecture_todo_list §1)。
  • 一个便宜的起步:dream 每次产出都保留原料指针(哪几条 session 提炼出这一条), 人抽查时能顺着回去看。可追溯比「可评分」便宜得多,也更早有用。

四、推进顺序 🔒 上游与 dream 一起做(2026-08-20 拍板)

进度:步骤 1、2、4、5 已实现;步骤 3 只落了骨架 + 注入缝, 两次 agent 调用(提炼 / 去敏判定)的默认实现尚未接线 —— 见 §5.3。

用户定的是两件一起做,不分先后。拆成可验收的步骤:

步 内容 为什么不能省
1 ✅ appendToMemoryMd 停止截断搬运(§1.1)—— 已完成 它原来把工具调用 id 当「关键事实」写进 memory.md 并注入每一轮提示词。dream 直接消费它 = 把这个错误放大到跨公司
2 ✅ 「有哪些公司」改查 company_agents 表(§1.3)—— 已完成 目录里混着 train/ workspace/ 和带时间戳的测试残留,拿目录枚举公司必然把它们当原料
3 🟡 dream v0:枚举 → 提炼 agent → 去敏判定 agent(决策 A)→ 写落点 —— 骨架已落,两次 agent 调用未接线 主体
4 ✅ 定义文件的 git 历史闸门(决策 B.3)—— 已完成 覆盖一份尚未进入 git 历史的内容 = 让上一版永久消失,且不报错
5 ✅ 可追溯:每条沉淀记住它是从哪几条记录提炼来的 —— 已完成 没有它,dream 的产出无法验证对错 —— 与 FDE 那边「LLM 自己汇报分数」是同一个问题

空转保护(决策 C 的直接要求):没有新原料就不起 agent 调用。

本文档仍不是实现规格。决策 D(质量怎么评)未定;B.2 里「定义文件直接写还是走提案」 也还需要你点头 —— 我按「走提案」写在了步骤 4,但那是我的建议,不是你拍的板。


五、实现记录

5.1 步骤 1:memory.md 的自动写入不再冒充「关键事实」(已完成)

改法三条:

  1. 换段:自动写入落到 ## 原始记录(未提炼),## 关键事实 从此只由提炼产物填。 读的人和 agent 都能一眼分清「这是日志」还是「这是结论」;这一段同时就是 dream 的输入面。
  2. 滤掉确定的噪声:工具调用标记 / 空结果 / 只有心跳标记的任务。 判据刻意窄 —— 只滤明确的噪声,不猜「哪条重要」。猜重要性需要理解内容, 那正是 dream 的活,在这里做等于把提炼下放到一个正则里。
  3. 不再截成半句:上限从 80/100 放宽到 200 并取完整第一行。 截断残渣读起来像事实、其实缺主语,比不留更坏。

踩到一条值得记的:噪声判据初版有「任务与结果都短于 8 个字符就丢」。 那是按英文长度想出来的阈值 —— 中文 8 个字是一句完整的话, 实测「结果甲甲甲甲甲」(7 字)直接被丢,整条记录凭空消失。已删掉该规则。 凡是拿长度当信息量代理的判据,都要先问一句「这是按哪种文字算的」。

守卫 memory-raw-section.test.js 5 例:关键事实段一个字不动、噪声一条不写、 长句不再被砍(带「样本确实超过旧上限」的前置断言)、多次写入累加在同一段、 无 toolCall 不写(保住原有行为,别顺手放宽)。

5.2 步骤 2 / 5 与 3 的骨架(dream-service.js)

步骤 2 —— 公司清单只认 company_agents 表。 companiesOf(agentId) 查表,另把保留段 _global 补上(全局 agent 的运行态落在那里, 没人「雇」它,所以不在表里)。目录从此只用来定位文件,不用来枚举公司。 守卫里造了两个「看起来像公司」的目录(train/ 与 tmp-w7-bootstrap-1786986291739-9n2xv), 断言它们不进原料 —— 那正是实查到的两种残留形态。

空转保护 —— 水位线按「条数」而不是 mtime。 mtime 会被任何一次无关写入推动(例如别处重写整份 memory.md),条数只在真的多出记录时增长。 拿 mtime 当判据的话,空转保护会在「文件被碰过但内容没变」时失效。 没有新原料时 dreamOnce 直接返回 skipped: 'no-new-material',一次 agent 调用都不起。

步骤 5 —— 可追溯。 每条原料带 source({公司}:memory.md#{行号}),提炼产物落盘时把 sources 一并写进 experience.md 的引用行。人抽查时能顺着回去看。

决策 A 的失效方向在代码里是硬的:判定 agent 抛异常也算拒绝, 并把「为什么」写进 rejected[] —— 否则现场只看到「少了一条」,查不出是判否还是判炸。

5.3 步骤 3 还差什么(别当成做完了)

setDistiller / setScreener 两个注入缝已经在,dreamOnce 的编排、空转保护、 去敏闸门、落盘、可追溯都跑通了(6 例守卫)。缺的是两次真 agent 调用的默认实现:

  • 提炼 agent 与去敏判定 agent 的本体还不存在(需要两份 agent.md + soul.md);
  • 默认实现现在是一句显式的 throw(「尚未接线」),不是静默返回空 —— 静默返回空会让调用方以为「跑了但没提炼出东西」,那是本仓库反复出现的失效形态。

还没写定义文件:writeDistilled 目前只写本体根的 memory/experience.md (纯经验积累,删掉无副作用)。SKILL.md / soul.md / agent.md 三个 git 跟踪的定义 留给下一步,definitionWritability() 已经能报出「这三份现在能不能写、不能写是为什么」。