ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从evals到harness:LLM应用可重复测试工程化解析

从evals到harness:LLM应用可重复测试工程化解析 如果你正在把大模型接进一个真实业务系统大概会遇到这样的场景上个月还是“智能客服工单分类”的演示 demo进入联调后产品经理改了一句需求——“取消订单但已经发货的要单独走售后流程”。你花了半小时调整 prompt用例跑了一遍看起来分类效果不错。可到了第二天测试丢过来一条很普通的“我重复扣款了怎么退款”它居然被分到了 account 而不是 billing。你查了半天也没找到原因这版 prompt 相比上版只加了一句指令为什么上版一直正常的场景突然就失败了真正的问题不是某个 prompt 写得不好而是你的项目里没有一套能回答“这一版行为相对于上一版到底改变了什么”的机制。传统软件有编译报错、有单元测试、有回归集代码行为是可重复验证的。LLM 应用则不然哪怕输入完全相同模型输出也可能抖动改一个不相关的系统指令可能影响十几个下游场景跑一次真实模型评估还要计算 token 成本和 API 延迟。于是大家开始意识到模型能力已经不是瓶颈真正重要的是给 LLM 应用建立一套可重复、可隔离、可观测的测试与评估工程体系。Elvis Saravia 把这件事的优先级说得很清楚harness engineering 正在成为 AI 工程师仅次于 evals 的重要技能。这篇文章不打算给你介绍一个新框架的名字而是想拆解四件事harness engineering 到底是什么为什么它不是 evals 的重复而是 evals 能落地的前提它对 AI 工程师的角色和技能栈意味着什么以及如何用不到 200 行代码搭一个你自己项目里能用的最小 harness。读完你应该能判断自己团队现在缺的是评测指标还是让评测可重复运行的夹具层。1. 先看问题AI 应用“能跑”不等于“敢上线”很多团队在引入大模型时会走过同一个阶段demo 期一切顺利生产期问题集中爆发。原因是 demo 里你关注的是“模型会不会做这道题”而生产环境关心的是另外三件事。第一可重复性。同样的 prompt、同样的参数昨天输出合格今天输出不合格。这不是玄学而是采样温度、模型版本动态更新、上下文长度截断等共同作用的结果。如果一次失败无法复现那你根本没法做根因分析。第二可归因性。模型输出质量下降到底是 prompt 改坏的还是召回内容变了还是模型供应商灰度了新版本没有基线存档任何问题讨论都会变成“我觉得它变笨了”这种不可验证的争论。第三可防护性。上线之后你不可能每次都人工看一遍输出。你需要一种机制在发布前发现高风险行为或者在生产环境用低成本方式持续采样。这三件事传统软件工程其实早有答案单元测试、集成测试、测试夹具、CI 流水线。但 LLM 应用把这三件事都变难了因为它没有“确定性的预期输出”。你不能写一个assert response OK就算完事你需要回答的是模型输出好还是坏、相似还是不同、是否包含了幻觉内容、是否遵守了输出格式。这就是为什么现在“模型选型”已经不是 AI 应用的第一瓶颈。可以这样判断只要团队还在靠“人工多测几条样例”来验收 AI 功能那无论模型多强工程质量都不会稳定。而 harness engineering正是把“人工多测几条”变成“工程化可重复验证”的那一层能力。2. 什么是 harness engineering给 AI 应用夹上测试台架Harness 这个词最早来自硬件测试场景中文可以理解为“测试夹具”或“测试台架”。你去查汽车碰撞测试车本身不是直接开上街去撞的而是被固定在一套复杂的台架上周围布置各种传感器、假人、高速相机碰撞过程被反复录制、量化和对比。这里有一个关键区分台架不是被测对象而是让被测对象可以被科学测试的整套辅助系统。LLM 应用里的 harness engineering 也是如此。被测对象不是一行 prompt也不是一个模型而是由“系统提示词 用户输入 工具调用 上下文内容 模型参数”共同构成的动态系统。要稳定地测它你需要至少四层能力。第一层样本与期望管理。你得有一批有代表性、有标注、可持续追加的评测样本。每一条样本对应什么输入、期望什么行为、哪些语义必须出现、哪些内容不能出现都要有结构化管理。第二层执行编排。跑评测不是简单地写个 for 循环调 API。你需要处理并发限制、超时、重试、模型切换、不同运行模式本地 mock、测试环境、生产样本之间的隔离。第三层依赖隔离与替身。AI 应用在生产环境中会调用数据库、搜索接口、外部工具。如果每一次评测都依赖真实上游那么评测结果会因为上游抖动而抖动。Harness 需要能注入固定数据、mock 工具返回、模拟真实但可控的环境。第四层观测与归档。每次运行都应记录模型输入输出、token 消耗、延迟、prompt 版本、模型版本、样本命中情况。这些记录是未来做回归对比、badcase 分析、成本优化的重要基础。值得注意的是不要把 harness engineering 理解成一个新的测试框架。你可以用 pytest、可以用 LangSmith、可以用自己团队的工具但关键是团队是否具有这种工程能力。没有夹具的评测是脆弱的没有归档的评测是失忆的。维度传统单元测试AI EvalHarness Engineering被测对象确定性的函数或模块模型或应用的输出质量Prompt、模型、工具、上下文组成的动态系统预期结果固定值或固定行为概率性正确需要指标描述可重复执行、可对比基线的整套评测流程主要目标防止代码逻辑回归衡量模型效果好坏让评测稳定、低成本、能进 CI常见交付物测试用例和断言指标报告样本集、缓存、替换层、归档、幂等执行机制3. 为什么“evals 第一harness 第二”Elvis Saravia 把 harness engineering 排在第二位后面还有一层潜台词AI 工程师技能栈中最优先的事情永远是先想清楚你要评测什么其次才是怎么稳定地把评测跑起来。3.1 Evals 解决的是“目标”问题Evals 的本质是定义“什么是好”。比如客服工单分类场景我们不仅要看准确率还要看账单类错误是否被误判为账号类、关键实体是否正确提取、风险场景是否被漏掉。没有 evals你连“这版 prompt 更好还是更差”都无法回答。这也是为什么很多大模型团队会反复强调“先做 eval set再调 prompt”。目标是第一位的。3.2 Harness 解决的是“可重复执行”问题但只有 eval 还不够。场景会问你凭什么相信这次 eval 的结果许多团队已经有 eval 脚本但它们往往是 Jupyter Notebook 或临时 Python 脚本存在作者电脑里。跑一次要人工看输出参数靠手改样本不统一结果没有存档。换一个人跑结果就可能不一样换一个模型没人知道整体影响。这是典型的“有 eval但没 harness”。Harness 补上的是 eval 背后的工程质量样本放进版本库、代码可复用、运行环境可重复、mock 与真实模型可切换、输出结果自动生成报告并能与历史基线对比。没有 harness 的 eval只是实验结果有了 harness 的 eval才是工程验收。3.3 两者构成一个闭环实践中最顺的链路是这样的从真实业务日志或用户反馈中采集代表性样本定义分类标签、期望输出格式、边界规则形成 eval set用 harness 统一执行批量评测记录指标和样本明细将结果归档为基线修改 prompt 或调整模型后重新执行对比新结果与基线输出差异报告决定是否发布。Evals 决定了闭环的“目标”harness 决定了闭环能不能“重复转起来”。如果你想成为一个能对模型应用质量负责的 AI 工程师这两项缺一不可。4. 从岗位热词看 AI 工程师的技能栈变化最近关于“AI 出来后前端工程师是不是没了”的讨论很多背后其实折射出一个更真实的趋势AI 正在把很多工程岗位从“写代码”推向“定义和验证智能行为”。算法工程师、AI 平台工程师、AI 测试工程师、AI 运维工程师、AI 应用工程师这些新 title 的边界还很模糊但有一个共同交集开始变得清晰——谁能让模型行为可解释、可评测、可回归谁就掌握了话语权。角色方向核心职责与 harness engineering 的关系AI 算法工程师模型微调、能力优化需要评估集判断训练效果harness 是实验基线AI 应用工程师Prompt 编写、Agent 编排、工具接入需要为自己交付的应用写行为验收 harnessAI 测试工程师质量保障、风险发现需要把测试思维嵌入 eval set 和回归流程AI 运维工程师模型上线、灰度、稳定性需要对线上输出做采样评测形成轻量上线 harnessAI 平台工程师构建团队公共评测与可观测平台harness 常常是平台内最核心的模块之一现在的现实是AI 算法工程师往往懂 eval但对 harness 里工程化、可重复、成本控制这些事不够敏感AI 应用工程师擅长写 prompt但对“建立行为回归护栏”没有概念AI 测试工程师熟悉传统测试方法论但需要补上 LLM 概率性输出的评估经验。因此harness engineering 不专属于某一个岗位它是一种横向能力。就像 DevOps 不只是一个职位而是一套协作文化。未来团队里最吃香的人未必是调 prompt 最快的人而是能把 LLM 应用质量变成可验证工程资产的人。当公司决定把模型从 A 换成 B谁能快速给出“切换后行为差异报告”谁做的事情就不容易被替代。5. 最小可落地示例写一个能跑通的 LLM Harness为了把概念落到代码里这里做一个非常小的演示一个客服工单分类应用目标是判断用户输入属于 billing、technical、account 还是 other。我们会给这个应用写一个最小 harness包含样本文件、真实模型调用、mock 运行模式、结果报告生成。先看一下目录结构llm_harness_demo/ ├── src/ │ └── llm_app/ │ └── classifier.py ├── tools/ │ └── harness/ │ └── run_harness.py ├── tests/ │ └── samples/ │ └── customer_service.json ├── reports/ └── requirements.txt5.1 待测应用一个简单的工单分类器# src/llm_app/classifier.py import json from typing import Any from openai import OpenAI SYSTEM_PROMPT 你是一个客服工单分类器。 请把用户问题划分为以下类别之一 - billing账单、扣款、退款、支付相关 - technical产品功能报错、接口异常、系统崩溃 - account登录、账号、密码、权限相关 - other其他或无法明确归类 只输出 JSON不要输出多余文字格式如下 {category: ..., reason: 简要判断原因} def _extract_json(text: str) - dict[str, Any]: try: start text.index({) end text.rindex(}) return json.loads(text[start : end 1]) except (ValueError, json.JSONDecodeError): return {} def classify_inquiry(client: OpenAI, inquiry: str, model: str) - dict[str, Any]: 调用大模型对用户问题进行工单分类。 返回 dict 中至少包含 category、reason、tokens 三个字段 方便 harness 统一做断言和统计。 response client.chat.completions.create( modelmodel, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: inquiry}, ], temperature0, ) raw_text response.choices[0].message.content or parsed _extract_json(raw_text) usage response.usage return { category: parsed.get(category, other), reason: parsed.get(reason, ), tokens: usage.total_tokens if usage else 0, raw_response: raw_text[:200], }这里的核心设计是业务代码被封装成一个可调用的函数不直接在 prompt 里做评测逻辑。Harness 只关心这个函数的输入和输出不关心它内部是不是用了最新的大模型。这样后续才能在 harness 层自由切换真实模型和 mock。5.2 定义一份最小评测样本{ desc: 客服工单分类最小回归集, cases: [ { id: case001, inquiry: 我同一个订单被扣了两次款怎么申请退款, expected_category: billing, reason_hint: 扣款 }, { id: case002, inquiry: 每次登录都提示密码错误但我肯定没改过密码, expected_category: account, reason_hint: 登录 }, { id: case003, inquiry: 调用你们的 API 一直返回 500是不是接口崩了, expected_category: technical, reason_hint: 接口 }, { id: case004, inquiry: 想改一下绑定的手机号在设置里找不到入口, expected_category: account, reason_hint: 账号 }, { id: case005, inquiry: 你好在吗, expected_category: other, reason_hint: } ] }样本里面有两个东西值得注意expected_category是硬性分类期望reason_hint是对模型推理理由的弱约束。真实场景里你可以增加更多的断言维度比如“回复中不得出现某类词汇”“必须引用某个上下文片段”“禁止编造订单号”等。5.3 编写最小 Harness Runner# tools/harness/run_harness.py import argparse import json import os import sys import time from pathlib import Path from typing import Any ROOT Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from openai import OpenAI from src.llm_app.classifier import classify_inquiry def build_args(): parser argparse.ArgumentParser(descriptionMinimal LLM harness runner) parser.add_argument(--samples, typePath, requiredTrue, help评测样本 JSON 文件路径) parser.add_argument(--model, defaultgpt-4o-mini, help模型名称以实际可用模型为准) parser.add_argument(--report, typePath, defaultPath(reports/harness_report.json), help结果报告输出路径) parser.add_argument(--mock, actionstore_true, help使用 mock 模式不真实调用模型) parser.add_argument(--sleep, typefloat, default0.0, help每次调用后的休眠秒数用于限流) return parser.parse_args() def load_samples(path: Path) - list[dict[str, Any]]: with open(path, encodingutf-8) as f: data json.load(f) return data.get(cases, []) def mock_classify(inquiry: str) - dict[str, Any]: 本地 mock 版本用于不调用模型时验证 harness 流程。 text inquiry.lower() if any(k in text for k in [扣款, 退款, 账单, charge, billing]): return {category: billing, reason: 订单存在重复扣款建议走退款流程, tokens: 0} if any(k in text for k in [登录, 密码, 账号, login, password]): return {category: account, reason: 用户登录或账号相关问题, tokens: 0} if any(k in text for k in [api, 接口, 报错, error, 崩溃]): return {category: technical, reason: 系统接口或服务异常, tokens: 0} return {category: other, reason: 无法明确归类的用户输入, tokens: 0} def run_one_case(case: dict[str, Any], client: OpenAI | None, model: str, mock: bool): start time.time() if mock: prediction mock_classify(case[inquiry]) else: if client is None: raise RuntimeError(client must be initialized when mock is False) prediction classify_inquiry(client, case[inquiry], model) latency_ms (time.time() - start) * 1000 category_ok prediction.get(category) case.get(expected_category) reason_hint case.get(reason_hint, ) if reason_hint: reason_ok reason_hint in (prediction.get(reason) or ) else: reason_ok True passed category_ok and reason_ok return { id: case.get(id), inquiry: case[inquiry], expected_category: case.get(expected_category), actual_category: prediction.get(category), passed: passed, latency_ms: round(latency_ms, 2), tokens: prediction.get(tokens, 0), reason: prediction.get(reason, ), } def main(): args build_args() cases load_samples(args.samples) client None if not args.mock: if not os.environ.get(OPENAI_API_KEY): print(Missing OPENAI_API_KEY: 真实模型模式需要设置环境变量) sys.exit(2) client OpenAI() results [] for idx, case in enumerate(cases): if args.sleep and idx 0: time.sleep(args.sleep) results.append(run_one_case(case, client, args.model, args.mock)) total len(results) passed sum(1 for r in results if r[passed]) total_tokens sum(r[tokens] for r in results) avg_latency sum(r[latency_ms] for r in results) / total if total else 0 metrics { total_cases: total, passed_cases: passed, failed_cases: total - passed, pass_rate: round(passed / total, 4) if total else 0, total_tokens: total_tokens, avg_latency_ms: round(avg_latency, 2), } report { mode: mock if args.mock else live, model: args.model if not args.mock else mock-classifier, metrics: metrics, cases: results, } args.report.parent.mkdir(parentsTrue, exist_okTrue) with open(args.report, w, encodingutf-8) as f: json.dump(report, f, ensure_asciiFalse, indent2) print(json.dumps(report, ensure_asciiFalse, indent2)) if passed total: print([harness] all cases passed) sys.exit(0) else: print([harness] some cases failed) sys.exit(1) if __name__ __main__: main()这段代码虽短但已经把 harness 的几个核心部件带出来了样本加载与断言规则组合expected_category做严格断言reason_hint做语义弱断言运行模式切换--mock和真实模型调用可以自由切换保证离线跑通和在线评估两不误观测数据归档每次运行都会把每一条样本的输入、输出、延迟、token 消耗记录成 JSON 报告退出码设计全部通过返回 0有失败返回 1方便接入 CI 流水线。6. 运行与验证如何判断 Harness 真正可用先把 mock 模式跑通它不消耗 token也能验证整个链路没有写错。python tools/harness/run_harness.py \ --samples tests/samples/customer_service.json \ --mock \ --report reports/harness_report.json如果一切正常你会看到终端输出末尾包含类似内容total_cases: 5, passed_cases: 5, failed_cases: 0, pass_rate: 1.0 [harness] all cases passedmock 模式的意义不是证明模型能力而是证明你的 harness 本身跑得通、样本格式正确、报告能生成。很多人一上来就跑真实模型结果代码报错都没定位到是 harness 写错了还是模型真不行这是很低效的调试方式。mock 跑通后再设置环境变量并运行真实模型模式export OPENAI_API_KEY你的密钥 python tools/harness/run_harness.py \ --samples tests/samples/customer_service.json \ --model gpt-4o-mini \ --report reports/harness_report.json注意这里模型名只是示例具体名称以你实际申请到的模型为准。如果真实模型模式下发生失败先做三件事打开reports/harness_report.json看每一条 case 的actual_category和reason判断是分类错还是理由不充分把失败的用户输入单独提取出来直接在对话界面里复测排除 harness 代码本身的 bug再检查是不是模型输出格式没被正确解析比如返回了 json 以外的额外说明文字导致_extract_json解析结果为空值。从工程经验看早期 harness 最常见的问题不是模型能力不够而是解析不健壮、样本期望标注错误、mock 与真实模型行为差距过大。先把这三类问题排除再谈调 prompt 优化效果。7. 把 Harness 接进工程流水线从一次性脚本到团队资产能手动跑通之后下一步是把 harness 变成 AI 应用交付流程的一部分。否则它还是躺在你电脑里的脚本不是团队资产。7.1 用 Makefile 固化常用命令# Makefile .PHONY: harness-mock harness-live harness-mock: python tools/harness/run_harness.py \ --samples tests/samples/customer_service.json \ --mock \ --report reports/harness_report.json harness-live: python tools/harness/run_harness.py \ --samples tests/samples/customer_service.json \ --model $(EVAL_MODEL) \ --report reports/harness_report.jsonmake harness-mock EVAL_MODELgpt-4o-mini make harness-live7.2 最小 GitHub Actions 工作流接入 CI 时要注意成本策略不要每次 PR 都跑大而全的真实模型评测。更稳妥的做法是PR 阶段只跑 mock 链路确认代码没写坏、样本能加载、报告能生成主分支或定时任务再跑真实模型评测并自动对比历史报告重大 prompt 改动时由触发人手动跑完整数据集。# .github/workflows/harness-ci.yml name: llm-harness-ci on: pull_request: push: branches: [main] workflow_dispatch: jobs: harness: runs-on: ubuntu-latest env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} steps: - name: Checkout uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv5
返回列表