56 KiB
NM2 Agent Lab — 验证结果与发现
本文件记录实跑出来的数字,不写未验证的推测。测量对象:
qwen3_5_4b_natural_memory_v2_1(NM2.1)。 最后更新:2026-09-13。
1. 环境与产物
| 项 | 值 |
|---|---|
| 模型包 | H:\Memory\V2_dpskw\qwen3_5_4b_natural_memory_v2_1(package_label: NM2.1) |
| 路由器 | 已换为 router_replay_v7_v2_128/memory_router_v2.pt(sha256 69f8295e…,128 维 / 8 头,512 字节/记录) |
| 新增子系统 | coverage gate:memory_attribute_head.pt(24 个属性词表)+ memory_coverage_gate: true |
| 模型 server | agent_lab/model_server/server.py,OpenAI 兼容 + 15 个 NM2 端点,运行在 LLM conda 环境 |
| Agent 侧 | agent_lab/agent/(Bun + Pi [email protected] + [email protected]),独立 node_modules |
| 自检 | agent_lab/model_server/selftest.py |
2. 控制面自检:29/29 通过
python agent_lab/model_server/selftest.py --base-url http://127.0.0.1:8766/v1
覆盖并通过的项目:
health/nm2/state/nm2/config/nm2/trace可用;- coverage gate 已加载并绑定到实时记忆库(
configured=True loaded=True bound=True,词表 24 个属性); reset清空;空库上probe正确拒答(stop=below_read_threshold);- 可信写入(
action=inserted);probe命中写入的记录(need=True stop=evidence_found); probe返回完整路由决策(hop、score_margin、hop_trace、coverage);- 版本化纠正:新值 active、旧值 superseded(实测
active=1 superseded=1); - 撤回把记录移出 active;
audit健康(healthy=true issues=[]);- KV 压缩:预算内不动(1800→1800);把
kv_budget_tokens压到 1024 后超预算分块并归档; - 用户隔离:
session save→reset(active=0)→session load恢复该用户记忆; - 配置读写往返 + 非法旋钮被明确拒绝(
unknown knob/out of range); - 无记忆对照组:
memory_mode=off时probe不召回(memory_disabled); - OpenAI 聊天补全 + 原生 tool calling:
finish=tool_calls、tool_calls=['nm2_read']; - 每轮 NM2 诊断随补全返回(前缀 token 数、路由耗时、路由决策、策略概率、压缩报告、coverage 判定)。
3. 验证中发现并修复的 4 个真缺陷
| # | 位置 | 缺陷 | 后果 | 状态 |
|---|---|---|---|---|
| 1 | 模型 qwen_integration.setup_attribute_coverage |
加载了 attribute head,却没有绑定到已构造的记忆库(bank 在模型初始化时就建好,那时 head 还是 None) | NM2.1 的核心拒答机制被加载但从不被调用(configured=True loaded=True bound=False) |
已修(加载后重绑定)并验证 |
| 2 | 模型 memory_os_v2.read(coverage 判定) |
拒答检查硬编码实体名 f"user::{attribute}",而同一文件的 coverage() 用 key.split("::",1)[1](实体任意)——自相矛盾 |
只要库里的实体不是字面量 user,gate 一旦激活就拒绝一切问题,包括它确实持有的属性(实测误拒 100%) |
已修(按属性名跨实体匹配)并验证 |
| 3 | 我的 server probe |
未处理 memory_mode=off |
「无记忆对照组」在探针端点上仍会召回 | 已修(返回 memory_disabled)并验证 |
| 4 | 我的 server update_config |
旋钮写入 memory_config,但 KV 预算在 KVBudgetManagerV2 dataclass 构造时固化 |
旋钮报成功、压缩却永不触发(hot_limit 始终 32768) |
已修(镜像到运行时预算器)并验证 |
4. Agent 侧验证中发现并修复的 5 个 harness 缺陷
| # | 缺陷 | 实证 | 修复 |
|---|---|---|---|
| 1 | nm2_write 强制要求 text |
模型按最自然的方式给 entity/attribute/value → 被拒 "text is required" → activeRecords=0 |
缺 text 时由结构化字段合成记录文本 |
| 2 | 工具动作枚举过窄 | 模型调 nm2_admin{action:"search"}(search 在 nm2_read 里)→ schema 拒绝 → 模型自己编造 record_id → /nm2/correct 500 |
别名放开 + server 把未知 record id 映射为可读 404 |
| 3 | 工具调用无熔断 | 一次交互发出 81 次工具调用、耗时 421 秒,最后上下文退化成 'assistant\nuser\nuser...' echo |
beforeToolCall 预算(6 次)+ 重复调用熔断(2 次)+ agent.abort() 硬中止(terminate 单独不够,模型会另开批次) |
| 4 | nm2_write action=correct 无恢复路径 |
自动遗忘在生成前撤回记录 → 模型的 correct 命中 only active records can be edited → 服务器包成 500 → 模型编造假工具返回并死循环 → 断言 1/3 |
失败时自动降级为结构化写入;服务器把该冲突改为 409 + 恢复提示(详见 §6.1) |
| 5 | memory_mode=off 不是真正的对照组 |
关掉的只是自动路径,显式 nm2_write 仍能写 → 对照组建出记忆并拿到 3/3 |
模式下所有写入端点 409 拒绝(详见 §6.5) |
5. 端到端 A/B 结果(runs/suite7,同模型/同提示/同工具,仅记忆层开关不同)
对照组已核验为空:每一条 OFF 轨迹末尾 activeRecords=0,
且 memory_mode=off 时所有写入端点返回 409(见 §6.5)。
| 场景 | 记忆开启 | 记忆关闭(对照) | 说明 |
|---|---|---|---|
cross_session_recall |
3/3 = 100% | 0/3 = 0% | 会话 1 写入 → 会话 2/3(全新上下文)询问 |
conflict_update |
3/3 = 100% | 1/3 = 33% | 改写旧值后跨会话问新值 |
multi_hop_chain |
2/2 = 100% | 1/2 = 50% | 项目→负责人→工号 两跳 |
unknown_abstention |
3/3 = 100% | 2/3 = 67% | 库里没有的事实必须拒答 |
forget_retraction |
1/2 = 50% ⚠️ | 2/2 = 100% | 记忆组更差——见 §6.6,是真实缺陷 |
| 合计 | 12/13 = 92% | 6/13 = 46% |
记忆层在 5 个场景里赢 4 个,且差距最大的一条(跨会话)是 100% vs 0%。 唯一输的一条是遗忘,原因是记忆层把用户要求忘记的事实留了个"兄弟记录"(§6.6), 这条要看的是记忆层自己的缺陷,不是 agent 的。
决定性的那一条
cross_session_recall 记忆组跨全新会话准确复述两条事实(user;项目代号;蓝鲸-47、部署区域是 ap-southeast-3。),
路由显示 need=True stop=evidence_found 并命中同一条记录;对照组在同一问题下编造 '我的项目代号是:Project Phoenix',
第三个会话甚至去 find / -type d -name "deployment" 满盘搜索。对照组虽然持有文件便签工具、也确实把事实写进了便签,
但它没有回头去读——这正是模型内记忆要解决的问题。
每轮耗时(同一套场景,秒/轮)
| 场景 | ON | OFF |
|---|---|---|
| cross_session_recall | 4.77 | 5.51 |
| conflict_update | 8.09 | 10.64 |
| unknown_abstention | 9.90 | 14.23 |
| multi_hop_chain | 13.79 | 33.50 |
| forget_retraction | 18.11 | 23.02 |
记忆组在全部 5 个场景都更快——因为答案来自注入的证据,而不是让模型反复试 shell 命令。
6. 已定论的问题
6.1 conflict_update 记忆组"写入不生效"——已修复(1/3 → 3/3)
(此前记录的"quarantine 未被提升"是错误推测,已作废。)
6.1 根因:自动遗忘在生成之前就撤回了记录,而工具层没有恢复路径
用静态复现(只跑 ON 组、跑完立刻查库、中间不做任何 reset)稳定复现 1/3,然后逐层定位:
/nm2/correct对一条确实处于 active 的记录单独调用时完全正常: 写日语→correct→ 返回version=1的新记录、旧记录superseded。控制面本身没有缺陷。- 差别在于场景里那条记录是自动记忆路径写入的(
source: "automatic"), 而第 1 轮用户说的是「更正一下:我的默认语言改成 葡萄牙语,日语作废」。 - 模型服务每轮的顺序是
_write_turn(...)(server.py:487)→ 生成(:513)。 而_write_turn内部包含自动遗忘逻辑(qwen_integration.py:2166):auto_forget_probability >= auto_forget_threshold(实测该轮为 0.985 ≥ 0.9)时, 按本轮文本_forget_text_memory_by_key(...)把日语记录撤回。 - 这一步是 NM2 的正确行为——用户说了「作废」。
但它在模型生成之前执行,于是模型这一轮手里那个上一轮学到的
record_id已经变成retracted。 - 接下来模型做的是最自然的事:
nm2_write{action:"correct", record_id:<已撤回>}→only active records can be edited→ 而服务器把它包成 HTTP 500,且错误文本不含任何恢复指引。 - 4B 模型拿到 500 后不会自己恢复:先编造一段假的工具返回(伪造
record_id: "mem_abc123"与 JSON), 然后反复nm2_admin search直到被硬熔断中止 → 新值从未落库 → 会话 2 读到空前缀(prefix_tokens=0)→ 答中文。
6.2 修复(两处,均已实测验证)
| 位置 | 修复 |
|---|---|
agent_lab/agent/src/tools.ts |
action=correct 带 record_id 失败时,自动降级为结构化写入(entity/attribute/value)并把 recovered_from 写进工具返回;action=correct 不带 record_id 时也直接走结构化写入(NM2 本来就会按 entity::attribute 自动版本化,强制要 id 只是给模型挖坑)。另把缺省 entity 补成 user,与记忆层自己写的记录对齐 entity::attribute 冲突键。 |
agent_lab/model_server/server.py |
领域冲突不再包成 500:only active records can be edited → 409 record_not_active,并附 hint 指明恢复调用;其余 ValueError → 400。 |
6.3 修复后的实测(runs/conflict_fixed)
turn0 写 日语 → active=1
turn1 “改成葡萄牙语,日语作废”
├ 自动遗忘撤回 日语(正确行为)
├ nm2_write{correct, record_id=日语} → 透明降级为结构化写入 → 新记录 active=1
└ nm2_write{retract, 日语} → 幂等
最终:active=1(葡萄牙语) retracted=1(日语)
s2(全新会话)问“我的默认语言是什么?” → 葡萄牙语 ✓
断言 3/3 (100%),每轮 22.31s → 8.64s
6.4 尚未定论
模型在工具报错后不会自行恢复、倾向重复同一调用(已用硬熔断 + 让工具不再报错两条路兜住, 但根因仍在模型侧)。下面 §6.5–§6.7 是后续定位出来的三个新问题。
6.5 【方法论缺陷,已修复】memory_mode=off 曾经不是对照组
修改系统提示词、让 agent 主动使用记忆工具之后,对照组在 cross_session_recall 拿到 3/3、activeRecords=2。
原因:memory_mode=off 只关掉了自动路径(证据前缀注入 + 自动写入),
而 agent 显式调用 nm2_write / nm2_read 依然能写能读 —— 于是"无记忆"那一组悄悄又有了记忆。
证据:OFF 组轨迹末尾 activeRecords=2/1/2,本应恒为 0。
修复:memory_mode != "on" 时,所有写入类端点(write/correct/retract/approve/compact/session/memory)
一律 409 拒绝;read_only 同样拒绝写入;模式切换与 reset 不受影响。
读路径保持开放——写入既已全堵,库必然为空,留着读接口正好让诊断证明这一点。
修复后核验:off 与 read_only 下 nm2/write 与 nm2/correct 均返回 409;
probe 返回 stop_reason=memory_disabled;records 返回 0;
suite7 每条 OFF 轨迹 activeRecords=0。控制面自检仍 29/29。
教训:"关掉记忆"必须在每个端点上成立。只关自动路径、留着显式工具, 得到的不是对照组,而是"另一种用记忆的方式"。
6.6 【已修复 2026-09-15】遗忘从"按 key"改成"按轮次"
原缺陷(实测):forget_retraction 记忆组不如对照组(1/2 vs 2/2),失败方式是泄露:
turn1 用户:记一下:我的紧急联系人是 王工,电话分机 7781。
→ 落库两条 active 记录:紧急联系人=王工、电话分机=7781
turn2 用户:请遗忘我的紧急联系人信息,不要再保留和使用它。
→ 自动遗忘只撤回了 key 命中的那一条(紧急联系人)
→ 库:active=1(电话分机=7781 仍在) retracted=1
turn3 用户:我的紧急联系人分机是多少?如果已经不保留这条信息,就直接说明。
→ 路由把仍 active 的电话分机记录注入了上下文
→ 模型回答:"您的紧急联系人分机是 7781。" ← 泄露
机制:撤回是按 key/文本相似度匹配的(_forget_text_memory_by_key /
_forget_text_memory_by_metadata),同一句话/同一轮写入的其余记录不在匹配范围内。
修复(已实施,改动在 V2_dpskw):给记录加 origin(该记录来自的那一轮用户话的短哈希),
撤回时先按原逻辑命中目标记录,再 retract_origin(target.origin) 把同一轮写入的全部记录一起撤回。
| 文件 | 改动 |
|---|---|
V2_dpskw/memory_os_v2.py |
MemoryRecordV2.origin 字段;write(origin=);edit_record 继承 origin;新增 PagedMemoryBankV2.retract_origin() 与 MemoryOSV2.retract_origin();memory_record_to_dict 暴露 origin |
V2_dpskw/qwen_integration.py |
新增模块级 memory_origin(text);自动写入带 origin=;两个遗忘路径命中后追加 retract_origin() |
V2_dpskw/tiered_memory_store_v2.py |
关键:SQLite 表有独立列清单,加字段不会自动持久化 → 补 origin 列(schema / _record_values / INSERT / load_records / 隔离区 payload),并对已有库做 PRAGMA table_info 检测 + ALTER TABLE ADD COLUMN 原地迁移 |
agent_lab/model_server/server.py |
每轮记住 _turn_origin,显式 nm2_write 带上它——否则 agent 逐字段写出的记录与自动记录分属不同组,实测的那个泄露就修不掉 |
V2_dpskw/tests/test_memory_origin_forget.py |
新增 9 个 CPU 回归测试 |
验证(CPU,不占显卡):新增 9/9 通过,覆盖:同轮兄弟记录一起撤回、别的轮次不受影响、
origin 为空的历史记录永不匹配(retract_origin("") 是 no-op)、版本化继承 origin、
asdict 往返 + 缺 origin 的旧 payload 仍可加载、经 SQLite 存储往返后 origin 不丢、
旧库(无 origin 列)打开时被原地迁移、MemoryOSV2 门面转发。
既有测试套件 61/61 通过(python -m unittest discover -s V2_dpskw/tests -t .)。
尚未做(需要显卡,等你的作业跑完):端到端复测 forget_retraction 两个组,
确认泄露消失且其它场景不回归。
已知残留风险(如实报告):轮次粒度是策略选择——若同一轮里除了被撤回的事实,
还说了无关的事,那件无关的事也会一起被撤回(例如"我的编辑器是 VSCode,紧急联系人是王工"
之后说"忘掉紧急联系人",两条都会没)。要收紧的话,可以只在"同一轮 + 属性/取值相关"时成组,
抓手是记录里已有的 token/input_sha1 证据;本次先做整轮,因为用户说的"忘掉这条信息"
在实测样例里指的正是整句话。
尝试过但无效的做法(记录以免重蹈):在系统提示词里写清
"search → 逐条 retract → 再 search 核验"的多步协议。
结果 4B 模型卡在第一步:连发 13 次 nm2_read action=search 直到预算熔断,一次 retract 都没发,
该场景从 1/2 掉到 0/2。已回退。
结论:4B 模型执行不了多步协议,这个洞必须由记忆层自己补。
6.7 另一处已修复的 harness 缺陷:agent 不去读记忆,改用 shell 满盘搜
修复前的证据(runs/suite5):
multi_hop_chain:路由已经把全部 4 条记录(含两跳所需的双方)注入上下文(prefix_tokens=324), 库中active=4,答案所需的证据就在提示词里——模型却去执行run_shell find / -name "*星尘*"。forget_retraction:路由正确返回stop=below_read_threshold(确实没有可召回的东西), 模型却去grep文件系统找"紧急联系人"。
根因是 harness:原默认系统提示词一个字都没提 NM2(只有 "You are a test agent operating a small workspace")。
已改为 NM2 感知的提示词:说明事实存在记忆层、【长期记忆证据】 块是权威的、
回答此类问题前先 nm2_read action=probe、不要用 find/grep 去找关于用户的事实、
没有证据就明说没有。两臂使用完全相同的提示词,所以 A/B 仍然成立。
效果:multi_hop_chain 记忆组 1/2 → 2/2,cross_session_recall 的 ON 组每轮耗时降到 4.77s。
6.8 【真实缺陷,已修复】更正只传 value 时静默失效:模型继续答旧值
发现方式:在 NM2.0 上用「我叫Wpy → 不对我叫王五」试 TUI 时,干净进程里被问「我叫什么名字」,
路由弃权、未注入证据,模型却答出已被取代的旧值 Wpy。逐层排查后定位到与路由无关:
根因:模型读的是记录的存储文本(record.text 与它的 token 缓存),不是结构化字段。
而自动层写入时把文本存成按当时取值预先渲染好的证据卡片:
【长期记忆证据】
事实:我的名字是 Wpy
已确认值:Wpy ← 说 Wpy
可直接复述的关键短语:user;名字;Wpy ← 又说一遍
/nm2/correct 只传 value=王五 时,edit_record 更新的是 value 字段、保留旧 text,并继承旧 token_ids。
于是卡片里「已确认值」三处全是旧值,模型照着念 → 答 Wpy。
接口全程报告成功(corrected:true、版本升到 v1、旧记录 superseded、审计 healthy),
只有答案是旧的,从外部完全看不出来。
为什么以前没暴露:项目自带的测试与 Agent 工具都同时传 text(text="用户喜欢绿色" + value="绿色"),
所以这条路径从没被测到。我查过全部脚本,edit_record 只被 natural_memory_service 与测试调用,
训练/评测脚本不碰它,因此既有评测数字不受影响。
修复(按"少外部参数、让模型自己负责"的原则,不加任何开关、不给调用方加义务):
| 文件 | 改动 |
|---|---|
V2_dpskw/qwen_integration.py |
新增 memory_fact_sentence()(从卡片里取出原句)与 compose_corrected_evidence():由改后的字段重建整张卡片;并把原句里的旧值就地替换(保留自然措辞,与模型自己走 Agent 路径时写的「我的名字是 王五」一致)。无字段变更、或记录缺结构化身份时返回 None,保持原行为。 |
V2_dpskw/natural_memory_service.py |
只传字段的更正:先重建卡片,再照常编码 token(token 缓存随之更新)。 |
agent_lab/model_server/server.py |
同一处理,保证两个入口行为一致。 |
核心思路:记录不可能再自相矛盾——同一事实原来存了两遍(字段一遍、烤进 text 一遍),改一边就会漂; 现在改字段必然重建它自己的卡片。这加的是不变量,不是控制参数。
验证:
- CPU 测试 88/88 通过(新增 5 项:旧值在卡片各处消失且措辞自然、旧值不在句中时的回退、无字段变更返回 None、缺身份返回 None、Agent 写的普通句子记录)。
- 端到端最小复现(NM2.0):自动记住「我的名字是 Wpy」→ 只传
value=王五更正 → 两种问法(我的名字是什么/我叫什么名字)都命中更正后的记录并答 王五;修复前答 Wpy。
顺带观察(未改):修复后回答形如 王五;user;名字;王五——这是项目前缀里
「可直接复述的关键短语」+「回答要求:…并原样复述上面的关键短语」的设计所致,属既有行为。
如果它影响你的评测判分口径,可以再看。
6.9 外部参数面收敛:判断权还给模型(2026-09-15,按用户方针)
用户方针原话:「我希望更多配置由模型决定,外部不要传入那么多参数(必要的保留)」。 判据:凡是模型自己已经学过的判断,就不该由调用方传数字。据此收敛面向 Agent 的参数面:
| 原来外传的参数 | 处置 | 理由 |
|---|---|---|
nm2_write.importance / confidence |
删除 | 检查点里本来就有学到的 importance 头(AutomaticMemoryPolicyV2.importance,从该轮隐状态打分)。让模型手填 importance: 0.9,等于把「这轮值不值得记」变成外部常数。 |
nm2_admin.config_set:read_threshold / min_read_margin / require_evidence |
删除 | 路由器已有 need_memory 头与 margin/confidence 输出,阈值只是覆盖它。 |
同上:auto_memory_threshold / auto_forget_threshold |
删除 | 自动策略是学到的(auto_memory_probability / auto_forget_probability)。 |
同上:max_hops |
删除 | 路由器已有 hop_controller 头。 |
同上:coverage_gate / coverage_vocabulary_fraction |
删除 | 覆盖门由学到的属性头驱动。 |
同上:top_k_pages / top_k_records / hot_pages / write_threshold |
删除(Agent 面) | 检索与分层预算属记忆层自己的判断。 |
kv_budget_tokens / kv_keep_recent_tokens / context_chunk_tokens / gpu_cache_records / gpu_cache_tokens |
保留 | 机器硬约束,模型无从知晓 —— 属「必要」。 |
nm2_read.limit / status |
保留 | 只是显示条数与过滤,不改变记忆行为。 |
服务端 write_record 私自补的 importance=0.9 / confidence=0.99 |
删除该默认值 | 调用方没给就落到记忆层自己的门槛,而不是服务端编一个常数。 |
被拒绝时会明确报错(而不是静默丢弃):模型若以为自己改动了路由阈值,会一直那么以为。
注意:HTTP 的 /v1/nm2/config 仍保留全部旋钮——那是你研究脚本在用的(门槛扫描等)。
收敛的是 Agent 能看到、能传的参数,不是把研究工具砍掉。
实测(NM2.0):不带 importance/confidence 的写入 → inserted、importance=0.5(层默认)、可召回(score 8.661);
Agent 侧两次写入均为 importance=0.5 且召回正常(命中 user|默认语言 = 葡萄牙语 score 8.640)。
类型检查通过、CPU 测试 88/88。
仍未做(需你定):显式写入拿不到该轮隐状态,所以「让 importance 头打分」的理想路径目前只有自动层走得通; 要让显式写入也由模型打分,得把写入挪进当轮生成路径,属较大改动。
本次试跑顺带发现的同族新问题:同一句「我叫Wpy,默认语言是 葡萄牙语。」下,Agent 写的属性是 默认语言,
自动层抽出的却是 语言——键不同(user::语言 vs user::默认语言)所以不会互相取代,
库里于是留下同一事实的两条生效记录。后果与 §6.6/§6.8 同族:将来只更正其中一个名字,另一个仍会带旧值被检索。
可能的修法是把「同轮吸收」推广为:同一轮里模型自己写下的结构化记录,取代该轮的自动记录(不论自动记录有无身份)。
代价:若一轮说了两件事而模型只写了一件,被取代的原始记录不再被检索(仍留在库里可审计)。
属语义变更,等你点头再动。
6.10 生产级实测(NM2.0,真实任务):当前不可日用 —— 诚实结论(2026-09-15)
用户要求:「给这个模型手动做完整测试,包括真生产任务,这不是一个表在相框里的勺子,而是摆在抽屉里的——每天都要用」。 于是拿这台机器上有客观基准可对的真活来试,不用合成场景。基准先自己量好,避免用模型输出去验证模型。
基准:H:\Memory\V2_dpskw 下 .py 文件 126 个(含子目录);C 盘可用 34.45 GB;
WRITE_PATH_FIX.md 存在、4023 字符。
第一轮(修复前)
| 任务 | 结果 | 判定 |
|---|---|---|
| 统计 .py 文件数 | 一次 Get-ChildItem -Recurse -Filter *.py | Measure-Object → 126 |
✅ 对,5.4 秒 |
| C 盘可用空间 | 先编 Get-CDisk(不存在);再写 Where-Object {$_.DriveLetter -eq 'C:'} → 成功但零输出;随后同一调用重发 10+ 次,62 秒,助手文本退化成 assistant\nassistant… |
❌ 彻底失败 |
读 WRITE_PATH_FIX.md 说根因 |
内容答对了(「confirmed_update 依赖学习打分 0.95、缺 (entity,attribute) 结构校验 → 误删」),但把上一条失败命令又重发了 5 次,99 秒,尾部乱码 |
⚠️ 内容对、过程崩 |
| 「昨天买的股票涨了多少」 | 「我没有你购买的股票信息,无法回答」 | ✅ 诚实拒答(但耗时 50 秒,因上下文已被污染) |
根因查实(不是推断):Get-Volume 的 DriveLetter 是字符 'C',
'C' -eq 'C:' 永远为假 → 命令成功、零输出。实测确认:-eq 'C:' 输出长度 0,-eq 'C' 正常输出。
模型分不清"成功没数据"和"还没查到",于是死循环;一次静默失败污染整段会话。
第二轮(我加了三处补丁之后)
补丁:① run_shell 零输出时明确解释「命令成功但没打印任何东西,多半是过滤条件没匹配上,不要再发同一条」;
② 熔断改为同步 agent.abort();③ 熔断后替换掉被污染的助手消息(会话卫生)。
| 任务 | 结果 | 判定 |
|---|---|---|
| C 盘可用空间 | 答「100 GB」——编造(真值 34.45 GB) | ❌ 更糟 |
| 统计 .py 文件数 | 外层又套 powershell -NoProfile -Command "…$_…",$_ 被外层吃掉(.DeviceID : The term ... is not recognized),死循环 → 中止 |
❌ |
| 再问 C 盘 | 又编「100 GB」,并把前几轮对话原文整段复读 | ❌ |
补丁不足,如实记录:③ 的会话卫生显然没生效(下一轮仍在复读旧对话,需查 agent.state.messages 赋值是否真被 Pi 采纳);
② 同步 abort 生效了(日志出现 Operation aborted),但仍不足以救回回合。
并且暴露一个新的、更危险的失败模式:拿不到值时编造具体数字。
日用结论(当前)
- 能用的:单步、命令正确、结果非空的任务(教一遍、答对、5 秒级)。
- 不能用的:任何需要"命令写错→自己发现→换写法"的任务;一次静默失败即毁掉整段会话;拿不到值时会编造数字。
- 对一个"每天都要用"的工具,这三条都是硬伤。
第三轮(窄接口工具 + 编造检测,2026-09-15 目标轮次 2)
依据是前两轮反复撞同一面墙:卡住真活的不是记忆,是模型手写命令行。于是:
- 新增窄接口工具(
tools.ts):disk_space {path}与list_files {path, pattern?, recursive?}, 由 Node 直接实现(fs.statfs/fs.readdir),模型只填路径、不拼命令;提示词里明确要求这两类问题走它们。fs.statfs实测与 PowerShell 完全一致(34.62 GB)。 - 编造检测(
agent.ts+memory.ts+tui.ts):问题在问数量、回答里有数字、而本轮一次工具都没调 → 标为unverifiedQuantity,TUI 打红色警告。这是检查器不是修复,目的是不让编造冒充测量结果。
| 任务(有客观基准) | 前两轮 | 本轮 |
|---|---|---|
| C 盘可用空间(真值 34.62 GB) | 编「100 GB」;再前一轮死循环 62 秒 | disk_space{C:\} → 34.64 GB ✓ 一次调用 |
E:\deepseek\artifacts 文件数(顶层真值 46) |
死循环、无答案、64.6 秒 | list_files{path} → 46 ✓(工具自报"仅顶层") |
| 跨会话记忆:新进程问「报告放哪」 | 召回成功但干活被 shell 挡住 | 召回成功(命中 2 条 · 205 词元)+ 目录答对 ✓ |
新暴露的失败(已两次独立复现,且可稳定重放):模型对"数量"类问题会跳过工具直接给数字—— 问文件数答 0(真值 46)而一次工具都没调;上一轮问磁盘答 100 GB。 提示词里"不要猜任何没从工具拿到的数字"对它无效。现在这类回答会被明确标红,不再静默溜过。
下一步(本轮未做):把"标记"升级为"强制"——问题问数量而本轮未调工具时,
不给它收尾的机会,追加一轮要求它先调 disk_space/list_files 再回答。
第四轮(强制重问 + 多步链路,2026-09-15 目标轮次 3)
做了:把编造检查从「标记」升级为「强制」——问题问数量、回答有数字、本轮零工具调用时,
不给它收尾,追加一轮(ENFORCE_TOOL_USE_NUDGE,点明该用哪个工具)并要求只答核实到的结果;
被替换掉的首答留在记录里,TUI 提示「首答没有出处,已强制重问」。上限两轮,防死循环。
| 真实任务(有基准) | 结果 |
|---|---|
| 跨会话问「评测报告目录里有多少文件」(真值 46,上一轮此处编造 0) | 自己调 list_files → 答 46 ✓ 6.0 秒 |
| 同会话问同一件事 | 调 list_files → 46 ✓ 5.7 秒 |
| 「模型包有多少个参数」(工具答不出) | 「我不知道。没有证据表明您的模型包包含多少个参数。」✓ 诚实 |
多步依赖任务:数 client-*.txt(真值 9)+ 报首个文件首行(真值 PASS /api/send 返回成功) |
❌ 失败,见下 |
重要修正(推翻我上一轮的判断):真正治好「0 个文件」编造的不是强制重问,而是上一轮加的窄接口工具。 两次试图触发强制重问都没触发(模型的答案都不再是"凭空数字"了),所以兜底机制至今未在实战中启动过。 合理解释:以前它要么手写命令(易错)要么干脆不查;现在有省事的正路,它就走正路。
新暴露的硬墙(失败点很具体):多步链路里,它不会把上一步工具结果里的值用到下一步。
list_files 已经返回了正确的 9 个文件名(client-api-check.txt 就在输出里),
它却去读自己编的 client-0001.txt(ENOENT),随后重试 6 步、夹一次空参数的 run_shell {},
最后一步 read_file 用对了名字却被中止,最终只吐出 <tool_response>,等于没答。
结论(日内可用性分级):
- 单步 + 明确工具 + 结果非空 → 可用(5–6 秒,答对)。
- 需要"从工具输出里抄一个值给下一次调用"的多步链路 → 不可用。
- 诚实性(拿不到就说拿不到)→ 可用(本轮两次都诚实)。
下一步(本轮未做):针对这堵墙,两条路——① 让工具结果更易抄(返回可复制的完整路径/编号并明确要求照抄); ② 继续收窄:把"列出并取第一个匹配文件的头几行"这类常用链路做成一个工具,从根上取消值传递(更符合"窄接口"路线)。
第五轮(多步链路修复 + 更正的生产形态,2026-09-15 目标轮次 4)
① 多步链路:已通过。而且我上轮的诊断是错的。
上轮我判定「4B 不会在调用之间传值」。真因是我的工具输出逼它做字符串拼接:list_files 只回裸文件名,
它得自己拼「目录 + 文件名」——于是它编了 client-0001.txt。改成每项返回完整路径后:
→ list_files {pattern:"client-*.txt"} ← 9 个,带完整路径
→ read_file {"path":"E:\\deepseek\\artifacts\\client-api-check.txt"} ← 照抄,没编
智能体 › 有 9 个。最靠前的是 client-api-check.txt,其第一行:PASS /api/send 返回成功 ✓ 17.7 秒
教训(值得记牢):小模型链式失败时,先查是不是我的工具输出逼它去构造东西,别急着归因给模型能力。
为拆这堵墙新加的 peek_files(一次调用完成"计数+首个文件开头")没用上——真正起作用的是取消拼接。
② 更正的生产形态:失败,且方式严重。
自然措辞「改了:我的评测报告以后放到 H:\Memory\agent_lab\runs」→ 模型回「已确认」,一次工具都没调
(没有 nm2_write{action:correct})。库里 生效 3 · 取代 0 · 撤回 0,三条全是 active:
[active] user|评测报告路径 = E:\deepseek\artifacts ← Agent 写的原值
[active] user|评测报告放在 e = \deepseek\artifacts ← 自动层,属性被 `E:` 的冒号劈歪
[active] user|评测报告以后放到 h = \Memory\agent_lab\runs ← 自动层,「改了」那句
两个问法都把旧路径排在前面(旧 8.598/8.655,新 8.547/8.533)——会话 C 答出新路径是运气,不可靠。
两个叠加的缺陷:
- 模型只在强对抗措辞(「不对,…」)下才调用
correct;「改了:…」这种最自然的更正说法它不认,只回一句"已确认"。 - 自动层把路径里的
E:当分隔符,抽出属性评测报告放在 e,于是同一件事变成两条无关键、永远无法互相取代 (与 §6.9 里语言vs默认语言同族)。
日内可用性分级更新:
| 能力 | 判定 |
|---|---|
| 单步 + 窄接口工具 | ✅ 可用 |
| 多步链路(工具给完整路径) | ✅ 可用(17.7 秒,两步都对) |
| 跨会话记忆召回 | ✅ 可用 |
| 诚实(拿不到就说拿不到) | ✅ 可用 |
| 自然措辞的更正(「改了…」) | ❌ 不可用:旧值仍 active 且排序更高 |
第六轮(更正失败的根因已定位到代码,2026-09-15 目标轮次 5)
① 修掉一个真 bug:盘符冒号被当成分隔符
infer_memory_metadata 的操作符列表里含裸冒号:(?:…|是|为|叫|:|:|=)。
句子「我的评测报告放在 E:\deepseek\artifacts」用的是「放在」而不是「是」,
正则于是抓到了盘符那个冒号,抽出属性 评测报告放在 E、取值 \deepseek\artifacts——
同一件事于是被归档在一个永远无法与 Agent 写的 评测报告路径 匹配的键下。
修法:冒号前是单个 ASCII 字母时不作为分隔符:(?<![A-Za-z])[::]。CJK 后的冒号仍是正常分隔符。
修复后:带「放在/放到」的路径句返回 episode(宁可不给属性,也不给错属性),
而 是-句、UTC+8、密钥:abc、密钥是 abc:def 全部不受影响。CPU 测试 92/92 通过(新增 4 项)。
② 但更正问题没有解决,根因已定位到具体代码行
修复后库确实干净了(无垃圾键,同轮吸收也生效),但旧值仍然 active 且排序更高:
[active] 评测报告路径 = E:\deepseek\artifacts ← 旧值
[active] (空字段,"改了"那轮的 episode,含新路径)
问「我的评测报告放在哪个目录」→ 旧 8.598 排第一,新 8.399
会话 B 依旧只回「已更新:…」、一次工具没调。于是去读代码,找到了硬性原因:
# qwen_integration.py:2423-2440 ——「措辞变了也要版本化」的补救路径
if (not candidate_conflict_key
and bool(valid_slots.any()) # 只扫遗留槽位库
and float(learned_best) >= threshold):
matched_slot = int(best_slot.item())
for record in self.memory_os_v2.bank.records.values():
if record.slot_index == matched_slot and record.conflict_key(): # ← 按 slot_index 匹配
inherited_conflict_key = record.conflict_key()
该路径按遗留槽位号匹配继承对象;而 Agent 经接口写入的记录 slot_index = -1(独立分页项,不占槽位)。
结论:凡是 Agent 自己写进记忆的事实,自动层永远无法把它版本化——只能版本化遗留层自己写的记录。
实测里那条旧路径正是 Agent 写的,所以自动层够不着,旧值一直活着并排在前面。
(这段代码的注释本身写着它的来历:「realistic 语料上 25/25 更新全部失败」—— 同一个 bug 类之前被修过一次,但修法绑在 slot_index 上,因此覆盖不到经接口写入的记录, 而那正是 Agent 与外部脚本走的路径。)
下一步(需你定,属模型包语义变更):把继承对象从「按槽位号匹配」换成
「在生效记录里找语义最匹配的一条,继承它的冲突键」。护栏沿用现有设计:只允许取代(superseded),
绝不允许撤回(撤回会销毁记录),并沿用同一学习阈值的门限。验证素材现成:data/realistic_v2 那批更新用例。
第七轮(尝试根治更正缺陷 → 引入更严重错误 → 已回退,2026-09-15 目标轮次 6)
做了什么:针对上轮定位的根因(继承只按 slot_index 匹配,够不着 Agent 写的记录),
实现了一条用模型自己的读取器判断"这一轮讲的是我已知的哪件事"、再继承其冲突键的路径——
刻意不引入新阈值(文件注释已警告:打包检索器会给不相关属性 ≥0.95,据此处置曾误伤 8/20 条),
护栏沿用"继承只导致取代、绝不撤回"。
结果:目标场景确实修好了,但代价不可接受。
修好后(同一场景):自动记录继承到 user::评测报告路径 并把旧记录取代,
[superseded] 评测报告路径 = E:\deepseek\artifacts,检索翻转为新值第一(8.595/8.610 对新),
两个全新进程、两种问法都答出新路径。当时看起来是成功的。
但误伤检查抓到它污染了不相关的事实。先存 默认语言=日语 与 项目代号=蓝鲸-47,
再说「我的项目代号是 蓝鲸-47」,那条自动记录错误继承了 user::默认语言,把语言事实取代掉:
[active] 默认语言 = 蓝鲸-47 inherited_key:user::默认语言 ← 事实被污染
[superseded] 默认语言 = 日语
问「我的默认语言是什么」→ 答 蓝鲸-47 ← 错答
一个错误取代会污染事实,比一个过时的旧值更糟(旧值至少真发生过)。这与文件注释记录过的
过度处置是同一类(当年 8/20 条被误伤)。已完整回退,并验证回退后 默认语言=日语 保持 active、
取代 0、问它答 日语(8.661)。CPU 测试 92/92。
一条有价值的负面结论:need_memory + 实体一致不构成足够的门。
读取器回答的是"这一轮需要记忆吗",不是"这一轮更新的是哪条记录"——任何关于用户的话它都会说需要记忆,
并返回某条记录。把"需要记忆"当成"就是这条",正是这次误伤的机理。
修正后的方向:用读取器的 score_margin(无歧义程度)做门——只有它明确认定"就是这一条"时才继承。
但必须先在真实更新用例(data/realistic_v2)上量出 margin 分布,分清"更新"与"新事实"两类的区间,
才谈得上定门限。在此之前保持现状(更正不生效属较轻缺陷,事实被污染属较重)。
日用现状(未变):单步、多步链路、跨会话召回、诚实拒答可用; 自然措辞的更正(「改了…」)仍不可用。
第八轮(margin 判别力测量 + 一次方法错误,2026-09-15 目标轮次 7)
上轮结论是"动更新语义前必须先量 margin 分布"。本轮就做这个测量,工具留在
agent_lab/measure_inherit_margin.py(走正在运行的控制面,不额外占卡;可 --rounds N 加压)。
第一版测量(每个属性 3 轮,各 36 次):
| 类别 | margin 区间 | 命中已存记录 |
|---|---|---|
| 正类=同一属性的更新(「我的A改成 v2」) | [8.6268, 8.6612] | 36/36 |
| 负类=另一属性的事实(「我的B是 v2」) | [0.7403, 8.5981] | 36/36 |
两类不重叠,门限约 8.61。同时解释了上轮误伤的机理:我只用 need_memory 做门,
而两类都是 12/12 为真——有判别力的是 margin,不是 need_memory。
⚠️ 我犯了一次方法错误(记录以免重蹈):随后做的"样本外检验"里,我把两条记录都写进库再去 probe, 读取器自然挑得对(margin 8.7402)。但写入路径的真实时序不是这样: 自动写入发生在模型生成之前,那一轮自己的事实还不在库里。测量 setup 必须复现真实时序, 否则量的是另一个问题。
按真实时序重测(库里只有旧记录):
| 情形 | margin | 指向 |
|---|---|---|
| A) 同一属性的真实更新 | 8.6463(换一对属性复现 8.6543) | 那条记录 ✓ |
| B) 完全不同属性的事实(上轮误伤那一幕) | 1.7218(复现 0.7795) | 也指向它,但分数很低 |
| C) 无关寒暄 | 0,need=False(router_abstained) |
正确弃权 ✓ |
结论:margin 确实带判别力,且按真实时序成立;门限 ~8.61 会拦住上轮那次误伤(1.72 ≪ 8.6), 同时放行全部 36 条真更新。
但我没有据此改代码,两个理由:
- 负类尾部有一条 8.5981,与正类最小值 8.6268 只差 0.03——间隔太薄,样本再大可能翻车。
- 那次误伤的机理我还没解释清:被污染的记录是
默认语言 = 蓝鲸-47(属性来自继承、取值来自句子), 而「我的项目代号是 蓝鲸-47」按结构解析器本应抽到属性项目代号,那样继承块根本不会执行 (not candidate_conflict_key是前置条件)。所以"读取器误判"这个解释站不住。
下一步:先把失败场景带仪表复现一次(在继承块里打印当时的 metadata、candidate_conflict_key、
读取器返回与 margin),弄清它是怎么走到继承那一步的;机理确认后再谈用 margin 做门。
在此之前不改更新语义。
第九轮(机理查清:错在我的门,不在读取器;两道门重建并验证,2026-09-15 目标轮次 8)
用仪表复现(临时环境变量 NM2_WRITE_TRACE,查完已删净):
--- memory_text: 我的项目代号是 蓝鲸-47。
metadata.attribute='项目代号' value='蓝鲸-47' kind=fact
candidate_conflict_key='user::项目代号' inherited='' ← 未继承
→ 写入 attribute='项目代号' ← 正确
而且把继承代码回退后,那个"误伤场景"根本复现不出来(项目代号=蓝鲸-47 与 默认语言=日语 双双完好)。
对照我写的那段与原有那段,差别一目了然:
结构门 not candidate_conflict_key |
|
|---|---|
| 原有(按槽位匹配) | 有 |
| 我写的 | 没有 ← 这就是原因 |
机理:我用读取器的"最近邻猜测"覆盖了解析器给出的正确结构——那一轮明明解析出 项目代号,
我的块却把它改写成 默认语言(当时库里只有那条),same_fact_key 随之变真 → 语言事实被取代。
不是读取器判断错,是我少了原有代码里明确写着的那道门。
按两道门重建并验证:① not candidate_conflict_key(解析器给出属性时一律不猜);② margin ≥ 8.61(只在读取器无歧义时才继承)。
| 验证项 | 结果 |
|---|---|
回归:存 默认语言=日语 + 项目代号=蓝鲸-47 |
无污染,取代 0,问默认语言答 日语 ✓ |
目标:记住:…放在 E:\… → 改了:…放到 H:\… → 新进程问 |
答出新路径 ✓ |
追踪:项目代号 那轮 |
被结构门挡住(inherited='')✓ |
追踪:改了 那轮 |
进入继承块(metadata.attribute='')但 inherited=''——被 margin 门挡住 |
⚠️ 我的第二次方法错误(记录以免重蹈):第一次标定的正类是「我的A改成 v2」,
而 改成 本身就在操作符列表里、会被结构解析,所以那批样本永远走不到继承块——
我标定的 population 是错的。按正确 population 重测(库里只存一条,只有"解析不出属性"的话才走这条路):
| 话 | margin | need_memory |
|---|---|---|
| 真·更新「改了:…以后放到 D-9021」 | 8.6325 | True |
| 真·更新「现在用 v3.14.2 了」 | 8.64 | True |
| 无关寒暄 / 新话题 | 0 | False |
| 无关求助「帮我看看磁盘空间」 | 0.682 | True |
| 无关别的属性 | 0.7421 | True |
0.74 对 8.63,间隔很宽,8.61 的门稳稳落在里面。上轮担心的"间隔只有 0.03"是假警报——那批样本会解析、走不到这条路。
留下的限制(如实记录):
- 口语化、过短的更新会被路由弃权:「我换到 E-4402 了。」margin=0、
need_memory=False→ 自动层不版本化它。这是路由自身的门,不是 margin 门的。 - 目标场景那一轮
inherited='',硬取代没发生:库里同时有 Agent 写的记录与该轮自动 episode, 读取器面对两条相近记录时 margin 变窄、被门挡住。答案仍正确(新记录排序在前), 属"软更正"而非"硬取代",旧记录依旧 active。
测试状态:语法通过、CPU 测试 92/92;临时仪表全部清除(残留 0)。
第十轮(换成绝对分数门,硬取代真正生效;2026-09-15 目标轮次 9)
上轮遗留问题:目标场景里 margin 门把"硬取代"挡住了。这轮先量清了原因:
| 库内状态(查询=「改了:…放到 H:\…」) | margin | 命中的有身份记录分数 |
|---|---|---|
| 只有 Agent 写的那条 | 8.591 | 8.591 |
| 再加一条无身份的自动 episode | 0.1523 | 8.591(没变) |
| 两条同属性、都有身份 | 0 | 8.591 / 8.591 |
结论:margin(top−second)在真实写入路径上天生不可用。 因为自动层每一轮都会插一条近似重复的记录,
它把 margin 从 8.591 直接砸到 0.1523——门正好卡在它本该放行的那一类上。而且状态1 的 8.591 本身也低于我设的 8.61。
改成:门看"有身份记录自己的绝对分数"(实测两簇:真更新 8.591/8.6325/8.64,无关 ≤0.682/0.7421,
门限 _INHERIT_SCORE_FLOOR = 5.0 落在中间的空档里)。两道门因此变成:
① 结构门 not candidate_conflict_key(解析器给出属性时一律不猜);
② 有身份记录的绝对分数 ≥ 5.0(读取器确实认为"就是这条记录")。
| 验证项 | 结果 |
|---|---|
目标:记住:…放在 E:\… → 改了:…放到 H:\… |
旧记录被 superseded(硬取代成功);新进程答 H:\Memory\agent_lab\runs ✓;库内 生效 2 · 取代 2 · 撤回 0 |
回归:默认语言=日语 + 项目代号=蓝鲸-47 |
无污染、取代 0、问默认语言答 日语 ✓ |
留下的限制:口语化、过短的更新会被路由弃权(「我换到 E-4402 了。」need_memory=False),
自动层因此不版本化它——这是路由自身的门,不是这道门的。
测试状态:语法通过、CPU 测试 92/92。
第十一轮(长跑稳定性:测到一半发现被显卡争用污染,已主动停手,2026-09-15 目标轮次 10)
目标里唯一还没测的一族是「长时间日常使用下的稳定性」,于是跑了 24 轮连续会话(--session longrun)。
完成 11 轮后我主动停止,原因见下。
已完成轮次的正确性(有客观基准的都在基准前量过):
| 轮次 | 任务 | 结果 |
|---|---|---|
| 1–2 | 寒暄 / 「你是谁」 | 「本地测试代理…不是云服务,也不具备我实际上没有的能力」✓ |
| 3–5 | 写入三条事实 | 三次 nm2_write ✓ |
| 6 | E:\deepseek\artifacts 文件数 |
list_files → 46 ✓(基准 46) |
| 7 | V2_dpskw 递归 .py 数 |
list_files{pattern,recursive} → 133 ✓(基准 133) |
| 8 | C 盘可用 | disk_space{C:\} → 34.52 GB ✓(基准 34.47,机器在跑) |
| 9 | 召回默认语言 | 命中 默认语言 = 葡萄牙语 score 8.64 ✓ |
但耗时数据全部作废——我差点把测量假象当成发现。 表面上每轮生成从 12.7 秒涨到 137.6 秒, 而 prompt 词元只从 3106 涨到 5248(1.7 倍),时间却涨 11 倍——这个比例本身就不对劲,于是我去查了显卡:
11557 MiB / 12227 MiB, 利用率 100 %
PID 18884: python -m V2_dpskw.eval_end_to_end_memory …(输出 locomo_nm21_e2e.md)
是你自己的 LoCoMo(NM2.1) 端到端评测在满负荷跑。那些"耗时暴涨"是我的长跑在跟它抢算力。 而且更糟:我的长跑正在污染你那次评测。
处置:立即停掉我的长跑和我的模型服务(PID 42384)—— 释放约 6.4 GB 显存(11.5 → 5.2 GB;你的评测此前只剩约 0.7 GB 余量,很可能正被逼到 OOM)。 现在卡上只剩你的评测。
⚠️ 方法教训(记牢):测延迟之前必须先查显卡上还有谁。我的测试设施和你的评测共用一块卡, 所以任何性能数字都必须附带一次显卡状态检查,否则测出来的可能是别人的负载。 本次差点把"争用"报成"会话退化"——这会是一个完全错误的结论。
仍未完成:长跑稳定性(需要独占显卡)、以及更正修复在生产形态下的复跑。 两者都依赖显卡空闲;我将继续把卡留给你的评测。
- 长跑稳定性(需独占显卡):24 轮连续会话只跑完 11 轮就因你的评测占满显卡而中止。重跑前先查显卡,并记录完整耗时序列与上下文增长。
- 把「改动只生效一半」的情形查清:目标场景已能硬取代,但库内仍留一条空值的
评测报告路径(继承来的 episode)。 对日用无碍(答对、排序对),但语义上不干净——看是否该让它随被取代者一起退场。 - 继续扩大窄接口工具覆盖面:进程、端口、日志尾行、目录大小。
- 继续扩大窄接口工具覆盖面:进程、端口、日志尾行、目录大小。
- 迫使兜底机制被真正验证:构造必然触发「零工具 + 数字」的场景,确认强制重问与两轮上限行为正确。
- 会话卫生查实:确认
agent.state.messages替换是否生效;不生效就换一条能真正清尾的路子。 - 两代模型都测(目前只用 NM2.0)。
- 仍未覆盖:长时间(几十轮)日用稳定性。
7. 本轮改动的文件
| 文件 | 改动 |
|---|---|
agent_lab/agent/src/tui.ts |
新增:交互式终端。驱动同一个 Pi Agent(同 8 个工具、同提示词),实时显示工具调用与记忆层决策;支持 /memory /trace /probe /arm /reset /tools,以及 --say 脚本化多轮 |
agent_lab/agent/src/prompt.ts |
新增:系统提示词单独成文件,TUI 与套件共用同一份(否则套件测出来的数字不能代表 TUI 的行为) |
agent_lab/agent/src/tools.ts |
run_shell 说明里点明「这是 Windows 上的 PowerShell,不是 bash」(实测模型会写 2>/dev/null,被当成路径 H:\dev\null 白烧三次调用);零输出时解释「多半是过滤条件没匹配上、不要再发同一条」;删除 nm2_write 的 importance/confidence 与 nm2_admin 的判断类旋钮(见 §6.9) |
agent_lab/agent/src/agent.ts |
**导出 AgentEventLike;去掉一处 as any;熔断改为同步 agent.abort();熔断后替换被污染的助手消息(会话卫生,实测未生效,见 §6.10) |
agent_lab/agent/src/memory.ts |
TurnDiagnostics 补齐服务端实际会发的字段;RecordedTurn.abortReason |
agent_lab/agent/src/agent.ts |
导出 AgentEventLike;去掉一处 as any |
agent_lab/try_nm2.py |
新增:直接打控制面接口的中文试用台(不是 Agent,只是接口层的快速体检;要体验 Agent 请用 bun run tui) |
agent_lab/agent/src/run_scenario.ts |
提示词改为从 prompt.ts 引入 |
agent_lab/RESULTS.md |
本文件 |
agent_lab/agent/src/tools.ts |
correct 失败自动降级为结构化写入;correct 允许不带 record_id;缺省 entity 补 user;三个 NM2 工具在记忆关闭时返回可读观察而非报错 |
agent_lab/model_server/server.py |
记忆冲突 409 + 恢复提示(原 500);其余 ValueError → 400;memory_mode != on 时拒绝所有写入端点;客户端中途断开不再被记成 500(_send/_send_stream 的响应头写入原本在异常保护之外,客户端断流会让 _route 往死 socket 上再发一个 500,日志里出现误导性的 500 与 socketserver traceback) |
agent_lab/RESULTS.md |
本文件 |
V2_dpskw/memory_os_v2.py |
origin 字段 + retract_origin()(见 §6.6);_absorb_unidentified_same_turn()——结构化写入落地时,把同一轮里那条没有身份的自动记录标为已被取代 |
V2_dpskw/qwen_integration.py |
memory_origin();自动写入带 origin;两个遗忘路径追加按轮次撤回;memory_fact_sentence() + compose_corrected_evidence()(见 §6.8);属性抽取不再把盘符冒号当分隔符(见第六轮) |
agent_lab/agent/src/tools.ts |
list_files 每项返回完整路径、新增 disk_space / peek_files 窄接口工具 |
V2_dpskw/natural_memory_service.py |
只传字段的更正先重建证据卡片(见 §6.8) |
V2_dpskw/tiered_memory_store_v2.py |
origin 列持久化 + 旧库原地迁移 |
V2_dpskw/tests/test_memory_origin_forget.py |
新增 19 个回归测试 |
验证状态:CPU 测试 88/88 通过;遗忘修复与更正修复均已端到端复现验证(NM2.0)。 NM2.1 未复测(代码共用,修复对两代都生效;要不要在 2.1 上再跑一遍由你定)。
8. 复现命令
# 1) 模型 server(LLM conda 环境,占卡)
$env:PYTHONPATH='H:\Memory'
& C:\Users\Administrator\miniconda3\envs\LLM\python.exe H:\Memory\agent_lab\model_server\server.py `
--model-path qwen3_5_4b_natural_memory_v2_1 --port 8766 --no-auto-persist
# 2) 控制面自检(29 项;需 server 已在运行)
& C:\Users\Administrator\miniconda3\envs\LLM\python.exe H:\Memory\agent_lab\model_server\selftest.py `
--base-url http://127.0.0.1:8766/v1 --wait 200
# 3) Agent 侧
cd H:\Memory\agent_lab\agent
$env:NATURAL_MEMORY_API_KEY='local'
$env:NATURAL_MEMORY_BASE_URL='http://127.0.0.1:8766/v1'
bun run typecheck
# 3a) 交互式终端:直接跟这个 Agent 对话(最直观的试用方式)
bun run tui # 记忆开启
bun run tui --arm off # 对照组:记忆层完全不可写
bun run tui --session demo1 # 固定会话名,记忆跨进程保留
# 3b) 不用终端也能脚本化跑几轮(--say 可重复)
bun run tui --session demo1 --say '记一下:我的项目代号是 蓝鲸-47。' --say '/memory'
# 3c) 两臂对照套件
bun run suite # 全部场景 × 双组
终端里能用的命令
| 命令 | 作用 |
|---|---|
/memory /all |
看生效记录 / 看全部(含已取代、已撤回) |
/trace [条数] |
看最近几轮的记忆层决策 |
/probe 问题 |
只跑路由不生成:看清它到底检索到什么 |
/arm on|off |
切换记忆开关(off = 对照组,写入会被拒绝) |
/reset |
清空记忆库 |
/tools |
列出 Agent 手里的 8 个工具 |
实测注意:终端里同一进程内的追问可能靠对话上下文答对,不能证明是记忆在起作用。 要证明记忆,得像下面那样换一个进程(全新上下文)再问。
runs/tui那两条记录就是这么做出来的。
跑长套件时用后台作业:前台命令默认 600 秒上限会被截断,且不要给长跑的 python/bun 管道加
Select-Object -First N——它会提前掐掉进程,伪装成"运行失败"。