目录

起因

我们团队这一两年用 AI 做编码工作的比例越来越高。最初是本地跑 Claude Code 之类的 CLI,后来发现一个实际问题:我每开一个 agent 任务,我的笔记本就被它占着。Agent 在 build、跑测试、改文件、思考下一步;我同时想做点别的事,CPU 风扇就开始嚎叫。更别说想同时开五个 agent 跑五件不相关的事了。

更难受的是协作。我跑了一个有意思的 agent 流程,想给同事看看进展,只能截图或者复制粘贴对话。同事想接手,我得把工作目录的状态打包给他。每次都像在邮件附件里来回传 word 文档。

我们要的其实是一个远端、可分享、可扩展的 AI Coding 工作环境——本地 IDE 仍然在,但 agent 长时间任务跑在某个"别处",我们只是订阅它的进度。像 Anthropic Managed Agents 那种调一下 API、流式拿事件的体验,但跑在我们自己的基础设施上,用我们自己的密钥、连我们自己的内部工具。

我们看了几条路:

  • 自己搭一套 K8s 沙箱集群。 技术上行得通。但镜像、网络、配额、安全加固、跨区,光这一摊够开一个团队做一年。我们才几个人。
  • 用 Replit / E2B / Modal 之类的现成沙箱。 数据要走第三方,与 GitHub 的集成又要拼凑。
  • 直接调 Anthropic / OpenAI 的现成 managed agents。 不能用我们内部的 MCP server,不能用我们改过的 system prompt,不能用我们自己的工具策略,且数据走外。

最后我们绕回到一个看起来不大像主流答案的方向:用 GitHub Actions 的 runner 当沙箱。

听起来有点奇怪。GHA 一向是 CI/CD 的工具,有 6 小时硬性超时,runner 是临时的,不像是给长任务设计的。但仔细想想,它已经具备我们要的几乎所有性质:

  • 它是隔离的容器,每次任务全新开,不污染
  • 它原生有仓库的 checkout 权限,我们就是要 agent 操作 GitHub 上的代码
  • 它有完整出站网络,调内部服务、外部 API 都行
  • 它有成熟的 OIDC,让我们能做到"沙箱仓库零密钥"
  • 私有仓库部分我们自己买配额,公开仓库直接用 GitHub 免费额度
  • 它的不稳定性和超时是已知的、可以被工程化对待的

我们决定试一下。


三件我们想要的事

把"自己用"这层去除浪漫化,具体落到我们的日常,需求其实很务实:

第一,我们要能"撒出去"。 我开五个 agent 跑五件事,它们跑在不同的 runner 上,互不干扰。我合上电脑回家了,它们继续跑。第二天早上回来收果子。

第二,我们要团队可见可接手。 任何一个 agent 任务,谁都能用 SDK 订阅它的事件流,在 web 端看到 agent 现在在干什么、思考什么、用了什么工具。需要打断、追加输入、批准某个高风险动作,谁都能介入。它不是某个人本地的私有进程。

第三,我们要演化和实验自由。 想试新模型、想换 system prompt、想给 agent 接一个新的内部 MCP server、想加一条 hook 拦截某类工具——所有这些应该在中央配置一次,所有跑出去的 agent 立刻生效。我们不想每个工程师本地维护一份"我自己的 Claude 配置"。

这三件事合起来,就是我们要建的东西:一个我们自己用的 AI Coding 工作台。


第一件想清楚的事:不掩盖 GHA 的特性

最初的设计冲动是这样的:既然 GHA 6 小时会断、runner 偶尔不稳,那我们就做一套完整的 checkpoint 机制——agent 的工作目录、文件系统、临时文件、运行进程,全部快照下来,新沙箱起来后无缝恢复。技术上做得到。

我们做了几轮这种设计,越做越觉得别扭:

  • 每次工具调用都要做原子检查点(commit + 上传 + ack),正常路径被拖慢
  • 文件系统快照得用 git bundle 或者增量 tar 传到控制面,带宽和存储都不便宜
  • 控制面要管"影子仓库"或对象存储,这一摊比业务代码还复杂
  • 而且 agent 一旦做了有外部副作用的事(调过外部 API、发过 Slack),你根本就重放不了——重放就是重做副作用

真正的转折是承认一个事实:GHA 的不稳定是它的本质特性,不是缺陷。 我们自己作为这个工作台的用户,完全有能力理解这件事。我们的工作不是把 GHA 伪装成永远不死的容器,而是在这个特性之上提供清晰的契约。

于是我们把责任清楚划开:

谁负责 / 怎么处理
工作目录里的文件能不能跨沙箱保留agent 频繁 git push 是首选;沙箱正常结束时(包括超时)post 钩子兜底自动 commit + push
对话状态能不能跨沙箱继续平台
外部副作用的幂等我们自己(通过工具策略 / 平台的幂等代理)
GHA 的 6 小时上限我们自己的认知

