欢迎光临
我们一直在努力

CoPaw × ReMe 源码解析:LLM 绕过、文件化记忆与自动压缩的工程实践

导读:CoPaw(Co Personal Agent Workstation)是由 AgentScope 团队开源的 AI 个人助理,支持钉钉、飞书、QQ、Discord 等多端接入。当对话持续数百轮,上下文 token 总量远超模型窗口时,如何在保留关键信息的前提下维持正常运转?CoPaw 的解决方案是集成 ReMe 的工作记忆模块,以 ReMeFb(ReMe File-Based)作为基类,实现了自动化的上下文压缩、语义记忆检索和异步记忆持久化。本文将从架构集成、触发机制、数据流到工程细节,详解 ReMe 在 CoPaw 中的具体应用方式。


一、CoPaw 为什么需要记忆管理?

CoPaw 的定位是一个长期陪伴的 AI 个人助理——用户可能连续对话几个小时,让它帮忙做社交媒体摘要、邮件整理、文件搜索、代码编写等任务。在这种场景下,每一轮交互都可能涉及大量工具调用(shell 命令、文件读写、网页搜索等),每次工具返回的结果可能包含数千甚至数万个 token。

不做记忆管理的后果:

第 1-15 轮:一切正常,响应流畅
第 15-30 轮:上下文累积到 80K tokens,推理开始变慢
第 30-50 轮:上下文接近 128K 限制,出现"上下文腐烂"——回答重复、遗忘指令
第 50+ 轮:上下文溢出,对话被迫中断

CoPaw 需要一套能够自动触发、无感压缩、保留关键上下文的记忆管理机制。ReMe 的 ReMeFb 恰好提供了这一能力。


二、CoPaw 用了 ReMe 的哪个部分?

在深入细节之前,需要先澄清一个容易产生误解的关键点:ReMe 项目内部包含两套独立的记忆子系统,CoPaw 只用了其中一套。

ReMe 项目
|
|– ReMe 类(向量化长期记忆)
| |– ReMeSummarizer(ReAct Agent)
| | |– DelegateTask(路由到具体记忆类型)
| | |– PersonalSummarizer 个人记忆
| | |– ProceduralSummarizer 程序性记忆
| | |– ToolSummarizer 工具记忆
| |– ReMeRetriever(检索 Agent)
| |– Draft-Retrieve-Deduplicate 去重流程
|
|– ReMeFb 类(文件化工作记忆) <– CoPaw 只用了这个
|– FbCompactor(上下文压缩,BaseOp,无 ReAct)
|– FbSummarizer(记忆持久化到 Markdown,BaseReact)
|– MemorySearch(向量 + 全文混合检索)
|– MemoryGet(文件读取)
|– FbContextChecker(上下文阈值检测)

特性ReMe 类ReMeFb 类(CoPaw 使用)
记忆存储 向量数据库 MemoryNode Markdown 文件
记忆类型 personal / procedural / tool / task 四种 不区分类型,统一存为文件
提取方式 ReAct Agent 循环 + DelegateTask 路由 直接 prompt 构建(无 ReAct)
去重机制 Draft-Retrieve-Deduplicate 三阶段
when_to_use 有,解耦检索意图与存储内容
适用场景 跨 session 的长期知识积累 单 session 内的上下文管理

CoPaw 未使用的 ReMe 能力:ReAct 循环、DelegateTask、四种记忆类型路由、Draft-Retrieve-Deduplicate 去重、when_to_use 检索解耦——这些全部属于 ReMe 类,与 ReMeFb 无关。

CoPaw 没有长期记忆吗?

需要澄清的是:CoPaw 有跨对话的长期记忆能力,但不是通过 ReMe 的 ReMe 类实现的,而是利用 ReMeFb 的文件持久化 + 检索能力,构建了自己的长期记忆方案:

