本系列基于 mini-SWE-agent 源码学习整理;源码版本固定为 Asuhe404/mini-swe-agent@388da74,便于长期核对实现。

系列文章

  1. 01. Python 最小语法桥:从 JavaScript 读懂 mini-SWE-agent
  2. 02. 最小装配与入口:从命令找到 Agent
  3. 03. Protocol 与多态:用同一套插座更换三类组件
  4. 04. Agent 核心循环:让模型回答变成下一轮上下文
  5. 05. Environment 与退出协议:把 action 变成真实副作用
  6. 06. Model 适配与 action 翻译:把模型方言变成统一命令
  7. 07. 配置、工厂与交互子类:从 CLI 参数装配出可控 Agent
  8. 08. 测试驱动扩展:用可预测替身保护 Agent 三层契约

适合:零 Python 基础、已有 JavaScript 基础。
节奏:24 个学习单元,每个 60-90 分钟;不必按自然日完成。
原则:先跑通一条最小链路,再沿调用关系向外扩张。不要按目录逐文件硬读。

先给结论

这个仓库的核心不是“神秘的 AI 算法”,而是一个很小的反馈循环:

def step(self) -> list[dict]:
    return self.execute_actions(self.query())

它等价于下面的 JavaScript 心智模型:

async function step() {
  const modelMessage = await queryModel(messages);
  const outputs = modelMessage.actions.map(executeInEnvironment);
  messages.push(...formatOutputsForModel(outputs));
}

真正建议先读的五个文件合计只有 577 行:

  1. run/hello_world.py(42 行):怎样装配三大组件。
  2. __init__.py(92 行):三大组件必须满足什么接口。
  3. agents/default.py(188 行):循环如何运转。
  4. environments/local.py(92 行):命令如何执行与结束。
  5. models/litellm_model.py(163 行):模型回答如何变成命令。

吃透这 577 行后,仓库其余代码大多只是 Model、Environment、Agent 或 Run Script 的不同实现。

1. 用 5W2H 建立全局心智模型

What:它是什么?

mini-SWE-agent 是一个“让大模型反复观察代码仓库、选择 shell 命令、读取命令结果,直到完成任务”的软件工程 Agent。它不训练模型;它负责协调模型 API、命令执行环境和循环状态。

Why:为什么拆成三个对象?

  • Model 只关心“怎样问模型、怎样解析回答”。
  • Environment 只关心“在哪里、怎样执行 action”。
  • Agent 只关心“什么时候问、什么时候执行、什么时候停止”。

因此可以替换任一部分:LiteLLM 换 OpenRouter、Local 换 Docker、DefaultAgent 换 InteractiveAgent,主循环不需要重写。这和前端中把状态机、API client、运行平台分别注入类似。

Who:谁参与一次运行?

角色真实实现JS/TS 类比
组合根run/hello_world.pyrun/mini.pymain.ts / 应用 bootstrap
接口ModelEnvironmentAgent ProtocolTypeScript interface
调度器DefaultAgent状态机 / controller
模型适配器LitellmModelAPI client + response adapter
执行适配器LocalEnvironmentNode child_process wrapper
配置模型Pydantic BaseModelZod schema + typed config
提示词模板YAML + JinjaYAML + Nunjucks/Handlebars
控制信号SubmittedFormatError 等异常带 payload 的 throw

When:什么时候发生什么?

  1. 启动时读取 .env 和 YAML。
  2. Run Script 构造 Model、Environment、Agent。
  3. Agent.run(task) 生成 system/user 初始消息。
  4. 每一轮先 Model.query(messages),再执行其中的 actions。
  5. 执行结果被格式化为 observation,加入历史,下一轮重新发给模型。
  6. 完成、格式错误、超限、用户中断都通过携带消息的异常改变流程。
  7. 最后一条消息是 role="exit" 时循环结束,返回其 extra

Where:核心逻辑在哪里?

How:一次运行到底怎样流动?

流程图 1流程图 1

精确调用链:

run/mini.py:55 main
  -> config/__init__.py:56 get_config_from_spec
  -> utils/serialize.py:6 recursive_merge
  -> models/__init__.py:45 get_model
  -> environments/__init__.py:30 get_environment
  -> agents/__init__.py:25 get_agent
  -> agents/default.py:88 DefaultAgent.run
       -> model.format_message             # 初始 system/user
       -> agents/default.py:124 step
            -> agents/default.py:128 query
                 -> model.query
                 -> model 解析 tool call 为 action
            -> agents/default.py:152 execute_actions
                 -> environment.execute
                 -> model.format_observation_messages
                 -> add_messages
       -> save trajectory
       -> 下一轮或返回

