第 5 章 装进一个工作区:LocalWorkspace
本章代码
code/ch05/main.py· 读完约 14 分钟
5.0 上一章留下的问题
第 4.1 节那个裸循环有一个不易察觉的隐患:工具的工作目录,就是你的家目录。
Read 收到 file_path="~/Desktop/工资条.xlsx" 会照读不误,Write 收到一个绝对路径会照写不误。它没有任何"我该待在哪里"的概念。
这件事不能靠提示词解决。你在 system_prompt 里写十遍"请不要访问工作区以外的文件",它依然可能在某次推理里"顺手"看一眼——不是因为它想造反,而是因为它根本不知道边界在哪。
工程上的正解是:给它一个明确的、物理受限的工作目录,并且只在这个目录里给它工具。
5.1 工作区是什么
Workspace 在 AgentScope 里不是"一个文件夹",而是智能体的运行环境。它负责三件事:

1. 提供工具。 你不再手写 Read()、Write()、Bash()。工作区会根据当前平台自动给你一套,而且它们的工作目录已经锚定在工作区里。
2. 提供技能。 skills/ 目录下可以放 Markdown 指令集,让智能体按需查阅(第 15 章会提)。
3. 承接上下文卸载。 第 8 章会讲,被压缩掉的对话和被截断的工具结果,会卸载到 sessions/ 目录。原文没丢,智能体需要时可以自己去翻。
还有一件容易被忽略的事:工作区会自动生成一段约 3000 字符的系统提示,告诉模型这里有什么目录、该怎么组织项目、该怎么用 git。你不需要写这段提示,但要把它拼进 system_prompt。
5.2 用起来
from agentscope.workspace import LocalWorkspace
from agentscope.tool import Toolkit
async with LocalWorkspace(workdir="./sandbox") as workspace:
tools = await workspace.list_tools()
instructions = await workspace.get_instructions()
agent = Agent(
name="Buddy",
system_prompt="你叫 Buddy,所有文件操作都必须在工作区内完成。\n\n" + instructions,
model=build_model(),
toolkit=Toolkit(tools=tools),
offloader=workspace,
)四行关键代码,逐条说明:
async with LocalWorkspace(...):工作区是异步上下文管理器。进入时初始化目录结构,退出时清理。workdir 是相对路径的话,以当前进程的工作目录为基准。
await workspace.list_tools():注意是 await,它返回一个协程。在我的 Windows 机器上,拿到的是:
['PowerShell', 'Edit', 'Glob', 'Grep', 'Read', 'Write']在 macOS 和 Linux 上,PowerShell 会换成 Bash。同一份代码跨平台可用,这是工作区抽象带来的第一个好处。
await workspace.get_instructions():返回那段自动生成的提示,约 3000 字符。开头是这样:
<workspace>You have access to a local workspace at
C:\Users\xiong\Desktop\mini-workbuddy\code\ch05\sandbox with the following structure:注意它把绝对路径写了进去。这是好事——模型知道自己站在哪。
offloader=workspace:这一行现在看不出效果,到第 8 章上下文压缩被触发时,它的作用才显现。
为什么要把 instructions 拼进 system_prompt
Agent不会自动读取工作区的提示,你必须手动拼。漏了这一步,模型不知道自己有data/、skills/、sessions/这三个目录,会在根目录下乱建文件。 提示词不是越长越好,但关于环境的提示不能少——模型看不见你的文件系统,你告诉它什么,它的世界就有多大。
5.3 路径边界是怎么生效的
工作区不是在系统层面做隔离(LocalWorkspace 没有这个能力)。它做的是两件事:
- 把工具的工作目录锚定在
workdir,相对路径都以它为基准 - 在工具的权限检查里识别越界路径,超出工作集的操作会被拦下
第一件事决定了模型"默认看到哪里",第二件事决定了它"能不能硬闯"。
这两层防线是配套的:只有第一层,模型写个 ../../.. 就绕过去了;只有第二层,模型会在每次操作时都撞墙,体验很差。
下图是七种后端的对比。你只需要记住一件事:LocalWorkspace 不提供真正的隔离,它跑在你的系统上,PowerShell 或 Bash 能做的事它都能做,只是被权限规则约束着。

