為 Mintlify 的 AI Assistant 構建虛擬檔案系統
為 Mintlify 的 AI Assistant 構建虛擬檔案系統
RAG(檢索增強生成)很棒,直到它不再適用為止。
我們的 Assistant 只能檢索與查詢相符的文字區塊。如果答案分散在多個頁面中,或者使用者需要精確的語法卻沒有出現在前 K 個結果中,它就會卡住。我們希望它能像探索程式庫一樣探索文件。
Agent 正逐漸將檔案系統作為其主要介面,因為 grep、cat、ls 和 find 就是 Agent 所需的一切。如果每個文件頁面都是一個檔案,而每個章節都是一個目錄,那麼 Agent 就可以自行搜尋精確的字串、閱讀完整頁面並遍歷整個結構。我們只需要一個能反映即時文件網站的檔案系統。
容器瓶頸
最顯而易見的做法是直接給 Agent 一個真實的檔案系統。大多數解決方案是透過啟動一個隔離的「沙盒」並複製儲存庫來實現。我們已經在非同步背景 Agent 中使用了沙盒,那裡的延遲問題可以忽略不計,但對於使用者正盯著載入轉圈圈的前端 Assistant 來說,這種方法完全行不通。我們 p90 的工作階段建立時間(包括 GitHub clone 和其他設定)大約是 46 秒。
除了延遲之外,為閱讀靜態文件而配置專用的微型虛擬機(micro-VM)會帶來沉重的基礎設施費用。
以每月 850,000 次對話計算,即使是最小的配置(1 vCPU、2 GiB 記憶體、5 分鐘工作階段壽命),根據 Daytona 的每秒沙盒定價(每 vCPU 每小時 $0.0504,每 GiB 記憶體每小時 $0.0162),我們每年的成本將超過 70,000 美元。更長的工作階段時間會使成本翻倍。(這是基於純粹天真的做法,真正的生產環境工作流程可能會有預熱池和容器共享,但重點依然存在。)
我們需要檔案系統的工作流程既即時又便宜,這意味著必須重新思考檔案系統本身。
偽造一個 Shell
Agent 不需要真實的檔案系統;它只需要一個檔案系統的幻覺。我們的文件已經被索引、分塊並儲存在 Chroma 資料庫中以支援搜尋功能,因此我們構建了 ChromaFs:一個虛擬檔案系統,它會攔截 UNIX 指令並將其轉換為針對同一個資料庫的查詢。工作階段建立時間從約 46 秒縮短至約 100 毫秒,且由於 ChromaFs 重用了我們已經付費的基礎設施,因此每次對話的邊際運算成本為零。

