【vercel/eve】Filesystem-First Harness:用目录结构替代抽象类,写出可被 LLM 自己读懂的 Agent
“The filesystem is the authoring interface.” —— vercel/eve README
一个被严重低估的 Harness 设计选择
如果你让一个 LLM Agent 框架的设计者列出最关键的两个抽象,多半会得到这两个候选:
- Agent 类(承载模型 + 工具 + 指令)
- Tool 类(承载函数 + 输入 schema + 权限)
但 vercel/eve 给出了第三种答案:文件系统本身就是 Agent 的接口。
1 | my-agent/ |
没有 class Agent、registerTool() 调用、YAML 配置中心。Agent 是一个目录;工具、SOP、通道、定时任务都是这个目录下的文件。
这不是审美偏好的问题——这是一个被低估的工程决策,本文会拆解它。
一、为什么 Filesystem-First 是 Harness Engineering 的”更优解”
1.1 LLM 的工作方式决定了”代码可读性 > 类型可读性”
Agent 框架的设计者惯常的思路是:
“用 class + interface 把能力封装起来,让人类工程师能写出干净代码”
但 LLM 不是人类工程师。LLM 读懂一个能力,靠的不是 TypeScript 类型签名,而是 README、Markdown、文件路径。
eve 把这种现实转化成具体规则:
| 维度 | 传统 Agent 框架 | vercel/eve |
|---|---|---|
| Tool 身份 | class WeatherTool implements Tool | 文件路径 agent/tools/get_weather.ts |
| 身份冲突检测 | 编译期类型冲突 | 文件路径冲突(两个同路径文件 → 编译失败) |
| 发现方式 | 显式 register([...]) | 文件系统扫描 |
| 文档路径 | 单独写 README | 工具名本身就是文档(get_weather = “获取天气”) |
对 LLM 的好处:当 Agent 想调用一个工具时,它先看到一个人类可读的目录树,再调用 load_skill("plan_a_trip") 加载完整 SOP,最后才发 tool_call。整个流程没有”反序列化抽象”的损耗。
1.2 “Filesystem as API” 是 12-Factor App 的延伸
12-Factor App 的核心是”配置即环境变量”。eve 把这个原则推到极致:
- 身份即路径:删除
get_weather.ts= 自动移除该工具 - 能力即目录:添加
skills/子目录 = 自动获得 SOP 加载能力 - 运行时即文件存在:
channels/slack.ts存在 → eve 启动 Slack 监听;不存在 → 完全跳过
没有 enabled: true 字段。文件是否存在就是开关。这比任何”feature flag 服务”都更可靠。
二、vercel/eve 的 6 大原语
我从源码(packages/eve/src/)中提取出 6 个机制层面的原语——这 6 个原语组合起来,构成了整个 Harness:
2.1 原语 1:Identity from Path(路径即身份)
反模式:
1 | // 传统框架:身份来自字段 |
eve 的设计:
1 | // packages/eve/src/internal/authored-definition/source-identity.ts |
关键设计:
- 没有
name字段:InternalToolDefinition有name,但那是编译期从路径派生的(packages/eve/src/tools/definition.ts):1
2
3
4export interface InternalToolDefinition extends ToolDefinitionBase {
name: string; // ← 编译器从路径派生
inputSchema: JsonObject | null;
} - 冲突即模糊:两个同名文件 → 标记为
ambiguous,不会崩溃,但toolResultFrom匹配会失效 Symbol.for("eve:definition-source-key"):用全局 Symbol 注册身份,让 IDE/Linter 也能看到
为什么这个原语重要:
- 文件路径是 Git-friendly 的——重命名文件 = 工具改名,所有历史一目了然
- 文件路径是 GitHub-renderable 的——打开一个工具的 PR,diff 显示完整文件
- 文件路径是 LLM-readable 的——
find agent/tools -name "*.ts"就是一份能力清单
2.2 原语 2:Durable Subagent Dispatch(持久化的子 Agent 分发)
agent 工具是 eve 的 Sub-Agent 组件:
1 | // packages/eve/src/tools/framework/agent.ts |
关键事实:agent 工具的 execute 永远不会被调用。它是一个 “信号工具”——当 LLM 调它时,harness 接管,转入持久化子 Agent 启动流程。
为什么这样做?因为 Sub-Agent 涉及长期状态(parked child handle、replay-safe operation ids、credentials)——不适合塞进普通 Tool 的 execute 生命周期。
具体协议(packages/eve/src/tools/framework/agent-contract.ts):
1 | export const AGENT_TOOL_DESCRIPTION = [ |
对比传统 Sub-Agent 设计:
| 框架 | Sub-Agent 实现 | 持久化能力 |
|---|---|---|
| LangGraph | add_node + add_edge | 弱:需手动 checkpoint |
| OpenAI Agents SDK | Agent 类嵌套 | 中:handoff 时挂起 |
| vercel/eve | agent 工具 | 强:durable child handle + agentId 续接 |
2.3 原语 3:Approval Policies always/never/once(三态审批)
eve 的审批系统只有三个内置策略——这是我见过最干净的审批抽象:
1 | // packages/eve/src/tools/approval/policies.ts |
返回类型为什么是 "user-approval" | "not-applicable" 而不是 boolean?
这是 discriminated union 的妙用——以后想加 "silent-approval"(自动通过)或 "audit-required"(仅记录)时,类型系统自动提示所有需要更新的地方。boolean 做不到这一点。
once 策略的细节:
1 | // A tool is recorded as approved only on an explicit approval; |
关键点:超时未响应 ≠ 拒绝。eve 把”沉默”和”拒绝”分开处理——沉默不算通过,所以下一次还会问。这避免了”用户离开 = 永久放行”的安全漏洞。
2.4 原语 4:Durable Callbacks(持久化回调)
工具的 execute、approval、toModelOutput 都被做成可重放的回调:
1 | // packages/eve/src/tools/durable-callbacks.ts |
这个设计的妙处:
重放问题:Agent 跑完一轮,重启后 replay 工具调用。回调函数不能”序列化”,但 closure(数据)可以。所以只持久化 closure,重放时重新绑定 callback。
回滚代码 = 重新解析:当你部署新版,工具逻辑改了,重放旧事件会用新代码执行(因为回调注册是 lazy 的)。这避免了”代码回滚导致 replay 行为不一致”的经典 bug。
2.5 原语 5:Typed Tools via Standard Schema V1(标准化 Schema 协议)
eve 不绑死 Zod,而是支持 Standard Schema V1(一个 LLM 生态正在形成的 schema 互操作规范):
1 | // packages/eve/src/tools/definition.ts |
实际效果(来自 tools/provided/bash.ts):
1 | import { z } from "#compiled/zod/index.js"; |
strictObject 的细节——Zod 默认允许 unknown 字段,strictObject 不允许。这避免了模型在 tool_call 里塞额外字段时的”静默忽略”陷阱。
2.6 原语 6:OpenTelemetry-First Tracing(OTel 是默认观测栈)
eve 默认导出 OTel spans 而不是自建追踪格式:
1 | // packages/eve/src/tracing/agent-otel-provider.ts |
为什么不自建?因为:
- Harness 用户大概率已经在用 Datadog/Honeycomb/Tempo——OTel 是所有这些平台的”通用语”
- 跨 Harness 对比研究:如果都用 OTel,可以横向对比
vercel/evevsLoopXvsLongHorizon-Harness的 span 拓扑 - 免费拿到 trace context propagation——HTTP、Slack、Discord 通道自带 trace
三、整体架构:Filesystem → Compile → Durable Runtime
让我把这 6 个原语放到完整架构里:
graph TB
subgraph "📁 Authoring Layer(文件系统)"
A["agent/instructions.md<br/>📝 always-on 指令"]
B["agent/agent.ts<br/>⚙️ 模型 + 运行时配置"]
C["agent/tools/*.ts<br/>🔧 类型化函数工具"]
D["agent/skills/*.md<br/>📚 按需 SOP"]
E["agent/channels/*.ts<br/>📨 消息通道"]
F["agent/schedules/*.ts<br/>⏰ cron 触发"]
G["agent/subagents/*.ts<br/>👥 子 Agent"]
end
subgraph "🔨 Compile Layer(编译期)"
H["stampDefinitionKey<br/>🏷️ 路径 → 身份"]
I["normalizeAgentDefinition<br/>✅ schema 校验"]
J["registerDurableDynamicCallback<br/>📌 回调注册"]
end
subgraph "🚀 Runtime Layer(运行时)"
K["Harness Loop<br/>🔁 model ↔ tools"]
L["Agent Tool Interceptor<br/>🎯 拦截 durable dispatch"]
M["Approval Gate<br/>🛡️ always/never/once"]
N["TaskExec / TaskReceipt<br/>📨 异步任务管理"]
O["OpenTelemetry Exporter<br/>📊 span 导出"]
end
subgraph "🛡️ Persistence Layer(持久化)"
P["Session State<br/>💾 child handle + receipts"]
Q["Durable Callbacks Closure<br/>🔒 JSON 快照"]
R["OTel Trace Store<br/>📈 本地保留"]
end
A --> H
B --> H
C --> H
D --> H
E --> H
F --> H
G --> H
H --> I --> J
J --> K
K --> L
K --> M
K --> N
K --> O
L --> P
M --> Q
N --> P
O --> R
style A fill:#E8D5F5,stroke:#CE93D8,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#E8D5F5,stroke:#CE93D8,color:#333
style D fill:#E8D5F5,stroke:#CE93D8,color:#333
style E fill:#E8D5F5,stroke:#CE93D8,color:#333
style F fill:#E8D5F5,stroke:#CE93D8,color:#333
style G fill:#E8D5F5,stroke:#CE93D8,color:#333
style H fill:#FFDAB9,stroke:#FFAB76,color:#333
style I fill:#FFDAB9,stroke:#FFAB76,color:#333
style J fill:#FFDAB9,stroke:#FFAB76,color:#333
style K fill:#C7CEEA,stroke:#9FA8DA,color:#333
style L fill:#C7CEEA,stroke:#9FA8DA,color:#333
style M fill:#C7CEEA,stroke:#9FA8DA,color:#333
style N fill:#C7CEEA,stroke:#9FA8DA,color:#333
style O fill:#C7CEEA,stroke:#9FA8DA,color:#333
style P fill:#FFF9C4,stroke:#F9A825,color:#333
style Q fill:#FFF9C4,stroke:#F9A825,color:#333
style R fill:#FFF9C4,stroke:#F9A825,color:#3333.1 数据流:从 Markdown 到 Model Call
让我用一个具体的例子——“计划一次巴黎旅行”——跟踪完整数据流:
sequenceDiagram
actor User as 👤 用户
participant Channel as 📨 Channel<br/>(Slack/HTTP)
participant Harness as ⚙️ Harness Loop
participant Skills as 📚 Skill Loader
participant LLM as 🤖 LLM (Anthropic)
participant Tools as 🔧 Tool Sandbox
participant OTel as 📊 OTel Exporter
User->>Channel: "帮我计划巴黎旅行"
Channel->>Harness: TurnEvent (with session context)
Note over Harness: 加载 instructions.md
Harness->>Skills: list_available_skills()
Skills-->>Harness: [plan_a_trip, weather_lookup, ...]
Harness->>LLM: System prompt + skills 列表
LLM-->>Harness: tool_call: load_skill("plan_a_trip")
Harness->>Skills: load_skill("plan_a_trip")
Skills-->>Harness: 完整 SOP markdown
Harness->>LLM: SOP 已注入 + 原始指令
LLM-->>Harness: tool_call: weather_lookup("Paris")
Harness->>Tools: execute weather_lookup
Tools-->>Harness: {city: "Paris", temp: 18}
Harness->>OTel: export span {tool: weather_lookup, latency: 200ms}
Harness->>LLM: tool_result
LLM-->>Harness: final answer
Harness->>Channel: DeliverPayload
style User fill:#C7CEEA,stroke:#9FA8DA,color:#333
style Channel fill:#C7CEEA,stroke:#9FA8DA,color:#333
style Harness fill:#E8D5F5,stroke:#CE93D8,color:#333
style Skills fill:#E8D5F5,stroke:#CE93D8,color:#333
style LLM fill:#FFDAB9,stroke:#FFAB76,color:#333
style Tools fill:#B5EAD7,stroke:#80CBC4,color:#333
style OTel fill:#FFF9C4,stroke:#F9A825,color:#333关键观察:从用户消息到模型回复,没有任何配置文件参与运行时决策。所有能力都来自文件系统扫描 + 编译期注册。
四、可运行的 MVP:从零复刻 eve 的核心机制
让我用 200 行 Python 复刻 eve 的 Filesystem-First 核心:
1 | import os |
运行结果:
1 | 🔧 发现工具: tools/get_weather.py |
这个 MVP 演示了 eve 的 3 个核心机制:
- Identity from Path:
tools/get_weather.py的路径就是身份 - Filesystem-First Discovery:扫描目录自动注册,无
register()调用 - Durable Subagent Dispatch:
agent工具的 execute 永不执行,harness 拦截
五、与同类项目横向对比
5.1 vs LangChain / LangGraph
| 维度 | LangChain | vercel/eve |
|---|---|---|
| Agent 定义 | AgentExecutor(agent=..., tools=...) | 目录 |
| 工具发现 | 显式 list | 文件系统扫描 |
| Tool 身份 | name 字段 | 文件路径 |
| Sub-Agent | AgentExecutor 嵌套 | agent 工具 + durable handle |
| 持久化 | 需 LangGraph + checkpoint | 内建 durable callbacks |
关键差异:LangChain 把”Agent”当作对象,eve 把 Agent 当作目录。对象要注册到中心、目录只需存在即可。
5.2 vs OpenAI Agents SDK
| 维度 | OpenAI Agents SDK | vercel/eve |
|---|---|---|
| 工具定义 | Python 函数 + @function_tool | 文件 + defineTool |
| Schema 来源 | 自动从类型注解推导 | 必须显式 Standard Schema V1 |
| Handoff | handoff_to(agent) 显式调用 | agentId 续接,自动 parked |
| Trace | trace 装饰器 | OpenTelemetry 内建 |
| 部署形态 | Python 进程 | Vercel Functions + Nitro |
关键差异:OpenAI Agents SDK 是Python-first,eve 是 TypeScript-first。前者依赖运行时类型反射,后者依赖编译期 Normalize。
5.3 vs LoopX / LongHorizon-Harness
| 维度 | LoopX | LongHorizon | vercel/eve |
|---|---|---|---|
| 抽象 | 长期控制平面 | Long-horizon 协议 | 文件系统 + Agent 工具 |
| 持久化粒度 | 任务 | 任务 + Receipt | Tool + Callback Closure |
| 跨 Harness | 是(Codex + Claude Code) | 是 | 否(自闭环) |
关键差异:LoopX 和 LongHorizon 是元 Harness(覆盖多个 Agent 框架),eve 是自闭环 Harness。前者解决”管多个 Agent 的问题”,后者解决”把一个 Agent 做对的问题”。
六、优缺点分析
6.1 左侧:架构简洁性 / 扩展性 / 易用性
| 维度 | 评价 | 证据 |
|---|---|---|
| 架构简洁性 | ⭐⭐⭐⭐⭐ | 6 个原语覆盖所有能力 |
| 扩展性 | ⭐⭐⭐⭐ | 通过文件系统扩展,零配置 |
| 易用性 | ⭐⭐⭐⭐⭐ | npx eve@latest init 即可上手 |
| 路径即文档 | ⭐⭐⭐⭐⭐ | 文件树本身就是文档 |
6.2 右侧:性能 / 复杂度 / 维护性
| 维度 | 评价 | 证据 |
|---|---|---|
| 运行时性能 | ⭐⭐⭐ | 每次 turn 重新扫目录 |
| 概念复杂度 | ⭐⭐ | 6 个原语,门槛低 |
| 维护性 | ⭐⭐⭐⭐⭐ | 重命名文件 = 重命名能力 |
| 调试友好度 | ⭐⭐⭐⭐ | 文件路径直接对应 GitHub 行号 |
核心权衡:Filesystem-First 让人类 + LLM 共读变得极其自然,但运行时开销高于”预编译成注册表”的方案。对长会话场景,每次重新扫目录的成本不可忽略。
七、从零搭建启示:我在 eve 身上学到的 3 件事
7.1 MVP 必加的 3 个组件
如果让我自己复刻一个 Filesystem-First Harness,最小可行实现只需要:
- Filesystem Discovery(30 行 Python):扫描
agent/tools/*.py自动加载 - Identity from Path(5 行):
tool["id"] = str(file_path) - Agent Tool Interceptor(10 行):信号工具 +
dispatch_subagent()单独通道
不要先做的:approval、OTel、durable callbacks。这些都是高级特性,先证明”文件系统即 API”有价值,再加复杂度。
7.2 踩坑预警
坑 1:删除文件 ≠ 删除能力。如果用户 rm 了一个工具文件,但没有重启服务,eve 会怎样?答案:取决于实现。MVP 阶段用 mtime 检测 + 热重载,避免”改了不生效”的体验灾难。
坑 2:路径冲突。两个文件叫 get_weather.py,一个在 tools/ 一个在 tools/legacy/——eve 的设计是警告但不崩溃。但这会让 LLM 困惑(同名工具)。建议:检测到同名时返回 ambiguous,并 prompt 让用户重命名。
坑 3:跨平台路径。Windows 用 \、macOS 用 /。eve 通过 Node.js 生态统一处理,但 Python 复刻时必须用 pathlib.Path 而不是字符串拼接。
7.3 行动建议
给 Harness 设计者:
- ✅ 把”Agent 是目录”作为第一原则
- ✅ 工具身份用路径而非字段
- ✅ Agent 工具拦截做持久化分发
给 Harness 使用者:
- ✅ 用
git mv重命名工具(不是 sed) - ✅ 把 SOP 写进
skills/*.md,让 LLM 主动load_skill - ✅ 谨慎使用
agentId——续接既有子 Agent 会带来意外状态
八、总结:Filesystem-First 不是审美,是工程原则
vercel/eve 给我们的最大启示不是”又一个 Agent 框架”,而是:
当你的用户既包括人类工程师也包括 LLM 时,”文件路径” 是比 “类实例” 更好的抽象。
文件路径是 Git-friendly、GitHub-renderable、LLM-readable、IDE-aware 的。没有其他抽象同时具备这 4 个特性。
如果你正在设计一个 Harness,问自己一个问题:
“我能让一个完全没看过文档的 LLM,只通过
ls agent/tools/就能列出所有能力吗?”
如果答案是 “能”,你就在正确的路上。
下一步阅读:
- Subagents as tasks: additive delivery plan —— eve 子 Agent 的详细设计
- vercel/eve 官方文档 —— 上手教程
- 系列上一篇:【LongHorizon-Harness】Loop Engineering 深度解析