"""作用域定位(scope-link):候选值 → 本地树 embedding 余弦最近邻 → top-K。 对得上(高分)→ 复用现有节点原名,ingest 时按名挂靠;对不上 → 保留为新建值(丰富树)。 本地内存 + numpy 暴力最近邻,无需向量数据库。 编程接口: from scripts.scope_link import ScopeLinker ScopeLinker().link("撕裂共识", source_type="作用", top_k=5) 自测:python scripts/scope_link.py """ from __future__ import annotations import json from pathlib import Path import numpy as np from core.embedding import ArkEmbedConfig, embed_text OUT = Path("scope_trees") class ScopeLinker: def __init__(self, env_file: str = ".env") -> None: self.index = json.loads((OUT / "trees_index.json").read_text(encoding="utf-8")) emb = np.load(OUT / "trees_embeddings.npy") self.norm = emb / (np.linalg.norm(emb, axis=1, keepdims=True) + 1e-9) self.src = np.array([it["source_type"] for it in self.index]) self.cfg = ArkEmbedConfig.from_env(env_file) def link(self, candidate: str, source_type: str | None = None, top_k: int = 5) -> list[dict]: q = np.asarray(embed_text(candidate, self.cfg), dtype=np.float32) q = q / (np.linalg.norm(q) + 1e-9) sims = self.norm @ q idxs = (np.where(self.src == source_type)[0] if source_type else np.arange(len(sims))) order = idxs[np.argsort(-sims[idxs])][:top_k] return [{ "name": self.index[i]["name"], "path": self.index[i]["path"], "source_type": self.index[i]["source_type"], "score": round(float(sims[i]), 4), } for i in order] def _selftest() -> None: sl = ScopeLinker() cases = [ ("撕裂共识", "作用"), ("撕裂共识", "意图"), ("三幕结构", "形式"), ("角色代入共鸣", "感受"), ("反转", "形式"), ("引发讨论", "作用"), ] for cand, st in cases: print(f"\n[{cand}] @ {st}:") for hit in sl.link(cand, st, 3): print(f" {hit['score']:.3f} {hit['name']} ({hit['path']})") if __name__ == "__main__": _selftest()