Files
group_xinghuo_jinrong/scripts/eval/evaluate_agent.py
T

439 lines
16 KiB
Python
Raw Normal View History

"""Agent 评测脚本:执行单轮和多轮评测集,输出 JSON + Markdown 报告。
用法:
python scripts/eval/evaluate_agent.py --dataset docs/开发文档/51-Agent单轮评测集.md
python scripts/eval/evaluate_agent.py --dataset docs/开发文档/52-Agent多轮场景评测集.md
python scripts/eval/evaluate_agent.py --all
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any
@dataclass
class EvalResult:
"""单条评测结果"""
case_id: str
agent_type: str
user_input: str
expected_tool: str
expected_params: dict[str, Any]
actual_tool: str | None
actual_params: dict[str, Any]
actual_response: str
tool_match: bool
param_match: bool
passed: bool
error: str | None = None
class AgentEvaluator:
"""Agent 评测器"""
def __init__(self, base_url: str = "http://127.0.0.1:8000"):
self.base_url = base_url
self.results: list[EvalResult] = []
def _login(self, username: str, password: str) -> str:
"""登录获取 token"""
import httpx
resp = httpx.post(
f"{self.base_url}/api/v1/auth/login",
json={"username": username, "password": password},
)
resp.raise_for_status()
return resp.json()["data"]["access_token"]
def _chat(self, token: str, message: str, agent_type: str) -> dict:
"""调用 chat API"""
import httpx
resp = httpx.post(
f"{self.base_url}/api/v1/chat",
headers={
"Authorization": f"Bearer {token}",
"X-Agent-Type": agent_type,
"Content-Type": "application/json",
},
json={"message": message},
timeout=30.0,
)
resp.raise_for_status()
return resp.json()["data"]
def parse_single_turn_cases(self, md_path: Path) -> list[dict]:
"""解析单轮评测集 Markdown"""
content = md_path.read_text(encoding="utf-8")
cases = []
# 匹配表格行:| ID | 用户输入 | 预期工具 | 预期参数 | 预期回复要点 |
# 或:| ID | 用户输入 | 预期工具 | 预期回复要点 |
table_pattern = re.compile(
r'\|\s*([A-Z]+-\d+)\s*\|\s*(.+?)\s*\|\s*(\w+)\s*\|\s*(.+?)\s*\|'
)
agent_type_map = {
"C": "customer",
"A": "advisor",
"AN": "analyst",
"X": "customer", # 通用场景默认用 customer
}
for match in table_pattern.finditer(content):
case_id = match.group(1)
user_input = match.group(2).strip()
expected_tool = match.group(3).strip()
rest = match.group(4).strip()
# 尝试解析参数
expected_params = {}
if rest.startswith("{"):
try:
expected_params = json.loads(rest)
rest = ""
except json.JSONDecodeError:
pass
agent_prefix = case_id.split("-")[0]
agent_type = agent_type_map.get(agent_prefix, "customer")
cases.append({
"case_id": case_id,
"agent_type": agent_type,
"user_input": user_input,
"expected_tool": expected_tool if expected_tool != "无" else None,
"expected_params": expected_params,
"notes": rest,
})
return cases
def parse_multi_turn_cases(self, md_path: Path) -> list[dict]:
"""解析多轮场景评测集 Markdown"""
content = md_path.read_text(encoding="utf-8")
scenarios = []
# 按场景分割
scenario_pattern = re.compile(
r'### 场景 ([A-Z]+-M\d+):(.+?)\n.*?```text\n(.*?)```',
re.DOTALL
)
agent_type_map = {
"C": "customer",
"A": "advisor",
"AN": "analyst",
"X": "customer",
}
for match in scenario_pattern.finditer(content):
scenario_id = match.group(1)
scenario_name = match.group(2).strip()
scenario_content = match.group(3)
# 解析轮次
turns = []
turn_pattern = re.compile(
r'轮次 (\d+):\n\s*用户:(.+?)\n\s*预期:(.+?)(?:\n\s*验证点:(.+?))?(?=\n\n轮次|\Z)',
re.DOTALL
)
for turn_match in turn_pattern.finditer(scenario_content):
turn_num = int(turn_match.group(1))
user_input = turn_match.group(2).strip()
expected = turn_match.group(3).strip()
checkpoint = turn_match.group(4).strip() if turn_match.group(4) else ""
# 特殊处理:[模拟会话中断]
if user_input.startswith("["):
user_input = user_input.split("]")[-1].strip()
turns.append({
"turn": turn_num,
"user_input": user_input,
"expected": expected,
"checkpoint": checkpoint,
})
agent_prefix = scenario_id.split("-")[0]
agent_type = agent_type_map.get(agent_prefix, "customer")
scenarios.append({
"scenario_id": scenario_id,
"scenario_name": scenario_name,
"agent_type": agent_type,
"turns": turns,
})
return scenarios
def evaluate_single_turn(self, cases: list[dict], token: str) -> list[EvalResult]:
"""执行单轮评测"""
results = []
for case in cases:
try:
response = self._chat(token, case["user_input"], case["agent_type"])
actual_tool = response.get("tool_calls", [{}])[0].get("tool_name") if response.get("tool_calls") else None
actual_params = response.get("tool_calls", [{}])[0].get("tool_params", {}) if response.get("tool_calls") else {}
tool_match = actual_tool == case["expected_tool"]
param_match = True
if case["expected_params"]:
param_match = all(
actual_params.get(k) == v
for k, v in case["expected_params"].items()
)
results.append(EvalResult(
case_id=case["case_id"],
agent_type=case["agent_type"],
user_input=case["user_input"],
expected_tool=case["expected_tool"] or "",
expected_params=case["expected_params"],
actual_tool=actual_tool,
actual_params=actual_params,
actual_response=response.get("response", ""),
tool_match=tool_match,
param_match=param_match,
passed=tool_match and param_match,
))
except Exception as e:
results.append(EvalResult(
case_id=case["case_id"],
agent_type=case["agent_type"],
user_input=case["user_input"],
expected_tool=case["expected_tool"] or "",
expected_params=case["expected_params"],
actual_tool=None,
actual_params={},
actual_response="",
tool_match=False,
param_match=False,
passed=False,
error=str(e),
))
return results
def evaluate_multi_turn(self, scenarios: list[dict], token: str) -> list[EvalResult]:
"""执行多轮场景评测"""
results = []
for scenario in scenarios:
scenario_results = []
for turn in scenario["turns"]:
try:
response = self._chat(token, turn["user_input"], scenario["agent_type"])
# 简单验证:检查响应非空且无错误
has_response = bool(response.get("response"))
no_error = "error" not in response.get("response", "").lower()
scenario_results.append(EvalResult(
case_id=f"{scenario['scenario_id']}-T{turn['turn']}",
agent_type=scenario["agent_type"],
user_input=turn["user_input"],
expected_tool=turn["expected"][:50], # 截取前50字符
expected_params={},
actual_tool=None,
actual_params={},
actual_response=response.get("response", ""),
tool_match=has_response,
param_match=no_error,
passed=has_response and no_error,
))
except Exception as e:
scenario_results.append(EvalResult(
case_id=f"{scenario['scenario_id']}-T{turn['turn']}",
agent_type=scenario["agent_type"],
user_input=turn["user_input"],
expected_tool=turn["expected"][:50],
expected_params={},
actual_tool=None,
actual_params={},
actual_response="",
tool_match=False,
param_match=False,
passed=False,
error=str(e),
))
results.extend(scenario_results)
return results
def generate_report(self, results: list[EvalResult], output_path: Path) -> None:
"""生成评测报告"""
total = len(results)
passed = sum(1 for r in results if r.passed)
tool_correct = sum(1 for r in results if r.tool_match)
param_correct = sum(1 for r in results if r.param_match)
# 按 Agent 统计
by_agent = {}
for r in results:
if r.agent_type not in by_agent:
by_agent[r.agent_type] = {"total": 0, "passed": 0}
by_agent[r.agent_type]["total"] += 1
if r.passed:
by_agent[r.agent_type]["passed"] += 1
report = {
"timestamp": datetime.now().isoformat(),
"summary": {
"total": total,
"passed": passed,
"failed": total - passed,
"pass_rate": round(passed / total, 4) if total > 0 else 0,
"tool_accuracy": round(tool_correct / total, 4) if total > 0 else 0,
"param_accuracy": round(param_correct / total, 4) if total > 0 else 0,
},
"by_agent": by_agent,
"failed_cases": [
{
"case_id": r.case_id,
"user_input": r.user_input,
"expected_tool": r.expected_tool,
"actual_tool": r.actual_tool,
"error": r.error,
}
for r in results if not r.passed
],
}
# 保存 JSON
json_path = output_path.with_suffix(".json")
json_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
# 生成 Markdown
md_lines = [
"# Agent 评测报告",
"",
f"**评测时间**:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"",
"## 总体统计",
"",
f"- **总用例数**:{total}",
f"- **通过数**:{passed}",
f"- **失败数**:{total - passed}",
f"- **通过率**:{report['summary']['pass_rate']:.2%}",
f"- **工具准确率**:{report['summary']['tool_accuracy']:.2%}",
f"- **参数准确率**:{report['summary']['param_accuracy']:.2%}",
"",
"## 按 Agent 统计",
"",
]
for agent, stats in by_agent.items():
rate = stats["passed"] / stats["total"] if stats["total"] > 0 else 0
md_lines.append(f"- **{agent}**:{stats['passed']}/{stats['total']} ({rate:.2%})")
md_lines.extend([
"",
"## 失败用例",
"",
])
for r in results:
if not r.passed:
md_lines.extend([
f"### {r.case_id}",
"",
f"- **输入**:{r.user_input}",
f"- **预期工具**:{r.expected_tool}",
f"- **实际工具**:{r.actual_tool or 'None'}",
])
if r.error:
md_lines.append(f"- **错误**:{r.error}")
md_lines.append("")
output_path.write_text("\n".join(md_lines), encoding="utf-8")
print(f"报告已生成:{output_path}")
print(f"JSON 报告:{json_path}")
def main():
parser = argparse.ArgumentParser(description="Agent 评测脚本")
parser.add_argument("--dataset", type=Path, help="评测集 Markdown 文件路径")
parser.add_argument("--all", action="store_true", help="运行所有评测集")
parser.add_argument("--base-url", default="http://127.0.0.1:8000", help="API 基础 URL")
parser.add_argument("--output", type=Path, help="报告输出路径")
args = parser.parse_args()
evaluator = AgentEvaluator(args.base_url)
# 登录获取 token
print("正在登录...")
try:
token = evaluator._login("advisor_test", "advisor_test")
except Exception as e:
print(f"登录失败:{e}")
print("请确保服务已启动:uvicorn app.main:app --reload")
sys.exit(1)
all_results = []
if args.all or args.dataset:
datasets = []
if args.all:
datasets = [
Path("docs/开发文档/51-Agent单轮评测集.md"),
Path("docs/开发文档/52-Agent多轮场景评测集.md"),
]
else:
datasets = [args.dataset]
for dataset_path in datasets:
if not dataset_path.exists():
print(f"评测集不存在:{dataset_path}")
continue
print(f"\n正在评测:{dataset_path}")
if "单轮" in dataset_path.name:
cases = evaluator.parse_single_turn_cases(dataset_path)
print(f"解析到 {len(cases)} 条单轮用例")
results = evaluator.evaluate_single_turn(cases, token)
else:
scenarios = evaluator.parse_multi_turn_cases(dataset_path)
print(f"解析到 {len(scenarios)} 个多轮场景")
results = evaluator.evaluate_multi_turn(scenarios, token)
all_results.extend(results)
print(f"完成:{sum(1 for r in results if r.passed)}/{len(results)} 通过")
if all_results:
output_path = args.output or Path("docs/评测报告/agent_eval_report.md")
output_path.parent.mkdir(parents=True, exist_ok=True)
evaluator.generate_report(all_results, output_path)
# 输出摘要
total = len(all_results)
passed = sum(1 for r in all_results if r.passed)
print(f"\n总计:{passed}/{total} 通过 ({passed/total:.2%})")
# 判断是否达标
if passed / total >= 0.9:
print("✓ 达到通过标准(≥90%)")
sys.exit(0)
else:
print("✗ 未达到通过标准(<90%)")
sys.exit(1)
else:
print("未执行任何评测")
sys.exit(0)
if __name__ == "__main__":
main()