lzf_0626
|
d4836ed4df
|
feat: 装配投影删除客户端(记忆失效/销户清理图与向量),并修复画像字段残留
一、装配 projection_cleaner
新增 app/service/projection_cleanup_service.py,并在**组装层**(app/worker/__main__.py)
注入。此前该客户端一直未装配,清理链路只记录降级(skipped_no_client),实际后果是
**销户后图里仍留着偏好关系**——投顾仍能通过关系网络看到这个人。
清理策略是"以画像为准"而不是按标识直接删边:先删掉该记忆对应的长期事实,再重建画像,
最后用对账修复让图跟着画像收敛。这样即使一条记忆影响多条派生边也能删干净,不依赖
"记得它当初投影成了什么"。Milvus 侧按 memory_uuid 删除;集合不存在(语义召回未启用)
时视为无需清理,客户端不可用则如实报告未清理,绝不伪造成功。
放在组装层而不是 runtime 内部兜底:组件内部给默认实现会把"尚未装配"这一事实悄悄盖住,
而"未注入即显式降级并留痕"是 runtime 刻意保留的语义。最初的改法写成内部兜底,被 5 个
既有单测拦下——那些测试是对的,因此改为在入口处注入。
二、顺带修复:事实消失后画像字段残留
实测:让 preference:horizon 记忆失效并清理后,user_facts 与图边都正确清除,但
fin_customer_profile.investment_horizon 仍是"长期(5年以上)"。根因是 rebuild_profile
只写"本轮有新值"的字段;事实被删后该字段没有新值,旧值就留在画像里——于是记忆已经作废,
投顾还能看到一个客户从未授权继续生效的投资期限。
改为:本轮没有对应事实时**清空**该字段;investor_type 例外——问卷是唯一权威,本轮没有
问卷记录时保持原值(该列 NOT NULL,且清空会在重测空档抹掉开户时的等级)。
三、实测
· 让 preference:horizon 失效后清理:cleaned=True,
detail=profile_rebuilt; graph_cleaned; vector_collection_absent;
· user_facts 2→1;图边由 ['HAS_GOAL','PREFERS'] 变为 ['PREFERS'];
· 画像 investment_horizon 由"长期(5年以上)"变为 None;investor_type 保持 C2、
risk_tags 不受影响(与 horizon 无关);
· 恢复记忆状态后重建,画像与图边均正确复原;
· ruff 通过、mypy 113 文件无错、unit+contract 447 passed、integration 29 passed。
|
2026-09-10 21:58:42 +08:00 |
|
lzf_0626
|
962a0a116f
|
feat: 记忆→画像打通(事实提升 + 画像组装 + 版本快照)
补齐"记忆系统为画像服务"的断链,按 docs/23 的分层设计实现后三层。
1. 新增 app/model/profile.py:user_facts 与 profile_snapshots 的 ORM 映射。此前这两张表
只有结构、没有 Model,实际没有任何代码在用。两处表结构特例在 docstring 里显式标注,
避免后续有人按直觉写入踩坑:
· user_facts.id 无 auto_increment,主键必须由应用提供(本实现用微秒时间戳,单调递增);
· profile_snapshots.current_customer_id 是生成列(IF(is_current=1, customer_id, NULL)),
故意不映射——映射了反而会在写入时与之冲突。
2. 新增 app/service/profile_assembly_service.py,三段职责:
· 事实提升(中期→长期):evidence_count ≥ 2 或 confidence ≥ 0.90 才从 memory_unit
提炼进 user_facts —— 这条门槛就是"客户随口一说不能变成画像结论"的落地方式;
· 画像组装(长期→画像):按白名单映射进 fin_customer_profile,未列入白名单的事实
(如 profile:family)只进 user_facts,保证画像的信噪比;
· 版本留痕:每次重建写一条 profile_snapshots,generation_basis 逐字段记录来源,
用于回答"当时凭什么这么判断"。
3. 新增 tools/rebuild_profile.py:手工触发入口(单客户或 --all)。画像暂无自动触发,
这是目前唯一的重建方式,也便于排查"画像为什么没更新"。
红线由代码保证而非约定:investor_type 只从 fin_risk_assessment 最新一条读取,实现中
不存在任何记忆路径能写它。实测——客户 9001 问卷为 C2、对话自述"稳健型",重建后
investor_type 仍为 C2,自述信息进入 risk_tags 并标注"自述:"前缀。三方不一致保持可见,
但等级判定只认问卷,客户无法靠对话改变自己的可购范围。
另一处由实测修正的设计:fin_customer_profile 的 trade_account/real_name/total_asset/
behavior_score 均为 NOT NULL,说明画像行由开户流程创建(也印证了"注册时填问卷"是开户
前置条件)。原先"首次重建时创建画像行"的做法是错的——会写出一条假的开户记录,而画像
恰恰是风控要读的数据。已改为只更新已存在的画像,未开户时返回 reason=profile_row_not_opened
并如实报告,而不是静默成功。
同时新增 docs/23-记忆分层与画像设计.md:短期/中期/长期/画像四层各自存在哪里、谁写、
提升门槛、是否进画像,以及三条路径(问卷/行为/对话)在画像层汇合的设计。
验证:ruff 通过、mypy 109 文件无错;tools/rebuild_profile.py 对客户 9001 连续两次重建
产生 version=1/2 两条快照且 is_current 正确轮转(旧版本置 0)。
|
2026-09-10 21:36:23 +08:00 |
|