Mirage 深度拆解:当 Agent 的世界被挂载成一棵目录树——「一切皆文件」如何终结 N 套 API 对接地狱
一、背景:Agent 工具箱的「熵增困境」
2026 年的 AI Agent 生态,热闹得有点失控。OpenAI Agents SDK、LangChain、CAMEL、Vercel AI SDK……框架一个比一个能打,但所有做过生产级 Agent 的工程师,都绕不开同一个日益膨胀的痛点:每接入一个新数据源,就要新造一批工具(Tool)。
想让 Agent 读 S3 上的日志?封装一套 s3_list_objects / s3_get_object。想让它搜 Slack 记录?再来 slack_search_messages / slack_get_thread。GitHub Issue、Notion 页面、Gmail 邮件、MongoDB 文档……每个服务一套 SDK、一套鉴权、一套分页逻辑、一套错误码。等你接完 10 个服务,Agent 的工具清单已经膨胀到五六十个函数。
这带来三个非常具体的工程恶果:
1. 提示词膨胀与工具选择错误率上升。 工具越多,塞进上下文的 JSON Schema 越长。经验数据是:当可选工具超过 30 个时,主流模型的工具误选率会显著抬升——模型开始把 slack_search 用在该用 gmail_search 的地方。你只能靠更长的描述文本去纠偏,进一步加剧上下文膨胀,形成负反馈循环。
2. 每个工具都是一次「一次性对接」。 s3_get_object 的产出没法直接喂给 slack_post_message,中间要靠模型在对话轮次里搬运数据——大文件在 token 里过一遍,既贵又容易截断。工具之间没有「管道」。
3. 工程团队重复劳动。 每个团队都在写自己的 Slack 工具、自己的 S3 工具,接口语义各不相同,无法复用。MCP(Model Context Protocol)试图用协议统一「工具的接口形态」,但它统一的是调用方式,不是数据的形态——你还是要面对几十个各自为政的 tool。
而讽刺的是,所有大模型最流利的「母语」根本不是这些 API——是 Bash。GPT、Claude、Gemini 在 Shell 脚本和 Unix 命令上的训练语料,比任何单一 SDK 的文档多几个数量级。cat、grep、head、wc、管道、重定向,对模型来说是肌肉记忆级别的存在。可现实是:AI 面对 Slack 消息不能 grep,面对 S3 文件不能 cat,面对 MongoDB 不能用管道串联。
Mirage 做的事情,就是把这个割裂补上。
Mirage 全称「A Unified Virtual File System for AI Agents」,由 strukto-ai 团队开发,2026 年 5 月 6 日发布首个公开版本 v0.0.1-alpha.1,Apache 2.0 协议,发布一周即斩获 2000+ Star,至今热度不减。它的核心思路一句话可以说完:
把所有数据源映射成同一个文件系统,让 Agent 用 Bash 统一操作。
这不是一个新点子——这是 Unix 五十年前的老点子(「一切皆文件」)在 Agent 时代的复活。但老点子用对了地方,威力惊人。
二、核心概念:为什么「文件系统」是 Agent 的最优抽象
2.1 从「N 个工具」到「一棵目录树」
Mirage 启动后,Agent 看到的世界是这样的:
/
├── s3/ ← S3 存储桶
├── slack/ ← Slack 工作区(频道是目录,消息是文件)
├── github/ ← GitHub 仓库
├── gmail/ ← Gmail 邮箱
├── gdrive/ ← Google Drive
├── notion/ ← Notion 工作区
├── mongo/ ← MongoDB(集合是目录,文档是文件)
├── redis/ ← Redis 缓存
├── ssh/ ← 远程 SSH 服务器
└── data/ ← 本地内存 / 磁盘
于是原本需要三个不同 SDK、几轮工具调用才能完成的「统计 S3 日志里的告警数量」,变成一行:
grep alert /s3/data/log.jsonl | wc -l
「把 S3 报表复制到 Notion 附件目录」变成:
cp /s3/report.csv /notion/weekly-review/attachments/
注意这里发生了什么质变:数据流不再穿过模型的上下文。cp 是在 Mirage 运行时内部完成的,无论文件 5KB 还是 5GB,占用的 token 都只有这一行命令。这是工具调用范式做不到的——工具范式下,get 的结果必须先回到模型,再作为 post 的参数发出去。
2.2 与 MCP 的关系:不是替代,是不同层次的统一
很多人第一反应是「这不就是另一个 MCP 吗」。不是。两者统一的东西不同:
| 维度 | MCP | Mirage |
|---|---|---|
| 统一对象 | 工具的调用协议 | 数据的呈现形态 |
| Agent 视角 | 几十个异构 tool,各有 schema | 一个文件树 + 一套 Unix 命令 |
| 组合能力 | 工具间无管道,靠模型搬运 | 原生管道/重定向,数据不过模型 |
| 学习成本 | 每个 tool 要读描述 | 模型预训练早就会了 |
| 上下文开销 | 随工具数线性增长 | 常数(一句「你有一个文件系统」) |
更准确的说法是:MCP 统一了「动词的语法」,Mirage 统一了「名词的形态」。 两者甚至可以叠着用——Mirage 自己就可以作为一个 MCP Server 暴露出去,对外只需要一个 tool:execute(command: string)。
这是一种极简主义的胜利:与其教 AI 一百种 API,不如把所有东西变成 AI 已经会的东西。
2.3 Workspace:可快照、可迁移的执行环境
Mirage 的另一个被低估的设计是 Workspace 的可移植性。一个 Workspace = 挂载表 + 缓存 + 本地数据,整体可以打成一个 tar 包:
mirage workspace snapshot demo demo.tar
mirage workspace load demo.tar --id demo-restored
这意味着:
- Agent 执行环境可以回滚——任务跑砸了,恢复快照重来,而不是祈祷幂等性;
- 执行现场可以迁移——本地调试完打包,扔到 Serverless 或另一台机器直接恢复,不用重新配置十几个服务的凭证挂载;
- 可以做「执行现场归档」——审计场景下,把 Agent 干活时看到的世界原样封存。
对做 Agent 平台的团队来说,这个特性直接对标的是「沙箱即状态」的工程需求,价值不亚于统一挂载本身。
三、架构分析:一个 VFS 引擎的三层解剖
从公开代码与文档看,Mirage 的架构可以拆成三层:命令执行层 → 虚拟文件系统层 → 资源适配层,外加贯穿其中的两级缓存。
3.1 资源适配层:Resource 抽象
每种后端实现一个 Resource,本质是把该服务的语义翻译成文件语义。这是整个系统里「脏活最多」的一层,因为不同服务与文件模型的匹配度差异极大:
- 天然契合:S3/GCS/R2 本来就是对象存储,key 即路径,几乎零翻译成本;
- 中度翻译:GitHub(仓库→目录,文件→文件,Issue→伪文件)、Notion(页面树→目录树);
- 重度翻译:Slack(频道→目录,消息流→按时间分片的文件?还是一条消息一个文件?)、Redis(key 扁平空间如何目录化)、MongoDB(文档→JSON 文件,查询如何表达)。
Mirage 当前的选择是务实的:优先保证读路径的一致性(ls/cat/grep 处处可用),写路径按后端能力渐进支持。首批支持的资源已经覆盖云存储(S3/R2/OCI/Supabase/GCS)、Google 全家桶(Gmail/GDrive/GDocs/GSheets/GSlides)、协作工具(Slack/Discord/Telegram/Email)、项目管理(GitHub/Linear/Notion/Trello)、数据库(MongoDB/Redis/PostgreSQL)和 SSH 远程机。
3.2 虚拟文件系统层:挂载表与路径路由
VFS 层维护一张挂载表,把路径前缀路由到对应 Resource:
ws = Workspace({
"/data": RAMResource(),
"/s3": S3Resource(S3Config(bucket="my-bucket")),
"/slack": SlackResource(SlackConfig()),
"/docs": GDocsResource(GDocsConfig()),
})
这层的关键职责有三个:
- 路径解析与最长前缀匹配——
/s3/data/log.jsonl路由到 S3Resource,剩余路径data/log.jsonl交给它翻译成 bucket key; - 跨挂载点操作的编排——
cp /s3/a /slack/b实际是「从 S3 流式读 + 向 Slack 写」的组合,VFS 层负责流式桥接,避免全量落内存; - 统一元数据模型——不同后端的「文件大小」「修改时间」语义参差不齐,VFS 层给出统一的 stat 视图,让
ls -la、find -newer这类命令行为一致。
值得注意的是,Mirage 同时提供进程内虚拟执行与 FUSE 真实挂载(macOS/Linux)两种模式。进程内模式不依赖内核,能跑在 Serverless 和浏览器(TS SDK 有 @struktoai/mirage-browser 包);FUSE 模式则能让 Agent 之外的任何本地程序也看见这棵树。这个「双模」设计明显是冲着部署面去的——纯 FUSE 方案(如 rclone mount)根本进不了 Edge Runtime。
3.3 命令执行层:受控的「伪 Bash」
ws.execute("grep alert /s3/log.jsonl | wc -l") 里跑的并不是宿主机的 bash——那样等于把 shell 注入漏洞打包送给 LLM。Mirage 实现的是一个受控命令解释器:解析管道与重定向,白名单内的命令(cat/grep/head/wc/find/ls/cp/mv 等)由内部实现执行,所有路径访问都走 VFS 层的权限检查。
这个设计的安全含义值得展开:
- 命令白名单天然排除了
curl | sh这类外逃路径; - 路径即权限边界——挂载表就是 ACL,没挂载的服务不存在于 Agent 的世界里;
- 凭证不过模型——AWS key、Slack token 都在 Resource 配置里,Agent 只见路径不见凭证。对比把 SDK 直接给 Agent 的方案,凭证泄露的爆炸半径小了一个量级。
3.4 两级缓存:索引缓存 + 文件缓存
远程后端的延迟是 VFS 的天敌——ls 一下 Slack 要打 API,grep 一个 S3 大文件要全量下载。Mirage 的答案是每个 Workspace 内置两层缓存:
- 索引缓存(Index Cache):目录结构与元数据,默认 TTL 10 分钟。让重复的
ls/find零网络调用; - 文件缓存(File Cache):文件内容,默认内存 512MB,LRU 淘汰。同一文件第二次
grep直接命中本地。
两层缓存的后端均可独立替换。生产多进程/Serverless 场景可以切到 Redis 共享缓存:
from mirage import Workspace
from mirage.cache import RedisFileCacheStore, RedisIndexCacheStore
ws = Workspace(
{"/s3": S3Resource(S3Config(bucket="my-bucket"))},
cache=RedisFileCacheStore(url="redis://localhost:6379/0", limit="8GB"),
index=RedisIndexCacheStore(url="redis://localhost:6379/0", ttl=600),
)
多个 Agent 实例共享一份缓存,意味着热点数据(比如所有 Agent 都要看的那份配置文档)全集群只拉一次。
四、代码实战:从上手到造一个迷你 Mirage
4.1 安装与最小示例
环境要求:Python ≥ 3.12(Python SDK 与 CLI)或 Node.js ≥ 20(TS SDK);FUSE 挂载需 macOS/Linux。
# Python
uv add mirage-ai
# TypeScript(按运行环境选包)
npm install @struktoai/mirage-node # Node 服务端
npm install @struktoai/mirage-browser # 浏览器 / Edge
# CLI
curl -fsSL https://strukto.ai/mirage/install.sh | sh
Python 最小可用示例:
from mirage import Workspace
from mirage.resource.ram import RAMResource
from mirage.resource.s3 import S3Config, S3Resource
from mirage.resource.slack import SlackConfig, SlackResource
ws = Workspace({
"/data": RAMResource(),
"/s3": S3Resource(S3Config(bucket="my-bucket")),
"/slack": SlackResource(SlackConfig()),
})
# 跨服务复制:数据不过模型上下文
await ws.execute("cp /s3/report.csv /data/report.csv")
# 跨服务管道查询
await ws.execute("grep alert /s3/data/log.jsonl | wc -l")
# 打包整个执行环境
ws.snapshot("demo.tar")
CLI 侧的等价操作:
mirage workspace create ws.yaml --id demo
mirage execute --workspace_id demo --command "grep alert /s3/data/log.jsonl | wc -l"
# 大文件预热进缓存,后续命令零延迟
mirage provision --workspace_id demo --command "cat /s3/data/large.jsonl"
4.2 接入 OpenAI Agents SDK
Mirage 不要求换框架,它以「沙箱客户端」形态嵌入现有框架:
from agents import Runner
from agents.run import RunConfig
from agents.sandbox import SandboxAgent, SandboxRunConfig
from mirage.agents.openai_agents import MirageSandboxClient
client = MirageSandboxClient(ws)
agent = SandboxAgent(
name="ops-agent",
model="gpt-5.4-nano",
instructions=ws.file_prompt, # 自动生成的文件系统说明
)
result = await Runner.run(
agent,
"Summarize /s3/data/report.parquet into /data/report.txt.",
run_config=RunConfig(sandbox=SandboxRunConfig(client=client)),
)
注意 ws.file_prompt:Mirage 会根据挂载表自动生成一段紧凑的系统提示(「你有一个文件系统,挂载点如下…」),替代过去几十个 tool schema。这就是前文说的「上下文开销从线性降到常数」的落地形态。
4.3 造一个 200 行的迷你 Mirage,理解其本质
理解一个系统最好的方式是造一个丐版。下面用约 150 行 Python 实现 Mirage 的核心骨架:挂载表 + 路径路由 + 三个命令(ls/cat/grep)+ 一个内存后端和一个「伪 Slack」后端。生产别用,原理管够。
import fnmatch
import re
from abc import ABC, abstractmethod
class Resource(ABC):
"""所有后端的统一契约:只需实现 list 和 read"""
@abstractmethod
def list(self, rel_path: str) -> list[str]: ...
@abstractmethod
def read(self, rel_path: str) -> bytes: ...
class RAMResource(Resource):
def __init__(self):
self.files: dict[str, bytes] = {}
def write(self, path, data: bytes):
self.files[path.lstrip("/")] = data
def list(self, rel_path):
prefix = rel_path.lstrip("/")
out = set()
for k in self.files:
if k.startswith(prefix):
rest = k[len(prefix):].lstrip("/")
out.add(rest.split("/")[0])
return sorted(out)
def read(self, rel_path):
return self.files[rel_path.lstrip("/")]
class FakeSlackResource(Resource):
"""把『频道→目录、消息→文件』的翻译逻辑最小化演示"""
def __init__(self, channels: dict[str, list[str]]):
self.channels = channels # {channel: [msg, ...]}
def list(self, rel_path):
rel = rel_path.strip("/")
if not rel: # ls /slack → 频道列表
return sorted(self.channels)
msgs = self.channels.get(rel, [])
return [f"{i:06d}.txt" for i in range(len(msgs))]
def read(self, rel_path):
channel, fname = rel_path.strip("/").rsplit("/", 1)
idx = int(fname.removesuffix(".txt"))
return self.channels[channel][idx].encode()
class MiniVFS:
def __init__(self, mounts: dict[str, Resource]):
# 按前缀长度降序,实现最长前缀匹配
self.mounts = dict(
sorted(mounts.items(), key=lambda kv: -len(kv[0]))
)
def _route(self, path: str) -> tuple[Resource, str]:
for prefix, res in self.mounts.items():
if path == prefix or path.startswith(prefix + "/"):
return res, path[len(prefix):] or "/"
raise FileNotFoundError(path)
# ---- 命令实现:全部走统一路由,不碰宿主机 shell ----
def ls(self, path):
res, rel = self._route(path)
return res.list(rel)
def cat(self, path) -> bytes:
res, rel = self._route(path)
return res.read(rel)
def grep(self, pattern, path) -> list[str]:
text = self.cat(path).decode(errors="replace")
rx = re.compile(pattern)
return [ln for ln in text.splitlines() if rx.search(ln)]
def execute(self, command: str) -> str:
"""极简『受控解释器』:白名单 + 管道支持(仅演示 grep|wc -l)"""
parts = [p.strip() for p in command.split("|")]
head = parts[0].split()
if head[0] == "ls":
out_lines = self.ls(head[1])
elif head[0] == "cat":
out_lines = self.cat(head[1]).decode().splitlines()
elif head[0] == "grep":
out_lines = self.grep(head[1], head[2])
else:
raise PermissionError(f"command not allowed: {head[0]}")
for stage in parts[1:]:
if stage.replace(" ", "") == "wc-l":
out_lines = [str(len(out_lines))]
else:
raise PermissionError(f"pipe stage not allowed: {stage}")
return "\n".join(out_lines)
# ---- 演示 ----
ram = RAMResource()
ram.write("/logs/app.log", b"INFO boot\nALERT disk full\nALERT oom\n")
vfs = MiniVFS({
"/data": ram,
"/slack": FakeSlackResource({
"ops": ["deploy done", "ALERT: p0 incident", "resolved"],
}),
})
print(vfs.execute("ls /slack/ops")) # 000000.txt ...
print(vfs.execute("grep ALERT /data/logs/app.log | wc -l")) # 2
print(vfs.execute("cat /slack/ops/000001.txt")) # ALERT: p0 incident
麻雀虽小,但 Mirage 的三个关键决策全在里面了:
- Resource 契约极薄(list/read 起步),所以接新后端便宜;
- 路由用最长前缀匹配,挂载表即权限表;
- execute 是解释器不是 shell,白名单命令 + 受控管道,LLM 生成的命令再离谱也逃不出沙箱。
真实的 Mirage 在此之上加了:写路径(cp/mv/重定向)、流式 IO(大文件不落全量内存)、异步并发、两级缓存、快照序列化、FUSE 桥接,以及每个后端大量的语义翻译细节。但骨架就是这个骨架。
4.4 一个真实味道的场景:跨服务事故复盘
把上面的能力串起来,看一个 SRE Agent 的任务:「统计昨天 S3 日志里的 P0 告警,结合 Slack #ops 频道的讨论,生成复盘草稿存到 Notion」。
工具调用范式下,这是 6-8 轮 tool call,日志内容至少两次穿过模型上下文。Mirage 范式下,Agent 生成的核心动作是:
# 1. 告警统计(数据不过模型)
grep '"level":"P0"' /s3/logs/2026-07-28/*.jsonl | wc -l
# 2. 提取告警明细到工作区
grep '"level":"P0"' /s3/logs/2026-07-28/*.jsonl > /data/p0.jsonl
# 3. 拉取当天 ops 频道讨论
find /slack/ops -newer /data/marker -name "*.txt" | head -50
# 4. 模型只读取小体积的中间产物,写复盘
cat /data/p0.jsonl | head -20
# ...(模型生成复盘文本)
# 5. 落盘到 Notion
cat /data/retro-draft.md > /notion/incidents/2026-07-28-retro.md
只有第 4 步的小样本进入了上下文,其余全部在 VFS 内部流转。token 成本从「与数据量成正比」变成「与结论量成正比」——这句话是 Mirage 价值的最好概括。
五、性能优化与生产落地清单
alpha 阶段的项目谈生产要格外冷静。以下是实际压测和试用中总结的要点:
5.1 缓存策略
- 读多写少的挂载点拉高索引 TTL。文档库、归档日志类后端,索引 TTL 从默认 10 分钟拉到小时级,
find/ls开销几乎归零; - 大文件用
provision预热。mirage provision --command "cat /s3/big.jsonl"在任务开始前把热点文件灌进缓存,避免 Agent 执行中途卡在下载上; - 多实例上 Redis 共享缓存,文件缓存 limit 按「热点工作集 × 1.5」估算;注意 Redis 自身的内存上限与淘汰策略要和 Mirage 的 limit 对齐,否则两层 LRU 打架。
5.2 命令层的性能直觉要重建
grep一个未缓存的远程大文件 = 全量下载。对 S3 这类支持字节范围请求的后端,head -c 1M明显快于cat;但对 Slack 这类 API 型后端,「文件大小」本身就是懒加载的,ls -la可能比你想的贵;- 管道内的数据不计 token,但计内存。
cat 5GB 文件 | grep在流式实现下没问题,但sort这类需要全量物化的命令要小心(这也是 Mirage 命令白名单谨慎扩张的原因之一); - 跨挂载
cp的吞吐取决于两端较慢者,S3→GDrive 这类「云到云」复制实际是「云→本机→云」,大批量迁移别用它,该用专业迁移工具还是得用。
5.3 安全与权限
- 挂载表按任务最小化。给「日志分析 Agent」挂
/s3/logs而不是整个/s3;Mirage 的世界观里,不挂载 = 不存在,这是最便宜的 ACL; - 写敏感后端(Gmail 发信、Slack 发消息)建议挂成只读 + 人工审批出口:Agent 把产物写到
/data/outbox/,由外围系统审批后真正投递; - 凭证全部走 Resource 配置注入,严禁出现在 instructions 里——这本该是常识,但工具范式下泄露案例屡见不鲜,VFS 范式在结构上降低了这个风险。
5.4 当前的真实边界(劝退清单)
必须说清楚,Mirage 现在是 v0.0.1-alpha,以下场景暂时不适合:
- 强一致写场景。文件语义天然弱化了事务——「写入 /mongo/orders/123.json」没法表达条件更新与乐观锁。数据库类后端当前更适合读与简单写;
- 高频轮询。索引缓存 TTL 内看不到新消息,做「实时监听 Slack」这种事请用原生事件订阅,别拿
find -newer硬轮询; - 语义严重不匹配的后端。Redis 的扁平 key 空间、GSheets 的二维表格,翻译成文件树都有信息损耗,重度使用者仍需原生工具补位;
- API 配额敏感的场景。一条看似无害的
grep pattern /slack/**可能展开成上百次 API 调用,限流与配额监控要自己兜底。
六、总结与展望:接口的终点是文件?
Mirage 最打动我的不是代码量,而是它对问题的重新定义。过去两年,整个行业都在回答「怎么让 Agent 学会更多工具」,于是有了越来越长的 tool 列表、越来越复杂的工具检索(tool RAG)、越来越精巧的调用规划。Mirage 反过来问:能不能让工具消失?
它的答案继承自 Unix 最古老的智慧:一切皆文件。当所有数据源坍缩成一棵目录树,Agent 需要的能力就坍缩成它与生俱来的那一套 Bash 语感。上下文开销从 O(N) 工具数降到 O(1),数据流从「穿过模型」变成「绕过模型」,权限模型从散落各处的 token 变成一张挂载表。
当然,冷静地说,这条路也有天花板:文件抽象对「读」慷慨,对「复杂写」吝啬;alpha 版的稳定性、后端语义翻译的完成度、配额治理,都需要时间。它未必会取代 MCP——更可能的终局是分层共存:MCP 管动作,VFS 管数据;需要精确语义的操作走工具,需要浏览、检索、搬运的场景走文件树。
但方向上我愿意下注:下一代 Agent 基础设施的竞争,不在于谁的工具更多,而在于谁把世界呈现得更简单。 五十年前,Unix 用「一切皆文件」驯服了纷繁的硬件设备;今天,同样的抽象正在驯服纷繁的 SaaS API。历史不会重复,但会押韵。
项目地址:https://github.com/strukto-ai/mirage
如果你正在被 Agent 的工具地狱折磨,花十分钟 uv add mirage-ai 试一次跨服务 grep——这种体验,试过就回不去了。