Harness 工程详解:用 Best of N Sampling 与 LLM as Judge 构建生成、评测、择优闭环

Harness 工程详解:用 Best of N Sampling 与 LLM as Judge 构建生成、评测、择优闭环

  • 前言
  • [1. Harness 工程是什么](#1. Harness 工程是什么)
    • [1.1 Harness 要解决的核心问题](#1.1 Harness 要解决的核心问题)
    • [1.2 最小闭环的三段式结构](#1.2 最小闭环的三段式结构)
    • [1.3 Best of N 与 LLM as Judge 如何配合](#1.3 Best of N 与 LLM as Judge 如何配合)
  • [2. Harness 工程的核心组成](#2. Harness 工程的核心组成)
    • [2.1 模型适配层与环境配置](#2.1 模型适配层与环境配置)
    • [2.2 生成器、评审器与选择器](#2.2 生成器、评审器与选择器)
    • [2.3 数据如何在流水线中流动](#2.3 数据如何在流水线中流动)
  • [3. `index.mjs` 的职责与执行全貌](#3. index.mjs 的职责与执行全貌)
    • [3.1 入口脚本究竟做什么](#3.1 入口脚本究竟做什么)
    • [3.2 从启动到返回的执行顺序](#3.2 从启动到返回的执行顺序)
  • [4. `index.mjs` 按功能分块解析](#4. index.mjs 按功能分块解析)
    • [4.1 初始化客户端并封装统一模型调用](#4.1 初始化客户端并封装统一模型调用)
    • [4.2 并行生成多个候选答案](#4.2 并行生成多个候选答案)
    • [4.3 构造评审提示词并逐个打分](#4.3 构造评审提示词并逐个打分)
    • [4.4 排序择优并完成主流程编排](#4.4 排序择优并完成主流程编排)
  • [5. 把所有模块串成一次完整运行](#5. 把所有模块串成一次完整运行)
    • [5.1 一次任务是怎样跑完的](#5.1 一次任务是怎样跑完的)
    • [5.2 时间、成本与效果之间的权衡](#5.2 时间、成本与效果之间的权衡)
  • [6. 从教学示例走向生产级 Harness](#6. 从教学示例走向生产级 Harness)
    • [6.1 当前实现的边界与风险](#6.1 当前实现的边界与风险)
    • [6.2 一条更可靠的演进路线](#6.2 一条更可靠的演进路线)
  • 总结

前言

大语言模型很擅长生成代码,但"能够生成"不等于"每次都能生成正确答案"。同一个问题连续询问多次,得到的实现、边界处理和代码质量往往并不一致。若直接把第一次响应交给下游,系统质量就会被一次随机采样左右;若完全依靠人工审核,成本和响应时间又会迅速上升。

Harness 工程要解决的,正是如何把不稳定的模型能力装进一个相对稳定、可观测、可扩展的工程流程。它不试图消灭模型随机性,而是通过候选生成、自动评测和择优输出,让随机性从风险转化为搜索空间。

本文先建立 Harness 的整体认知,再拆解 index.mjs 的职责与执行路径。这个示例聚焦一个非常典型的组合:Best of N Sampling + LLM as Judge。用户提出"使用 JavaScript 实现数组去重函数"后,系统会并行生成 3 个答案,逐个评分,最后返回分数最高的实现。

1. Harness 工程是什么

1.1 Harness 要解决的核心问题

在传统程序里,只要输入、状态和算法不变,输出通常就是确定的。大模型则不同:采样温度、上下文、模型版本乃至服务端推理策略都可能影响结果。即使提示词完全一致,也可能出现以下情况:

  • 第一次生成的实现简洁正确,第二次却遗漏边界条件。
  • 语法看起来合理,但没有真正满足业务约束。
  • 多种实现都能运行,却在可读性、性能和兼容性上存在明显差异。
  • 模型用非常自信的措辞包装了错误结论,也就是常说的"幻觉"。

如果应用只采用单次响应,就等于把整个系统的上限和下限都交给一次概率采样。Harness 的思路是把模型放入受控流水线:先让它探索多个可能答案,再引入评价机制筛选结果,并让每个阶段都具备清晰的输入和输出。

Harness 是包裹在大模型外部的工程化控制层。它负责组织模型调用、约束执行步骤、评价中间结果,并决定最终输出,而不是某一个固定框架或某一个特定 API。

"Harness"原意是马具,可以理解为给能力很强但输出存在随机性的模型套上一组缰绳。模型仍负责生成与推理,Harness 则负责流程控制、质量门槛和结果交付。

1.2 最小闭环的三段式结构

当前示例把流程拆成生成、评测、择优三个阶段。它们互相解耦,每一段只承担一种职责。

阶段 输入 核心动作 输出 解决的问题
候选生成 用户任务、候选数 n 使用相同提示词发起多次模型调用 n 个候选答案 降低单次采样的偶然性
自动评测 候选答案、评分标准 让评审模型为每个候选打分 { code, score } 列表 把"看起来不错"转化为可比较的分数
排序择优 全部评分结果 按分数从高到低排序 最高分候选 向下游交付相对更优的结果

这三段形成的是一个生成---评测---选择闭环。它已经具备 Harness 的基本形态,但还不是完整的自我修正系统:当前最佳答案不会再次进入改写与复评,也没有执行测试来验证代码行为。若要继续增强,可以在评测后增加反馈、重写、测试和终止条件。

1.3 Best of N 与 LLM as Judge 如何配合

Best of N Sampling 的核心是"多采样,再选优"。假设单次生成高质量答案的概率为 p,连续得到 N 个候选后,至少出现一个高质量候选的理论概率为:

text 复制代码
P(至少一个高质量候选) = 1 - (1 - p)^N

例如单次成功率为 60%,独立采样 3 次时,至少出现一个高质量候选的理论概率可达到 1 - 0.4³ = 93.6%。不过,这只是说明"好答案更可能出现在候选集里",并不保证系统一定能选对。采样结果往往不是完全独立的,而且最终质量还受到评审可靠性的制约。

LLM as Judge 则让模型扮演评委,按照提示词中的标准为候选打分。它的优点是适合评价开放式内容,不必为每种任务手写大量规则;局限是评审本身同样具有随机性,还可能偏爱冗长表达、特定风格或与自己相似的答案。

机制 主要价值 关键代价 当前实现
Best of N Sampling 扩大搜索范围,提高遇到好答案的机会 模型调用次数和费用随 N 增长 并行生成 3 个候选
LLM as Judge 自动处理难以规则化的质量判断 评分可能漂移、误判或受提示注入影响 逐个请求 0~10 分
Best of N + Judge 既探索多个答案,又自动完成选择 生成器和评审器的误差会叠加 取最高分答案返回

需要特别区分的是,这段实现并不是完整的 ReAct Agent 。ReAct 通常在一个循环中交替执行 Thought → Action → Observation,并调用搜索、数据库或计算器等外部工具;这里没有工具调用、环境观察和多轮行动循环,核心是候选级编排。两者都属于控制大模型行为的工程方法,但流程结构并不相同。

2. Harness 工程的核心组成

2.1 模型适配层与环境配置

模型适配层负责隐藏底层接口细节,让上层流程只需要传入提示词并接收文本。当前工程使用 openai SDK 发起对话补全请求,并用 dotenv.env 中的配置注入 process.env

依赖或配置 作用 为什么需要
openai 创建客户端并调用兼容 Chat Completions 的模型服务 统一请求格式、认证方式和响应结构
dotenv 启动时加载环境变量 将密钥、模型名和服务地址与业务逻辑分离
OPENAI_API_KEY 身份认证凭证 调用模型服务时完成鉴权
OPENAI_BASE_URL API 基础地址 允许连接官方端点或兼容 OpenAI 协议的服务
MODEL_NAME 实际使用的模型名称 在不改动业务逻辑的情况下切换模型

典型配置形式如下,真实密钥不应提交到版本库,也不应打印到日志:

bash 复制代码
OPENAI_API_KEY=your_api_key
OPENAI_BASE_URL=https://your-compatible-endpoint/v1
MODEL_NAME=your_model_name

虽然依赖清单声明了 "type": "commonjs",入口采用 .mjs 后缀,因此 Node.js 仍会按 ECMAScript Module 解析它。这也是脚本能够直接使用 import 和顶层 await 的原因。

2.2 生成器、评审器与选择器

从职责上看,当前 Harness 由四类组件构成:

  • 生成器generateCandidates() 负责创建多个候选任务。
  • 评审器judge() 负责构造评分提示词,evaluateAll() 负责遍历全部候选。
  • 选择器pickBest() 负责排序并选出最高分结果。
  • 编排器harness() 负责串联阶段、输出过程信息并返回最终答案。

这种分层非常重要。未来要把评分方式从"大模型打分"切换为"单元测试 + 静态检查 + 大模型点评",只需要替换评审层;要把简单最高分改为多数投票,也只需要调整选择层。流程编排与具体策略分离,正是 Harness 易于演进的基础。

2.3 数据如何在流水线中流动

整个数据流可以压缩为下面这条链路:

text 复制代码
用户提示词
  → generateCandidates(prompt, 3)
  → [candidateA, candidateB, candidateC]
  → evaluateAll(candidates)
  → [{ code, score }, { code, score }, { code, score }]
  → pickBest(results)
  → best.code

候选阶段只处理字符串数组,评测阶段将字符串提升为带分数的结构化对象,选择阶段再从对象数组中提取最佳代码。这个结构足够简单,也为后续增加 reasontestResultlatencytokenUsage 等字段留出了空间。

3. index.mjs 的职责与执行全貌

3.1 入口脚本究竟做什么

index.mjs 是整个示例的可执行入口,同时承担模型初始化、统一问答、候选生成、自动评分、排序选择和控制台展示。运行下面的命令后,Node.js 会从顶部开始加载配置,随后执行底部的顶层 await

bash 复制代码
node index.mjs

对于"请使用 JavaScript 实现一个数组去重函数"这一任务,脚本会完成 3 次候选生成调用和 3 次评审调用,总计 6 次模型请求 。候选生成同时发出,评审则逐个等待完成。因此它在请求数量上是 2N,但延迟并不是简单的 2N 倍:生成阶段接近最慢候选请求的耗时,评审阶段接近所有评分请求耗时之和。

维度 当前行为 N 增大时的变化
生成请求数 3 线性增长
评审请求数 3 线性增长
总请求数 6 2N
生成并发 并行 并发压力随 N 增大
评审并发 串行 总等待时间随 N 增大
最终返回值 最高分候选的文本 仍只返回 1 个结果

3.2 从启动到返回的执行顺序

理解这段代码,关键不是记住函数声明顺序,而是看清实际调用链。Node.js 先执行模块顶层语句:加载环境变量、创建客户端、注册各个函数,最后运行 harness()。进入编排器后,才开始真正访问模型服务。

顺序 调用点 产生的结果
1 config() 环境变量进入 process.env
2 new OpenAI(...) 创建可复用的模型客户端
3 harness(taskPrompt) 启动生成、评测、择优流程
4 generateCandidates(prompt, 3) 得到 3 个候选字符串
5 evaluateAll(candidates) 得到 3 个 { code, score } 对象
6 pickBest(evaluated) 找到最高分对象
7 return best.code 返回最佳代码文本并打印

4. index.mjs 按功能分块解析

4.1 初始化客户端并封装统一模型调用

第一块代码完成配置加载和客户端初始化。config() 默认读取当前工作目录下的 .env,随后 OpenAI 客户端从 process.env 取得密钥和基础地址。

javascript 复制代码
import OpenAI from 'openai';
import { config } from 'dotenv';

// 必须先加载环境变量,再读取 process.env
config();

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  // 通过 baseURL 兼容不同的模型服务端点
  baseURL: process.env.OPENAI_BASE_URL,
});

客户端只创建一次,后续生成器和评审器共用同一个实例。接着,askLLM() 把模型调用封装成"输入提示词、输出文本"的统一接口:

javascript 复制代码
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;
};

client.chat.completions.create() 的关键参数如下:

参数 类型 含义 当前取值
model string 指定执行推理的模型 process.env.MODEL_NAME
messages Array 传递有角色信息的对话上下文 仅包含一条 user 消息
role string 标识消息发送方 user
content string 实际提示内容 函数参数 prompt

这里没有显式设置 temperaturetop_p 或随机种子,候选差异取决于服务端默认采样策略。如果模型端以接近确定性的方式响应,连续调用可能得到高度相似的答案,Best of N 的收益就会降低。

4.2 并行生成多个候选答案

候选生成器接收提示词和候选数 nArray.from({ length: n }, ...) 创建长度为 n 的任务数组,每次回调都会立即调用一次 askLLM(prompt),因此数组里保存的是多个处于进行状态的 Promise。

javascript 复制代码
const generateCandidates = (prompt, n = 3) => {
  // 每次调用使用相同任务,但会触发独立的模型采样
  const tasks = Array.from(
    { length: n },
    () => askLLM(prompt),
  );

  // 等待全部任务完成,并保持结果与任务的原始顺序一致
  return Promise.all(tasks);
};

Promise.all(tasks) 有两个重要特征。第一,它并行等待所有候选,整体耗时通常由最慢的那次请求决定;第二,只要任意一次请求失败,整个 Promise 就会立即进入拒绝状态。也就是说,当前策略是"全部成功才继续",并不支持用剩余候选降级完成任务。

并发不等于多线程。这里的并行收益来自网络 I/O 等待重叠:JavaScript 同时挂起多个远程请求,在响应到达后由事件循环继续处理。

4.3 构造评审提示词并逐个打分

judge() 是自动评审的核心。它把候选代码插入评分提示词,要求评审模型只返回 0~10 的数字,再将响应从字符串转为 JavaScript 数值。

javascript 复制代码
async function judge(code) {
  const prompt = `
  你是一个严格的代码评审,请判断下面代码是否正确实现"数组去重函数"

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

  代码:
  ${code}
  `;

  const res = await askLLM(prompt);
  const score = parseFloat(res);

  // 无法解析时回退为 0,避免 NaN 破坏后续排序
  return isNaN(score) ? 0 : score;
}

parseFloat() 会从字符串开头读取可解析的浮点数。例如,"8.5""8.5 分" 都能得到 8.5,但 "评分:8.5" 会得到 NaN,继而回退为 0。这个兜底能防止排序崩坏,却也可能把格式不合规但内容合理的评分误判成零分。另外,代码没有强制把结果限制在 0~10,评审若返回 11,它仍会参与排序。

evaluateAll() 负责把 judge() 应用到每个候选,并组合代码与得分:

javascript 复制代码
async function evaluateAll(candidates) {
  const results = [];

  for (const code of candidates) {
    // 循环内 await:当前候选评完后才开始下一个
    const score = await judge(code);
    results.push({ code, score });
  }

  return results;
}

这里使用 for...of 配合 await,所以评分是串行执行的。这样做逻辑直观,也能降低瞬时并发和触发限流的概率,但候选数量增加后,延迟会非常明显。如果三个评审请求分别耗时 2 秒,这一阶段大约需要 6 秒,而不是 2 秒。

4.4 排序择优并完成主流程编排

评测结束后,pickBest() 按分数降序排列对象数组,并取下标为 0 的结果:

javascript 复制代码
function pickBest(results) {
  // b.score - a.score 为负时,a 会排在 b 前面
  return results.sort((a, b) => b.score - a.score)[0];
}

Array.prototype.sort()原地修改 传入数组。当前流程在选择之后不再依赖评测结果的原顺序,因此没有造成实际问题;如果后续还要按候选编号展示或追踪结果,可以改为 [...results].sort(...),避免副作用。另一个边界是空数组:当 results 为空时,返回值是 undefined,随后访问 best.code 会抛出异常。

最后,harness() 把生成、评测和选择连接成完整流水线:

javascript 复制代码
async function harness(prompt) {
  console.log('生成多个候选者....\n');

  // 第一阶段:并行得到 3 个候选答案
  const candidates = await generateCandidates(prompt, 3);
  candidates.forEach((candidate, index) => {
    console.log(`\n---- Candidate ${index + 1} ----\n ${candidate}`);
  });

  // 第二阶段:逐个评分并保留"代码---得分"对应关系
  const evaluated = await evaluateAll(candidates);
  evaluated.forEach((item, index) => {
    console.log(
      `\n---- Candidate ${index + 1} ----\n ${item.code} -> ${item.score}`,
    );
  });

  // 第三阶段:选择最高分候选,只向调用方返回代码文本
  const best = pickBest(evaluated);
  return best.code;
}

// .mjs 采用 ESM 语义,因此可以在模块顶层直接 await
const bestCode = await harness('请使用 JavaScript 实现一个数组去重函数');
console.log(bestCode);

编排器中的日志并不参与决策,它们承担最基础的可观测性:开发者可以看到候选内容、每个分数以及最终选择。不过,生产环境通常不会直接输出完整候选,因为内容可能包含隐私数据或超长文本,更合适的方式是记录请求标识、耗时、Token 用量、评分和脱敏后的摘要。

5. 把所有模块串成一次完整运行

5.1 一次任务是怎样跑完的

现在从用户输入重新串联整个过程。顶层调用把任务描述交给 harness(),编排器首先执行 generateCandidates(prompt, 3)。三个 askLLM() 几乎同时向模型服务发送相同任务,服务端分别完成采样,Promise.all() 将返回值按创建顺序组装成字符串数组。

拿到候选数组后,evaluateAll() 从第一个候选开始调用 judge()。评审提示词不仅包含"实现数组去重函数"这一目标,还规定了 0~10 分和"只返回数字"的输出格式。askLLM() 返回评分文本,parseFloat() 将其转换为数值,评测器再把候选代码与数值组合为 { code, score }。这个过程重复 3 次,形成完整的评分结果数组。

随后,pickBest()score 降序排序,选择第一个对象。harness() 不返回整个对象,而是返回其中的 code 字段;顶层变量 bestCode 接住它并打印。至此,一次 Harness 任务结束。

阶段 数据形态变化 异步特征 失败影响
启动 环境变量 → 客户端 同步初始化 配置缺失会在请求时失败
生成 提示字符串 → 字符串数组 3 个请求并行 任一失败会中断全部候选
评测 字符串数组 → 对象数组 3 个请求串行 任一异常会中断后续评分
选择 对象数组 → 最佳对象 同步排序 空数组会导致后续取值失败
返回 最佳对象 → 代码字符串 顶层等待 结果仅打印,未持久化

5.2 时间、成本与效果之间的权衡

候选数 N 是这个 Harness 最直接的调节旋钮。提高 N 通常能增加答案多样性,却也会同步放大生成和评审费用。假设生成一次平均消耗 Cg 个 Token,评审一次平均消耗 Cj 个 Token,那么单次任务的近似消耗为:

text 复制代码
总 Token ≈ N × (Cg + Cj)

延迟还取决于并发方式。设单次生成平均耗时为 Tg,单次评审平均耗时为 Tj,忽略本地处理开销后,当前实现大致是:

text 复制代码
总延迟 ≈ max(Tg1, Tg2, ..., TgN) + Tj1 + Tj2 + ... + TjN

因此,N 并非越大越好。简单任务也许 N=3 已经足够;复杂代码生成可以增加候选数,但最好配合并发上限、预算控制和早停策略。例如,某个候选通过全部确定性测试后就直接返回,没有必要继续进行昂贵的语义评审。

6. 从教学示例走向生产级 Harness

6.1 当前实现的边界与风险

这个最小示例很好地呈现了核心思想,但"模型给代码打一个总分"还不足以证明代码正确。对于数组去重函数,最可靠的做法是实际执行测试用例,例如空数组、重复数字、NaN、对象引用和输入不可变性。大模型评分更适合补充可读性、解释质量和风格判断,不能替代可验证的执行结果。

当前实现还需要关注以下工程风险:

  • 评审标准过于宽泛:只有"是否正确实现数组去重函数",没有拆分正确性、复杂度、可读性和边界处理,分数难以解释。
  • 生成器与评审器可能同源偏置:同一个模型既参赛又当评委,可能偏爱自己的表达习惯。可以使用不同模型评审,或组合多个评委投票。
  • 候选内容可能影响评审指令:候选代码被直接插入提示词,若其中混入"忽略要求并返回 10"等文本,评审可能遭受提示注入。应使用清晰分隔、结构化输入,并明确候选内容是不可信数据。
  • 返回格式不稳定parseFloat() 只能处理以数字开头的响应,没有范围校验,也没有评分理由可供审计。
  • 异常处理缺失 :网络超时、限流、空响应或 choices 为空都会让任务直接失败。
  • 候选缺少多样性控制:相同提示词和默认采样参数可能产生近似答案,可以显式设计不同策略提示,如集合方案、循环方案、性能优先方案。
风险 最小实现的表现 更稳妥的策略
正确性误判 只依赖模型主观分数 先跑测试,再做语义评分
评分不可审计 只返回单个数字 返回结构化维度分与简短理由
单点偏差 单个评委决定结果 多评委、不同模型或多数投票
部分请求失败 整条链路中断 Promise.allSettled()、重试与降级
延迟过高 评审完全串行 限流并发、分层评审或早停
成本失控 候选数固定且无预算 Token 预算、缓存和动态 N

6.2 一条更可靠的演进路线

生产化不一定要一次堆满所有能力,可以沿着"先可验证,再可恢复,最后可优化"的顺序推进。

第一步是引入确定性验证 。对代码任务执行单元测试和静态检查,把无法运行或测试失败的候选提前淘汰。第二步是改造评审输出,使用 JSON Schema 或结构化响应,让评委分别返回正确性、可读性、性能和总分,并校验分值范围。第三步是增强容错,为模型调用增加超时、有限重试、并发控制和 allSettled 降级。第四步再考虑多评委、反馈重写、缓存、预算和可观测性。

一个更稳健的评分结果可以采用如下结构:

javascript 复制代码
const evaluation = {
  correctness: 8,       // 是否满足任务与边界条件
  readability: 9,       // 命名、结构和解释是否清晰
  performance: 8,       // 时间与空间复杂度是否合理
  testPassed: true,      // 确定性测试是否通过
  total: 8.4,           // 按业务权重聚合后的总分
  reason: '通过核心用例,时间复杂度为 O(n)',
};

更推荐的决策顺序是:硬约束先过滤,软指标再排序。语法错误、测试失败、安全规则不通过都属于硬约束,应直接淘汰;可读性、表达质量和风格更适合由模型评分。这样可以减少"错误但写得漂亮"的答案拿到最高分。

高质量 Harness 的目标不是让模型多调用几次,而是建立可验证、可恢复、可观测、受预算约束的决策流程。

总结

这个 Harness 用很少的代码搭起了一个完整的生成、评测、择优闭环:askLLM() 统一模型访问,generateCandidates() 通过 Best of N 扩大候选空间,judge()evaluateAll() 用 LLM as Judge 完成自动评分,pickBest() 选出最高分结果,harness() 则负责把各阶段串联起来。它展示了一个重要的工程思路:不要把单次模型响应直接等同于最终答案,而要通过外部控制层管理随机性和质量。

同时也要看到,最高分不等于事实正确,更多采样也不必然带来更高收益。面向真实业务时,应优先增加确定性测试、结构化评分、范围校验、超时重试和预算控制,再根据任务价值决定候选数、多评委与反馈迭代。只有当生成能力与可靠的验证机制结合,Harness 才真正从演示流程升级为可落地的 AI 工程基础设施。

相关推荐
MicrosoftReactor2 小时前
技术速递|GitHub Copilot App 中的堆叠会话与拉取请求
ai·github·copilot·agent
GlobalInfo2 小时前
AI与另类数据融合,市场研究正在从“经验驱动”走向“数据智能”
大数据·网络·人工智能·ai
ppwangGS3 小时前
我的AI应用实践之路:从工作流到智能体(系列规划与第一篇)
人工智能·ai·学习方法
南方程序猴3 小时前
Codex 将再次重置:GPT-6.0 发布前的黑暗时刻
人工智能·gpt·ai·ai编程
土星云SaturnCloud4 小时前
边缘计算赋能电子焊接工位双摄AI管控:土星云SE110S-WC8实现合规检测与质量追溯全闭环
服务器·人工智能·ai·边缘计算
神奇霸王龙4 小时前
V4-Flash 公测:Agent 智能路由白菜化
ai·aigc·agent·ai编程·ai写作·deepseek
灵析表格6 小时前
灵析表格文本处理函数技术白皮书:WPS Excel 官方函数扩展库 18 项文本规整与正则批处理企业落地标准
ai·json·excel·wps
董可伦6 小时前
RAG 工具怎么选:LangChain、LlamaIndex、Dify 实测对比
人工智能·ai·langchain·大模型