diff --git a/docs/29-架构对齐-记忆与画像投影链路.md b/docs/29-架构对齐-记忆与画像投影链路.md new file mode 100644 index 0000000..593d886 --- /dev/null +++ b/docs/29-架构对齐-记忆与画像投影链路.md @@ -0,0 +1,194 @@ +# 架构对齐 · 记忆→画像→图 这条链(ZSY_develop vs 主干) + +> **目的**:`ZSY_develop` 分支(张胜宇,最后更新 2026-09-11 20:45)包含一整套画像投影实现, +> 而主干 `qyqy_develop` 上也有同类实现。**在合并之前必须先做一次架构对齐**,否则两套会互相覆盖。 +> **本文只做核对与建议,不改任何代码**。 +> +> **核对时间**:2026-09-11 晚 **主干**:`qyqy_develop` @ `bbf623a` **ZSY**:`cbcf7c7` + +--- + +## 一、最重要的发现:主干那条链**已经接好了**(走的是另一条 outbox) + +之前(包括架构师的评审意见里)的说法是"**组件写好了、线没接**"。**核对后这个说法不准确** —— +主干上有一条**完整且已在装配层接线**的链路,只是它走的不是 `memory_sync_outbox`: + +``` +agent.run_completed + ↓ (app/worker/runtime.py 的 handler 字典,已注册) +memory.extraction_requested + ↓ dispatch_memory_extraction +MemoryExtractionService → 提升成 user_facts + ↓ +profile.rebuild_requested + ↓ dispatch_profile_rebuild(runtime.py L175-197) +ProfileAssemblyService.rebuild() → 重建画像快照 +ProfileGraphProjectionService.project_customer() → 投影到 Neo4j +``` + +**这条链的引入者与时间**: + +``` +d7f6ef7 09-10 21:52 卿云秋月(架构师) feat: 记忆→画像→图全自动触发 +``` + +⇒ **架构师 09-10 就做完了"全自动触发"**,并且是**直接接在 `runtime.py` 的 handler 字典里**的。 +`app/worker/runtime.py` 的 handler 白名单**包含** `"profile.rebuild_requested"` —— 这就是"线接上了"的证据。 + +### 1.1 与 ZSY 那条链的关系:**两条平行的 outbox** + +| | 主干(架构师的链) | ZSY(张胜宇的链) | +|---|---|---| +触发事件 | `profile.rebuild_requested` | `memory_sync_outbox` 表里的行 | +存储 | **`domain_event_outbox`**(领域事件表) | **`memory_sync_outbox`**(记忆同步表) | +消费方式 | `WorkerRuntime` 的 handler 字典(**已注册**) | 独立的 `MemorySyncOutboxWorker` | +消费者装配 | `app/worker/__main__.py` → `WorkerRuntime(...)` | `app/worker/__main__.py` → `MemorySyncOutboxWorker({...})` | +投影目标 | **Neo4j**(`ProfileGraphProjectionService` + `relationships`) | **Milvus + Neo4j**(两个 adapter) | +画像生成 | `ProfileAssemblyService`(从 `user_facts` 组装) | `CustomerProfileCandidateService`(候选 → 复核 → 已批准快照) | + +**⇒ 两条链做的是同一件事,但走不同的 outbox、不同的画像生成方式、不同的消费装配。** + +### 1.2 现场数据(我这边实测) + +| 表 | 行数 | 说明 | +|---|---|---| +`user_facts` | **0** | 事实层为空 ⇒ 主干那条链**从没跑过**(或跑过但被清理) | +`memory_unit` | **0** | 记忆单元为空 | +`profile_snapshots` | **5** | 我的 `seed_profile_demo.py` 种的演示数据 | +`memory_sync_outbox` | **2** | **我的 `profile_generation_service` 写的**,`status='待处理'`,无人消费 | + +**⇒ 关键因果链**:`user_facts` 为空 → 主干链没产出过东西;`memory_sync_outbox` 的 2 行是**我的**生产者写的。 + +### 1.3 `graph_projection_worker.py` 的真相 + +``` +app/worker/graph_projection_worker.py → GraphProjectionWorker 类 +git grep "GraphProjectionWorker(" → 零处实例化 +``` + +**它是死代码**(或备用路径)。架构师说的"`GraphProjectionWorker` 没有实例化点"**是对的**; +但由此推论"整条链没接"**不对** —— 接的是 `runtime.py` 的 handler,不是这个类。 + +--- + +## 二、ZSY 分支带来的**真正增量**(主干确实没有的) + +逐文件核对后,ZSY 分支**独有且主干没有**的只有这些: + +| 文件 | 作用 | 主干有没有等价物 | +|---|---|---| +`app/worker/memory_sync_outbox_worker.py` | `memory_sync_outbox` 的消费者(handler 注入 + 重试 + 死信) | **无**(主干消费的是另一个 outbox) | +`app/infrastructure/milvus_profile_projection.py` | 画像投影到 **Milvus** | **无**(主干只投影到 Neo4j) | +`app/infrastructure/neo4j_profile_projection.py` | 画像投影到 Neo4j(另一个实现) | 🟡 主干有 `ProfileGraphProjectionService` | +`app/service/knowledge_publication_service.py` | 知识**发布**状态机(草稿→审核→发布) | 🟡 主干有 `knowledge_ingest_service` + 审核字段 | +`app/service/knowledge_authority.py` / `knowledge_config.py` | 知识权威来源与运行期配置 | **无** | +`app/service/customer_service_session_memory_service.py` | 客服**多轮会话记忆** | 🟡 主干有 Redis 短期记忆 + `docs/24` 说多轮已跑通 | +`app/service/customer_service_handover_*.py` | 转人工上下文与后台 | 🟡 主干有 `handover-requests` 端点 + 工单表 | +`app/service/customer_profile_candidate_service.py` | 画像**候选 → 复核 → 批准**工作流 | **无**(我的 `profile_generation_service` 是直接生成快照) | +`app/worker/customer_profile_candidate_worker.py` | 候选流程的 worker | **无** | +`app/service/agent/customer_service_agent.py` | 他自己的客服 Agent | 🔴 **主干已有**(`implementations/customer_service.py`,老师验收第 4/5/6 条靠它) | +`app/service/agent/customer_service_routing.py` | 他自己的路由 | 🔴 主干已有 `core/customer_service_rules.py` | +`app/service/knowledge_tool_service.py` | 知识工具(另一份) | 🔴 主干已有 `knowledge_tool.py` | + +--- + +## 三、四个必须对齐的架构分歧 + +### 分歧 1:用哪条 outbox 承载"记忆→画像→图"? + +- **主干**:`domain_event_outbox` + `profile.rebuild_requested`(**已在 handler 白名单里**,架构师 09-10 接的) +- **ZSY**:`memory_sync_outbox` + `MemorySyncOutboxWorker` + +**我的判断**:`memory_sync_outbox` 是 `docs/00` 基线里定义的表(**存在即有其设计意图**), +而主干那条链并没有消费它 —— 所以**两套有各自的合法位置,不该二选一,而该明确分工**: + +| 场景 | 应该走哪条 | +|---|---| +记忆内容变化 → 画像重建 → 图投影 | **主干那条**(`profile.rebuild_requested`,已接线、已验) | +画像快照 → **同步到 Milvus 向量库**(用于语义召回) | **需要一个消费者**,这正是 ZSY 的 `milvus_profile_projection` 补的 | + +⇒ **建议**:主干链保留;ZSY 的 **Milvus 投影适配器**接进主干链的尾部(或接成 `memory_sync_outbox` 的消费者, +但那样要明确"这两个 outbox 各自负责什么",否则就是两套并存)。 + +### 分歧 2:画像生成"直接快照"还是"候选→复核→批准"? + +- **我的**:`profile_generation_service.py` —— 直接生成快照(符合 `docs/00` §6.4.6 的事务口径) +- **ZSY 的**:`customer_profile_candidate_service.py` —— 先生成候选、人工/规则复核后才成为正式快照 + +**我的判断**:这是**业务裁决**,不是技术裁决。候选复核流程更严(金融场景可能更合适), +但**成本更高**(需要复核人、需要审核界面)。**建议由架构师或业务方定**,不要由合并动作决定。 + +### 分歧 3:两套客服 Agent + +- **主干**:`implementations/customer_service.py`(**已进主干**,老师验收第 4/5/6 条靠它) +- **ZSY**:`agent/customer_service_agent.py` + +**我的判断**:**这是最危险的一条**。主干那套已通过验收(产品咨询 5/5、政策 3/3、多轮 3 轮), +ZSY 那套是从旧基线写的、**落后主干 144 个提交**。**不能因为合并把它顶掉。** + +### 分歧 4:ZSY 落后主干 144 个提交 —— 他的改动里有大量"过期内容" + +ZSY 的分叉点是 `c2178a9`(09-11 09:49),而它改过 68 个与主干重叠的文件,其中包含: + +``` +app/service/tool_executor.py、app/service/agent/base.py、app/service/agent/governance.py、 +app/worker/runtime.py、app/service/public_platform_service.py、app/main.py、app/core/contracts.py +``` + +这些文件在主干上**已经过多轮修改**(风控 P3、投顾线、我的画像线)。**整体合并会把他的旧版本顶回主干。** + +--- + +## 四、建议的收口方案(分三步,风险递增) + +### 第 1 步:**只移植两个投影适配器**(低风险、有明确收益) + +| 移植什么 | 从哪来 | 落到哪 | +|---|---|---| +Milvus 画像投影适配器 | `app/infrastructure/milvus_profile_projection.py` | 同一路径(主干没有同名文件) | +(可选)Neo4j 投影改进 | `app/infrastructure/neo4j_profile_projection.py` | 与主干 `ProfileGraphProjectionService` 比对后再定 | +测试 | `tests/unit/infrastructure/test_milvus_profile_projection.py` | 同路径 | + +**为什么安全**:这两个文件在主干**不存在** ⇒ 零冲突;它们是纯适配器(无业务改动)。 + +### 第 2 步:**`memory_sync_outbox` 的消费者**(中风险,需要先定分工) + +两种做法,**必须选一种**: + +| 做法 | 说明 | +|---|---| +**2a** | 把 ZSY 的 `MemorySyncOutboxWorker` 接进 `app/worker/__main__.py`(他那套本来就是这么设计的)—— 直接能用,但要明确它与主干链的分工 | +**2b** | 不引入新 worker,而是把主干链的尾部补上"同时写 Milvus"(改 `dispatch_profile_rebuild`)—— 单一链路更简单,但要改主干的已验代码 | + +我倾向 **2a**,理由:`memory_sync_outbox` 是基线表,让它有自己的消费者更符合原本设计; +且不用改主干已验的那条链。 + +### 第 3 步:**其余部分对齐后再谈**(需要架构师/业务裁决) + +- 画像**候选复核流程**要不要采纳(分歧 2) +- ZSY 的**客服 Agent / 知识工具 / 路由**是否全部放弃(分歧 3)—— 我建议放弃 +- ZSY 的**知识发布状态机**与主干的知识入库/审核是否合并 + +--- + +## 五、给架构师/张胜宇的四个问题 + +1. **`memory_sync_outbox` 与 `profile.rebuild_requested` 的分工是什么?** + (两个 outbox 都在用,但谁负责什么没写下来。我在 `AGENTS.md` 里已经记了"两个 outbox 不能混", + 但现在**两条链各用一个**,需要明确边界。) +2. **画像生成要"直接快照"还是"候选复核"?** 这是业务裁决。 +3. **ZSY 的客服 Agent 要不要保留?** 主干那套已通过老师验收第 4/5/6 条,我建议以主干为准。 +4. **ZSY 分支怎么收尾?** 它落后主干 144 个提交 —— 建议**不整体合并**,改为"按需移植 2-3 个文件" + (投影适配器 + outbox worker + 对应测试),其余明确标记为"已被主干实现取代"。 + +--- + +## 六、我这边确认不做的事 + +- **不改任何代码**(本文只是对齐) +- **不整体合并 ZSY 分支**(会让 144 个提交的旧改动回退主干) +- **不动主线那条已验链路**(`profile.rebuild_requested` 全自动触发) + +--- + +*核对依据:`git ls-tree` 逐文件比对 + `git grep` 引用追踪 + 现库 `information_schema` 与行数实测。*