|
@@ -0,0 +1,486 @@
|
|
|
|
|
+#!/usr/bin/env python
|
|
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
|
|
+"""
|
|
|
|
|
+目录审查器稳定性测试
|
|
|
|
|
+
|
|
|
|
|
+专门测试 CatalogReviewer 的 JSON 解析稳定性,复现服务端"LLM输出非JSON/畸形JSON"问题。
|
|
|
|
|
+测试目标:
|
|
|
|
|
+ 1. 多次调用结果是否都是合法 JSON
|
|
|
|
|
+ 2. 结果结构是否包含 "details" 键
|
|
|
|
|
+ 3. 是否落入 fallback(fallback 意味着 JSON 解析全部失败)
|
|
|
|
|
+ 4. 相同输入是否得到一致的结果(确定性)
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ # 每场景跑 20 次
|
|
|
|
|
+ # python utils_test/Catalog_Review_Test/test_catalog_review_stability.py --runs 20
|
|
|
|
|
+
|
|
|
|
|
+ # 或只跑真实目录场景 20 次
|
|
|
|
|
+ # python utils_test/Catalog_Review_Test/test_catalog_review_stability.py --scenario real --runs 20
|
|
|
|
|
+
|
|
|
|
|
+import asyncio
|
|
|
|
|
+import sys
|
|
|
|
|
+import os
|
|
|
|
|
+import json
|
|
|
|
|
+import time
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+
|
|
|
|
|
+current_dir = Path(__file__).parent.absolute()
|
|
|
|
|
+project_root = current_dir.parent.parent
|
|
|
|
|
+os.chdir(str(project_root))
|
|
|
|
|
+sys.path.insert(0, str(project_root))
|
|
|
|
|
+
|
|
|
|
|
+from core.construction_review.component.reviewers.catalog_reviewer import CatalogReviewer
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ======================== 测试用例 ========================
|
|
|
|
|
+#
|
|
|
|
|
+# 数据来源:真实业务场景的 structured_content.catalog,
|
|
|
|
|
+# 经过节点 json.loads → catalog_raw.get('formatted_text') 后得到的文本格式。
|
|
|
|
|
+# 如果 catalog_raw 中没有 formatted_text,则代码会从 chapters 构建
|
|
|
|
|
+# (chapter.title + subsections[].title),如下所示。
|
|
|
|
|
+
|
|
|
|
|
+# 场景 1:真实目录(来自系统实际业务数据)
|
|
|
|
|
+# 特点:只有第 3~10 章,缺第 1~2 章(编制依据、工程概况)
|
|
|
|
|
+REAL_CATALOG = """第三章 施工计划
|
|
|
|
|
+ 一、 施工进度计划
|
|
|
|
|
+ 二、 施工材料计划
|
|
|
|
|
+ 三、 施工设备计划
|
|
|
|
|
+ 四、 劳动力计划
|
|
|
|
|
+ 五、 安全生产费用使用计划
|
|
|
|
|
+第四章 施工工艺技术
|
|
|
|
|
+ 一、 主要施工方法概述
|
|
|
|
|
+ 二、 技术参数
|
|
|
|
|
+ 三、 工艺流程
|
|
|
|
|
+ 四、 施工准备
|
|
|
|
|
+ 五、 施工方法及操作要求
|
|
|
|
|
+ 六、 检查要求
|
|
|
|
|
+第五章 安全保证措施
|
|
|
|
|
+ 一、 安全保证体系
|
|
|
|
|
+ 二、 组织保证措施
|
|
|
|
|
+ 三、 技术保障措施
|
|
|
|
|
+ 四、 安全防护措施
|
|
|
|
|
+ 五、 监测监控措施
|
|
|
|
|
+ 六、 应急处置措施
|
|
|
|
|
+第六章 质量保证措施
|
|
|
|
|
+ 一、 质量保证体系
|
|
|
|
|
+ 二、 质量目标
|
|
|
|
|
+ 三、 工程创优规划
|
|
|
|
|
+ 四、 质量控制程序与具体措施
|
|
|
|
|
+第七章 环境保证措施
|
|
|
|
|
+ 一、 环境保证体系
|
|
|
|
|
+ 二、 环境保护组织机构
|
|
|
|
|
+ 三、 环境保护及文明施工措施
|
|
|
|
|
+第八章 施工管理及作业人员配备与分工
|
|
|
|
|
+ 一、 施工管理人员
|
|
|
|
|
+ 二、 专职安全生产管理人员
|
|
|
|
|
+ 三、 特种作业人员
|
|
|
|
|
+ 四、 其他作业人员
|
|
|
|
|
+第九章 验收要求
|
|
|
|
|
+ 一、 验收标准
|
|
|
|
|
+ 二、 验收程序
|
|
|
|
|
+ 三、 验收内容
|
|
|
|
|
+ 四、 验收时间
|
|
|
|
|
+ 五、 验收人员
|
|
|
|
|
+第十章 其他资料
|
|
|
|
|
+ 一、 计算书
|
|
|
|
|
+ 二、 相关施工图纸
|
|
|
|
|
+ 三、 附图附表
|
|
|
|
|
+ 四、 编制及审核人员情况"""
|
|
|
|
|
+
|
|
|
|
|
+# 场景 2:完整目录(实际目录与标准目录完全一致,用于验证"无缺失"是否稳定)
|
|
|
|
|
+FULL_CATALOG = """第一章 编制依据
|
|
|
|
|
+ 一、法律法规
|
|
|
|
|
+ 二、标准规范
|
|
|
|
|
+ 三、文件制度
|
|
|
|
|
+ 四、编制原则
|
|
|
|
|
+ 五、编制范围
|
|
|
|
|
+第二章 工程概况
|
|
|
|
|
+ 一、设计概况
|
|
|
|
|
+ 二、工程地质与水文气象
|
|
|
|
|
+ 三、周边环境
|
|
|
|
|
+ 四、施工平面及立面布置
|
|
|
|
|
+ 五、施工要求和技术保证条件
|
|
|
|
|
+ 六、风险辨识与分级
|
|
|
|
|
+ 七、参建各方责任主体单位
|
|
|
|
|
+第三章 施工计划
|
|
|
|
|
+ 一、施工进度计划
|
|
|
|
|
+ 二、施工材料计划
|
|
|
|
|
+ 三、施工设备计划
|
|
|
|
|
+ 四、劳动力计划
|
|
|
|
|
+ 五、安全生产费用使用计划
|
|
|
|
|
+第四章 施工工艺技术
|
|
|
|
|
+ 一、主要施工方法概述
|
|
|
|
|
+ 二、技术参数
|
|
|
|
|
+ 三、工艺流程
|
|
|
|
|
+ 四、施工准备
|
|
|
|
|
+ 五、施工方法及操作要求
|
|
|
|
|
+ 六、检查要求
|
|
|
|
|
+第五章 安全保证措施
|
|
|
|
|
+ 一、安全保证体系
|
|
|
|
|
+ 二、组织保证措施
|
|
|
|
|
+ 三、技术保证措施
|
|
|
|
|
+ 四、监测监控措施
|
|
|
|
|
+ 五、应急处置措施
|
|
|
|
|
+第六章 质量保证措施
|
|
|
|
|
+ 一、质量保证体系
|
|
|
|
|
+ 二、质量目标
|
|
|
|
|
+ 三、工程创优规划
|
|
|
|
|
+ 四、质量控制程序与具体措施
|
|
|
|
|
+第七章 环境保证措施
|
|
|
|
|
+ 一、环境保证体系
|
|
|
|
|
+ 二、环境保护组织机构
|
|
|
|
|
+ 三、环境保护及文明施工措施
|
|
|
|
|
+第八章 施工管理及作业人员配备与分工
|
|
|
|
|
+ 一、施工管理人员
|
|
|
|
|
+ 二、专职安全生产管理人员
|
|
|
|
|
+ 三、其他作业人员
|
|
|
|
|
+第九章 验收要求
|
|
|
|
|
+ 一、验收标准
|
|
|
|
|
+ 二、验收程序
|
|
|
|
|
+ 三、验收内容
|
|
|
|
|
+ 四、验收时间
|
|
|
|
|
+ 五、验收人员
|
|
|
|
|
+第十章 其他资料
|
|
|
|
|
+ 一、计算书
|
|
|
|
|
+ 二、相关施工图纸
|
|
|
|
|
+ 三、附图附表
|
|
|
|
|
+ 四、编制及审核人员情况"""
|
|
|
|
|
+
|
|
|
|
|
+# 场景 3:真实目录 + 自定义缺失(在真实数据上去掉"第四章 施工工艺技术")
|
|
|
|
|
+REAL_MISSING_CHAPTER = """第三章 施工计划
|
|
|
|
|
+ 一、 施工进度计划
|
|
|
|
|
+ 二、 施工材料计划
|
|
|
|
|
+ 三、 施工设备计划
|
|
|
|
|
+ 四、 劳动力计划
|
|
|
|
|
+ 五、 安全生产费用使用计划
|
|
|
|
|
+第五章 安全保证措施
|
|
|
|
|
+ 一、 安全保证体系
|
|
|
|
|
+ 二、 组织保证措施
|
|
|
|
|
+ 三、 技术保障措施
|
|
|
|
|
+ 四、 安全防护措施
|
|
|
|
|
+ 五、 监测监控措施
|
|
|
|
|
+ 六、 应急处置措施
|
|
|
|
|
+第六章 质量保证措施
|
|
|
|
|
+ 一、 质量保证体系
|
|
|
|
|
+ 二、 质量目标
|
|
|
|
|
+ 三、 工程创优规划
|
|
|
|
|
+ 四、 质量控制程序与具体措施
|
|
|
|
|
+第七章 环境保证措施
|
|
|
|
|
+ 一、 环境保证体系
|
|
|
|
|
+ 二、 环境保护组织机构
|
|
|
|
|
+ 三、 环境保护及文明施工措施
|
|
|
|
|
+第八章 施工管理及作业人员配备与分工
|
|
|
|
|
+ 一、 施工管理人员
|
|
|
|
|
+ 二、 专职安全生产管理人员
|
|
|
|
|
+ 三、 特种作业人员
|
|
|
|
|
+ 四、 其他作业人员
|
|
|
|
|
+第九章 验收要求
|
|
|
|
|
+ 一、 验收标准
|
|
|
|
|
+ 二、 验收程序
|
|
|
|
|
+ 三、 验收内容
|
|
|
|
|
+ 四、 验收时间
|
|
|
|
|
+ 五、 验收人员
|
|
|
|
|
+第十章 其他资料
|
|
|
|
|
+ 一、 计算书
|
|
|
|
|
+ 二、 相关施工图纸
|
|
|
|
|
+ 三、 附图附表
|
|
|
|
|
+ 四、 编制及审核人员情况"""
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class TestCatalogReviewStability:
|
|
|
|
|
+ """目录审查稳定性测试"""
|
|
|
|
|
+
|
|
|
|
|
+ def __init__(self):
|
|
|
|
|
+ self.reviewer = None
|
|
|
|
|
+ self.results = {}
|
|
|
|
|
+
|
|
|
|
|
+ def setup(self):
|
|
|
|
|
+ """初始化审查器"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ self.reviewer = CatalogReviewer()
|
|
|
|
|
+ print("CatalogReviewer 初始化成功")
|
|
|
|
|
+ return True
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"CatalogReviewer 初始化失败: {e}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def _validate_result(self, result: dict, scenario_name: str, attempt: int) -> dict:
|
|
|
|
|
+ """
|
|
|
|
|
+ 验证审查结果是否合法。
|
|
|
|
|
+
|
|
|
|
|
+ 返回 { "pass": bool, "issues": [str], "is_fallback": bool, ... }
|
|
|
|
|
+ """
|
|
|
|
|
+ issues = []
|
|
|
|
|
+ is_fallback = False
|
|
|
|
|
+
|
|
|
|
|
+ if not result:
|
|
|
|
|
+ issues.append("结果为空")
|
|
|
|
|
+ return {"pass": False, "issues": issues, "is_fallback": is_fallback}
|
|
|
|
|
+
|
|
|
|
|
+ if "execution_time" not in result:
|
|
|
|
|
+ issues.append("缺少 execution_time")
|
|
|
|
|
+
|
|
|
|
|
+ details = result.get("details")
|
|
|
|
|
+ if not details:
|
|
|
|
|
+ issues.append("缺少 details 或 details 为空")
|
|
|
|
|
+ return {"pass": False, "issues": issues, "is_fallback": is_fallback}
|
|
|
|
|
+
|
|
|
|
|
+ response = details.get("response")
|
|
|
|
|
+ if not response or not isinstance(response, list):
|
|
|
|
|
+ issues.append("details.response 缺失或不是列表")
|
|
|
|
|
+ return {"pass": False, "issues": issues, "is_fallback": is_fallback}
|
|
|
|
|
+
|
|
|
|
|
+ if len(response) == 0:
|
|
|
|
|
+ issues.append("details.response 为空列表")
|
|
|
|
|
+
|
|
|
|
|
+ for i, item in enumerate(response):
|
|
|
|
|
+ if not isinstance(item, dict):
|
|
|
|
|
+ issues.append(f"response[{i}] 不是字典")
|
|
|
|
|
+ continue
|
|
|
|
|
+ cr = item.get("check_result")
|
|
|
|
|
+ if not cr:
|
|
|
|
|
+ issues.append(f"response[{i}] 缺少 check_result")
|
|
|
|
|
+ continue
|
|
|
|
|
+ for field in ["issue_point", "location", "suggestion", "risk_level"]:
|
|
|
|
|
+ if field not in cr:
|
|
|
|
|
+ issues.append(f"response[{i}].check_result 缺少 {field}")
|
|
|
|
|
+ if "exist_issue" not in item:
|
|
|
|
|
+ issues.append(f"response[{i}] 缺少 exist_issue")
|
|
|
|
|
+
|
|
|
|
|
+ # 判断是否 fallback(有缺失场景却只返回一条"无")
|
|
|
|
|
+ if len(response) == 1:
|
|
|
|
|
+ cr = response[0].get("check_result", {})
|
|
|
|
|
+ if cr.get("issue_point") == "无" and cr.get("risk_level") == "无风险" and response[0].get("exist_issue") is False:
|
|
|
|
|
+ is_fallback = True
|
|
|
|
|
+
|
|
|
|
|
+ # === 语义验证 ===
|
|
|
|
|
+ semantic_issues = self._check_semantic(response, scenario_name)
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "pass": len(issues) == 0 and not semantic_issues,
|
|
|
|
|
+ "issues": issues,
|
|
|
|
|
+ "semantic_issues": semantic_issues,
|
|
|
|
|
+ "is_fallback": is_fallback,
|
|
|
|
|
+ "response_count": len(response),
|
|
|
|
|
+ "execution_time": result.get("execution_time", 0)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ def _check_semantic(self, response: list, scenario_name: str) -> list:
|
|
|
|
|
+ """
|
|
|
|
|
+ 验证审查结果的语义是否符合测试用例预期。
|
|
|
|
|
+ 返回语义问题列表,空列表表示符合预期。
|
|
|
|
|
+ """
|
|
|
|
|
+ semantic_issues = []
|
|
|
|
|
+
|
|
|
|
|
+ # 提取所有 issue_point
|
|
|
|
|
+ points = []
|
|
|
|
|
+ for item in response:
|
|
|
|
|
+ cr = item.get("check_result", {})
|
|
|
|
|
+ points.append(cr.get("issue_point", ""))
|
|
|
|
|
+
|
|
|
|
|
+ points_str = " ".join(points)
|
|
|
|
|
+
|
|
|
|
|
+ if "完整目录" in scenario_name or "无缺失" in scenario_name:
|
|
|
|
|
+ # 预期:无缺失,exist_issue=false
|
|
|
|
|
+ if not points:
|
|
|
|
|
+ semantic_issues.append("完整目录场景未返回任何结果")
|
|
|
|
|
+ else:
|
|
|
|
|
+ has_missing = any("缺失" in p for p in points)
|
|
|
|
|
+ if has_missing:
|
|
|
|
|
+ semantic_issues.append("完整目录场景不应报告缺失,但结果包含缺失标记")
|
|
|
|
|
+
|
|
|
|
|
+ elif "缺第1-2章" in scenario_name:
|
|
|
|
|
+ # 预期:至少报告第1、2章缺失
|
|
|
|
|
+ if not points:
|
|
|
|
|
+ semantic_issues.append("未返回任何结果")
|
|
|
|
|
+ else:
|
|
|
|
|
+ if not any("第一章" in p or "编制依据" in p for p in points):
|
|
|
|
|
+ semantic_issues.append("应检测到第一章(编制依据)缺失但未报告")
|
|
|
|
|
+ if not any("第二章" in p or "工程概况" in p for p in points):
|
|
|
|
|
+ semantic_issues.append("应检测到第二章(工程概况)缺失但未报告")
|
|
|
|
|
+
|
|
|
|
|
+ elif "再少一章" in scenario_name:
|
|
|
|
|
+ # 预期:至少报告第1、2、4章缺失
|
|
|
|
|
+ if not points:
|
|
|
|
|
+ semantic_issues.append("未返回任何结果")
|
|
|
|
|
+ else:
|
|
|
|
|
+ if not any("第一章" in p or "编制依据" in p for p in points):
|
|
|
|
|
+ semantic_issues.append("应检测到第一章(编制依据)缺失但未报告")
|
|
|
|
|
+ if not any("第二章" in p or "工程概况" in p for p in points):
|
|
|
|
|
+ semantic_issues.append("应检测到第二章(工程概况)缺失但未报告")
|
|
|
|
|
+ if not any("第四章" in p or "施工工艺技术" in p for p in points):
|
|
|
|
|
+ semantic_issues.append("应检测到第四章(施工工艺技术)缺失但未报告")
|
|
|
|
|
+
|
|
|
|
|
+ return semantic_issues
|
|
|
|
|
+
|
|
|
|
|
+ async def test_scenario(self, catalog_text: str, scenario_name: str, runs: int = 3):
|
|
|
|
|
+ """
|
|
|
|
|
+ 对同一个场景运行多次,验证:
|
|
|
|
|
+ 1. 每次调用都不抛异常
|
|
|
|
|
+ 2. 每次调用结果都是合法 JSON 结构
|
|
|
|
|
+ 3. 不落入 fallback(除非输入本身就是完整目录)
|
|
|
|
|
+ """
|
|
|
|
|
+ print(f"\n{'='*60}")
|
|
|
|
|
+ print(f"场景: {scenario_name}")
|
|
|
|
|
+ print(f"运行次数: {runs}")
|
|
|
|
|
+ print(f"{'='*60}")
|
|
|
|
|
+
|
|
|
|
|
+ scenario_results = []
|
|
|
|
|
+ all_pass = True
|
|
|
|
|
+
|
|
|
|
|
+ for i in range(runs):
|
|
|
|
|
+ print(f"\n--- 第 {i+1} 次调用 ---")
|
|
|
|
|
+ try:
|
|
|
|
|
+ start = time.time()
|
|
|
|
|
+ result = await self.reviewer.review(
|
|
|
|
|
+ actual_catalog_text=catalog_text,
|
|
|
|
|
+ trace_id_idx=f"test_stability_{scenario_name}_{i}"
|
|
|
|
|
+ )
|
|
|
|
|
+ elapsed = time.time() - start
|
|
|
|
|
+ val = self._validate_result(result, scenario_name, i)
|
|
|
|
|
+ val["elapsed"] = elapsed
|
|
|
|
|
+
|
|
|
|
|
+ status = "PASS" if val["pass"] else "FAIL"
|
|
|
|
|
+ if val["is_fallback"] and "完整" not in scenario_name and "一致" not in scenario_name:
|
|
|
|
|
+ print(f" [{status}] ⚠️ 落入 fallback(JSON 解析全部失败)")
|
|
|
|
|
+ all_pass = False
|
|
|
|
|
+ elif val["semantic_issues"]:
|
|
|
|
|
+ print(f" [{status}] ❌ 语义不符: {val['semantic_issues']}")
|
|
|
|
|
+ all_pass = False
|
|
|
|
|
+ elif val["pass"]:
|
|
|
|
|
+ print(f" [{status}] ✅ 合法结果 | response 数: {val['response_count']} | 耗时: {elapsed:.1f}s")
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(f" [{status}] ❌ 结构异常: {val['issues']}")
|
|
|
|
|
+ all_pass = False
|
|
|
|
|
+
|
|
|
|
|
+ scenario_results.append(val)
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f" [ERROR] ❌ 异常: {e}")
|
|
|
|
|
+ all_pass = False
|
|
|
|
|
+ scenario_results.append({"pass": False, "error": str(e)})
|
|
|
|
|
+
|
|
|
|
|
+ # 汇总
|
|
|
|
|
+ print(f"\n--- 场景汇总: {scenario_name} ---")
|
|
|
|
|
+ pass_count = sum(1 for r in scenario_results if r.get("pass"))
|
|
|
|
|
+ fallback_count = sum(1 for r in scenario_results if r.get("is_fallback"))
|
|
|
|
|
+ semantic_fail = sum(1 for r in scenario_results if r.get("semantic_issues"))
|
|
|
|
|
+ times = [r.get("elapsed", 0) for r in scenario_results if "elapsed" in r]
|
|
|
|
|
+ retry_count = sum(1 for r in scenario_results if r.get("was_retry", False))
|
|
|
|
|
+
|
|
|
|
|
+ print(f" 结构合法: {pass_count}/{runs}")
|
|
|
|
|
+ if semantic_fail > 0:
|
|
|
|
|
+ print(f" 语义不符: {semantic_fail}/{runs} ⚠️")
|
|
|
|
|
+ if fallback_count > 0 and "完整" not in scenario_name:
|
|
|
|
|
+ print(f" 落入 fallback: {fallback_count}/{runs}")
|
|
|
|
|
+ if retry_count > 0:
|
|
|
|
|
+ print(f" 经过重试: {retry_count}/{runs}")
|
|
|
|
|
+ if times:
|
|
|
|
|
+ print(f" 耗时: avg={sum(times)/len(times):.1f}s min={min(times):.1f}s max={max(times):.1f}s")
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "scenario": scenario_name,
|
|
|
|
|
+ "all_pass": all_pass,
|
|
|
|
|
+ "results": scenario_results
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ def print_summary(self, scenario_results: list):
|
|
|
|
|
+ """打印最终汇总"""
|
|
|
|
|
+ print(f"\n{'='*60}")
|
|
|
|
|
+ print("最终测试汇总")
|
|
|
|
|
+ print(f"{'='*60}")
|
|
|
|
|
+
|
|
|
|
|
+ total = 0
|
|
|
|
|
+ struct_ok = 0
|
|
|
|
|
+ semantic_ok = 0
|
|
|
|
|
+ semantic_fails = []
|
|
|
|
|
+ fallback_total = 0
|
|
|
|
|
+
|
|
|
|
|
+ for sr in scenario_results:
|
|
|
|
|
+ runs = len(sr["results"])
|
|
|
|
|
+ total += runs
|
|
|
|
|
+ sp = sum(1 for r in sr["results"] if r.get("pass"))
|
|
|
|
|
+ sf = sum(1 for r in sr["results"] if r.get("semantic_issues"))
|
|
|
|
|
+ fb = sum(1 for r in sr["results"] if r.get("is_fallback"))
|
|
|
|
|
+ struct_ok += sp
|
|
|
|
|
+ semantic_ok += (runs - sf)
|
|
|
|
|
+ fallback_total += fb
|
|
|
|
|
+
|
|
|
|
|
+ if sf > 0:
|
|
|
|
|
+ semantic_fails.append(f"{sr['scenario']}({sf}/{runs})")
|
|
|
|
|
+
|
|
|
|
|
+ issues_detail = []
|
|
|
|
|
+ if sf > 0:
|
|
|
|
|
+ issues_detail.append(f"语义不符{sf}")
|
|
|
|
|
+ if fb > 0 and "完整" not in sr['scenario']:
|
|
|
|
|
+ issues_detail.append(f"fallback{fb}")
|
|
|
|
|
+ detail = f" ({', '.join(issues_detail)})" if issues_detail else ""
|
|
|
|
|
+
|
|
|
|
|
+ if sr["all_pass"]:
|
|
|
|
|
+ print(f" ✅ {sr['scenario']}: {sp}/{runs} 通过{detail}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(f" ❌ {sr['scenario']}: {sp}/{runs} 通过{detail}")
|
|
|
|
|
+
|
|
|
|
|
+ print(f"\n结构合法: {struct_ok}/{total}")
|
|
|
|
|
+ print(f"语义正确: {semantic_ok}/{total}")
|
|
|
|
|
+ if fallback_total > 0:
|
|
|
|
|
+ print(f"fallback: {fallback_total}/{total}")
|
|
|
|
|
+ if semantic_fails:
|
|
|
|
|
+ print(f"语义不符场景: {'; '.join(semantic_fails)}")
|
|
|
|
|
+ return False
|
|
|
|
|
+ else:
|
|
|
|
|
+ print("所有场景全部通过 ✅")
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ async def run_all(self, runs_per_scenario: int = 3):
|
|
|
|
|
+ """运行所有测试场景"""
|
|
|
|
|
+ if not self.setup():
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ scenarios = [
|
|
|
|
|
+ (REAL_CATALOG, "真实目录(缺第1-2章)"),
|
|
|
|
|
+ (FULL_CATALOG, "完整目录(无缺失预期)"),
|
|
|
|
|
+ (REAL_MISSING_CHAPTER, "真实目录再少一章"),
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ all_results = []
|
|
|
|
|
+ for catalog_text, scenario_name in scenarios:
|
|
|
|
|
+ sr = await self.test_scenario(catalog_text, scenario_name, runs=runs_per_scenario)
|
|
|
|
|
+ all_results.append(sr)
|
|
|
|
|
+
|
|
|
|
|
+ return self.print_summary(all_results)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def main():
|
|
|
|
|
+ import argparse
|
|
|
|
|
+
|
|
|
|
|
+ parser = argparse.ArgumentParser(description="目录审查器稳定性测试")
|
|
|
|
|
+ parser.add_argument("--runs", type=int, default=3, help="每个场景运行次数(默认 3)")
|
|
|
|
|
+ parser.add_argument("--scenario", type=str, default=None,
|
|
|
|
|
+ help="指定场景: real | full | real_missing")
|
|
|
|
|
+ args = parser.parse_args()
|
|
|
|
|
+
|
|
|
|
|
+ tester = TestCatalogReviewStability()
|
|
|
|
|
+
|
|
|
|
|
+ if args.scenario:
|
|
|
|
|
+ scenario_map = {
|
|
|
|
|
+ "real": (REAL_CATALOG, "真实目录(缺第1-2章)"),
|
|
|
|
|
+ "full": (FULL_CATALOG, "完整目录(无缺失预期)"),
|
|
|
|
|
+ "real_missing": (REAL_MISSING_CHAPTER, "真实目录再少一章"),
|
|
|
|
|
+ }
|
|
|
|
|
+ if args.scenario not in scenario_map:
|
|
|
|
|
+ print(f"未知场景: {args.scenario},可选: {list(scenario_map.keys())}")
|
|
|
|
|
+ return False
|
|
|
|
|
+ catalog_text, scenario_name = scenario_map[args.scenario]
|
|
|
|
|
+ if not tester.setup():
|
|
|
|
|
+ return False
|
|
|
|
|
+ sr = await tester.test_scenario(catalog_text, scenario_name, runs=args.runs)
|
|
|
|
|
+ return sr["all_pass"]
|
|
|
|
|
+ else:
|
|
|
|
|
+ return await tester.run_all(runs_per_scenario=args.runs)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ success = asyncio.run(main())
|
|
|
|
|
+ sys.exit(0 if success else 1)
|