evaluate_agents.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. """使用真实 HTTP、真实大模型和 LangSmith Trace 评测两种 Agent 人格。"""
  2. import argparse
  3. import json
  4. import sys
  5. import time
  6. from contextlib import suppress
  7. from pathlib import Path
  8. from typing import Any
  9. from uuid import NAMESPACE_URL, UUID, uuid5
  10. import httpx
  11. from langsmith import Client
  12. from app.core.config import Settings
  13. BACKEND_ROOT = Path(__file__).resolve().parents[2]
  14. def load_cases(suite: str) -> list[dict[str, Any]]:
  15. suffix = "smoke" if suite == "smoke" else "full"
  16. cases: list[dict[str, Any]] = []
  17. for persona in ("customer", "operation"):
  18. path = BACKEND_ROOT / "evals" / f"{persona}-{suffix}.jsonl"
  19. cases.extend(
  20. json.loads(line)
  21. for line in path.read_text(encoding="utf-8").splitlines()
  22. if line.strip()
  23. )
  24. return cases
  25. def login(client: httpx.Client, persona: str, admin_password: str) -> str:
  26. if persona == "customer":
  27. response = client.post(
  28. "/h5/auth/login",
  29. json={"mobile": "18800000001", "code": "147258"},
  30. )
  31. else:
  32. response = client.post(
  33. "/admin/auth/login",
  34. json={"username": "admin", "password": admin_password},
  35. )
  36. response.raise_for_status()
  37. return str(response.json()["data"]["tokens"]["access_token"])
  38. def run_case(
  39. client: httpx.Client,
  40. case: dict[str, Any],
  41. admin_password: str,
  42. ) -> dict[str, Any]:
  43. persona = str(case["persona"])
  44. token = login(client, persona, admin_password)
  45. prefix = "" if persona == "customer" else "/admin"
  46. headers = {"Authorization": f"Bearer {token}"}
  47. thread_response = client.post(
  48. f"{prefix}/agent/threads",
  49. headers=headers,
  50. json={"title": f"评测-{case['id']}"},
  51. )
  52. thread_response.raise_for_status()
  53. thread_id = thread_response.json()["data"]["id"]
  54. response = client.post(
  55. f"{prefix}/agent/threads/{thread_id}/messages",
  56. headers=headers,
  57. json={
  58. "content": {"type": "text", "text": case["input"]},
  59. "client_message_id": f"eval-{case['id']}",
  60. },
  61. timeout=120,
  62. )
  63. response.raise_for_status()
  64. data = response.json()["data"]
  65. run_id = str(data["run_id"])
  66. for _ in range(5):
  67. if data.get("trace_url"):
  68. break
  69. time.sleep(1)
  70. detail = client.get(
  71. f"{prefix}/agent/runs/{run_id}",
  72. headers=headers,
  73. )
  74. detail.raise_for_status()
  75. data["trace_url"] = detail.json()["data"].get("trace_url")
  76. text = str(data["assistant_message"]["text"])
  77. tools = set(data.get("invoked_tools", []))
  78. expected_tools = set(case["expected_tools"])
  79. expected_terms = [str(term) for term in case["expected_terms"]]
  80. scores = {
  81. "response_nonempty": bool(text.strip()),
  82. "tool_coverage": expected_tools.issubset(tools),
  83. "term_coverage": all(term in text for term in expected_terms),
  84. "trace_linked": bool(data.get("trace_id")),
  85. }
  86. return {
  87. "id": case["id"],
  88. "persona": persona,
  89. "run_id": run_id,
  90. "trace_id": data.get("trace_id"),
  91. "trace_url": data.get("trace_url"),
  92. "invoked_tools": sorted(tools),
  93. "scores": scores,
  94. "passed": all(scores.values()),
  95. "answer": text,
  96. }
  97. def sync_langsmith(
  98. settings: Settings,
  99. suite: str,
  100. cases: list[dict[str, Any]],
  101. results: list[dict[str, Any]],
  102. ) -> None:
  103. if not settings.langsmith_api_key:
  104. return
  105. client = Client(
  106. api_url=settings.langsmith_endpoint,
  107. api_key=settings.langsmith_api_key,
  108. )
  109. dataset_name = f"智保通-S1-{suite}"
  110. if not client.has_dataset(dataset_name=dataset_name):
  111. client.create_dataset(
  112. dataset_name,
  113. description="智保通第一阶段客户与运营 Agent 真实模型评测集",
  114. metadata={"project": "智保通", "stage": 1, "suite": suite},
  115. )
  116. result_by_id = {result["id"]: result for result in results}
  117. for case in cases:
  118. example_id = uuid5(NAMESPACE_URL, f"{dataset_name}:{case['id']}")
  119. # 示例 ID 固定;重复执行时保留第一次创建的基准样本。
  120. with suppress(Exception):
  121. client.create_example(
  122. example_id=example_id,
  123. dataset_name=dataset_name,
  124. inputs={"persona": case["persona"], "text": case["input"]},
  125. outputs={
  126. "expected_tools": case["expected_tools"],
  127. "expected_terms": case["expected_terms"],
  128. },
  129. metadata={"suite": suite, "case_id": case["id"]},
  130. )
  131. result = result_by_id[str(case["id"])]
  132. trace_id = result.get("trace_id")
  133. if not trace_id:
  134. continue
  135. for key, score in result["scores"].items():
  136. client.create_feedback(
  137. run_id=UUID(str(trace_id)),
  138. key=f"eval.{key}",
  139. score=bool(score),
  140. comment=f"智保通 S1 {suite} 自动评测",
  141. )
  142. def main() -> int:
  143. if hasattr(sys.stdout, "reconfigure"):
  144. sys.stdout.reconfigure(encoding="utf-8")
  145. parser = argparse.ArgumentParser()
  146. parser.add_argument("--suite", choices=("smoke", "full"), default="smoke")
  147. parser.add_argument("--base-url", default="http://127.0.0.1:8000/api/v1")
  148. parser.add_argument("--admin-password", default="zaq1XSW@")
  149. arguments = parser.parse_args()
  150. settings = Settings()
  151. cases = load_cases(arguments.suite)
  152. with httpx.Client(base_url=arguments.base_url, timeout=120) as client:
  153. results = [run_case(client, case, arguments.admin_password) for case in cases]
  154. sync_langsmith(settings, arguments.suite, cases, results)
  155. print(json.dumps({"suite": arguments.suite, "results": results}, ensure_ascii=False, indent=2))
  156. return 0 if all(result["passed"] for result in results) else 1
  157. if __name__ == "__main__":
  158. raise SystemExit(main())