跳到主要内容

Memobase — 架构与原理

30 秒导读: Memobase 是给 LLM 应用装的长期用户记忆。它不在对话的热路径上做记忆抽取,而是把聊天先攒进每个用户一块的缓冲区;缓冲区满了(默认 1024 token)才触发一次批处理,用固定三次 LLM 调用把这批对话炼成两样东西——槽位式的用户画像(work::title: 软件工程师 这种)和事件时间线。读的时候不再调 LLM,几条 SQL 就把画像 + 相关事件拼成一段 context 字符串,在线延迟压到亚百毫秒。


1. 这是什么(零基础也能懂)

  • 一句话定义: Memobase 是一个基于用户画像(user profile)的记忆系统——它替你的 AI 记住"这个用户是谁、喜欢什么、最近发生了什么",并把这些记忆打包成可以直接塞进 prompt 的一段文字。

  • 解决什么问题 / 给谁用: 假设你在做一个 AI 陪伴、AI 家教或个性化助手。用户跟它聊了几百轮,你希望 AI 下次还记得"他叫 Gus、是软件工程师、喜欢墨西哥菜、上周在做一个游戏项目"。你不想每轮都把全部历史喂进去(太贵、太慢),也不想自己维护一套记忆抽取管线。Memobase 就是这层:你把聊天记录丢给它,它负责攒、炼、存、取

  • 它能做什么(功能):

    • 把任意对话("blob")按用户存起来,批量处理成结构化记忆。
    • 维护一份会随时间演化的用户画像(结构化的 topic / sub_topic / content)。
    • 维护一条事件时间线,回答"用户最近/某时在干嘛"这类时间相关的问题。
    • 提供一个 context 接口,一次调用就把该用户最重要的记忆拼成一段 prompt 文本。
    • 通过配置精确控制要捕获哪些画像字段(profile config)。
  • 用起来什么样: 一个最小的 Python 使用示例(来自项目 README):

    from memobase import MemoBaseClient, ChatBlob

    client = MemoBaseClient(project_url=PROJECT_URL, api_key=PROJECT_TOKEN)
    uid = client.add_user()
    u = client.get_user(uid)

    # 插入一段对话(此时只进缓冲区,还没炼成记忆)
    u.insert(ChatBlob(messages=[
    {"role": "user", "content": "Hello, I'm Gus"},
    {"role": "assistant", "content": "Hi, nice to meet you, Gus!"},
    ]))

    u.flush(sync=True) # 手动触发:把缓冲区炼进记忆
    print(u.profile()) # -> basic_info::name: Gus
    print(u.context(max_token_size=500)) # -> 一段可直接进 prompt 的记忆文本
  • 一句话直觉 / 类比: 把它想成 ChatGPT 背后那套记忆——每个用户永远有一份现成的画像和事件时间线躺在数据库里。写入(炼记忆)慢而贵,所以攒起来批量做;读取(用记忆)快而频繁,所以做成"查一下就有",不碰 LLM。用离线的预计算,换在线的低延迟。

本节不涉及任何底层代码。目标:完全不懂的人读完知道"这是干嘛的"。


2. 顶层全景(它大概怎么转)

Memobase 的骨架就是一条冷热分离的主线:热路径只管"收下并攒起来",冷路径(批处理)才真正"炼记忆",取用又是另一条独立的快路径。

怎么读下面这张图: 从左到右是三条独立的路。上面一条是写入攒数据(便宜、常发生);中间一条是缓冲区满了才触发的批处理(贵、固定三次 LLM);下面一条是读取(快、只有 SQL/向量检索,不碰 LLM)。

┌─────────────────────────────────────────────┐
insert(ChatBlob) ───▶ │ ① 入口 + 缓冲区(每用户一块,按 token 计) │
(热路径,便宜) │ 存 blob → 进 BufferZone(status=idle) │
└───────────────┬─────────────────────────────┘
│ 满了(>1024 token)或 flush() 或 idle 1h

