diff --git a/docs/todo/system-design/29-dream-design.md b/docs/todo/system-design/29-dream-design.md index d60bdae..919cdda 100644 --- a/docs/todo/system-design/29-dream-design.md +++ b/docs/todo/system-design/29-dream-design.md @@ -210,8 +210,8 @@ const entry = `- [${timestamp}] ${taskOneLine} → ${resultOneLine}`; ## 四、推进顺序 🔒 **上游与 dream 一起做**(2026-08-20 拍板) -> **进度**:步骤 1、2、3、4、5 全部实现。**未做的是:定时挂载(决策 C)、 -> 写定义文件(`SKILL.md` / `soul.md` / `agent.md`)、以及一次真 CLI 验收** —— 见 §5.4。 +> **进度**:步骤 1、2、3、4、5 全部实现,**并已真 CLI 跑通一次**(§5.5)。 +> 未做的是:定时挂载(决策 C)、写定义文件(`SKILL.md` / `soul.md` / `agent.md`)—— 见 §5.4。 用户定的是两件一起做,不分先后。拆成可验收的步骤: @@ -311,8 +311,64 @@ mtime 会被任何一次无关写入推动(例如别处重写整份 `memory.md (纯经验积累,删掉无副作用)。`SKILL.md` / `soul.md` / `agent.md` 三个 git 跟踪的定义 留给下一步;`definitionWritability()` 已经能报出「这三份现在能不能写、不能写是为什么」, 闸门(§B.3)也已就位,缺的是「往里写什么、写成什么形状」这半边设计。 -3. **没有一次真 CLI 验收**。CI 里换掉的是 `callDreamAgent` 那一层, - 验的是「回复 → 结构化产物」的解析与失效方向,**不是模型写得好不好**。 - 两个本体的提示词至今**没有被任何模型读过** —— 这正是 P3B W4 记过的形态 - (mock 从不读提示词,所以提示词在第一次真跑前从未被检验)。 -4. **决策 D(提炼质量怎么评)仍未定**。可追溯(`sources`)是它的便宜起步,不是替代品。 +3. **决策 D(提炼质量怎么评)仍未定**。可追溯(`sources`)是它的便宜起步,不是替代品。 + §5.5 那次抽查是**人工**顺着 sources 回查的,不是自动的。 + +--- + +## 5.5 第一次真 CLI 跑通(2026-08-20) + +### 前置:存量记忆的搬家 + +写入侧改了(§5.1),但**存量不会自己搬家**。实查现有 `## 关键事实` 里 +**没有一条真事实** —— 全是重复的心跳行、工具调用 id、和以 `...` 开头的半句: + +``` +- [2026-08-18] [SYSTEM HEARTBEAT] → 🔧 [TOOL_CALL] Bash (ID: toolu_01M1t4...) +- [2026-04-09] [TASK from master] → ...ates/community-manager/train/evolve/SKILL_v1.md` | +``` + +而 `memory.md` 会被注入**每一轮**提示词 —— 这些东西一直在冒充事实。 +`scripts/migrate-memory-raw-section.js` 把它们搬进「原始记录(未提炼)」: +**19 个文件 / 269 行**,先 dry-run 再 `--apply`,改前逐个 `.bak` +(这些是 gitignore 的运行态,没有 git 历史兜底)。 + +判据刻意窄:只搬 `- [YYYY-MM-DD] … → …` 这一种形状(旧自动追加的唯一形状)。 +人手写的事实不长这样,不动它 —— **搬错比留错更坏**:留下最多多一条噪声, +搬错是把人写的东西降级成日志。 + +### 真跑结果 + +对 `innovator-evolver`(40 条原料,本仓库最厚的一份)跑 `dreamOnce`: + +``` +{ "distilled": 4, "accepted": 4, "rejected": [] } 耗时 114 s +``` + +四条产出**都做到了抽象**,原料里的具体名字全被抽掉: +`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 | 「没有公司名/路径/URL…读起来是重写过的原则而非原始对话摘录」 | + +三条判据(可识别主体 / 原文搬运 / 正常放行)都按 agent.md 写的那样生效了, +**而且拒绝理由都点到了具体是哪几个字** —— 那是 agent.md 里明确要求的。 + +### 空转保护实跑 + +第二次调用(原料没变):`{"skipped":"no-new-material"}`,**一次 agent 调用都没起**。 diff --git a/scripts/migrate-memory-raw-section.js b/scripts/migrate-memory-raw-section.js new file mode 100644 index 0000000..a44a2b6 --- /dev/null +++ b/scripts/migrate-memory-raw-section.js @@ -0,0 +1,98 @@ +#!/usr/bin/env node +/** + * 一次性迁移:把 `memory.md` 里**自动追加**的那些行,从 `## 关键事实` + * 搬到 `## 原始记录(未提炼)`。 + * + * ── 为什么 ──────────────────────────────────────────────────────────── + * 旧的 `appendToMemoryMd` 取任务与结果的第一行、砍到 80/100 字符,写进「关键事实」。 + * 那不是提炼,是搬运 + 截断。实查(2026-08-20)现有内容里**没有一条真事实**: + * + * - [2026-08-18] [SYSTEM HEARTBEAT] → 🔧 [TOOL_CALL] Bash (ID: toolu_01M1t4...) + * - [2026-04-09] [TASK from master] → ...ates/community-manager/train/evolve/SKILL_v1.md` | + * + * 前者是工具调用 id,后者是**半句**(`...` 开头,主语被截掉了)。 + * 而 `memory.md` 会被注入**每一轮**提示词 —— 也就是说这些东西一直在冒充事实。 + * + * 写入侧已改(29-DREAM §四 步骤 1),但**存量不会自己搬家**。这个脚本补那一步。 + * + * ── 判据(刻意窄)──────────────────────────────────────────────────── + * 只搬形如 `- [YYYY-MM-DD] … → …` 的行 —— 那是旧自动追加的**唯一**形状。 + * 人手写的事实不长这样,不动它。判不准就不搬:留在原地最多是「多了一条噪声」, + * 搬错则是把人写的东西降级成日志。 + * + * ── 用法 ────────────────────────────────────────────────────────────── + * node scripts/migrate-memory-raw-section.js # dry-run,只打印 + * node scripts/migrate-memory-raw-section.js --apply # 真改(先自动备份到 .bak) + */ + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const AGENTS = path.join(ROOT, 'system', 'agents'); +const RAW_SECTION = '## 原始记录(未提炼)'; +/** 旧自动追加的唯一形状 */ +const AUTO_LINE = /^-\s*\[\d{4}-\d{2}-\d{2}\]\s.*→/; + +const apply = process.argv.includes('--apply'); + +/** 找出所有 memory.md(本体根 + 公司态两层都可能有) */ +function findMemoryFiles() { + const out = []; + const walk = (dir, depth) => { + if (depth > 3) return; + let entries; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } + for (const e of entries) { + const p = path.join(dir, e.name); + if (e.isDirectory()) walk(p, depth + 1); + else if (e.name === 'memory.md' && path.basename(dir) === 'memory') out.push(p); + } + }; + walk(AGENTS, 0); + return out; +} + +function migrate(text) { + const factsIdx = text.indexOf('## 关键事实'); + if (factsIdx < 0) return null; + const head = text.slice(0, factsIdx); + const rest = text.slice(factsIdx + '## 关键事实'.length); + const body = rest.split(/\n## /)[0]; + const tail = rest.slice(body.length); + + const lines = body.split('\n'); + const moved = lines.filter((l) => AUTO_LINE.test(l.trim())); + if (moved.length === 0) return null; + const kept = lines.filter((l) => !AUTO_LINE.test(l.trim())); + + // 「关键事实」清空后要留占位,否则下游读到空段会以为文件坏了 + const keptText = kept.join('\n').trim() || '\n_(尚无提炼产物)_\n'; + let out = `${head}## 关键事实\n${keptText.startsWith('\n') ? keptText : `\n${keptText}\n`}${tail}`; + + // 合并进已有的原始记录段;没有就补一个(放最后,提炼产物的位置在前) + if (out.includes(RAW_SECTION)) { + out = out.replace( + new RegExp(`${RAW_SECTION}\\n([\\s\\S]*?)(?=\\n## |$)`), + (m, existing) => `${RAW_SECTION}\n${moved.join('\n')}\n${existing.trim() ? `${existing.trim()}\n` : ''}` + ); + } else { + out = `${out.replace(/\s*$/, '')}\n\n${RAW_SECTION}\n${moved.join('\n')}\n`; + } + return { out, movedCount: moved.length }; +} + +let files = 0; let lines = 0; +for (const f of findMemoryFiles()) { + const text = fs.readFileSync(f, 'utf-8'); + const r = migrate(text); + if (!r) continue; + files += 1; lines += r.movedCount; + console.log(`${apply ? '改' : '待改'} ${path.relative(ROOT, f)} 搬 ${r.movedCount} 行`); + if (apply) { + fs.writeFileSync(`${f}.bak`, text, 'utf-8'); // 这些是 gitignore 的运行态,没有 git 历史兜底 + fs.writeFileSync(f, r.out, 'utf-8'); + } +} +console.log(`\n${apply ? '已改' : 'dry-run'}:${files} 个文件 / ${lines} 行`); +if (!apply && files) console.log('要真改:node scripts/migrate-memory-raw-section.js --apply');