Mintlify分享了AI文档助手的创新架构:用虚拟文件系统替代传统RAG,提升了检索效率和准确性。这个架构设计对构建AI应用的开发者有直接参考价值。
RAG 很好用——直到它不好用的那一刻。
我们的助手只能检索与查询匹配的文本分块。如果答案分散在多个页面中,或者用户需要的精确语法没有进入 top-K 结果,它就无能为力。我们希望它能像探索代码库一样探索文档。
Agent 正逐渐将文件系统作为主要交互界面,因为 grep、cat、ls 和 find 已经足以满足 Agent 的需要。如果每个文档页面都是一个文件,每个章节都是一个目录,Agent 就能搜索精确字符串、读取完整页面,并自行遍历文档结构。我们只需要一个能够实时映射线上文档站点的文件系统。
最直接的做法,就是给 Agent 一个真正的文件系统。大多数 Agent harness 都通过启动隔离的 sandbox 并克隆 repo 来解决这个问题。我们已经在异步后台 Agent 中使用 sandbox——在这种场景下,延迟并不是首要考虑因素。但对于前端助手来说,用户正盯着加载动画等待响应,这套方案就行不通了。我们的 p90 会话创建时间,包括 GitHub clone 和其他初始化工作,大约为 46 秒。
除了延迟,使用专用 micro-VM 读取静态文档,还会带来一笔高昂的基础设施账单:
以每月 85 万次对话计算,即使采用最低配置——1 个 vCPU、2 GiB RAM、会话生命周期 5 分钟——按照 Daytona 的 sandbox 按秒计费标准(每个 vCPU 每小时 0.0504 美元、每 GiB RAM 每小时 0.0162 美元),每年的成本也会超过 7 万美元。会话时长延长一倍,成本也会随之翻倍。(这里采用的是一种完全朴素的估算方式。真正的生产工作流可能会使用 warm pool 和容器共享,但核心问题依然存在。)
我们需要让文件系统工作流既即时又便宜,这意味着必须重新思考文件系统本身。
Agent 并不需要真正的文件系统,它只需要一个文件系统的假象。为了支持搜索,我们的文档已经完成了索引和分块,并存储在 Chroma 数据库中。因此,我们构建了 ChromaFs:一个可以拦截 UNIX 命令,并将其转换为针对同一数据库查询的虚拟文件系统。会话创建时间从约 46 秒降至约 100 毫秒;由于 ChromaFs 复用了我们已经付费使用的基础设施,每次对话产生的边际计算成本为零。
ChromaFs 基于 Vercel Labs 的 just-bash 构建(特别感谢 Malte!)。just-bash 是使用 TypeScript 重新实现的 bash,支持 grep、cat、ls、find 和 cd。just-bash 提供了可插拔的 IFileSystem 接口,因此命令解析、管道和 flag 逻辑都由它负责,而 ChromaFs 则将底层的每一次文件系统调用转换为 Chroma 查询。
查看 ChromaFs 的核心实现
在 Agent 执行第一条命令之前,ChromaFs 就需要知道存在哪些文件。我们将完整的文件树保存为经过 gzip 压缩的 JSON 文档(path_tree),并存储在 Chroma collection 中:
{
"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[]>。
构建完成后,ls、cd 和 find 都可以直接在本地内存中完成解析,不需要任何网络调用。目录树还会被缓存,因此同一站点的后续会话可以完全跳过从 Chroma 获取数据的过程。
请注意路径树中的 isPublic 和 groups 字段。在构建文件树之前,ChromaFs 会使用当前用户的 session token 对 slug 进行裁剪,并对之后的所有 Chroma 查询应用对应的过滤条件。如果用户无权访问某个文件,该文件就会被完全排除在目录树之外,因此 Agent 不仅无法访问它,甚至无法引用已经被裁剪掉的路径。
在真正的 sandbox 中,要实现这种细粒度的用户级访问控制,可能需要管理 Linux 用户组和 chmod 权限,或者为不同的客户等级维护彼此隔离的容器镜像。而在 ChromaFs 中,只需在 buildFileTree 运行前添加几行过滤代码。
Chroma 中的页面会被切分成多个分块用于 embedding。因此,当 Agent 执行 cat /auth/oauth.mdx 时,ChromaFs 会获取所有页面 slug 匹配的分块,按照 chunk_index 排序,然后将它们拼接成完整页面。结果会被缓存,因此在 grep 工作流中重复读取同一内容时,绝不会再次访问数据库。
并非所有文件都必须存在于 Chroma 中。对于存储在客户 S3 bucket 中的大型 OpenAPI spec,我们会注册惰性文件指针,仅在访问时解析。Agent 执行 ls /api-specs/ 时可以看到 v2.json,但只有当它执行 cat 时,系统才会真正获取文件内容。
所有写操作都会抛出 EROFS (Read-Only File System) error。Agent 可以自由探索,但永远无法修改文档。这让整个系统保持无状态,不需要清理会话,也不存在某个 Agent 破坏另一个 Agent 所见内容的风险。
cat 和 ls 的虚拟化相对直接,但如果 grep -r 朴素地通过网络扫描每个文件,速度会慢得无法接受。我们拦截 just-bash 的 grep,使用 yargs-parser 解析 flag,再将命令转换为 Chroma 查询:固定字符串使用 $contains,模式匹配则使用 $regex。
Chroma 充当粗粒度过滤器,用于识别哪些文件可能包含匹配内容。随后,我们通过 bulkPrefetch 将这些匹配的分块批量预取到 Redis cache 中。在此基础上,我们重写 grep 命令,使其只针对匹配到的文件执行,再将命令交还给 just-bash,在内存中完成细粒度过滤。因此,即使是大型递归查询,也能在几毫秒内完成。
查看 grep 优化的实现
ChromaFs 为数十万用户提供文档助手服务,每天处理超过 3 万次对话。通过在现有 Chroma 数据库之上构建虚拟文件系统来取代 sandbox,我们实现了即时创建会话、零边际计算成本和内置 RBAC,而且没有引入任何新的基础设施。
你可以在任意 Mintlify 文档站点或 mintlify.com/docs 上体验它。
在由 Mintlify 提供支持的文档站点中,Agent 目前已经占到可测量 Web 流量的 66%。截至 7 月即将结束时,当月记录的 Agent Web 请求已超过 2.13 亿次,而人类用户的页面加载量为 1.05 亿次。
我们在 20 个 Mintlify 文档站点上进行了 2,400 次测试,对比了四种向 AI Agent 提供文档的方式:HTML、纯 Markdown、包含 llms.txt 链接的 Markdown,以及内联 llms.txt 内容的 Markdown。结果发现,只需添加一个指向 llms.txt 的链接,就能在不增加成本的情况下消除绝大多数 Agent 404 错误。