Guide · 使用教程
Giskard v3 教程:从知识库建立问答回归测试
用四个合成输出准备金额、来源和退款承诺检查,提供完整代码与预期失败;本次已在固定版本 SDK 验证一条通过、三条预期失败,不代表真实模型质量。
这篇教程会完成什么
把一份合成政策整理成四条可重复的回归案例,再按 Giskard v3 Checks 的官方模式组织场景和断言。案例包括正确输出、错误金额、缺失来源和无依据退款承诺。目标是理解输入、检查和失败报告之间的关系。
本文已在 Python 3.12.13、Giskard 3.0.1 和 giskard-checks 1.0.6 下完成 SDK 实测:一个正确案例通过,三个固定反例失败,且无执行异常。原生结果与本地字段判断分别保存。没有调用真实回答模型或执行自动质量扫描,因此这些结果不代表 RAG 准确率。
开始前需要准备
官方 v3 迁移文档要求 Python 3.12 或以上。建议新建虚拟环境,避免把旧版 Giskard 2.x 混入同一环境。本次验证固定 Giskard 3.0.1、Checks 1.0.6,并保存完整依赖清单。团队和 CI 应复用同一份锁定结果,升级后重新验收。
教学案例使用合成信息,不需要真实客服记录,也不会调用模型服务。后面接入知识库自动扫描或模型评判时,需要另外配置对应服务和预算。不要把这个不含模型调用的演示理解成完整的事实质量评估。
第一步:建立隔离环境
以下命令中的 Python 版本应换成你已安装、符合官方要求的版本。
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install "giskard==3.0.1" "giskard-checks==1.0.6"
python -c "from importlib.metadata import version; print(version('giskard'))"
检查版本输出确实为 v3,再继续运行示例。若安装到了 2.x,不要直接尝试旧的 giskard.rag 教程;先核对环境的 Python 版本和官方安装说明。安装失败应保留原始错误,避免为了完成教程而关闭证书校验。
第二步:定义参考政策和反例
合成政策约定:基础套餐为 99 元,来源编号为 POLICY-DEMO-731,退款资格需要人工审核,助手不能保证退款成功。这里没有真实的交易承诺,金额和编号都只服务于教学。
四个案例只改变一个字段,便于观察失败原因。真实任务不能只保留这种简单反例,还需要覆盖多个条件相互作用、材料缺失、历史版本和不同表达方式。将错误输出用于测试时,也要明确标记,避免它们被误当成有效参考答案。
第三步:保存完整示例
将下面代码保存为 regression_demo.py。Giskard 按参数名注入输入,所以这里用 outputs=lambda inputs: inspect_answer(inputs) 适配业务函数;直接传入参数名为 answer 的函数会在场景构建时抛出 TypeError。inspect_answer 只检查显式字段:金额类型和值、来源编号是否出现、是否没有退款保证,以及是否要求人工审核。来源编号出现不等于来源内容真的支持回答;字段为假也不保证自由文本没有相反承诺,这些都需要后续检查。
"""Four local fixtures, verified with Giskard 3.0.1 / Checks 1.0.6."""
import asyncio
import json
from pathlib import Path
POLICY = {
"version": "demo-policy-2026-10",
"source_id": "POLICY-DEMO-731",
"plan_price_cny": 99,
"refund_requires_review": True,
}
BASE_ANSWER = {
"plan_price_cny": 99,
"citations": ["POLICY-DEMO-731"],
"refund_guaranteed": False,
"needs_human_review": True,
}
CASES = [
{"name": "correct", "answer": BASE_ANSWER.copy(), "expected_pass": True},
{"name": "wrong_amount", "answer": {**BASE_ANSWER, "plan_price_cny": 199}, "expected_pass": False},
{"name": "missing_source", "answer": {**BASE_ANSWER, "citations": []}, "expected_pass": False},
{"name": "unsupported_promise", "answer": {**BASE_ANSWER, "refund_guaranteed": True}, "expected_pass": False},
]
def inspect_answer(answer):
"""Only checks explicit fields; it does not understand free text."""
return {
"price_matches": type(answer.get("plan_price_cny")) is int
and answer["plan_price_cny"] == POLICY["plan_price_cny"],
"source_present": isinstance(answer.get("citations"), list)
and POLICY["source_id"] in answer["citations"],
"no_refund_guarantee": answer.get("refund_guaranteed") is False,
"review_required": answer.get("needs_human_review") is True,
}
async def main():
from giskard.checks import Equals, Scenario
from importlib.metadata import version
expected = {key: True for key in inspect_answer(BASE_ANSWER)}
rows = []
sdk_results = []
for case in CASES:
# This callable is a local fixture, not a live RAG application.
scenario = (
Scenario(case["name"])
.interact(inputs=case["answer"], outputs=lambda inputs: inspect_answer(inputs))
.check(Equals(
name="structured_policy_requirements",
expected_value=expected,
target_key="trace.last.outputs",
))
)
result = await scenario.run()
result.print_report()
sdk_results.append(result.model_dump(mode="json"))
# Save local field checks separately from Giskard's printed report.
checks = inspect_answer(case["answer"])
rows.append({
"case": case["name"],
"policy_version": POLICY["version"],
"field_checks": checks,
"fixture_expected_pass": case["expected_pass"],
"local_field_gate": all(checks.values()),
"sdk_status": result.status.value,
})
if result.errored or result.skipped:
raise RuntimeError(f"{case['name']}: SDK execution did not complete normally")
if result.passed != case["expected_pass"]:
raise AssertionError(f"{case['name']}: unexpected SDK result {result.status}")
Path("fixture-field-checks.json").write_text(
json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8"
)
Path("sdk-results.json").write_text(
json.dumps({"giskard": version("giskard"),
"giskard_checks": version("giskard-checks"),
"results": sdk_results}, ensure_ascii=False, indent=2),
encoding="utf-8",
)
if __name__ == "__main__":
asyncio.run(main())
第四步:运行并区分预期与实测
python regression_demo.py
本次原生 SDK 结果为:correct 的状态是 pass,其余 wrong_amount、missing_source、unsupported_promise 均为 fail,没有 error 或 skip。它们是故意提供给检查函数的固定输出,不是模型生成结果。执行报告的状态符合设计预期,不应把一条通过、三条失败换算成模型准确率。
脚本将字段判断保存到 fixture-field-checks.json,将 SDK 的原生序列化结果保存到 sdk-results.json。使用 result.errored、result.skipped 和 result.passed 检查状态,结果偏离案例预期时抛出异常并以非零状态退出。因为三个反例本来就应失败,本次演示得到预期结果时退出码为 0;真实发布门槛应为正常业务样本要求通过,不要照搬教学反例的预期值。
第五步:保存可复现环境
成功运行后,再保存依赖:
python -m pip freeze > requirements.lock.txt
python -c "import sys; print(sys.version)"
同一套测试至少记录政策版本、测试集版本、被测系统版本和评判配置。引入模型后,还要保存模型名称、请求参数和服务异常。模型服务返回超时,与回答违反政策是不同结果,报告应分别统计。
第六步:接入真实助手
先把真实助手的输出整理成明确结构,再对金额、来源和业务状态做确定性检查。不要在适配器中根据预期答案修正真实输出,否则测试会掩盖缺陷。保留原始回答和适配后的字段,才能知道失败发生在模型、解析器还是检查规则。
自由文本需要额外分析:金额可能正确但适用对象不对;来源编号可能存在但没有支持该句结论;助手可能一边设置 refund_guaranteed=false,一边在文本里承诺“一定能退”。应补充针对这些矛盾的用例,结合语义评判和人工抽查,而不是继续扩大简单字段门槛的含义。
接入后先复现真实的历史失败,再引入独立验收集。生成更多样本前先检查参考材料是否有效,避免自动扩大错误的事实依据。每次修复只调整必要变量,观察是否同时影响正常请求和拒答行为。
什么时候再使用知识库质量扫描
当手写回归已能稳定运行,参考政策也有清楚版本时,可以按官方 quality_scan 文档探索自动生成的动态情境。生成、执行和评判可能涉及模型调用,应该先限定规模、预算和材料范围。
自动生成的情境用于寻找遗漏。审核后有价值的失败应转为稳定回归,避免每次扫描都只得到一批难以比较的新题。本文没有执行这一步,也不提供未经测试的扫描结果或节省成本结论。
常见错误
把旧版示例用于 v3。 官方明确说明 v3 不是兼容升级,先核对版本,再选择对应 API。
把故意失败当成程序错误。 本例三个反例本就应该失败。但导入失败、超时和断言不通过要分开看,不能都解释成“成功发现问题”。
只比较参考答案的字符串。 同义表达可能被误拦,关键否定也可能被漏掉。确定性字段、语义判断与人工复核应各自承担清楚的职责。
把一组教学用例的比例写成准确率。 四个固定输出没有代表真实用户分布,也没有运行生成模型。它们只说明某些门槛如何设计。
什么时候该比较其他工具
需要自定义执行流程和工具行为,可以看Giskard vs Inspect AI;需要补充漏洞探测,可看Giskard vs garak。按检索指标和测试工作方式选型,可看知识库问答测试工具推荐。
官方资料
常见问题
- 这篇教程调用了真实模型吗?
- 没有。示例使用固定的合成输出,只说明明确字段的检查方式。
- Giskard SDK 已在本次环境运行吗?
- 已经在 Giskard 3.0.1、Checks 1.0.6 下运行,得到一个通过、三个预期失败,没有调用真实模型。
- 来源编号出现就证明回答有依据吗?
- 不能。还需要核对来源内容、适用范围与回答之间是否一致。
- 三个反例失败是否意味着程序有问题?
- 这些反例预期应不通过,但需要与导入失败、超时等执行异常分开处理。
- 打印失败报告就能让 CI 失败吗?
- 不能直接假定。应核对实际版本的结果 API,明确处理场景失败和执行异常,并设置退出码。
- 旧版 giskard.rag 示例能原样用于 v3 吗?
- 不能。官方将 v3 描述为重写,应按当前版本选择 Checks 或 Scan 接口。