这个划分一旦做出来,设计立刻清爽:

  • 我们不需要管"工作目录的持久化"这个本质上无解的事
  • 我们只需要保证一件事:对话不会丢,新沙箱能从对话中断的地方继续

只要这一件事做到了,agent 自己会处理工作目录的差异——它有完整的对话记忆,知道"我之前编辑过这些文件",它会用 git 检查、必要时重做。Token 多花一点,但工程复杂度量级下降。

不过 agent 频繁 push 终究是契约,实际上 agent 写代码时不会每编辑一个文件都立刻 commit。这里 GHA 给了我们一个干净的钩子:post 步骤。Action 在 main 退出之后会再跑一次清理代码,无论 main 是正常结束、被取消、还是被超时杀。 我们把这个钩子用足:每个 segment 退出时,post 步骤检查工作目录是否有未提交的改动,统一 commit + push 到工作分支,顺便把最新一份会话快照传回控制面。

只有当 runner 真的硬崩(OOM、infra 故障,post 也跑不到),才会丢 agent 在最后一段时间的工作。这种情况罕见,且我们不去试图救它——救它就是回到最早被砍掉的那条复杂路径。日常使用的绝大多数场景里,即便 agent 自己懒得 push,代码最终也都妥妥落到了 GitHub 上。

我们写了一份很短的"用户契约"贴在 README 第一页:Agent 跑在临时沙箱里,频繁 commit + push 是好习惯;沙箱正常结束时我们会兜底,但硬崩时丢失不可避免。 团队所有用这个工作台的人,都从这一句话开始。


第二件想清楚的事:控制面拥有真相,沙箱是一次性燃料

把"对话连续性"作为唯一硬承诺之后,系统结构立刻清晰:

控制面是一切真相的源头。 它持有:

  • 每条对话事件(agent 说的每句话、用过的每个工具、思考的每一步)
  • 每份会话快照(用于重新启动 agent 时还原内部状态)
  • 每个任务的状态机(创建、执行、暂停、恢复、完成)
  • 每份运行配置(模型、系统提示、工具白名单、限额等)

沙箱是无状态的执行燃料。 它启动时:

  • 从控制面拉运行配置
  • 从控制面拉会话快照(如果是续接)
  • 从 GitHub clone 我们的目标代码
  • 跑 agent
  • 把事件实时回推控制面
  • 周期性回推会话快照
  • 收到中断信号或完结时优雅退出

如果沙箱在过程中崩了,控制面知道。它从池里取一个新沙箱,把上一份快照塞过去,新沙箱起来从那里接着跑。Agent 不感知,我们不感知。

会话快照是 Claude Code 自己的内部 jsonl 格式。我们决定把它当成不透明二进制 blob——不试图理解,不试图解析,Claude Code 自己的格式交给 Claude Code 自己用。控制面只负责存进去、拿出来、校验完整性。“少懂一点反而更稳"的取舍贯穿了整个设计。


第三件想清楚的事:沙箱仓库里 0 个 Secret

这件事让我最满意。我们的沙箱仓库里没有任何长期密钥——没有 ANTHROPIC_API_KEY,没有控制面访问 token,没有 GitHub PAT。

听起来不可能。Action 里要调 Anthropic API,要回推数据到控制面,这些总要密钥吧?

GitHub Actions 内置的 OIDC 让这件事变得优雅。每个 workflow run 都能向 GitHub 索要一份签名后的身份令牌(JWT),令牌里写明了"我是这个仓库、这个 workflow、这个 run、跑在这个分支”。这份令牌发给控制面,控制面用 GitHub 公开的密钥验签,然后核对令牌身份是否匹配它在派发任务时的预期。匹配上,发一个 15 分钟有效期的访问令牌。匹配不上,门都进不去。

整个机制下,任何能拿到控制面 API 的人,必须是当下正在沙箱组织里跑的、特定 workflow、特定 run 的 runner——别人冒充不了。哪怕沙箱仓库的全部 git history 都被泄漏,也没有任何能复用的 token。

最美的地方是:沙箱仓库就是一个空仓库。 上面只有十行 workflow 模板和一个 README。没有 secrets 配置面板要担心,没有泄漏面要审计。安全模型干净得让人想笑。


我们现在用起来的样子

技术决策的最终检验是日常体验。

我们用一个简单的 SDK 发起任务:指定哪个 agent、哪个仓库、要它做什么,然后拿到一个任务对象,流式订阅它的事件,必要时回应审批和提问,等终态。整个调用从笔记本、内部 web 端、甚至从另一个 agent 里都能发起(agent 套娃也是常见用法)。任务一旦派出去,我合上电脑都可以,事件继续流向控制面;同事打开 web 端立刻能看见进度,可以直接接管。

而沙箱仓库里那个 workflow 文件,十行不到,永远长这样,业务变化时不会动到这个文件。它只声明触发方式、声明权限、调用我们发布的那个 Action,就结束了。没有业务行,没有 secret 引用,没有 per-任务的配置——所有这些都在控制面。

