skills-eval/run_real_eval.py

380 lines
15 KiB
Python
Raw Permalink Normal View History

"""直接运行 product-design-module skill 的评估(内联 sub-agent 输出)。
本脚本
1. 读取 evals.json 中的 eval cases
2. 从指定目录读取 sub-agent 的输出文件
3. 运行确定性检查内置 + 自定义
4. 生成 benchmark 报告JSON + Markdown
5. 输出改进建议
"""
import json
import re
import sys
import time
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent
sys.path.insert(0, str(PROJECT_ROOT))
from eval_framework.checker import run_checks, load_custom_checks, CHECK_REGISTRY
from eval_framework.models import (
CheckResult, RunResult, CaseResult,
ModeResult, BenchmarkResult, TimingInfo, GradingSummary
)
def load_eval_cases(skill_name: str):
eval_path = PROJECT_ROOT / "evals" / skill_name / "evals.json"
with open(eval_path, "r", encoding="utf-8") as f:
return json.load(f)
def load_agent_outputs(skill_name: str) -> dict:
output_dir = PROJECT_ROOT / "eval_output" / skill_name / "real_agent"
outputs = {}
for case_dir in sorted(output_dir.glob("case_*")):
case_id = int(case_dir.name.split("_")[1])
output_file = case_dir / "output.md"
if output_file.exists():
with open(output_file, "r", encoding="utf-8") as f:
outputs[case_id] = f.read()
return outputs
def run_evaluation(skill_name: str):
print(f"\n{'='*60}")
print(f" 评估 Skill: {skill_name}")
print(f"{'='*60}\n")
# 1. 加载 eval 配置
eval_data = load_eval_cases(skill_name)
evals = eval_data["evals"]
print(f" 加载了 {len(evals)} 个 eval case")
# 2. 加载 sub-agent 输出
agent_outputs = load_agent_outputs(skill_name)
print(f" 加载了 {len(agent_outputs)} 个 sub-agent 输出")
# 3. 加载自定义检查
custom_checks = load_custom_checks(skill_name)
if custom_checks:
print(f" 加载了 {len(custom_checks)} 个自定义检查: {list(custom_checks.keys())}")
# 4. 对每个 eval case 运行检查
case_results = []
total_checks = 0
total_passed = 0
all_check_details = []
for eval_case in evals:
case_id = eval_case["id"]
if case_id not in agent_outputs:
print(f"\n [SKIP] Case {case_id}: 无 sub-agent 输出")
continue
output = agent_outputs[case_id]
expectations = eval_case["expectations"]
print(f"\n --- Case {case_id}: {eval_case['prompt'][:50]}... ---")
print(f" 输出长度: {len(output)} 字符")
print(f" 期望条件数: {len(expectations)}")
start_time = time.time()
check_results = run_checks(output, expectations, custom_checks)
elapsed = time.time() - start_time
passed = sum(1 for r in check_results if r.passed)
total = len(check_results)
total_checks += total
total_passed += passed
print(f" 检查结果: {passed}/{total} 通过 ({passed/total*100:.1f}%)")
for r in check_results:
status = "PASS" if r.passed else "FAIL"
exp_short = r.expectation[:60] + ("..." if len(r.expectation) > 60 else "")
print(f" [{status}] {exp_short}")
if not r.passed:
print(f" 原因: {r.reason[:80]}")
all_check_details.append({
"case_id": case_id,
"expectation": r.expectation,
"passed": r.passed,
"reason": r.reason
})
run_result = RunResult(
eval_case_id=case_id,
run_index=1,
output=output[:500] + "..." if len(output) > 500 else output,
checks=[r.model_dump() for r in check_results],
timing=TimingInfo(total_seconds=elapsed)
)
case_result = CaseResult(
eval_case_id=case_id,
runs=[run_result],
pass_rate=1.0 if passed == total else 0.0,
avg_check_pass_rate=passed / total if total > 0 else 0.0
)
case_results.append(case_result)
# 5. 聚合结果
overall_pass_rate = total_passed / total_checks if total_checks > 0 else 0.0
passed_cases = sum(1 for c in case_results if c.pass_rate == 1.0)
# 6. 保存结果
output_dir = PROJECT_ROOT / "eval_output" / skill_name / "iteration-2"
output_dir.mkdir(parents=True, exist_ok=True)
# 保存 benchmark JSON
benchmark_data = {
"skill_name": skill_name,
"iteration": 2,
"agent_type": "real_sub_agent",
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S'),
"grading": {
"total_cases": len(case_results),
"passed_cases": passed_cases,
"overall_pass_rate": overall_pass_rate,
"total_checks": total_checks,
"total_passed": total_passed
},
"case_results": []
}
for cr in case_results:
case_data = {
"eval_case_id": cr.eval_case_id,
"pass_rate": cr.pass_rate,
"avg_check_pass_rate": cr.avg_check_pass_rate,
"checks": all_check_details
}
benchmark_data["case_results"].append(case_data)
benchmark_path = output_dir / "benchmark.json"
with open(benchmark_path, "w", encoding="utf-8") as f:
json.dump(benchmark_data, f, ensure_ascii=False, indent=2)
print(f"\n Benchmark JSON 已保存: {benchmark_path}")
# 7. 生成 Markdown 报告
report = generate_markdown_report(benchmark_data, evals, case_results, all_check_details)
report_path = output_dir / "benchmark.md"
with open(report_path, "w", encoding="utf-8") as f:
f.write(report)
print(f" Benchmark 报告已保存: {report_path}")
# 8. 输出改进建议
suggestions = generate_improvement_suggestions(all_check_details)
suggestions_path = output_dir / "improvement_suggestions.md"
with open(suggestions_path, "w", encoding="utf-8") as f:
f.write(suggestions)
print(f" 改进建议已保存: {suggestions_path}")
return benchmark_data
def generate_markdown_report(benchmark_data, evals, case_results, all_checks):
lines = []
lines.append(f"# Skill 评估报告: {benchmark_data['skill_name']}")
lines.append(f"\n**迭代**: {benchmark_data['iteration']} (真实 Sub-Agent)")
lines.append(f"**评估时间**: {benchmark_data['timestamp']}")
lines.append(f"**Agent 类型**: {benchmark_data['agent_type']}")
lines.append("")
grading = benchmark_data["grading"]
lines.append("## 总体结果")
lines.append("")
lines.append("| 指标 | 值 |")
lines.append("|------|-----|")
lines.append(f"| Eval Cases 总数 | {grading['total_cases']} |")
lines.append(f"| 完全通过 Cases | {grading['passed_cases']} |")
lines.append(f"| 总体检查通过率 | {grading['overall_pass_rate']*100:.1f}% |")
lines.append(f"| 总检查项 | {grading['total_checks']} |")
lines.append(f"| 通过检查项 | {grading['total_passed']} |")
lines.append("")
# 各 Case 结果
lines.append("## 各 Case 结果概览")
lines.append("")
lines.append("| Case | Prompt 摘要 | 通过率 | 通过/总数 |")
lines.append("|------|-----------|--------|----------|")
for cr in case_results:
case_id = cr.eval_case_id
eval_case = next((e for e in evals if e["id"] == case_id), None)
prompt_short = eval_case["prompt"][:40] + "..." if eval_case else "N/A"
rate = cr.avg_check_pass_rate * 100
case_checks = [c for c in all_checks if c["case_id"] == case_id]
passed = sum(1 for c in case_checks if c["passed"])
total = len(case_checks)
lines.append(f"| {case_id} | {prompt_short} | {rate:.1f}% | {passed}/{total} |")
lines.append("")
# 详细检查结果
lines.append("## 详细检查结果")
lines.append("")
for cr in case_results:
case_id = cr.eval_case_id
eval_case = next((e for e in evals if e["id"] == case_id), None)
if not eval_case:
continue
lines.append(f"### Case {case_id}")
lines.append(f"**用户输入**: {eval_case['prompt']}")
lines.append(f"**通过率**: {cr.avg_check_pass_rate*100:.1f}%")
lines.append("")
case_checks = [c for c in all_checks if c["case_id"] == case_id]
lines.append("| # | 期望条件 | 结果 | 原因 |")
lines.append("|---|---------|------|------|")
for i, check in enumerate(case_checks):
status = "PASS" if check["passed"] else "FAIL"
exp = check["expectation"][:50] + ("..." if len(check["expectation"]) > 50 else "")
reason = check["reason"][:60] + ("..." if len(check["reason"]) > 60 else "")
lines.append(f"| {i+1} | {exp} | {status} | {reason} |")
lines.append("")
return "\n".join(lines)
def generate_improvement_suggestions(all_checks):
"""基于检查结果生成改进建议。"""
lines = []
lines.append("# Skill 改进建议")
lines.append("")
lines.append(f"生成时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
lines.append("")
# 分析失败模式
failed = [c for c in all_checks if not c["passed"]]
passed = [c for c in all_checks if c["passed"]]
lines.append("## 失败模式分析")
lines.append("")
# 按类别分组
categories = {
"三级结构": [],
"KANO分类": [],
"ICE评分": [],
"MindMapNode JSON": [],
"MindMapNode type约束": [],
"默认模块归属": [],
"汇总表": [],
"产品类型识别": [],
"特定模块覆盖": [],
"其他": []
}
for check in failed:
exp = check["expectation"]
if "三级" in exp or "结构" in exp or "层级" in exp:
categories["三级结构"].append(check)
elif "KANO" in exp and "type" not in exp.lower():
categories["KANO分类"].append(check)
elif "ICE" in exp:
categories["ICE评分"].append(check)
elif "MindMapNode" in exp and "type" in exp:
categories["MindMapNode type约束"].append(check)
elif "MindMapNode" in exp or "JSON" in exp:
categories["MindMapNode JSON"].append(check)
elif "默认模块" in exp or "归属" in exp:
categories["默认模块归属"].append(check)
elif "汇总表" in exp or "表格" in exp:
categories["汇总表"].append(check)
elif "识别" in exp or "混合型" in exp or "B端" in exp or "C端" in exp or "OA" in exp:
categories["产品类型识别"].append(check)
elif "包含" in exp and ("模块" in exp or "管理" in exp or "审批" in exp or "移动" in exp):
categories["特定模块覆盖"].append(check)
else:
categories["其他"].append(check)
lines.append(f"总失败数: {len(failed)}/{len(all_checks)}")
lines.append("")
for cat, checks in categories.items():
if checks:
lines.append(f"### {cat} ({len(checks)} 项失败)")
for c in checks:
lines.append(f"- Case {c['case_id']}: {c['expectation'][:60]}")
lines.append(f" 原因: {c['reason'][:80]}")
lines.append("")
# 改进建议
lines.append("## 具体改进建议")
lines.append("")
if categories["三级结构"]:
lines.append("### 1. 三级结构检查优化")
lines.append("- **问题**: 关键词检查无法识别语义等价的结构表达")
lines.append("- **建议**: 使用自定义检查函数 `three_level_structure` 替代关键词检查")
lines.append("- **建议**: 在 evals.json 中为结构类 expectation 添加 `check_type` 字段指定检查方式")
lines.append("")
if categories["KANO分类"]:
lines.append("### 2. KANO 分类检查优化")
lines.append("- **问题**: 期望 '每个L3功能点标注了KANO分类基本型/期望型/兴奋型)' 要求三种都出现,但某些 case 可能只有两种")
lines.append("- **建议**: 使用自定义检查 `kano_classification` 替代关键词检查")
lines.append("- **建议**: 放宽检查条件——只要存在至少一种 KANO 分类即通过")
lines.append("")
if categories["ICE评分"]:
lines.append("### 3. ICE 评分检查优化")
lines.append("- **问题**: 关键词检查无法识别 `ICE9×9×7=567` 这样的格式")
lines.append("- **建议**: 使用自定义检查 `ice_score` 替代关键词检查")
lines.append("")
if categories["MindMapNode type约束"]:
lines.append("### 4. MindMapNode type 约束检查优化")
lines.append("- **问题**: 关键词检查无法验证 JSON 中的 type 字段值")
lines.append("- **建议**: 使用自定义检查 `mindmap_type_constraint` 替代关键词检查")
lines.append("")
if categories["默认模块归属"]:
lines.append("### 5. 默认模块归属检查优化")
lines.append("- **问题**: 期望 '质量/资源/性能需求单独标注' 但不是所有 case 都需要这些模块")
lines.append("- **建议**: 使用自定义检查 `default_module_label` 替代关键词检查")
lines.append("- **建议**: 区分 '必须包含全部4个默认模块''至少标注了归属' 两种检查级别")
lines.append("")
if categories["产品类型识别"]:
lines.append("### 6. 产品类型识别检查优化")
lines.append("- **问题**: 关键词检查要求精确匹配 'B端C端混合型' 等表述,但 agent 可能使用不同表述")
lines.append("- **建议**: 使用语义化检查——只要输出中体现了 C端和B端的区分即可通过")
lines.append("")
if categories["特定模块覆盖"]:
lines.append("### 7. 特定模块覆盖检查优化")
lines.append("- **问题**: 关键词检查要求精确匹配模块名称(如 '租户管理'),但 agent 可能使用不同命名")
lines.append("- **建议**: 使用同义词匹配或语义检查")
lines.append("")
# 通用建议
lines.append("## 通用改进建议")
lines.append("")
lines.append("### 检查框架优化")
lines.append("1. **为每个 expectation 指定 check_type**: 在 evals.json 中添加 `check_type` 字段,明确指定使用哪个检查函数")
lines.append("2. **增加语义检查**: 对于产品类型识别、模块覆盖等,使用 LLM-as-Judge 进行语义评估")
lines.append("3. **分级检查**: 区分 '必须通过''建议通过' 两个级别")
lines.append("")
lines.append("### Skill 内容优化")
lines.append("1. **强化输出格式约束**: 在 skill 中更明确地要求输出格式,减少 agent 的自由发挥空间")
lines.append("2. **添加输出检查清单**: 在 skill 末尾添加自检清单,让 agent 在输出前自行验证")
lines.append("3. **统一术语**: 在 skill 中明确要求使用特定术语(如 'B端C端混合型''能力需求' 等)")
return "\n".join(lines)
if __name__ == "__main__":
skill_name = "product-design-module"
result = run_evaluation(skill_name)
print(f"\n{'='*60}")
print(f" 评估完成!")
print(f" 总体通过率: {result['grading']['overall_pass_rate']*100:.1f}%")
print(f" 通过 Cases: {result['grading']['passed_cases']}/{result['grading']['total_cases']}")
print(f" 通过检查项: {result['grading']['total_passed']}/{result['grading']['total_checks']}")
print(f"{'='*60}")