Files
natural-memory-agent-lab/measure_inherit_margin.py

120 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""测量:读取器的 score_margin 能否区分「同一属性的更新」与「另一属性的事实」。
背景:为了让自动层能版本化 Agent 经接口写入的记录,曾尝试用读取器判断"这一轮讲的是哪件事"再继承冲突键;
结果误伤了不相关事实(存了 默认语言=日语 与 项目代号=蓝鲸-47 后,说「我的项目代号是 蓝鲸-47」
却把默认语言那条取代掉)。因此动更新语义之前,先量 margin 分布。
做法(走正在运行的控制面,不额外占卡):
正类 = 先存「我的<A>是 v1」,再问「我的<A>改成 v2」 —— 期望:读取器明确指向那条记录
负类 = 先存「我的<A>是 v1」,再问「我的<B>是 v2」(B≠A) —— 期望:不该指向那条记录
每类各跑一轮,输出 need_memory / stop_reason / margin / 命中是否是那条记录,并给两类区间。
"""
from __future__ import annotations
import argparse
import json
import statistics
import urllib.error
import urllib.request
BASE = "http://127.0.0.1:8766/v1"
# 取自 data/realistic_v2/eval.manifest.json 的 eval 属性族(未出现在训练里)
FAMILIES = [
"主库地址", "仓库库位", "代码仓库", "供应商", "保险到期", "值班电话",
"值班表", "发布窗口", "合同编号", "告警阈值", "周会时间", "培训周期",
]
VALUES_A = ["A-7719", "v3.14.2", "64GB", "B3-204", "91.5%", "C1-105", "D-9021", "v1.9.7"]
VALUES_B = ["v4.0.0-rc1", "128GB", "E-4402", "73%", "F-1180", "v2.0.1", "G-6613", "88%"]
def call(path: str, payload: dict | None = None, method: str = "POST") -> dict:
url = f"{BASE}{path}"
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None
request = urllib.request.Request(
url, data=data, headers={"Content-Type": "application/json; charset=utf-8"}, method=method
)
try:
with urllib.request.urlopen(request, timeout=120) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
return {"__error__": f"HTTP {error.code}: {error.read().decode('utf-8', 'replace')[:200]}"}
def trial(store_attribute: str, store_value: str, query: str) -> dict:
call("/nm2/reset", {})
written = call("/nm2/write", {
"text": f"我的{store_attribute}是 {store_value}。",
"entity": "user", "attribute": store_attribute, "value": store_value,
})
record_id = (written.get("record") or {}).get("record_id", "")
probed = call("/nm2/probe", {"query": query})
if "__error__" in probed:
return {"error": probed["__error__"]}
hits = probed.get("records") or []
return {
"need_memory": bool(probed.get("need_memory")),
"stop_reason": str(probed.get("stop_reason")),
"margin": probed.get("score_margin"),
"top_score": probed.get("top_score"),
"hit_is_stored": bool(hits) and str(hits[0].get("record_id")) == record_id,
"hits": len(hits),
}
def summarise(name: str, rows: list[dict]) -> dict:
margins = [float(r["margin"]) for r in rows if r.get("margin") is not None]
usable = [r for r in rows if "error" not in r]
pointed = sum(1 for r in usable if r.get("hit_is_stored"))
print(f"\n=== {name}({len(usable)} 次)===")
print(f" need_memory 为真: {sum(1 for r in usable if r['need_memory'])}/{len(usable)}")
print(f" 命中就是那条已存记录: {pointed}/{len(usable)}")
if margins:
print(f" margin: 最小 {min(margins):.4f} 中位 {statistics.median(margins):.4f} 最大 {max(margins):.4f}")
print(f" margin 取值: {', '.join(f'{m:.4f}' for m in sorted(margins))}")
reasons: dict[str, int] = {}
for r in usable:
reasons[r["stop_reason"]] = reasons.get(r["stop_reason"], 0) + 1
print(f" stop_reason: {reasons}")
return {"margins": margins, "pointed": pointed, "total": len(usable)}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--rounds", type=int, default=1, help="每个属性跑几轮")
args = parser.parse_args()
positive: list[dict] = []
negative: list[dict] = []
for round_index in range(args.rounds):
for index, attribute in enumerate(FAMILIES):
value = VALUES_A[(index + round_index) % len(VALUES_A)]
other = FAMILIES[(index + 1) % len(FAMILIES)]
other_value = VALUES_B[(index + round_index) % len(VALUES_B)]
# 正类:同一属性的更新("改成")
positive.append(trial(attribute, value, f"我的{attribute}改成 {VALUES_B[(index + round_index) % len(VALUES_B)]}。"))
# 负类:另一个属性的事实
negative.append(trial(attribute, value, f"我的{other}是 {other_value}。"))
pos = summarise("正类:同一属性的更新", positive)
neg = summarise("负类:另一属性的事实", negative)
print("\n=== 判别力检查 ===")
if pos["margins"] and neg["margins"]:
print(f" 正类 margin 区间: [{min(pos['margins']):.4f}, {max(pos['margins']):.4f}]")
print(f" 负类 margin 区间: [{min(neg['margins']):.4f}, {max(neg['margins']):.4f}]")
overlap_lo = max(min(pos["margins"]), min(neg["margins"]))
overlap_hi = min(max(pos["margins"]), max(neg["margins"]))
if overlap_hi > overlap_lo:
print(f" → 两类区间重叠于 [{overlap_lo:.4f}, {overlap_hi:.4f}]:**单靠 margin 无法干净分开**")
else:
print(" → 两类区间不重叠:存在可用门限")
print(f" 命中率:正类 {pos['pointed']}/{pos['total']},负类 {neg['pointed']}/{neg['total']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())