☰
Operit 记忆空间 Profile 文档体系全解析:从全局 `user.md` 到“一空间一文档“的存储、迁移、运行时注入与独立配置 UI
2026/9/28 3:05:38 网站建设 项目流程
  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

导读:本文以 memory_space_profile_documents 系列设计文档为主线,系统讲解 Operit(Android 端 AI Agent 应用)如何把 Release 1.12.0+5 时代的"单一全局私有user.md"演进为"每个记忆空间(memory space)独占一份user.md"的完整方案。你将掌握:档案文档的磁盘存储约定(目录、原子写入与 12,000 字符上限)、+4/+5 两代历史数据的迁移算法与原始快照恢复的判定顺序、update_user_profile/update_user_preferences两个工具与提示词注入的运行链路,以及独立"用户配置(User Preferences)"设置页的 UI 结构与策略控制。文章同时以仓库源码(MemorySpaceProfileDocumentRepository.kt、MemoryQueryToolExecutor.kt、ConversationService.kt 等)作为实现证据,帮助读者把设计文档落到真实代码调用链上。

一、背景与设计目标:为什么要让每个记忆空间拥有自己的user.md

1.1 现状问题

在 Release 1.12.0+5 中,Operit 只在应用私有文件目录下存放一个全局user.md文档。此时记忆空间(memory space)本身已经通过稳定的标识符(identifier)互相隔离,但"用户画像文档"并没有跟随记忆空间一起隔离——所有空间共享同一份全局文档。

而更早的 Release +4 则存储了多个结构化档案(structured profiles),每个档案使用与其记忆数据库(ObjectBox database)相同的标识符。

这意味着存在两代互不兼容的数据形态:

  • +4 形态:多份结构化PreferenceProfileJSON,键为profile_<id>;
  • +5 形态:一份根级user.md+ 一份legacy-user-profiles.md归档,外加memory_space_list、active_memory_space_id、memory_space_<id>元数据。

1.2 设计目标(Intent)

index.md 明确了目标状态:

  • 每个记忆空间拥有且仅拥有一份user.md;
  • 当前激活空间(active space)负责向提示词提供文档上下文,并接收自动画像更新;
  • 面向用户的管理入口是独立的User Preferences 设置页,而不是记忆库(memory library)界面;
  • 已发布的全局文档不再被读取、注入、复制或展示(仅作为一次性迁移的输入)。

一句话概括:档案的所有权(ownership)从"全局共享"下沉到"记忆空间私有",同时把管理界面与记忆库解耦。

二、存储层设计:目录、原子写入与字符上限

2.1 存储位置与文件命名

依据 1_StorageAndMigration.md,每份文档存放在应用私有文件目录下:

filesDir/memory-space-profiles/<memory-space-id>/user.md

源码实现位于 MemorySpaceProfileDocumentRepository.kt,其中常量与路径逻辑一一对应:

常量值含义
STORAGE_DIRECTORY"memory-space-profiles"档案根目录
USER_FILE_NAME"user.md"文档文件名
MAX_CONTENT_CHARS12_000单文档字符上限
PUBLISHED_GLOBAL_USER_FILE_NAME"user.md"已发布的全局文档(只读输入)
PUBLISHED_LEGACY_ARCHIVE_FILE_NAME"legacy-user-profiles.md"+5 归档文件(只读输入)
MIGRATION_PREFERENCES"memory_space_profile_documents"迁移专用 SharedPreferences
SCHEMA_VERSION_KEY/CURRENT_SCHEMA_VERSION"schema_version"/2迁移 schema 标记

路径构造逻辑见profileDirectory()与documentFile()(第 223-230 行):先按记忆空间 ID 建立子目录,再拼接user.md。load()在文件不存在时会以空字符串原子创建,保证"一空间必有文档"的不变量。

2.2 原子写入与字符上限

save()(第 84-92 行)在写入前强制校验:

require(markdown.length <= MAX_CONTENT_CHARS) { "user.md exceeds the $MAX_CONTENT_CHARS character limit" }

写入通过 Android 的AtomicFile完成(writeAtomically(),第 299-311 行):startWrite()写临时文件 →finishWrite()提交;任何异常都会调用failWrite()回滚,避免半截文件。整个写入路径由writeMutex串行化,防止并发覆盖。reset()则把某空间的文档清空为"",delete()同时删除文档文件与其目录。

2.3 独立的 schema 标记

