
之前做 AI Agent 应用时最头疼的往往不是模型能力而是“记忆”。试过向量数据库、图数据库、KV 存储也接过各种 Agent Memory 框架要么是接入成本高要么是返回结果不可解释。后来在一个内部 Benchmark 里发现一个基于 Markdown 文件的 Wiki 方案在多项指标上都超过了那些专门的 Agent Memory 产品。这篇文章就围绕这个结论展开为什么简单的 Markdown 能赢以及如何把它落地成一个可用的 Agent 记忆系统。如果你正在做智能客服、AI 编程助手、个人知识库或 Agent 工作流本文的内容可以直接复用。我会从 Benchmark 的设计思路讲起再给出完整的目录结构、存储代码、检索代码和 LLM 接入示例。1. 背景与核心概念1.1 什么是 Agent MemoryAgent Memory 是 AI Agent 用来保存和回忆信息的能力。可以把 Agent 理解成一个没有长期记忆的人每一次对话、每一次任务执行对模型来说都是全新的开始。为了让 Agent 能记住用户偏好、项目上下文、历史决策和历史错误就需要一个“外部记忆系统”。常见的 Agent Memory 类型有短期记忆当前会话内的上下文通常由大模型上下文窗口承担。长期记忆跨会话保留的信息比如用户习惯、历史对话摘要、任务执行记录。工作记忆Agent 在执行多步骤任务时临时存放的中间状态。长期记忆是当前工程落地的重点也是一个“看起来简单、做起来复杂”的问题。1.2 主流的 Agent Memory 方案有哪些目前业界常见的 Agent 记忆产品和技术方案大概分四类向量数据库类把文本切块用 Embedding 模型转成向量存入 Milvus、Pinecone、Weaviate、Chroma 等查询时做相似度检索。图数据库类用 Neo4j 等存储实体和关系适合需要多跳推理的记忆场景。KV 或文档存储类用 Redis、MongoDB、S3 等保存会话摘要和结构化字段。框架内置记忆比如 LangChain 的 Memory 模块、LlamaIndex 的 ChatMemoryBuffer以及各家 Agent 平台自带的 Memory 服务。这些方案各有优势但在实际工程里也各有痛点向量数据库需要维护 Embedding Pipeline图数据库需要设计 Schema框架内置记忆则经常出现“存入容易、取出不准”的问题。1.3 为什么 Markdown Wiki 能成为黑马Markdown Wiki 的核心思路非常朴素用 Markdown 文件作为记忆载体用文件目录作为索引用规范化的命名和 Frontmatter 作为元数据。LLM 本身就极其擅长阅读和生成 Markdown所以这个方案天然适合 AI Agent。它带来的优势很直接可读性强开发者可以直接打开文件查看 Agent 记住了什么不需要额外工具。可修改性强出了问题可以直接改文件不需要写脚本操作数据库。可解释性强检索结果返回的是原文能够追溯来源。零依赖只需要文件系统和少量 Python 代码不需要部署数据库。与主流工具链兼容VS Code、Obsidian、Typora 都能直接打开和编辑这些文件。这不是说 Markdown Wiki 能取代向量数据库而是说在很多 Agent 记忆场景中它的“够用性”和“可维护性”远超那些复杂系统。2. 基准测试思路为什么 Markdown 能胜出标题提到“一个 Markdown wiki outscored every AI agent memory product we benchmarked”很多人第一反应是惊讶。实际上如果我们把 Benchmark 的维度拆开看就会发现这个结果并不奇怪。2.1 为什么需要 BenchmarkAgent Memory 产品往往会在官方介绍里放一些漂亮指标比如“召回率 95%”“延迟 100ms”。但在真实项目中效果和这些指标经常对不上。原因在于官方测试数据通常经过清洗而生产环境的数据是杂乱、重复、跨主题的。因此一个可信的 Benchmark 至少需要覆盖以下维度检索准确率给定一个查询是否能返回真正相关的记忆。响应延迟从发起查询到拿到结果的耗时。数据可修改性修正错误记忆的成本有多高。可解释性返回结果时能否清楚解释“为什么返回这条”。开发接入成本集成到现有 Agent 项目的成本。长期维护成本索引更新、数据清理、容量扩张是否麻烦。2.2 一个可复现的对比实验设计下面给出一个通用的实验设计思路你可以用你自己的数据集复现。这里不写死具体数字因为不同数据集和实现方式差异很大但流程是通用的。实验准备准备 500 条记忆条目覆盖用户偏好、项目任务、常见问题、历史对话摘要等类型。准备 50 个查询问题确保有明确答案且答案分布在不同的记忆条目中。分别接入三种方案方案 A 为向量数据库方案 B 为某 Agent Memory 框架方案 C 为 Markdown Wiki 关键词检索。评估流程对每种方案分别执行 50 个查询。记录返回结果中正确命中的数量。记录平均响应耗时。尝试对某条记忆执行“修改过期内容”的操作记录完成时间和操作步骤。这个实验本身并不复杂但可以很直观地反映出方案差异。Markdown Wiki 在“修改成本”和“可解释性”上几乎是碾压级的在数据量不大、主题边界清晰的场景下检索准确率也不落下风。2.3 什么情况下 Markdown Wiki 不适用任何方案都有边界Markdown Wiki 也不例外。如果你的 Agent 需要处理以下几类场景还是要考虑向量数据库或图数据库海量非结构化文本超过几十万条记忆文件名和关键词索引已经不够用。语义模糊查询用户说的是“上次那个蓝色按钮的改动”但记忆库里没有“蓝色按钮”这几个字。多跳关联推理需要跨多个实体推理比如“哪个客户投诉了某个订单然后退款了”。高并发写入多 Agent 实例同时高频读写同一个文件目录会带来锁竞争和一致性问题。理解了这个边界你就能判断什么时候该用 Markdown Wiki什么时候该升级方案。3. 设计一个 Markdown 记忆系统3.1 目录结构设计Markdown 记忆系统的核心是“像组织 Wiki 一样组织文件”。推荐使用下面的目录结构markdown-memory/ ├── core/ │ ├── __init__.py │ ├── storage.py # 记忆文件的读写操作 │ ├── index.py # 目录索引构建 │ ├── search.py # 关键词检索与排序 │ └── llm.py # LLM 调用封装可选 ├── memory/ # 记忆根目录 │ ├── users/ # 按用户维度 │ │ ├── user-001.md │ │ └── user-002.md │ ├── projects/ # 按项目维度 │ │ ├── project-001.md │ │ └── project-002.md │ ├── conversations/ # 按对话摘要维度 │ │ └── 2025-06-01.md │ └── index.md # 总索引页 ├── scripts/ │ └── init_memory.py └── tests/ └── test_storage.py设计原则是先按“领域”分目录再按“实体”分文件最后在文件内部用 Frontmatter 和标题组织结构化内容。3.2 文件命名与 Frontmatter 规范文件命名要做到“见名知意”。推荐格式是类型-名字或日期-描述user-张三.mdproject-订单系统优化.mdconversation-2025-06-01-上午.md每个 Markdown 文件建议在开头加入 Frontmatter 元数据方便程序解析--- title: 用户-张三 tags: [user, preference, 产品讨论] created: 2025-06-01T10:00:00 updated: 2025-06-02T16:30:00 status: active source: conversation/conversation-2025-06-01.md --- # 用户张三 ## 偏好 - 喜欢简洁接口 - 对响应速度敏感 - 不愿意使用复杂操作流程 ## 关键背景 - 当前负责订单模块 - 团队规模 5 人 - 技术栈以 Java Vue 为主 ## 最近动态 - 2025-06-01讨论了订单列表分页优化 - 2025-06-02确认需要支持 Excel 导出Frontmatter 里的updated字段很重要它用来判断记忆是否过期、是否需要归档。source字段用于追溯记忆来源这能极大提升可解释性。3.3 记忆条目模板为了保证不同文件之间的结构一致应该定义一套基础模板。下面是一个通用的“项目记忆模板”--- title: 项目-订单系统优化 tags: [project, 订单, 性能] created: 2025-05-20 updated: 2025-06-02 status: active --- # 项目订单系统优化 ## 背景 简要描述项目背景和解决的问题。 ## 当前进度 - [x] 分页接口优化 - [ ] 导出功能开发 - [ ] 压测验证 ## 关键决策 记录重要的技术选型和决策原因。 ## 待办事项 - 补充超时异常处理 - 联调第三方物流接口 ## 踩坑记录 记录本项目遇到的问题和解决方式。模板的价值在于降低维护成本。Agent 每次写入记忆时只需要按照模板的字段结构去填充内容后续检索和解析都会更容易。4. 完整实战从零搭建 Markdown 记忆库下面进入实操部分。这里会从项目初始化开始逐步完成一个可用的 Markdown 记忆系统核心代码。4.1 初始化项目首先创建项目目录mkdir -p markdown-memory/{core,memory,scripts,tests} cd markdown-memory python3 -m venv venv source venv/bin/activate本项目的 Python 版本建议使用 3.9 及以上不需要额外的第三方依赖。为了让 Agent 记忆系统具备扩展能力后续可以按需安装requests和向量检索相关库。4.2 实现记忆文件读写核心代码文件路径core/storage.py Markdown 记忆文件读写模块。 提供 create / read / update / delete 四个基础操作。 所有操作都使用 utf-8 编码并包含基本路径安全校验。 import os import re from datetime import datetime from pathlib import Path class MarkdownMemory: def __init__(self, memory_root: str memory): self.memory_root Path(memory_root) self.memory_root.mkdir(parentsTrue, exist_okTrue) def _safe_path(self, relative_path: str) - Path: 将相对路径转换为绝对路径并防止路径穿越攻击。 path self.memory_root / relative_path # resolve 会解析 .. 和符号链接这里做一层校验 resolved path.resolve() if not str(resolved).startswith(str(self.memory_root.resolve())): raise PermissionError(f非法路径: {relative_path}) return resolved def _ensure_suffix(self, path: Path) - Path: 确保文件以 .md 结尾。 if path.suffix ! .md: path path.with_suffix(.md) return path def create_memory(self, relative_path: str, content: str, overwrite: bool False) - str: 创建一条新记忆。 - relative_path: 例如 users/user-001.md - content: Markdown 正文内容 - overwrite: 是否允许覆盖已存在文件 path self._ensure_suffix(self._safe_path(relative_path)) path.parent.mkdir(parentsTrue, exist_okTrue) if path.exists() and not overwrite: raise FileExistsError(f记忆文件已存在: {relative_path}) # 自动更新 Frontmatter 中的 created / updated 字段 now datetime.now().isoformat(timespecseconds) content self._ensure_frontmatter(content, now, now) path.write_text(content, encodingutf-8) return str(path) def read_memory(self, relative_path: str) - str: 读取一条记忆。 path self._ensure_suffix(self._safe_path(relative_path)) if not path.exists(): raise FileNotFoundError(f记忆文件不存在: {relative_path}) return path.read_text(encodingutf-8) def update_memory(self, relative_path: str, new_content: str) - str: 更新一条记忆。 会把 updated 字段替换为当前时间。 path self._ensure_suffix(self._safe_path(relative_path)) if not path.exists(): raise FileNotFoundError(f记忆文件不存在: {relative_path}) now datetime.now().isoformat(timespecseconds) new_content self._ensure_frontmatter(new_content, None, now) path.write_text(new_content, encodingutf-8) return str(path) def delete_memory(self, relative_path: str) - bool: 删除一条记忆。 注意此操作不可恢复生产环境中建议先备份。 path self._ensure_suffix(self._safe_path(relative_path)) if not path.exists(): return False path.unlink() return True def list_memories(self, sub_dir: str ) - list: 递归列出指定目录下的所有 .md 文件。 base self._safe_path(sub_dir) if not base.exists(): return [] result [] for file in base.rglob(*.md): relative file.relative_to(self.memory_root) result.append(str(relative)) return result def _ensure_frontmatter(self, content: str, created: str, updated: str) - str: 确保 Markdown 内容包含 Frontmatter。 如果已有 Frontmatter则只替换 updated 字段。 frontmatter_pattern r^---\n(.*?)\n---\n match re.match(frontmatter_pattern, content, re.DOTALL) if match: inner match.group(1) lines inner.split(\n) filtered [line for line in lines if not line.startswith(created:) and not line.startswith(updated:)] if created: filtered.insert(1, fcreated: {created}) filtered.insert(1, fupdated: {updated}) frontmatter ---\n \n.join(filtered) \n---\n rest content[match.end():] return frontmatter rest else: meta f---\ncreated: {created}\nupdated: {updated}\n---\n return meta content这段代码的核心是_safe_path方法它避免了路径穿越问题。在 Agent 场景中路径通常由大模型生成如果不加校验模型可能会写入../../之类的路径造成安全风险。4.3 构建记忆索引与检索文件路径core/index.py 目录索引模块。 扫描 memory 目录下所有 Markdown 文件提取标题、标签和更新时间。 import re from datetime import datetime from pathlib import Path class MemoryIndex: def __init__(self, memory_root: str memory): self.memory_root Path(memory_root) self.index {} self.rebuild() def rebuild(self): 全量重建索引。 self.index.clear() if not self.memory_root.exists(): return for file in self.memory_root.rglob(*.md): relative str(file.relative_to(self.memory_root)) meta self._extract_meta(file) meta[path] relative self.index[relative] meta def _extract_meta(self, file: Path) - dict: 提取 Frontmatter 中的 title、tags、updated 等字段。 text file.read_text(encodingutf-8, errorsignore) meta { title: file.stem, tags: [], updated: None, created: None, } frontmatter_pattern r^---\n(.*?)\n---\n match re.match(frontmatter_pattern, text, re.DOTALL) if match: for line in match.group(1).split(\n): if line.startswith(title:): meta[title] line.split(:, 1)[1].strip() elif line.startswith(tags:): raw line.split(:, 1)[1].strip() meta[tags] [x.strip() for x in raw.strip([]).split(,) if x.strip()] elif line.startswith(updated:): meta[updated] line.split(:, 1)[1].strip() elif line.startswith(created:): meta[created] line.split(:, 1)[1].strip() return meta def get_all(self) - dict: return self.index文件路径core/search.py 轻量级检索模块。 这里使用简单的关键词 权重排序方式不依赖外部服务。 当数据量增长到万级以上时再考虑接入向量检索。 import re from collections import Counter class MemorySearch: def __init__(self, memory_index): self.index memory_index def search(self, query: str, top_k: int 5) - list: 在索引中执行关键词匹配。 返回格式为 [{path, title, tags, score, snippet}] query_words self._tokenize(query) results [] for rel_path, meta in self.index.get_all().items(): title meta.get(title, ) tags meta.get(tags, []) score 0 # 标题命中权重最高 title_words self._tokenize(title) for qw in query_words: if qw in title_words: score 3 for tag in tags: if qw in tag: score 2 if score 0: results.append({ path: rel_path, title: title, tags: tags, score: score, }) results.sort(keylambda x: x[score], reverseTrue) return results[:top_k] def _tokenize(self, text: str) - set: 按中英文常见情况做简单分词。 text text.lower() words set(re.findall(r[\w\u4e00-\u9fff], text)) return words这个检索模块确实很“简陋”但它是理解 Agent Memory 检索流程的最好起点。当关键词匹配不足以覆盖语义查询时可以在检索层加入向量召回再把 Markdown 中的内容作为候选文档输给 LLM。4.4 接入 LLM 的完整示例文件路径core/llm.py LLM 接入模块。 使用 OpenAI 兼容的 HTTP API通过环境变量配置 base_url 和 api_key。 在真实项目中请把密钥放入环境变量或密钥管理服务不要写死在代码里。 import os import requests def chat_with_memory(query: str, context: str, model: str None) - str: 将检索到的 Markdown 记忆作为上下文连同用户问题一起发送给大模型。 api_key os.getenv(LLM_API_KEY, ) base_url os.getenv(LLM_BASE_URL, https://api.openai.com/v1) model model or os.getenv(LLM_MODEL, gpt-4o-mini) url f{base_url}/chat/completions headers { Authorization: fBearer {api_key}, Content-Type: application/json, } system_prompt ( 你是一个 AI Agent需要基于给定的记忆上下文来回答用户的问题。 如果记忆上下文中没有相关信息请如实说明不要编造。\n\n f【记忆上下文】\n{context} ) payload { model: model, messages: [ {role: system, content: system_prompt}, {role: user, content: query}, ], temperature: 0.3, } response requests.post(url, headersheaders, jsonpayload, timeout30) response.raise_for_status() return response.json()[choices][0][message][content]实际调用时你只需要先检索出相关的 Markdown 片段拼成上下文然后调用chat_with_memory即可。4.5 运行与验证写一个简单的主程序来验证整个流程。文件路径scripts/demo.pyimport sys sys.path.insert(0, .) from core.storage import MarkdownMemory from core.index import MemoryIndex from core.search import MemorySearch # 1. 初始化记忆库 memory MarkdownMemory(memory_rootmemory) # 2. 写入两条记忆 content1 --- title: 项目-订单系统优化 tags: [project, 订单, 性能] --- # 项目订单系统优化 ## 当前进度 - 分页接口优化已完成 - 导出功能开发中 ## 踩坑记录 - 大批量导出时内存溢出改用流式写入 content2 --- title: 用户-张三 tags: [user, 偏好, 订单] --- # 用户张三 ## 偏好 - 喜欢简洁接口 - 对响应速度敏感 memory.create_memory(projects/project-001.md, content1) memory.create_memory(users/user-001.md, content2) # 3. 重建索引并检索 index MemoryIndex(memory_rootmemory) searcher MemorySearch(index) results searcher.search(订单接口优化, top_k3) print(检索结果) for r in results: print(f- {r[path]} (score{r[score]})) # 4. 读取具体记忆内容 if results: print(\n命中的具体内容) print(memory.read_memory(results[0][path]))预期输出效果大致如下检索结果 - projects/project-001.md (score5) - users/user-001.md (score3) 命中的具体内容 --- title: 项目-订单系统优化 tags: [project, 订单, 性能] updated: 2025-06-02T10:00:00 created: 2025-06-02T10:00:00 --- ...到这里你已经拥有一个最小可用的 Markdown 记忆系统了。5. 常见问题与排查思路问题现象常见原因解决思路Agent 写入的 Markdown 文件缺少 Frontmatter模型没有严格遵循模板在后处理代码中强制补全 Frontmatter 字段缺失字段使用默认值检索结果不准确相关记忆排不到前面关键词匹配粒度不够观察查询词是否与记忆文本存在“同义不同词”必要时引入 Embedding 向量召回记忆文件越来越多搜索越来越慢全量扫描目录导致增量更新索引或按子目录分段建立索引多个 Agent 同时写同一个文件内容互相覆盖并发写入无锁为每个 Agent 分配独立目录或使用文件锁 版本号控制修改了 Markdown 文件但检索结果没更新索引缓存未刷新每次写入后手动调用rebuild()或增加文件监听机制Agent 把敏感信息写入记忆文件缺少安全过滤在写入前增加敏感词检测和白名单目录控制关键信息加密后再存储中文标题文件名在部分系统中无法访问文件名编码问题用拼音或英文 ID 作为文件名把中文标题放在 Frontmatter 的 title 字段中下面重点说明两个最容易踩坑的问题。5.1 大模型输出的 Markdown 不完整LLM 在生成记忆文件时有时会漏掉 Frontmatter 的---或把 tags 写成中文字符串而不是数组。解决方案不是强行要求模型输出完美格式而是让程序容错。建议在写入层做一次规范化缺失 title 时用文件名替代。tags 不是合法列表时用逗号分隔字符串补充解析。updated 字段缺失时用当前时间补齐。5.2 向量数据库和 Markdown 如何协同如果你已经拥有向量数据库也不冲突。常见的模式是“Markdown 作为源数据向量索引作为加速层”。Markdown 文件还是唯一数据源向量库里只保存切块后的向量和指向 Markdown 文件的引用。这样既保留了 Markdown 的可读性又能获得语义检索能力。6. 最佳实践与工程建议6.1 设计上遵循“人机共读”Markdown 记忆系统最大的优势是“人也能读”。所以设计时不要只考虑程序解析效率也要考虑人类阅读体验。建议做到标题语义化目录分层清晰。每个文件只保存一个主题避免臃肿。正文使用列表、表格和短段落方便 LLM 理解。定期人工抽查记忆文件及时修正模型写入的低质量内容。6.2 写入策略先摘要后入库不要让 Agent 把原始对话全量写入记忆。正确做法是先让 LLM 对对话做摘要再把摘要写入 Markdown。例如用户说了一堆关于订单导出格式的抱怨 → LLM 摘要用户希望增加 CSV 导出且日期格式为 yyyy-MM-dd → 写入 project-001.md 的 TODO 或需求记录这样记忆文件会非常精炼检索效率和后续维护成本都会好很多。6.3 更新策略保留历史避免覆盖对重要记忆文件不要直接覆盖内容。推荐追加“历史版本”区域或者把历史内容移动到archive/目录。Agent 在输出时也更容易理解“哪个信息是最新的”。6.4 安全与权限控制Markdown 记忆文件往往包含用户信息和项目内部信息需要注意以下几点将 memory 目录纳入 Git 版本管理但不要提交到公开仓库。敏感字段如密钥、身份证号不要明文写入 Markdown需要加密后存储。文件写入要限制在 memory 根目录内防止路径穿越。生产环境遵循最小权限原则Agent 服务只允许读写指定目录。6.5 何时升级到向量检索当记忆文件数量超过 1 万条且你明确遇到以下情况时再考虑引入向量检索用户经常用同义不同词的方式描述同一件事。跨多个文件的语义关联频繁出现。关键词检索的命中率低于 70%。升级时只需要在MemorySearch内部增加一个向量召回通道不需要推翻整个 Markdown 设计方案。7. 写在最后回到标题那句话一个简单的 Markdown wiki 能胜过专门的 AI Agent 记忆产品根本原因不是数据库技术不够先进而是很多 Agent 记忆场景的复杂度根本没到需要数据库撑场面的程度。用文件系统 规范化命名 Frontmatter 元数据已经能解决 80% 的长期记忆问题而且可维护性更好。如果你正在评估 Agent 记忆方案可以先花半天时间把本文的代码跑起来再把真实记忆数据灌进去试试。你会发现把“记忆”这种听起来很玄的能力落实到一堆 Markdown 文件上反而更容易控制、更容易调优也更容易让团队成员理解 Agent 到底记住了什么。等跑通这套方案之后再决定是否需要引入向量数据库或其他重型组件。