跳到主要内容

第 2 章 · 执行环境与“无 shell 会话”

本章讲清楚:环境层(environments/local.pyenvironments/docker.py)如何执行一条命令;为什么“每条命令开新进程、没有持久 shell”是 mini 的一个关键赌注;以及 agent 怎么靠一个哨兵字符串自己宣告完成。

2.1 环境的契约小得惊人

一个环境只需实现三个方法(Environment Protocol,src/minisweagent/__init__.py:61-70):execute(action)get_template_vars()serialize()。真正干活的就 execute——输入一个 {"command": ...},输出一个 {"output", "returncode", "exception_info"}。就这么简单。

正因为契约这么小,项目里塞得下七种环境(environments/__init__.py_ENVIRONMENT_MAPPING):local、docker、singularity、swerex_docker、swerex_modal、bubblewrap、contree。

2.2 思路:动作之间完全独立(无 shell 会话)

这是 mini 最反直觉、也最值得学的一点。它不维护一个一直开着的 shell。每条命令都是一次全新的子进程。

看本地环境的执行(environments/local.py:24-43):它调用模块级的 _run,而 _runsubprocess.Popen(command, shell=True, ...) 开一个新进程跑这条命令,拿到 stdout 和返回码就结束。下一条命令又是全新的一个。

这带来一组连锁好处与代价:

后果好处 / 代价
命令彼此独立好处:subprocess.run 换成 docker exec 就直接跑在容器里;并行 N 个实例毫无共享状态
没有持久状态代价: cd /fooexport X=1 不会保留到下一条命令
进程组隔离好处: 超时能整组 kill,不留孤儿进程

项目作者认为这是“a big deal”(README 反复强调),因为有状态的 shell 会话在长时间运行里极易出各种诡异故障(卡住、半个命令、状态污染),而独立子进程稳定、可复现、可扩展。

怎么补偿“状态不持久”? 不在代码里补,在 prompt 里告诉模型(config/default.yaml:38-40):“目录/环境变量改动不持久,每个动作在新子shell里执行;但你可以用 MY_ENV_VAR=val cd /path && ... 这样把它们串进一条命令”。又一次:能丢给模型解决的,就不写进脚手架。

2.3 超时:连孩子一起杀

_run(environments/local.py:72-92)有个容易被忽略的细节——用 start_new_session=True 把命令放进新进程组,超时后 os.killpg(process.pid, SIGKILL)整个进程组杀掉:

# 示意,非源码:local.py:72-92 的核心
process = subprocess.Popen(command, shell=True, start_new_session=(os.name == "posix"), ...)
try:
stdout, _ = process.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL) # 杀整组,别留孤儿
stdout, _ = process.communicate()
raise subprocess.TimeoutExpired(command, timeout, output=stdout)

为什么重要: 模型常会跑出 fork 子进程的命令(比如启动一个服务器、跑测试套件)。只杀父进程会留下一堆吃 CPU 的孤儿。杀整个进程组才干净。默认超时 30 秒(LocalEnvironmentConfig.timeout,local.py:13-16)。

2.4 精华:哨兵字符串 + Submitted 异常 = 自我了结

agent 怎么知道“任务做完了”?mini 不另设一个“finish 工具”,而是约定一个魔法字符串。prompt 里教模型:做完后运行 echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT(config/default.yaml:32-33)。

环境在每次执行后检查输出第一行(local.py:45-56_check_finished):

# 真实逻辑,local.py:45-56
def _check_finished(self, output: dict):
lines = output.get("output", "").lstrip().splitlines(keepends=True)
if lines and lines[0].strip() == "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT" and output["returncode"] == 0:
submission = "".join(lines[1:]) # 哨兵之后的内容 = 提交物
raise Submitted({"role": "exit", "content": submission,
"extra": {"exit_status": "Submitted", "submission": submission}})

妙在哪:

  1. 第一行精确匹配 + 返回码为 0,避免模型只是在讨论这个字符串时误触发。
  2. 哨兵之后的所有行就是“提交物”(submission)——在 SWE-bench 里通常是 git diff 的补丁。所以模型可以 echo COMPLETE...; git diff 一把把补丁交出来。
  3. Submitted(继承自 InterruptAgentFlow),异常自带的 exit 消息让 第 1 章的主循环干净退出。完成逻辑因此完全长在环境层,Agent 不必管。

Docker 环境里有一模一样_check_finished(environments/docker.py:140-151)——这是刻意复制而非抽象共享,保持每个环境文件自包含、好读。

2.5 Docker 环境:换个执行器而已

DockerEnvironment(environments/docker.py)和本地版长得几乎一样,差别只在 execute:它把命令拼成 docker exec -w <cwd> <container> bash -lc <command>(docker.py:101-138)。容器在 __init__ 时用 docker run -d ... sleep <container_timeout> 起一个常驻容器(docker.py:74-99),析构时后台 docker stop(docker.py:153-161)。

几个生产级细节:

  • forward_env / env:可把宿主机环境变量转发进容器,或直接设值(docker.py:107-112)。
  • interpreter 可配(默认 ["bash", "-lc"],docker.py:38-42):想用非登录 shell 或换成 python 解释命令都行。
  • run_args 默认 ["--rm"]:容器退出即删,不留垃圾。

这一章证明了 第 1 章开头那句:换沙箱真的就是换一个 execute 实现,Agent 与 Model 一行不动。SWE-bench 批量跑分正是用 docker 环境(见 第 4 章)。

2.6 边界与坑

  • shell=True 意味着环境会原样执行模型给的任意 shell——这正是设计意图(给模型完整 shell),但也意味着安全边界完全靠外层沙箱(docker/singularity/bubblewrap),本地环境直接在你机器上跑命令,务必清楚风险。交互式 confirm 模式(见 第 4 章)就是给本地跑加的人工闸门。
  • cd 不持久是最常绊到模型的点;靠 prompt 提醒缓解,但偶尔模型仍会忘。
  • 输出编码errors="replace"(local.py:80-83),二进制垃圾不会让进程崩,而是替换成占位符。

2.7 代码地图

主题文件符号
环境契约(Protocol)src/minisweagent/__init__.pyEnvironment
本地执行一条命令src/minisweagent/environments/local.pyLocalEnvironment.execute
新进程组 + 超时杀整组src/minisweagent/environments/local.py_run
哨兵检测 → 抛 Submittedsrc/minisweagent/environments/local.pyLocalEnvironment._check_finished
本地环境配置(timeout/env)src/minisweagent/environments/local.pyLocalEnvironmentConfig
Docker 起常驻容器src/minisweagent/environments/docker.pyDockerEnvironment._start_container
Docker exec 执行src/minisweagent/environments/docker.pyDockerEnvironment.execute
容器后台清理src/minisweagent/environments/docker.pyDockerEnvironment.cleanup
环境工厂表src/minisweagent/environments/__init__.pyget_environment _ENVIRONMENT_MAPPING