用虚拟文件系统和Bash构建智能体,文件优于工具
速览
该文章探讨了如何通过虚拟文件系统和Bash构建一个智能体,提出“文件优于工具”的设计理念。这种方法旨在简化智能体与环境的交互,提升灵活性和效率。文章可能涉及具体实现细节和实际应用场景。
AI 深度解读
背景
2026年3月,Knock 公司正式发布了 Knock Agent——一个用于管理所有客户消息资源的 AI Agent。它可以创建和更新工作流、模板和受众,帮助细分用户,并回答关于客户消息效果的问题。用户可以通过 Knock 仪表盘、连接的 Slack 工作空间、API 或 MCP 服务器调用该 Agent。在这篇文章中,团队揭秘了他们如何使用 bash、虚拟文件系统(virtual filesystem)以及管理 API 来架构和构建这个 Agent。
核心内容
最初的愿景
Knock 希望打造一个能管理仪表盘上所有功能的 Agent:创建符合公司设计系统的消息、匹配语气、理解数据建模方式。为此,他们需要构建一个丰富的 Agent 框架,能访问用户 Knock 账户中的全部数据,并利用上下文来生成消息和回答问题。
第一版原型:工具-类型模式
团队最初只针对“工作流”这一深度领域构建原型。他们采用了“一项工具对应一种类型”(tool-per-type)的模式:每项工具暴露管理 API 中的一个原语,例如在流程中添加延迟步骤,或使用可视化块语言构建电子邮件模板。工具描述编码了使用该工具的特殊性并给出示例,工具输入则描述字段类型和要求。
虽然这种方法有效且结果不错,但他们意识到无法规模化:为管理 API 中的每个资源暴露一个工具会膨胀 Agent 的上下文窗口,除非引入更复杂的工具路由层或让 Agent 使用脚本语言。
更少的工具,更多的文件
团队从 Vercel 的博客《How to build agents with filesystems and bash》中获得启发。Vercel 为其内部数据工具 d0 构建 Agent 时,发现文件系统是 Agent 发现所需上下文的绝佳方式,可以将领域映射到文件系统来支撑上下文层。
Knock 早已有一个 CLI 工具,允许团队拉取消息模板、组件等资源在本地文件系统上操作。因此简单思路是:不再暴露镜像管理 API 的工具,而是给 Agent 一个包含账户内容的文件系统,以及使用 bash 在文件系统之上编写脚本的能力。这样 Agent 可以浏览文件系统收集上下文,用 bash 高效编写所需脚本。当 Agent 要编辑工作流或模板时,它直接在文件系统中修改文件,然后回调 Knock 来持久化更改。
这种模式更接近 Claude Code 等编码 Agent,而非传统的产品内助手,并且契合 Knock 已有的 CLI 和本地优先维护客户消息的理念。
用 Elixir 实现 Agent 的 bash
Vercel 的 Malte 在 2025 年假期发布了 just-bash:一个让 Agent 拥有 Unix 文件系统和 bash 环境,而无需启动 Linux 镜像的库。它本质上是 TypeScript 实现的虚拟 bash,附带可被 Agent 调用的虚拟文件系统。
Knock 是扎根于 Elixir 生态的公司,他们想要自己的虚拟 bash 解释器。选项有两个:放弃已有工作,用 TypeScript 重建 Agent;或者将 just-bash 移植到 Elixir。幸运的是,像 just-bash 这样的库具有可复现的目标:定义明确的接口和全面的测试套件(支持 jq、ls、cat 等命令),Elixir 移植版直接复用了这些测试 fixture。
他们说服了朋友 Ivar Vong 来完成这个移植。经过大量 token 的消耗,他们得到了一个完整的 Elixir 版 bash 解释器和虚拟文件系统(同样命名为 just-bash),带有广泛的测试套件和稳健的安全模型。
为什么不用沙箱?
显而易见的问题是:为什么不给 Agent 一个完整的计算机(沙箱),而不是虚拟文件系统?毕竟市面上有很多沙箱供应商。
Knock 认为在这个阶段,全功能沙箱对于他们简单的需求来说是过度设计。启动沙箱会带来性能开销(相比内存中的虚拟方案),还会引入更棘手的内容同步问题——那些在应用外部所做的更改。他们决定先搁置这个问题。如果复杂度增长到需要改变,他们会重新评估。例如,将来若要在 bash 之外添加完整的脚本语言(如 Python),可能会成为转向真实沙箱的决定性因素。他们遵循“分离手与脑”的原则,所以迁移到真实沙箱只需要将对虚拟沙箱的调用替换为真实沙箱。
整合所有组件
拥有了 Elixir 版的 just-bash 和所有构建块后,团队组装了基于文件系统的 Agent。他们直接使用 Anthropic API 运行典型的 Agent 循环,根据任务切换不同模型。该循环由持久化工作流支持(利用 Elixir 中优秀的 Oban 库,将状态存储于 Postgres),可以安全地重试步骤。
在一个 Agent 会话中,会附加一个沙箱(sandbox),其中包含 just-bash 实例和完整的虚拟文件系统。每个沙箱只是运行在 Elixir 集群中的一个进程。沙箱在请求首次需要使用文件系统时被惰性启动,从而确保不需要文件系统的请求能快速返回答案,占用最小资源。
当没有已运行的沙箱进程时,文件系统会先加载账户中对象的元数据(作为引导)。这样 Agent 拥有所需资源的完整目录,但在需要操作之前,可以惰性加载完整的大型资源。文件系统结构如下(示例):
/accounts/abc/
workflows/
workflow-1.json
templates/
email-template-1.json
当 Agent 想要探索特定工作流或模板时,可以调用 load_resources 工具批量加载它们到文件系统。沙箱进程是长生命周期的,因此后续轮次可以直接访问已加载的内容,非常适合多轮对话中反复修改工作流或消息模板。
Agent 可用的工具
除了 bash 命令外,Agent 暴露以下文件操作工具:
bash– 执行 bash 命令。read_file– 从文件系统读取文件(优先于cat)。edit_file– 对文件进行定向编辑(将字符串 x 替换为 y)。write_file– 将整个文件写入文件系统。upsert_resource– 将在文件系统上对资源所做的更改持久化回 Knock 账户。该工具通过读取虚拟文件系统上存储的 bundle,并调用与管理 API 相同的内部方法来实现。
事件日志
Agent 循环会输出完整的事件日志:接收到的提示、执行的操作、发送的响应。该日志先缓存在内存中,然后在 Agent 执行时异步写入
