跳到主要内容

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 / §11v1-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-12v1-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 个问题:

  1. V1 记忆体系由哪些对象组成?哪些落 profile,哪些落 thread,哪些只 task-scoped?
  2. 系统在哪些时刻用户记忆?哪些时刻?读时按 route 类型选择性载入哪些 slice?
  3. V1 阶段允许的个性化边界在哪里?哪些路径禁止个性化?
  4. 用户撤回 / 导出 / 删除时记忆层做什么动作?

本文重写 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-1v1-product-object-and-schema-design.md §8 UserContext / ProfileConsent§11 TrainingAssetCandidate 的字段 down-pour 为可执行的「何时注入 / 何时仅供后端使用」规则(直接对应 Wave 1 anchor open_question 3
G-2V1 trial 3 人规模下,对用户可见的个性化收益(语气、advisor 出场顺序、Skill 推荐);对用户不可见但有质量影响的路径(evidence 排序)做个性化
G-3B-2 §6.5 ProfileConsent 撤回的副作用 + D-12 完全对齐:撤回 / 导出 / 删除路径在记忆层无遗漏
G-4引入 Persona Drift Log 新对象,用于跟踪用户对同一 thesis 的偏好变化(如 grade 反转);这是 V1 内测对「记忆是否漂移」做工程层观测的依据
G-5Cold start(新用户首次任务)有 minimum viable user context 路径,不阻塞 onboarding 完成前的首次输出
G-6Memory read 按 route 选择性载入相关 slice;把全量 memory 灌入 system prompt(避免 attention dilution + token 浪费)

2.2 Non-Goals

Non-Goal理由
不引入向量数据库 / RAG over user memoryV1 内测 3 人,长期记忆体量 < 10 MB;按 thread 检索 + 显式 slice 选择足够
不实现 cross-session 自动 thesis trackingthread refresh 已经承担「持续跟踪」职责(v1-prd.md §6.5
不为 evidence 排序做个性化(避免 echo chamber)§7 显式禁止
不引入 user embedding / clusterV1 trial 3 人样本太小;V2 评估
不实现「LLM-as-memory-summarizer」自动压缩长期记忆V1 用结构化字段保存,不让模型自由 paraphrase 用户长期记忆(合规边界)
不为 advisor / Skill 做 reinforcement learningD-08 仅授权 evaluation runs,不做训练闭环
不暴露 admin endpoint 修改其他用户的 memoryuser_id 隔离硬约束(B-3 §3.3

2.3 与既有决策对齐

3. Memory Object Layer

3.1 对象层

V1 的记忆体系由 3 类对象组成,构成 Task-Scoped → Thread → Profile 三层结构:

对象ScopeMutabilitySource
Task-ScopedUserContext (task_scope)单次任务task-end 时 24h 内清理Task Router / Onboarding
ThreadUserContext (thread_scope)单个 Threadthread refresh 时可 mutate;thread closed 后只读save-as-thread 时建立
ProfileUserContext (profile_scope)全用户用户主动编辑可 mutateProfileConsent grant 时建立
AuditProfileConsent全用户mutable + version-trackedProfileConsentDialog (B-4 §6)
AuditPersona Drift Log新引入全用户append-onlygrade 反转 / feedback 冲突时触发

Persona Drift Log 是本文新引入的对象,目的:观测用户在不同 session 对同一 thesis / asset 的偏好是否漂移。详见 §4。

3.2 UserContext 的三种 scope

v1-product-object-and-schema-design.md §8.1 UserContext 已定义字段。本文细化 scope 边界:

Scoperetention(B-2 §7.1允许字段注入位置
task任务结束 24h全部字段(含未 consent 的 user_stated_position_context / constraints仅本次 task 的 context_engine
thread跟随 Thread;thread 关闭后保持只读需 ProfileConsent.save_to_thread=truethread 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=revokedB-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_reversalFeedback Adapter 写入新 feedback 时比较与同一 related_object_ref 的历史 feedback 的 rating 字段
feedback_conflict同上用户在新 feedback 中显式说「我之前的判断错了」 / 关键词检测
refresh_stance_flipThread refresh 完成时比较新 snapshot 的 main_thesis 与上次 snapshot;如 thesis 显著反转(嵌入相似度 < 阈值或人工识别)
watch_question_resolved_then_reopenedwatch_question_resolved 事件后又被同 thread 标记 reopenedwatch 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) + ProfileConsentprofile用户在 ProfileConsentDialog grant;§8.2
save-as-threadUserContext (thread)threadrequest.save_user_context=true + consent_ack.consent_id 有效(B-3 §4.2.6
Refresh 触发UserContext (thread).updated_at;可能 update fieldsthread用户 in-flight 提供 user_supplied_contextB-3 §4.2.10
用户显式 reviewUserContext (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 成功后:

  1. data/consents/<user_id>.yaml v1(B-2 §4.1);
  2. save_to_profile=true:从 ProfileConsentDialog 收集的偏好字段(language_preference / research_depth / risk_prompt_preference)写到 data/user_contexts/<user_id>/_profile/<context_id>.json
  3. 推送 SSE 不需要(onboarding 走 REST 同步);
  4. 写 metric profile_consent_granted_total
  5. 写 trace span profile_consent_grantB-5 §6.2)。

5.3 Save-as-Thread

POST /api/snapshots/{snapshot_id}/save-as-thread 成功后:

  1. 写 thread header(B-2 §6.1);
  2. 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(如有);
  3. 自动把 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 检测
含敏感金融上下文且未 consentSensitive 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 promptattention dilution;浪费 token;隐藏假设
把全部 thread context 都拼进 prompttoken 爆炸;refresh 任务 token 成本失控
把整个 ProfileConsent JSON 灌入 prompt合规字段不应进入 LLM input

V1 正确做法:Context Engine 按 route 选择性载入相关 slice。

6.2 Slice 定义

Slice来源大小估算(token)
S1_language_prefUserContext.language_preference< 20
S2_research_depthUserContext.research_depth< 20
S3_risk_prompt_prefUserContext.risk_prompt_preference< 20
S4_user_focus_reasonUserContext.user_focus_reason(仅 thread refresh)< 100
S5_research_stylethread research_style< 200
S6_recent_active_threads_titles最近 3 个 active thread 的标题 + cognition_object 摘要< 300
S7_user_stated_position_contexttask-scoped position context< 200
S8_constraintstask-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
snapshotS1, S2, S3S6 (若用户在 prompt 引用了 "我的 thread")S4, S5, S7, S8, S9
thread_refreshS1, S2, S3, S4, S5S9(drift 提示但不展开)S6, S7, S8
risk_challengeS1, S3S5(若来自 thread)S2, S4, S6, S7, S8, S9
pre_executionS1, S2, S3S7, S8(task-scoped)S4, S5, S6, S9
evidence_auditS1S2-S9 (evidence 不个性化 — §7.2)
narrative_mapperS1, S2S6S3, S4, S5, S7, S8, S9
event_impact_readerS1, S2S6S3, S4, S5, S7, S8, S9

6.4 Slice 加载顺序与覆盖

  1. 优先级:task-scoped > thread-scoped > profile-scoped;高优先级覆盖低优先级(如 task user_context_overrides.research_depth 覆盖 saved profile.research_depth);
  2. 缺失字段:直接跳过该 slice(不写空字段,避免 noise);
  3. Cold start(无 profile,无 thread):仅 S1 = "default English" + S2 = "default standard" + S3 = "default balanced"(§8);
  4. 注入位置: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_preferenceAdvisor 出场顺序
balanced(默认)Asset Research → Market/Macro → Risk → Counter-Thesis
counter_firstCounter-Thesis → Asset Research → Market/Macro → Risk
risk_firstRisk → 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 userdata/consents/<user_id>.yaml 不存在;任何 user_contexts 都不存在
Onboarding-incompletetoken 已发但 ProfileConsent 未 grant
Profile-revokedProfileConsent.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 项 ConsentScopePOST /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 ProfileConsentPOST /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_reversalrelated_object_ref 的 feedback rating 在 14 天内出现反向(thumbs_up → not_helpful 或反向)
feedback_conflictfeedback comment 关键词反向(§5.5)≥ 2 次
refresh_stance_flipthread refresh 的 change_summarychanged_inferences[] 数量 > 5 且 thread current_thesis 与 14 天前 main_thesis 完全不重合(关键词 jaccard < 0.2)
watch_question_resolved_then_reopenedwatch 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 logUI 主动询问「保留哪个 grade」
24 小时内 ≥ 3 次 driftlog warn + 项目发起人 digest 中可见自动暂停个性化(slice 不加载)
feedback_conflict 累计 ≥ 5 次log warn触发人工 review queue
refresh_stance_fliplog warn在 thread refresh 弹窗中显式呈现 thesis 反转

V1 范围内 drift 仅作为观测信号,不自动改变用户 profile 或 personalization 行为。

10. Privacy and Retention

10.1 Retention Matrix(与 B-2 §7.1 一致 + 增量)

Objectretention
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 / constraintstask-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/撤回时同步
TrainingAssetCandidatewithdrawal_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 账号

B-2 §7.2 user-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 scopeProfileConsentDialog(B-4 §6DELETE /api/users/{user_id}/profile-consent/{consent_id} → §10.4
撤回全部 consent(保留账号)Settings 页同上,删除 user_id 当前生效 consent_id
删除单个 saved profile preferenceSettings 页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 logV1 不支持单条删除;只能撤回整个 consent → 一起 purge
Trial closeout 时人工复核 drift log项目发起人在 closeout report 中聚合(不含 PII)

11.4 Memory 修改的可观测性

每次 memory layer 写入 / 删除都写:

信号落点
log event memory_write / memory_deleteB-5 §4.1 component: agent.context_engine
metric memory_op_total{op, scope}B-5 §5.2.2 扩展
trace span memory_write / memory_deletescope, slice_id, consent_id_ref
Audit historyProfileConsent 的 _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 anchor risks_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 但 mark training_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 中标注。