ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Markdown当数据库:构建AI会话存储的文件协议实践

用Markdown当数据库:构建AI会话存储的文件协议实践 1. 为什么是 Markdown轻量数据层的前世今生1.1 AI 会话存储的“三座大山”这两年我带团队做了不少 AI 应用从客服知识库到内部 Copilot绕来绕去总会撞上同一个问题AI 的对话数据到底存哪里一开始大家习惯走常规路子上 MySQL、PostgreSQL甚至直接怼一个 MongoDB觉得这才叫“正经数据库”。但搞着搞着就发现AI 会话这种数据跟传统业务数据完全是两种脾气。第一座大山是平台锁定。今天用 A 家的模型调 API明天换 B 家的开源模型会话格式各有各的脾气字段名对不上、角色定义不一样迁移一次就想骂人。第二座大山是格式碎片化。前端传过来的消息可能带 function call、带工具结果、带引用片段、带图片 base64用关系型数据库存这种半结构化嵌套数据要么疯狂拆表要么干脆塞 JSON 字段最后查询时一脸懵。第三座大山是检索困难。等会话攒到几万条你想知道“上个月跟用户讨论过哪些关于权限设计的方案”在传统数据库里写 SQL 能写到怀疑人生更别说做语义检索了。这不是说传统数据库不行而是用错了场景。AI 会话数据本质上是“文档型”数据它的读写模式更接近文件操作而不是事务操作。你很少需要“update 某条消息 set 内容 新内容”更多时候是追加写入、批量读取、按主题归档、跨会话检索。这个需求一旦想明白“把 Markdown 当数据库”这个方案就呼之欲出了。1.2 Markdown 天生自带的“半结构化”底子很多人一提 Markdown 就觉得它只是个写文档的标记语言这种印象太浅了。你仔细拆开看Markdown 其实天生自带一套轻量级的数据结构表达能力。标题层级对应树状分类列表对应数组表格对应结构化记录引用块对应注释或来源代码块对应原始数据负载再加一个 YAML front-matter连元数据都有地方放了。换句话说Markdown 是一种“半结构化”格式它既不像纯文本那样完全没有约束也不像 XML/JSON 那样需要严格的标签配对。这种性格特别适合 AI 会话数据——你不能保证每一条消息都长得一模一样工具调用有时有参数有时没有引用片段有时候是一段代码有时候是网页链接这种“差不多但又不完全一样”的数据正好是 Markdown 的舒适区。更关键的是Markdown 是人类可读的。这一点在 AI 应用里被严重低估。会话数据你总要调试吧总要人工复核吧总要拿去喂给下一个模型做 few-shot 吧如果是二进制格式或者严格的 JSON 嵌套出了问题排查起来很痛苦。但 Markdown 文件用纯文本编辑器打开就能看用 VS Code 打开还能渲染哪里不对劲一眼就能瞅出来。1.3 文件协议听起来玄乎其实就是“约定”“文件协议”这个词听起来像是要搞什么大工程其实本质就是一套约定目录怎么建、文件怎么命名、字段往哪里放、正文怎么分段、元数据用什么格式。只要参与读写的各方——你的 Python 脚本、AI Agent、前端工具、甚至你自己——都遵守这套约定那么 Markdown 文件就不仅仅是给人看的文档而是一个可以被程序批量处理的“数据库”。这跟 HTTP 协议、TCP/IP 协议是同一个思路只是层级完全不同。协议的本质是“通信双方共同遵守的格式规范”文件协议就是这个意思AI 会话产生方按约定写文件消费方按约定读文件双方不依赖同一个数据库实例也不依赖同一个平台只要文件在数据就在这就是文件协议的威力——解耦存储与消费。我个人的观点是在 AI 应用越来越轻量、越来越个性化的今天过于笨重的数据层往往是瓶颈而不是支撑。一个目录、一堆 Markdown 文件、一套简单约定就能覆盖大量 AI 会话管理、提示词版本管理、Agent 记忆存储等场景。这不是要替代数据库而是给“不需要数据库杀鸡用牛刀”的场景一个更顺手的选项。2. 整体架构把目录变成表把文件变成行2.1 先设计目录结构你的“表结构”既然决定把 Markdown 当数据库那么设计表结构的第一步就变成了设计目录结构。我在实际项目中沉淀了一套比较通用的布局你可以根据自己的场景裁剪ai-sessions/ ├── sessions/ # 会话主目录相当于数据表 │ ├── 2025-06/ │ │ ├── 20250601-0930-用户登录流程设计.md │ │ ├── 20250601-1430-权限模型讨论.md │ │ └── 20250602-1000-数据库索引优化方案.md │ └── 2025-07/ ├── templates/ # 是会话模板目录相当于字段默认值 │ └── session-template.md ├── archives/ # 已归档的会话相当于冷数据 ├── assets/ # 附件和图片相当于 BLOB 存储 └── index.md # 总索引相当于数据库的 catalog按月份分子目录是为了避免单个文件夹里文件过多导致文件系统性能下降同时方便按时间范围做冷热归档。每个会话文件名采用“日期-时间-主题”的三段式命名日期是YYYYMMDD时间是HHMM主题用英文短横线连接。这样光看文件名就能排序、筛选、定位甚至不需要打开文件内容。提示主题部分不要用中文倒不是中文不行而是有些工具链对中文文件名的处理不够友好尤其在跨平台同步和 URL 引用时容易出幺蛾子。可以在文件内部用 front-matter 存中文标题。2.2 Front-matter每个文件的“元数据卡片”YAML front-matter 是 Markdown 文件头部用---包裹的元数据区域这是整个“文件即数据库”方案里最核心的约定之一。为什么强调它因为正文是给人看的、给模型读的而元数据是给程序用的。两者混在一起会导致解析复杂度飙升分开就清爽多了。下面是我常用的一个会话文件 front-matter 模板字段不算多但覆盖了大部分检索和管理需求--- session_id: 20250601-0930 title: 用户登录流程设计 created_at: 2025-06-01T09:30:0008:00 updated_at: 2025-06-01T11:20:0008:00 model: gpt-4o platform: custom-agent tags: - 登录 - 产品设计 - 会话 participants: [product_manager, ai_assistant] status: active related_sessions: - 20250601-1430 ---这里的每个字段都有存在理由。session_id是主键全局唯一建议跟文件名保持一致方便相互引用。model记录当时用的是哪个模型这个字段在复盘时很有价值——同一个问题不同模型的表现对比靠它就能拉出来。tags是标签索引相当于给每条记录加二级索引。related_sessions是实现关联查询的关键相当于外键。我自己踩过的一个坑是updated_at一开始不记后来发现追加消息后想知道“这条会话最近一次活跃是什么时候”完全抓瞎只能靠文件系统的修改时间但文件一旦被同步工具碰过修改时间就失真了。所以这类时间字段一定要由程序写入不要依赖文件系统。2.3 会话 ID 与命名的“主键策略”在传统数据库里主键的选择是个大学问。在文件协议里文件名承担了这个职责。我推荐的主键格式是“时间戳 会话主题”原因有三一是天然有序按文件名排序就是按时间排序二是可读性强不需要查字典就知道这个文件大概在讲什么三是冲突概率极低同一分钟里开两个同主题会话的概率几乎可以忽略。如果你需要更强的唯一性可以在后面加一段随机短码比如20250601-0930-用户登录流程设计-a3f9.md。短码可以用 Python 的secrets.token_hex(2)生成4 位十六进制足够在单目录下避免冲突了。不要用 UUID 当文件名一长串无意义的字符会让目录变成灾难现场。3. 文件协议核心约定Markdown 里的“数据字段”3.1 用标题层级表达会话轨迹目录和文件名只是骨架正文里的结构约定才是灵魂。我在多次迭代后打磨出一套“标题层级协议”核心思想是用##表示一轮完整的“用户-助手”交互用###表示这一轮里的子步骤比如工具调用、消息片段等。看一个实际例子## 用户帮我查一下昨天的订单数据 ### 用户指令 原始输入帮我查一下昨天的订单数据 ### 助手响应 好的我来查询昨天的订单数据。 ### 工具调用query_orders 参数{date: 2025-06-01, status: all} 结果共 128 条订单总金额 45,230 元 ### 助手最终回答 昨天共有 128 条订单总金额 45,230 元。需要我按支付方式拆分一下吗这套结构的好处是无论是人读还是程序解析都能很清楚地知道哪条是用户消息、哪条是助手消息、哪条触发了工具调用、工具返回了什么。而且这种格式对模型的上下文构建特别友好把它拼接成对话历史喂回给模型时角色边界非常清晰不会出现角色错乱的问题。这里有个细节值得注意不要用#一级标题来标记对话轮次。因为在实际使用中一级标题往往要留给文档大标题如果对话结构和文档标题混用解析时会产生歧义。##到####的结构足够表达三层嵌套再深就该考虑拆文件了而不是继续堆层级。3.2 表格与列表存储结构化信息AI 会话里经常会出现需要结构化呈现的信息比如对比方案、列出清单、给出参数表。我在文件协议里约定这类信息一律用 Markdown 表格或有序列表表达因为这不仅是给人看的更是给程序做抽取用的。举个例子如果 AI 在一次会话中对比了三种数据库方案| 方案 | 性能 | 一致性 | 运维成本 | 适用场景 | |------|------|--------|----------|----------| | MySQL | 中 | 强 | 中 | 传统业务系统 | | MongoDB | 高 | 弱 | 中 | 文档型数据 | | SQLite | 低 | 强 | 低 | 边缘设备、单机应用 |后续写脚本分析这类会话时用 Python 的pandas.read_html或者简单的正则匹配就能把这些表格批量抽取出来汇总成总表。这个能力在传统数据库里反而麻烦——你存进去的时候是结构化的但 AI 回答的时候是自然语言抽取和转换的逻辑一样不能少。列表的用法也一样凡是枚举类型的信息比如“三个备选方案”“五个注意点”“七步操作流程”我都要求 AI 在回答时按列表输出并在文件协议里明确记录。这样后续做知识库构建时拆条、打标、向量化就特别顺畅。3.3 用代码块封装 JSON/PayloadAI 会话中不可避免会遇到结构化数据的传递典型场景包括工具调用的参数、API 返回值、模型返回的 JSON 片段、配置文件等。这些内容如果直接以纯文本形式混在正文里解析时会有两个问题一是难以区分“这是在说代码”还是“这就是代码”二是里面的花括号、引号可能跟 Markdown 语法冲突。我的约定是所有程序数据一律用带语言标识的围栏代码块包裹并且明确标注这是什么类型的数据。json:tool_params {date: 2025-06-01, status: all} json:tool_result {total: 128, amount: 45230.00} 加了这个自定义标识符之后解析脚本就可以直接定位所有json:tool_params类型的数据块不需要上下文推断。这就相当于在文件协议里定义了“字段类型”比纯文本约定严谨了一个数量级。注意 这个四反引号围栏是我故意用的为的是示例里能显示三层反引号。实际使用时如果代码块内容里已经有三重反引号嵌套比如教你写 Markdown 的 Markdown就用四个反引号做外层的围栏这是一个很实用的小技巧。3.4 双链与引用让会话之间产生关联文件协议不光是给机器读的也是给人设计的一种“知识网络”。我在目录里专门留了related_sessions字段同时在正文里也支持双链语法只要在文件里写[[20250601-1430]]或者[[20250602-1000-数据库索引优化方案]]就表示这条会话引用了另一条会话。这招是跟 Obsidian 学的。一开始我只是把它当记录习惯后来发现这个设计在构建 AI 知识库时有奇效。当我要把一批会话喂给模型做行业分析时可以用脚本自动解析双链关系构建一个会话之间的引用图谱相当于给模型提供了一条“沿着引用找上下文”的路径比把全部会话一股脑塞进上下文的效果好得多。4. 写入端实操把 AI 会话改写成 Markdown 文件4.1 写入工作流怎么搭API → 格式化 → 落盘明确了协议之后接下来的问题就是怎么把 AI 会话自动写成符合协议的文件。我推荐的写入工作流分三步采集、格式化、落盘。采集环节指的是从 API 请求或 Webhook 里拿到原始会话数据这一步各个平台差异很大。如果你的 AI 应用是自己开发的那直接在回调函数里把消息列表接住就行如果你用的是现成的平台比如各种 Agent 平台就需要通过它们的 Webhook 或 API 把消息拉出来。不管哪种方式核心目标是拿到一个按时间排序的消息数组。格式化环节是根据我们前面定义的协议把消息数组转换成 Markdown 字符串。这一步的关键在于“确定性”——同一份消息数组不管跑多少次生成的 Markdown 必须完全一致。这样后续做校验、做差异对比才有可能。落盘环节就简单了把字符串写入文件更新 front-matter 时间戳如果有多轮追加还需要处理文件已存在的情况。4.2 代码示例一个简单可靠的 Python 写入器下面这个代码是我在实际项目里用的写入器简化版核心逻辑没有删减可以直接抄import os import re import json import secrets from datetime import datetime from pathlib import Path SESSION_ROOT Path(./ai-sessions/sessions) def generate_session_id(theme: str) - str: 生成会话 ID时间戳 主题 随机短码 now datetime.now() ts now.strftime(%Y%m%d-%H%M) safe_theme re.sub(r[^a-z0-9\-], -, theme.lower()).strip(-) rand secrets.token_hex(2) return f{ts}-{safe_theme}-{rand} def build_frontmatter(meta: dict) - str: 根据元数据字典生成 YAML front-matter 字符串 lines [---] for key, value in meta.items(): if isinstance(value, list): rendered , .join(f{item} for item in value) lines.append(f{key}: [{rendered}]) elif isinstance(value, str): lines.append(f{key}: {value}) else: lines.append(f{key}: {value}) lines.append(---) return \n.join(lines) def serialize_message(msg: dict) - str: 把一条消息转换成符合协议的 Markdown 块 role msg.get(role, unknown) content msg.get(content, ).strip() tool_calls msg.get(tool_calls, []) tool_result msg.get(tool_result) sections [f## {role.capitalize()}] if content: sections.append(content) for call in tool_calls: sections.append(f### 工具调用{call.get(name, unknown)}) if call.get(arguments): sections.append( fjson:tool_params\n{json.dumps(call[arguments], ensure_asciiFalse, indent2)}\n ) if tool_result is not None: sections.append(### 工具结果) if isinstance(tool_result, (dict, list)): sections.append( fjson:tool_result\n{json.dumps(tool_result, ensure_asciiFalse, indent2)}\n ) else: sections.append(str(tool_result)) return \n\n.join(sections) def append_message(session_id: str, msg: dict, theme: str untitled) - None: 追加一条消息到指定会话文件文件不存在则自动创建 month_dir SESSION_ROOT / datetime.now().strftime(%Y-%m) month_dir.mkdir(parentsTrue, exist_okTrue) file_path month_dir / f{session_id}.md serialized serialize_message(msg) if file_path.exists(): # 如果文件已存在直接追加并更新 updated_at with open(file_path, a, encodingutf-8) as f: f.write(\n\n serialized) # 重新生成 frontmatter 里的 updated_at用正则替换 update_timestamp(file_path) else: # 新文件带完整 front-matter 创建 meta { session_id: session_id, title: theme, created_at: datetime.now().isoformat(timespecseconds), updated_at: datetime.now().isoformat(timespecseconds), model: msg.get(model, unknown), tags: [], participants: [], status: active, } content build_frontmatter(meta) \n\n serialized \n with open(file_path, w, encodingutf-8) as f: f.write(content) def update_timestamp(file_path: Path) - None: 更新已存在文件的 updated_at 字段 text file_path.read_text(encodingutf-8) now datetime.now().isoformat(timespecseconds) updated_text re.sub( r(updated_at: )[^]*(), f\\g1{now}\\g2, text, count1, ) file_path.write_text(updated_text, encodingutf-8)这段代码里有几个细节值得展开说一下。generate_session_id里把主题做了降级处理转小写、只保留字母数字和短横线这样生成的文件名在任何操作系统上都不会出问题。serialize_message中对工具调用做了单独处理每个工具调用都标记了工具调用xxx的二级标题下面用带标识的代码块包裹参数解析时天然就能区分。追加消息的时候没有重写整个文件而是以 append 模式打开直接写尾部性能上完全不是问题。但要注意追加模式下不会自动更新 front-matter 的updated_at所以我在追加后额外调用了一次update_timestamp用正则只替换第一个updated_at字段的值这样既保证了元数据准确又不需要重写整个文件。4.3 对接工具链把写入器变成工作流的一环脚本本身只是基础真正要落地成“工作流”还得把它接到你现有的工具链里。我在实际项目中尝试过几种接入方式体验各不相同。第一种是 CRON 定时拉取。如果你的 AI 应用会把会话缓存到某个中间存储比如 Redis 列表可以写一个定时任务每 5 分钟批量拉取新会话并落盘。优点是简单可靠缺点是实时性差一点。第二种是消息队列驱动。在 AI 应用的回调里把消息推送到消息队列RabbitMQ 或者 Kafka写入器作为消费者异步处理。这样实时性高而且天然解决了并发写入的问题——消息队列保证同一时间只有一条消息被消费避免多进程同时写同一个文件的竞争。第三种是事件订阅推送。一些 AI 平台提供了会话结束事件或新消息事件的 Webhook可以配置一个 HTTP 服务接收推送直接在处理器里调用上面的append_message函数。这种方式耦合度最低只要平台支持 Webhook 就能接。我个人比较推荐第二种。原因很直接AI 会话的写入峰值不可预测可能白天一小时只有几条晚上某个用户批量处理数据时突然涌进来几百条。如果没有队列缓冲直接把压力打到文件系统上容易出现文件锁冲突。有了队列之后写入器按自己的节奏消费数据一条都不会少。4.4 增量写入多轮会话的“续写”策略多轮会话是 AI 场景的常态用户跟助手聊了十轮、二十轮你得保证每一轮都追加到同一个文件里。实现层面依赖两样东西会话 ID 的稳定传递和追加逻辑的正确性。会话 ID 的传递我一般建议在 AI 应用的最外层就生成好然后透传到所有环节包括前端页面、后端 API、模型调用参数这样整个链路上都拿着同一个 ID 操作文件。如果等会话结束才生成 ID中间过程没法落盘一旦进程崩溃就会丢数据。追加逻辑的正确性除了上面代码里的 append 模式之外还有一个容易忽略的点文件编码。务必统一使用 UTF-8 编码读写并且 Python 里显式指定encodingutf-8不要依赖操作系统默认编码。我踩过一次坑在 Windows 上跑默认编码是 GBK写进去的中文看起来正常但换到 macOS 上读取就乱码了排查半天才发现是编码问题。5. 读取端把 Markdown 当查询接口来用5.1 三种轻量检索姿势grep、正则、脚本文件型数据库在读取端最大的优势就是“万物皆可查”。不需要启动数据库服务不需要写 SQL任何有文本处理能力的工具都能来消费。最简单粗暴的是直接用grep适合快速定位。比如我想找所有提到“权限”的会话grep -rl 权限 ./sessions/-r是递归-l是只输出文件名。一次扫描所有文件秒级出结果。如果嫌路径太深不好记忆可以在项目根目录配一个 aliasalias ssearchgrep -rl --include*.md稍微复杂一点的场景比如“找出所有在某天创建的、标签包含‘数据库’的会话”grep 就有点吃力了这时候可以用 Python 脚本做结构化解。from pathlib import Path import yaml import re def find_sessions(tagsNone, date_prefixNone): results [] for md_file in Path(./sessions).rglob(*.md): text md_file.read_text(encodingutf-8) m re.match(r^---\n(.*?)\n---, text, re.DOTALL) if not m: continue meta yaml.safe_load(m.group(1)) if tags and not set(tags).issubset(set(meta.get(tags, []))): continue if date_prefix and not md_file.name.startswith(date_prefix): continue results.append(meta) return results # 用法找所有 20250601 当天创建的、带“数据库”标签的会话 sessions find_sessions(tags[数据库], date_prefix20250601) for s in sessions: print(s[session_id], s[title])这段脚本的思路是把“解析 front-matter”和“扫目录”结合起来先 grep 处理不了的结构化条件再对候选文件做精确过滤。文件量在一万以下时这种 Python 脚本的性能完全够用跑一次也就是几百毫秒的事。5.2 向量化从文件库到语义检索纯文本检索解决不了“我记得聊过这个话题但不记得用的什么词”的问题。这时候就需要向量化了。不过很多教程一上来就让你搭 Milvus、搞 PGVector我觉得对文件协议场景来说完全是杀鸡用牛刀。更贴合的做法是用文件协议作为“文档分割”的天然边界把每个##标题对应的内容块抽出来作为一条独立文档然后调用嵌入模型比如 text-embedding 系列生成向量最后用一个轻量的向量库比如chromadb或者sqlite-vec存储向量。文件名和块标题作为元数据检索到了就能直接定位到具体文件、具体段落。这个方案的优雅之处在于Markdown 的标题结构已经帮你把文本切好了。你不需要用什么专门的文档分割器去猜哪里是语义边界——AI 会话里每次用户提问和每次工具调用天然就是一个独立的语义单元。标题就是锚点代码块就是载荷向量化之前几乎不需要额外清洗。我跑过一次两千多条会话的向量化总共花了大概三分钟索引文件才 200MB 左右查询时间在几十毫秒内。这个量级的性能个人项目和中小团队完全够用没必要一开始就上分布式向量库。5.3 给 AI Agent 当记忆上下文把 Markdown 文件作为记忆存储是这套方案最有价值、也最能体现“文件协议”思想的应用场景。AI Agent 在一次会话中需要长期记住的用户偏好、历史决策、项目约束都可以沉淀为 Markdown 文件。具体做法是Agent 每次会话结束后从对话中抽取“值得记住的信息”按约定格式写入一个memory/目录下的 Markdown 文件。下次 Agent 启动时先扫一遍记忆目录把相关文件读进上下文再开始新的会话。这里有一个非常关键的体验用非结构化的自然语言“记住”做出来的记忆库往往不可用。因为 Agent 在写记忆时容易写一段含糊的话读的时候自己都看不懂。所以我在记忆文件的协议里明确要求了结构化格式## 用户偏好 - 语言中文为主英文术语保留 - 风格偏好简洁直接给结论 - 禁忌不喜欢嘲讽式回应 ## 项目约束 - 技术栈Python 3.11 FastAPI - 数据库PostgreSQL 15 - 部署单机 Docker Compose这样的结构对 Agent 来说提取容易、匹配精准。从记忆目录里检索“用户偏好”和“项目约束”两个字段的值拼成一个上下文块直接塞进 system prompt 前面的记忆区效果比把所有原始会话都塞进上下文好得多还大大减少了 token 消耗。5.4 可视化探索用现有工具消费文件库文件化存储还有一个隐性福利所有支持 Markdown 的现成工具都能直接用来消费这套数据。Obsidian 可以打开整个目录当作知识库还能基于双链生成关系图谱VS Code 可以批量搜索替换Typora 可以排版导出Git 可以做版本管理。我自己最喜欢的组合是“Obsidian Git”。Obsidian 负责浏览和书写Git 负责版本历史和多人协作。会话文件有任何改动Git 都记录得清清楚楚哪次会话在什么时候改过、谁改的一条命令就能查出来。这在复盘 AI 效果、追踪问题时非常有用——你是不是也在为“上星期那版 prompt 到底是什么样的”发愁文件化存储加 Git 提交记录直接解决。6. 常见问题与避坑实录6.1 正文里的代码块把解析器搞坏了这是 Markdown 当数据库最容易踩的坑。AI 会话里经常会讨论代码如果代码块里又嵌套了 Markdown 语法或者代码块本身内容里包含三重反引号就会导致解析器在找代码块结束位置时提前终止后面的内容全部沦为人眼都难分的纯文本。解决思路有两个层级。第一层是在写入时做转义如果代码内容里包含三重反引号就把外层围栏换成四个反引号这在前面已经提过。第二层是解析时用容错逻辑不要直接按正则匹配代码块而是用成熟的 Markdown 解析器比如 Python 的markdown-it-py它能正确处理多层围栏嵌套。6.2 并发写入导致的文件内容错乱多个进程同时往同一个 Markdown 文件追加内容轻则丢数据重则文件损坏。这个问题在引入消息队列之前我真实遇到过两个测试进程同时触发同一个会话的追加逻辑结果文件尾部出现了两次不完整的 front-matter。解决方案按推荐优先级排列第一引入消息队列或单消费者模型从架构上避免并发写同一个文件第二如果没办法用队列至少要在写入时加文件锁比如filelock库的FileLock第三写完后立即关闭文件句柄并且每次写入都做一次完整性校验读一遍文件确保 front-matter 解析正常。6.3 文件多了怎么办分区与分片当会话文件数量超过几万个单目录下的文件列表会变得有些迟钝ls都要等好几秒。我的建议是提前规划分区策略不要等文件多了再迁移。目录分区有两个维度按时间分区和按业务维度分区。按时间分区是最常见的做法比如按月建目录配合冷热归档策略——三个月前的文件自动移动到archives/目录日常检索只扫sessions/。按业务维度分区适合多项目并存的情况比如sessions/project-a/和sessions/project-b/这样不同项目的会话天然隔离权限控制和备份策略都能独立设置。6.4 跨机器同步时的文件冲突文件化数据库最怕的就是多台机器同时写同一批文件然后通过网盘工具同步。每次同步冲突都会生成一个“xxx 的冲突副本”目录很快就乱掉了。我的经验是如果有多人或多机协作需求果断用 Git而不是网盘。Git 对文本文件的合并能力远强于网盘工具虽然也会冲突但 Markdown 文件的冲突相对容易手工解决至少不会出现莫名其妙的重名副本。如果必须用网盘同步那就要在写入架构上做约束——保证同一时刻只有一个客户端在写其他客户端只读不变更这样基本不会触发冲突。写在最后的经验这套方法我一共用了大半年从最初只在个人项目里试验到后来带团队在小规模 AI 应用里正式采用中间踩过不少坑也推翻过几版设计。现在回头看的体会是Markdown 当数据库这件事能不能成立关键不在于格式本身而在于你是否愿意为自己的数据定义一个足够清晰的协议。协议越严解析越省心协议越松后期补坑的成本越高。如果你也想试一试我建议不要一上来就追求大而全的协议。先从最核心的两三条约定开始目录按时间分、文件名带时间戳和主题、front-matter 里记上必要元数据。等跑通了再逐步增加工具调用记录、双链引用、向量索引这些高级特性。数据量小的时候改协议成本极低一旦攒了几千个文件再回头改那才是真的痛苦。最后分享一个小技巧给写入脚本写一个自检模式定时随机抽查一部分文件校验 front-matter 是否完整、标题层级是否符合协议、代码块是否配对。这个检查看似多余但在 AI 生成内容不可控的情况下它是我睡得着觉的保障。
返回列表