ARCHITECTURE DEEP DIVE

FastClaw 深度架构分析

为什么它宣称自己是「更适合作为 SaaS 服务底座」的 Agent 运行时
—— 存算分离、多租户就绪、从第一天就为云而生

📦 Go 1.25 单二进制 · 无外部运行时依赖 🗄️ SQLite / Postgres 双模存储 ☸️ K8s 原生 · HPA 就绪

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)
}
LocalFS

把文件写到 ~/.fastclaw/workspaces/<agent>/ 下面

S3

使用 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 表里,而沙箱只是一个临时容器——两者没有耦合关系。

两条完全独立的路径

✅ 正确做法:create_cron_job
Agent → create_cron_job 工具
     → INSERT INTO cron_jobs (Pg)
     → Scheduler 每个 tick 轮询
     → 到点通过 MessageBus 触发
     → Agent 收到消息开始推理
❌ 错误做法:crontab -e
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_fileexec 成功返回后,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 查询延迟。对低延迟场景(如实时语音对话)可能需要额外的缓存层。

⚠️

沙箱文件同步不是原子操作

syncSnapshothydrateWorkspace 都是逐文件操作。极端情况下(沙箱在同步过程中崩溃),可能丢失部分文件。源码中也标注了这是"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 底座」不是营销话术——它在源码级别体现了存算分离的设计理念:

  1. Gateway Pod 完全无状态。 所有状态(会话、配置、用户、Agent 定义)都在 Postgres 中。Pod 之间除了共享 DB 和对象存储外没有通信依赖。
  2. 文件存储与计算分离。 Agent 生成的文件存在 S3 兼容的对象存储中,Signed URL 让下载绕过 Gateway。Pod 本地磁盘(emptyDir)只放 ephemeral 缓存。
  3. 沙箱按需启停,数据不丢。 沙箱懒启动、空闲自动回收——销毁前把文件同步回对象存储,新建时从对象存储还原。计算资源弹性伸缩,数据持久不灭。
  4. 内置多租户。 User → API Key → Agent 三级资源模型。Chatter 维度支持公开 Agent 的多用户共享。Agent 配额控制每个用户能创建多少 Agent。
  5. K8s 原生部署。 2 副本起步,HPA 到 10。没有 PersistentVolume,没有 StatefulSet,没有 ConfigMap 挂载配置文件。所有配置通过 DB 管理、SIGHUP 热加载。

一句话: FastClaw 不是又一个 Agent 框架,而是一个面向 SaaS 交付的 Agent 基础设施层。 它将「运行 Agent」这个行为拆成了无状态 API 网关 + 共享关系型数据库 + 共享对象存储 + 弹性沙箱四个正交组件, 让 Agent 服务可以像普通的 Web 服务一样部署、扩缩容和运维。