diff --git a/.claude/skills/llm/scripts/llm.py b/.claude/skills/llm/scripts/llm.py index f80cfff..5e74923 100644 --- a/.claude/skills/llm/scripts/llm.py +++ b/.claude/skills/llm/scripts/llm.py @@ -22,19 +22,25 @@ DEFAULT_MODEL = "MiniMax-M3" def chat(prompt, model=DEFAULT_MODEL, max_tokens=32000, temperature=0.2, - retries=2, timeout=900): + retries=2, timeout=900, system=None, top_p=None): """单轮对话,返回 (content, usage)。网络错/5xx/429 指数退避重试。 content 已剥离 (推理模型可能把思考混进正文)。 + system:身份段与任务材料分离(角色遵从更稳、身份段利于上游缓存)。 + top_p:随 temperature 分化实验用(M 家族官方推荐 1.0/0.95,eval A/B 后定版)。 """ s = requests.Session() s.trust_env = False # 本机代理 env 会劫持内网直连 + messages = ([{"role": "system", "content": system}] if system else []) \ + + [{"role": "user", "content": prompt}] payload = { "model": model, - "messages": [{"role": "user", "content": prompt}], + "messages": messages, "max_tokens": max_tokens, "temperature": temperature, } + if top_p is not None: + payload["top_p"] = top_p last_err = None for attempt in range(retries + 1): try: diff --git a/.claude/skills/parse-book/scripts/parse_llm.py b/.claude/skills/parse-book/scripts/parse_llm.py index 8f4f9b4..480657e 100644 --- a/.claude/skills/parse-book/scripts/parse_llm.py +++ b/.claude/skills/parse-book/scripts/parse_llm.py @@ -39,18 +39,40 @@ ENTITY_CRITERIA = ("实体型判据(脚手架级,只要 型/名称/一句话摘 "item=有名字且有跨章戏份的装备/机甲/物品;event=已发生的重大事件(战役/事故/仪式)。" "立卡门槛:有跨章戏份潜力;一次性龙套与单场景道具不收。") -CONTRACTS = { - "combat": {"中文名": "打斗桥段", "判据": "以武力/超自然力分胜负的对抗场景范式 ‖ 非武力博弈(商战/权谋/斗嘴)→scene_pattern;情绪弧主导→emotion", "字段": [ - {"key": "对抗类型", "说明": "斗法/斗宝/阵战/围杀/车轮战/追杀"}, {"key": "开局态势", "说明": "强弱差及其理由——没有落差就没有戏"}, {"key": "节拍结构", "说明": "回合推进——试探/僵持/变数/分胜负各占多少"}, {"key": "反转机制", "说明": "靠什么翻盘——法宝/援军/情报/环境/代价兑换"}, {"key": "代价结构", "说明": "赢者付出什么——白赢是范式的失败"}, {"key": "败者处理", "说明": "死/逃/收服/结仇"}]}, - "craft": {"中文名": "叙事技法", "判据": "单点装置——删去它场景仍成立、读者体验变平 ‖ 承载整场戏→桥段三型;跨章公式→trope", "字段": [ - {"key": "装置类型", "说明": "伏笔/契诃夫之枪/信息差/重复意象/倒计时/身份错认"}, {"key": "埋设手法", "说明": "如何放得自然——挂在什么载体上不显眼又可回指"}, {"key": "履约方式", "说明": "何时、以什么形式兑现;兑现时如何唤起读者记忆"}, {"key": "间隔纪律", "说明": "埋与收的典型距离;太近廉价、太远失效"}, {"key": "失败模式", "说明": "这个装置最常见的用砸方式"}]}, - "emotion": {"中文名": "情感桥段", "判据": "以情绪弧为主体的场景(告白/离别/爆发) ‖ 武力分胜负→combat;关系实体本身→character_relation", "字段": [ - {"key": "情绪类型", "说明": "愤怒/悲怆/温情/羞耻/释然/敬畏"}, {"key": "铺垫结构", "说明": "情绪蓄水的台阶——哪几步把水位抬起来"}, {"key": "爆发点设计", "说明": "临界事件与引爆台词/动作的形态"}, {"key": "收束方式", "说明": "爆发后如何落地——转场/留白/反差"}, {"key": "常见失误", "说明": "这类情绪戏最常写砸的地方"}]}, - "scene_pattern": {"中文名": "通用桥段", "判据": "承载一场戏的通用场景公式(非打斗非情感主导):拍卖/比试/谈判/夜袭/审讯/宴会 ‖ 武力对抗→combat;情绪弧→emotion", "字段": [ - {"key": "场景类型", "说明": "拍卖/比试/谈判/夜袭/审讯/宴会/交易"}, {"key": "参与结构", "说明": "几方角色与各自诉求——冲突的来源"}, {"key": "推进节拍", "说明": "这场戏的标准起承转合"}, {"key": "变数设计", "说明": "打破常规走向的手段"}, {"key": "出口设计", "说明": "各方如何离场,留下什么后续"}]}, - "trope": {"中文名": "套路", "判据": "跨章/跨作品复用的情节公式(扮猪吃虎/废柴逆袭/打脸循环),有可枚举的步骤链 ‖ 单场景装置→craft;单场戏→桥段三型", "字段": [ - {"key": "公式步骤", "说明": "可枚举的步骤链——每步做什么"}, {"key": "适用条件", "说明": "什么设定/关系下这个套路才成立"}, {"key": "变体谱系", "说明": "常见变形与升级版"}, {"key": "失效风险", "说明": "读者疲劳点;用几次会腻"}, {"key": "组合搭配", "说明": "常与哪些套路/桥段连用"}]}, -} +PATTERN_TYPES = ("craft", "combat", "emotion", "scene_pattern", "trope") + +# 基础公共字段(yudao 惯例列,出卡时由 ingest/payload 承载,不进抽取字段表) +BASE_KEYS = {"名称", "别名", "一句话摘要", "标签", "来源", "状态", "例证出处"} + + +def load_contracts(conn): + """从库内 schema 版本快照动态渲染五型合同——prompt 与 ingest 守卫同源。 + + 教训(B4 fable 审查坐实的 bug 级冤案):此前 prompt 手写合同与库内字段名漂移 + (emotion/scene_pattern/trope 三型全对不上),M3 正确产出的 11 张非 craft 卡 + 全部被守卫按另一套字段名冤杀——「prompt 自立合同」违反元数据驱动公理,禁止再犯。 + """ + contracts = {} + for t in PATTERN_TYPES: + snap = conn.execute( + """SELECT v.field_contract_snapshot FROM muse_meta_schema_version v + JOIN muse_meta_schema s ON s.id=v.schema_id + WHERE s.tenant_id=%s AND s.schema_key=%s AND v.deleted=FALSE + ORDER BY v.id DESC LIMIT 1""", (TENANT, t)).fetchone()[0] + fields = [f for f in snap.get("特有字段", []) if f.get("key") not in BASE_KEYS] + contracts[t] = {"中文名": snap.get("中文名", t), "判据": snap.get("判据", ""), + "字段": fields} + return contracts + + +def render_contracts(contracts): + """合同渲染为 markdown 表格形态(比单行 JSON dump 对 LLM 遵从率更友好)。""" + parts = [] + for t, c in contracts.items(): + rows = "\n".join(f"| {f['key']} | {f.get('说明', '')} |" for f in c["字段"]) + parts.append(f"### {t}({c['中文名']})\n判据:{c['判据']}\n\n" + f"| 字段 key | 说明 |\n|---|---|\n{rows}") + return "\n\n".join(parts) def scaffold_prompt(title, ch, ch_title, text, prev_entities): @@ -58,10 +80,7 @@ def scaffold_prompt(title, ch, ch_title, text, prev_entities): # 绝对字数上限比抽象比例对 LLM 更可执行(首轮实测 M3 按比例会写到 9.8%–19%) wc = len(text.replace("\n", "").replace(" ", "")) cap = max(60, int(wc * 0.05)) - return f"""【muse 创作实验台·拆书 2b·脚手架 pass】 -{IDENTITY} - -【功能指令(parse-book 逐章内环)】 + return f"""【功能指令(parse-book 逐章内环)】 对参考书《{title}》第 {ch} 章《{ch_title}》(正文附后,约 {wc} 字): 1) 逆推本章细纲:章目标/关键事件/出场角色/伏笔动作(埋·推·收)/章末钩子。**细纲全文不得超过 {cap} 字**(章正文的 5%;超标会被校验脚本机械退回)。细纲是结构骨架,不是缩写复述——用短语与分号,不写完整句子。 2) 抽实体增量(脚手架级索引):{ENTITY_CRITERIA} @@ -77,14 +96,12 @@ def scaffold_prompt(title, ch, ch_title, text, prev_entities): {text}""" -def patterns_prompt(title, ch, ch_title, text, outline, existing_cards): +def patterns_prompt(title, ch, ch_title, text, outline, existing_cards, contracts): cards = json.dumps(existing_cards, ensure_ascii=False) if existing_cards else "(尚无)" - return f"""【muse 创作实验台·拆书 2b·范式 pass】 -{IDENTITY} - -【功能指令(parse-book 范式拆取+脱敏红线)】 + return f"""【功能指令(parse-book 范式拆取+脱敏红线)】 对参考书《{title}》第 {ch} 章《{ch_title}》(正文附后),在脚手架细纲之上拆「范式五型」卡: -{json.dumps(CONTRACTS, ensure_ascii=False)} + +{render_contracts(contracts)} 纪律: - 宁缺毋滥:一章 0–3 张为常态,只收本章表现突出、可跨书复用的写法;平庸章可出 0 张。 - 归型走判据;五型之外一律不出卡。**先按判据定 type,再只用该 type 字段表里的中文 key 填 fields——混用他型字段=机械拒卡**(如 trope 的「公式步骤」不得出现在 craft 卡里)。 @@ -106,9 +123,9 @@ def patterns_prompt(title, ch, ch_title, text, outline, existing_cards): {text}""" -def m3_json(prompt, model, need_keys): - """调 M3 → 容错提取 JSON → 形状校验;不合格带错误提示重试 1 次。""" - content, usage = chat(prompt, model=model) +def m3_json(prompt, model, need_keys, system=IDENTITY): + """调 M3(身份段走 system 角色)→ 容错提取 JSON → 形状校验;不合格带错误提示重试 1 次。""" + content, usage = chat(prompt, model=model, system=system) for retry in range(2): try: data = extract_json(content) @@ -119,7 +136,7 @@ def m3_json(prompt, model, need_keys): err = str(e)[:200] if retry == 0: content, u2 = chat(prompt + f"\n\n【重试提示】上次输出无法解析({err}),请严格按输出规则只输出一个 JSON 对象。", - model=model) + model=model, system=system) usage = {k: usage.get(k, 0) + u2.get(k, 0) for k in set(usage) | set(u2)} raise RuntimeError(f"JSON 形状重试仍失败: {err}") @@ -147,6 +164,7 @@ def main(work_id, from_, to, model): capture_output=True, text=True) with psycopg.connect(DSN) as conn: title = conn.execute("SELECT title FROM muse_content_work WHERE id=%s", (work_id,)).fetchone()[0] + contracts = load_contracts(conn) # prompt 与 ingest 守卫同源(库内 schema 快照) total_in = total_out = 0 for ch in range(from_, to + 1): with psycopg.connect(DSN) as conn: @@ -209,8 +227,9 @@ def main(work_id, from_, to, model): # pass2 范式(done 跳过;卡被守卫拒属正常,不算失败) if p_st != "done": try: - data, usage = m3_json(patterns_prompt(title, ch, ch_title, text, outline, cards), model, - ("cards",)) + data, usage = m3_json( + patterns_prompt(title, ch, ch_title, text, outline, cards, contracts), + model, ("cards",)) total_in += usage.get("prompt_tokens", 0) total_out += usage.get("completion_tokens", 0) # source 机械回填:book/chapter 是调用侧确定数据,不依赖 LLM 自觉 diff --git a/.claude/skills/review-cards/scripts/review_cards.py b/.claude/skills/review-cards/scripts/review_cards.py index a5f8e61..6da0dac 100644 --- a/.claude/skills/review-cards/scripts/review_cards.py +++ b/.claude/skills/review-cards/scripts/review_cards.py @@ -39,17 +39,23 @@ CRITERIA = """审查判定要点(每张卡都过一遍): 4. 伪精确检测:"隔两章以上"这类数字是从原文数出来的还是编的?编的=危险的 false precision; 5. 同功重复:与本批其他卡是否同一手法换皮(先知降压/面瘫外化这类 family); 6. 干货位检查:失败模式字段是否比手法本身更值钱(是→说明手法总结空洞); -7. 专名泄漏:字段里出现原书人名/地名/数字/梗=破坏跨书复用。""" +7. 专名泄漏:字段里出现原书人名/地名/数字/梗=破坏跨书复用; +8. 场景走位判定:间隔纪律/复用节奏写明「埋收同场景闭环」的,不是跨章装置而是场景内走位, + 可参考性上限 2 分(反例:某"借道具看直播"卡把单场走位包装成范式——金标准 2.67 分)。""" -REVIEW_PROMPT = """你是{role_desc} +# 机械降档(已知弱点双保险,fable 审查回测 19 卡零误伤): +# pass 卡的间隔/节奏字段若声明同场景闭环 → 场景走位而非跨章装置,强制降 revise +SCENE_WALK_PAT = ("同一场景", "同场景内", "一两个段落", "同一场对话", "同一场戏") -背景:muse 是 AI 长篇创作系统,从经典网文拆出「公共范式卡」,未来 AI 写新书时检索这些卡作写作参考。你负责质量审核:内容是否成立、AI 创作时是否可用、是否可参考(跨书复用)。 +SYSTEM_TMPL = """你是{role_desc} -{criteria} +背景:muse 是 AI 长篇创作系统,从经典网文拆出「公共范式卡」,未来 AI 写新书时检索这些卡作写作参考。你负责质量审核:内容是否成立、AI 创作时是否可用、是否可参考(跨书复用)。""" -下面是本批 {n} 张卡(JSON)。逐卡评分(1-5 整数)并给一句毒舌判词。 -输出规则(只输出一个 JSON 对象): -{{"reviews": [{{"name": "卡名", "cheng": 成立性, "yong": 可用性, "can": 可参考性, "verdict": "一句话判词"}}], "overall": "本批系统性问题一句话"}} +REVIEW_PROMPT = """{criteria} + +下面是本批 {n} 张卡(带序号)。逐卡评分(1-5 整数)并给一句毒舌判词。 +输出规则(只输出一个 JSON 对象;idx=卡片序号原样带回——放量后 LLM 复述卡名易漂移,序号是唯一可靠匹配键): +{{"reviews": [{{"idx": 序号, "cheng": 成立性, "yong": 可用性, "can": 可参考性, "verdict": "一句话判词"}}], "overall": "本批系统性问题一句话"}} 【卡片】 {cards}""" @@ -82,22 +88,24 @@ def review(work_id, batch, model, dry_run): if not rows: click.echo("无待审卡") return - cards_json = json.dumps([r[1] for r in rows], ensure_ascii=False, indent=1) + cards_json = json.dumps([{"idx": i + 1, **r[1]} for i, r in enumerate(rows)], + ensure_ascii=False, indent=1) all_reviews = {} for role, desc in ROLES.items(): - content, usage = chat(REVIEW_PROMPT.format( - role_desc=desc, criteria=CRITERIA, n=len(rows), cards=cards_json), model=model) + content, usage = chat( + REVIEW_PROMPT.format(criteria=CRITERIA, n=len(rows), cards=cards_json), + model=model, system=SYSTEM_TMPL.format(role_desc=desc)) data = extract_json(content) - all_reviews[role] = {r["name"]: r for r in data.get("reviews", [])} + all_reviews[role] = {int(r["idx"]): r for r in data.get("reviews", []) if r.get("idx")} click.echo(f"[{role}] 评 {len(data.get('reviews', []))} 卡 " f"(in={usage.get('prompt_tokens')}) 总评: {data.get('overall', '')[:80]}") - # 汇总判定并写库 + # 汇总判定并写库(按序号匹配) stats = {"pass": 0, "revise": 0, "reject": 0} - for cid, payload in rows: + for i, (cid, payload) in enumerate(rows): name = payload.get("名称") scores, per_role = [], {} for role in ROLES: - r = all_reviews[role].get(name) + r = all_reviews[role].get(i + 1) if r: s = [r.get("cheng", 0), r.get("yong", 0), r.get("can", 0)] scores += s @@ -106,6 +114,10 @@ def review(work_id, batch, model, dry_run): continue avg = round(statistics.mean(scores), 2) verdict = "pass" if avg >= 3.5 else ("revise" if avg >= 2.5 else "reject") + # 机械双保险:pass 但间隔/节奏字段声明同场景闭环 → 强制降 revise + fields_text = json.dumps(payload.get("字段") or {}, ensure_ascii=False) + if verdict == "pass" and any(p in fields_text for p in SCENE_WALK_PAT): + verdict = "revise" stats[verdict] += 1 click.echo(f" {verdict:6s} {avg} 《{name}》") if dry_run: diff --git a/docs/2026-07-13-B4-fable独立审查报告.md b/docs/2026-07-13-B4-fable独立审查报告.md new file mode 100644 index 0000000..fb3758b --- /dev/null +++ b/docs/2026-07-13-B4-fable独立审查报告.md @@ -0,0 +1,34 @@ +# B4 收敛环 · fable 独立质量审查报告(2026-07-13) + +> 审查者=fable 子代理(独立上下文);输入=全链脚本+金标准+库内 19 卡/15 章细纲/拒卡记录。 +> 本文件是 B4 实施的 SoT;实施进度见文末勾选。 + +## 裁决摘要(ROI 排序) + +1. **【bug 级】CONTRACTS 漂移冤案**:parse_llm 手写合同与库内字段名漂移(emotion/scene_pattern/trope 全对不上),M3 正确产出的 11 张非 craft 卡全被守卫冤杀(拒卡记录字段名=prompt 教的名字,已交叉验证坐实)。「craft 偏科」一半是冤案。治=从 muse_meta_field/version snapshot 动态渲染,prompt 与守卫同源。 +2. **出卡权上移窗级**:章级只产候选指纹(并入 scaffold pass,省 ~8-9M token),窗级(复用大纲窗切分)聚类归并出母卡+多出处实例;间隔数字由锚点章号差**机械计算**,伪精确灭绝。同功 family/单章证不成 trope/伪精确三病同根同治。 +3. **判重名录废除**:窗内判重=聚类天然解决;跨窗跨书=入库前 embedding 相似(阈值 0.85 起校准)→LLM 归并判定。重跑自噬两补丁随之退役。 +4. **craft 单型双模板**(不拆型):加「装置形态」枚举(结构装置/场景手法),条件必填/禁填——结构装置必填埋点/回收点/记忆维持;场景手法必填复用节奏/异质化/单章上限,禁填间隔纪律。 +5. **审核环修补**:CRITERIA 第 8 条(场景走位判定)+机械降档(间隔/节奏含"同一场景"类→pass 强制 revise),回测 19 卡零误伤,修 №66 假阳。 +6. **LLM 参数**:身份段进 system;范式/大纲线 temperature 1.0/top_p 0.95(M 家族官方推荐,M3 属推断)待 eval n=5 A/B 定版;审核分页 ≤30 卡/批;回写按 idx 不按卡名。 +7. **抽取窗口×型映射**(创始人议题):单章=6 实体型+范式候选hint;窗级=出卡聚类+trope+character_relation;书级=pacing/style。pacing 字段合同 8 字段草案(黄金三章画像/开局策略/上架节奏切换/张力曲线/爽点间隔/文戏武戏配比/钩子密度/例证出处)。 + +## 关键证据 + +- 库内 scene_pattern 合同=目标结构/推进机制/收束方式/信息功能/角色功能位;prompt 教的=参与结构/推进节拍/变数设计/出口设计——拒卡记录中的非法字段名恰好全是后者。 +- M3 pass 面精度是要害:5 张 pass 按金标准只 2 张该 pass,而 pass 直通 B5 授权门。 +- 摘要复制「定位」:19 卡中 17 张摘要=剧情复述的直接来源,prompt 须禁止。 +- 专名机械检查升级:example_parse_scaffold.entities 就是本书专名词典,ingest 加「卡文本命中实体名→拒」。 + +## 实施序与状态 + +- [x] **S1 修地基**:CONTRACTS 动态渲染+system 分离+审核 idx 匹配(2026-07-13 已实施,验证=重跑 15 章范式 pass 非 craft 应存活) +- [x] **S4 审核环**:CRITERIA#8+机械降档(已实施;分页 ≤30 待放量前加) +- [ ] **S2 schema 批**:craft 双模板+装置闭合枚举判定测试+读者收益字段(craft/combat/scene_pattern)+emotion 目标情绪+pacing 8 字段启用 → 改 YAML → seed 重跑 → ingest 条件校验+专名词典+枚举校验+禁复合标签 +- [ ] **S3 管线重构**:scaffold 扩 hints+窗级聚类出卡+embedding 判重+幂等键改 from_order+间隔机械计算;金标准七条全落 prompt(迁移用例字段/占位符白名单/原理「当X做Y因为读者会Z」/触发条件/滥用反例/摘要模板禁复制定位/短名命名) +- [ ] **S5 参数 A/B**:eval n=5,范式线 0.2 vs 1.0(依赖 S3) +- [ ] **S6 pacing 书级抽取**(依赖 S2+整本拆完) + +## 不建议做(防过度工程) + +不拆 craft 为两 target_type;不做三段出卡;不改良名录塞 prompt;不加 frequency/presence penalty;不动审核阈值与三角色;B4 不做 pacing 公共面跨书聚合;不做章级并行化;不给细纲加机械格式校验。