AntV Harness:LLM 自我进化的闭环优化系统

Harness Engineering is the practice of building the infrastructure (guides, sensors, and feedback loops) that surrounds an AI model to steer its behavior, validate its outputs, and enable continuous self-improvement---essentially everything in an AI agent except the model itself.

一、问题起源:从「能跑」到「持续变好」

「让 LLM 根据自然语言生成图表代码」------这个目标听起来简单。把 API 文档塞进 prompt,初版 demo 两天就能跑起来。但真正的挑战在于:如何系统性地让生成质量持续提升?

传统做法是:人工分析失败案例 → 手动修改知识库 → 重新测试。这个流程可以工作,但它是线性的、依赖人工的、难以规模化的。

AntV chart-visualization-skills 项目的 Harness 子系统给出了一个不同的答案:把优化循环本身自动化------让 LLM 分析自己的失败,改进知识库,再重新评测


二、架构概览:五 Agent + 控制器的闭环流水线

整个 Harness 系统由五个职责单一的 Agent 和一个 Controller 组成:

Controller Loop

每个 Agent 职责清晰,通过 Controller 协调形成闭环:

javascript 复制代码
// controller.js 核心循环
while (consecutivePasses < MAX_PASSES) {
  const resultPath = await runEval(ids);
  const errorCases = await renderAgent.run(resultPath);
  const { skillToErrors, orphanCases } = analyzeAgent.run(errorCases);
  await optimizeAgent.run(skillToErrors, { orphanCases });
  await indexAgent.run();
}

三、五 Agent 详解

3.1 EvalAgent

EvalAgent 负责调用 eval CLI,让 LLM 基于现有 skill 生成图表代码。支持三种检索策略:

策略 工作方式 适用场景
bm25 预先检索相关 skill,直接注入 prompt 快速测试、低成本
tool-call LLM 通过多轮工具调用主动拉取 skill 精准检索、复杂查询

关键设计------ --ids** 参数定向重测**:支持指定 case ID 重测,而非每次全量运行。这为后续调度逻辑奠定了基础。

3.2 RenderAgent

生成代码是否正确,最终要靠执行来验证。RenderAgent 使用 Playwright 无头浏览器实际运行代码,判断三种失败状态:

  1. error:JavaScript 报错,代码根本没跑起来
  2. blank:画布为空,代码执行了但什么都没画出来(白屏)
  3. 低视觉得分:图表渲染了,但通过 VL(视觉语言)模型判定与预期不符

白屏检测的重要性:白屏通常意味着数据绑定错误或 API 用法有误,但没有 JS 异常。这类问题纯靠代码静态分析抓不到,必须实际渲染后检查画布是否有像素内容。

3.3 AnalyzeAgent

把 error cases 映射到具体的 skill 文件,但这里有一个根本性的难题:一次 LLM 调用可能读取了多个 skill,你不知道是哪个 skill 导致了错误

最朴素的归因策略是"谁最后被读就改谁",但这往往是错的。比如:LLM 先读了 bar-chart skill 拿到基础用法,再读了 color-mapping skill 调整颜色,结果最终代码在 bar chart 的数据格式上出了问题------最后读的是 color-mapping,但真正的 bug 在 bar chart skill 的示例里。

AnalyzeAgent 的做法是:收集 LLM 实际读取的所有 skill,构成候选归因集合,然后把整个集合交给 OptimizeAgent,让 OptimizeAgent 的 LLM 自己判断根因在哪里

对于没有对应 skill 的 case,AnalyzeAgent 将其标记为 "孤儿 case"(orphanCases)。孤儿 case 的存在意味着知识库里有盲区------某些图表类型压根没有 skill 覆盖,这会触发 OptimizeAgent 的新 skill 创建流程

3.4 OptimizeAgent

模型选择上,会区别于 Eval 阶段使用的模型,质量优先。

OptimizeAgent 对每个有问题的 skill 并行调用 LLM 重写。System Prompt 设计非常关键:

