07:记忆系统:基于文件的行式记忆存储与自动总结

0 阅读4分钟

引言

长期记忆是 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 助手场景,值得在类似项目中借鉴。