框架: B4-S1修地基+S4审核环——CONTRACTS漂移冤案修复(prompt与守卫同源,动态渲染库内合同)+system角色分离+审核idx匹配+CRITERIA#8场景走位判定+机械降档;fable独立审查报告落档(S1-S6实施序)

This commit is contained in:
zizi 2026-07-13 18:30:47 +08:00
parent 3279cd7974
commit 86c5e2641a
4 changed files with 115 additions and 44 deletions

View File

@ -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 已剥离 <think></think>推理模型可能把思考混进正文
system身份段与任务材料分离角色遵从更稳身份段利于上游缓存
top_p temperature 分化实验用M 家族官方推荐 1.0/0.95eval 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:

View File

@ -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)}
纪律
- 宁缺毋滥一章 03 张为常态只收本章表现突出可跨书复用的写法平庸章可出 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 自觉

View File

@ -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:

View File

@ -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.95M 家族官方推荐,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 公共面跨书聚合;不做章级并行化;不给细纲加机械格式校验。