FinClaw V1 Memory and Personalization Design
状态:Accepted Initial Design / B-6 工程蓝图(Wave 2) 日期:2026-05-16 项目:FinClaw 文档级别:项目级工程蓝图(V1 记忆与个性化规则) 上游文档:v1-product-object-and-schema-design.md §8 / §11、v1-data-and-persistence-design.md §2.3 / §6.5 / §7 / §9(B-2)、v1-api-contract-design.md §4.2.15(B-3)、v1-engineering-kickoff-decisions.md D-12、v1-agent-orchestration-design.md §2 / §3 配套文档:v1-tech-stack-and-architecture-design.md §6(B-1)、v1-ui-prototype-and-design-system.md §6(B-4)
1. Purpose
本文是 FinClaw V1 工程仓库 /Users/mlabs/Programs/CurvatureLabs/finclaw/ 在记忆(memory)与个性化(personalization)层的工程蓝图。
回答 4 个问题:
- V1 记忆体系由哪些对象组成?哪些落 profile,哪些落 thread,哪些只 task-scoped?
- 系统在哪些时刻写用户记忆?哪些时刻读?读时按 route 类型选择性载入哪些 slice?
- V1 阶段允许的个性化边界在哪里?哪些路径禁止个性化?
- 用户撤回 / 导出 / 删除时记忆层做什么动作?
本文不重写 ProfileConsent / UserContext / TrainingAssetCandidate 的 schema(已在 v1-product-object-and-schema-design.md §8 / §11 锁定)、不展开持久化路径(已在 B-2 §4 / §7 / §9 锁定)、不改 API endpoint(B-3 §4.2.15)。
1.1 关键术语
| 术语 | 含义 |
|---|---|
| Memory | 系统对用户的「记忆」总称:包括 task-scoped context、saved profile、persona drift log |
| Personalization | 系统根据 memory 对输出做的差异化处理(语言、advisor 出场顺序、Skill 推荐) |
| Slice | 一段可单独 inject 到 system prompt 的 memory 子集(如「研究深度偏好」「语言偏好」「最近 3 个活跃 thread 主题」) |
| Profile | 持久化到 user-scoped 长期记忆(受 ProfileConsent 闸门) |
| Thread context | 持久化到单个 thread 的中期记忆 |
| Task-scoped context | 仅本次任务有效(默认 24 小时后清理,B-2 §7.1) |
2. Goals and Non-Goals
2.1 Goals
| Goal | 落点 |
|---|---|
| G-1 | 把 v1-product-object-and-schema-design.md §8 UserContext / ProfileConsent 与 §11 TrainingAssetCandidate 的字段 down-pour 为可执行的「何时注入 / 何时仅供后端使用」规则(直接对应 Wave 1 anchor open_question 3) |
| G-2 | V1 trial 3 人规模下,对用户可见的个性化收益(语气、advisor 出场顺序、Skill 推荐);对用户不可见但有质量影响的路径(evidence 排序)不做个性化 |
| G-3 | 与 B-2 §6.5 ProfileConsent 撤回的副作用 + D-12 完全对齐:撤回 / 导出 / 删除路径在记忆层无遗漏 |
| G-4 | 引入 Persona Drift Log 新对象,用于跟踪用户对同一 thesis 的偏好变化(如 grade 反转);这是 V1 内测对「记忆是否漂移」做工程层观测的依据 |
| G-5 | Cold start(新用户首次任务)有 minimum viable user context 路径,不阻塞 onboarding 完成前的首次输出 |
| G-6 | Memory read 按 route 选择性载入相关 slice;不把全量 memory 灌入 system prompt(避免 attention dilution + token 浪费) |
2.2 Non-Goals
| Non-Goal | 理由 |
|---|---|
| 不引入向量数据库 / RAG over user memory | V1 内测 3 人,长期记忆体量 < 10 MB;按 thread 检索 + 显式 slice 选择足够 |
| 不实现 cross-session 自动 thesis tracking | thread refresh 已经承担「持续跟踪」职责(v1-prd.md §6.5) |
| 不为 evidence 排序做个性化(避免 echo chamber) | §7 显式禁止 |
| 不引入 user embedding / cluster | V1 trial 3 人样本太小;V2 评估 |
| 不实现「LLM-as-memory-summarizer」自动压缩长期记忆 | V1 用结构化字段保存,不让模型自由 paraphrase 用户长期记忆(合规边界) |
| 不为 advisor / Skill 做 reinforcement learning | D-08 仅授权 evaluation runs,不做训练闭环 |
| 不暴露 admin endpoint 修改其他用户的 memory | user_id 隔离硬约束(B-3 §3.3) |
2.3 与既有决策对齐
- D-12 隐私 / 合规:本文 §10 + §11 在记忆层先行落地撤回 / 导出 / 删除路径;
- Wave 1 anchor open_question 3:本文 §6.5 / §10.4 直接覆盖 ProfileConsent 撤回副作用;
- B-2 §6.5:撤回后已生成的 Snapshot / Thread 保留,但后续 LLM 调用不再注入 saved profile — 本文 §10.4 详化注入侧动作;
- v1-product-object-and-schema-design.md §13 API and Persistence Boundaries:不在未 consent 时把金融上下文存到 profile / thread。
3. Memory Object Layer
3.1 对象层
V1 的记忆体系由 3 类对象组成,构成 Task-Scoped → Thread → Profile 三层结构:
| 层 | 对象 | Scope | Mutability | Source |
|---|---|---|---|---|
| Task-Scoped | UserContext (task_scope) | 单次任务 | task-end 时 24h 内清理 | Task Router / Onboarding |
| Thread | UserContext (thread_scope) | 单个 Thread | thread refresh 时可 mutate;thread closed 后只读 | save-as-thread 时建立 |
| Profile | UserContext (profile_scope) | 全用户 | 用户主动编辑可 mutate | ProfileConsent grant 时建立 |
| Audit | ProfileConsent | 全用户 | mutable + version-tracked | ProfileConsentDialog (B-4 §6) |
| Audit | Persona Drift Log ⭐新引入 | 全用户 | append-only | grade 反转 / feedback 冲突时触发 |
⭐ Persona Drift Log 是本文新引入的对象,目的:观测用户在不同 session 对同一 thesis / asset 的偏好是否漂移。详见 §4。
3.2 UserContext 的三种 scope
v1-product-object-and-schema-design.md §8.1 UserContext 已定义字段。本文细化 scope 边界:
| Scope | retention(B-2 §7.1) | 允许字段 | 注入位置 |
|---|---|---|---|
task | 任务结束 24h | 全部字段(含未 consent 的 user_stated_position_context / constraints) | 仅本次 task 的 context_engine |
thread | 跟随 Thread;thread 关闭后保持只读 | 需 ProfileConsent.save_to_thread=true | thread refresh 时注入 |
profile | 跟随 ProfileConsent(永久 / 撤回 48h) | 需 ProfileConsent.save_to_profile=true | 所有任务的 context_engine(如 §6.1) |
落盘路径与 B-2 §4.1 一致:
data/user_contexts/<user_id>/
├── _task/<context_id>.json ← scope=task;24h TTL
├── _thread/<thread_id>/<context_id>.json ← scope=thread;thread 生命周期内有效
└── _profile/<context_id>.json ← scope=profile;与 consent 联动
3.3 Persona Drift Log
3.3.1 Schema
persona_drift_log:
drift_id: "drift_<iso>_<6-char-hash>"
user_id: string # path 中体现;不入字段
object_ref: string # snapshot:.../thread:.../checkpoint:... (B-3 §6)
cognition_object: object # 简化引用:asset / theme / event
signal_kind: enum # grade_reversal | feedback_conflict | refresh_stance_flip | watch_question_resolved_then_reopened
prior_signal:
value: string # 例:thumbs_up / agree
captured_at: datetime
captured_in_object_ref: string
current_signal:
value: string
captured_at: datetime
captured_in_object_ref: string
delta_summary: string # 人类可读:「2026-05-18 grade SOL as helpful,2026-05-30 grade SOL as not_helpful」
triggers_alert: bool # signal_kind ∈ {grade_reversal, refresh_stance_flip} 默认 true
schema_version: "1.0"
3.3.2 落盘路径
data/persona_drift/<user_id>/<drift_id>.json
data/persona_drift/<user_id>/_index.jsonl
append-only。ID 前缀 drift_(新增到 B-2 §5.1 的 ID prefix 表 — 见 §12 acceptance 中标记为 schema 锁定增量,并入 anchor)。
3.3.3 触发关系
详细触发详见 §9 Drift Detection。
3.4 与 v1-product-object-and-schema-design.md §11 TrainingAssetCandidate 的关系
TrainingAssetCandidate 是「未来训练资产候选」治理对象,不是 V1 训练循环。V1 阶段:
- 候选对象由 Object Writer 在 Snapshot / Thread refresh / Checkpoint 写盘后追加生成(仅当 ProfileConsent.training_use_allowed=true);
- 候选对象 status 默认
pending,不进入任何训练流(V1 没有训练流); - 候选对象与本文记忆层逻辑独立:记忆层负责「下次任务该用什么 context」,TrainingAssetCandidate 是「未来训练时该用什么样本」;
- ProfileConsent 撤回时 TrainingAssetCandidate 全部
withdrawal_status=revoked(B-2 §6.5)。
4. Persona Drift Tracking
4.1 为什么需要
V1 内测期间,用户对同一 thesis 可能给出不同 grade(例如 D-1 对 SOL bullish thesis grade thumbs_up;D-30 同 thesis grade not_helpful)。如系统盲目把最近 grade 作为 memory 写入 profile,会形成「记忆漂移」:
- 早期偏好被覆盖 → 用户感觉「系统忘了我刚说过的话」;
- 反复偏好被平均 → 系统输出向中间靠拢,无法体现用户实际想法;
- 没有 drift 跟踪 → 工程师 / PM 无法解释「为什么这周 advisor 推荐变了」。
Persona Drift Log 是append-only 观测:捕获 drift 信号,但不自动决策(不自动修改 profile)。drift 处理决策权在用户(通过 §11 review / delete UX)或项目发起人(trial closeout 时人工审)。
4.2 Drift Signal 来源
| signal_kind | 触发位置 | 数据源 |
|---|---|---|
grade_reversal | Feedback Adapter 写入新 feedback 时 | 比较与同一 related_object_ref 的历史 feedback 的 rating 字段 |
feedback_conflict | 同上 | 用户在新 feedback 中显式说「我之前的判断错了」 / 关键词检测 |
refresh_stance_flip | Thread refresh 完成时 | 比较新 snapshot 的 main_thesis 与上次 snapshot;如 thesis 显著反转(嵌入相似度 < 阈值或人工识别) |
watch_question_resolved_then_reopened | watch_question_resolved 事件后又被同 thread 标记 reopened | watch question lifecycle |
4.3 与现有 SSE event 的关系
drift 检测不发独立 SSE event(避免给用户造成「系统判断你错了」的压力)。drift log 仅入:
data/persona_drift/<user_id>/...持久化;- metric
persona_drift_total{signal_kind, triggers_alert}(B-5 §5.2.2 扩展); - 项目发起人
make obs-summary周报。
4.4 用户可见性
| 项 | V1 是否给用户看 |
|---|---|
| Drift Log 原文 | 否(仅 export 时含) |
| Drift 提示「你之前对 X 是 thumbs_up,现在 thumbs_down,要保留哪个?」 | 否(V1 不打扰;trial 退出后评估) |
Drift 在 /api/users/me/export 中可见 | 是(§11.2) |
5. Memory Write Triggers
5.1 触发关系总览
| 触发 | 写入对象 | scope | 写入条件 |
|---|---|---|---|
| Onboarding 完成 | UserContext (profile) + ProfileConsent | profile | 用户在 ProfileConsentDialog grant;§8.2 |
| save-as-thread | UserContext (thread) | thread | request.save_user_context=true + consent_ack.consent_id 有效(B-3 §4.2.6) |
| Refresh 触发 | UserContext (thread).updated_at;可能 update fields | thread | 用户 in-flight 提供 user_supplied_context(B-3 §4.2.10) |
| 用户显式 review | UserContext (profile) | profile | 用户在 settings 编辑 saved profile(V1 UI 雏形,B-4 §5) |
| Feedback 反复出现的关键词 | drift signal 候选(非 profile 直写) | drift | §5.5 |
| 任务结束(每次) | UserContext (task) 落盘 + 24h TTL 标记 | task | 必写(用于 trace) |
V1 不自动从 LLM output 推断用户偏好后再写 profile(避免无 consent 的隐式学习)。Profile mutation 只有 2 个入口:onboarding grant 与用户显式 review。
5.2 Onboarding 完成
POST /api/users/{user_id}/profile-consent 成功后:
- 写
data/consents/<user_id>.yamlv1(B-2 §4.1); - 若
save_to_profile=true:从 ProfileConsentDialog 收集的偏好字段(language_preference / research_depth / risk_prompt_preference)写到data/user_contexts/<user_id>/_profile/<context_id>.json; - 推送 SSE 不需要(onboarding 走 REST 同步);
- 写 metric
profile_consent_granted_total; - 写 trace span
profile_consent_grant(B-5 §6.2)。
5.3 Save-as-Thread
POST /api/snapshots/{snapshot_id}/save-as-thread 成功后:
- 写 thread header(B-2 §6.1);
- 若
save_user_context=true:- 读取 ProfileConsent,验证
save_to_thread=true(否则返回 403); - 写
data/user_contexts/<user_id>/_thread/<thread_id>/<context_id>.json,含user_focus_reason+research_style+ 本次user_stated_position_context(如有);
- 读取 ProfileConsent,验证
- 不自动把 thread context 升级到 profile(profile 升级只能用户 explicit review)。
5.4 Refresh 触发的 mutate
Thread refresh 期间若用户提供 user_supplied_context:
| 字段 | 写入位置 |
|---|---|
| 与现有 thread context 一致的偏好微调 | append 到 data/user_contexts/<user_id>/_thread/<thread_id>/ 新增 context(不覆盖旧的) |
| 显著反转的判断(thesis flip) | 写 drift signal 候选 → 进入 §9 检测 |
| 含敏感金融上下文且未 consent | Sensitive Input Classifier 拦截;走 SensitiveInputHandling 路径 |
5.5 Feedback 反复出现的关键词
POST /api/feedback 写入时:
| 步骤 | 动作 |
|---|---|
| 1 | 写 feedback 到 review queue(B-3 §4.2.14) |
| 2 | 提取 comment 字段关键词(local sentiment lite + 关键词词典,不调 LLM);过滤敏感字段(B-5 §12.4) |
| 3 | 与同 related_object_ref 类的 cognition_object 的历史 feedback 比对 |
| 4 | 若关键词反向出现 ≥ 2 次 → 写 drift signal feedback_conflict |
| 5 | 不直接 mutate profile;等待用户在 §11 review |
V1 不让 LLM 自由 paraphrase 用户 feedback 后写 profile(合规边界:自由 paraphrase 风险 = 隐式学习 = 未 consent 的训练)。
5.6 任务结束的 task-scoped 落盘
每次任务结束(含 failed):
| 项 | 写入 |
|---|---|
data/user_contexts/<user_id>/_task/<context_id>.json | 写本次 UserContext 全量(含未 consent 字段;24h TTL) |
| 元数据 | task_id, created_at, expires_at(now + 24h) |
| 用途 | 仅用于 trace 关联(B-5 §6.2)与同一任务的多次 cognition_step 共享 context;24h 后由 archive_expired 脚本物理删除 |
6. Memory Read Strategy (per Route)
6.1 总策略:选择性载入
V1 不把全量 memory 灌入 system prompt:
| 反模式(不做) | 原因 |
|---|---|
| 把所有 saved profile 字段一次性塞进 system prompt | attention dilution;浪费 token;隐藏假设 |
| 把全部 thread context 都拼进 prompt | token 爆炸;refresh 任务 token 成本失控 |
| 把整个 ProfileConsent JSON 灌入 prompt | 合规字段不应进入 LLM input |
V1 正确做法:Context Engine 按 route 选择性载入相关 slice。
6.2 Slice 定义
| Slice | 来源 | 大小估算(token) |
|---|---|---|
S1_language_pref | UserContext.language_preference | < 20 |
S2_research_depth | UserContext.research_depth | < 20 |
S3_risk_prompt_pref | UserContext.risk_prompt_preference | < 20 |
S4_user_focus_reason | UserContext.user_focus_reason(仅 thread refresh) | < 100 |
S5_research_style | thread research_style | < 200 |
S6_recent_active_threads_titles | 最近 3 个 active thread 的标题 + cognition_object 摘要 | < 300 |
S7_user_stated_position_context | task-scoped position context | < 200 |
S8_constraints | task-scoped constraints | < 200 |
S9_persona_drift_recent | 最近 30 天该 user 的 drift signal 数(数值),不含原文 | < 50 |
slice 由 server/agent/context_engine.py 装配,输出到 system prompt 一个明显标记的段落:
<user_context>
{slice_S1}
{slice_S2}
...
</user_context>
6.3 Route → Slice 映射
7 类 route(v1-agent-orchestration-design.md §3)+ slice 加载矩阵:
| Route | 必载 slice | 可选 slice | 不载 slice |
|---|---|---|---|
snapshot | S1, S2, S3 | S6 (若用户在 prompt 引用了 "我的 thread") | S4, S5, S7, S8, S9 |
thread_refresh | S1, S2, S3, S4, S5 | S9(drift 提示但不展开) | S6, S7, S8 |
risk_challenge | S1, S3 | S5(若来自 thread) | S2, S4, S6, S7, S8, S9 |
pre_execution | S1, S2, S3 | S7, S8(task-scoped) | S4, S5, S6, S9 |
evidence_audit | S1 | — | S2-S9 (evidence 不个性化 — §7.2) |
narrative_mapper | S1, S2 | S6 | S3, S4, S5, S7, S8, S9 |
event_impact_reader | S1, S2 | S6 | S3, S4, S5, S7, S8, S9 |
6.4 Slice 加载顺序与覆盖
- 优先级:task-scoped > thread-scoped > profile-scoped;高优先级覆盖低优先级(如 task
user_context_overrides.research_depth覆盖 saved profile.research_depth); - 缺失字段:直接跳过该 slice(不写空字段,避免 noise);
- Cold start(无 profile,无 thread):仅 S1 = "default English" + S2 = "default standard" + S3 = "default balanced"(§8);
- 注入位置:system prompt 末尾,紧邻 user message 之前(避免被 instruction 段挤压)。
6.5 ProfileConsent 撤回后的读取行为(与 B-2 §6.5 对齐)
撤回后,Context Engine 加载 slice 时按如下规则:
| Scope | 撤回后行为 |
|---|---|
profile | 全部 slice 不加载(profile_scope context 视为不存在) |
thread | 已生成的 thread 仍可访问;但新任务的 thread refresh 不再注入 thread-scoped slice;新 thread 创建被拒(需重新 consent) |
task | 不受影响(仅本次任务有效) |
实现位置:server/agent/context_engine.py::load_memory_slices(user_id, route) 第一行 → 检查 consent;未 consent 直接跳到 §8 cold-start fallback。
6.6 Slice 加载的可观测性
每次 task 在 trace context_built span(B-5 §6.3)中记录:
attributes:
slices_loaded: ["S1", "S2", "S3"]
slices_skipped_missing: ["S4", "S5"]
slices_blocked_by_consent: []
total_memory_tokens_estimated: 60
便于 debug 与 trial closeout 复核「记忆是否被滥用 / 缺漏」。
7. Personalization Boundaries
7.1 允许的个性化(V1 范围)
| 路径 | 个性化程度 | 来源 slice |
|---|---|---|
| Advisor 出场顺序 | 轻度个性化:根据 S3_risk_prompt_pref(balanced / counter_first / risk_first)调整 advisor 出场顺序 | S3 |
| Skill 推荐(Home 页 / save thread sheet) | 轻度个性化:基于 S6_recent_active_threads_titles 把相关 skill 提到列表前面 | S6 |
| 输出语言 | 完全个性化:S1 决定输出主语言(中 / 英) | S1 |
| 输出深度 / 报告长度 | 中度个性化:S2 决定 research_depth enum(quick / standard / deep) | S2 |
| Refresh 提示频率 | 不个性化(V1 不实现自动 refresh;用户手动触发) | — |
7.2 显式禁止个性化的路径(避免 echo chamber)
| 路径 | 禁止个性化理由 |
|---|---|
| Evidence 排序 | 用户偏好不应影响事实呈现顺序;individual user grade 不能让该 evidence 在另一 user 视图中被压低 |
| Counter-thesis 生成 | counter-thesis 必须独立于 user 偏好生成;如个性化会形成「同温层」 |
| Invalidators 列表 | 同上 |
| Data Quality Note | 数据质量是客观的;user 偏好不应改变标注 |
| BoundaryGuard 判定 | 边界判定完全独立于 user;不允许 "trusted user" 概念 |
| Sensitive Input Classifier | 同上;任何 user 都按同一规则分类 |
| Pre-Execution Checkpoint 的 forbidden_execution_fields | 全 user 同一清单(v1-product-object-and-schema-design.md §5.2) |
实现位置:上述路径的 Skill / advisor prompt 不注入任何 user-context slice(load_memory_slices(route='evidence_audit') 仅返回 S1 语言)。
7.3 Advisor 出场顺序的个性化规则
risk_prompt_preference | Advisor 出场顺序 |
|---|---|
balanced(默认) | Asset Research → Market/Macro → Risk → Counter-Thesis |
counter_first | Counter-Thesis → Asset Research → Market/Macro → Risk |
risk_first | Risk → Asset Research → Counter-Thesis → Market/Macro |
permission_or_credential(仅 Pre-Execution Checkpoint) | Pre-Execution Advisor 强制首位 |
Advisor 个性化仅调整顺序,不调整是否调用 advisor(每条 route 的 advisor 必走集合不变;保证认知完整性)。
7.4 Skill 推荐的个性化规则
| 场景 | 个性化 |
|---|---|
| Home 页 task input | 显示用户最近 3 次使用过的 task_type 提示(默认 snapshot / thread_refresh / risk_challenge) |
| Save Thread Sheet 默认 refresh_conditions | 基于 thread 的 cognition_object 类型(asset / theme / event)映射到推荐 refresh_conditions;不基于 user history |
| Onboarding 完成后第一次任务 | 显示「跟你刚才设置的偏好对应」的示例 question(语言匹配) |
不个性化的入口:
- 错误页 / boundary 拒绝页(B-4 §6):通用文案,不个性化;
- Sensitive input rejection 文案:通用,不个性化;
- Kill switch 页:通用,不个性化。
8. Cold Start and Onboarding
8.1 Cold Start 定义
| 状态 | 表征 |
|---|---|
| Brand-new user | data/consents/<user_id>.yaml 不存在;任何 user_contexts 都不存在 |
| Onboarding-incomplete | token 已发但 ProfileConsent 未 grant |
| Profile-revoked | ProfileConsent.revoked_at 非空(但保留账号) |
8.2 Onboarding Minimum Viable Path(B-4 §6 onboarding flow 的 down-pour)
V1 onboarding 流程对记忆层的最小约束:
| 步骤 | 必须? | 落点 |
|---|---|---|
| 显示 Labs 内部知会函(D-13) | 是 | 仅前端展示;写 metric onboarding_step_total{step=acknowledgement} |
| ProfileConsentDialog 4 项 ConsentScope | 是 | POST /api/users/{user_id}/profile-consent |
填写 minimum viable preferences:language_preference + research_depth + risk_prompt_preference | 可选(用户跳过则用默认值) | 若填则随 consent 一并写 |
填写 user_focus_reason(首条 thesis 兴趣) | 不强制(避免 onboarding 流程过长) | 若填则保存到 profile context |
8.3 First-Task Behavior
用户 onboarding 完成后第一次发起 task:
| 状态 | Context Engine 行为 |
|---|---|
| 用户填了所有 minimum viable preferences | 加载 S1 / S2 / S3 → 正常路径 |
| 用户填了一部分 | 缺失 slice 用默认值(English / standard / balanced) |
| 用户跳过全部偏好 | 仅 S1=English / S2=standard / S3=balanced;UI 在结果尾部提示「设置偏好可让结果更贴合你」(轻提示,不强制) |
8.4 Onboarding-Incomplete 但试图发任务
| 场景 | 行为 |
|---|---|
| token 有效但未 grant ProfileConsent | POST /api/tasks 返回 403 + code: profile_consent_required + details.required_scopes: [...] |
| 前端响应 | 弹回 ProfileConsentDialog;不允许跳过 |
实施位置:server/api/cognition_routes.py middleware 在 task 创建前调用 consent_gate.require(user_id, scope='create_task')。
8.5 Profile-Revoked 后的 Cold Start
撤回 consent 后用户仍可发任务,但记忆层视为 cold start:
| 项 | 行为 |
|---|---|
| Profile slice 不加载 | §6.5 |
| Thread slice 不再加载新 task;但已生成 thread 仍可只读 | 同 |
| UI 提示 | "已撤回 profile;如需个性化结果,请重新设置 consent" |
| 重新 grant consent | 重新走 §8.2 onboarding minimum viable path |
9. Drift Detection
9.1 检测算法(V1 lite)
V1 不引入嵌入相似度计算;用结构化规则:
| signal_kind | 算法 |
|---|---|
grade_reversal | 同 related_object_ref 的 feedback rating 在 14 天内出现反向(thumbs_up → not_helpful 或反向) |
feedback_conflict | feedback comment 关键词反向(§5.5)≥ 2 次 |
refresh_stance_flip | thread refresh 的 change_summary 含 changed_inferences[] 数量 > 5 且 thread current_thesis 与 14 天前 main_thesis 完全不重合(关键词 jaccard < 0.2) |
watch_question_resolved_then_reopened | watch question id 在 watch_question_updates 中标记 resolved 后又在 14 天内被相同 id 标 reopened |
阈值(14 天、jaccard 0.2 等)写入 server/config/drift_thresholds.yaml,trial 期间可调。
9.2 检测触发时机
| 事件 | 触发 detector |
|---|---|
POST /api/feedback 写入完成 | grade_reversal / feedback_conflict detector |
thread_refreshed SSE 推送前 | refresh_stance_flip detector |
watch_question_resolved 写入 | watch_question_resolved_then_reopened detector(推迟 1 小时后扫描,避免立即误判) |
9.3 检测结果落点
data/persona_drift/<user_id>/<drift_id>.json
data/persona_drift/<user_id>/_index.jsonl
metric persona_drift_total{signal_kind, triggers_alert}
trace span persona_drift_detected (attributes: drift_id, signal_kind)
不发独立 SSE event(§4.3)。
9.4 Drift 后的下游动作(V1 内只观测,不自动决策)
| 触发条件 | V1 动作 | V2 候选动作(仅记录在 §13) |
|---|---|---|
grade_reversal 单次 | 记录 drift log | UI 主动询问「保留哪个 grade」 |
| 24 小时内 ≥ 3 次 drift | log warn + 项目发起人 digest 中可见 | 自动暂停个性化(slice 不加载) |
feedback_conflict 累计 ≥ 5 次 | log warn | 触发人工 review queue |
refresh_stance_flip | log warn | 在 thread refresh 弹窗中显式呈现 thesis 反转 |
V1 范围内 drift 仅作为观测信号,不自动改变用户 profile 或 personalization 行为。
10. Privacy and Retention
10.1 Retention Matrix(与 B-2 §7.1 一致 + 增量)
| Object | retention |
|---|---|
| UserContext (task) | 24h(B-2 §7.1) |
| UserContext (thread) | 跟随 Thread;撤回 consent 后保留只读 |
| UserContext (profile) | 跟随 ProfileConsent;撤回后 48h 内 purge |
| ProfileConsent | 永久(含 _history/;合规证据) |
| Persona Drift Log | 永久(与 ProfileConsent 同生命周期;撤回 48h 内 purge) |
| TrainingAssetCandidate | 受 ProfileConsent.training_use_allowed 约束(B-2 §7.1) |
Persona Drift Log 的特殊性:drift log 含 object_ref 反查能定位用户具体观点,故视为「user_voluntary_context」分类(B-2 §9.3),随 consent 撤回一起 purge。
10.2 PII / 敏感信息边界
| 字段 | V1 在记忆层是否保存? |
|---|---|
language_preference / research_depth / risk_prompt_preference | 保存(ordinary_preference 类,v1-product-object-and-schema-design.md §9) |
user_focus_reason(自由文本) | 保存,但 Sensitive Input Classifier 在写入前过滤敏感片段 |
user_stated_position_context (含持仓 / 成本) | 仅 task-scoped 24h;profile-scope 需显式 consent.save_to_profile + 显式 prompt 提示 |
time_horizon / constraints | task-scoped 24h;profile-scope 同上 |
| 真实姓名 / 邮箱 / 钱包地址 / API key / 私钥 | 永不保存 → Sensitive Input Classifier 拒收(v1-product-object-and-schema-design.md §9) |
10.3 与 B-5 §12 隐私在 telemetry 的边界
| 范围 | 责任 |
|---|---|
| Memory layer(本文) | 保证 profile / thread / task context 在写入时不含 sensitive_input;撤回时按 §10.4 行动 |
| Telemetry layer(B-5) | 保证 logs / metrics / traces 不直接写 user_id 明文 / prompt / response;用户撤回 consent 不影响 telemetry(不含 PII) |
记忆层撤回会物理删除对应 user_context 文件;telemetry 层撤回不删除(不含 PII)。两层职责物理隔离。
10.4 撤回 ProfileConsent 的记忆层副作用
按 B-2 §6.5 + 本文 §6.5 + §10.1:
| 项 | 动作 | 时机 |
|---|---|---|
data/user_contexts/<user_id>/_profile/* | 物理删除 | 撤回时同步(同事务内) |
data/user_contexts/<user_id>/_thread/<thread_id>/* | 保留(用户仍可查看 thread;但 Context Engine 不再读取) | 撤回时改 consent_blocked: true flag |
data/user_contexts/<user_id>/_task/* | 自然 24h TTL 清理;不主动 purge | — |
data/persona_drift/<user_id>/* | 全部物理删除 | 撤回 48h 内 |
data/consents/<user_id>.yaml | 写新版本含 revoked_at;旧版本归档到 _history/ | 撤回时同步 |
TrainingAssetCandidate | withdrawal_status: revoked | 撤回时同步 |
| 进行中的 LLM 调用 | 不强行中断本 task;本 task 内的 prompt 仍可能含尚未清理的 profile slice;下一个 task 起完全失效 | — |
实现入口:server/api/consent_routes.py::revoke_consent() 调用 memory_layer.on_consent_revoked(user_id)。
10.5 用户 hard-delete 账号
python -m server.scripts.delete_user --user_id=X
记忆层动作:物理删除 data/user_contexts/<user_id>/、data/persona_drift/<user_id>/、data/sensitive/<user_id>/、data/training_candidates/<user_id>/。
ProfileConsent 保留 deletion stub(合规证据):data/consents/_deleted/<user_id>.yaml,仅含 deleted_at + consent_version_at_deletion,无具体偏好字段。
11. Export and Delete UX
11.1 Export
用户可通过 admin-assisted 路径或 V2 暴露的 API 下载自己的 memory:
python -m server.scripts.export_user_data --user_id=X --out=./out/
记忆层在导出包中的内容(与 B-2 §8.2 一致 + 增量):
<out>/<user_id>/
├── consents/ ← ProfileConsent 全部版本
├── user_contexts/
│ ├── _profile/<context_id>.json
│ ├── _thread/<thread_id>/<context_id>.json
│ └── _task/ ← 仅最近 7 天(task-scoped 默认 24h,覆盖率小)
├── persona_drift/
│ └── <drift_id>.json
├── training_candidates/ ← 仅 metadata(不含 raw cognition_output)
├── manifest.yaml ← 文件清单 + checksum + V1 schema_version
导出不包含:
- 凭证 / 私钥(永不入库;B-2 §8.3);
- 其他用户的 memory;
- LLM raw prompt / response。
11.2 Delete 路径(V1 trial)
| 操作 | UI 入口 | 后端处理 |
|---|---|---|
| 撤回 single consent scope | ProfileConsentDialog(B-4 §6) | DELETE /api/users/{user_id}/profile-consent/{consent_id} → §10.4 |
| 撤回全部 consent(保留账号) | Settings 页 | 同上,删除 user_id 当前生效 consent_id |
| 删除单个 saved profile preference | Settings 页 | POST /api/users/{user_id}/profile-consent 重写覆盖 |
| 删除 saved thread context(不删 thread) | Thread 设置 → "Forget this thread's preferences" | PATCH /api/threads/{thread_id} 把 thread context flag consent_blocked=true |
| Hard-delete 账号 | Settings 页 → 二次确认 | V1 内测:手动联系项目发起人触发 delete_user 脚本(V2 暴露 API) |
11.3 Drift Log 的可见性 / 删除
| 项 | 行为 |
|---|---|
| UI 显示 drift log 列表 | V1 不展示;V2 评估 |
| 用户 export 含 drift log 全文 | 是(§11.1) |
| 用户单独删除某条 drift log | V1 不支持单条删除;只能撤回整个 consent → 一起 purge |
| Trial closeout 时人工复核 drift log | 项目发起人在 closeout report 中聚合(不含 PII) |
11.4 Memory 修改的可观测性
每次 memory layer 写入 / 删除都写:
| 信号 | 落点 |
|---|---|
log event memory_write / memory_delete | B-5 §4.1 component: agent.context_engine |
metric memory_op_total{op, scope} | B-5 §5.2.2 扩展 |
trace span memory_write / memory_delete | 含 scope, slice_id, consent_id_ref |
| Audit history | ProfileConsent 的 _history/ 已记录 consent 变更;memory 层操作通过 trace 关联(无独立 audit log) |
12. Acceptance
本文满足 V1 工程化 B-6 任务的接收条件:
| 项 | 状态 |
|---|---|
| 对象层 = UserContext (task / thread / profile) + ProfileConsent + Persona Drift Log(§3) | 是 |
| Persona Drift Log 新对象(schema + 触发 + retention)(§3.3 + §4 + §9) | 是 |
| 记忆写入 5 类触发(onboarding / save-as-thread / refresh / explicit review / feedback keywords)(§5) | 是 |
| 记忆 read 按 route 选择性载入 slice,不全量灌入 system prompt(§6) | 是 |
| 个性化边界明确:advisor 出场顺序 + Skill 推荐可个性化;evidence / counter-thesis / DataQualityNote / BoundaryGuard / Sensitive Classifier 禁止个性化(§7) | 是 |
| Cold start / onboarding minimum viable user context 路径(§8) | 是 |
| Drift Detection V1 lite 算法(4 类 signal_kind + 阈值)(§9) | 是 |
| 撤回 / 导出 / 删除路径与 B-2 §6.5 + §7 + D-12 完全对齐(§10 + §11) | 是 |
| 与 v1-product-object-and-schema-design.md §13 API and Persistence Boundaries 不冲突(未引入未 consent 的 profile 自动 mutate) | 是 |
| 与 B-5 §12 telemetry 隐私边界 物理隔离(§10.3) | 是 |
13. Open Items
- O-1:Persona Drift Log 的 ID prefix
drift_需要在 B-2 §5.1 ID prefix 表中增量加入 — 建议在 Wave 2 anchorrisks_or_debt标记,并由后续 sub-packet 在 W-2 移植 models.py 时一并补; - O-2:
feedback_conflict关键词词典从哪里来 — 倾向于 trial 启动后从真实 feedback corpus 提取;V1 启动用最小词典(thumbs_up / not_helpful / agree / disagree / 错 / 对); - O-3:Drift detection
refresh_stance_flip的 jaccard 阈值 0.2 是否合适 — 待 trial 跑 ≥ 2 周后人工复核; - O-4:记忆 export 是否需要在 trial 期间暴露给用户 self-serve — V1 倾向项目发起人协助;V2 暴露 API;
- O-5:当 ProfileConsent 部分撤回(只撤
training_use_allowed不撤save_to_profile),记忆层是否需要保留 profile context 但 marktraining_excluded— 倾向是;待 W-2 实现时确认; - O-6:Cold start 的「轻提示」(onboarding-skipped 用户结果尾部提示「设置偏好可让结果更贴合你」)出现频率是否 throttle — 默认每 user 每 24 小时最多 1 次;trial 期可调;
- O-7:drift log 在 trial closeout report 中的聚合方式 — 与 v1-trial-operations-plan.md closeout sub-packet 联调;
- O-8:是否需要项目发起人在 trial 启动前确认「V1 不展示 drift log 给用户」这一决策 — 倾向项目发起人决策;在 anchor
open_questions_for_next_packet中标注。