解读下这个https://tetral.ai/blog/the-next-scaling-problem/
回复 joye: 看完了。这篇是 Tetral 团队(作者从 Anoma 做到 Tetral)写的一篇云 Agent 基础设施设计长文,核心一句话:他们之前把 Agent 的容量单位搞错了——不该是 sandbox,而应该是 runtime。全文基本是在讲怎么把「跑 Agent 的循环」从「跑代码的沙箱」里拆出来。 第一部分,为什么原来的做法不对。本地 Agent(Claude Code、Codex CLI)看起来就是装在电脑上的程序,于是 Manus 那篇 context engineering 给了个自然推论:action 跑在 VM sandbox 里,文件系统就是可恢复的 context。作者第一版产品照着最字面的路线把 agent loop 塞进 E2B,结果 sandbox 成了容量单位,扩 Agent 就等于扩沙箱。问题有三个:一是沙箱机队本身要管理;二是 session 历史和 trace 留在沙箱里,调试得去碰装着用户隐私数据的环境;三是密钥在沙箱外,还得额外搞个网关代理模型请求。后来考虑 K8s(pod 不适合高频创建销毁的沙箱,Modal 也公开写过同样的错配)和 EC2+Firecracker+自建控制面,但后者等于自己去做一个沙箱供应商。所以结论是:Agent 和沙箱是两条不同的扩缩容曲线——Agent 是稳定的共享服务,沙箱是突发、一次性、隔离的执行资源,必须分开。「电脑应该是 Agent 去调用的东西,而不是 Agent 居住的地方。」 第二部分,runtime 边界怎么划。判断标准很干脆:任何 owner、生命周期或扩缩容模式不同的职责,都必须搬出 runtime。于是 Gateway 拿走供应商格式/凭证/流式协议(无 session 状态,可独立扩);PostgreSQL 成为已接受输入、进度和结果的唯一真相源;Bridge 拿住数据库边界,负责归属校验、顺序、事务和幂等回执;Queue 负责排队、租约、重试、取消和死信;Sandbox Service 负责机器生命周期;Public API 和 Event Stream 也在执行路径之外。剩下的只有「对 session 和 thread 的计算」:session 是一段持续的 Agent 工作,thread 是类似 actor 的有序执行路径(一个 session 有 root thread,可以 fork 出子 thread 给 subagent),reducer 是纯函数、不做任何 IO,只回答下一步是调模型、路由工具、等输入还是结束;runtime pod 只是可替换的算力宿主,什么都不拥有。 这里有个值得注意的路线对照:Cursor 走的是 Temporal——worker 跑 agent loop,用 Event History 重放 workflow 代码恢复控制流,另外维护一份 append-only 的对话存储。Tetral 不走重放,它让 Postgres 同时持有状态迁移、对话历史和投影,替换的 runtime 从记录重建 thread checkpoint,再问纯 reducer 要当前状态和下一步。好处是控制状态只归一个事务性数据库权威管,代价是要自己实现一套协议。 第三部分最硬核,write-ahead execution。可替换 runtime 的死结是:pod 挂了以后,新 pod 怎么知道某个操作是没开始、进行中、还是已完成?猜错要么漏做、要么重复副作用。他们的解法借了 WAL 的排序规则——先记录被授权的迁移,再执行操作。runtime 向 Bridge 提交带稳定身份的不可变声明确认归属和顺序后,Bridge 在一个事务里提交迁移加回执,只有拿到 ack 才允许真正 dispatch;稳定身份就是幂等键,重试同一份声明只会拿回已有回执,拿同一身份装不同内容会被拒。文里用「模型返回一个 Bash 调用」走了全流程:输入先提交才进 context,span.model_request_start 记下精确的上下文边界,agent.tool_use 先提交才能路由,Bridge 一个事务里同时记账沙箱执行和 Queue job,agent.tool_result、session_messages 更新、标记结果已消费、写回执还是同一个事务——只有 ack 之后 reducer 才允许依赖这个结果。存储模型借鉴 OpenCode v2 的「事件表 + session-message 投影」:session_events 是有序执行日志,session_messages 是喂模型的上下文投影,compaction 只是生成新的摘要检查点、不擦掉旧日志,所以历史以后还能变成可检索上下文,而不是让一份摘要背下整个过去。系统不存运行状态机的可变快照,冷加载就靠重建。同时作者老实承认边界:这套协议没法把任意外部系统变事务化,它只是给每个副作用一个已提交的身份和归属,让重试收敛到同一个操作。 第四部分讲投递的持久化。用户消息到达时如果没 runtime pod 就绪,就走 outbox 式的原子入队:一个事务里追加输入事件、建 Inbox 记录、建指向它的 Queue job,提交后 API 才返回 200,processed_at 只有 runtime 真正把输入提交进目标 thread 才置位。pg_notify 只负责叫醒 Job Runner,不带内容、不持有投递状态,丢了还有轮询兜底。Queue 按 thread 保证顺序,后面的普通消息不能越过正在投递或重试的那条,不同 thread 并发;session 级变更构成跨 lane 的屏障,interrupt 单独走一条不被普通输入堵住的通道。投递收尾分两步:Bridge 记 Inbox 为 accepted,Job Runner 再 ack Queue job。丢 RPC 响应就同身份重试;丢 ack 就让下一个 runner 看到 accepted 直接把 job 关掉不再重投。这里有个细节挺讲究:超时本身不能证明旧 runtime 停了,所以用 binding generation + pod UID 当 fencing token,必须等 K8s 确认绑定的 pod 身份已经消失,Bridge 才把已 accepted 的投递退回 Queue。 第五部分,模型和工具调用怎么不碰密钥。请求里不带任何 API key 或 OAuth token;Gateway 校验 pod 的 K8s workload identity 加一个短命 Bridge token(证明它还拥有那个 session/thread),被替换的 pod 刷新不了 token 就调不动原来的 thread。凭证按 session 选,缺失/吊销/归档/解不开一律拒绝而不是静默 fallback,只有完全没选凭证的 session 才能用平台公共池。OAuth 刷新也在这条边界上做:锁行、刷新、存回、只把新 access token 注入出站请求,明文永远不进 runtime、Bridge、session log 或沙箱。格式转换走 Vercel AI SDK,返回归一化的 ProviderStreamEvent,Gateway 不选模型、不留状态,所以任何副本都能服务。流还开着时 pod 挂了,Bridge 把这个 request 标成 runtime_pod_lost 收尾,让 reducer 重建一致状态,而不是把半截响应当完整结果。MCP 一样:初始化时只存已接受的 tool list 版本,runtime 拿到名字、描述和 input schema,拿不到服务器凭证和连接细节;调用前先提交 agent.mcp_tool_use,连接器再校验权限、注入 session 凭证、处理 OAuth 刷新。 第六部分,电脑当资源用。大部分能力不需要电脑——模型和网页请求归 Gateway,MCP 归 connector,memory 走 Bridge,只有 shell 和文件系统工具才要执行环境。所以沙箱是懒分配的。激活(activation)可以是复用、启动、新建或替换,选中的文件和 helper 状态没就位就再来一个 materialization job,并发调用可以共享同一次激活而不是各起一台机器。提交命令前 Sandbox Service 会把这次执行绑到当前那台具体机器上,防止过期 worker 把已经接受的命令发给新机器;runtime 挂了也不会取消电脑操作、不需要重发命令。现在实现是每 session 懒绑一台 Daytona 机器,但作者强调这只是当前实现、不是抽象——以后文件访问可能走虚拟文件系统,小任务用轻量容器,其他工作可能要 Linux/macOS/Windows 或专用算力。这也呼应了腾讯云那类「同源同构沙箱」的思路,但这里更强调接口抽象而不是同构。 第七部分是二维扩展的收尾:workspace 放长生命周期资源(文件、memory),session 是一次持续任务(配置、历史、生命周期、选定资源),thread 是 session 内一条有序上下文和执行路径。thread 共享 session 但不共享 context,session 共享 workspace 但不共享执行历史——这两句话是全篇架构的分水岭。子 Agent 控制模型借鉴 Codex 的多 Agent 设计:父可以按选定上下文 fork 子(fork_turns 支持 none / all / 最近 N 轮,解析成不可变的起始上下文)、继续自己干活、发消息、等待、打断。spawn_agent 也是先提交后返回:Bridge 一个事务里建父子关系、存上下文和指令、把指令放进子线程 Inbox、建投递 job,ack 之后工具调用才返回 child thread ID,重试返回同一个 child。fork 完两个上下文彻底隔离,父的新消息不会自动出现在子里,互相也不能直接写对方上下文,后续通信和子线程完成都走同一条有序输入路径(send_message、child completion、wait_agent),子线程的最终答案先提交再进父的 Inbox,等待不引入第二条持久内容通道、也不会重跑子线程的工作。最后还区分了粒度:多条执行路径仍属同一任务就用 child thread;而写代码、写文档、审计、知识维护这类需要独立 root agent、独立上下文/agent 版本/凭证集/生命周期的,应该拆成不同 session,只是可以从同一个 workspace 里选资源。 我的评价:这不是又一篇「context engineering」技巧文章,而是在回答云 Agent 的产品化问题——多租户并发下,状态放哪、副作用怎么不重复、密钥怎么不进沙箱、扩缩容怎么做成两个维度。它最值得带走的三点是:把可替换性当作一等设计目标(于是有了 WAL 式先记账再执行);把「身份/顺序/幂等」收进一个事务性权威(Bridge+Postgres),而不是散在各个服务里;以及明确「电脑是资源、不是家」。局限也很明显:整套东西强绑 PostgreSQL 的强事务能力和 K8s 的身份体系,纯 serverless 或事件溯源路线要照搬得重新算账;文里也没给延迟和成本数据,活跃 thread 的 reducer 单点调度在高并发下会不会成为新瓶颈,得看后续。