Files
group_fqcd_jr/docs/23-记忆分层与画像设计.md
T
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

8.3 KiB
Raw Blame History

记忆分层与画像设计

目的:讲清短期/中期/长期/画像四层各自存在哪里、谁写、什么条件下向上提升、是否进画像。 全部基于底座已有的真实表,不是另起一套;每节末尾标注实现现状。

0. 一句话

短期(会话上下文)→ 中期(候选事实)→ 长期(稳定事实)→ 画像(决策依据)

逐层提高门槛,是为了让"随口一说"永远变不成画像结论; 三条路径(问卷/行为/对话)在画像层汇合,但按字段划分所有权,不互相覆盖。

1. 总览

层 存在哪里 装什么 谁写 提升门槛 进画像
短期 Redis 会话列表(设计);conversation_message 表(现状只有存档) 当前会话最近的对话轮次 每次 run 追加 — ❌ 不进
中期 memory_unit + memory_evidence 从对话抽出的候选事实,带证据链 Worker 记忆抽取 evidence_count ≥ 2 或 confidence ≥ 0.90 ✅ 过门槛后提升
长期 user_facts(is_critical 标关键事实) 已确认的稳定事实 由中期提升 由 user_facts 判定 ✅ 唯一用途就是喂画像
画像 fin_customer_profile(当前)+ profile_snapshots(版本化,带 generation_basis) 整合三路来源的客户视图 画像组装服务 — 它就是画像

2. 三条路径在画像汇合

这是整套设计的核心结构。只有对话路径需要累积证据,另外两条是权威/客观数据,直接写:

对话消息 ──→ [短期] 会话上下文 ──→ Agent 用它理解指代("它""那个")
                  │
                  └─(run 完成,发 memory.extraction_requested 事件)
                          ↓
                    [中期] memory_unit + memory_evidence
                          │ 证据累积过门槛
                          ↓
                    [长期] user_facts
                          │
                          │            ┌── 问卷测评(权威)────┐
                          └──组装──────┤                      ├──→ [画像]
                                       └── 行为/流水(客观)──┘
                                                                    │
                                                        投顾 / 风控 / 客服

为什么问卷和行为不需要走前三层:它们是权威(问卷由客户答题、系统评分)与客观(交易记录无法自称)数据,不需要"多次出现才可信"这层保护;对话自述才有"记错、修饰、被引导"的风险。

3. 各层细节

3.1 短期:会话上下文

  • 装什么:当前会话最近若干轮对话,用于解析指代与保持连贯。
  • 怎么工作:run 开始时按 session_id 读出历史 → 与当前消息一起交给意图分类与回答生成;run 结束时把本轮追加进去。
  • 生命周期:会话结束或长时间无活动即失效(方案 §2.2 给的是 TTL 30min、最长 24h、超 4096 token 截断旧消息)。
  • 为什么不进画像:会话上下文是"他刚才说了什么",不是"他是什么样的人"。把临时对话当画像会造成画像抖动。
  • 实现现状:❌ 未实现。conversation_message 表存了全部消息、svc_conversation_session.message_count 只在计数,但没有任何"加载最近 N 条"的代码,run 时只拿到当前这一条消息。后果是多轮指代无法解析(问"那它风险高吗"无法知道"它"指谁)。

