| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- """使用真实 HTTP、真实大模型和 LangSmith Trace 评测两种 Agent 人格。"""
- import argparse
- import json
- import sys
- import time
- from contextlib import suppress
- from pathlib import Path
- from typing import Any
- from uuid import NAMESPACE_URL, UUID, uuid5
- import httpx
- from langsmith import Client
- from app.core.config import Settings
- BACKEND_ROOT = Path(__file__).resolve().parents[2]
- def load_cases(suite: str) -> list[dict[str, Any]]:
- suffix = "smoke" if suite == "smoke" else "full"
- cases: list[dict[str, Any]] = []
- for persona in ("customer", "operation"):
- path = BACKEND_ROOT / "evals" / f"{persona}-{suffix}.jsonl"
- cases.extend(
- json.loads(line)
- for line in path.read_text(encoding="utf-8").splitlines()
- if line.strip()
- )
- return cases
- def login(client: httpx.Client, persona: str, admin_password: str) -> str:
- if persona == "customer":
- response = client.post(
- "/h5/auth/login",
- json={"mobile": "18800000001", "code": "147258"},
- )
- else:
- response = client.post(
- "/admin/auth/login",
- json={"username": "admin", "password": admin_password},
- )
- response.raise_for_status()
- return str(response.json()["data"]["tokens"]["access_token"])
- def run_case(
- client: httpx.Client,
- case: dict[str, Any],
- admin_password: str,
- ) -> dict[str, Any]:
- persona = str(case["persona"])
- token = login(client, persona, admin_password)
- prefix = "" if persona == "customer" else "/admin"
- headers = {"Authorization": f"Bearer {token}"}
- thread_response = client.post(
- f"{prefix}/agent/threads",
- headers=headers,
- json={"title": f"评测-{case['id']}"},
- )
- thread_response.raise_for_status()
- thread_id = thread_response.json()["data"]["id"]
- response = client.post(
- f"{prefix}/agent/threads/{thread_id}/messages",
- headers=headers,
- json={
- "content": {"type": "text", "text": case["input"]},
- "client_message_id": f"eval-{case['id']}",
- },
- timeout=120,
- )
- response.raise_for_status()
- data = response.json()["data"]
- run_id = str(data["run_id"])
- for _ in range(5):
- if data.get("trace_url"):
- break
- time.sleep(1)
- detail = client.get(
- f"{prefix}/agent/runs/{run_id}",
- headers=headers,
- )
- detail.raise_for_status()
- data["trace_url"] = detail.json()["data"].get("trace_url")
- text = str(data["assistant_message"]["text"])
- tools = set(data.get("invoked_tools", []))
- expected_tools = set(case["expected_tools"])
- expected_terms = [str(term) for term in case["expected_terms"]]
- scores = {
- "response_nonempty": bool(text.strip()),
- "tool_coverage": expected_tools.issubset(tools),
- "term_coverage": all(term in text for term in expected_terms),
- "trace_linked": bool(data.get("trace_id")),
- }
- return {
- "id": case["id"],
- "persona": persona,
- "run_id": run_id,
- "trace_id": data.get("trace_id"),
- "trace_url": data.get("trace_url"),
- "invoked_tools": sorted(tools),
- "scores": scores,
- "passed": all(scores.values()),
- "answer": text,
- }
- def sync_langsmith(
- settings: Settings,
- suite: str,
- cases: list[dict[str, Any]],
- results: list[dict[str, Any]],
- ) -> None:
- if not settings.langsmith_api_key:
- return
- client = Client(
- api_url=settings.langsmith_endpoint,
- api_key=settings.langsmith_api_key,
- )
- dataset_name = f"智保通-S1-{suite}"
- if not client.has_dataset(dataset_name=dataset_name):
- client.create_dataset(
- dataset_name,
- description="智保通第一阶段客户与运营 Agent 真实模型评测集",
- metadata={"project": "智保通", "stage": 1, "suite": suite},
- )
- result_by_id = {result["id"]: result for result in results}
- for case in cases:
- example_id = uuid5(NAMESPACE_URL, f"{dataset_name}:{case['id']}")
- # 示例 ID 固定;重复执行时保留第一次创建的基准样本。
- with suppress(Exception):
- client.create_example(
- example_id=example_id,
- dataset_name=dataset_name,
- inputs={"persona": case["persona"], "text": case["input"]},
- outputs={
- "expected_tools": case["expected_tools"],
- "expected_terms": case["expected_terms"],
- },
- metadata={"suite": suite, "case_id": case["id"]},
- )
- result = result_by_id[str(case["id"])]
- trace_id = result.get("trace_id")
- if not trace_id:
- continue
- for key, score in result["scores"].items():
- client.create_feedback(
- run_id=UUID(str(trace_id)),
- key=f"eval.{key}",
- score=bool(score),
- comment=f"智保通 S1 {suite} 自动评测",
- )
- def main() -> int:
- if hasattr(sys.stdout, "reconfigure"):
- sys.stdout.reconfigure(encoding="utf-8")
- parser = argparse.ArgumentParser()
- parser.add_argument("--suite", choices=("smoke", "full"), default="smoke")
- parser.add_argument("--base-url", default="http://127.0.0.1:8000/api/v1")
- parser.add_argument("--admin-password", default="zaq1XSW@")
- arguments = parser.parse_args()
- settings = Settings()
- cases = load_cases(arguments.suite)
- with httpx.Client(base_url=arguments.base_url, timeout=120) as client:
- results = [run_case(client, case, arguments.admin_password) for case in cases]
- sync_langsmith(settings, arguments.suite, cases, results)
- print(json.dumps({"suite": arguments.suite, "results": results}, ensure_ascii=False, indent=2))
- return 0 if all(result["passed"] for result in results) else 1
- if __name__ == "__main__":
- raise SystemExit(main())
|