本系列基于 mini-SWE-agent 源码学习整理;源码版本固定为 Asuhe404/mini-swe-agent@388da74,便于长期核对实现。
系列导航
返回路线图 · 上一篇:01. Python 最小语法桥:从 JavaScript 读懂 mini-SWE-agent · 下一篇:03. Protocol 与多态:用同一套插座更换三类组件
适合:零 Python 基础、已有 JavaScript 基础。
重点文件:
src/minisweagent/run/hello_world.pysrc/minisweagent/config/default.yamlpyproject.tomlsrc/minisweagent/__main__.py
本节目标:能从用户输入的一条命令,追到 Python callable、YAML 配置、三个组件的构造和 agent.run(task)。
1. 先用 JavaScript 建立心智模型
| Python 项目概念 | JavaScript / Node.js 类比 |
|---|---|
run/hello_world.py | 极简 bootstrap.ts / main.ts |
config/default.yaml | YAML 配置对象 + Nunjucks/Handlebars 模板 |
pyproject.toml | package.json + ESLint/Prettier/Jest 配置的合集 |
[project.scripts] | package.json 的 bin,不是 npm scripts |
mini = "minisweagent.run.mini:app" | 安装命令 mini,加载模块并调用导出的 app |
python -m minisweagent | 执行包约定的 minisweagent/__main__.py |
if __name__ == "__main__" | CommonJS if (require.main === module) |
yaml.safe_load(...)["agent"] | YAML.parse(readFileSync(...)).agent |
DefaultAgent(model, env, **config) | constructor injection;config 属性展开成 named arguments |
用 JS 伪代码表示 hello_world.py:
function main({ task, modelName = process.env.MSWEA_MODEL_NAME }) {
const fullConfig = YAML.parse(readFileSync("config/default.yaml", "utf8"));
const agent = new DefaultAgent(
new LitellmModel({ modelName }),
new LocalEnvironment(),
fullConfig.agent,
);
agent.run(task);
return agent;
}
注意:JS 伪代码把 fullConfig.agent 写成第三个 object 只是为了表达意图。真实 Python 的 **full_config["agent"] 会展开成多个命名参数。
2. 5W2H
| 问题 | 回答 |
|---|---|
| What | Python 包的安装元数据、可执行入口、最小组件装配和默认配置。 |
| Why | 让同一套 Agent 能从 console command、python -m、直接脚本或 Python import 启动。 |
| Who | 安装器读取 pyproject.toml;Python 读取 __main__.py;Typer 解析参数;Run Script 构造三组件。 |
| When | 安装时生成 console scripts;运行时解析 CLI、读取 YAML、构造对象并调用 Agent。 |
| Where | pyproject.toml、包级 __main__.py、run/*.py 和 config/*.yaml。 |
| How | module:callable 映射、Python 的 -m 约定、依赖注入和 YAML/Jinja。 |
| How much | 入口转发只有 7 行;hello-world 装配 42 行;真正成本在模型 API 与命令执行,不在入口。 |
3. 四种启动方式不是同一条路径
| 用户操作 | 首个项目入口 | 最终 Run Script | 默认配置 |
|---|---|---|---|
mini ... | 安装器生成的 wrapper | run/mini.py | mini.yaml |
mini-swe-agent ... | 安装器生成的 wrapper | run/mini.py | mini.yaml |
python -m minisweagent ... | minisweagent/__main__.py | run/mini.py | mini.yaml |
python .../run/hello_world.py ... | hello_world.py 的 main guard | run/hello_world.py | 只取 default.yaml["agent"] |
pipx run mini-swe-agent 走安装后的 console script,并不会真的先执行 __main__.py;两条路径只是最终复用了同一个 run.mini:app。
4. 逐行读 run/hello_world.py
第 1-3 行:声明它是教学入口
docstring 明确说明这是 Python bindings 的最简示例;完整 CLI 应看 mini.py。
第 5-10 行:入口需要的基础设施
import logging
import os
from pathlib import Path
import typer
import yaml
logging:设置调试日志。os:读取默认模型名环境变量。Path:定位包内 YAML。typer:把函数签名变成 CLI。yaml:把配置文本变成 dict。
第 12-15 行:入口选择具体实现
from minisweagent import package_dir
from minisweagent.agents.default import DefaultAgent
from minisweagent.environments.local import LocalEnvironment
from minisweagent.models.litellm_model import LitellmModel
这是 Run Script 的核心职责:明确选择一个 Agent、一个 Environment、一个 Model。
package_dir 定义在 minisweagent/__init__.py:23,它从当前包文件的位置计算路径,因此安装进 site-packages 后仍能找到随 wheel 发布的配置。
第 17-20 行:创建 Typer app 并注册 main
app = typer.Typer()
@app.command()
近似:
const program = new Command();
program.action(main);
第 21-30 行:定义 CLI contract
task通过-t/--task传入;...是 Typer 的必填 sentinel。model_name通过-m/--model传入,默认值来自MSWEA_MODEL_NAME。prompt=表示 CLI 缺参时询问用户。-> DefaultAgent是返回类型提示。
typer.Option(...) 和其中的 os.getenv(...) 在模块 import、函数定义时求值。普通 Python 代码直接调用 main(task="x") 时不会经过 Typer 参数解析,省略的 model_name 可能仍是 OptionInfo 对象。
第 31 行:全局日志副作用
logging.basicConfig(level=logging.DEBUG)
它配置根 logger,不只影响当前函数。作为教学脚本很方便,作为可复用库函数则偏重。
第 32-36 行:最小装配
agent = DefaultAgent(
LitellmModel(model_name=model_name),
LocalEnvironment(),
**yaml.safe_load(Path(package_dir / "config" / "default.yaml").read_text())["agent"],
)
严格从内到外读:
package_dir / "config" / "default.yaml"拼出包内路径。- 外层
Path(...)重新包装 Path,本例中没有改变值。 .read_text()得到 YAML 字符串。yaml.safe_load(...)得到三段式 dict。["agent"]只取 Agent 配置。**把四个 key 展开成DefaultAgent的命名参数。- Model 和 Environment 没收到 YAML 中各自的 section,只使用类默认配置。
真实效果近似:
DefaultAgent(
LitellmModel(model_name=model_name),
LocalEnvironment(),
system_template="...",
instance_template="...",
step_limit=0,
cost_limit=0.0,
)
第 37-38 行:启动并保留状态
agent.run(task) 才会进入模型/命令循环。函数返回 Agent 实例,测试和 Python 调用者可以继续读取 messages、cost、n_calls。
第 41-42 行:直接执行才启动 CLI
直接运行或用 python -m minisweagent.run.hello_world 时 __name__ == "__main__",于是调用 app()。普通 import minisweagent.run.hello_world 不会启动 CLI。
5. 逐段读 config/default.yaml
YAML 与 Jinja 是两层语言
instance_template: |
Please solve this issue: {{task}}
第一层 YAML:
- 缩进表达嵌套,近似 JS object。
key: value表示属性。|表示保留换行的多行字符串。0解析成 int;0.解析成 float。
第二层 Jinja:
{{ task }}输出变量,类似 Nunjucks/Handlebars。{% if … %}是控制语句。{% set … %}创建模板局部变量。| length、| tojson是 filter。
YAML 解析时不会处理 Jinja;字符串进入 Agent/Model 后才渲染。
第 1-105 行:agent section
第 2-17 行:system template
规定助手角色和旧式文本 action 协议:回答里必须包含一个 ```mswea_bash_command 代码块。
消费者:AgentConfig.system_template,在 DefaultAgent.run() 创建第一条 system message 时渲染。
第 18-103 行:instance template
19:把用户任务{{task}}注入 prompt。23-33:建议的软件工程流程和完成标记。35-40:一次一个 action、每次新 subshell 等规则。42-44:注入系统平台信息。46-103:向模型展示正确输出、文件编辑和查看命令示例。72-76:只有system == "Darwin"时渲染 macOS 的sed -i ''提醒。
变量来源:
| Jinja 变量 | 来源 |
|---|---|
task | DefaultAgent.run(task) |
system/release/version/machine | LocalEnvironment.get_template_vars() 的 platform.uname() |
Agent 使用 StrictUndefined 渲染,缺少变量会直接报错,不会静默输出空字符串。
第 104-105 行:限制
step_limit: 0
cost_limit: 0.
在 DefaultAgent.query() 中只有 0 < limit 才启用限制,所以两个 0 都表示无限制。0. 只是显式写成浮点数。
第 106-112 行:environment section
这些变量本来用于减少命令噪声:让 pager 直接输出、关闭进度条等。若传给 LocalEnvironment(**config["environment"]),会与宿主 os.environ 合并后传给子进程。
但 hello_world.py 没有传这一段,所以这条入口中它们不生效。
第 113-171 行:model section
第 114-141 行:observation template
把命令执行结果转成模型下一轮能读的 XML 风格文本:
- 命令异常时显示
<exception>。 - 显示 return code。
- 输出少于 10,000 字符时完整发送。
- 输出达到 10,000 字符时只保留头尾各 5,000 字符,并提示省略数量。
边界细节:条件是 < 10000,恰好 10,000 字符会进入截断分支,却显示 0 characters elided。
第 142-143 行:LiteLLM kwargs
model_kwargs:
drop_params: true
如果传给 Model,会透传到 litellm.completion,让 LiteLLM 丢弃目标 provider 不支持的参数。
第 144-171 行:format-error template
模型没有按 action 协议输出时,这段文字会作为纠错 message 发回模型。它还区分 token 截断与一般格式错误。
但 hello_world.py 同样没有传 model section,所以 observation 截断、drop_params 和这份纠错模板全部不生效。
当前值得识别的协议漂移
default.yaml 要求 fenced code block;hello_world.py 却构造 LitellmModel:
LitellmModel._query()向 API 声明原生bashtool。LitellmModel._parse_actions()只读取message.tool_calls。- 模型若严格按 YAML 输出代码块,解析器会报告“No tool calls found”。
真正解析 ```mswea_bash_command 的是 LitellmTextbasedModel。因此当前组合“配置字段结构兼容,action 交互协议不兼容”。
6. 逐段读 pyproject.toml
TOML 的 [section] 类似嵌套 object;key = value 类似属性赋值。
第 1-3 行:构建系统
告诉 pip/uv 用 setuptools 构建 wheel。近似 Node 项目选择构建/发布工具,但它不是应用运行入口。
第 6-31 行:发布元数据
- 包名是
mini-swe-agent;import 名是minisweagent,两者可以不同。 dynamic = ["version"]表示版本不直接写在 TOML。requires-python = ">=3.10"是运行时版本约束。- 其余是说明、许可证、作者和 PyPI classifiers。
第 33-48 行:运行依赖
近似 package.json.dependencies。安装项目时会安装 PyYAML、Jinja、Pydantic、LiteLLM、Typer 等。
第 50-82 行:optional dependency extras
近似可选择的功能组合:
pip install 'mini-swe-agent[dev]'
pip install 'mini-swe-agent[modal]'
pip install 'mini-swe-agent[contree]'
第 84-87 行:项目链接
发布到 PyPI 的文档、仓库和问题链接。
第 89-93 行:安装后命令
[project.scripts]
mini = "minisweagent.run.mini:app"
mini-swe-agent = "minisweagent.run.mini:app"
mini-extra = "minisweagent.run.utilities.mini_extra:main"
mini-e = "minisweagent.run.utilities.mini_extra:main"
语义是:
命令名 = "Python 模块路径:模块中的 callable"
安装器会生成小型 wrapper。执行 mini 时,它导入 minisweagent.run.mini,取得 app 并调用。这里最接近 package.json.bin;npm scripts 则是开发者手动定义的命令别名,语义不同。
hello_world.py 没有出现在这里,所以安装后没有 hello-world 命令。
第 95-106 行:打包范围
include-package-data = true:允许包含非 Python 文件。- 版本来自
minisweagent.__version__。 - 从
src/发现minisweagent*包。 config/**/*被放进发行包;否则安装后package_dir/config/default.yaml不存在。
第 108 行以后:开发工具
- Ruff 部分近似 ESLint + Prettier。
- pytest 部分近似 Jest/Vitest config。
[dependency-groups].dev是另一种开发依赖声明。
这些不参与本节入口调用链,第一遍知道用途即可。
7. 逐行读 minisweagent/__main__.py
第 1 行:shebang
#!/usr/bin/env python3
直接把文件当可执行脚本时用于寻找 Python;python -m 不依赖它。
第 2 行:模块说明
说明 python -m minisweagent 的入口意图。pipx run mini-swe-agent 功能上进入同一 app,但真实路径是 console-script wrapper,不是这个文件。
第 4 行:唯一转发点
from minisweagent.run.mini import app
这里不重新实现 CLI,只复用 run/mini.py 中的 Typer app。import 时还会执行 minisweagent/__init__.py 的包初始化,包括创建全局配置目录、打印版本信息和加载 .env。
第 6-7 行:作为 -m 入口才调用
if __name__ == "__main__":
app()
python -m minisweagent 会让这个模块的 __name__ 为 "__main__",因此启动 Typer。普通 import minisweagent.__main__ 不会调用。
8. 所有直接使用位置
hello-world
- 文档入口:
docs/quickstart.md:83-87。 - 端到端测试:
tests/run/test_run_hello_world.py:23-46。 - 测试在第 5 行 import
main,第 34 行绕过 Typer 直接调用。
default.yaml
- 生产直接读取:
run/hello_world.py:35。 - DefaultAgent/InteractiveAgent 测试 fixture:
tests/agents/test_default.py:59-74、test_interactive.py:104-119。 - 轨迹保存测试:
tests/run/test_save.py:10-20,51-61。 - CLI 输出文件测试会复制整份配置:
tests/run/test_cli_integration.py:563-643。
console scripts 与 __main__
mini/mini-swe-agent映射:pyproject.toml:89-93。python -m minisweagent测试:tests/run/test_cli_integration.py:341-351。- 安装命令测试:同文件
354-378。 __main__.py和两个主要 console script 最终都复用run/mini.py:50的 app。
9. 无 API 小练习
这个练习读取真实 TOML/YAML,但用三个假组件替代 Model、Environment、Agent。它不访问网络,也不执行 shell。教程使用的 Python 3.13 自带 tomllib;Python 3.10 需要另装 tomli 才能解析 TOML。
运行前先预测最后五行输出:
MSWEA_SILENT_STARTUP=1 PYTHONPATH=src .venv/bin/python - <<'PY'
from pathlib import Path
import tomllib
import yaml
class FakeModel:
def __init__(self, model_name: str):
self.model_name = model_name
class FakeEnvironment:
name = "offline-memory"
class FakeAgent:
def __init__(self, model: FakeModel, env: FakeEnvironment, **config):
self.model = model
self.env = env
self.config = config
def run(self, task: str) -> dict:
return {
"model": self.model.model_name,
"environment": self.env.name,
"task": task,
"step_limit": self.config["step_limit"],
"cost_limit": self.config["cost_limit"],
}
def main(task: str) -> dict:
config = yaml.safe_load(Path("src/minisweagent/config/default.yaml").read_text())
agent = FakeAgent(FakeModel("offline-demo"), FakeEnvironment(), **config["agent"])
return agent.run(task)
def simulate_module(module_name: str):
if module_name == "__main__":
return main("learn assembly")
return "not started"
scripts = tomllib.loads(Path("pyproject.toml").read_text())["project"]["scripts"]
module_name, callable_name = scripts["mini"].split(":")
print(f"mini command -> module={module_name}, callable={callable_name}")
config = yaml.safe_load(Path("src/minisweagent/config/default.yaml").read_text())
print(f"YAML top-level sections -> {list(config)}")
print(f"agent keys -> {list(config['agent'])}")
print(f"imported module -> {simulate_module('lesson_module')}")
print(f"direct module -> {simulate_module('__main__')}")
PY
已验证输出:
mini command -> module=minisweagent.run.mini, callable=app
YAML top-level sections -> ['agent', 'environment', 'model']
agent keys -> ['system_template', 'instance_template', 'step_limit', 'cost_limit']
imported module -> not started
direct module -> {'model': 'offline-demo', 'environment': 'offline-memory', 'task': 'learn assembly', 'step_limit': 0, 'cost_limit': 0.0}
动手改两次:
- 把
**config["agent"]改成**config,观察 FakeAgent 收到的 key,并解释为什么run()找不到step_limit。 - 把
simulate_module("lesson_module")改成simulate_module("__main__"),确认 main guard 控制是否启动,与函数定义本身无关。
10. 三道检查题(先不要看答案)
mini = "minisweagent.run.mini:app"中,模块路径和 callable 分别是什么?为什么它更像package.json.bin而不是 npm scripts?- 为什么
hello_world.py必须传**config["agent"],不能直接传**config?这同时意味着哪两个 YAML section 在该入口被忽略? import minisweagent.__main__与python -m minisweagent会给__main__.py的__name__分别赋什么值?哪一种会调用app()?
11. 可维护性、性能与安全审查
高优先级:hello-world 的 action 协议不一致
default.yaml 要求代码块 action,但 LitellmModel 只解析原生 tool calls。现有测试替换了 LitellmModel,并自己从代码块解析 action,所以没有暴露真实路径问题。
可选修复:
- 保留
default.yaml,改用LitellmTextbasedModel,并把model/environmentsection 也传给对应组件。 - 保留
LitellmModel,改用与 tool-call 协议一致的mini.yaml或新的最小 tool-call 配置。
风险:这会改变模型 API 参数、消息角色、observation 格式、文档示例和端到端测试,必须先明确 hello-world 想教学的是旧文本协议还是当前默认 tool-call 协议。
高优先级:默认没有预算与隔离
step_limit=0、cost_limit=0,加上 Agent 默认墙钟限制 0,表示不设限。LocalEnvironment用shell=True在宿主机执行模型命令,并继承环境变量。- hello-world 使用
DefaultAgent,没有 InteractiveAgent 的人工确认层。 - DEBUG 日志可能输出完整模型内容。
因此初学阶段不要直接运行真实 hello_world.py;先使用本节假组件练习。
配置 section 被静默丢弃
hello-world 只取 Agent section:
- Environment 的降噪变量不生效。
- Model 的
drop_params、长输出截断和格式错误模板不生效。 - 类默认 observation template 不截断长输出,可能增加上下文和费用。
可改为先读取完整 config,再分别传给三个组件。风险是暴露上面的 action 协议冲突,不能只做机械拆分。
CLI 与 Python API 耦合
Typer 的 OptionInfo 同时被当作函数默认值。更清晰的设计是:
def build_agent(model_name: str) -> DefaultAgent: ...
@app.command()
def main(task: str = typer.Option(...), model_name: str = typer.Option(...)):
return build_agent(model_name).run(task)
风险:拆分会改变直接调用 main() 的测试、文档和可能存在的外部 Python 用户。
打包与入口存在多处真相
- pyproject console scripts 和
__main__.py分别写了一次run.mini:app。 - 文档声称
MSWEA_DEFAULT_RUN能覆盖入口,但__main__.py实际硬编码run.mini。 pyproject.toml同时有 optionaldevextra 与 dependency groupdev,内容已经不同。
这些重复配置未来可能漂移;合并前要确认 pip extras、uv dependency groups 和 Python -m 各自的兼容需求。
性能判断
- 每次调用 hello-world 都重新读取并解析 YAML,但 171 行配置的开销相对模型 API 可以忽略。
- prompt 很长,会增加每次模型请求的 token;这是比 YAML 解析更实际的成本。
- observation template 的字符截断只减少发给模型的内容;Environment 在此之前仍已把全部命令输出读入内存。