plain 复制代码
当前 skill 是候选归因,不一定是真正的根本原因。
- 如果你判断该 skill 确实有问题,输出修正后的完整文档
- 如果你认为该 skill 没有问题,原样输出即可

把"是否需要修改"的判断权交给 LLM,而非强制对每个候选 skill 都生成修改版本。OptimizeAgent 还支持工具调用,可以查阅本地官方文档和源码,但有路径访问白名单保护:

javascript 复制代码
function assertAllowed(filePath) {
  const realRoots = allowedRoots.map((r) => fs.realpathSync(path.resolve(r)));
  if (!realRoots.some((r) => realPath === r || realPath.startsWith(r + path.sep))) {
    throw new Error(`Access denied: ${filePath} is outside allowed ref paths.`);
  }
}

安全机制

  • 响应长度 < 原文件 50%:跳过(防止截断导致文档大幅缩水)
  • 响应与原文完全相同:跳过(LLM 判定非根因,正确行为)
  • 写入前先备份 .bak 文件,失败自动还原

对于孤儿 case,OptimizeAgent 会创建全新的 skill 文件。为了让 LLM 一次生成多个 skill,设计了结构化分隔符协议:

plain 复制代码
<<<SKILL_START:grouped-bar-chart>>>
---
title: Grouped Bar Chart
tags: [...]
---
# 分组柱状图
...
<<<SKILL_END>>>

3.5 IndexAgent

每轮优化后重建 BM25 索引,确保下一轮 eval 能检索到最新内容。这一步看似平淡无奇,但省略则整个优化循环形同虚设------LLM 改了文档,但检索引擎用的还是旧索引。


四、Controller

4.1 终止条件:连续通过机制

javascript 复制代码
while (consecutivePasses < MAX_PASSES) {
  // ...
  if (errorCases.length === 0) {
    consecutivePasses++;
    console.log(`Clean pass (${consecutivePasses}/${MAX_PASSES})`);
  }
}

为什么是「连续」通过而非单次通过?因为修复一个 bug 可能引入新 bug。只有连续多轮无错误,才能确信知识库质量稳定。

4.2 固定样本 + 定向重测:让「通过」有意义

问题:如何判断一轮优化真的有效?

固定样本 :第一轮确定的 case IDs 锁定为 fixedCaseIds,后续每轮用同一批 case。如果每轮随机采样,可能出现「恰好这轮都是简单 case」的假阳性。

定向重测 :每轮优化后,把失败的 case IDs 存入 priorityCaseIds,下一轮只测这些 case,快速验证修复。

配合流程

plain 复制代码
第 N 轮:全量 fixedCaseIds → [case-3, case-7, case-12] 失败
        → 优化相关 skill
        → priorityCaseIds = [case-3, case-7, case-12]

第 N+1 轮(定向):只测 [case-3, case-7, case-12] → 全部通过
                 → 不计入 consecutivePasses
                 → 跑完整 fixedCaseIds 确认

第 N+2 轮(全量):全量 fixedCaseIds → 全部通过 → consecutivePasses++

4.3 Worktree 隔离 + Baseline 对比

所有 skill 优化操作在独立的 git worktree 上进行,优化完成后对比 worktree 和 main 分支,这防止了「修好 A 但引入 B 的回归」,因为每个 skill 可能被多个 case 使用。

五、Memory 系统:跨轮次的持久化学习

每一轮优化的信息不应该随着进程退出而消失。Memory 系统实现了跨轮次的记忆持久化:

javascript 复制代码
// memory.js
class HarnessMemory {
  // 记录错误
  recordErrors(skillPath, errorCases, iteration) {
    const history = this._skill(skillPath);
    for (const c of errorCases) {
      history.errors.push({
        caseId: c.id,
        errorType: this._errorType(c),
        query: c.query,
        iteration,
        ts: Date.now(),
      });
    }
    this._save();
  }