LocalWorkspace 适合本地开发和教学——快、直观、文件能直接用编辑器打开看。但只要这个 Agent 会接受来自他人的输入,就该换成 Docker。原因很简单:容器里 rm -rf / 只是删掉一个容器,本机上 rm -rf / 是另一回事。
5.4 先读后写:一条会救你的规则
第 3 章提过 Write 的"先读后写"规则,现在能说清它的价值了。
设想这个场景:工作区里有一份 config.yaml,模型要"把数据库地址改成新地址"。如果没有这条规则,它可能直接 Write 一份全新的 config.yaml——里面只有数据库地址,其他配置全没了。它没有恶意,它只是没看过原文,以为那文件里只有地址。
有了这条规则,Write 会失败,模型收到"文件已存在但未读取",于是乖乖先 Read,看到全部配置,再用 Edit 做精确替换。
这条规则把"模型必须有完整信息才能改动"编码进了工具的行为里。 这是工程约束优于提示词约束的又一个例子。
5.5 完整代码
from agentscope.permission import PermissionMode
async def main() -> None:
prepare_files()
async with LocalWorkspace(workdir=WORKDIR) as workspace:
tools = await workspace.list_tools()
print("工作区提供了这些工具:", [t.name for t in tools])
instructions = await workspace.get_instructions()
agent = Agent(
name="Buddy",
system_prompt=(
"你叫 Buddy,是一个文件管理助手。"
"所有文件操作都必须在当前工作区内完成,不要访问工作区以外的路径。"
"\n\n" + instructions
),
model=build_model(),
toolkit=Toolkit(tools=tools),
offloader=workspace,
)
agent.state.permission_context.mode = PermissionMode.BYPASS
question = (
"看一下工作区里有哪些文件,把 README 的内容总结成三行,"
"然后写入 summary.md。"
)
reply = await agent.reply(UserMsg(name="user", content=question))
print(reply.get_text_content())
# 退出上下文后,文件仍然在磁盘上——工作区是持久化的
print(tree(WORKDIR))运行结果(目录部分是真实的):
工作区提供了这些工具: ['PowerShell', 'Edit', 'Glob', 'Grep', 'Read', 'Write']
工作区还会自动写一段系统提示(共 2955 字符),开头是这样:
----------------------------------------------------------------------
<workspace>You have access to a local workspace at
C:\Users\xiong\Desktop\mini-workbuddy\code\ch05\sandbox with the following structure:
----------------------------------------------------------------------
>>> 你:看一下工作区里有哪些文件,把 README 的内容总结成三行,然后写入 summary.md。
<<< Buddy:已读取 README.md,总结如下,并已写入 summary.md。
======================================================================
工作区当前结构(./sandbox):
sandbox/
README.md
summary.md
todo.txtsummary.md 是 Agent 自己创建的。退出 async with 之后它还在——工作区是持久化的。这里的 __aexit__ 不在本章代码里,而是 LocalWorkspace 从框架基类继承的:它只调用 LocalWorkspace.close() 关闭 MCP 连接和运行状态,不会删除 workdir 里的文件。
5.6 三个常见错误
错误 1:忘了 await
tools = workspace.list_tools() # 拿到一个协程对象
agent = Agent(..., toolkit=Toolkit(tools=tools), ...)报错信息通常是 'coroutine' object is not iterable。list_tools()、get_instructions()、get_tool_schemas() 全是异步方法。
错误 2:把工作区当成安全边界
LocalWorkspace 只约束路径,不隔离进程。模型通过 PowerShell 执行 Get-Content C:\Users\你\Desktop\a.txt 依然可能成功——因为那是命令,不是路径参数。
真正的隔离需要容器,或者在权限系统里对命令做静态分析(第 7 章)。
错误 3:workdir 用了绝对路径,然后在别的机器上跑
LocalWorkspace(workdir="C:/Users/xiong/Desktop/sandbox") # 换台机器就废了除非你确定部署环境固定,否则用相对路径,或者从环境变量读。
5.7 小结
- 工作区是运行环境:提供工具、提供技能、承接上下文卸载
list_tools()按平台给工具,Windows 上是PowerShell,Linux 上是Bash- 工作区会自动生成约 3000 字符的系统提示,必须手动拼进
system_prompt LocalWorkspace只约束路径,不隔离进程;要隔离就换 Docker- "先读后写"把工程约束编码进了工具行为,比提示词可靠
下一章要解决的问题:工作区给的工具是通用的——读、写、搜、跑命令。但你的业务是具体的:查内部数据库、调某个接口、按公司规则算报价。这些工具,框架没有,得你自己写。