working_dir/
MEMORY.md <– 长期记忆(决策、偏好、持久事实)
memory/
2025-02-12.md <– 每日日志(自动摘要 + 手动记录)
2025-02-13.md

sessions/ <– 会话状态序列化(JSON,恢复对话上下文)

机制实现方式跨对话持久?触发方式
MEMORY.md Agent 通过 write/edit 工具主动写入 Agent 自主判断
memory/每日日志 上下文压缩时由 summary_memory() 自动写入 上下文溢出时自动触发
memory_search 通过 ReMeFb 的向量+BM25 混合检索找回记忆 Agent 推理时调用
sessions/ save/load_session_state 序列化对话状态 每次对话结束/开始时
Heartbeat 定时任务(默认 30 分钟)执行自检和记忆整理 周期性自动触发

也就是说,CoPaw 的跨对话记忆是一个 “FbSummarizer 写入 Markdown -> FileWatcher 更新索引 -> MemorySearch 检索回忆” 的完整循环。

sessions/ 与 memory/ 的区别

需要特别说明 sessions/ 目录的作用——它存的不是所有对话原文,而是 Agent 的"内存快照",类似游戏存档。每个文件是一个 JSON(格式为 {user_id}_{session_id}.json),内容由 CoPawInMemoryMemory.state_dict() 序列化:

# sessions/*.json 的内容结构
{
"content": [
[msg.to_dict(), marks], # 每条近期消息 + 标记(如 COMPRESSED)
...
],
"_compressed_summary": "之前对话的摘要…" # 被压缩部分的历史摘要
}

注意 content 中只保留未被压缩的近期消息——那些已被标记为 COMPRESSED 的老消息早已从内存中移除,它们的精华被提炼到了 _compressed_summary 中。

两者的职责对比:

