Commit Graph
8 Commits
Author SHA1 Message Date
wangjianlong_0626 57f56bbcbe fix(profile): 修 profile_snapshots 重复定义(会打挂 Worker);消费端兜底 memory_sources
## 1. 独立缺陷:`profile_snapshots` 被两个 ORM 类重复映射

排查 `memory_sync_outbox` 中 `target_store='neo4j'` 那行 `last_error='InvalidRequestError'`
时发现,根因不在图库,而在模型层:

- `app/model/profile.py` → `ProfileSnapshot` 映射 `profile_snapshots`
- `app/model/risk_questionnaire.py` → **另一个** `ProfileSnapshot` 也映射 `profile_snapshots`

SQLAlchemy 不允许两个类映射同一张表。实测:

| 场景 | 结果 |
|---|---|
| 单独导入 `app.main` / `app.worker.runtime` | 正常 |
| 单独导入 `profile_assembly_service` / `risk_questionnaire_service` | 正常 |
| **两者同时导入** | `InvalidRequestError: Table 'profile_snapshots' is already defined` |

Worker 在同一进程里既要处理 `profile.rebuild_requested`(走 `app.model.profile`),
又要处理投顾风险问卷(走 `risk_questionnaire.py`)——所以这是**会打挂 Worker 的缺陷**,
不是理论风险。

修法:`app/model/risk_questionnaire.py` 不再重复定义,改为从 `app.model.profile`
转出(re-export),既有 4 处 `from app.model.risk_questionnaire import ProfileSnapshot`
无需改动。原定义多映射的 `current_customer_id` 经全仓核查无人使用,故不保留
(`app.model.profile` 明确注明该列由数据库维护、故意不映射)。

## 2. 消费端兜底 `memory_sources`

`memory_sources` 是本线新增的投影入参,而投顾线两处生产者的 payload
(`{customer_id, profile_uuid, version, profile}`)没有这个键,原样会导致它们每次画像
变更都 `memory_sources is invalid` → 重试至死信。

新增 `WorkerRuntime._with_memory_sources()`:**键缺失或为 None** 时回退查询该客户
`memory_unit` 中 `status='active'` 的记忆,并记 warning(使"谁没提供"保持可见)。
语义成立:长期记忆是**客户级**而非画像版本级的,每条记忆自带 `version`,
适配器按 `memory_uuid + version` 幂等,故"用的是哪一版"仍确定。

**兜底不掩盖真错误**:键**存在但格式不对**时**不兜底**,原样交给适配器失败关闭。
实现上用键存在性判断而非 `isinstance`——后者会把"缺失"与"格式错"混为一谈,
那是初版实现里的一个真 bug,被新测试抓出后修正。

## 3. 补上此前欠缺的消费端全路径验证

此前"整合验证"是直接调适配器,跳过了 outbox 的领取→分派→状态更新。

- **失败分支**(Milvus 断开时实测):行被领取、按 target_store 分派、异常被捕获、
  `status`/`retry_count`/`last_error`/`next_retry_at` 正确落库。
- **成功分支**(注入替身向量客户端):outbox 行 → `processed`、`processed_at` 已写;
  不可投影的 `constraint:` 被跳过(只写 1 行);维度 1024;字符串客户号转 int;
  **手机号脱敏生效**(`稳健型投资者,手机号 [手机号已隐藏] 请勿外泄`)。
- **兜底实证**:历史行 `id=5`(payload 无 `memory_sources`)经兜底后成功投递为 `processed`。
- **修复实证**:`id=6` 的 `last_error` 从 `InvalidRequestError` 变为
  `RecoverableAgentError`(图库不可用)——证明重复定义缺陷确已消除,剩下的是环境问题。

## 4. 测试与验证

- 新增 `tests/unit/worker/test_runtime_profile_projection.py`(5 用例:已提供原样透传、
  缺失兜底、空记忆给空列表而非删键、无客户号不兜底、格式错不兜底)
- 全量:`2 failed, 1312 passed, 2 skipped`(2 个失败为既有环境项,非本次引入)
- mypy:`Success: no issues found in 227 source files`
- 表结构审计:89 张业务表无缺失/意外(未改动任何表结构)
- 文档守卫:41 份文档无编号冲突

## 5. 文档

`docs/32-记忆投影链路实现说明.md` 增补 §6.1(兜底)、§6.2(重复定义缺陷)、
§7.1(消费端全路径验证)并更新验证表与文件清单;
`AGENTS.md` 新增"一张表只能有一个 ORM 类"易错点、校正测试基线数字。

## 未做

