Appearance
工作区与沙箱
工具会执行代码、读写文件、访问网络。如果直接在宿主进程里跑,一个 rm -rf 或一条越权 网络请求就能把整台机器拖垮。AgentScope 2.x 用 Workspace 把"工具在哪里执行"这件事 抽象成一层可替换的后端:LocalWorkspace、DockerWorkspace、E2BWorkspace 共享同一套 面向 Agent 的接口,你可以按安全需求切换,而 Agent 代码一行都不用改。
版本基线:本文以 Python
agentscope 2.0.4(PyPI,2026-07-07)为准,要求 Python>=3.11。导入路径为from agentscope.workspace import WorkspaceBase, LocalWorkspace, DockerWorkspace, E2BWorkspace, Offloader(源:src/agentscope/workspace/__init__.py)。 2.x 文档站存在版本化陷阱:非版本化的/en/URL 会 404,本页所有官方链接均使用versions/2.0.4/en/形式。
为什么需要工作区
AgentScope 的内置工具 Bash / Edit / Glob / Grep / Read / Write 都是"会动手" 的--它们执行 shell 命令、改文件、遍历目录。这些操作必须落在一个有边界的执行环境里, 而不是裸跑在宿主文件系统上。Workspace 就是这个边界,它同时承担两件事:
- 供应工具:
list_tools()返回六个绑定到当前后端的内置工具,list_mcps()返回已注册 的 MCP 客户端,list_skills()返回工作区内的 Skills。Agent 通过Toolkit把它们一次性 装配进来。 - 卸载上下文:当上下文逼近 token 上限时,Agent 把压缩后的历史消息和大体积工具结果 卸载到 Workspace 的
sessions/目录,用文件引用替代全量保留。
三后端共享同一套 WorkspaceBase 接口,差别只在"执行发生在哪里":本地进程、Docker 容器、 还是 E2B 云沙箱。
前端类比
如果你来自前端,可以把 Workspace 类比为 CodeSandbox / StackBlitz 的运行容器:你在 编辑器里写的代码不是跑在你电脑上,而是跑在一个隔离的、可销毁的、可重建的环境里。切换 容器后端(本地 Node、Docker、云端 VM)不影响代码本身,只影响隔离级别。
把 WorkspaceBase 类比为抽象的 Runtime 接口,LocalWorkspace / DockerWorkspace / E2BWorkspace 是三种具体实现--就像 Buffer 在 Node.js 里有不同后端一样,Workspace 的 list_tools() / offload_context() 在所有后端上签名一致,只是底层 I/O 走不同通道。
AgentScope 原生语义:Workspace 不是"容器"的别名,而是一个同时实现工具供应与 Offloader 协议的抽象。Offloader 是一个 typing.Protocol,只要实现 offload_context(session_id, msgs) -> str 与 offload_tool_result(session_id, tool_result) -> str 两个 async 方法就算满足协议;WorkspaceBase 恰好实现了它们,所以任何 Workspace 实例都可以直接作为 Agent 的 offloader= 传入。Local 用 LocalBackend 直接操作宿主文件 系统;Docker / E2B 通过各自的 BackendBase 子类把 I/O 路由进容器或云沙箱。不要把前端 "容器 = 浏览器 tab"的心智模型硬套过来--Workspace 是协议实现,不是进程边界。
三后端对比
| 后端 | 隔离级别 | 执行位置 | 持久化 | 典型场景 |
|---|---|---|---|---|
LocalWorkspace | 无强隔离 | 宿主进程文件系统 | workdir 目录天然持久 | 本地开发、调试、单元测试 |
DockerWorkspace | 容器隔离 | Docker 容器内 | 仅当设置 host_workdir 时持久 | 生产部署、CI、需要 OS 级隔离 |
E2BWorkspace | 云沙箱隔离 | E2B 云端 VM | close() 暂停沙箱,文件留存 | 高隔离要求、无本地 Docker、多租户 |
2.0.4 还导出了另外三个后端(源:workspace/__init__.py 的 __all__),API 与上面三者在 WorkspaceBase 层面一致,本文不展开:
DaytonaWorkspace/DaytonaBackend- Daytona 云开发环境沙箱K8sWorkspace/K8sBackend- Kubernetes Pod 沙箱OpenSandboxWorkspace/OpenSandboxBackend- OpenSandbox 远程沙箱
三后端的 workdir 内部布局统一(源:_base.py):
{workdir}/
├── .mcp # 持久化的 MCP 客户端配置(JSON 数组)
├── data/ # 卸载的多模态文件(图片等)
├── skills/ # Skill 子目录,每个含 SKILL.md
└── sessions/ # 按 session_id 分区的上下文与工具结果文件LocalWorkspace 示例
LocalWorkspace 直接在宿主文件系统上工作,没有进程隔离。构造时必须传 workdir(工作区 根目录,不存在会自动创建),然后 await ws.initialize() 完成 MCP 恢复与 Skill 播种:
python
import asyncio
import os
from agentscope.workspace import LocalWorkspace
from agentscope.agent import Agent
from agentscope.model import DashScopeChatModel
from agentscope.credential import DashScopeCredential
from agentscope.tool import Toolkit
from agentscope.message import UserMsg
async def main() -> None:
# 1. 构造并初始化工作区
ws = LocalWorkspace(
workdir="./my_workspace",
skill_paths=["./my_skills"], # 可选:首次 initialize 时播种的 Skill 目录
)
await ws.initialize()
# 2. 从工作区装配 Toolkit
toolkit = Toolkit(
tools=await ws.list_tools(),
mcps=await ws.list_mcps(),
skills_or_loaders=await ws.list_skills(),
)
# 3. 构造 Agent,把 ws 同时作为工具来源(经 Toolkit)与上下文卸载目标
agent = Agent(
name="local_agent",
system_prompt="你是一个能操作文件系统的助手,所有文件操作限制在工作区内。",
model=DashScopeChatModel(
credential=DashScopeCredential(
api_key=os.environ["DASHSCOPE_API_KEY"],
),
model="qwen-plus",
),
toolkit=toolkit,
offloader=ws,
)
# 4. 正常调用 reply,工具会在 ws.workdir 下执行
result = await agent.reply(
UserMsg(name="user", content="在当前工作区创建一个 hello.txt,写入你好。"),
)
print(result.get_text_content())
# 5. 用完关闭(LocalWorkspace 会断开有状态 MCP 连接,workdir 保留)
await ws.close()
asyncio.run(main())LocalWorkspace 也支持 async with 语法,进入时自动 initialize(),退出时自动 close():
python
async with LocalWorkspace(workdir="./my_workspace") as ws:
tools = await ws.list_tools()
# ... 使用 ws ...⚠️
initialize()是幂等的--已 alive 时再调用是 no-op。但忘记initialize()就直接 调用list_tools()会抛RuntimeError: ... has no active backend。
DockerWorkspace 示例
DockerWorkspace 在 Docker 容器内执行所有工具 I/O。构造时不启动容器,需要显式 await ws.initialize()(或用 async with)才会构建/复用镜像并启动容器:
python
import asyncio
import os
from agentscope.workspace import DockerWorkspace
from agentscope.agent import Agent
from agentscope.model import DashScopeChatModel
from agentscope.credential import DashScopeCredential
from agentscope.tool import Toolkit
from agentscope.message import UserMsg
async def main() -> None:
ws = DockerWorkspace(
base_image="python:3.11-slim", # 默认值,必须提供 python3
host_workdir="./docker_workspace", # 绑定挂载到容器 /workspace,None 则容器临时
extra_pip=["httpx", "beautifulsoup4"], # 构建镜像时额外安装的 Python 包
skill_paths=["./my_skills"],
)
await ws.initialize()
toolkit = Toolkit(
tools=await ws.list_tools(),
mcps=await ws.list_mcps(),
skills_or_loaders=await ws.list_skills(),
)
agent = Agent(
name="docker_agent",
system_prompt="你在 Docker 容器内工作,所有操作限制在 /workspace 下。",
model=DashScopeChatModel(
credential=DashScopeCredential(
api_key=os.environ["DASHSCOPE_API_KEY"],
),
model="qwen-plus",
),
toolkit=toolkit,
offloader=ws,
)
result = await agent.reply(
UserMsg(name="user", content="列出当前工作目录下的所有文件。"),
)
print(result.get_text_content())
await ws.close()
asyncio.run(main())关键构造参数(源:_docker/_docker_workspace.py):
| 参数 | 默认值 | 作用 |
|---|---|---|
base_image | "python:3.11-slim" | 基础镜像,必须提供 python3 |
host_workdir | None | 宿主目录绑定挂载到容器 /workspace;None = 临时容器,关闭即丢 |
extra_pip | None | 构建镜像时额外 pip install 的包列表 |
node_version | None | 注入指定主版本的 Node.js(如 "20") |
env | None | 容器内环境变量字典 |
skill_paths | None | 首次 initialize 时播种到容器 skills/ 的本地 Skill 目录 |
镜像按 Dockerfile + COPY 内容做内容哈希打标签(agentscope-workspace:<12hex>),缓存命中 时跳过构建,所以改 extra_pip 或 base_image 才会触发重建。
E2BWorkspace 示例
E2BWorkspace 把执行环境放到 E2B 云沙箱,不需要本地装 Docker。构造时同样不启动沙箱, await ws.initialize() 会按 workspace_id 元数据查找已有沙箱并恢复,找不到则新建:
python
import asyncio
import os
from agentscope.workspace import E2BWorkspace
from agentscope.agent import Agent
from agentscope.model import DashScopeChatModel
from agentscope.credential import DashScopeCredential
from agentscope.tool import Toolkit
from agentscope.message import UserMsg
async def main() -> None:
ws = E2BWorkspace(
template="base", # E2B 默认模板,含 Ubuntu + python3 + curl
api_key=os.environ["E2B_API_KEY"], # 空字符串会回退到 E2B_API_KEY 环境变量
timeout_seconds=300, # 沙箱保活超时
extra_pip=["httpx"],
skill_paths=["./my_skills"],
)
await ws.initialize()
toolkit = Toolkit(
tools=await ws.list_tools(),
mcps=await ws.list_mcps(),
skills_or_loaders=await ws.list_skills(),
)
agent = Agent(
name="e2b_agent",
system_prompt="你在 E2B 云沙箱内工作,所有操作限制在 /home/user/workspace 下。",
model=DashScopeChatModel(
credential=DashScopeCredential(
api_key=os.environ["DASHSCOPE_API_KEY"],
),
model="qwen-plus",
),
toolkit=toolkit,
offloader=ws,
)
result = await agent.reply(
UserMsg(name="user", content="在沙箱里装一个 cowsay 并运行。"),
)
print(result.get_text_content())
# close() 暂停沙箱(pause),文件系统留存,下次 initialize 可按 workspace_id 恢复
await ws.close()
asyncio.run(main())关键构造参数(源:_e2b/_e2b_workspace.py):
| 参数 | 默认值 | 作用 |
|---|---|---|
template | "base" | E2B 模板 id |
api_key | "" | E2B API key,空字符串回退到 E2B_API_KEY 环境变量 |
domain | "" | 自定义 E2B 域名 |
timeout_seconds | 300 | 沙箱保活超时(秒) |
env | None | 沙箱内环境变量字典 |
extra_pip | None | 引导时额外安装到 gateway venv 的 Python 包 |
skill_paths | None | 首次 initialize 时播种到沙箱 skills/ 的本地 Skill 目录 |
E2B 的 close() 调用的是 sandbox.pause() 而非 kill()--沙箱文件系统(skills、.mcp、 sessions、data)完整保留,下次 initialize() 通过 workspace_id 元数据定位并自动恢复。 这使得 E2B 适合需要跨会话保持工作区状态的场景。
Workspace 的双重职责
Workspace 不是单纯"执行环境",它同时承担两个职责,这也是为什么 Agent 构造时既要从 Workspace 装配 Toolkit、又要把 Workspace 作为 offloader 传入的原因。
职责一:供应工具、MCP 与 Skills
WorkspaceBase 提供三个发现方法(源:_base.py),Agent 启动时用它们装配 Toolkit:
| 方法 | 返回类型 | 作用 |
|---|---|---|
list_tools() | list[ToolBase] | 六个内置工具(Bash/Edit/Glob/Grep/Read/Write),均绑定到当前后端 |
list_mcps() | list[MCPClient] | 已注册的 MCP 客户端列表 |
list_skills() | list[Skill] | skills/ 目录下解析出的 Skills |
用户侧还可以动态管理 MCP 与 Skill:
python
# 运行时添加 / 移除 MCP
await ws.add_mcp(mcp_client)
await ws.remove_mcp("some_mcp_name")
# 运行时添加 / 移除 Skill
await ws.add_skill("./new_skill") # 本地 Skill 目录,含 SKILL.md
await ws.remove_skill("skill_agent_name") # 按 agent-facing name 移除add_mcp / remove_mcp 会重写 .mcp 持久化文件(is_persistent=False 的临时容器除外); add_skill 会把本地 Skill 目录打包复制进工作区 skills/,remove_skill 按 Skill 的 agent-facing name 删除其目录。这些操作对 Agent 不是实时生效的--已经装配进 Toolkit 的 工具集不会自动更新,需要重建 Toolkit 或使用 ResetTools 机制(见 工具与 MCP)。
职责二:作为 Offloader 卸载上下文
Offloader 是一个 typing.Protocol(源:_offload_protocol.py),只要求两个 async 方法:
python
class Offloader(Protocol):
async def offload_context(self, session_id: str, msgs: list[Msg]) -> str: ...
async def offload_tool_result(
self, session_id: str, tool_result: ToolResultBlock,
) -> str: ...WorkspaceBase 实现了这两个方法,所以任何 Workspace 实例都满足 Offloader 协议,可以直接 作为 Agent(offloader=ws) 传入。具体行为:
offload_context(session_id, msgs):把消息列表追加到{workdir}/sessions/<session_id>/context.jsonl,每条消息一行 JSON。内联 base64 数据块 会被提取到data/并改写为file://URL 引用,控制单行大小。offload_tool_result(session_id, tool_result):把单个工具结果写成{workdir}/sessions/<session_id>/tool_result-<id>.txt纯文本文件,返回文件路径。
Agent 在上下文压缩触发时(context_config.trigger_ratio 超阈值)自动调用这些方法,把旧 消息和大结果卸载到工作区,上下文里只保留摘要与文件引用。这让 Agent 在长任务中不会因 OOM 而崩溃,同时保留了可检索的原始数据。卸载机制的完整闭环见 上下文与压缩。
与权限系统的关系
权限(Permission)和沙箱(Workspace)是执行链上互补的两道防线,不是替代关系:
| 维度 | Permission | Workspace |
|---|---|---|
| 时机 | 工具执行前 | 工具执行时 |
| 职责 | 三态决策(allow / deny / ask) | 提供隔离的执行环境 |
| 失败半径 | 拦截不该执行的调用 | 限制已执行调用的影响范围 |
一次工具调用先经 Permission 决策:deny 直接拒绝,ask 暂停等用户确认,allow 才进入 Workspace 执行。Permission 阻止"不该跑的命令"(如 rm -rf /),Workspace 保证"该跑的 命令"只在工作区内产生副作用。两者缺一不可--只有 Permission 没有 Workspace,一个被 allow 的 Bash("rm -rf .") 照样能删光宿主目录;只有 Workspace 没有 Permission,Agent 可能在沙箱里执行危险操作后把恶意结果回灌上下文。权限规则的配置见 权限系统。
常见错误
- 把
LocalWorkspace当生产沙箱:LocalWorkspace没有进程隔离,工具直接操作宿主文件 系统与网络。生产环境必须用DockerWorkspace或E2BWorkspace。LocalWorkspace只适合 本地开发与测试。 - Docker 未装 Docker daemon:
DockerWorkspace依赖aiodocker与本地 Docker daemon。 daemon 未运行时initialize()会在连接阶段报错。CI 环境需要确保 Docker service 已启动。 - E2B
api_key未设:E2BWorkspace的api_key=""会回退到E2B_API_KEY环境变量, 但如果环境变量也没设,创建沙箱时会被 E2B SDK 拒绝。务必在进程环境里注入E2B_API_KEY,不要把 key 写死在代码里。 - 忘记
await ws.initialize():构造函数只设置配置,不启动后端。直接调用list_tools()会抛RuntimeError: ... has no active backend。用async with ws:或显式await ws.initialize()。 DockerWorkspace传workdir=而非host_workdir=:2.0.4 的正确参数名是host_workdir。旧名workdir仍被接受(触发 deprecation warning),但应迁移到host_workdir避免未来版本移除。E2BWorkspace.close()后以为沙箱被销毁:close()调用的是sandbox.pause(),文件 系统完整保留。如果确实要销毁沙箱释放资源,需要到 E2B 控制台手动删除,或使用 E2B SDK 的kill()接口(AgentScope 2.0.4 未暴露此路径)。- 期望
add_mcp/add_skill对已运行的 Agent 实时生效:这些方法只改工作区状态,已 装配进Toolkit的工具集不会自动更新。要生效需要重建Toolkit并重新构造 Agent,或使用ResetTools运行时切换工具集。
先修
- 权限系统 - 理解 Permission 三态决策,它是 Workspace 执行前的闸门
下一步
- RAG - Workspace 的
sessions/卸载数据可被 agentic 检索复用