sessions/*.jsonmemory/*.md
存的是什么 Agent 的内存快照(近期消息 + 历史摘要) LLM 提炼后的知识和日志
生命周期 随会话更新,保存最新的会话状态 持久存在,不断累积
谁来读 框架自动 load/save,用户不需要关心 Agent 通过 memory_search 检索,用户也能直接打开查看
作用 恢复上次对话的上下文(“短期记忆存档”) 积累长期知识(“长期记忆笔记本”)

两者协作确保 CoPaw 既能恢复上次对话的即时上下文,又能通过 memory_search 回忆更久远的历史。

看似冗余,实则互补

读者可能会疑惑:sessions/ 里的 _compressed_summary 和 memory/*.md 都是"摘要",不冗余吗?

答案是不冗余——它们由不同的组件生成,生命周期和使用方式完全不同。从触发压缩的代码可以看到,同一批消息同时产生了两个输出:

# 同一批 messages_to_compact,产生两个不同的输出:

# 输出 1:异步写入 memory/YYYY-MM-DD.md(FbSummarizer 生成)
self.memory_manager.add_async_summary_task(messages=messages_to_compact)

# 输出 2:更新内存中的 _compressed_summary(FbCompactor 生成)
compact_content = await self.memory_manager.compact_memory(...)
await agent.memory.update_compressed_summary(compact_content)

_compressed_summarymemory/*.md
生成者 FbCompactor(压缩器) FbSummarizer(持久化器)
更新策略 每次压缩时覆盖旧摘要 每次压缩时追加/合并到当天日志
使用方式 自动注入到 LLM 上下文(<previous-summary> 标签) 按需检索,LLM 调用 memory_search 时才读取
生命周期 滚动覆盖,只保留最新一次压缩结果 持久累积,永不覆盖

举例说明:

第 1-50 轮对话 → 第一次压缩
_compressed_summary = "用户讨论了项目架构…" ← 覆盖,只有这一版
memory/2025-02-12.md += "讨论了项目架构…" ← 追加

第 51-100 轮对话 → 第二次压缩
_compressed_summary = "完成架构设计并开始编码…" ← 覆盖旧的,第一版丢失
memory/2025-02-12.md += "完成架构设计,开始编码…" ← 追加,两次都在

此时:
_compressed_summary 只记得"第二次压缩"的内容
memory/2025-02-12.md 记录了两次压缩的完整内容

简言之:_compressed_summary 是滚动窗口(LLM 每次推理自动看到,但只保留最新),memory/*.md 是永久存档(需要 memory_search 才能找到,但永不丢失)。两者一个负责"最近在聊什么",一个负责"以前发生过什么"。

两种方案的对比

它和 ReMe 的 ReMe 类方案的核心区别在于:

维度ReMe 的 ReMe 类CoPaw 基于 ReMeFb 的方案
存储格式 结构化 MemoryNode(向量数据库) 非结构化 Markdown 文件
记忆分类 LLM 自动分类为四种类型 不分类,按日期和文件组织
去重 Draft-Retrieve-Deduplicate 自动去重 无自动去重,依赖 Agent 手动维护
可编辑性 需通过 API 操作 用户和 Agent 都可以直接编辑 Markdown 文件
可检查性 需要专用工具查看 打开文件就能看到所有记忆内容

CoPaw 选择 ReMeFb 路线的一个重要优势是透明性——用户可以直接打开 MEMORY.md 查看和编辑 Agent 记住了什么,这对于一个面向终端用户的个人助理产品来说至关重要。


三、整体架构:CoPaw 如何集成 ReMeFb

CoPaw 对 ReMeFb 的集成采用了继承 + Hook 注入的双层架构:

┌─────────────────────────────────────────────────────────────┐
│ CoPawAgent │
│ (ReActAgent 子类) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 工具集 │ │ Skills │ │ 命令处理器 │ │
│ │ (shell,文件, │ │ (pptx,docx, │ │ (/new,/save │ │
│ │ 浏览器等) │ │ xlsx 等) │ │ /compact) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Hooks (pre_reasoning) │ │
│ │ ┌────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ BootstrapHook │ │ MemoryCompactionHook │ │ │
│ │ │ (首次交互检查) │ │ (自动上下文压缩) │ │ │
│ │ └────────────────┘ └────────────┬────────────┘ │ │
│ └───────────────────────────────────┼──────────────┘ │
│ │ │
│ ┌───────────────────────────────────▼──────────────┐ │
│ │ MemoryManager │ │
│ │ (继承自 ReMeFb) │ │
│ │ │ │
│ │ compact_memory() ← 上下文压缩 │ │
│ │ summary_memory() ← 记忆持久化 │ │
│ │ memory_search() ← 语义检索(注册为工具) │ │
│ │ memory_get() ← 记忆片段读取 │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

┌──────▼──────┐
│ ReMe │
│ ReMeFb │
│ (基础能力) │
│ │
│ FbCompactor │
│ FbSummarizer│
│ MemorySearch│
│ MemoryGet │
└─────────────┘

3.1 继承关系:MemoryManager <- ReMeFb

CoPaw 的 MemoryManager 直接继承自 ReMe 的 ReMeFb 类:

from reme import ReMeFb

class MemoryManager(ReMeFb):
"""Memory manager that extends ReMeFb functionality for CoPaw agents."""

def __init__(self, *args, working_dir: str, **kwargs):
if not _REME_AVAILABLE:
raise RuntimeError("reme package not installed.")

super().__init__(
*args,
working_dir=working_dir,
llm_api_key="", # 故意留空,不让 ReMe 管理 LLM
llm_base_url="", # 故意留空
default_llm_config={}, # 故意留空
embedding_api_key=embedding_api_key, # Embedding 交给 ReMe 管理
embedding_base_url=embedding_base_url, # Embedding 交给 ReMe 管理
default_embedding_model_config={
"model_name": embedding_model_name,
"dimensions": embedding_dimensions,
},
default_file_store_config={
"backend": memory_backend, # Windows 用 local,其他用 chroma
"store_name": "copaw",
"vector_enabled": vector_enabled,
"fts_enabled": fts_enabled,
},
**kwargs,
)

注意 llm_api_key=""、llm_base_url=""、default_llm_config={} 这三个参数全部为空——CoPaw 故意不给 ReMeFb 配置任何 LLM。但 Embedding 相关的配置是正常传入的。

这决定了两者的核心分工:ReMeFb 负责 Embedding(向量化和检索),CoPaw 负责 LLM 推理(压缩和持久化的文本生成)。

这种继承关系意味着 MemoryManager 天然获得了 ReMeFb 的全部能力:

  • compact() — 基于 FbCompactor 的上下文压缩(包含 Split Turn 检测)
  • summary() — 基于 FbSummarizer 的记忆持久化到 Markdown 文件
  • memory_search() — 基于 MemorySearch 的向量 + 全文混合检索
  • memory_get() — 记忆文件的分页读取
  • context_check() — 基于 FbContextChecker 的上下文阈值检测

3.2 优雅降级设计

CoPaw 对 ReMe 的依赖采用了可选导入 + 优雅降级的策略:

try:
from reme import ReMeFb
_REME_AVAILABLE = True
except ImportError:
logger.warning("reme not found!")
_REME_AVAILABLE = False

class ReMeFb: # type: ignore
"""Placeholder when reme is not available."""

当 reme 包未安装时,MemoryManager 使用一个空的占位类。这意味着 CoPaw 可以在不安装 ReMe 的情况下运行——只是不具备记忆管理功能。这种设计保证了 CoPaw 核心功能的独立性。


四、自动触发机制:MemoryCompactionHook

CoPaw 最核心的 ReMe 集成点,是通过 Hook 机制在每次 LLM 推理前自动检查并触发上下文压缩。

4.1 Hook 注册

在 CoPawAgent 初始化时,如果记忆管理器可用,系统会注册一个 pre_reasoning Hook:

def _register_hooks(self):
if self._enable_memory_manager and self.memory_manager is not None:
memory_compact_hook = MemoryCompactionHook(
memory_manager=self.memory_manager,
memory_compact_threshold=self._memory_compact_threshold,
keep_recent=MEMORY_COMPACT_KEEP_RECENT,
)
self.register_instance_hook(
hook_type="pre_reasoning",
hook_name="memory_compact_hook",
hook=memory_compact_hook.__call__,
)

pre_reasoning 意味着这个 Hook 在每次 LLM 推理前都会执行。也就是说,CoPaw 在每一步 ReAct 循环中都会自动检查上下文是否需要压缩——用户完全无感知。

4.2 压缩触发的判断逻辑

MemoryCompactionHook.__call__() 的判断流程如下:

async def __call__(self, agent, kwargs):
# 1. 获取所有未被压缩的消息
messages = await agent.memory.get_memory(
exclude_mark=_MemoryMark.COMPRESSED,
prepend_summary=False,
)

# 2. 分离系统提示 + 可压缩消息 + 最近消息
system_prompt_messages = [msg for msg in messages if msg.role == "system"]
remaining = messages[len(system_prompt_messages):]

# 3. 保留最近 N 条消息(默认 10 条)
messages_to_compact = remaining[:keep_recent]
messages_to_keep = remaining[keep_recent:]

# 4. 估算可压缩区域的 token 数
estimated_tokens = await safe_count_message_tokens(
await agent.formatter.format(msgs=messages_to_compact)
)

# 5. 超过阈值时触发压缩
if estimated_tokens > self.memory_compact_threshold:
await compress(...)

阈值的计算方式:

# CoPawAgent 中
self._memory_compact_threshold = int(
max_input_length * MEMORY_COMPACT_RATIO
)

# MemoryManager 中(额外 0.9 安全系数)
self._memory_compact_threshold = int(
max_input_length * MEMORY_COMPACT_RATIO * 0.9
)

其中 max_input_length 默认为 128K tokens,MEMORY_COMPACT_RATIO 是一个可配置的比例参数。双重阈值设计确保了 MemoryManager 的压缩时机略早于 Agent 的判断,留出了安全余量。

4.3 三段式消息分区

CoPaw 的消息分区策略:

[System Prompt] + [可压缩区(被标记为 COMPRESSED)] + [最近 N 条消息]
保留 摘要化 保留

这与 ReMe 的三段式切割模型(messages_to_summarize / turn_prefix / left_messages)理念一致,但 CoPaw 在 Hook 层面做了简化——它按消息条数而非 token 数来划分"最近消息"的保留范围,然后委托 ReMe 的 compact() 方法完成实际的压缩工作。


五、CoPaw 对 ReMeFb 的适配与扩展

CoPaw 并非简单地调用 ReMeFb 的 API,而是在多个环节进行了适配和扩展。

5.1 消息格式转换与 Prompt Only 模式

CoPaw 使用 AgentScope 的 Msg 格式,而 ReMe 内部使用自己的 Message 格式。MemoryManager.compact_memory() 负责格式转换:

async def compact_memory(self, messages_to_summarize, turn_prefix_messages, previous_summary):
formatter = TimestampedDashScopeChatFormatter(
memory_compact_threshold=self._memory_compact_threshold,
)

# AgentScope Msg → DashScope 格式 → ReMe 可接受的 dict 列表
if messages_to_summarize:
messages_to_summarize = await formatter.format(messages_to_summarize)

if turn_prefix_messages:
turn_prefix_messages = await formatter.format(turn_prefix_messages)

# 关键:return_prompt=True
# ReMeFb 的 FbCompactor 会组装好 prompt(系统提示 + 对话序列化 + 用户指令)
# 但不会调用自己的 LLM 生成摘要——因为 llm_api_key 是空的
# 它只返回一个 dict: {"system": "…", "history_user": "…", "turn_prefix_user": "…"}
prompt_dict = await super().compact(
messages_to_summarize=messages_to_summarize,
turn_prefix_messages=turn_prefix_messages,
previous_summary=previous_summary,
language=self.language,
return_prompt=True,
)
...

return_prompt=True 的效果体现在 ReMeFb 内部的 FbCompactor.execute() 中——当此参数为 True 时,FbCompactor 只返回组装好的 prompt dict,跳过 self.llm.chat() 调用。也就是说,ReMe 的 LLM 推理在 CoPaw 中永远不会被执行。

5.2 使用 CoPaw 的 LLM 生成摘要

获取 prompt 后,CoPaw 使用自己的 ReActAgent 来执行实际的摘要生成:

# 用 CoPaw 的 LLM 生成 History Summary
agent = ReActAgent(
name="history_summary",
model=self.chat_model, # CoPaw 用户配置的模型
sys_prompt=system_prompt, # ReMe 生成的系统提示
formatter=self.formatter, # CoPaw 的消息格式化器
)

history_summary_msg = await agent.reply(
Msg(name="reme", content=history_user, role="user"),
)

这意味着:

  • ReMe 负责"要压缩什么、怎么组织 prompt"(算法和提示词工程)
  • CoPaw 负责"用哪个 LLM 来做压缩"(模型管理)

这种职责分离是一个非常干净的架构设计。

5.3 时间戳感知的消息格式化

CoPaw 实现了一个自定义的 TimestampedDashScopeChatFormatter,在标准的 DashScope 消息格式基础上增加了时间戳:

msg_dashscope = {
"role": msg.role,
"content": content_blocks,
"time_created": msg.timestamp, # 注入时间戳
}

时间戳的保留使得 ReMe 在生成摘要时可以了解消息的时间线,这对于"按时间段压缩"等高级策略至关重要。

5.4 工具结果的智能截断

在压缩前,CoPaw 对工具返回的超长文本进行截断处理:

if self.enable_truncate_tool_result_texts and messages_to_keep:
_truncate_tool_result_texts(messages_to_keep)

_truncate_tool_result_texts() 会遍历所有消息中的 tool_result 块,将超过 TOOL_RESULT_MAX_LENGTH(默认 10000 字符)的文本截断,保留头尾部分。这是对压缩策略的前置优化——先用低成本的规则截断处理大块工具输出,再交给 LLM 做语义级压缩。


六、记忆持久化:异步写入 Markdown 文件

CoPaw 在触发上下文压缩的同时,会异步启动一个记忆持久化任务:

# MemoryCompactionHook 中
self.memory_manager.add_async_summary_task(
messages=messages_to_compact,
)

add_async_summary_task 的实现:

def add_async_summary_task(self, messages, date="", version="default"):
# 清理已完成的任务
remaining_tasks = [t for t in self.summary_tasks if not t.done()]
self.summary_tasks = remaining_tasks

# 创建新的异步任务
self.summary_tasks.append(
asyncio.create_task(
self.summary_memory(
messages=messages,
date=date or datetime.datetime.now().strftime("%Y-%m-%d"),
version=version,
),
),
)

summary_memory() 内部调用 ReMeFb 的 FbSummarizer(同样使用 return_prompt=True 获取 prompt,由 CoPaw 的 LLM 执行),将被压缩的对话内容写入 memory/YYYY-MM-DD.md 每日日志文件。

写入目标:memory/YYYY-MM-DD.md

FbSummarizer 的 prompt 模板明确指定了写入路径和工作流程:

立即存储持久化记忆(使用路径 memory/YYYY-MM-DD.md)。

工作流程:
1. 先 read memory/YYYY-MM-DD.md(如文件不存在,会返回错误提示)
2. 智能合并新信息与现有内容(若文件不存在则跳过合并):
– 避免重复已记录的信息
– 在相关时丰富现有条目的新细节
– 在适用时保持时间顺序
3. 写入更新后的内容:
– 尽可能使用 edit 更新特定部分
– 如需大幅重构则使用 write 覆盖整个文件

也就是说,每次上下文压缩时,CoPaw 的 LLM 会按照 ReMeFb 提供的这套 prompt 指令,将对话中的关键信息智能合并到当天的日志文件中——不是简单追加,而是先读取已有内容、避免重复、丰富细节。

memory/*.md 与 MEMORY.md 的分工

CoPaw 的记忆文件体系分为两层:

文件定位写入方式内容示例
MEMORY.md 长期记忆,存放极少变动的关键事实 Agent 通过 write/edit 工具主动写入 “项目使用 Python 3.12”、“偏好 pytest 框架”
memory/YYYY-MM-DD.md 每日日志,记录当天的交互和工作 上下文压缩时由 summary_memory() 自动写入 “今天修复了登录 Bug”、“部署了 v2.1”

用户说"记住这个"时,Agent 也会主动将信息写入对应的文件。两类文件都被 FileWatcher 监控,修改后自动更新向量索引,通过 memory_search 可随时检索。

注意这里的关键设计:持久化是异步的,不阻塞主对话流程。asyncio.create_task 创建任务后立即返回,用户的当前交互不会因为记忆写入而变慢。


七、FileWatcher:文件与索引的实时同步

前面提到 FbSummarizer 会将摘要写入 Markdown 文件,后面会介绍 memory_search 通过向量检索找回记忆。但一个关键问题是:Markdown 文件被修改后,向量数据库里的索引并不会自动更新。如果 Agent 向 MEMORY.md 写入了一条"项目使用 Python 3.12",但向量数据库里没有对应的 embedding,那 memory_search("项目语言") 就搜不到这条记忆。

FileWatcher 就是解决这个问题的组件——它充当 Markdown 文件和向量数据库之间的实时同步桥梁。

工作原理

MEMORY.md 被修改
|
v
FileWatcher 检测到变化(基于 watchfiles 库)
|
v
读取文件内容 → 切成 chunk → 调用 Embedding 模型向量化
|
v
删除旧索引 → 写入新索引(file_store.upsert_file)
|
v
memory_search 现在能搜到最新内容了

核心实现在 ReMeFb 的 FullFileWatcher._on_changes() 中:

async def _on_changes(self, changes):
for change_type, path in changes:
if change_type in [Change.added, Change.modified]:
# 1. 读取修改后的文件
file_meta = await self._build_file_metadata(path)
# 2. 将文件内容切成若干 chunk
chunks = chunk_markdown(file_meta.content, file_meta.path, ...)
# 3. 调用 Embedding 模型,将每个 chunk 转为向量
chunks = await self.file_store.get_chunk_embeddings(chunks)
# 4. 先删除旧索引,再写入新索引
await self.file_store.delete_file(file_meta.path, ...)
await self.file_store.upsert_file(file_meta, ..., chunks)

elif change_type == Change.deleted:
# 文件被删除,同步清除索引
await self.file_store.delete_file(path, ...)

CoPaw 中的监控范围

CoPaw 在初始化 ReMeFb 时配置了 FileWatcher 监控以下路径:

default_file_watcher_config={
"watch_paths": [
str(working_path / "MEMORY.md"), # 长期记忆文件
str(working_path / "memory.md"), # 备选文件名
str(working_path / "memory"), # 每日日志目录
],
},

也就是说,无论是 Agent 通过 write_file 主动写入 MEMORY.md,还是 summary_memory() 自动生成 memory/2025-02-12.md,FileWatcher 都会在毫秒级别检测到变化并更新向量索引。这保证了"写入 → 检索"的循环永远是闭合的。


八、语义记忆检索:memory_search 工具

CoPaw 将 ReMe 的 memory_search 方法注册为一个可供 LLM 调用的工具:

def _setup_memory_manager(self, enable_memory_manager, memory_manager):
if self._enable_memory_manager and self.memory_manager is not None:
memory_search_tool = create_memory_search_tool(self.memory_manager)
self.toolkit.register_tool_function(memory_search_tool)

注册后,CoPaw 的 LLM 可以在推理过程中主动调用 memory_search 工具。例如,当用户问"我之前让你做的那个项目进展怎样了?"时,LLM 会判断这需要检索记忆,自动调用:

memory_search(query="项目进展", max_results=5, min_score=0.1)

底层由 ReMe 的 MemorySearch 工具执行,支持向量搜索和全文搜索的混合检索。检索范围包括:

  • MEMORY.md — 主记忆文件
  • memory/*.md — 分类记忆文件
  • 通过 File Watcher 监控的其他文件

九、向量存储的平台适配

CoPaw 对 ReMe 的向量存储后端进行了平台感知适配:

memory_store_backend = os.environ.get("MEMORY_STORE_BACKEND", "auto")
if memory_store_backend == "auto":
memory_backend = "local" if platform.system() == "Windows" else "chroma"
else:
memory_backend = memory_store_backend

平台默认后端原因
Windows local(JSONL 文件) ChromaDB 在 Windows 上的依赖安装较为复杂
macOS / Linux chroma(ChromaDB) 在 Unix 系统上安装和运行更顺畅

用户也可以通过 MEMORY_STORE_BACKEND 环境变量覆盖默认选择,支持 local、chroma、elasticsearch、qdrant 等所有 ReMe 支持的后端。


十、CoPaw 对 ReMeFb 能力的使用总结

下表汇总了 CoPaw 使用 ReMeFb 的各个功能点,以及是否使用了 ReMe 的 LLM:

ReMeFb 能力CoPaw 中的使用方式使用 ReMe 的 LLM?触发时机
FbContextChecker 通过 MemoryCompactionHook 在每次推理前检测 token 是否超限 不涉及 LLM 自动(pre_reasoning Hook)
FbCompactor return_prompt=True 获取 prompt,CoPaw 自己的 LLM 执行 绕过 token 超限时自动触发
FbSummarizer return_prompt=True 获取 prompt,CoPaw 自己的 ReActAgent 执行 绕过 压缩时异步执行
MemorySearch 注册为 LLM 可调用的工具,使用 ReMe 的 Embedding 做向量检索 使用 Embedding LLM 自主判断何时调用
MemoryGet 注册为工具,纯文件读取 不涉及 LLM 在检索后按需调用
Split Turn 检测 通过 FbCompactor 内嵌的三段式切割逻辑自动处理 不涉及 压缩时自动生效
向量存储管理 通过 ReMeFb 的 FileStore 和 FileWatcher 自动管理 使用 Embedding 启动时初始化,运行中自动更新

十一、架构设计的亮点

11.1 LLM 绕过 + Embedding 委托

CoPaw 对 ReMeFb 能力的使用策略可以概括为:LLM 推理全部绕过,Embedding 和向量检索全部委托。

  • llm_api_key="" + return_prompt=True 确保 ReMeFb 的 FbCompactor 和 FbSummarizer 永远不会调用 self.llm.chat()
  • embedding_api_key=真实key 确保 ReMeFb 的 MemorySearch 和 FileStore 可以正常做向量化和语义检索

这种设计使得:

  • 用户在 CoPaw 控制台中配置的模型直接用于所有 LLM 操作(包括记忆压缩和持久化),无需为 ReMe 单独配置
  • Embedding 模型相对固定(如 text-embedding-v4),交给 ReMeFb 统一管理更简洁
  • 模型切换时无需同步修改 ReMe 的配置

11.2 Hook 驱动的自动压缩

通过 pre_reasoning Hook 实现了完全自动化的上下文管理——开发者无需在业务逻辑中显式调用压缩函数,Agent 的每次推理步骤前都会自动检查。这种非侵入式的设计保持了 CoPawAgent 核心逻辑的简洁。

11.3 可选依赖 + 优雅降级

reme 作为可选依赖,未安装时系统自动降级为无记忆模式。这保证了 CoPaw 的核心功能(对话、工具调用、Skills)不受记忆模块可用性的影响。

11.4 异步双轨处理

上下文压缩(同步,用户可感知)和记忆持久化(异步,用户无感知)并行执行,在保证上下文精简的同时不增加用户等待时间。


十二、结语

CoPaw 对 ReMe 的集成,展示了一个值得注意的策略:只用框架的一部分,且以自己的方式用。CoPaw 没有使用 ReMe 完整的向量化长期记忆系统(ReMe 类),而是选择了更轻量的文件化工作记忆子系统(ReMeFb 类),并在此基础上进一步将 LLM 推理权收归自身,只保留 ReMeFb 的提示词模板和 Embedding 能力。

对于希望在自己的 Agent 应用中集成 ReMe 的开发者而言,CoPaw 的实现提供了一个值得参考的集成范式:

  • 选择合适的子系统:根据需求选 ReMe(长期记忆)或 ReMeFb(工作记忆),不必全盘接入
  • Hook 驱动而非硬编码:通过 Hook 机制实现自动化触发
  • LLM 绕过 + Embedding 委托:return_prompt=True 绕过 ReMe 的 LLM,Embedding 和检索委托给 ReMe
  • 可选依赖保证独立性:记忆功能作为增强而非核心依赖

相关链接:

  • CoPaw GitHub:https://github.com/agentscope-ai/CoPaw
  • ReMe GitHub:https://github.com/agentscope-ai/ReMe
  • CoPaw 文档:https://copaw.agentscope.io
  • AgentScope 团队:https://github.com/agentscope-ai
赞(0)
未经允许不得转载:171主机测评 » CoPaw × ReMe 源码解析:LLM 绕过、文件化记忆与自动压缩的工程实践
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址