ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

只改Harness不换模型:提升LLM编码能力的工程实践

只改Harness不换模型:提升LLM编码能力的工程实践 在实际的 LLM Coding 评测和 Agent 开发中有一个很反直觉的现象模型权重完全没动提示词也没改只是把模型外层的执行脚手架从“单次生成答案”改成“生成代码 - 运行测试 - 返回报错 - 让模型修改”的闭环许多不同模型的编码指标会同时上升。标题里的 “Only the harness changed” 说的就是这个场景。harness 在这里不是模型参数也不是 prompt 模板而是包裹在模型之外负责输入构造、工具调用、代码执行、结果反馈和重试策略的一整套工程系统。这篇文章会围绕 harness 展开先说明它是什么为什么能影响 15 个模型的编码表现再一步步实现一个最小可复现的 coding harness并用它批量评估多个 LLM。适合正在做模型评测、编码 Agent、AI 编程工具链的工程师阅读。读完以后你可以把这套方案复制到自己的项目里用同一套 harness 横向对比不同模型也能理解为什么“换模型不如换 harness”。1. Harness 到底是什么模型之外的执行与评测系统1.1 从一个“模型集体变强”的现象说起如果只看结果很多人会以为某个新版本模型“一夜之间变强了”。但标题里的现象并不是模型参数升级而是推理时的运行方式变了。可以这样理解同样是做一张数学卷子之前要求直接写答案现在允许在草稿纸上演算、检查、改正最后再上交。草稿纸没有改变考生大脑里的知识但确实提高了最终得分。LLM 也一样。同一个模型在同样的权重下如果外部系统允许它先执行代码、看到报错、再修改答案它的 coding 指标就可能明显提升。这就是 harness 的作用它不是一个更大的模型也不是更长的 prompt而是把“推理一次”扩展成“推理、执行、反馈、再推理”的完整循环。这里要特别说明不同评测集、不同模型供应商、不同任务难度下harness 带来的提升幅度并不一样。不要把这个现象理解成“任何模型都能被 harness 救活”。模型本身的能力仍然是上限harness 负责把已经存在但未被充分利用的能力释放出来。1.2 Harness 在 LLM Coding 场景中的技术定位在 LLM coding 场景里harness 通常包含几个核心部分输入构造把题目、代码模板、历史错误、测试结果组装成模型能理解的上下文。动作执行让模型有机会运行代码、执行命令、读取文件而不是只输出静态文本。环境反馈把执行后的 stdout、stderr、退出码、测试断言结果返回给模型。迭代控制决定最大尝试次数、何时停止、何时切换策略。结果评估用隐藏测试、静态检查、人工规则判断最终答案是否真正正确。模型、prompt、harness 是三个不同层面的东西。用一张表可以看得更清楚层面决定因素典型改动影响范围模型权重和架构换模型、微调基础语言理解、代码生成能力Prompt输入文本和示例改任务描述、Few-shot 示例模型对当前任务的理解方式Harness外部执行和反馈循环增加测试执行、工具调用、重试模型能否利用执行结果自我修正很多团队在优化 coding 效果时第一反应是换更大模型或写更多 prompt 示例。实际上如果模型生成的代码经常只差一个边界条件harness 里加一轮“跑测试再修复”通常更便宜、更稳定。1.3 Harness 与 Agent 的关系Agent 是 harness 的高级形态但二者不是一回事。最小可用的 coding harness 可以只有四步生成、执行、反馈、重试。Agent 则在此基础上增加了更复杂的规划、记忆、多工具选择和长期目标维护。换句话说Agent 框架本质上是一套加强版 harness。理解 harness 的底层逻辑再去看 LangChain、自研 Agent、MCP 等工具时会更容易抓住核心它们都在解决“模型如何观察外部世界、如何行动、如何根据反馈调整”的问题。所以这篇文章先从一个最小 harness 开始。它不需要复杂的 Agent 框架也能实验出“只改 harness 就能提升多个模型”的效果。2. 为什么只改 Harness 就能提升多个 LLM 的编码能力2.1 单次生成的盲区模型看不到运行结果不接 harness 时LLM 编码是单向过程用户给题目模型给代码结束。如果代码有语法错误、边界条件错误、逻辑缺陷模型完全没有机会看见。它只能依赖训练时见过的相似问题模式来推断。这种模式适合“短答案生成”但不适合“程序必须可运行”的场景。一个函数能否通过所有测试用例不取决于生成时看起来是否合理而取决于在真实 Python 环境中的执行结果。模型无法运行代码就无法获得这一关键信息。2.2 反馈回路让“试错”成为推理的一部分接入 harness 后模型的每次错误都不是终点而是下一步推理的输入。举个例子模型第一次生成def two_sum(nums, target): for i in range(len(nums)): for j in range(len(nums)): if i ! j and nums[i] nums[j] target: return [i, j]用样例测试[2, 7, 11, 15], 9可以通过但用边界测试[3, 2, 4], 6会失败因为正确答案是[1, 2]而双重循环可能先返回[0, 0]这种非法组合。如果不执行测试模型永远不知道这个问题。加了 harness 后反馈信息里会包含断言失败assert res [1, 2] failed: expected [1, 2], got [0, 0]模型看到这个反馈后通常会在下一次生成时修正逻辑让内层循环从i 1开始。这个过程非常接近人类工程师调试代码。2.3 工具调用扩展了模型的信息边界更强的 harness 不只是运行代码还可以让模型读取文件、执行 shell 命令、查询 API 文档、搜索代码仓库。模型训练数据里没有的依赖版本、内部 API 签名、最新框架用法都可以通过工具调用即时获取。这也解释了为什么 15 个模型都能从 harness 改动中受益它们不需要共享同一套秘密技巧只需要共用同一个能执行、能反馈、能搜索的外部系统。每个模型都会根据自己的能力从反馈中提取信息最终整体通过率上升。2.4 更合理的评判标准交付前先验证很多模型在 benchmark 上得分高但实际交付时仍然需要工程师修改。原因之一是评测只检查“模型输出是否看起来对”而不检查“代码是否真的能运行通过”。harness 改变了评判方式不是模型生成完就交卷而是生成后先进入沙箱执行测试测试通过后才算完成。如果失败就继续反馈和重试。这样最终产物是经过执行验证的代码而不是一段没有运行过的字符串。对于 Coding 类任务这种“执行优先”的评判思路比单纯比较生成文本相似度可靠得多。3. 动手实现一个最小可复现的 Coding Harness3.1 场景与工作机制我们要实现一个最小但有完整闭环的 harness。它接收一个编程题目和若干测试用例调用任意 OpenAI 兼容接口的 LLM 生成 Python 代码在本地沙箱中执行测试。如果测试失败就把错误信息返回给模型让模型继续修复最多重试若干次。技术栈选择Python 3.10 以上openaiPython SDK用于调用 OpenAI 兼容接口PyYAML用于读取配置文件subprocesstempfile用于隔离运行模型生成的代码学习环境下使用subprocess加超时已经足够。生产环境要换成 Docker 容器或云沙箱不要直接在本机执行不受信任的模型代码。3.2 环境准备与依赖安装先创建虚拟环境并安装依赖python -m venv .venv source .venv/bin/activate pip install openai pyyamlOpenAI 兼容接口的模型可以通过环境变量配置export OPENAI_API_KEY替换为你的 API Key export OPENAI_BASE_URLhttps://api.deepseek.com/v1这里使用OPENAI_BASE_URL的好处是可以平滑切换到任意兼容 OpenAI 协议的网关。模型名在配置文件中单独指定例如deepseek-chat、qwen-plus、gpt-4o-mini等。实际运行时要把密钥和域名替换成自己环境对应的值。3.3 数据模型与配置文件为了让评测可重复先用配置文件管理模型参数和 sandbox 参数# config.yaml model: deepseek-chat temperature: 0.2 max_tokens: 2048 max_attempts: 3 sandbox_timeout: 10 test_timeout: 5 truncate_feedback: 2000字段含义参数默认值示例作用modeldeepseek-chat指定要调用的模型名temperature0.2控制生成随机性越低越稳定max_tokens2048限制单次输出长度max_attempts3允许模型修复代码的最大轮数sandbox_timeout10每次运行测试脚本的超时时间单位秒truncate_feedback2000截断返回给模型的错误信息长度避免上下文膨胀任务文件用 JSON 保存题目和测试用例{ id: two-sum, instruction: 实现函数 two_sum(nums, target)返回两个下标组成的列表使两个数之和等于 target。, entry_point: two_sum, tests: [ {args: [[2, 7, 11, 15], 9], expected: [0, 1]}, {args: [[3, 2, 4], 6], expected: [1, 2]}, {args: [[0, 4, 3, 0], 0], expected: [0, 3]} ] }这里测试用例不仅仅是样例。[0, 4, 3, 0], 0这种边界测试会暴露“返回相同下标”的错误是评测 harness 必须覆盖的场景。3.4 核心代码实现先定义数据类# harness.py import argparse import json import os import subprocess import sys import tempfile import textwrap from dataclasses import dataclass from openai import OpenAI import yaml dataclass class TestCase: args: list expected: object dataclass class Task: id: str instruction: str entry_point: str tests: list[TestCase] dataclass class HarnessConfig: model: str temperature: float 0.2 max_tokens: int 2048 max_attempts: int 3 sandbox_timeout: int 10 test_timeout: int 5 truncate_feedback: int 2000加载任务和配置def load_task(path: str) - Task: with open(path, r, encodingutf-8) as f: data json.load(f) tests [TestCase(argst[args], expectedt[expected]) for t in data[tests]] return Task( iddata[id], instructiondata[instruction], entry_pointdata[entry_point], teststests, ) def load_config(path: str) - HarnessConfig: with open(path, r, encodingutf-8) as f: data yaml.safe_load(f) return HarnessConfig(**data)构造测试脚本。这里把用户代码和断言拼成一个独立脚本通过subprocess执行def build_test_script(user_code: str, task: Task) - str: parts [user_code, ] for idx, t in enumerate(task.tests): args_repr , .join(json.dumps(arg) for arg in t.args) expected_repr json.dumps(t.expected, ensure_asciiFalse) parts.append(fres {task.entry_point}({args_repr})) parts.append( fassert res {expected_repr}, fcase {idx} failed: expected {expected_repr}, got repr(res) ) parts.append(print(PASS)) parts.append(print(ALL_PASS)) return \n.join(parts) def run_tests(user_code: str, task: Task, timeout: int) - dict: test_script build_test_script(user_code, task) with tempfile.TemporaryDirectory() as tmpdir: try: proc subprocess.run( [sys.executable, -I, -c, test_script], capture_outputTrue, textTrue, timeouttimeout, cwdtmpdir, ) except subprocess.TimeoutExpired: return { passed: False, stdout: , stderr: TimeoutExpired: 代码运行超时, } if proc.returncode 0 and ALL_PASS in proc.stdout: return {passed: True, stdout: proc.stdout, stderr: proc.stderr} stderr proc.stderr or proc.stdout return {passed: False, stdout: proc.stdout, stderr: stderr}从模型输出中提取代码。为保证兼容性允许模型输出 Markdown 代码块import re def extract_code(raw_output: str) - str: pattern r(?:python)?\s*(.*?) match re.search(pattern, raw_output, re.S) if match: return match.group(1).strip() return raw_output.strip()核心重试循环def solve_task(client: OpenAI, config: HarnessConfig, task: Task) - dict: messages [ { role: system, content: 你是一个 Python 编程助手。只输出可运行的 Python 代码不要添加多余解释。, }, {role: user, content: task.instruction}, ] attempts [] for attempt in range(1, config.max_attempts 1): resp client.chat.completions.create( modelconfig.model, messagesmessages, temperatureconfig.temperature, max_tokensconfig.max_tokens, ) raw_output resp.choices[0].message.content or code extract_code(raw_output) result run_tests(code, task, config.sandbox_timeout) attempts.append( { attempt: attempt, code: code, passed: result[passed], stderr: result[stderr][-config.truncate_feedback :], } ) if result[passed]: return {status: solved, attempts: attempts, final_code: code} feedback f第 {attempt} 次运行失败请修复代码\n{result[stderr][-config.truncate_feedback:]} messages.append({role: assistant, content: code}) messages.append({role: user, content: feedback}) return {status: failed, attempts: attempts, final_code: None}最后是命令行入口def main(): parser argparse.ArgumentParser() parser.add_argument(--task, requiredTrue, help任务 JSON 文件路径) parser.add_argument(--config, defaultconfig.yaml, helpharness 配置文件路径) args parser.parse_args() config load_config(args.config) task load_task(args.task) client OpenAI( api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_BASE_URL), ) result solve_task(client, config, task) print(json.dumps(result, ensure_asciiFalse, indent2)) if __name__ __main__: main()这段代码有几个关键点需要解释模型输出先经过extract_code再进入沙箱执行避免模型在代码块外添加解释导致运行失败。每次失败后把模型自己的代码和报错一起追加到对话历史中让模型在同一上下文里继续修改。truncate_feedback防止巨型 traceback 撑爆上下文。-I参数让 Python 以隔离模式运行忽略当前 PYTHONPATH 影响降低环境干扰。3.5 运行与验证保存好config.yaml、task.json、harness.py后执行python harness.py --task task.json --config config.yaml如果一切正常会输出类似下面的 JSON{ status: solved, attempts: [ { attempt: 1, passed: false, stderr: assert res [1, 2] failed: expected [1, 2], got [0, 0] }, { attempt: 2, passed: true, stderr: } ], final_code: def two_sum(nums, target):\n seen {}\n for i, n in enumerate(nums):\n if target - n in seen:\n return [seen[target - n], i]\n seen[n] i\n }注意status是solved只代表模型通过了任务里配置的测试用例。它不能证明代码在所有情况下都正确。这个区分很重要后面专门讲。4. 用同一套 Harness 评估 15 个 LLM 的流程设计4.1 为什么必须使用同一套 Harness如果你要横向对比多个模型必须让 harness 完全一致。因为不同的重试次数、不同的测试用例、不同的沙箱超时都会直接影响最终得分。有些模型在 1 次尝试内表现不佳但如果在 3 次尝试中能通过最终排名就会不一样。“只改 harness 就提升 15 个 LLM”的场景里提升幅度并不只属于某一个模型。所有模型共享同一个改进后的执行反馈机制。如果你在评测时给 A 模型 5 次重试给 B 模型 1 次重试那结果没有可比性。统一内容至少包括同一个任务集和测试用例。同一个max_attempts。同一个temperature。同一个沙箱超时时间。同一个反馈截断长度。同一个系统提示词。任何一项不一致都应视为不同 harness而不是不同模型的真实对比。4.2 评测集与判定规则评测集不能只有公开样例还要包含隐藏测试用例。最简单的方式是像前面task.json一样把每个任务拆成“展示给模型的 instruction”和“模型看不到的 tests”两部分。模型自己不能提前看到全部测试否则它可能生成专门针对测试用例的硬编码。更完整的评测集应该覆盖基础功能正常输入。边界条件空数组、最小值、最大值、重复元素。异常输入不符合题目约束的数据是否被合理处理。性能约束大数据量下是否超时。不同难度的题目混合在一起能看出模型在不同复杂度下的表现差异。如果只有一个简单样例15 个模型可能全都满分无法区分 harness 的作用。4.3 批量评估与指标输出批量评估脚本可以循环遍历任务文件和模型列表。为了控制成本建议把配置参数单独抽取出来python evaluate.py \ --task-dir tasks/ \ --models deepseek-chat qwen-plus gpt-4o-mini \ --config config.yaml \ --concurrency 4 \ --output report.jsonl评估脚本内部可以这样组织def evaluate_one(client, config, task, model): result solve_task(client, config, task) return { task_id: task.id, model: model, status: result[status], attempt_count: len(result[attempts]), token_usage_estimate: estimate_tokens(result[attempts]), } def evaluate_all(client, config, tasks, models): records [] for model in models: for task in tasks: cfg HarnessConfig(**{**config.__dict__, model: model}) records.append(evaluate_one(client, cfg, task, model)) return records收集完记录后可以统计每个模型的首次通过率、最终通过率和平均尝试次数指标计算方式首次通过率第 1 次尝试即通过的任务数 / 总任务数最终通过率在max_attempts内通过的任务数 / 总任务数平均尝试次数所有已通过任务的尝试次数平均值平均耗时单任务从开始到结束的墙钟时间平均值注意这里展示的表格结构是通用指标设计不是任何特定模型的真实分数。你在自己的评测集上得到的结果可以作为内部选型依据。4.4 成本与并发控制批量评估 15 个模型时token 消耗会迅速增加。每个失败尝试都会额外消耗一次模型调用和一次反馈上下文。控制成本可以从几方面入手限制max_attempts一般 3 到 5 次足够。对相同请求做缓存相同模型、相同任务、相同重试不必重复调用。使用并发时控制请求速率避免触发限流。记录每次调用的 prompt token 和 completion token便于核算成本。并发不是越高越好。限流和超时会导致评测结果不稳定最终浪费更多重试。更稳妥的做法是先单线程跑通一个小样本确认流程稳定后再加大并发。5. Harness 关键参数与调优5.1 重试次数、温度与最大 token这几个参数直接影响评测结果和成本。参数取值范围参考调小的影响调大的影响max_attempts1 - 5成本低但模型没有自纠机会成本高可能在简单错误上反复打转temperature0 - 0.7输出稳定但容易重复相同错误探索更多但可能引入随机错误max_tokens512 - 4096代码较长时容易被截断输出更完整但可能生成多余内容sandbox_timeout5 - 30 秒快速失败但误杀复杂计算减少误杀但死循环会拖慢评测在实际调优中不要一开始就追求高重试次数。先把max_attempts设为 1看模型在不自纠时的基线水平再逐步增加到 3 和 5观察通过率提升和成本增长曲线。如果 3 次到 5 次的通过率提升很小说明继续增加重试的意义不大。5.2 测试用例生成策略测试用例来源有三种人工编写质量最高但覆盖不全成本高。从题目约束自动生成用枚举、随机、边界值生成覆盖性好。让另一个 LLM 生成速度快但可能出现错误断言。更推荐先人工写题目和核心边界再用脚本补充随机测试。模型生成的测试用例可以作为补充但必须经过人工审查。绝不能把 LLM 生成的测试直接当成 ground truth否则可能出现“错误测试认定正确代码”的情况。5.3 反馈信息的长度控制多轮重试时上下文会越来越长。如果每次都把完整 traceback 塞给模型几轮之后 prompt 会膨胀模型容易丢失最早的任务描述。推荐做法只保留最后一次异常的最后 1000 到 2000 字符。把重复出现的相同错误去重避免模型反复看同一个 traceback。在反馈中明确告诉模型当前是第几次尝试避免它生成“这是第一次”的回答。如果上下文仍然过长可以把历史反馈压缩成“之前尝试过的修改摘要”。5.4 沙箱安全与资源限制模型生成的代码不可信。学习环境里用subprocess和超时只是为了快速验证思路生产环境必须做更强的隔离。生产环境至少做到使用 Docker 容器运行模型代码限制 CPU、内存和网络。设置timeout和内存上限避免死循环和内存耗尽。模型代码没有网络访问权限除非任务明确需要。所有执行结果先落盘再决定是否返回给模型。不要用宿主机的高权限账号运行任意生成代码。注意如果你的 harness 会执行模型生成的代码安全性是第一优先级。宁可牺牲一点执行速度也不要让不可信代码直接接触宿主机。6. 常见问题与排错路径6.1 模型输出不是合法代码现象run_tests报错 NameError 或者无法解析。可能原因模型没有按“只输出代码”的要求返回。模型把代码放在 Markdown 代码块中但extract_code没匹配到。模型返回了 JSON 包装的字符串。检查方式打印raw_output确认模型实际返回内容。处理建议增强解析函数。支持去掉python标记如果模型返回 JSON先尝试解析code字段如果仍然失败可以让模型输出固定格式例如BEGIN_CODE ... END_CODE6.2 同一个错误反复重试现象max_attempts用完但每次错误相同。可能原因反馈信息被截断到完全没有错误细节。temperature0导致模型每次都输出相同结果。模型没有真正“看到”反馈因为消息顺序或角色设置错误。检查方式查看attempts中每一轮返回给模型的stderr。处理建议把temperature提高到 0.2 到 0.4确认反馈中保留最后一行错误类型和断言信息如果错误信息过长只保留后半段。6.3 公开测试通过但隐藏测试失败现象harness 输出solved但人工复测时发现代码不正确。可能原因测试用例太少模型恰好通过了所有用例。模型学会了针对测试样例硬编码。测试用例只有正常输入没有边界。检查方式把模型生成的final_code放到更大的隐藏测试集上执行。处理建议评测集必须分离公开样例和隐藏测试。最终指标以隐藏测试为准一次评测同时加入边界、随机、性能三类用例。6.4 API 调用超时或限流现象批量评估时大量请求报错。可能原因并发过高触发 API 限流。部分模型响应慢超过客户端默认超时。网络抖动。检查方式查看client.chat.completions.create抛出的异常类型和错误码。处理建议加入指数退避重试降低并发数为每次调用设置合理 timeout对相同请求做缓存避免重复调用。6.5 沙箱卡死与资源耗尽现象单个任务执行时间特别长甚至整机变卡。可能原因模型生成死循环代码。代码创建超大列表或递归无终止。subprocess的timeout没有覆盖到子进程内部再创建的子进程。检查方式检查 sandbox 日志确认是否出现TimeoutExpired。处理建议在subprocess.run中设置严格 timeout生产环境用 Docker 的--memory和--pids-limit限制资源不要相信模型生成的代码天然安全。排错路径可以按这个顺序走先检查输入任务文件、测试数据、配置格式。再检查解析模型输出是否正确提取。再检查执行沙箱能否运行最简单的脚本。再检查反馈模型是否收到了完整报错。再检查限制超时、token、并发是否导致假失败。最后检查评测集测试用例本身是否可靠。7. 生产环境 Harness 的最佳实践与扩展方向7.1 从评测脚本走向在线编码 Agent最小 harness 是评测工具但它的核心结构可以直接扩展成生产级的编码 Agent。区别在于生产系统需要更多工程能力支持多文件仓库模型可以读取目录、打开文件、修改文件。支持工具协议模型能调用格式检查、静态分析、测试运行、git 操作。支持持久记忆保存模型已经尝试过的方案避免重复犯错。支持人工介入在自动修复多次失败后把任务转给工程师处理。支持版本回滚模型对代码库的所有修改都记录 diff异常时可以回滚。这些能力本质上都是“在模型外面加 harness”。模型还是那个模型但外部系统越完善它能完成的任务越复杂。7.2 评测前检查清单把下面这份清单当作可复用模板每次开始新的评测前过一遍测试用例是否包含隐藏用例而不是只给模型公开样例。所有模型是否使用完全相同的max_attempts、温度、超时和系统提示词。反馈信息是否保留异常类型和关键断言而不是只返回“失败”。错误信息是否做了长度截断防止上下文膨胀。沙箱是否设置了超时、内存和 CPU 限制。是否记录每轮 attempt 的原始输出、最终代码和错误日志。是否固定依赖版本和模型版本保证结果可复现。是否使用passk多次采样而不是只跑一次。是否区分“通过公开测试”和“通过隐藏测试”两个指标。是否把随机种子、并发数、API 端点写入实验记录。这十项做完你的评测结果才有横向对比价值。7.3 扩展方向MCP、RAG 与更丰富的工具层再往前走harness 可以连接 MCP 协议、向量检索、内部文档库和外部 API。模型在编码时不再只依靠训练记忆而是能实时获取依赖文档、历史代码、团队规范和安全策略。例如harness 里增加一个search_code工具模型在写某个函数前先搜索项目里已有的相似实现再生成代码。另一个read_docs工具让模型在不确定某个 SDK 参数时先查官方文档。这些能力都会影响编码结果而它们仍然属于“模型之外的系统”。学习建议是先把这个最小 harness 跑通再逐步加入一个工具、一个记忆模块观察每次改动对多个模型的影响。你会慢慢理解为什么“只改 harness”能提升 15 个模型——因为 harness 改变的不是参数而是模型与外部世界互动的方式。这种工程能力比单纯追新模型版本更可控也更值得长期投入。
返回列表