迁移版本标记与"已发布全局文档标记"完全分离:仓库使用memory_space_profile_documents这个专属 SharedPreferences 文件记录schema_version,当前为2。initialize()(第 54-71 行)在首次启动时比较版本,低于目标版本则触发migrateReleasedData()并原子提交版本号。分离的意义在于:全局user.md的存在与否不能用来判断是否已迁移,必须依赖专用标记。

三、两代历史数据迁移:+4 结构化档案与 +5 全局文档

迁移入口为migrateReleasedData()(第 114-129 行),其判定顺序至关重要:

if (manager.hasLegacyUserProfileMetadata()) { // 存在 profile_list → 视为 +4 migrateLegacyStructuredProfiles(manager) return } if (manager.hasMemorySpaceMetadata()) { // 存在 memory_space_list → 视为 +5 migratePublishedGlobalDocuments(manager) ... } migrateLegacyStructuredProfiles(manager) // 兜底

3.1 +4 结构化档案 → 记忆空间文档

数据来源:+4 把每个档案以PreferenceProfileJSON 存储在profile_<id>键下,其持久化字段恰好为:id、name、birthDate、gender、personality、identity、occupation、aiStyle、isInitialized;迁移专用的LegacyUserProfile具有完全相同的序列化形状。

迁移动作(migrateLegacyStructuredProfiles(),第 131-140 行):

  1. 读取profile_list、active_profile_id、各profile_<id>记录(readLegacyUserProfiles()汇总为快照);
  2. 为每个档案保留原 ID,写入documentFile(profile.id),内容由LegacyUserProfile.toUserMarkdown()生成;
  3. 调用migrateLegacyProfilesToMemorySpaces()创建对应记忆空间并保留 ObjectBox 数据库标识符。

关键设计点(来自 1_StorageAndMigration.md 与 UserPreferencesManager.kt):

  • 按 ID 而非显示名对应:即使两个档案重名,也不会互相选中或覆盖;
  • Markdown 段落格式与 +5 转换器一致:toUserMarkdown()(第 256-263 行)输出# About me标题,然后按"Basic information / Personality / Preferred assistant style"三段式组织(appendProfileSections(),第 270-297 行),与已发布 +5 转换器产物同构;
  • 分类锁 → 待决迁移状态:+4 的逐字段分类锁(BIRTH_DATE_LOCKED等)无法映射为"整文档锁",因此迁移时把profileAutoUpdateLocked = snapshot.hasLegacyCategoryLocks置为锁定(UserPreferencesManager.kt),自动重写保持禁用,直到用户在新的整文档锁策略下显式选择;同时清理profile_<id>、PROFILE_LIST、ACTIVE_PROFILE_ID及各分类锁键。

3.2 +5 全局文档 → 唯一缺失空间

数据来源:+5 用memory_space_list、active_memory_space_id、memory_space_<id>替代了档案键,把激活档案写到filesDir/user.md,其余 +4 档案以 Markdown 段落形式归档到filesDir/legacy-user-profiles.md,并打上独立的user_profile_documentschema 标记。

迁移动作(migratePublishedGlobalDocuments(),第 148-195 行):

  1. 解析归档文件,按##二级标题切分段落(排除 "Basic information / Personality / Preferred assistant style" 三个固定节名),得到ArchivedProfileDocument(name, content)列表;
  2. 对每个候选空间位置(rootIndex)检查:移除该空间后,剩余空间的名字序列是否是归档名字序列的子序列(isSubsequenceOf(),第 197-205 行)。唯一满足条件的空间即根user.md的归属者;
  3. 其余归档段落按名字顺序逐一匹配剩余空间,写入其文档(仅当目标文档为空时才写入,writeIfDocumentEmpty());
  4. 根文档写入唯一缺失空间。

为什么不用当前激活空间判定?注释与文档都明确指出:active_memory_space_id可能在 +5 迁移之后又被用户切换过,只有归档保留的 +4 顺序才是"名单序",按名单顺序做子序列匹配才能还原原始所有权。若候选空间不唯一(如空归档导致无法判定),源码选择保留源文件并记录警告,不做破坏性处理。

3.3 一次性迁移与"不再触碰"

迁移完成后,根级user.md与legacy-user-profiles.md保留在磁盘上,但仅被这一次性的 +5 重建迁移读取,此后永不修改,也不会注入提示词。这与设计目标"已发布全局文档不再被读取、注入、复制或展示"一致。

四、原始快照恢复兼容:+4 载荷的判定优先级与即时重启

4.1 问题

+4 生成的原始快照(raw snapshot)包含完整的profile_list载荷。将其恢复到运行中的新版本进程时,磁盘上可能同时存在遗留的 +4 键与不完整的记忆空间列表。若仅凭"存在 memory-space 列表"就判定为 +5,会跳过剩余的 +4 记录,最终只留下一个空的默认空间。

此外,恢复路径在用户选择重启前会让当前进程继续存活,其已打开的 DataStore 可能在备份文件被替换后写回过期的内存态偏好。

4.2 变更(6_RawSnapshotRestoreCompatibility.md)

  • 分类顺序修正:先检查profile_list(hasLegacyUserProfileMetadata()),存在即归类为 +4,之后才考虑 memory-space 元数据。源码注释(UserPreferencesManager.kt)明确说明:"raw +4 快照可被恢复到已创建默认空间的新进程,遗留列表在记录被消费前始终是权威来源";
  • 整目录恢复:恢复目录时以快照的完整状态为准,而不是与新文件合并,从而移除陈旧的迁移标记;
  • 原子恢复 + 立即重启:文件原子替换,成功导入后立刻重启,保证下一个进程成为恢复后 DataStore 的第一个读取者,杜绝旧进程回写陈旧偏好。

预期结果:被检查的 +4 原始快照能恢复其三个结构化用户档案及原有记忆空间标识符,而不是退化为只有一个空默认空间。

五、运行时与自动更新:注入、锁定与工具适配

5.1 按激活空间加载文档并注入提示词

2_RuntimeAndAutoUpdate.md 规定:使用激活记忆空间的标识符加载一份档案文档,用于提示词注入与工具更新。

提示词注入的实际调用点在 ConversationService.kt:当userProfileMarkdown非空且未禁用时,系统提示词尾部会追加:

<user_profile source="memory-space/$effectiveMemorySpaceId/user.md"> ...文档内容... </user_profile>

effectiveMemorySpaceId即当前激活空间 ID,source属性直接暴露"文档属于哪个记忆空间"的语义,供模型理解上下文边界。

5.2 自动画像更新管线与锁定门

记忆自动保存候选管线本就按记忆空间维度运行(每个空间独立执行)。画像提取只写匹配的那个空间的文档,并在"仓库边界"(repository boundary)检查整文档自动更新锁定。

实现即saveAutomatic()(MemorySpaceProfileDocumentRepository.kt):

val space = UserPreferencesManager.getInstance(context) .getMemorySpaceFlow(memorySpaceId).first() if (!space.profileAutoUpdateEnabled || space.profileAutoUpdateLocked) return false save(memorySpaceId, markdown) return true

两个策略开关直接决定自动写入是否放行:

字段含义
profileAutoUpdateEnabled是否允许对话过程中的自动画像更新写入本文档
profileAutoUpdateLocked整文档锁;锁定后任何自动重写被拒(手动编辑不受影响)

5.3 两个工具:update_user_profile与update_user_preferences

发布名不变,但行为统一指向"激活记忆空间的文档":

  • update_user_profile:面向提示词可见的文档工具。实现见 MemoryQueryToolExecutor.kt:要求markdown参数,缺参时报错;随后在Dispatchers.IO中调用MemorySpaceProfileDocumentRepository.save(resolveActiveProfileId(tool), markdown),成功返回 "Successfully updated user.md";
  • update_user_preferences:隐藏适配器(hidden adapter),兼容已发布旧包与历史持久化调用。旧调用没有markdown参数,因此该路径把偏好信息以## Imported profile update段落的形式保留进user.md,而不恢复结构化档案运行时(executeLegacyUserPreferencesUpdate(),同文件第 752-802 行附近),避免新旧两套运行时并存。

工具注册与提示词可见性配置位于 ToolRegistration.kt 与 SystemToolPromptsInternal.kt(其中update_user_profile在两处被定义,分别对应不同提示词上下文)。此外,extended_memory_tools.js(及 TS 源 extended_memory_tools.ts)提供了脚本层面对用户画像工具的封装入口。

六、用户配置 UI 的演进:从记忆库内嵌到独立设置页

6.1 第一阶段:记忆空间配置 UI(已被取代)

3_MemorySpaceConfigurationUi.md 记录了最初实现:移除全局用户偏好路由与设置入口,把文档编辑、自动更新控制、整文档锁全部塞进"选中的记忆空间"配置界面。该界面仍使用 ObjectBox 与角色卡绑定所依赖的稳定标识符,只编辑该空间自己的user.md。

该方案被标注为SUPERSEDED:存储所有权与稳定标识符不变,只是面向用户的管理位置后移——因为把个人档案管理放进记忆库的空间选择器,会让"个人画像管理"看起来像"记忆库操作"。

6.2 第二阶段:独立用户配置 UI

4_StandaloneUserConfigurationUi.md 确立最终形态:

  • 恢复 Settings 中的 User Preferences 入口:它拥有配置选择器与全部动作,并把选中配置呈现为一个user.mdMarkdown 编辑器,附带自动更新与整文档锁控制;
  • 记忆库瘦身:只保留浏览对应记忆数据库所需的激活空间选择器,不再创建、重命名、删除或编辑用户配置;
  • 兼容性:配置继续使用既有记忆空间 ID,因此升级自 +4、+5 或当前 worktree 实现的用户,其memory-space-profiles/<id>/user.md、ObjectBox 数据库与固定角色卡绑定全部保持不变。

6.3 第三阶段:独立页打磨(UI polish)

5_StandaloneUiPolish.md 针对初版"动作区 + 两行全宽策略行 + 标签页 + 编辑器 + 保存按钮纵向堆叠"的问题做了分层重构,仓库实现位于 UserPreferencesSettingsScreen.kt:

  • 恢复 +5 编辑器结构:居中内容宽度、紧凑的标签/保存工具栏、全高编辑器、语法高亮、空态占位文案、字符计数(见第 560-571 行,实时显示draftMarkdown.length / 12000,超限变红)、文档菜单;
  • 保留 +4 档案管理器的有用部分:收敛为一条紧凑的选择器栏;
  • 动作菜单化:激活、重命名、删除归入档案动作菜单(重命名/新建走AlertDialog,见第 664-719 行,新建后自动设为激活空间);
  • 策略 Sheet:自动更新与整文档锁两个开关放入ModalBottomSheet(第 580-662 行),切换即持久化(persistPolicy()),无需额外保存按钮;
  • 紧凑跳转动作:一键激活所选档案关联的记忆空间并直接打开记忆库。

本步骤不改变任何存储、迁移、运行时注入或记忆绑定行为,是纯 UI 层的收敛。

七、兼容性总览与升级路径

综合 index.md 与各子文档,三类升级路径与最终状态可归纳如下:

来源迁移方式结果
+5 安装归档legacy-user-profiles.md按记忆空间名单序匹配,根user.md归属唯一缺失空间;源文件一次性读取后不再触碰每个空间一份文档,所有权不随激活空间切换漂移
+4 安装profile_list/active_profile_id/profile_<id>直接迁移为对应空间的user.md,保留 ID 与 ObjectBox 数据库;分类锁转为待决整文档锁重名档案不串位、不覆盖
原始 +4 快照恢复先判profile_list为 +4,整目录恢复 + 原子替换 + 立即重启结构化档案与记忆空间标识符完整还原

完成迁移后,运行时行为统一为:

  1. 系统提示词按激活空间注入<user_profile source="memory-space/<id>/user.md">;
  2. 自动画像写入经仓库边界profileAutoUpdateEnabled && !profileAutoUpdateLocked门控;
  3. update_user_profile(可见)与update_user_preferences(隐藏适配)都写激活空间文档;
  4. 用户通过独立 User Preferences 设置页管理文档、策略开关与空间动作,记忆库仅保留空间选择器。

附:关键源码与文档索引

  • 设计总览:docs/TODO/memory_space_profile_documents/index.md
  • 存储与迁移:1_StorageAndMigration.md
  • 运行时与自动更新:2_RuntimeAndAutoUpdate.md
  • 独立配置 UI:4_StandaloneUserConfigurationUi.md、5_StandaloneUiPolish.md
  • 快照恢复兼容:6_RawSnapshotRestoreCompatibility.md
  • 存储实现:MemorySpaceProfileDocumentRepository.kt
  • 偏好/空间元数据:UserPreferencesManager.kt
  • 提示词注入:ConversationService.kt
  • 工具执行:MemoryQueryToolExecutor.kt
  • 独立配置 UI 实现:UserPreferencesSettingsScreen.kt
  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

相关推荐

上一篇:Barlow字体终极指南:为什么这款几何无衬线字体能提升你的设计质感
下一篇:Modbus.Net:如何构建一个支持多协议、可扩展的工业通信框架?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询