注意动态分派:默认 mini 实际构造的是 InteractiveAgent,但它继承 DefaultAgent.run()run() 里的 self.step()self.query()self.execute_actions() 会调用子类重写的方法,和 JS class 的多态一致。

How much:运行状态有多少?

核心状态并不多:

  • messages:完整对话/轨迹,是循环的主状态。
  • n_callscost:模型调用次数和实例费用。
  • n_consecutive_format_errors:连续格式错误计数。
  • _start_time:墙钟超时基准。
  • config:提示词、预算、输出路径等。
  • modelenv:两个被注入的策略对象。

2. Python 生存语法:只学仓库马上会用到的

不要先通读一本 Python 书。先掌握下面这组映射,再边读边补。

PythonJavaScript / TypeScript 心智模型仓库例子
Nonenulloutput_path: Path | None
dict / listobject / arraymessages: list[dict]
deffunction / methoddef query(...)
selfthisself.messages
__init__constructorDefaultAgent.__init__
ProtocolTS interfaceModel(Protocol)
BaseModelZod schema + config objectAgentConfig(BaseModel)
A | NoneTS A | nullPython 3.10 union type
*messagesrest args ...messagesadd_messages(*items)
**kwargsnamed options + object spreadClass(**config)
{**a, **b}{...a, ...b}CLI config overlay
a | b{...a, ...b}dict merge
a |= bObject.assign(a, b)task vars merge
[f(x) for x in xs]xs.map(f)action execution
any(...).some(...)whitelist 判断
with ...:自动清理的作用域Rich status / retry attempt
raise / exceptthrow / catchSubmitted 控制流
super()superInteractiveAgent 扩展父类
@app.command()注册函数的 decoratorTyper CLI 命令
match/caseswitch + 模式匹配交互模式命令
:=条件内赋值run_task := ...
_name私有约定,不是真私有_render_template
importlib.import_module动态 import()class factory

第一阶段可以暂时不学:asyncio、descriptor、metaclass、复杂 decorator、生成器协议、Python packaging 细节。

3. 24 个学习单元

阶段 0:Python 最小语法桥(单元 1-3)

目标:能读,不追求先会写完整 Python 项目。

读:

  1. run/hello_world.py
  2. exceptions.py
  3. utils/serialize.py