3.2 中期:候选事实

  • 装什么:从对话里抽出的事实(风险偏好自述、投资期限、家庭情况、资产类别偏好等),每条都带证据。
  • 怎么工作:
    • 写入:run 完成 → 发布 memory.extraction_requested → Worker 消费事件 → 调模型抽取 → 写 memory_unit,同时写一条 memory_evidence(含原文摘录,可回溯"这句话从哪来")。
    • 读取:run 开始时 recall_memory 自动召回,recall_count 累加。
    • 演进:同一事实再次出现 → evidence_count 增加;出现相反证据 → conflict_count 增加(不直接覆盖,留冲突待判)。
  • 生命周期:valid_from / valid_until;过期或长期无新证据即降级/失效(memory_lifecycle_service 负责级联失效)。
  • 进画像的条件:evidence_count ≥ 2 或 confidence ≥ 0.90 —— 这条门槛就是"一次性说法不该成为画像结论"的落地方式。
  • 实现现状:✅ 已实现并可验证。实测:客户说"我的风险偏好是稳健型,平时只买债券基金" → 抽出 preference:risk_level = 稳健型(置信 0.95)+ 1 条证据。
  • ⚠️ 运维前提:抽取依赖常驻 Worker 消费 Outbox 事件。只起 API 服务不起 Worker,事件会一直堆在 pending,记忆永远不产生。

3.3 长期:稳定事实

  • 装什么:经过证据累积确认的稳定事实,is_critical 标记其中对决策关键的那些(如风险偏好、投资期限)。
  • 怎么工作:只由中期提升而来(不直接从对话写);source_portal 记录事实来自哪个入口,source_episode_id 记录来自哪个会话片段。
  • 为什么不直接从对话写:长期层是画像的输入,必须保证"这条结论经过验证",否则画像会被一次性说法污染。
  • 进画像:✅ 它的存在意义就是喂画像。
  • 实现现状:❌ 表在(9 列),无生成代码,当前 0 行。

3.4 画像:决策依据

  • 装什么:整合三路来源后的客户视图,是投顾、风控、客服共同读取的唯一客户数据入口。

  • 怎么工作:画像组装服务按字段所有权写入 → fin_customer_profile 保存当前值 → 每次重建写一条 profile_snapshots(带 version、generation_basis、snapshot_hash、is_current),从而每次变化都可追溯。

  • 字段所有权(关键:按字段切开,避免互相覆盖):

    字段 唯一写入方 性质
    investor_type(C1–C5) 只有问卷测评 权威、合规硬约束
    total_asset、trading_frequency、behavior_score 交易/流水侧 客观行为
    preferred_asset_class、investment_horizon、risk_tags 记忆系统(经长期层) 软信息、需累积
    real_name、birth_date、occupation、mobile_masked、opened_at 注册/账户流程 身份
  • 客户自述风险偏好放哪:放 risk_tags 并标注"客户自述",不得写入 investor_type。 保留它的价值在于:当出现「问卷 C4 / 自述稳健 / 行为买 R4」三方不一致时,这种矛盾本身就是风控信号。

  • 实现现状:❌ 两张表都在,无组装代码,当前 0 行。

4. 红线(必须由代码保证,不能只靠约定)

  1. 适当性判定只认问卷。investor_type 只能由问卷写入,记忆与对话在任何情况下都不得修改它。客户在对话里说"我是激进型"不能让他买到 R5。
  2. 风控与投顾只读画像,不直接读 memory_unit。否则同一条记忆会被多处按各自口径解释。
  3. 每条画像字段都要能回答"凭什么"。写入时通过 generation_basis 记录依据来源。
  4. 画像变更留版本,不原地覆盖 —— 风控复盘时需要"当时看到的是什么"。
  5. 内部资料不进入面向客户的知识库(已按此处理:反洗钱手册 visibility=internal)。

5. 现状与缺口一览

环节 状态
对话 → 中期记忆(抽取 + 证据 + 可回溯) ✅ 已实现(依赖常驻 Worker)
中期 → 长期(证据累积与门槛) ❌ 待实现
长期 → 画像(组装 + 版本快照) ❌ 待实现
问卷 → 画像 ❌ 待实现(注册问卷流程未做)
行为 → 画像 ❌ 待实现(交易模块未做)
短期会话上下文 ❌ 待实现(影响多轮指代)
画像 → 投顾 / 风控 ❌ 待实现

建议顺序:先做「中期 → 长期 → 画像」这一段(现在就有记忆数据可验证),再接问卷与行为两路,最后接下游消费者。