  // 记录优化动作
  recordOptimization(skillPath, errorCases, iteration) { /* ... */ }

  // 生成历史摘要注入 LLM prompt
  getOptimizationContext(skillPath) { /* ... */ }
}

5.1 持续错误类型高亮

getOptimizationContext高亮持续出现的错误类型(在 ≥2 轮中出现),相当于给 LLM 提供「重点关注清单」:

plain 复制代码
【历史优化记录 --- bar-chart-grouped.md】
共优化 3 次,累计错误 7 条。

- 第 2 轮:3 个错误,类型:blank, TypeError: ...
- 第 3 轮:2 个错误,类型:blank

⚠️ 持续出现的错误类型(跨多轮未解决):blank

5.2 原子写入

javascript 复制代码
_save() {
  const tmp = `${this._path}.tmp`;
  fs.writeFileSync(tmp, JSON.stringify(this._data, null, 2));
  fs.renameSync(tmp, this._path); // POSIX 原子操作
}

5.3 路径归一化

解决 worktree 和主仓库绝对路径不同的问题:

javascript 复制代码
_normalizeKey(skillPath) {
  if (path.isAbsolute(skillPath)) {
    return path.relative(PROJECT_ROOT, skillPath);
  }
  return skillPath;
}

这确保同一个 skill 文件在不同环境下被视为同一条记录。


六、模式的通用性

如何为一个 SKILL 系统建立知识库质量的持续改进机制。这个问题不局限于图表生成。任何「LLM + 结构化知识库」的系统都面临同样挑战------代码生成工具、客服 FAQ、法律/医疗垂直领域的专业知识库。

核心模式可以抽象为:

plain 复制代码
评测(量化当前质量)
  → 执行验证(在真实环境中验证)
    → 分析归因(定位到具体文档)
      → LLM 优化(驱动文档改进)
        → 重索引(让改动立即生效)
          → 循环

这条流水线的前提是:「评测」和「执行验证」这两步可以自动化

对图表生成,Playwright 无头浏览器就是执行环境;对其他场景,你需要找到对应的自动验证方式。


七、结语

Harness 系统的代码量并不大,但它把一个复杂的质量优化问题拆解成了五个职责清晰的 Agent,用一套严谨的调度逻辑把它们串联起来,通过 **Feedforward **和 **Feedback **进行相关指引和校正。

Feedforwardskills/ 目录提供结构化知识,通过 BM25/tool-call 策略注入 prompt;

Feedback:RenderAgent 检测 JS 错误/白屏/低视觉评分,AnalyzeAgent 归因,OptimizeAgent 重写文档。

如果文章对你有用,别忘了给个小星星。

github.com/antvis/char...

相关推荐
fpcc2 小时前
信号处理与AI中的卷积的关系
c++·人工智能·信号处理
基算仿真2 小时前
AI如何用MCP“玩转”仿真软件?
人工智能
掘金一周2 小时前
现在面试 AI 相关问题,不把底层原理扒得明明白白,真的分分钟被问麻😭 | 沸点周刊 4.16
openai·ai编程·沸点
大转转FE2 小时前
转转前端周刊第192期: 财务数仓 Claude AI Coding 应用实战
前端·人工智能
cd_949217212 小时前
灵析数智:以 AI GEO 重构品牌增长,领跑生成式引擎优化新赛道
人工智能·搜索引擎·重构
yunhuibin2 小时前
videopipe学习之demo运行
人工智能·深度学习·学习
晓晓不觉早2 小时前
Hermes Agent 与 OpenClaw 全面对比:从“宠物级”到“自我进化”的本地 AI Agent 之争
人工智能
fzil0012 小时前
让 AI 自己协作 —— 多智能体(Swarm)系统的设计与实现
java·开发语言·人工智能·ubuntu
跨境卫士-小汪3 小时前
多渠道获客复杂化跨境卖家如何优化整体结构
大数据·人工智能·产品运营·跨境电商·跨境