练:

  • hello_world.main() 手写成等价 JS 伪代码。
  • 在 Python REPL 中练 dict/list、class、*args/**kwargs、list comprehension。
  • 不查资料,口述 recursive_merge 的输入优先级。

验收:看到 DefaultAgent(model, env, **config),能解释每个实参从哪里来。

阶段 1:看懂最小装配(单元 4-5)

目标:回答“Agent 是怎样被造出来的”。

读:

  1. run/hello_world.py:20
  2. config/default.yaml
  3. pyproject.toml:89
  4. __main__.py

练:画出 mini 命令到 agent.run(task) 的五个节点。先忽略 API 调用细节。

验收:能说明 Run Script 为什么是项目的组合根,以及 YAML 中 agent/model/environment 三段分别交给谁。

阶段 2:协议与鸭子类型(单元 6-7)

目标:理解这个项目所说的 polymorphism。

读:

  1. __init__.py:39
  2. models/test_models.py:16143
  3. agents/__init__.py
  4. models/__init__.py:45
  5. environments/__init__.py

重点:Python Protocol 是结构化接口;具体类不必 extends Model,只要方法形状匹配。这非常接近 TypeScript 的 structural typing。

练:列出 Model 与 Environment 的“显式协议”和真正隐藏在裸 dict 中的“隐式数据协议”。

验收:能解释为什么 DeterministicModel 可以替换 LitellmModel,且不需要发 API 请求。

阶段 3:吃透 Agent 主循环(单元 8-11)

目标:这是全仓最重要的阶段。

按方法而不是按文件顺序读 agents/default.py

  1. __init__:38-50,认出所有状态。
  2. run:88-122,先只看正常路径。
  3. step:124-126,记住全仓核心句。
  4. query:128-150,理解限额、调用、计费、消息追加。
  5. execute_actions:152-155,理解 action -> output -> observation。
  6. 回到 run:100-119,再看异常路径和每轮保存。
  7. serialize/save:157-188,理解 trajectory。

配套测试:

练:在纸上模拟两轮后 messages 的顺序:system -> user -> assistant -> tool -> assistant -> exit

验收:关闭代码后,能自己写出十几行的伪 Agent 循环。

阶段 4:Environment 与退出协议(单元 12-14)

目标:理解“模型说做什么”如何变成真实副作用。

读:

  1. environments/local.py:13:配置。
  2. environments/local.py:24:标准输出结构。
  3. environments/local.py:45:完成标记。
  4. environments/local.py:72:进程组与 timeout。
  5. exceptions.py:为什么 Submitted 不是错误。

安全边界:LocalEnvironment 使用 shell=True 执行模型产生的任意 shell 命令。默认 CLI 用 InteractiveAgent(mode="confirm") 降低风险;学习时不要开 --yolo 跑不可信任务,优先用临时目录或 Docker。

练:直接调用 LocalEnvironment.execute({"command": "printf hello"}),观察标准化输出;再调用完成标记并观察 Submitted.messages

验收:能解释为什么完成标记必须是 stdout 第一条有效行且 return code 为 0。

阶段 5:Model 适配层(单元 15-17)

目标:理解仓库并不“实现 LLM”,它实现的是不同 API 的适配器。

先读:

  1. models/test_models.py:104:最简单、无需 API 的 Model。
  2. models/utils/actions_toolcall.py:11:bash tool schema。
  3. models/utils/actions_toolcall.py:30:tool call 校验。
  4. models/utils/actions_toolcall.py:79:output 变 tool result。
  5. models/litellm_model.py:64:真正 API 请求。
  6. models/litellm_model.py:81:重试、计费、解析、标准化。

再比较:

第一遍先跳过 Portkey、OpenRouter、Requesty、Responses API 变体;它们是相同角色的不同适配器。

验收:能区分三层数据:厂商原始 response、项目统一的 message/action、环境统一 output。

阶段 6:配置、工厂与交互子类(单元 18-20)

目标:看懂真实 mini CLI 为什么比 hello world 多很多代码。

顺序:

  1. config/mini.yaml:默认 prompt 与 observation 模板。
  2. config/__init__.py:YAML/点号配置解析。
  3. utils/serialize.py:后配置覆盖前配置,UNSET 表示“不覆盖”。
  4. run/mini.py:55:Typer 参数。
  5. run/mini.py:70:配置合并。
  6. run/mini.py:99:三个工厂和 run
  7. agents/interactive.py:只看它重写了哪些父类方法。

练:分别追踪 --model--yolo--cost-limit 从 CLI 参数到最终 Pydantic config 的路径。

验收:能解释 mini -c mini.yaml -c agent.cost_limit=1 为什么后者覆盖前者,以及为什么默认 CLI 实际是 InteractiveAgent

阶段 7:测试驱动地做一个扩展(单元 21-24)

目标:从“看懂”升级到“能按仓库哲学扩展”。

先读这四组测试:

  1. tests/agents/test_default.py
  2. tests/environments/test_local.py
  3. tests/models/test_test_models.py
  4. tests/config/test_init.py

毕业项目:实现一个 RecordingEnvironment,不执行 shell,只记录 actions 并返回标准 output;再写一个最小 Run Script,将 DeterministicModel + RecordingEnvironment + DefaultAgent 装配起来。不要先改主循环。

毕业验收:

  • 能自己定义一个满足 Protocol 的替代实现。
  • 能用 deterministic model 覆盖成功、格式错误、超限三个路径。
  • 能解释 trajectory 中每类 message 的生产者。
  • 能指出新用例应该从 Run Script 开始,而不是在 DefaultAgent 塞条件分支。

4. 无 API 的第一条完整实验

第一周只读核心循环时,使用这个已验证的最小环境;它刻意不安装 LiteLLM 和 provider SDK。uv 只是项目使用的 Python 环境/依赖工具,不需要先深入理解:

uv venv --python 3.13
uv pip install pyyaml python-dotenv platformdirs rich pydantic jinja2 pytest pytest-asyncio

进入真实 Model、CLI 和完整测试前,再安装项目的全部开发依赖:

uv sync --python 3.13 --extra dev

然后在仓库根目录运行下面的实验。它使用 DeterministicModel,不会调用真实模型 API;但会通过 LocalEnvironment 执行两个安全的 printf 命令。

MSWEA_SILENT_STARTUP=1 PYTHONPATH=src .venv/bin/python - <<'PY'
from pathlib import Path

import yaml

from minisweagent.agents.default import DefaultAgent
from minisweagent.environments.local import LocalEnvironment
from minisweagent.models.test_models import DeterministicModel, make_output

config = yaml.safe_load(Path("src/minisweagent/config/default.yaml").read_text())["agent"]
model = DeterministicModel(
    outputs=[
        make_output("先观察", [{"command": "printf 'hello from environment\\n'"}]),
        make_output(
            "完成任务",
            [{"command": "printf 'COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT\\nlearned the loop\\n'"}],
        ),
    ]
)
agent = DefaultAgent(model, LocalEnvironment(), **config)

print(agent.run("理解两轮 Agent 循环"))
print([message.get("role") for message in agent.messages])
PY

预期核心输出:

{'exit_status': 'Submitted', 'submission': 'learned the loop\n'}
['system', 'user', 'assistant', 'user', 'assistant', 'exit']

接着运行三组无外部服务测试:

MSWEA_SILENT_STARTUP=1 PYTHONPATH=src .venv/bin/pytest \
  tests/agents/test_default.py \
  tests/environments/test_local.py \
  tests/utils/test_serialize.py -q

5. 核心符号使用地图

这里列生产代码中的直接调用点;测试目录用于验证相同契约。

符号定义生产使用位置
DefaultAgentagents/default.py:38run/hello_world.py:32 直接构造;agents/interactive.py:33run/benchmarks/utils/common.py:7 继承;agents/__init__.py:9 工厂映射
Agent.runagents/default.py:88run/mini.py:102run/hello_world.py:37run/benchmarks/swebench_single.py:96swebench.py:154programbench.py:101
Agent.stepagents/default.py:124default.py:98 循环调用;interactive.py:109benchmarks/utils/common.py:15 重写/扩展
Model.queryProtocol __init__.py:48agents/default.py:147 是主调用点;models/extra/roulette.py:31 做模型转发
Environment.executeProtocol __init__.py:66agents/default.py:154agents/interactive.py:132;benchmark 另用于启动/准备环境
get_modelmodels/__init__.py:45run/mini.py:99、三个 benchmark 入口、models/extra/roulette.py:19
get_environmentenvironments/__init__.py:30run/mini.py:100、benchmark 入口
get_agentagents/__init__.py:25run/mini.py:101run/benchmarks/swebench_single.py:90
recursive_mergeutils/serialize.py:6CLI/benchmark 配置、Agent 模板变量与序列化、各 Environment 模板变量
Submittedexceptions.py:9Local/Docker/Singularity/Bubblewrap/SWE-ReX/ConTree 环境检测完成;Agent 捕获
FormatErrorexceptions.py:25text/tool-call/Responses action parser 抛出;Agent 转成纠错轮次

复查完整调用点的命令:

rg -n --glob '*.py' 'DefaultAgent|\.run\(|\.query\(|\.execute\(|recursive_merge\(' src tests

6. 第一遍明确跳过什么

在能独立讲清 run -> step -> query -> execute_actions 前,先跳过:

  • run/benchmarks/:批处理、数据集、并发和恢复逻辑会遮住核心循环。
  • environments/docker.pysingularity.pyextra/:先把 Local 看懂。
  • Portkey、OpenRouter、Requesty、Responses API 模型:先把一个 LiteLLM tool-call 路径看懂。
  • run/utilities/inspector.py:这是轨迹查看 UI,不负责 Agent 推理。
  • multimodal、cache control、provider-specific retry:属于适配细节。
  • 全量测试:部分需要 Docker、Singularity、网络或 provider 凭据。
  • tests/test_fire.py:它会调用真实模型 API 并产生费用,不属于无 API 学习测试。

7. 读懂后再审视:可维护性、性能与风险

这些不是入门时要马上重构的任务,而是检验你是否真正理解边界的思考题。

高价值问题

  1. 全局调用次数限制有 off-by-one
    GlobalModelStats.add() 先把 _n_calls 加 1,再判断 call_limit < _n_calls + 1。限制为 N 时,第 N 次调用完成后就抛异常,只有前 N-1 次正常返回;现有测试只覆盖启动打印,没覆盖限制行为。修复还应考虑检查发生在 API 请求之后、并发一致性和既有用户是否依赖当前语义。

  2. 单 Agent 的成本可能漏记格式错误轮次
    LitellmModel.query() 在解析 action 前已计算费用和更新全局统计;若解析抛 FormatErrorDefaultAgent.query() 收不到 message,实例 self.cost 不会增加。实际消费和实例 cost_limit 可能不一致。修复会涉及 Model/Agent 协议及所有实现,必须补跨实现测试。

  3. 复用同一个 Agent 的状态语义不清晰
    run() 清空 messages,但不重置 costn_calls_start_time 和旧的 extra_template_vars。这可能是累计预算,也可能让第二次独立运行意外继承第一次状态。修复前要先定义“一个 Agent 实例能否多次独立 run”的契约。

  4. 轨迹保存的累计 I/O 接近 O(n²)
    设置 output_path 后,每轮都把不断增长的全部消息重新 json.dumps 并覆盖文件。长任务中总写入量近似二次增长,而且写入不是原子的。可考虑原子临时文件、降低 checkpoint 频率或追加式事件日志;风险是 inspector、恢复逻辑和 trajectory 格式兼容性。

  5. dict 隐藏关键跨模块契约
    message.extra.actionsaction.commandoutput.returncode 等只靠约定。自定义实现容易在远处触发 KeyErrorTypedDict 或小型 Pydantic schema 能提高可维护性,但三种模型消息格式不同,过度统一会损害项目的“最小、可替换”目标。

  6. 本地环境是明确的安全边界
    shell=True 是产品能力,不是偶然细节。--yolo 会让模型不经确认执行命令。若做面向不可信输入的产品,应优先更换 Environment(Docker/Bubblewrap)而不是在 prompt 中承诺安全。

  7. 配置可能把密钥写进 trajectory
    Model 和 Environment 的 serialize() 会保存完整 config;项目又允许把 API key 放进 model_kwargs,环境 config 也可能含密码或自定义变量。保存轨迹前应设计统一脱敏;风险是改变调试与复现实验所需的信息。

  8. 部分 extra Model 没满足 Model Protocol
    RouletteModel/InterleavingModel 没有 format_message()format_observation_messages(),直接交给 DefaultAgent 会在启动或 observation 阶段失败。若补代理方法,还必须记录本轮选中的子模型,保证 query 与 observation 使用同一消息协议。

  9. 动态 import 可能掩盖插件内部错误
    工厂把 ImportError 统一转为 Unknown ... type;如果目标模块存在但其内部依赖导入失败,诊断信息会变得不准确。缩小捕获范围能改善可诊断性,但可能改变公开错误信息。

次要维护点

  • 多个 Environment 重复实现完成标记检测,可考虑共享 helper;要先核对各后端 output 结构差异。
  • InteractiveAgent.cost_last_confirmed_add_observation_messages 当前没有生产调用点,可能是残留设计。
  • 交互帮助与模式切换用递归重新提问;极端重复输入存在递归深度风险,循环更稳健。
  • LocalEnvironment.get_template_vars() 把整个 os.environ 暴露给 Jinja 上下文;模板若引用敏感变量会把它发给模型。
  • 当前文档提到 MSWEA_DEFAULT_RUN 可以覆盖入口,但 __main__.py 仍直接导入 run.mini:app;学习时以代码行为为准。

性能判断

  • 正常瓶颈几乎总是模型网络延迟和命令执行,不是 Python 循环本身。
  • action 顺序执行,语义稳定且简单;盲目并行会改变共享工作目录和命令副作用顺序。
  • prompt/output 长度控制比微优化 list/dict 更重要;mini.yaml 已对过长 observation 做头尾截断。

8. 最终“吃透”检查表

当下面十项都能脱离源码讲清楚,你已经掌握仓库主干:

  • 能用一句话说清 Model、Environment、Agent 各自职责。
  • 能从 mini 入口追到 DefaultAgent.run()
  • 能写出 run -> step -> query -> execute_actions 调用链。
  • 能画出两轮运行后的 messages 顺序。
  • 能解释 action、output、observation 三种对象。
  • 能解释 Submitted 为什么是正常控制流。
  • 能解释 FormatError 如何变成下一轮纠错消息。
  • 能追踪 CLI/YAML/环境变量怎样变成三类 config。
  • 能用 DeterministicModel 写一个不花钱的端到端测试。
  • 能新增一个 Model 或 Environment,而不修改 DefaultAgent 主循环。

完成主干后,再进入 benchmark:swebench_single.py 看单任务装配,swebench.py 看批量运行与恢复,programbench.py 看另一种任务/环境。此时它们会是已知组件的新组合,而不是新的谜题。