- AI Agent
- 人工智能
- 大模型
- AI 应用
- 工具调用
- 本地部署
- MCP Clients
- Agent 记忆
【免费下载链接】Operit
The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent
导读:本文以 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_CHARS | 12_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 行):
- 读取
profile_list、active_profile_id、各profile_<id>记录(readLegacyUserProfiles()汇总为快照); - 为每个档案保留原 ID,写入
documentFile(profile.id),内容由LegacyUserProfile.toUserMarkdown()生成; - 调用
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 行):
- 解析归档文件,按
##二级标题切分段落(排除 "Basic information / Personality / Preferred assistant style" 三个固定节名),得到ArchivedProfileDocument(name, content)列表; - 对每个候选空间位置(rootIndex)检查:移除该空间后,剩余空间的名字序列是否是归档名字序列的子序列(
isSubsequenceOf(),第 197-205 行)。唯一满足条件的空间即根user.md的归属者; - 其余归档段落按名字顺序逐一匹配剩余空间,写入其文档(仅当目标文档为空时才写入,
writeIfDocumentEmpty()); - 根文档写入唯一缺失空间。
为什么不用当前激活空间判定?注释与文档都明确指出: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,整目录恢复 + 原子替换 + 立即重启 | 结构化档案与记忆空间标识符完整还原 |
完成迁移后,运行时行为统一为:
- 系统提示词按激活空间注入
<user_profile source="memory-space/<id>/user.md">; - 自动画像写入经仓库边界
profileAutoUpdateEnabled && !profileAutoUpdateLocked门控; update_user_profile(可见)与update_user_preferences(隐藏适配)都写激活空间文档;- 用户通过独立 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
相关推荐
时光宝盒:一键留存QQ空间完整记忆档案
时光宝盒:一键留存QQ空间完整记忆档案 在数字化浪潮中,那些记录青春岁月的QQ空间动态正面临不可预见的风险。账号异常、平台政策调整、服务器故障都可能让这些珍贵的
网页爬虫数据分析3步打造智能文档空间:Supermemory空间与文档管理全攻略
3步打造智能文档空间:Supermemory空间与文档管理全攻略 Supermemory是一款强大的个人知识管理工具,帮助用户构建专属的第二大脑。它就像为你的书
人工智能RAGAgent 记忆AI Agent后端MCP 服务知识图谱前端一键永久存档:GetQzonehistory帮你完整备份QQ空间所有记忆
一键永久存档:GetQzonehistory帮你完整备份QQ空间所有记忆 你是否曾担心那些承载青春回忆的QQ空间说说会随着时间流逝而消失?数字时代的记忆如此脆弱
网页爬虫数据分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考