Commit 1d20af1b01826829ac2799892d5f4f7231ede794
1 parent
abd65859
feat(dream): 第一次真 CLI 跑通 + 存量记忆搬家脚本
## 前置:存量记忆的搬家(19 文件 / 269 行)
写入侧上一轮改了,但**存量不会自己搬家**。实查现有 `## 关键事实` 里
**没有一条真事实** —— 全是重复心跳行、工具调用 id、和以 `...` 开头的半句。
而 memory.md 会被注入**每一轮**提示词,这些东西一直在冒充事实。
scripts/migrate-memory-raw-section.js 把它们搬进「原始记录(未提炼)」。
先 dry-run 再 --apply,改前逐个 .bak(这些是 gitignore 的运行态,没有 git 历史兜底)。
判据刻意窄:只搬 `- [YYYY-MM-DD] … → …` 这一种形状(旧自动追加的唯一形状)。
**搬错比留错更坏** —— 留下最多多一条噪声,搬错是把人写的东西降级成日志。
## 真跑:innovator-evolver,40 条原料
{ "distilled": 4, "accepted": 4, "rejected": [] } 耗时 114s
四条产出都做到了抽象:yt-dlp / Invidious → 「本地下载工具 → 备用镜像源」;
scorer.js / SKILL_v1.md / system/templates/... 一个都没出现。
**顺着 sources 抽查过**(可追溯的第一次实际用途):4 条引用全部对得上原料。
「按关键词覆盖率打分会被刷到虚高分」那条引了 #21/#22/#27/#28,而 #27 原文正是
「从零重写,采用 expected_output 逐 case rubric + dimCeil 上限机制,杜绝 coverage-based」。
**它是跨 4 条记录归纳出来的**,不是逐条改写 —— 正是「一次调用吃掉全部原料」
那个选择要换来的东西。
## 闸门单独验过:零拒绝 ≠ 闸门有效
真跑里 4 条全过,说明去敏闸门**一次都没触发**。所以单独喂了三条给 dream-screener 真跑:
· 含公司名+域名+绝对路径 → false,理由点名「域名 lemonai.ai 和绝对路径 /Users/…」
· 心跳日志原文摘录 → false,理由点名「原文搬运」「工具调用 ID 可关联到具体运行实例」
· 干净的抽象经验 → true
三条判据都按 agent.md 写的那样生效,**且拒绝理由都点到了具体是哪几个字** ——
那是 agent.md 里明确要求的。
## 空转保护实跑
第二次调用(原料没变):{"skipped":"no-new-material"},一次 agent 调用都没起。
## 还没做的
定时挂载(决策 C)、写定义文件(SKILL.md / soul.md / agent.md)、
决策 D(质量怎么评 —— §5.5 那次抽查是**人工**顺着 sources 回查的,不是自动的)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
2 changed files
with
161 additions
and
7 deletions
docs/todo/system-design/29-dream-design.md
| ... | ... | @@ -210,8 +210,8 @@ const entry = `- [${timestamp}] ${taskOneLine} → ${resultOneLine}`; |
| 210 | 210 | |
| 211 | 211 | ## 四、推进顺序 🔒 **上游与 dream 一起做**(2026-08-20 拍板) |
| 212 | 212 | |
| 213 | -> **进度**:步骤 1、2、3、4、5 全部实现。**未做的是:定时挂载(决策 C)、 | |
| 214 | -> 写定义文件(`SKILL.md` / `soul.md` / `agent.md`)、以及一次真 CLI 验收** —— 见 §5.4。 | |
| 213 | +> **进度**:步骤 1、2、3、4、5 全部实现,**并已真 CLI 跑通一次**(§5.5)。 | |
| 214 | +> 未做的是:定时挂载(决策 C)、写定义文件(`SKILL.md` / `soul.md` / `agent.md`)—— 见 §5.4。 | |
| 215 | 215 | |
| 216 | 216 | 用户定的是两件一起做,不分先后。拆成可验收的步骤: |
| 217 | 217 | |
| ... | ... | @@ -311,8 +311,64 @@ mtime 会被任何一次无关写入推动(例如别处重写整份 `memory.md |
| 311 | 311 | (纯经验积累,删掉无副作用)。`SKILL.md` / `soul.md` / `agent.md` 三个 git 跟踪的定义 |
| 312 | 312 | 留给下一步;`definitionWritability()` 已经能报出「这三份现在能不能写、不能写是为什么」, |
| 313 | 313 | 闸门(§B.3)也已就位,缺的是「往里写什么、写成什么形状」这半边设计。 |
| 314 | -3. **没有一次真 CLI 验收**。CI 里换掉的是 `callDreamAgent` 那一层, | |
| 315 | - 验的是「回复 → 结构化产物」的解析与失效方向,**不是模型写得好不好**。 | |
| 316 | - 两个本体的提示词至今**没有被任何模型读过** —— 这正是 P3B W4 记过的形态 | |
| 317 | - (mock 从不读提示词,所以提示词在第一次真跑前从未被检验)。 | |
| 318 | -4. **决策 D(提炼质量怎么评)仍未定**。可追溯(`sources`)是它的便宜起步,不是替代品。 | |
| 314 | +3. **决策 D(提炼质量怎么评)仍未定**。可追溯(`sources`)是它的便宜起步,不是替代品。 | |
| 315 | + §5.5 那次抽查是**人工**顺着 sources 回查的,不是自动的。 | |
| 316 | + | |
| 317 | +--- | |
| 318 | + | |
| 319 | +## 5.5 第一次真 CLI 跑通(2026-08-20) | |
| 320 | + | |
| 321 | +### 前置:存量记忆的搬家 | |
| 322 | + | |
| 323 | +写入侧改了(§5.1),但**存量不会自己搬家**。实查现有 `## 关键事实` 里 | |
| 324 | +**没有一条真事实** —— 全是重复的心跳行、工具调用 id、和以 `...` 开头的半句: | |
| 325 | + | |
| 326 | +``` | |
| 327 | +- [2026-08-18] [SYSTEM HEARTBEAT] → 🔧 [TOOL_CALL] Bash (ID: toolu_01M1t4...) | |
| 328 | +- [2026-04-09] [TASK from master] → ...ates/community-manager/train/evolve/SKILL_v1.md` | | |
| 329 | +``` | |
| 330 | + | |
| 331 | +而 `memory.md` 会被注入**每一轮**提示词 —— 这些东西一直在冒充事实。 | |
| 332 | +`scripts/migrate-memory-raw-section.js` 把它们搬进「原始记录(未提炼)」: | |
| 333 | +**19 个文件 / 269 行**,先 dry-run 再 `--apply`,改前逐个 `.bak` | |
| 334 | +(这些是 gitignore 的运行态,没有 git 历史兜底)。 | |
| 335 | + | |
| 336 | +判据刻意窄:只搬 `- [YYYY-MM-DD] … → …` 这一种形状(旧自动追加的唯一形状)。 | |
| 337 | +人手写的事实不长这样,不动它 —— **搬错比留错更坏**:留下最多多一条噪声, | |
| 338 | +搬错是把人写的东西降级成日志。 | |
| 339 | + | |
| 340 | +### 真跑结果 | |
| 341 | + | |
| 342 | +对 `innovator-evolver`(40 条原料,本仓库最厚的一份)跑 `dreamOnce`: | |
| 343 | + | |
| 344 | +``` | |
| 345 | +{ "distilled": 4, "accepted": 4, "rejected": [] } 耗时 114 s | |
| 346 | +``` | |
| 347 | + | |
| 348 | +四条产出**都做到了抽象**,原料里的具体名字全被抽掉: | |
| 349 | +`yt-dlp` / `Invidious` → 「本地下载工具 → 备用镜像源」; | |
| 350 | +`scorer.js` / `SKILL_v1.md` / `system/templates/...` 一个都没出现。 | |
| 351 | + | |
| 352 | +**顺着 `sources` 抽查过**(可追溯的第一次实际用途):4 条引用全部对得上原料。 | |
| 353 | +例如「按关键词覆盖率打分会被刷到虚高分」那条引了 #21/#22/#27/#28, | |
| 354 | +而 #27 原文正是「从零重写,采用 `expected_output` 逐 case rubric + `dimCeil` 上限机制, | |
| 355 | +杜绝 coverage-based」。**它是跨 4 条记录归纳出来的**,不是逐条改写 —— | |
| 356 | +这正是「一次调用吃掉全部原料」那个选择要换来的东西。 | |
| 357 | + | |
| 358 | +### 闸门单独验过(这一轮零拒绝 = 没被考验) | |
| 359 | + | |
| 360 | +真跑里 4 条全过,说明**去敏闸门一次都没触发**。零拒绝不等于闸门有效, | |
| 361 | +所以单独喂了三条给 `dream-screener` 真跑: | |
| 362 | + | |
| 363 | +| 输入 | 判定 | 理由(原文) | | |
| 364 | +|---|---|---| | |
| 365 | +| 含公司名 + 域名 + 绝对路径 | ❌ false | 「出现了具体域名 lemonai.ai 和绝对路径 /Users/…,第三方能据此直接认出是哪家公司/哪台机器」 | | |
| 366 | +| 心跳日志原文摘录 | ❌ false | 「是心跳日志的原文摘录,不是重写过的经验,属于原文搬运;且工具调用 ID 是可能关联到具体运行实例的标识符」 | | |
| 367 | +| 干净的抽象经验 | ✅ true | 「没有公司名/路径/URL…读起来是重写过的原则而非原始对话摘录」 | | |
| 368 | + | |
| 369 | +三条判据(可识别主体 / 原文搬运 / 正常放行)都按 agent.md 写的那样生效了, | |
| 370 | +**而且拒绝理由都点到了具体是哪几个字** —— 那是 agent.md 里明确要求的。 | |
| 371 | + | |
| 372 | +### 空转保护实跑 | |
| 373 | + | |
| 374 | +第二次调用(原料没变):`{"skipped":"no-new-material"}`,**一次 agent 调用都没起**。 | ... | ... |
scripts/migrate-memory-raw-section.js
0 → 100644
| 1 | +#!/usr/bin/env node | |
| 2 | +/** | |
| 3 | + * 一次性迁移:把 `memory.md` 里**自动追加**的那些行,从 `## 关键事实` | |
| 4 | + * 搬到 `## 原始记录(未提炼)`。 | |
| 5 | + * | |
| 6 | + * ── 为什么 ──────────────────────────────────────────────────────────── | |
| 7 | + * 旧的 `appendToMemoryMd` 取任务与结果的第一行、砍到 80/100 字符,写进「关键事实」。 | |
| 8 | + * 那不是提炼,是搬运 + 截断。实查(2026-08-20)现有内容里**没有一条真事实**: | |
| 9 | + * | |
| 10 | + * - [2026-08-18] [SYSTEM HEARTBEAT] → 🔧 [TOOL_CALL] Bash (ID: toolu_01M1t4...) | |
| 11 | + * - [2026-04-09] [TASK from master] → ...ates/community-manager/train/evolve/SKILL_v1.md` | | |
| 12 | + * | |
| 13 | + * 前者是工具调用 id,后者是**半句**(`...` 开头,主语被截掉了)。 | |
| 14 | + * 而 `memory.md` 会被注入**每一轮**提示词 —— 也就是说这些东西一直在冒充事实。 | |
| 15 | + * | |
| 16 | + * 写入侧已改(29-DREAM §四 步骤 1),但**存量不会自己搬家**。这个脚本补那一步。 | |
| 17 | + * | |
| 18 | + * ── 判据(刻意窄)──────────────────────────────────────────────────── | |
| 19 | + * 只搬形如 `- [YYYY-MM-DD] … → …` 的行 —— 那是旧自动追加的**唯一**形状。 | |
| 20 | + * 人手写的事实不长这样,不动它。判不准就不搬:留在原地最多是「多了一条噪声」, | |
| 21 | + * 搬错则是把人写的东西降级成日志。 | |
| 22 | + * | |
| 23 | + * ── 用法 ────────────────────────────────────────────────────────────── | |
| 24 | + * node scripts/migrate-memory-raw-section.js # dry-run,只打印 | |
| 25 | + * node scripts/migrate-memory-raw-section.js --apply # 真改(先自动备份到 .bak) | |
| 26 | + */ | |
| 27 | + | |
| 28 | +const fs = require('fs'); | |
| 29 | +const path = require('path'); | |
| 30 | + | |
| 31 | +const ROOT = path.resolve(__dirname, '..'); | |
| 32 | +const AGENTS = path.join(ROOT, 'system', 'agents'); | |
| 33 | +const RAW_SECTION = '## 原始记录(未提炼)'; | |
| 34 | +/** 旧自动追加的唯一形状 */ | |
| 35 | +const AUTO_LINE = /^-\s*\[\d{4}-\d{2}-\d{2}\]\s.*→/; | |
| 36 | + | |
| 37 | +const apply = process.argv.includes('--apply'); | |
| 38 | + | |
| 39 | +/** 找出所有 memory.md(本体根 + 公司态两层都可能有) */ | |
| 40 | +function findMemoryFiles() { | |
| 41 | + const out = []; | |
| 42 | + const walk = (dir, depth) => { | |
| 43 | + if (depth > 3) return; | |
| 44 | + let entries; | |
| 45 | + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } | |
| 46 | + for (const e of entries) { | |
| 47 | + const p = path.join(dir, e.name); | |
| 48 | + if (e.isDirectory()) walk(p, depth + 1); | |
| 49 | + else if (e.name === 'memory.md' && path.basename(dir) === 'memory') out.push(p); | |
| 50 | + } | |
| 51 | + }; | |
| 52 | + walk(AGENTS, 0); | |
| 53 | + return out; | |
| 54 | +} | |
| 55 | + | |
| 56 | +function migrate(text) { | |
| 57 | + const factsIdx = text.indexOf('## 关键事实'); | |
| 58 | + if (factsIdx < 0) return null; | |
| 59 | + const head = text.slice(0, factsIdx); | |
| 60 | + const rest = text.slice(factsIdx + '## 关键事实'.length); | |
| 61 | + const body = rest.split(/\n## /)[0]; | |
| 62 | + const tail = rest.slice(body.length); | |
| 63 | + | |
| 64 | + const lines = body.split('\n'); | |
| 65 | + const moved = lines.filter((l) => AUTO_LINE.test(l.trim())); | |
| 66 | + if (moved.length === 0) return null; | |
| 67 | + const kept = lines.filter((l) => !AUTO_LINE.test(l.trim())); | |
| 68 | + | |
| 69 | + // 「关键事实」清空后要留占位,否则下游读到空段会以为文件坏了 | |
| 70 | + const keptText = kept.join('\n').trim() || '\n_(尚无提炼产物)_\n'; | |
| 71 | + let out = `${head}## 关键事实\n${keptText.startsWith('\n') ? keptText : `\n${keptText}\n`}${tail}`; | |
| 72 | + | |
| 73 | + // 合并进已有的原始记录段;没有就补一个(放最后,提炼产物的位置在前) | |
| 74 | + if (out.includes(RAW_SECTION)) { | |
| 75 | + out = out.replace( | |
| 76 | + new RegExp(`${RAW_SECTION}\\n([\\s\\S]*?)(?=\\n## |$)`), | |
| 77 | + (m, existing) => `${RAW_SECTION}\n${moved.join('\n')}\n${existing.trim() ? `${existing.trim()}\n` : ''}` | |
| 78 | + ); | |
| 79 | + } else { | |
| 80 | + out = `${out.replace(/\s*$/, '')}\n\n${RAW_SECTION}\n${moved.join('\n')}\n`; | |
| 81 | + } | |
| 82 | + return { out, movedCount: moved.length }; | |
| 83 | +} | |
| 84 | + | |
| 85 | +let files = 0; let lines = 0; | |
| 86 | +for (const f of findMemoryFiles()) { | |
| 87 | + const text = fs.readFileSync(f, 'utf-8'); | |
| 88 | + const r = migrate(text); | |
| 89 | + if (!r) continue; | |
| 90 | + files += 1; lines += r.movedCount; | |
| 91 | + console.log(`${apply ? '改' : '待改'} ${path.relative(ROOT, f)} 搬 ${r.movedCount} 行`); | |
| 92 | + if (apply) { | |
| 93 | + fs.writeFileSync(`${f}.bak`, text, 'utf-8'); // 这些是 gitignore 的运行态,没有 git 历史兜底 | |
| 94 | + fs.writeFileSync(f, r.out, 'utf-8'); | |
| 95 | + } | |
| 96 | +} | |
| 97 | +console.log(`\n${apply ? '已改' : 'dry-run'}:${files} 个文件 / ${lines} 行`); | |
| 98 | +if (!apply && files) console.log('要真改:node scripts/migrate-memory-raw-section.js --apply'); | ... | ... |