workspace 模块|三层抽象、四种后端、一个不暴露端口的网关
本文是 AgentScope 2.0 源码解析系列 第 9 篇(workspace 模块)。
已发:第 1-8 篇(agent / event / message / model / tool / permission / middleware / rag 等) | 下一篇:mcp 模块(workspace 的网关深度依赖它)。
关注追更,后续会持续更新 mcp / skill / state 等模块。
整个 workspace 只对外暴露 WorkspaceBase一个抽象基类,LocalWorkspace直接落地,DockerWorkspace/E2BWorkspace/K8sWorkspace都继承自中间层SandboxedWorkspaceBase——定制差异被压进两个抽象方法_provision_backend/_teardown_backend所有跨沙箱文件/进程操作最终收拢成 BackendBase的 3 个原语:exec_shell/read_file/write_file;Docker 的get_archive、E2B 的files.read、K8s 的exec全部翻译成这 3 个,上层内置工具(Bash/Read/Write/Edit/Grep/Glob)零感知沙箱后端里的 MCP 服务器不直接暴露给宿主机:容器内跑一个 FastAPI 网关( _mcp_gateway_app.py),宿主机用python3 -c拼一段 stdlib 脚本(_gateway_shim.py)走exec_shell代理每一次 HTTP 请求——不需要端口映射,不需要宿主机能连到沙箱网络MCP 注册表持久化在 ${workdir}/.mcp(一个 JSON 数组),这是唯一跨重启的配置;网关启动时直接读它当配置文件LocalWorkspace多了一套基于SKILL.mdSHA-256 哈希的技能去重和.skills索引,沙箱后端走更朴素的 tar 打包路径offload_context/offload_tool_result把对话上下文和工具结果落到${workdir}/sessions/,inline base64 多模态载荷先抽到data/再改写成file://引用
如果只记一件事:workspace 把「在哪儿执行」(BackendBase 3 个原语)和「执行什么」(工具/MCP/技能)彻底解耦,再用一个只绑 loopback、靠 exec_shell 代理的进程内网关吸收掉所有沙箱的网络异质性。这套「抽象最小执行原语 + 把网络可达性难题关进网关」的思路,搬到任何「宿主要驱动一个网络不可达的隔离环境」的场景都成立。
适合谁读:在搭 Agent 工程化方案、需要给 Agent 一个安全执行环境(本地/容器/云沙箱)的工程师。预计阅读:主线约 22 分钟(字段表较多,附录另需 8 分钟)。
一、这个模块到底在解决什么问题?
一个能跑代码、调工具的 Agent,光有 LLM 是不够的。它得有一个地方放文件、跑 shell、装技能、挂 MCP 服务器——而且这个地方最好能被换掉:本地开发时用宿主机目录最快,上生产时换成 Docker 容器隔离,要做多租户或弹性时再换成 E2B 云沙箱或 K8s Pod。
workspace 模块就是来解决这件事的。一句话概括它的职责(来自 _base.py 的模块 docstring):
一个 workspace 提供 Resources(技能)、Tools(MCP 和内置工具)、Offload(上下文与工具结果的持久化)。
如果没有这个模块会怎样?每换一个执行环境,Agent、工具、MCP 调用方都得重写一遍 I/O 逻辑——Docker 要走 exec API、E2B 要走 commands.run、K8s 又是另一套。workspace 模块把这层差异整个吃掉了:上层只看到统一的 list_tools / add_mcp / offload_context,底下是本地目录还是云沙箱,对 Agent 透明。
二、这个模块在整个框架中的位置
先看它在框架里和谁打交道。
上游(谁用它): Agent是主消费者,调list_tools/list_mcps/offload_context/offload_tool_result;用户侧动态增删走add_mcp/remove_mcp/add_skill/remove_skill;开发者管生命周期initialize/close。下游(它依赖谁): tool.BackendBase(执行后端抽象)、mcp.MCPClient(MCP 客户端)、skill.Skill(技能对象)、message.Msg(上下文持久化的消息类型)。谁依赖它:Agent 实例持有它; mcp模块后续文章中会展开,沙箱后端里网关就是agentscope.mcp的直接用户。
三、为什么这样设计?
这是理解这个模块的关键。我读到一半的时候,最强烈的一个感受是:作者在用「抽象层数」换「新增后端的成本」。下面把几个关键设计决策拆开讲。
决策 1:为什么是三层抽象,而不是一个大基类 + 四个子类?
workspace 的继承树是这样的:基类 WorkspaceBase 下分两支——LocalWorkspace 直接落地,SandboxedWorkspaceBase 作为中间层,下面再挂 DockerWorkspace / E2BWorkspace / K8sWorkspace 三个沙箱后端。
为什么中间要插一层 SandboxedWorkspaceBase?因为三个沙箱后端共享的东西太多了:都要在沙箱内启动一个 MCP 网关、都要轮询 /health、都要从 .mcp 恢复注册表、reset / add_mcp / remove_mcp 的网关路由逻辑完全一样。如果只有两层,这三段逻辑要在三个子类里各抄一遍。

