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

系列导航

返回路线图 · 上一篇:01. Python 最小语法桥:从 JavaScript 读懂 mini-SWE-agent · 下一篇:03. Protocol 与多态:用同一套插座更换三类组件

适合:零 Python 基础、已有 JavaScript 基础。
重点文件:

  • src/minisweagent/run/hello_world.py
  • src/minisweagent/config/default.yaml
  • pyproject.toml
  • src/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.yamlYAML 配置对象 + Nunjucks/Handlebars 模板
pyproject.tomlpackage.json + ESLint/Prettier/Jest 配置的合集
[project.scripts]package.jsonbin,不是 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

问题回答
WhatPython 包的安装元数据、可执行入口、最小组件装配和默认配置。
Why让同一套 Agent 能从 console command、python -m、直接脚本或 Python import 启动。
Who安装器读取 pyproject.toml;Python 读取 __main__.py;Typer 解析参数;Run Script 构造三组件。
When安装时生成 console scripts;运行时解析 CLI、读取 YAML、构造对象并调用 Agent。
Wherepyproject.toml、包级 __main__.pyrun/*.pyconfig/*.yaml
Howmodule:callable 映射、Python 的 -m 约定、依赖注入和 YAML/Jinja。
How much入口转发只有 7 行;hello-world 装配 42 行;真正成本在模型 API 与命令执行,不在入口。

3. 四种启动方式不是同一条路径

流程图 1流程图 1
用户操作首个项目入口最终 Run Script默认配置
mini ...安装器生成的 wrapperrun/mini.pymini.yaml
mini-swe-agent ...安装器生成的 wrapperrun/mini.pymini.yaml
python -m minisweagent ...minisweagent/__main__.pyrun/mini.pymini.yaml
python .../run/hello_world.py ...hello_world.py 的 main guardrun/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"],
)

严格从内到外读:

  1. package_dir / "config" / "default.yaml" 拼出包内路径。
  2. 外层 Path(...) 重新包装 Path,本例中没有改变值。
  3. .read_text() 得到 YAML 字符串。
  4. yaml.safe_load(...) 得到三段式 dict。
  5. ["agent"] 只取 Agent 配置。
  6. ** 把四个 key 展开成 DefaultAgent 的命名参数。
  7. 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 调用者可以继续读取 messagescostn_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 变量来源
taskDefaultAgent.run(task)
system/release/version/machineLocalEnvironment.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 声明原生 bash tool。
  • 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-74test_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}

动手改两次:

  1. **config["agent"] 改成 **config,观察 FakeAgent 收到的 key,并解释为什么 run() 找不到 step_limit
  2. simulate_module("lesson_module") 改成 simulate_module("__main__"),确认 main guard 控制是否启动,与函数定义本身无关。

10. 三道检查题(先不要看答案)

  1. mini = "minisweagent.run.mini:app" 中,模块路径和 callable 分别是什么?为什么它更像 package.json.bin 而不是 npm scripts?
  2. 为什么 hello_world.py 必须传 **config["agent"],不能直接传 **config?这同时意味着哪两个 YAML section 在该入口被忽略?
  3. import minisweagent.__main__python -m minisweagent 会给 __main__.py__name__ 分别赋什么值?哪一种会调用 app()

11. 可维护性、性能与安全审查

高优先级:hello-world 的 action 协议不一致

default.yaml 要求代码块 action,但 LitellmModel 只解析原生 tool calls。现有测试替换了 LitellmModel,并自己从代码块解析 action,所以没有暴露真实路径问题。

可选修复:

  1. 保留 default.yaml,改用 LitellmTextbasedModel,并把 model/environment section 也传给对应组件。
  2. 保留 LitellmModel,改用与 tool-call 协议一致的 mini.yaml 或新的最小 tool-call 配置。

风险:这会改变模型 API 参数、消息角色、observation 格式、文档示例和端到端测试,必须先明确 hello-world 想教学的是旧文本协议还是当前默认 tool-call 协议。

高优先级:默认没有预算与隔离

  • step_limit=0cost_limit=0,加上 Agent 默认墙钟限制 0,表示不设限。
  • LocalEnvironmentshell=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 同时有 optional dev extra 与 dependency group dev,内容已经不同。

这些重复配置未来可能漂移;合并前要确认 pip extras、uv dependency groups 和 Python -m 各自的兼容需求。

性能判断

  • 每次调用 hello-world 都重新读取并解析 YAML,但 171 行配置的开销相对模型 API 可以忽略。
  • prompt 很长,会增加每次模型请求的 token;这是比 YAML 解析更实际的成本。
  • observation template 的字符截断只减少发给模型的内容;Environment 在此之前仍已把全部命令输出读入内存。