ChromaFs:把文档检索伪装成文件系统
摘要
ChromaFs 的核心不是“又一种 RAG”,而是把已有的文档索引能力重新包装成 Agent 最擅长使用的接口:文件系统 + Shell 工具。相比传统“query → top-k chunk → prompt”的标准 RAG,它更强调:
- 保留知识结构:把页面视作文件、把站点层级视作目录。
- 把检索变成可探索过程:Agent 通过
ls/find/grep/cat/cd多步探索,而不是一次性赌 top-k。 - 复用现有索引基础设施:底层依然可用 Chroma 存 chunk 和元数据,但上层暴露为虚拟文件系统。
- 把权限控制前移到路径层:先裁剪目录树,再允许 Agent 探索,天然适合文档分层授权。
对 Agent 时代的 retrieval 来说,这篇文章最重要的启发是:很多场景真正需要的不是更“聪明”的向量召回,而是更“可操作”的信息接口。
来源
- 原文(Mintlify):https://www.mintlify.com/blog/how-we-built-a-virtual-filesystem-for-our-assistant
- 中文解读(知乎同文转发可见于腾讯云社区镜像):https://zhuanlan.zhihu.com/p/2023869911366115750
- 参考镜像:https://cloud.tencent.com/developer/article/2652369
背景:为什么标准 RAG 不够
Mintlify 的文档助手最初采用常见 RAG 流程:
- 把文档切成 chunks
- 为 chunk 建 embedding
- 用户 query 向量化
- top-k 召回相关 chunk
- 拼上下文给 LLM 生成答案
这个流程在“问一个模糊问题,返回几段相关上下文”时工作良好,但在以下场景容易失效:
1. 答案跨多个页面分布
如果真正答案分散在多个页面、多个章节、甚至需要顺着目录逐层定位,那么一次 top-k 召回很容易丢掉关键上下文。
2. 需要精确匹配而不是语义相似
比如:
- 某个精确配置项名
- 某个 API 字段
- 某段错误码
- 某个函数名 / 参数名 / 命令行 flag
这类问题更接近 grep,而不是 embedding similarity。
3. 文档原有结构被打散
目录、章节、路径、父子关系本身就是强语义信号。把它们先切碎,再只靠向量找回,本质上是在丢弃一部分人工组织好的知识结构。
ChromaFs 的核心想法
Agent 不一定需要真实文件系统,只需要“文件系统的错觉”。
Mintlify 已经有一套基于 Chroma 的文档存储与检索系统,因此他们没有再为每次会话拉起真实 sandbox,而是在已有数据库之上实现了一个虚拟文件系统:
- Agent 看到的是目录、文件、路径
- Agent 调用的是
ls/cd/find/grep/cat - 实际执行时,这些操作被翻译为对 Chroma 的元数据查询、chunk 拉取与缓存操作
这相当于把:
“面向 chunk 的检索系统”
改造成:
“面向 agentic exploration 的文件系统接口”
为什么不用真实 sandbox
Mintlify 原文给出的数据很有代表性:
- 真实 sandbox / clone repo 的 p90 会话创建时间约 46 秒
- 改为 ChromaFs 后,启动约 100 毫秒
- 由于复用现有 Chroma 基础设施,边际计算成本接近 0
他们估算:若每月约 85 万次对话,若每次都起轻量沙箱,仅基础设施成本每年就可能超过 7 万美元。
因此,ChromaFs 解决的是一个非常实际的工程约束:
- 前台用户不能等几十秒
- 纯读场景不值得起完整 VM / sandbox
- 已有索引基础设施应该被复用
架构拆解
1. 用 just-bash 承接 Shell 语义
Mintlify 基于 Vercel Labs 的 just-bash:
- 它是一个 TypeScript 实现的 bash 解释器
- 支持
grep、cat、ls、find、cd - 暴露可插拔的
IFileSystem接口
因此 Shell 解析、管道、flag 处理不需要自己从头实现;只要实现 filesystem backend 即可。
2. 预存目录树:__path_tree__
系统在 Chroma collection 中保存一个 gzip 压缩的 JSON 路径树,例如:
{
"auth/oauth": { "isPublic": true, "groups": [] },
"auth/api-keys": { "isPublic": true, "groups": [] },
"internal/billing": { "isPublic": false, "groups": ["admin", "billing"] },
"api-reference/endpoints/users": { "isPublic": true, "groups": [] }
}初始化时把它解压到内存,构建两类结构:
Set<string>:所有文件路径Map<string, string[]>:目录到 children 的映射
这样:
lscdfind
都可以直接在内存中完成,避免每次查询都打到数据库。
3. 文件内容通过 chunk 重组
当 Agent 执行:
cat /auth/oauth.mdxChromaFs 会:
- 查询 page slug 对应的全部 chunks
- 按
chunk_index排序 - 拼接成完整页面
- 放入缓存,避免重复读取时再次访问数据库
这一步把“向量检索时期为了 embedding 切碎的内容”,在读取时重新还原成“完整页面”。
4. grep 做两阶段过滤
如果 grep -r 对所有文件都走网络扫描,性能会很差。ChromaFs 的优化是:
阶段 A:数据库粗筛
把 grep 查询翻译为 Chroma 查询:
- 固定字符串 →
$contains - 正则模式 →
$regex
目标是找到“可能命中”的文件集合。
阶段 B:内存精筛
对粗筛得到的文件:
- bulk prefetch 相关 chunks 到 Redis / cache
- 再交给
just-bash在内存中做真正的 grep 匹配
也就是说,数据库负责 缩小候选集,bash 负责 忠实执行 grep 语义。
这个思路非常关键:
不要试图让数据库完整模拟 grep;让数据库做 coarse filter,让 shell/runtime 做 fine filter。
5. 权限控制在“构建树”时完成
路径树中附带:
isPublicgroups
系统先根据用户 token 与组权限裁剪目录树,再允许 Agent 访问。
这样带来的好处是:
- Agent 根本“看不见”无权限路径
- 不只是不能读,而且不能引用、不能猜路径
- 比在真实 sandbox 里维护 Linux 用户、组、chmod 或不同镜像简单很多
这个设计为什么适合 Agent
1. 它符合 LLM 的工具使用偏好
现代 agent 对以下接口天然友好:
- 文件
- 路径
- shell 命令
- 分步观察-行动循环
因为训练数据里本来就大量包含代码库、终端操作、目录浏览等模式。相比“请从 top-k chunk 里猜答案”,ls -> find -> grep -> cat 更接近 LLM 已熟悉的工作流。
2. 它把检索从“一次性召回”改成“可回溯探索”
传统 RAG 往往是:
query -> retrieve -> generateChromaFs 对应的是:
goal -> ls/find -> grep -> cat -> follow links/paths -> refine search -> answer这是一个更接近 agentic workflow 的闭环,允许:
- 多步定位
- 中途修正
- 先宽后窄
- 结合结构和内容信号
3. 它天然更可解释、更易调试
如果回答错了,可以直接追踪:
- 它看了哪些路径
- grep 命中了哪些文件
- cat 读了哪些页面
- 哪一步过滤掉了关键内容
比起“embedding 为什么没召回来这个 chunk”,可解释性更强。
4. 它复用了已有存储,而不是推倒重来
ChromaFs 并不是“抛弃向量数据库”,而是:
- 继续使用 Chroma 存 chunk / 元数据
- 继续利用现有 indexing pipeline
- 只是在 retrieval interface 层换了抽象
这说明一个重要工程原则:
真正要替换的往往不是底层存储,而是面向 Agent 的交互接口。
对 RAG 的真正修正
这篇文章最值得记住的一点是:
RAG != 向量数据库 top-k chunk retrieval
RAG 里的 “R” 是 Retrieval,检索手段完全可以多样化:
- 向量搜索:处理语义模糊性
- 全文搜索 / grep:处理精确匹配
- SQL:处理结构化过滤
- 图查询:处理关系遍历
- 文件系统遍历:处理层级结构与探索式导航
因此更合理的看法不是“RAG 已死”,而是:
单一的、扁平化的向量召回,不再足以支撑 Agent 时代的复杂 retrieval。
我学到的设计原则
1. 先设计信息接口,再设计召回算法
如果使用者是 Agent,优先问:
- 它最容易用什么接口探索?
- 它需要一步拿答案,还是多步查证?
- 它是否需要精确字符串匹配?
很多知识库场景中,答案是:
- 目录 + 文件 + grep 比单纯 top-k 更自然
2. 保留文档/代码原有结构是高价值信号
不要轻易把:
- 目录结构
- 页面边界
- 文件边界
- 章节顺序
- 权限边界
全部打平为 chunk。即使要切 chunk,也应该保留可回溯到 page/path 的结构信息。
3. coarse-to-fine 检索是高性价比套路
ChromaFs 的 grep 优化是一个非常通用的模式:
- 先用廉价索引缩小候选
- 再用昂贵但精确的执行器完成最终判定
这个模式可以迁移到:
- SQL + rerank
- inverted index + model reader
- metadata filter + full document reconstruction
- vector recall + deterministic verifier
4. 读多写少场景非常适合“只读虚拟系统”
如果目标数据是:
- 文档站
- 知识库
- 代码快照
- API 参考
那只读虚拟文件系统几乎天然成立:
- 无需 session cleanup
- 无并发写冲突
- 无状态更容易扩缩容
- 安全模型更简单
5. 权限应该尽量前置到“可见性”层,而不是只做读时拒绝
“用户看不见路径”通常比“读文件时报权限错误”更稳健,因为它减少了:
- prompt injection 猜路径
- side channel 信息泄露
- 工具调用级试探
局限与边界
ChromaFs 不是所有 retrieval 场景的通用答案。
更适合的场景
- 文档站 / 知识库 / API 文档
- 明确的页面边界和目录层级
- 读多写少
- 需要 grep / 精确匹配
- Agent 允许多步探索
不那么适合的场景
- 高度非结构化语料
- 需要跨文档深层语义聚合,但文件边界弱
- heavily personalized dynamic content
- 强写入型交互式工作流
在这些情况下,向量召回、图检索、SQL、甚至 workflow memory 仍然重要。
对 Agent / Memory 系统的启发
对长期记忆或助手知识库来说,这篇文章带来的启发非常直接:
1. 知识库应首先是“可遍历”的,而不只是“可召回”的
好的知识库不只是方便 embedding,而是要方便:
- 看目录
- 找主题
- 精确搜关键词
- 读原文
- 追溯来源
2. page/path 应该成为一级实体
相比只存 chunk,应该显式保存:
pathpagesectionchunk_indexvisibility / group
这样未来既能走向量检索,也能走虚拟文件系统或分层检索。
3. Agentic retrieval 往往比一次性 context stuffing 更稳
不要急着把“最像的 20 个 chunk”全塞给模型。 更稳的方式往往是:
- 先探索
- 再读取
- 再压缩
- 最后回答
4. 知识工程会重新重要起来
目录设计、命名规范、边界划分、权限模型、文档组织方式,在 Agent 时代不是次要问题,而是 retrieval 质量的重要组成部分。
可迁移到自己系统里的实现清单
如果以后在自己的 agent / memory 系统里借鉴 ChromaFs,我会优先考虑:
- 保留 page/path 级索引
- chunk 不能脱离页面边界
- 单独构建 path tree
- 目录树要能独立缓存
- 工具接口优先提供
ls/find/grep/cat语义- 不只提供
search(query)
- 不只提供
- 对 grep / 全文搜索采用 coarse-to-fine
- metadata/inverted index 初筛
- 原文精筛
- 把权限做成路径可见性过滤
- 不是最后一步才 deny
- 只读优先
- 先把读路径优化到极致,再考虑写入
- 多检索器并存
- filesystem / full-text / vector / SQL 各司其职
一句话结论
ChromaFs 的本质不是“用文件系统替代数据库”,而是“用 Agent 更擅长的文件系统接口,重组底层数据库能力”。
它提醒我:在 Agent 时代,最有效的 retrieval 不一定是最花哨的,而往往是最符合工具使用习惯、最保留信息结构、最容易解释和调试的那一种。
相关主题
topics/下后续可继续补:Agentic Retrieval、RAG 分层架构、知识库信息架构、Memory filesystem abstraction