引言
长期记忆是 AI 助手从"工具"进化为"伙伴"的关键能力。CountBot 实现了一套轻量但实用的记忆系统,基于文件的行式存储,支持关键词搜索和 LLM 自动总结。本文将深入分析其设计思路和实现细节。
记忆存储格式
CountBot 采用了极简的行式文本格式:
日期|来源|内容事项1;事项2;事项3
实际示例:
2026-02-15|web-chat|用户询问天气API方案;决定使用OpenWeatherMap;缓存策略选Redis TTL=3600s
2026-02-15|telegram|用户要求每天早上9点发送日报;已创建cron任务
2026-02-16|web-chat|用户偏好深色主题;常用Python和TypeScript
这种格式的优势:
- 人类可读:直接打开文件就能看懂
- 易于搜索:简单的文本搜索即可
- 版本友好:可以用 Git 追踪变更
- 零依赖:不需要向量数据库
MemoryStore 实现
class MemoryStore:
def __init__(self, memory_dir: Path):
self.memory_dir = memory_dir
self.memory_file = memory_dir / "MEMORY.md"
def append_entry(self, source: str, content: str) -> int:
"""追加一条记忆"""
date_str = datetime.now().strftime("%Y-%m-%d")
entry = f"{date_str}|{source}|{content}"
lines = self._read_lines()
lines.append(entry)
self._write_lines(lines)
return len(lines)
def search(self, keywords: str) -> str:
"""关键词搜索"""
lines = self._read_lines()
results = []
for i, line in enumerate(lines, 1):
if all(kw.lower() in line.lower() for kw in keywords.split()):
results.append(f"[{i}] {line}")
return "\n".join(results) if results else "未找到匹配的记忆"
def read_lines(self, start: int, end: int | None = None) -> str:
"""按行号读取"""
lines = self._read_lines()
if end is None:
end = start
return "\n".join(f"[{i}] {lines[i-1]}" for i in range(start, end+1))
记忆工具
记忆系统通过三个工具暴露给 LLM:
class MemoryWriteTool(Tool):
"""写入记忆"""
name = "memory_write"
# LLM 可以主动记录重要信息
class MemorySearchTool(Tool):
"""搜索记忆"""
name = "memory_search"
# LLM 可以搜索历史记忆
class MemoryReadTool(Tool):
"""读取记忆"""
name = "memory_read"
# LLM 可以按行号读取记忆
这意味着 LLM 可以自主决定何时读写记忆,而不是被动地依赖系统触发。
对话自动总结
总结提示词
CONVERSATION_TO_MEMORY_PROMPT = """你是一个对话总结器。将下面的对话总结为简洁的记忆条目。
要求:
1. 输出格式: 一行文本,多个事项用中文分号(;)分隔
2. 只记录有价值的事实信息(决策、偏好、结论、关键数据)
3. 不要记录寒暄、确认、重复内容
4. 每个事项尽量包含具体信息(名称、数字、时间、地点等)
5. 如果对话没有值得记录的信息,输出: 无需记录
对话内容:
{messages}
输出(一行,事项用;分隔):"""
消息分析器
MessageAnalyzer 负责判断哪些消息值得总结:
class MessageAnalyzer:
_SKIP_PREFIXES = (
"好的", "知道了", "明白", "收到", "谢谢", "嗯", "哦",
"ok", "OK", "thanks", "got it", "sure", "cool",
)
def format_messages_for_summary(self, messages, max_chars=4000):
lines = []
for msg in messages:
content = msg.get("content", "").strip()
# 跳过短消息中的寒暄词
if len(content) <= 8 and any(content.startswith(p) for p in self._SKIP_PREFIXES):
continue
# 截断过长内容
if len(content) > 300:
content = content[:300] + "..."
lines.append(f"{role}: {content}")
return "\n".join(lines)
过滤掉"好的"、"收到"等无意义的确认消息,只保留有价值的对话内容。
上下文溢出处理
当对话历史超过上下文窗口时,旧消息会被总结后写入记忆:
OVERFLOW_SUMMARY_PROMPT = """你是一个对话总结器。以下是一段即将被截断的旧对话历史,
请将其中有价值的信息总结为简洁的记忆条目。
要求:
1. 输出格式: 一行文本,多个事项用中文分号(;)分隔
2. 只记录有价值的事实信息
3. 不要记录寒暄、确认、重复内容
...
"""
这确保了即使对话历史被截断,重要信息也不会丢失。
记忆在上下文中的使用
ContextBuilder 在构建系统提示词时会加载记忆:
def build_system_prompt(self):
parts = []
parts.append(self._get_identity())
# 加载记忆摘要
if self.memory:
recent_memory = self.memory.get_recent(10)
if recent_memory:
parts.append(f"# 记忆\n{recent_memory}")
return "\n\n".join(parts)
最近的记忆会被注入到系统提示词中,让 LLM 在回答时能够参考历史信息。
与向量数据库方案的对比
| 特性 | CountBot 行式存储 | 向量数据库方案 |
|---|---|---|
| 部署复杂度 | 零(纯文件) | 需要额外服务 |
| 搜索方式 | 关键词匹配 | 语义相似度 |
| 存储成本 | 极低 | 较高 |
| 可读性 | 人类可直接阅读 | 需要工具查看 |
| 扩展性 | 适合个人助手 | 适合大规模应用 |
| 精确度 | 依赖关键词 | 语义理解 |
CountBot 的选择适合其定位——个人/小团队的桌面 AI 助手。对于需要处理海量记忆的场景,可以考虑引入向量数据库。
总结
CountBot 的记忆系统展示了"简单即有效"的设计哲学。通过行式文本存储、LLM 自动总结和智能过滤,在零外部依赖的前提下实现了实用的长期记忆能力。这种方案特别适合个人 AI 助手场景,值得在类似项目中借鉴。