中间层用模板方法模式收口:initialize / close / reset 是定稿的模板,子类只填两个钩子——
_provision_backend:把沙箱拉起来,绑好self._backend_teardown_backend:把沙箱拆掉
外加一个可选的 _bootstrap_commands:首次启动时要在沙箱里跑的 shell 命令(装 uv、建 venv、装 agentscope)。Docker 因为镜像构建时已经装好,返回空列表走快速路径;E2B / K8s 返回真实的 apt + curl 命令。
这个设计的好处很直接:新增一个后端,只要实现三个方法,其余全部白拿。
决策 2:为什么所有 I/O 最终只有 3 个原语?
这是整个模块最值得品味的一处。BackendBase(在 tool 模块)定义了一堆文件系统方法:file_exists / is_dir / list_dir / stat_mtime / delete_path……但只有 3 个是抽象方法,其余全是基于这 3 个的默认实现:exec_shell、read_file、write_file。
为什么这样切?因为这 3 个是任何能跑 shell 的环境都必然支持的。file_exists 可以用 exec_shell(["test", "-f", path]) 实现,list_dir 可以 ls,delete_path 可以 rm -rf。于是 DockerBackend 只实现这 3 个(分别映射到 container.exec / get_archive / put_archive),E2BBackend 也只实现这 3 个,其余全部继承。
这套切法的代价是:is_dir 这种操作要多走一次 exec_shell 往返。但换来的是新增一个后端只要写 3 个方法——极度降低了抽象的「实现税」。
决策 3:为什么沙箱里的 MCP 不直接暴露端口给宿主机?
这是读这个模块时最容易困惑的一点。常规思路是:容器里跑个 FastAPI,宿主机映射个端口,直接 HTTP 调。但 workspace 偏不这么干。原因写在 _gateway_client.py 开头:
每个请求都在沙箱内执行:宿主机把请求体写进沙箱的临时文件,通过 backend.exec_shell 拉起一段小 Python 脚本,解析脚本打到 stdout 的 JSON 信封。不需要宿主机→沙箱的网络可达性——网关只绑沙箱的 loopback。
为什么这么设计?因为三种沙箱的「宿主机能不能连到沙箱网络」差异极大:Docker 可以 publish 端口但要改配置且多租户下端口冲突;E2B 云沙箱宿主根本不在同一个网络;K8s Pod 端口要 Service / port-forward,运维负担重。作者选了对所有后端都成立的那条最小公倍数:沙箱里总能跑 python3,那就用 exec_shell 跑一段 stdlib 脚本去访问 loopback 上的网关。

