让 AI 自己卷自己:50 行代码实现「LLM 当评委 + 多选一」的 Harness 工程

让 AI 自己卷自己:50 行代码实现「LLM 当评委 + 多选一」的 Harness 工程

你是否遇到过这种情况:让大模型写一段代码,第一次跑出来的结果有 bug,第二次换个说法又好了,第三次......它居然开始胡言乱语了?

大模型不是"稳定输出"的机器。同样的 Prompt,问十次可能给你十个不同的答案,质量参差不齐------这就是我们常说的幻觉(Hallucination)随机性

那我能不能不把宝押在"某一次运气好"上,而是让它多答几次,再挑最好的那个

答案是可以。今天要聊的,就是一套把大模型"驯服"成稳定输出流水线的工程方法------Harness 工程 ,以及它背后两个关键武器:Best-of-N SamplingLLM as Judge


一、什么是 Harness?先认识这根"缰绳"

Harness 这个词,本意是"马具"------套在马身上的那套装备,用来驾驭和控制马的方向。

放在 LLM 语境里,harness 指的是一套把模型生成、自动评测、择优筛选串联成闭环的流水线编排框架。它的目标很朴素:

与其让大模型"裸奔",不如给它套上缰绳,在结构化流程里逼出更高质量的结果。

你可能会说:这不就是多问几次取最好吗?听起来很简单。

但魔鬼藏在细节里------谁来"取最好"? 如果靠人肉去看 N 个答案然后挑一个,那这个方案根本没法规模化。真正让这套方案能落地闭环的,是让 LLM 自己当评委

我们来看一张图,理清整个闭环:

flowchart LR A[Prompt 需求] --> B[生成阶段<br/>Best-of-N 并行采样<br/>产出 N 个候选] B --> C[评测阶段<br/>LLM as Judge<br/>给每个候选打分] C --> D[择优阶段<br/>Pick Best<br/>排序取最高分] D --> E[输出最优结果] E -.反馈闭环.-> A

三个环节,各司其职、完全解耦。下面我用一个真实的"数组去重函数"需求,把整条流水线写出来。


二、三阶段拆解:从一段 50 行的真实代码说起

先看成品。下面是基于 openai SDK 的一个最小可运行 harness:

javascript 复制代码
// index.mjs  ------ 一个最小 Harness 示例
import OpenAI from 'openai';
import { config } from 'dotenv';
config(); // 读取 .env 里的 API Key / BaseURL / 模型名

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL, // 任何兼容 OpenAI 协议的网关都行
});

// 最基础的"问一次"封装
const askLLM = async (prompt) => {
  const res = await client.chat.completions.create({
    model: process.env.MODEL_NAME,
    messages: [{ role: 'user', content: prompt }],
  });
  return res.choices[0].message.content;
};

这段代码有个亮点:baseURL 是可配置的。也就是说,只要是兼容 OpenAI 协议的模型网关(通义、DeepSeek、vLLM、Ollama 等),这套 harness 全都能无缝切换,模型升级不需要改业务代码。

阶段一:生成(Best-of-N Sampling)

javascript 复制代码
// 并行生成 N 个候选,让随机性帮我们"广撒网"
const generateCandidates = (prompt, n = 3) => {
  const tasks = Array.from({ length: n }, () => askLLM(prompt));
  return Promise.all(tasks); // 注意:这里是并行,不是串行
};

关键点在于 Promise.all。如果 N=3,三次请求是同时发出去的,总耗时约等于最慢的那一次,而不是三次之和。

这里其实藏着一个工程常识:LLM 的 token 是按量计费的,但延迟却是按"轮次"感知的。并行采样用一点额外 token 成本,换来了更短的等待时间,对用户体感友好得多。

阶段二:评测(LLM as Judge)

javascript 复制代码
// 让 LLM 当评委:输入代码,只吐一个 0-10 的数字
async function judge(code) {
  const prompt = `
  你是一个严格的代码评审,请判断下面代码是否正确实现"数组去重函数"

  要求:
  - 只返回一个数字评分(0-10)
  - 不要解释

  代码:
  ${code}
  `;
  const res = await askLLM(prompt);
  const score = parseFloat(res); // string => number
  return isNaN(score) ? 0 : score; // 兜底:解析失败就当 0 分
}

这就是 LLM as Judge 的精髓:用模型的能力,去评模型自己的输出

你可能会质疑:"用一个会犯错的模型去评判另一个模型的错误,靠谱吗?"

