本地模型驱动的自构建dev harness:416次运行仅176美元的低成本AI编程闭环
Ducklab 这个项目最抓人的地方,不是“又一个开发工具”的新增功能,而是它自带的一组记录:416 次运行、总成本 176 美元、全程接入本地模型。翻译成更容易理解的说法就是,这是一个用本地模型驱动、在循环中不断修改代码和验证结果的 dev harness,而且它在低成本条件下真的跑出了足够多的有效迭代。如果你打算做 AI 编程工具链,或者在研究“让工具自己改进自己”的工作流,这篇文章值得看完。这里不会吹它能替代人工编码,而是把它背后的执行闭环拆开,讲清楚这类 dev harness 到底怎么搭、跑起来有哪些边界、遇到问题从哪排查。
先给一个总体判断:这类“自我构建”工具链的核心不在模型多聪明,而在验证循环多便宜。Ducklab 的 416 次运行能控制在 176 美元,说明它把单次运行的边际成本压到了很低,低到可以不断试错。本地模型在这里不是噱头,而是成本控制和数据隐私控制的关键。下面我按实际落地顺序,把这个思路拆成可执行的方案。
1. 先确认它解决的到底是开发验证问题,还是“自动写代码”问题
很多人看到“built itself”容易往“模型自动写整个软件”方向理解,这个方向其实偏了。把 416 次运行和“自我构建”组合在一起看,它更接近一个开发验证问题:让模型在每一次运行里承担一部分代码生成、修改、修错的职责,然后由测试命令判断结果是否可用。和纯聊天式 AI 编程不同,这种 harness 有一个明确的闭环。
1.1 dev harness 到底是个什么东西
dev harness 可以理解成开发阶段的脚手架。它负责把“开发任务”和“验证环境”捆绑在一起,典型组成包括:
- 一个任务来源:可以是 issue、任务描述、失败测试、TODO 列表。
- 一个工作区:实际存放代码的目录,通常是 git 仓库。
- 一组验证命令:构建脚本、单元测试、lint、集成测试。
- 日志采集:每次运行之后把 stdout、stderr、退出码、diff 保存下来。
- 反馈回路:把验证结果再次送入模型,让它调整下一步动作。
日常开发里,很多团队其实已经有一个简化版 harness:改完代码跑测试,测试挂了看日志,再改。Ducklab 这类项目的特殊性,是把“看日志—改代码—再跑”这个循环自动化,并让本地模型充当那个“改代码”的角色。
这解释了为什么它强调 416 次运行。手动改 416 次要耗费大量时间,机器跑 416 次测试则非常快。只要每次验证的脚本足够聚焦,这个循环就可以很廉价。
1.2 “built itself”不是玄学,而是一个生成—验证—修正循环
把“self-built”拆开看,本质是:
- 从任务列表取一个明确的开发任务。
- 把任务描述、相关代码、当前失败信息拼接成 prompt。
- 本地模型输出代码补丁或完整文件。
- 把补丁应用到工作区。
- 运行测试命令验证。
- 测试通过,提交这次变更;测试失败,回滚并记录失败信息。
- 把失败信息作为新上下文,进入下一轮。
整个过程中,模型只负责“生成候选方案”,真正决定能不能合入的是测试脚本。所以这个 harness 的核心是验证能力,不是生成能力。没有自动化测试覆盖的代码仓库,跑这类循环会非常不稳,因为模型改出来的东西可能测试通过了但功能不对,甚至测试本身没有覆盖关键逻辑。
1.3 416 次运行和 176 美元这两个数字意味着什么
这两个数字放在一起,信息量很大。按 176 美元除以 416 次计算,单次平均成本大约 0.42 美元。具体每次成本不同,但这个量级说明它没有用高价的云端大模型作为主力,而是靠本地模型把单次推理成本压了下来。
更重要的是 416 次这个样本量。“跑一次”不等于“成功一次”,真实项目中会有大量失败、回滚、重新生成。如果每次运行要花几美元,416 次会是一笔不小的开支;如果能压到几十美分,那这种试错策略就可持续。本地模型的价值在这里体现得最直接:它不按 token 计费,主要成本是电费、硬件折旧和运行维护时间。
需要提醒的是,416 次运行可能包含多种类型:生成代码、补丁失败、重试、成功提交、环境检查、回归验证。不要把它理解成“416 次都成功”。这种循环里,失败是正常状态,成功的标准是最后产生了可合入的变更。
2. 本地模型驱动的开发工具链,到底适合什么场景
不是所有编码任务都适合丢给本地模型。这个方案最适用的场景,往往同时具备三个特征:任务结果可以被程序判定、代码仓库有测试覆盖、模型需要访问的上下文不会超长。缺了任何一个,跑起来都会很别扭。
2.1 哪些任务适合交给本地模型
按我偏向的经验,适合先尝试的任务类型包括:
- 单元测试补全:已经有了待测函数,模型生成边界测试。
- 简单 bug 修复:测试报错信息明确,比如数组越界、空指针、函数签名对不上。
- 代码格式化或机械重构:变量重命名、函数提取、常量替换。
- 配置修复:依赖版本冲突、CI 脚本拼写错误。
- 文档与注释生成:结合代码结构生成说明,再通过 lint 或格式检查验证。
这些任务有一个共同点:成功与否有相对客观的判断标准。单元测试能过就是能过,修复配置后构建成功就是成功。模型不需要理解太多业务背景,只需要根据当前文件和错误信息调整。
不适合的任务也有明显特征。比如“把这个模块设计得更好”“优化老系统性能”“理解未文档化的业务规则”,这类目标很难用一个测试命令表达,模型生成结果后你无法自动判断正确性。勉强放进 harness,只能靠人工审核每个 diff,成本反而更高。
2.2 本地模型和云端模型的取舍
我在实际项目中的感受是,不要做“二选一”,而是做“任务分级”。本地模型适合高频、低风险、上下文可控的任务;云端大模型适合低频、复杂、需要大量常识或长上下文的任务。
本地模型的好处不是质量全场最优,而是三个点:
- 隐私好:代码不用离开本机,很多公司对这个有硬性要求。
- 成本稳定:跑多少次主要看电费和占用时间,没有突发的 token 账单。
- 离线可用:网络受限环境里也能持续跑。
劣势也很明显:推理速度慢、模型参数量有限、对长上下文处理容易丢信息。同样一个任务,云端模型可能一次就改对,本地模型可能要试五次。但只要测试脚本能拦住错误,多试几次的成本仍然可控。
更实际的策略是混合模式:harness 默认用本地模型跑验证循环,复杂任务或连续 N 次失败后,人工介入,或者把人切换到云端模型完成一次补丁,再回到本地模型继续循环。这样既控制成本,又保留兜底能力。
2.3 运行环境到底要准备什么
按照常见配置,本地模型跑代码生成至少要有这样几条:
| 环境项 | 起步要求 | 推荐配置 | 说明 |
|---|---|---|---|
| 操作系统 | Linux / macOS | Linux 服务器或开发机 | Windows 也能跑,但命令、路径、权限问题更多 |
| CPU | 4 核以上 | 8 核以上 | 加载模型、跑测试、处理日志都会占 CPU |
| 内存 | 16 GB | 32 GB 以上 | 模型推理和测试进程同时跑时会明显吃内存 |
| GPU | 可选 | NVIDIA 显卡,显存 8 GB 以上 | 没有 GPU 也能跑小模型,但很慢 |
| 磁盘 | 20 GB 空闲 | 50 GB 以上 | 模型权重、git 历史、日志都会累积 |
| 推理运行时 | 任选一种 | Ollama / llama.cpp / 其他兼容服务 | 要能通过接口或命令行向模型发请求 |
| 代码仓库 | git 仓库 | git 仓库 + 干净的基线 | 回滚和审计依赖 git |
| 验证命令 | 一条可重复运行的测试命令 | 秒级单测 + 分钟级回归 | 验证越慢,试错成本越高 |
比起具体型号,我更建议先确认测试命令的“成本”。如果测试命令要跑 10 分钟,哪怕模型单次生成只要 1 分钟,整个循环也会被拖慢到不可用。所以第一步不是选模型,而是把验证命令压缩到足够快,至少能接受连续跑几十次。
注意:这里不要一上来就把并发和参数量拉满,先用一个小模型、一条最快测试命令跑通全流程,再逐步放大。
3. 搭一个可复用的 self-building harness:核心循环怎么写
这类 harness 的本质是一个循环脚本。下面给出的是通用闭环设计,不是 Ducklab 的原始代码,但思路完全够用。
3.1 最小闭环:任务描述、生成代码、跑测试
先从一个最简单版本开始。流程是:
- 从 tasks 目录读取一个任务描述文件。
- 用模板拼 prompt,包括当前代码片段、任务描述、失败的测试输出。
- 调用本地模型接口,拿到补丁。
- 把补丁写入代码目录。
- 运行测试命令,收集退出码和输出。
- 通过则提交 git,不通过则 git 回滚。
- 把结果追加到日志,开始下一个任务或进入重试。
这个最小闭环看着简单,真正能跑起来要解决三件事:prompt 怎么拼、补丁怎么应用、失败信息怎么反馈。三者分别对应输入格式、变更应用、上下文管理。
3.2 让失败信息变成模型的下一步提示
很多自构建工具跑不好,不是因为模型弱,而是因为失败信息没有有效传回模型。典型错误是把整个终端输出塞进 prompt,模型看到几百行无关日志,反而找不到关键错误点。
更有效的方式是提取关键信号:
- 退出码:说明是测试失败、超时、还是崩溃。
- 第一处错误定位:文件名、行号、函数名。
- 断言信息:期望值和实际值。
- stderr 中包含的异常类型。
- diff 与最近一次变更的差异。
把这些信息按固定结构拼回 prompt,模型才知道“刚才你改了哪个文件,测试在哪里失败,现在需要调整什么方向”。我一般会在日志里保留完整输出,但只把关键片段送入上下文,避免上下文被无用日志撑爆。
3.3 用 git 做回滚和审计
没有 git 的 self-building 循环非常危险。模型连续修改代码后,如果每一轮都基于上一个失败状态叠加补丁,代码会迅速变得不可维护。正确做法是每一轮实验开始前,把当前 HEAD 记下来,应用补丁后如果验证失败,就立刻 reset 回基线。
审计也很有价值。每次运行至少应该记录:
- 运行编号和开始时间
- 使用的模型和后端
- 任务 ID
- prompt 的输入摘要
- 模型生成的补丁或代码
- 测试命令、退出码、关键输出
- 是否回滚
这些日志既是排查依据,也是成本分析的数据来源。没有日志,416 次运行结束后你只能看到一个结果,无法知道哪些任务反复失败、哪个模型配置更适合哪类问题。
3.4 一个最小示意脚本
下面给一个 Python 风格的逻辑框架,重点不是语法,而是循环结构。实际使用时要根据你的推理接口调整。
import subprocess import os repo_dir = "/path/to/repo" task_file = "/path/to/task.txt" model_url = "http://127.0.0.1:11434/api/generate" def call_model(prompt_text): # 示例:向本地模型服务发请求 # response = requests.post(model_url, json={"model": "local-model", "prompt": prompt_text}) # return response.json()["response"] return "生成的补丁内容" def apply_patch(patch_text): # 示例:将补丁写入文件或执行 git apply pass def run_tests(): result = subprocess.run(["pytest", "-q"], cwd=repo_dir, capture_output=True, text=True) return result.returncode, result.stdout[-500:] + result.stderr[-500:] max_runs = 5 for run in range(max_runs): task = open(task_file).read() prompt = build_prompt(task, current_code, last_error) # 需要另写 patch = call_model(prompt) apply_patch(patch) code, output = run_tests() log(run, code, output, patch) if code == 0: commit_changes("task finished") break else: rollback_to_head() last_error = extract_failure(output)这套逻辑跑通之后,再往里面加任务队列、模型切换、人工审批、并发控制都会容易很多。
4. 关键参数设置与成本控制:416 次、176 美元是怎么做到的
只看结果数字很容易忽略中间的过程约束。成本能压到 176 美元,通常不是靠某一个技巧,而是靠多个参数共同控制。下面拆开讲。
4.1 最大运行次数:任务粒度和预算的平衡
每一个开发任务不要无限重试。我给任务设上限时一般看两类指标:
- 任务复杂度:简单修变量名,3 次内不成功就停止;涉及多文件改动,可以到 10 次。
- 单次运行成本:如果测试很重,重试次数应减少;如果模型很快、测试秒级,可以适当放宽。
416 次这个总数如果是多个任务累计出来的,那单个任务平均运行次数可能并不高。按经验,一个任务反复重试超过 5 次后,边际收益会明显下降,因为模型往往在同一个错误点打转。与其让它继续耗,不如换个模型、换个任务描述,或者人工看一眼。
4.2 单次成本怎么算
本地模型的成本不是零。要准确核算,至少要把这几项加进去:
- 电力成本:GPU 满载和待机功耗差异很大。
- 硬件折旧:显卡、内存、硬盘按使用年限分摊。
- 人工维护时间:调 prompt、清理日志、处理环境问题都算成本。
- 测试耗时占用:测试跑得越久,占用开发机器的时间越多。
如果按“176 美元 / 416 次”这个平均思路,单个任务重试超过一定次数后,成本贡献就会放大。控制成本的第一步就是记录每次运行的耗时和资源占用,没有数据就谈不上优化。
4.3 并发、重试和超时
本地模型推理并行处理时,速度不一定是线性提升。多个进程同时向同一个模型服务发请求,可能因为显存或内存不够而互相等待,甚至导致 OOM。我自己习惯先跑单任务,稳定后再尝试 2 个并发,观察资源占用,再决定是否加。
超时设置也很关键。模型生成可能卡住,测试命令也可能因为资源竞争而挂起。一定要给模型请求和测试命令分别设置超时时间,比如模型生成 120 秒无响应就中断重试,测试命令按正常耗时的 2 到 3 倍设置上限。
重试策略同样要区分。模型调用失败可以重试,测试失败不要盲目重试。测试失败带回滚,模型调用失败只需要重新调用,两者性质完全不同。
4.4 控制成本的小技巧
从实际运行经验看,下面几个点对成本影响很大:
- 模型常驻加载,不重复加载权重。每启动一次推理服务都要重新读模型文件,时间和电费都不少。
- 测试命令优先跑针对性测试,不要每次都跑全量。先一条相关用例验证,通过后再跑完整回归。
- 只保留最近 N 条日志关键行,不要把整个输出写入 prompt。
- 代码变更尽量用 diff 或补丁方式,不要让模型输出完整文件,完整文件容易把无关内容改坏。
- 定期清理临时文件、模型缓存和旧日志,避免磁盘和内存被垃圾占满。
- 同一批任务尽量按类型分组,避免频繁切换模型和任务上下文。
建议:第一轮先跑 20 到 30 次小任务,记录平均耗时和单次成本,再决定要不要大规模推广。这样比直接放 400 次更稳。
5. 结果怎么判断:不能只看“测试通过”
自构建 harness 里最迷惑人的情况是测试显示绿色,但代码实际是错的。比如模型删掉了某个关键分支、改了配置后测试绕过了真实路径、或者补丁本身没生效但测试恰好通过。所以结果判断要从多个维度看。
5.1 一条运行算成功的标准是什么
我的判断顺序是这样的:
- 测试命令退出码为 0。
- 补丁确实被应用到了预期文件,不是空操作。
- diff 中没有意外删除的功能代码。
- 测试的输出里有真实执行了相关用例,而不是 skipped 或 no tests ran。
- 变更可以通过 git diff 审查。
第五点最容易被忽略。模型生成的补丁即使通过测试,也可能包含大量无关改动。harness 可以自动合入“测试通过”的补丁,但人工审计仍是必要的。尤其在没有测试覆盖的老代码上,这种风险会放大。
5.2 看日志不能只看最终结果
单次运行的成功和失败只是最粗粒度信息。真正有价值的是过程数据。每次运行都应该能回答这几类问题:
- 这一轮改了哪些文件、哪些行?
- 测试失败时模型拿到了什么反馈?
- 模型是第一次就成功,还是尝试了五次?
- 失败原因集中在语法错误、逻辑错误、还是超时?
- 哪些任务反复回滚,消耗了多少成本?
把这些日志汇总后,你会发现自己能针对性地优化 prompt、调整模型、裁剪任务范围。没有过程数据的 harness,等于把每一次试错的经验都丢了。
5.3 常见失败模式与识别方法
第一类是模型输出不完整。常见表现是补丁文件被截断、括号不闭合、或者只输出了说明文字没有代码。这类问题可以在应用补丁前先做基础语法检查,比如用 Python 的 compile 检查 .py 文件,用 prettier 或 eslint 检查前端代码。尽早拦截,避免把坏补丁带入测试。
第二类是测试不稳定。测试本身有时序依赖、端口冲突、网络请求超时,会导致同样的代码这次过、下次挂。如果发现同一份代码反复出现“一次成功一次失败”,优先怀疑测试环境,不要反复让模型修代码。
第三类是环境不干净。上一轮任务留下的临时文件、数据库状态、缓存,会影响下一轮测试结果。每次运行前把工作区清理到基线状态,能减少大量假失败。
6. 排查链路:卡住、乱改、反复失败从哪里查起
任何自动化工具跑久了都会出问题。最有效的排查方式不是先看模型,而是按“现象—输入—环境—参数—工具”的顺序逐层收紧。
6.1 先给现象分类
我看到现象时会先分四类:
- 完全没有输出:可能是模型调用失败、服务没启动、请求格式错误。
- 测试失败:这是最常见的,说明补丁被应用了,但没解决问题。
- 测试通过但 diff 异常:说明生成逻辑偏离任务,需要检查 prompt 和上下文。
- 进程卡死:多半是资源问题或超时没生效。
现象不同,排查起点完全不同。如果卡死,不要反复检查 prompt,先看进程资源占用。
6.2 按顺序排查:输入、环境、参数、工具
具体顺序可以参考下这一套:
- 看运行日志和退出码,确认失败发生在生成阶段、应用阶段、还是测试阶段。
- 看输入任务是否清晰,上下文是否包含正确文件,模型是否读到了最新代码。
- 看测试环境是否干净:依赖版本是否固定,是否有残留进程,磁盘是否写满。
- 看参数是否合理:最大重试次数、超时、并发、temperature、上下文窗口。
- 最后才看模型本身:量化等级、后端配置、是否被其他任务抢占。
大多数“模型乱改代码”的问题,最后查出来都是 prompt 里给的信息不完整。比如只给了函数名,没给函数签名和预期输入输出,模型只能猜。
6.3 几个容易踩的坑
| 现象 | 常见原因 | 排查优先级 |
|---|---|---|
| 模型反复修同一个错误 | 失败信息没完整进 prompt,或日志被截断 | 高 |
| 测试有时过有时挂 | 测试存在时序依赖或端口冲突 | 高 |
| 补丁不能正常 apply | 模型输出带了 Markdown 代码围栏 | 中 |
| git 回滚后代码还是乱 | 没有记录实验前的 HEAD | 高 |
| 模型请求超时 | 模型服务负载过高 | 中 |
| 内存持续上涨 | 日志未清理、模型频繁加载 | 中 |
补丁 apply 失败是很典型的问题。很多本地模型喜欢在回复里写 ```python 代码块围栏,如果脚本没有剥离围栏,直接写文件就会混入多余字符。我自己会在应用补丁前先做一个清洗步骤:删除代码围栏、删除“这是修改后的代码”一类说明文字,再尝试 apply。
另一个容易忽略的是路径权限。如果 harness 以服务方式运行,工作目录权限不够,模型生成的代码写入失败,但测试用的又是旧文件,就会出现“补丁没有生效但测试通过”的假象。排查时先确认脚本工作的用户和目录权限。
7. 实际落地后的一些判断和边界
把整个思路整理下来,我认为 Ducklab 这类项目最有价值的参考点,不是“本地模型能写代码”,而是“让机器自己迭代代码的验证循环可以做到多便宜”。这个方向对很多团队是有启发意义的,但也要看清边界。
7.1 不要对本地模型抱有不切实际的期待
我建议把本地模型当成一个“愿意反复试错的初级工程师”,而不是“资深架构师”。它能根据错误信息调整代码,但容易忽略大范围上下文,也缺少长期记忆。你的 harness 设计得越好,它犯错的成本就越低;反过来,如果指望模型一次写好,本地模型很容易让人失望。
所以真正值得花时间的,是完善测试断言、优化失败信息、控制补丁范围。这些工作能让“笨一点的模型”也产出可用的结果。416 次运行之所以能产生可接受的结果,大概率不是模型每次都很准,而是验证和回滚机制兜住了错误。
7.2 这个方案适合谁,不适合谁
适合的团队通常具备这些条件:
- 核心代码有自动化测试覆盖,测试命令可以快速执行。
- 大量任务属于重复性修改,比如补测试、修告警、做机械重构。
- 数据隐私要求高,不希望代码发送到云端。
- 有预算约束或希望成本可预测。
不适合的情况也很明确:
- 项目几乎没有测试,模型改完无法自动验证。
- 业务逻辑高度依赖团队内部知识和历史决策。
- 任务描述无法量化,只能靠人工看结果。
如果你所在的项目还在积累测试覆盖阶段,不要急着搭这种自构建工具。先把几个核心模块的测试补齐,再让模型参与,否则它只能帮你制造更多待审阅的 diff。
7.3 下一步可以怎么扩展
跑通最小闭环后,有几个方向可以继续做:
- 加任务队列:从文件清单变成支持动态追加任务。
- 接入多模型:本地模型失败后自动切换到更复杂的模型作为兜底。
- 增加人工审批:模型生成的 diff 先暂存,人工确认后再合入。
- 输出可视化:把每次运行的耗时、成本、失败原因汇总成报告。
- 加失败聚类:把相同错误信息归为一类,避免同一个问题反复消耗预算。
更进阶的方向是让 harness 不仅能修 bug,还能自己补充测试用例、评估补丁质量、回滚异常提交。这些功能都会让“自我构建”更接近真实开发流程,但每加一层,复杂度也会上升。
我个人更建议先把单任务跑稳,再考虑批量和扩展。踩过几次教训后你会发现,这类项目真正决定成败的,往往不是模型选得多新,而是任务描述、验证命令、日志和回滚策略有没有组织好。把这几样做扎实,416 次运行和 176 美元这样的结果,就不只是运气,而是可以被复制的工作方式。
