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 写定义文件的硬约束(这几条不是建议)
- ✅
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 字符仍然自足(职能说清楚), 新增内容往后放;改动后应当有一条检查盯住这个窗口。 - ✅ 这四个文件里有三个是 git 跟踪的定义(
agent.md/soul.md/SKILL.md), 不是运行态。dream 改它们 = 改仓库里的文件 → 会被边界守卫看见,也会进git status。 → 需要明确:dream 的产出是直接落盘还是提案 + 人工合并。 - ✅ 毕业闸门读这些文件(
missingBodyDefinition/promoteDraftIfComplete)。 把agent.md写空 / 写坏会让本体退回「定义没填满」,雇不进任何公司。 - ✅ 必须可回滚且能分别回滚:经验沉淀写坏了只影响上下文,
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、3、4、5 全部实现。未做的是:定时挂载(决策 C)、 写定义文件(
SKILL.md/soul.md/agent.md)、以及一次真 CLI 验收 —— 见 §5.4。
用户定的是两件一起做,不分先后。拆成可验收的步骤:
| 步 | 内容 | 为什么不能省 |
|---|---|---|
| 1 | ✅ appendToMemoryMd 停止截断搬运(§1.1)—— 已完成 |
它原来把工具调用 id 当「关键事实」写进 memory.md 并注入每一轮提示词。dream 直接消费它 = 把这个错误放大到跨公司 |
| 2 | ✅ 「有哪些公司」改查 company_agents 表(§1.3)—— 已完成 |
目录里混着 train/ workspace/ 和带时间戳的测试残留,拿目录枚举公司必然把它们当原料 |
| 3 | ✅ dream v0:枚举 → 提炼 agent → 去敏判定 agent(决策 A)→ 写落点 —— 已完成 | 主体 |
| 4 | ✅ 定义文件的 git 历史闸门(决策 B.3)—— 已完成 | 覆盖一份尚未进入 git 历史的内容 = 让上一版永久消失,且不报错 |
| 5 | ✅ 可追溯:每条沉淀记住它是从哪几条记录提炼来的 —— 已完成 | 没有它,dream 的产出无法验证对错 —— 与 FDE 那边「LLM 自己汇报分数」是同一个问题 |
空转保护(决策 C 的直接要求):没有新原料就不起 agent 调用。
本文档仍不是实现规格。决策 D(质量怎么评)未定;B.2 里「定义文件直接写还是走提案」 也还需要你点头 —— 我按「走提案」写在了步骤 4,但那是我的建议,不是你拍的板。
五、实现记录
5.1 步骤 1:memory.md 的自动写入不再冒充「关键事实」(已完成)
改法三条:
- 换段:自动写入落到
## 原始记录(未提炼),## 关键事实从此只由提炼产物填。 读的人和 agent 都能一眼分清「这是日志」还是「这是结论」;这一段同时就是 dream 的输入面。 - 滤掉确定的噪声:工具调用标记 / 空结果 / 只有心跳标记的任务。 判据刻意窄 —— 只滤明确的噪声,不猜「哪条重要」。猜重要性需要理解内容, 那正是 dream 的活,在这里做等于把提炼下放到一个正则里。
- 不再截成半句:上限从 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:两个本体 + 默认实现(已完成)
两个本体(都过得了毕业闸门,missingBodyDefinition 为空):
| 本体 | 职责 | 产出契约 |
|---|---|---|
dream-distiller |
把一批原始记录提炼成可复用经验 | ``json 数组,每条{title, body, sources};没有值得留的就交[]` |
dream-screener |
逐条判「这条能不能给别家公司看」 | ``json 对象{shareable: boolean, reason}` |
决策 A 的独立性写在两处,缺一不可:
- 代码层:判定一条一次调用,提示词里只放
title+body,不放原料; - 本体层:
dream-screener/agent.md明写「你看不到原料,也看不到是哪家公司——这是故意的」。 守卫里有一条专门断言这句话在(expect(screener).toContain('看不到原料'))—— 否则下一个人会顺手把原料塞进提示词「让它判得更准」,而那恰好把独立性拆掉了。
两处失效方向都是硬的:
- 提炼取不出 JSON → 抛,不是静默返回空。「产出格式坏了」和「没提炼出东西」是两件事, 混为一谈会让前者以后者的形式永远藏下去(每天显示「今天没经验可沉淀」,真相是解析一直在失败)。
- 判定解析不了 /
shareable不是布尔 → 判否,并把原文片段带进reason, 让人看得出是「判否」还是「判炸」。
测试写法上纠了自己一次:第一版把解析与失效分支在测试里重抄了一遍再断言 —— 那是在验一份复制品,代码改了它照样绿。改成加一条
setDreamAgentCaller缝, 只换掉「真起 CLI」那一层,解析、判空、失效方向全走真代码。本体写错过一次,被守卫抓住:
agent.md里的相对引用按 cwd 深度 3 ({aid}/{cid}/{taskId}/)解析,不是按文件自身位置 —— 仓库根是../../../../../。 我按文件位置写了../../../,agent-md-refs.test.js当场判红。
5.4 还没做的(别当成 dream 上线了)
- 定时挂载没做(决策 C 说每日一次)。现在只有
dreamOnce(agentId)这个可调用的单元, 没有挂到任何调度上,也没有 REST / 控制台入口。空转保护已经在,挂之前先想清楚 「每天对哪些本体跑」——全量 76 个本体逐个跑,即使空转也要 76 次目录读。 - 还没写定义文件。
writeDistilled目前只写本体根的memory/experience.md(纯经验积累,删掉无副作用)。SKILL.md/soul.md/agent.md三个 git 跟踪的定义 留给下一步;definitionWritability()已经能报出「这三份现在能不能写、不能写是为什么」, 闸门(§B.3)也已就位,缺的是「往里写什么、写成什么形状」这半边设计。 - 没有一次真 CLI 验收。CI 里换掉的是
callDreamAgent那一层, 验的是「回复 → 结构化产物」的解析与失效方向,不是模型写得好不好。 两个本体的提示词至今没有被任何模型读过 —— 这正是 P3B W4 记过的形态 (mock 从不读提示词,所以提示词在第一次真跑前从未被检验)。 - 决策 D(提炼质量怎么评)仍未定。可追溯(
sources)是它的便宜起步,不是替代品。