决策 4:为什么 .mcp 是唯一的持久化配置?
workspace 的标准目录布局:.mcp(持久化的 MCP 客户端配置,JSON 数组)、data/(卸载的多模态载荷)、skills/(技能子目录)、sessions/(每会话的上下文和工具结果文件)。
其中 .mcp 是唯一一份「配置」级别的持久化,因为 MCP 注册表是跨重启要恢复的核心状态。它的格式就是 [MCPClient.model_dump(mode="json"), ...] 的 JSON 数组,add_mcp / remove_mcp 改完内存就重写它;网关启动时直接拿它当 --config。这个设计让「配置即文件」:没有数据库、没有额外的配置服务。
持久化是有条件的:_save_mcp_file 在 is_persistent == False 时直接 return。Docker 如果没挂 host_workdir,容器是临时的,写 .mcp 没意义,就跳过——这个判断在 DockerWorkspace.is_persistent 里(return self.host_workdir is not None)。
四、跟着我阅读源码
23 个文件不用全读。按重要度排,真正必看的是这几个。
必读文件 1:_base.py(734 行)—— 整个模块的脊梁
这个文件定义了 WorkspaceBase,它不是个空壳抽象类,而是带着大量共享实现的基类。读它要抓住三条主线:
派生路径( _data_dir/_skills_dir/_sessions_dir/_mcp_file这几个 property):全部通过self.get_backend().join_path(self.workdir, ...)拼出来。注意它们都依赖workdir——子类必须在调用任何基类方法前设好self.workdir。offload 家族( offload_context/offload_tool_result/_offload_data_block):这是 Agent 长上下文管理的落地实现。技能管理的朴素实现( list_skills/add_skill/remove_skill):基类版本走「tar 打包 → 写到沙箱 tmp → 容器内python3 -c解压」的通用路径;LocalWorkspace会覆盖成更精细的哈希去重版本。
有个细节值得停下来看:add_skill 里内嵌了一段 _EXTRACT_TAR_SHIM 字符串(_base.py:81),它是在沙箱内跑的解压脚本,里面手动做了 path traversal 防护(检查每个 tar member 解压后是不是落在 dst 目录内)。为什么不用 Python 的 tarfile.extractall(filter='data')?因为沙箱里的 Python 版本不一定够新(3.12+ 才有 filter 参数),手动检查更可控。
必读文件 2:_sandboxed_base.py(455 行)—— 沙箱后端的共享大脑
如果说 _base.py 是「对所有 workspace 都成立」的逻辑,那 _sandboxed_base.py 就是「对所有沙箱 workspace 都成立」的逻辑。核心是 _setup_mcp_gateway(_sandboxed_base.py:365),它干四件事:
首次启动 bootstrap:如果 _gateway_script不存在,跑_bootstrap_commands()装 venv 和脚本;Docker 镜像构建时已装好,走快速路径跳过清残留: pkill -f _mcp_gateway_app.py || true杀掉上次 resume 残留的网关进程,让新进程能干净绑定端口拉起网关: nohup {venv_python} -u {gateway_script} --config {mcp_file} --port {port} > {log} 2>&1 &轮询 /health:带指数退避( delay = min(delay * 1.5, 1.0))轮询最多 30 秒,超时就 tail 网关日志报错
第 4 步的失败处理很值得学。超时后它不是简单抛「timeout」,而是 read_file(self._gateway_log) 读最后 2000 字节日志一起塞进异常消息——因为「网关没起来」的原因(依赖装失败、端口冲突、.mcp 损坏)全在日志里,不带上日志调用方根本没法排查。
必读文件 3:_gateway_client.py(668 行)—— 网关代理的三件套
这个文件有三个类:GatewayClient(宿主机侧的门面)、GatewayMCPClient(继承自 MCPClient,但把 connect / close / list_raw_tools / get_tool 全部改成走网关 HTTP)、GatewayMCPTool(__call__ 就是 POST /mcps/{name}/tools/{tool} 一次代理)。
最值得读的是 exec_request(_gateway_client.py:474)——这是「代理」二字的全部实现。它把请求体写到沙箱临时文件,调 exec_shell(["python3", "-c", SHIM_SCRIPT, method, url, body_file, ...]),然后解析 shim 打到 stdout 的 JSON 信封。大响应(>4MB)会溢出到沙箱临时文件再读回来,避免 stdout 通道塞爆。
还有个细节:_diagnose_failure(_gateway_client.py:587)。任何请求失败,它会先探一下 /health,如果网关死了,就把网关日志 tail 出来打到 ERROR。注意它特意排除了 /health 路径本身——否则一个死网关的 health 探测失败会递归触发对 health 的诊断。
必读文件 4:_gateway_shim.py + _mcp_gateway_app.py —— 两端的两半
这两个文件是一对:_gateway_shim.py(93 行)是宿主机侧的一段字符串,通过 python3 -c 在沙箱里跑,用纯 stdlib(urllib.request)发一次 HTTP 请求。为什么不用 curl?因为不能假设每个后端镜像都有 curl(K8s 的 python:3.11-slim 就没有),但网关 venv 必然有 python3。
_mcp_gateway_app.py(191 行)是沙箱内的 FastAPI 应用,6 个端点。它直接 from agentscope.mcp import MCPClient,给每个 MCP 配置项建一个真实 client。注意它用绝对导入——是为了避免触发 agentscope.workspace.__init__ 的重导入图(那会拉进 skill / tool 一大堆沙箱不需要的东西)。
字段表
下面这些是后续调试/扩展时要回头查的索引。第一次读只需精读表 1(BackendBase 三个抽象原语)一张,其余按需查阅,不影响理解主线。
表 1:BackendBase 三个抽象原语(核心,必读)
exec_shell | (command: list[str], *, cwd, timeout) | ExecResult | ["sh","-c",line]) |
read_file | (path: str) | bytes | FileNotFoundError |
write_file | (path: str, data: bytes) | None |
ExecResult 的字段:exit_code: int(传输错误为 -1)、stdout: bytes、stderr: bytes。这三个原语就是所有沙箱 I/O 的最终落点。
表 2:WorkspaceBase.__init__ 构造参数(按需查阅)
workspace_id | str \| None | None | |
default_mcps | list[MCPClient] \| None | None | .mcp 不存在时的种子 MCP |
skill_paths | list[str] \| None | None | skills/ 的本地技能目录 |
注意 default_mcps 和 skill_paths 都是种子:只有「全新工作区」才会用,一旦 .mcp / skills/ 已有内容就以已有内容为准。
表 3:WorkspaceBase 关键实例属性(按需查阅)
workdir | str | /workspace) |
is_alive | bool | |
_backend | BackendBase \| None | get_backend() 取 |
_mcps | list[MCPClient] | |
_mcp_lock_skill_lock | asyncio.Lock | .mcp 文件 / skills/ 目录的并发改 |
表 4:DockerWorkspace.__init__ 参数(按需查阅)
base_image | str | "python:3.11-slim" | python3 |
host_workdir | str \| None | None | /workspace 的宿主目录;None 则容器临时 |
node_version | str \| None | None | "20")则从官方 Node 镜像拷 node/npm |
extra_pip | list[str] \| None | None | |
gateway_port | int | 5600 | |
env | dict[str,str] \| None | None |
表 5:SandboxedWorkspaceBase 子类要填的钩子(核心,扩展时看)
_provision_backend | self._backend;返回前必须保证 backend 可 exec_shell | |
_teardown_backend | ||
_bootstrap_commands | [] |
类属性 _gateway_home(网关 venv/脚本/日志的目录)、gateway_port、_bootstrap_cmd_timeout(Docker 1800s / E2B 600s / K8s 继承 1800s)也由子类设。
五、代码到底是怎么运行起来的?
用一个具体场景串起来:用户 async with DockerWorkspace(host_workdir="/tmp/ws", default_mcps=[...], skill_paths=[...]) as ws:,然后 await ws.list_mcps()。跟着这条线索走全程。
第 1 步:__aenter__ → initialize()
SandboxedWorkspaceBase.initialize 是模板方法,依次执行四步:
_provision_backend()(Docker 子类填的钩子): aiodocker.Docker()连 daemon →_build_or_reuse_image()用prepare_build_context渲染 Dockerfile + 算内容哈希 tag(agentscope-workspace:<12hex>),先images.inspect(tag)探缓存命中,没命中才images.build走 tar 流构建 →_create_and_start_container():Cmd=["sleep","infinity"]让容器常驻,可选 bind-mounthost_workdir:/workspace,最后self._backend = DockerBackend(container, "/workspace")_ensure_workspace_layout(): mkdir -p出workdir/data/skills/sessions/_gateway_home;然后处理.mcp:存在就校验它是合法 JSON 数组(损坏就重播种 defaults),不存在就写 defaults_setup_mcp_gateway():Docker 镜像已含脚本跳过 bootstrap; pkill清残留 →nohup拉起网关 → 轮询/health→ 成功后self._gateway = GatewayClient(...),再self._mcps = await self._gateway.list_mcps()把种子 spec 换成网关侧的 live proxy_setup_skills():如果 skills/已有内容就跳过(尊重已有状态);否则遍历skill_paths逐个add_skill
第 2 步:await ws.list_mcps()
直接返回 list(self._mcps)——这些是第 1 步从网关拉回来的 GatewayMCPClient 实例。注意它们和本地 MCP 的区别:GatewayMCPClient.connect 不建本地 stdio/HTTP,而是 POST /mcps 在网关上注册。
第 3 步:Agent 调某个 MCP 工具
走到 GatewayMCPTool.__call__(_gateway_client.py:129):POST /mcps/{mcp_name}/tools/{tool_name},body 是 {"arguments": kwargs}。这个请求通过 GatewayClient.exec_request 走 shim 进沙箱,网关收到后 _call_tool 找到对应 MCPClient,get_tool(tool) 拿到真实工具对象再执行,结果打包成 {"chunk": ...} 回传。
整条链路的精妙之处在于:Agent 完全不知道工具其实在另一个隔离环境里跑。它拿到的 ToolBase 接口和本地 MCP 工具一模一样,差异全被 GatewayMCPTool 吸收。
第 4 步:长上下文 offload_context
offload_context(_base.py:398):deepcopy 一份 → 遍历每条消息,把里面的 inline base64 DataBlock 抽到 data/(用 base64 文本的 SHA-256 当文件名,同块第二次卸载直接命中已存在文件短路)并改写成 URLSource(file://...) → 每条消息 model_dump_json() 一行 → 追加写到 sessions/{session_id}/context.jsonl。offload_tool_result 类似,但落成 tool_result-{id}.txt 纯文本。
第 5 步:close()
先 self._gateway.aclose()(其实是 no-op),再 _teardown_backend()(Docker:Linux 下先 chown 还原 bind-mount 文件属主给宿主用户,再 kill + delete(force=True) 容器,最后关 aiodocker client)。整个 teardown 吞异常,保证 close 永远安全可调。
六、如何开始调试源码
按这个顺序下断点,每一步观察什么:
第一站:SandboxedWorkspaceBase.initialize( _sandboxed_base.py:165)。观察四个步骤的执行顺序。如果启动卡住,多半卡在第 3 步的/health轮询。关键变量:self._backend在第 1 步后必须非 None;self._gateway在第 3 步后必须非 None。第二站:health 轮询( _sandboxed_base.py:432-449)。网关起不来时,断点设在raise RuntimeError那行,看tail变量——它是网关日志最后 2000 字节,错误原因都在里面。常见死因:.mcp文件损坏、bootstrap 命令失败、端口被残留进程占用。第三站:GatewayClient.exec_request( _gateway_client.py:474)。调 MCP 工具报错时看这里。观察result.exit_code和result.stdout——shim 的 JSON 信封解析失败说明 shim 脚本本身崩了,看result.stderr。第四站:_offload_data_block( _base.py:513)。调试上下文持久化时看这里。观察hash_str(base64 文本的 SHA-256,不是解码后字节的)——理解为什么用 base64 文本哈希(同块第二次卸载能短路)。第五站:LocalWorkspace._reconcile_skills_dir( _local_workspace.py:539)。调试技能索引问题时看这里。观察current_mtimevsskills_file["skills_dir_mtime"]——不等才会触发调和。
七、如何扩展这个模块
应该改(推荐路径)
新增一个沙箱后端:继承 SandboxedWorkspaceBase,实现_provision_backend/_teardown_backend,按需覆盖_bootstrap_commands。这是设计好的官方扩展点,参考E2BWorkspace(最简洁)和K8sWorkspace(最复杂,带 PVC 生命周期)。自定义网关行为:改 _mcp_gateway_app.py。它是独立脚本,宿主侧通过_read_gateway_script_bytes()打包进镜像/沙箱。注意改完要同步更新 Docker 的内容哈希。自定义本地技能管理:覆盖 LocalWorkspace的list_skills/add_skill/remove_skill。基类也提供了通用版本(tar 打包路径),如果你的后端支持更高效的同步方式可以覆盖。
不应该改(有更优替代)
不要直接改 self._mcps 绕过 add_mcp / remove_mcp:这两个方法管着 .mcp文件持久化和网关注册/注销。直接改内存列表会导致.mcp和网关状态不一致。不要在 WorkspaceBase 层加后端特有逻辑:本地特有的放 LocalWorkspace,沙箱共有的放SandboxedWorkspaceBase,某后端特有的放对应子类。
千万不要改(动了会破坏不变量)
不要改 BackendBase 的 3 个抽象原语契约: exec_shell返回ExecResult(exit_code为-1表传输错误)、read_file不存在抛FileNotFoundError、write_file自动建父目录——这三个契约是上层所有内置工具的共同假设。不要改 get_backend() 的「实时取」语义:它每次都读 self._backend而不是让调用方缓存——因为 Docker/E2B 在重连时会替换 backend,缓存会拿到 stale 引用。不要删 _ensure_workspace_layout 里对 .mcp 的合法性校验:那段 try/except 不是多余的—— _save_mcp_file写到一半 crash 会留下半个 JSON,下次启动网关直接 brick。校验 + 重播种是容灾关键。
新增后端 → 填两个钩子;改 MCP → 走 add/remove;3 个原语契约和 get_backend 实时语义 → 别碰。这三条守住,扩展就不会破坏既有不变量。
八、本模块最值得学习的设计
读完整块代码,我带走这几个判断。
1.「抽象最小执行原语」的切法,值得反复揣摩。BackendBase 有十几个方法,但只把 3 个定为抽象。这个切法的依据不是「哪些方法常用」,而是「哪些方法在任何能跑 shell 的环境里都必然可实现」。file_exists 能用 test -f 实现,所以不抽象;exec_shell 没法用别的东西实现,所以抽象。判断标准是「实现的最小性」,不是「调用的频率」。
2. 用「进程内网关 + exec_shell 代理」吸收网络异质性。 这是最能体现工程功底的一处。面对「三种沙箱的网络可达性差异巨大」这个问题,作者没有去给每个后端写一套端口暴露方案,而是选了「所有后端都支持 exec_shell」这条最小公倍数,把 HTTP 请求塞进一次 shell 调用里。代价是每次请求一次往返,换的是「同一份网关客户端代码、三种沙箱零改动」。这是个典型的「用确定的、小的运行时成本,换掉一整类跨后端的不确定性」的权衡。
3. 把「配置即文件」贯彻到底。.mcp 既是持久化、又是网关启动配置、又是 add/remove 的写入目标——三重身份一份文件。没有数据库,没有配置中心。前提是各角色对格式的需求一致,不一致时不能硬套。
4. 容灾细节藏在不起眼的地方。.mcp 写坏会校验重播种(防 crash brick);offload_context 的 base64 哈希用文本而不是字节(同块第二次短路);网关 /health 失败带日志 tail;Docker teardown 在 Linux 下 chown 还原属主;shim 用 stdlib urllib 而非 curl。每一个都是「踩过坑才会写」的代码。
5. 中间层 SandboxedWorkspaceBase 是教科书式的模板方法。initialize / close / reset 定稿成模板,子类只填 provision/teardown 两个钩子。新增后端的成本被压到了最低。
九、阅读建议
如果你打算自己读这 23 个文件,建议这个顺序:
先读 _base.py的模块 docstring(前 44 行)——它用一段 ASCII 树把整个标准布局画清楚了,是理解所有路径的地图。然后读 _base.py的WorkspaceBase——重点看派生路径 property 和 offload 家族。跳到 _sandboxed_base.py——读initialize/close模板和_setup_mcp_gateway。这是整个模块设计含量最高的地方。挑一个沙箱后端细读,推荐 E2BWorkspace(最简洁)——看它怎么填三个钩子。读这个就懂了「新增一个后端要做什么」。再读 _gateway_client.py的三个类——理解宿主侧怎么驱动沙箱内网关。exec_request是核心。_gateway_shim.py和_mcp_gateway_app.py一起读——它们是网关的两半,对照着看请求怎么往返。_local_workspace.py最后读——它覆盖了基类的技能管理,逻辑最繁(哈希去重 + mtime 调和)。_make_dockerfile.py和三个 Backend 文件按需查——扩展时再细看。
可以跳过的:__init__.py(只是导出)、_offload_protocol.py(一个纯 Protocol)、各 _constants.py(路径常量)。
十、阅读完成以后
读完后你应该能回答:workspace 为什么要三层抽象?因为沙箱后端共享逻辑太多,中间层用模板方法收口能最大化复用。代码怎么运行?一条 initialize → provision backend → 建布局 → 起网关 → 种技能 的模板,之后所有调用走 exec_shell 代理的进程内网关。要改应该改哪里?新增后端填两个钩子,改 MCP 走 add/remove,不要碰 3 个原语契约和 get_backend 的实时语义。
真正应该带走的:把「在哪儿执行」收敛成最小原语、把「网络可达性」这类跨后端难题关进一个进程内网关——这两条思路在任何要驱动多种隔离环境的系统里都用得上。
workspace 的精髓不在「支持了四种后端」,而在「用 3 个执行原语 + 1 个进程内网关,把这四种后端的差异整个吸收掉」——上层完全感知不到底下换了什么环境。
留两个问题
你做 Agent 工程化时,给 Agent 的执行环境是怎么选的——本地目录、Docker、还是云沙箱?踩过「换个环境 I/O 全得重写」或者「容器里跑的服务宿主连不上」的坑吗?
想深入的同学再想一个:如果让你加一个 Firecracker 微虚机后端,_bootstrap_commands 你会怎么写?网关启动失败的排查链路你会怎么设计?
觉得有用?点个「在看」或转发给同样在搞 Agent 工程化的朋友。系列持续更新,关注不迷路。
下一篇:Mcp 模块源码解析,敬请关注。
夜雨聆风