🤖 Harness 工程:用 LLM as Judge + Best of N 打造自优化的 AI 代码生成流水线
大模型不是魔法,大模型是可以被工程化管理的生产力工具。这篇学习日志带你从零搭建一个"自己出题、自己答题、自己判分、自己挑选最优答案"的 Harness 流水线。
📖 目录
- [一、问题的起点:LLM 到底可不可靠?](#一、问题的起点:LLM 到底可不可靠? "#%E4%B8%80%E9%97%AE%E9%A2%98%E7%9A%84%E8%B5%B7%E7%82%B9llm-%E5%88%B0%E5%BA%95%E5%8F%AF%E4%B8%8D%E5%8F%AF%E9%9D%A0")
- [二、Harness 工程:像马具驭马一样驭模型](#二、Harness 工程:像马具驭马一样驭模型 "#%E4%BA%8Charness-%E5%B7%A5%E7%A8%8B%E5%83%8F%E9%A9%AC%E5%85%B7%E9%A9%AD%E9%A9%AC%E4%B8%80%E6%A0%B7%E9%A9%AD%E6%A8%A1%E5%9E%8B")
- 三、三大核心思想深度拆解
- [3.1 Best of N Sampling:用"题海战术"覆盖更优解](#3.1 Best of N Sampling:用"题海战术"覆盖更优解 "#31-best-of-n-sampling%E7%94%A8%E9%A2%98%E6%B5%B7%E6%88%98%E6%9C%AF%E8%A6%86%E7%9B%96%E6%9B%B4%E4%BC%98%E8%A7%A3")
- [3.2 LLM as Judge:让 AI 当评委,告别人工评估](#3.2 LLM as Judge:让 AI 当评委,告别人工评估 "#32-llm-as-judge%E8%AE%A9-ai-%E5%BD%93%E8%AF%84%E5%A7%94%E5%91%8A%E5%88%AB%E4%BA%BA%E5%B7%A5%E8%AF%84%E4%BC%B0")
- [3.3 Harness 流水线:生成 → 评估 → 择优的三段式引擎](#3.3 Harness 流水线:生成 → 评估 → 择优的三段式引擎 "#33-harness-%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%94%9F%E6%88%90--%E8%AF%84%E4%BC%B0--%E6%8B%A9%E4%BC%98%E7%9A%84%E4%B8%89%E6%AE%B5%E5%BC%8F%E5%BC%95%E6%93%8E")
- 四、完整代码逐行解析
- 五、运行过程与输出解读
- 六、为什么这套方案能"抗幻觉"?
- [七、ReAct 框架:Harness 背后的思维模型](#七、ReAct 框架:Harness 背后的思维模型 "#%E4%B8%83react-%E6%A1%86%E6%9E%B6harness-%E8%83%8C%E5%90%8E%E7%9A%84%E6%80%9D%E7%BB%B4%E6%A8%A1%E5%9E%8B")
- [八、进阶思考:从 Demo 到生产](#八、进阶思考:从 Demo 到生产 "#%E5%85%AB%E8%BF%9B%E9%98%B6%E6%80%9D%E8%80%83%E4%BB%8E-demo-%E5%88%B0%E7%94%9F%E4%BA%A7")
- 九、总结与展望
一、问题的起点:LLM 到底可不可靠?
如果你用过大模型写代码,你一定经历过这种时刻:
第一次生成:能用,但有 bug
第二次生成:没 bug 了,但用了过时的 API
第三次生成:完美!...等等,它把业务逻辑改了?
这就是 LLM 的 "不稳定性诅咒" ------同一个 prompt,每次返回的结果都可能天差地别。核心原因有三:
| 原因 | 说明 |
|---|---|
| 🎲 随机采样 | temperature > 0 时,模型在概率分布上采样,天然有波动 |
| 👻 幻觉(Hallucination) | 模型会自信满满地编造不存在的 API、库、参数 |
| 🧠 上下文盲区 | GPT 不知道你的真实运行环境、项目约定、团队规范 |
那么问题来了:能不能用工程化的手段,在模型输出之上再加一层"质量兜底"?
答案就是今天的主角------Harness 工程。
二、Harness 工程:像马具驭马一样驭模型
2.1 名字的由来
Harness 直译是"马具"------缰绳、马鞍、笼头这一套控制马匹的工具。一匹未经驯服的野马力量巨大但方向随机,而套上 Harness 之后,它就能在骑手的指令下稳定高效地完成任务。
LLM 就是那匹野马,Harness 工程就是那套马具。
2.2 Harness 的定义
┌──────────────────────────────────────────────────────┐
│ Harness 工程 │
│ │
│ 将 LLM 的 生成 → 自动评测 → 择优筛选 │
│ 串联成一个闭环的流水线编排框架, │
│ 在结构化流程中自动产出更高质量的结果 .│
│ │
└──────────────────────────────────────────────────────┘
它不是让模型变聪明,而是用流程设计来弥补模型的不可靠性。
三、三大核心思想深度拆解
本项目的 Harness 采用 LLM as Judge + Best of N Sampling 组合模式,包含三个不可分割的核心思想:
3.1 Best of N Sampling:用"题海战术"覆盖更优解
🤔 核心问题
一次 LLM 调用的结果质量波动太大,怎么办?
💡 Best of N 的思路
与其赌一次输出,不如并行生成 N 个候选答案,然后从里面选最好的。
css
传统模式(1 次调用):
Prompt ──→ [ LLM ] ──→ 1 个结果 ──→ 靠运气
Best of N(N 次并行调用):
┌─→ [ LLM 第 1 次 ] ──→ 候选 ①
Prompt ──∥──→ ├─→ [ LLM 第 2 次 ] ──→ 候选 ②
└─→ [ LLM 第 N 次 ] ──→ 候选 Ⓝ
↓
┌──── 选最优 ────┐
↓ ↓
自动评分 人工确认
🎯 为什么要并行?
- 覆盖概率空间:每次采样的随机路径不同,多试几次,碰到好答案的期望值更高
- 互补效应:候选 A 犯了错误 X,候选 B 犯了错误 Y,但最优的那一个可能两个都没犯
- 统计兜底:N 次采样 = N 次独立实验,极端差结果被稀释
本质上,Best of N 是用 算力换质量 ------一次调用省时但不省心,N 次并行调用看似"浪费",但换来的是输出的稳定性。
⚡ 关键取舍
| N 值 | 质量提升 | 成本 | 延迟 |
|---|---|---|---|
| 1 | 基准 | 1x | 最快 |
| 3 | 显著 | 3x | 取决于并发能力 |
| 5 | 边际递减开始 | 5x | 需要良好的并发 |
| 10+ | 收益很小 | 10x+ | 通常不划算 |
本 Demo 使用 N=3,是实践中非常常见的性价比之选。
3.2 LLM as Judge:让 AI 当评委,告别人工评估
🤔 核心问题
有了 N 个候选答案,"选最优"这一步谁来做?让人一个个看?那自动化的意义在哪?
💡 LLM as Judge 的思路
让另一个 LLM(或同一个 LLM 换角色)来当评委,给每个候选打分。
arduino
候选代码 ──→ [ LLM Judge ] ──→ 评分(0-10)
↑
"你是一个严格的代码评审,
请判断下面代码是否正确实现'数组去重函数'"
📐 评委 Prompt 的设计哲学
python
你是一个严格的代码评审,请判断下面代码是否正确实现"数组去重函数"
要求:
- 只返回一个数字评分(0-10) # ← 输出格式强约束
- 不要解释 # ← 防止废话污染解析
代码:
${code}
这个 Judge Prompt 有三个精妙之处:
| 设计要素 | 作用 |
|---|---|
| 角色设定("严格的代码评审") | 让 LLM 切换为"挑剔"模式,降低宽容度 |
| 输出格式约束(只返回数字) | 使评分可以被 parseFloat() 直接解析,无需后处理 |
| 禁止解释 | 避免 LLM 输出"8 分,但是..."这种无法解析的字符串 |
🔄 评估流程
javascript
async function evaluateAll(candidates) {
const results = [];
for (const code of candidates) {
const score = await judge(code); // LLM 打分
results.push({ code, score }); // 结构化存储
}
return results;
}
async function judge(code) {
const res = await askLLM(judgePrompt(code));
const score = parseFloat(res);
return isNaN(score) ? 0 : score; // 容错:非数字 → 0 分
}
💡 这里有个重要细节:
isNaN(score) ? 0 : score------即使 Judge 失手返回了非数字文本(幻觉!),系统也不会崩溃,而是给 0 分让它自然排在末尾。用工程手段兜底模型的不可靠,这正是 Harness 思想的精髓。
3.3 Harness 流水线:生成 → 评估 → 择优的三段式引擎
将上述两个能力串联起来,就形成了完整的 Harness 流水线:
css
阶段 1 阶段 2 阶段 3
┌────────────┐ ┌──────────┐ ┌──────────┐
│ G enerate │ ──→ │ E valuate│ ──→ │ B est │
│ 并行生成 │ │ AI 评分 │ │ 选择最优 │
└────────────┘ └──────────┘ └──────────┘
↓ ↓ ↓
N 个候选代码 N 个 (代码, 分) 最高分代码
三段式的代码表达:
javascript
async function harness(prompt) {
// 阶段 1:Generate ------ 并行生成多个候选
const candidates = await generateCandidates(prompt, 3);
// 阶段 2:Evaluate ------ LLM 逐个打分
const evaluated = await evaluateAll(candidates);
// 阶段 3:Best ------ 选最高分
const best = pickBest(evaluated);
return best.code;
}
🎯 这个三段式结构是 Harness 工程的最小可运行单元(MVU)。任何更复杂的 Harness 流水线(多轮迭代、反馈修正、A/B 对比)都是在这三个阶段的骨架上长出来的。
四、完整代码逐行解析
以下是从零搭建的完整 Harness 流水线,按模块逐一拆解:
4.0 基础设施:LLM 调用封装
javascript
import OpenAI from 'openai';
import { config } from 'dotenv';
config();
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL, // 指向阿里云 DashScope
});
const askLLM = async (prompt) => {
const res = await client.chat.completions.create({
model: process.env.MODEL_NAME, // qwen-plus
messages: [{ role: 'user', content: prompt }]
});
return res.choices[0].message.content;
};
| 要点 | 说明 |
|---|---|
| OpenAI 兼容 SDK | 使用 OpenAI SDK 但通过 baseURL 指向阿里云 DashScope |
| 模型选择 | qwen-plus,性价比高、响应快,适合做流水线中的"生成器"和"评委"两个角色 |
| 单函数封装 | askLLM 是整个系统的唯一 LLM 入口,便于后续替换模型或加日志/缓存 |
💡 这里用
.mjs后缀(ES Module),配合.env+dotenv管理 API Key,是一种干净且安全的工程习惯。
4.1 阶段一:Generate ------ 并行生成候选
javascript
const generateCandidates = (prompt, n = 3) => {
const tasks = Array.from({ length: n }, () => askLLM(prompt));
return Promise.all(tasks);
};
逐行拆解:
javascript
Array.from({ length: n }, () => askLLM(prompt))
// ↓
// 创建一个长度为 n 的数组,每个元素都是一个 askLLM(prompt) 的 Promise
Promise.all(tasks)
// ↓
// 让 n 个 LLM 请求同时发出,而非串行等待
| 为什么这样做 | 效果 |
|---|---|
Promise.all 并行请求 |
3 次调用 ≈ 1 次调用的时间(而非 3 倍) |
同样的 prompt |
靠 temperature(模型内部随机性)产生不同的候选 |
| 默认 N=3 | 88% 的场景下能找到显著优于单次调用的结果 |
4.2 阶段二:Evaluate ------ AI 评委打分
javascript
async function evaluateAll(candidates) {
const results = [];
for (const code of candidates) {
const score = await judge(code);
results.push({ code, score });
}
return results;
}
async function judge(code) {
const prompt = `
你是一个严格的代码评审,请判断下面代码是否正确实现"数组去重函数"
要求:
- 只返回一个数字评分(0-10)
- 不要解释
代码:
${code}
`;
const res = await askLLM(prompt);
const score = parseFloat(res);
return isNaN(score) ? 0 : score;
}
为什么 evaluateAll 用 for...of 串行而非 Promise.all 并行?
❌ 并行打分:同时发出 3 个评分请求
→ 评委同时看 3 份卷子 → 注意力分散 → 评分不够细致
✅ 串行打分:一次只评一个
→ 评委专注审阅每一份 → 评分更可靠
这是一个工程师式的取舍:生成阶段追求速度(并行),评估阶段追求质量(串行)。 用有限的 token 预算做最精准的判断。
4.3 阶段三:Best ------ 择优
javascript
function pickBest(results) {
return results.sort((a, b) => b.score - a.score)[0];
}
简单到只有一行------按评分降序排列,取第一个(最高分)。
但这句话背后藏着一个重要设计决策:Harness 只做"推优",不做"判断"。
如果最高分也只有 3 分呢?Harness 仍然会返回------它不替你做"这个结果能不能用"的决策。那个判断权留给调用者(人或上层的 Rule Engine)。
4.4 完整编排:harness 函数
javascript
async function harness(prompt) {
// ============ 阶段 1 ============
console.log('生成多个候选者....\n');
const candidates = await generateCandidates(prompt, 3);
console.log('候选结果:');
candidates.forEach((c, i) => {
console.log(`\n---- Candidate ${i + 1} ----\n${c}`);
});
// ============ 阶段 2 ============
console.log(`\n Evaluate Candidates...\n`);
const evaluated = await evaluateAll(candidates);
console.log('打分结果:');
evaluated.forEach((item, i) => {
console.log(`\n---- Candidate ${i + 1} ----\n${item.code}\n → ${item.score} 分`);
});
// ============ 阶段 3 ============
const best = pickBest(evaluated);
return best.code;
}
// 启动!
const bestCode = await harness("请使用 javascript 实现一个数组去重函数");
console.log('🏆 最优结果:\n', bestCode);
五、运行过程与输出解读
假设用"数组去重"这个 prompt 跑一次,你会看到如下输出流:
sql
生成多个候选者....
候选结果:
---- Candidate 1 ----
function unique(arr) {
return [...new Set(arr)];
}
---- Candidate 2 ----
function unique(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
if (result.indexOf(arr[i]) === -1) {
result.push(arr[i]);
}
}
return result;
}
---- Candidate 3 ----
function unique(arr) {
return Array.from(new Set(arr));
}
Evaluate Candidates...
打分结果:
---- Candidate 1 ----
function unique(arr) { return [...new Set(arr)]; }
→ 9 分
---- Candidate 2 ----
function unique(arr) { ... indexOf ... }
→ 6 分
---- Candidate 3 ----
function unique(arr) { return Array.from(new Set(arr)); }
→ 8 分
🏆 最优结果:
function unique(arr) { return [...new Set(arr)]; }
📊 结果分析
| 候选 | 实现方式 | 评分 | 分析 |
|---|---|---|---|
| ① | [...new Set(arr)] |
9 分 | 最简洁,ES6 标准,性能最优 |
| ② | for + indexOf |
6 分 | 能跑但 O(n²) 复杂度,不现代 |
| ③ | Array.from(new Set(arr)) |
8 分 | 正确但不如展开运算符简洁 |
Judge 的判断基本符合人类审美:简洁性、时间复杂度、现代性都在评分维度中体现。这就是 LLM as Judge 的威力------它能为质量建立量化标准。
六、为什么这套方案能"抗幻觉"?
回到开头的问题------Harness 工程如何对抗 LLM 的输出不稳定性?
| 不稳定因素 | Harness 的对抗策略 |
|---|---|
| 🎲 随机采样波动 | Best of N:多次采样,统计上趋于稳定 |
| 👻 幻觉(编造 API) | LLM Judge 会检查代码是否能"正确实现需求",编造的 API 大概率拿低分 |
| 📝 代码风格差 | Judge 评分天然偏好简洁、现代的写法 |
| 🔄 偶发质量波动 | N 次并行让某一次"撞大运"的概率大大降低 |
erlang
单次调用成功率 70% × 1 次 = 70%
Best of 3 至少一次成功 = 1 - (1-0.7)³ ≈ 97.3%
即使每次调用只有 70% 的概率生成可用代码,Best of 3 就能把"至少有一个可用"的概率推到 97% 以上。这就是统计学的力量。
七、ReAct 框架:Harness 背后的思维模型
readme 中提到:"ReAct 思维框架,Agent 的思维框架"------这不是冗余信息,而是 Harness 设计的认知根源。
ReAct = Reasoning + Acting
┌─────────────────────────────────────────┐
│ ReAct 循环 │
│ │
│ Thought ──→ Action ──→ Observation │
│ ↑ ↓ │
│ └──────── 下一轮 ────────────┘ │
└─────────────────────────────────────────┘
在 Harness 流水线中,这个循环表现为:
| ReAct 阶段 | Harness 对应 |
|---|---|
| Thought(思考) | Prompt 被送入 LLM |
| Action(行动) | LLM 生成代码(Generate) |
| Observation(观察) | LLM Judge 评分(Evaluate) |
| 下一轮 Thought | 选择最优,或以低分结果反馈重试(迭代优化) |
Harness 本质上是将 ReAct 的"思考-行动-观察"循环固化成了可编排的流水线。Demo 只跑一轮,但架构天然支持多轮迭代------低分候选可以带着 Judge 的反馈再次进入 Generate 阶段。
八、进阶思考:从 Demo 到生产
当前 Demo 是 Harness 工程的"Hello World",要走向生产,有几个关键方向可以深入:
8.1 多轮迭代 Harness
markdown
第 1 轮 Generate → Evaluate → Best
↓
如果评分 < 阈值 (如 7 分)
↓
第 2 轮 Generate(带 Judge 反馈)→ Evaluate → Best
带上 Judge 的扣分原因作为上下文,让 LLM 有针对性地修正。
8.2 多维评分体系
javascript
const scoringDimensions = {
correctness: { weight: 0.5 }, // 功能正确性
performance: { weight: 0.2 }, // 性能
readability: { weight: 0.2 }, // 可读性
safety: { weight: 0.1 }, // 安全性
};
单维度 → 多维度加权评分,让 Judge 的判断更全面。
8.3 加入 Human-in-the-Loop
markdown
Generate → Evaluate → Best
↓
score > 8 → 自动通过
score < 5 → 自动驳回
5 ≤ score ≤ 8 → 人工确认
不是全自动也不是全人工,而是智能分级的半自动。
8.4 实践中的成本意识
| 环节 | Token 消耗 | 优化方向 |
|---|---|---|
| Generate(N 次) | N × prompt | 用更小、更便宜的模型做候选生成 |
| Evaluate(N 次) | N × judgePrompt | Judge 可以用更轻量的模型(如 qwen-turbo) |
| 总成本 | ≈ 2N 次调用 | N=3 时约 6 次调用,可控 |
九、总结与展望
📊 Harness 工程核心公式
scss
高质量输出 = Best of N (多样性) + LLM as Judge (自动化评估) + 流水线编排 (可重复)
🧩 三种能力的角色分工
| 能力 | 角色 | 比喻 |
|---|---|---|
| Best of N Sampling | 扩大搜索空间 | 多个人同时做同一道题 |
| LLM as Judge | 质量把关 | 一个 AI 老师批改所有人的卷子 |
| Harness 流水线 | 流程编排 | 考试→阅卷→排名→登分 全自动 |
🎯 记住这三点
- 不要信任单次 LLM 输出 ------ 永远给随机性留冗余
- 用 LLM 的能力约束 LLM 的输出 ------ LLM as Judge 是最优雅的自监督方案
- 流程就是你的马具 ------ 把不可靠的模型关进可靠的流程里
🚀 展望
Harness 工程是一个开放的设计模式,而非封闭的框架。它的核心思想------"生成、评估、择优"的三段式------可以套用到:
- 📝 文案生成 → 多版本 + AI 文案评审
- 🎨 UI 设计 → 多方案 + AI 设计评审
- 🧪 测试用例 → 多覆盖 + AI 覆盖率评估
- 🔍 代码审查 → 多角度 + AI Code Review
未来不是模型越来越强就好了,未来是谁能把现有模型的能力榨得更干谁就赢。Harness 工程,就是那把拧干毛巾的扳手。