ChromaFs 是基於 Vercel Labs 的 just-bash 構建的(向 Malte 致敬!),這是一個支援 grep、cat、ls、find、cd 等功能的 TypeScript 版 bash 重新實作。just-bash 公開了一個可插拔的 IFileSystem 介面,因此它處理了所有的解析、管道傳輸和旗標邏輯,而 ChromaFs 則將每個底層檔案系統呼叫轉換為 Chroma 查詢。
export class ChromaFs implements IFileSystem {
private files = new Set<string>();
private dirs = new Map<string, string[]>();
async readFile(path: string): Promise<string> {
this.assertInit();
const normalized = normalizePath(path);
// Serve from cache or fetch from Chroma
const slug = normalized.replace(/\\.mdx$/, '').slice(1);
// Pages are chunked in Chroma. Reassemble them on the fly:
const results = await this.collection.get<ChunkMetadata>({
where: { page: slug },
include: [IncludeEnum.documents, IncludeEnum.metadatas],
});
const chunks = results.ids
.map((id, i) => ({
document: results.documents[i] ?? '',
chunkIndex: parseInt(String(results.metadatas[i]?.chunk_index ?? 0), 10),
}))
.sort((a, b) => a.chunkIndex - b.chunkIndex);
return chunks.map((c) => c.document).join('');
}
// Enforce completely stateless, read-only interaction
async writeFile(): Promise<void> { throw erofs(); }
async appendFile(): Promise<void> { throw erofs(); }
async mkdir(): Promise<void> { throw erofs(); }
async rm(): Promise<void> { throw erofs(); }
}
運作原理
引導目錄樹
ChromaFs 需要在 Agent 執行任何指令之前知道有哪些檔案存在。我們將整個檔案樹作為一個經過 gzip 壓縮的 JSON 文件(__path_tree__)儲存在 Chroma 集合中:
{
"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 會根據目前使用者的權限修剪檔案樹,並將相符的篩選器套用到所有後續的 Chroma 查詢中。
在真實的沙盒中,這種層級的個別使用者存取控制需要管理 Linux 使用者群組、chmod 權限,或為每個客戶層級維護隔離的容器映像檔。而在 ChromaFs 中,這只是在執行 buildFileTree 之前幾行的篩選程式碼。
從區塊重組頁面
Chroma 中的頁面為了嵌入而被分割成區塊,因此當 Agent 執行 cat /auth/oauth.mdx 時,ChromaFs 會獲取所有具有相符頁面 slug 的區塊,按 chunk_index 排序,並將它們合併成完整的頁面。結果會被快取,因此在 grep 工作流程中重複讀取時,永遠不會兩次存取資料庫。
並非每個檔案都需要存在於 Chroma 中。我們註冊了延遲檔案指標(lazy file pointers),這些指標會在存取時解析,用於儲存在客戶 S3 儲存貯體中的大型 OpenAPI 規格。Agent 在 /api-specs/ 中看到 v2.json,但內容只有在執行 cat 時才會獲取。
每個寫入操作都會拋出 EROFS(唯讀檔案系統)錯誤。Agent 可以自由探索,但永遠無法修改文件,這使得系統保持無狀態,無需清理工作階段,也不存在一個 Agent 破壞另一個 Agent 視圖的風險。
優化 Grep
cat 和 ls 的虛擬化很簡單,但如果 grep -r 天真地透過網路掃描每個檔案,速度會太慢。我們攔截了 just-bash 的 grep,使用 yargs-parser 解析旗標,並將其轉換為 Chroma 查詢(固定字串使用 $contains,模式使用 $regex)。
Chroma 作為一個粗略的篩選器,識別出哪些檔案可能包含搜尋結果,我們將這些相符的區塊批次預取(bulkPrefetch)到 Redis 快取中。隨後,我們重寫 grep 指令,使其僅針對相符的檔案,並將其交回給 just-bash 進行記憶體內的精確篩選執行,這意味著大型遞迴查詢可以在毫秒內完成。
const chromaFilter = toChromaFilter(
scannedArgs.patterns,
scannedArgs.fixedStrings,
scannedArgs.ignoreCase
);
// 1. Coarse Filter: Ask Chroma for slugs matching the string/regex
const matchedSlugs = await chromaFs.findMatchingFiles(chromaFilter, slugsUnderDirs);
if (matchedSlugs.length === 0) return { stdout: ‘’, exitCode: 1 };
// 2. Prefetch: Pull the chunked files into local cache concurrently
await chromaFs.bulkPrefetch(matchedSlugs);
// 3. Fine Filter: Narrow the arguments to ONLY the resolved hits
const matchedPaths = matchedSlugs.map((s) => ‘/’ + s + ‘.mdx’);
const narrowedArgs = [...args, ...matchedPaths]; // e.g. ["-i", "OAuth", "/docs/auth.mdx"]
// 4. Exec: Let the in-memory RegExp engine format the final output
return execBuiltin(narrowedArgs, ctx);
結論
ChromaFs 為每天超過 30,000 次對話、數十萬名使用者的文件 Assistant 提供支援。透過以我們現有的 Chroma 資料庫之上的虛擬檔案系統取代沙盒,我們實現了即時的工作階段建立、零邊際運算成本,並在無需任何新基礎設施的情況下內建了 RBAC(角色存取控制)。
歡迎在任何 Mintlify 文件網站或 mintlify.com/docs 上試用。
[閱讀完整文章:https://www.mintlify.com/blog/how-we-built-a-virtual-filesystem-for-our-assistant]
