Skip to content

Commit 21677b3

Browse files
wehosHongzhi Wenclaude
authored
fix(memory): Stage-2 target_type 修正 log 合并去重,按 (猜→实际) 计数 (#1414)
* fix(memory): Stage-2 target_type 修正 log 合并去重,按 (猜→实际) 计数 Stage-2 LLM 单次调用经常返回 N 条 signal 都对同一 reflection 报告 target_type 不一致(LLM 在猜命名规范:"persona" vs "persona.relationship" vs "persona.relationship.prom",看到啥前缀就抄啥),兜底逻辑按设计在跑 不丢数据,但每条一行 log 在 review 高峰会刷 N 行同样内容。 按 (LLM 报的 → obs 实际) 去重计数,循环结束统一一行汇总: Stage-2 target_type 修正 'persona.relationship'→persona×3 这样既不丢"哪种猜法在被反复纠"的可观测性,又不刷屏。功能完全等价 (兜底依旧以 obs['target_type'] 为准 append 到 validated)。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(memory): guard ttype hashability before进 dict key (codex review #1414) ttype 来自 LLM JSON,理论上是 str/None;如果 hallucinate 成 list/dict 等 非 hashable,直接进 `target_type_fixes[(ttype, ...)]` 的 dict key 会 TypeError 把整个 Stage-2 validation 拖崩。改前的 f-string log 任何值都能 渲染,不会 crash —— 这版让 ttype 加入 hashable 要求是新增 surface。 兜底:非 str/None 用 repr() 转 str。功能等价,且 log 里能看到 LLM 实际 输出的怪东西,方便排查。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 18bedaf commit 21677b3

1 file changed

Lines changed: 24 additions & 5 deletions

File tree

memory/facts.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,12 @@ async def _allm_detect_signals(
883883
# source 落到 evidence 计数器更新里。
884884
new_fact_ids = {f['id'] for f in new_facts if f.get('id')}
885885
validated: list[dict] = []
886+
# 单次 Stage-2 调用可能返回 N 条 signal 都对同一 reflection 报告
887+
# target_type 不一致——LLM 在猜命名规范("persona.relationship"
888+
# vs "persona" vs "persona.relationship.prom"),看到啥前缀就抄啥。
889+
# 兜底逻辑一直按设计在跑,但每条一行 log 会刷屏;按 (LLM值→实际值)
890+
# 去重计数,循环结束后一行汇总,方便看出"哪种猜法在被反复纠"。
891+
target_type_fixes: dict[tuple[str | None, str], int] = {}
886892
for s in raw_signals:
887893
if not isinstance(s, dict):
888894
continue
@@ -914,11 +920,14 @@ async def _allm_detect_signals(
914920
continue
915921
obs = id_to_obs[candidate_full]
916922
if obs['target_type'] != ttype:
917-
# LLM 说的 target_type 与实际不符 → 以实际为准(修正),
918-
# 但仍记录原值便于 debug
919-
logger.info(
920-
f"[FactStore] {lanlan_name}: Stage-2 target_type 修正 "
921-
f"{ttype}{obs['target_type']} for {candidate_full}"
923+
# LLM 说的 target_type 与实际不符 → 以实际为准(修正)。
924+
# 不在 loop 里 log,循环结束统一汇总输出。
925+
# ttype 来自 LLM JSON,理论上是 str/None;hallucinate 成
926+
# list/dict 时直接进 dict key 会 TypeError 把整个 Stage-2 拖崩
927+
# (codex review #1414)。用 repr 把非 hashable 值兜成 str。
928+
key_ttype = ttype if ttype is None or isinstance(ttype, str) else repr(ttype)
929+
target_type_fixes[(key_ttype, obs['target_type'])] = (
930+
target_type_fixes.get((key_ttype, obs['target_type']), 0) + 1
922931
)
923932
validated.append({
924933
'source_fact_id': s.get('source_fact_id'),
@@ -929,6 +938,16 @@ async def _allm_detect_signals(
929938
'signal': signal,
930939
'reason': s.get('reason', ''),
931940
})
941+
if target_type_fixes:
942+
summary = ", ".join(
943+
f"{src!r}{dst}×{n}" if n > 1 else f"{src!r}{dst}"
944+
for (src, dst), n in sorted(
945+
target_type_fixes.items(), key=lambda kv: -kv[1]
946+
)
947+
)
948+
logger.info(
949+
f"[FactStore] {lanlan_name}: Stage-2 target_type 修正 {summary}"
950+
)
932951
return validated
933952

934953
async def aextract_facts_and_detect_signals(

0 commit comments

Comments
 (0)