ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

揭秘Harness Engineering:不改模型也能让LLM编码能力大增?

揭秘Harness Engineering:不改模型也能让LLM编码能力大增? 过去两年里AI 编程类工具最大的认知误区之一就是很多人把“编程效果”等同于“模型本身”。模型换得更强、参数更大似乎就代表代码写得更好。但真实工程里你会遇到一个非常反直觉的现象同一个模型在 A 团队的 Agent 里表现得像高级工程师在 B 团队的 Agent 里却连“给函数加个参数”都会改错文件。这篇文章想聊的正是这类现象背后被反复提及、却又很少被讲透的概念——harness engineering。它对应的场景可以浓缩成一个英文标题We improved 15 LLMs at coding in one afternoon. Only the harness changed。翻译过来就是一个下午改进 15 个 LLM 的编码能力而且只改了 harness模型一个都没换。这篇文章不是要论证“某个工具比某个模型强”而是想拆开“harness”这个词讲清楚它到底由哪些工程组件构成为什么它能在不碰模型参数的情况下显著改变编码质量以及一个普通开发者如何为自己的项目搭建最小编码 harness。如果你正在做 AI Agent、AI 编程插件、代码评审机器人或者只是想把 LLM 接入自己的研发流程这篇文章会给你一个可以直接落地的框架。1. 一个下午、15 个 LLM变的是什么先回到这个标题本身。它之所以有传播力是因为它挑战了大家默认的因果链编码能力 模型能力。如果这个等式成立那么不改模型编码只能原地踏步。但“only the harness changed”恰恰说明模型能力只是结果的一部分甚至只是一小部分。从工程视角来看模型在编码任务里真正消费的输入不是“整个代码仓库”而是你喂给它的上下文。它真正执行的输出也不是“直接改好的代码”而是它说出来的、被你的程序解析并落到磁盘上的文本。那么模型最终能不能写对代码很大程度上取决于三个变量模型看到了什么上下文范围、文件内容、相关依赖、历史修改记录。模型被要求怎么行动输出 JSON 调用工具还是直接输出补丁还是自由对话。模型行动后得到了什么反馈编译错误、测试输出、lint 结果还是什么都没有。这三个变量都是 harness 的职责。所以“一个下午改进 15 个 LLM”并不是魔法。更合理的解释是这套 harness 重新设计了上下文收集、工具调用、错误反馈和重试策略而这些策略对 15 个不同的模型都有效。模型变了但 harness 的“环境红利”还在。反过来说如果你把同一个模型接到 15 套粗糙的 harness 里效果波动也会非常大。理解这件事对技术选型有直接影响。团队在为 AI 编程工具做模型选型时常常只比较“模型榜单”和“跑分”却忽略了同样的模型在自己的执行框架里能发挥几分。很多团队在纠结“要不要换更强的模型”之前其实应该先问一句我们的执行框架是不是已经拖累了当前模型2. 什么是 LLM Harness基础概念与核心原理“harness”这个词直译是“挽具”或“线束”在软件领域早期更多出现在测试领域比如“test harness”指一套准备数据、执行测试、收集结果的脚手架。到了 LLM 场景harness 的含义被扩展了它是模型之外的全部执行与交互系统。换一种更直白的说法harness 是模型的“外部大脑”。模型负责根据当前 token 序列生成下一个 token但“当前 token 序列”从哪里来、生成一段文本后接下来干什么、失败后怎么重来这些全部由 harness 决定。要理解 harness 和 prompt engineering 的区别可以用一个类比。提示工程像是“说服一个人”你尽量把话讲清楚把背景交代完整让对方一步到位。Harness 工程则像是“给一个人搭一套工作台”工具有序摆放图纸摊开在合适位置旁边有测量仪器做完一步马上有质检报告。前者依赖一次对话的质量后者依赖整套工作流程的可靠性。在编码场景里harness 至少包含以下层次上下文管理层决定哪些文件进 prompt、哪些不进文件怎么摘要代码怎么切片。工具调用层定义模型能调用的函数解析模型输出的结构化指令执行后把结果写回对话。迭代决策层第一次结果不满意怎么办测试失败怎么办是重试、改方案还是放弃。执行与回滚层修改文件前是否做快照能否一键回退代码合入前是否自动跑校验。观测与日志层每一轮模型输入、工具输出、中间结果是否被完整记录。这五个层次协同工作才能让一个基础模型稳定地完成“读代码 - 改代码 - 验证 - 修错”的循环。很多人把 coding agent 理解为“一个很会写代码的模型”但真实架构里模型只是循环里的一个环节。社区里经常提到的“DeepSeek harness”“codex harness”这类词其实也证明了同一趋势开发者开始围绕具体的编码 Agent 构建外挂的执行层。模型负责内容生成开源社区负责把上下文、工具、验收、重试这些环节做厚。这个分工以后会越来越清晰。3. Harness Engineering 的五个关键维度理解了 harness 的概念接下来要看它在编码场景中到底强在哪。以下五个维度是最值得投入精力的地方也是“只改 harness”就能看到明显变化的主要原因。3.1 上下文压缩与检索编码任务和普通问答任务最大的不同是代码仓库往往有大量上下文但模型窗口有限。把一个十万行仓库全部塞进 prompt 既不现实也浪费成本。较好的做法是做分层压缩先扫描仓库结构生成文件树和模块说明。根据任务关键词定位可能相关的文件和函数。对单个文件截取关键类和函数而不是整文件导入。对历史信息只保留最近几轮修改的 diff。这套逻辑和 RAG 非常像但它检索的不是泛泛的“知识”而是“与本次修改直接相关的代码范围”。一个编码 harness 的上下文质量可以直接决定模型会不会改错文件、会不会重复实现已有函数、会不会把依赖关系搞混。3.2 工具调用协议模型不直接操作终端和文件。它通过结构化输出把“执行哪个函数、传什么参数”表达出来。Harness 需要解析这种表达并安全地执行。目前比较常见的协议是 function calling。模型输出一个 JSON里面包含函数名和参数harness 验证后调用对应的工具。这个环节的难点在于容错模型可能输出非法 JSON、可能编造不存在的函数、可能传错参数类型。一套成熟的 harness必须能在这些错误发生时自动降级而不是让整个任务崩溃。3.3 多轮反馈机制模型第一次生成的代码很少能直接通过全部测试。更常见的路径是生成代码 - 编译失败 - 看到错误信息 - 修改代码 - 再跑测试 - 还有一部分不通过 - 再改。这个“反馈回路”是否顺畅是编码 harness 最核心的价值。许多模型单轮表现一般但只要能拿到准确的错误日志第二轮、第三轮的修复准确率会迅速上升。反之如果 harness 只会把代码写进文件然后就“等用户自己看”模型的纠错能力就完全发挥不出来。3.4 状态管理与回滚编码任务是有风险的。Agent 可能在改一个 bug 时顺手把另外两个函数改坏了。如果 harness 不管理代码状态开发者的工作区会快速失控。合理的状态管理至少包括修改前对目标文件做快照运行验收命令失败时恢复快照。这样即使模型连续多轮修改也能随时回到稳定版本。3.5 观测与可调试性最后一个是容易被忽略的维度。如果 harness 的每一步都不可见那么模型表现变差时你根本无法定位是上下文问题、工具调用问题、还是模型本身问题。好的 harness 会记录每一轮的完整 prompt、模型输出、工具执行结果、代码 diff、测试日志。这样你才能把一个失败的 case 回放并判断“换 harness 组件”还是“换模型”。4. 环境准备与前置条件在开始构建自己的 coding harness 之前先明确运行环境。以下是一个通用环境清单版本信息请以你实际使用的官方文档为准这里只展示通用思路。操作系统Linux / macOS 均可Windows 建议配合 WSL 或 Docker 环境。编程语言推荐 Python 3.10 及以上社区工具链完善。LLM API选择一个支持 function calling 或结构化输出的模型服务。代码仓库建议准备一个小的测试项目最好包含多个文件、若干测试用例。沙箱环境如果允许 Agent 执行任意命令建议使用 Docker 或受限容器隔离。如果你不确定从哪里开始可以先用一个很小的任务验证整套链路比如“在一个 Python 项目里新增一个函数并补充测试”。这个任务足够简单但能完整暴露 harness 的上下文、工具调用、反馈三个核心环节。5. 最小可用 Coding Harness完整示例下面给出一个极简但可运行的 coding harness 示例。它主要演示四个部分上下文压缩、工具调用协议、测试反馈回路、主循环入口。代码风格以“通用实现思路”为主具体方法名和参数以你使用的模型 SDK 为准。5.1 第一步上下文收集与代码摘要我们先实现一个函数它接收一个任务描述扫描仓库结构输出一个精简上下文。# file_path: harness/context.py import os from pathlib import Path def create_repo_context(repo_path: str, max_files: int 10) - str: 扫描仓库生成一个可放入 prompt 的精简上下文。 repo Path(repo_path) tree_lines [] file_contents [] for i, path in enumerate(repo.rglob(*)): if i max_files: break if path.is_file() and not path.name.startswith(.): tree_lines.append(f- {path.relative_to(repo)}) if path.suffix in {.py, .js, .ts, .java, .go, .rs, .md}: content path.read_text(encodingutf-8, errorsignore) if len(content) 800: content content[:800] \n# ... (truncated) file_contents.append(f### {path.relative_to(repo)}\n\n{content}\n) header ## 仓库文件结构\n \n.join(tree_lines) body \n\n.join(file_contents) return f{header}\n\n## 关键文件内容\n{body}这个函数会把仓库中最多 10 个文件生成摘要。真实项目中你通常需要结合 grep 工具、语义检索或 AST 分析来精准定位文件这里的最小实现只用于跑通链路。5.2 第二步模型调用与工具执行我们定义一个简单的工具集。为了让示例清晰只提供两个工具读取文件内容和执行指定命令。# file_path: harness/tools.py import subprocess from pathlib import Path def read_file(path: str) - str: 读取文本文件内容返回给模型继续分析。 p Path(path) if not p.exists(): return f错误文件不存在 {path} return p.read_text(encodingutf-8, errorsignore) def run_command(command: str, cwd: str) - str: 在指定目录下执行命令返回标准输出和错误信息。 try: result subprocess.run( command, shellTrue, cwdcwd, capture_outputTrue, textTrue, timeout30, ) output result.stdout[-2000:] \n result.stderr[-2000:] return output.strip() or 命令执行完成无输出。 except Exception as e: return f命令执行异常{e}这里的工具函数直接执行 shell 命令存在安全风险。生产环境请将命令限制在白名单中并在沙箱容器内运行。5.3 第三步测试反馈回路模型写完代码后harness 需要自动运行测试并把结果返回给模型。这部分逻辑可以封装成一个反馈函数。# file_path: harness/feedback.py import subprocess def run_tests(repo_path: str, test_command: str python -m pytest --tbshort) - str: 运行测试返回适合模型阅读的紧凑反馈。 result subprocess.run( test_command.split(), cwdrepo_path, capture_outputTrue, textTrue, timeout60, ) tail (result.stdout result.stderr)[-2000:] if result.returncode 0: return f测试全部通过。\n{tail} else: return f测试未通过返回码 {result.returncode}末尾输出如下\n{tail}这个反馈回路是“一个下午改进 15 个 LLM”的关键。模型第一次写代码可能出错但一旦把错误信息喂回去模型会自动调用修复流程。如果你的 harness 少了这一环模型能力一定会被严重低估。5.4 第四步主循环最后把各模块拼起来写入 main.py。# file_path: harness/main.py from context import create_repo_context from tools import read_file, run_command from feedback import run_tests TOOLS { read_file: lambda args: read_file(args[path]), run_command: lambda args: run_command(args[command], args[cwd]), } def main(): repo_path demo_project task 在 calculator.py 中新增一个 multiply 函数并在 test_calculator.py 中补充测试。 context create_repo_context(repo_path) # 构建初始消息把工具说明注入 system prompt system_prompt f你是一个编码 Agent。你可以使用以下工具 - read_file: 读取文件内容参数为 {{path: 文件路径}} - run_command: 运行命令参数为 {{command: 命令, cwd: 工作目录}} 请先阅读文件然后修改代码。修改完成后运行测试。最终输出不超过 2000 字。 {context} messages [{role: system, content: system_prompt}] for step in range(5): # 这一行需要替换成你真实使用的模型 SDK 调用 response call_llm(messages, toolsTOOLS.keys()) if response.get(tool_calls): for call in response[tool_calls]: tool_name call[name] args call[arguments] result TOOLS[tool_name](args) messages.append({role: tool, name: tool_name, content: result}) messages.append({role: assistant, content: response[text]}) else: # 模型认为任务已完成我们跑一遍测试验证 test_feedback run_tests(repo_path) messages.append({role: user, content: f请检查测试结果{test_feedback}}) if 测试全部通过 in test_feedback: print(任务完成全部测试通过。) break if step 4: print(达到最大迭代次数测试仍未通过。) break # 保存对话记录方便排查 import json with open(trace.json, w, encodingutf-8) as f: json.dump(messages, f, ensure_asciiFalse, indent2) if __name__ __main__: main()上面的call_llm函数是示意占位你需要替换为真实 SDK。例如 OpenAI 风格是openai.ChatCompletion.create也可以使用其他兼容服务。关键是保持“模型输出工具调用 - harness 执行 - 结果回填 - 再交给模型”这个循环。6. 运行结果与效果验证代码写完后怎么验证 harness 确实有效这里需要一套可持续复用的评估方法而不是拍脑袋看一两个案例。建议维护一份固定任务集至少包含 10 个不同类型的编码任务新增一个简单函数。修改函数签名并同步所有调用点。修复一个单元测试失败。重构一个重复代码块。给模块补充参数校验。在两个文件之间新增依赖。处理一个错误日志定位崩溃原因。迁移某个 API 的调用方式。评估时记录三个指标任务通过率最终测试是否全部通过。迭代轮数模型从开始到完成经过多少轮工具调用。修改正确性是否产生预期外的改动是否破坏了其他测试。运行方式可以用一个简单的 Bash 命令python -m harness.main --repo demo_project --task 新增 multiply 函数并补充测试对比“只换 harness”的效果时可以在同一模型、同一任务集下分别跑旧版配置和新版配置记录通过率。预期结果是新版 harness 的通过率明显更高且迭代轮数更少。如果效果不明显优先检查三个位置测试反馈是否真的被截取并回填给了模型模型是否看到了失败信息。上下文是否覆盖了所有需要修改的文件是不是只给了部分代码。工具调用失败时错误信息是否返回给了模型让模型自行修正。7. 常见问题与排查思路问题现象可能原因排查方式解决方案模型反复调用同一个工具始终不写代码上下文里缺少明确的终止条件查看对话历史确认是否只有工具结果没有要求模型输出总结在 system prompt 中强调“分析完成后必须输出最终代码”或限制单轮工具调用次数修改后测试全部通过但代码风格破坏缺少 lint 和格式化反馈检查测试阶段是否只跑了 pytest没有跑 ruff 或 prettier在验收命令中加入格式检查把 lint 失败信息一并回填给模型指令任务没理解反复改错文件上下文没有给出准确的文件路径检查模型收到的文件列表是否缺少完整路径在上下文中明确标注“你只能修改以下文件”工具调用返回非法 JSON模型结构化输出不稳定查看原始响应是否被截断或包含额外文本启用 JSON 模式或 function calling 严格模式解析失败时将原始输出作为错误信息重新提问上下文膨胀token 消耗过高每轮都重复全量仓库摘要对比首轮和后几轮的 prompt 大小对上下文做缓存把不变的部分只发送一次后续轮次只拼接增量生产环境中 Agent 执行了危险命令没有限制命令白名单检查工具层是否有 shellTrue 的任意命令接口使用白名单命令启用沙箱容器禁止网络请求和文件系统越权这里真正容易踩坑的地方是“测试通过但改动错误”。很多初次搭建 harness 的开发者只关注测试是否绿却忽略了模型可能删掉了不该删的注释或者改了无关模块的排版。解决方案是为每个任务记录 diff并要求模型在提交前输出变更清单。8. 最佳实践与工程建议把 harness 从“能跑”做到“生产可用”需要补充以下几层工程能力。8.1 可观测性优先每轮对话、每次工具调用、每次文件 diff都应该以结构化日志落盘。这样可以复现失败案例也可以用于后续评估。没有日志的 Agent 是不适合上生产的。8.2 控制工具权限编码 Agent 的工具不能等价于“本机终端”。应尽量拆分成只读工具和写工具只读工具负责搜索、查看、分析写工具负责修改文件、执行测试。对写工具设置独立审批或确认流程。命令执行要避免直接shellTrue尤其是当模型生成的命令可能包含拼接参数时。8.3 用快照做回滚在 Agent 开始修改前对目标分支打快照。一旦连续迭代失败可以快速恢复到初始状态。Git 的stash或独立分支都可以实现这一点。不要等代码被改乱再后悔。8.4 成本控制模型调用费用和 token 消耗是真实限制。可以启动上下文缓存、控制最大迭代次数、限制单轮工具结果长度并对长文件的读取做截断。任务完成后要日志记录总 token 消耗便于后续成本评估。8.5 建立稳定评估集团队如果要持续迭代 harness就必须形成一套固定任务集。任务集应该覆盖多语言、多文件、多种 bug 类型。评估任务集不能频繁修改否则你无法判断效果变化来自 harness 调整还是任务难度变化。8.6 共享配置而不是共享 prompt个人开发者的 prompt 经验难以在团队中直接复用。更有效的方式是把 harness 拆成配置文件上下文策略、工具地址、验收命令、最大迭代轮数都做成可配置项。这样团队可以共享一套工程基础设施而不是各自维护个性化提示词。9. 总结与后续学习方向回到标题里那 15 个 LLM。“一个下午改进 15 个模型”听起来像玄学但如果把“模型”重新理解为“模型 外部系统”这个结果就变得很自然。外部系统决定了模型看到什么、能做什么、怎么纠错它也直接决定了最终编码质量。这篇文章真正想表达的是在编码任务里模型是必要条件但不是充分条件。如果你手头的 AI 编程工具效果不佳先别急着换模型。先把上下文压缩、工具调用、反馈回路、快照回滚、日志观测这五件事做扎实。很多时候你能从“工具不太好用”变成“模型看起来很聪明”靠的就是 harness 层面的优化。你可以在自己的项目里做一个对比实验同一批编码任务先让模型“裸奔”再把它接进上面这套最小 harness你会发现通过率和稳定性有明显区别。做完这个实验也就理解了当前社区里“harness engineering”被反复提及的原因。下一步可以继续研究的方向包括更精确的代码检索、基于 AST 的上下文切片、多 Agent 协作框架、自动生成测试用例、以及针对特定语言和框架的校验工具。这些都是 harness 工程的延伸。编码 Agent 的未来大概率不是“哪个模型更强”而是“谁的系统能把模型能力包装得更好、更稳、更可控”。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进