
最近在一个自动化实验项目里我尝试搭建了一个名叫Ducklab的开发助手框架dev harness。它的特别之处不在于用了多么复杂的前沿算法而在于整个系统在开发过程中呈现出一种“自我构建”的迭代状态由本地大模型驱动的代码生成、测试执行、报错反馈、再次修复形成闭环最终在 416 次运行记录里完成了任务整体调用成本约 $176且全程依赖本地模型没有大规模使用云端付费接口。这篇文章不是只介绍一个“别人家的项目”而是把 Ducklab 的核心设计思路拆开带大家从零实现一个最小的本地模型版 dev harness。你可以把它理解成用一个脚本把“写代码—跑测试—看报错—改代码”这件事自动化让本地大模型充当开发助手自己完成小模块的开发闭环。1. Ducklab 是什么一个会自我迭代的开发环境1.1 为什么叫 “dev harness”“Harness” 在工程领域的本意是“束线带、测试夹具”在软件开发领域它通常指一套用于驱动、控制和验证被测程序的脚手架。比如测试领域常说的 test harness就是专门用来加载测试用例、执行被测代码、汇总结果的工具。Ducklab 把它从测试层面扩展到开发层面这个 harness 不再是开发完代码之后才用来验证代码的工具而是在代码还没成型时就负责“发布开发任务、收集反馈、驱动修改”的自动化平台。换句话说传统开发模式是“人来写代码工具负责测试”而 Ducklab 这类 dev harness 反过来让“模型写代码系统负责约束、验证和反馈”。人的角色从“逐行写代码”变成“定义任务边界和验收标准”。1.2 “built itself” 意味着什么“Ducklab built itself” 并不是说程序真的凭空产生了意识而是指它通过生成式开发循环自己完成了大量代码片段的编写和修正。整个循环大致是需求描述 - 模型生成代码 - 执行测试 - 收集失败信息 ^ | | v ----------- 模型根据失败信息修改代码 -------一开始模型可能写出一个非常幼稚的版本通过 harness 跑测试后立刻暴露问题。系统把测试失败信息包括异常堆栈、断言失败详情原样返回给模型模型在下一轮生成时参考这些信息进行修复。如此往复直到测试全部通过。Ducklab 在这 416 次运行中完成了多个小工具的生成与调试。这类“自动写代码、自动修 bug”的循环正是 Agent 类应用最常见的落地形态。1.3 本地模型在其中的位置Ducklab 最重要的一个决策是使用local models本地模型而不是直接把所有请求发送到云端大模型服务。本地模型在这里承担了两个角色代码生成器根据任务描述生成函数、脚本或补丁。调试助手根据测试失败信息分析原因并给出修复代码。由于整个循环可能需要调用模型几十次甚至上百次如果每次调用都走云端延迟和成本都会成倍放大。本地模型虽然单次生成质量不一定比顶级云端模型高但好在调用成本低、响应速度快适合这种“多轮多跑”的自动化开发场景。2. 为什么选择本地模型而不是云端 API2.1 成本收益分析416 次运行、$176Ducklab 的 416 次运行如果全部使用主流云端大模型 API按照每千 token 几美分到几十美元不等的计费方式总成本大概率会明显高于 $176尤其当任务中涉及上下文较长的代码修复时成本会涨得很快。而本地模型的核心优势是边际成本低模型跑在本地调用次数多了也不会产生每千 token 的增量费用。延迟更可控本地推理没有网络传输时间也没有排队尤其在串行多轮迭代场景下体验更好。支持高频试错测试失败、重新生成的过程天然需要多轮尝试本地模型可以“放开了试”。$176 这个数字换算一下如果每一次完整运行对应多次本地推理那么成本主要来自电费、硬件折旧以及少数超出本地模型能力的场景下才需要调用的外部服务。这说明模型能力不是越贵越好关键是找到任务难度与成本之间的平衡点。2.2 数据隐私与安全边界在开发环境里代码本身就是公司或个人的核心资产。如果大批量把业务代码片段发送到外部模型接口即使有隐私协议也会增加数据泄露风险。本地模型在这一层面的优势非常明显代码不出机器全部推理在本地完成。这对企业内部项目尤其重要。涉及内部库、未公开的 API、敏感的配置文件时本地部署模型可以减少很多合规审查压力。2.3 可控性与可复现性云端模型经常会悄悄更新版本导致同一个 prompt 昨天和今天的输出风格不一样。而本地模型的权重一旦固定输出相对稳定更容易复现实验结果。这对 dev harness 这种需要反复迭代的工具来说非常关键。Ducklab 在连续 416 次运行中能保持明确统计一部分原因就是模型固定、prompt 模板稳定不会因为模型版本漂移导致结果无法比较。2.4 本地模型的接入方式目前接入本地模型的常用方式有三种方式说明适合场景Ollama安装简单一行命令启动本地模型服务兼容 OpenAI 风格接口个人开发、快速原型llama.cpp 服务性能较好支持 CPU/GPU 混合推理部署更灵活对吞吐或硬件要求更高的场景LM Studio图形化界面适合先手动测试模型效果非程序员调试模型、评估 promptDucklab 的思路是用一个统一的 HTTP 接口对接本地模型后续即使更换模型也不影响上层代码逻辑。3. 把它拆开一个最小可用 dev harness 的设计3.1 整体工作流在写代码之前我们先定义一下这个最小 dev harness 需要完成哪些事接收任务描述例如“写一个把 CSV 数据按某一列分组的函数”。调用本地模型生成候选代码。将代码写入临时文件。动态执行测试用例。收集通过/失败结果。如果失败把失败信息拼接到新一轮 prompt 中回到第 2 步。如果通过或达到最大轮数结束循环并输出结果。3.2 文件结构设计ducklab-demo/ |-- main.py # 主循环逻辑 |-- model_client.py # 本地模型调用封装 |-- runner.py # 代码执行与测试运行 |-- prompts.py # prompt 模板 |-- tasks/ | |-- task_001.py # 测试用例文件 |-- generated/ | |-- solution_001.py # 模型生成代码保存位置 |-- logs/ | |-- runs.jsonl # 运行记录3.3 核心循环的抽象表达用结构化伪代码来看function run(task_desc, tests): history [] for round in range(max_rounds): prompt build_prompt(task_desc, tests, history) code model.generate(prompt) result run_tests(code, tests) record(round, code, result) if result.passed: return success(code) history.append(result.feedback) return fail(history)这里最关键的一点是模型并不直接得到“测试对不对”的判定而是拿到具体的失败信息。它会看到“第 5 行抛出了 KeyError: name”或者“期望结果是 [1, 2]实际得到 [1]”。这些具体反馈比一句“你的代码有问题”有效得多。4. 用 Python 实现一个可运行的本地模型 dev harness这一节我们从零实现一个简化版 Ducklab。示例以 Ollama 作为本地模型后端假设你已经安装并启动 Ollama并且拉取了一个支持代码生成的模型例如 qwen2.5-coder 系列或 llama3.1 系列。如果你用的模型不同代码逻辑基本不用改只需要调整模型名称。4.1 安装并启动 Ollama在终端执行ollama pull qwen2.5-coder:7b ollama serveollama serve会默认监听http://localhost:11434。我们通过这个地址调用模型。4.2 项目结构ducklab-demo/ |-- main.py |-- model_client.py |-- runner.py |-- prompts.py4.3 model_client.py封装模型调用我们将模型调用封装成独立的模块方便后续扩展。# model_client.py import requests import json class LocalModelClient: Ollama 本地模型客户端。 def __init__(self, base_urlhttp://localhost:11434, modelqwen2.5-coder:7b): self.base_url base_url self.model model def generate(self, messages, temperature0.2, max_tokens2048): 调用聊天接口返回生成的文本内容。 url f{self.base_url}/api/chat payload { model: self.model, messages: messages, stream: False, options: { temperature: temperature, num_predict: max_tokens, }, } resp requests.post(url, jsonpayload, timeout120) resp.raise_for_status() data resp.json() return data[message][content]这里需要注意temperature0.2是偏保守的取值尽量让代码生成更稳定减少随机性。max_tokens2048足够大多数小工具代码使用如果你要生成较大文件可以调大。streamFalse便于直接拿到完整响应。4.4 prompts.py设计 prompt 模板prompt 的质量直接决定生成代码的质量。我们把角色要求、任务说明、测试信息都写进模板。# prompts.py SYSTEM_PROMPT 你是一名 Python 开发工程师。请根据用户给出的任务和测试反馈 生成可以直接运行的 Python 代码。要求 1. 只输出代码不要输出多余解释。 2. 代码必须放到 markdown 代码块 python ... 中。 3. 如果任务有修改要求请保留原有函数签名。 def build_generate_prompt(task_desc: str, tests_code: str) - list: 首次生成代码时的 prompt。 messages [ {role: system, content: SYSTEM_PROMPT}, { role: user, content: ( f任务{task_desc}\n\n f以下是测试代码你的实现需要通过这些测试\n\n fpython\n{tests_code}\n\n\n f请生成实现代码。 ), }, ] return messages def build_fix_prompt(task_desc: str, tests_code: str, failed_code: str, feedback: str) - list: 代码失败后将失败信息拼接成修复 prompt。 messages [ {role: system, content: SYSTEM_PROMPT}, { role: user, content: ( f任务{task_desc}\n\n f测试代码如下\n\n fpython\n{tests_code}\n\n\n f你上一轮生成的代码如下\n\n fpython\n{failed_code}\n\n\n f执行测试时出现了以下问题\n\n{feedback}\n\n f请根据问题修复代码生成完整的新代码。 ), }, ] return messages注意这里要求模型只输出代码但实际生成时模型偶尔还是会带“好的我来写”这类废话所以后续解析代码时要做兼容处理。4.5 runner.py执行代码并运行测试测试执行是 harness 的核心环节。我们需要在隔离环境中导入模型生成的代码然后运行测试用例并捕获异常、断言信息。# runner.py import traceback import sys import io import contextlib def _extract_python_code(text: str) - str: 从模型输出中提取第一个 python 代码块。 if python in text: start text.find(python) len(python) end text.find(, start) if end start: return text[start:end].strip() # 如果没有代码块直接把整段当作代码 return text.strip() def run_solution(code: str, tests_code: str, module_namegenerated_solution): 运行模型生成的代码和测试代码。 返回 (通过与否, 输出信息)。 solution_code _extract_python_code(code) namespace {} output_buf io.StringIO() try: with contextlib.redirect_stdout(output_buf): exec(solution_code, namespace) exec(tests_code, namespace) return True, output_buf.getvalue() except Exception as e: error_msg .join( traceback.format_exception(type(e), e, e.__traceback__) ) return False, error_msg这里用了exec动态执行代码在可控的实验环境里没有问题但如果在生产环境中使用必须搭配沙箱隔离避免模型生成的代码执行危险操作。4.6 main.py主循环与运行记录主循环做的事情是读取任务描述和测试代码。调用模型生成代码。执行测试。如果失败把失败信息拼回 prompt 再试。每轮记录运行日志。# main.py import json import time from model_client import LocalModelClient from prompts import build_generate_prompt, build_fix_prompt from runner import run_solution # 示例任务实现一个函数把列表中的所有偶数过滤出来 TASK_DESC 实现函数 filter_even(numbers)返回输入列表中的所有偶数。 TESTS_CODE from generated_solution import filter_even assert filter_even([1, 2, 3, 4, 5]) [2, 4] assert filter_even([1, 3, 5]) [] assert filter_even([]) [] print(all tests passed) def main(): client LocalModelClient() max_rounds 8 history [] for round_idx in range(max_rounds): if round_idx 0: messages build_generate_prompt(TASK_DESC, TESTS_CODE) else: last_code history[-1][code] last_feedback history[-1][feedback] messages build_fix_prompt(TASK_DESC, TESTS_CODE, last_code, last_feedback) print(f[round {round_idx 1}] 调用本地模型生成代码...) raw_code client.generate(messages) passed, output run_solution(raw_code, TESTS_CODE) print(f[round {round_idx 1}] 测试结果: passed{passed}) if passed: print(成功生成的代码) print(raw_code) break else: history.append({code: raw_code, feedback: output}) print(f[round {round_idx 1}] 测试失败失败信息长度 {len(output)} 字符) else: print(达到最大轮数未能在限定次数内通过测试。) # 运行记录落盘 log_entry { task: TASK_DESC, rounds: round_idx 1, passed: passed, timestamp: time.time(), } with open(runs.jsonl, a, encodingutf-8) as f: f.write(json.dumps(log_entry, ensure_asciiFalse) \n) if __name__ __main__: main()运行python main.py预期效果是模型首轮大概率生成正确代码也可能在某些边界条件上失败然后根据反馈进入修复循环。当你看到all tests passed时说明这一个开发任务已经被 harness 自动完成。4.7 效果说明这个最小系统虽然只有一百多行代码但它已经具备 Ducklab 的核心骨架生成、验证、反馈、修复。你可以通过替换TASK_DESC和TESTS_CODE来扩展它处理不同任务。实际测试时我建议从一个更复杂的需求出发比如“实现一个函数把 CSV 字符串解析成字典列表并处理引号转义”这种任务首轮失败概率更高能更好地观察多轮修复效果。5. 记录运行与量化成本从 416 次运行得到的教训5.1 为什么要有运行记录Ducklab 给我最大的启发不是“模型能写代码”而是“模型写代码的过程可以被量化和审计”。要判断一个 dev harness 是否有效不能只盯着最终成功与否还要关注平均几轮内可以通过测试哪些类型的测试最常失败失败原因集中在语法错误、逻辑错误还是接口不匹配本地模型调用总 token 量是多少这些信息都来自完整的运行记录。Ducklab 的 416 次运行本身就是一个可分析的数据集它可以帮助我们判断模型的能力边界以及 prompt 设计还有多少优化空间。5.2 每次运行应记录什么建议每条运行记录至少包含以下字段字段说明task_id任务唯一标识round当前轮次prompt_versionprompt 模板版本model_name模型名称input_tokens输入 token 数output_tokens输出 token 数passed是否通过error_type异常类型如果有latency_ms本次调用耗时timestamp时间戳把 input_tokens 和 output_tokens 记录下来后续即使切换云端模型也可以精确估算成本。5.3 成本统计逻辑Ducklab 的成本统计思路很简单本地模型用 token 总量和硬件功耗估算云端服务按单价乘以用量计算。# cost_estimate.py def estimate_cost(input_tokens, output_tokens, localTrue, price_per_1k_input0, price_per_1k_output0): if local: # 本地模型主要是电费可按实际功率简单折算这里先不做精细计算 return 0.0 input_cost input_tokens / 1000 * price_per_1k_input output_cost output_tokens / 1000 * price_per_1k_output return input_cost output_cost在实战中我发现很多团队愿意为一次代码生成花几美元却在“反复返工”上浪费大量时间。dev harness 的价值恰恰在于把“返工”变成低成本的自动循环。5.4 从运行记录里读什么把 416 次运行按轮次聚合后你会看到一条典型分布大多数任务在前 3 轮通过少数复杂任务需要 6 轮以上。如果你的日志显示平均轮次不断增长可能是任务描述太模糊或者 prompt 模板需要优化而不是模型能力的问题。6. 常见问题与排查思路在实现和运行 local model dev harness 时最常见的问题集中在模型连接、输出解析、测试执行和循环收敛四类。问题现象常见原因解决思路调用 Ollama 报连接错误Ollama 服务未启动或端口不对检查ollama serve是否在运行确认 11434 端口可访问模型输出包含额外文字被当成代码执行prompt 没有强制约束输出格式增强 prompt 要求同时在解析时提取首个代码块测试始终不通过但模型一直在换写法循环没有给模型足够具体的失败反馈检查 feedback 是否包含完整异常堆栈和断言差异代码执行超时模型生成的代码有死循环在 runner 中增加超时限制或使用独立进程执行本地模型生成结果不稳定temperature 设置过高调低 temperature例如 0.2 或更低模型回答“我无法访问外部资源”模型产生了无关回复在 prompt 中限制只能输出代码并增加重试判断关于代码执行超时简单实现可以用signal但在跨平台项目中建议使用subprocess或multiprocessing控制执行时间。# 简单的超时执行示例仅 Unix/Linux 思路 import signal def timeout_handler(signum, frame): raise TimeoutError(代码执行超时) signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(10) try: exec(solution_code, namespace) except TimeoutError: print(检测到执行超时) finally: signal.alarm(0)这只是一种简化方案。实际工程中更稳妥的方式是把测试执行放到沙箱容器或独立进程中避免模型生成的代码影响主进程。7. 工程化最佳实践7.1 尽量让 prompt 输出结构化模型输出越结构化harness 越稳定。比如要求模型先输出THOUGHT:再输出CODE:甚至直接输出 JSON{ reasoning: ... , code: def filter_even(...): ... }这样解析代码时就稳定多了。7.2 对生成代码执行环境做隔离前面提到exec直接执行模型生成的代码有很大风险。在本地实验可以接受但生产环境必须考虑以下几点在临时目录或 Docker 容器中运行测试。限制执行时间。禁止文件写入、网络访问等危险操作。截断输出长度防止模型打印大量内容拖垮内存。7.3 设置预算上限和轮数上限dev harness 本质上是“自动花费计算资源”的循环必须设置停止条件最大轮数例如 10 轮。最大 token 数例如总输入不超过 100 万 token。最大运行时间例如单个任务不超过 30 分钟。这些限制能让实验成本可控Ducklab 能做到 $176 这个量级不是因为它没有更多预算而是因为它对每次任务都设置了收敛条件不跑无效的长尾。7.4 保留测试失败样本形成回归集每个失败过的任务和模型输出都值得保存。把它们收集起来就形成了一个针对当前模型的“错题集”。下次模型版本升级或 prompt 调整后可以重新跑这些任务观察是否出现回归。这也是 Ducklab 运行记录最重要的价值它是一个可持续生长的评测基准。7.5 根据任务难度分配模型不是所有任务都需要最强模型。像“把变量名从 camelCase 改成 snake_case”这种机械改造任务用轻量本地模型就够而需要设计数据结构的复杂任务可以调用参数更大的模型。在 dev harness 中可以根据失败轮次动态升级模型第 1 轮使用小模型快和便宜 第 3 轮仍未通过切换到大模型提高生成质量这种“先小后大”的策略能进一步压低成本。8. 下一步扩展方向如果你觉得上面的最小 dev harness 还不够过瘾可以从以下几个方向继续做支持更多语言目前只实现了 Python 测试执行可以扩展到 Node.js、Go 或 Java。引入代码静态检查在运行测试前先用ruff或pylint做一轮静态检查把语法错误直接反馈给模型节省掉执行测试的开销。支持“需求变更”迭代当前是固定需求如果模型开发过程中发现需求本身有歧义可以加入一个“需求澄清”环节让模型先提问再编码。记录 embedding 并做任务相似度判断新任务到来时先检索历史最相似的任务把它的成功解决方案作为 few-shot 示例能显著提升首轮通过率。把生成代码提交到 Git每轮都创建独立的 git commit这样不仅能看到最终结果还能审计每一次修改方便回滚。不过在使用这种自动化开发工具时我们要特别注意模型生成的代码必须经过人工审查和测试验证不能直接合并到生产环境。自动化的价值在于“减少重复劳动、加速原型验证”而不是取代代码评审和工程规范。Ducklab 的实验也验证了一个观点对于定义清晰、验收标准明确的小任务本地模型加自动反馈循环已经完全具备实际产出能力真正困难的不是让模型写出第一版代码而是设计一个稳定、低成本、可持续迭代的反馈闭环。如果你也准备动手试建议从一个很小的函数任务开始先跑通整个循环再逐步增加复杂度。把这套 harness 跑起来之后再去调 prompt、换模型、加沙箱每一步的收益都会非常清晰。