81 lines
6.6 KiB
Markdown
81 lines
6.6 KiB
Markdown
# 记忆链路端到端验收(2026-09-09)
|
||||
|
|
|
|||
|
|
对照 `docs/evidence/20260909-memory-baseline-before.md` 定义的判据,验证记忆链路是否真正连通。
|
|||
|
|
本文件在 P1 验收后更新,已纳入 P2 的抽取环节。
|
|||
|
|
|
|||
|
|
## 一、验收方式
|
|||
|
|
|
|||
|
|
`tools/memory_chain_probe.py`——真实 MySQL、真实 `WorkerRuntime` 租约与治理链、真实
|
|||
|
|
`OutboxWorker` 消费、生产公开接口。为不依赖尚未配置的模型端点,探针通过
|
|||
|
|
`WorkerRuntime(model_service=..., endpoint_resolver=...)` 注入两个确定性替身,
|
|||
|
|
使「抽取」这一段同样被端到端覆盖,而不是靠单元测试代替。
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
python tools/memory_chain_probe.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 二、验收结果:全部通过(含抽取)
|
|||
|
|
|
|||
|
|
| 环节 | 断言 | 结果 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 生产装配 | 模型服务、工具执行器、意图分类器均已注入工厂 | PASS |
|
|||
|
|
| 受理 | `AgentRunApplicationService.accept` 返回 run_id | PASS |
|
|||
|
|
| 执行 | Worker 领取并执行完成,`agent_run.status = succeeded` | PASS |
|
|||
|
|
| 事件契约 | `complete_run` 同事务写入 `memory.extraction_requested` | PASS |
|
|||
|
|
| 事件载荷 | payload **不含正文**,只有 `run_id` / `message_id` / `customer_id` | PASS |
|
|||
|
|
| 消费 | Worker 轮询 3 轮清空该 run 的事件队列 | PASS |
|
|||
|
|
| 抽取 | 抽取模型被真实调用、端点经解析器解析 | PASS |
|
|||
|
|
| 记忆写入 | `memory_unit` 由 0 变为 1 行 | PASS |
|
|||
|
|
| 抽取结果 | 键为受控词表 `preference:risk_level`、内容为结构化值「稳健型」而非用户原文、类型 `preference` 与键前缀同构 | PASS |
|
|||
|
|
| 归属 | `customer_id` 为发起运行的客户 | PASS |
|
|||
|
|
| 幂等 | 同一 `event_id` 重复消费被拦截,记忆行数 1 → 1 | PASS |
|
|||
|
|
| 清理 | 探针数据清理后残留 0 行 | PASS |
|
|||
|
|
|
|||
|
|
## 三、断点与修复对照
|
|||
|
|
|
|||
|
|
| 修复前 | 修复后 | 证据 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `WorkerRuntime` 只注册 `agent.run_requested`,记忆事件永不消费 | 注册 4 个 handler(含 `memory.extraction_requested`) | 探针第 5 步 PASS |
|
|||
|
|
| 消费者读 `payload["content"]`,而事件按设计不含正文 | 按 `message_id`/`run_id` 回查 `conversation_message` | 探针内容断言 PASS |
|
|||
|
|
| 无幂等边界,重复消费重复写 | `event_id` 作为 `memory_evidence.idempotency_key` | 探针第 6 步 PASS |
|
|||
|
|
| 记忆内容是用户原文整句、键为 `conversation.{session_id}` | 模型严格 JSON 抽取 + 受控语义键 | 探针抽取结果断言 PASS |
|
|||
|
|
| `MemoryConflict` left/right 自引用、ORM 列名与库不符 | 已修复并与库列名对齐 | `tools/audit_constraints.py` PASSED |
|
|||
|
|
| `memory_unit` 缺 `active_memory_key` 生成列与唯一键 | 迁移 `20260909_memory_active_key` | 空库重建与既有库审计均 PASSED |
|
|||
|
|
| P2 引入的回归:同一 Session 先查询触发 autobegin,`complete_run` 抛 "A transaction is already begun" | 只读查询改用独立 Session,并补失败堆栈日志 | 探针由 FAILED 转为 PASSED |
|
|||
|
|
| 信号识别重叠误命中与漏命中 | 重写模式表 + 11 个回归测试 | `tests/unit/service/test_memory_taxonomy_signals.py` |
|
|||
|
|
| P3 三个服务零调用点(召回 / episode / 级联都是死代码) | 分别接入治理链、Worker 轮询与事件 handler | 探针 PASSED;episode `persisted_rows=1`;级联 `memory_status=invalidated` + 审计 1 行 |
|
|||
|
|
| `EpisodeWorker` 只 `flush` 不提交,接线方漏 commit | 接线层显式 `session.begin()` 并写明契约 | 由 `inserted=1 但 rows=0` 暴露,修正后 `rows=1` |
|
|||
|
|
| 召回缓存命中后降级状态丢失(写入带 `degraded`、读取硬编码 `False`) | 缓存读写都携带 `degraded_reasons` | `test_cached_degraded_result_keeps_degraded_flag`(原为 strict-xfail,已转正) |
|
|||
|
|
| 向量后端直接抛异常会中断整条召回(与"只降级不阻塞"承诺不符) | `_vector` 增加异常兜底 | `test_vector_backend_exception_is_degraded_not_raised`(原为 strict-xfail,已转正) |
|
|||
|
|
| `recall_with_decay` 中 `Decimal * float` 抛 `TypeError` | 排序键显式转 `float` | 接线后首个真实数据集成测试触发并修复 |
|
|||
|
|
|
|||
|
|
## 四、必须记录的环境事实
|
|||
|
|
|
|||
|
|
1. **零配置环境无法端到端运行 Agent**。`model_endpoint_config` 为 0 行,`IntentClassifier` 在端点
|
|||
|
|
为空时按设计失败关闭(`RecoverableAgentError`),run 无法进入 `complete_run`。记忆抽取同样
|
|||
|
|
失败关闭(无端点时不落任何记忆)。业务接入前必须至少配置一个模型端点与一个受控备用端点;
|
|||
|
|
本探针用替身绕开这一环境依赖,只验证链路本身。
|
|||
|
|
2. **Outbox 消费吞吐**。`dispatch_one` 每轮只领取一条事件,`jr` 库现有 262 条 pending
|
|||
|
|
`agent.run_requested`,Worker 常驻后需约 262 轮(默认轮询 1 秒)清空。不是缺陷,但积压深度
|
|||
|
|
需要纳入运维观测。
|
|||
|
|
3. **记忆键已语义化**(P1 遗留问题已由 P2 解决)。键来自受控词表,同一会话的不同事实落到不同键,
|
|||
|
|
不再互相覆盖;同一客户同一键的并发写入由 `active_memory_key` 唯一键保证只有一条有效记录。
|
|||
|
|
4. **向量召回通道已启用,只差环境配置**。`MemoryRecallService` 已接入主链路,组装层
|
|||
|
|
`build_memory_recall_service` 现同时传入 `vector=VectorMemoryAdapter(...)`(Milvus 可达时)
|
|||
|
|
与 `embed=_embed_text`。`_embed_text` 走发布配置解析 `task_type=embedding` 端点;当前
|
|||
|
|
`model_endpoint_config` 为 0 行,因此带查询文本的召回会按设计降级为
|
|||
|
|
`embedding_failed` 并**保留结构化结果**(实测:`items`/`degraded_reasons` 符合预期,
|
|||
|
|
向量化失败不抛异常、不中断召回)。配好 embedding 端点后语义召回无需改代码即可生效。
|
|||
|
|
注意:无查询文本时 `_vector` 直接跳过,不产生降级标记——这也是实测中 `degraded=False`
|
|||
|
|
的原因,属设计行为而非漏检。
|
|||
|
|
5. **结构化召回是字面匹配,语义匹配依赖向量通道**。实测查询「风险偏好」在只有结构化通道时
|
|||
|
|
返回 0 条:`recall_with_decay` 的关键词过滤按空白切分,中文查询整体作为一个 term,
|
|||
|
|
只有字面出现在记忆内容或键里才命中。语义等价的查询(记忆内容是「稳健型」)需要向量召回,
|
|||
|
|
这正是通道 4 的价值所在——不应通过放宽关键词过滤去"修"它。
|
|||
|
|
|
|||
|
|
## 五、边界
|
|||
|
|
|
|||
|
|
本验收证明"受理 → 执行 → 事件 → 消费 → 抽取 → 记忆 → 幂等"链路连通;不证明抽取质量本身
|
|||
|
|
(由 `test_memory_extraction_service.py` 的契约测试覆盖)、不证明向量召回的语义效果
|
|||
|
|
(Milvus 未接真实实例,降级路径由 P3 单元测试覆盖)、不证明 episode 聚合的业务价值。
|