修复: 关系冲突专用重整后再失败关闭
This commit is contained in:
parent
7f675922b2
commit
a1066eba8b
@ -318,6 +318,8 @@ def relation_prompt(contracts, title, a, b, text, char_cards, existing_rels):
|
||||
{render_entity_contracts(contracts, [RELATION_TYPE])}
|
||||
|
||||
【输出规则(只输出一个 JSON 对象;甲乙用 draft_id 指认)】
|
||||
存储合同规定一对人物只对应一张关系卡:同一无序人物对(甲,乙)与(乙,甲)视为同一对,输出中同一无序人物对最多一条;完全相同的反向重复也不要重复输出。
|
||||
如果同一人物对同时存在多个关系维度,必须合并为一条综合记录,统一写入一个「关系类型」、一个「本窗演变」和同一条「其他字段」对象,**不得按关系维度拆成多条**。
|
||||
{{"关系": [{{"甲方": 数字, "乙方": 数字, "关系类型": "", "本窗演变": "≤60字", "其他字段": {{}}}}]}}
|
||||
|
||||
━━━ 本窗材料 ━━━
|
||||
@ -332,6 +334,24 @@ def relation_prompt(contracts, title, a, b, text, char_cards, existing_rels):
|
||||
{rels}"""
|
||||
|
||||
|
||||
def relation_repair_prompt(contracts, title, a, b, text, char_cards, existing_rels,
|
||||
first_relations):
|
||||
"""构造关系冲突后的唯一一次专用重整提示,要求模型返回可直接替换的完整关系数组。"""
|
||||
first_output = json.dumps(first_relations or [], ensure_ascii=False, indent=1)
|
||||
return f"""{relation_prompt(contracts, title, a, b, text, char_cards, existing_rels)}
|
||||
|
||||
【关系专用重整】
|
||||
首次关系输出如下,其中同一无序人物对出现了内容冲突的重复项:
|
||||
{first_output}
|
||||
|
||||
请结合本窗正文与已有关系,对首次关系输出做一次完整重整后重新输出「关系」数组:
|
||||
- 同一无序人物对最多保留一条关系记录;甲乙反向只算同一对。
|
||||
- 同一人物对的多个关系维度必须合并进同一条综合记录的「关系类型」「本窗演变」「其他字段」,不得拆成多条。
|
||||
- 不得选择性静默丢弃冲突信息,不得按首条或末条覆盖;合并时保留正文有证据的各维度信息。
|
||||
- 除解决上述重复冲突外,仍遵守原有字段合同、正文证据和输出格式纪律。
|
||||
"""
|
||||
|
||||
|
||||
# ── 合并与审计(三类字段演进 + 撤销依据)──
|
||||
|
||||
WIN_PREFIX_RE = None # 延迟编译(模块顶部 import re 已有)
|
||||
@ -2476,9 +2496,30 @@ def _run(work_id, max_windows, max_calls, model, redo_window, semantic_on):
|
||||
rel_out, _ = call(relation_prompt(contracts, title, a, b, text,
|
||||
char_cards, rels), ("关系",))
|
||||
id2name = {d: p.get("名称") for d, p in char_cards}
|
||||
relation_items = _normalize_relation_output(
|
||||
rel_out.get("关系") or [], set(id2name)
|
||||
)
|
||||
raw_relations = rel_out.get("关系") or []
|
||||
try:
|
||||
relation_items = _normalize_relation_output(
|
||||
raw_relations, set(id2name)
|
||||
)
|
||||
except RelationOutputConflict:
|
||||
# 首轮关系冲突只给模型一次关系专用重整机会;重整仍冲突时保持失败关闭,
|
||||
# 交由外层既有整窗重试与停止策略处理,禁止首条/末条覆盖或静默丢弃。
|
||||
repaired_rel_out, _ = call(
|
||||
relation_repair_prompt(
|
||||
contracts,
|
||||
title,
|
||||
a,
|
||||
b,
|
||||
text,
|
||||
char_cards,
|
||||
rels,
|
||||
raw_relations,
|
||||
),
|
||||
("关系",),
|
||||
)
|
||||
relation_items = _normalize_relation_output(
|
||||
repaired_rel_out.get("关系") or [], set(id2name)
|
||||
)
|
||||
exist = {tuple(sorted((r.get("甲方draft"), r.get("乙方draft")))): (rid, r)
|
||||
for rid, r in rels
|
||||
if r.get("甲方draft") and r.get("乙方draft")}
|
||||
|
||||
@ -1306,6 +1306,109 @@ def test_run_conflicting_duplicate_relations_fail_before_any_relation_write():
|
||||
check("relation-冲突重复整窗失败", db.window_status == "failed")
|
||||
|
||||
|
||||
def _run_relation_repair_case(first_relations, repaired_relations):
|
||||
"""执行关系首轮冲突与一次专用重整,返回模型调用和关系写入证据。"""
|
||||
|
||||
characters = {
|
||||
11: {"type": "character", "名称": "甲", "字段": {}, "_work_id": 8},
|
||||
12: {"type": "character", "名称": "乙", "字段": {}, "_work_id": 8},
|
||||
}
|
||||
db = _RelationConn(characters)
|
||||
db.window_status = "pending"
|
||||
db.window_errors = []
|
||||
relation_prompts = []
|
||||
|
||||
def fake_m3_json(prompt, model, need_keys, system=None):
|
||||
if need_keys == ("新名字", "已知实体新信息", "纯出场"):
|
||||
return ({"新名字": [], "已知实体新信息": [], "纯出场": []}, {})
|
||||
if need_keys == ("关系",):
|
||||
relation_prompts.append(prompt)
|
||||
if len(relation_prompts) % 2 == 1:
|
||||
return ({"关系": deepcopy(first_relations)}, {})
|
||||
return ({"关系": deepcopy(repaired_relations)}, {})
|
||||
raise AssertionError(f"不应出现的离线模型调用:{need_keys}")
|
||||
|
||||
name_map = {
|
||||
"甲": (11, "character", ""),
|
||||
"乙": (12, "character", ""),
|
||||
}
|
||||
with patch.object(pu.psycopg, "connect", side_effect=lambda *_: _RelationRunConn(db)), \
|
||||
patch.object(pu, "load_entity_contracts", return_value=_fake_contracts()), \
|
||||
patch.object(pu, "load_window_material", return_value=(
|
||||
"甲与乙的关系发生变化。", {57: "甲与乙的关系发生变化。"},
|
||||
)), \
|
||||
patch.object(pu, "load_known", return_value=(name_map, {}, {}, {11: 0, 12: 0})), \
|
||||
patch.object(pu, "m3_json", side_effect=fake_m3_json), \
|
||||
patch.object(pu, "undo_window") as undo_mock, \
|
||||
patch.object(pu, "_update_relation_card", wraps=pu._update_relation_card) as update_mock, \
|
||||
patch.object(pu, "_insert_relation_card", wraps=pu._insert_relation_card) as insert_mock:
|
||||
pu._run(8, 0, 0, "MiniMax-M3", 0, False)
|
||||
return db, relation_prompts, update_mock.call_count, insert_mock.call_count, undo_mock.call_count
|
||||
|
||||
|
||||
def test_run_relation_conflict_repair_merges_to_one_card():
|
||||
"""首轮同一无序人物对冲突时,关系专用重整合并后只写一张卡并完成整窗。"""
|
||||
|
||||
first = [
|
||||
{"甲方": 11, "乙方": 12, "关系类型": "盟友", "本窗演变": "并肩作战",
|
||||
"其他字段": {"当前状态": "互信"}},
|
||||
{"甲方": 12, "乙方": 11, "关系类型": "上下级", "本窗演变": "接受指挥",
|
||||
"其他字段": {"权力结构": "乙指挥甲"}},
|
||||
]
|
||||
repaired = [{
|
||||
"甲方": 11, "乙方": 12, "关系类型": "盟友兼上下级",
|
||||
"本窗演变": "并肩作战并接受指挥",
|
||||
"其他字段": {"当前状态": "互信", "权力结构": "乙指挥甲"},
|
||||
}]
|
||||
db, prompts, update_calls, insert_calls, undo_calls = _run_relation_repair_case(first, repaired)
|
||||
relation_cards = [
|
||||
row for row in db.drafts.values()
|
||||
if row["payload"].get("type") == pu.RELATION_TYPE
|
||||
]
|
||||
check("relation-repair-首轮冲突后只额外调用一次", len(prompts) == 2,
|
||||
detail=f"calls={len(prompts)}")
|
||||
check("relation-repair-专用prompt包含首轮输出与唯一性纪律",
|
||||
"关系专用重整" in prompts[1]
|
||||
and "首次关系输出" in prompts[1]
|
||||
and "同一无序人物对最多一条" in prompts[1],
|
||||
detail=prompts[1])
|
||||
check("relation-repair-合并后只写一张关系卡",
|
||||
update_calls == 0 and insert_calls == 1 and len(relation_cards) == 1,
|
||||
detail=f"update={update_calls}, insert={insert_calls}, cards={relation_cards}")
|
||||
check("relation-repair-合并后整窗成功", db.window_status == "done",
|
||||
detail=str(db.window_errors))
|
||||
check("relation-repair-成功不触发整窗重试", undo_calls == 0,
|
||||
detail=f"undo_calls={undo_calls}")
|
||||
|
||||
|
||||
def test_run_relation_conflict_repair_conflict_fails_window_without_relation_write():
|
||||
"""关系专用重整仍冲突时不放宽比较,整窗重试后仍零关系写并失败关闭。"""
|
||||
|
||||
conflicting = [
|
||||
{"甲方": 11, "乙方": 12, "关系类型": "盟友", "本窗演变": "继续合作",
|
||||
"其他字段": {"当前状态": "互信"}},
|
||||
{"甲方": 12, "乙方": 11, "关系类型": "对手", "本窗演变": "公开决裂",
|
||||
"其他字段": {"当前状态": "敌对"}},
|
||||
]
|
||||
db, prompts, update_calls, insert_calls, undo_calls = _run_relation_repair_case(
|
||||
conflicting, conflicting
|
||||
)
|
||||
relation_cards = [
|
||||
row for row in db.drafts.values()
|
||||
if row["payload"].get("type") == pu.RELATION_TYPE
|
||||
]
|
||||
check("relation-repair-二次冲突每次只补一次重整", len(prompts) == 4,
|
||||
detail=f"calls={len(prompts)}")
|
||||
check("relation-repair-二次冲突零关系写入",
|
||||
update_calls == 0 and insert_calls == 0 and not relation_cards,
|
||||
detail=f"update={update_calls}, insert={insert_calls}, cards={relation_cards}")
|
||||
check("relation-repair-二次冲突最终整窗失败",
|
||||
db.window_status == "failed" and len(db.window_errors) == 2,
|
||||
detail=str(db.window_errors))
|
||||
check("relation-repair-二次冲突交给既有整窗重试", undo_calls == 1,
|
||||
detail=f"undo_calls={undo_calls}")
|
||||
|
||||
|
||||
def test_run_identical_duplicate_relations_write_existing_card_once():
|
||||
"""完全相同的反向重复项规范成同一无序对,既有关系卡最多更新一次。"""
|
||||
|
||||
@ -2918,6 +3021,10 @@ def test_prompts_disciplines():
|
||||
rel = pu.relation_prompt(c, "测试书", 1, 12, "正文占位",
|
||||
[(10, {"名称": "张三"}), (11, {"名称": "李四"})], [])
|
||||
check("relation-构造器可渲染", "人物关系增量" in rel)
|
||||
check("relation-同一无序人物对最多一条", "同一无序人物对最多一条" in rel)
|
||||
check("relation-多维关系必须合并不得拆条",
|
||||
"必须合并为一条综合记录" in rel
|
||||
and "不得按关系维度拆成多条" in rel)
|
||||
|
||||
# 判重终判 prompt(小样校准后):近邻六元组带字段摘选进证据 + 名称互含判据
|
||||
nbs = [(6223, "power_system", "生物机甲", "机甲体系", '{"境界阶梯":"IV代→V代"}', 0.66)]
|
||||
@ -2944,6 +3051,8 @@ if __name__ == "__main__":
|
||||
test_run_conflicting_duplicate_entity_updates_fail_before_any_write,
|
||||
test_run_relation_cards_enter_touched_embedding,
|
||||
test_run_conflicting_duplicate_relations_fail_before_any_relation_write,
|
||||
test_run_relation_conflict_repair_merges_to_one_card,
|
||||
test_run_relation_conflict_repair_conflict_fails_window_without_relation_write,
|
||||
test_run_identical_duplicate_relations_write_existing_card_once,
|
||||
test_run_prompt_revision_rejects_entity_and_relation_stale_results,
|
||||
test_relation_participants_lock_before_prompt_and_reject_external_drift,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user