✷ Harness
读 190 行代码,把 harness 这个词彻底讲清楚
harness 这个词现在到处都是,但很少有人点破它同时指着两样东西:跑循环的那个程序,和你在自己仓库里为它准备的那些文件。分不清这两者,你读到的每一篇 harness 文章都会在某处突然对不上。这篇用 mini-swe-agent 的主循环把它们拆开——190 行,一口气读得完,而指令、工具、环境、状态、反馈这五个子系统,在里面各能指到具体的行号。
基于 SWE-agent/mini-swe-agent@25941c8 · 读于 2026年8月26日
引用 31 处
- src/minisweagent/agents/default.pyThe agent is 190 lines end to end (171 without blank lines and comments) — small enough to read in one sitting.
- README.md:26-26The README bills the agent class as 'some 100 lines'; at this commit it is 190.
- src/minisweagent/agents/default.py:126-128step() is a single line: query the model, execute the actions. That is the whole trunk of an agent.
- src/minisweagent/agents/default.py:96-96The main loop in run() is a bare while True — the turn limit is not here but in the query() limiters, and it is off by default.
- src/minisweagent/agents/default.py:122-123The stop condition is the last message carrying the role `exit` — not the model announcing that it is finished.
- src/minisweagent/agents/default.py:92-95Instruction slot: system_template and instance_template are rendered into the first two messages.
- src/minisweagent/agents/default.py:66-67Templates render with Jinja2's StrictUndefined: a missing variable raises on the spot instead of silently becoming an empty string.
- src/minisweagent/agents/default.py:156-156Tool slot: on the agent's side the only exit to the world is env.execute; the list the model actually sees holds one tool, bash (see mini.yaml:143).
- src/minisweagent/agents/default.py:42-42State slot: the entire runtime state is one list[dict].
- src/minisweagent/agents/default.py:120-121save() sits in a finally, so the trajectory is on disk even for the turn that raised.
- src/minisweagent/agents/default.py:182-190State that outlives the process: messages, cost, call count and config serialized into a JSON file.
- src/minisweagent/agents/default.py:26-32Defaults for the four limiters: steps 0 (off), cost $3, wall clock 0 (off), three consecutive format errors.
- src/minisweagent/agents/default.py:132-148The first three limiters are checked inside query(), before the model is called; 0 switches a limiter off.
- src/minisweagent/agents/default.py:98-99Any clean step resets the consecutive-format-error counter — an occasional glitch is not a runaway.
- src/minisweagent/agents/default.py:100-102The format-error branch books the cost by hand: the model already spoke and was billed, but parsing failed before the normal accounting. Leave it out and a model can burn past the cost limiter.
- src/minisweagent/__init__.py:61-70Environment is a Protocol with three methods, which is why swapping in a Docker implementation changes not one line of the agent.
- src/minisweagent/environments/local.py:16-16LocalEnvironment's default command timeout is 30 seconds.
- src/minisweagent/environments/local.py:24-43Commands run in a freshly started subprocess, and exceptions are folded into the same dict shape as successes.
- src/minisweagent/environments/local.py:74-91On timeout it killpg's the whole process group; kill only the subshell and backgrounded children survive as orphans on your machine.
- src/minisweagent/environments/local.py:45-56Where done is judged: the first stdout line must be exactly the magic string and the return code must be 0 before Submitted is raised. The verdict belongs to the environment, not the model.
- src/minisweagent/environments/local.py:58-59The environment drops all of platform.uname() into the template variables — it describes itself into the instruction layer.
- src/minisweagent/config/mini.yaml:2-5The instruction slot's actual content lives in a config file, not in code.
- src/minisweagent/config/mini.yaml:18-19How the instruction layer defines finishing: run `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`, never combined with another command.
- src/minisweagent/config/mini.yaml:39-39Hard constraint: directory and environment variable changes do not persist, because every action runs in a new subshell.
- src/minisweagent/config/mini.yaml:69-73Instructions fork on the environment: the macOS `sed -i` note is appended only when system is Darwin.
- src/minisweagent/config/mini.yaml:112-128The feedback slot's entire context budget is one if in a template: past 10,000 characters, keep 5,000 at each end and replace the middle with a count of what was dropped.
- src/minisweagent/config/mini.yaml:129-148An error message written for an agent: not just what went wrong, but the correct call shape and the command that ends the task.
- src/minisweagent/exceptions.py:1-26The exit vocabulary: InterruptAgentFlow and its subclasses — Submitted / LimitsExceeded / TimeExceeded / FormatError, one per kind of ending.
- src/minisweagent/config/mini.yaml:4-100The full extent of instance_template: 96 lines (69 without blanks) — the same order of magnitude as the 'roughly 100 lines' OpenAI settled on for AGENTS.md.
- src/minisweagent/config/mini.yaml:55-10046 lines teaching the model the command line: heredocs to create files, sed to replace by line, nl to read with line numbers — complexity saved in the tool slot is spent in the instruction slot.
- src/minisweagent/config/mini.yaml:143-145The tool list in full: one tool called bash whose parameter table is a single command, shown to the model inside format_error_template.
一、模型很强,活还是干不成
Anthropic 做过一次对照实验:同一个模型(Opus 4.5)、同一个任务(做一个复古 2D 游戏制作器)、跑两遍。
第一遍让模型裸跑,20 分钟、9 美元。作者的原话是「我的实体出现在屏幕上了,但对输入毫无反应」。第二遍套上一个 planner + generator + evaluator 的三 agent 结构,6 小时、200 美元——「我真的能移动我的实体,能玩这个游戏了」。
他们自己的总结是:贵了 20 多倍,但产出质量的差别一眼就能看出来。
模型一个字没换。换的是模型外面那圈东西。
那圈东西就叫 harness。可一旦你想动手改进它,第一个问题就来了:**我到底该去改什么?**去改我装的那个 agent 吗?还是在仓库里加一个 AGENTS.md?两种说法你都见过,而且都自称在讲 harness。
二、这个词指两样东西,这是你没懂的原因
问题出在定义本身。目前流传最广的说法把 harness 定义成「模型权重之外的一切工程基础设施」,然后紧接着举例说 Claude Code、Cursor、Codex 都是 harness。
两句话都对,但它们说的不是一层东西:
- 一层是机制。「问一次模型 → 执行 → 把结果给回模型」这个环怎么转、什么时候停、模型能碰到哪些工具、结果以什么形态回灌。
mini-swe-agent、Claude Code、Codex 都属于这一层,而这一层是别人写好的程序。 - **一层是内容。**指令文件、验证命令、进度记录、目录约定——真正流经机制、最终被送到模型眼前的那些东西。它有一部分是程序出厂自带的,其余归你。
它们的关系不是并列,是插槽与填充物:机制那层的代码开出若干个槽,内容是流经这些槽的东西。
分清这两层,一个很常见的困惑就解开了——既然 Claude Code 本身就是个 harness,那我还谈什么「harness engineering」?我又没在写 Claude Code。
答案是这两层的归属不一样。机制那层确实轮不到你改。但槽里能被你改动的那部分,全都直接落在模型眼前——你写 AGENTS.md、定下验证命令、留一份进度文件,做的就是这件事。所以 harness engineering 不是「造一个 harness」,是把别人开好的槽填对。
不过「往槽里填」这个比喻要打个折扣:槽不是空的,两边都自带填充。mini-swe-agent 的那份就是随仓库出厂的 config/mini.yaml,Claude Code、Codex 这类现成工具(装上就能用、你看不到里面)则把系统提示、工具清单、截断规则预置在程序内部。真正的差别是看不看得见:前者你打得开、读得完、改得动;后者你只能在它之上再叠一层自己的。每个槽能叠多少差别很大,第四节会把这笔账按槽算清。
OpenAI 那支用 Codex 写了一百万行代码、五个月里没人手写过一行的团队,把这半边说得更直白。他们描述自己工作性质变化时用的原话是:一个软件工程团队的主要工作不再是写代码,而是**「设计环境、明确意图、搭建反馈回路」**。环境、指令、反馈——三样都是下面要讲的槽。他们没在改 Codex 本身,他们在填槽。
而这也正是光读概念文章学不会 harness 的原因:槽长什么样,只有读代码才看得见。
下面这 190 行,就是把槽画出来给你看。
三、190 行代码里的五个插槽
SWE-agent/mini-swe-agent 的核心是 src/minisweagent/agents/default.py。仓库 README 自称「some 100 lines」,我们在 25941c8 这一版实际数出来是 190 行(去掉空行与注释 171 行)——数字有出入,但结论不变:它小到可以整个读完。
整个循环只有两个方法。run() 是一个 while True,而 step() 只有一行:
def step(self) -> list[dict]:
"""Query the LM, execute actions."""
return self.execute_actions(self.query())
问模型,执行模型要求的动作。一个 agent 的全部主干就是这个。
**循环怎么停?**注意这里,它是整个设计里最值得学的一处:
if self.messages[-1].get("role") == "exit":
break
停止条件不是「模型说它做完了」,而是消息列表的最后一条带着 exit 这个角色。模型没有权限直接写出这样一条消息——它只能通过触发某个具体事件,让别人替它写。谁有这个权限?第四节和第五节各回答一半。
接下来要往这个骨架上按的,是一个借来的分法:把 harness 拆成指令、工具、环境、状态、反馈五个子系统。它出自 learn-harness-engineering 的第二讲。
五个槽在这 190 行里都指得到具体行号。下面每一节先用一句话说清这个槽回答的是什么问题,再在真实代码里把它指出来——概念文章通常只做前半件,而槽长什么样,只有后半件才看得见。
槽 1 · 指令
**指令回答的问题是:模型在开口之前,知道些什么。**它是你对模型说的、不随每一步变化的那部分话——它是谁、守什么规矩、这次要干什么、什么算完成。五个槽里只有它纯粹由文字构成,也因此最容易被误当成 harness 的全部。
run() 开头,两个模板被渲染成头两条消息:
self.add_messages(
self.model.format_message(role="system", content=self._render_template(self.config.system_template)),
self.model.format_message(role="user", content=self._render_template(self.config.instance_template)),
)
system_template 与 instance_template 是配置项,不是代码。它们住在 config/mini.yaml 里,你可以改。
模板里留了占位符,要拿别处的值来填。比如指令里有一行写着 {{system}} {{release}} {{version}} {{machine}},这四个值都来自环境——第四节会看到它们具体从哪儿来。填这些占位符用的是 Jinja2 的 StrictUndefined:有一个填不上就当场抛错。
Jinja2 默认不是这个行为。默认是把填不上的占位符渲染成空字符串,然后若无其事地继续。放在这里,那意味着模型会收到一份少了一句话的指令,照常开工,而你从头到尾不会收到任何信号。选 StrictUndefined 就是拒绝这个结局:指令缺一块,宁可整个不跑。
槽 2 · 工具
**工具回答的问题是:模型能对世界做什么。**除了工具,它只能产出文字——所以这个槽的大小,就是 agent 能力的上限。
outputs = [self.env.execute(action) for action in message.get("extra", {}).get("actions", [])]
多数 agent 会给模型一份工具清单:read_file、write_file、search、run_command……每一项都得声明名字、参数和类型,模型每一步从清单里挑一个来用。
mini-swe-agent 的清单只有一项:一个叫 bash 的工具,而它的参数表也只有一个 command。mini.yaml 把这份「清单」原样写给模型看:
Call the bash tool with your command as the argument:
- Tool: bash
- Arguments: {"command": "your_command_here"}
也就是说,模型每一步能做的事只有一件——说出一条 shell 命令。要读文件就 cat,要改文件就 sed,要搜索就 grep。
这么做的收益是:**你永远不用再加工具。**机器上装了什么,模型就能用什么——jq、rg、git、python,不需要在 harness 这边为它们各写一份参数声明和一套错误处理。
代价是模型得真的会用命令行。所以 mini.yaml 里专门拿出 46 行教它:怎么用 heredoc 建文件、怎么用 sed 按行替换、怎么用 nl 带行号看文件。**工具那边省下的复杂度,原样搬进了指令里。**两个槽加起来的工作量没变,只是换了个地方付。
槽 3 · 环境
**环境回答的问题是:工具动作落在哪儿。**同一句 rm -rf 落在你的笔记本上和落在一个一次性容器里是两件事——这个槽决定的是后果的范围。
Environment 是一个只有三个方法的 Protocol——Python 里的一种「接口约定」:任何一个类,只要身上长着这三个方法,它就算是一个 Environment,不需要继承谁,也不需要去哪儿注册。所以下面方法体里的 ... 不是我省略了代码,是文件里真的没有实现——约定只规定形状,不规定做法。
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: ...
三个方法各管一件事:
execute—— 跑一条命令,把结果交回来。槽 2 那个action字典就是喂给它的,整个工具槽最后都落在这一个方法上。get_template_vars—— 方向相反:把环境自己的信息(操作系统、版本……)交出去,给指令模板填占位符。这是环境反过来影响指令的那条线,第四节会专门看它。serialize—— 把环境自身导出成一个 dict,槽 4 那份轨迹里就有它一份。
LocalEnvironment 是最朴素的实现:用 subprocess.Popen(command, shell=True, ...) 起一个新的子 shell 来跑,带 30 秒默认超时。
超时的时候它杀的是整个进程组(killpg),不是只杀那个子 shell。原因在 & 上:shell 里给命令末尾加一个 &,意思是「放到后台跑,别等它」,命令立刻返回。模型很容易这么写——比如 python -m http.server & 起个服务器,下一步再去 curl 它。可这么派生出来的进程挂在子 shell 底下,只杀子 shell 的话它们就成了孤儿:没了父亲,但没死,继续留在你机器上跑。killpg 是连整个进程组一起收掉。
而这三个方法就是全部的约定。所以你换一个「在 Docker 容器里执行」的实现塞进来,agent 那 190 行一个字都不用改。这就是「槽」的字面意思:形状钉死,填什么随你。
槽 4 · 状态
**状态回答的问题是:下一次调用时,模型还记得什么。**模型本身是无状态的,每次调用都从零开始,你不重放的东西对它就不存在。所以「这个 agent 记得什么」从来不是模型的属性,是 harness 的属性。
self.messages: list[dict] = []
一个列表。这就是全部的运行时状态。
另一半状态要活过进程本身,靠的是 save():把 messages、成本、调用次数、配置一起序列化成 JSON 写进文件——也就是常说的轨迹(trajectory),事后复盘、比对、算账看的都是它。
关键在于这句调用写在哪儿:run() 的循环里,包着 step() 的那个 try 的 finally 分支。finally 的语义是「不管这段代码怎么收场都要执行」——即便 step() 正在抛异常,也得先让 save() 跑完,异常才接着往外传、去找 run() 里那些接得住它的 except 分支。finally 只是插在中间做收尾,它不会把异常吞掉。
这在别处只算好习惯,在这里是必需的,因为这个 agent 的绝大多数收场都是异常:模型宣布完成抛 Submitted,撞上成本闸抛 LimitsExceeded,超时抛 TimeExceeded(第五节讲这几道闸)。要是把 save() 写在正常返回的那条路上,最该留下现场的那几种结局反而一片空白。
槽 5 · 反馈
**反馈回答的问题是:执行结果以什么形态回到模型眼前。**它必然要经过加工,因为上下文窗口有限而命令输出无限——问题从来不是要不要处理,只是在哪儿处理、按什么规则。
执行结果不是直接扔回去的,它要先过一层模板:
observation_template: |
{%- if output.output | length < 10000 -%}
{ "returncode": {{ output.returncode }}, "output": {{ output.output | tojson }} }
{%- else -%}
{ "output_head": ..., "output_tail": ..., "elided_chars": ..., "warning": "Output too long." }
{%- endif -%}
一条 find / 能打出几十万字符,直接回灌会把上下文窗口一次吃光。所以超过一万字符就只留头尾各 5000,中间换成一个「省略了多少」的计数。
这件事有个正经名字,叫上下文预算管理:窗口就那么大,谁有资格占位置,总得有人决定。在大一些的 harness 里它往往是一整套装置——专门的组件、负责摘要的模型、分级存储。而在这里,它的全部实现就是上面那个 {%- if -%}:一个长度阈值,截头留尾。
更值得注意的是它落在配置里,而不是代码里。想让模型多看见一些,把 10000 改大就行,Python 一行都不用碰——按第二节那个划分,上下文预算在这个 harness 里属于内容,不属于机制。
四、你写的东西,是往哪个槽里填
现在回头看那些「harness 最佳实践」文章让你做的事,每一件都能对上一个槽。
mini.yaml 的 instance_template 里,装的正好是那些文章要你写进 AGENTS.md 的东西——推荐工作流、硬约束、完成的定义。比如这条硬约束:
Directory or environment variable changes are not persistent. Every action is executed in a new subshell.
这句是字面意义上的真:每次 execute 都新起一个 subprocess,上一条命令 cd 去了哪儿、export 了什么,下一条完全不知情。所以它不是在提醒模型小心,是在如实描述槽 3 的实现。
指令层和环境层必须对得上。假如这句话没写,模型很可能先敲 cd src,下一步直接 ls,以为看的是 src/ 里的东西——实际上它早就回到了起点,看到的是别处,而且它不会察觉。模型只能照着指令描述的那个世界行动;描述错了,它的每一步都会稳定地错在同一个地方。
顺带一个值得注意的巧合:mini.yaml 里这份 instance_template 正好 96 行(去掉空行 69 行)。而 OpenAI 那支团队试过「一个大 AGENTS.md」并且失败了,最后落到的做法是把 AGENTS.md 当目录而不是百科全书,长度「roughly 100 lines」,细节推到 docs/ 里按需读取。一个是给 agent 的开场指令,一个是给 agent 的仓库入口,两边互不相干,却收敛到同一个量级。
更有意思的是两个槽之间的连线。LocalEnvironment.get_template_vars() 会把 platform.uname() 整个塞进模板变量,于是 mini.yaml 里可以写:
{%- if system == "Darwin" -%}
<important>
You are on MacOS. For all the below examples, you need to use `sed -i ''` instead of `sed -i`.
</important>
{%- endif -%}
**环境把自己描述进了指令。**你在 macOS 上跑,模型收到的指令就和 Linux 上不一样。这种细节在概念文章里永远看不到,因为它不属于任何一个子系统——它属于两个子系统的接缝。
「完成」的定义也在槽里
那些文章反复强调:不能让 agent 自己说做完了。这个原则在这里被实现成了一条具体的路径。
指令层告诉模型完成的方式是执行一条特定命令:
Submit your changes and finish your work by issuing the following command:
echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT.
而环境层在每次执行之后嗅探 stdout:
lines = output.get("output", "").lstrip().splitlines(keepends=True)
if lines and lines[0].strip() == "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT" and output["returncode"] == 0:
raise Submitted({"role": "exit", ...})
第一行必须正好是那个魔法字符串,而且返回码必须是 0。条件成立才抛出 Submitted,而 Submitted 携带的正是那条 role: "exit" 的消息——第三节那个停止条件,到这里才闭合。
所以「模型不能自己宣布完成」在代码里的含义是:模型能做的只是发出一条 shell 命令;这条命令得真的执行成功,环境才会替它写下那条退出消息。判定权在环境手里,不在模型手里。
你的份额,按槽算
上面这些都是在 mini-swe-agent 里说的——那里每个槽的填充物都摊在 mini.yaml 里,你想动哪个动哪个。换成 Claude Code 这类现成工具,填充物封在程序内部,于是每个槽你能动多少,差别很大:
| 槽 | 机制(程序定死) | 内容(你供) |
|---|---|---|
| 指令 | 何时注入、读哪些文件、谁压过谁 | AGENTS.md / CLAUDE.md、skill 正文、你这次提的需求 |
| 工具 | 清单、调用协议、权限模型 | MCP server、skill 捎带的脚本,以及机器上装了什么命令 |
| 环境 | 本机起 subprocess 还是进容器 | 选哪个,以及里面装了什么、有没有网、数据库可不可写 |
| 状态 | 消息列表、上下文压缩、读哪个 memory 文件 | 进度文档写什么、记到什么粒度 |
| 反馈 | 截断规则、结果怎么回灌 | 你的命令打印出什么——尤其是失败的那几行 |
有两处值得单独点出来。
**工具那一栏看着最不归你,其实不然。**只要清单里有一个 shell 工具,这个槽的内容就变成了「你这台机器上装了什么」——装上 rg、jq、gh,模型立刻多三个工具,harness 一行都不用改。这就是第三节那笔账的现实版:mini-swe-agent 把它做到了极端,现成工具则是把 shell 混在清单里一起给你。
反馈的机制归 agent,内容几乎全归你。截断规则你动不了,但回灌给模型的那段字是你的仓库产生的。一个测试跑完只打 1 test failed,和一个打出断言 diff 加文件行号的,模型的下一步完全不同;类型检查是静默退出还是列出 12 个错误,也是你决定的。OpenAI 那句「搭建反馈回路」指的就是这件事——他们没在改 Codex,他们在让失败说人话。
五、四道闸:agent 为什么会跑飞,以及怎么拦
AgentConfig 上有四个限制,它们是这 190 行里最该被抄走的部分:
step_limit: int = 0
cost_limit: float = 3.0
wall_time_limit_seconds: int = 0
max_consecutive_format_errors: int = 3
前三道闸的检查都在 query() 里,在调用模型之前:
if 0 < self.config.step_limit <= self.n_calls or 0 < self.config.cost_limit <= self.cost:
raise LimitsExceeded(...)
if 0 < self.config.wall_time_limit_seconds <= int(time.time() - self._start_time):
raise TimeExceeded(...)
0 < self.config.step_limit <= self.n_calls 是 Python 的链式比较,读作两个条件同时成立:step_limit > 0(这道闸开着)并且 n_calls >= step_limit(已经撞上了)。
于是把限额设成 0,就等于关掉这道闸——左半边 0 < 0 当场为假,右半边连算都不会算。这是个常见的哨兵值约定:配置项总得有个值用来表示「不限」,这里挑的是 0。
再回头看默认值:step_limit = 0、wall_time_limit_seconds = 0、cost_limit = 3.0。所以开箱跑起来只有成本闸是活的,3 美元封顶;步数不限,墙钟(真实流逝的时间)也不限。
第四道闸管的是另一类失败:模型的输出解析不出动作。它不是一次就死——max_consecutive_format_errors 数的是连续次数,而任何一次干净的 step 都会把计数清零:
self.step()
self.n_consecutive_format_errors = 0 # reset on any clean step
偶尔抽风不算问题,连着三次抽风才算跑飞了。
还有一个只有读代码才知道的细节。格式错误的分支里有这么一行,带着一句注释:
# The call was billed before parsing failed, so query() never got to charge it.
self.cost += e.messages[0].get("extra", {}).get("cost", 0.0)
模型已经出过话、账已经扣了,只是解析失败没走到正常记账的地方。不补这一笔,一个反复输出错格式的模型就能绕过成本闸无限烧钱——因为成本永远停在原地。
这四道闸加起来不到十行。它们回答的是「agent 为什么既越界又完不成」这个问题——答案不是提示词写得不够好,是没人给它设终止条件。
六、所以,harness 是什么
回到开头那个词。现在可以给一个不会再让你卡住的说法:
harness 是模型外面那圈东西,它由两半组成。一半是机制:跑循环的那个程序规定了有哪些槽——指令、工具、环境、状态、反馈——以及什么时候必须停。另一半是流经这些槽的内容。前一半你通常改不了;后一半也未必全归你,但能被你改动的东西,全在这里。
mini-swe-agent 的价值在于它把前一半压缩到了 190 行,小到你能一次看完,于是那些槽第一次变成了具体的东西:指令是 run() 开头渲染的两个模板,工具是 env.execute(),环境是一个三方法的 Protocol,状态是一个 list 加一次 save(),反馈是一个带 if 的模板。
看清楚槽在哪儿,你才知道自己写的那份 AGENTS.md 到底是在往哪儿塞。
参考与出处
本文的代码论断全部来自 SWE-agent/mini-swe-agent 的 25941c8,逐条引用见文首的证据条。外部材料:
- Anthropic: Harness design for long-running application development —— 第一节那组对照数据的一手出处。
- OpenAI: Harness engineering — leveraging Codex in an agent-first world —— 第二节与第四节引的两处出自这里:工程师职责的转变,以及把
AGENTS.md当目录而非百科全书、约 100 行。 - Anthropic: Building effective agents —— 本文所依据的那个 agent 定义("LLMs using tools based on environmental feedback in a loop")出自这里。
- walkinglabs/learn-harness-engineering —— 五子系统的划分方式来自这门课的第二讲,本文沿用了它的分法。
本站也收录了这个项目:mini-swe-agent 的目录页,有 star 曲线和一段简介。