1. 从“记忆”的痛点说起为什么我们需要Markdown驱动的记忆系统在构建一个智能体Agent或聊天机器人时我们常常会赋予它一个听起来很酷的能力——“记忆”。简单来说就是希望它能记住和用户之前的对话内容从而在后续的交流中表现得更有上下文感更像一个“老朋友”。然而这个看似简单的需求在实际工程化落地时往往会变成一场灾难。最常见的实现方式是将每次对话的上下文包括用户的问题和模型的回答一股脑地塞进一个文本字符串里然后在下一次对话时将这个越来越长的字符串作为“历史记录”再次喂给模型。这种做法在初期看似有效但随着对话轮次的增加问题会接踵而至上下文长度爆炸大语言模型LLM的上下文窗口Context Window是有限的。无论是4K、8K、16K还是128K这个“内存”终有耗尽的一天。当历史记录超过这个限制时最直接的结果就是模型无法处理对话被迫中断或丢失早期记忆。信息噪音与成本激增并非所有历史对话都具有同等价值。一些寒暄、无关紧要的细节会混杂在核心信息中它们不仅无助于当前对话反而会成为干扰模型的“噪音”。更现实的是向LLM API发送的令牌Token数量直接与费用挂钩。为这些无效信息付费无疑是巨大的浪费。记忆结构僵化纯文本的历史记录是一维的、扁平的。我们很难从中高效地提取出“用户上周三提到的项目截止日期”或者“用户最喜欢的咖啡口味”这类结构化的信息。记忆变成了一个难以查询和利用的“黑箱”。正是在这样的背景下当我看到nanobot这个项目将自己定位为openclaw的平替并特别强调其“Markdown 驱动的记忆系统”时立刻提起了兴趣。这听起来不像是一个简单的文本拼接方案而更像是一种为记忆赋予“格式”和“结构”的尝试。Markdown作为一种轻量级标记语言其核心优势在于通过简单的语法如标题、列表、代码块、粗体/斜体来清晰地表达文档的结构与层次。如果记忆也能用Markdown来“书写”和“组织”那是否意味着我们可以更智能地压缩、检索和利用这些记忆nanobot的这套系统本质上是在探索如何将非结构化的对话流转化为结构化的、易于管理的知识片段并利用Markdown的天然特性来实现这一过程。接下来我们就深入其源码拆解这套系统是如何设计、运作并尝试解决上述痛点的。2. 记忆系统的顶层设计从对话到知识库的转化流水线在nanobot的架构中记忆系统并非一个孤立的模块而是一条贯穿数据处理生命周期的“流水线”。它的目标不是保存原始对话日志而是生产可用的“记忆资产”。我们可以将其顶层工作流程分解为以下几个核心阶段2.1 记忆的捕获从原始消息到待处理素材记忆的源头是每一次交互。nanobot通常会从消息队列或事件总线中接收原始的对话消息Message。这些消息包含了用户输入User Input、智能体回复Agent Response以及可能的系统指令。在捕获阶段系统并不急于存储而是进行初步的过滤和包装。例如系统可能会忽略某些特定指令如/reset重置对话或者将连续的多轮简短对话合并为一个逻辑上的“对话块”以减少碎片化。捕获后的数据被封装为一个内部数据结构我们暂且称之为MemorySeed记忆种子它包含了原始的文本内容、时间戳、会话ID、消息类型等元数据为后续加工做好准备。2.2 记忆的加工提取、摘要与结构化这是整个系统的核心环节也是“Markdown驱动”理念开始发挥作用的地方。nanobot会调用LLM对MemorySeed中的文本内容进行深度加工。这个过程不是简单的复制粘贴而是要求LLM扮演一个“信息提炼师”的角色。其提示词Prompt的核心指令可能如下“请将以下对话内容提炼成一份结构化的Markdown格式摘要。要求如下使用##二级标题概括本段对话的核心主题。使用列表-或1.列出对话中涉及的关键事实、用户偏好、待办事项或重要结论。对任何提到的代码、配置项或关键数据请使用 代码块进行包裹。将重要的实体如项目名、人名、时间用粗体标出。如果对话中包含了需要后续跟进的‘任务’或‘问题’请单独用一个### 待办标题列出来。”通过这样的指令LLM的输出就不再是一段普通的回复文本而是一份自带层级结构的Markdown文档。例如一段关于“部署项目”的对话可能被加工成如下形式## 项目A的服务器部署讨论 - 用户决定采用 **Docker** 容器化部署方案。 - 服务器环境为 **Ubuntu 22.04**IP地址为 192.168.1.100。 - 数据库密码已设置为 Str0ngPss!并记录于安全位置。 yaml # docker-compose.yml 关键配置 version: 3.8 services: app: image: myapp:latest environment: - DB_PASSWORD${DB_PASSWORD}待办需要在服务器上安装docker-compose。下周一下午3点检查部署状态。这份Markdown文档就是一条结构化的“记忆”。它比原始对话更精炼信息密度更高并且由于Markdown的语法其内部结构对机器后续的检索模块和人类维护者查看都变得可读、可解析。 **2.3 记忆的存储向量化与索引** 加工好的Markdown记忆会进入存储层。这里通常采用双轨制存储 1. **原始文本存储**将Markdown格式的记忆原文以时间戳和会话ID为索引存入数据库如SQLite、PostgreSQL或文件系统。这相当于“源文件”备份用于完整查看或重新处理。 2. **向量化存储**这是实现智能检索的关键。系统会使用文本嵌入模型Embedding Model将每一条Markdown记忆转换为一个高维度的向量Vector。这个向量在数学空间中的位置代表了这段记忆的“语义”。所有记忆的向量会被存入专门的向量数据库如Chroma、Qdrant、Milvus或PGVector。 Markdown格式在这里再次体现出优势。由于标题、加粗文本、代码块等内容通常包含了信息的核心与精华一些高级的实现会在向量化时为这些部分赋予更高的权重或者甚至将标题、列表项单独拆分出来进行向量化从而在检索时能更精准地匹配到关键信息点。 **2.4 记忆的检索在需要时被唤醒** 当新的用户查询到来时系统不会直接去翻阅所有历史对话的“长文本”而是启动检索流程 1. 首先将用户的当前问题也进行向量化。 2. 然后在向量数据库中执行相似性搜索Similarity Search找出与当前问题语义最接近的几条历史“记忆”。 3. 最后将这些检索到的、结构化的Markdown记忆片段作为最相关的上下文与当前问题一起组装成新的提示词提交给LLM生成最终回复。 这个过程相当于智能体在回答前快速“回忆”起了与当前话题最相关的几段“结构化笔记”而不是通读一本混乱的日记。这极大地提升了上下文利用的效率和精准度。 ## 3. 源码核心模块拆解如何用代码实现这套流水线 理论流程清晰后我们深入到 nanobot 的源码层面看看各个模块是如何具体实现的。虽然无法看到确切的代码但我们可以根据其设计理念和常见模式推断出关键模块的职责和可能的实现方式。 **3.1 记忆处理器MemoryProcessor 类** 这个类是流水线的“中央处理器”。它可能包含以下主要方法 - capture(raw_message): 接收原始消息进行基础清洗和包装生成 MemorySeed 对象。 - process(seed): 核心方法。它持有与LLM交互的客户端负责构造加工记忆的Prompt调用LLM API并解析返回的Markdown文本。这里需要处理LLM调用失败、输出格式不符合预期等异常情况。 - _build_summarization_prompt(seed): 一个私有方法专门用于构建那个要求输出Markdown的提示词模板。模板的优劣直接决定了记忆加工的质量。 一个简化的伪代码示例 python class MemoryProcessor: def __init__(self, llm_client, embedding_client): self.llm llm_client self.embedder embedding_client def process(self, memory_seed): # 1. 构建Prompt prompt self._build_markdown_prompt(memory_seed.content) # 2. 调用LLM进行加工 try: response self.llm.chat_completion(prompt) markdown_memory response.choices[0].message.content except Exception as e: # 降级策略如果LLM加工失败至少保存原始文本的简单摘要 markdown_memory f## 原始对话摘要\n- {memory_seed.content[:200]}... # 3. 验证和清理Markdown格式可选 cleaned_memory self._sanitize_markdown(markdown_memory) # 4. 创建记忆对象 memory Memory( idgenerate_uuid(), session_idmemory_seed.session_id, raw_contentmemory_seed.content, markdown_contentcleaned_memory, timestampmemory_seed.timestamp ) return memory def _build_markdown_prompt(self, content): # 返回一个精心设计的提示词字符串 return f请将以下对话内容提炼成结构化的Markdown摘要 {content} 请遵循以下格式要求 ...具体格式要求如前文所述... 3.2 记忆存储管理器MemoryStore类这个类负责与底层数据库打交道实现双轨制存储。save(memory): 接收一个Memory对象。其内部逻辑可能是将memory.markdown_content和元数据ID时间戳等存入关系型数据库的memories表。调用嵌入模型将memory.markdown_content转换为向量。将向量和对应的memory.id存入向量数据库。search(query, top_k5): 接收一个查询字符串query。首先使用同样的嵌入模型将query转换为查询向量。然后调用向量数据库的similarity_search接口获取最相似的top_k个向量结果及其对应的memory.id。最后根据这些id去关系型数据库中取出完整的Memory对象包含Markdown原文返回给调用者。3.3 记忆检索与上下文组装器ContextBuilder类这个类在每次需要生成回复时被调用。build_context(current_query, session_id): 它的工作流程是调用MemoryStore.search(current_query)获取相关记忆列表。将这些记忆的markdown_content按照时间或相关性排序拼接成一个大的上下文字符串。这里可能需要处理长度限制如果拼接后超出模型上下文窗需要采用策略如优先保留相关性最高的、或对较旧的记忆进行二次摘要进行截断。将拼接好的记忆上下文、当前查询以及系统指令System Prompt组合成最终发送给LLM的完整提示词。class ContextBuilder: def __init__(self, memory_store, max_context_tokens8000): self.store memory_store self.max_tokens max_context_tokens def build(self, query, session_id): # 1. 检索相关记忆 related_memories self.store.search(query, top_k10) # 2. 组装上下文 context_parts [# 相关历史记忆回顾] total_estimated_tokens 0 for memory in related_memories: mem_content memory.markdown_content mem_tokens estimate_tokens(mem_content) # 估算token的函数 if total_estimated_tokens mem_tokens self.max_tokens: break # 达到上限停止添加 context_parts.append(mem_content) total_estimated_tokens mem_tokens final_context \n\n.join(context_parts) return final_context4. 实战中的挑战与优化策略实现一个可用的Markdown记忆系统只是第一步要让它在生产环境中稳定、高效、经济地运行还需要解决一系列工程挑战。4.1 加工质量的稳定性如何让LLM“听话”地输出Markdown这是最大的不确定性来源。LLM并不总是严格遵循格式指令。你可能会得到没有标题的文本、使用HTML标签而不是Markdown、或者列表格式混乱的输出。优化策略一Prompt工程强化。除了给出格式描述可以在Prompt中提供一个完美的示例Few-shot Learning。例如“请严格按照如下示例的格式和风格输出” 然后附上一个标准的Markdown记忆样本。这能显著提升LLM输出的规范性。优化策略二后处理与纠错。实现一个MarkdownSanitizer模块对LLM的原始输出进行清洗。例如使用正则表达式将**粗体**不规范的空格去掉将1.开头的列表项纠正为1.甚至将误用的HTML标签如b替换为Markdown语法。也可以集成一个轻量级的Markdown解析器如mistune尝试解析如果解析失败则触发一个降级的格式化流程。优化策略三模型选择与调参。经验表明某些模型在遵循指令和格式化输出方面表现更佳例如GPT-4优于GPT-3.5。同时调整LLM的temperature降低以增加确定性和response_format参数如果API支持指定格式也能有所帮助。4.2 处理成本与延迟的平衡每一次对话都调用LLM进行记忆加工意味着双倍的LLM API调用一次生成回复一次加工记忆成本和延迟都会翻倍。优化策略一异步与批处理。记忆加工不必阻塞实时回复。可以将MemorySeed放入一个任务队列如Redis, RabbitMQ由后台工作进程异步、批量地处理。用户能立刻得到回复而记忆则在后台“慢慢”形成。这牺牲了一点记忆的“实时性”但换来了更好的用户体验和可能更低的批量API调用成本。优化策略二条件性触发加工。并非所有对话都值得形成长期记忆。可以设置规则仅当对话超过一定轮数、或用户主动标记如“记住这个”、或检测到对话中包含特定关键词如“项目”、“配置”、“密码”时才触发记忆加工流程。对于简单的问候和闲聊则跳过此步骤。优化策略三使用更小、更快的模型进行加工。记忆加工任务对“创造性”要求不高但对“遵循指令”和“概括能力”要求高。可以考虑使用专门微调过的、参数更小的模型如一些7B/13B的微调模型来处理这个任务从而大幅降低成本。4.3 向量检索的精准度问题Markdown文档可能包含多种信息简单的全文向量化可能无法让检索聚焦于核心。优化策略分块与加权向量化。在将Markdown记忆存入向量数据库前先对其进行“智能分块”。例如将每个##标题及其下属内容作为一个独立的块。将每个代码块作为一个独立的块。将### 待办部分单独作为一个块。 然后对不同的块类型可以赋予不同的元数据权重。在检索时可以优先检索“标题块”和“待办块”因为它们通常包含了最凝练的主题和行动项。这相当于为记忆建立了更精细的“索引目录”。4.4 记忆的更新、合并与遗忘记忆不是只增不减的。用户可能修正之前的说法“我之前说的截止日期是周五其实是下周一”或者一个任务被完成后其“待办”状态需要更新。优化策略实现记忆的版本管理与关联。当检测到新对话与某条旧记忆高度相关且内容可能冲突时系统可以尝试触发一个“记忆合并”流程将新旧两条记忆的Markdown内容再次提交给LLM指令其生成一份统一的、更新后的版本。同时在存储层面需要建立记忆之间的关联关系如“取代了”、“细化了”而不是简单地覆盖或新增以便于追溯信息的变化历程。对于过时或无用的记忆可以设计基于时间、使用频率的“遗忘”算法将其归档或删除保持记忆库的活性。5. 对比与展望Markdown记忆系统的独特价值与传统的对话历史拼接方法相比nanobot这类Markdown驱动的记忆系统其优势是显而易见的信息密度高节省上下文窗口一份结构化的摘要其信息量可能等同于原始对话的5-10轮内容但Token消耗可能只有原来的1/3。这直接延长了有效对话的轮次并降低了API成本。结构清晰便于机器与人类理解Markdown的标题、列表等结构天然适合后续的自动化处理如分块、关键信息提取和人工审查。维护者打开记忆库看到的是一份条理清晰的“会议纪要”而不是杂乱无章的聊天记录。检索精度提升通过基于语义的向量检索系统能更准确地找到相关记忆。结合Markdown分块技术甚至可以做到“在记忆的某个小节中”进行精准定位。当然这套系统也引入了新的复杂度LLM加工的不确定性、异步处理架构、向量数据库的维护等。它更适合对对话质量、长期上下文有较高要求的场景如智能客服、个人知识管理助手、项目协作机器人等。从我个人的实践经验来看引入类似的结构化记忆系统是智能体从“玩具”走向“工具”的关键一步。它迫使开发者以“知识管理”的视角而非“日志记录”的视角来设计对话系统。nanobot的源码实现为我们提供了一个很好的范本展示了如何利用现有工具链LLM Embedding VectorDB和一种简单的格式标准Markdown来构建一个相对优雅且强大的记忆中枢。未来的优化方向可能会集中在加工流程的自动化评估与调优、多模态记忆的支持如何将图片、文件对话也结构化摘要、以及更复杂的记忆图谱Memory Graph构建上让智能体的“回忆”不仅准确还能富有逻辑和关联性。