diff --git a/.gitignore b/.gitignore index d8686c6..5e1649f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ pnpm-debug.log* *.tsbuildinfo next-env.d.ts .gstack/ + +e2e/thread-chat/shots/** diff --git a/docs/memory/01-survey.md b/docs/memory/01-survey.md new file mode 100644 index 0000000..a8df16f --- /dev/null +++ b/docs/memory/01-survey.md @@ -0,0 +1,384 @@ +# Chatbot / Agent 记忆全景调研报告 + +> **版本提示(2026-07-24)**:本文是阶段 1 的历史研究基线。快速变化的方案以阶段 2 源码快照为准,尤其是 Mem0 当前 OSS 主干已从本文描述的 ADD/UPDATE/DELETE/NOOP 管线演进为 V3 ADD-only + memory linking,Letta 的推荐入口也已从 V1 server 转向 Agent SDK。详见 [`02-deep-dives/`](./02-deep-dives/)。 + +> 阶段 1 产出 · 2026-07 · 深度调研流程:5 个检索角度并行 → 24 个来源 → 抽取 114 条候选结论 → 对 25 条关键结论做 3 票对抗性核实(24 确认 / 1 证伪)。 +> +> **证据分级标记**(贯穿全文): +> - ✅ **已核实** —— 经 3 票对抗性验证、原文逐字比对通过; +> - ⚠️ **有保留** —— 已核实但来源单一或为厂商自报数据,采信需打折; +> - ❓ **待核验** —— 来自社区博客/厂商文档的一手抽取,未进入本轮验证名单,将在阶段 2(逐方案深入)中核验; +> - ❌ **已证伪** —— 对抗验证中被否决的说法,供透明披露。 + +--- + +## 目录 + +1. [为什么 Chatbot 需要「记忆」,记忆和 RAG 有什么区别](#1-为什么需要记忆) +2. [学术分类学:给「记忆程度」找到骨架](#2-学术分类学) +3. [记忆程度阶梯:L0–L7 分级与对应实现方法(本报告核心)](#3-记忆程度阶梯) +4. [代表方案全景:逐个画像](#4-代表方案全景) +5. [统一架构对比表](#5-统一架构对比表) +6. [批判性发现:基准之战、公认短板与安全风险](#6-批判性发现) +7. [关键论文与文献清单](#7-关键论文清单) +8. [本项目落地路线图(总纲)](#8-本项目落地路线图) +9. [遗留问题与阶段 2 核验清单](#9-遗留问题与阶段-2-核验清单) + +--- + +## 1. 为什么需要记忆 + +一个没有记忆机制的 LLM 应用,每次请求都只能依赖「塞进上下文窗口的内容」。上下文窗口是有限的、无状态的、按 token 计费的——这带来三类问题: + +1. **长对话撑爆窗口**:对话越长,历史越多,最终截断或报错; +2. **跨会话失忆**:用户昨天说过「我对坚果过敏」,今天新开会话模型一无所知; +3. **成本与延迟随历史线性增长**:每轮都把全部历史重发一遍,token 费用和首字延迟持续累加。 + +学术界对记忆的定位(✅):多篇综述将记忆视为 LLM Agent **自我进化与长期交互的核心组件**(arXiv:2404.13501,ACM TOIS),是支撑长程推理、持续适应与有效交互的基石能力(arXiv:2512.13564)。 + +**记忆 ≠ RAG**(❓,多来源一致):向量检索式 RAG 只是记忆实现的一个子集。真正的 Agent 记忆系统与纯 RAG 的本质区别在于**写入路径**——记忆需要同时具备**写入策略、检索策略和更新策略**,而 RAG 通常是只读检索。换句话说:RAG 回答「如何把已有知识读出来」,记忆还要回答「哪些交互值得写进去、写成什么形态、过时了怎么办」。 + +一条被两篇独立综述逐字确认的写入原则(✅): + +> "Storing every interaction verbatim is tempting and almost always wrong."(逐字存储所有交互很诱人,但几乎总是错的)—— arXiv:2603.07670 +> +> "Forgetting is not a bug; it is a feature——essential for robustness, privacy, and efficiency."(遗忘不是缺陷而是特性)—— 同上 + +--- + +## 2. 学术分类学 + +「记忆做到什么程度」不是一个社区拍脑袋的问题——2024–2026 年间至少五篇系统性综述给出了互补的分类框架(✅,每篇的分类维度均经原文逐字核验): + +| 综述 | 分类维度 | 对本报告的贡献 | +| --- | --- | --- | +| **arXiv:2404.13501**(人大/华为诺亚,ACM TOIS,同行评审)| 记忆来源(试验内 in-trial / 跨试验 cross-trial / 外部知识)× 形式(文本/参数化)× 操作(读 reading / 写 writing / 管理 management)| 「单会话 vs 跨会话」层级的直接学术对应 | +| **arXiv:2504.15965**《From Human Memory to AI Memory》| 对象(个人 vs 系统)× 形式(参数化 vs 非参数化)× 时间(短期 vs 长期)三维八象限;并把人类记忆操作映射到 AI:编码、存储、检索、巩固、再巩固、反思、遗忘 | 「更新/冲突/遗忘」对比维度的统一表头;短期记忆被明确定义为「当前会话内临时维持的上下文」 | +| **arXiv:2512.13564**《Memory in the Age of AI Agents》(47 位作者)| Forms(token 级 / 参数化 / 潜在状态)× Functions(事实性 / 经验性 / 工作记忆)× Dynamics(形成 / 演化整合遗忘 / 检索)| 明确「RAG 只是记忆的子集」;配套 200+ 论文索引仓库 | +| **arXiv:2602.06052**(60 位作者)| 记忆基质(内部/外部)× 认知机制(情景/语义/感觉/工作/程序性)× 记忆主体(Agent 中心 vs 用户中心)| 「Agent 的记忆」和「关于用户的记忆」是两件事——本项目主要做后者 | +| **arXiv:2603.07670**《Memory for Autonomous LLM Agents》| 时间尺度(工作/情景/语义/程序性)× 表示基底(上下文文本/向量库/结构化存储/可执行仓库)× 控制策略(启发式/提示自控/RL 学习式)| 与本报告 L0–L7 阶梯最接近的层级体系 | + +### 认知四分法(贯穿所有层级的横向维度) + +「情景 vs 语义 vs 程序性记忆」的学术源头是 **CoALA**(arXiv:2309.02427,TMLR 2024,同行评审),已被 LangChain/LangMem 文档、Cognee 及后续综述沿用为标准参照(✅): + +- **工作记忆(working)**:当前决策周期内的活跃信息 ≈ 当前上下文窗口内容; +- **情景记忆(episodic)**:过往经历/轨迹 ≈「用户在 5 月 3 日的会话里说过 X」; +- **语义记忆(semantic)**:关于世界与自身的去情境化知识 ≈「用户对坚果过敏」(不关心是哪次说的); +- **程序性记忆(procedural)**:隐式存于 LLM 权重 + 显式写在 Agent 代码/系统提示中的「怎么做」知识 ≈「回答该用户时始终用中文、代码示例用 TypeScript」。 + +CoALA 还把读写机制形式化为(✅):**检索动作把长期记忆读入工作记忆,学习动作把信息写入长期记忆**——这是后文对比所有方案「写入时机/检索注入方式」的统一分析框架。 + +> 注意(✅,验证者附注):各家分类学是**正交维度**切分,不是严格的「程度递进」。下一章的 L0–L7 阶梯是本报告为落地做的层级映射,映射关系已尽量对齐上表文献。 + +--- + +## 3. 记忆程度阶梯 + +这是本报告的核心:把「记忆可以做到什么程度」整理为 8 级递进阶梯。每一级给出定义、社区通行实现方法、代表方案与工程复杂度。**每一级都包含前面所有级的能力**;实践中通常组合多级,而非只选一级。 + +### L0 · 无记忆(stateless) + +- **定义**:每次请求独立,不携带任何历史。 +- **实现**:什么都不做。单轮工具型应用(翻译、改写)常刻意停在这一级。 +- **成本/复杂度**:零。 + +### L1 · 单会话上下文窗口管理(截断 / 滑动窗口) + +- **定义**:把当前会话的消息历史随请求重发;超出窗口预算时做截断。对应学术上的**工作记忆**、arXiv:2504.15965 的「短期记忆」(✅)。 +- **实现方法**: + - **全量重发**:历史不长时最简单,也是 AI SDK `streamText({ messages })` 的默认形态; + - **滑动窗口**:只保留最近 N 条消息或最近 K tokens; + - **头尾保留**:保留 system prompt + 最早几条(任务设定)+ 最近若干条,砍中间; + - **重要性截断**:按消息类型加权(保留工具结果摘要、丢弃寒暄)。 +- **代表**:所有聊天框架的默认行为;LangChain 早期的 `ConversationBufferWindowMemory` 即此级的封装。 +- **本项目现状**:`app/api/chat/route.ts` 目前就是全量重发,**尚无任何窗口管理**。 +- **成本/复杂度**:极低。纯确定性代码,无额外 LLM 调用。 +- **失效模式**(❓):上下文膨胀——历史一长,成本涨、注意力稀释、最终截断丢信息。 + +### L2 · 会话内摘要压缩(summarization / compaction) + +- **定义**:会话过长时,用 LLM 把较早的消息压缩成摘要,替换原文继续对话。仍是会话内行为,对应「巩固 consolidation」操作的最初级形态(✅,2504.15965 将 consolidation 定义为「将短期记忆转化为长期记忆」)。 +- **实现方法**: + - **阈值触发滚动摘要**:token 数超过阈值 → 把最早 50% 压成一段摘要,作为一条 system/assistant 消息放回队首(LangChain `ConversationSummaryBufferMemory`、Claude Code 的 compaction、OpenAI Agents SDK 的 session summarization 均为此思路); + - **分层摘要**:摘要的摘要,适合超长会话; + - **选择性保留**:摘要时显式保留实体、数字、决定等高价值信息。 +- **理论上限警示**(⚠️,来自 schema-grounded 论文 arXiv:2604.27906,vendor 相邻但论证独立成立):由数据处理不等式 I(A;Z) ≤ I(A;X)——**写入时无法预知未来查询**,摘要必然丢失低显著性细节(精确数值、被否决的选项、否定式陈述、时间戳)。摘要是有损压缩,不能当作可靠的事实存储。 +- **成本/复杂度**:低。每次压缩一次 LLM 调用;难点在触发时机与「摘要丢了关键细节」的兜底。 + +### L3 · 跨会话持久化(thread persistence) + +- **定义**:把完整消息历史落库,刷新页面、换设备、隔天回来还能继续这个会话。对应学术上的 in-trial → cross-trial 边界(✅,2404.13501)。 +- **注意**:这一级持久化的是**聊天历史**,还不是「记忆」——换一个新会话,模型依然不认识你(❓,db0 等多来源一致强调此区分)。Vercel AI SDK 本身只提供消息持久化与钩子(`prepareStep`、`onFinish`),**没有内置的跨会话长期记忆原语**(❓)。 +- **实现方法**: + - 消息整体存 JSONB(**本项目已实现**:`lib/db/schema.ts` 的 `threads`/`messages`,`messages.content` 存完整 `UIMessage`); + - 规范化三表设计(chats / messages / parts,每种 part 类型专用前缀列)——Vercel 官方示例 `vercel-labs/ai-sdk-persistence-db` 的做法,但该仓库已于 2026-06 归档,仅作架构参考(❓)。 +- **本项目现状**:✅ 已完成(`RemoteThreadListAdapter` + `ThreadHistoryAdapter` + Postgres)。**本项目的记忆工程从 L3 之上开始。** +- **成本/复杂度**:低-中(本项目已付讫)。 + +### L4 · RAG 向量检索式记忆(retrieval memory) + +- **定义**:把历史消息/文档切块、嵌入为向量存入向量库;回答前按语义相似度检索 top-k 注入提示。综述确认这是**当前 Agent 外部记忆的主导实现**(✅,2602.06052 原文:"The dominant implementation of external memory in the current work of agents or LLMs")。 +- **标准实现**(✅):记忆条目与查询嵌入到共享向量空间 → 近似最近邻 top-k → 片段追加进 prompt 作为 grounding 上下文。 +- **实现变体**: + - **对会话历史做 RAG**:把旧消息按轮次/块嵌入,新问题来时检索相关旧轮次(情景记忆的向量化); + - **对知识库做 RAG**:AI SDK 官方 RAG Chatbot 指南的路线(❓,但为官方一手文档)——技术栈与本项目完全一致(Next.js App Router + AI SDK + Drizzle + Postgres/pgvector): + - 写入:模型通过 `addResource` **工具自主决定写入时机**(工具描述指示「用户主动提供知识时无需确认即写入」); + - 检索:Drizzle `cosineDistance` 算余弦相似度、过滤 > 0.5、取 top-4,embedding 列建 **HNSW 索引**(`vector_cosine_ops`); + - 注入:**按需工具检索**(`getInformation` 工具 + `stopWhen` 5 步上限),而非每轮自动前置注入; + - 分块刻意极简(按句号切分、无重叠),生产需自行增强。 + - **检索排序不只看相似度**(❓,db0 实践文章):语义相似度 ×0.7 + 时近性 ×0.2 + 使用频度 ×0.1 的加权公式是社区常见做法(Generative Agents 的 recency × importance × relevance 三因子是其源头)。 +- **能力边界**(⚠️,schema-grounded 论文核心论点,✅ 核实其原文):**嵌入捕捉主题相关性,但不是谓词求值器**——对精确事实查找、状态跟踪、聚合、关系查询、否定/排除查询,纯向量检索「在构造上就不够用」(高相似 ≠ 事实存在)。向量记忆擅长「找回大概相关的内容」,不擅长「精确回答用户现在住哪」。 +- **成本/复杂度**:中。需要 embedding 调用、pgvector 扩展、索引运维;无写入管线时仍接近只读 RAG。 + +### L5 · 结构化事实 / 实体记忆(extraction + fact store) + +- **定义**:不再存原文块,而是用 LLM 从对话中**抽取离散事实**(「用户住在杭州」「项目截止日是 8/1」),存为结构化条目,并有显式的**更新/冲突/去重**管线。这是「记忆系统区别于 RAG」的分水岭——有了真正的写入策略。 +- **代表实现——Mem0 的两阶段管线**(✅,论文原文 + 第三方代码走读双重核实): + 1. **抽取阶段**:LLM 从新消息对(结合会话摘要与近期上下文)抽取显著事实; + 2. **更新阶段**:LLM 本身(非独立分类器)把候选事实与向量检索到的 top-s 相似既有记忆比较,决定 **ADD / UPDATE / DELETE / NOOP** 四操作之一——一个管线同时完成写入、增补、冲突消解与去重。 +- **图增强变体**(✅):Mem0^g 将记忆存为有向带标签图 G=(V,E,L)(节点=实体、边=关系、标签=语义类型),实体抽取 + 关系三元组两阶段写入,检索结合实体导航与语义三元组匹配;Zep/Graphiti 则把事实建模为**带有效时间窗口的图边**——新旧冲突时旧边标记失效但保留历史,支持「时间点正确性」查询,且查询路径不调 LLM(❓,厂商文档,P95 检索延迟约 300ms 为自报)。 +- **量化收益**(⚠️,Mem0 论文自报,数字经逐项核验、算术自洽,但为 vendor 自评且存在竞品争议,详见第 6 章):相比 full-context 全量历史,p95 端到端延迟降低 91%(约 1.44s vs 17.1s)、token 成本节省超 90%(约 1,764 vs 26,031 tokens)——**代价是准确率略降**(LOCOMO J 分 66.88% vs full-context 的 72.90%)。即抽取式记忆的本质是**用少量准确率换一个数量级的成本/延迟优势**。 +- **抽取的另一条路**(❓):确定性规则匹配抽取(零 LLM 调用、近零延迟,但只覆盖约 60–70% 场景)vs LLM 抽取(每轮 +200–500ms、$0.001–0.01)——db0 实践文章的实测取舍。 +- **成本/复杂度**:中-高。每轮(或缓冲后)额外 1–3 次 LLM 调用;需要设计事实 schema、作用域(user/session/agent)、冲突策略。**这一级是社区公认收益/复杂度比最高的一级,也是各方案分化最大的一级。** + +### L6 · 用户画像与偏好记忆(profile memory / 程序性记忆) + +- **定义**:把零散事实进一步**聚合成稳定的用户画像**(结构化档案)或**行为规则**(程序性记忆),每轮对话直接注入,不走检索。对应 2602.06052 的「用户中心记忆」与 2404.13501 的「个人记忆用于个性化」(✅ 综述层面)。 +- **实现方法**: + - **层级化画像**(❓,Memobase 路线,"Memory for User, not Agent"):结构化 topic/sub-topic 档案(`basic_info.name`、`interest.games`、`psychological.goals`),**写入不在热路径**——新消息先进缓冲区,累计约 1024 tokens 或闲置约 1 小时才触发 LLM 抽取 flush;读取只是把预计算好的画像打包成字符串插入 prompt,在线延迟可 <100ms(自报); + - **固定槽位画像**:ChatGPT memory 的「Saved memories」形态——离散记忆条目 + 从历史中隐式学到的偏好(官方披露有限,待阶段 2 核验,❓); + - **程序性记忆 / 自更新系统提示**(❓,LangMem 路线):LangMem 在 LangGraph 持久化 store 之上支持情景/语义/程序性三类记忆,其中**程序性记忆允许 Agent 更新自己的系统提示指令**——「用户偏好简洁回答」不是存起来等检索,而是直接改写到下轮 system prompt;Letta 的 core memory blocks(persona/human 块常驻上下文、由 Agent 自编辑)同属此思路。 +- **与 L5 的关系**:L5 是「事实数据库」,L6 是「每轮必带的压缩视图」。典型组合:画像常驻注入(L6)+ 长尾事实按需检索(L4/L5)。 +- **成本/复杂度**:中。画像抽取可离线/缓冲进行,不增加在线延迟;难点是画像 schema 设计与「何时该进画像、何时留在事实库」的分层。 + +### L7 · 自我反思与记忆整合(reflection / consolidation / forgetting) + +- **定义**:记忆系统定期「回顾自己的记忆」——把低层记忆综合成高层洞见(反思)、合并去重(整合)、按时间/价值衰减修剪(遗忘)。这是目前最高级也最不成熟的一级。 +- **奠基设计——Generative Agents**(✅,arXiv:2304.03442):memory stream 以自然语言记录全部经历(带时间戳与重要性分数),检索按 recency × importance × relevance 加权;**反思机制**将低层记忆随时间综合为高层结论;消融实验证明去掉反思后,智能体行为在 48 模拟小时内从连贯的多日规划退化为重复的无上下文响应(❓ 该消融细节来自抽取,方向与 ✅ 核实的「反思整合是该论文核心贡献」一致)。 +- **遗忘的实现例证**(✅):MemoryBank(AAAI 2024)按 **Ebbinghaus 遗忘曲线**对记忆做时间衰减——被引用越少、越久远的记忆越先淡出。 +- **记忆演化**(❓,A-Mem,arXiv:2502.12110):受卡片盒笔记法(Zettelkasten)启发——每条新记忆写入时 LLM 生成结构化笔记(上下文描述/关键词/标签),并自动触发两个操作:**链接生成**(嵌入检索候选 + LLM 判断建链)与**记忆演化**(LLM 可回头更新既有记忆的描述与标签);自报每次记忆操作仅约 1,200 tokens(对比 MemGPT 基线约 16,900)。 +- **反思的量化收益**(❓):Reflexion 在 HumanEval 上 91% pass@1(无反思 GPT-4 基线 80%)。 +- **成本/复杂度**:高。需要后台任务(cron/队列)、多轮 LLM 调用、且效果难评估。社区共识是**先把 L4–L6 做扎实再考虑 L7**;但轻量版(定期把事实表去重合并一次)值得早做。 + +### 阶梯总览 + +| 级别 | 名称 | 记忆类型(CoALA) | 额外 LLM 调用 | 存储 | 本项目现状 | +| --- | --- | --- | --- | --- | --- | +| L0 | 无记忆 | — | 无 | 无 | — | +| L1 | 窗口管理 | 工作记忆 | 无 | 无 | ❌ 未做(全量重发) | +| L2 | 会话内摘要 | 工作记忆→巩固 | 压缩时 1 次 | 会话内 | ❌ 未做 | +| L3 | 跨会话持久化 | (聊天历史,非记忆) | 无 | Postgres | ✅ 已完成 | +| L4 | 向量检索记忆 | 情景记忆 | embedding | pgvector | ❌ 未做 | +| L5 | 结构化事实记忆 | 语义记忆 | 每轮/缓冲 1–3 次 | 事实表(±向量/图) | ❌ 未做 | +| L6 | 画像/偏好记忆 | 语义+程序性 | 离线/缓冲 | 画像表 | ❌ 未做 | +| L7 | 反思/整合/遗忘 | 元记忆操作 | 后台批量 | 同上 | ❌ 未做 | + +--- + +## 4. 代表方案全景 + +> 本章除标注 ✅/⚠️ 的条目外,框架细节多为 ❓(社区来源一手抽取,未经对抗核验)——这正是阶段 2 要逐个深入的对象。 + +### 4.1 MemGPT → Letta(OS 式分层记忆的鼻祖) + +- **论文**(✅):arXiv:2310.08560,UC Berkeley(Packer 等,团队后创立 Letta 公司)。提出**虚拟上下文管理**:借鉴操作系统分层内存(RAM/磁盘),在**主上下文(main context)、召回数据库(recall,对话历史)、归档向量存储(archival)**之间由 **LLM 自主函数调用**搬移数据(「分页」是类比,实际是工具调用)。 +- **产品形态 Letta**(❓):core memory(persona/human 块,常驻上下文、Agent 自编辑)+ recall storage(近期对话+向量搜索)+ archival storage(长期事实+语义检索);完全开源可自托管。 +- **定位**:「Agent 自己管理自己的记忆」路线的代表——把记忆决策权交给模型(提示自控策略),而非工程管线。 +- **适合**:Agent 自主性高的场景;**不适合**:想要确定性、可审计记忆行为的场景。 + +### 4.2 Generative Agents(反思/整合路线的鼻祖) + +- **论文**(✅):arXiv:2304.03442(Park 等,2023-04,斯坦福 25-agent 小镇)。memory stream + recency/importance/relevance 检索 + reflection 合成高层洞见。早于 MemGPT/Mem0,是几乎所有后续记忆方案引用的上游。 + +### 4.3 Mem0(LLM 抽取式结构化事实记忆) + +- **论文**(✅):arXiv:2504.19413。两阶段管线(抽取 → ADD/UPDATE/DELETE/NOOP 更新),图变体 Mem0^g;开源实现与论文一致(第三方代码走读印证)。 +- **自报成绩**(⚠️):LOCOMO 上相对 OpenAI memory +26% 相对提升(66.88% vs 52.90%);vs full-context 延迟 -91%/token -90%,但准确率低于 full-context(详见 §6.1 的基准争议)。 +- **商业化**(❓):多存储架构(向量+图+KV);三层作用域 user/session/agent;图记忆需付费 Pro;托管服务 $19/月起,核心开源。 +- **已知短板**(❓/⚠️):事实覆盖更新而非带效期版本化 → 时序推理弱(LongMemEval 49.0% vs Zep 63.8%,第三方转引);FactConsolidation 冲突消解仅 18%(⚠️,见 §6.2)。 + +### 4.4 Zep / Graphiti(时序知识图谱) + +- **路线**(❓,厂商文档+多方转述一致):Graphiti 引擎把每条事实存为**带有效期窗口(valid-from / invalid-at)的图边**;新信息冲突时旧边失效但保留历史 → 支持「时间点正确性」("用户 3 月时住哪?");查询路径不调用 LLM,自报 P95 检索约 300ms。 +- **商业化**(❓):Zep 平台是 SaaS(约 $25/月起),仅 Graphiti 引擎开源。 +- **强项**(❓):时序推理(LongMemEval 63.8% vs Mem0 49.0%,转引自第三方基准);**短板**(⚠️):FactConsolidation 单跳仅 7%(见 §6.2);有社区评测称写入后需较长索引时间(❓,来自 Mem0 论文的竞品评测,存在争议)。 + +### 4.5 LangChain / LangGraph memory 与 LangMem + +- **路线**(❓):LangGraph 提供持久化 checkpointer(线程内状态)+ store(跨线程记忆);LangMem 在其上实现 CoALA 三类长期记忆(情景/语义/程序性),**程序性记忆可自更新系统提示**。 +- **注意**(⚠️,Mem0 论文竞品数据,利益相关):LangMem 被测出检索延迟极高(p50 ~18s / p95 ~60s)——数字可疑且过时,阶段 2 需独立核验。 +- **对本项目的参考价值**:不引入 LangChain 依赖,但其记忆 API 设计(namespace、memory type、background vs hot-path 写入)值得抄概念。 + +### 4.6 Cognee(知识图谱管线) + +- **路线**(❓):Extract-Cognify-Load(ECL)管道把数据加载为**类型化知识图谱**,多存储后端(向量 + Neo4j/FalkorDB/KuzuDB/NetworkX + 关系型元数据),可 Ollama 全离线,开源免费。 + +### 4.7 Memobase(用户画像路线) + +- **路线**(❓):见 L6——层级化用户档案 + 缓冲区异步抽取 + `context()` 预计算注入;FastAPI + **Postgres** + Redis,全 Docker 化,Apache 2.0。技术栈与本项目亲和度最高的开源方案,值得阶段 2 重点读源码。 + +### 4.8 A-Mem(记忆演化 / Zettelkasten) + +- **论文**(❓):arXiv:2502.12110。每条记忆是 LLM 生成的结构化笔记,写入自动触发建链 + 回头演化既有记忆;纯向量检索(all-minilm-l6-v2,k=10)。轻量、无框架依赖,适合借鉴其「写入时让 LLM 顺手整理」的思路。 + +### 4.9 OpenAI ChatGPT memory / Anthropic Claude memory tool(产品级参照) + +- **ChatGPT memory**(❓,官方披露有限):Saved memories(显式条目)+ 从历史隐式学习偏好;用户可查看/删除。是 Mem0 论文对比的基线(其 LOCOMO J 分 52.90%,⚠️ 竞品评测数字)。 +- **Claude memory tool**(❓):Anthropic 提供 **client-side memory tool**——模型通过工具调用读写一个由开发者托管的记忆目录(文件式),写入时机由模型自主决定,存储与执行完全在开发者侧。这个「记忆=模型可调用的工具,存储归开发者」的模式与本项目已有的前端工具(`writeNote`)机制同构,是很自然的落地候选之一。 +- 两者的确切写入时机/存储结构均属阶段 2 核验清单(本轮无已确认一手证据)。 + +### 4.10 自建路线(Postgres/pgvector + 提示注入)——本项目的默认路线 + +- **官方蓝本**(❓,一手官方文档):AI SDK RAG Chatbot 指南(§L4 已详述),技术栈与本项目 100% 重合。 +- **社区实践共识**(❓,db0 等):抽取事实 + embedding 存储 + token 预算内检索 + system prompt 注入;「聊天历史」与「记忆」分表分概念。 +- **schema-grounded 主张**(⚠️→详见 §6.3):可靠的记忆应像 **system of record(记录系统)而非搜索**——结构化事实表为主、向量为辅。这与本项目 Drizzle/Postgres 技术栈天然契合,是调研得出的最重要落地启示。 + +--- + +## 5. 统一架构对比表 + +对比表头来自 ✅ 核实的统一分析框架(CoALA 读写形式化 + 2404.13501 读/写/管理三操作 + 2504.15965 的巩固/反思/遗忘映射)。表内框架细节多为 ❓(阶段 2 核验)。 + +| 维度 | MemGPT/Letta | Mem0 | Zep/Graphiti | LangMem | Memobase | A-Mem | 自建 pgvector | 自建结构化事实表 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| **写入时机** | Agent 自主工具调用 | 每轮消息对后管线触发 | 每条消息进图管线 | hot-path 工具 或 后台任务 | 缓冲区攒够才 flush(非热路径) | 每条写入即触发 | 模型工具调用(addResource 式) | 每轮后/缓冲后管线 | +| **抽取方式** | LLM 自决定存什么 | LLM 抽取显著事实 | LLM 抽实体/关系/时间 | LLM(可配 schema) | LLM 抽画像字段 | LLM 生成结构化笔记 | 无抽取(原文分块) | LLM 或规则抽取 | +| **存储结构** | 上下文块+召回库+归档向量 | 向量+图+KV 混合 | 时序知识图谱 | LangGraph store | 层级画像+Postgres | 笔记+向量+链接 | pgvector 表 | 关系表(±pgvector 列) | +| **检索/注入** | Agent 工具调用换页 | 向量 top-k 注入 | 图查询(无 LLM),亚秒 | store 检索注入 | 预计算画像整包注入 | 余弦 top-k 注入 | 相似度>阈值 top-k | SQL 谓词查询+注入 | +| **更新/冲突** | Agent 自编辑 core memory | LLM 决策 ADD/UPDATE/DELETE/NOOP | 旧边失效+新边生效(保历史) | 依赖开发者策略 | 画像字段覆盖更新 | LLM 回头演化旧笔记 | 无(append-only) | 显式版本化/UPSERT,确定性 | +| **遗忘** | 归档驱逐 | DELETE 操作 | 边失效(软删) | 开发者自定 | 画像修剪 | 演化中合并 | 无 | TTL/衰减列,自定 | +| **在线延迟** | 取决于 Agent 步数 | 检索 p95 ~0.2s(自报⚠️) | 检索 p95 ~0.3–0.6s | 有争议数据(⚠️) | <100ms(预计算,自报) | 微秒级检索(自报) | 一次 embedding+SQL | 纯 SQL,最低 | +| **额外成本** | 每轮多次工具调用 | 每轮 1–3 次 LLM | 每条消息图抽取 LLM | 每次写入 LLM | 缓冲摊薄,固定 3 次/flush | 每写入 1–2 次 LLM | 仅 embedding | 抽取 LLM(可缓冲) | +| **开源/托管** | 全开源可自托管 | 核心开源,图记忆付费 | 仅引擎开源,平台 SaaS | 开源(MIT) | 开源(Apache 2.0) | 研究代码 | 全自有 | 全自有 | +| **复杂度** | 高 | 中(SDK 简单,行为黑盒) | 中-高 | 中 | 低-中 | 低-中 | 低 | 中 | + +> ❌ **已证伪提醒**:「各主流系统在更新/冲突策略上统一采用最新事实胜出(supersession)惯例」这一说法被 3 票全数否决(0-3)。**冲突消解策略在各系统间既不统一、也普遍不可靠**(见下章)——上表「更新/冲突」一行恰是各方案区分度最高、也最需逐个核验的维度。 + +--- + +## 6. 批判性发现 + +调研中最有价值的部分往往不是「大家都怎么做」,而是「哪里有争议、哪里都做不好」。 + +### 6.1 基准之战:厂商自报数字全部需要打折 + +- Mem0 自报 LOCOMO 成绩与第三方独立复测存在巨大差距(自报 91.6% vs 第三方复测 58–66%,❓); +- **Zep 公开发文质疑 Mem0 论文**(❓,getzep.com 博客,双方均利益相关):称 Mem0 对 Zep 的评测配置有误(角色设置、时间戳字段、串行检索),按 Zep 自己的实现重跑 LoCoMo 得 J=75.14%(Mem0 论文只给它 65.99%),反超 Mem0;并批评 LoCoMo 基准本身太短(16k–26k tokens,塞得进现代上下文窗口)、缺知识更新类问题、有数据质量问题; +- Mem0 的 full-context 对照仅 ~26k tokens、用 GPT-4o-mini、未开 prompt caching(✅ 验证者附注)——「抽取式优于长上下文」不能外推为普遍结论; +- **结论**:本项目阶段 3(实验)必须自建小规模评测,不能采信任何一家的 SOTA 数字。 + +### 6.2 公认短板:冲突消解 / 事实新鲜度(对本项目最重要的发现) + +- **所有已发表系统在 MemoryAgentBench 的 FactConsolidation(冲突消解)任务上显著欠佳**(⚠️,2-1 分票通过,数字经基准论文 v4 Table 3 交叉核验):Zep 单跳仅 **7.0%**、Mem0 与 Contriever 各 **18.0%**、HippoRAG-v2 54.0%、BM25 48.0%;多跳变体所有系统 ≤7%,几乎无解; +- **确定性管线显著优于 LLM 判断**(✅,arXiv:2606.01435 受控实验):同骨干/同检索/同分块/同 top-k 下,把「让 LLM 判断哪条事实最新」换成「候选抽取 + Python `max(serial)`」确定性消解,提升 +10.8pp(67.2→78.0),且**上下文越长优势越大**(6K 时 +8pp → 262K 时 +21pp)。LLM 判断的失败模式:训练先验覆盖(无视「新者胜」规则)与长上下文下的序号比较漂移; +- 限定(✅ 验证者附注):FC 任务是合成数据(含显式序号,利好 max 原语);在带真实时间戳的 LongMemEval knowledge-update 上确定性版本仅与 LLM 打平——**确定性聚合是「当前值冲突消解」的正确原语,但不是万能**; +- **对本项目的直接启示**:不要指望「把所有历史扔给 LLM 让它自己搞清楚哪条是最新的」;事实要带时间戳/版本号存进结构化表,「取最新值」用 SQL `ORDER BY ... LIMIT 1` 这类确定性逻辑完成,LLM 只负责抽取。 + +### 6.3 schema-grounded 主张:记忆应是记录系统,不是搜索 + +(⚠️,arXiv:2604.27906,vendor 相邻论文——作者推销自家 xmemory 系统、实证数字自报;但其架构论点经 3-0 核实原文,且与 §6.2 的独立基准证据方向一致) + +- 核心论点:嵌入不是谓词求值器;对状态跟踪类负载(实体字段随时间变化),所有无 schema 约束的系统都积累大量错误,**在向量之上加图层也无显著改善**(Mem0 有图/无图的状态错误数几乎相同); +- 其解法:把解释成本移到**写入路径**——schema 感知的写入管线(对象检测→字段检测→值抽取,带校验门与重试),读取变成对已验证记录的受约束查询; +- **与本项目的契合**:这条路线的基础设施就是「一个带 schema 的关系数据库」——正是本项目已有的 Drizzle/Postgres。 + +### 6.4 其他值得记录的发现(均 ❓,待核验) + +- **成本交叉点**:约 10 轮交互(~100k token 累计)后,记忆系统比长上下文更便宜(检索成本趋稳 vs 全量重发持续累加); +- **被动召回 ≠ 主动使用**:在 LoCoMo 上接近饱和的模型,换到多会话相互依赖任务(MemoryArena)跌至 40–60%; +- **记忆投毒(memory poisoning)**:MINJA 类攻击对 Agent 记忆注入成功率 >95%——恶意内容一旦写入记忆会作为「受信状态」持久生效,不同于一次性提示注入。落地时写入管线要有内容边界(用户可见/可删的记忆面板既是产品功能也是安全功能)。 + +--- + +## 7. 关键论文清单 + +**同行评审(可放心引用)** +- CoALA:*Cognitive Architectures for Language Agents*,arXiv:2309.02427,TMLR 2024 —— 记忆四分法与读写形式化的源头 +- *A Survey on the Memory Mechanism of LLM-based Agents*,arXiv:2404.13501,ACM TOIS —— 最常被引的记忆综述 +- MemoryBank(Ebbinghaus 遗忘曲线),AAAI 2024 + +**奠基系统论文(arXiv,被广泛引用)** +- MemGPT:*Towards LLMs as Operating Systems*,arXiv:2310.08560(2023-10) +- Generative Agents:*Interactive Simulacra of Human Behavior*,arXiv:2304.03442(2023-04) +- Mem0:arXiv:2504.19413(2025-04) +- A-Mem:arXiv:2502.12110(2025-02) + +**综述(预印本,注意时效)** +- *From Human Memory to AI Memory*,arXiv:2504.15965 +- *Memory in the Age of AI Agents: A Survey*,arXiv:2512.13564(配套论文索引:github.com/Shichun-Liu/Agent-Memory-Paper-List,200+ 篇) +- arXiv:2602.06052(60 作者统一分类学) +- *Memory for Autonomous LLM Agents*,arXiv:2603.07670 + +**评测与批判** +- MemoryAgentBench:arXiv:2507.05257 +- 确定性新鲜度消解实验:arXiv:2606.01435 +- schema-grounded 记忆主张(xmemory):arXiv:2604.27906 +- LoCoMo:Maharana et al. 2024;LongMemEval:Wu et al. 2025(arXiv:2501.13956) +- Zep 对 Mem0 的质疑:blog.getzep.com "Lies, Damn Lies & Statistics: Is Mem0 Really SOTA in Agent Memory?" + +**工程一手资料** +- AI SDK 官方 RAG Chatbot 指南:ai-sdk.dev/cookbook/guides/rag-chatbot(技术栈与本项目一致) +- vercel-labs/ai-sdk-persistence-db(已归档,仅作参考) +- Memobase:github.com/memodb-io/memobase + +--- + +## 8. 本项目落地路线图 + +综合调研结论,为本项目(Next.js 16 + assistant-ui + AI SDK v7 + Drizzle/Postgres + MiniMax)规划五个里程碑。**总体策略**:走「自建 Postgres 结构化事实表 + pgvector」路线——理由是 (a) §6.2/§6.3 表明结构化+确定性消解是当前证据下最可靠的路线,且与已有技术栈零摩擦;(b) 自建才能达成「学习」目标;(c) 不排除阶段 2 深入后在某一层换用开源件(如 Memobase 的画像 schema 设计)。 + +### M0 · 已有基础(完成) +threads/messages 持久化(L3)、前后端工具双通道(`writeNote` 前端工具机制可直接复用为「记忆工具」)、Drizzle 迁移体系。 + +### M1 · 工作记忆管理(L1+L2)—— 低垂果实 +- 请求前按 token 预算做滑动窗口;超阈值触发滚动摘要(一次 LLM 调用),摘要落库避免重复压缩; +- 用户体验点:超长对话不再变慢/变贵/丢头部设定。 + +### M2 · 情景记忆检索(L4) +- 启用 pgvector(Docker Postgres 装扩展 + Drizzle vector 列 + HNSW 索引); +- 对历史消息按轮次嵌入;检索采用相似度×时近性加权,token 预算内注入; +- 参照 AI SDK 官方 RAG 指南的检索参数起步(cosine >0.5、top-4),实验阶段调参; +- 用户体验点:「我们上次聊到哪了」「你之前说过 X」跨会话可答。 + +### M3 · 结构化事实 + 画像记忆(L5+L6)—— 核心里程碑 +- `memories` 事实表:`(id, user_id, kind, subject, value, source_message_id, valid_from, superseded_by, created_at)` + 可选 embedding 列——**带时间戳与版本链,取最新值用 SQL 而非 LLM**(§6.2 的直接应用); +- 写入管线学 Mem0 两阶段(LLM 抽取 → 与既有记忆比对决策 ADD/UPDATE/DELETE/NOOP),但**冲突消解走确定性 supersede 链**;写入时机学 Memobase 用缓冲/`onFinish` 异步,不阻塞流式响应; +- 画像视图:从事实表聚合出常驻注入的紧凑画像块; +- **用户体验点(产品化重点)**:记忆面板 UI——用户可查看/编辑/删除自己的记忆(assistant-ui 组件 + 新 API 路由),记忆写入时消息旁给出「已记住 ✓」标记(复用 `useAssistantTool` 的生成式 UI 能力)。这同时是 §6.4 记忆投毒风险的产品化缓解。 + +### M4 · 反思与遗忘(L7,可选进阶) +- 后台定期任务:事实表去重合并、久未引用记忆衰减(Ebbinghaus 式)、从情景记忆中反思提炼语义事实(Generative Agents 式); +- 自建 20–50 题小评测集(含知识更新类问题,弥补 LoCoMo 缺陷),横向验证 M2/M3 的效果——回应 §6.1「不可采信厂商数字」。 + +### 关键待决技术点(进入阶段 2 前须回答) +1. **注入挂载点**:记忆注入选在 route.ts 的 system prompt 拼装、AI SDK `prepareStep`、还是暴露为模型工具(Claude memory tool 式)?三者对流式延迟与可控性的影响需要实验; +2. 抽取用 MiniMax 主模型还是更便宜的小模型(成本/质量权衡); +3. embedding 供应商选择(MiniMax 是否有 embedding API,或另接一家)。 + +--- + +## 9. 遗留问题与阶段 2 核验清单 + +本轮已核实证据集中于学术综述与 Mem0 论文;以下条目**只有 ❓ 级证据**,是阶段 2(`02-deep-dives/`)的任务清单,按优先级排序: + +| # | 深入对象 | 要回答的问题 | 优先级(对本项目) | +| --- | --- | --- | --- | +| 1 | **自建路线蓝本**:AI SDK RAG 指南 + Memobase 源码 | 缓冲写入、画像 schema、context() 注入的具体实现 | ★★★(M2/M3 直接依赖) | +| 2 | **Mem0 源码** | 两阶段管线的 prompt 设计、向量库抽象、作用域实现 | ★★★(M3 写入管线参照) | +| 3 | **Anthropic Claude memory tool** | 工具 schema、文件式存储约定、与本项目前端工具机制的兼容性 | ★★☆ | +| 4 | **Zep/Graphiti 源码** | bi-temporal 边模型、无 LLM 查询路径 | ★★☆(时序版本化设计参照) | +| 5 | **LangMem** | 程序性记忆(自更新系统提示)的实现与安全边界 | ★★☆ | +| 6 | **OpenAI ChatGPT memory** | 产品行为逆向:写入时机、用户控制面 | ★☆☆(产品参照) | +| 7 | **Letta** | core memory blocks 的自编辑协议 | ★☆☆ | +| 8 | **评测方法**:LoCoMo/LongMemEval/MemoryAgentBench | 抽出适合本项目的小评测集设计 | ★★☆(M4 依赖) | + +**开放性问题**(调研流程自动生成,保留原样供阶段 2 参考): +1. 在 Next.js + assistant-ui + AI SDK v7 技术栈中,记忆注入的最佳挂载点是什么——adapter 层叠加、`prepareStep`/middleware、还是前/后端工具?(本轮无任何已验证证据覆盖 assistant-ui 生态的记忆实践) +2. OpenAI/Anthropic 官方记忆的实际写入时机与存储结构,以及与开源方案在同一独立基准下的公平对比? +3. Zep 对 Mem0 的质疑孰是孰非?是否存在无利益相关方的第三方复现? +4. 对本项目规模(单用户/小团队),自建「事实表 + LLM 抽取 + 提示注入」相比直接接 Mem0/Zep 托管,在工程成本/延迟/可控性上的实测权衡? + +--- + +*报告完。阶段 2 已完成:见 [逐方案深入](./02-deep-dives/)。* diff --git a/docs/memory/02-deep-dives/01-self-built-postgres.md b/docs/memory/02-deep-dives/01-self-built-postgres.md new file mode 100644 index 0000000..3981bf6 --- /dev/null +++ b/docs/memory/02-deep-dives/01-self-built-postgres.md @@ -0,0 +1,130 @@ +# 自建 Postgres 路线:本项目的候选蓝本 + +## 为什么它是首选 + +当前项目已经具备这条路线最贵的基础设施: + +- [`lib/db/schema.ts`](../../../lib/db/schema.ts) 已使用 Drizzle/Postgres,并有 `vector(1536)` 与 HNSW 索引; +- [`lib/ai/embeddings.ts`](https://github.com/hifizz/thread-chatbot/blob/main/lib/ai/embeddings.ts) 已封装 OpenAI-compatible embedding; +- [`lib/chat/retrieve.ts`](https://github.com/hifizz/thread-chatbot/blob/main/lib/chat/retrieve.ts) 已实现 cosine 检索; +- [`app/api/chat/route.ts`](../../../app/api/chat/route.ts) 已有用户身份、服务端 system prompt、`prepareStep`、`onFinish` 和 `after()`; +- 分支聊天树已经整体持久化,但它保存的是会话状态,不是跨树共享的用户事实。 + +因此“自建”不是从零实现向量数据库,而是给现有数据层增加正确的记忆语义。 + +## 现有 RAG 可以复用什么 + +附件 RAG 的链路是: + +```text +PDF pages + -> chunkPages(size=1000, overlap=150) + -> embedMany + -> attachment_chunks + HNSW + -> embedQuery + -> cosine top-k + -> 带页码片段注入消息 +``` + +可复用:embedding provider、批量写入、HNSW 建索引方式、相似度查询和失败降级。 + +不可直接复用: + +- `attachment_chunks` 以 `attachment_id` 分区,记忆必须以 `user_id` 为第一隔离边界; +- 文档块是 append/replace 内容,事实需要来源、置信度、有效时间、撤销和用户编辑; +- `gt(similarity, 0)` 对文档兜底可以接受,对用户记忆召回过宽; +- `EMBEDDING_DIMENSIONS=1536` 是数据库契约,更换模型不能只改环境变量; +- 向量近邻不能可靠完成“用户现在住哪里”这类当前值查询。 + +## 目标数据模型 + +建议阶段 3 先验证四层,不一次性做完整产品表: + +```text +memory_events + id, user_id, source_tree_id, source_message_id + observed_at, payload, extraction_version + +memory_facts + id, user_id, subject, predicate, value_json + valid_from, valid_to, status, confidence + source_event_id, supersedes_id, created_at, updated_at + +memory_profiles + user_id, profile_json, version, refreshed_at + +memory_embeddings (可选) + fact_id, embedding, searchable_text +``` + +关键不变量: + +1. 所有查询必须显式带 `user_id`; +2. 事实来源不可丢,用户能追溯到树和消息; +3. 当前值由 `status/valid_to/supersedes_id` 和 SQL 决定,不让 LLM 临场猜; +4. 删除默认先做可审计的失效,隐私删除再做物理清理; +5. embedding 是事实的索引,不是事实本体。 + +## 写入路径 + +```text +assistant 流完成 + -> 记录待处理 event(幂等键 = source_message_id + extractor_version) + -> 后台/缓冲任务读取若干新 turn + -> LLM 输出受 schema 约束的候选事实 + -> 确定性校验(作用域、敏感字段、枚举、时间) + -> 按 (user, subject, predicate) 查当前值 + -> ADD / supersede / ignore + -> 异步生成 embedding 与刷新 profile +``` + +`onFinish` 当前承担计费,不能把昂贵抽取直接塞进去。`after()` 只保证请求后的工作有机会完成,不是持久队列;阶段 3 可以用它做最小实验,生产实现要有持久状态、重试和幂等。 + +写入不应默认记住 assistant 的所有回答。第一版只抽取用户明确陈述和明确“请记住”的内容;assistant 生成的计划另设 `kind`,避免把模型幻觉升级成用户事实。 + +## 读取与注入路径 + +每次请求建议只做一次读: + +1. SQL 获取紧凑画像和与问题中明确实体匹配的当前事实; +2. 若问题需要长尾回忆,再对 active facts/events 做向量召回; +3. 按固定 token/字符预算打包; +4. 拼入 [`app/api/chat/route.ts`](../../../app/api/chat/route.ts) 的服务端 `system`。 + +`prepareStep` 适合“模型在本轮调用记忆工具后,需要让下一步看到最新记忆”的场景。若记忆只在请求开始前读取,直接构造 `system` 更简单,也避免每个 tool step 重复查库。AI SDK v7 源码显示 `prepareStep` 返回的 `instructions/messages` 会传递到后续步骤,实施时必须避免重复追加同一 memory block。 + +建议注入格式: + +```xml + + 仅在与当前问题相关时使用;不得把记忆当作用户本轮明确陈述。 + ... + ... + ... + +``` + +## 用户控制面 + +最小可用 UI 必须同时交付: + +- 查看记忆、来源与最近更新时间; +- 编辑当前值并标记为用户确认; +- 删除单条/清空全部; +- 关闭自动记忆或使用临时聊天; +- 在回答旁解释“本次使用了哪些记忆”。 + +没有控制面就不应默认上线自动写入,因为错误记忆和投毒会跨会话持续生效。 + +## 阶段 3 的最小切片 + +先只做 `preferred_language`、`dietary_restriction`、`current_project` 三类 predicate: + +- 结构固定,容易验证冲突; +- 能覆盖偏好、敏感约束和短期状态; +- 不需要一开始就做通用 ontology; +- 可直接比较“全上下文 / 纯向量 / 结构化事实”三条路线。 + +## 判定 + +**进入实验。** 技术栈匹配、数据主权最好,也最符合阶段 1 对确定性新鲜度消解的结论。主要风险不在数据库,而在抽取质量、后台可靠性、隐私控制和 schema 演进。 diff --git a/docs/memory/02-deep-dives/02-mem0.md b/docs/memory/02-deep-dives/02-mem0.md new file mode 100644 index 0000000..e01af7c --- /dev/null +++ b/docs/memory/02-deep-dives/02-mem0.md @@ -0,0 +1,80 @@ +# Mem0:从四操作管线到 V3 ADD-only + +> 源码快照:`mem0ai/mem0@d6d89c987bddf580870db14c69db974edfc5263c`(2026-07-23)。重点文件:[`mem0/memory/main.py`](https://github.com/mem0ai/mem0/blob/d6d89c987bddf580870db14c69db974edfc5263c/mem0/memory/main.py)、[`mem0/configs/prompts.py`](https://github.com/mem0ai/mem0/blob/d6d89c987bddf580870db14c69db974edfc5263c/mem0/configs/prompts.py)。 + +## 设计动机 + +Mem0 把“从对话里挑出值得长期保存的信息”包装成一个通用 memory API,并用 `user_id`、`agent_id`、`run_id` 做作用域。调用者可选择: + +- `infer=True`:由 LLM 抽取记忆; +- `infer=False`:把非 system 消息原文直接存为记忆; +- `memory_type="procedural_memory"`:生成代理执行历史的过程性摘要。 + +## 必须修正的旧认识 + +阶段 1 根据论文和旧源码把 Mem0 描述为: + +```text +事实抽取 -> 检索相似旧记忆 -> LLM 决定 ADD/UPDATE/DELETE/NONE +``` + +当前 OSS 主干仍保留 `DEFAULT_UPDATE_MEMORY_PROMPT`,但 `Memory._add_to_vector_store()` 的主路径已经标成 `V3 PHASED BATCH PIPELINE`,实际采用 **ADD-only + memory linking**。不能仅看到旧 prompt 就断言运行时仍走四操作管线。 + +## 当前源码调用链 + +`Memory.add()` 的通用记忆路径最终进入 `_add_to_vector_store()`: + +1. 从 history DB 取同一 session 最近 10 条消息; +2. `parse_messages()` 把新消息串成抽取输入; +3. 对输入生成 query embedding,在当前 `user_id/agent_id/run_id` 范围内检索 top 10; +4. 把真实 UUID 映射为短整数 ID,降低模型伪造 ID 的概率; +5. 使用 `ADDITIVE_EXTRACTION_PROMPT` 做一次 JSON 抽取; +6. prompt 明确规定唯一操作是 ADD,旧记忆仅用于去重和 `linked_memory_ids`; +7. 批量 embedding 新记忆;失败时退回逐条 embedding; +8. 用文本 MD5 对既有结果与本批次做精确去重; +9. 批量写向量条目、history,并抽取实体建立“实体 -> memory ids”关联; +10. 保存原消息,供后续抽取读取最近上下文。 + +V3 prompt 还要求: + +- 同时从 user 与 assistant 消息抽取,但正确标注来源; +- 用 observation date 把相对时间转为绝对时间; +- 记忆保持自包含、保留专名和精确数字; +- 发生偏好变化或矛盾时,新增一条并链接旧记忆,而不是覆盖旧值。 + +## 数据与检索 + +向量条目的 payload 至少包含 `data`、`hash`、`created_at`、`updated_at`、`text_lemmatized` 和作用域字段。代码同时维护 SQLite history,并引入 BM25 词形化、多信号排序与 entity boost;具体 vector store、LLM、embedder、reranker 都经 factory 配置。 + +这带来良好的适配性,但也意味着语义取决于多个可替换组件,不能把单一 benchmark 数字当作固定产品能力。 + +## 优点 + +- API 面小,作用域清晰,provider/vector store 可替换; +- V3 单次抽取与批量 embedding 比逐事实多轮判断更省调用; +- 旧记忆 ID 映射、hash 去重、history、实体关联都有明确工程防线; +- ADD-only 保留演化轨迹,避免 LLM 直接破坏历史。 + +## 风险 + +- ADD-only 把“冲突消解”推迟到读取或后处理;当前值仍不能只靠相似检索得到; +- prompt 采用“有疑问就抽取”,更偏召回率,可能制造大量低价值记忆; +- assistant 推荐也会成为记忆,若不分 source/kind,模型输出可能被再次当作用户事实; +- OSS 的 temporal `timestamp` 参数在代码中明确报“platform-only”,不能假设开源版已提供完整时间语义; +- hash 只消除字面重复,语义重复仍依赖 LLM 与检索。 + +## 对本项目的可复用点 + +- 作用域字段必须在所有写/读路径中保持不变; +- 给模型短 ID,执行时再映射真实主键; +- 抽取失败与“没有事实”必须是两种状态,便于重试; +- 先批量 embedding,失败再逐条降级; +- 保留原始 event/history,派生事实可以重算。 + +## 不直接接入的原因 + +本项目核心需求是“当前用户事实可解释、可编辑、可确定性更新”。Mem0 V3 更像高召回的记忆事件层,而不是强 schema 的记录系统。直接接入仍需额外实现当前值、权限、来源和 UI,收益不足以抵消一层外部抽象。 + +## 判定 + +**借鉴,不接入。** 阶段 3 可把 Mem0 V3 的“单次 ADD-only 抽取”作为候选 B,与“schema 候选 + 确定性 supersede”候选 A 对比。 diff --git a/docs/memory/02-deep-dives/03-anthropic-memory-tool.md b/docs/memory/02-deep-dives/03-anthropic-memory-tool.md new file mode 100644 index 0000000..53341f4 --- /dev/null +++ b/docs/memory/02-deep-dives/03-anthropic-memory-tool.md @@ -0,0 +1,82 @@ +# Anthropic memory tool:文件协议与 JIT 读取 + +> 文档快照:2026-07-24。官方入口:[Memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool)。工具 `memory_20250818` 已 GA,但部分 SDK helper 仍位于 beta namespace。 + +## 设计动机 + +Anthropic 没有替开发者托管记忆。模型只发出文件操作请求,应用在自己控制的存储上执行,再以 `tool_result` 返回。`/memories` 是逻辑前缀,可以映射到本地目录、对象存储或数据库。 + +核心思路不是“每轮把所有记忆塞进 prompt”,而是: + +```text +任务开始 -> view /memories + -> view 相关文件/行 + -> 使用信息 + -> create/edit/rename/delete 持久化新进展 +``` + +这是 just-in-time context retrieval,尤其适合长程任务和跨 session 恢复。 + +## 工具协议 + +请求只需声明: + +```json +{ "type": "memory_20250818", "name": "memory" } +``` + +应用必须实现六个命令: + +| command | 关键参数 | 语义 | +| --- | --- | --- | +| `view` | `path`, `view_range?` | 列目录或按 1-based 行号读文件 | +| `create` | `path`, `file_text` | 创建文件;官方 handler 对已存在路径报错 | +| `str_replace` | `path`, `old_str`, `new_str?` | 仅在旧文本唯一出现时替换 | +| `insert` | `path`, `insert_line`, `insert_text` | 在指定行后插入,0 表示文件头 | +| `delete` | `path` | 删除文件/目录,但不能删除根目录 | +| `rename` | `old_path`, `new_path` | 移动或改名,不覆盖已有目标 | + +Python/TypeScript SDK 提供本地文件 helper;生产实现仍应替换为每用户隔离的持久存储。 + +## 为什么 `str_replace` 值得借鉴 + +它要求 `old_str` 唯一匹配,否则拒绝操作。这相当于一种面向文本的乐观并发控制:模型必须先读到精确旧值,才能修改。相比“整文件覆盖”,它减少误删与基于过期视图覆盖新内容的风险。 + +对结构化事实表,可映射成: + +```text +update memory_facts +set value = :new, version = version + 1 +where id = :id and version = :expected_version +``` + +## 安全边界 + +官方文档明确把安全责任交给应用: + +- 所有路径必须位于 `/memories`; +- canonicalize 后再次验证根目录; +- 拒绝 `../`、`..\\` 和 URL-encoded traversal; +- 限制文件大小、单次 `view` 返回量与文件寿命; +- 不把敏感数据交给模型自行决定保存; +- 错误用 `tool_result.is_error=true` 返回,不能静默成功。 + +多租户实现还必须把逻辑路径绑定到已鉴权 `userId`,绝不能让模型输入决定真实租户目录。 + +## 与当前项目的映射 + +项目已有 AI SDK tool loop 和 `prepareStep`,可以用普通 `tool()` 模拟相同协议;但原生 Anthropic schema 只适用于 Claude provider。当前产品支持多模型,因此若采用,应定义 provider-neutral 的 `viewMemory/updateMemory` 工具,而不是把 `memory_20250818` 写死到主链路。 + +此外,Thread Chat 的主要对象是“关于用户的事实”,不是“agent 自己维护的项目文件”。对用户画像而言,文件缺少 predicate、来源和有效时间,不应成为主存储。 + +## 可复用点 + +- 存储完全由应用控制; +- 先读后写、精确替换、显式错误; +- 按需读取而非全量常驻; +- memory 与 compaction 分工:前者跨 session,后者压缩当前会话; +- 目录/文件可直接成为用户可查看的审计界面。 + +## 判定 + +**借鉴工具协议,不照搬文件模型。** 若阶段 4 增加“模型主动管理记忆”,应把可写动作限制为候选提议或带版本条件的 CRUD,并继续以 Postgres 事实表作为记录系统。 diff --git a/docs/memory/02-deep-dives/04-graphiti.md b/docs/memory/02-deep-dives/04-graphiti.md new file mode 100644 index 0000000..c5064a0 --- /dev/null +++ b/docs/memory/02-deep-dives/04-graphiti.md @@ -0,0 +1,100 @@ +# Graphiti / Zep:时序 Context Graph + +> 源码快照:`getzep/graphiti@3bb2d0bba56f8e22311574c045452c420a012f49`(2026-07-23)。重点文件:[`graphiti_core/graphiti.py`](https://github.com/getzep/graphiti/blob/3bb2d0bba56f8e22311574c045452c420a012f49/graphiti_core/graphiti.py)、[`graphiti_core/edges.py`](https://github.com/getzep/graphiti/blob/3bb2d0bba56f8e22311574c045452c420a012f49/graphiti_core/edges.py)。 + +## Graphiti 与 Zep 不是同一个交付物 + +- **Graphiti**:开源 Python 时序图引擎;调用者自建用户、消息、图数据库与运维。 +- **Zep**:托管 context infrastructure,使用其生产图引擎并提供 SDK、治理和运维能力。 + +不能把 Zep 的托管延迟或规模声明直接归因于自托管 Graphiti。 + +## 数据模型 + +Graphiti 的核心对象: + +- `EpisodicNode`:原始输入,承担 provenance; +- `EntityNode`:人、地点、项目等实体; +- `EntityEdge`:实体之间的事实/关系; +- community/saga:更高层组织与连续 episode。 + +`EntityEdge` 源码字段直接体现 bi-temporal 设计: + +```text +fact, fact_embedding, episodes +created_at, expired_at +valid_at, invalid_at +reference_time +source_node_uuid, target_node_uuid, group_id +``` + +`valid_at/invalid_at` 描述事实何时在现实中成立;`created_at/expired_at` 描述系统何时知道或使其失效;`episodes` 保留来源。 + +## 写入调用链 + +`Graphiti.add_episode()` 及其内部方法大致执行: + +1. 保存带 `group_id`、`source_description`、`reference_time` 的 episode; +2. 读取前序 episodes 作为上下文; +3. LLM 结构化抽取 entity nodes; +4. `resolve_extracted_nodes()` 与图中实体去重; +5. LLM 抽取 relation edges 与属性; +6. `resolve_extracted_edges()` 处理重复与矛盾,返回 resolved、invalidated、new 三组边; +7. 为事实生成 embedding; +8. 批量保存 episode、entity、关系与 provenance edges; +9. 可选更新 community/saga 摘要。 + +源码提供 bulk 路径并使用 semaphore 控制并发。官方 README 明确说明 structured output 质量决定 ingestion 稳定性,小模型或仅“声称兼容 JSON schema”的 provider 可能失败。 + +## 读取路径 + +Graphiti 不是在查询时让 LLM 总结整张图,而是组合: + +- fact/entity embedding 的语义检索; +- 全文/BM25; +- 图距离或节点邻接; +- 预设 search recipes 和 reranking。 + +检索结果是事实边/实体,LLM 只消费返回的 context。查询本身可以不调用 LLM,但写入已经付出多次 LLM 和 embedding 成本。 + +## 优点 + +- 时间语义、provenance 和历史保留是一等字段; +- 同一实体的演化比扁平向量条目更自然; +- hybrid retrieval 能回答关系和多跳问题; +- prescribed ontology 可用 Pydantic 约束实体/边类型。 + +## 成本与风险 + +- 需要 Python 服务和 Neo4j/FalkorDB/Neptune;当前项目只有 Postgres; +- 一个 episode 的写入包含多次结构化抽取、去重、关系消解和 embedding; +- `group_id` 是图分区,不自动等于应用的鉴权边界; +- LLM 仍参与实体/边消解,“自动失效”不等于永远正确; +- 项目当前需要的主要是少量用户当前值,图建模收益尚未覆盖运维复杂度。 + +## 对本项目的可复用点 + +不引入图数据库也应借用四个字段: + +```text +valid_from / valid_to # 现实有效时间 +recorded_at # 系统观察时间 +source_event_id # provenance +supersedes_id # 演化链 +``` + +还应把“原始 episode”和“派生 fact”分开:抽取模型升级后可从 episode 重算事实,而不丢原始证据。 + +## 何时再考虑 Graphiti + +满足以下至少两项再评估: + +- 需要跨用户/组织实体关系; +- 多跳关系查询是主要产品能力; +- 需要回答历史时点真值; +- 单用户事实量已让关系表查询变得笨重; +- 团队能承担独立 Python/graph 服务。 + +## 判定 + +**暂缓。** 学习其 bi-temporal 与 provenance 设计,但第一版用 Postgres 版本化事实表实现相同的关键不变量。 diff --git a/docs/memory/02-deep-dives/05-langmem.md b/docs/memory/02-deep-dives/05-langmem.md new file mode 100644 index 0000000..20895d5 --- /dev/null +++ b/docs/memory/02-deep-dives/05-langmem.md @@ -0,0 +1,92 @@ +# LangMem:hot path 工具、后台 manager 与程序性记忆 + +> 源码快照:`langchain-ai/langmem@a2d580946465137c89162e67dc0b18108bd4850c`(2026-07-15)。重点文件:[`knowledge/tools.py`](https://github.com/langchain-ai/langmem/blob/a2d580946465137c89162e67dc0b18108bd4850c/src/langmem/knowledge/tools.py)、[`knowledge/extraction.py`](https://github.com/langchain-ai/langmem/blob/a2d580946465137c89162e67dc0b18108bd4850c/src/langmem/knowledge/extraction.py)、[`prompts/optimization.py`](https://github.com/langchain-ai/langmem/blob/a2d580946465137c89162e67dc0b18108bd4850c/src/langmem/prompts/optimization.py)。 + +## 三组能力 + +LangMem 不是单一“记忆数据库”,而是构建在 LangGraph store 之上的三组 primitive: + +1. hot path 的 `manage_memory` / `search_memory` 工具; +2. background 的 `MemoryManager` 抽取与整合; +3. 根据轨迹和反馈更新系统提示的 prompt optimizer。 + +## Hot path 工具 + +`create_manage_memory_tool()` 生成一个结构化工具: + +```text +content: str | 自定义 Pydantic schema +action: create | update | delete +id: UUID(update/delete 必填) +``` + +实现本身很薄: + +- namespace 通过模板和 runtime config 解析,可做 per-user 分区; +- delete 调 `store.delete/adelete`; +- create 生成 UUID; +- create/update 都调用 `store.put/aput`,value 为 `{"content": ...}`; +- 默认说明要求模型主动保存偏好、显式记忆请求、工作上下文,并修正过期 memory。 + +因此“一致性”主要依赖模型是否调用正确 action;BaseStore 只提供存取,不自动理解冲突。 + +## Background MemoryManager + +`MemoryManager` 使用 Trustcall 的 schema extraction: + +- 输入:新消息、可选 existing memories、`max_steps`; +- 默认支持 insert/update,delete 默认关闭; +- `_prepare_messages()` 把轨迹和“extract/contextualize、compare/update、synthesize”说明组成 memory subroutine; +- `create_extractor()` 接收自定义 schemas 和已有文档 ID; +- 每步可并行产生多个 tool calls;第二步起增加 `Done` 工具; +- 外部传入的 memory ID 被保留,update 继续使用同一 ID; +- 返回的是变更后的 memory objects,`create_memory_store_manager()` 再负责持久化。 + +这比“让聊天主模型自己顺便记住”更易测试,因为 memory manager 是独立纯函数式边界。 + +## 程序性记忆 + +`create_prompt_optimizer()` 支持: + +- `prompt_memory`:单次调用提取成功模式; +- `metaprompt`:多轮分析/更新; +- `gradient`:反思与应用更新分开,调用最多。 + +它实际上会改写 agent 的行为 prompt。此能力风险高于事实记忆:错误或恶意反馈可能长期改变系统行为,必须版本化、评测并审批,不能直接让终端用户对生产 system prompt 写入。 + +## 优点 + +- 工具、抽取 manager、存储分层清楚; +- schema 可由应用定义; +- namespace 明确,PostgresStore 可用于生产; +- background manager 可以独立回放与评测; +- delete 默认关闭是合理保守值。 + +## 与本项目的摩擦 + +- Python + LangGraph/Trustcall 生态,与当前 TypeScript + AI SDK v7 不同; +- 引入它仍需部署 Python 服务或重写桥接; +- current project 已有 AI SDK `tool()`、Zod schema、Drizzle 和 `prepareStep`,核心 primitive 可原生实现; +- prompt optimizer 不应成为第一版用户记忆的一部分。 + +## 可移植设计 + +在本项目中可以复刻,而无需引入依赖: + +```text +Zod CandidateFact[] + <- 独立 generateText/object 输出 + <- new events + selected current facts + +deterministic reducer + -> insert/supersede/ignore + +Drizzle transaction + -> event + facts + extraction run +``` + +模型只提议候选,reducer 决定允许的状态迁移;这比 hot-path 工具直接 CRUD 更符合当前需求。 + +## 判定 + +**借鉴 background manager,不接入 LangMem。** 程序性记忆等事实记忆和评测体系稳定后再单独立项。 diff --git a/docs/memory/02-deep-dives/06-memobase.md b/docs/memory/02-deep-dives/06-memobase.md new file mode 100644 index 0000000..5eacf34 --- /dev/null +++ b/docs/memory/02-deep-dives/06-memobase.md @@ -0,0 +1,101 @@ +# Memobase:缓冲写入、画像与事件时间线 + +> 源码/文档快照:`memodb-io/memobase@358c16bbc6d687937d79bc2f984a11c3be8da901`(2026-01-11)。入口:[仓库 README](https://github.com/memodb-io/memobase/tree/358c16bbc6d687937d79bc2f984a11c3be8da901)。 + +## 设计动机 + +Memobase 的口号是 “Memory for User, not Agent”。它不让每条聊天立刻进入 agentic tool loop,而是维护: + +- 用户 blob buffer; +- topic/sub-topic 结构化 profile; +- 带时间的 event timeline; +- 把画像与事件打包进 prompt 的 `context()`。 + +它解决的是在线产品的稳定个性化,而不是让 agent 自主维护任意知识。 + +## 调用链 + +公开 API 展示的路径: + +```text +u.insert(ChatBlob(messages)) + -> blob 进入该用户 buffer + -> 达到阈值 / 闲置超时 / 手动 u.flush() + -> 后台工作流批量抽取与更新 + -> profile(topic, sub_topic, content) + events + +u.context(max_token_size, prefer_topics) + -> SQL 读取预计算结果 + -> 按预算打包 user background + latest events + -> 注入聊天 prompt +``` + +默认异步 flush;README 给出的典型触发值是约 1024 tokens 或闲置约 1 小时,也允许在会话结束时手动 flush。处理后的原始 blob 默认可删除,是否留存由配置控制。 + +## 为什么 buffer 很重要 + +每轮都抽取会造成: + +- LLM 固定开销被短消息放大; +- 同一话题被多次拆成碎片; +- 写入延迟落在用户等待路径; +- 多个并发 turn 更容易发生旧结果覆盖新结果。 + +缓冲让模型一次看到一个较完整的局部情境,成本按多轮摊薄。代价是 eventual consistency:刚说完的信息可能暂时没有进入 profile。 + +## 画像 schema + +Profile 以 `topic/sub_topic/content` 组织,例如: + +```text +basic_info.name +interest.games +work.title +psychological.goals +``` + +这比无 schema 的向量条目更适合: + +- 直接构造 system prompt; +- 产品 UI 按分类展示; +- SQL 过滤或分析; +- 限制哪些信息允许被记住。 + +但开放式 topic 仍会漂移。项目落地时应先固定少量 predicate,不直接复制大而全的人格画像分类。 + +## 读取语义 + +常规 `profile()` 是预计算结果,读取便宜;`context()` 负责 token budget、topic 偏好和事件打包。README 同时说明较新 context 搜索可能调用 embedding、耗时高于单纯 profile 读取。两者不应混称为同一延迟。 + +## 可复用点 + +- 写入脱离热路径; +- buffer 以 token 和 idle time 双阈值触发; +- 画像与事件分层:稳定属性常驻,长尾经历按需; +- 注入 API 自带预算,而不是把整张画像无限拼接; +- profile schema 同时服务模型和用户控制面。 + +## 不直接接入的原因 + +- 需要独立 FastAPI/Postgres/Redis 服务或托管 API; +- 本项目已经有 Postgres、鉴权和 embedding,双写另一套用户系统会增加一致性成本; +- 厂商自报的 LOCOMO/延迟不能替代本项目数据上的验证; +- 泛化画像类别可能收集超出产品必要范围的敏感信息。 + +## 本项目落地映射 + +建议复制其节奏,而不是 SDK: + +```text +memory_events(status=pending) + -> 每用户 token_count >= N 或 oldest_event_age >= T + -> extraction_run + -> facts transaction + -> profile refresh +``` + +阶段 3 先测试 `N=4~8 turns` 和会话结束 flush;不要先锁死 1024 tokens/1 小时,这些数值没有本项目证据。 + +## 判定 + +**重点借鉴。** 在所有外部方案中,它与“用户画像 + 低在线延迟 + 自有 Postgres”的产品目标最接近。 diff --git a/docs/memory/02-deep-dives/07-letta.md b/docs/memory/02-deep-dives/07-letta.md new file mode 100644 index 0000000..93e7b6e --- /dev/null +++ b/docs/memory/02-deep-dives/07-letta.md @@ -0,0 +1,70 @@ +# Letta / MemGPT:常驻 memory blocks 与后台 agent + +> 生态快照:2026-07-24。`letta-ai/letta` 主仓库已明确标注为 legacy V1 server;新项目推荐 Letta Agent SDK。用于通用记忆的实验性封装快照为 `letta-ai/ai-memory-sdk@4494e00410469082bf298b8b03b7c9f93e244f14`。 + +## 设计动机 + +MemGPT/Letta 的核心比喻是“LLM 操作系统”:上下文窗口像有限内存,agent 通过工具主动编辑常驻内容,并把更大历史放入可检索的外部存储。 + +当前可见的层次: + +- **memory blocks**:常驻上下文的带 label 文本块,如 `human`、`persona`、`preferences`; +- **files**:较大的只读/可开关资料; +- **archival memory**:可写、语义检索的长尾 passages; +- **external RAG**:通过自定义工具/MCP 访问。 + +Letta 文档对旧 V1 的建议是:重要且小的内容放 blocks,较弱的情景记忆放 archival。 + +## AI Memory SDK 的实现模型 + +实验性 Memory SDK 没有把主聊天模型换成 Letta,而是创建一个“subconscious agent”: + +```text +subject_id + -> 一个 Letta memory agent + -> 多个 labeled blocks + +主应用先读取 blocks -> 拼进自己的 system prompt +主应用完成对话 -> 批量 add_messages() +memory agent 异步处理 -> 更新 blocks +可选 skip_vector_storage=false -> 写 archival passages +``` + +一个 subject 可以是用户、项目或团队。SDK 建议每次批量发送 5–10 条消息或只在消息被挤出上下文时处理,以降低 agent 调用成本。 + +## 与早期 MemGPT 的变化 + +不能再按 2023 年论文假设当前产品 API: + +- V1 server/SDK 仍存在,但官方主仓库已称 legacy; +- 新入口是 Agent SDK、Letta Code/App Server; +- memory filesystem、git-backed context、sleeptime/dreaming 等能力仍在快速演进; +- 因此源码级集成必须钉版本,不能只依据论文概念。 + +## 优点 + +- memory block 是“每轮必带压缩视图”的直接实现; +- block label/description 给模型明确写入边界; +- blocks 可跨 agent 共享; +- 主 agent 与后台 memory agent 分离,避免阻塞用户响应; +- archival 层补足 block 容量限制。 + +## 风险 + +- 记忆 agent 本身是有状态 runtime,接入不只是加一个数据库 SDK; +- block 自编辑仍由 LLM 决定,缺少强 schema 当前值约束; +- 一用户一 agent 带来生命周期、成本、删除和并发管理; +- 官方推荐入口变化快,V1、Agent SDK、Memory SDK、Code SDK 容易混用; +- 当前项目已有自己的 tree state、streaming、tools 与计费,替换运行时侵入面很大。 + +## 可复用点 + +- 用多个短 block,而不是一个无限增长的“用户简介”; +- label + description 同时约束写入和注入语义; +- 画像常驻、长尾事件检索; +- 后台 worker/agent 处理 memory,不占主聊天延迟; +- 对记忆变更做版本化,借鉴 git-backed context 的可回滚思想。 + +## 判定 + +**不接入。** 其 memory blocks 是有价值的产品抽象,但本项目可以用 `memory_profiles.profile_json` 与固定 sections 更轻量地实现。 diff --git a/docs/memory/02-deep-dives/08-chatgpt-memory.md b/docs/memory/02-deep-dives/08-chatgpt-memory.md new file mode 100644 index 0000000..5633700 --- /dev/null +++ b/docs/memory/02-deep-dives/08-chatgpt-memory.md @@ -0,0 +1,68 @@ +# ChatGPT memory:闭源产品行为参考 + +> 证据边界:ChatGPT memory 是闭源产品,本篇只记录 OpenAI 官方公开行为,不声称知道内部表结构、向量库或 prompt。官方来源:[Memory FAQ](https://help.openai.com/en/articles/8590148-memory-faq)、[Dreaming: Better memory for a more helpful ChatGPT](https://openai.com/index/chatgpt-memory-dreaming/)。 + +## 2026 年的产品模型 + +旧系统包含两条路径: + +- saved memories:用户明确要求记住,或聊天中触发保存; +- reference chat history:从历史聊天中提取上下文。 + +OpenAI 2026 年披露的新架构以 **dreaming** 为基础:后台过程持续综合过去对话,更新一个可用于新对话的 memory state。官方强调它会随时间修正状态,例如把“将去新加坡”演化成“2026 年 7 月去过新加坡”。 + +## 能确认的行为 + +- memory 是跨聊天的个性化上下文; +- 后台 synthesis 不要求用户每次明确说“记住”; +- memory summary 只展示高层视图,不保证列出所有内部上下文; +- 用户能刷新 summary、纠正信息、要求不要再提及; +- memory sources 可解释一次回答使用了哪些来源; +- Temporary Chat 不使用/不写入 memory; +- 删除聊天不等于删除独立保存的 memory; +- legacy saved memories 可单独管理,删除日志可能为安全/调试保留最多 30 天。 + +## 产品设计上真正值得学的部分 + +### 1. 记忆必须有可见表面 + +用户要能知道系统记住了什么、为什么使用、如何纠正。只在后端静默注入会把错误个性化变成不可诊断行为。 + +### 2. 来源与记忆分离 + +删除来源聊天和删除派生 memory 是不同动作,但 UI 必须解释清楚。对本项目意味着 `source_message_id` 不能省略。 + +### 3. 时间会主动改变语义 + +事实不只在新消息到达时变化。计划、地点、短期目标会自然过期,需要 `valid_from/valid_to` 或周期性 consolidation。 + +### 4. summary 不是完整记录 + +用户可见 summary 是控制面,不一定是数据库真相。若本项目也采用聚合画像,应同时允许展开来源事实,避免只提供不可核验的总括。 + +## 不应推断的内容 + +官方没有公开: + +- 具体 embedding、数据库或检索 top-k; +- dreaming 的模型、触发频率、prompt 与成本; +- 冲突消解算法; +- memory summary 与实际注入 context 是否一一对应; +- 敏感信息分类器的完整规则。 + +因此不能把“ChatGPT 看起来能记住”翻译成某个可复制的源码架构。 + +## 对本项目的最小 UX 要求 + +在自动记忆默认开启前,至少提供: + +1. memory summary/列表; +2. 每条来源与时间; +3. 编辑、删除、清空; +4. 临时聊天或关闭自动记忆; +5. 回答级“使用了哪些记忆”; +6. 明确区分“删除对话”和“删除派生记忆”。 + +## 判定 + +**重点借鉴控制面与后台持续综合,不把闭源行为当作实现证据。** diff --git a/docs/memory/02-deep-dives/09-evaluation.md b/docs/memory/02-deep-dives/09-evaluation.md new file mode 100644 index 0000000..e39a3e7 --- /dev/null +++ b/docs/memory/02-deep-dives/09-evaluation.md @@ -0,0 +1,130 @@ +# 评测方案:从公开 benchmark 到本项目最小实验 + +## 为什么不能直接排名选型 + +公开分数常同时改变 memory backend、生成模型、judge、top-k、prompt、数据清洗版本与 full-context 上限。厂商复测还可能只公布有利配置。阶段 3 的目标不是复现“SOTA”,而是回答本项目的工程问题: + +- 应该记什么? +- 新事实如何替换旧事实? +- 读取是否把正确证据送给模型? +- 延迟、token 与错误写入是否可接受? + +## 可用公开集 + +| 数据集 | 主要能力 | 适合用途 | 不足 | +| --- | --- | --- | --- | +| [LoCoMo](https://github.com/snap-research/locomo/tree/3eb6f2c585f5e1699204e3c3bdf7adc5c28cb376) | 多 session 事实、时间、多跳 | 兼容业界结果、长对话回放 | 仅 10 段对话;judge 与配置敏感 | +| [LongMemEval](https://github.com/xiaowu0162/LongMemEval/tree/9e0b455f4ef0e2ab8f2e582289761153549043fc) | extraction、多 session、knowledge update、时间、拒答 | 当前值与新鲜度 | 合成/编排成分较高 | +| [LongMemEval-V2](https://github.com/xiaowu0162/LongMemEval-V2) | agent 经验、环境定制 | 后续程序性记忆 | 超出第一版用户事实范围 | + +第一轮不要跑完整排行榜。先抽 20–50 题,并加入本项目自己的中文/英文 fixture。 + +## 必须分层计分 + +端到端“答案对不对”无法定位问题。每个样本至少记录: + +1. **write precision/recall**:该写的事实是否写入,不该写的是否被记住; +2. **state accuracy**:当前值、旧值、有效时间是否正确; +3. **retrieval recall@k**:正确证据是否进入候选; +4. **context precision**:注入的无关/矛盾记忆比例; +5. **answer correctness**:模型最终答案; +6. **abstention**:没有证据时是否承认不知道; +7. **privacy/control**:删除后是否停止召回、跨用户是否绝不泄露; +8. **cost/latency**:写入调用数、tokens、读 p50/p95、增加的输入 tokens。 + +## 本项目 fixture 格式 + +建议把测试用例版本化为 JSON: + +```json +{ + "id": "knowledge-update-city-zh", + "userId": "u-a", + "events": [ + { "at": "2026-01-01", "text": "我现在住在杭州。" }, + { "at": "2026-06-01", "text": "我搬到新加坡了。" } + ], + "query": { "at": "2026-07-01", "text": "我现在住哪里?" }, + "expected": { + "answerFacts": ["current_location=Singapore"], + "inactiveFacts": ["current_location=Hangzhou"], + "mustNotContain": ["Hangzhou is current"] + } +} +``` + +同一 fixture 必须可喂给三个 backend,避免每种方案使用不同数据加工。 + +## 第一轮 30 题组成 + +| 类别 | 数量 | 示例 | +| --- | ---: | --- | +| 稳定偏好 | 5 | 语言、格式、饮食 | +| 当前值更新 | 6 | 城市、项目、职位变化 | +| 时间与过期 | 4 | 旅行结束、临时计划 | +| 多 session 组合 | 4 | 分散透露的人/项目关系 | +| 否定与撤回 | 3 | “我不再…”、“忘掉…” | +| 不应记忆 | 3 | 寒暄、assistant 幻觉、一次性敏感信息 | +| 删除与隔离 | 3 | 删除后不召回、u-a/u-b 隔离 | +| 拒答 | 2 | 从未提供的信息 | + +至少一半中文,全部包含稳定 ID 和绝对时间。 + +## 对比组 + +在相同聊天模型、embedding、答案 prompt、预算下比较: + +- A0:只给当前 session; +- A1:全历史; +- A2:历史 turn 向量 top-k; +- A3:Mem0 风格 ADD-only 记忆 + vector; +- A4:结构化 fact + 确定性 supersede; +- A5:A4 + 紧凑 profile + 长尾 vector。 + +若无法锁定模型采样,至少把原始抽取 JSON、检索结果和最终 prompt 保存,允许人工复核。 + +## 成功门槛 + +第一版不是追求最高 QA 分,而是满足上线底线: + +- 跨用户泄露:0; +- 删除后召回:0; +- current-value 冲突准确率:≥95%; +- 不应记忆 precision:≥95%; +- retrieval recall@6:≥90%; +- 在线读路径 p95:目标 <150ms(不含主模型); +- 注入预算:默认不超过主上下文的 10%; +- 写入失败不影响聊天成功,且可重试、无重复事实。 + +这些是阶段 3 的初始工程门槛,不是已经测得的结果。 + +## 实验顺序 + +1. 先做纯函数 reducer:candidate facts -> state; +2. 再比较抽取 prompt/schema; +3. 再接 Postgres 与向量召回; +4. 最后接 `route.ts` 测真实流式延迟; +5. 通过后才设计 memory UI 和自动写入默认值。 + +## 需要记录的反例 + +每次失败都归入一个稳定 taxonomy: + +```text +WRITE_MISSED +WRITE_FALSE_POSITIVE +ATTRIBUTION_ERROR +TEMPORAL_NORMALIZATION_ERROR +CONFLICT_RESOLUTION_ERROR +RETRIEVAL_MISS +CONTEXT_DISTRACTION +ANSWER_IGNORED_EVIDENCE +DELETION_LEAK +TENANT_LEAK +``` + +只看总分会掩盖安全性失败;taxonomy 才能指导下一轮修改。 + +## 阶段 3 的明确输入 + +阶段 2 推荐从 A4 开始,实现三个 predicate 的 30 题 fixture;A3 用同一数据作为对照。若 A4 没有在 current-value、删除和隔离上明显胜出,就不进入通用 schema 和 UI 开发。 diff --git a/docs/memory/02-deep-dives/README.md b/docs/memory/02-deep-dives/README.md new file mode 100644 index 0000000..ca5f5ca --- /dev/null +++ b/docs/memory/02-deep-dives/README.md @@ -0,0 +1,57 @@ +# 阶段 2:逐方案深入 + +> 研究快照:2026-07-24。本文档集完成 PR [#2](https://github.com/hifizz/thread-chatbot/pull/2) 中“每个代表方案一篇:设计动机、架构、源码级实现细节”的任务。外部项目仍在快速变化,引用的主干快照与关键文件均在各篇中标明。 + +## 结论先行 + +本项目不应直接引入某个通用记忆框架作为核心依赖。优先进入实验的是: + +1. 以现有 Postgres、Drizzle、pgvector 为底座,自建“结构化事实 + 事件时间线 + 可选向量召回”; +2. 写入采用 Memobase 的缓冲思想,但事实冲突由 schema 和确定性版本规则处理; +3. 读取采用“紧凑画像常驻 + 精确事实查询 + 长尾语义召回”的分层组合; +4. 借鉴 ChatGPT 的可审计控制面和 Anthropic memory tool 的受限 CRUD 协议; +5. 暂不引入 Graphiti、LangMem、Letta 或 Mem0 运行时;它们分别带来图数据库、Python/LangGraph、外部 agent runtime 或不可控写入语义。 + +这只是阶段 2 的架构决策,不等于已经实现产品记忆。数据表、写入任务、注入与 UI 属于阶段 3/4。 + +## 文档导航 + +| 文档 | 重点 | 对本项目的判定 | +| --- | --- | --- | +| [01-self-built-postgres.md](./01-self-built-postgres.md) | 基于当前源码的目标架构与挂载点 | **进入实验** | +| [02-mem0.md](./02-mem0.md) | V3 ADD-only 管线、检索、作用域 | 借鉴,不接入 | +| [03-anthropic-memory-tool.md](./03-anthropic-memory-tool.md) | 文件式 JIT 记忆协议与安全边界 | 借鉴工具协议 | +| [04-graphiti.md](./04-graphiti.md) | bi-temporal 图、写入与混合检索 | 暂缓 | +| [05-langmem.md](./05-langmem.md) | hot path 工具、后台 manager、提示优化 | 借鉴 manager | +| [06-memobase.md](./06-memobase.md) | 缓冲写入、画像、事件、context 注入 | **重点借鉴** | +| [07-letta.md](./07-letta.md) | core blocks、archival memory、sleeptime | 不接入 | +| [08-chatgpt-memory.md](./08-chatgpt-memory.md) | 产品行为、后台 synthesis、用户控制面 | **重点借鉴 UX** | +| [09-evaluation.md](./09-evaluation.md) | 评测边界与阶段 3 最小实验集 | **阶段 3 输入** | + +## 横向比较 + +| 方案 | 主存储形态 | 写入控制者 | 冲突/时间 | 读取路径 | 接入摩擦 | +| --- | --- | --- | --- | --- | --- | +| 自建 Postgres | 关系表 + 可选 vector | 应用管线 | schema + 版本链 | SQL + vector | 低 | +| Mem0 V3 | 向量条目 + links + history | 单次 LLM 抽取 | 新记忆关联旧记忆 | 多信号检索 | 中 | +| Anthropic tool | 客户端文件/自定义存储 | Claude 工具调用 | 文件编辑语义 | 按需 `view` | 中;模型绑定 | +| Graphiti | 实体/事实边/episode 图 | 多阶段 LLM 管线 | `valid_at`/`invalid_at` | 语义 + BM25 + 图 | 高 | +| LangMem | LangGraph BaseStore | agent 工具或 manager | LLM update/delete | store search/注入 | 高;Python | +| Memobase | 画像 + 事件 + buffer | 后台固定工作流 | 画像更新 + timeline | `context()` | 中 | +| Letta | core blocks + archival | 主 agent/后台 agent | block 自编辑 | 常驻 + 检索 | 高;runtime 替换 | +| ChatGPT | 未公开 | 后台 synthesis | 持续重写、时间演化 | 产品内部 | 不可接入 | + +## 研究方法与证据边界 + +- 优先读官方仓库、官方文档与当前项目源码,而不是二手博客。 +- “源码级”表示定位到数据模型、入口函数、关键 prompt 或 tool schema;不把厂商性能数字当作独立证据。 +- 闭源产品只能做行为层分析。`08-chatgpt-memory.md` 明确区分官方披露与推断,不伪造内部实现。 +- 每篇都包含“可复用点 / 不可照搬点 / 对本项目结论”,保证调研能直接进入实验设计。 + +## 阶段 2 验收 + +- [x] 覆盖 PR #2/`01-survey.md` 阶段 2 清单中的代表方案 +- [x] 对快速变化的 Mem0、Letta 做版本修正 +- [x] 给出本项目当前源码的注入、写入和存储挂载点 +- [x] 给出阶段 3 的可执行实验集与成功门槛 +- [x] 更新总入口的阶段状态与阅读导航 diff --git a/docs/memory/README.md b/docs/memory/README.md new file mode 100644 index 0000000..23d0dc0 --- /dev/null +++ b/docs/memory/README.md @@ -0,0 +1,31 @@ +# Chatbot 记忆体系化工程 · 总入口 + +本目录承载「为本项目引入记忆能力」的完整过程:**从零开始 → 全景调研 → 逐方案深入 → 动手实验 → 产品落地 → 经验总结**。目标有两个: + +1. **学习**:搞清楚 Chatbot / Agent 到底是如何实现记忆的(分几层、每层怎么做、社区有哪些代表方案、各自的取舍)。 +2. **落地**:把合适的方案在本项目(Next.js 16 + assistant-ui + AI SDK v7 + Drizzle/Postgres)中实现出来,让用户真实体验到。 + +## 目录结构与阶段 + +| 阶段 | 目录/文件 | 状态 | 说明 | +| --- | --- | --- | --- | +| 0. 总纲 | `README.md`(本文件) | ✅ | 路线图与导航 | +| 1. 全景调研 | `01-survey.md` | ✅ | 记忆分层分类学 + 社区方案全景 + 对比 + 路线图(本阶段产出) | +| 2. 逐方案深入 | [`02-deep-dives/`](./02-deep-dives/) | ✅ | 8 个代表方案 + 评测设计:设计动机、架构、源码级实现细节与本项目判定 | +| 2.5 成品架构研究 | [`research/`](./research/) | ✅ | A 托管服务 / B 混合内核 / C 事件图:共享类型、安全契约、Mermaid 图与落地计划 | +| 3. 实验 | `03-experiments/` | ⬜ | 用最小可复现实验验证关键设计点(抽取质量、检索命中、延迟、成本) | +| 4. 落地实现 | `04-implementation/` | ⬜ | 本项目的记忆功能设计文档、分期实施记录 | +| 5. 总结 | `05-retrospective.md` | ⬜ | 实践结果、踩坑、经验与最佳实践 | + +## 阅读顺序 + +从零开始的读者按 1 → 2 → 3 → 4 → 5 顺序读即可;只关心「本项目怎么做」的读者可先读 [`01-survey.md`](./01-survey.md) 的最后一章(落地路线图),再读[阶段 2 结论](./02-deep-dives/#结论先行)和[自建 Postgres 蓝本](./02-deep-dives/01-self-built-postgres.md)。 + +## 当前进度 + +- [x] 阶段 1:全景调研报告 +- [x] 阶段 2:逐方案深入(自建 Postgres / Mem0 / Anthropic memory tool / Graphiti / LangMem / Memobase / Letta / ChatGPT memory / 评测) +- [x] 成品架构研究:A / B / C 三套可比较落地方案 +- [ ] 阶段 3:关键实验 +- [ ] 阶段 4:分期落地 +- [ ] 阶段 5:总结复盘 diff --git a/docs/memory/research/00-shared-product-model.md b/docs/memory/research/00-shared-product-model.md new file mode 100644 index 0000000..2402f63 --- /dev/null +++ b/docs/memory/research/00-shared-product-model.md @@ -0,0 +1,402 @@ +# 三种方案共用的产品模型与安全契约 + +## 1. 先定义边界,再选择存储 + +Memory 服务、Postgres 和知识图谱回答的是“如何保存与检索”;产品还必须先回答: + +- 什么对象具有权威性; +- 谁可以修改; +- 哪些修改必须确认; +- 冲突时谁覆盖谁; +- 删除后哪些派生数据必须一起消失; +- 模型看到的内容如何与普通指令隔离。 + +以下契约应当独立于 A/B/C。切换后端时,前端控制面、Project 语义和安全规则不应一起重写。 + +## 2. 领域对象 + +```ts +type ID = string +type ISODateTime = string + +interface Project { + id: ID + ownerUserId: ID + title: string + status: "active" | "archived" + createdAt: ISODateTime + updatedAt: ISODateTime +} + +interface ProjectTreeBinding { + projectId: ID + treeId: ID + isPrimary: boolean + createdAt: ISODateTime +} + +interface ProjectGoal { + id: ID + projectId: ID + objective: string + successCriteria: string[] + constraints: string[] + version: number + updatedBy: "user" | "system" + updatedAt: ISODateTime +} +``` + +当前实现可以在创建 tree 时一并创建 Project 和绑定记录。不要继续让 `tree_id` 同时承担 Project 身份,否则未来一 Project 多树时,目标和记忆都需要迁移主键。 + +## 3. 记忆作用域与类型 + +```ts +type MemoryScope = + | { type: "user"; userId: ID } + | { type: "project"; userId: ID; projectId: ID } + +type MemoryKind = + | "preference" + | "instruction" + | "fact" + | "decision" + | "constraint" + | "glossary" + | "open_question" + +type MemoryAuthority = "explicit_user" | "confirmed_user" | "inferred" +type MemoryStatus = "active" | "superseded" | "deleted" +type Sensitivity = "normal" | "sensitive" | "prohibited" + +interface MemorySource { + treeId: ID + threadId: ID + messageId: ID + quote?: string +} + +interface MemoryItem { + id: ID + scope: MemoryScope + kind: MemoryKind + key: string + value: unknown + searchableText: string + authority: MemoryAuthority + sensitivity: Sensitivity + status: MemoryStatus + confidence?: number + source: MemorySource + supersedesId?: ID + validFrom?: ISODateTime + validTo?: ISODateTime + version: number + createdAt: ISODateTime + updatedAt: ISODateTime +} +``` + +`key` 提供确定性覆盖语义。例如: + +```text +user / instruction / output_language +project / decision / memory_architecture +project / glossary / thread +``` + +同一 scope、kind、key 可以有历史版本,但只能有一个 active 当前值。 + +## 4. 候选、确认和拒绝 + +模型不能直接创建 `MemoryItem`。它只能输出候选: + +```ts +type CandidateDecision = + | "auto_accept" + | "needs_confirmation" + | "reject" + +interface MemoryCandidate { + id: ID + proposed: Omit< + MemoryItem, + "id" | "status" | "version" | "createdAt" | "updatedAt" + > + extractedFrom: MemorySource[] + extractorVersion: string + rationaleCode: + | "explicit_remember_request" + | "stable_preference" + | "project_decision" + | "project_constraint" + | "possible_sensitive_data" + | "assistant_only_claim" + | "ephemeral_statement" + decision: CandidateDecision + status: "pending" | "accepted" | "rejected" | "expired" + createdAt: ISODateTime +} +``` + +推荐决策表: + +| 内容 | 默认结果 | +| --- | --- | +| 用户明确说“记住……”且不敏感 | `auto_accept` | +| 普通 Project 决策,有用户原话来源 | `auto_accept` | +| 自动推断的稳定偏好 | `needs_confirmation` 或低权威生效 | +| 用户全局 instruction 的新增/修改 | `needs_confirmation` | +| 健康、财务、身份凭证、精确位置等敏感信息 | `needs_confirmation` 或 `reject` | +| 只有 assistant 说过、用户未确认 | `reject` | +| 寒暄、一次性请求、临时上下文 | `reject` | + +“低权威生效”不能覆盖用户明确设置;它只能在没有更高优先级值时作为建议。 + +## 5. Thread 总结 + +当前领域模型中的稳定节点是 `Thread`,不是 UI 列,也不是单条消息: + +```ts +interface ThreadSummary { + id: ID + projectId: ID + treeId: ID + threadId: ID + title: string + summary: string + keyPoints: string[] + decisions: string[] + openQuestions: string[] + sourceMessageIds: ID[] + inputRevision: string + generation: number + state: "draft" | "locked" | "stale" + editedBy: "ai" | "user" + createdAt: ISODateTime + updatedAt: ISODateTime +} +``` + +规则: + +1. Thread 消息稳定后异步生成; +2. `inputRevision` 是参与总结的消息 ID、内容版本和生成器版本的 hash; +3. 新消息到达后,AI 草稿变成 `stale` 并可自动重算; +4. 用户编辑后设置 `locked`,系统不得静默覆盖; +5. locked 总结有新消息时只提示“有更新可合并”,由用户决定; +6. 总结不是事实本体,不自动写入全局记忆; +7. 从总结发现的决策只能生成 `MemoryCandidate`,继续走确认策略。 + +## 6. 思维导图 + +思维导图是只读派生视图: + +```ts +interface MindMapNode { + id: string + kind: "goal" | "thread" | "decision" | "question" + label: string + sourceRef: { + projectId: ID + treeId?: ID + threadId?: ID + memoryId?: ID + } +} + +interface MindMapEdge { + from: string + to: string + kind: "contains" | "branches_to" | "supports" | "blocks" | "answers" +} + +interface MindMapView { + projectId: ID + sourceRevision: string + nodes: MindMapNode[] + edges: MindMapEdge[] + generatedAt: ISODateTime +} +``` + +输入只包括: + +- Project 目标、成功标准和约束; +- 当前 tree 拓扑; +- Thread 标题与总结; +- 已生效的 Project 决策、约束和 open question。 + +不直接输入所有原始消息,也不读取用户全局偏好作为导图内容。全局偏好只影响导图的语言和展示格式。 + +```mermaid +flowchart TD + Goal["Project Goal"] --> Build["Mind Map Builder"] + Tree["Thread Tree Topology"] --> Build + Summary["Thread Summaries"] --> Build + PM["Active Project Memories"] --> Build + Build --> Validate["验证 sourceRef 与节点数量"] + Validate --> Cache["按 sourceRevision 缓存"] + Cache --> View["只读思维导图"] +``` + +思维导图缓存删除或过期都不会损失信息;任何反向修改都必须通过目标编辑或记忆确认 UI 完成。 + +## 7. 九个产品安全问题如何解决 + +### 7.1 什么内容允许被记住 + +使用 allowlist,而不是让模型自由发明类别。首批允许: + +- 用户全局:语言、输出格式、稳定的交互偏好; +- Project:术语、决策、约束、已确认事实、开放问题; +- 禁止默认收集:密码、API key、支付信息、精确证件、无必要的健康和身份数据。 + +抽取器输出未知 `kind/key` 时直接拒绝,不自动扩展 schema。 + +### 7.2 assistant 幻觉能否写入 + +不能。`source` 必须能追溯到用户消息,或用户对 assistant 产物的明确确认。assistant 生成的计划、总结和 Artifact 都是派生内容,只能产生候选。 + +### 7.3 敏感信息是否需要确认 + +敏感分类先由确定性检测和模型分类双重判断。默认做三挡: + +- normal:按写入策略处理; +- sensitive:明确说明内容和用途后确认; +- prohibited:不创建候选,日志也不得保留明文。 + +### 7.4 用户 ID 如何绑定 + +`userId` 只能从服务端 session 获取,不能接受模型、客户端 body 或 Memory 服务返回值指定。每条 Project 查询必须同时验证 `owner_user_id`。 + +```text +authenticated session + -> resolve ownerUserId + -> authorize projectId + -> derive provider namespace +``` + +### 7.5 什么场景读取记忆 + +不做“每轮全量读取”。Context Compiler 先分类: + +- 全局 instruction:每轮读取少量 active 值; +- Project goal/constraint:Project 对话每轮读取; +- 精确 key:按实体和 key 查; +- 模糊历史:只有当前问题需要回忆时才语义检索; +- Thread summary:只加载当前 lineage 和明确引用的分支。 + +### 7.6 如何安全注入 system prompt + +记忆被当作带来源的数据,不是更高权限指令: + +```xml + + 记忆可能过期;与用户当前消息冲突时,以当前消息为准。 + ... + ... + ... + +``` + +实现要求: + +- 服务端构造,不接受客户端 system role; +- 严格 token/字符预算; +- 对来源、作用域和状态二次过滤; +- 不执行记忆文本中包含的工具调用或“忽略系统规则”等指令; +- 记录本轮实际注入的 memory IDs,支持解释和审计。 + +### 7.7 用户如何查看、编辑和删除 + +控制面至少包括: + +- 全局设置页; +- Project 的 Goal 与 Memory 面板; +- Thread 总结的编辑、锁定、重新生成; +- 每条记忆的来源跳转、状态、最后更新时间; +- 删除单条、清空 Project、关闭自动记忆; +- 当前回答“使用了哪些记忆”的解释入口。 + +编辑必须使用 `version` 做乐观并发控制,不能整对象无条件覆盖。 + +### 7.8 删除后是否真的停止召回 + +删除是跨存储工作流: + +```mermaid +sequenceDiagram + actor U as 用户 + participant API as Memory API + participant DB as Canonical Store + participant IDX as Memory/Vector Index + participant C as Context Cache + U->>API: 删除 memoryId + API->>DB: 标记 deleted + 写 deletion job + DB-->>API: 提交成功 + API-->>U: 已从在线读取路径移除 + API->>IDX: 删除外部/向量条目 + IDX-->>API: 删除确认 + API->>C: 失效相关缓存 + API->>DB: deletion job = verified +``` + +第一笔事务完成后,所有在线查询必须过滤 deleted,因此即使外部索引删除暂时失败,也不能再注入。后台任务持续重试并核验供应商删除结果。 + +### 7.9 错误记忆如何追溯 + +保存: + +- 来源 message IDs 与用户原话片段; +- extractor/model/prompt 版本; +- 候选决策原因; +- 每次接受、编辑、覆盖和删除的 actor; +- 每次回答实际使用的 memory IDs; +- 外部 provider ID 映射和删除状态。 + +审计日志不应该保存 prohibited 敏感明文。 + +## 8. 共用接口 + +```ts +interface MemoryEngine { + propose(input: { + scope: MemoryScope + messages: Array<{ id: ID; role: "user" | "assistant"; text: string }> + }): Promise + + index(items: MemoryItem[]): Promise + + search(input: { + scope: MemoryScope + query: string + kinds?: MemoryKind[] + limit: number + }): Promise> + + remove(memoryIds: ID[]): Promise +} + +interface ContextCompiler { + compile(input: { + userId: ID + projectId: ID + treeId: ID + threadId: ID + userMessage: string + budgetTokens: number + }): Promise +} + +interface CompiledMemoryContext { + globalSettings: MemoryItem[] + projectGoal: ProjectGoal + projectMemories: MemoryItem[] + threadSummaries: ThreadSummary[] + usedMemoryIds: ID[] + renderedText: string +} +``` + +方案差异主要体现在 `MemoryEngine` 背后和 `MemoryItem` 的真相来源,不应泄漏到聊天 UI。 diff --git a/docs/memory/research/01-option-a-managed-memory.md b/docs/memory/research/01-option-a-managed-memory.md new file mode 100644 index 0000000..95cde94 --- /dev/null +++ b/docs/memory/research/01-option-a-managed-memory.md @@ -0,0 +1,438 @@ +# 方案 A:以成熟 Memory 服务为主要记忆后端 + +## 1. 方案定义 + +方案 A 选择 Mem0 一类成熟 Memory 服务/库作为长期记忆的主要存储和检索系统。应用自己的 Postgres 仍然保存 Project、目标、tree 绑定和 Thread 总结,因为这些对象不是通用 Memory API 可以替代的。 + +```mermaid +flowchart LR + UI["Thread Chat UI"] --> API["Next.js API"] + API --> PG["App Postgres
Project / Goal / Tree / Summary"] + API --> MEM["Managed Memory
Mem0 Platform / self-hosted"] + MEM --> V["Memory records / vector / history"] + API --> CC["Context Compiler"] + PG --> CC + MEM --> CC + CC --> LLM["Chat Model"] +``` + +这里的“服务为主”指: + +- 用户全局偏好和 Project 事实/决策以 provider memory ID 为主要记录; +- 应用只保存 provider 映射、审计和必要的本地投影; +- 检索排序、相似度、记忆压缩和供应商内部更新语义由 provider 决定。 + +## 2. Mem0 的位置与当前实现语义 + +Mem0 提供 `user_id`、`agent_id`、`run_id` 作用域,以及 add/search/update/delete 一类 API。根据本项目阶段 2 使用的源码快照: + +[`mem0ai/mem0@d6d89c987bddf580870db14c69db974edfc5263c`](https://github.com/mem0ai/mem0/tree/d6d89c987bddf580870db14c69db974edfc5263c) + +OSS 主路径已经从旧的 ADD/UPDATE/DELETE/NONE 管线转为 V3 ADD-only + memory linking。完整源码笔记见[Mem0 深入](../02-deep-dives/02-mem0.md)。 + +### Mem0 V3 写入图 + +```mermaid +flowchart TD + M["新消息"] --> H["读取同 session 最近消息"] + H --> Q["对输入生成 query embedding"] + Q --> S["在 user / agent / run scope
检索 top memories"] + S --> IDs["真实 UUID 映射为短 ID"] + IDs --> E["LLM additive extraction"] + E --> N{"有新增记忆?"} + N -- 否 --> Done["只保存消息历史"] + N -- 是 --> BE["批量生成 embedding"] + BE --> D["文本 hash 精确去重"] + D --> W["写 memory + history"] + W --> L["建立 linked_memory_ids / entity links"] + L --> Done +``` + +它适合作为高召回的语义记忆层,但 ADD-only 意味着“杭州 → 新加坡”可能同时存在。若产品必须稳定返回当前值,应用仍需: + +- 在 metadata 中保存 key、authority、valid time; +- 或维护本地当前值投影; +- 或在读取时增加确定性 reducer。 + +这也是方案 A 最重要的边界:即使采用成熟服务,Project 产品语义仍然不能全部外包。 + +## 3. 作用域映射 + +推荐把 provider namespace 视为服务端派生值: + +```ts +interface ProviderScope { + userId: string + agentId: string + runId?: string +} + +function toProviderScope( + scope: + | { type: "user"; userId: string } + | { type: "project"; userId: string; projectId: string } +): ProviderScope { + return scope.type === "user" + ? { + userId: scope.userId, + agentId: "global-preferences", + } + : { + userId: scope.userId, + agentId: `project:${scope.projectId}`, + } +} +``` + +不能让客户端直接传 `agentId`。服务端先验证 Project owner,再生成 provider scope。 + +Thread ID 不建议直接作为一级 provider namespace。Thread 总结本身已经是派生知识;如果每个 Thread 都建独立记忆空间,跨分支检索会变得困难。可以把 `treeId/threadId/messageId` 放进 metadata,供过滤、来源跳转和删除。 + +## 4. 本地数据模型 + +即使 provider 是主要记忆后端,本地仍需要以下表: + +```ts +interface ManagedMemoryRef { + id: string + ownerUserId: string + projectId?: string + provider: "mem0" + providerMemoryId: string + kind: + | "preference" + | "instruction" + | "fact" + | "decision" + | "constraint" + | "glossary" + | "open_question" + key: string + authority: "explicit_user" | "confirmed_user" | "inferred" + sensitivity: "normal" | "sensitive" + sourceTreeId: string + sourceThreadId: string + sourceMessageId: string + providerState: "active" | "delete_pending" | "deleted" | "error" + lastSyncedAt: string + createdAt: string + updatedAt: string +} +``` + +另外还要有: + +- `projects` +- `project_tree_bindings` +- `project_goals` +- `thread_summaries` +- `memory_candidates` +- `memory_audit_events` +- `memory_provider_jobs` + +本地 ref 不是完整真相副本,但必须足够完成: + +- 身份授权; +- 来源展示; +- provider 删除失败时阻止在线召回; +- 查询某 Project 有哪些 provider IDs; +- 迁移供应商。 + +## 5. Provider Adapter + +应用不能在 route 中直接散落 Mem0 SDK 调用: + +```ts +interface ManagedMemoryProvider { + add(input: { + scope: ProviderScope + messages: Array<{ + id: string + role: "user" | "assistant" + content: string + }> + metadata: Record + infer: boolean + }): Promise> + + search(input: { + scope: ProviderScope + query: string + limit: number + filters?: Record + }): Promise< + Array<{ + providerMemoryId: string + text: string + score: number + metadata: Record + }> + > + + update(input: { + providerMemoryId: string + text: string + metadata: Record + }): Promise + + remove(providerMemoryId: string): Promise + + removeScope(scope: ProviderScope): Promise +} +``` + +生产实现还需要: + +- timeout 和 retry; +- 限流与熔断; +- provider request ID 日志; +- SDK/API 版本固定; +- 请求和响应 schema 验证; +- 内容长度限制; +- 禁止把 API key 或 prohibited 内容发给 provider。 + +## 6. 写入流程 + +成熟产品不应该在聊天响应前同步等待 Memory 服务。 + +```mermaid +sequenceDiagram + actor U as 用户 + participant CHAT as Chat API + participant DB as App Postgres + participant Q as Durable Worker + participant P as Policy Engine + participant M as Mem0 + + U->>CHAT: 发送消息 + CHAT->>DB: 保存消息 + memory extraction job + CHAT-->>U: 流式回答 + Q->>DB: 领取 job(幂等键) + Q->>M: add(messages, provider scope) + M-->>Q: extracted memories + Q->>P: 分类 kind/key/authority/sensitivity + P-->>Q: accept / confirm / reject + alt 自动生效 + Q->>DB: 保存 ManagedMemoryRef + audit + else 需要确认 + Q->>DB: 保存 MemoryCandidate + DB-->>U: 控制面显示待确认 + else 拒绝 + Q->>DB: 只保存不含敏感明文的拒绝原因 + end +``` + +这里存在一个现实问题:Mem0 可能已经写入内容,应用 Policy Engine 才决定拒绝。为避免 prohibited 内容短暂进入外部服务,有两种实现: + +1. **先本地预分类,再调用 Mem0 infer**:推荐;敏感/禁止内容在出站前拦截; +2. 调用自有抽取器生成候选,确认后用 `infer=false` 写 Mem0:控制更强,但方案已经向 B 靠近。 + +方案 A 若要保持安全,至少必须有出站前的 deterministic + model risk classifier。 + +## 7. 读取流程 + +```mermaid +sequenceDiagram + actor U as 用户 + participant API as Chat API + participant PG as App Postgres + participant M as Mem0 + participant C as Context Compiler + participant L as Chat Model + + U->>API: Project 内提问 + API->>PG: 校验 owner + 读 goal/summary/settings + API->>M: search(user/project scope, query) + M-->>API: provider memories + API->>PG: 过滤本地 delete_pending/deleted refs + API->>C: 合并、去重、优先级和预算 + C-->>API: rendered memory context + used IDs + API->>L: system + messages + L-->>U: 流式回答 + API->>PG: 记录本轮 used memory IDs +``` + +### Provider 故障降级 + +如果 Mem0 search 超时: + +- 聊天继续成功; +- 仍注入本地全局设置、Project 目标和 Thread 总结; +- 不使用过期的任意 provider cache,除非 cache 中保存了 scope 和删除 epoch; +- 记录 degraded 状态,但不向用户伪装“已使用完整记忆”。 + +## 8. 冲突处理 + +方案 A 有三个可选层级: + +### A1. 完全相信 provider + +优点是简单,缺点是无法保证当前值。只适合开放式回忆,不适合 Project instruction 和关键决策。 + +### A2. Provider memory + metadata key + +应用为每条返回结果补充 `key/valid_from/status`,查询后按 key 选择最新有效值。需要本地 ref 或 provider 支持强 metadata filtering。 + +### A3. Provider 保存全文,本地保存 current projection + +所有语义历史在 Mem0;本地表保存每个 scope/kind/key 的当前 provider ID。这是方案 A 中最成熟的实现,但已经引入小型关系型真相层。 + +建议至少采用 A3,否则无法可靠实现全局输出语言和 Project 覆盖规则。 + +## 9. 用户编辑 + +用户编辑“输出语言 = 中文”时,不应该只更新一段自然语言: + +```mermaid +sequenceDiagram + actor U as 用户 + participant API as Settings API + participant DB as App Postgres + participant M as Mem0 + + U->>API: 更新 output_language,expectedVersion=3 + API->>DB: 校验 version,写本地 current projection + DB-->>API: version=4 + provider sync job + API-->>U: 更新成功 + API->>M: update/add provider memory + M-->>API: provider ID + API->>DB: 更新 ref;旧 ID 标为 delete_pending + API->>M: 删除旧 provider memory +``` + +用户体验不能被 provider 同步阻塞。本地 projection 已更新后,Context Compiler 应立即使用新值,同时后台完成 provider 对齐。 + +## 10. 删除语义 + +```mermaid +stateDiagram-v2 + [*] --> active + active --> delete_pending: 用户删除/清空 + delete_pending --> deleted: provider 删除已核验 + delete_pending --> delete_pending: 重试失败 + deleted --> [*] +``` + +规则: + +1. 本地 ref 进入 `delete_pending` 后立即从读取路径排除; +2. provider 删除使用持久 job 重试; +3. 清空 Project 时按本地 ref 枚举并删除,不能只依赖一次不透明的 provider `delete_all`; +4. provider 返回成功后再次 search 验证,或使用官方删除状态接口; +5. 相关 Context cache 和思维导图 cache 一并失效; +6. 原始聊天消息是否删除是独立产品操作,不能暗中级联。 + +## 11. Thread 总结和思维导图 + +这两部分不交给 Mem0: + +```mermaid +flowchart TD + Msg["Thread messages"] --> Sum["Summary Worker"] + Sum --> TS["App Postgres: ThreadSummary"] + TS --> MM["Mind Map Builder"] + Goal["Project Goal"] --> MM + Tree["Tree topology"] --> MM + PM["Mem0 project memories
经本地 ref 过滤"] --> MM + MM --> View["Read-only Mind Map"] +``` + +原因: + +- 总结的锁定、stale 和用户编辑状态是本产品语义; +- 思维导图必须保留 `threadId/sourceRef`; +- provider 的相似检索不能替代真实 tree 拓扑。 + +## 12. 建议模块边界 + +```text +lib/memory/ + contracts.ts + policy.ts + context-compiler.ts + provider.ts + providers/mem0.ts + provider-scope.ts + audit.ts + +lib/projects/ + repository.ts + goal.ts + tree-bindings.ts + thread-summary.ts + mind-map.ts + +app/api/ + projects/[projectId]/goal/route.ts + projects/[projectId]/memories/route.ts + projects/[projectId]/mind-map/route.ts + threads/[threadId]/summary/route.ts + user/memory-settings/route.ts +``` + +数据库写入、provider 调用和 UI 类型不要共享一个巨大 `memory.ts` 文件。 + +## 13. 分阶段落地 + +### A-1:Project 身份与权威配置 + +- 新增 `projects` 和 `project_tree_bindings`; +- 新增 `project_goals`; +- 用户全局 instruction 和 Project override 先保存在本地; +- Context Compiler 先只处理确定性设置和目标。 + +### A-2:接入 provider + +- 实现 `ManagedMemoryProvider`; +- 服务端作用域映射; +- durable extraction/sync jobs; +- 本地 provider refs 与审计; +- 超时降级。 + +### A-3:控制面与删除核验 + +- 全局设置和 Project Memory 面板; +- 候选确认; +- 来源跳转; +- 删除状态和清空; +- provider deletion reconciliation。 + +### A-4:Thread 总结和思维导图 + +- Thread summary revision/lock; +- 只读 mind map; +- cache key 使用 goal、tree、summary、memory revisions。 + +### A-5:供应商退出演练 + +- 导出所有 provider memories; +- 用本地 ref 校验数量和 scope; +- 导入替代 provider; +- 随机抽样验证删除项没有复活; +- 在 staging 完成一次真实切换。 + +没有退出演练,“provider 可替换”只是文档声明。 + +## 14. 适用与不适用 + +选择 A 的前提: + +- 供应商在本项目 fixture 上通过准确性和删除测试; +- 可以接受数据处理和成本模型; +- provider SLA 满足要求; +- 团队愿意让语义检索和记忆演进部分依赖供应商; +- 已完成出口和删除核验。 + +不应选择 A 的信号: + +- 大量记忆必须作为确定性的业务状态; +- provider ADD-only/更新语义造成当前值错误; +- 必须对每次写入提供完整本地证据链; +- 敏感数据不能发送到外部服务; +- provider 故障会让产品核心工作流不可用。 + +## 15. 结论 + +方案 A 并不是“接一个 SDK 就完成记忆”。成熟版本仍需本地 Project 模型、Policy Engine、provider ref、审计、删除 job、Context Compiler、Thread 总结和思维导图。 + +它真正节省的是通用的抽取、Embedding、索引和语义检索基础设施。若这些能力在统一评测中稳定可靠,而且供应商边界可接受,A 是合理的产品选择;否则它会变成一套难以调试的第二真相来源。 diff --git a/docs/memory/research/02-option-b-hybrid-kernel.md b/docs/memory/research/02-option-b-hybrid-kernel.md new file mode 100644 index 0000000..0222978 --- /dev/null +++ b/docs/memory/research/02-option-b-hybrid-kernel.md @@ -0,0 +1,766 @@ +# 方案 B:关系型记忆内核 + 可插拔 Memory 引擎 + +## 1. 方案定义 + +方案 B 把产品必须准确理解的内容保存在本地 Postgres,把通用的候选抽取、Embedding、语义召回和聚类放在可替换的 `MemoryEngine` 后面。 + +```mermaid +flowchart TB + UI["Thread Chat / Settings / Project UI"] --> API["Next.js API"] + API --> Kernel["Memory Kernel"] + Kernel --> PG["Postgres Canonical Store"] + Kernel --> Policy["Policy + Deterministic Reducer"] + Kernel --> Engine["Pluggable Memory Engine"] + Engine --> Hosted["Managed Service
可选"] + Engine --> Local["Local Extractor / pgvector
可选"] + PG --> Compiler["Context Compiler"] + Engine --> Compiler + Compiler --> Model["Chat Model"] +``` + +权威状态包括: + +- 用户全局设置; +- Project 目标; +- 已确认的 Project 事实、决策、约束和术语; +- 事实的当前版本、来源、确认和删除状态; +- Thread 总结及其锁定状态。 + +Memory Engine 的结果只能是候选或索引命中,不能直接覆盖权威状态。 + +## 2. 为什么称为“内核” + +Memory Kernel 是一组稳定的领域规则,而不是某个 LLM prompt: + +```ts +interface MemoryKernel { + propose(input: ExtractionInput): Promise + decide(candidate: MemoryCandidate): PolicyDecision + apply(command: MemoryCommand): Promise + delete(command: DeleteMemoryCommand): Promise + compileContext(input: CompileContextInput): Promise +} +``` + +它保证: + +1. 所有作用域都从服务端身份派生; +2. 模型输出必须通过 schema; +3. 同一 scope/kind/key 只有一个 active 当前值; +4. 高权威值不被低权威候选覆盖; +5. 删除项不会因外部索引延迟再次出现; +6. 每次状态变化都有来源和审计。 + +这些规则不依赖使用 Mem0、其他服务还是本地模型。 + +## 3. 数据模型 + +### 3.1 Project 与 tree + +```text +projects + id + owner_user_id + title + status + created_at + updated_at + +project_tree_bindings + project_id + tree_id + is_primary + created_at + +project_goals + id + project_id + objective + success_criteria_json + constraints_json + version + updated_by + updated_at +``` + +现有 `branch_trees` 继续保存完整 `ThreadTreeState`。第一阶段一棵 tree 创建一个 Project;未来新增 tree 只增加 binding,不迁移目标和记忆。 + +### 3.2 权威记忆 + +```text +memory_items + id + owner_user_id + scope_type user | project + project_id nullable + kind + key + value_json + searchable_text + authority + sensitivity + status + confidence + source_tree_id + source_thread_id + source_message_id + source_quote + supersedes_id + valid_from + valid_to + version + created_at + updated_at + deleted_at +``` + +约束: + +```text +scope_type = user => project_id IS NULL +scope_type = project => project_id IS NOT NULL +``` + +active current-value 唯一性用 partial unique index 表达: + +```sql +unique ( + owner_user_id, + scope_type, + coalesce(project_id, ''), + kind, + key +) +where status = 'active' +``` + +具体迁移需要按 PostgreSQL/Drizzle 支持方式实现;这里表达的是数据库不变量,不是可直接执行的最终 migration。 + +### 3.3 候选、事件和索引 + +```text +memory_candidates + id + owner_user_id + project_id + proposed_json + source_message_ids_json + extractor_version + rationale_code + policy_decision + status + expires_at + created_at + +memory_audit_events + id + memory_id + action proposed | accepted | edited | superseded | deleted + actor_type user | system | worker + actor_id + before_json + after_json + source_message_id + created_at + +memory_index_entries + memory_id + provider + external_id + embedding_model + index_version + status pending | ready | delete_pending | deleted | error + last_error + updated_at +``` + +### 3.4 Thread 总结 + +```text +thread_summaries + id + project_id + tree_id + thread_id + title + summary + key_points_json + decisions_json + open_questions_json + source_message_ids_json + input_revision + generation + state draft | locked | stale + edited_by + created_at + updated_at +``` + +`(project_id, tree_id, thread_id)` 唯一。 + +## 4. 写入架构 + +### 4.1 消息进入系统 + +聊天成功与记忆写入解耦: + +```mermaid +sequenceDiagram + actor U as 用户 + participant CHAT as Chat API + participant DB as Postgres + participant W as Memory Worker + participant E as Memory Engine + participant P as Policy Engine + participant R as Deterministic Reducer + + U->>CHAT: 发送消息 + CHAT->>DB: 保存消息 + outbox event + CHAT-->>U: 流式回答 + W->>DB: 领取未处理 event + W->>E: propose(messages, scope) + E-->>W: schema-valid candidates + W->>P: classify risk + authority + action + P-->>W: accept / confirm / reject + alt 自动接受 + W->>R: apply candidate + R->>DB: 事务写 active/superseded + audit + index job + else 等待确认 + W->>DB: 保存 pending candidate + else 拒绝 + W->>DB: 保存最小拒绝审计 + end +``` + +### 4.2 Outbox + +不要依赖 Next.js 请求结束后的内存任务作为生产队列。聊天事务内同时写: + +```ts +interface MemoryOutboxEvent { + id: string + type: + | "messages_appended" + | "memory_index_requested" + | "memory_delete_requested" + | "thread_summary_requested" + | "mind_map_invalidated" + aggregateId: string + payload: unknown + idempotencyKey: string + attempts: number + availableAt: string + processedAt?: string +} +``` + +`idempotencyKey` 示例: + +```text +extract:{projectId}:{threadId}:{lastMessageId}:{extractorVersion} +index:{memoryId}:{version}:{indexVersion} +summary:{threadId}:{inputRevision}:{summaryVersion} +delete-index:{memoryId}:{indexVersion} +``` + +Worker 可以由独立进程、平台队列或定时领取器实现;领域层只依赖持久 outbox 契约。 + +## 5. 确定性 Reducer + +LLM 只提出候选,Reducer 决定状态: + +```ts +type ReduceAction = + | { type: "ADD" } + | { type: "SUPERSEDE"; currentMemoryId: string } + | { type: "IGNORE"; reason: string } + | { type: "REQUIRE_CONFIRMATION"; reason: string } + +function decideMutation( + current: MemoryItem | null, + candidate: MemoryCandidate +): ReduceAction { + // 伪代码:最终规则需要单元测试覆盖 + if (candidate.proposed.sensitivity === "prohibited") { + return { type: "IGNORE", reason: "prohibited" } + } + if (candidate.proposed.authority === "inferred" && current?.authority) { + return { + type: "REQUIRE_CONFIRMATION", + reason: "inferred-cannot-overwrite-authoritative", + } + } + if (!current) return { type: "ADD" } + if (deepEqual(current.value, candidate.proposed.value)) { + return { type: "IGNORE", reason: "same-current-value" } + } + return { type: "SUPERSEDE", currentMemoryId: current.id } +} +``` + +事务更新: + +```mermaid +flowchart TD + C["候选"] --> Load["SELECT active by scope/kind/key FOR UPDATE"] + Load --> Same{"值相同?"} + Same -- 是 --> Ignore["记录 ignore audit"] + Same -- 否 --> Auth{"允许覆盖当前 authority?"} + Auth -- 否 --> Confirm["等待用户确认"] + Auth -- 是 --> Old["旧项 status=superseded
valid_to=now"] + Old --> New["插入新 active
supersedes_id=old.id"] + New --> Audit["写 audit + outbox"] +``` + +## 6. 全局设置与 Project override + +指令类记忆不走语义 top-k,而是精确读取: + +```ts +interface EffectiveSetting { + key: string + value: T + source: "current_turn" | "project" | "user_global" | "inferred" + memoryId?: string +} +``` + +解析算法: + +```text +1. 当前消息是否明确设置本轮要求; +2. Project scope 是否有 active instruction/key; +3. user scope 是否有 active instruction/key; +4. 是否存在 inferred preference; +5. 否则使用产品默认值。 +``` + +示例: + +```text +全局:output_language = zh-CN +Project:deliverable_language = en-US +当前消息:这次请先用中文解释 + +聊天解释:中文 +Project 最终交付物:英文 +``` + +这里需要区分 `output_language` 和 `deliverable_language`,不能让模型把自然语言冲突临场解释成同一个 key。 + +## 7. Project 目标 + +Project Goal 是独立权威对象,不作为普通向量记忆: + +```mermaid +sequenceDiagram + actor U as 用户 + participant UI as Goal Editor + participant API as Project API + participant DB as Postgres + participant INV as Invalidation Worker + + U->>UI: 编辑目标/成功标准/约束 + UI->>API: PUT expectedVersion + API->>DB: UPDATE WHERE version=expectedVersion + alt 版本匹配 + DB-->>API: new version + API->>INV: 失效 context/mind-map cache + API-->>U: 保存成功 + else 版本冲突 + DB-->>API: current goal + API-->>U: 显示冲突,禁止静默覆盖 + end +``` + +AI 可以调用 `proposeGoalChange`,但工具结果只能创建候选,不直接修改 Goal。 + +## 8. Thread 总结 + +### 8.1 生成触发 + +建议触发条件: + +- assistant 回复完成且 Thread 有新增稳定消息; +- Thread 一段时间无新消息; +- 用户切出 Thread; +- 用户主动点击重新生成。 + +不要每个 token 或每条短消息都重算。 + +```mermaid +sequenceDiagram + participant CHAT as Chat Store + participant O as Outbox + participant S as Summary Worker + participant DB as Postgres + participant UI as Thread UI + + CHAT->>O: thread_summary_requested(inputRevision) + S->>O: 领取最新 revision + S->>DB: 读取 Thread 消息 + 继承边界 + S->>S: 生成 summary/key points/decisions/questions + S->>DB: compare inputRevision + alt 当前是 AI draft 且 revision 未变化 + S->>DB: upsert draft summary + DB-->>UI: 展示新总结 + else 用户已 locked + S->>DB: 保存 update suggestion,不覆盖正文 + else revision 已过期 + S->>O: 放弃并让更新事件重试 + end +``` + +### 8.2 锁定规则 + +| 当前状态 | 新消息到达 | Worker 行为 | +| --- | --- | --- | +| draft | 是 | 标 stale,重算后替换 | +| stale | 是 | 合并到最新 revision,旧 job 放弃 | +| locked | 是 | 不覆盖;生成“有更新”提示或候选 diff | +| locked | 用户点重新生成 | 创建新 draft preview,确认后替换 | + +### 8.3 总结与记忆的关系 + +总结中的 `decisions` 不是正式 Project 决策。Summary Worker 可以发出候选: + +```text +ThreadSummary.decisions + -> MemoryCandidate(kind=decision, scope=project) + -> policy/confirmation + -> active Project memory +``` + +这条单向边界可以防止总结幻觉污染长期行为。 + +## 9. 读取与 Context Compiler + +```mermaid +flowchart TD + Req["userId/projectId/treeId/threadId/query"] --> Auth["Auth + Project ownership"] + Auth --> Global["精确读全局 settings"] + Auth --> Goal["读 Project goal"] + Auth --> Project["精确读 Project active memories"] + Auth --> Lineage["读当前 Thread lineage summaries"] + Req --> Need{"需要模糊历史?"} + Need -- 是 --> Search["MemoryEngine.search"] + Need -- 否 --> Merge + Global --> Merge["优先级 + 去重 + 来源 + token budget"] + Goal --> Merge + Project --> Merge + Lineage --> Merge + Search --> Merge + Merge --> Render["结构化 memory_context"] + Render --> Prompt["server-owned system"] +``` + +### 9.1 是否需要语义检索 + +第一版可以使用规则 + 小模型分类: + +- “我们之前为什么……”:需要; +- “另一个分支讨论了什么……”:需要; +- “按照项目约束继续……”:只需精确 Project memory; +- 普通概念问题:可能不需要; +- 当前 Thread 已包含全部相关消息:不需要重复召回。 + +分类失败时可以少召回,不应默认把全 Project 历史塞满 prompt。 + +### 9.2 注入预算 + +示例预算不是固定最终值: + +```text +Project goal + hard constraints 20% +Effective settings 10% +Current lineage summaries 30% +Exact project memories 20% +Semantic long-tail recall 20% +``` + +某类为空时预算可以让给其他类。每条内容必须保留 ID/source,便于回答后解释。 + +## 10. Memory Engine 的可插拔位置 + +```ts +interface MemoryEngine { + propose(input: { + scope: MemoryScope + messages: Array<{ id: string; role: "user" | "assistant"; text: string }> + }): Promise + + index(items: MemoryItem[]): Promise + + search(input: { + scope: MemoryScope + query: string + kinds?: MemoryKind[] + limit: number + }): Promise> + + remove(memoryIds: string[]): Promise +} +``` + +可以有三种实现: + +```text +LocalStructuredEngine + propose: 自有 schema extraction + index/search: pgvector + +Mem0Engine + propose/search: Mem0 + index: 写 provider,并保留 canonical memoryId metadata + +NoSemanticEngine + propose: 规则或模型 + search: 空结果 +``` + +聊天和 UI 只依赖 `MemoryKernel`,不直接知道 provider。 + +## 11. 删除与一致性 + +Canonical Store 是读取闸门: + +```mermaid +sequenceDiagram + actor U as 用户 + participant API as Memory API + participant DB as Canonical Store + participant W as Index Worker + participant E as Memory Engine + participant Cache as Context/MindMap Cache + + U->>API: 删除 memoryId, expectedVersion + API->>DB: 事务标 deleted + audit + outbox + DB-->>API: committed + API-->>U: 删除成功 + Note over API,DB: 从此所有读取立即过滤 deleted + W->>E: remove(memoryId) + alt 成功 + E-->>W: removed + W->>DB: index status=deleted + else 失败 + E-->>W: error + W->>DB: retry with backoff + end + W->>Cache: invalidate revisions +``` + +如果索引返回已删除 ID,Context Compiler 必须回查 canonical active 状态并丢弃。这条防线也处理索引延迟和 provider 数据复活。 + +## 12. 思维导图 + +思维导图不调用 Memory Engine 自由生成结构。先由应用构造受限输入,再允许模型补充边标签: + +```mermaid +flowchart LR + G["Goal + criteria + constraints"] --> D["Deterministic Draft"] + T["Thread tree topology"] --> D + S["Active/locked summaries"] --> D + M["Active project decisions/questions"] --> D + D --> L["可选 LLM:压缩 label / 判断 supports/blocks"] + L --> V["Schema validation"] + V --> R["Read-only MindMapView"] +``` + +确定性草稿保证: + +- 每个 Thread 至少一个 node; +- `branches_to` 来自真实 parent/children; +- 所有 node 都有 `sourceRef`; +- 模型不能生成不存在的 threadId/memoryId; +- 超过节点上限时按 subtree 做分层折叠。 + +缓存键: + +```text +hash( + goal.version, + tree.updatedAt, + threadSummary generations, + projectMemory revision +) +``` + +## 13. 建议 API + +```text +POST /api/projects +GET /api/projects/:projectId +PUT /api/projects/:projectId/goal + +GET /api/user/memories +POST /api/user/memories +PATCH /api/user/memories/:memoryId +DELETE /api/user/memories/:memoryId + +GET /api/projects/:projectId/memories +POST /api/projects/:projectId/memories +PATCH /api/projects/:projectId/memories/:memoryId +DELETE /api/projects/:projectId/memories/:memoryId + +GET /api/projects/:projectId/memory-candidates +POST /api/memory-candidates/:candidateId/accept +POST /api/memory-candidates/:candidateId/reject + +GET /api/projects/:projectId/threads/:threadId/summary +PATCH /api/projects/:projectId/threads/:threadId/summary +POST /api/projects/:projectId/threads/:threadId/summary/regenerate + +GET /api/projects/:projectId/mind-map +``` + +所有 project route 都从 session 取 userId 并验证 owner,不能只按不可猜 UUID 判断授权。 + +## 14. 建议代码边界 + +```text +lib/memory/ + contracts.ts + repository.ts + policy.ts + reducer.ts + context-compiler.ts + prompt-renderer.ts + audit.ts + outbox.ts + engines/ + engine.ts + local.ts + mem0.ts + +lib/projects/ + repository.ts + goals.ts + tree-bindings.ts + thread-summaries.ts + mind-map.ts + +app/api/projects/ +app/api/user/memories/ +app/api/memory-candidates/ + +constants/ + memory.ts + memory-policy.ts +``` + +`constants/` 中保存 token 预算、允许 kind、敏感策略和重试上限;不要把 magic strings 分散到 route 和 worker。 + +## 15. 分阶段落地 + +### B-1:Project 身份与 Context Compiler 骨架 + +- 新增 Project 和 tree binding; +- tree 创建时自动创建 Project; +- 目标编辑和版本控制; +- 全局 output language 与 Project override; +- chat request 带 `projectId/treeId/threadId`,服务端重新授权; +- 编译并记录 `CompiledMemoryContext`,暂不自动抽取。 + +### B-2:权威记忆 CRUD + +- memory_items 和 audit; +- Project Memory 控制面; +- 来源跳转; +- optimistic concurrency; +- 删除与清空; +- 只允许用户显式创建。 + +### B-3:候选抽取与 Reducer + +- outbox/worker; +- schema extraction; +- policy table; +- auto/confirm/reject; +- current-value supersede; +- 失败重试和幂等。 + +### B-4:Thread 总结 + +- revision hash; +- draft/stale/locked; +- 用户编辑; +- decision candidates; +- lineage summary 注入。 + +### B-5:语义引擎 + +- 实现统一 `MemoryEngine`; +- 先接一个 provider 或本地 index; +- canonical recheck; +- semantic recall gating; +- provider failure 降级; +- 删除 reconciliation。 + +### B-6:只读思维导图 + +- deterministic tree nodes; +- LLM 可选压缩; +- schema validation; +- revision cache; +- sourceRef 跳转。 + +### B-7:产品化 + +- 自动记忆开关与临时聊天; +- 敏感确认; +- “本次使用的记忆”解释; +- 数据导出和清空; +- 监控抽取误报、删除积压和 provider 错误; +- 回归 fixture 进入 CI。 + +## 16. 运维指标 + +```text +memory_candidate_accept_rate +memory_false_write_rate +memory_supersede_conflict_rate +memory_extraction_job_lag +memory_index_job_lag +memory_index_delete_backlog +memory_context_compile_p50/p95 +memory_context_tokens +thread_summary_stale_count +thread_summary_locked_update_count +provider_error_rate +``` + +安全告警: + +- tenant scope mismatch 必须为 0; +- deleted memory 被 Context Compiler 选中必须为 0; +- prohibited 内容出站必须为 0; +- assistant-only claim 自动生效必须为 0。 + +## 17. 优点与代价 + +优点: + +- 产品真相、删除和优先级可验证; +- Memory 服务可以替换或移除; +- 全局设置、目标、总结和思维导图具有清晰边界; +- 外部服务故障不会破坏权威状态; +- 可以逐步演进到事件图,而不推翻 API。 + +代价: + +- 需要维护 schema、Reducer、outbox 和控制面; +- provider 与 canonical store 存在索引一致性工作; +- 团队必须持续维护记忆 taxonomy; +- 抽取质量仍需要自己的 fixture 和观测。 + +## 18. 结论 + +方案 B 的核心不是“自己实现一切”,而是明确区分: + +```text +产品必须准确拥有的状态 -> 自己保存 +通用的语义能力 -> 成熟引擎 +可重新生成的展示 -> 派生视图 +``` + +对于 Project 驱动、需要全局设置覆盖、目标、Thread 总结和用户控制面的成熟产品,这是当前最平衡的方案。 diff --git a/docs/memory/research/03-option-c-event-graph.md b/docs/memory/research/03-option-c-event-graph.md new file mode 100644 index 0000000..7af09ce --- /dev/null +++ b/docs/memory/research/03-option-c-event-graph.md @@ -0,0 +1,648 @@ +# 方案 C:事件溯源 + 时序知识图谱 + +## 1. 方案定义 + +方案 C 不直接把“当前记忆”作为唯一真相,而是把所有有意义的变化先写入不可变事件日志,再投影出: + +- 当前用户设置; +- 当前 Project 目标; +- 当前 Project 事实和决策; +- 时间版本; +- Thread 总结; +- 实体和关系图。 + +```mermaid +flowchart TB + CMD["Commands
用户操作 / AI 候选 / 系统任务"] --> ES["Append-only Event Store"] + ES --> P1["Current State Projection"] + ES --> P2["Audit Timeline Projection"] + ES --> P3["Temporal Knowledge Graph"] + ES --> P4["Search Index"] + P1 --> CC["Context Compiler"] + P3 --> CC + P4 --> CC + CC --> LLM["Chat Model"] + P1 --> UI["Settings / Goal / Memory UI"] + P2 --> UI + P3 --> MM["Mind Map / Relationship View"] +``` + +事件是事实记录;投影可以删除后重建。图不是聊天消息的简单向量化,而是从事件中形成有时间语义的 entity/relation。 + +## 2. 适用问题 + +方案 C 擅长回答: + +- 某个 Project 决策经历了哪些变化? +- 这个结论基于哪些分支和证据? +- 哪些约束阻塞了哪些目标? +- 用户偏好从何时开始生效? +- 某次回答在当时看到了哪些版本的记忆? +- 删除、编辑或修正是由谁、在什么时间发生的? + +如果产品只需要“默认用中文”和“记住当前项目”,事件图会明显过度设计。只有关系、时间和可追溯推理成为核心产品能力时,复杂度才有回报。 + +## 3. 三层真相 + +```text +Level 1: Event Store + 不可变的业务事件,是最终审计真相 + +Level 2: Projections + 为读取优化的当前状态、历史、搜索和图 + +Level 3: Derived Views + Prompt context、Thread 总结、思维导图 +``` + +删除是特殊情况:审计事件可以保留“发生过删除”,但 prohibited 或隐私删除的原文必须从 payload、投影和索引中物理清除,事件只留下不含内容的 tombstone。 + +## 4. 事件模型 + +```ts +type MemoryEventType = + | "project_created" + | "tree_bound_to_project" + | "project_goal_set" + | "project_goal_revised" + | "memory_candidate_proposed" + | "memory_candidate_accepted" + | "memory_candidate_rejected" + | "memory_added" + | "memory_superseded" + | "memory_deleted" + | "thread_summary_generated" + | "thread_summary_edited" + | "thread_summary_locked" + | "thread_summary_marked_stale" + +interface DomainEvent { + eventId: string + eventType: MemoryEventType + aggregateType: "user_memory" | "project" | "project_memory" | "thread_summary" + aggregateId: string + ownerUserId: string + projectId?: string + aggregateVersion: number + occurredAt: string + actor: { + type: "user" | "system" | "worker" + id?: string + } + correlationId: string + causationId?: string + payload: T + metadata: { + schemaVersion: number + modelId?: string + extractorVersion?: string + sourceTreeId?: string + sourceThreadId?: string + sourceMessageId?: string + } +} +``` + +`aggregateVersion` 用于乐观并发。写事件时要求: + +```sql +expected_version = current aggregate version +``` + +不匹配就返回冲突,调用方重新加载状态后再决定。 + +## 5. Command 与 Event 分离 + +外部输入先成为命令: + +```ts +type MemoryCommand = + | { + type: "AcceptMemoryCandidate" + candidateId: string + expectedVersion: number + } + | { + type: "ReviseProjectGoal" + projectId: string + objective: string + successCriteria: string[] + constraints: string[] + expectedVersion: number + } + | { + type: "DeleteMemory" + memoryId: string + expectedVersion: number + reason: "user_request" | "privacy" | "correction" + } + | { + type: "LockThreadSummary" + summaryId: string + expectedVersion: number + } +``` + +Command Handler 完成身份验证、业务校验和 policy 后,才产生事件: + +```mermaid +flowchart LR + Input["HTTP / Worker / Tool"] --> Command["Typed Command"] + Command --> Auth["Auth + Scope"] + Auth --> Load["Load Aggregate"] + Load --> Rules["Domain Rules"] + Rules --> Events["0..n Domain Events"] + Events --> Append["Atomic Append"] + Append --> Dispatch["Projection / Worker"] +``` + +LLM 永远不能直接 append 任意事件。它只能产生 `ProposeMemoryCandidate` 命令,事件类型和 payload 由应用生成。 + +## 6. Event Store + +最小 PostgreSQL 表: + +```text +memory_domain_events + sequence_id bigserial,全局排序 + event_id uuid unique + aggregate_type + aggregate_id + owner_user_id + project_id + aggregate_version + event_type + occurred_at + actor_json + payload_json + metadata_json + correlation_id + causation_id + +unique (aggregate_id, aggregate_version) +index (owner_user_id, sequence_id) +index (project_id, sequence_id) +``` + +另有: + +```text +projection_checkpoints + projection_name + last_sequence_id + status + updated_at + +projection_dead_letters + event_id + projection_name + error + attempts + next_retry_at +``` + +Event Store 不是普通日志表: + +- 只允许 append; +- schema version 必须显式; +- 事件发布与 append 在同一事务,使用 outbox/顺序轮询; +- 事件 payload 需要数据保留和隐私策略; +- 备份、恢复和 projection rebuild 必须演练。 + +## 7. 当前状态投影 + +为在线查询维护关系型 projection: + +```text +current_user_settings +current_project_goals +current_project_memories +current_thread_summaries +``` + +投影器示例: + +```ts +function projectMemory( + state: MemoryProjection | null, + event: DomainEvent +): MemoryProjection | null { + switch (event.eventType) { + case "memory_added": + return fromAddedEvent(event) + case "memory_superseded": + return applySupersede(state, event) + case "memory_deleted": + return state ? { ...state, status: "deleted" } : null + default: + return state + } +} +``` + +投影必须幂等: + +- 记录最后处理的 `sequence_id`; +- 同一 event 重放不会重复产生记录; +- 新版本投影使用新 projection name,从头重建并切换; +- 不能让在线请求依赖每次重放全部事件。 + +## 8. 时序知识图谱 + +### 8.1 图模型 + +```ts +type EntityKind = + | "user" + | "project" + | "goal" + | "thread" + | "decision" + | "constraint" + | "concept" + | "artifact" + +interface TemporalNode { + id: string + kind: EntityKind + canonicalKey: string + properties: Record + validFrom: string + validTo?: string + sourceEventIds: string[] +} + +interface TemporalEdge { + id: string + fromNodeId: string + toNodeId: string + relation: + | "HAS_GOAL" + | "HAS_THREAD" + | "BRANCHES_TO" + | "DECIDED" + | "CONSTRAINS" + | "SUPPORTS" + | "BLOCKS" + | "ANSWERS" + | "SUPERSEDES" + validFrom: string + validTo?: string + sourceEventIds: string[] + confidence: number +} +``` + +### 8.2 双时间 + +成熟时间模型可以区分: + +- `valid time`:事实在业务世界何时有效; +- `transaction time`:系统何时知道/记录它。 + +```text +用户 7 月 10 日说: +“我从 7 月 1 日起,Project 输出改用英文。” + +valid_from = 7 月 1 日 +recorded_at = 7 月 10 日 +``` + +若产品没有“回到历史时点回答”的需求,可以先只在事件层保留 `occurred_at` 和 memory `valid_from/valid_to`,不急于引入完整双时间数据库。 + +### 8.3 图写入 + +```mermaid +sequenceDiagram + participant ES as Event Store + participant GP as Graph Projector + participant R as Entity Resolver + participant G as Graph Store + participant DLQ as Dead Letter + + ES->>GP: memory/goal/summary event + GP->>R: 提取受限 entity/relation candidates + R->>G: 查 canonicalKey / aliases + G-->>R: existing nodes + R-->>GP: resolved IDs + confidence + alt 达到阈值或确定性关系 + GP->>G: upsert temporal nodes/edges + GP->>ES: checkpoint + else 不确定 + GP->>DLQ: 保存待确认候选 + end +``` + +`Project HAS_THREAD`、`Thread BRANCHES_TO Thread` 来自确定性 tree 数据,不经过 LLM。`Decision SUPPORTS Goal` 等语义边才允许模型提议。 + +## 9. 写入时序 + +```mermaid +sequenceDiagram + actor U as 用户 + participant CHAT as Chat API + participant ES as Event Store + participant EX as Extraction Worker + participant P as Policy + participant CH as Command Handler + participant CP as Current Projection + participant GP as Graph Projection + + U->>CHAT: 发送消息 + CHAT->>ES: append messages_appended reference + CHAT-->>U: 流式回答 + EX->>ES: 读取新消息事件 + EX->>P: 提取并分类 candidate + P->>CH: Accept/RequestConfirmation/Reject command + CH->>ES: append candidate + memory events + ES-->>CP: 顺序投影当前状态 + ES-->>GP: 顺序投影实体/关系 +``` + +如果 projection 落后,在线读取可能暂时看不到刚写事件。解决方式: + +- 用户显式编辑返回后,把新 aggregate version 放进 read-your-writes token; +- API 在投影未追上 token 时短暂等待或从 aggregate events 局部 fold; +- 后台自动候选允许 eventual consistency。 + +## 10. 读取时序 + +```mermaid +sequenceDiagram + actor U as 用户 + participant API as Chat API + participant CP as Current Projection + participant G as Temporal Graph + participant S as Search Index + participant C as Context Compiler + participant L as Chat Model + + U->>API: Project 内提问 + API->>CP: goal/settings/current memories/summaries + API->>API: 判断是否需要历史/关系推理 + par 需要关系 + API->>G: bounded graph query + G-->>API: nodes/edges + sourceEventIds + and 需要语义 + API->>S: semantic search + S-->>API: memory/event IDs + end + API->>CP: 回查 active/deleted 状态 + API->>C: 合并、优先级、预算、来源 + C->>L: memory context + L-->>U: 流式回答 +``` + +图查询必须 bounded: + +- 最大 hop; +- 最大节点/边数; +- 允许的 relation; +- Project scope; +- 时间范围; +- 每条结果必须能回到 sourceEvent。 + +不能让模型生成任意 Cypher/SQL 直接访问全库。 + +## 11. Thread 总结 + +Thread 总结仍是派生知识,但在方案 C 中它的所有变化都形成事件: + +```mermaid +stateDiagram-v2 + [*] --> Draft: thread_summary_generated + Draft --> Stale: thread_summary_marked_stale + Stale --> Draft: thread_summary_generated + Draft --> Locked: thread_summary_locked + Locked --> Locked: 新消息只产生 update suggestion + Locked --> Draft: 用户确认 regenerate +``` + +`thread_summary_edited` 保存用户编辑后的完整新版本或受保护的 diff。不要只保存自然语言“用户修改了总结”,否则 projection 无法重建。 + +## 12. 思维导图 + +方案 C 最容易生成关系丰富的思维导图,但仍然必须区分: + +```text +真实 tree 关系 -> BRANCHES_TO,确定性 +Project Goal 结构 -> HAS_GOAL / CONSTRAINS,确定性 +确认的 Project 记忆 -> DECIDED / glossary,权威 +LLM 推断关系 -> SUPPORTS / BLOCKS,带 confidence +``` + +```mermaid +flowchart TD + Q["MindMapQuery(projectId, asOf?)"] --> G["读取 bounded subgraph"] + G --> Filter["过滤 invalid/deleted/低 confidence"] + Filter --> Fold["按 Thread subtree 折叠"] + Fold --> Label["生成短 label"] + Label --> Validate["sourceRef + DAG/size 校验"] + Validate --> View["Read-only MindMapView"] +``` + +当前产品不需要 `asOf` 历史导图,可以不暴露 UI,但事件结构允许未来增加。 + +## 13. 删除与隐私 + +事件不可变与隐私删除存在天然张力。不能用“事件溯源”作为永不删除用户数据的借口。 + +推荐 crypto-shredding + redaction: + +1. 敏感 payload 使用每用户/Project data key 加密; +2. 普通删除追加 `memory_deleted`,projection 立即移除; +3. 隐私删除同时清除 projection、search、graph、cache; +4. 对原事件 payload 做受控 redaction,或销毁专属 data key; +5. 保留不含原文的 tombstone:event ID、删除时间、原因代码; +6. 重建 projection 时,redacted 事件只能产生 deleted 状态,不能恢复内容。 + +```mermaid +sequenceDiagram + actor U as 用户 + participant API as Privacy API + participant ES as Event Store + participant CP as Projections + participant G as Graph/Search + participant K as Key Store + + U->>API: 隐私删除 memory + API->>ES: append memory_deleted tombstone + API->>CP: remove current/history content + API->>G: remove nodes/edges/index + API->>K: destroy scoped content key / redact payload + API->>ES: mark redaction verified + API-->>U: 删除完成 +``` + +这比 B 的删除复杂得多,必须在选 C 前验证法规和审计需求是否真的需要事件不可变性。 + +## 14. 存储选择 + +### C1. PostgreSQL 事件 + PostgreSQL 投影 + 图表 + +先用关系表存 node/edge: + +```text +knowledge_nodes +knowledge_edges +``` + +优点是单一数据库、事务和备份简单;有限 hop 可以递归 CTE。适合作为 C 的最小成熟起点。 + +### C2. PostgreSQL 事件 + 专用 Graph Store + +Postgres 是 event source,Neo4j/其他图存储只是 projection。 + +优点是复杂 traversal 和图工具丰富;缺点是: + +- 双系统一致性; +- 删除核验; +- 备份和重建; +- 新运维能力; +- scope 查询安全。 + +没有实际图查询压力前,不建议先引入专用图数据库。 + +## 15. 建议代码边界 + +```text +lib/events/ + contracts.ts + event-store.ts + command-bus.ts + aggregate.ts + projector.ts + checkpoints.ts + +lib/memory/ + commands.ts + events.ts + aggregate.ts + current-projection.ts + context-compiler.ts + policy.ts + +lib/knowledge-graph/ + contracts.ts + projector.ts + entity-resolver.ts + repository.ts + query.ts + mind-map.ts + +lib/projects/ + commands.ts + events.ts + aggregate.ts + +workers/ + event-projector.ts + memory-extractor.ts + graph-projector.ts + search-projector.ts +``` + +各 aggregate 和 projection 分文件,避免把所有 event switch 堆进单文件。 + +## 16. 分阶段落地 + +### C-1:事件基础设施 + +- Event Store、aggregate version 和 append transaction; +- Project/tree/goal 事件; +- checkpoint 和 dead letter; +- projection rebuild 工具; +- 备份恢复演练。 + +### C-2:当前状态 projection + +- user settings; +- project goal; +- project memories; +- Context Compiler 只读 projection; +- read-your-writes token。 + +### C-3:候选与审计 + +- extraction events; +- policy command handler; +- accept/reject/supersede/delete; +- 完整 correlation/causation; +- 控制面时间线。 + +### C-4:Thread summary events + +- revision、draft/stale/locked; +- summary projection; +- Project 决策候选; +- rebuild 验证。 + +### C-5:关系图 projection + +- 先投影确定性 Project/Thread/Goal 边; +- 再引入受限 entity resolver; +- confidence 和人工确认; +- bounded graph query; +- 性能与 scope 安全测试。 + +### C-6:思维导图 + +- 从图 projection 读取; +- subtree folding; +- sourceRef; +- 只读缓存; +- 可选历史 `asOf`。 + +### C-7:隐私与灾备 + +- payload encryption/redaction; +- graph/search deletion reconciliation; +- projection 全量重建; +- provider/graph store 丢失恢复; +- deletion 不复活测试。 + +## 17. 测试重点 + +除了方案 B 的共同 fixture,C 必须额外测试: + +- 相同 aggregate 并发写只接受一个 version; +- 事件重复投递不重复投影; +- 任意 checkpoint 重启后结果一致; +- 从空 projection 全量重建结果一致; +- 旧 schema event 经过 upcaster 后可读取; +- graph edge 都有 sourceEventIds; +- 删除后重建不会复活; +- owner/project scope 不会跨图遍历; +- projection lag 可观测且不会无限增长; +- redacted event 不含可恢复明文。 + +## 18. 优点与代价 + +优点: + +- 最完整的演化历史和审计; +- 当前状态、历史状态和关系视图边界清楚; +- 投影可按新需求重建; +- 非常适合未来的因果、时间和关系分析; +- 思维导图可以直接利用真实关系。 + +代价: + +- Command/Event/Projection 心智负担高; +- eventual consistency 和 read-your-writes 更难; +- schema 演进、重放和死信需要长期维护; +- 隐私删除复杂; +- 图抽取和 entity resolution 仍会出错; +- 对当前个人 Project 需求可能是显著过度设计。 + +## 19. 结论 + +方案 C 应当由真实查询需求触发,而不是由“成熟产品”四个字触发。以下信号同时出现时值得选择: + +- 用户经常跨大量分支追踪决策演化; +- 产品需要时间点回放或完整审计; +- 关系推理和图导航成为核心体验; +- 数据量和查询已经证明关系表 + 简单版本链难以维护; +- 团队具备事件溯源和图投影的长期运维能力。 + +如果这些信号尚未出现,方案 B 的 audit event + version chain 已经覆盖大部分成熟产品需要,并保留向 C 演进的路径。 diff --git a/docs/memory/research/04-decision-and-delivery.md b/docs/memory/research/04-decision-and-delivery.md new file mode 100644 index 0000000..af4af54 --- /dev/null +++ b/docs/memory/research/04-decision-and-delivery.md @@ -0,0 +1,393 @@ +# A / B / C 决策方法与统一落地计划 + +## 1. 不用“成熟度”代替选择标准 + +三个方案都可以做成成熟产品,区别是复杂度放在哪里: + +```text +A:复杂度主要交给供应商,应用承担集成、一致性和退出成本 +B:复杂度主要在本地域模型,语义能力可外包 +C:复杂度主要在事件、投影、时间和关系基础设施 +``` + +最终选择需要回答: + +- 哪些状态绝不能依赖 LLM 或供应商临场判断? +- 产品是否真的需要复杂时间和图关系查询? +- 团队是否愿意长期运维事件投影或外部服务一致性? +- 删除、导出和供应商退出能否验证? +- 哪个方案在本项目数据上得到更好的正确性与总拥有成本? + +## 2. 必须先交付的共同基础 + +以下工作不是某个方案特有,因此可以在最终选型前完成: + +```mermaid +flowchart TD + P["Project identity
project_id 与 tree_id 分离"] --> G["Project Goal"] + P --> S["User Global Settings"] + P --> TS["Thread Summary contract"] + G --> C["Context Compiler contract"] + S --> C + TS --> C + C --> E["Evaluation fixtures"] + E --> A["接 A adapter"] + E --> B["接 B kernel"] + E --> C2["接 C projection prototype"] +``` + +共同基础包括: + +1. `projects` 和 `project_tree_bindings`; +2. Goal 的主目标、成功标准、约束和版本; +3. 全局 instruction 与 Project override; +4. 服务端 `ContextCompiler` 接口; +5. ThreadSummary 的 draft/stale/locked 契约; +6. 统一 MemoryCandidate、MemoryItem 和 source 类型; +7. 固定 fixture 与观测格式。 + +这部分不会迫使团队选择某个 provider,也不会提前引入图数据库。 + +## 3. 统一对比矩阵 + +### 3.1 功能与语义 + +| 维度 | A:托管服务 | B:混合内核 | C:事件图 | +| --- | --- | --- | --- | +| 用户全局设置 | 需要本地 current projection | 原生关系表 | 当前状态 projection | +| Project override | 需要本地规则 | 原生规则 | Command + projection | +| Project Goal | 本地对象 | 本地对象 | Goal aggregate | +| Project 事实/决策 | provider 为主,本地 refs | 本地权威 | event + projection | +| Thread 总结 | 本地派生 | 本地派生 | event + projection | +| 思维导图 | 本地 tree + provider memories | 本地 tree + summaries + memories | graph projection | +| 当前值冲突 | 依赖 provider + 本地补充 | deterministic reducer | event fold | +| 时间历史 | provider 能力决定 | version chain | 原生强项 | +| 多跳关系 | 弱—中 | 弱—中 | 强 | +| 用户控制面 | 仍需自建 | 自建 | 自建 | + +### 3.2 工程与运维 + +| 维度 | A | B | C | +| --- | ---: | ---: | ---: | +| 领域代码量 | 中 | 高 | 最高 | +| 基础设施代码量 | 低 | 中 | 最高 | +| 外部依赖 | 高 | 可选 | 图存储可选 | +| 故障模式数量 | 中 | 中 | 高 | +| 数据迁移难度 | provider 导出决定 | 低—中 | 事件 schema 演进高 | +| 删除工作流 | 跨系统 | canonical + index | event + projection + graph | +| 本地可调试性 | 中 | 高 | 中;需要事件工具 | +| 重建能力 | provider 决定 | 可重建索引 | 可重建全部 projection | +| 团队学习成本 | 中 | 中 | 高 | + +### 3.3 产品风险 + +| 风险 | A | B | C | +| --- | --- | --- | --- | +| 供应商锁定 | 高 | 低 | 低 | +| LLM 候选污染权威状态 | 中—高 | 低 | 低 | +| 架构过度设计 | 低—中 | 中 | 高 | +| 关系需求增长后受限 | 高 | 中 | 低 | +| 隐私删除实现错误 | 中 | 中 | 高 | +| 在线读延迟 | provider 网络影响 | 可本地优先 | projection 快,图查询需控 | + +## 4. 建议加权评分 + +权重必须由产品目标决定。针对当前已确认的“个人 Project + 全局设置 + 目标 + Thread 总结 + 只读思维导图”,可以先使用: + +| 维度 | 权重 | +| --- | ---: | +| 当前值、覆盖和删除的确定性 | 25% | +| 用户控制、来源和可解释性 | 20% | +| 与 Project/Thread 模型贴合 | 20% | +| 长期维护和可替换性 | 15% | +| 语义召回能力 | 10% | +| 时间/关系扩展能力 | 10% | + +初始架构评估,不是实测结果: + +| 方案 | 确定性 | 控制面 | 领域贴合 | 维护/替换 | 语义能力 | 时间/关系 | 加权 | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| A | 3 | 3 | 3 | 2 | 5 | 3 | 3.05 | +| B | 5 | 5 | 5 | 4 | 4 | 3 | 4.50 | +| C | 5 | 5 | 4 | 2 | 4 | 5 | 4.25 | + +分值是待验证假设。团队如果把复杂关系查询的权重提高,C 会超过 B;如果把内部研发投入权重降到最低且 provider 通过全部测试,A 会更有吸引力。 + +## 5. 统一测试对象 + +### 5.1 Fixture 类型 + +```ts +interface MemoryEvaluationCase { + id: string + ownerUserId: string + projectId: string + initialGlobalSettings?: Record + projectGoal?: { + objective: string + successCriteria: string[] + constraints: string[] + } + events: Array<{ + at: string + treeId: string + threadId: string + role: "user" | "assistant" + text: string + }> + operation?: + | { type: "edit"; key: string; value: unknown } + | { type: "delete"; key: string } + | { type: "lock_summary"; threadId: string } + query: { + at: string + treeId: string + threadId: string + text: string + } + expected: { + effectiveSettings?: Record + activeMemories?: Array<{ kind: string; key: string; value: unknown }> + inactiveKeys?: string[] + requiredSourceMessageIds?: string[] + forbiddenText?: string[] + } +} +``` + +同一个 fixture adapter 分别驱动 A/B/C,不能为不同方案修改输入。 + +### 5.2 必测场景 + +| 类别 | 场景 | +| --- | --- | +| 全局设置 | 全局中文,Project 英文,当前 turn 临时中文 | +| 当前值 | 城市、当前项目、输出格式发生更新 | +| authority | inferred 值不得覆盖 explicit 值 | +| assistant 幻觉 | assistant 建议不得自动成为用户事实 | +| Project 决策 | 一个分支作出决策,另一个分支能够引用 | +| Goal | AI 提议修改目标但未确认,目标不变化 | +| Thread 总结 | draft 自动更新;locked 不被覆盖 | +| 删除 | 删除后在线读取、索引、缓存都不再返回 | +| 隔离 | user A 与 user B、Project A 与 Project B 不串数据 | +| 思维导图 | 只出现真实 thread/sourceRef,不生成幽灵节点 | +| 拒答 | 无来源时承认不知道 | +| 故障 | provider/worker/graph 超时不阻断聊天 | + +## 6. 统一指标 + +### 6.1 正确性 + +```text +candidate precision / recall +auto-accept precision +current-value accuracy +authority precedence accuracy +retrieval recall@k +context precision +answer correctness +abstention accuracy +thread-summary factuality +mind-map source coverage +``` + +### 6.2 安全 + +硬门槛: + +```text +跨用户泄露 = 0 +跨 Project 泄露 = 0 +deleted memory 在线召回 = 0 +assistant-only claim 自动生效 = 0 +prohibited 内容发送到 provider = 0 +无 sourceRef 的导图事实节点 = 0 +``` + +### 6.3 性能与成本 + +分别记录: + +- Context Compiler p50/p95; +- provider search p50/p95; +- graph query p50/p95; +- 每轮增加的 prompt tokens; +- 每个 turn 的抽取和 embedding 成本; +- background job lag; +- provider 月度成本; +- 工程维护和 on-call 时间。 + +不应只比较 API 账单。方案 C 的人员成本可能远高于存储成本,方案 A 的供应商退出成本也必须计入。 + +## 7. 验收门槛 + +可以沿用并扩展[阶段 2 评测方案](../02-deep-dives/09-evaluation.md): + +```text +current-value 冲突准确率 >= 95% +auto-accept precision >= 98% +retrieval recall@6 >= 90% +Thread summary 关键事实支持率 >= 95% +Mind map source coverage = 100% +在线 Context Compiler p95 < 150ms(不含主模型) +默认记忆注入预算 <= 主上下文 10% +聊天成功不依赖后台写入成功 = 100% +``` + +对于 A,`<150ms` 可能受外部网络影响,应同时测冷缓存与 provider 降级。对于 C,图查询必须限制 hop 和结果数,不能用无界 traversal 达成召回率。 + +## 8. 推荐的实施顺序 + +即使最终选择 A 或 C,也建议按以下顺序落地: + +```mermaid +flowchart LR + D1["1. Project 与 tree 身份分离"] --> D2["2. Goal / Settings / Override"] + D2 --> D3["3. Context Compiler 契约"] + D3 --> D4["4. 显式 Memory CRUD 与审计"] + D4 --> D5["5. 删除 / 清空 / 导出"] + D5 --> D6["6. Candidate / Policy / Worker"] + D6 --> D7["7. Thread Summary"] + D6 --> A["A Provider Adapter"] + D6 --> B["B Canonical Index"] + D6 --> C["C Event Projection Prototype"] + D7 --> M["8. Read-only Mind Map"] + A --> V["9. 对比、故障与删除演练"] + B --> V + C --> V + M --> V +``` + +## 9. 当前仓库的落地点 + +### 9.1 现有对象 + +```text +branch_trees.state + -> 完整 ThreadTreeState + -> threads[threadId].messages + -> artifacts +``` + +现有 UI 列是 Thread 的展示,不是稳定数据作用域。Project、Summary 和 Memory 都必须绑定稳定 ID: + +```text +Project -> ProjectTreeBinding -> branch_tree +branch_tree -> ThreadTreeState.threads[threadId] +ThreadSummary -> projectId + treeId + threadId +MemorySource -> treeId + threadId + messageId +``` + +### 9.2 Chat 请求 + +当前 thread-chat body 只有: + +```ts +threadChat: { anchorText: string | null } +``` + +落地后客户端还需要发送必要的资源 ID,但服务端必须重新授权: + +```ts +interface ThreadChatContextRef { + projectId: string + treeId: string + threadId: string + anchorText: string | null +} +``` + +不能接收客户端拼好的记忆或 system prompt。服务端根据 session、Project ownership 和 Context Compiler 生成。 + +### 9.3 原有继承上下文 + +当前 `collectInherited()` 沿 lineage 收集父会话消息。引入记忆后仍保留: + +```text +当前 Thread 原始消息 ++ lineage 截断继承 ++ Project/Memory Context +``` + +ThreadSummary 不能直接替换当前 Thread 的原始对话;它主要用于: + +- 跨远端分支回忆; +- lineage 太长后的压缩; +- Project 概览; +- 思维导图。 + +## 10. 迁移路径 + +### 从 A 迁移到 B + +```mermaid +flowchart LR + Export["导出 provider memories"] --> Map["按本地 refs 映射 scope/kind/key/source"] + Map --> Import["导入 canonical memory_items"] + Import --> Verify["数量/删除/当前值 fixture"] + Verify --> Dual["短期双读,canonical 优先"] + Dual --> Cut["关闭 provider 主存储,仅保留 engine"] +``` + +如果 A 没有本地 refs 和 source,迁移会非常困难,因此 refs 不是可选优化。 + +### 从 B 演进到 C + +```mermaid +flowchart LR + Audit["B 的 audit/outbox events"] --> Normalize["标准化 DomainEvent schema"] + Normalize --> ES["建立 Event Store"] + ES --> Rebuild["从 canonical snapshot + audit 建初始 stream"] + Rebuild --> Projection["新 projection"] + Projection --> Compare["双读比对"] + Compare --> Switch["切换 Event Store 为真相"] +``` + +B 应从第一天保留足够的审计和版本链,但不需要假装它已经是完整 event sourcing。 + +### 从 C 简化回 B + +保留 current projection 作为 canonical snapshot,停止产生复杂图事件;导出 active memory、goal、summary 和 version。难点是失去历史查询能力,而不是在线功能。 + +## 11. 决策闸门 + +### 选择 A,必须满足 + +- provider 在统一 fixture 上通过所有硬门槛; +- 删除和导出已经实测; +- provider 失败时本地设置、Goal 和 Summary 仍可工作; +- 安全审查允许内容出站; +- 完成一次 staging 供应商退出演练。 + +### 选择 B,必须接受 + +- 团队长期拥有 taxonomy、Policy 和 Reducer; +- 要维护 durable worker 和索引 reconciliation; +- 不能把 Memory Engine 当作黑箱真相; +- 要持续运行自动写入误报评测。 + +### 选择 C,必须证明 + +- 已有明确、频繁、可量化的多跳/时间查询; +- B 的版本链无法合理支持需求; +- 团队能维护 event schema、projection rebuild 和 graph scope; +- 隐私删除设计通过审查与恢复演练; +- 基础设施成本由真实产品价值支撑。 + +## 12. 建议结论 + +基于目前已确认的产品边界: + +- A 能提供成熟语义能力,但仍需大量本地域代码,而且容易产生双真相; +- B 把通用能力和产品权威状态拆开,最符合全局设置、Project override、Goal、Thread Summary 与可审计控制面; +- C 对思维导图和关系演化最强,但当前只读导图可以直接从 tree + summary 生成,不足以证明引入事件图。 + +因此建议: + +```text +目标架构:B +对照实现:A +演进预留:C +``` + +这不是让团队只实现 B、不验证其他方案。应当先交付共同基础,再让 A/B 使用相同 fixture 对比;只有真实关系查询出现后,才启动 C 的 projection prototype。 diff --git a/docs/memory/research/README.md b/docs/memory/research/README.md new file mode 100644 index 0000000..2774de1 --- /dev/null +++ b/docs/memory/research/README.md @@ -0,0 +1,99 @@ +# 成熟产品记忆架构:A / B / C 方案研究 + +> 设计日期:2026-07-26。本文档集讨论的是成品级架构,不以“最快做出 MVP”为决策前提,也不把现有 PDF RAG 当作选择自建的理由。 + +## 已确认的产品边界 + +本轮讨论已经确定: + +1. 当前一棵 branch tree 对应一个 Project,但 `project_id` 与 `tree_id` 分离,未来允许一个 Project 包含多棵 tree; +2. Project 目标采用“一个主目标 + 成功标准 + 约束”,不扩展成完整项目管理系统; +3. 用户全局设置是默认值,Project 明确设置可以覆盖它; +4. 每个 `Thread`(主线或分支会话)有一份 AI 生成、用户可编辑并可锁定的总结; +5. 记忆采用分级写入:低风险内容可以自动生效,高影响或敏感内容必须确认; +6. 思维导图是只读派生视图,不是记忆真相来源; +7. 当前产品只支持个人 Project,不设计多人权限流。 + +## 三个方案 + +| 方案 | 核心主张 | 真相来源 | 最适合 | +| --- | --- | --- | --- | +| [A:托管 Memory 服务为主](./01-option-a-managed-memory.md) | 把抽取、存储、更新和检索交给 Mem0 一类服务 | Memory 服务 + 本地 Project 元数据 | 希望把通用记忆基础设施外包,并接受供应商语义 | +| [B:关系型记忆内核 + 可插拔 Memory 引擎](./02-option-b-hybrid-kernel.md) | 本地保存权威状态,外部引擎只做候选抽取和语义能力 | 本地 Postgres | 要求目标、设置、决策、删除和来源保持确定性 | +| [C:事件溯源 + 时序知识图谱](./03-option-c-event-graph.md) | 所有变化先写事件,再投影成当前状态和知识图 | 追加式事件日志 | 需要复杂时间推理、关系查询和长期演化分析 | + +所有方案共用的产品语义、安全边界和 TypeScript 契约见: + +- [00-shared-product-model.md](./00-shared-product-model.md) +- [04-decision-and-delivery.md](./04-decision-and-delivery.md) + +## 共同上下文模型 + +无论采用哪个存储方案,每次请求都不应把“所有记忆”直接塞给模型,而应先编译出有来源、预算和优先级的上下文: + +```mermaid +flowchart LR + Turn["用户当前消息"] --> Compiler["Context Compiler"] + Global["用户全局设置"] --> Compiler + Goal["Project 目标"] --> Compiler + PM["Project 记忆"] --> Compiler + TS["当前 Thread 总结与 lineage"] --> Compiler + Recall["按需语义召回"] --> Compiler + Compiler --> Policy["优先级、权限、相关性、token 预算"] + Policy --> Prompt["服务端 System / Context"] + Prompt --> Model["聊天模型"] +``` + +固定优先级: + +```text +用户当前消息中的明确要求 + > Project 明确设置 + > 用户全局明确设置 + > 系统自动推断的偏好 +``` + +这个优先级只解决同类指令冲突。系统安全规则永远不允许被任何记忆覆盖。 + +## “记忆”不是一个表 + +成熟产品至少要区分以下对象: + +| 对象 | 是否权威 | 是否允许模型自动修改 | 典型例子 | +| --- | --- | --- | --- | +| 用户全局设置 | 是 | 高影响修改必须确认 | 所有回答默认使用中文 | +| Project 目标 | 是 | 只能提出修改建议 | 完成记忆系统设计 | +| Project 记忆 | 是 | 按风险自动或确认 | 已决定使用 Postgres;术语定义 | +| 记忆候选 | 否 | 是 | 从新对话抽取的可能事实 | +| Thread 总结 | 派生知识 | 可自动生成;用户编辑后锁定 | 这个分支比较了三种存储方案 | +| 思维导图 | 派生视图 | 只读生成 | 目标—主题—分支—结论结构 | +| 原始消息/事件 | 证据 | 不由模型改写 | 用户原话和来源消息 | + +把这些内容塞进统一的 `memories` 向量集合,会丢失权限、优先级和生命周期语义。 + +## 快速比较 + +| 维度 | A:托管服务 | B:混合内核 | C:事件图 | +| --- | --- | --- | --- | +| 初始工程量 | 低—中 | 中 | 高 | +| 长期产品控制 | 中 | 高 | 最高 | +| 当前值/覆盖规则 | 依赖供应商 + 本地补丁 | SQL/Reducer 确定 | 事件投影确定 | +| 时间历史 | 服务能力决定 | 版本链 | 原生强项 | +| 关系推理 | 一般 | 有限 | 强 | +| 删除证明 | 需要跨系统核验 | 本地事务 + 索引清理 | 墓碑事件 + 投影清理 | +| 供应商锁定 | 高 | 低 | 低,但基础设施复杂 | +| 运维成本 | 低—中 | 中 | 高 | +| 与 Project 领域模型的贴合度 | 中 | 高 | 高,但可能过度设计 | + +## 当前倾向 + +在不考虑现有 RAG 复用、也不以 MVP 速度为前提时,仍不能用“成熟产品”直接推出方案 C。成熟产品首先需要清晰的数据所有权、可验证行为和可演进边界,不等于选择最复杂的架构。 + +当前倾向是 **B**: + +- 自己拥有用户设置、Project 目标、Project 记忆、Thread 总结和审计; +- 通过 `MemoryEngine` 接口使用 Mem0 或其他成熟能力; +- 外部服务故障不会让权威状态丢失; +- 若未来确实出现多跳关系、复杂时间查询,再从事件表演进到 C。 + +这只是待评审建议。最终选择应按 [统一验收与交付计划](./04-decision-and-delivery.md) 用相同 fixture 验证,而不是根据架构图复杂度决定。