第六篇:记忆的奥秘 —— 对话上下文与持久化存储

📅 发布时间:2026/7/8 19:59:15 👁️ 浏览次数:
第六篇:记忆的奥秘 —— 对话上下文与持久化存储
从滑动窗口到MEMORY.md看 nanobot 如何让 Agent 拥有“过目不忘”的本领在前五篇文章中我们完整地走过了 nanobot 的架构、核心循环、插件系统和 LLM 交互。一个 Agent 如果每次对话都“从头开始”那它永远只是一个无情的问答机器。真正的智能体需要记忆——记住用户说过的话、记住上下文、记住长期积累的知识。nanobot 的记忆系统设计得非常巧妙它既没有引入复杂的向量数据库默认情况也没有依赖外部服务而是利用文件系统和结构化文本实现了一套完整的分层记忆机制。如果把 Agent 比作一个人短期记忆对话历史就像工作记忆记住最近几句话长期记忆MEMORY.md就像大脑皮层存储重要事实每日笔记memory/YYYY-MM-DD.md就像日记记录每天的交互技能文档SKILL.md就像参考手册指导如何使用工具今天我们就来深入解析这套记忆系统的源码实现。1. 记忆系统的整体架构nanobot 的记忆系统分为三层每层对应不同的存储介质和生命周期知识层长期记忆层短期记忆层写入更新对话历史内存中滑动窗口MEMORY.md持久化重要事实每日笔记memory/YYYY-MM-DD.md技能文档SKILL.md / 自定义技能用户消息ContextBuilder发送给LLM工具调用架构图参考1.1 三层记忆的职责层级存储位置生命周期用途短期记忆内存滑动窗口单次会话保持对话连贯性长期记忆MEMORY.md永久存储用户偏好、重要事实每日笔记memory/YYYY-MM-DD.md永久按日期归档记录每日交互供后续回顾技能文档SKILL.md等永久指导 Agent 如何使用特定工具这种分层设计的核心思想是让 Agent 知道什么该记住什么该忘记什么该查阅。2. 短期记忆对话历史的滑动窗口短期记忆的核心是维护当前会话的对话历史确保 Agent 理解上下文。nanobot 在session/manager.py中实现了会话管理。2.1 会话管理器的数据结构# session/manager.pyclassSessionManager:def__init__(self,max_history10):self.sessions{}# session_id - dequeself.max_historymax_historydefadd_message(self,session_id,role,content):添加一条消息到会话历史ifsession_idnotinself.sessions:self.sessions[session_id]deque(maxlenself.max_history)self.sessions[session_id].append({role:role,content:content,timestamp:time.time()})defget_history(self,session_id,max_tokensNone):获取会话历史可选的 token 限制ifsession_idnotinself.sessions:return[]historylist(self.sessions[session_id])ifmax_tokens:# 从后往前截断直到 token 数不超过限制returnself._truncate_by_tokens(history,max_tokens)returnhistorydef_truncate_by_tokens(self,history,max_tokens):根据 token 数量截断历史truncated[]total_tokens0formsginreversed(history):msg_tokensestimate_tokens(msg[content])iftotal_tokensmsg_tokensmax_tokens:breaktruncated.insert(0,msg)total_tokensmsg_tokensreturntruncated这里使用了collections.deque(maxlenN)自动维护滑动窗口当添加新消息时最旧的消息会自动被丢弃。2.2 在 ContextBuilder 中的使用在第五篇文章中我们看到了ContextBuilder如何获取历史# agent/context.pyhistoryawaitself.session_manager.get_history(message.session_id,max_tokensself.config.max_history_tokens# 例如 2000 token)messages.extend(history[-self.config.max_history_messages:])# 双重限制nanobot 对历史应用了双重截断策略消息数量限制最多保留最近 N 条消息如 20 条Token 数量限制历史总 token 数不超过 M如 2000这种设计既保证了上下文相关性又防止了 token 溢出。3. 长期记忆可读可写的MEMORY.md长期记忆是 nanobot 最有趣的设计之一。它没有使用数据库而是使用一个普通的 Markdown 文件MEMORY.md来存储重要信息。这带来了几个好处人类可读你可以随时打开文件查看 Agent 记住了什么可编辑你可以手动添加或修改记忆Agent 下次对话就会读取版本可控可以用 Git 管理记忆的变更历史3.1 MemoryStore 的实现# agent/memory.pyimportaiofilesfrompathlibimportPathclassMemoryStore:def__init__(self,workspace_path):self.workspacePath(workspace_path)self.memory_fileself.workspace/MEMORY.mdself.memory_dirself.workspace/memoryself.memory_dir.mkdir(exist_okTrue)asyncdefget_long_term(self)-str:读取长期记忆ifnotself.memory_file.exists():returnasyncwithaiofiles.open(self.memory_file,r)asf:returnawaitf.read()asyncdefupdate_long_term(self,new_info:str):追加或更新长期记忆existingawaitself.get_long_term()# 简单的去重如果内容已存在且相似则不重复添加ifnew_infoinexisting:return# 追加新内容asyncwithaiofiles.open(self.memory_file,a)asf:awaitf.write(f\n\n##{datetime.now().isoformat()}\n{new_info})asyncdefsearch_long_term(self,keyword:str)-List[str]:简单关键字搜索未来可替换为向量检索contentawaitself.get_long_term()ifnotcontent:return[]linescontent.split(\n)results[]forlineinlines:ifkeyword.lower()inline.lower():results.append(line)returnresults3.2 长期记忆的写入时机长期记忆不是每条消息都写入的而是在特定条件下触发# agent/loop.py (在 process_message 中)ifself._should_remember(context,response):# 提取重要信息important_factsself._extract_important_facts(context,response)forfactinimportant_facts:awaitself.memory_store.update_long_term(fact)_should_remember的判断逻辑包括用户明确要求记住如“记住我喜欢喝咖啡”Agent 发现新信息如用户第一次提到自己的名字工具执行结果包含重要数据如创建的日历事件3.3 用户如何手动管理记忆用户可以直接编辑~/.nanobot/workspace/MEMORY.md文件。例如## 用户偏好 - 用户名张三 - 喜欢喝咖啡尤其是拿铁 - 工作领域人工智能研究 ## 项目信息 - 当前项目nanobot 源码解析 - 关键截止日期2026-03-01 ## 重要事实 - 用户有一个 8 岁的女儿 - 用户使用 macOS 系统下次对话时Agent 会自动读取这些信息并应用到回答中。4. 每日笔记时间维度的记忆除了长期记忆nanobot 还会按日期记录每日的交互摘要形成时间维度的记忆。这类似于人类的“日记”。4.1 笔记的写入在 AgentLoop 处理完一条消息后会将关键信息写入当天的笔记文件# agent/memory.pyasyncdefappend_today(self,content:str):追加到今天的日记todaydatetime.now().strftime(%Y-%m-%d)pathself.memory_dir/f{today}.mdtimestampdatetime.now().strftime(%H:%M:%S)asyncwithaiofiles.open(path,a)asf:awaitf.write(f\n\n##{timestamp}\n{content})笔记内容包括用户请求的摘要Agent 执行的重要操作工具调用的结果用户反馈4.2 笔记的回顾当用户问“昨天我们聊了什么”或“上周我让你做过什么”时Agent 可以检索每日笔记asyncdefget_notes_by_date(self,date_str:str)-str:获取指定日期的笔记pathself.memory_dir/f{date_str}.mdifpath.exists():asyncwithaiofiles.open(path,r)asf:returnawaitf.read()returnf没有找到{date_str}的笔记。asyncdefsearch_notes(self,keyword:str,days30)-str:在最近 N 天的笔记中搜索关键词results[]foriinrange(days):datedatetime.now()-timedelta(daysi)pathself.memory_dir/f{date:%Y-%m-%d}.mdifpath.exists():contentawaitpath.read_text()ifkeyword.lower()incontent.lower():# 提取包含关键词的上下文行linescontent.split(\n)forlineinlines:ifkeyword.lower()inline.lower():results.append(f{date:%Y-%m-%d}:{line.strip()})return\n.join(results)ifresultselse未找到相关记录。这种基于文件的存储方式使得回顾变得非常简单也便于用户手动整理。5. 技能文档指导 Agent 行为的“知识库”nanobot 中的Skill是一种特殊类型的记忆——它告诉 Agent 如何完成特定任务。Skill 以 Markdown 文件的形式存储在skills/目录下。5.1 技能文件示例skills/weather_check.md# 天气查询技能 ## 描述 本技能用于查询指定城市的实时天气。 ## 使用场景 - 用户问“今天天气怎么样” - 用户问“明天会下雨吗” - 用户计划出行需要天气信息 ## 执行步骤 1. 使用 get_weather 工具查询天气 2. 工具需要参数 city城市名 3. 工具返回格式{temperature: 25, condition: 晴, humidity: 60%} 4. 将结果组织成自然语言回答 ## 示例 用户北京今天冷吗 Agent调用 get_weather(city北京)返回 {temperature: -2, condition: 多云}然后回答“北京今天多云气温 -2℃比较冷建议穿羽绒服。” ## 注意事项 - 如果用户没有指定城市可以询问用户所在地 - 如果工具调用失败告知用户暂时无法获取天气5.2 技能加载与使用在上下文构建阶段SkillsLoader会读取所有技能文件并生成摘要供 LLM 参考# agent/skills.pyclassSkillsLoader:def__init__(self,skills_path):self.skills_pathPath(skills_path)self.skills[]asyncdefload_all(self):formd_fileinself.skills_path.glob(*.md):contentawaitself._read_file(md_file)skillself._parse_skill(md_file.stem,content)self.skills.append(skill)asyncdefget_summary(self)-str:返回技能摘要只包含名称和描述summary## 可用技能\nforskillinself.skills:summaryf- **{skill.name}**{skill.description}\nreturnsummaryasyncdefget_full_skill(self,skill_name:str)-str:获取完整技能文档按需加载forskillinself.skills:ifskill.nameskill_name:returnskill.contentreturn当 LLM 决定使用某个技能时它可以通过工具调用read_file读取对应的完整技能文档获取详细指导。这种设计实现了知识库与代码的分离技能文档可以随时更新无需修改代码Agent 可以按需学习新技能而不需要重新训练。6. 完整记忆流程从写入到回忆下面我们通过一个完整的例子展示记忆系统如何协同工作。6.1 场景用户让 Agent 记住一个偏好用户消息“记住我喜欢喝拿铁咖啡。”AgentLoop 处理ContextBuilder读取MEMORY.md当前内容可能为空发送给 LLM 的提示中包含“如果用户要求你记住某事你应该更新长期记忆”LLM 响应{ content: 好的我记住了。, tool_calls: [ { name: update_memory, arguments: {content: 用户喜欢喝拿铁咖啡。} } ] }工具执行update_memory工具调用memory_store.update_long_term(用户喜欢喝拿铁咖啡。)内容被追加到MEMORY.md文件同时写入每日笔记memory_store.append_today(用户告知偏好喜欢喝拿铁咖啡。)6.2 场景用户再次询问用户消息“你记得我喜欢喝什么咖啡吗”ContextBuilder 构建提示从MEMORY.md读取长期记忆“用户喜欢喝拿铁咖啡。”将长期记忆作为系统消息加入提示LLM 响应直接回答“我记得您喜欢喝拿铁咖啡。”整个过程中记忆从用户告知到存储再到回忆完全基于文件系统实现无需数据库。7. 向量数据库的扩展可选虽然默认实现基于文件但 nanobot 的架构允许轻松替换为向量数据库如 Chroma、Qdrant来实现更强大的语义检索。在config.json中可以配置{memory:{type:vector,vector_store:chroma,collection:nanobot_memory,embedding_model:text-embedding-3-small}}对应的VectorMemoryStore实现可以替换文件存储classVectorMemoryStore:asyncdefadd(self,text:str,metadata:dict):# 生成向量并存入数据库passasyncdefsearch(self,query:str,k5)-List[str]:# 语义搜索passasyncdefget_long_term(self)-str:# 对于长期记忆可能返回最近的重要条目pass这种扩展性使得 nanobot 既能保持轻量默认文件存储也能在需要时升级到高级检索能力。8. 小结记忆系统的设计智慧回顾整个记忆系统的实现我们可以总结出几个关键的设计智慧设计要点解决的问题实现方式滑动窗口避免上下文无限增长deque(maxlenN) token 截断文件化长期记忆可读可编辑的持久化MEMORY.md文件按日期归档时间维度检索memory/YYYY-MM-DD.md技能即文档知识库与代码分离技能存储在 Markdown 中双重截断兼顾数量与 token 限制同时限制消息数和 token 数可拔插存储后端支持向量检索扩展通过配置切换MemoryStore实现正是这些设计让 nanobot 能够在 4000 行代码内实现一个完整且实用的记忆系统。它既保持了极简主义的优雅又为高级需求留下了扩展空间。下篇预告在下一篇文章中我们将探讨 nanobot 的工具调用机制——这是 Agent 执行动作的关键。你将看到工具调用的完整流程从 LLM 请求到执行结果返回函数调用的参数解析和校验内置工具的实现Shell、文件操作、网络请求等如何编写自定义工具敬请期待《工具调用 —— 让 Agent 拥有“手脚”》本文基于 nanobot v0.1.3 版本撰写实际代码可能随项目迭代有所变化建议结合最新源码阅读。