AI断言+可重跑探针:把模型评测变成可验证的工程实践

发布时间:2026/8/29 19:17:57
AI断言+可重跑探针:把模型评测变成可验证的工程实践 如果你经常逛 AI 圈子大概率遇到过这种场景某个新模型发布官方演示视频里效果惊艳你满怀期待地注册 API、复现同样的提示词结果和宣传完全不是一回事。问题未必是模型真的差而是你看到的只是一个“断言”——一句关于模型能力的口头承诺背后没有任何可以自己动手验证的路径。这个项目的思路非常朴素却恰好击中了 AI 评测领域最致命的痛点把页面上每一条关于 AI 的断言都绑定一个公开的、可重新运行的探针probe。读者不再需要相信“我们测过了效果很好”而是可以直接把探针拉到本地或者 CI 里重跑一遍亲眼看到这个断言在当前模型版本、当前参数配置下的真实表现。这篇文章我会从理念到落地把这个思路完整展开先讲为什么“可重新运行的探针”比跑分截图更有价值再讲探针该怎么设计最后用完整的代码示例搭建出一个“断言 探针 自动运行 页面展示”的验证系统。读完你不仅能理解这套思路还能直接把它用到自己的 AI 产品、模型评测和团队协作中。1. 为什么需要给 AI 断言配一个可重跑的探针AI 行业现在最不缺的就是声称。跑分表、演示视频、宣传文案每天都在制造新的“最强模型”。但这些声称有一个共同问题它们大多是不可复现的一次性快照。你看到某个模型在某份评测集上准确率 90%但你不知道用的训练数据、微调数据是什么评测时温度参数是多少是否多次采样取最优同样的模型版本换一个环境和依赖是否还能复现你实际业务里的提示词风格和官方评测集差异有多大。这些信息在大多数宣传材料里都是缺失的。结果就是开发者在选型时只能凭感觉、凭口碑、凭偶然刷到的文章做判断等接入生产环境后才发现模型在某些边界场景完全不可用。“AI 断言 可重跑探针”解决的正是这个信任问题。它把抽象的宣传话语翻译成一组可以自动执行、自动判定通过或失败的探针任务。读者不需要信任作者只需要自己跑一遍。用一个软件工程里更熟悉的比喻传统 benchmark 相当于给你一份期末考试成绩单而带探针的断言页面相当于把考试题目和判卷标准都公开你随时可以重新组织一次考试。前者看结果后者看可复现性。在工程领域只有可复现的结论才值得被信赖。这个思路背后其实还有一层更深的含义AI 评测正在从“一份静态报告”走向“一套持续运行的测试系统”。模型每更新一个版本探针就重新跑一遍所有结论自动更新。这本质上是在把模型能力验证工程化、回归测试化。谁先建立起这套机制谁就在 AI 选型和交付上掌握了主动权。2. 探针的核心构成与设计原则2.1 什么是探针“探针”这个词在 AI 可解释性研究里也有出现通常指用线性分类器去分析模型内部表征。本文说的探针不是那个意思而是软件测试意义上的探针任务一个可运行的、有明确输入输出、有明确判定标准的自动化评测单元。一条完整的 AI 断言页至少要包含以下四部分组成部分作用举例断言Claim描述模型具备某种能力“gpt-4o-mini 能正确数出 strawberry 中的 r 数量”探针Probe实际执行的任务代码调用模型 API输入提示词拿到回答判定标准Verdict自动判断通过/失败回答是否为数字 3复现方式Reproduce其他人重跑的命令bash scripts/run_probe.sh strawberry-count如果缺少判定标准探针就只能输出“模型说了什么”无法自动判断“模型说得对不对”。如果缺少复现方式探针就退化成一次性的实验脚本项目标题里“public probe you can rerun”的价值也就不存在了。2.2 探针的设计原则设计探针时应该遵循几个原则可证伪性。探针必须有可能失败。如果一个探针对任何模型输出都判定通过那它没有信息量。比如“模型能否生成一段文本”这种断言就太弱几乎总是通过而“模型能否生成一段符合 JSON 语法且包含指定字段的文本”就有明确的失败边界。确定性优先。AI 模型的输出天然具有随机性。同一个提示词、同一套参数两次运行结果可能不同。设计探针时要尽量固定 temperature、top_p、随机种子同时建议多次采样取通过率而不是一次运行定生死。成本可感知。每次调用模型 API 都要花钱。探针页面如果不控制运行频率和调用深度很容易变成一台烧钱机器。给每个探针设定合理的调用次数和超时时间是工程化必须考虑的事情。可审计性。探针结果必须记录模型版本、参数配置、运行时间、提示词内容。这样当结果发生变化时可以定位是模型升级了还是提示词被改动了还是依赖环境变了。没有这些元信息探针跑出来的“失败”是没有排查线索的。2.3 探针的分级机制在实际工程中不同探针的复杂度和成本差异很大建议分级管理级别定位运行频率典型任务L1 自检探针快速、低成本验证基础能力每天或每次 CI数数、格式校验、简单分类L2 标准探针覆盖核心业务场景每周或每次发布多轮对话、结构化输出、代码生成L3 深度探针高成本验证复杂能力按需触发长文档理解、多步推理、复杂 Agent 任务分级的好处是日常监控跑 L1版本发布前跑 L2深度调优时手动触发 L3。这样既保证了关注度又不会让成本失控。3. 整体架构与数据流一个完整的“断言 探针”验证系统从下到上分为四层探针层Probes存放每个探针的实现代码、说明文档、判定逻辑。执行层Runner遍历探针目录调用探针函数收集结果并写入 JSON。页面层Site读取结果 JSON渲染成公开页面。调度层CI定时或手动触发执行层自动更新页面。数据流向是断言提交者先写清“我声称模型能做什么”然后由工程人员实现对应的探针代码推送代码后 CI 自动运行所有探针结果写入results/目录再渲染成静态页面。读者访问页面时看到的不只是断言和结论还有完整的复现命令和原始输出。技术选型方面核心依赖只需要这几样Python 3.10统一管理探针脚本pytest用于代码类探针的断言OpenAI 兼容 API 客户端覆盖大部分主流模型服务Jinja2渲染 HTML 页面GitHub Actions做定时调度和自动部署。这套组合最大的优点是门槛低、好扩展。探针本质上一个 Python 函数新增探针只需要往probes/目录里加一个文件夹再把结果文件名写入流程即可。下面的章节我会从零开始搭建一个可运行的最小系统。如果你想跟着操作建议准备一个能调用 OpenAI 兼容接口的 API Key没有的话也可以用本地模型服务替代核心逻辑不变。4. 环境准备与项目初始化4.1 目录结构创建一个项目目录ai-claim-probe-site结构如下ai-claim-probe-site/ ├── probes/ │ ├── strawberry-count/ │ │ ├── probe.py │ │ └── README.md │ ├── json-format/ │ │ ├── probe.py │ │ └── README.md │ └── quicksort-code/ │ ├── probe.py │ └── README.md ├── results/ │ └── manifest.json ├── scripts/ │ ├── run_probes.py │ └── build_site.py ├── templates/ │ └── index.html.j2 ├── docs/ ├── requirements.txt ├── .env.example └── .gitignore各目录职责probes/每个子目录是一个独立探针子目录名是探针 ID。results/探针运行后生成的 JSON 结果manifest.json汇总全部结果。scripts/统一执行脚本和页面构建脚本。templates/HTML 页面模板。docs/构建后的静态页面方便部署到 GitHub Pages 或任意静态托管。4.2 依赖管理创建requirements.txtopenai1.30.0 jinja23.1.0 python-dotenv1.0.0 pytest8.0.0创建.env.exampleOPENAI_API_KEYsk-xxxx PROBE_MODELgpt-4o-mini PROBE_TIMEOUT60把.env加入.gitignore避免密钥泄露.env __pycache__/ *.pyc results/ docs/注意这里将results/和docs/加入忽略是合理的因为 CI 会自动生成它们如果你希望把结果也纳入版本管理可以自行调整。4.3 模型服务说明示例代码使用 OpenAI 兼容的 API 客户端环境变量OPENAI_API_KEY和OPENAI_API_BASE可以直接指向主流模型服务也可以指向私有化部署的模型网关。核心逻辑是统一的不会绑定特定厂商。5. 完整示例把三个 LLM 断言变成可重跑探针这一节是全文的核心。我用三个典型断言演示完整流程断言 A模型能正确输出strawberry中字母r的数量。断言 B模型能输出符合 JSON 语法且包含指定字段的内容。断言 C模型生成的 Python 排序函数能通过单元测试。这三个探针分别覆盖了基础问答、结构化输出、代码能力三种常见评测维度难度递增且都能自动化判定。5.1 探针统一结构约定每个探针目录下必须有probe.py并提供一个run_probe()函数。函数不接收参数返回(passed, detail)二元组passed布尔值表示探针是否通过detail字典记录输入、输出、判定依据等细节。5.2 探针 A数数任务文件路径probes/strawberry-count/probe.pyimport os from openai import OpenAI def run_probe(): client OpenAI( api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_API_BASE), ) prompt ( How many letters r are in the word strawberry? Answer only the number. ) response client.chat.completions.create( modelos.getenv(PROBE_MODEL, gpt-4o-mini), messages[{role: user, content: prompt}], temperature0.0, max_tokens10, ) answer response.choices[0].message.content.strip() try: number int(answer.split()[0]) passed number 3 except (ValueError, IndexError): passed False detail { prompt: prompt, answer: answer, expected: 3, } return passed, detail这段代码的逻辑很清楚调用模型让它只回答一个数字然后把输出转成整数和期望值 3 比较。这里选择temperature0.0是为了减少随机性让结果更稳定。注意strawberry中字母r实际出现了 3 次这个任务看似简单但对不少大模型来说仍然是边界场景。这就是一个典型的“宣传中很少提及、但实际使用会踩坑”的能力点。文件路径probes/strawberry-count/README.md## 断言 模型能够正确回答 strawberry 中字母 r 的数量。 ## 判定标准 如果模型输出为数字 3则通过否则失败。 ## 运行方式 bash scripts/run_probes.py strawberry-count5.3 探针 BJSON 格式约束文件路径probes/json-format/probe.pyimport json import os from openai import OpenAI def run_probe(): client OpenAI( api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_API_BASE), ) prompt ( Return a JSON object with the following keys: name, version, released, tags. The value of tags must be an array of strings. Use strict JSON only. ) response client.chat.completions.create( modelos.getenv(PROBE_MODEL, gpt-4o-mini), messages[{role: user, content: prompt}], temperature0.0, max_tokens200, ) raw response.choices[0].message.content try: obj json.loads(raw) required_keys {name, version, released, tags} passed required_keys.issubset(obj.keys()) and isinstance(obj.get(tags), list) except json.JSONDecodeError: passed False obj None detail { prompt: prompt, raw_output: raw, parsed_keys: list(obj.keys()) if obj else None, required_keys: sorted([name, version, released, tags]), } return passed, detail这个探针考察的是结构化输出稳定性。很多业务场景需要模型直接输出 JSON 供程序解析但模型偶尔会在 JSON 前后添加解释性文字导致json.loads失败。这个探针的判定标准是不仅 JSON 能解析成功还必须包含全部指定字段并且tags字段必须是数组类型。如果模型服务商支持强制 JSON 输出模式可以在请求参数里追加response_format{type: json_object}但不是所有模型网关都支持这个参数。为了兼容性这里的示例不依赖该参数判定完全靠json.loads完成。5.4 探针 C代码生成 单测文件路径probes/quicksort-code/probe.pyimport os import subprocess import tempfile from openai import OpenAI TEST_CODE def test_quicksort(): assert quicksort([3, 1, 2]) [1, 2, 3] assert quicksort([]) [] assert quicksort([5, 4, 3, 2, 1]) [1, 2, 3, 4, 5] assert quicksort([1]) [1] def run_probe(): client OpenAI( api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_API_BASE), ) prompt ( Write a Python function named quicksort that sorts a list of integers in ascending order. Only output the function code, without any explanation or Markdown code block markers. ) response client.chat.completions.create( modelos.getenv(PROBE_MODEL, gpt-4o-mini), messages[{role: user, content: prompt}], temperature0.2, max_tokens500, ) code response.choices[0].message.content with tempfile.TemporaryDirectory() as tmpdir: test_path os.path.join(tmpdir, test_sort.py) with open(test_path, w, encodingutf-8) as f: f.write(code) f.write(\n\n) f.write(TEST_CODE) result subprocess.run( [python, -m, pytest, test_path, -q, --disable-warnings], capture_outputTrue, textTrue, timeout30, ) passed result.returncode 0 detail { prompt: prompt, generated_code: code, stdout: result.stdout[:500], stderr: result.stderr[:200], } return passed, detail这个探针比前两个复杂一些关键点在于让模型只输出函数代码不要输出 Markdown 代码块标记把模型生成的代码和固定的测试代码写入同一个临时文件用 pytest 子进程执行测试通过返回码判断成功失败。这里有一个容易踩坑的地方如果只把模型输出写入测试文件可能出现“pytest 没有收集到任何测试”的情况导致返回码为 5被误判为失败。所以在文件末尾追加固定的TEST_CODE保证至少存在一个测试函数这样判定就只取决于模型函数实现是否正确。5.5 统一执行脚本文件路径scripts/run_probes.pyimport datetime import importlib.util import json import os import pathlib import sys PROJECT_ROOT pathlib.Path(__file__).resolve().parent.parent PROBES_DIR PROJECT_ROOT / probes RESULTS_DIR PROJECT_ROOT / results def load_probe_module(probe_id): probe_path PROBES_DIR / probe_id / probe.py spec importlib.util.spec_from_file_location(fprobe_{probe_id}, probe_path) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def run_single_probe(probe_id): module load_probe_module(probe_id) try: passed, detail module.run_probe() except Exception as exc: passed False detail {error: str(exc)} result { probe_id: probe_id, model: os.getenv(PROBE_MODEL, default), passed: passed, detail: detail, run_at: datetime.datetime.utcnow().isoformat() Z, reproduce: fpython scripts/run_probes.py {probe_id}, } os.makedirs(RESULTS_DIR, exist_okTrue) result_path RESULTS_DIR / f{probe_id}.json result_path.write_text( json.dumps(result, ensure_asciiFalse, indent2), encodingutf-8, ) print(f[{probe_id}] passed{passed}) return result def run_all_probes(): manifest [] for probe_dir in sorted(PROBES_DIR.iterdir()): if not (probe_dir / probe.py).exists(): continue result run_single_probe(probe_dir.name) manifest.append(result) manifest_path RESULTS_DIR / manifest.json manifest_path.write_text( json.dumps(manifest, ensure_asciiFalse, indent2), encodingutf-8, ) print(fAll probes done. Manifest written to {manifest_path}) if __name__ __main__: if len(sys.argv) 1: run_single_probe(sys.argv[1]) else: run_all_probes()这个脚本做了三件事遍历probes/下的目录动态加载每个探针的run_probe()执行探针格式化结果并写入results/生成汇总文件manifest.json。支持两种运行方式不带参数运行全部探针带探针 ID 运行单个探针。这样既方便本地排错也方便 CI 按需触发。5.6 页面渲染脚本文件路径scripts/build_site.pyimport json import pathlib from jinja2 import Template PROJECT_ROOT pathlib.Path(__file__).resolve().parent.parent RESULTS_DIR PROJECT_ROOT / results TEMPLATE_FILE PROJECT_ROOT / templates / index.html.j2 OUTPUT_DIR PROJECT_ROOT / docs def main(): manifest_path RESULTS_DIR / manifest.json manifest json.loads(manifest_path.read_text(encodingutf-8)) template Template(TEMPLATE_FILE.read_text(encodingutf-8)) html template.render(probesmanifest) OUTPUT_DIR.mkdir(exist_okTrue) output_path OUTPUT_DIR / index.html output_path.write_text(html, encodingutf-8) print(fSite built: {output_path}) if __name__ __main__: main()文件路径templates/index.html.j2!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleAI Claims Verification Page/title style body { font-family: -apple-system, Segoe UI, PingFang SC, sans-serif; max-width: 960px; margin: 0 auto; padding: 24px; } .probe { border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; margin: 16px 0; } .pass { color: #1a7f37; font-weight: bold; } .fail { color: #cf222e; font-weight: bold; } code { background: #f6f8fa; padding: 2px 6px; border-radius: 4px; } pre { background: #f6f8fa; padding: 12px; border-radius: 8px; overflow-x: auto; } /style /head body h1AI Claims with Public Probes/h1 p页面上的每一条 AI 断言都配有一个可以重新运行的公开探针。/p {% for probe in probes %} section classprobe h2{{ probe.probe_id }}/h2 p模型code{{ probe.model }}/code/p p结果strong class{{ pass if probe.passed else fail }}{{ PASS if probe.passed else FAIL }}/strong/p p运行时间{{ probe.run_at }}/p p复现命令code{{ probe.reproduce }}/code/p pre{{ probe.detail | tojson(indent2) }}/pre /section {% endfor %} /body /html5.7 CI 自动运行与部署文件路径.github/workflows/probes.ymlname: Run AI Probes on: schedule: - cron: 0 4 * * * workflow_dispatch: jobs: probe: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv5 with: python-version: 3.11 - name: Install dependencies run: | pip install -r requirements.txt - name: Run probes env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_API_BASE: ${{ secrets.OPENAI_API_BASE }} PROBE_MODEL: ${{ vars.PROBE_MODEL }} run: | python scripts/run_probes.py - name: Build site run: | python scripts/build_site.py - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pagesv4 with: publish_dir: ./docs github_token: ${{ secrets.GITHUB_TOKEN }}这个工作流做了四件事每天凌晨 4 点自动运行也支持手动触发安装 Python 依赖用 GitHub Secrets 中的 API Key 执行全部探针生成静态页面并推送到 GitHub Pages。如果你的仓库还未开启 Pages 服务需要在仓库 Settings → Pages 中选择 “Deploy from a branch”并把分支设为gh-pages。也可以换用actions/upload-pages-artifact等官方部署方式功能类似。6. 运行验证与效果解读6.1 本地运行全部探针先安装依赖pip install -r requirements.txt加载环境变量export OPENAI_API_KEYsk-xxxx export PROBE_MODELgpt-4o-mini运行全部探针python scripts/run_probes.py预期输出类似[strawberry-count] passedTrue [json-format] passedTrue [quicksort-code] passedFalse All probes done. Manifest written to results/manifest.json这里探针是否通过取决于你使用的模型版本和实际输出千万不要把上面结果当作固定结论。探针的意义是给出当前环境下的真实结果而不是预设答案。6.2 查看单个探针结果运行单个探针python scripts/run_probes.py strawberry-count查看生成的文件results/strawberry-count.json{ probe_id: strawberry-count, model: gpt-4o-mini, passed: true, detail: { prompt: How many letters r are in the word strawberry? Answer only the number., answer: 3, expected: 3 }, run_at: 2025-01-01T04:00:00Z, reproduce: python scripts/run_probes.py strawberry-count }6.3 构建页面python scripts/build_site.py打开docs/index.html可以看到每个探针的断言、运行结果、模型名、复现命令和原始输出。这是公开页面最核心的展示层。6.4 如何判断探针是否可信判断一个探针值不值得信重点看三个地方判定标准是否明确不是“看起来对”而是程序能自动判断对错输出细节是否完整能不能看到模型的原始回答复现命令是否简单别人能不能一键重跑。如果这三个条件都满足这个探针就是可验证的如果只给了结论没有代码和原始输出那它就只是一个带了截图的高级断言可信度有限。7. 常见问题与排查方法问题现象可能原因排查方式解决方案探针偶发失败重跑又通过模型采样存在随机性查看连续多次运行结果固定 temperature/seed多次采样取通过率API 返回 401 UnauthorizedAPI Key 未配置或配置错误检查 .env 或环境变量确认 Key 正确重新加载环境变量response_formatjson_object报错模型网关不支持该参数查看模型文档和报错信息移除该参数靠 json.loads 兼容解析pytest 返回码 5提示无测试收集模型生成的代码没有测试函数查看生成的临时文件名在探针中固定注入测试代码页面结果长期不变CI 定时任务失败或未启用 Pages查看 GitHub Actions 日志手动触发 workflow检查部署配置运行成本过高探针数量多或调用深度大查看 API 调用量和费用引入 L1/L2/L3 分级降低运行频率模型版本升级后结果变化模型行为跟随版本漂移查看结果文件中的 model 字段结果记录模型版本必要时固定模型版本本地复现时结果与 CI 不一致依赖版本或环境变量不同对比 requirements 和 .env使用同一份依赖锁文件统一环境8. 工程化最佳实践8.1 探针命名与文档规范探针 ID 建议采用行为场景-能力点的命名方式例如strawberry-count、json-format、quicksort-code。每个探针目录下必须有 README内容包括断言原文、判定标准、运行命令。这样读者不需要看源码也能理解这个探针在验证什么。8.2 结果记录元信息探针结果至少要包含模型名、模型版本、temperature、prompt、原始输出、运行时间、复现命令。没有这些信息一旦结果出问题根本没法归因。8.3 用通过率代替单次布尔值由于模型输出的随机性单次通过并不代表稳定。更稳妥的做法是每个探针重复采样 3 到 5 次记录“通过次数 / 总次数”。比如3/5表示五次采样中三次通过这比单个True或False更有参考价值。8.4 安全边界这是最容易忽略也最重要的一点API Key 绝不能提交到 Git 仓库必须通过环境变量或 GitHub Secrets 注入探针生成并执行的代码如代码类探针应运行在隔离环境中避免对宿主机造成影响探针涉及外部 API 时必须设置超时时间防止服务端长时间挂起。8.5 统一成本管理给每个探针添加budget字段标注预计调用次数和最大成本。CI 中可以增加一个总调用次数的统计超过阈值就告警。L1 探针每天跑L2 探针每周跑L3 探针按需跑这是最经济和有效的平衡。8.6 把 AI 评测当作回归测试来做模型服务商更新模型版本应该像依赖库升级一样触发回归全量重跑 L1 和 L2 探针对比新旧版本差异。如果某个断言从通过变成了失败就说明这次升级引入了行为回退。这套机制坚持跑三个月你会积累出一份非常有价值的模型行为变化日志远比任何宣传材料都更能指导选型。9. 总结与后续学习方向这个项目真正讲清楚的事情是AI 评测的信任基础不是跑分数字而是可复现的验证路径。每个断言配一个公开可重跑的探针等于把“信我”变成“你自己验”。这是把软件工程里的可重复性、可审计性、回归测试思想迁移到 AI 能力验证上的一个非常务实的方向。你可以从几个方向继续深入把探针从单个 API 调用扩展到多轮对话和 Agent 流程验证复杂任务链路引入更多数据样本把“通过/失败”升级为“通过率 置信区间”把探针接入内部 CI让模型选型、Prompt 优化、版本升级都有数据支撑用同样的页面结构建设团队内部的模型评测门户。如果现在你手头正在做 AI 项目建议先挑业务里最常踩坑的三个场景写成探针挂到 CI 上每天跑一遍。坚持两周你会发现对模型的判断依据已经从“看谁宣传得好”变成了“看谁经得起反复验证”。