┌─────────────────────────────────────────────┐
│ ② 批处理管线(冷路径,固定 3 次 LLM) │
│ │
│ 调用1 摘要:一批对话 → 结构化 user_memo │
│ │ │
│ ├──并行──┐ │
│ 调用2/3 │ │ (并行) │
│ 抽取facts │ │ 给事件打标签(tag) │
│ +合并画像 │ │ │
│ ▼ ▼ │
│ 画像增删改 事件 + 事件 gist │
└───────┬──────────────────┬───────────────────┘
▼ ▼
┌──────────────┐ ┌────────────────────────┐
│ user_profiles │ │ user_events │
│ (画像槽位) │ │ + user_event_gists │
└──────┬───────┘ └───────────┬────────────┘
│ │
└───────────┬───────────┘

┌─────────────────────────────────────────────┐
context() ──────────▶ │ ③ 取用(快路径,不碰 LLM): │
(读路径,亚百毫秒) │ 并行拉画像 + 事件 gist → 截断 → 拼成一段文本 │
└─────────────────────────────────────────────┘

部件一句话职责:

部件干什么在哪个文件(相对克隆根)
入口 API收下 blob、存库、塞进缓冲区、判断是否要 flushsrc/server/api/memobase_server/api_layer/blob.py
缓冲区(BufferZone)每用户一块的待处理队列,按 token 累计,满了才炼src/server/api/memobase_server/controllers/buffer.py
后台 flush用 Redis 锁 + 队列,异步、去重地跑批处理src/server/api/memobase_server/controllers/buffer_background.py
批处理编排固定三次 LLM 的主循环:摘要 → (抽取+合并 ‖ 打标)src/server/api/memobase_server/controllers/modal/chat/__init__.py
画像存储topic/sub_topic/content 的槽位表src/server/api/memobase_server/models/database.py(UserProfile)
事件 + gist 存储事件时间线 + 细粒度可检索的事件 gist同上(UserEventUserEventGist)
取用 context并行拉画像+事件、按 token 截断、拼成 prompt 文本src/server/api/memobase_server/controllers/context.py

主线走一遍(高层,不进代码):

  1. 写入(热路径,便宜): insert_blob 把对话存进 general_blobs,再往 buffer_zones 加一条 idle 记录,然后只做一次判断——当前用户这块缓冲区的 token 总量是否超过阈值(默认 1024)。没超,直接返回;不炼记忆。见 api_layer/blob.py:50-82

  2. 触发(缓冲满 / 手动 flush / 闲置超时): 满了就把这批 buffer id 交给后台任务异步处理(或 wait_process=True 时同步跑)。后台用 Redis 锁保证同一用户不会被并发重复 flush(buffer_background.py:101-132)。

  3. 炼记忆(冷路径,固定 3 次 LLM): process_blobs 是核心编排(controllers/modal/chat/__init__.py:38)。先摘要这批对话成一段结构化 user_memo;然后并行跑两支——一支抽取 facts 再合并进画像(2 次调用),一支给事件打标签;最后落库成画像的增删改 + 一条事件(含若干 gist)。

  4. 取用(快路径,不碰 LLM): context() 并行地从 SQL 拉画像、从事件 gist 表拉(可选向量检索)最近事件,各自按 token 预算截断,拼成 # Memory ... 一段文本直接进 prompt(controllers/context.py:115)。整条路只有数据库操作(可选一次 embedding 查询),这就是亚百毫秒的来源。

目标:看懂"大盘"——写入攒、批处理炼、读取拼,三条路各自独立。


3. 阅读地图(建议顺序)