未改 `docs/00` 基线、未动数据库迁移、未改投顾线生产者代码、未启动常驻 Worker。
遗留:投顾线两处生产者的 payload 仍缺 `memory_sources`(已有兜底,不再死信,
但根治应由投顾线确认);Milvus/Neo4j 容器本轮不可用(Docker Desktop 崩溃),
`id=6` 停在 failed 属环境不可用、非代码缺陷。
2026-09-12 11:24:50 +08:00
wangjianlong_0626 8a0cbab636 fix(memory-projection): 订正 outbox 取值口径并接通画像投影链路
背景:memory_sync_outbox 这条链此前**完全没有消费者**,且生产端照 docs/00 §6.4.6
写成大写 MILVUS/NEO4J + 中文「待处理」,而消费端按 target_store 的**值**分派 handler、
且只领 status in {pending, failed} —— 两个条件都不满足,事件任何消费者都领不到、
永久滞留且不报错(唯一键 (event_uuid, target_store) 对大小写无约束,MySQL 也不报错)。
根因是代码与测试都硬编码字面量,所以测试跟着一起错、谁也没拦住。

订正
- profile_generation_service:取值改为全仓一致的小写(milvus/neo4j/upsert/pending)
- 测试改为引用常量并断言消费端契约,不再硬编码(硬编码是本次跑偏的直接原因)
- 新增契约回归测试:断言大写值分派不到 handler、会进死信,谁改回大写立刻红
- 新增 tools/normalize_memory_sync_outbox.py:订正历史脏行(默认 dry-run、幂等)

接通投影链路(此前零消费者)
- 新增 Milvus 集合 user_long_term_memory_v1 及建集合工具(幂等、不覆盖已有集合)
- 新增 MilvusProfileProjection / MilvusProfileVectorClient,并修掉移植带来的两处必炸点:
  customer_id 由「必须 int」放宽为接受数字字符串(本仓所有生产者都写 str,
  不放宽则每个事件必然失败);不可投影的 memory_key 由「整批 raise」改为跳过留痕
  (否则一条 constraint: 记忆毒死该客户整批,而受控词表 13 个键里有 7 个不满足前缀)
- 新增 MemorySyncOutboxWorker(领取/指数退避/死信骨架保留原样)并接入 WorkerRuntime
- milvus → 向量投影;neo4j → 复用主干 ProfileGraphProjectionService(方案 A,
  不引入第二套投影,避免同一事实在图中两种说法、违反主干既有的只投影已确认事实的不变式)
- 生产端从 memory_unit(status=active) 组装 memory_sources,随事件带上确定快照
- 前置移植 conversation_privacy:写外部存储前脱敏手机号/证件号/银行卡等

验证
- 新增 17 个单测;全量 2 failed, 1307 passed, 2 skipped
  (2 个失败为既有环境项:断言请求体中文原文而 httpx 序列化成 \uXXXX,非本次引入)
- mypy app → 0 错(227 文件);audit_schema → 89 张业务表无缺失/意外,未改动表结构
- 真机:真实 embedding(1024 维) + 真实 Milvus 写入并回读通过
- 整合链路(测试记忆 → 生产端组装 → outbox → 消费端投递 → Milvus 回读)通过,
  且 MySQL 已回滚、Milvus 无残留

文档
- 新增 docs/32-记忆投影链路实现说明.md:真实口径、根因、契约与验证证据(供接手)
- AGENTS.md:新增该易错点;新增 Windows 中文输出乱码的正确命令(-X utf8);
  校正测试基线与 mypy 文件数

未做:未改 docs/00 基线、未动数据库迁移、未改投顾线代码、未启动常驻 Worker。
遗留:投顾线两处生产者的 payload 缺 memory_sources,会被消费至死信,待架构师确认是否投影。
2026-09-12 10:45:40 +08:00
qyqy 123c273bc8 merge: 把主干(架构师的 11 个提交)拉进 NL_develop,并更新 AGENTS.md 基线口径
合并结果:**零冲突**(自动合并 21 文件 / +1430 行)。合并后 HEAD = origin/qyqy_develop = 76e87a3,
两边完全一致(rev-list 双向均为 0)。

架构师这轮做的事(我这边此前没有):
- f09ea9e 把我的 NL_develop 并进主干(**第二父就是我的 58849ad,我这条线全部提交已在主干里**)
- 3029d0c 补发画像工具白名单完成(release 254 active)+ release-state 证据
- 76e87a3 AGENTS.md 表数口径 51 → 68(并指向我写的 docs/28)
- f60915b 补声明 aiosqlite(同事那 6 个用例在干净环境会 ModuleNotFoundError)
- b5b0680 修掉我留下的 3 个 mypy 错(全在同事的场外邮件模块,各一行、不动逻辑)
- 6c09cde/8268646 新增补发画像白名单脚本(dry-run + 合并前防呆),并发现两个既有发布脚本会丢提示词
- 新增文档:他的评审意见与答复(docs/NL_develop交付说明-评审意见.md 等)、
  docs/evidence/knowledge-collections.json、tools/probe_* 两个探针

