第 04 章 · Executor 与工作区边界
本章目标:理解 Agent 中最值得谨慎设计的不是 prompt,而是“模型意图如何变成真实副作用”的边界。
1. Executor 是信任边界
模型输出来自概率系统,也可能被仓库文件、网页或错误日志里的恶意文本影响。Executor 不能因为参数是合法 JSON 就认为动作安全。它必须重新验证工具名、参数类型、路径、命令、超时、输出大小和权限。
可以把一次执行拆成六步:解析参数、规范化路径、权限判定、实际执行、结果归一化、记录审计。任何一步失败都应返回结构化错误给模型,而不是让整个 Python 进程崩溃。
2. 路径为什么必须先解析再比较
只判断字符串是否以 workspace 开头是不安全的。路径可能包含 ..、符号链接或大小写差异。正确做法是把 workspace 和目标都解析成绝对规范路径,再判断目标是否位于允许根目录之下。
root = WORKSPACE.resolve()
target = (root / user_path).resolve()
if target != root and root not in target.parents:
raise PermissionError("path escapes workspace")
还要考虑符号链接在检查后被替换的竞态、特殊设备文件、巨大文件和二进制内容。教学版可以先守住目录穿越,生产版需要更强的操作系统沙箱。
3. 工具应该小而清楚
read_file、write_file 和 run_shell 足以理解核心,但生产 Agent 通常拆得更细:
| 工具 | 目的 | 为什么不都交给 shell |
|---|---|---|
| list_files | 有界地查看目录结构 | 返回可控、易压缩 |
| search_text | 搜索符号和文本 | 可以限制数量并忽略大目录 |
| read_file | 读取指定范围 | 避免一次塞入整个大文件 |
| apply_patch | 精确修改 | 能审查 diff,减少覆盖用户改动 |
| run_shell | 构建、测试、Git | 只处理确实需要进程的动作 |
工具越宽泛,模型越难正确选择,权限也越难精细控制。一个“execute_any_python”工具很灵活,但几乎等于把整台电脑交出去。
4. 结果也需要边界
命令输出可能有几百 MB。Runtime 应限制字节数、保留头尾、标注截断,并保存完整日志到文件供按需读取。返回值最好包含:
{
"ok": false,
"exit_code": 1,
"stdout": "...",
"stderr": "AssertionError at tests/test_api.py:42",
"truncated": false,
"duration_ms": 831
}
这样模型能区分“工具函数异常”“命令正常运行但测试失败”“进程超时”和“输出被截断”。如果所有错误都变成字符串 ERROR,后续策略会非常粗糙。
5. 副作用和可撤销性
读取、写入、网络请求、删除和外部发送的风险不同。Executor 应给工具标记副作用级别。写文件前可以记录旧内容或依赖 Git diff;删除应优先移动到临时区;外部发送必须在动作发生前审批,而不是发送后通知。
对于有副作用的工具,还需要幂等键或事务思维。创建 PR、支付、发邮件、写数据库不能因为 API 重试而执行两次。Agent 框架必须知道“模型调用重试”和“真实动作重放”不是一回事。
6. 对照当前项目
当前 minimal_swe_agent.py 把文件操作限制在 agent-lab/workspace,并让 run_shell 默认询问 y/n。这是两个不同机制:路径检查限制技术范围,询问机制让人决定一次动作。后面的安全阶段会把它们扩展成沙箱和审批策略。
红线
API key 不属于模型上下文,也不应出现在工具输出、命令历史和教学截图里。Executor 需要把秘密当作独立的运行时输入,并在日志层脱敏。
本章验收不是记住路径代码,而是能解释:为什么模型提出动作之后,仍必须经过独立的参数校验、权限判定和结果归一化。