Skip to content

Memory 与 Context

Kairox 把 memory、retrieval 和 compaction 都当作每一 turn 的一等公民。长 session 需要一种方式来记住用户偏好、项目约定和正在进行的工作,而不必每次都把它们重新粘贴一遍;它也需要能检索相关 workspace 文档,并在模型 window 满了之后丢弃旧的上下文。

本页解释 LLM 如何通过 <memory> 标签来"提议"记忆、memory store 如何按作用域保存这些记忆、workspace RAG 与配置的 knowledge base 如何注入检索上下文、context assembler 如何在每一 turn 内基于 tiktoken budget 构建 prompt,以及 compaction 如何在不丢失主线的前提下压缩历史。

<memory> 标记协议

LLM 不是通过调用某个 tool 来写入 memory 的。它是通过在响应里发出 <memory> 标签来写入 memory 的。agent-memory::extract_memory_markers 会把这些标签从 stream 中解析出来,从可见文本中剥离掉,并按每个标记自身的作用域走相应的审批流程。

作用域

作用域何时使用生命周期审批
sessionagent 想在同一个 session 后续中复用的对话内事实。session 被归档时丢弃。自动接受。发出 MemoryProposed + MemoryAccepted
user个人偏好、工作风格、长期的人体工学选择。跨当前用户的所有 session 持久化。弹给 UI。发出 MemoryProposed,等待 MemoryAccepted / MemoryRejected
workspace项目约定、仓库相关事实、会约束此 workspace 后续 session 的决策。在 workspace 内持久化。弹给 UI。发出 MemoryProposed,等待审批。

标记语法

模型在响应流中发出标记:

<memory scope="session">User prefers explanations before code.</memory>
<memory scope="user" key="editor.font">JetBrains Mono 14px.</memory>
<memory scope="workspace" key="commit.style">Conventional Commits with scope prefix.</memory>

scope="session" 不要求带 key —— session memory 很小,而且时间是有界的。scope="user"scope="workspace" 必须带 key,这样后续写入会覆盖而不是重复。这些标记在 UI 渲染 assistant 消息之前就会从可见输出中被剥离,所以最终用户看到的只是说明文本,而看不到标记本身。

一次完整的来回

UI 永远看不到 <memory> 的原始标记。它看到的是清洗后的文本,以及紧挨着它的一个 MemoryProposed 事件,聊天流会把它渲染成一个内联的"要记住这个吗?"卡片,带有"批准 / 拒绝"按钮。

为什么审批很重要

如果对 userworkspace 这两种 memory 自动落库,模型就可以悄悄改掉用户并没有同意要改的设置。更糟糕的是,一个被注入恶意内容的 tool 结果可能反过来引导模型写出一段会影响未来 session 的 memory。要求显式审批,可以把这种影响半径圈起来,让 memory 这一层始终是用户自己拥有的东西。

MemoryStore

MemoryStoreagent-memory 中的一个 trait。出厂实现是 SqliteMemoryStore,跟 event store 共享同一个 SQLite 数据库,但用的是另一组表。

一条 memory 记录包含:

  • scope(Session | User | Workspace)
  • workspace_id / session_id(取决于作用域)
  • key(供 user / workspace 使用)
  • body(memory 的文本内容)
  • created_atupdated_at
  • approved_at(对 session memory 来说为 null)

查询是按 scope + workspace/session + key 来做的,还有一个 GUI 的 memory browser 用的"列出此 scope 下全部"的回退查询。持久化 memory 仍然是精确且可审视的记录;相似度检索和全文检索放在下面单独介绍的 workspace retrieval 层。

Context 装配

每一 turn 都从头重新构建 prompt。ContextAssembler 就是决定"放什么进去"的组件。它拿到当前模型的 context window、最近的消息历史、相关的 memory、workspace retrieval 命中、knowledge-base 命中和新的用户 prompt,产出一个能塞进模型 token budget 之内的 Vec<Message>

输入

  1. System prompt。 当前策略对应的 system prompt,加上正在生效的 instructions 配置。
  2. Memory。 当前 workspace 下所有 userworkspace memory,加上当前 session 的 session memory。当 budget 紧张时,会根据相关性启发式(key 命中、时效)做过滤。
  3. Workspace retrieval。 来自本地 WorkspaceRagIndex 以及已启用 WorkspaceRetriever 实现的命中。
  4. Knowledge base。 按 profile 作用域启用的配置源,例如 SQLite FTS knowledge base。
  5. 历史。 来自事件流的近期消息 —— UserMessageAdded、被清洗过并由 AssistantMessageCompleted 定稿的 ModelTokenDelta,以及 ToolInvocationCompleted 的 payload。
  6. 新的用户 prompt。 永远会原文带上。

Token 核算

tiktoken(通过 tiktoken-rs crate)是用于度量 budget 消耗的权威 tokenizer。assembler 用每个模型声明的 context window 减去一个可配置的预留量("headroom",留给模型自身响应使用)。然后它按优先级顺序遍历输入,一旦再加一条消息就会超出 budget,就停止继续加历史。memory 优先于旧历史;为了让一条已批准的 user memory 留在 context 中,assembler 会愿意丢掉最早的几轮 user / assistant 来回。

流水线

Workspace retrieval 与 knowledge base

Memory record 用来保存用户或模型明确选择保留的事实。Workspace retrieval 用来处理另一类问题:"找出我没有显式保存、但当前 turn 相关的上下文"。

