框架: M3管线P0/P1——llm skill统一入口+clean四道守卫三级匹配+执行计划落仓(README§九/CLAUDE.md同步拍板)

This commit is contained in:
zizi 2026-07-13 14:03:36 +08:00
parent 11cdf93347
commit 2b6f83c097
9 changed files with 469 additions and 66 deletions

View File

@ -1,39 +1,39 @@
---
name: clean
description: LLM 辅助正文清洗——LLM 按窗口约10万字只输出待删垃圾段原文与理由代码做精确匹配删除并全程留审计example_clean_log。LLM 不改写正文,删不删由守卫规则最终裁决。
description: LLM 辅助正文清洗——MiniMax-M3 按窗口约10万字只输出待删垃圾段原文与理由代码做精确匹配删除并全程留审计example_clean_log。LLM 不改写正文,删不删由四道守卫规则最终裁决。
---
# clean —— LLM 检测 + 代码执行的正文清洗
分工(创始人方案 2026-07-13**规则层**已在 import 解决结构性垃圾(水印正则/重贴章题/目录页/分页尾巴/残破实体);本 skill 处理**语义垃圾**——变体书站广告、作者拉票/PS 段、乱码水印等正则打不全的散落噪声。LLM 只当探测器(输出待删片段逐字原文),删除由脚本执行:零改写、可审计、可回放。
分工(创始人方案 2026-07-13**规则层**已在 import 解决结构性垃圾(水印正则/重贴章题/目录页/分页尾巴/残破实体);本 skill 处理**语义垃圾**——变体书站广告、作者拉票/PS 段、乱码水印等正则打不全的散落噪声。LLM 只当探测器(输出待删片段逐字原文),删除由脚本执行:零改写、可审计、可回放。探测模型=New-API `MiniMax-M3`(创始人拍板,经 llm skill 统一入口调用)。
## 流程
```bash
# 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) 执行:精确匹配删除 + 审计入库 + 字数刷新(守卫规则见下)
.venv/bin/python .claude/skills/clean/scripts/clean_apply.py --work-id 5 --batch <批次号> --file /tmp/muse-clean/5/deletions-all.json
# 3) 执行:先 dry-run 对账出删除明细(质检样张),确认后去掉 --dry-run 落库
.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) 审查:删除对账(每章删了几段几字+ 审计行抽查
.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"
# 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=7 AND batch='<批次号>' GROUP BY reason"
```
## 探测 prompt 模板(编排层内联给 haiku
## 四道守卫脚本硬闸LLM 建议 ≠ 必删;全部为首窗真跑实测病对症
> 你是网文正文清洗探测器。下面是《书名》第 AB 章的原文(含章题行)。找出**所有非正文垃圾**:书站广告及变体(如「一秒记住♂粒÷小÷说→网」)、网址残留、求票拉票/打赏鸣谢、章尾作者话ps、上架感言类整段、乱码水印。输出 deletions 数组,每项 `{chapter_order, exact, reason}``exact` 必须与原文**逐字一致**含标点空白≤300字一段一项只报垃圾拿不准的不报**情节正文一个字都不许报**。章题行不报(即使含求票字样——章题另行处理)。
1. **长度界**`exact` 4500 字;
2. **章题保护**:不得含/命中所在章章题;
3. **单章 20% 顶**:单章累计删除超章长 20% → 整章回退不落库;
4. **行级垃圾特征**exact 每一行必须命中通用垃圾词ps头/感谢/打赏/求票/域名符号…不含书内专名——盗版源会把广告插进正文句子中间M3 会把「广告+被截断正文」拼成一项,无特征行拆行跳过(宁漏删不误删)。
## 守卫规则脚本硬闸LLM 建议 ≠ 必删)
匹配三级(对症 M3 两类实测病):精确命中 → 空白弹性M3 折叠 `\n\n\n``\n\n`)→ 邻章 ±1 容错(长窗内偶发章号偏移)。三级全失败 → 拒绝。每次删除一行审计(原文/理由/模型/批次),源 txt 永远不动(可整书重导回滚);删完刷新 block/work 字数。
- `exact` 长度 4500 字;在指定章内**逐字命中**才删(命中多次全删并记 occurrences
- 不得命中/包含章题行;单章累计删除 ≤ 章长 20%(超限整章建议弃用并报告);
- 每次删除一行审计(原文/理由/模型/批次),源 txt 永远不动(可整书重导回滚);
- 删完刷新 block.word_count 与 work.word_count。
## 量级实测2026-07-13 首窗定标)
## 量级参考2026-07-13 估)
- 拆书范围5 书×前50章 ≈ 90万字≈ 912 窗 ≈ 12 次 haiku 调用;
- 全语料8 书 ≈ 3,300万字≈ 340 窗 ≈ 350 次 haiku 调用(输入 ~35M token 量级)。
- M3 中文 tokenizer ≈ **0.58 token/字**10 万字窗 in≈5.9 万 token、out≈2k、耗时 ~24s
- 机破星河前 50 章12.9 万字2 窗39 条建议 → 40 段删除(拆行后)/2,188 字,误删 0
- 全语料8 书 ≈3,300 万字)≈ **340 窗 ≈ 340 次 M3 调用,输入 ~19M token**

View File

@ -2,6 +2,12 @@
"""clean skill精确匹配删除执行器LLM 建议 ≠ 必删,守卫规则最终裁决)+ 审计入库。
输入 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 pathlib
@ -17,18 +23,53 @@ TENANT, ACTOR = 1, "1"
MIN_LEN, MAX_LEN = 4, 500
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.option("--work-id", type=int, required=True)
@click.option("--batch", required=True, help="清洗批次号(审计用)")
@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="只对账不落库")
def main(work_id, batch, file_, model, dry_run):
raw = json.loads(pathlib.Path(file_).read_text())
if isinstance(raw, dict):
raw = raw.get("deletions") or raw.get("删除") or []
# 按章分组
# 按章分组(键名 ASCII 为主,兼容中文键)
by_ch = {}
for d in raw:
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
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:
for no, dels in sorted(by_ch.items()):
row = conn.execute(
"""SELECT c.id, c.title, b.id, b.content_text
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.order_no=%s AND c.deleted=FALSE""",
(TENANT, work_id, no)).fetchone()
if not row:
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()):
if no not in chapters:
stats["拒绝"].append(f"#{no}: 章不存在")
continue
ch_id, ch_title, blk_id, text = row
new_text = text
ch_removed = 0
applied = []
for exact, reason in dels:
# 守卫1长度界
if not (MIN_LEN <= len(exact) <= MAX_LEN):
stats["拒绝"].append(f"#{no}: 长度越界({len(exact)}) {exact[:24]!r}")
continue
# 守卫2不得含/命中章题
if ch_title and ch_title in exact:
stats["拒绝"].append(f"#{no}: 含章题 {exact[:24]!r}")
# 守卫4行级垃圾特征——无特征的行=疑混正文
lines = [ln for ln in exact.split("\n") if ln.strip()]
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
n = new_text.count(exact)
if n == 0:
stats["拒绝"].append(f"#{no}: 未命中 {exact[:24]!r}")
if plain:
# 拆行降级:只删有垃圾特征的行,疑正文行跳过并显式报告(宁漏删不误删)
for ln in plain:
stats["拒绝"].append(f"#{no}: 拆行跳过疑正文行 {ln[:24]!r}")
units = [(ln, reason + "(拆行)") for ln in lines
if GARBAGE_LINE_PAT.search(ln) and MIN_LEN <= len(ln) <= MAX_LEN]
else:
units = [(exact, reason)]
for u_exact, u_reason in units:
# 本章两级匹配 → 邻章容错(章号偏移是 LLM 在长窗里的已知病)
hit_no = None
for cand in (no, no - 1, no + 1):
ch = chapters.get(cand)
if not ch:
continue
new_text = new_text.replace(exact, "")
ch_removed += len(exact) * n
applied.append((exact, n, reason))
# 守卫3单章删除比例硬顶
if ch_removed > len(text) * MAX_CH_RATIO:
stats["拒绝"].append(f"#{no}: 累计删除 {ch_removed} 字超章长20%上限,整章放弃")
# 守卫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
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
new_text = re.sub(r'\n{3,}', '\n\n', new_text) # 删除后合并多余空行
stats["删除段"] += len(applied)
stats["删除字数"] += ch_removed
if dry_run:
continue
wc = len(re.sub(r'\s', '', new_text))
ch["text"] = re.sub(r"\n{3,}", "\n\n", ch["text"]) # 删除后合并多余空行
dirty.append((no, ch))
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",
(new_text, wc, ACTOR, blk_id))
for exact, n, reason in applied:
(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_id, batch, exact, n, reason[:200], model, ACTOR, ACTOR, TENANT))
if not dry_run:
(work_id, ch["ch_id"], batch, exact, n, note[:200], model, ACTOR, ACTOR, TENANT))
conn.execute(
"""UPDATE muse_content_work w SET word_count=(
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))
conn.commit()
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]:
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__":

