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 内置的 LitellmModelLocalEnvironmentDefaultAgent 三者之间只通过这三个接口耦合,不依赖彼此的具体实现。

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-20250929
  • model_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(每条回复必须至少有一个工具调用)
  • 如果工具名不是 bashFormatError
  • 如果参数里没有 command → `FormatError
  • JSON 解析失败 → FormatError

每个 FormatError 都携带着用 format_error_template 渲染的错误消息,这条消息被写回消息历史,模型下一轮就能看到"你上一轮的格式不对,正确格式应该是什么"。这是一种自纠正机制——不是直接失败退出,而是给模型重新来过的机会。

4.4 其他模型实现

除了 LitellmModel,还有 OpenRouterModelPortkeyModel 等变体。它们的核心差异在于 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_OUTPUTreturncode == 0,抛出 Submitted 异常。这个设计让"任务完成"的判断完全在环境侧完成,agent 不需要解析模型的输出内容来判断是否结束。

5.2 LocalEnvironment

最简单的实现。subprocess.run(command, shell=True, ...),stdout 和 stderr 合并,超时可控。环境变量预设了 PAGER=catMANPAGER=cat(防止模型调用的命令意外进入交互式 pager),以及 PIP_PROGRESS_BAR=offTQDM_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 提供了完整的批量推理脚本。它会:

  1. 从 datasets 库加载 SWE-bench 实例
  2. 为每个实例创建对应的 Docker 环境
  3. 运行 agent
  4. 收集轨迹和预测文件
  5. 批量导出进度条和统计

核心原理和 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 模板 + 模型配置 + 环境配置
posted @ 2026-05-03 19:40  湾仔码农  阅读(8)  评论(0)    收藏  举报