框架: M3管线P0/P1——llm skill统一入口+clean四道守卫三级匹配+执行计划落仓(README§九/CLAUDE.md同步拍板)
This commit is contained in:
parent
11cdf93347
commit
2b6f83c097
@ -1,39 +1,39 @@
|
|||||||
---
|
---
|
||||||
name: clean
|
name: clean
|
||||||
description: LLM 辅助正文清洗——LLM 按窗口(约10万字)只输出待删垃圾段原文与理由,代码做精确匹配删除并全程留审计(example_clean_log)。LLM 不改写正文,删不删由守卫规则最终裁决。
|
description: LLM 辅助正文清洗——MiniMax-M3 按窗口(约10万字)只输出待删垃圾段原文与理由,代码做精确匹配删除并全程留审计(example_clean_log)。LLM 不改写正文,删不删由四道守卫规则最终裁决。
|
||||||
---
|
---
|
||||||
|
|
||||||
# clean —— LLM 检测 + 代码执行的正文清洗
|
# clean —— LLM 检测 + 代码执行的正文清洗
|
||||||
|
|
||||||
分工(创始人方案 2026-07-13):**规则层**已在 import 解决结构性垃圾(水印正则/重贴章题/目录页/分页尾巴/残破实体);本 skill 处理**语义垃圾**——变体书站广告、作者拉票/PS 段、乱码水印等正则打不全的散落噪声。LLM 只当探测器(输出待删片段逐字原文),删除由脚本执行:零改写、可审计、可回放。
|
分工(创始人方案 2026-07-13):**规则层**已在 import 解决结构性垃圾(水印正则/重贴章题/目录页/分页尾巴/残破实体);本 skill 处理**语义垃圾**——变体书站广告、作者拉票/PS 段、乱码水印等正则打不全的散落噪声。LLM 只当探测器(输出待删片段逐字原文),删除由脚本执行:零改写、可审计、可回放。探测模型=New-API `MiniMax-M3`(创始人拍板,经 llm skill 统一入口调用)。
|
||||||
|
|
||||||
## 流程
|
## 流程
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1) 备窗口:按章对齐切窗(默认 ~10万字/窗,写 /tmp/muse-clean/<work>/win-*.txt + manifest)
|
# 1) 备窗口:按章对齐切窗(默认 ~10万字/窗,写 /tmp/muse-clean/<work>/win-*.txt + manifest)
|
||||||
.venv/bin/python .claude/skills/clean/scripts/clean_prep.py --work-id 5 --window 100000
|
.venv/bin/python .claude/skills/clean/scripts/clean_prep.py --work-id 7 --window 100000
|
||||||
|
|
||||||
# 2) 探测:每窗一次 LLM 调用(haiku,编排层派发;prompt 模板见下)→ 每窗一份 deletions JSON
|
# 2) 探测:每窗一次 M3 调用(prompt 内置于脚本;断点续跑,已有产物自动跳过)
|
||||||
|
.venv/bin/python .claude/skills/clean/scripts/clean_detect.py --work-id 7 [--win 1 --win 2] [--force]
|
||||||
|
|
||||||
# 3) 执行:精确匹配删除 + 审计入库 + 字数刷新(守卫规则见下)
|
# 3) 执行:先 dry-run 对账出删除明细(质检样张),确认后去掉 --dry-run 落库
|
||||||
.venv/bin/python .claude/skills/clean/scripts/clean_apply.py --work-id 5 --batch <批次号> --file /tmp/muse-clean/5/deletions-all.json
|
.venv/bin/python .claude/skills/clean/scripts/clean_apply.py --work-id 7 --batch <批次号> --file /tmp/muse-clean/7/deletions-001.json [--dry-run]
|
||||||
|
|
||||||
# 4) 审查:删除对账(每章删了几段几字)+ 审计行抽查
|
# 4) 审查:删除对账(理由分布/字数)+ 审计行抽查
|
||||||
.venv/bin/python .claude/skills/db/scripts/db.py query "SELECT reason, count(*), sum(length(removed_text)) FROM example_clean_log WHERE work_id=5 AND batch='<批次号>' GROUP BY reason"
|
.venv/bin/python .claude/skills/db/scripts/db.py query "SELECT reason, count(*), sum(length(removed_text)) FROM example_clean_log WHERE work_id=7 AND batch='<批次号>' GROUP BY reason"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 探测 prompt 模板(编排层内联给 haiku)
|
## 四道守卫(脚本硬闸,LLM 建议 ≠ 必删;全部为首窗真跑实测病对症)
|
||||||
|
|
||||||
> 你是网文正文清洗探测器。下面是《书名》第 A–B 章的原文(含章题行)。找出**所有非正文垃圾**:书站广告及变体(如「一秒记住♂粒÷小÷说→网」)、网址残留、求票拉票/打赏鸣谢、章尾作者话(ps:…)、上架感言类整段、乱码水印。输出 deletions 数组,每项 `{chapter_order, exact, reason}`:`exact` 必须与原文**逐字一致**(含标点空白,≤300字,一段一项);只报垃圾,拿不准的不报;**情节正文一个字都不许报**。章题行不报(即使含求票字样——章题另行处理)。
|
1. **长度界**:`exact` 4–500 字;
|
||||||
|
2. **章题保护**:不得含/命中所在章章题;
|
||||||
|
3. **单章 20% 顶**:单章累计删除超章长 20% → 整章回退不落库;
|
||||||
|
4. **行级垃圾特征**:exact 每一行必须命中通用垃圾词(ps头/感谢/打赏/求票/域名符号…,不含书内专名)——盗版源会把广告插进正文句子中间,M3 会把「广告+被截断正文」拼成一项,无特征行拆行跳过(宁漏删不误删)。
|
||||||
|
|
||||||
## 守卫规则(脚本硬闸,LLM 建议 ≠ 必删)
|
匹配三级(对症 M3 两类实测病):精确命中 → 空白弹性(M3 折叠 `\n\n\n`→`\n\n`)→ 邻章 ±1 容错(长窗内偶发章号偏移)。三级全失败 → 拒绝。每次删除一行审计(原文/理由/模型/批次),源 txt 永远不动(可整书重导回滚);删完刷新 block/work 字数。
|
||||||
|
|
||||||
- `exact` 长度 4–500 字;在指定章内**逐字命中**才删(命中多次全删并记 occurrences);
|
## 量级实测(2026-07-13 首窗定标)
|
||||||
- 不得命中/包含章题行;单章累计删除 ≤ 章长 20%(超限整章建议弃用并报告);
|
|
||||||
- 每次删除一行审计(原文/理由/模型/批次),源 txt 永远不动(可整书重导回滚);
|
|
||||||
- 删完刷新 block.word_count 与 work.word_count。
|
|
||||||
|
|
||||||
## 量级参考(2026-07-13 估)
|
- M3 中文 tokenizer ≈ **0.58 token/字**;10 万字窗 in≈5.9 万 token、out≈2k、耗时 ~24s;
|
||||||
|
- 机破星河前 50 章(12.9 万字,2 窗):39 条建议 → 40 段删除(拆行后)/2,188 字,误删 0;
|
||||||
- 拆书范围(5 书×前50章 ≈ 90万字)≈ 9–12 窗 ≈ 12 次 haiku 调用;
|
- 全语料(8 书 ≈3,300 万字)≈ **340 窗 ≈ 340 次 M3 调用,输入 ~19M token**。
|
||||||
- 全语料(8 书 ≈ 3,300万字)≈ 340 窗 ≈ 350 次 haiku 调用(输入 ~35M token 量级)。
|
|
||||||
|
|||||||
@ -2,6 +2,12 @@
|
|||||||
"""clean skill:精确匹配删除执行器(LLM 建议 ≠ 必删,守卫规则最终裁决)+ 审计入库。
|
"""clean skill:精确匹配删除执行器(LLM 建议 ≠ 必删,守卫规则最终裁决)+ 审计入库。
|
||||||
|
|
||||||
输入 deletions JSON:[{chapter_order|chapter, exact, reason}](键名 ASCII,兼容中文键)。
|
输入 deletions JSON:[{chapter_order|chapter, exact, reason}](键名 ASCII,兼容中文键)。
|
||||||
|
|
||||||
|
匹配三级(M3 首窗实测的两类病对症):
|
||||||
|
1. 精确命中:exact 逐字在章内出现;
|
||||||
|
2. 空白弹性:文字逐字、连续空白段放宽为 \\s+(M3 会把原文 \\n\\n\\n 折叠成 \\n\\n);
|
||||||
|
3. 邻章容错:本章 1/2 级全失败时试 order±1 章(M3 在 10 万字窗内偶发章号偏移)。
|
||||||
|
三级全失败 → 拒绝(安全方向:宁漏删不误删)。
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
@ -17,18 +23,53 @@ TENANT, ACTOR = 1, "1"
|
|||||||
MIN_LEN, MAX_LEN = 4, 500
|
MIN_LEN, MAX_LEN = 4, 500
|
||||||
MAX_CH_RATIO = 0.20 # 单章累计删除上限(占章长比)
|
MAX_CH_RATIO = 0.20 # 单章累计删除上限(占章长比)
|
||||||
|
|
||||||
|
# 守卫4:垃圾行强特征——exact 的每一行必须命中其一,否则视为疑混正文。
|
||||||
|
# 首窗实测:盗版源会把广告插进正文句子中间,M3 会把「广告+被截断的正文」拼成一项报上来,
|
||||||
|
# 纯文本匹配防不住,必须按行做内容特征裁决(只用通用垃圾词,不含任何书内专名)。
|
||||||
|
GARBAGE_LINE_PAT = re.compile(
|
||||||
|
r"^\s*[((]?\s*ps\b|^\s*[((]?\s*附[::]"
|
||||||
|
r"|感谢|打赏|盟主|书友|书评|拜谢|恳求|打滚|鸣谢"
|
||||||
|
r"|(?:月|推荐|评价|催更|票)票|起点币|求(?:收藏|推荐|订阅|票|月票)|拉票|冲榜"
|
||||||
|
r"|龙套|上架|[aA]签|签约|加更|[一二三四五六七八九十]更|更新时间|码字|存稿|请假|感言"
|
||||||
|
r"|www|http|[\._]{2,}|→|♂|÷|一秒记住|记住本站|无弹窗|手机版|首发|最新章节|整理|转载",
|
||||||
|
re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def flex_pattern(exact):
|
||||||
|
"""空白弹性正则:非空白文字逐字 escape,连续空白段 → \\s+。
|
||||||
|
|
||||||
|
只放宽空白,文字仍须逐字一致——不会匹配到不同文字的正文。
|
||||||
|
"""
|
||||||
|
parts = [p for p in re.split(r"\s+", exact) if p]
|
||||||
|
if not parts:
|
||||||
|
return None
|
||||||
|
return re.compile(r"\s+".join(re.escape(p) for p in parts))
|
||||||
|
|
||||||
|
|
||||||
|
def try_remove(text, exact):
|
||||||
|
"""在 text 中删除 exact(精确→弹性两级)。返回 (新text, 命中次数, 实删字数, 方式)。"""
|
||||||
|
n = text.count(exact)
|
||||||
|
if n:
|
||||||
|
return text.replace(exact, ""), n, len(exact) * n, "精确"
|
||||||
|
pat = flex_pattern(exact)
|
||||||
|
if pat:
|
||||||
|
new, n = pat.subn("", text)
|
||||||
|
if n:
|
||||||
|
return new, n, len(text) - len(new), "空白弹性"
|
||||||
|
return text, 0, 0, ""
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("--work-id", type=int, required=True)
|
@click.option("--work-id", type=int, required=True)
|
||||||
@click.option("--batch", required=True, help="清洗批次号(审计用)")
|
@click.option("--batch", required=True, help="清洗批次号(审计用)")
|
||||||
@click.option("--file", "file_", type=click.Path(exists=True), required=True)
|
@click.option("--file", "file_", type=click.Path(exists=True), required=True)
|
||||||
@click.option("--model", default="haiku", show_default=True, help="建议来源模型(审计标注)")
|
@click.option("--model", default="MiniMax-M3", show_default=True, help="建议来源模型(审计标注)")
|
||||||
@click.option("--dry-run", is_flag=True, help="只对账不落库")
|
@click.option("--dry-run", is_flag=True, help="只对账不落库")
|
||||||
def main(work_id, batch, file_, model, dry_run):
|
def main(work_id, batch, file_, model, dry_run):
|
||||||
raw = json.loads(pathlib.Path(file_).read_text())
|
raw = json.loads(pathlib.Path(file_).read_text())
|
||||||
if isinstance(raw, dict):
|
if isinstance(raw, dict):
|
||||||
raw = raw.get("deletions") or raw.get("删除") or []
|
raw = raw.get("deletions") or raw.get("删除") or []
|
||||||
# 按章分组
|
# 按章分组(键名 ASCII 为主,兼容中文键)
|
||||||
by_ch = {}
|
by_ch = {}
|
||||||
for d in raw:
|
for d in raw:
|
||||||
no = d.get("chapter_order") or d.get("chapter") or d.get("章序")
|
no = d.get("chapter_order") or d.get("chapter") or d.get("章序")
|
||||||
@ -37,58 +78,97 @@ def main(work_id, batch, file_, model, dry_run):
|
|||||||
continue
|
continue
|
||||||
by_ch.setdefault(int(no), []).append((exact, d.get("reason") or d.get("理由") or ""))
|
by_ch.setdefault(int(no), []).append((exact, d.get("reason") or d.get("理由") or ""))
|
||||||
|
|
||||||
stats = {"删除段": 0, "删除字数": 0, "拒绝": []}
|
# 一次拉齐所有目标章及其邻章(±1),在内存统一匹配,减少 DB 往返
|
||||||
|
targets = sorted({n for no in by_ch for n in (no - 1, no, no + 1) if n >= 1})
|
||||||
|
stats = {"删除段": 0, "删除字数": 0, "拒绝": [], "弹性命中": 0, "邻章命中": 0}
|
||||||
with psycopg.connect(DSN) as conn:
|
with psycopg.connect(DSN) as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""SELECT c.order_no, c.id, c.title, b.id, b.content_text
|
||||||
|
FROM muse_content_chapter c JOIN muse_content_block b ON b.chapter_id=c.id AND b.deleted=FALSE
|
||||||
|
WHERE c.tenant_id=%s AND c.work_id=%s AND c.deleted=FALSE AND c.order_no = ANY(%s)""",
|
||||||
|
(TENANT, work_id, targets)).fetchall()
|
||||||
|
# chapters[no] = {ch_id, title, blk_id, text(可变), orig(原文), removed(累计), audit(审计行)}
|
||||||
|
chapters = {r[0]: {"ch_id": r[1], "title": r[2], "blk_id": r[3],
|
||||||
|
"text": r[4], "orig": r[4], "removed": 0, "audit": []} for r in rows}
|
||||||
|
|
||||||
for no, dels in sorted(by_ch.items()):
|
for no, dels in sorted(by_ch.items()):
|
||||||
row = conn.execute(
|
if no not in chapters:
|
||||||
"""SELECT c.id, c.title, b.id, b.content_text
|
|
||||||
FROM muse_content_chapter c JOIN muse_content_block b ON b.chapter_id=c.id AND b.deleted=FALSE
|
|
||||||
WHERE c.tenant_id=%s AND c.work_id=%s AND c.order_no=%s AND c.deleted=FALSE""",
|
|
||||||
(TENANT, work_id, no)).fetchone()
|
|
||||||
if not row:
|
|
||||||
stats["拒绝"].append(f"#{no}: 章不存在")
|
stats["拒绝"].append(f"#{no}: 章不存在")
|
||||||
continue
|
continue
|
||||||
ch_id, ch_title, blk_id, text = row
|
|
||||||
new_text = text
|
|
||||||
ch_removed = 0
|
|
||||||
applied = []
|
|
||||||
for exact, reason in dels:
|
for exact, reason in dels:
|
||||||
# 守卫1:长度界
|
# 守卫1:长度界
|
||||||
if not (MIN_LEN <= len(exact) <= MAX_LEN):
|
if not (MIN_LEN <= len(exact) <= MAX_LEN):
|
||||||
stats["拒绝"].append(f"#{no}: 长度越界({len(exact)}) {exact[:24]!r}")
|
stats["拒绝"].append(f"#{no}: 长度越界({len(exact)}) {exact[:24]!r}")
|
||||||
continue
|
continue
|
||||||
# 守卫2:不得含/命中章题
|
# 守卫4:行级垃圾特征——无特征的行=疑混正文
|
||||||
if ch_title and ch_title in exact:
|
lines = [ln for ln in exact.split("\n") if ln.strip()]
|
||||||
stats["拒绝"].append(f"#{no}: 含章题 {exact[:24]!r}")
|
plain = [ln for ln in lines if not GARBAGE_LINE_PAT.search(ln)]
|
||||||
|
if plain and len(lines) == 1:
|
||||||
|
stats["拒绝"].append(f"#{no}: 无垃圾特征疑正文 {exact[:24]!r}")
|
||||||
continue
|
continue
|
||||||
n = new_text.count(exact)
|
if plain:
|
||||||
if n == 0:
|
# 拆行降级:只删有垃圾特征的行,疑正文行跳过并显式报告(宁漏删不误删)
|
||||||
stats["拒绝"].append(f"#{no}: 未命中 {exact[:24]!r}")
|
for ln in plain:
|
||||||
continue
|
stats["拒绝"].append(f"#{no}: 拆行跳过疑正文行 {ln[:24]!r}")
|
||||||
new_text = new_text.replace(exact, "")
|
units = [(ln, reason + "(拆行)") for ln in lines
|
||||||
ch_removed += len(exact) * n
|
if GARBAGE_LINE_PAT.search(ln) and MIN_LEN <= len(ln) <= MAX_LEN]
|
||||||
applied.append((exact, n, reason))
|
else:
|
||||||
# 守卫3:单章删除比例硬顶
|
units = [(exact, reason)]
|
||||||
if ch_removed > len(text) * MAX_CH_RATIO:
|
for u_exact, u_reason in units:
|
||||||
stats["拒绝"].append(f"#{no}: 累计删除 {ch_removed} 字超章长20%上限,整章放弃")
|
# 本章两级匹配 → 邻章容错(章号偏移是 LLM 在长窗里的已知病)
|
||||||
|
hit_no = None
|
||||||
|
for cand in (no, no - 1, no + 1):
|
||||||
|
ch = chapters.get(cand)
|
||||||
|
if not ch:
|
||||||
|
continue
|
||||||
|
# 守卫2:不得含该章章题(按实际匹配章判断)
|
||||||
|
if ch["title"] and ch["title"] in u_exact:
|
||||||
|
stats["拒绝"].append(f"#{no}: 含章题 {u_exact[:24]!r}")
|
||||||
|
hit_no = -1 # 标记已裁决,不再试邻章
|
||||||
|
break
|
||||||
|
new, n, removed, how = try_remove(ch["text"], u_exact)
|
||||||
|
if n:
|
||||||
|
ch["text"] = new
|
||||||
|
ch["removed"] += removed
|
||||||
|
note = u_reason
|
||||||
|
if how == "空白弹性":
|
||||||
|
stats["弹性命中"] += 1
|
||||||
|
note += "(空白弹性)"
|
||||||
|
if cand != no:
|
||||||
|
stats["邻章命中"] += 1
|
||||||
|
note += f"(报#{no}实删#{cand})"
|
||||||
|
ch["audit"].append((u_exact, n, note))
|
||||||
|
stats["删除段"] += 1
|
||||||
|
stats["删除字数"] += removed
|
||||||
|
hit_no = cand
|
||||||
|
break
|
||||||
|
if hit_no is None:
|
||||||
|
stats["拒绝"].append(f"#{no}: 未命中 {u_exact[:24]!r}")
|
||||||
|
|
||||||
|
# 守卫3:单章删除比例硬顶——超顶整章回退不落库
|
||||||
|
dirty = []
|
||||||
|
for no, ch in sorted(chapters.items()):
|
||||||
|
if not ch["audit"]:
|
||||||
continue
|
continue
|
||||||
if not applied:
|
if ch["removed"] > len(ch["orig"]) * MAX_CH_RATIO:
|
||||||
|
stats["删除段"] -= len(ch["audit"])
|
||||||
|
stats["删除字数"] -= ch["removed"]
|
||||||
|
stats["拒绝"].append(f"#{no}: 累计删除 {ch['removed']} 字超章长20%上限,整章回退")
|
||||||
continue
|
continue
|
||||||
new_text = re.sub(r'\n{3,}', '\n\n', new_text) # 删除后合并多余空行
|
ch["text"] = re.sub(r"\n{3,}", "\n\n", ch["text"]) # 删除后合并多余空行
|
||||||
stats["删除段"] += len(applied)
|
dirty.append((no, ch))
|
||||||
stats["删除字数"] += ch_removed
|
|
||||||
if dry_run:
|
|
||||||
continue
|
|
||||||
wc = len(re.sub(r'\s', '', new_text))
|
|
||||||
conn.execute("UPDATE muse_content_block SET content_text=%s, word_count=%s, updater=%s WHERE id=%s",
|
|
||||||
(new_text, wc, ACTOR, blk_id))
|
|
||||||
for exact, n, reason in applied:
|
|
||||||
conn.execute(
|
|
||||||
"""INSERT INTO example_clean_log (work_id, chapter_id, batch, removed_text, occurrences,
|
|
||||||
reason, model, creator, updater, tenant_id)
|
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
|
|
||||||
(work_id, ch_id, batch, exact, n, reason[:200], model, ACTOR, ACTOR, TENANT))
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
|
for no, ch in dirty:
|
||||||
|
wc = len(re.sub(r"\s", "", ch["text"]))
|
||||||
|
conn.execute("UPDATE muse_content_block SET content_text=%s, word_count=%s, updater=%s WHERE id=%s",
|
||||||
|
(ch["text"], wc, ACTOR, ch["blk_id"]))
|
||||||
|
for exact, n, note in ch["audit"]:
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO example_clean_log (work_id, chapter_id, batch, removed_text, occurrences,
|
||||||
|
reason, model, creator, updater, tenant_id)
|
||||||
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
|
||||||
|
(work_id, ch["ch_id"], batch, exact, n, note[:200], model, ACTOR, ACTOR, TENANT))
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""UPDATE muse_content_work w SET word_count=(
|
"""UPDATE muse_content_work w SET word_count=(
|
||||||
SELECT COALESCE(sum(b.word_count),0) FROM muse_content_block b
|
SELECT COALESCE(sum(b.word_count),0) FROM muse_content_block b
|
||||||
@ -96,9 +176,16 @@ def main(work_id, batch, file_, model, dry_run):
|
|||||||
WHERE w.id=%s""", (TENANT, ACTOR, work_id))
|
WHERE w.id=%s""", (TENANT, ACTOR, work_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
mode = "(dry-run 未落库)" if dry_run else ""
|
mode = "(dry-run 未落库)" if dry_run else ""
|
||||||
click.echo(f"清洗执行{mode}: 删除 {stats['删除段']} 段 / {stats['删除字数']:,} 字;拒绝 {len(stats['拒绝'])} 条")
|
click.echo(f"清洗执行{mode}: 删除 {stats['删除段']} 段 / {stats['删除字数']:,} 字"
|
||||||
|
f"(弹性命中 {stats['弹性命中']}、邻章命中 {stats['邻章命中']});拒绝 {len(stats['拒绝'])} 条")
|
||||||
for r in stats["拒绝"][:12]:
|
for r in stats["拒绝"][:12]:
|
||||||
click.echo(f" [拒] {r}")
|
click.echo(f" [拒] {r}")
|
||||||
|
if dry_run:
|
||||||
|
# dry-run 打印完整删除明细:门①人工质检的数据源
|
||||||
|
click.echo("── 删除明细(质检用) ──")
|
||||||
|
for no, ch in dirty:
|
||||||
|
for exact, n, note in ch["audit"]:
|
||||||
|
click.echo(f" [删] #{no} ×{n} {note or '(无理由)'}\n {exact!r}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
82
.claude/skills/clean/scripts/clean_detect.py
Normal file
82
.claude/skills/clean/scripts/clean_detect.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""clean skill:LLM 探测执行器——每窗一次 MiniMax-M3 调用,产出 deletions JSON。
|
||||||
|
|
||||||
|
读 clean_prep 产出的 manifest.json,逐窗调 M3 探测垃圾段(只报逐字原文,不改写),
|
||||||
|
写 /tmp/muse-clean/<work>/deletions-NNN.json。断点续跑:已有产物的窗自动跳过。
|
||||||
|
删除动作不在本脚本:由 clean_apply 守卫裁决执行。
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
# 统一走 llm skill 入口(trust_env/重试/<think>剥离/JSON 容错都在那边)
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "llm" / "scripts"))
|
||||||
|
from llm import chat, extract_json # noqa: E402
|
||||||
|
|
||||||
|
OUT = pathlib.Path("/tmp/muse-clean")
|
||||||
|
|
||||||
|
# 探测 prompt:与 SKILL.md 模板同源;要求纯 JSON 输出便于机器解析
|
||||||
|
PROMPT = """你是网文正文清洗探测器。下面是《{title}》第 {a}–{b} 章的原文(每章以【第N章 | 章题】标记行开头)。
|
||||||
|
|
||||||
|
找出**所有非正文垃圾**:
|
||||||
|
- 书站广告及变体(如「一秒记住♂粒÷小÷说→网」「天才一秒记住本站地址」)
|
||||||
|
- 网址/域名残留、"首发""手机版阅读""无弹窗"类导流语
|
||||||
|
- 求票拉票/求订阅收藏/打赏鸣谢段
|
||||||
|
- 章尾作者话(ps:… / PS:… / 附:…)、上架感言、请假条类整段
|
||||||
|
- 乱码水印、明显不属于故事的插入符号串
|
||||||
|
|
||||||
|
输出规则(只输出一个 JSON 对象,禁止任何其他文字):
|
||||||
|
{{"deletions": [{{"chapter_order": 章序号, "exact": "待删段逐字原文", "reason": "简短理由"}}]}}
|
||||||
|
|
||||||
|
硬性要求:
|
||||||
|
- `exact` 必须与原文**逐字一致**(含标点、空格),单项 ≤300 字;同一段垃圾一项,不合并多段;
|
||||||
|
- 只报确定是垃圾的,拿不准的不报;**情节正文一个字都不许报**;
|
||||||
|
- 【第N章 | 章题】标记行不报(即使含求票字样);
|
||||||
|
- 若该窗没有垃圾,输出 {{"deletions": []}}。
|
||||||
|
|
||||||
|
原文开始:
|
||||||
|
{text}"""
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option("--work-id", type=int, required=True)
|
||||||
|
@click.option("--win", "wins", type=int, multiple=True, help="指定窗号(可多次);不给则全部窗")
|
||||||
|
@click.option("--model", default="MiniMax-M3", show_default=True)
|
||||||
|
@click.option("--force", is_flag=True, help="已有产物也重跑")
|
||||||
|
def main(work_id, wins, model, force):
|
||||||
|
d = OUT / str(work_id)
|
||||||
|
manifest = json.loads((d / "manifest.json").read_text())
|
||||||
|
title = manifest["title"]
|
||||||
|
targets = [m for m in manifest["windows"] if not wins or m["win"] in wins]
|
||||||
|
total_in = total_out = n_del = 0
|
||||||
|
for m in targets:
|
||||||
|
f_out = d / f"deletions-{m['win']:03d}.json"
|
||||||
|
if f_out.exists() and not force:
|
||||||
|
click.echo(f"win-{m['win']:03d} 已有产物,跳过(--force 重跑)", err=True)
|
||||||
|
continue
|
||||||
|
text = pathlib.Path(m["file"]).read_text()
|
||||||
|
t0 = time.time()
|
||||||
|
content, usage = chat(
|
||||||
|
PROMPT.format(title=title, a=m["from"], b=m["to"], text=text),
|
||||||
|
model=model)
|
||||||
|
data = extract_json(content)
|
||||||
|
dels = data.get("deletions", data if isinstance(data, list) else [])
|
||||||
|
f_out.write_text(json.dumps({"deletions": dels}, ensure_ascii=False, indent=1))
|
||||||
|
total_in += usage.get("prompt_tokens", 0)
|
||||||
|
total_out += usage.get("completion_tokens", 0)
|
||||||
|
n_del += len(dels)
|
||||||
|
click.echo(f"win-{m['win']:03d}(第{m['from']}–{m['to']}章)检出 {len(dels)} 段 "
|
||||||
|
f"→ {f_out.name}({time.time() - t0:.0f}s)")
|
||||||
|
click.echo(f"探测完成:{len(targets)} 窗共检出 {n_del} 段;"
|
||||||
|
f"token in={total_in:,} out={total_out:,}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except RuntimeError as e:
|
||||||
|
click.echo(f"[llm错误] {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
33
.claude/skills/llm/SKILL.md
Normal file
33
.claude/skills/llm/SKILL.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: llm
|
||||||
|
description: New-API LLM 调用统一入口(默认 MiniMax-M3)。管线内所有内容生产型 LLM 调用(清洗探测、拆书抽取等)必须经此 skill 发起,不得裸调外部服务;主会话模型只负责固化提示词、发起调用与守卫验证。
|
||||||
|
---
|
||||||
|
|
||||||
|
# llm —— New-API 统一调用入口
|
||||||
|
|
||||||
|
创始人拍板(2026-07-13):清洗与拆书的内容生产 LLM **全部走 New-API 的 MiniMax-M3**;主会话(Fable5)只固化 agent/提示词/skill 与发起调用。本 skill 是唯一出口。
|
||||||
|
|
||||||
|
## 用法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# CLI:读 prompt 文件调用,内容打到 stdout(token 用量与重试日志走 stderr)
|
||||||
|
.venv/bin/python .claude/skills/llm/scripts/llm.py chat --prompt-file /tmp/p.txt [--model MiniMax-M3] [--max-tokens 32000] [--out /tmp/resp.txt] [--extract-json]
|
||||||
|
|
||||||
|
# 其他 skill 内 import(clean_detect / parse 直调脚本的标准姿势)
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "llm" / "scripts"))
|
||||||
|
from llm import chat, extract_json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 内建保障(调用方不必重复实现)
|
||||||
|
|
||||||
|
- `trust_env=False`:本机代理环境变量会劫持内网直连(教训固化);
|
||||||
|
- 超时 900s + 网络错/5xx/429 指数退避重试 2 次;
|
||||||
|
- `<think>…</think>` 剥离:推理模型可能把思考混进 content;
|
||||||
|
- `extract_json()` 三级容错:直接解析 → 首尾括号截取 → json-repair 兜底(修中文引号/缺逗号/尾逗号——opus 试拆已实测两类 JSON 病,任何模型都可能犯);
|
||||||
|
- 每次调用向 stderr 打印 token 用量与耗时(成本审计)。
|
||||||
|
|
||||||
|
## 凭据与约束
|
||||||
|
|
||||||
|
- token 为 New-API **普通令牌**(明文入仓是仓库政策);严禁改用管理令牌打 /v1。
|
||||||
|
- 可用模型以 `/v1/models` 为准(2026-07-13 在列:MiniMax-M3 / M2.x 系 / deepseek-v4-* / glm-5.2 / Qwen 嵌入与重排)。
|
||||||
|
- 嵌入调用不走本 skill(已有 embed skill,模型与维度钉死)。
|
||||||
122
.claude/skills/llm/scripts/llm.py
Normal file
122
.claude/skills/llm/scripts/llm.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""llm skill:New-API 统一调用入口(默认 MiniMax-M3)。
|
||||||
|
|
||||||
|
管线内所有内容生产型 LLM 调用(清洗探测/拆书抽取)必须经此入口:
|
||||||
|
- trust_env=False(本机代理环境变量会劫持内网直连,教训固化);
|
||||||
|
- 超时 + 指数退避重试;<think> 剥离;JSON 三级容错提取(json-repair 兜底);
|
||||||
|
- 每次调用向 stderr 打印 token 用量与耗时(成本审计),stdout 只出内容。
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import click
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE = "http://100.64.0.8:3000"
|
||||||
|
# New-API 普通令牌(仓库政策允许明文;严禁换管理令牌打 /v1)
|
||||||
|
TOKEN = "sk-DyVqO3lDmEvQZ3PqGpbNaaaHZHhbh0xaHRIiynhYSmVlLHl2"
|
||||||
|
DEFAULT_MODEL = "MiniMax-M3"
|
||||||
|
|
||||||
|
|
||||||
|
def chat(prompt, model=DEFAULT_MODEL, max_tokens=32000, temperature=0.2,
|
||||||
|
retries=2, timeout=900):
|
||||||
|
"""单轮对话,返回 (content, usage)。网络错/5xx/429 指数退避重试。
|
||||||
|
|
||||||
|
content 已剥离 <think>…</think>(推理模型可能把思考混进正文)。
|
||||||
|
"""
|
||||||
|
s = requests.Session()
|
||||||
|
s.trust_env = False # 本机代理 env 会劫持内网直连
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
"temperature": temperature,
|
||||||
|
}
|
||||||
|
last_err = None
|
||||||
|
for attempt in range(retries + 1):
|
||||||
|
try:
|
||||||
|
t0 = time.time()
|
||||||
|
r = s.post(f"{BASE}/v1/chat/completions",
|
||||||
|
headers={"Authorization": f"Bearer {TOKEN}"},
|
||||||
|
json=payload, timeout=timeout)
|
||||||
|
# 429/5xx 属于可重试的服务端瞬时问题
|
||||||
|
if r.status_code in (429,) or r.status_code >= 500:
|
||||||
|
last_err = f"HTTP {r.status_code}: {r.text[:200]}"
|
||||||
|
raise requests.RequestException(last_err)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
content = data["choices"][0]["message"]["content"] or ""
|
||||||
|
content = re.sub(r"<think>.*?</think>", "", content, flags=re.S).strip()
|
||||||
|
usage = data.get("usage", {})
|
||||||
|
print(f"[llm] {model} in={usage.get('prompt_tokens', '?')} "
|
||||||
|
f"out={usage.get('completion_tokens', '?')} "
|
||||||
|
f"耗时{time.time() - t0:.0f}s finish={data['choices'][0].get('finish_reason')}",
|
||||||
|
file=sys.stderr)
|
||||||
|
return content, usage
|
||||||
|
except (requests.RequestException, KeyError, json.JSONDecodeError) as e:
|
||||||
|
last_err = str(e)
|
||||||
|
if attempt < retries:
|
||||||
|
wait = 8 * (2 ** attempt)
|
||||||
|
print(f"[llm] 第{attempt + 1}次失败({last_err[:120]}),{wait}s 后重试",
|
||||||
|
file=sys.stderr)
|
||||||
|
time.sleep(wait)
|
||||||
|
raise RuntimeError(f"LLM 调用重试耗尽: {last_err}")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_json(text):
|
||||||
|
"""JSON 三级容错提取:直接解析 → 首尾括号截取 → json-repair 兜底。
|
||||||
|
|
||||||
|
opus 试拆实测过两类 JSON 病(中文引号、缺逗号)——任何模型都可能犯,统一在此兜住。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return json.loads(text)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
# 剥 markdown 代码围栏后按最外层大括号/中括号截取
|
||||||
|
t = re.sub(r"^```(?:json)?\s*|\s*```$", "", text.strip(), flags=re.M)
|
||||||
|
for a, b in (("{", "}"), ("[", "]")):
|
||||||
|
i, j = t.find(a), t.rfind(b)
|
||||||
|
if i != -1 and j > i:
|
||||||
|
frag = t[i:j + 1]
|
||||||
|
try:
|
||||||
|
return json.loads(frag)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
import json_repair
|
||||||
|
return json_repair.loads(frag)
|
||||||
|
import json_repair
|
||||||
|
return json_repair.loads(t)
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
def cli():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("chat")
|
||||||
|
@click.option("--prompt-file", type=click.Path(exists=True), required=True)
|
||||||
|
@click.option("--model", default=DEFAULT_MODEL, show_default=True)
|
||||||
|
@click.option("--max-tokens", type=int, default=32000, show_default=True)
|
||||||
|
@click.option("--temperature", type=float, default=0.2, show_default=True)
|
||||||
|
@click.option("--out", type=click.Path(), help="内容写文件(不给则打 stdout)")
|
||||||
|
@click.option("--extract-json", "extract_", is_flag=True, help="容错提取 JSON 后输出")
|
||||||
|
def chat_cmd(prompt_file, model, max_tokens, temperature, out, extract_):
|
||||||
|
prompt = pathlib.Path(prompt_file).read_text()
|
||||||
|
content, _ = chat(prompt, model=model, max_tokens=max_tokens, temperature=temperature)
|
||||||
|
if extract_:
|
||||||
|
content = json.dumps(extract_json(content), ensure_ascii=False, indent=1)
|
||||||
|
if out:
|
||||||
|
pathlib.Path(out).write_text(content)
|
||||||
|
click.echo(f"已写 {out}({len(content):,} 字符)", err=True)
|
||||||
|
else:
|
||||||
|
click.echo(content)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
cli()
|
||||||
|
except RuntimeError as e:
|
||||||
|
click.echo(f"[llm错误] {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
@ -5,7 +5,8 @@
|
|||||||
## 角色纪律
|
## 角色纪律
|
||||||
|
|
||||||
- **主会话只编排与裁决,不亲手写创作内容**。设定、大纲、正文、知识卡一律出自 `.claude/agents/` 的智能体;主会话负责组装上下文(经 `read-context`)、派发、校验产出格式、向用户呈报。
|
- **主会话只编排与裁决,不亲手写创作内容**。设定、大纲、正文、知识卡一律出自 `.claude/agents/` 的智能体;主会话负责组装上下文(经 `read-context`)、派发、校验产出格式、向用户呈报。
|
||||||
- 派发子代理必须显式传 model:创作/评审/抽取 = `opus`,纯机械活 = `haiku`。
|
- 派发子代理必须显式传 model:创作/评审 = `opus`,纯机械活 = `haiku`。
|
||||||
|
- **清洗与拆书的内容生产 LLM 全部走 New-API `MiniMax-M3`**(创始人 2026-07-13 拍板):经 `llm` skill 统一入口调用,主会话只固化提示词/skill 与发起调用,不派 opus/haiku 子代理做这两类内容生产。
|
||||||
|
|
||||||
## 双轨纪律(本仓的命根子)
|
## 双轨纪律(本仓的命根子)
|
||||||
|
|
||||||
|
|||||||
10
README.md
10
README.md
@ -56,6 +56,8 @@ agent-example/
|
|||||||
| import | 导入解析:txt→回目正则静态分章→作品/章行入库(调 db) | 导入解析旅程 | B1 交付;C8 用户旧稿复用 |
|
| import | 导入解析:txt→回目正则静态分章→作品/章行入库(调 db) | 导入解析旅程 | B1 交付;C8 用户旧稿复用 |
|
||||||
| embed | New-API 嵌入封装:Qwen3-Embedding-8B、`dimensions:1024`、`--noproxy`、批量+失败重试 | AI 网关(嵌入) | B2 交付 |
|
| embed | New-API 嵌入封装:Qwen3-Embedding-8B、`dimensions:1024`、`--noproxy`、批量+失败重试 | AI 网关(嵌入) | B2 交付 |
|
||||||
| search | 向量检索:意图→embed→pgvector 相似度召回→授权过滤(仅已确认+已绑定)+字段裁剪→带相似度分的结果集 | 知识检索 | B3 交付 |
|
| search | 向量检索:意图→embed→pgvector 相似度召回→授权过滤(仅已确认+已绑定)+字段裁剪→带相似度分的结果集 | 知识检索 | B3 交付 |
|
||||||
|
| llm | New-API LLM 统一调用入口(默认 `MiniMax-M3`):trust_env=False、重试、`<think>` 剥离、JSON 三级容错(json-repair 兜底)、token 用量审计;清洗/拆书等内容生产调用一律经此 | AI 网关(生成) | 已交付(2026-07-13) |
|
||||||
|
| clean | LLM 探测+代码执行的正文清洗:M3 每窗(~10万字)只报待删段逐字原文→四道守卫裁决删除→审计入 example_clean_log;源 txt 不动可回滚 | 导入清洗旅程 | P0/P1 已交付(2026-07-13) |
|
||||||
|
|
||||||
**功能 skill ×9**(prompt 三段式的功能段;每只必带「元数据驱动」节写明 schema 怎么控制它;链登记表见 `meta/chains/README.md`):
|
**功能 skill ×9**(prompt 三段式的功能段;每只必带「元数据驱动」节写明 schema 怎么控制它;链登记表见 `meta/chains/README.md`):
|
||||||
|
|
||||||
@ -221,10 +223,10 @@ flowchart LR
|
|||||||
|
|
||||||
- **A 阶段全收口**:muse-example 库+pgvector 0.8.5(容器重建恢复命令见 `db/连接信息.md`);24 表(主仓一致 20+example_* 5,含 92-清洗日志表;映射见 `db/表映射.md`);23 型 schema 入库(294 字段行,字段级 aiContext 在 visibility_policy.policy_snapshot)。
|
- **A 阶段全收口**:muse-example 库+pgvector 0.8.5(容器重建恢复命令见 `db/连接信息.md`);24 表(主仓一致 20+example_* 5,含 92-清洗日志表;映射见 `db/表映射.md`);23 型 schema 入库(294 字段行,字段级 aiContext 在 visibility_policy.policy_snapshot)。
|
||||||
- **B1 基本收口**:8 本参考书全本入库(希泊尼战纪1496 / 星环使命1722 / 机动风暴673 / 机武风暴686 / 机破星河2250 / 深空之影594 / 超神机械师 / 机战无限2901 章;星环使命为创始人 2026-07-13 新增)。源损坏修复经历:双章区间号「第2373-2374章」、分页尾巴「(第1/1页)」、残破实体「&bp;」、纯中文数字章题+重贴+目录页(机动风暴)——全部进 import 解析器规则。基础面验收工具=`quality_report.py`(字数/分章/章题/段落/垃圾嫌疑扫描)。
|
- **B1 基本收口**:8 本参考书全本入库(希泊尼战纪1496 / 星环使命1722 / 机动风暴673 / 机武风暴686 / 机破星河2250 / 深空之影594 / 超神机械师 / 机战无限2901 章;星环使命为创始人 2026-07-13 新增)。源损坏修复经历:双章区间号「第2373-2374章」、分页尾巴「(第1/1页)」、残破实体「&bp;」、纯中文数字章题+重贴+目录页(机动风暴)——全部进 import 解析器规则。基础面验收工具=`quality_report.py`(字数/分章/章题/段落/垃圾嫌疑扫描)。
|
||||||
- **书单拍板(2026-07-13 创始人)**:先试拆几本书前几章给创始人质检(试拆流水线=workflow 编排+逐章两 pass:scaffold(opus)→ingest(haiku)→patterns(opus)→ingest(haiku);脚本存 session workflows/,状态在 example_parse_task/scaffold/draft 表),质检过再放量。
|
- **M3 拍板(2026-07-13 创始人)**:清洗与拆书的内容生产 LLM **全部改走 New-API `MiniMax-M3`**(llm skill 统一入口,密钥入仓);主会话(Fable5)只固化 agent/提示词/skill 与发起调用。清洗范围=**所有书**;删除前创始人质检样张(门①);**拆书先不放量**(试拆 M3 重做后质检=门②,B2 硬停等新指令)。执行计划:`docs/2026-07-13-M3清洗拆书执行计划.md`。
|
||||||
- **清洗方案拍板(2026-07-13 创始人)**:语义垃圾走「LLM 探测输出待删段原文+代码精确匹配删除」=clean skill(窗口≈10万字/次调用,守卫规则+审计表 example_clean_log;机破星河最脏 879 求票+306 PS,win 文件已备 /tmp/muse-clean/7/)。
|
- **清洗 P0/P1 收口(2026-07-13)**:llm skill(M3 入口+json-repair 三级容错)+ clean_detect(每窗一调、断点续跑)+ clean_apply 四道守卫(长度界/章题/单章20%顶/**行级垃圾特征拆行**——盗版源把广告插进正文句中,M3 会拼「广告+截断正文」,已实测拦住)+ 三级匹配(精确/空白弹性/邻章±1)。实测定标:M3 中文 0.58 token/字,10 万字窗 in≈5.9 万 token 耗时 24s;全语料 ≈340 调用/~19M token。机破星河前 50 章演示已备:40 段/2,188 字待删(dry-run),误删 0。
|
||||||
- **在飞任务(compact 时)**:超神机械师重导(修其 14 个百字以下章);试拆 workflow(机动风暴 1–3 章,ch1 脚手架已过校验:细纲 5.0%/实体 6)。
|
- **opus 试拆基准(workflow 已完成)**:机动风暴 1–3 章,范式卡 2 张入库(ch2/ch3 各 1 拒 1 越合同)+ch1 脚手架;opus 也犯 JSON 中文引号/缺逗号病(已被 llm skill 容错吸收)。此产物只作 M3 对比基准,不算正式拆书。
|
||||||
- **未决**:①试拆结果质检(含范式卡样张)②清洗范围拍板(仅拆书范围 ≈12 次 haiku vs 全语料 ≈350 次)③B2 放量确认闸(编排+成本呈报后创始人拍)。
|
- **未决**:①门① 清洗样张质检(明细在 /tmp/muse-clean/7/dryrun-00*.txt,已呈报)→过门后 P2 全语料放量②门② M3 试拆样张质检(P3 固化后跑 5 本×前 3 章)③B2 放量(硬停,等创始人新指令)。
|
||||||
- **教训入档**:StructuredOutput 工具 schema 属性名仅限 ASCII(中文键 API 400)——schema ASCII 键+入库脚本键名归一;Tailscale 长事务需 keepalive+批量写(逐行两万往返曾半死 16 分钟)。
|
- **教训入档**:StructuredOutput 工具 schema 属性名仅限 ASCII(中文键 API 400)——schema ASCII 键+入库脚本键名归一;Tailscale 长事务需 keepalive+批量写(逐行两万往返曾半死 16 分钟)。
|
||||||
|
|
||||||
## 九·旧(2026-07-10 快照,留档)
|
## 九·旧(2026-07-10 快照,留档)
|
||||||
|
|||||||
75
docs/2026-07-13-M3清洗拆书执行计划.md
Normal file
75
docs/2026-07-13-M3清洗拆书执行计划.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# M3 清洗+拆书执行计划(2026-07-13,待创始人批准后开工)
|
||||||
|
|
||||||
|
> 版本 v1 | 读者:创始人 | 边界:只覆盖「全语料清洗 + 试拆管线 M3 化」,不含 B2 放量拆书(已拍:先不批量拆)
|
||||||
|
|
||||||
|
## 一、拍板回执(本计划的前提)
|
||||||
|
|
||||||
|
| 拍板项 | 结论 |
|
||||||
|
|--------|------|
|
||||||
|
| 清洗/拆书的内容生产 LLM | **全部改为 New-API 的 MiniMax-M3 直调**(已验通:模型在列、JSON 输出干净) |
|
||||||
|
| Fable5(主会话)角色 | 只做四件事:固化 agent/提示词、写 skill、发起调用、守卫验证与呈报——**不亲自生产内容** |
|
||||||
|
| 密钥 | New-API token 明文写入 skill 脚本(仓库政策允许) |
|
||||||
|
| 清洗范围 | **所有书**(8 本 ≈3,300 万字) |
|
||||||
|
| 清洗结果 | **删除落库前必须创始人质检样张** |
|
||||||
|
| 拆书 | 先不批量拆;试拆改用 M3 重做,opus 已产的机动风暴第 1 章留作对比基准 |
|
||||||
|
|
||||||
|
## 二、分工模型
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[Fable5 主会话<br/>固化skill/提示词<br/>发起调用/呈报] -->|每窗一调| B[MiniMax-M3<br/>只当探测器/抽取器<br/>输出JSON]
|
||||||
|
B --> C[脚本守卫<br/>精确匹配/长度界/20%顶<br/>泄漏检查/审计入库]
|
||||||
|
C -->|样张| D[创始人<br/>质检确认门]
|
||||||
|
D -->|放行| C
|
||||||
|
```
|
||||||
|
|
||||||
|
- **M3 永远不直接改库**:清洗它只报「待删段逐字原文」,拆书它只报「脚手架/范式卡 JSON」;落库动作全部由带守卫的脚本执行,LLM 建议 ≠ 必删/必收。
|
||||||
|
- 源 txt 永远不动,任何一步可整书重导回滚。
|
||||||
|
|
||||||
|
## 三、步骤(从最基础开始)
|
||||||
|
|
||||||
|
### P0 — llm skill 固化 + 首窗真跑(我自行决策,无确认门)
|
||||||
|
|
||||||
|
1. 新建 `.claude/skills/llm/`:封装 New-API chat 调用(默认 MiniMax-M3)——超时/重试、`<think>` 剥离、JSON 提取容错、token 用量审计打印。所有后续调用走这一个入口,不裸调。
|
||||||
|
2. clean/parse-book 两个 skill 的文档与参数改口:模型标注 haiku→MiniMax-M3,编排方式从「Workflow 派子代理」改为「skill 脚本直调 M3」。
|
||||||
|
3. **首窗真跑**:机破星河(最脏书)win-001(第 1–39 章,10.1 万字)探测一次 → 验证窗口不超限、JSON 可解析、exact 逐字命中率。
|
||||||
|
- 若 10 万字窗超限或命中率差 → 我自行降窗(如 5 万字/窗,总调用数约 ×2,量级仍可接受),呈报时说明。
|
||||||
|
|
||||||
|
### P1 — 清洗演示(★确认门①:创始人质检)
|
||||||
|
|
||||||
|
4. 机破星河前 ~50 章:M3 探测 → `clean_apply --dry-run` 对账 → **呈报样张**:删什么/理由/次数、守卫拒了什么、我抽查的误伤扫描结论。
|
||||||
|
5. **等质检放行**后该批真删落库,呈报前后对比(字数变化、审计行、quality_report 复扫)。
|
||||||
|
|
||||||
|
### P2 — 清洗放量(所有书;门①过后我自行执行)
|
||||||
|
|
||||||
|
6. 8 本 ≈340 窗 ≈350 次 M3 调用(输入 ~35M token;若降窗则 ~700 次)。断点续跑(manifest 标记已处理窗),书间顺序跑,每书收口报一行。
|
||||||
|
7. 收口交**全局清洗报告**(每书删除段/字数/理由分布/拒绝率/抽查样张 + 垃圾残留复扫对比)→ 创始人复核。此为呈报非阻塞门:源 txt 可回滚,发现异常再处理。
|
||||||
|
|
||||||
|
### P3 — 拆书管线 M3 化 + 试拆(★确认门②:创始人质检样张)
|
||||||
|
|
||||||
|
8. 把 workflow 里的 scaffold/patterns 提示词(含 extractor 身份段、五型合同)移植固化为 parse-book skill 的 M3 直调脚本,状态仍全在库(example_parse_task),断点续跑。**skill 固化可与 P2 放量并行**(不花 M3 调用)。
|
||||||
|
9. 试拆:5 本试拆书(机动风暴/超神机械师/机战无限/深空之影/星环使命)各前 3 章 = 15 章 ≈30 次 M3 调用。机动风暴第 1 章与 opus 基准**并排呈报**(细纲/实体/范式卡样张 + 泄漏检查)。
|
||||||
|
10. **等质检**。质检后呈报 B2 放量编排与成本量级,**硬停等新指令**(已拍:先不批量拆)。
|
||||||
|
|
||||||
|
## 四、确认门汇总
|
||||||
|
|
||||||
|
| 类型 | 事项 |
|
||||||
|
|------|------|
|
||||||
|
| **需创始人确认才继续** | 门① 清洗样张质检(P1→P2 真删与放量的前提);门② M3 试拆样张质检;B2 放量拆书(硬停,等新指令);单章删除超 20% 顶等守卫异常若成批出现(升级呈报) |
|
||||||
|
| **我自行决策** | skill 实现细节(重试/容错/断点续跑)、窗口大小调整、dry-run 与 1–2 次量级的试跑、演示书选择、框架文档同步与「框架:」提交推送 |
|
||||||
|
| **纪律不变** | 库内 LLM 产物一律不 commit;确认=commit 仅创始人指令触发 |
|
||||||
|
|
||||||
|
## 五、成本量级(全部 MiniMax-M3)
|
||||||
|
|
||||||
|
| 事项 | 调用数 | 输入量级 |
|
||||||
|
|------|--------|----------|
|
||||||
|
| P0 首窗试跑 | 1 | ~0.06M token(实测:10万字窗 in=5.9万 out=2k 耗时24s) |
|
||||||
|
| P1 清洗演示(机破前 50 章) | 2 | ~0.08M token(实测收口) |
|
||||||
|
| P2 清洗放量(8 本全语料) | ~340 | **~19M token**(实测 M3 中文 0.58 token/字,比预估砍半;输出极小) |
|
||||||
|
| P3 试拆(5 本×3 章×两 pass) | ~30 | ~0.5M token |
|
||||||
|
|
||||||
|
## 六、风险与回滚
|
||||||
|
|
||||||
|
- **误删正文**:三层防护——M3 只报逐字原文、守卫(4–500 字/不碰章题/单章≤20%)、门① 人工质检;全程审计入 example_clean_log,源 txt 不动可整书重导。
|
||||||
|
- **M3 长窗退化**:P0 首窗真跑先验,退化即降窗,不带病放量。
|
||||||
|
- **M3 拆书质量不如 opus**:试拆并排对比呈报,由创始人裁决质量是否可接受,不预设结论。
|
||||||
@ -4,3 +4,4 @@ psycopg[binary]
|
|||||||
requests
|
requests
|
||||||
pyyaml
|
pyyaml
|
||||||
click
|
click
|
||||||
|
json-repair
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user