评测脚本
eval.py
import asyncio
import os
import csv
import pandas as pd
from openai import AsyncOpenAI
from dotenv import load_dotenv
from ragas.llms import llm_factory
from ragas.embeddings.base import embedding_factory
from ragas.metrics.collections import (
ContextPrecision,
ContextRecall,
AnswerRelevancy,
Faithfulness,
AnswerCorrectness
)
## 加载 .env 文件中的环境变量(如 OPENAI_API_KEY)
load_dotenv()
## --- 1. 全局设置大语言模型 (LLM) 与 Embeddings ---
openai_client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
## 创建评估所需的 LLM 实例
eval_llm = llm_factory(
"qwen3.6-plus",
client=openai_client,
temperature=0, # 降低温度使评估结果更稳定、确定
seed=42, # 固定随机种子以实现可复现的输出
extra_body={ "chat_template_kwargs": {"enable_thinking": False} }, # 关闭思考模式,避免token超限
max_tokens=10240
)
## 创建评估所需的 Embeddings 实例 (Answer Correctness 等指标需要用到向量相似度)
embeddings = embedding_factory("openai", model="text-embedding-v4", client=openai_client)
## --- 2. 初始化所有评估指标评分器 ---
context_precision_scorer = ContextPrecision(llm=eval_llm)
context_recall_scorer = ContextRecall(llm=eval_llm)
answer_relevancy_scorer = AnswerRelevancy(llm=eval_llm, embeddings=embeddings)
faithfulness_scorer = Faithfulness(llm=eval_llm)
answer_correctness_scorer = AnswerCorrectness(llm=eval_llm, embeddings=embeddings)
## --- 3. 定义单条数据的异步评测函数 ---
async def evaluate_single_row(row_data):
"""
对单行数据进行五大指标评测
row_data: 包含 user_input, response, retrieved_contexts, reference 的字典
"""
try:
# 提取字段,retrieved_contexts 在CSV中可能是字符串,需转为列表
user_input = row_data['user_input']
response = row_data['response']
reference = row_data['reference']
# 如果CSV里的 contexts 是类似 "['文本1', '文本2']" 的字符串,可能需要 ast.literal_eval 解析
# 这里假设已经处理好或者是用特定分隔符分割的,根据实际情况调整:
raw_contexts_str = row_data['retrieved_contexts']
# 1. 检查是否为空值或 NaN (防止报错)
if not raw_contexts_str or pd.isna(raw_contexts_str):
contexts = []
else:
# 2. 按照双换行符进行切分(这是处理您提供格式的关键)
# split('\n\n') 会将 "文本A\n\n文本B" 变成 ["文本A", "文本B"]
split_parts = raw_contexts_str.split('\n\n')
# 3. 去除每个部分首尾的空白字符,并过滤掉空字符串
contexts = [part.strip() for part in split_parts if part.strip()]
print(f"成功切分出 {len(contexts)} 个上下文片段") # 调试用,可选
# --- 结束:切分逻辑 ---
# 并发调用各个指标的 ascore 方法
cp_task = context_precision_scorer.ascore(
user_input=user_input,
reference=reference,
retrieved_contexts=contexts
)
cr_task = context_recall_scorer.ascore(
user_input=user_input,
reference=reference,
retrieved_contexts=contexts
)
ar_task = answer_relevancy_scorer.ascore(
user_input=user_input,
response=response
)
f_task = faithfulness_scorer.ascore(
user_input=user_input,
response=response,
retrieved_contexts=contexts
)
ac_task = answer_correctness_scorer.ascore(
user_input=user_input,
response=response,
reference=reference
)
# 等待所有任务完成
cp_res, cr_res, ar_res, f_res, ac_res = await asyncio.gather(
cp_task, cr_task, ar_task, f_task, ac_task
)
return {
'user_input': user_input,
'Context Precision': cp_res.value,
'Context Recall': cr_res.value,
'Answer Relevancy': ar_res.value,
'Faithfulness': f_res.value,
'Answer Correctness': ac_res.value
}
except Exception as e:
print(f"评测出错: {e}")
return None
## --- 4. 主程序入口:读取CSV、批量执行、保存结果 ---
async def main():
input_csv_path = 'generated_dataset.csv' # 替换为你的输入CSV文件名
output_csv_path = 'evaluation_results.csv' # 替换为你想保存的结果文件名
print("--- 脚本启动,开始读取测试集 ---")
# 读取 CSV 文件
rows_to_evaluate = []
with open(input_csv_path, mode='r', encoding='utf-8') as file:
reader = csv.DictReader(file)
for row in reader:
rows_to_evaluate.append(row)
print(f"检测到 CSV 文件中的列名为: {reader.fieldnames}")
print(f"共读取到 {len(rows_to_evaluate)} 条测试数据,开始并发评测...")
# 批量并发执行评测 (可根据 API QPS 限制调整并发数量,例如使用 asyncio.Semaphore)
tasks = [evaluate_single_row(row) for row in rows_to_evaluate]
results = await asyncio.gather(*tasks)
# 过滤掉失败的 None 结果
valid_results = [res for res in results if res is not None]
# 将结果写入新的 CSV 文件
if valid_results:
fieldnames = ['user_input', 'Context Precision', 'Context Recall', 'Answer Relevancy', 'Faithfulness', 'Answer Correctness']
with open(output_csv_path, mode='w', newline='', encoding='utf-8-sig') as file:
writer = csv.DictWriter(file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(valid_results)
print(f"--- 评测完成!共成功评测 {len(valid_results)} 条数据 ---")
print(f"结果已保存至: {output_csv_path}")
else:
print("--- 评测结束,但没有产生有效结果 ---")
if __name__ == "__main__":
asyncio.run(main())
运行前,确保脚本同级目录中存在generated_dataset.csv文件,其中包含测评数据。详细内容见前面的课程讲解。
uv run eval.py 运行成功后,会生成一个评测结果文件:evaluation_results.csv

常见问题
1、The output is incomplete due to a max_tokens length limit
<completion>
ChatCompletion(id='chatcmpl-c22cac9d-3780-9ed6-a640-be330a65091e', choices=[Choice(finish_reason='length', index=0, logprobs=None, message=ChatCompletionMessage(content='{\n "statements": [\n {\n "statement": "建议智界R7车主在车辆维修和保养过程中优先使用原厂配件。",\n "reason": "The context does not mention Zhijie R7 or the use of original factory parts.",\n "verdict": 0\n },\n {\n "statement": "原厂配件经过严格的质量测试。",\n "reason": "The context does not mention original factory parts or their quality testing.",\n "verdict": 0\n },\n {\n "statement": "原厂配件能够确保与智界R7的设计标准完全匹配。",\n "reason": "The context does not mention Zhijie R7 or original factory parts.",\n "verdict": 0\n },\n {\n "statement": "使用原厂配件能够保证智界R7的性能、安全性和可靠性。",\n "reason": "The context does not mention Zhijie R7 or original factory parts.",\n "verdict": 0\n },\n {\n "statement": "使用非原厂配件可能会因规格不符或质量差异导致问题。",\n "reason": "The context does not mention non-original parts or their potential issues.",\n "verdict": 0\n },\n {\n "statement": "非原厂配件可能无法完全适配智界R7的制动系统和空气悬架等关键部件。",\n "reason": "The context does not mention non-original parts or Zhijie R7.",\n "verdict": 0\n },\n {\n "statement": "非原厂配件无法完全适配智界R7关键部件可能导致智界R7性能下降。",\n "reason": "The context does not mention non-original parts or Zhijie R7.",\n "verdict": 0\n },\n {\n "statement": "更换不符合规格的制动衬片可能导致智界R7制动效果减弱。",\n "reason": "The context mentions worn brake pads, not non-specification ones, and does not mention Zhijie R7.",\n "verdict": 0\n },\n {\n "statement": "更换不符合规格的制动衬片可能损坏智界R7整个制动系统并危及行车安全。",\n "reason": "The context warns about worn brake pads, not non-specification ones, and does not mention Zhijie R7.",\n "verdict": 0\n },\n {\n "statement": "多家车辆制造商要求在保修期内使用原厂配件进行维修和保养。",\n "reason": "The context does not mention warranty periods or original factory parts.",\n "verdict": 0\n },\n {\n "statement": "在保修期内未使用原厂配件进行维修和保养可能被视为违反保修条款。",\n "reason": "The context does not mention warranty terms or original factory parts.",\n "verdict": 0\n },\n {\n "statement": "选择原厂配件进行维修保养可以确保智界R7的最佳状态并避免保修纠纷。",\n "reason": "The context does not mention Zhijie R7, original factory parts, or warranty disputes.",\n "verdict": 0\n },\n {\n "statement": "私自改装可能会对智界R7的保修产生影响。",\n "reason": "The context does not mention unauthorized modifications or the warranty of Zhijie R7.",\n "verdict": 0\n },\n {\n "statement": "私自改装对智界R7保修的影响取决于改装的范围和性质。",\n "reason": "The context does not mention unauthorized modifications or warranty impacts.",\n "verdict": 0\n },\n {\n "statement": "涉及智界R7制动系统、悬架系统、动力系统等核心系统的改装且未经过官方授权服务中心认可可能会直接导致被改装核心部件的保修失效。",\n "reason": "The context does not mention modifications to core systems or warranty invalidation.",\n "verdict": 0\n },\n {\n "statement": "擅自更换不符合规格的车轮可能影响智界R7胎压监测系统的正常运行。",\n "reason": "The context mentions that replacing wheels with non-specified ones may affect the tire pressure monitoring system, but it does not mention Zhijie R7.",\n "verdict": 0\n },\n {\n "statement": "擅自更换不符合规格的车轮影响胎压监测系统正常运行会导致胎压监测功能的保修失效。",\n "reason": "The context does not mention warranty invalidation for the tire pressure monitoring system.",\n "ver', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None, reasoning_content='用户要求根据给定的上下文(context)判断一系列陈述(statements)的忠实度(faithfulness)。\n如果陈述可以直接从上下文中推断出来,则verdict为1;否则为0。\n输出必须符合提供的JSON Schema,并且使用双引号,正确转义。\n\n上下文内容分析:\n- 警告:不更换磨损的制动衬片会损坏制动系统,可能导致危险。每次保养检查制动盘和衬片,必要时更换。\n- 胎压监测系统:主动式,传感器在气阀上,监测压力和温度。仪表显示屏和中控屏界面描述。\n- 注意:仅允许更换指定规格的车轮,否则可能影响胎压监测系统正常运行。\n- 服务中心:发现和管理服务卡片。\n- 空气悬架高度调节抑制:条件不满足时不调节(车门未关、充电、气泵温度高、方向盘转角大、维修模式、特殊模式、故障、非稳态工况)。\n\n陈述列表及判断:\n1. "建议智界R7车主在车辆维修和保养过程中优先使用原厂配件。" -> 上下文没有提到“智界R7”或“原厂配件”。(0)\n2. "原厂配件经过严格的质量测试。" -> 上下文未提及。(0)\n3. "原厂配件能够确保与智界R7的设计标准完全匹配。" -> 上下文未提及。(0)\n4. "使用原厂配件能够保证智界R7的性能、安全性和可靠性。" -> 上下文未提及。(0)\n5. "使用非原厂配件可能会因规格不符或质量差异导致问题。" -> 上下文未提及非原厂配件,只提到“指定规格的车轮”。(0)\n6. "非原厂配件可能无法完全适配智界R7的制动系统和空气悬架等关键部件。" -> 上下文未提及。(0)\n7. "非原厂配件无法完全适配智界R7关键部件可能导致智界R7性能下降。" -> 上下文未提及。(0)\n8. "更换不符合规格的制动衬片可能导致智界R7制动效果减弱。" -> 上下文提到“如果不更换磨损的制动衬片将会损坏制动系统”,没提到“不符合规格的制动衬片”或“智界R7”。(0)\n9. "更换不符合规格的制动衬片可能损坏智界R7整个制动系统并危及行车安全。" -> 上下文提到“不更换磨损的制动衬片将会损坏制动系统,并可能导致危险”,未提及“不符合规格”和“智界R7”。(0)\n10. "多家车辆制造商要求在保修期内使用原厂配件进行维修和保养。" -> 上下文未提及。(0)\n11. "在保修期内未使用原厂配件进行维修和保养可能被视为违反保修条款。" -> 上下文未提及。(0)\n12. "选择原厂配件进行维修保养可以确保智界R7的最佳状态并避免保修纠纷。" -> 上下文未提及。(0)\n13. "私自改装可能会对智界R7的保修产生影响。" -> 上下文未提及。(0)\n14. "私自改装对智界R7保修的影响取决于改装的范围和性质。" -> 上下文未提及。(0)\n15. "涉及智界R7制动系统、悬架系统、动力系统等核心系统的改装且未经过官方授权服务中心认可可能会直接导致被改装核心部件的保修失效。" -> 上下文未提及。(0)\n16. "擅自更换不符合规格的车轮可能影响智界R7胎压监测系统的正常运行。" -> 上下文提到“仅允许更换指定规格的车轮,否则可能会影响到胎压监测系统正常运行。”,但未提及“智界R7”。不过通常这种特定车型的文档如果没提名字,可能算0,或者如果默认这是智界R7的文档?上下文完全没有“智界R7”字眼。严格按照“directly inferred”,没有提到智界R7,所以是0。\n17. "擅自更换不符合规格的车轮影响胎压监测系统正常运行会导致胎压监测功能的保修失效。" -> 上下文未提及保修失效。(0)\n18. "改装智界R7内饰和音响系统等不影响车辆核心性能的部件通常不会直接影响保修。" -> 上下文未提及。(0)\n19. "改装智界R7非核心部件需确保改装过程不会对智界R7未改装系统造成间接损害。" -> 上下文未提及。(0)\n20. "根据多家车辆制造商的保修政策,未经官方授权的改装或维修操作若导致车辆故障,制造商有权拒绝提供保修服务。" -> 上下文未提及。(0)\n21. "建议智界R7车主在进行改装前咨询官方服务中心了解改装是否会影响保修。" -> 上下文未提及。(0)\n22. "建议智界R7车主选择符合智界R7规格的配件和专业机构进行改装操作。" -> 上下文未提及。(0)\n23. "智界R7车主在改装前请务必咨询官方服务中心并遵循官方服务中心的建议。" -> 上下文未提及。(0)\n24. "智界R7车主如有进一步疑问可随时联系智界R7的官方服务中心获取详细指导。" -> 上下文未提及。(0)\n\n等等,所有的陈述都包含“智界R7”或者“原厂配件”、“保修”等上下文完全没有的信息。\n上下文主要讲了:\n- 制动衬片磨损警告\n- 胎压监测系统(仪表和中控界面)\n- 更换指定规格车轮的注意事项\n- 服务中心和服务卡片\n- 空气悬架高度调节抑制条件\n\n因此,所有24个陈述的verdict都应该是0,因为上下文中没有提到“智界R7”、“原厂配件”、“保修”、“改装”等概念(除了“更换指定规格的车轮”和“维修模式”等,但远不足以推断出这些陈述)。\n\n让我仔细检查第16条:"擅自更换不符合规格的车轮可能影响智界R7胎压监测系统的正常运行。"\n上下文:"仅允许更换指定规格的车轮,否则可能会影响到胎压监测系统正常运行。"\n虽然内容相似,但上下文中并没有提到“智界R7”。在严格的NLI中,引入未提及的特定实体(智界R7)会导致无法直接推断(0)。如果放宽一点,可能认为是1?但为了安全起见,应判断为0,因为“智界R7”在上下文中未出现。或者,也许整个上下文就是智界R7的手册,但在给定的文本中确实没有“智界R7”这个词。根据“directly inferred based on the context”,必须是0。\n\n再检查第8、9条:\n上下文:"如果不更换磨损的制动衬片将会损坏制动系统,并可能导致危险。"\n陈述8:"更换不符合规格的制动衬片可能导致智界R7制动效果减弱。" (0)\n陈述9:"更换不符合规格的制动衬片可能损坏智界R7整个制动系统并危及行车安全。" (0)\n上下文说的是“磨损的”,不是“不符合规格的”,且没有“智界R7”。所以是0。\n\n所有陈述的verdict均为0。\n\n编写JSON输出。确保双引号,没有单引号,格式正确。\n```json\n{\n "statements": [\n {\n "statement": "建议智界R7车主在车辆维修和保养过程中优先使用原厂配件。",\n "reason": "The context does not mention \'Zhijie R7\' or the use of original factory parts.",\n "verdict": 0\n },\n ...\n ]\n}\n```\n\nReason可以用中文或英文,示例中是英文,我就用英文。\n\n1. "建议智界R7车主在车辆维修和保养过程中优先使用原厂配件。" -> The context does not mention Zhijie R7 or original factory parts. (0)\n2. "原厂配件经过严格的质量测试。" -> The context does not mention original factory parts or their quality testing. (0)\n3. "原厂配件能够确保与智界R7的设计标准完全匹配。" -> The context does not mention Zhijie R7 or original factory parts. (0)\n4. "使用原厂配件能够保证智界R7的性能、安全性和可靠性。" -> The context does not mention Zhijie R7 or original factory parts. (0)\n5. "使用非原厂配件可能会因规格不符或质量差异导致问题。" -> The context does not mention non-original parts. (0)\n6. "非原厂配件可能无法完全适配智界R7的制动系统和空气悬架等关键部件。" -> The context does not mention non-original parts or Zhijie R7. (0)\n7. "非原厂配件无法完全适配智界R7关键部件可能导致智界R7性能下降。" -> The context does not mention non-original parts or Zhijie R7. (0)\n8. "更换不符合规格的制动衬片可能导致智界R7制动效果减弱。" -> The context mentions worn brake pads, not non-specification ones, and does not mention Zhijie R7. (0)\n9. "更换不符合规格的制动衬片可能损坏智界R7整个制动系统并危及行车安全。" -> The context warns about worn brake pads, not non-specification ones, and does not mention Zhijie R7. (0)\n10. "多家车辆制造商要求在保修期内使用原厂配件进行维修和保养。" -> The context does not mention warranty periods or original factory parts. (0)\n11. "在保修期内未使用原厂配件进行维修和保养可能被视为违反保修条款。" -> The context does not mention warranty terms or original factory parts. (0)\n12. "选择原厂配件进行维修保养可以确保智界R7的最佳状态并避免保修纠纷。" -> The context does not mention Zhijie R7, original factory parts, or warranty disputes. (0)\n13. "私自改装可能会对智界R7的保修产生影响。" -> The context does not mention unauthorized modifications or Zhijie R7\'s warranty. (0)\n14. "私自改装对智界R7保修的影响取决于改装的范围和性质。" -> The context does not mention unauthorized modifications or warranty impacts. (0)\n15. "涉及智界R7制动系统、悬架系统、动力系统等核心系统的改装且未经过官方授权服务中心认可可能会直接导致被改装核心部件的保修失效。" -> The context does not mention modifications to core systems or warranty invalidation. (0)\n16. "擅自更换不符合规格的车轮可能影响智界R7胎压监测系统的正常运行。" -> The context mentions that replacing wheels with non-specified ones may affect the tire pressure monitoring system, but it does not mention Zhijie R7. (0)\n17. "擅自更换不符合规格的车轮影响胎压监测系统正常运行会导致胎压监测功能的保修失效。" -> The context does not mention warranty invalidation for the tire pressure monitoring system. (0)\n18. "改装智界R7内饰和音响系统等不影响车辆核心性能的部件通常不会直接影响保修。" -> The context does not mention interior or audio system modifications or their impact on warranty. (0)\n19. "改装智界R7非核心部件需确保改装过程不会对智界R7未改装系统造成间接损害。" -> The context does not mention modifications to non-core parts or indirect damage. (0)\n20. "根据多家车辆制造商的保修政策,未经官方授权的改装或维修操作若导致车辆故障,制造商有权拒绝提供保修服务。" -> The context does not mention manufacturers\' warranty policies regarding unauthorized modifications. (0)\n21. "建议智界R7车主在进行改装前咨询官方服务中心了解改装是否会影响保修。" -> The context does not mention consulting service centers about modifications and warranty. (0)\n22. "建议智界R7车主选择符合智界R7规格的配件和专业机构进行改装操作。" -> The context does not mention Zhijie R7 or professional institutions for modifications. (0)\n23. "智界R7车主在改装前请务必咨询官方服务中心并遵循官方服务中心的建议。" -> The context does not mention Zhijie R7 or consulting service centers before modifications. (0)\n24. "智界R7车主如有进一步疑问可随时联系智界R7的官方服务中心获取详细指导。" -> The context does not mention contacting Zhijie R7\'s official service center for further questions. (0)\n\n检查所有reason和verdict,确保没有语法错误,格式符合JSON要求。\n全部verdict都是0。\n确保没有单引号,比如 "Zhijie R7" 而不是 \'Zhijie R7\'。\n"Zhijie R7" is fine, or just "the specific car model". I will use "the specific car model" or "Zhijie R7" with double quotes if needed, but it\'s easier to just say "the context does not mention the specific car model or original factory parts." Let\'s just use "Zhijie R7" without quotes in the reason string to avoid escaping issues, or just escape them properly. "The context does not mention Zhijie R7 or original factory parts." is perfectly fine.\n\nLet\'s do a quick final review of the JSON structure.\n{\n "statements": [\n {\n "statement": "...",\n "reason": "...",\n "verdict": 0\n },\n ...\n ]\n}\nAll good.\nI will generate the JSON now.'))], created=1780064286, model='qwen3.7-max', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=11928, prompt_tokens=8478, total_tokens=20406, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=8844, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=4352)))
</completion>
</generation>
</failed_attempts>
<last_exception>
The output is incomplete due to a max_tokens length limit.
</last_exception>
The output is incomplete due to a max_tokens length limit 表明,模型在生成评估结果时达到了预设的最大生成长度(max_tokens)限制,导致输出被强制截断。 解决方式1,关闭模型的思考模式:
eval_llm = llm_factory(
"qwen3.7-max",
client=openai_client,
temperature=0, # 降低温度使评估结果更稳定、确定
seed=42, # 固定随机种子以实现可复现的输出
extra_body={"enable_thinking": False} # 关闭思考模式,避免token超限
)
关闭后,确实可以减少token的消耗,虽然还是报错,但是reasoning_tokens已经变成0了:
usage=CompletionUsage(completion_tokens=2048, prompt_tokens=5750, total_tokens=7798, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0)
解决方式2,调高max_token数:
eval_llm = llm_factory(
"qwen3.7-max",
client=openai_client,
temperature=0,
seed=42,
max_tokens=10240,
chat_template_kwargs={"enable_thinking": False}
)