我的回答是:在"相对比较"这件事上,LLM 的表现比想象中可靠得多。 它未必能给出绝对准确的"9.2 分",但它判断"这段代码 A 比代码 B 更可能是对的"时,准确率相当高。而这正是 Best-of-N 场景唯一需要的------我们只需要一个相对排序,取最高分就够了

阶段三:择优(Pick Best)

javascript 复制代码
// 串行评测所有候选,然后排序取最高分
async function evaluateAll(candidates) {
  const results = [];
  for (const code of candidates) {
    const score = await judge(code); // 注意:这里是串行
    results.push({ code, score });
  }
  return results;
}

const pickBest = (result) => result.sort((a, b) => b.score - a.score)[0];

最后是主流程,把三段串起来:

javascript 复制代码
async function harness(prompt) {
  // 1. 并行生成多个候选
  const candidates = await generateCandidates(prompt, 3);

  // 2. 逐个打分
  const evaluated = await evaluateAll(candidates);

  // 3. 择优返回
  return pickBest(evaluated).code;
}

const best = await harness('请使用JavaScript实现一个数组去重函数');
console.log(best);

就这么点代码,一个完整的"生成→评测→择优"闭环就跑起来了。三个函数各自独立,任何一阶段想换实现(比如把 0-10 分换成成对比较、把 3 个候选换成 10 个),都不会牵动另外两段。这就是 harness 抽象的价值:解耦。


三、别急着跑,这里有几个真实的"坑"

上面的代码是教学版,真要在生产环境用,下面几个问题你会一个一个撞上。

坑 1:评测阶段又变回串行了

生成阶段是并行的(Promise.all),但 evaluateAll 里的 for...of串行 await。如果 N=10,你就要等 10 轮评测排队。

javascript 复制代码
// ❌ 串行评测,N 越大越慢
for (const code of candidates) {
  const score = await judge(code);
  ...
}

// ✅ 并行评测
const evaluateAll = (candidates) =>
  Promise.all(
    candidates.map(async (code) => ({ code, score: await judge(code) }))
  );

改一行,评测阶段立刻从 O(N) 延迟变成 O(1) 感知延迟。

坑 2:parseFloat 太脆了

Prompt 要求"只返回数字",但模型偶尔会皮一下,返回:

  • "评分:9 分"parseFloat 能勉强解析出 9,侥幸通过
  • "9/10"parseFloat 得到 9,也还行
  • "这段代码还行,我给 8.5 分"parseFloat 得到 8.5,居然也对
  • "这是一段很棒的代码"parseFloat 得到 NaN,兜底成 0 分

你会发现 parseFloat 靠"截取开头的数字"误打误撞对了很多,但一旦模型把文字写在数字前面,就直接归零。一个更稳的做法是让它输出结构化 JSON

javascript 复制代码
const res = await askLLM(`
  请评审以下代码,只返回 JSON,不要有任何其他内容:
  {"score": 0-10之间的数字}

  代码:${code}
`);
const { score } = JSON.parse(res); // 结构化,天然可校验

配合 OpenAI SDK 的 response_format: { type: 'json_object' }(或工具调用),可以从协议层强制模型吐 JSON,彻底告别字符串解析的脆弱性。

坑 3:LLM 评委也有"私心"

LLM as Judge 并非完美,学术界早就发现它有几类系统性偏见:

  1. 位置偏见(Position Bias):让模型一次比较多个选项时,它偏爱排在前面的(这在成对比较场景更明显,本文的逐个打分受此影响较小)。
  2. 自我偏好(Self-Preference):模型倾向于给自己生成的答案打高分------毕竟都是"一家人"。
  3. 长度偏见:更长的回答往往被打更高分,哪怕内容是注水。

缓解思路:换个评委模型(让模型 A 生成、模型 B 评分,交叉评审)、随机打乱比较顺序、多个评委取平均。这些都属于 harness 框架里可以"插件化"替换的细节。

坑 4:成本是 N 倍的

Best-of-N 意味着 token 消耗放大 N 倍 ,再加上评测阶段的 N 次调用,总成本是 N(生成) + N(评测) 的量级。这不是免费的午餐。

所以 N 的取值要权衡:简单任务 N=3 就够,复杂推理任务才值得上 N=10 甚至更高。这也是为什么这套方法更常用于"质量要求高、调用频次低"的场景(代码生成、数据标注、写作润色),而不是每条消息都这么干。