**他抓到了一个我漏声明的依赖**:`python-docx` —— 我的 `document_parser.py` 用它解析 .docx,
但 pyproject.toml / requirements.txt 里没有,换干净环境跑知识入库会直接
`ModuleNotFoundError: No module named 'docx'`。已随合并进来(本机原本恰好装了,所以本地没暴露)。

本次我只改 AGENTS.md 的基线口径(合并把过期的 mypy/测试数字带回来了):
- 测试基线 1034 → **1219 passed / 3 failed**,并逐条说明那 3 个失败都不是代码缺陷
  (1 个既有空集缺陷 + 2 个 httpx 中文序列化的环境相关)
- mypy 从"181 个错、双方不可比、未装 sqlalchemy2-stubs"改为
  **`Success: no issues found in 180 source files`(0 错)**,并附四组复现矩阵说明真因是
  SQLAlchemy 补丁版旧(**明确写上"不要装 sqlalchemy2-stubs"**,那是 1.4 的包)

门禁全绿:测试 1219 passed / 文档守卫 37 份无重号 / 结构审计 68 张表 / mypy 0 错 /
迁移 head 一致 / aiosqlite·python-docx·python-pptx 均已声明且已安装。
2026-09-11 20:47:48 +08:00
lzf_0626 76e87a33a7 更新 AGENTS.md 的表数口径:51 → 68(场内 51 + 场外/推广 17)
合并袁聪那 17 张表后,本文件仍写"52 张含 alembic_version = 51 张业务表",与
tools/audit_schema.py 实测的 68 张对不上。改为写明构成,并指向
docs/28-场外与推广域数据表登记.md —— 否则下一个人看到 68 会误以为是有人绕过基线
偷偷建表(audit_schema 的期望值是从迁移动态推导的,不是手写清单)。

核验命令里的 .\.venv\Scripts\python.exe 一并改成 python,与本节"各用本机可用的那个"
的口径一致(架构师环境是 conda)。
2026-09-11 20:24:39 +08:00
qyqy 928d0bcea3 chore: 按评审恢复 5 份文档、审计补 agent_type、修订环境口径
评审意见落地(§3.3 驳回删除 / §3.1 审计补充 / §1.4 解释器 / §2 编号):
- docs/04、06、10、13、99 全部恢复(评审:删除收益为零、保留成本同样为零);
  AGENTS.md 改为"保留但仅作历史参考"并列入 D 类,ARCHIVE 归档说明加作废声明
- 治理审计补留痕:interaction_audit.detail 增加 agent_type 与 governance_rewrite
  (治理层会改写对外输出,事后必须能追溯到是哪个 Agent 触发的;不改表结构,detail 是 JSON 列)
  新增 tests/unit/service/test_agent_persistence_audit.py 锁住该契约
- AGENTS.md 修订环境口径:解释器各用本机可用的那个(.venv 被 gitignore、不进仓库);
  config_release 与 Milvus schema 均属环境数据、不随代码合并,相关结论必须带环境限定;
  测试基线 1034;mypy 数字双方不可比(本机未装 sqlalchemy2-stubs,报错集中在模型层)
- docs/26 JWT 文档:因 21 已被风控迁移清单占用而改名,PR 描述里会单独说明
2026-09-11 18:42:46 +08:00
qyqy 96a6e01634 docs: 交接文档与 AGENTS.md 同步到合并后状态
- 交接文档 §0/§5/§6 重写:分支改为 NL_develop、测试基线 1013、已知问题逐条标注当前状态、
  Git 段改为已推送 + PR 注意事项(5 份文档删除/26 号新增/字段映射三处跨分支决策)
- AGENTS.md 入口指向与基线数字同步:工具名改 search_knowledge、生效版本 id=216、
  测试 1013、mypy 181(含归属说明)、新增 Agent 与文档编号现状
2026-09-11 15:41:49 +08:00
wangjianlong_0626 e4c4099aaa wip: 客服Agent + RAG + 画像收尾(基于 6516ccb) 2026-09-11 14:46:40 +08:00
Codex b1497fd2c6 chore: initialize project repository 2026-09-09 21:55:37 +08:00