agent-memory 提供 WorkspaceRagIndex,这是一个 SQLite-backed chunk store,并通过可插拔的 EmbeddingBackend 生成向量。当前的确定性 backend 保持了本地优先和可测试性;后续本地 embedding 模型或远端 API 可以实现同一个 trait。turn 准备阶段,runtime 会查询 workspace index,并以独立的 workspace retrieval context source 注入命中结果。

配置的 knowledge base 走同一个 retrieval 边界。[knowledge_bases.<id>] 当前可以启用 SQLite FTS,配置模型也已经覆盖 Tantivy、Bedrock Knowledge Bases、Pinecone 和 Weaviate,方便对应服务客户端接入 WorkspaceRetrieverprofile_aliases 决定哪些模型 profile 可以看到该来源。多个 retriever 同时启用时,CompositeWorkspaceRetriever 会在 context 装配前合并命中结果。

trace/context source 名称会把这些输入和 durable memory 区分开:已批准记录来自 MemoryStore,检索 chunk 则来自 workspace retrieval 或 knowledge-base source。

Compaction

Compaction 是用来防止消息历史涨过当前模型 context window 的机制。它有两种形态:

自动 compaction

runtime 在每一 turn 末尾都会检查一次 assembler 的 budget(race-free 的钩子是在 #533 中落地的;关于为什么要在 session actor 中排队执行,也可参见 #532)。如果按当前的历史规模,下一 turn 就会超出 budget,runtime 会在把 actor 交还给下一个用户输入之前就触发一次 compaction。

Compaction 会把最早一层的历史总结为一条 assistant 风味的 memory note,assembler 会把它当作持久化上下文来对待。原始事件不会从 store 里删除;summary 是带 provenance 地作为一个事件被 append 进去的,这样 trace 仍然是完整的。

手动 compaction

UI 可以显式请求 compaction:TUI 的键盘快捷键、GUI 的菜单项,或者 AppFacade::compact(session) 调用。手动 compaction 走的是和自动一样的流水线,只是不需要等到 budget 溢出。

事件

事件何时发出
ContextCompactionStartedSession actor 开始执行 compaction(自动或手动)。
CompactionSummary带 provenance 的 summary 已 append。
ContextCompactionCompleted当前 context 开始使用 summary。
ContextCompactionFailed总结过程出错(模型失败、budget 边界情况)。
ContextCompactionSkippedturn 末尾触发被抑制,因为 compaction 已在运行或已禁用。

compaction 失败不会阻塞 session,但下一 turn 仍然要受 budget guard 约束 —— 如果装配好的 prompt 溢出,runtime 会拒绝发送。

Busy 状态守卫

Compaction 不能在 turn 进行中运行。session actor 通过把 compaction 请求排在当前 turn 之后来强制保证这一点。同样地,profile switch 会排在 compaction 之后,这样切换到一个更小 window 的模型时,永远不会落在总结进行到一半的状态上。两者合起来,可以杜绝那一整族"模型同时处于两种状态"的 bug。

UI 如何呈现 memory

  • TUI 在聊天流中内联地渲染"被提议的 memory",a 批准、r 拒绝。trace 面板会把 MemoryAccepted 事件连同 scope 和 key 一起打出来。
  • GUI 使用聊天流的 stream item(permission 走 ChatPermissionItem.vue;memory 这边对应的是"被提议的 memory 卡片"),以及一个专门的 MemoryBrowser.vue 视图,随时可以查看并编辑已有 memory。被接受的编辑会在 MemoryStore 中覆盖该 key 下的旧 body。

两个表面都通过 facade 跟同一个 MemoryStore 对话。不存在任何 UI-only 的 memory 缓存;如果你在 GUI 里改了一条 memory,而 TUI 同时也开在同一个 workspace 上,TUI 下一 turn 就会拿到新值。

模式与陷阱

  • memory 应该是事实,而不是指令。 "User prefers TypeScript over JavaScript for new files." 是好的。"Always write TypeScript." 是一条指令,应该放进 instructions 配置(见 Configuration)。
  • 保持 key 的稳定。 一条 key 为 editor.fontuser memory 会被下一次同 key 的写入覆盖。一条 key 为 font-preference-2026 的 memory 是一条新 memory,什么也覆盖不了。
  • workspace memory 是项目级的。 切换到不同的 workspace,会看到不同的一组 memory。模型永远不会看到不属于它当前所在 workspace 的 memory。
  • 检索命中不是 memory。 Workspace RAG 或 knowledge-base 命中可以帮助当前 turn,但不会自动变成持久化 memory;只有模型提出 <memory> 标记并通过正常审批路径后,它才会进入 memory store。
  • compaction 在设计上就是 lossy 的。 summary 是一段意译,而不是逐字记录。对那些必须逐字保留的内容,请用 memory;不管怎样,event store 始终保留着原始消息,可供审计。
  • 审批 prompt 是在消耗用户的注意力。 一个模型如果在一 turn 内连续提议十条 workspace memory,会让人很烦。除非真有多条彼此无关的新事实,否则 prompt 工程应该鼓励模型把它合并成"一 turn 一条 memory"。

本页不涉及的内容

本页讲的是哪些东西会被记住,以及 prompt 是如何被构建出来的。它不涉及模型可以调用哪些工具(Permissions & Tools),也不涉及外部能力是如何被打包发布的(扩展性)。

基于 Apache-2.0 协议发布。