evaluate_retrieval.py 3.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. import argparse
  2. import asyncio
  3. import json
  4. import os
  5. from collections import defaultdict
  6. from pathlib import Path
  7. from time import perf_counter
  8. from uuid import UUID
  9. import httpx
  10. from zhifu.evaluation import RetrievalManifest, digest, write_report
  11. async def evaluate(args, manifest):
  12. report = {"manifest_sha256": digest(manifest.model_dump(mode="json")),
  13. "dataset_version": manifest.dataset_version, "origin": manifest.origin,
  14. "generation_attempts": 0, "request_limit": len(manifest.queries),
  15. "total_timeout_seconds": manifest.total_timeout_seconds,
  16. "embedding_model": manifest.embedding_model,
  17. "embedding_revision": manifest.embedding_revision, "results": [],
  18. "status": "RUNNING"}
  19. started = perf_counter()
  20. # 仅检索现有发布资料。写入和发布由正常业务接口完成,评测令牌仅需读取权限。
  21. try:
  22. async with asyncio.timeout(manifest.total_timeout_seconds), httpx.AsyncClient(
  23. base_url=args.base_url, timeout=30, trust_env=False,
  24. headers={"Authorization": "Bearer " + os.environ[args.token_env]},
  25. ) as client:
  26. for item in manifest.queries:
  27. began = perf_counter()
  28. response = await client.post(
  29. f"/openapi/v1/stores/{manifest.store_id}/knowledge/search",
  30. json={"query": item.query, "limit": 10},
  31. )
  32. response.raise_for_status()
  33. found = []
  34. for document in response.json()["items"]:
  35. identifier = UUID(document["document_id"])
  36. if manifest.documents.get(identifier) != document["version"]:
  37. raise ValueError("检索结果出现未登记资料或版本变化")
  38. if identifier not in found:
  39. found.append(identifier)
  40. recall = len(set(found) & set(item.expected)) / len(set(item.expected))
  41. report["results"].append({"query": item.query, "category": item.category,
  42. "expected": [str(key) for key in item.expected],
  43. "retrieved": [str(key) for key in found], "recall_at_10": recall,
  44. "seconds": perf_counter() - began})
  45. await asyncio.to_thread(write_report, args.output, report)
  46. categories = defaultdict(list)
  47. for result in report["results"]:
  48. categories[result["category"]].append(result["recall_at_10"])
  49. report["categories"] = {key: {"count": len(values),
  50. "recall_at_10": sum(values) / len(values)} for key, values in categories.items()}
  51. report["recall_at_10"] = sum(row["recall_at_10"] for row in report["results"]) / len(
  52. report["results"])
  53. report["status"] = "PASSED" if report["recall_at_10"] >= 0.9 else "FAILED"
  54. finally:
  55. if report["status"] == "RUNNING":
  56. report["status"] = "INCOMPLETE"
  57. report["seconds"] = perf_counter() - started
  58. await asyncio.to_thread(write_report, args.output, report)
  59. print(json.dumps({key: value for key, value in report.items() if key != "results"}))
  60. if report["status"] != "PASSED":
  61. raise ValueError("Recall@10 未达到 90%")
  62. if __name__ == "__main__":
  63. parser = argparse.ArgumentParser(description="真实知识检索 Recall@10 验收")
  64. parser.add_argument("--manifest", type=Path, required=True)
  65. parser.add_argument("--output", type=Path, required=True)
  66. parser.add_argument("--base-url", default="http://127.0.0.1:18000")
  67. parser.add_argument("--token-env", default="EVALUATION_ACCESS_TOKEN")
  68. args = parser.parse_args()
  69. manifest = RetrievalManifest.model_validate_json(args.manifest.read_bytes())
  70. if manifest.review:
  71. manifest.review.verify()
  72. if manifest.deployment_review:
  73. manifest.deployment_review.verify()
  74. asyncio.run(evaluate(args, manifest))