From fd1628084d4cb59d407bb1b92bb23001b2ab4e5b Mon Sep 17 00:00:00 2001 From: zizi Date: Wed, 15 Jul 2026 17:08:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A1=86=E6=9E=B6:=20README=C2=A7=E4=B9=9D?= =?UTF-8?q?=E6=89=B96-7=E5=85=A8=E7=BA=BF=E6=94=BE=E9=87=8F=E8=AE=B0?= =?UTF-8?q?=E5=BD=95+compact=E6=81=A2=E5=A4=8D=E9=94=9A=E7=82=B9;=E5=9B=BA?= =?UTF-8?q?=E5=8C=96salvage/rewash=E8=84=9A=E6=9C=AC=E5=85=A5=E4=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../skills/parse-book/scripts/parse_rewash.py | 129 ++++++++++++++++++ .../parse-book/scripts/parse_salvage.py | 111 +++++++++++++++ README.md | 1 + 3 files changed, 241 insertions(+) create mode 100644 .claude/skills/parse-book/scripts/parse_rewash.py create mode 100644 .claude/skills/parse-book/scripts/parse_salvage.py diff --git a/.claude/skills/parse-book/scripts/parse_rewash.py b/.claude/skills/parse-book/scripts/parse_rewash.py new file mode 100644 index 0000000..eb9f9bd --- /dev/null +++ b/.claude/skills/parse-book/scripts/parse_rewash.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""存量回洗(一次性,抽检 2026-07-15 后): +① 追加字段前缀堆叠/[本窗]残留清洗 + 同文去重(34+9 张卡) +② 关系卡顶层演变轨迹迁移进 字段.演变轨迹(双份分裂修复) +③ 出场章全量重建:卡名+别名 机械预扫全书正文(零 AI,比增量准) +④ 脏别名清理:备忘录行/括号注释/错认两行(橡树妖/雷行天下) +⑤ 占位值清理:字段值含「续写不可见」的删字段 +""" +import json +import re +import sys + +import psycopg + +DSN = ("postgresql://root:f6710e2d0294eb1c10e26a805a64bc54@100.64.0.8:5433/muse-example" + "?keepalives=1") +TENANT = 1 +PREFIX_RE = re.compile(r"^(?:\[[窗本][^\]]{0,6}\]\s*)+") # 含 [窗本窗] 等模型自造变体 +WIN_NUMS = re.compile(r"\[窗(\d+)\]") + +# 7 型合同合法 key(越合同 key 清洗用,运行时从库加载) +VALID_KEYS = {} + + +def load_valid_keys(conn): + for (sk, snap) in conn.execute( + """SELECT s.schema_key, v.field_contract_snapshot FROM muse_meta_schema s + JOIN muse_meta_schema_version v ON v.id=s.active_version_id + WHERE s.tenant_id=1 AND s.schema_key IN + ('character','location','item','faction','power_system','event','character_relation')"""): + VALID_KEYS[sk] = {f["key"] for f in snap.get("特有字段", [])} | {"一句话摘要", "演变轨迹"} + + +def clean_list(items): + """剥堆叠前缀+同文去重;窗号取原条目最内层(最后一个),无窗号保持裸条目。""" + out, seen = [], set() + for it in items: + s = str(it) + core = PREFIX_RE.sub("", s).strip() + if not core or core in seen: + continue + seen.add(core) + nums = WIN_NUMS.findall(s[:len(s) - len(core)]) + out.append(f"[窗{nums[-1]}] {core}" if nums else core) + return out + + +def rebuild_presence(chapters, names): + """全书逐章扫描名字集合 → 出场章号列表。""" + hits = set() + for order_no, text in chapters: + if any(nm in text for nm in names): + hits.add(order_no) + return sorted(hits) + + +def main(): + stats = {"前缀清洗卡": 0, "关系迁移卡": 0, "出场章重建卡": 0, "删别名行": 0, "删占位字段": 0} + with psycopg.connect(DSN) as conn: + load_valid_keys(conn) + for work_id in (4, 8): + chapters = conn.execute( + """SELECT c.order_no, 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 + ORDER BY c.order_no""", (TENANT, work_id)).fetchall() + cards = conn.execute( + """SELECT id, draft_payload FROM muse_knowledge_draft + WHERE tenant_id=%s AND work_id=%s AND source_type='upgrade_book' + AND deleted=FALSE""", (TENANT, work_id)).fetchall() + for did, p in cards: + changed = False + fields = p.get("字段") or {} + # ② 关系卡顶层演变迁移 + if p.get("type") == "character_relation" and "演变轨迹" in p: + legacy = p.pop("演变轨迹") + base = fields.get("演变轨迹") or [] + fields["演变轨迹"] = (base if isinstance(base, list) else [base]) + \ + (legacy if isinstance(legacy, list) else [legacy]) + p["字段"] = fields + changed = True + stats["关系迁移卡"] += 1 + # ①⑤ 数组前缀清洗 + 占位字段删除 + 越合同畸形 key 清洗(窗29黄蜂针实测) + vk = VALID_KEYS.get(p.get("type")) + for k in list(fields.keys()): + if vk is not None and k not in vk: + del fields[k] + changed = True + stats["删越合同key"] = stats.get("删越合同key", 0) + 1 + continue + v = fields[k] + if isinstance(v, str) and "续写不可见" in v: + del fields[k] + changed = True + stats["删占位字段"] += 1 + elif isinstance(v, list): + nv = clean_list(v) + if nv != v: + fields[k] = nv + changed = True + stats["前缀清洗卡"] += 1 + # ③ 出场章全量重建(实体卡;关系卡无出场章语义) + if p.get("type") != "character_relation": + names = {p.get("名称", "")} | set(p.get("别名") or []) + names = {n for n in names if n and len(n) >= 2} # 单字名误命中太多,跳过 + if names: + nv = rebuild_presence(chapters, names) + if nv and nv != p.get("出场章"): + p["出场章"] = nv + changed = True + stats["出场章重建卡"] += 1 + if changed: + conn.execute( + "UPDATE muse_knowledge_draft SET draft_payload=%s, updater='rewash' WHERE id=%s", + (json.dumps(p, ensure_ascii=False), did)) + # ④ 脏别名:备忘录/括号/超长 + 抽检定点两行错认 + r = conn.execute( + """UPDATE example_upgrade_alias SET deleted=TRUE, updater='rewash' + WHERE tenant_id=%s AND deleted=FALSE AND ( + alias ~ '[(())。,:]' OR length(alias) > 12 + OR canonical_name ~ '[(())。,:]' + OR alias IN ('橡树妖','雷行天下'))""", (TENANT,)) + stats["删别名行"] = r.rowcount + conn.commit() + print(json.dumps(stats, ensure_ascii=False)) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/skills/parse-book/scripts/parse_salvage.py b/.claude/skills/parse-book/scripts/parse_salvage.py new file mode 100644 index 0000000..ec41cd7 --- /dev/null +++ b/.claude/skills/parse-book/scripts/parse_salvage.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""顽固敏感章分段抢救(创始人 2026-07-15「定点处理遗留」)。 + +原理:内容安全拦截多为全文综合触发——把章正文对半拆成两段,各自独立走 +scaffold 抽取(含 M3→M2.7→deepseek 降级链),机械合并两半结果后走原守卫入库。 +每半的细纲上限=半章正文 5%,合并后自然满足全章 5% 上限(ingest 校验不变)。 +任一半降级链仍全败 → 该章保持 failed,不硬磕(不无限烧额度)。 + +用法:salvage --work-id N 或 salvage --all(跑全部 failed 章) +""" +import json +import sys + +import click +import psycopg + +sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent)) +sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parents[2] / "llm" / "scripts")) +from parse_llm import (m3_json, scaffold_prompt, ingest, SensitiveHardStop, # noqa: E402 + DSN, TENANT) + +MODEL = "MiniMax-M3" + + +def salvage_chapter(conn, work_id, title, ch, ch_title, text, prev): + """对半拆分两段抽取→机械合并→守卫入库。返回 (是否成功, 说明)。 + + 极短章(<1500 字)不拆半:细纲 60 字下限×2 段会超过正文本身(星环#97 实测 + 比例 113.7% 被守卫退回),整章单发重试一次。""" + # 感言/公告章(极短+作者口吻):无正文可拆,诚实标注直接入库,零 LLM(星环#97 实测 116 字感言) + plain = text.strip() + if len(plain) < 500 and any(w in plain for w in ("作者", "感言", "请假", "推荐票", "月票", "补更", "上架")): + ok, out = ingest("scaffold", work_id, ch, + {"outline": "(作者感言/公告章,无正文内容)", "entities": [], "hints": []}) + return ok, "感言章标注入库" if ok else out[-120:] + if len(text) < 1500: + halves = [text] + else: + mid = len(text) // 2 + # 对半点对齐段落边界(往后找最近换行,避免句子拦腰斩) + cut = text.find("\n", mid) + cut = cut if cut != -1 else mid + halves = [text[:cut], text[cut:]] + outlines, ents, hints = [], {}, [] + n = len(halves) + for i, half in enumerate(halves, 1): + seg_note = f"(第{i}/{n}段)" if n > 1 else "" + p = scaffold_prompt(title, ch, f"{ch_title}{seg_note}", half, prev) + try: + data, _ = m3_json(p, MODEL, ("outline", "entities", "hints")) + except SensitiveHardStop: + return False, f"第{i}/2段降级链仍全败" + outlines.append(str(data.get("outline") or "").strip()) + for e in data.get("entities") or []: + if isinstance(e, dict): + nm = (e.get("名称") or e.get("name") or "").strip() + if nm and nm not in ents: + ents[nm] = e + hints.extend(h for h in (data.get("hints") or []) if isinstance(h, dict)) + outline = ";".join(o for o in outlines if o) + # 合并后超 8% 硬顶(降级模型偶超标,星环#1664 实测 11%):按「;」从尾机械砍段到 7% 留余量 + import re as _re + wc = len(_re.sub(r"\s", "", text)) + cap = max(60, int(wc * 0.07)) + while len(_re.sub(r"\s", "", outline)) > cap and ";" in outline: + outline = outline.rsplit(";", 1)[0] + merged = {"outline": outline, + "entities": list(ents.values()), "hints": hints} + ok, out = ingest("scaffold", work_id, ch, merged) + return ok, out[-120:] + + +@click.command() +@click.option("--work-id", type=int, default=0, help="只跑指定书(0=全部)") +@click.option("--limit", type=int, default=0, help="最多抢救几章(0=不限)") +def main(work_id, limit): + with psycopg.connect(DSN) as conn: + cond = "AND t.work_id=%s" % work_id if work_id else "" + rows = conn.execute(f""" + SELECT t.work_id, w.title, c.order_no, c.title, b.content_text + FROM example_parse_task t + JOIN muse_content_chapter c ON c.id=t.chapter_id + JOIN muse_content_block b ON b.chapter_id=c.id AND b.deleted=FALSE + JOIN muse_content_work w ON w.id=t.work_id + WHERE t.scaffold_status='failed' AND c.deleted=FALSE {cond} + ORDER BY t.work_id, c.order_no""").fetchall() + click.echo(f"待抢救 failed 章:{len(rows)}") + saved = failed = 0 + for wid, title, ch, ch_title, text in rows[:limit or None]: + # 前文实体名清单(复用章级压缩口径) + with psycopg.connect(DSN) as conn: + prev = [e for (es,) in conn.execute( + """SELECT s.entities FROM example_parse_scaffold s + JOIN muse_content_chapter c ON c.id=s.chapter_id + WHERE s.tenant_id=%s AND s.work_id=%s AND c.order_no<%s AND s.deleted=FALSE""", + (TENANT, wid, ch)).fetchall() for e in es] + try: + ok, note = salvage_chapter(conn, wid, title, ch, ch_title, text, prev) + except Exception as e: # 单章异常不挡后续章 + ok, note = False, f"异常: {str(e)[:100]}" + if ok: + saved += 1 + click.echo(f" ✓ 救回 {title}#{ch}") + else: + failed += 1 + click.echo(f" ✗ 仍败 {title}#{ch}: {note}") + click.echo(f"=== 抢救收尾:救回{saved} / 仍败{failed} ===") + + +if __name__ == "__main__": + main() diff --git a/README.md b/README.md index d77415d..4ad9a0c 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,7 @@ flowchart LR - **B2 全本拆书·限流分批(2026-07-14 创始人拍板:暂停→分批,每批 ≤3000 次请求、批间隔 5 小时)**:首夜 5 路并行拆至 425–544/书(合计 ~2570 章落库)后按指令暂停。**限流纪律**:请求含重试保守全计,每批按 ~2700 章排(章级=每章 1 次调用+少量病症重试;窗级出卡=每窗 5–8 次);批间隔 ≥5 小时,宁晚勿早。**批1 定时失败→09:09 脱离补跑**:sleep 18000 定时挂在 Claude Code 后台任务上,上会话退出被带走→06:40 未开跑、零请求(额度未亏);改 nohup+disown 脱离会话补跑五路(深空→594/机动→673/超神→1455/星环→1722/机战→827,共 2700 章,断点续跑跳已完成章,日志 /tmp/muse-b1-*.log)。**跨会话根因未除**:本地后台长任务随 Claude Code 会话消亡,彻底可靠需放 mini-desktop tmux/nohup(待定);`pkill -f parse_llm` 模式串会连带杀定时器。**批2**=机战章级收口(→2901)+四书窗大纲续切+余量开出卡;批3+=出卡/终检/审核/样张,每批开跑前按预算精排。**shell 教训**:定时批命令 `A && B & C &` 中 C 不等 A(`&` 分组陷阱),必须 `A && { B & C & wait; }`——首挂曾致 4 书抢跑约 1 分钟,当场杀掉,烧个位数请求。**章级完成后的续跑序**:窗大纲续切(`window --to 末章`——机动已有 win1-37/深空 win1-36/其余至 50,续跑自动从缺口接;换窗参数才需清窗行)→ 分型出卡 `cards --redo 不带`(断点续跑跳已出窗)→ `check` 终检 → `review` 审核 → `export` 样张呈报。 - **B2 停跑·缓存与敏感优化前置(2026-07-14 创始人拍板)**:创始人点破**限次数是果不是因**——每次请求 token 太贵→同配额可用次数缩水→被迫连砍额度(4000→3000→1000→500);省 token=省次数一根链,先前把两者当并列选项是错判。停跑时批2实发280(机动16/深空43/超神81/星环67/机战73),**其中66次(23%)被上游内容安全 new_sensitive(1026) 拦截空耗**(机动/星环暴力战争情节,每敏感章重试3次)。双重漏水:①缓存没吃到(scaffold_prompt/window_cards_prompt 把 title/章号/cap/batch_note 变量前置,破坏 read-context 规定的"稳定度递减吃前缀缓存")②敏感空耗23%。**优化落地前不重启批次**,三前置(均省次数):①缓存重排(固定规则前置、正文/章号/大纲尾置;窗级合同+金标准纪律上千字是最大靶,拆书自拼消息不受"阶段一子代理不能共享缓存"豁免)②拆书补敏感换模型/跳过机制(清洗侧已有先例)③读 token 明细验 M3+New-API 真支持缓存(usage 带 *_tokens_details 疑含 cached_tokens,需1次诊断)。方案待出给创始人。 - **B2 优化落地·批3重启(2026-07-14 实测纠正预判)**:①**真凶是前文实体膨胀,不是缓存**——章级 prompt 把前文实体全档(型+名+摘要)累积重发,深空557章2851实体≈15万token/章占输入97%、随章号雪崩;缓存只缓存"每次一样的固定前缀"、救不了每章都变的实体清单,此路错判。②**压缩(主力)**:实体索引全档json→名字清单(判重只需名字),每章16万→2万token砍86%(深空500实测14万→1.95万,细纲/实体/线索质量正常),同预算次数×7-8,直达创始人目标2-3000。③**缓存诊断**:M3经New-API自动缓存恒命中128token(其内部模板,与我方内容无关);换 Anthropic /v1/messages+cache_control 仍 cache_creation=0(M3上游不支持显式缓存);M2.7/deepseek=0——要吃缓存须换模型,但压缩后缓存仅省固定前缀1k小头,不值当;提示词已缓存重排(固定前置)备将来换模型。④**敏感降级链**:撞 new_sensitive→MiniMax-M2.7→deepseek-v4-flash→全失败硬停,实测 M2.7 救回深空/机动密集敏感章(深空连测3章皆敏感)。**批3=500次自停闸**(wrapper PID精确kill+计数 grep [llm]|[敏感] 含敏感失败请求防超支,哨兵盯收尾)。**教训**:创始人两次纠偏单一归因(省钱=省次数是一根链;缓存/压缩/检索是多重方向非二选一)。检索召回(名字清单再压)留后续。 +- **批6-7 全线放量+质量闭环(2026-07-15,compact恢复锚点)**:批6(2400闸→1211章,敏感救回423,超神/星环/机战大步收口)。批7(创始人放量5000,中途令牌余额见底403中断→充值恢复):**章级 7344/7345 收官**(顽固59章分段抢救救回57、感言章#97/坏章#811诚实标注入库;唯一余量见库);fable5抽检(事实4.4/合同3.8/生长2.8/归并4.0,判"需修后再放量")→**4高2中全修+两轮存量回洗**(前缀去重/出场章全书重扫重建312卡/关系落点统一57卡/覆写回退拦截/越合同key裁剪38处/别名准入);定点修复7调用全过(4卡audit旧值合并/黄蜂针净化剥雷行天下/泰加+白色游魂2对并卡)。**关键机制修复(勿回退)**:max_tokens=512000(创始人拍板不设限;New-API按max_tokens预扣费,余额须≥并发×$0.154否则403)+chat()撞模型上限自适应降档(M2.7上限196608实测)+细纲守卫60字绝对豁免+升格failed窗重跑先撤销+连续2窗失败才停书+模型输出str防御。**compact时在跑(nohup脱离会话,勿重启)**:升格路(机动52+/80窗→深空116窗,日志/tmp/muse-b7-upgrade.log)+出卡双路(5书窗大纲续切→分型出卡,/tmp/muse-b7-cards[AB].log)+全局守护闸4700(/tmp/muse-b7-cards-main.log出STOP行=收尾)。跑完续跑序:check终检→review审核→export样张+升格样张(机动全书+深空末段压测数据)呈报创始人。固化脚本:parse_salvage.py(顽固章分段抢救)/parse_rewash.py(存量回洗)/parse_upgrade.py(作品面升格)。待创始人:样张过目、B3检索验证、B5确认门。 - **批3-5 分批拆(2026-07-14,创始人逐批给额度)**:批3(501次→322章)、批4(1000次→572章,529升5%)、**批5 收尾(2000次自停零超支→1133章:星环250/机战510/超神373;M2.7救回281章,0硬停,529降至1.7%)**。压缩+敏感降级实测有效。**章级全局 5500/7345=75%**:机动673✓/深空593(仅#143)/超神1438(差17)/星环1194/机战1602。**待办**:批6待创始人发额度(剩余约1845章≈2100次可全收口章级)、深空#143顽固敏感(备用模型疑也撞,单独攻)、机动/深空章级已完成可进窗大纲+出卡。续跑序:章级完→window切窗大纲→cards分型出卡→check终检→review审核→export样张。 - **参考书作品面数据入库(2026-07-14 创始人指令+同日认可开工)**:「这几本书本身也是被导入的作品,对应的数据也都需要入库」——**方案 v6 定稿并已批准:`docs/2026-07-14-参考书作品面数据入库方案.md`**。四轮独立评审+创始人五条反馈换核,核心机制=**全实体统一生长**(废v4「前15全卡/轻卡」分层——创始人质疑成立):一实体一卡(唯一键=作品×型×归一名×范围,范围恒「本书私有」),每窗「机械预扫在场实体→实体观察→判重(留档/别名→嵌入近邻→AI终判)→卡更新(只对有新信息,批≤6张)→关系增量」,字段三类演进(底色覆写留审计/演进追加带窗号/当前态保最新);两阶段合并:拆书期候选卡就地累积,创作期走变更提案(draft表entity_id/proposed_changes/snapshot三列原生支持);正文窗直抽(3-5万字/窗,同窗调用连发吃缓存);防膨胀双设计(索引=窗内机械预扫命中集不随书涨/观察输出超12新名或15章对半分段)。**5张升格工作表已建**(`db/ddl/94`:别名/出场留档/窗状态/卡水位/覆写审计)。作品面抽取估3000-4800次,**总额闸5000封顶**,试跑校准;全链从2026-07-14起约5900-8400次。**下一步**:首书机动风暴~10窗+深空末段3窗压测出样张→创始人过目→放量。M3无思考确认(实测reasoning_tokens=0,中位out=331),维持不开(质量已达标,思考按输出计费烧次数,深推理单点用强模型)。 - **未决**:①机动残留变体与感言章处理方式②跨书同功母卡归并=放量后公共面课题③S5 参数 A/B(M3 输出方差根治口)④S6 pacing 书级抽取(等整本拆完,依赖全本+作品面入库)。