我们要换模型?改控制面。要调系统提示词?改控制面。要新增一个内部 MCP server 让 agent 能查公司 wiki?改控制面。要给某类高风险工具加一道 hook 审批?改控制面。沙箱仓库一行不动。这是把"基础设施"和"配置"严格拆开之后才能拿到的体验,也是它和"在自己电脑跑一份 Claude Code"最本质的区别——后者每个工程师维护自己的一份配置,前者整个团队共享一份事实。


续接发生时,什么都不会发生

最让我们自己满意的,是日常使用中续接基本是隐形的

假设一个 agent 跑了 5 小时 55 分,GHA 触发 timeout,给 runner 发 SIGTERM。Action 的 main 接到信号,告诉 claude “完成手头这一轮就停”,等子进程退出,自己也退出。这时 GHA 的 post 步骤接力跑:把工作目录里 agent 没来得及推的内容统一 commit + push 到工作分支,把最新一份会话快照传回控制面,发出"段落暂停"事件,然后退出。控制面把任务标记为待续接,几十秒后从池里取一个新沙箱发任务,新沙箱起来,clone 工作分支(已经包含 post 兜底的那一笔 commit),下载快照,重启 claude code,注入一句"沙箱重建了,你的工作分支当前在 commit X,如果发现内存中的某些文件改动不在,请用 git 检查",agent 继续做。

整个过程对订阅事件流的我们来说,只是收到了一个 segment.suspended 事件和几十秒后的 segment.resumed 事件。如果 web 端 UI 想做炫酷一点,可以显示"正在重新组装沙箱…"。如果不在意,直接当什么都没发生。

事件流的连续性比物理执行的连续性重要——这是这套设计最核心的洞察。只要"对外可见的对话流"看起来连续,内部基础设施怎么折腾都行。


关于复杂度的感想

设计这个工作台的过程,反复出现同一种模式:

想到一个炫酷的方案 → 列出实现细节 → 发现细节里有大量反复出现的复杂度根源 → 退一步问"这部分到底是谁的责任" → 把责任划清楚之后,炫酷方案变得不必要。

每一轮"砍掉炫酷功能"的设计,都让系统更简洁、更可靠、用起来更清晰。这次特别明显的一点是——很多复杂度不是来自需求,而是来自我们错误地认为"自己不应该理解某些底层事实"

GHA 不稳定?我们应该理解。Agent 工作目录不持久?我们应该理解。外部副作用不能重放?我们应该理解。

这些理解一旦变成契约的一部分,系统就能极简。如果硬要替自己隐藏这些事实,系统会膨胀,而且永远无法在所有边界情况下保持一致——总会有那么一两个角落,我们最终还是要直面这些事实,而那时对系统的信任已经被之前的"贴心隐藏"提前透支了。

工作台是给自己用的。所谓"自己用",意味着我们既是工程师又是用户,享受不到给外部用户做产品时那种"用户教育成本"的借口——因为我们就是那些用户。所以反过来,我们也不必假装自己什么都不懂。


这套设计不擅长的事

需要诚实地说,有些场景这个工作台不合适:

单 segment 6 小时不够的任务,会让 prompt cache 大量失效。 跨段重启时如果间隔超过 1 小时,缓存就过期了,token 消耗会显著放大。极长任务我们的做法是在 system prompt 里教 agent 主动把进度写到 issue 或 PR description,确保关键信息不只在内存里。

严依赖外部副作用的工作流要谨慎。 比如 agent 一边跑一边发 Slack、一边触发 CI/CD、一边写下游数据库,这些副作用在 segment 重启时无法撤销。我们提供工具策略层禁掉危险工具,提供幂等代理让常见副作用安全可重试,但本质上这是工程问题,不是平台能消化的。

它不替代实时延迟敏感的应用。 端到端事件延迟 p95 大约 400ms,对代码生成、PR 自动化、PR review、bug 调查这类工作完全够用,但对实时聊天、语音交互这类场景不合适——那种场景应该直接走 LLM 厂商的 API,不需要套一层 GHA。


为什么写这篇

我们自己用得越来越顺手之后,越来越觉得这个思路或许对其他类似规模的团队有用——不是说这套架构应该被复制,而是说这种"主动减小自己承诺范围"的设计取向在 AI 工具领域可能被低估了。

回头看这套设计最不寻常的地方,是它主动减小自己的承诺范围。我们没有承诺"完美的容器持久化",没有承诺"突破 GHA 时间限制",没有承诺"无感隐藏所有底层细节"。

我们只承诺一件事:对话不会丢,新沙箱能从对话中断的地方继续。

围绕这一件事,我们设计了一个干净的状态机、一个清晰的事件协议、一个零密钥的认证模型、一个十行的 workflow 模板。

很多技术决策,只有在你放弃试图什么都做之后,才能找到。