View File

@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""clean skillLLM 探测执行器——每窗一次 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)

View 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 文件调用,内容打到 stdouttoken 用量与重试日志走 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 内 importclean_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模型与维度钉死

View File

@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""llm skillNew-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)

View File

@ -5,7 +5,8 @@
## 角色纪律
- **主会话只编排与裁决,不亲手写创作内容**。设定、大纲、正文、知识卡一律出自 `.claude/agents/` 的智能体;主会话负责组装上下文(经 `read-context`)、派发、校验产出格式、向用户呈报。
- 派发子代理必须显式传 model创作/评审/抽取 = `opus`,纯机械活 = `haiku`
- 派发子代理必须显式传 model创作/评审 = `opus`,纯机械活 = `haiku`
- **清洗与拆书的内容生产 LLM 全部走 New-API `MiniMax-M3`**(创始人 2026-07-13 拍板):经 `llm` skill 统一入口调用,主会话只固化提示词/skill 与发起调用,不派 opus/haiku 子代理做这两类内容生产。
## 双轨纪律(本仓的命根子)

View File

@ -56,6 +56,8 @@ agent-example/
| import | 导入解析txt→回目正则静态分章→作品/章行入库(调 db | 导入解析旅程 | B1 交付C8 用户旧稿复用 |
| embed | New-API 嵌入封装Qwen3-Embedding-8B、`dimensions:1024``--noproxy`、批量+失败重试 | AI 网关(嵌入) | B2 交付 |
| 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`
@ -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
- **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 编排+逐章两 passscaffold(opus)→ingest(haiku)→patterns(opus)→ingest(haiku);脚本存 session workflows/,状态在 example_parse_task/scaffold/draft 表),质检过再放量
- **清洗方案拍板2026-07-13 创始人)**语义垃圾走「LLM 探测输出待删段原文+代码精确匹配删除」=clean skill窗口≈10万字/次调用,守卫规则+审计表 example_clean_log机破星河最脏 879 求票+306 PSwin 文件已备 /tmp/muse-clean/7/
- **在飞任务compact 时)**:超神机械师重导(修其 14 个百字以下章);试拆 workflow机动风暴 13 章ch1 脚手架已过校验:细纲 5.0%/实体 6
- **未决**:①试拆结果质检(含范式卡样张)②清洗范围拍板(仅拆书范围 ≈12 次 haiku vs 全语料 ≈350 次③B2 放量确认闸(编排+成本呈报后创始人拍)。
- **M3 拍板2026-07-13 创始人)**:清洗与拆书的内容生产 LLM **全部改走 New-API `MiniMax-M3`**llm skill 统一入口密钥入仓主会话Fable5只固化 agent/提示词/skill 与发起调用。清洗范围=**所有书**;删除前创始人质检样张(门①);**拆书先不放量**(试拆 M3 重做后质检=门②B2 硬停等新指令)。执行计划:`docs/2026-07-13-M3清洗拆书执行计划.md`
- **清洗 P0/P1 收口2026-07-13**llm skillM3 入口+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
- **opus 试拆基准workflow 已完成)**:机动风暴 13 章,范式卡 2 张入库ch2/ch3 各 1 拒 1 越合同)+ch1 脚手架opus 也犯 JSON 中文引号/缺逗号病(已被 llm skill 容错吸收)。此产物只作 M3 对比基准,不算正式拆书
- **未决**:①门① 清洗样张质检(明细在 /tmp/muse-clean/7/dryrun-00*.txt已呈报→过门后 P2 全语料放量②门② M3 试拆样张质检P3 固化后跑 5 本×前 3 章③B2 放量(硬停,等创始人新指令)。
- **教训入档**StructuredOutput 工具 schema 属性名仅限 ASCII中文键 API 400——schema ASCII 键+入库脚本键名归一Tailscale 长事务需 keepalive+批量写(逐行两万往返曾半死 16 分钟)。
## 九·旧2026-07-10 快照,留档)

View 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第 139 章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 与 12 次量级的试跑、演示书选择、框架文档同步与「框架:」提交推送 |
| **纪律不变** | 库内 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 只报逐字原文、守卫4500 字/不碰章题/单章≤20%)、门① 人工质检;全程审计入 example_clean_log源 txt 不动可整书重导。
- **M3 长窗退化**P0 首窗真跑先验,退化即降窗,不带病放量。
- **M3 拆书质量不如 opus**:试拆并排对比呈报,由创始人裁决质量是否可接受,不预设结论。

View File

@ -4,3 +4,4 @@ psycopg[binary]
requests
pyyaml
click
json-repair