新增 客服agent\D2.7-客服Agent中长期记忆与画像联动设计-2026-09-20.md(域 2,D2.x 续号)。 - 主设计主张:让记忆改变「系统行为」而非「模型输入」——记忆只作确定性信号(检索分区加权 / 澄清候选排序 / E2 计算入参 / 适当性过滤),永不进生成上下文(FR-CS-009 + FR-CS-049)。 - 新增记忆子域不变量 INV-M1~INV-M6(INV-M6:记忆只能收窄、不能放宽)。 - 登记两处过期理由:D2.2 §1.7 第 12 项 + 第 998 行澄清框仍引「投顾已整体清除」,而投顾已于 2026-09-20 恢复(D4.7)。 - 计数同步:D1.1 v1.5→v1.6,58→59 份(57→58 份编号),客服agent\ 6→7 份;§4.0 总表与 §4.1 明细各新增 D2.7 行;注入校验 54→55 份;新增 §25 轮次段。 - D2.1 标题 v6.30→v6.31(新增 v6.31 段);D1.6 新增 §4.44。 - 本轮不改代码、不改行为;门禁:check_authoritative_docs.py 通过(54 份无编号冲突)+ _consistency.py GATE PASS。
17 KiB
客服 Agent 中长期记忆与画像联动设计(2026-09-20)
体系编号:
D2.7· 域:二、对外交付 · 编号体系见D1.1§4.0 读者:答辩评委 + 答辩当天答疑的人 + 接手记忆子系统的开发者。 性质:回答一个问题 —— 「这个 Agent 跟用户画像是什么关系?对话能不能更新画像?要不要做中长期记忆?」 口径:本文所有代码级结论均为逐行读码取证(2026-09-20,本文件不改任何代码)。 配套:D2.2§1.7 与 §1.8.3(记忆 / 画像口径,含第 12 / 18 / 21 项)、D7.3§1.3 与 §6.2(三层记忆模型上游依据)、D4.7(投顾恢复)、D6.4.1(研判规则与熔断)、D2.4与D3.5(知识库侧)、D2.6(答辩主文档)、D1.6§4.43(本轮咨询记录)。
0. 一句话结论
Agent 与画像的关系是「读,不写」;「不写」不是技术限制,是 DEC-19 的合规裁定。
「对话能不能更新画像」——链路已经在仓库里(memory_unit → user_facts → fin_customer_profile → 快照 + 投影),技术上完全可行。真正卡住它的是三件事:
| # | 卡点 | 性质 |
|---|---|---|
| 1 | 客服是重构前唯一的画像候选来源,而 DEC-19 裁定「客服不产生画像候选」 |
合规裁定 |
| 2 | FR-CS-009(仅基于检索内容回答)+ FR-CS-049(证据约束生成)要求答案能追到 chunk id,记忆注入生成上下文会直接破坏证据约束 |
结构性冲突 |
| 3 | investor_type(风险等级)只准来自问卷,是代码级红线 |
合规底线 |
因此本文的主设计主张是一句话:让记忆改变「系统行为」,而不是改变「模型输入」。
1. 三问直答
| # | 用户的问题 | 直接回答 |
|---|---|---|
| 1 | 我们这个 Agent 跟用户画像有什么关系? | 单向只读。Agent 可读本人画像的白名单字段(risk_level / customer_level),且仅用于确定性规则(适当性过滤 / 转人工优先级 / 画像问答字段直返)。不写、不回写、不把画像原文塞进生成上下文。 |
| 2 | 用户跟 Agent 的对话能否作为更新画像的依据? | 能,且代码已在(见 §4)。但必须遵守字段分域:3 个字段可由对话更新,1 个字段绝对不可(见 §5)。当前整体关闭,原因是 DEC-19,不是"做不到"。 |
| 3 | 如果可以做,是不是要做中长期记忆? | 要做,但不做"把记忆塞进 prompt"那种。要做的是「面向行为的记忆」:记忆只作为路由与排序的输入,不作为回答的素材。见 §6。 |
2. 现状取证:客服侧记忆相关的五道闸门
(逐行读码,2026-09-20;均为显式不变量,不是"暂时没写")
| 层 | 载体 | 客服侧现状 | 证据(仓库相对路径:行号) |
|---|---|---|---|
| 短期会话记忆 | conversation_message + conversation_session(MySQL) |
✅ 开(读 / 写) | DEC-19 裁定 (b);D1.6 §4.40 |
| 中期记忆 | memory_unit(+ memory_evidence) |
❌ 客服不写 | app\worker\runtime.py:93 NO_LONG_TERM_MEMORY_AGENT_TYPES = frozenset({"customer_service"}) |
| 画像候选 | memory_unit.status = 'candidate' |
❌ 整体关闭 | app\worker\runtime.py:84 PROFILE_CANDIDATE_AGENT_TYPES: frozenset[str] = frozenset() |
| 长期记忆召回 | memory_unit / user_facts |
❌ 关 | app\service\agent\implementations\customer_service.py:606 recalls_customer_memory=False;基类闸门 app\service\agent\base.py:143 |
| 画像读取 | fin_customer_profile → 快照 → 投影 |
⚠️ 字段级只读 | query_customer_profile(self 作用域)+ app\core\profile_projection.py:36 PROFILE_FIELD_POLICY |
runtime.py:81-83 的注释原文(关键):
当前为空集,且不是「重构期临时状态」:
DEC-19裁定 (a) 明确「客服不产生画像候选」,而客服是重构前唯一的候选来源,故候选链路整体关闭。要重新打开必须先改DEC-19口径,而不是往本集合里加agent_type—— 那样等于绕过合规裁定。
🔎 这句话决定了本文的设计边界:任何"打开记忆"的方案,第一步都是改裁定,不是改代码。
3. 上游模型:三层记忆(D7.3 §1.3)
| 特性 | 人类记忆 | AI Agent 映射 | 技术实现 |
|---|---|---|---|
| 短期保持 | 工作记忆(几秒到几分钟) | 会话上下文 | Redis · 30 分钟 TTL |
| 中期巩固 | 反复激活的记忆变牢固 | 用户画像 / 偏好标签 | MySQL + Redis 缓存 |
| 长期遗忘 | 不用的记忆逐渐模糊 | 知识库 / 历史经验向量 | Milvus + 时间衰减 |
D7.3 是通用技术教材体裁,但 §6.2「记忆单元与身份标识」是被正式引用的上游依据(D2.2 §0.2、D2.4 §0.2 / §1.8.3、FR-CS-042 裁决理由)。文档头部状态标注为 保留,不归档(2026-09-17 曾误归档,同日 16:40 撤销)。
⚠️ 读
D7.3的正确姿势:把它当方法论来源,不要当本项目的实施方案 —— 它的三层划分是通用的,本项目对客服侧的裁剪(哪层开、哪层关)是DEC-19决定的。
4. 关键发现①:「对话 → 画像」链路已经在仓库里(不是待开发)
对话 → memory_unit(中期) → user_facts(长期) → fin_customer_profile(画像)
→ profile_snapshots(版本留痕) + Milvus / Neo4j 投影
门槛:evidence_count ≥ 2 或 confidence ≥ 0.90
证据(三份服务,全部既有):
| 文件 | 职责 |
|---|---|
app\service\memory_taxonomy.py |
受控记忆词表:13 个记忆键 + 显式信号表述表;MEMORY_TYPES = {preference, constraint, profile, goal, fact} |
app\service\profile_assembly_service.py |
模块文档首句即写「这是记忆系统为画像服务的落地环节」;三段职责 = ① 事实提升(中期→长期)② 画像组装(长期→画像,按白名单)③ 版本留痕 |
app\service\profile_generation_service.py |
同事务写新版本 + 两条同步事件(milvus / neo4j);is_current 先清旧再插新;snapshot_hash 未变则不写行 |
profile_assembly_service.py 关键常量:MIN_EVIDENCE = 2(第 38 行)、HIGH_CONFIDENCE = 0.90(第 39 行)。
⇒ 「能不能做」在技术上是既有能力,卡点全在合规裁定。这也是本文档存在的意义:把"能不能"和"该不该"分开讲清楚。
5. 关键发现②:字段分域 —— 谁能被对话更新,谁绝对不能
| 画像字段 | 对话能否更新 | 机制 |
|---|---|---|
preferred_asset_class |
✅ 能 | FACT_TO_PROFILE_FIELD["preference:asset_class"] |
investment_horizon |
✅ 能 | FACT_TO_PROFILE_FIELD["preference:horizon"] |
risk_tags(自述标签) |
✅ 能,标注来源「自述」 | SELF_REPORTED_PREFIXES = ("preference:risk_level", "preference:", "profile:") |
investor_type(风险等级) |
❌ 绝对不可 | 只来自 fin_risk_assessment 最新一条 |
total_asset / trading_frequency / behavior_score |
❌ 不可 | 交易侧所有(字段所有权) |
customer_tier |
❌ 不参与 | 快照按设计排除该字段(D-11) |
profile_assembly_service.py 第 15 行的红线原话:
investor_type只来自问卷测评(fin_risk_assessment最新一条)。……客户在对话里说"我是激进型"不会改变它 —— 这是合规底线,不能只靠约定。
PROFILE_OWNED_FIELDS = ("investor_type", "preferred_asset_class", "investment_horizon", "risk_tags")(第 59 行)—— 按字段所有权分域写入;CRITICAL_FACTS = {preference:risk_level, preference:horizon, preference:asset_class}(第 54 行)。
5.1 设计亮点(值得在答辩里主动讲)
自述等级不丢 —— 客户说"我是激进型",这句话不会被丢弃:它进 risk_tags 并标注来源「自述」。
于是当出现「问卷 C4 / 自述稳健 / 行为买 R4」这种三方不一致时,这个矛盾本身即风控信号(D3.1 FR-CS-024 的转人工摘要也含画像关键标签)。
一句话对外说法:我们不是"不 update",而是"分层 update + 矛盾留痕"。问卷管结论,对话管线索。
6. 关键设计主张:让记忆改变「系统行为」,而不是改变「模型输入」
6.1 五条允许的消费通道(记忆只到这里为止)
| 记忆键 | 消费点(代码位置) | 效果 | 碰生成上下文 |
|---|---|---|---|
preference:asset_class |
检索层分区 / 档位加权(_answer_from_knowledge 的检索入参装配,customer_service.py:784) |
问「有什么产品」时优先生命中族 | ❌ 不碰 |
preference:horizon |
E1 澄清候选排序(_exit_clarify,customer_service.py:2071) |
少问一轮 | ❌ 不碰 |
constraint:* / goal:* |
E2 计算型入参装配(费率参数位 / 匹配矩阵,customer_service.py:952 / 1341 / _exit_calc_miss:1232) |
试算带上已知约束,不必追问 | ❌ 不碰 |
preference:risk_level |
适当性过滤(已实现) | 无需改动 | ❌ 不碰 |
| 画像白名单字段 | 画像问答字段直返(render_profile,customer_service.py:228) |
已有能力 | ❌ 不碰 |
6.2 明确禁止
| 禁止项 | 理由 |
|---|---|
| ❌ 把任意记忆键注入生成上下文 / prompt | 违反 FR-CS-009 + FR-CS-049:生成式回答必须能追到 chunk id |
| ❌ 用记忆提升知识档位 | 档位由鉴权结果推导(knowledge_contracts.tiers_for_roles),customer_service.py 内零个档位字面量,不得由记忆旁路 |
| ❌ 用记忆放宽适当性 / 降低转人工门槛 | 记忆只能"收窄"不能"放宽"(见 §7 INV-M6) |
❌ 用记忆改动 investor_type |
§5 红线 |
6.3 为什么「不进生成上下文」是硬要求,不是偏好
这条冲突是结构性的,不会因为换模型或调 prompt 而消失:
FR-CS-009:仅基于检索内容回答;FR-CS-049:证据约束生成;- 输出数字一致性校验:答案里的数字要在已授权来源里找得到。
一旦把 memory_unit 的文本塞进 prompt,模型就会说出**"没有出处、但读起来像事实"**的句子 —— 这正是零容忍里的「无出处数字」与事实正确率的直接杀手。
🔑 对外一句话:记忆是「路由与排序的输入」,不是「回答的素材」。
7. 安全设计:记忆子系统不变量 INV-M1 ~ INV-M6
(与 D3.6 的 INV-1~INV-5 并列,属记忆子域)
| 编号 | 不变量 | 违反后果 |
|---|---|---|
INV-M1 |
记忆永不进入生成上下文;只作为确定性信号改变检索 / 排序 / 入参 | 无出处数字、事实正确率崩塌 |
INV-M2 |
investor_type 永不因对话改变;只来自 fin_risk_assessment 最新一条 |
合规事故 |
INV-M3 |
短期会话记忆恒开,与长期记忆开关解耦 | 多轮指代消解失效(H-01/H-02 类问题暴增) |
INV-M4 |
任何记忆写入须过 evidence_count ≥ 2 或 confidence ≥ 0.90,且不得绕过 FM-01~FM-05 |
画像被噪声 / 单次口误污染 |
INV-M5 |
只允许写 13 个受控记忆键;客服侧不写 profile:occupation / profile:family / profile:income_stability 三个 PII 键 |
个人信息越界采集 |
INV-M6 |
记忆只能收窄、不能放宽:不得提升知识档位、不得放宽适当性、不得降低转人工门槛 | 安全边界被记忆旁路 |
FM-01~FM-05(D6.4.1 第三章)速查:年龄限制 / 无收入且低资产 / 风险评估过期(>12 月冻结购买)/ 身份信息异常 / 异常交易熔断。
8. 已过期理由的更正登记(如实记录)
8.1 D2.2 §1.7 第 12 项(D2.2-客服Agent需求文档.html 第 986 行)
| 原文理由 | 2026-09-20 现状 |
|---|---|
| ① 「收益方(投顾)已整体清除」 | 🔴 已失效 —— 投顾模块 2026-09-20 已随合并恢复(见 D4.7) |
| ② 「注入生成上下文与证据约束生成直接冲突」 | ✅ 仍然成立,且是结构性的(见 §6.3) |
8.2 同文档第 998 行的澄清框(一并登记)
该框内同样写着「收益方投顾已清除」——与 8.1 属同一处过期依据,应与第 12 项一并更正。
⚠️ 为什么必须更正:答辩追问「为什么关长期记忆」时,如果引的是已失效的理由①,等于把一个已经被事实推翻的依据当论据 —— 风险远大于收益。关闭的正确依据只剩理由②(证据约束)+
DEC-19裁定本身。
9. 分期实施路径
| 期 | 时点 | 内容 | 前置 |
|---|---|---|---|
| P0 | 演示前 | 什么都不开。只做 §8 的文档更正与 §10 的反向守卫单测(加测试不改行为) | 无 |
| P1 | 演示后 | ① 先改 DEC-19 口径(书面);② 只开 preference:* / constraint:* / goal:*,不开 3 个 PII 键;③ 只接 §6.1 的五条确定性消费通道 |
P0 完成 + DEC-19 修订 |
| P2 | 可选 | 记忆→澄清排序的效果量化(用 E1 过度触发 14/46 这条基线做对照,目标:少问一轮) |
P1 有数据 |
🔴 P1 的硬前置是"先改裁定、再改代码" —— 直接往
PROFILE_CANDIDATE_AGENT_TYPES里加customer_service等于绕过合规裁定(runtime.py:81-83注释已明确禁止)。
10. 验收与守卫清单
| # | 守卫 | 类型 | 现状 |
|---|---|---|---|
| 1 | 对话说「我是激进型」后,fin_customer_profile.investor_type 必须不变 |
单测(INV-M2) |
🔴 缺失 —— 这条红线目前只在服务文档里写着,没有测试固定 |
| 2 | 生成上下文中不含任何 memory_unit 片段 |
单测(INV-M1) |
🔴 待补 |
| 3 | 带记忆请求 registered 内容仍被拒(记忆不放宽档位) |
单测(INV-M6) |
🔴 待补 |
| 4 | 46 条金标不回归(转人工率 10.9% / 出口准确率 100% / 事实正确率 100% / 四项零容忍全 0) | 回归 | ✅ 既有基线 |
| 5 | P0 / P1 / P2 / 明确要求 四类仍确定性拦下 |
单测 + 回归 | ✅ 既有 |
📌 第 1 条是本文档最推荐的立即动作:成本为零、不影响演示行为、却把一条"只写在文档里的合规底线"变成"代码可验证的合规底线"。
11. 与知识库设计的关系(D2.4 / D3.5)
记忆与知识库是两层不同的东西,答辩时最容易被混为一谈:
| 维度 | 知识库(D2.4 / D3.2) |
记忆(本文 D2.7) |
|---|---|---|
| 内容 | 公开 / 通用的产品、规则、FAQ | 个人的偏好、约束、目标 |
| 生命周期 | 版本化发布,不被对话改变 | 随对话累积(若开启) |
| 用途 | 回答的素材(要能引用 chunk id) | 行为的路由输入(不作出处) |
| 当前状态 | ✅ 已建成(3 集合 / 3 档可见性 / 分区隔离) | ❌ 客服侧整体关闭(DEC-19) |
「知识库还有没有更优方案」属另一个议题,备选池与各方案否决理由见
D3.5与D2.4,本文不重复。
12. 待决项与我方最优建议
| # | 待决事项 | 我方最优建议 |
|---|---|---|
| 1 | 是否打开画像候选 + 中期记忆 | 演示后(P1)再做;只开 preference:* / constraint:* / goal:*;不开 profile:occupation / profile:family / profile:income_stability 三个 PII 键 |
| 2 | 记忆消费方式 | 限定为确定性信号,并把「记忆不进生成上下文」写进 D2.2 §1.7 第 12 项,成为可测试的不变量 |
| 3 | 是否更正 D2.2 §1.7 第 12 项已过期的理由①(含第 998 行澄清框) |
更正 —— 否则答辩被追问时引的是失效依据 |
| 4 | investor_type 红线加回归 |
加一条单测:对话说「我是激进型」后画像 investor_type 必须不变 |
13. 诚实声明
| 已做 | 未做(因此哪些结论仍是推断) |
|---|---|
逐行读码:runtime.py / customer_service.py / base.py / profile_assembly_service.py / profile_generation_service.py / memory_taxonomy.py / profile_projection.py |
❌ 本文件未改任何代码、未跑任何测试 |
| 本文件所有常量、行号、类名、注释原文均已逐条核对 | ❌ 「记忆→检索加权的效果增益」尚无实测数字(P2 才有) |
与 D2.2 / D7.3 / D6.4.1 / D4.7 的口径对读 |
❌ DEC-19 的书面修订文本尚未起草(P1 前置) |
引用本文件时请注意:§6.1 的五条消费通道是设计主张,除
preference:risk_level(适当性过滤)与画像字段直返已实现外,其余三条尚未实现 —— 不要读成"已经能做"。
维护责任:本文件为活文档。
DEC-19口径修订、或记忆消费通道落地后,须回填 §9 / §10 的状态列,并同步D2.1(看板)与D1.6(会话记录)。 同步方向:权威副本(D:\桌面\金融\客服agent\)→ 仓库(group_fqcd_jr\客服agent\)单向覆盖(D1.6§4.38 既定)。