本系列基于 mini-SWE-agent 源码学习整理;源码版本固定为 Asuhe404/mini-swe-agent@388da74,便于长期核对实现。
系列文章
- 01. Python 最小语法桥:从 JavaScript 读懂 mini-SWE-agent
- 02. 最小装配与入口:从命令找到 Agent
- 03. Protocol 与多态:用同一套插座更换三类组件
- 04. Agent 核心循环:让模型回答变成下一轮上下文
- 05. Environment 与退出协议:把 action 变成真实副作用
- 06. Model 适配与 action 翻译:把模型方言变成统一命令
- 07. 配置、工厂与交互子类:从 CLI 参数装配出可控 Agent
- 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 行:
run/hello_world.py(42 行):怎样装配三大组件。__init__.py(92 行):三大组件必须满足什么接口。agents/default.py(188 行):循环如何运转。environments/local.py(92 行):命令如何执行与结束。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.py、run/mini.py | main.ts / 应用 bootstrap |
| 接口 | Model、Environment、Agent Protocol | TypeScript interface |
| 调度器 | DefaultAgent | 状态机 / controller |
| 模型适配器 | LitellmModel | API client + response adapter |
| 执行适配器 | LocalEnvironment | Node child_process wrapper |
| 配置模型 | Pydantic BaseModel | Zod schema + typed config |
| 提示词模板 | YAML + Jinja | YAML + Nunjucks/Handlebars |
| 控制信号 | Submitted、FormatError 等异常 | 带 payload 的 throw |
When:什么时候发生什么?
- 启动时读取
.env和 YAML。 - Run Script 构造 Model、Environment、Agent。
Agent.run(task)生成 system/user 初始消息。- 每一轮先
Model.query(messages),再执行其中的 actions。 - 执行结果被格式化为 observation,加入历史,下一轮重新发给模型。
- 完成、格式错误、超限、用户中断都通过携带消息的异常改变流程。
- 最后一条消息是
role="exit"时循环结束,返回其extra。
Where:核心逻辑在哪里?
- 协议:
src/minisweagent/__init__.py:43 - 主循环:
src/minisweagent/agents/default.py:88 - 单步:
src/minisweagent/agents/default.py:124 - 模型调用:
src/minisweagent/agents/default.py:128 - action 执行:
src/minisweagent/agents/default.py:152 - 本机 shell:
src/minisweagent/environments/local.py:24 - 模型 API 适配:
src/minisweagent/models/litellm_model.py:81 - action 解析:
src/minisweagent/models/utils/actions_toolcall.py:30 - CLI 组合:
src/minisweagent/run/mini.py:68
How:一次运行到底怎样流动?
精确调用链:
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_calls、cost:模型调用次数和实例费用。n_consecutive_format_errors:连续格式错误计数。_start_time:墙钟超时基准。config:提示词、预算、输出路径等。model、env:两个被注入的策略对象。
2. Python 生存语法:只学仓库马上会用到的
不要先通读一本 Python 书。先掌握下面这组映射,再边读边补。
| Python | JavaScript / TypeScript 心智模型 | 仓库例子 |
|---|---|---|
None | null | output_path: Path | None |
dict / list | object / array | messages: list[dict] |
def | function / method | def query(...) |
self | this | self.messages |
__init__ | constructor | DefaultAgent.__init__ |
Protocol | TS interface | Model(Protocol) |
BaseModel | Zod schema + config object | AgentConfig(BaseModel) |
A | None | TS A | null | Python 3.10 union type |
*messages | rest args ...messages | add_messages(*items) |
**kwargs | named options + object spread | Class(**config) |
{**a, **b} | {...a, ...b} | CLI config overlay |
a | b | {...a, ...b} | dict merge |
a |= b | Object.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 / except | throw / catch | Submitted 控制流 |
super() | super | InteractiveAgent 扩展父类 |
@app.command() | 注册函数的 decorator | Typer CLI 命令 |
match/case | switch + 模式匹配 | 交互模式命令 |
:= | 条件内赋值 | run_task := ... |
_name | 私有约定,不是真私有 | _render_template |
importlib.import_module | 动态 import() | class factory |
第一阶段可以暂时不学:asyncio、descriptor、metaclass、复杂 decorator、生成器协议、Python packaging 细节。
3. 24 个学习单元
阶段 0:Python 最小语法桥(单元 1-3)
目标:能读,不追求先会写完整 Python 项目。
读:
练:
- 把
hello_world.main()手写成等价 JS 伪代码。 - 在 Python REPL 中练
dict/list、class、*args/**kwargs、list comprehension。 - 不查资料,口述
recursive_merge的输入优先级。
验收:看到 DefaultAgent(model, env, **config),能解释每个实参从哪里来。
阶段 1:看懂最小装配(单元 4-5)
目标:回答“Agent 是怎样被造出来的”。
读:
练:画出 mini 命令到 agent.run(task) 的五个节点。先忽略 API 调用细节。
验收:能说明 Run Script 为什么是项目的组合根,以及 YAML 中 agent/model/environment 三段分别交给谁。
阶段 2:协议与鸭子类型(单元 6-7)
目标:理解这个项目所说的 polymorphism。
读:
__init__.py:39models/test_models.py:16到143agents/__init__.pymodels/__init__.py:45environments/__init__.py
重点:Python Protocol 是结构化接口;具体类不必 extends Model,只要方法形状匹配。这非常接近 TypeScript 的 structural typing。
练:列出 Model 与 Environment 的“显式协议”和真正隐藏在裸 dict 中的“隐式数据协议”。
验收:能解释为什么 DeterministicModel 可以替换 LitellmModel,且不需要发 API 请求。
阶段 3:吃透 Agent 主循环(单元 8-11)
目标:这是全仓最重要的阶段。
按方法而不是按文件顺序读 agents/default.py:
__init__:38-50,认出所有状态。run:88-122,先只看正常路径。step:124-126,记住全仓核心句。query:128-150,理解限额、调用、计费、消息追加。execute_actions:152-155,理解 action -> output -> observation。- 回到
run:100-119,再看异常路径和每轮保存。 serialize/save:157-188,理解 trajectory。
配套测试:
tests/agents/test_default.py:130:成功结束。tests/agents/test_default.py:153:步数限制。tests/agents/test_default.py:333:消息历史。tests/agents/test_default.py:364:单步增加哪些消息。tests/agents/test_default.py:480:连续格式错误。
练:在纸上模拟两轮后 messages 的顺序:system -> user -> assistant -> tool -> assistant -> exit。
验收:关闭代码后,能自己写出十几行的伪 Agent 循环。
阶段 4:Environment 与退出协议(单元 12-14)
目标:理解“模型说做什么”如何变成真实副作用。
读:
environments/local.py:13:配置。environments/local.py:24:标准输出结构。environments/local.py:45:完成标记。environments/local.py:72:进程组与 timeout。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 的适配器。
先读:
models/test_models.py:104:最简单、无需 API 的 Model。models/utils/actions_toolcall.py:11:bash tool schema。models/utils/actions_toolcall.py:30:tool call 校验。models/utils/actions_toolcall.py:79:output 变 tool result。models/litellm_model.py:64:真正 API 请求。models/litellm_model.py:81:重试、计费、解析、标准化。
再比较:
litellm_textbased_model.py:模型用代码块表达 action。models/utils/actions_text.py:正则提取旧式 action。
第一遍先跳过 Portkey、OpenRouter、Requesty、Responses API 变体;它们是相同角色的不同适配器。
验收:能区分三层数据:厂商原始 response、项目统一的 message/action、环境统一 output。
阶段 6:配置、工厂与交互子类(单元 18-20)
目标:看懂真实 mini CLI 为什么比 hello world 多很多代码。
顺序:
config/mini.yaml:默认 prompt 与 observation 模板。config/__init__.py:YAML/点号配置解析。utils/serialize.py:后配置覆盖前配置,UNSET表示“不覆盖”。run/mini.py:55:Typer 参数。run/mini.py:70:配置合并。run/mini.py:99:三个工厂和run。agents/interactive.py:只看它重写了哪些父类方法。
练:分别追踪 --model、--yolo、--cost-limit 从 CLI 参数到最终 Pydantic config 的路径。
验收:能解释 mini -c mini.yaml -c agent.cost_limit=1 为什么后者覆盖前者,以及为什么默认 CLI 实际是 InteractiveAgent。
阶段 7:测试驱动地做一个扩展(单元 21-24)
目标:从“看懂”升级到“能按仓库哲学扩展”。
先读这四组测试:
tests/agents/test_default.pytests/environments/test_local.pytests/models/test_test_models.pytests/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. 核心符号使用地图
这里列生产代码中的直接调用点;测试目录用于验证相同契约。
| 符号 | 定义 | 生产使用位置 |
|---|---|---|
DefaultAgent | agents/default.py:38 | run/hello_world.py:32 直接构造;agents/interactive.py:33、run/benchmarks/utils/common.py:7 继承;agents/__init__.py:9 工厂映射 |
Agent.run | agents/default.py:88 | run/mini.py:102、run/hello_world.py:37、run/benchmarks/swebench_single.py:96、swebench.py:154、programbench.py:101 |
Agent.step | agents/default.py:124 | default.py:98 循环调用;interactive.py:109、benchmarks/utils/common.py:15 重写/扩展 |
Model.query | Protocol __init__.py:48 | agents/default.py:147 是主调用点;models/extra/roulette.py:31 做模型转发 |
Environment.execute | Protocol __init__.py:66 | agents/default.py:154、agents/interactive.py:132;benchmark 另用于启动/准备环境 |
get_model | models/__init__.py:45 | run/mini.py:99、三个 benchmark 入口、models/extra/roulette.py:19 |
get_environment | environments/__init__.py:30 | run/mini.py:100、benchmark 入口 |
get_agent | agents/__init__.py:25 | run/mini.py:101、run/benchmarks/swebench_single.py:90 |
recursive_merge | utils/serialize.py:6 | CLI/benchmark 配置、Agent 模板变量与序列化、各 Environment 模板变量 |
Submitted | exceptions.py:9 | Local/Docker/Singularity/Bubblewrap/SWE-ReX/ConTree 环境检测完成;Agent 捕获 |
FormatError | exceptions.py:25 | text/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.py、singularity.py、extra/:先把 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. 读懂后再审视:可维护性、性能与风险
这些不是入门时要马上重构的任务,而是检验你是否真正理解边界的思考题。
高价值问题
-
全局调用次数限制有 off-by-one
GlobalModelStats.add()先把_n_calls加 1,再判断call_limit < _n_calls + 1。限制为 N 时,第 N 次调用完成后就抛异常,只有前 N-1 次正常返回;现有测试只覆盖启动打印,没覆盖限制行为。修复还应考虑检查发生在 API 请求之后、并发一致性和既有用户是否依赖当前语义。 -
单 Agent 的成本可能漏记格式错误轮次
LitellmModel.query()在解析 action 前已计算费用和更新全局统计;若解析抛FormatError,DefaultAgent.query()收不到 message,实例self.cost不会增加。实际消费和实例cost_limit可能不一致。修复会涉及 Model/Agent 协议及所有实现,必须补跨实现测试。 -
复用同一个 Agent 的状态语义不清晰
run()清空messages,但不重置cost、n_calls、_start_time和旧的extra_template_vars。这可能是累计预算,也可能让第二次独立运行意外继承第一次状态。修复前要先定义“一个 Agent 实例能否多次独立 run”的契约。 -
轨迹保存的累计 I/O 接近 O(n²)
设置output_path后,每轮都把不断增长的全部消息重新json.dumps并覆盖文件。长任务中总写入量近似二次增长,而且写入不是原子的。可考虑原子临时文件、降低 checkpoint 频率或追加式事件日志;风险是 inspector、恢复逻辑和 trajectory 格式兼容性。 -
裸
dict隐藏关键跨模块契约
message.extra.actions、action.command、output.returncode等只靠约定。自定义实现容易在远处触发KeyError。TypedDict或小型 Pydantic schema 能提高可维护性,但三种模型消息格式不同,过度统一会损害项目的“最小、可替换”目标。 -
本地环境是明确的安全边界
shell=True是产品能力,不是偶然细节。--yolo会让模型不经确认执行命令。若做面向不可信输入的产品,应优先更换 Environment(Docker/Bubblewrap)而不是在 prompt 中承诺安全。 -
配置可能把密钥写进 trajectory
Model 和 Environment 的serialize()会保存完整 config;项目又允许把 API key 放进model_kwargs,环境 config 也可能含密码或自定义变量。保存轨迹前应设计统一脱敏;风险是改变调试与复现实验所需的信息。 -
部分 extra Model 没满足 Model Protocol
RouletteModel/InterleavingModel没有format_message()和format_observation_messages(),直接交给DefaultAgent会在启动或 observation 阶段失败。若补代理方法,还必须记录本轮选中的子模型,保证 query 与 observation 使用同一消息协议。 -
动态 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 看另一种任务/环境。此时它们会是已知组件的新组合,而不是新的谜题。