mini-swe-agent 源码总览
mini-swe-agent 是 Princeton & Stanford SWE-bench 团队在 2025 年重新设计的一个极简 AI 软件工程 agent。它不是 SWE-agent 的功能删减版,而是基于一个核心观察的重新思考:随着语言模型能力提升,2024 年那套"自定义工具 + ACI 接口 + 复杂历史处理器"的 agent 架构里,很多东西已经不必要了。
mini 只保留了一个工具——bash。模型不需要学会调用各种自定义工具,它只需要像人类工程师一样在终端里敲命令:用 ls 探索目录,用 cat/nl/sed 查看和编辑文件,用 grep 搜索,用 python 跑脚本验证,最后用 echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT 提交结果。架构简单到核心 agent 类不到 100 行 Python。
这份笔记按运行职责展开,涵盖设计哲学、组件接口、配置系统和模板渲染四条主线。
1. 设计哲学
1.1 为什么只要 bash
mini 从 README 起就明确了自己的立场:
- 没有任何 bash 以外的工具——甚至不需要用模型的 tool-calling 接口。任何模型只要有基本的指令遵循能力就能跑。
- 所有动作通过
subprocess.run执行——每次调用都是独立的 subshell,不保持有状态的 shell session。这使得沙箱化变得极其简单(把subprocess.run换成docker exec就完成了隔离),也让批量扩展不再需要处理 session 状态同步。 - 完全线性的消息历史——每一步只是往消息列表后面追加,轨迹(trajectory)就是传给模型的消息历史本身。调试和微调时不需要从复杂的历史处理器里还原"模型实际看到了什么"。
1.2 与 SWE-agent 的区别
README 里有一段官方对比,直接说明了两边各自的适用场景:
用 mini-swe-agent 如果你需要:
- 本地命令行工具,快速启动
- 极简的控制流
- 更快的沙箱化与 benchmark 评估
- 做微调或强化学习,不希望 agent scaffold 本身成为过拟合的来源
用 SWE-agent 如果你需要:
- 试验不同工具组合,每种工具有自己的接口
- 试验不同的历史处理器
两者都能在 SWE-Bench 上拿到很好的分数。
1.3 代码规模
整个项目的 Python 源码结构非常扁平:
src/minisweagent/
├── agents/ # 2 个 agent 类(default + interactive)
├── config/ # YAML 配置 + 配置加载逻辑
├── environments/ # local, docker, singularity + extra 沙箱
├── models/ # litellm 模型 + utils(action 解析等)
├── run/ # CLI 入口 + benchmark 运行脚本 + 工具
└── utils/ # 日志、序列化
每一层都不深。Agent 不继承复杂的基类体系,Model 和 Environment 只靠 Protocol 做 duck typing,配置通过 YAML + Jinja2 模板驱动。整个项目的复杂度上限被刻意压得很低。
2. 组件接口
2.1 三个 Protocol
__init__.py 里定义了三个核心组件的 Protocol,它们不是抽象基类,只是类型标注的骨架。真正的运行时不需要显式继承任何东西:
class Model(Protocol):
config: Any
def query(self, messages: list[dict[str, str]], **kwargs) -> dict: ...
def format_message(self, **kwargs) -> dict: ...
def format_observation_messages(self, message: dict, outputs: list[dict], template_vars: dict | None = None) -> list[dict]: ...
def get_template_vars(self, **kwargs) -> dict[str, Any]: ...
def serialize(self) -> dict: ...
class Environment(Protocol):
config: Any
def execute(self, action: dict, cwd: str = "") -> dict[str, Any]: ...
def get_template_vars(self, **kwargs) -> dict[str, Any]: ...
def serialize(self) -> dict: ...
class Agent(Protocol):
config: Any
def run(self, task: str, **kwargs) -> dict: ...
def save(self, path: Path | None, *extra_dicts) -> dict: ...
用 Protocol 而不写抽象基类,意味着你可以传任意对象进去,只要它实现了这些方法就行。mini 内置的 LitellmModel、LocalEnvironment、DefaultAgent 三者之间只通过这三个接口耦合,不依赖彼此的具体实现。
2.2 组件装配
hello_world.py 展示了最小绑定方式:
agent = DefaultAgent(
LitellmModel(model_name="anthropic/claude-sonnet-4-5-20250929"),
LocalEnvironment(),
**yaml.safe_load(Path("config/default.yaml").read_text())["agent"],
)
agent.run("Write a sudoku game")
mini.py(CLI 入口)更完整:用 YAML 配置 + 命令行参数合并得出最终配置,再通过 get_model() / get_environment() / get_agent() 三个工厂函数创建组件。工厂函数支持字符串短名(如 "local"、"interactive")和完整类路径两种指定方式。
2.3 配置合并
配置来源有多个层次,通过 recursive_merge 逐层叠加:
默认 YAML 文件(如 mini.yaml)
→ 额外的 -c 配置文件
→ 命令行 key=value 覆盖(如 -c model.model_name=...)
→ 内联生成的 run/agent/model/environment dict
→ 最终的 recursive_merge(*all_configs)
recursive_merge 的逻辑是:后面的覆盖前面的,嵌套字典递归合并,UNSET 这个 sentinel 值被跳过。CLI 用 UNSET 标记用户没有显式设置的可选参数,这样配置文件里的默认值不会被空值覆盖掉。
3. 模板系统
3.1 模板渲染
Agent 启动时会渲染两个 Jinja2 模板:
system_template→ 第一条system消息instance_template→ 第二条user消息(包含任务描述)
渲染时的变量来自四个来源的合并:
def get_template_vars(self, **kwargs) -> dict:
return recursive_merge(
self.config.model_dump(), # agent 配置的所有字段
self.env.get_template_vars(), # 环境的配置 + 系统信息(uname)
self.model.get_template_vars(), # 模型的配置
{"n_model_calls": self.n_calls, "model_cost": self.cost},
self.extra_template_vars, # 外部注入的额外变量(如 task)
kwargs,
)
Jinja2 使用 StrictUndefined:模板里引用任何未定义的变量都会直接报错,避免静默地渲染出不完整内容。
3.2 观察模板
模型每次调用 bash 工具后,执行结果通过 observation_template 格式化后再发回模型。mini.yaml 里的默认格式是 JSON 风格的:
{
"returncode": 0,
"output": "<命令输出>"
}
如果输出超过 10000 字符,会自动裁成 output_head(前 5000 字符)+ output_tail(后 5000 字符),并加上 elided_chars 和警告。这个机制不太复杂,但它避免了模型上下文被长命令输出占满。
default.yaml(text-based 模式)则用 XML 标签格式:
<returncode>0</returncode>
<output>命令输出</output>
两种格式只是模板不同,底层逻辑完全一样。
4. 模型层
4.1 统一入口
LitellmModel 是最核心的模型实现。它通过 litellm 统一访问各家 API,关键字段:
model_name:推荐带 provider 前缀,如anthropic/claude-sonnet-4-5-20250929model_kwargs:透传给 API 的额外参数- 只有一个工具定义
BASH_TOOL,其 schema 极其简单:{"command": "string"}
4.2 query 流程
messages → _prepare_messages_for_api(去掉 extra 字段 + 排序 thinking block + 设置 cache control)
→ _query(调用 litellm.completion,工具列表=[BASH_TOOL])
→ 重试循环(指数退避,abort_exceptions 类型不重试)
→ 计算 cost
→ 解析 tool_calls → actions 列表
→ 包装成标准消息返回
每一步都有明确边界。_prepare_messages_for_api 负责把内部消息格式(带 extra 字段)转成 API 需要的格式;_query 只做 API 调用和认证错误提示;cost 计算有独立的错误处理策略(ignore_errors 模式用于本地模型)。
4.3 Action 解析
parse_toolcall_actions 对模型的 tool_call 做严格校验:
- 如果没有 tool_calls →
FormatError(每条回复必须至少有一个工具调用) - 如果工具名不是
bash→FormatError - 如果参数里没有
command→ `FormatError - JSON 解析失败 →
FormatError
每个 FormatError 都携带着用 format_error_template 渲染的错误消息,这条消息被写回消息历史,模型下一轮就能看到"你上一轮的格式不对,正确格式应该是什么"。这是一种自纠正机制——不是直接失败退出,而是给模型重新来过的机会。
4.4 其他模型实现
除了 LitellmModel,还有 OpenRouterModel、PortkeyModel 等变体。它们的核心差异在于 API 入口不同,但都实现了相同的 Model 接口。text-based 模型(LitellmTextbasedModel 等)的差异在于 action 解析用的是正则而非 tool_call:模型输出 <mswea_bash_command> 包裹的命令文本,系统用正则提取。这只为兼容不支持原生 tool-call 的模型。
5. 环境层
5.1 核心语义
环境只做一件事:收到 {"command": "..."} 的 action,执行它,返回输出字典:
{"output": "...", "returncode": 0, "exception_info": ""}
所有环境实现共享同一个 _check_finished 机制:如果命令输出的第一行是 COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT 且 returncode == 0,抛出 Submitted 异常。这个设计让"任务完成"的判断完全在环境侧完成,agent 不需要解析模型的输出内容来判断是否结束。
5.2 LocalEnvironment
最简单的实现。subprocess.run(command, shell=True, ...),stdout 和 stderr 合并,超时可控。环境变量预设了 PAGER=cat 和 MANPAGER=cat(防止模型调用的命令意外进入交互式 pager),以及 PIP_PROGRESS_BAR=off 和 TQDM_DISABLE=1(禁止进度条输出污染上下文)。
5.3 DockerEnvironment
把 subprocess.run 换成 docker exec。启动时创建容器(docker run -d --name <random> <image> sleep 2h),退出的容器在后台清理。命令执行时通过 docker exec -w <cwd> -e KEY=VALUE <container> bash -lc <command> 完成。
与其他 agent 框架的 Docker 方案不同,mini 不维持一个长期运行的 shell session。每条命令是一次独立的 docker exec。这样做的好处是:没有 shell 状态需要管理(cd 不持久、环境变量不累积、后台进程不存在),每条命令的执行环境是可重现的。代价是模型需要在每一条需要特定目录或环境变量的命令前加上 cd /path && ... 或 VAR=value command 的前缀。系统 prompt 里明确告诉了模型这个约束。
5.4 额外沙箱
environments/extra/ 下还有 bubblewrap、contree、swerex_docker、swerex_modal 等更严格的沙箱实现。它们不是本地开发的常用方案,但在 benchmark 批量推理和安全敏感场景下很有价值。
6. Agent 控制流
6.1 DefaultAgent
核心循环不到 20 行:
def run(self, task: str = "", **kwargs) -> dict:
self.messages = []
self.add_messages(
self.model.format_message(role="system", content=...),
self.model.format_message(role="user", content=...),
)
while True:
try:
self.step()
except InterruptAgentFlow as e:
self.add_messages(*e.messages)
except Exception as e:
self.handle_uncaught_exception(e)
raise
finally:
self.save(self.config.output_path)
if self.messages[-1].get("role") == "exit":
break
return self.messages[-1].get("extra", {})
step() 内部只有一行:self.execute_actions(self.query())—先问模型要动作,再执行动作,工具结果自动追加为模型下一轮的上下文。
6.2 InteractiveAgent
继承 DefaultAgent,增加了三种模式:
- yolo:所有命令直接执行,不确认
- confirm:非白名单命令需要用户按回车确认。用户也可以拒绝并附上反馈,反馈会作为模型下一轮上下文
- human:用户直接输入命令,模型不参与
模式通过 /y、/c、/u 斜杠命令实时切换。用户按 Ctrl+C 中断时可以输入反馈消息,该消息写回模型上下文。
6.3 异常驱动的控制流
Agent 循环用异常而不是返回值来控制状态变化。所有控制异常都继承自 InterruptAgentFlow,携带要追加到消息列表的消息:
class InterruptAgentFlow(Exception):
def __init__(self, *messages: dict):
self.messages = messages
class Submitted(InterruptAgentFlow): ... # 环境检测到完成任务
class LimitsExceeded(InterruptAgentFlow): ... # 超出步数/成本限制
class UserInterruption(InterruptAgentFlow): ... # 用户中断
class FormatError(InterruptAgentFlow): ... # 模型输出格式不对
每个异常携带的 messages 会被追加到历史中(self.add_messages(*e.messages)),然后循环继续或终止的逻辑由消息的 role 决定。当 role == "exit" 时循环结束。Submitted 携带 exit role 的消息,所以会正常退出;FormatError 携带 user role 的格式化错误提示,模型下一轮会看到并尝试纠正。
这种设计避免了所有循环控制逻辑中散布大量状态判断。异常直接携带"发生了什么事以及它对应的消息",Agent 只负责把它追加上去。
7. 序列化与轨迹
7.1 轨迹格式
每次 step() 后,finally 块都会保存轨迹。轨迹是一个 JSON 文件,包含:
{
"info": {
"model_stats": {"instance_cost": ..., "api_calls": ...},
"config": {"agent": {...}, "agent_type": "...", "model": {...}, "environment": {...}},
"mini_version": "2.2.8",
"exit_status": "Submitted",
"submission": "..."
},
"messages": [...],
"trajectory_format": "mini-swe-agent-1.1"
}
关键在于 messages 本身既是轨迹记录又是模型的上下文。不需要任何"从轨迹还原上下文"的翻译层。
7.2 recursive_merge 的序列化用途
serialize 方法的实现很简单:
def serialize(self, *extra_dicts) -> dict:
return recursive_merge(agent_data, self.model.serialize(), self.env.serialize(), *extra_dicts)
三个组件的 serialize() 都返回标准化的字典片段,通过 recursive_merge 拼到一起。这样新增组件只要实现 serialize() 方法,轨迹文件的顶层结构不用改动。
8. Benchmark 运行
8.1 SWE-bench 集成
run/benchmarks/swebench.py 提供了完整的批量推理脚本。它会:
- 从 datasets 库加载 SWE-bench 实例
- 为每个实例创建对应的 Docker 环境
- 运行 agent
- 收集轨迹和预测文件
- 批量导出进度条和统计
核心原理和 mini.py 一样,只是把单次交互循环变成了批量 pipeline。
8.2 Config Profile
config/benchmarks/ 下有多个 YAML 配置变体:
swebench.yaml:标准配置,使用 toolcall 模式swebench_xml.yaml:使用 XML 标签的 text-based 模式(兼容旧版模型)swebench_modal.yaml:使用 Modal 云计算平台运行swebench_backticks.yaml:使用反引号格式
每个 YAML 是 mini.yaml 的变体,只覆写差异字段。配置系统不需要"继承"或"include"机制,靠 recursive_merge 的多文件叠加就能实现。
代码入口速查
| 主题 | 文件 | 入口 |
|---|---|---|
| Agent 主循环 | agents/default.py |
DefaultAgent.run, DefaultAgent.step |
| 人机交互 | agents/interactive.py |
InteractiveAgent.query, InteractiveAgent.execute_actions |
| Model 查询 | models/litellm_model.py |
LitellmModel.query |
| Tool call 解析 | models/utils/actions_toolcall.py |
parse_toolcall_actions, format_toolcall_observation_messages |
| Text action 解析 | models/utils/actions_text.py |
parse_regex_actions, format_observation_messages |
| 本地环境 | environments/local.py |
LocalEnvironment.execute |
| Docker 环境 | environments/docker.py |
DockerEnvironment.execute |
| CLI 入口 | run/mini.py |
main |
| Hello World | run/hello_world.py |
main |
| 配置加载 | config/__init__.py |
get_config_from_spec |
| 配置合并 | utils/serialize.py |
recursive_merge |
| 异常定义 | exceptions.py |
InterruptAgentFlow, Submitted, FormatError 等 |
| 组件 Protocol | __init__.py |
Model, Environment, Agent |
| YAML 配置 | config/mini.yaml |
agent 模板 + 模型配置 + 环境配置 |

浙公网安备 33010602011771号