基于断言的提示词测试
使用 contains()、正则表达式、JSON 模式和 LLM 评审来检查输出。
基于断言的提示词测试 是 CoddyKit 上的免费 AI Prompt Engineering 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Prompt Engineering 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Prompt Engineering 课程共包含 4 节课。
对 LLM 输出进行断言
基于断言的测试应用于 LLM 时,遵循与单元测试相同的原则:明确声明输出必须包含或不得包含哪些内容,并在声明被违反时立即失败。
与使用确定性函数的单元测试不同,LLM 断言处理的是概率性文本输出,因此需要更灵活的断言类型:contains、matches_schema、satisfies_regex、llm_judge_score_above。
基础断言:contains 和 not_contains
最简单的断言会检查关键词是否存在或缺失。这些断言非常适合分类任务、结构化输出和安全检查。
import openai
client = openai.OpenAI(api_key='sk-...')
def call_prompt(system, user, temperature=0):
resp = client.chat.completions.create(
model='gpt-4o',
messages=[
{'role': 'system', 'content': system},
{'role': 'user', 'content': user}
],
temperature=temperature
)
return resp.choices[0].message.content
# Keyword presence assertion
def assert_contains(output, keyword, case_sensitive=False):
text = output if case_sensitive else output.lower()
kw = keyword if case_sensitive else keyword.lower()
assert kw in text, f'Expected "{keyword}" in output, got: {output[:100]}'
# Keyword absence assertion
def assert_not_contains(output, forbidden, case_sensitive=False):
text = output if case_sensitive else output.lower()
kw = forbidden if case_sensitive else forbidden.lower()
assert kw not in text, f'Forbidden "{forbidden}" found in output: {output[:100]}'JSON 架构验证
当您的提示词应返回结构化 JSON 时,请根据架构验证输出。架构验证失败意味着提示词存在格式问题——可能是模型添加了说明文字,也可能是 JSON 结构不正确。
import json
from jsonschema import validate, ValidationError
PRODUCT_SCHEMA = {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'price': {'type': 'number', 'minimum': 0},
'available': {'type': 'boolean'}
},
'required': ['name', 'price', 'available'],
'additionalProperties': False
}
def assert_valid_json_schema(output, schema):
try:
data = json.loads(output.strip())
except json.JSONDecodeError as e:
raise AssertionError(f'Output is not valid JSON: {e}\nOutput: {output[:200]}')
try:
validate(instance=data, schema=schema)
except ValidationError as e:
raise AssertionError(f'JSON does not match schema: {e.message}\nOutput: {output[:200]}')
return data
# Test
output = call_prompt(
'Extract product info as JSON: {"name": ..., "price": ..., "available": ...}',
'Widget Pro costs $49.99 and is in stock.'
)
product = assert_valid_json_schema(output, PRODUCT_SCHEMA)
print('Parsed product:', product)正则表达式匹配
正则表达式断言可以精确验证输出格式,适用于应遵循特定模式的输出,例如日期、电话号码或结构化代码。
import re
def assert_matches_regex(output, pattern, flags=0):
if not re.search(pattern, output, flags):
raise AssertionError(
f'Output does not match pattern /{pattern}/\nOutput: {output[:200]}'
)
def assert_output_is_label(output, valid_labels):
cleaned = output.strip().upper()
assert cleaned in valid_labels, (
f'Expected one of {valid_labels}, got: {repr(cleaned)}'
)
# Examples
output = call_prompt('Classify sentiment as POSITIVE, NEGATIVE, or NEUTRAL:', 'Great product!')
assert_output_is_label(output, {'POSITIVE', 'NEGATIVE', 'NEUTRAL'})
date_output = call_prompt('Extract the date in YYYY-MM-DD format:', 'Meeting on November 15, 2024')
assert_matches_regex(date_output, r'^\d{4}-\d{2}-\d{2}$')LLM 评审评分
对于开放式输出,请使用第二次 LLM 调用来评估质量。这称为 LLM 评审。评审模型会接收原始提示词、输出和评估标准,然后返回分数。
def llm_judge_score(original_prompt, output, criteria, max_score=10):
judge_prompt = (
f'Evaluate the following AI response on a scale of 1-{max_score}.\n'
f'Evaluation criteria: {criteria}\n\n'
f'Original prompt: {original_prompt}\n\n'
f'AI response: {output}\n\n'
f'Return only a number from 1 to {max_score}.'
)
resp = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': judge_prompt}],
temperature=0
)
score_text = resp.choices[0].message.content.strip()
return int(score_text)
def assert_llm_score_above(original_prompt, output, criteria, min_score=7):
score = llm_judge_score(original_prompt, output, criteria)
assert score >= min_score, f'LLM judge score {score} < minimum {min_score}'使用 pytest 进行提示词测试
pytest 是标准的 Python 测试框架,非常适合提示词测试。每个测试函数对应一个测试用例。pytest 会自动收集、运行并报告测试结果。
# test_sentiment_prompt.py
import pytest
import openai
client = openai.OpenAI(api_key='sk-...')
SYSTEM_PROMPT = 'Classify the sentiment as POSITIVE, NEGATIVE, or NEUTRAL. Return only the label.'
def classify(text):
resp = client.chat.completions.create(
model='gpt-4o',
messages=[
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': text}
],
temperature=0
)
return resp.choices[0].message.content.strip().upper()
# pytest automatically discovers functions starting with test_
def test_positive_sentiment():
assert classify('I love this product!') == 'POSITIVE'
def test_negative_sentiment():
assert classify('Terrible experience.') == 'NEGATIVE'
def test_neutral_sentiment():
assert classify('It arrived on time.') == 'NEUTRAL'
# Run: pytest test_sentiment_prompt.py -vpytest 中的参数化测试
使用 @pytest.mark.parametrize,可以让同一个测试函数处理多个输入,而无需重复编写代码。这是构建全面测试套件最简洁的方式。
# test_sentiment_parametrized.py
import pytest
TEST_CASES = [
('I love this!', 'POSITIVE'),
('Worst purchase ever.', 'NEGATIVE'),
('It works.', 'NEUTRAL'),
('Amazing!', 'POSITIVE'),
('Terrible!', 'NEGATIVE'),
('OK I guess.', 'NEUTRAL'),
]
@pytest.mark.parametrize('text,expected', TEST_CASES)
def test_sentiment_classification(text, expected):
result = classify(text)
assert result == expected, f'For "{text}": expected {expected}, got {result}'
# pytest test_sentiment_parametrized.py -v
# Output shows each test case individually:
# PASSED test_sentiment_parametrized.py::test_sentiment_classification[I love this!-POSITIVE]
# PASSED test_sentiment_parametrized.py::test_sentiment_classification[Worst purchase ever.-NEGATIVE]用于共享提示词状态的测试固件
使用 pytest 的测试固件,在多个测试之间共享开销较大的准备工作,例如每个测试会话只加载一次提示词模板或创建一次 API 客户端。
# conftest.py — fixtures available to all test files in the directory
import pytest
import openai
@pytest.fixture(scope='session')
def llm_client():
return openai.OpenAI(api_key='sk-...')
@pytest.fixture(scope='session')
def sentiment_prompt():
with open('prompts/sentiment_v3.txt') as f:
return f.read()
# test_sentiment.py
def test_positive_with_fixture(llm_client, sentiment_prompt):
resp = llm_client.chat.completions.create(
model='gpt-4o',
messages=[
{'role': 'system', 'content': sentiment_prompt},
{'role': 'user', 'content': 'I love this!'}
],
temperature=0
)
assert 'POSITIVE' in resp.choices[0].message.content.upper()处理不稳定的测试
LLM 的输出具有概率性——即使温度设置为 0,不同的模型部署或版本也可能产生不同的输出。请使用重试逻辑和容差阈值来处理这种不稳定性。
import pytest
def run_with_retry(fn, n=3):
'''Run fn up to n times, pass if any run succeeds.'''
failures = []
for _ in range(n):
try:
fn()
return # passed
except AssertionError as e:
failures.append(str(e))
raise AssertionError(f'Failed all {n} attempts. Last: {failures[-1]}')
def test_positive_with_retry():
def check():
result = classify('I love this!')
assert result == 'POSITIVE'
run_with_retry(check, n=3)
# Or use pytest-retry plugin:
# @pytest.mark.flaky(reruns=3)
# def test_positive_sentiment():
# assert classify('I love this!') == 'POSITIVE'测试性能与成本
每个测试用例都会发起一次 API 调用——100 个测试用例按每次调用 0.005 美元计算,每次完整测试运行的成本为 0.50 美元。管理成本的策略:
- 为静态测试输入缓存响应,并在持续集成中从缓存运行
- 每晚运行完整测试套件;每次 PR 只运行一个冒烟测试子集(10 个用例)
- 大多数测试使用更便宜的模型(gpt-4o-mini);只有回归测试套件使用 gpt-4o
import hashlib, json
RESPONSE_CACHE = {}
def cached_classify(text, use_cache=True):
key = hashlib.md5(text.encode()).hexdigest()
if use_cache and key in RESPONSE_CACHE:
return RESPONSE_CACHE[key]
result = classify(text)
RESPONSE_CACHE[key] = result
return result
# Persist cache to disk for CI
def load_cache(path='test_cache.json'):
global RESPONSE_CACHE
try:
with open(path) as f:
RESPONSE_CACHE = json.load(f)
except FileNotFoundError:
RESPONSE_CACHE = {}
def save_cache(path='test_cache.json'):
with open(path, 'w') as f:
json.dump(RESPONSE_CACHE, f, indent=2)测试输出报告
pytest 会生成详细报告,突出显示哪些测试用例失败以及失败原因。使用 pytest --tb=short -v 获取简洁的失败信息。在持续集成中,使用 --junitxml 生成 JUnit XML 报告,该报告兼容 GitHub Actions、GitLab CI 和 Jenkins。
# Run test suite and generate reports
# In terminal:
# pytest tests/prompt/ -v --tb=short --junitxml=test_results.xml
# In Python (for programmatic use):
import subprocess
def run_prompt_tests(test_dir='tests/prompt'):
result = subprocess.run(
['pytest', test_dir, '-v', '--tb=short', '--junitxml=test_results.xml'],
capture_output=True, text=True
)
print(result.stdout)
if result.returncode != 0:
print('TESTS FAILED')
print(result.stderr)
return result.returncode == 0
passed = run_prompt_tests()知识检查
在提示词测试中,什么时候应使用LLM 评审评分,而不是精确匹配断言?
回顾:基于断言的提示词测试
LLM 输出的主要断言类型:
- contains / not_contains:关键词是否存在,适合标签和安全检查
- JSON 架构验证:验证结构化输出格式
- 正则表达式匹配:验证特定模式(日期、代码)
- LLM 评审:评估开放式文本的质量
使用 pytest 和 @pytest.mark.parametrize 构建简洁、可扩展的测试套件。缓存响应以控制成本。每次 PR 运行冒烟测试子集;每晚运行完整测试套件。下一课:跨模型更新进行回归测试。
常见问题解答
「基于断言的提示词测试」课时是免费的吗?
是的 — 「基于断言的提示词测试」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Prompt Engineering 课程的其余内容,请升级到 CoddyKit PRO。 AI Prompt Engineering 课程共包含 4 节课。
「基于断言的提示词测试」这节课中我会学到什么?
使用 contains()、正则表达式、JSON 模式和 LLM 评审来检查输出。 你通过在浏览器中直接运行的动手代码来练习 AI Prompt Engineering,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 AI Prompt Engineering 需要有经验吗?
无需任何先前经验。CoddyKit 上的 AI Prompt Engineering 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「基于断言的提示词测试」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 AI Prompt Engineering 课中编写并运行代码吗?
能。每节 AI Prompt Engineering 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 编写提示词测试用例
- 基于断言的提示词测试
- 跨模型更新的回归测试
- 构建提示词测试套件