本子库拆成一个索引 + 四章,按"由浅入深、跟着数据流走"排序。建议顺序:

  1. Memobase — 全景与阅读地图(本文)——先建立冷热分离的整体心智模型。

  2. 数据入口与缓冲区(为什么先攒再处理)——讲清 blob → 缓冲区 → 触发的完整写入路径:为什么不在热路径炼记忆、缓冲区按什么触发(满/手动/闲置)、后台如何用 Redis 锁去重。这是 Memobase "省钱 + 低延迟"设计的起点

  3. 核心:三次 LLM 调用的抽取-合并管线——全库最核心的一章。逐步拆解 process_blobs:摘要(调用1)→ 抽取 facts(调用2)→ 合并进画像(调用3,YOLO 版给出 UPDATE/APPEND/ABORT 动作)+ 并行的事件打标;以及 organize/re-summary 这两个条件触发的额外调用。

  4. 存储模型:画像槽位、事件与事件 gist——数据落在哪:UserProfile(topic/sub_topic/content 的槽位)、UserEvent(带 profile_delta 和 tags 的事件)、UserEventGist(把一条事件切成可单独检索的小句 + 向量)。理解这层才能理解取用为什么快。

  5. 取用记忆:低延迟 context 拼装与画像配置——读路径:并行拉画像 + 事件 gist、按 profile_event_ratio 切分 token 预算、可选的对话相关性过滤与向量检索、最终拼成 prompt 文本;以及 profile config 如何控制"记哪些字段"。

给 AI agent 的路由提示: 想知道"记忆怎么被炼出来 / 三次 LLM 分别干嘛"→ 读第 2 章;想知道"记忆存成什么结构 / 表长什么样"→ 读第 3 章;想知道"怎么读出来、延迟为什么低"→ 读第 4 章;想知道"为什么要缓冲、什么时候 flush"→ 读第 1 章。


4. 巧妙之处(可借鉴的技术)

这一节提炼几个读完要带走的"精华",每条先点出妙在哪,再给 file:line

  • 冷热路径彻底分离,把成本挪到写入侧、把延迟省在读取侧。 写入只做一次 token 计数判断(controllers/buffer.py:66 detect_buffer_full_or_not),读取只查库不调 LLM(controllers/context.py:149-170)。记忆抽取的贵,被摊到"攒够一批才做一次"上。

  • "固定次数"的 LLM 调用,而不是放任 agent 自由发挥。 项目刻意把单次批处理的 LLM 调用数从早期的 3–10 次收敛到固定 3 次(README 0.0.40 更新),编排写死在 process_blobs(controllers/modal/chat/__init__.py:75-82,用 asyncio.gather 并行画像支与事件支)。没有"自主 agent"意味着成本可预测。

  • 缓冲区按 token(而非条数)计量,且是每用户分区。 触发阈值是 token 总量(controllers/buffer.py:83-86,默认 max_chat_blob_buffer_token_size=1024,见 env.py:86),天然适配"有人话多有人话少"。

  • 合并画像用"动作"而非"重写",且带幂等式的失败处理。 合并 LLM 对每条新 fact 只返回 UPDATE / APPEND / ABORT(controllers/modal/chat/merge_yolo.py:118-178),ABORT 表示"和已有画像无冲突、不必改"。缓冲区状态机 idle→processing→done/failed 让并发与重试可控(controllers/buffer.py:157-249)。

  • 事件被切成 "gist" 以支持细粒度检索。 一条事件的摘要按行拆成多条以 - 开头的小句,每句单独存 UserEventGist 并各自 embedding(controllers/event.py:113-161),检索时按句级向量相似度召回(controllers/event_gist.py:65-115),比整段事件粒度更准。

  • 画像"自我治理"。 当某个 topic 的 sub_topic 太多(超 max_profile_subtopics=15)会触发 organize 归并;某个槽位内容太长(超 max_pre_profile_token_size=128)会触发 re-summary 压缩(controllers/modal/chat/organize.py:29-35controllers/modal/chat/summary.py:31-40)。这两支是条件触发的,不计入"固定 3 次"。


5. 代码地图(导航索引)

一张跳转表:读者/agent 该打开的关键文件与真实符号名(符号名比行号抗上游漂移,可直接 grep)。

