框架: B4-S1修地基+S4审核环——CONTRACTS漂移冤案修复(prompt与守卫同源,动态渲染库内合同)+system角色分离+审核idx匹配+CRITERIA#8场景走位判定+机械降档;fable独立审查报告落档(S1-S6实施序)
This commit is contained in:
parent
3279cd7974
commit
86c5e2641a
@ -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.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:
|
||||
|
||||
@ -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 自觉
|
||||
|
||||
@ -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:
|
||||
|
||||
34
docs/2026-07-13-B4-fable独立审查报告.md
Normal file
34
docs/2026-07-13-B4-fable独立审查报告.md
Normal 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.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 公共面跨书聚合;不做章级并行化;不给细纲加机械格式校验。
|
||||
Loading…
x
Reference in New Issue
Block a user