AI 产品的宣传页总是充满各种声明AI claim支持长上下文、能稳定输出 JSON、可以理解复杂 SQL、代码补全准确率很高。声明本身并不难写难的是让每一个声明都经得起验证。更麻烦的是模型不断升级prompt 稍作调整判断标准也会漂移。如果某个声明在发布当天成立等到下次模型更新后可能已经不再成立。解决这个问题的一个做法是构建一个页面让每一条 AI 声明都配对一个可以重跑的公开探针public probe。读者打开页面后可以自己点击“重跑”立刻看到当前模型是否还满足这条声明。这里要实现的并不是一个复杂的评测平台而是一个能帮助团队持续核对 AI 声明的最小验证系统。你可以把它理解成一个介于“模型调用日志”和“标准评测集”之间的工具每条声明都对应一个探针probe探针里保存着输入 prompt、期望输出规则和运行参数。任何人打开页面都可以重跑某个探针系统会实时调用大模型并给出“通过 / 不通过 / 异常”三种结果。先解释这个思路的边界再给出完整的 Python 后端和前端实现。项目规模不大但足够说明如何把 AI 声明变成可执行的验证任务。1. 先理解“AI声明 公共探针”这个思路1.1 为什么要给 AI 声明配探针AI 声明总是以自然语言出现比如“支持高并发”“能处理 10 万 token 长文本”或者“不会编造不存在的 API”。自然语言的问题在于无法直接执行。你无法通过阅读文档来判断声明是否成立只能通过真实请求去验证。问题是手动的验证结果很难沉淀下次换一个模型版本、换一个 temperature 参数结果可能就变了。如果把声明改写成探针它就从“一句话”变成了“一个可执行的单元”。一条探针至少包含三部分输入prompt执行方式调用哪个模型、哪些参数验证规则输出应该符合什么条件。这样一来任何人不需要理解原始声明的上下文只要运行探针就能知道当前模型的表现。这种做法还有一个隐藏价值它可以作为模型升级前的回归测试。当你准备从模型 A 切到模型 B或者升级某个模型版本时先跑一遍现有探针能看到哪些声明仍然成立哪些已经不再成立。1.2 探针和传统自动化测试的区别很多人第一反应是把探针当成单元测试。思路有相似之处但目标不一样。单元测试针对的是我们自己写的代码输入输出是确定性的AI 探针针对的是模型行为模型输出本身具有随机性即使是同一个 prompt也可能返回不同文本。维度传统自动化测试AI 声明探针被测对象自己维护的代码块外部模型或 API 服务输出预期确定性结果允许变体需要容错断言运行环境本地或 CI 中确定依赖依赖模型版本、网络、超时参数失败归因通常是代码缺陷可能是 prompt、参数、模型版本或服务波动结果价值验证功能是否正常验证“声明是否仍然成立”表格里的差异决定了探针不能照搬单元测试的写法。比如断言不能只看字符串完全相等因为模型可能用不同措辞表达同一个意思探针结果必须记录模型名称和调用时间否则后续无法溯源。1.3 探针应该具备哪些能力结合上面的差异一个合格的 AI 声明探针应该具备几个能力可重跑即同一个探针可以被反复执行独立每条探针不依赖其他探针的状态公开页面上的探针定义和结果都应可见而不是只保留一个通过或失败的结论有明确断言不能只打印模型输出还要用规则判断是否通过有控制成本的手段调用模型会产生费用所以探针运行要限制输入长度、输出长度和超时时间有结果记录每次运行结果都能追溯。这些能力是后面代码实现的目标。如果探针缺少断言它只是一个调用示例如果缺少结果记录它只是一个在线体验工具只有把几个能力结合起来才称得上“每个声明都配对一个可重跑的公共探针”。2. 环境准备和项目骨架2.1 技术选型为了把读取探针、调用模型、保存结果、展示页面串起来选择 Python 技术栈最省事。Python 生态里有成熟的 OpenAI SDK也有轻量级的 Web 框架适合快速实现。下面的选型表可以按团队已有规范调整不构成唯一答案。用途选择原因语言Python 3.10类型注解、异步支持成熟示例代码可读Web 框架FastAPI自带请求校验和 OpenAPI 文档适合快速提供 JSON API运行服务器UvicornFastAPI 默认搭配启动简单大模型调用openai Python SDK兼容主流 OpenAI 接口也支持自定义 base_url页面模板Jinja2FastAPI/Starlette 原生支持示例更直观结果存储SQLite单机部署简单不需要额外数据库服务如果你的模型不是通过 OpenAI 兼容接口暴露可以把调用层替换成对应的 SDK。这里示例使用 openai 包但探针模型本身与具体 SDK 无关。2.2 创建项目和目录结构项目沿用常见结构文件比较少。建议按下面分布创建目录ai-claim-probe/ ├── app.py # FastAPI 应用 ├── claims.json # 探针声明数据 ├── requirements.txt # 依赖 ├── .env.example # 环境变量模板 ├── templates/ │ └── index.html # 页面 ├── static/ │ └── main.js # 页面交互脚本 └── data/ └── probe_results.db # SQLite 数据库运行时生成claims.json 是核心数据文件负责保存所有声明的探针定义。app.py 会读取它并在启动时初始化数据库。templates 和 static 目录用于页面展示。2.3 配置环境变量调用大模型时API Key 不能在页面里出现也不能硬编码到代码中。通过环境变量注入是安全且常见的做法。# .env.example OPENAI_API_KEYsk-your-key OPENAI_BASE_URLhttps://api.openai.com/v1 AI_MODELgpt-4o-mini PROBE_TIMEOUT30其中 OPENAI_BASE_URL 允许接兼容 OpenAI 协议的模型服务或本地推理服务。AI_MODEL 是探针默认模型也可以在 claims.json 的每条探针里单独覆盖。PROBE_TIMEOUT 控制每次调用的最大等待秒数避免某个模型服务无响应时卡住整个页面。安装依赖使用 pippip install fastapi uvicorn openai pydantic python-dotenv jinja2需要提醒的是openai SDK 版本更新较快示例代码中的 Client 初始化方式适合 openai 1.x如果使用其他版本要参考对应文档调整。3. 实现探针模型和声明数据3.1 定义数据模型为了让探针在代码里可以被严格校验先用 Pydantic 定义模型。这里使用 Pydantic v2 的写法和 v1 的字段定义方式略有差异。from typing import Any, Literal from pydantic import BaseModel, Field class ProbeExpectation(BaseModel): type: Literal[contains, json_schema, regex] contains value: str | None None # contains / regex 时的期望值 schema: dict[str, Any] | None None # json_schema 时的期望结构 class Probe(BaseModel): id: str category: str title: str claim: str prompt: str expectation: ProbeExpectation Field(default_factoryProbeExpectation) model: str | None None # 不填则使用全局默认模型 temperature: float 0.0 max_tokens: int 512字段里最关键的是 expectation。它把一个人工判断标准例如“输出应该包含 API 名称”转换成机器可执行规则。model 字段单独支持每条探针覆盖方便对比不同模型在同一声明上的表现。使用 Literal 限制 type 可选值可以避免运行时出现无法处理的断言类型。schema 用 dict 而不是特定 JSON Schema 类是因为这里只做最小校验不需要完整实现 JSON Schema 标准。3.2 编写一份探针数据集接下来在 claims.json 里预置几条探针。这里故意选择不同类别便于展示探针如何覆盖不同场景。[ { id: json-output, category: structured-output, title: 生成合法 JSON, claim: 在用户强制要求只输出 JSON 时模型返回的内容可以被 json.loads 解析。, prompt: 只输出一个 JSON 对象不要输出解释字段包括 name字符串和 age整数。, expectation: { type: json_schema, schema: { type: object, properties: { name: {type: string}, age: {type: integer} }, required: [name, age] } } }, { id: sql-join, category: sql, title: 根据中文需求生成 JOIN 查询, claim: 当用户用中文描述订单和用户关联查询时模型生成的 SQL 包含 JOIN 关键字。, prompt: 用 SQL 查询所有已下单用户的姓名和订单金额只需要 SQL不要解释。, expectation: { type: contains, value: JOIN } }, { id: code-completion, category: code, title: 补全 Python 函数体, claim: 给定函数签名和 docstring模型能补全 return 语句。, prompt: 补全以下 Python 函数\ndef add(a, b):\n \\\返回 a 和 b 的和。\\\\n, expectation: { type: contains, value: return } } ]数据集里的 claim 字段是给人类读的prompt 是给模型看的expectation 是给程序判断用的。三者分离后页面上可以正确展示“声明是什么”和“当前验证结果”逻辑不混淆。注意这些探针只是示例不代表任何模型一定能通过。实际部署时先用你的目标模型跑出基线再根据结果决定是否调整 prompt 或断言。3.3 断言机制如何设计断言不能写成一句神秘规则应该尽量贴近真实使用场景。下面实现三种类型。import json import re def check_expectation(output: str, expectation: ProbeExpectation) - tuple[bool, str]: if expectation.type contains: value expectation.value or return value in output, f输出中{包含 if value in output else 不包含} {value!r} if expectation.type regex: value expectation.value or matched re.search(value, output, re.S) return bool(matched), f正则{匹配 if matched else 不匹配} {value!r} if expectation.type json_schema: try: data json.loads(output) except json.JSONDecodeError as exc: return False, fJSON 解析失败: {exc} schema expectation.schema or {} props schema.get(properties, {}) required schema.get(required, []) missing [k for k in required if k not in data] if missing: return False, f缺少字段: {missing} for field, meta in props.items(): if field in data and type in meta: expected_type meta[type] actual_type type(data.get(field)).__name__ if expected_type string and not isinstance(data.get