主题文件路径(相对克隆根)符号名
写入入口:存 blob + 塞缓冲 + 判满src/server/api/memobase_server/api_layer/blob.pyinsert_blob
缓冲区:插入 / 判满 / flush / 状态机src/server/api/memobase_server/controllers/buffer.pyinsert_blob_to_buffer · detect_buffer_full_or_not · flush_buffer_by_ids
后台异步 flush(Redis 锁 + 队列去重)src/server/api/memobase_server/controllers/buffer_background.pyflush_buffer_by_ids_in_background · flush_buffer_background_running
批处理主编排(固定 3 次 LLM)src/server/api/memobase_server/controllers/modal/chat/__init__.pyprocess_blobs · process_profile_res · process_event_res
调用1 摘要:对话 → user_memosrc/server/api/memobase_server/controllers/modal/chat/entry_summary.pyentry_chat_summary
调用2 抽取 factssrc/server/api/memobase_server/controllers/modal/chat/extract.pyextract_topics · merge_by_topic_sub_topics
调用3 合并进画像(UPDATE/APPEND/ABORT)src/server/api/memobase_server/controllers/modal/chat/merge_yolo.pymerge_or_valid_new_memos
并行支:事件打标签src/server/api/memobase_server/controllers/modal/chat/event_summary.pytag_event
条件触发:画像归并 / 压缩src/server/api/memobase_server/controllers/modal/chat/organize.py · .../summary.pyorganize_profiles · re_summary
落库:画像增删改src/server/api/memobase_server/controllers/profile.pyadd_update_delete_user_profiles
落库:事件 + 事件 gist(切句 + embedding)src/server/api/memobase_server/controllers/event.pyappend_user_event
取用:context 拼装(并行、按比例切 token)src/server/api/memobase_server/controllers/context.pyget_user_context · get_user_profiles_data · get_user_event_gists_data
事件 gist 读取 / 向量检索src/server/api/memobase_server/controllers/event_gist.pyget_user_event_gists · search_user_event_gists · truncate_event_gists
数据表定义(画像/事件/gist/缓冲)src/server/api/memobase_server/models/database.pyUserProfile · UserEvent · UserEventGist · BufferZone
事件/画像响应模型src/server/api/memobase_server/models/response.pyEventData · ProfileDelta · ProfileData
关键配置常量(阈值/模型)src/server/api/memobase_server/env.pymax_chat_blob_buffer_token_size · max_profile_subtopics · buffer_flush_interval · summary_llm_model

附:关键源码锚点(as-of 358c16b)

  • 缓冲触发阈值默认 1024 token、处理窗口 16384 token、闲置 1 小时:src/server/api/memobase_server/env.py:85-87(buffer_flush_interval / max_chat_blob_buffer_token_size / max_chat_blob_buffer_process_token_size)。
  • 主编排"摘要 → 并行(画像支 ‖ 事件支)":src/server/api/memobase_server/controllers/modal/chat/__init__.py:58-103(entry_chat_summaryasyncio.gather(process_profile_res, process_event_res)handle_session_event / handle_user_profile_db)。
  • 画像支内部"抽取 → 合并 → (条件)organize → (条件)re_summary":src/server/api/memobase_server/controllers/modal/chat/__init__.py:121-182(process_profile_res)。
  • 合并动作 UPDATE/APPEND/ABORT 的分支处理:src/server/api/memobase_server/controllers/modal/chat/merge_yolo.py:118-186
  • 事件切句成 gist(以 - 开头的行,逐句 embedding):src/server/api/memobase_server/controllers/event.py:113-161(append_user_event)。
  • 取用按 profile_event_ratio 切分 token 预算、并行拉两侧:src/server/api/memobase_server/controllers/context.py:134-170(get_user_context)。
  • 画像槽位表结构(content + attributes JSON 内含 topic/sub_topic):src/server/api/memobase_server/models/database.py:419-455(UserProfile)。
  • 事件 gist 表(带 pgvectorembedding 列)与句级相似度检索:src/server/api/memobase_server/models/database.py:515-586(UserEventGist)、src/server/api/memobase_server/controllers/event_gist.py:99-115