相对第一版 46fc976 的完整变更。组员迁移对照表见 docs/20。
一、对外契约对齐 docs/05(破坏性,共 4 处,组员需按 docs/20 调整)
1) 配置发布端点改为文档规定的复数资源名:submit→validations、
approve→reviews(需 body decision)、activate→activations、
rollback→rollbacks;第一版这 4 个动词式路径 docs/05 从未定义过。
2) 错误码由 8 个笼统码改为 15 个具体语义码(FORBIDDEN→AGENT_PERMISSION_DENIED、
UNAUTHORIZED→AUTHENTICATION_REQUIRED、CONFLICT→RESOURCE_VERSION_CONFLICT、
RESOURCE_NOT_FOUND→RUN_NOT_FOUND/SESSION_NOT_FOUND 等),
输入类错误状态码 400→422。
3) POST /api/v1/agent-runs 与 GET /api/v1/agent-runs/{run_id} 统一为
{data, meta} 信封(data 内字段名与语义未变)。
4) 错误响应体统一为 {error:{code,message,retryable,field_errors}, meta:{trace_id}},
不再返回 FastAPI 默认的 {"detail": ...}。
二、数据库基线与约束
新增 39 张表的基线迁移(链根)与联合唯一键纠偏(4 张表、删 8 增 4,幂等收敛);
撤下 config_release 的双人复核 CHECK(应用层已允许自审,审核节点保留,
自审如实写入 reviewer_id);记忆 active key 生成列与唯一键;
activate 开始记录 supersedes_release_id 使版本链可追溯。
docs/00 基线未修改,未重命名或删除任何表与字段。
三、修复会静默出错或无报错的缺陷
- 跑完集成测试后平台会静默失去生效配置:清理只删自己创建的版本,却没有恢复被它
顶成 superseded 的原生效版本,且审计一并删除因而完全无痕,表现为所有工具被拒
但没有任何报错。已修清理逻辑并加恢复。
- Worker 单轮异常导致进程退出;记忆抽取调用方的“事务已开始”异常;
召回缓存丢失 degraded 标记;连接时区未生效导致 created_at/updated_at 差 8 小时;
.env 与 os.getenv 密钥来源分裂导致“没有可用的已批准模型端点”。
- 记忆信号识别漏判与跨键误命中;SSE 未带 Accept 的协商行为。
四、功能补齐
记忆链路 P1/P2/P3(抽取、受控词表、召回与缓存、生命周期级联及投影事件)、
fin_* 场内交易只读 ORM 层、agent_intent_config 状态流转并在运行期真正生效、
限流(Redis 固定窗口、故障一律放行)、游标校验、trace_id 中间件、
示例业务 Agent fund_query_demo 与一键端到端验证脚本,以及审计/指纹/迁移状态工具。
五、文档与验证
新增 docs/19(业务 Agent 接入实操)、docs/20(第一版迁移指南)与 docs/evidence 证据;
docs/01/02/06/08/09/17 同步实现现状。
验证结果:ruff 通过、mypy 103 文件无错、unit+contract 447 passed、
integration 29 passed、acceptance_check --production 7 PASS、
demo_agent_e2e 9/9 PASS(含失败关闭反证)。
6.6 KiB
6.6 KiB
记忆链路端到端验收(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=...) 注入两个确定性替身,
使「抽取」这一段同样被端到端覆盖,而不是靠单元测试代替。
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 |
接线后首个真实数据集成测试触发并修复 |
四、必须记录的环境事实
- 零配置环境无法端到端运行 Agent。
model_endpoint_config为 0 行,IntentClassifier在端点 为空时按设计失败关闭(RecoverableAgentError),run 无法进入complete_run。记忆抽取同样 失败关闭(无端点时不落任何记忆)。业务接入前必须至少配置一个模型端点与一个受控备用端点; 本探针用替身绕开这一环境依赖,只验证链路本身。 - Outbox 消费吞吐。
dispatch_one每轮只领取一条事件,jr库现有 262 条 pendingagent.run_requested,Worker 常驻后需约 262 轮(默认轮询 1 秒)清空。不是缺陷,但积压深度 需要纳入运维观测。 - 记忆键已语义化(P1 遗留问题已由 P2 解决)。键来自受控词表,同一会话的不同事实落到不同键,
不再互相覆盖;同一客户同一键的并发写入由
active_memory_key唯一键保证只有一条有效记录。 - 向量召回通道已启用,只差环境配置。
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的原因,属设计行为而非漏检。 - 结构化召回是字面匹配,语义匹配依赖向量通道。实测查询「风险偏好」在只有结构化通道时
返回 0 条:
recall_with_decay的关键词过滤按空白切分,中文查询整体作为一个 term, 只有字面出现在记忆内容或键里才命中。语义等价的查询(记忆内容是「稳健型」)需要向量召回, 这正是通道 4 的价值所在——不应通过放宽关键词过滤去"修"它。
五、边界
本验收证明"受理 → 执行 → 事件 → 消费 → 抽取 → 记忆 → 幂等"链路连通;不证明抽取质量本身
(由 test_memory_extraction_service.py 的契约测试覆盖)、不证明向量召回的语义效果
(Milvus 未接真实实例,降级路径由 P3 单元测试覆盖)、不证明 episode 聚合的业务价值。