|
|
@@ -31,6 +31,13 @@ PHASE2 = (SKILL / "extraction" / "phase2-scope.md").read_text(encoding="utf-8")
|
|
|
GATE_ADMIT = load_prompt("gate_admit") # ①.5 创作判定闸:判"是创作"
|
|
|
GATE_REFUTE = load_prompt("gate_refute") # ①.5:挑刺"是制作/越界"
|
|
|
GATE_TIEBREAK = load_prompt("gate_tiebreak") # ①.5:分歧裁决(边界倾向排除)
|
|
|
+NORMALIZE = load_prompt("normalize_scope") # ③前:作用域值名词化(③LLM)
|
|
|
+
|
|
|
+# ① 窄表确定性兜底:只放无歧义裸动作动词(绝不放 营造/引导/表达 等名词语素)
|
|
|
+_STRIP_VERBS = ("寻找", "定位", "推导", "核验", "提取", "挖掘", "捕捉",
|
|
|
+ "识别", "梳理", "归纳", "判断", "验证", "确认", "复盘")
|
|
|
+_PROTECT = ("营造", "引导", "表达", "塑造", "叙述", "呈现", "刻画",
|
|
|
+ "升华", "控制", "推进", "转化", "传达") # 名词语素,永不砍
|
|
|
|
|
|
# from: fixture(读 tests/fixtures)/ live(实时 crawler 取数)
|
|
|
SOURCES = [
|
|
|
@@ -140,6 +147,39 @@ def scope_candidates(knowledges: list[dict]) -> list:
|
|
|
return chat_json(PHASE2, user, timeout=120).get("scopes") or []
|
|
|
|
|
|
|
|
|
+def strip_verb_tail(v: str) -> str:
|
|
|
+ """① 窄表确定性兜底:砍掉值开头/结尾的无歧义裸动词;砍到 <2 字则回退原值。"""
|
|
|
+ if not v or len(v) < 3:
|
|
|
+ return v
|
|
|
+ for verb in _STRIP_VERBS: # 开头裸动词
|
|
|
+ if v.startswith(verb) and len(v) - len(verb) >= 2:
|
|
|
+ v = v[len(verb):]
|
|
|
+ break
|
|
|
+ for verb in _STRIP_VERBS: # 结尾裸动词(_PROTECT 与之不相交,名词语素天然不在表里)
|
|
|
+ if v.endswith(verb) and len(v) - len(verb) >= 2:
|
|
|
+ v = v[:-len(verb)]
|
|
|
+ break
|
|
|
+ return v
|
|
|
+
|
|
|
+
|
|
|
+def nounify_scopes(scopes: list) -> list:
|
|
|
+ """作用域值名词化:③ 一次批量 LLM 名词化(语义,不误砍"氛围营造")→ ① 窄表兜底。在回扣前做。"""
|
|
|
+ vals = sorted({it["value"] for sc in scopes for it in (sc.get("items") or []) if it.get("value")})
|
|
|
+ if not vals:
|
|
|
+ return scopes
|
|
|
+ try:
|
|
|
+ mp = chat_json(NORMALIZE, json.dumps(vals, ensure_ascii=False), timeout=90).get("映射") or {}
|
|
|
+ except Exception:
|
|
|
+ mp = {}
|
|
|
+ for sc in scopes:
|
|
|
+ for it in sc.get("items") or []:
|
|
|
+ v = it.get("value")
|
|
|
+ if not v:
|
|
|
+ continue
|
|
|
+ it["value"] = strip_verb_tail(mp.get(v) or v) # ③ 映射优先,再 ① 兜底
|
|
|
+ return scopes
|
|
|
+
|
|
|
+
|
|
|
def link_scope(linker: ScopeLinker, scope_type: str, value: str) -> dict:
|
|
|
try:
|
|
|
hits = linker.link(value, source_type=SRC2CN.get(scope_type, scope_type), top_k=3)
|
|
|
@@ -279,7 +319,7 @@ def main() -> None:
|
|
|
if k.get("role") == "组件" and (k.get("parent") or {}).get("how_id") not in how_ids: # 守卫:组件 parent 必指向同帖 how
|
|
|
k["role"] = "主"; k["parent"] = None
|
|
|
print(f" ② {len(knowledges)} 颗:" + ", ".join(f"{k.get('type')}/{k.get('role')}" for k in knowledges))
|
|
|
- apply_scopes(knowledges, scope_candidates(knowledges), linker)
|
|
|
+ apply_scopes(knowledges, nounify_scopes(scope_candidates(knowledges)), linker) # ③名词化+①兜底 → 回扣
|
|
|
print(" ③⑤ 作用域回扣 + 组装")
|
|
|
posts_out.append({**meta, "knowledges": knowledges})
|
|
|
payloads += [build_payload(post, k, how_titles) for k in knowledges]
|