四、往深了想:这套东西为什么能 work?

如果只把 harness 当成"多试几次取最好",那太小看它了。它背后站着几个扎实的技术思想:

1. Best-of-N ≈ 穷人版"蒙特卡洛"

模型每次采样都在概率分布上"掷一次骰子"。单次采样可能落在分布的低概率区域(也就是烂答案),但并行采样 N 次,覆盖分布高概率区域(好答案)的概率大幅上升。温度(temperature)越高,分布越散、多样性越强,Best-of-N 的收益也越明显------当然,烂答案也更多,这时"评委"的作用就体现出来了。

2. LLM as Judge 是"能力复用"的典范

传统自动化评测要写一堆规则、测试用例、甚至训练一个评分模型。而 LLM as Judge 直接把模型本身的理解和推理能力 借过来当"万能评分器",几乎零成本适配任意任务。它不追求绝对精确,只追求相对可靠------而这恰好是择优排序所需的全部。

3. 它和 Self-Consistency 是一家人

如果你听说过 Self-Consistency(自洽性,CoT 推理场景下对同一问题采样多次、取"多数投票"的答案),会发现它就是 Best-of-N 在"推理题"上的一个特例------只不过"评委"换成了投票,而不是打分。

而 readme 里提到的 ReAct Agent 思维框架 ,则是在这条流水线之上再叠一层"思考---行动---观察"的循环。harness 可以理解成这些更复杂框架的原子地基


五、这套模式还能怎么进化?

顺着这个思路往下推,你其实已经在朝"多智能体编排"的方向走了:

进化方向 核心改动 适用场景
Best-of-N(本文) 并行采样 + LLM 打分择优 代码生成、文案、翻译
Self-Consistency 并行采样 + 多数投票 数学/逻辑推理
成对比较(Pairwise) 两两 PK 取胜者 需要精细排序的场景
交叉评审 生成模型 ≠ 评分模型 规避自我偏好
Reflexion / ReAct 评分后带着反馈"回炉重造" 需要多轮迭代修正

你现在手上有了一根"缰绳",往上可以接到多智能体框架(AutoGen、LangGraph、MetaGPT 这类把 harness 做成产品的项目),往下则是最朴素的"多问几次取最好"。


六、写在最后

回过头看,这篇东西想讲清楚的核心只有一句话:

别把 LLM 当一次性工具,把它当成一个需要"驾驭"的组件。

套上 harness 这根缰绳之后,你要做的就三件事:多生成几个候选(广撒网)→ 让 LLM 自己当评委(自动评)→ 排序取最优(择优)。三段解耦,各自可替换,整体可复用------这才是"工程化"该有的样子。

这套代码我已经精简到 50 行内,你只要有一把兼容 OpenAI 协议的 API Key(通义、DeepSeek、vLLM 都行),把 .env 填好,node index.mjs 就能跑通,亲眼看看"AI 自己卷自己"是什么体验。


大家在实际项目里,有没有用过类似"多采样 + 自动择优"的套路?或者你对"LLM 当评委"靠不靠谱这件事有自己的看法?欢迎在评论区聊聊 👇

相关推荐
AINative软件工程3 小时前
LLM 请求合并工程实践:用 Single-Flight 把并发重复调用从 N 次砍成 1 次
后端·llm·ai编程
Flynt11 小时前
花一下午把Qwen3.8-27B跑在本地,最坑的不是显存
开源·llm·llama
XLYcmy12 小时前
京东 算法实习一面 下+手撕
c++·python·llm·概率论·数据处理·训练·codebert
北斗落凡尘14 小时前
LangGraph 入门实战(9)--中断
后端·langchain
赵广陆17 小时前
企业实战:主体识别
langchain·pdf·langgraph
众人皆醒我独醉19 小时前
AI 工作负载可观测性:DCGM + Prometheus + Grafana
面试·llm·gpu
qpsj20 小时前
DeepSeek 缓存命中涨价 12 倍:4 个改动,账单回到涨价前
人工智能·llm
uncle_ll1 天前
Llama 3 私有化落地全栈实战:从 Ollama 极速部署到 LLaMA Factory LoRA 定制微调
llm·nlp·llama·ollama·大模型微调
小马过河R1 天前
Graph Engineering 深度解析:模型越强,越需要给它画好“地图”
人工智能·langchain·graph·ai工程化·harness·驾驭工程