LLM 测试如何从答案匹配转向质量评估
文章针对聊天、RAG 和文档理解等非确定性功能,介绍用评分维度和阈值替代固定答案断言的评估方法。重点涵盖正确性、依据一致性、相关性,以及同时检查误拒答和不当回答,并引入黄金数据集。
AI 深度解读
LLM 功能最难测的地方,是同一个问题可能得到两份措辞不同、却都正确的答案。原文给出的办法,是把答案质量拆成可评分的维度,再用数据集、自动评测和线上反馈持续追踪;能精确断言的工具调用、接口行为,仍然照常断言。
这套思路适用于聊天助手、RAG 搜索和文档理解。它的价值在于把「感觉回答还行」变成可比较的发布依据。不过,原文是一份实践工具清单,没有提供团队实测结果,也没有证明某套框架优于其他方案。
答案不能逐字比较,但质量可以持续测量
传统测试可以写 assert response == expected,因为确定性函数通常有明确输出。LLM 的自然语言回答允许多种表达,逐字比较会误伤正确答案,也无法可靠识别一本正经的错误。
作者建议评估正确性、依据充分程度、相关性和拒答行为,通常按 0 到 1 评分,再设置阈值。依据充分程度可以理解为:回答里的主张,能不能在提供的上下文中找到支持。
拒答尤其需要双向检查。该拒绝时有没有拒绝,正常请求又有没有被误拦,都影响产品质量。只追求拒绝危险问题,可能得到一个什么都不肯回答的助手。
这些指标需要一份黄金数据集(golden dataset)承载。它是一组经过整理、带有正确预期的输入,既可以记录参考答案,也可以记录预期行为。原文的退款案例规定「收货后 30 天内可退」,另一条案例则要求助手拒绝泄露其他客户的地址。
参考答案提供的是语义依据,不必成为唯一合法措辞。作者还建议给案例标注类别,加入歧义、错别字、多语言和上下文没有答案的问题,并把数据集放进 Git 管理。
模型、提示词或检索流程改动后,应该用同一套案例检查质量变化。
幻觉率也要持续记录。原文将其定义为「包含上下文无法支持的主张的回答占比」。这个定义便于追踪,但使用时需要明确:它衡量的是回答有没有依据,不能单独代表所有事实是否正确。
哪些交给模型打分,哪些继续严格断言
原文列出 DeepEval、Ragas、promptfoo 和 LangSmith,但没有做能力或效果对比。它用 DeepEval 展示了如何把评测写成类似 pytest 的测试。
下面这段代码检查退款回答是否忠于检索内容,以及是否回答了用户的问题:
from deepeval import assert_test
from deepeval.test_case import LLMTestCase
from deepeval.metrics import FaithfulnessMetric, AnswerRelevancyMetric
def test_refund_policy_answer():
test_case = LLMTestCase(
input="What is your refund window for fans?",
actual_output=call_my_assistant("What is your refund window for fans?"),
retrieval_context=[
"Refund policy: Products can be returned within 30 days of delivery."
],
)
assert_test(test_case, [
FaithfulnessMetric(threshold=0.8),
AnswerRelevancyMetric(threshold=0.7),
])
要看的是最后两行:FaithfulnessMetric 的阈值为 0.8,AnswerRelevancyMetric 为 0.7。它们是示例配置,原文没有解释阈值如何校准,也没有说明适用于哪些业务。执行命令是 deepeval test run test_assistant.py;call_my_assistant 需要接入自己的助手实现。
很多指标背后采用模型裁判(LLM-as-a-judge),即让另一个模型按评分规则评价答案。自动化因此更容易,但裁判也会判断错误或带有偏差。作者建议人工抽查判分、固定裁判模型及版本,并对关键案例重复运行,避免把单次采样当成稳定结论。
进入工具调用环节后,可以检查更明确的事实。下面这段代码验证查订单时是否调用了正确工具,并传入正确订单号:
def test_order_status_calls_correct_tool():
result = run_agent("Where is my order #4521?")
assert result.tool_calls[0].name == "get_order_status"
assert result.tool_calls[0].args["order_id"] == "4521"
这里应关注 name 和 args["order_id"] 两处断言。回答怎么组织语言可以变化,工具名 get_order_status 和订单号 4521 却有明确预期。
真实使用还包括连续对话和故障。助手是否记住上下文,会不会在第 4 轮自相矛盾,工具超时或返回异常内容后是否编造结果,都需要单独覆盖。首轮回答正确,不能替整个交互流程过关。
发布门禁之外,还要接住安全和普通软件故障
安全测试需要维护专门的攻击案例库,包括提示词注入、越狱、个人身份信息(PII)泄露和产品禁止涉及的话题。注入不只来自用户输入,也可能藏在模型读取的文档或网页中。作者提到 promptfoo 的红队测试功能可帮助生成和运行攻击案例,但没有给出覆盖率或拦截效果。
评测还要进入 CI/CD。原文示例把工作流放在 .github/workflows/llm-eval.yml,在 pull_request 时运行,使用 ubuntu-latest、actions/checkout@v4、actions/setup-python@v5 和 Python "3.11"。
流水线先执行 pip install -r requirements.txt,再执行 deepeval test run tests/eval/,通过 ${{ secrets.OPENAI_API_KEY }} 注入环境变量 OPENAI_API_KEY。每次 PR 跑小而快的子集,完整评测放在夜间或发布前执行,以控制调用成本和耗时。关键指标越过阈值时阻止构建,同时保存结果,供不同版本比较。
上线后则抽样监控质量,记录延迟、每次请求或每段对话的成本,以及输入输出是否发生漂移。生产事故应回填数据集,但回填只能提高再次发现同类问题的机会,不能保证同一个问题永不复发。
普通软件测试也不能省。接口应对照 Swagger/OpenAPI 校验,可用 Postman 或 Python 的 requests/pytest;RAG 数据可用 SQL,或在 S3 场景下用 Athena 检查完整性、新鲜度、重复和敏感信息脱敏。
原文还提醒:服务撑得住 1 个用户,不代表撑得住 500 个。Playwright、Selenium 和 Appium 依然有用,因为回答质量再好,页面渲染崩溃也会让功能失效。
我的判断
我认可这套分层方法:语言质量用评分追踪,确定性行为用断言检查,再把线上失败带回回归集。它保留了传统测试的有效部分,也承认自然语言输出无法只有一个标准字符串。
但标题里的「不再只看通过或失败」,并不等于取消发布门槛。评分最后仍要通过阈值决定是否放行,真正改变的是判断依据。
原文没有给出数据集规模、重复运行次数、人工复核结果或统计误差,因此这些属于实践建议。0.8 和 0.7 不能直接当成通用标准,模型裁判也不能自动成为真值。
对一般团队,先整理案例、人工复核,再逐步自动化,是可以考虑的起点。只有分数而没有清晰的评分规则,容易把「裁判更喜欢这个答案」误当成「产品更可靠」。
能拿来做什么
- 给现有聊天助手或 RAG 搜索整理一份 Git 管理的数据集,覆盖正常回答、无依据问题、歧义输入和拒答,并按类别比较改动前后的表现。
- 在 Node 或其他后端的工具调用层,直接断言工具名和参数;对工具超时、异常返回单独检查恢复行为,把语言评分留给需要评价表达质量的部分。
- 把前端渲染检查和答案评测接入发布流程,PR 跑核心案例,夜间跑完整集合,并保留评分结果及裁判版本。
- 每次发现线上幻觉或误拒答,就记录原始输入、当时上下文和正确预期,将它补进回归集,检验后续修改是否有效。