用Python实现AI声明探针:让模型行为可验证、可回归
2026/8/29 16:11:22 网站建设 项目流程

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_KEY=sk-your-key OPENAI_BASE_URL=https://api.openai.com/v1 AI_MODEL=gpt-4o-mini PROBE_TIMEOUT=30

其中 OPENAI_BASE_URL 允许接兼容 OpenAI 协议的模型服务或本地推理服务。AI_MODEL 是探针默认模型,也可以在 claims.json 的每条探针里单独覆盖。PROBE_TIMEOUT 控制每次调用的最大等待秒数,避免某个模型服务无响应时卡住整个页面。

安装依赖使用 pip:

pip 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_factory=ProbeExpectation) 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, f"JSON 解析失败: {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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询