1 核心命题:什么是「存算分离」的 Agent 底座?
传统的 Agent 运行时(如 LangChain、AutoGPT)将状态和计算耦合在同一个进程或同一台机器上。 这在单机使用场景下没问题,但一旦要作为 SaaS 服务提供——支持多用户、多 Agent、多副本部署——就会暴露出几个致命问题:
- ❌ 会话丢失:一个用户的聊天跑在 Pod A 上,Pod A 重启后什么都没了。
- ❌ 文件不可见:Agent 生成的 PDF / 图片存在 Pod A 的本地磁盘,Pod B 上完全看不到。
- ❌ 沙箱膨胀:每个用户如果都开着 Docker 容器,单机很快撑不住。但杀掉容器 = 丢失文件。
- ❌ 集群调度:无法水平扩展,因为状态是按 Pod 隔离的。
FastClaw 的核心答案: Compute(推理、工具执行、Agent 逻辑)和 Storage(会话、配置、文件、记忆)从一开始就设计为独立层,通过明确的接口解耦。
2 架构全景:三层存储 + 两级计算
┌─────────────────────────────────────────────────────────────────────┐
│ 用户 / 客户端 │
│ Web · API · Telegram · Discord · Slack · 飞书 · 微信 │
└──────────────────────────────┬──────────────────────────────────────┘
│
┌───────────────────────────▼──────────────────────────┐
│ Gateway Pod × N(无状态) │ ← Compute Tier-1
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ HTTP API │ │ Agent │ │ Channel │ │
│ │ Server │ │ Loop │ │ Manager │ ... │
│ ├──────────┤ ├──────────┤ ├──────────┤ │
│ │ Cron │ │ Plugin │ │ Webhook │ │
│ │ Scheduler│ │ Manager │ │ Server │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Pod 本地磁盘 (emptyDir): 仅放 ephemeral 缓存 │
└──┬────────────────────────────────┬──────────────────┘
│ │
┌─────▼──────────────┐ ┌───────────▼───────────────────────┐
│ Storage Tier 1 │ │ Storage Tier 2 │
│ PostgreSQL / │ │ S3 兼容对象存储 │
│ SQLite │ │ (AWS S3 / R2 / B2 / OSS / MinIO) │
│ │ │ │
│ • 用户 & API Key │ │ • Agent 生成文件 (PDF/图片/音频) │
│ • Agent 定义 │ │ • Skills 文件包 │
│ • 会话/聊天历史 │ │ • 跨 Pod 共享的工作空间 │
│ • 配置 (Provider, │ │ │
│ Channel, …) │ │ 每次工具调用后自动同步 │
│ • SOUL.md / │ │ 沙箱创建时从对象存储还原 │
│ MEMORY.md │ │ │
└────────────────────┘ └───────────────────────────────────┘
│
┌────────────▼────────────────┐
│ Compute Tier 2 — 沙箱 │
│ │
│ Docker 容器 或 E2B 云沙箱 │
│ • 懒启动 (首次工具调用) │
│ • 空闲自动回收 │
│ • 创建时从对象存储还原 │
│ • 销毁前快照到对象存储 │
└─────────────────────────────┘
🗄️ 存储层 1:PostgreSQL(状态)
存储所有「元数据」——用户、Agent、会话历史、API Key、Provider 配置、Channel 绑定、Cron 任务、SOUL.md / MEMORY.md 等 Agent 文件。支持 SQLite(单机开发)和 PostgreSQL(多副本生产)两种驱动。
📦 存储层 2:对象存储(文件)
存储 Agent 运行时产生的二进制文件:生成的 PDF、图表、图片、音频、中间数据。支持 6 种 S3 兼容后端:AWS S3、Cloudflare R2、Backblaze B2、阿里云 OSS、MinIO 或任意 S3 兼容服务。提供 Signed URL 让浏览器直接下载,绕过 Gateway。
⚙️ 计算层 1:Gateway Pod(推理)
运行 Agent 推理循环、HTTP API Server、Channel Manager(IM 机器人)、Cron 调度器、Plugin Manager。Pod 完全无状态——每次请求从 Postgres 加载会话,处理完写回。K8s 部署支持 2-10 副本 + HPA 自动扩缩。
🛡️ 计算层 2:沙箱(工具执行)
执行 Agent 的实际工具调用(exec / read_file / write_file / list_dir)。每个 (Agent, Project, Session) 三元组对应一个隔离沙箱。懒启动:没有工具调用的纯聊天不创建。空闲自动回收:超时后自动销毁,销毁前将 /workspace 快照同步回对象存储。
3 源码级别的存算分离证据
证据 1:Workspace 存储接口 — 完全与 Pod 本地磁盘解耦
internal/workspace/workspace.go
workspace.Store 接口定义了 Agent 文件的所有操作,它不关心底层是本地磁盘还是 S3:
type Store interface {
Put(ctx, agentID, projectID, sessionID, path, reader, size, contentType) error
Get(ctx, agentID, projectID, sessionID, path) (ReadCloser, error)
Stat(ctx, agentID, projectID, sessionID, path) (*ObjectInfo, error)
List(ctx, agentID, projectID, sessionID) ([]ObjectInfo, error)
Delete(ctx, agentID, projectID, sessionID, path) error
Move(ctx, fromProject, fromSession, toProject, toSession) error
SignedURL(ctx, agentID, projectID, sessionID, path, ttl) (string, error)
}
把文件写到 ~/.fastclaw/workspaces/<agent>/ 下面
使用 MinIO SDK,写到任何 S3 兼容的桶。Pod 之间通过同一个桶共享文件。
注意 SignedURL — S3 实现可以生成预签名 URL,文件下载请求完全绕过 Gateway,不占用 Pod 带宽。
证据 2:会话管理 — 每次请求从 Postgres 重新加载
internal/session/manager.go
// 在 multi-replica 部署中,每次 Get() 都从 Store 重新加载 Messages,
// 确保 Pod B 处理的请求能看到 Pod A 刚写入的内容。
//
// 如果不这样做,每个 Pod 的内存缓存会与 Postgres 逐渐偏离。
//
// File-backed 模式保持 cache-first(因为只有单进程)。
func (m *Manager) Get(channel, accountID, chatID, projectID string) *Session {
// ... 每次都从 SessionStore (Postgres) 重新加载消息 ...
}
关键设计:不会出现"用户先请求 Pod A,然后请求 Pod B,发现聊天记录没了"的问题。因为每个 Pod 处理请求前都从共享数据库加载最新状态。
证据 3:双写机制 — 工作集 + 归档分离
internal/session/manager.go · Session.Append
| 表 / 位置 | 存储内容 | 特点 |
|---|---|---|
| sessions.messages (JSONB) | LLM 工作集(可能被压缩/摘要) | Agent 推理循环读取这个 |
| session_messages (逐行) | 完整历史,永不压缩 | Dashboard 历史页 / 审计读取这个 |
无论 Compaction 把工作集压缩多少次,完整历史在 session_messages 中保持不变——这对 SaaS 的审计和用户体验至关重要。
证据 4:沙箱懒启动 + 空闲回收 + 自动同步
internal/sandbox/lifecycle.go
LifecyclePool 包装了底层沙箱池,提供两个关键能力:
只有首次工具调用时才创建沙箱。纯聊天的 Agent 永远不启动沙箱,零成本。
后台 goroutine 定期扫描,超过 IdleTTL 未使用的沙箱自动销毁。在 K8s 上意味着按需付费的 E2B 沙箱不会白白计费。
销毁前调用 SnapshotWorkspace → 对比对象存储 → 把差异文件 Put 回去。下次创建沙箱时 hydrateWorkspace 从对象存储还原。
这意味着计算资源(沙箱)是按需分配和回收的,但数据(文件)永远不会丢失——这就是存算分离的核心闭环。
证据 5:Skills 安装 → 对象存储镜像 → 全 Pod 可见
internal/skills/objectstore.go
| 操作 | 做了什么 |
|---|---|
| SyncSkillUp | 安装 Skill 后,将所有文件同步到对象存储 |
| HydrateSkillsDown | Pod 启动 / 重载时,从对象存储下载所有 Skill 到本地磁盘 (双向同步:远程有但本地没有 → 下载;本地有但远程没有 → 删除) |
| MirrorSkillsUp | 反向镜像:检测本地新增的 Skill(如 npx skills add),上传到对象存储 |
证据 6:K8s 部署文件 — 2 副本起步,HPA 扩展到 10
deploy/k8s/fastclaw.yaml
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate: { maxUnavailable: 0, maxSurge: 1 }
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 60 }
Pod 的 volume 是 emptyDir,注释明确写了:
# /data/.fastclaw only holds pod-local ephemeral state (sandbox cache,
# etc.). Workspace artifacts go to the object store (FASTCLAW_OBJECT_STORE_*)
# so they're shared across pods and survive horizontal scaling.
- { name: data, emptyDir: {} }
没有任何 PersistentVolume。所有持久状态在 Postgres + S3 里。这就是存算分离在生产级的最终证明。
4 与典型 Agent 框架的对比
| 维度 | 典型 Agent 框架 (LangChain, CrewAI, …) |
FastClaw |
|---|---|---|
| 会话存储 | 内存 / 本地文件 / 需自行集成 DB | Postgres 内置,多 Pod 共享。工作集 + 归档双表设计。 |
| 文件存储 | 本地磁盘,Pod 重启丢失 | S3 兼容对象存储。LocalFS 用于单机开发,S3 用于生产。 |
| 沙箱 | 无 / 需外部工具 (code Interpreter) | 内置 Docker + E2B 沙箱。懒启动、空闲回收、自动同步。 |
| 多副本 | 需大量自定义工作 | 开箱即用。K8s manifest 直接配 2-10 副本 + HPA。 |
| 多租户 | 需自行实现 | 内置 User / API Key / Agent 三级资源隔离。UserSpace 懒加载。Chater 维度支持公开 Agent 的多用户聊天。 |
| 配置管理 | 配置文件 / 环境变量 | 配置全部存在 DB 中(Configs 表)。System → User → Agent 三层继承。Dashboard 实时修改。CLI 写完后 SIGHUP 热加载。 |
| 运行形态 | Python 脚本 / 库 | Go 单二进制 (~30MB)。daemon 模式。launchd/systemd 服务。 |
5 定时任务的存算分离:沙箱回收 ≠ 任务丢失
这是理解 FastClaw 存算分离最直观的切入点:Agent 通过 create_cron_job 工具创建的定时任务,存在哪里?沙箱回收后还在吗?
答案:在,完全不受影响。
定时任务存在 Postgres 的 cron_jobs 表里,而沙箱只是一个临时容器——两者没有耦合关系。
两条完全独立的路径
Agent → create_cron_job 工具
→ INSERT INTO cron_jobs (Pg)
→ Scheduler 每个 tick 轮询
→ 到点通过 MessageBus 触发
→ Agent 收到消息开始推理
Agent → exec "crontab -e"
→ 写入沙箱容器 /var/spool/cron/
→ 沙箱回收 → 全部丢失
源码证据:Scheduler 直接从 DB 查询到期任务,不依赖任何内存状态:
// scheduler.go — 每个 tick 直接从 DB 查,无内存 job list
func (s *Scheduler) processDueJobs(ctx context.Context) {
jobs, _ := s.store.ListDueCronJobs(ctx, time.Now())
for _, job := range jobs {
s.fireJob(job) // → MessageBus.Inbound → Agent Loop
}
}
// database.go — cron_jobs 表有完整的索引和多 Pod 选主机制
CREATE TABLE IF NOT EXISTS cron_jobs (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
agent_id TEXT NOT NULL,
schedule TEXT NOT NULL, -- cron 表达式 或间隔
next_run TIMESTAMP, -- 调度器按此字段查询
locked_by TEXT, -- 多 Pod 选主:谁抢到锁谁执行
failure_count INTEGER DEFAULT 0,
...
);
CREATE INDEX idx_cron_jobs_schedule ON cron_jobs (enabled, next_run);
触发链路也很清晰:Cron → MessageBus → Agent Loop → 如果沙箱已被回收,LifecyclePool.Get() 会自动创建新沙箱并从对象存储还原 /workspace。
cron_jobs 表 → Scheduler.pollStore() → fireJob() → MessageBus → Agent Loop → 工具调用
↓
LifecyclePool.Get()
(沙箱没了就懒创建一个新的)
而且 Agent 的 prompt 里已经做了约束,禁止把定时需求写到 HEARTBEAT.md:
# context.go — Agent 系统提示词片段
When the user asks you to do something at a specific moment, after a delay,
or on a recurring schedule (e.g. "5 分钟后提醒我", "每天 9 点"),
call the create_cron_job tool.
NEVER write timed reminders into HEARTBEAT.md: that file is reviewed only
on a coarse 30-minute heartbeat tick and is wrong for short-fuse reminders.
要点:在 FastClaw 的架构中,所有需要"在未来某个时间点发生的事"都必须通过 DB 持久化(cron_jobs 表、goals 表),绝不能依赖沙箱内的 Linux 原生 cron。这不是限制,而是存算分离必须遵守的纪律——否则数据就跟计算资源的生命周期绑定了。
6 后台业务能力全景:五种异步机制
FastClaw 支持后台业务,但不是"在沙箱里开个后台进程"——而是把任务意图持久化到 DB,通过 MessageBus 解耦触发,统一由 Agent 推理循环消费。
┌──────────────────────────────┐
│ cron_jobs 表 (DB) │
│ · 定时提醒 · 周期任务 · 延迟 │
└──────────┬───────────────────┘
│ 每个 tick 轮询
┌──────────▼───────────────────┐
│ Cron Scheduler │
│ (Gateway 内置,多 Pod 锁选主) │
└──────────┬───────────────────┘
│ fireJob()
▼
┌──────────┐ ┌────────────────────────────┐ ┌──────────────┐
│ Heartbeat│───▶│ MessageBus │◀───│ /goal 续跑 │
│ (~30min) │ │ (Go channel, 全异步) │ │ (每 turn 后) │
└──────────┘ └──────────┬─────────────────┘ └──────────────┘
│
┌────────▼────────────────┐
│ Agent 推理循环 │
│ HandleMessage() │
│ (统一入口,不管来源) │
└────────┬────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
LLM 调用 工具执行 delegate_task
(子智能体)
1. 定时任务(Cron Jobs)
存储
cron_jobs 表 (Postgres)
触发
Scheduler 轮询 → MessageBus
用途
精确时间点 / 周期性任务 / 延迟提醒
2. 长任务自动续跑(/goal)
用户设定一个目标后,Agent 会自动多轮推进直到完成、预算耗尽或被用户中止。这是 FastClaw 最核心的"后台无人值守"能力。
用户: /goal 把这份200页的文档翻译成英文,预算500K tokens Agent: 开始翻译第1-10页... └─ Turn 结束 → PostTurn hook → goal 还是 Active → 自动续跑 Agent: 继续翻译第11-30页... └─ Turn 结束 → PostTurn hook → 自动续跑 Agent: 继续翻译第31-50页... └─ ...直到完成或预算耗尽 Agent: update_goal status=complete ✓
存储
goals 表 (objectives + token budget + 状态)
触发
PostTurn hook → TryFireContinuation → MessageBus
状态
Active → BudgetLimited → Complete / Paused
源码:internal/agent/goal/ — 独立的 goal 包,有完整的生命周期管理(创建/继续/暂停/恢复/完成/预算耗尽),token 用量实时计费(FoldUsage),注入结构化的 <goal_context> XML 审计提示。
3. 子智能体(delegate_task)
Agent 可以拆解复杂任务,派生子智能体并行或串行处理。子智能体共享父 Agent 的 Provider / Model / Tools,15 分钟超时,一级嵌套(子智能体不能再生子智能体)。
与主 Agent 的关系
- 共享 Provider / Model / Tool Registry
- 独立的 ReAct 循环(私有消息切片)
- 不写 session_messages、不触发 hooks
- 无编译、无 slash command
- 返回结果作为 tool_result 给父 Agent
典型场景
- 并行搜索多个信息源
- 批量处理多个文件
- 把复杂子任务交给专职 Agent
- 父 Agent 汇总多个子 Agent 的结果
注意:子智能体的中间过程不持久化,只有最终结果返回给父 Agent。如果需要完整审计,应在父 Agent 层面记录。
4. 心跳自检(Heartbeat)
频率
约 30 分钟一次
机制
Gateway 以 SourceHeartbeat 身份向 Agent 注入消息
用途
Agent 读取 HEARTBEAT.md 中自己写的待办事项,判断是否需要行动
注意:Heartbeat 不是精确的定时任务。官方文档明确写了:"NEVER write timed reminders into HEARTBEAT.md — that file is only for conditional self-checks"。精确的定时需求必须用 create_cron_job。
5. 任务队列(Task Queue)
并发控制
全局 maxConcurrent 信号量 + 每 chat FIFO 串行化
生命周期
超时机制 + 空闲聊天队列自动回收 + 已完成任务定期清理
可观察
RecentTasks(limit) 支持监控和调试
核心枢纽:MessageBus
internal/bus/bus.go
MessageBus 是所有后台机制的统一消息中枢。四种异步来源通过不同的 Source 标签注入,Agent 循环统一消费,通过 Source 区分是否需要特殊处理:
| Source 标签 | 来源 | 处理差异 |
|---|---|---|
| "" (空) | 用户直接消息 | 默认行为,可以触发 goal 续跑 |
| cron | Cron 调度器 | 不触发 goal 续跑(防止循环) |
| goal_context | Goal 续跑 / 预算耗尽 | 消息标记 OriginGoalContext,Compaction 时过滤 |
| heartbeat | 心跳自检 | 不触发 goal 续跑 |
| subagent | 子智能体 | 不触发 goal 续跑 |
7 值得关注的架构设计细节
🔐 Channel 租约(Lease)机制
多副本部署下,Telegram / Discord / Slack 等 IM 机器人的 webhook 可能打到任意 Pod。FastClaw 用 channel_leases 表 + holder_id(UUID,每个 Pod 启动时生成)来确保同一时刻只有一个 Pod 驱动某个 IM 机器人。租约定期续期,Pod 宕机后会被其他 Pod 抢占。这就避免了 WeChat bot 被两个 Pod 同时回复的尴尬。
🌐 Session Affinity 只为 SSE/WebSocket
SSE 和 WebSocket 是长连接,不能跨 Pod 恢复。K8s 通过 Ingress cookie affinity 将同一用户的 SSE 流粘在同一 Pod 上。但所有数据读写仍然走 Postgres/S3——如果 Pod 挂了,用户重连到 Pod B,Pod B 从 Postgres 加载完整会话历史,继续对话。
🔄 Workspace 文件的双向同步
沙箱里 Agent 写了文件:每次 write_file 或 exec 成功返回后,LifecyclePool.mirrorSandboxWrite 立即将文件同步到对象存储。沙箱被回收时,syncSnapshot 扫描整个 /workspace,把对象存储里没有的文件全部 Put 上去。下次创建沙箱时 hydrateWorkspace 从对象存储 List + Get 所有文件写回沙箱。用户感觉文件一直都在,但实际上沙箱是全新的。
📊 Token 用量计量
SQLMeter 直接将 token 用量 UPSERT 到同一 Postgres 的 token_usage_daily 表。不需要额外的时序数据库或外部计量服务。对 SaaS 场景意味着:用量查询、月度账单、配额限制都可以直接用 SQL 实现。
👥 User / Chatter 双层用户模型
user_id 是 Agent 的拥有者,chatter_user_id 是实际对话参与者。公开 Agent(is_public=true)可以让多个 chatter 与同一个 Agent 对话,每个人都拥有独立的会话和 MEMORY.md,但共享 Agent 的 SOUL.md 和 Skills。这是 SaaS「用户购买一个 bot 并分享给团队使用」场景的原生支持。
🧩 Plugin 系统 — JSON-RPC 子进程
FastClaw 支持通过 JSON-RPC 子进程扩展功能:Plugin 可以是工具提供者、LLM Provider、Channel 适配器或 Hook。Plugin 进程与 Gateway 进程完全解耦——它可以是 Python、Node.js、任何语言的独立进程。这对 SaaS 意味着:不同租户可以运行不同的 Plugin,安全隔离,互不影响。
8 存算分离的代价与限制
每次请求都从 DB 加载会话
相对于内存缓存的 Agent 框架,这引入了额外的 DB 查询延迟。对低延迟场景(如实时语音对话)可能需要额外的缓存层。
沙箱文件同步不是原子操作
syncSnapshot 和 hydrateWorkspace 都是逐文件操作。极端情况下(沙箱在同步过程中崩溃),可能丢失部分文件。源码中也标注了这是"best-effort"。
Docker 沙箱与 K8s 存在天然矛盾
K8s Pod 通常没有 Docker daemon。在 K8s 上只能使用 E2B 云沙箱(付费服务)。官方文档也明确提到了这点。如果不想依赖第三方沙箱服务,需要自己解决"Pod 内启动容器"的问题(如使用 KubeVirt 或 Firecracker)。
Skills 仍需本地磁盘
Skills 最终需要存在于本地文件系统(因为代码执行需要文件)。对象存储只是作为"跨 Pod 同步的管道"。每个 Pod 的 Skills 目录仍然是本地磁盘上的一个副本。
许可证限制
FastClaw Community License 禁止以多租户 SaaS 形态对外提供 FastClaw 本身(需要商业许可)。作为自己产品的后端使用是允许的。
✓ 结论
FastClaw 说自己「更适合作为 SaaS 服务的 Agent 底座」不是营销话术——它在源码级别体现了存算分离的设计理念:
- Gateway Pod 完全无状态。 所有状态(会话、配置、用户、Agent 定义)都在 Postgres 中。Pod 之间除了共享 DB 和对象存储外没有通信依赖。
- 文件存储与计算分离。 Agent 生成的文件存在 S3 兼容的对象存储中,Signed URL 让下载绕过 Gateway。Pod 本地磁盘(emptyDir)只放 ephemeral 缓存。
- 沙箱按需启停,数据不丢。 沙箱懒启动、空闲自动回收——销毁前把文件同步回对象存储,新建时从对象存储还原。计算资源弹性伸缩,数据持久不灭。
- 内置多租户。 User → API Key → Agent 三级资源模型。Chatter 维度支持公开 Agent 的多用户共享。Agent 配额控制每个用户能创建多少 Agent。
- K8s 原生部署。 2 副本起步,HPA 到 10。没有 PersistentVolume,没有 StatefulSet,没有 ConfigMap 挂载配置文件。所有配置通过 DB 管理、SIGHUP 热加载。
一句话: FastClaw 不是又一个 Agent 框架,而是一个面向 SaaS 交付的 Agent 基础设施层。 它将「运行 Agent」这个行为拆成了无状态 API 网关 + 共享关系型数据库 + 共享对象存储 + 弹性沙箱四个正交组件, 让 Agent 服务可以像普通的 Web 服务一样部署、扩缩容和运维。