不微调模型也能提升LLM编程能力?Coding Harness设计实践全解析
最近和团队一起做了一次有意思的实验:我们在一个下午的时间里,没有微调任何模型,仅仅替换和改进了外围的 harness(编码执行框架),就让 15 个不同型号的 LLM 在编程任务上的表现明显提升。这个结果听起来有点“魔法”,但背后的原理非常朴素:对 Coding 任务来说,LLM 的权重只是表达能力的一半,另一半取决于我们怎么调用它、给它什么信息、如何验证它的输出,以及出错后怎么让它修正。
这篇文章就来完整复盘这次实践,讲清楚 LLM Coding Harness 到底是什么、为什么改 harness 能提升模型表现、一个可落地的 harness 应该包含哪些模块,并给出可以复制运行的示例代码和常见问题排查思路。
无论你是正在集成 AI Coding 能力的后端开发者,还是想深入理解“为什么同一个模型在不同工具里表现不一样”的 LLM 应用开发者,这篇文章都值得收藏。
1. Harness 是什么?为什么只改 Harness 就能提升编码能力
1.1 从一个观察说起
我们平时使用 ChatGPT、Claude 或者开源模型写代码时,会发现一个现象:同一道算法题,在 A 平台上模型能一次通过,在 B 平台上却反复报错,甚至给不出可用代码。很多人会以为这是“模型被平台限制了”,但实际上,更多时候是平台背后的 harness 不同。
Harness 直译为“线束、马具”,在 LLM 应用领域,我们通常把它理解为:包裹在模型输入输出之外的整套自动化流程。它负责把用户需求转换成模型能理解的 prompt,调用模型接口,拿到结果后做解析、过滤、执行、校验,如果不满足要求,再让模型重新尝试。
换句话说,模型负责“思考”,harness 负责“让思考有效发生”。
1.2 Harness 与模型权重的关系
我们需要先建立一个认知:模型参数在推理阶段是固定的,不会因为你写了一段漂亮的 prompt 而被改变。Harness 所做的一切,都是在模型外部做文章。
一个 Coding Harness 可能包含:
- 系统提示词设计与上下文管理;
- 算法题目或需求的结构化描述;
- 测试用例的生成与执行;
- 代码提取与格式清洗;
- 多轮工具调用与错误反馈;
- 超时控制、重试策略、成本控制。
这些环节互相配合,决定了同一个模型在具体编码任务上的上限。
1.3 为什么一个下午能提升 15 个模型的编程表现
这次实验的核心思路非常简单:我们并不是为每个模型单独调 prompt,而是打造了一套“通用增强型 harness”,再用同一个 harness 去接入多个模型。 因为大多数模型在基础代码生成上已经具备一定能力,以往的明显失败,往往是输在“没有测试反馈”“没有结构化输出约束”“不知道何时停止”这些问题上。
当我们补上这些机制后,所有模型的编程成功率都跟着上涨。这正是 harness 的价值:它不是银弹,但它能把模型已有的能力稳定地释放出来。
2. LLM Coding Harness 的核心组成部分
想要改进 harness,先要拆解它。一个完整的 Coding Harness 通常由以下模块构成:
2.1 模型调用层
模型调用层负责适配不同厂商的 API 或本地推理服务。它需要统一输入输出格式,屏蔽不同模型在参数命名、上下文长度、输出风格上的差异。
一个好的调用层至少要做到:
- 支持流式和非流式输出;
- 统一处理
temperature、max_tokens等采样参数; - 能接受 OpenAI-compatible 接口,方便切换模型;
- 对超时、限流、鉴权错误做统一处理。
2.2 Prompt 管理模块
Prompt 管理不是简单拼字符串,而是要解决以下问题:
- 如何让模型明确自己的角色是“资深工程师”而不是“通用助手”;
- 如何把需求、约束、示例、测试用例放进上下文;
- 历史对话过多时如何截断;
- 系统消息、用户消息、工具消息如何组织。
2.3 工具调用与结构化输出
在 Coding 场景中,模型往往需要调用外部工具,例如执行 Shell 命令、读取文件、运行单元测试。这要求 harness 支持 function calling 或 tool use 协议,并能安全地执行工具返回结果。
同时,模型给出的完整回复里通常混杂着解释性文字和代码块,harness 需要从文本中精确抽取代码,并转换为可执行对象。
2.4 执行沙箱
代码生成后必须被验证。执行沙箱是 harness 的核心模块之一,它提供了隔离的 Python/Node/Shell 运行环境,能安全地运行模型生成的代码,并捕获标准输出、标准错误、异常、运行时间等信息。
没有执行沙箱的 Coding Harness 是不完整的,因为“模型认为能跑”和“代码真的能跑”是两回事。
2.5 评估与反馈循环
最后一个关键模块是评估器。它根据测试用例判断模型输出是否正确,并把失败信息反馈给模型,让模型进行自我修复。
这个循环通常被称为 agent loop 或 self-refine loop。它会反复执行:生成代码 → 执行测试 → 分析失败原因 → 重新生成。循环次数通常受预算和延迟限制。
3. 环境准备与最小 Harness 示例
在动手改进 harness 之前,我们需要准备好环境,并先建立一个最朴素的“调用模型生成代码”的例子,这样才能直观看到问题点在哪。
3.1 环境说明
本文示例使用 Python 3.10+,需要安装以下依赖:
如果你使用的是国内云厂商提供的 OpenAI 兼容接口,通过 base_url 和 api_key 即可接入。建议准备一个可以访问的 LLM API(例如 Qwen、DeepSeek、GLM 等兼容接口),本文不限定具体厂商。
版本方面,OpenAI SDK 建议使用 1.x 及以上版本,因为 0.x 与 1.x 的 API 差异较大。示例代码以 1.x 为准。
3.2 项目结构示例
我们用一个简易的目录结构来演示:
后面每个模块都会逐步填充。
3.3 基础模型调用客户端
这里我们使用 OpenAI SDK 作为统一入口,只要目标服务提供 /chat/completions 接口,都能通过 base_url 适配。
3.4 最朴素的“生成代码”流程
这个流程能跑通,但它存在明显问题:
- 模型可能会同时输出解释和代码,代码格式不稳定;
- 没有自动提取代码块;
- 没有执行与测试,无法验证代码是否正确;
- 失败后没有反馈和修复机制。
这就是一个“裸调用”的 harness。接下来我们逐步对它进行增强。
4. 改进 Harness 的五个关键方向
4.1 优化系统提示词,让模型进入“工程师模式”
很多模型默认把自己当成“AI 助手”,回答问题时喜欢先解释一堆,再给代码,甚至只给片段。我们需要通过系统提示词,把模型的行为约束成“可直接执行代码的输出器”。
一个经过验证的系统提示词模板如下:
这里的关键是 “只输出代码” 和 “不要使用 input()”。很多模型在自动评测环境中喜欢生成 input(),导致测试挂起,这是评测失败的重灾区。
4.2 将问题转化为可验证的测试用例
提示词优化只能改善格式,真正决定模型输出正确与否的是测试用例。改进 harness 的一个重要方向是:在请求模型之前,准备好输入/输出验签逻辑。
例如,让模型实现一个函数,我们同时要求它回答一组输入输出对:
更常见的做法是,将测试用例直接写入 prompt:
这样做的好处是让模型在生成代码时就知道“边界条件应该是什么”,显著减少因误解题意导致的错误。
4.3 加入代码执行沙箱,给模型真实反馈
如果只是生成代码而不运行,harness 就是“盲人摸象”。我们这里用一个轻量级沙箱来执行模型输出,并捕获异常。
执行后,我们可以把 stdout/stderr 内容交给模型,让模型判断问题原因并重新生成。这就是“执行反馈”。
需要特别提醒:使用 subprocess 直接执行模型生成的代码风险很高,可能包含恶意操作或死循环。在本地实验时可以控制超时,但生产环境必须使用容器、云函数或其他隔离沙箱。
4.4 设计输出解析器,从杂讯中提取可用代码
模型很可能输出 Markdown 代码块,或者在开头写“好的,如下所示”。因此,我们需要一个健壮的代码提取器。
这里使用了非贪婪匹配,可以处理多代码块场景。如果提取失败,我们还可以调用 LLM 让模型只输出代码,但这一步通常不需要。
4.5 构建多轮自我修复循环
有了测试用例和沙箱反馈,就可以让模型在失败后自己看报错并修复。这是提升模型编码能力最重要的机制。
这个循环的本质是模拟真实工程师的工作流:写代码 → 跑测试 → 看报错 → 改代码。大部分模型在第一次生成时可能会有小错误,但只要能拿到错误信息,第二轮修复成功率会显著提高。
5. 完整实战:搭建一个能提升多模型表现的迷你 Harness
前面我们已经拆解了每个模块,这一节把它们组合起来,完成一个真正可以运行的完整示例。
5.1 创建项目与安装依赖
创建 requirements.txt:
5.2 编写完整代码
本示例中,所有代码文件需放在同一目录下。
先写入 llm_client.py,内容与 3.3 节一致。
再写入 sandbox.py,内容与 4.3 节一致。
再写入 prompts.py:
再写入 main.py:
5.3 运行与验证
将 base_url、api_key、model 替换为你的真实配置,运行:
预期输出会包含每一轮生成的代码,以及执行成功后的 stdout。由于示例中要求模型输出 print(fib(10)),最终 stdout 中会打印 55。
5.4 扩展到其他模型
因为我们的 LLMClient 使用 OpenAI-compatible 接口,所以它可以轻松接入其他模型。你只需要修改连接参数:
- 阿里云百炼 DashScope 兼容模式;
- DeepSeek 开放平台;
- 智谱开放平台;
- 本地部署的 vLLM、Ollama(需开启兼容 API)。
如果你要同时对比多个模型,可以建立一个模型名单循环调用:
这也是我们“一个下午提升 15 个 LLM”实验能够落地的原因:harness 与模型解耦,同样的机制可以复用到所有模型上。
6. 常见问题与排查思路
在实际改造 harness 的过程中,我们遇到了一些高频问题,整理如下:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 模型输出大量解释性文字,代码无法直接运行 | 系统提示词约束不足 | 在提示词中明确“只输出代码”,并使用解析器提取代码块 |
生成的代码包含 input() 导致执行挂起 |
模型没有理解自动评测环境 | 提示词中明确“不要使用 input(),通过函数参数接收输入” |
| 沙箱执行报错,但模型反馈后仍反复出现同样错误 | 反馈信息不够具体 | 将 stdout、stderr、异常堆栈完整返回给模型 |
| 模型在自修复循环中上下文越来越长,成本飙升 | 历史消息无截断机制 | 只保留最近 1-2 轮错误信息,或对历史做摘要 |
| 不同模型 API 格式不一致,无法接入统一 harness | 模型接口兼容性差 | 使用 OpenAI-compatible 客户端,或为每个模型封装 adapter |
| 本地运行模型生成代码时沙箱被恶意代码破坏 | 缺少隔离机制 | 使用 Docker、nsjail、云函数等安全沙箱 |
| 测试用例覆盖不全,代码虽然能运行但答案错误 | 评测器设计不充分 | 增加边界测试、随机测试、性能测试 |
| 多次调用后成本超预算 | 没有控制最大轮数和 token 消耗 | 设置 max_rounds、max_tokens,并在每次请求前估算成本 |
以“模型输出解释性文字”为例,排查顺序通常是:
- 查看原始返回内容,判断是 prompt 问题还是解析问题;
- 确认系统提示词是否能被模型遵循;
- 检查
extract_code_from_output是否适配该模型的代码块风格; - 如果模型不遵循“只输出代码”,尝试在其输出后追加“现在请只输出代码”等强约束消息;
- 若仍然失败,考虑使用超参调整,如降低 temperature。
7. 最佳实践与工程建议
7.1 Harness 与模型版本解耦
不要把 harness 写死成只适配某一个模型。建议在配置层用 JSON/YAML 维护不同模型的接入参数、提示词偏好、上下文长度。这样当模型升级或新增模型时,只需改配置,不用改代码。
7.2 用测试驱动方式设计提示词
提示词不是一次性写好的,它是随测试结果持续迭代的。每当你修改 harness,都应该用固定的评测集回归测试。没有评测集,任何 harness 改进都只能靠“感觉”。
7.3 安全边界必须前置
所有模型生成的代码都属于不可信任代码。生产环境务必做到:
- 使用隔离容器执行代码;
- 设置 CPU、内存、时间限制;
- 禁止网络访问或限制白名单;
- 禁止挂载宿主机敏感目录;
- 记录所有执行日志,便于审计。
7.4 控制成本与延迟
自修复循环每增加一轮,就会多一次模型调用。建议:
- 默认最多 2-3 轮修复;
- 首轮使用较高的 max_tokens 和适当的 temperature;
- 后续修复轮次可降低 max_tokens;
- 对不同难度题目动态调整修复预算;
- 对长时间不收敛的样本设置熔断。
7.5 保留中间结果与日志
每次都把原始输出、提取后的代码、执行结果、反馈消息记录到本地或日志系统。这在排查“为什么模型在 A 问题上表现差”时非常重要。建议日志字段至少包含:请求 ID、模型名、轮次、输入 prompt、输出内容、退出码、stderr、耗时。
7.6 框架选择要克制
现在市面上已经有许多 LLM 应用编排框架,例如 LangChain、LlamaIndex、Spring AI 等,热搜中也经常出现“LLM 编排框架”相关词。但并不是所有项目都需要引入完整框架。如果只是做 Coding Harness,使用轻量代码加统一客户端接口往往更可控、更好排查。只有当你需要复杂 Agent、多工具、长期记忆时,再考虑引入框架。
8. 总结与下一步学习路线
这次“只改 harness,不改模型”的实验,让我重新理解了 LLM 工程的本质:模型能力是基础,但交付质量是由系统设计决定的。 一个合格的 Coding Harness,应该像一位严格的代码评审员一样,持续约束、验证、反馈,让模型的能力真正落地到可运行的代码上。
通过本文的示例,你已经可以动手搭建一个包含模型调用、提示词约束、沙箱执行、失败反馈、自我修复的迷你 harness。下一步,建议按以下路线继续深入:
- 把单函数题目扩展到完整项目级任务,增加文件读写与多文件修改能力;
- 引入 function calling,让模型可以自主执行 Shell 命令;
- 接入 Docker 沙箱,让代码执行更安全;
- 构建一个自动化评测集,用 Pass@K 指标衡量 harness 改进效果;
- 尝试将 harness 封装成 HTTP 服务,集成到 CI/CD 流水线中。
如果你对某个具体模块感兴趣,比如“如何设计高质量测试用例”或“如何用 Docker 构建安全代码执行沙箱”,可以在评论区留言,后续会继续拆解。如果这篇文章对你有帮助,欢迎收藏备用,也欢迎把它分享给正在折腾 LLM Coding 的同事和朋友。