ForcePilot/src/services/evaluation_service.py

753 lines
33 KiB
Python
Raw Normal View History

import asyncio
import glob
import json
import os
import uuid
from datetime import datetime
from typing import Any
from server.services.tasker import TaskContext, tasker
from src import config
from src.knowledge import knowledge_base
from src.models import select_model
from src.utils import logger
from src.utils.evaluation_metrics import EvaluationMetricsCalculator
class EvaluationService:
"""RAG评估服务"""
def __init__(self):
pass
def _get_benchmark_dir(self, db_id: str) -> str:
kb_instance = knowledge_base.get_kb(db_id)
base_dir = os.path.join(kb_instance.work_dir, db_id)
path = os.path.join(base_dir, "benchmarks")
os.makedirs(path, exist_ok=True)
return path
def _get_result_dir(self, db_id: str) -> str:
kb_instance = knowledge_base.get_kb(db_id)
base_dir = os.path.join(kb_instance.work_dir, db_id)
path = os.path.join(base_dir, "results")
os.makedirs(path, exist_ok=True)
return path
# 已移除基准回退逻辑,统一使用集中元数据
# 已移除结果回退逻辑,统一通过 db_id 定位
async def upload_benchmark(
self, db_id: str, file_content: bytes, filename: str, name: str, description: str, created_by: str
) -> dict[str, Any]:
"""上传评估基准文件"""
try:
content_str = file_content.decode("utf-8")
questions = []
has_gold_chunks = False
has_gold_answers = False
# 解析 JSONL
for line_num, line in enumerate(content_str.strip().split("\n"), 1):
if not line.strip():
continue
try:
item = json.loads(line)
if "query" not in item:
raise ValueError(f"{line_num}行缺少必需的'query'字段")
if item.get("gold_chunk_ids"):
has_gold_chunks = True
if item.get("gold_answer"):
has_gold_answers = True
questions.append(item)
except json.JSONDecodeError as e:
raise ValueError(f"{line_num}行JSON格式错误: {str(e)}")
if not questions:
raise ValueError("文件中没有有效的问题数据")
benchmark_id = f"benchmark_{uuid.uuid4().hex[:8]}"
benchmark_dir = self._get_benchmark_dir(db_id)
# 保存数据文件 (.jsonl)
data_file_path = os.path.join(benchmark_dir, f"{benchmark_id}.jsonl")
with open(data_file_path, "w", encoding="utf-8") as f:
f.write(content_str)
meta = {
"id": benchmark_id,
"benchmark_id": benchmark_id,
"name": name,
"description": description,
"db_id": db_id,
"question_count": len(questions),
"has_gold_chunks": has_gold_chunks,
"has_gold_answers": has_gold_answers,
"benchmark_file": data_file_path,
"created_by": created_by,
"created_at": datetime.utcnow().isoformat(),
"updated_at": datetime.utcnow().isoformat(),
}
kb_instance = knowledge_base.get_kb(db_id)
if db_id not in kb_instance.benchmarks_meta:
kb_instance.benchmarks_meta[db_id] = {}
kb_instance.benchmarks_meta[db_id][benchmark_id] = meta
kb_instance._save_metadata()
return meta
except Exception as e:
logger.error(f"上传评估基准失败: {e}")
raise
async def get_benchmarks(self, db_id: str) -> list[dict[str, Any]]:
"""获取知识库的评估基准列表"""
try:
kb_instance = knowledge_base.get_kb(db_id)
benchmarks_map = kb_instance.benchmarks_meta.get(db_id, {})
benchmarks = list(benchmarks_map.values())
benchmarks.sort(key=lambda x: x.get("created_at", ""), reverse=True)
return benchmarks
except Exception as e:
logger.error(f"获取评估基准列表失败: {e}")
raise
async def get_benchmark_detail(self, benchmark_id: str) -> dict[str, Any]:
"""获取评估基准详情 (包含问题列表)"""
try:
for kb_instance in knowledge_base.kb_instances.values():
for db_id, m in kb_instance.benchmarks_meta.items():
if benchmark_id in m:
meta = m[benchmark_id]
data_file_path = meta.get("benchmark_file")
questions = []
if data_file_path and os.path.exists(data_file_path):
with open(data_file_path, encoding="utf-8") as f:
for line in f:
if line.strip():
questions.append(json.loads(line))
meta_with_q = meta.copy()
meta_with_q["questions"] = questions
return meta_with_q
raise ValueError("Benchmark not found")
except Exception as e:
logger.error(f"获取评估基准详情失败: {e}")
raise
async def get_benchmark_detail_by_db(self, db_id: str, benchmark_id: str) -> dict[str, Any]:
"""根据 db_id 直接获取评估基准详情"""
try:
kb_instance = knowledge_base.get_kb(db_id)
benchmarks_map = kb_instance.benchmarks_meta.get(db_id, {})
if benchmark_id not in benchmarks_map:
raise ValueError("Benchmark not found")
meta = benchmarks_map[benchmark_id]
data_file_path = meta.get("benchmark_file")
questions = []
if data_file_path and os.path.exists(data_file_path):
with open(data_file_path, encoding="utf-8") as f:
for line in f:
if line.strip():
questions.append(json.loads(line))
meta_with_q = meta.copy()
meta_with_q["questions"] = questions
return meta_with_q
except Exception as e:
logger.error(f"获取评估基准详情失败: {e}")
raise
async def delete_benchmark(self, benchmark_id: str) -> None:
"""删除评估基准"""
try:
# 在所有KB中查找并删除
for kb_instance in knowledge_base.kb_instances.values():
for db_id, m in list(kb_instance.benchmarks_meta.items()):
if benchmark_id in m:
meta = m[benchmark_id]
data_file_path = meta.get("benchmark_file")
if data_file_path and os.path.exists(data_file_path):
os.remove(data_file_path)
del kb_instance.benchmarks_meta[db_id][benchmark_id]
kb_instance._save_metadata()
logger.info(f"成功删除评估基准: {benchmark_id}")
return
raise ValueError("Benchmark not found")
except Exception as e:
logger.error(f"删除评估基准失败: {e}")
raise
async def delete_evaluation_result(self, task_id: str) -> None:
"""删除评估结果"""
raise ValueError("Endpoint requires db_id; use delete_evaluation_result_by_db")
async def generate_benchmark(self, db_id: str, params: dict[str, Any], created_by: str) -> dict[str, Any]:
task_id = f"gen_benchmark_{uuid.uuid4().hex[:8]}"
await tasker.enqueue(
name="生成评估基准",
task_type="benchmark_generation",
payload={"task_id": task_id, "db_id": db_id, "created_by": created_by, **params},
coroutine=self._generate_benchmark_task,
)
return {"task_id": task_id, "message": "基准生成任务已提交"}
async def _generate_benchmark_task(self, context: TaskContext):
import random
import math
await context.set_progress(0, "初始化")
task = context._tasker._tasks.get(context.task_id)
payload = task.payload if task else {}
db_id = payload.get("db_id")
name = payload.get("name", "自动生成评估基准")
description = payload.get("description", "")
count = int(payload.get("count", 10))
neighbors_count = int(payload.get("neighbors_count", 0))
embedding_model_id = payload.get("embedding_model_id")
llm_model_spec = payload.get("llm_model_spec") or (payload.get("llm_config") or {}).get("model_spec")
if neighbors_count < 0:
neighbors_count = 0
if neighbors_count > 10:
neighbors_count = 10
kb_instance = knowledge_base.get_kb(db_id)
if not kb_instance:
await context.set_message("知识库不存在")
raise ValueError("Knowledge Base not found")
if kb_instance.kb_type == "lightrag":
await context.set_message("暂不支持该类型知识库生成评估基准")
raise ValueError("Unsupported KB type for benchmark generation")
await context.set_progress(5, "加载chunks")
all_chunks = []
for fid, finfo in kb_instance.files_meta.items():
if finfo.get("database_id") != db_id:
continue
try:
content_info = await kb_instance.get_file_content(db_id, fid)
lines = content_info.get("lines", [])
for line in lines:
all_chunks.append(
{
"id": line.get("id"),
"content": line.get("content", ""),
"file_id": fid,
"chunk_index": line.get("chunk_order_index"),
}
)
except Exception:
continue
if not all_chunks:
await context.set_message("知识库为空或未解析到chunks")
raise ValueError("No chunks found in knowledge base")
contents = [c["content"] for c in all_chunks]
await context.set_progress(15, "向量化")
if not embedding_model_id:
db_meta = kb_instance.databases_meta.get(db_id, {})
embed_info = db_meta.get("embed_info", {})
embedding_model_id = embed_info.get("name") or embed_info.get("model") or ""
if not embedding_model_id:
raise ValueError("Embedding model not specified")
from src.models import select_embedding_model, select_model
embed_model = select_embedding_model(embedding_model_id)
# TODO: Performance Optimization
# Currently, we re-calculate embeddings for ALL chunks in the KB for every benchmark generation.
# This is inefficient for large KBs (O(N) embedding calls).
# Optimization: Reuse existing embeddings from Vector DB if embedding_model_id matches the KB's embedding model.
embeddings = await embed_model.abatch_encode(contents, batch_size=40)
norms = [math.sqrt(sum(x * x for x in vec)) or 1.0 for vec in embeddings]
def cosine(a, b, na, nb):
s = 0.0
for i in range(len(a)):
s += a[i] * b[i]
return s / (na * nb)
llm = select_model(model_spec=llm_model_spec)
benchmark_id = f"benchmark_{uuid.uuid4().hex[:8]}"
bench_dir = self._get_benchmark_dir(db_id)
data_file_path = os.path.join(bench_dir, f"{benchmark_id}.jsonl")
generated = 0
attempts = 0
await context.set_progress(0, "准备生成样本")
with open(data_file_path, "w", encoding="utf-8") as f:
# Allow more attempts to generate enough questions
max_attempts = max(count * 5, 50)
while generated < count and attempts < max_attempts:
attempts += 1
i0 = random.randrange(len(all_chunks))
e0 = embeddings[i0]
n0 = norms[i0]
sims = []
for j in range(len(all_chunks)):
if j == i0:
continue
s = cosine(e0, embeddings[j], n0, norms[j])
sims.append((j, s))
sims.sort(key=lambda x: x[1], reverse=True)
top_js = [j for j, _ in sims[:neighbors_count]]
ctx_items = []
ctx_items.append((all_chunks[i0]["id"], all_chunks[i0]["content"]))
for j in top_js:
ctx_items.append((all_chunks[j]["id"], all_chunks[j]["content"]))
allowed_ids = {cid for cid, _ in ctx_items}
context_text = "\n\n".join([f"片段ID={cid}\n{content}" for cid, content in ctx_items])
prompt = (
"你将基于以下上下文生成一个可由上下文准确回答的问题与标准答案。"
"仅返回一个JSON对象不要包含其他文字。"
"键为 query、gold_answer、gold_chunk_ids。gold_chunk_ids 必须是上述上下文片段的ID子集。\n\n上下文:\n"
+ context_text
+ "\n"
)
try:
resp = await asyncio.to_thread(llm.call, prompt, False)
content = resp.content if resp else ""
import json_repair
obj = json_repair.loads(content)
q = obj.get("query")
a = obj.get("gold_answer")
gids = obj.get("gold_chunk_ids")
if not q or not a or not isinstance(gids, list):
logger.warning(f"Generated JSON missing fields or invalid format: {obj}")
continue
gids = [str(x) for x in gids if str(x) in allowed_ids]
if not gids:
logger.warning("Generated gold_chunk_ids not found in allowed context")
continue
line = {"query": q, "gold_chunk_ids": gids, "gold_answer": a}
f.write(json.dumps(line, ensure_ascii=False) + "\n")
generated += 1
await context.set_progress(0 + int(99 * generated / max(count, 1)), f"已生成 {generated}/{count}")
except Exception as e:
logger.warning(f"Benchmark generation failed for one item: {e}")
continue
meta = {
"id": benchmark_id,
"benchmark_id": benchmark_id,
"name": name,
"description": description,
"db_id": db_id,
"question_count": generated,
"has_gold_chunks": True,
"has_gold_answers": True,
"benchmark_file": data_file_path,
"created_by": payload.get("created_by"),
"created_at": datetime.utcnow().isoformat(),
"updated_at": datetime.utcnow().isoformat(),
}
kb_instance = knowledge_base.get_kb(db_id)
if db_id not in kb_instance.benchmarks_meta:
kb_instance.benchmarks_meta[db_id] = {}
kb_instance.benchmarks_meta[db_id][benchmark_id] = meta
kb_instance._save_metadata()
await context.set_progress(100, "完成")
async def run_evaluation(
self, db_id: str, benchmark_id: str, model_config: dict[str, Any] = None, created_by: str = "system"
) -> str:
"""运行RAG评估"""
try:
task_id = f"eval_{uuid.uuid4().hex[:8]}"
kb_instance = knowledge_base.get_kb(db_id)
bm = kb_instance.benchmarks_meta.get(db_id, {}).get(benchmark_id)
if not bm:
raise ValueError("Benchmark not found")
benchmark_meta = bm
# 从知识库元数据中获取检索配置
retrieval_config = {}
try:
kb_meta = knowledge_base.global_databases_meta.get(db_id, {})
query_params = kb_meta.get("query_params", {})
retrieval_config = query_params.get("options", {})
logger.info(f"从知识库 {db_id} 加载检索配置: {list(retrieval_config.keys())}")
except Exception as e:
logger.error(f"获取知识库检索配置失败: {e}")
# 使用空配置作为默认值
# 合并前端传递的模型配置
if model_config:
retrieval_config.update(model_config)
# 初始化结果文件 (Status: running)
result_dir = self._get_result_dir(db_id)
result_file_path = os.path.join(result_dir, f"{task_id}.json")
initial_result = {
"id": task_id, # for compatibility
"task_id": task_id,
"benchmark_id": benchmark_id,
"db_id": db_id,
"retrieval_config": retrieval_config,
"metrics": {},
"status": "running",
"total_questions": benchmark_meta.get("question_count", 0),
"completed_questions": 0,
"started_at": datetime.utcnow().isoformat(),
"completed_at": None,
"interim_results": [],
}
with open(result_file_path, "w", encoding="utf-8") as f:
json.dump(initial_result, f, ensure_ascii=False, indent=2)
await tasker.enqueue(
name=f"RAG评估({benchmark_meta.get('name')})",
task_type="rag_evaluation",
payload={
"task_id": task_id,
"db_id": db_id,
"benchmark_id": benchmark_id,
"retrieval_config": retrieval_config,
"created_by": created_by,
},
coroutine=self._run_evaluation_task,
)
return task_id
except Exception as e:
logger.error(f"启动评估失败: {e}")
raise
async def _run_evaluation_task(self, context: TaskContext):
"""运行评估任务"""
try:
task = context._tasker._tasks.get(context.task_id)
if not task:
raise ValueError("Task not found")
payload = task.payload
task_id = payload["task_id"]
db_id = payload["db_id"]
benchmark_id = payload["benchmark_id"]
retrieval_config = payload["retrieval_config"]
# 加载基准数据
await context.set_progress(5, "加载基准数据")
kb_instance = knowledge_base.get_kb(db_id)
benchmark_meta = kb_instance.benchmarks_meta.get(db_id, {}).get(benchmark_id)
if not benchmark_meta:
raise ValueError("Benchmark not found")
data_path = benchmark_meta.get("benchmark_file")
if not data_path or not os.path.exists(data_path):
raise ValueError("Benchmark file not found")
benchmark_data = []
with open(data_path, encoding="utf-8") as f:
for line in f:
if line.strip():
benchmark_data.append(json.loads(line))
# 开始评估
kb_instance = knowledge_base.get_kb(db_id)
if not kb_instance:
raise ValueError(f"Knowledge Base {db_id} not found")
if kb_instance.kb_type == "lightrag":
raise ValueError("暂不支持对 LightRAG 类型的知识库进行 RAG 评估")
# 初始化 Judge LLM
judge_llm = None
if benchmark_meta.get("has_gold_answers"):
# 优先使用配置中的 judge_llm否则回退到 answer_llm或者默认
judge_model_spec = retrieval_config.get("judge_llm") or retrieval_config.get("answer_llm")
if judge_model_spec:
try:
logger.debug(f"Initializing Judge LLM: {judge_model_spec}")
judge_llm = select_model(model_spec=judge_model_spec)
except Exception as e:
logger.error(f"Failed to load judge LLM: {e}")
total_questions = len(benchmark_data)
interim_results = []
all_retrieval_metrics = []
all_answer_metrics = []
# 更新结果文件 helper
result_file_path = os.path.join(self._get_result_dir(db_id), f"{task_id}.json")
def update_result_file(status="running", completed=0, metrics=None, interim=None, final_score=None):
try:
if os.path.exists(result_file_path):
with open(result_file_path, encoding="utf-8") as f:
data = json.load(f)
else:
data = {} # Should have been created in run_evaluation
data["status"] = status
data["completed_questions"] = completed
if metrics:
data["metrics"] = metrics
if interim is not None:
data["interim_results"] = interim
if final_score is not None:
data["overall_score"] = final_score
if status in ["completed", "failed"]:
data["completed_at"] = datetime.utcnow().isoformat()
with open(result_file_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"Failed to update result file: {e}")
for i, question_data in enumerate(benchmark_data):
# 检查任务是否被取消
await context.raise_if_cancelled()
progress = 10 + (i / total_questions) * 80
await context.set_progress(progress, f"评估 {i + 1}/{total_questions}")
# 执行查询
query_result = await kb_instance.aquery(question_data["query"], db_id, **retrieval_config)
# 处理结果
if isinstance(query_result, dict):
generated_answer = query_result.get("answer", "")
retrieved_chunks = query_result.get("retrieved_chunks", [])
else:
retrieved_chunks = query_result if isinstance(query_result, list) else []
generated_answer = ""
# 如果没有生成的答案,但有检索结果且配置了 LLM则生成答案
if not generated_answer and retrieved_chunks and retrieval_config.get("answer_llm"):
logger.debug(f"使用 LLM {retrieval_config.get('answer_llm')} 生成答案...")
try:
# 从配置中获取 LLM
model_spec = retrieval_config["answer_llm"]
llm = select_model(model_spec=model_spec)
# 构建上下文
context_docs = []
for idx, chunk in enumerate(retrieved_chunks[:5]): # 使用前5个最相关的文档
content = chunk.get("content", "")
if content:
context_docs.append(f"文档 {idx + 1}:\n{content}")
context_text = "\\n\\n".join(context_docs)
# 构建提示词
prompt = (
f"基于以下上下文信息,请回答用户的问题。\n\n"
f"上下文信息:{context_text}\n\n"
f"用户问题:{question_data["query"]}\n\n"
"请根据上下文信息准确回答问题。如果上下文中没有相关信息,请说明。\n\n"
)
# 生成答案 - 使用 asyncio.to_thread 避免阻塞事件循环
response = await asyncio.to_thread(llm.call, prompt, stream=False)
generated_answer = response.content if response else ""
logger.debug(f"LLM 生成的答案长度: {len(generated_answer) if generated_answer else 0}")
except Exception as e:
logger.error(f"LLM 生成答案失败: {e}")
generated_answer = ""
# 计算指标
current_metrics = {}
retrieval_scores = {}
answer_scores = {}
if benchmark_meta.get("has_gold_chunks") and question_data.get("gold_chunk_ids"):
retrieval_scores = EvaluationMetricsCalculator.calculate_retrieval_metrics(
retrieved_chunks, question_data["gold_chunk_ids"]
)
current_metrics.update(retrieval_scores)
all_retrieval_metrics.append(retrieval_scores)
if benchmark_meta.get("has_gold_answers") and question_data.get("gold_answer"):
if judge_llm:
# 评判过程包含 LLM 调用,使用 asyncio.to_thread 避免阻塞
answer_scores = await asyncio.to_thread(
EvaluationMetricsCalculator.calculate_answer_metrics,
query=question_data["query"],
generated_answer=generated_answer,
gold_answer=question_data["gold_answer"],
judge_llm=judge_llm,
)
current_metrics.update(answer_scores)
all_answer_metrics.append(answer_scores)
else:
logger.warning("需要计算答案指标但未配置 Judge LLM")
interim_results.append(
{
"query": question_data["query"],
"gold_chunk_ids": question_data.get("gold_chunk_ids"),
"gold_answer": question_data.get("gold_answer"),
"generated_answer": generated_answer,
"retrieved_chunks": retrieved_chunks,
"metrics": current_metrics,
}
)
# 计算当前累计指标
current_overall_metrics = {}
if all_retrieval_metrics:
keys = all_retrieval_metrics[0].keys()
for k in keys:
current_overall_metrics[k] = sum(m.get(k, 0) for m in all_retrieval_metrics) / len(
all_retrieval_metrics
)
if all_answer_metrics:
scores = [m.get("score", 0) for m in all_answer_metrics]
current_overall_metrics["answer_correctness"] = sum(scores) / len(scores) if scores else 0.0
# 更新 Tasker 的 result 以便实时获取当前指标
await context.set_result(
{
"current_metrics": current_overall_metrics,
"completed_questions": i + 1,
"total_questions": total_questions,
}
)
# 定期更新文件 (每5个或最后一个)
if (i + 1) % 5 == 0 or (i + 1) == total_questions:
update_result_file(completed=i + 1, interim=interim_results)
# 最终计算
await context.set_progress(95, "计算最终指标")
# 汇总指标
overall_metrics = {}
# 检索指标平均值
if all_retrieval_metrics:
keys = all_retrieval_metrics[0].keys()
for k in keys:
overall_metrics[k] = sum(m.get(k, 0) for m in all_retrieval_metrics) / len(all_retrieval_metrics)
# 答案指标平均值
if all_answer_metrics:
scores = [m.get("score", 0) for m in all_answer_metrics]
overall_metrics["answer_correctness"] = sum(scores) / len(scores) if scores else 0.0
overall_score = EvaluationMetricsCalculator.calculate_overall_score(
all_retrieval_metrics, all_answer_metrics
)
overall_metrics["overall_score"] = overall_score
update_result_file(
status="completed",
completed=total_questions,
metrics=overall_metrics,
interim=interim_results,
final_score=overall_score,
)
await context.set_progress(100, "完成")
except Exception as e:
logger.error(f"Task failed: {e}")
# Try to update status to failed
try:
# Need to find the file path again or pass it around.
# Re-deriving from payload if available
if "payload" in locals():
path = os.path.join(self._get_result_dir(payload["db_id"]), f"{payload['task_id']}.json")
if os.path.exists(path):
with open(path, encoding="utf-8") as f:
d = json.load(f)
d["status"] = "failed"
d["error"] = str(e)
with open(path, "w", encoding="utf-8") as f:
json.dump(d, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"Error updating result file: {e}")
pass
await context.set_message(f"Error: {str(e)}")
raise
async def get_evaluation_results(self, task_id: str) -> dict[str, Any]:
"""获取评估结果"""
raise ValueError("Endpoint requires db_id; use get_evaluation_results_by_db")
async def get_evaluation_history(self, db_id: str) -> list[dict[str, Any]]:
"""获取知识库的评估历史记录"""
try:
result_dir = self._get_result_dir(db_id)
history = []
# 查找所有 .json 文件
result_files = glob.glob(os.path.join(result_dir, "*.json"))
for result_file in result_files:
try:
with open(result_file, encoding="utf-8") as f:
data = json.load(f)
# 只返回摘要信息不返回详细的interim_results
summary = {
"task_id": data.get("task_id"),
"benchmark_id": data.get("benchmark_id"),
"status": data.get("status"),
"started_at": data.get("started_at"),
"completed_at": data.get("completed_at"),
"total_questions": data.get("total_questions"),
"completed_questions": data.get("completed_questions"),
"overall_score": data.get("overall_score"),
# 包含检索配置
"retrieval_config": data.get("retrieval_config", {}),
# 也可以带上部分 metrics 摘要
"metrics": data.get("metrics"),
}
history.append(summary)
except Exception as e:
logger.error(f"Failed to load result file {result_file}: {e}")
# 按开始时间倒序
history.sort(key=lambda x: x.get("started_at", ""), reverse=True)
return history
except Exception as e:
logger.error(f"获取评估历史失败: {e}")
raise
# 索引与回退逻辑已移除,统一通过 db_id 定位
async def get_evaluation_results_by_db(self, db_id: str, task_id: str) -> dict[str, Any]:
result_file_path = os.path.join(self._get_result_dir(db_id), f"{task_id}.json")
if not os.path.exists(result_file_path):
task = await tasker.get_task(task_id)
if task:
return {
"task_id": task_id,
"status": task.status,
"progress": task.progress,
"message": task.message,
}
raise ValueError(f"Result not found for task {task_id}")
with open(result_file_path, encoding="utf-8") as f:
return json.load(f)
async def delete_evaluation_result_by_db(self, db_id: str, task_id: str) -> None:
result_file_path = os.path.join(self._get_result_dir(db_id), f"{task_id}.json")
if os.path.exists(result_file_path):
os.remove(result_file_path)
logger.info(f"成功删除评估结果: {task_id}")
return
raise ValueError("Result not found")