Parcourir la source

feat(semantic_logic): 语义逻辑审查独立模型配置 + 前端测试页面

1. config/model_setting.yaml: 新增 semantic_logic_check 独立模型配置
2. semantic_logic.py: function_name 从 grammar_check 改为 semantic_logic_check
3. 模型调用指南.md: 追加 semantic_logic_check 功能说明
4. utils_test/Semantic_Logic_Test/:
   - 重写 test_semantic_logic.py: 匹配真实代码链路
   - 新增 semantic_logic_server.py: 轻量级 HTTP 测试后端
   - 新增 index.html: 前端测试页面(输入内容→调用API→展示结果)
   - 新增 测试案例.md: 14 个可直接复制测试的语义逻辑问题示例
   - 更新 test_data.py / README.md / run_tests.py / run_tests.bat / pytest.ini
WangXuMing il y a 1 mois
Parent
commit
4510001972

+ 8 - 2
config/model_setting.yaml

@@ -96,10 +96,16 @@ model_settings:
 
   # 语法检查
   grammar_check:
-    model: shutian_qwen3_5_35b
+    model: shutian_qwen3_5_122b
     enable_thinking: false
     description: "语法快速检查,蜀天35B"
 
+  # 语义逻辑检查
+  semantic_logic_check:
+    model: shutian_qwen3_5_122b
+    enable_thinking: false
+    description: "语义逻辑审查,蜀天35B"
+
   # 时效性审查
   timeliness_review:
     model: shutian_qwen3_5_35b
@@ -132,4 +138,4 @@ model_settings:
 # 默认配置(当功能未指定时使用)
 default:
   model: shutian_qwen3_5_35b
-  enable_thinking: false
+  enable_thinking: true

+ 1 - 0
config/模型调用指南.md

@@ -222,6 +222,7 @@ default_model = get_model_for_function("default")
 | `rag_answer_generate` | RAG答案生成 | shutian_qwen3_5_122b |
 | `sensitive_check` | 敏感信息检查 | shutian_qwen3_5_35b |
 | `grammar_check` | 语法检查 | shutian_qwen3_5_35b |
+| `semantic_logic_check` | 语义逻辑审查 | shutian_qwen3_5_122b |
 | `timeliness_review` | 时效性审查 | shutian_qwen3_5_35b |
 | `reference_review` | 规范性审查 | shutian_qwen3_5_35b |
 | `directory_extraction` | 目录提取 | shutian_qwen3_5_35b |

+ 1 - 1
core/construction_review/component/reviewers/semantic_logic.py

@@ -64,7 +64,7 @@ class SemanticLogicReviewer:
             model_response = await self.model_client.get_model_generate_invoke(
                 trace_id=trace_id,
                 messages=messages,
-                function_name="grammar_check"
+                function_name="semantic_logic_check"
             )
             
             logger.info(f"语义逻辑检查模型响应成功,响应长度: {len(model_response)}")

+ 50 - 193
utils_test/Semantic_Logic_Test/README.md

@@ -1,221 +1,78 @@
-# 语义逻辑审查模块单元测试
+# 语义逻辑审查模块测试
 
-## 📋 测试概述
+测试 `core/construction_review/component/reviewers/semantic_logic.py` 中的 `SemanticLogicReviewer` 完整链路。
 
-本测试套件用于测试 `core/construction_review/component/reviewers/semantic_logic.py` 模块的功能。
+## 审查链路
 
-## 🎯 测试覆盖范围
-
-### 1. 基础功能测试 (TestSemanticLogicReviewer)
-
--   ✅ 审查器初始化测试
--   ✅ 全局单例实例测试
--   ✅ 模型配置验证
--   ✅ 语义逻辑检查成功场景
--   ✅ 无状态字典的检查场景
--   ✅ API 调用失败处理
--   ✅ 空内容处理
--   ✅ 带参考信息的检查
--   ✅ 消息格式转换
--   ✅ 执行时间跟踪
-
-### 2. 集成测试 (TestIntegration)
-
--   ✅ 完整工作流程测试(需要实际 API)
-
-### 3. 边界情况测试 (TestEdgeCases)
-
--   ✅ 超长内容处理
--   ✅ 特殊字符处理
--   ✅ Unicode 字符处理
-
-## 🚀 运行测试
-
-### 前置条件
-
-确保已安装必要的依赖:
-
-```bash
-pip install pytest pytest-asyncio pytest-mock
 ```
-
-### 运行所有测试
-
-```bash
-# 在项目根目录下运行
-cd /h:/UGit/LQAgentPlatform
-
-# 运行所有测试
-pytest Semantic_Logic_Test/test_semantic_logic.py -v
-
-# 运行测试并显示详细输出
-pytest Semantic_Logic_Test/test_semantic_logic.py -v -s
-
-# 运行测试并生成覆盖率报告
-pytest Semantic_Logic_Test/test_semantic_logic.py --cov=core.construction_review.component.reviewers.semantic_logic --cov-report=html
+check_semantic_logic()
+  ├── prompt_loader.get_prompt_template("basic", "semantic_logic_check", review_content=..., review_references="")
+  ├── ChatPromptTemplate.format_messages() → [SystemMessage, HumanMessage]
+  ├── generate_model_client.get_model_generate_invoke(trace_id, messages, function_name="grammar_check")
+  └── ReviewResult(success, details={name, response}, error_message, execution_time)
 ```
 
-### 运行特定测试类
+## 文件结构
 
-```bash
-# 只运行基础功能测试
-pytest Semantic_Logic_Test/test_semantic_logic.py::TestSemanticLogicReviewer -v
-
-# 只运行边界情况测试
-pytest Semantic_Logic_Test/test_semantic_logic.py::TestEdgeCases -v
 ```
-
-### 运行特定测试方法
-
-```bash
-# 测试初始化
-pytest Semantic_Logic_Test/test_semantic_logic.py::TestSemanticLogicReviewer::test_reviewer_initialization -v
-
-# 测试成功场景
-pytest Semantic_Logic_Test/test_semantic_logic.py::TestSemanticLogicReviewer::test_check_semantic_logic_success -v
-
-# 测试错误处理
-pytest Semantic_Logic_Test/test_semantic_logic.py::TestSemanticLogicReviewer::test_check_semantic_logic_api_error -v
+Semantic_Logic_Test/
+├── conftest.py                  pytest 配置
+├── pytest.ini                   pytest 参数
+├── test_data.py                 测试用例数据
+├── test_semantic_logic.py       pytest 单元测试 + 链路测试
+├── semantic_logic_server.py     前端测试服务器 (HTTP API)
+├── index.html                   前端测试页面
+├── run_tests.py                 交互式测试启动脚本
+├── run_tests.bat                Windows 批处理启动脚本
+├── requirements_test.txt        测试依赖
+└── README.md                    本文档
 ```
 
-### 跳过集成测试
+## 运行方式
 
-集成测试需要实际的 API 服务可用,如果不想运行集成测试:
+### 1. pytest 单元测试
 
 ```bash
-pytest Semantic_Logic_Test/test_semantic_logic.py -v -m "not integration"
-```
-
-## 📊 测试结果示例
-
+# 设置 PYTHONPATH 后运行
+$env:PYTHONPATH = "D:\wx_work\sichuan_luqiao\LQAgentPlatform"
+pytest utils_test/Semantic_Logic_Test/test_semantic_logic.py -v
 ```
-============================= test session starts =============================
-platform win32 -- Python 3.x.x, pytest-7.x.x, pluggy-1.x.x
-collected 15 items
-
-test_semantic_logic.py::TestSemanticLogicReviewer::test_reviewer_initialization PASSED     [  6%]
-test_semantic_logic.py::TestSemanticLogicReviewer::test_global_singleton_instance PASSED   [ 13%]
-test_semantic_logic.py::TestSemanticLogicReviewer::test_model_config PASSED                [ 20%]
-test_semantic_logic.py::TestSemanticLogicReviewer::test_check_semantic_logic_success PASSED [ 26%]
-test_semantic_logic.py::TestSemanticLogicReviewer::test_check_semantic_logic_without_state PASSED [ 33%]
-test_semantic_logic.py::TestSemanticLogicReviewer::test_check_semantic_logic_api_error PASSED [ 40%]
-test_semantic_logic.py::TestSemanticLogicReviewer::test_check_semantic_logic_empty_content PASSED [ 46%]
-test_semantic_logic.py::TestSemanticLogicReviewer::test_check_semantic_logic_with_references PASSED [ 53%]
-test_semantic_logic.py::TestSemanticLogicReviewer::test_message_format_conversion PASSED   [ 60%]
-test_semantic_logic.py::TestSemanticLogicReviewer::test_execution_time_tracking PASSED     [ 66%]
-test_semantic_logic.py::TestIntegration::test_full_workflow SKIPPED                        [ 73%]
-test_semantic_logic.py::TestEdgeCases::test_very_long_content PASSED                       [ 80%]
-test_semantic_logic.py::TestEdgeCases::test_special_characters PASSED                      [ 86%]
-test_semantic_logic.py::TestEdgeCases::test_unicode_content PASSED                         [ 93%]
-
-======================== 14 passed, 1 skipped in 2.34s ========================
-```
-
-## 🔍 测试详解
-
-### 1. 初始化测试
-
-验证 `SemanticLogicReviewer` 类是否正确初始化,包括:
-
--   OpenAI 客户端创建
--   模型配置加载
--   参数设置
-
-### 2. 成功场景测试
-
-模拟正常的语义逻辑检查流程:
-
--   提示词模板加载
--   OpenAI API 调用
--   结果处理
--   进度通知
-
-### 3. 错误处理测试
-
-验证各种错误场景的处理:
-
--   API 连接失败
--   超时处理
--   异常捕获
--   错误通知
-
-### 4. 边界情况测试
-
-测试极端情况下的系统行为:
-
--   超长文本(10000+ 字符)
--   特殊字符和符号
--   多语言 Unicode 字符
 
-## 🛠️ Mock 说明
-
-测试使用了以下 Mock 对象:
-
-1. **AsyncOpenAI Client**: 模拟 OpenAI API 客户端
-2. **prompt_loader**: 模拟提示词加载器
-3. **progress_manager**: 模拟进度管理器
-4. **ChatPromptTemplate**: 模拟提示词模板
-
-这样可以在不依赖实际 API 服务的情况下进行测试。
-
-## 📝 注意事项
-
-1. **集成测试**: 标记为 `@pytest.mark.integration` 的测试需要实际的 API 服务,默认会被跳过
-2. **异步测试**: 所有涉及 async/await 的测试都使用 `@pytest.mark.asyncio` 装饰器
-3. **Mock 数据**: 测试使用 Mock 数据,不会产生实际的 API 调用费用
-4. **执行时间**: 某些测试包含 `asyncio.sleep()` 来模拟延迟,可能需要几秒钟
-
-## 🐛 调试技巧
-
-### 查看详细日志
+或通过交互式脚本:
 
 ```bash
-pytest Semantic_Logic_Test/test_semantic_logic.py -v -s --log-cli-level=DEBUG
+python utils_test/Semantic_Logic_Test/run_tests.py
 ```
 
-### 只运行失败的测试
+### 2. 前端测试页面 (Web UI)
 
 ```bash
-pytest Semantic_Logic_Test/test_semantic_logic.py --lf
-```
-
-### 进入调试模式
+# 启动服务器
+python utils_test/Semantic_Logic_Test/semantic_logic_server.py --port 8766
 
-```bash
-pytest Semantic_Logic_Test/test_semantic_logic.py --pdb
+# 浏览器访问
+http://localhost:8766
 ```
 
-### 生成 HTML 报告
+前端页面功能:
+- 输入施工方案内容
+- 加载示例数据(逻辑错误 / 正常内容)
+- 一键执行语义逻辑审查
+- 查看 AI 模型返回的审查结果
+- 查看完整 JSON 响应
+- 显示模型耗时、端到端耗时等元数据
 
-```bash
-pytest Semantic_Logic_Test/test_semantic_logic.py --html=report.html --self-contained-html
-```
+### 3. Windows 快捷方式
 
-## 📈 持续集成
-
-可以将测试集成到 CI/CD 流程中:
-
-```yaml
-# .github/workflows/test.yml 示例
-name: Run Tests
-on: [push, pull_request]
-jobs:
-    test:
-        runs-on: ubuntu-latest
-        steps:
-            - uses: actions/checkout@v2
-            - name: Set up Python
-              uses: actions/setup-python@v2
-              with:
-                  python-version: "3.9"
-            - name: Install dependencies
-              run: |
-                  pip install -r requirements.txt
-                  pip install pytest pytest-asyncio pytest-mock pytest-cov
-            - name: Run tests
-              run: pytest Semantic_Logic_Test/test_semantic_logic.py -v --cov
-```
+双击运行 `run_tests.bat`,按提示选择操作。
 
-## 📞 联系方式
+## 测试覆盖
 
-如有问题或建议,请联系开发团队。
+| 测试类 | 说明 |
+|---|---|
+| `TestInit` | 初始化:model_client 类型、全局单例 |
+| `TestCheckSemanticLogicSuccess` | 成功链路:prompt_loader → format_messages → model_client → ReviewResult |
+| `TestCheckSemanticLogicError` | 错误链路:模型调用失败、prompt_loader 失败、无 state 错误处理 |
+| `TestChainIntegration` | 集成链路:使用真实 prompt_loader 加载模板,仅 mock AI 调用 |
+| `TestEdgeCases` | 边界:空内容、超长内容、特殊字符、Unicode、执行时间跟踪 |
+| `TestLiveAPI` | 真实 API 调用(标记 integration,默认跳过) |

+ 519 - 0
utils_test/Semantic_Logic_Test/index.html

@@ -0,0 +1,519 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>语义逻辑审查 — 前端测试</title>
+    <style>
+        * { box-sizing: border-box; margin: 0; padding: 0; }
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+            background: #f5f7fa;
+            color: #333;
+            line-height: 1.6;
+        }
+        .container {
+            max-width: 1200px;
+            margin: 0 auto;
+            padding: 20px;
+        }
+        header {
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: #fff;
+            padding: 30px 20px;
+            border-radius: 12px;
+            margin-bottom: 24px;
+            box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
+        }
+        header h1 {
+            font-size: 26px;
+            font-weight: 600;
+        }
+        header p {
+            opacity: 0.9;
+            margin-top: 6px;
+            font-size: 14px;
+        }
+
+        /* 输入区域 */
+        .input-section {
+            background: #fff;
+            border-radius: 12px;
+            padding: 20px;
+            margin-bottom: 20px;
+            box-shadow: 0 2px 12px rgba(0,0,0,0.06);
+        }
+        .input-section h2 {
+            font-size: 16px;
+            margin-bottom: 12px;
+            color: #444;
+            display: flex;
+            align-items: center;
+            gap: 6px;
+        }
+        .input-section h2 .icon { font-size: 18px; }
+
+        textarea {
+            width: 100%;
+            min-height: 280px;
+            padding: 14px;
+            border: 1px solid #e0e3e9;
+            border-radius: 8px;
+            font-size: 14px;
+            line-height: 1.7;
+            resize: vertical;
+            transition: border-color 0.2s;
+            font-family: inherit;
+        }
+        textarea:focus {
+            outline: none;
+            border-color: #667eea;
+            box-shadow: 0 0 0 3px rgba(102,126,234,0.1);
+        }
+
+        /* 工具栏 */
+        .toolbar {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 10px;
+            margin-top: 14px;
+            align-items: center;
+        }
+        .btn {
+            padding: 9px 18px;
+            border: none;
+            border-radius: 8px;
+            font-size: 14px;
+            cursor: pointer;
+            transition: all 0.2s;
+            display: inline-flex;
+            align-items: center;
+            gap: 6px;
+            font-weight: 500;
+        }
+        .btn-primary {
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: #fff;
+        }
+        .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102,126,234,0.4); }
+        .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; }
+
+        .btn-secondary {
+            background: #f0f2f5;
+            color: #555;
+            border: 1px solid #e0e3e9;
+        }
+        .btn-secondary:hover { background: #e4e7ec; }
+
+        .btn-sample {
+            background: #e8f0fe;
+            color: #1a73e8;
+            border: 1px solid #c6dafc;
+        }
+        .btn-sample:hover { background: #d2e3fc; }
+
+        .server-status {
+            margin-left: auto;
+            display: flex;
+            align-items: center;
+            gap: 6px;
+            font-size: 13px;
+            color: #888;
+        }
+        .status-dot {
+            width: 8px; height: 8px;
+            border-radius: 50%;
+            background: #ccc;
+        }
+        .status-dot.online { background: #34a853; }
+        .status-dot.offline { background: #ea4335; }
+
+        /* 加载动画 */
+        .loading-overlay {
+            display: none;
+            position: fixed;
+            top: 0; left: 0; right: 0; bottom: 0;
+            background: rgba(255,255,255,0.85);
+            z-index: 999;
+            justify-content: center;
+            align-items: center;
+            flex-direction: column;
+            gap: 16px;
+        }
+        .loading-overlay.active { display: flex; }
+        .spinner {
+            width: 48px; height: 48px;
+            border: 4px solid #e0e3e9;
+            border-top-color: #667eea;
+            border-radius: 50%;
+            animation: spin 1s linear infinite;
+        }
+        @keyframes spin { to { transform: rotate(360deg); } }
+        .loading-text { color: #667eea; font-size: 15px; font-weight: 500; }
+
+        /* 结果区域 */
+        .result-section {
+            background: #fff;
+            border-radius: 12px;
+            padding: 20px;
+            box-shadow: 0 2px 12px rgba(0,0,0,0.06);
+        }
+        .result-section h2 {
+            font-size: 16px;
+            margin-bottom: 14px;
+            color: #444;
+            display: flex;
+            align-items: center;
+            gap: 6px;
+        }
+
+        .result-meta {
+            display: grid;
+            grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+            gap: 12px;
+            margin-bottom: 16px;
+        }
+        .meta-card {
+            background: #f8f9fb;
+            border-radius: 8px;
+            padding: 12px 14px;
+            border: 1px solid #eaecf0;
+        }
+        .meta-label { font-size: 12px; color: #888; margin-bottom: 4px; }
+        .meta-value { font-size: 15px; font-weight: 600; color: #333; }
+        .meta-value.success { color: #34a853; }
+        .meta-value.error { color: #ea4335; }
+
+        /* 审查结果内容 */
+        .review-content-box {
+            background: #f8f9fb;
+            border: 1px solid #eaecf0;
+            border-radius: 8px;
+            padding: 16px;
+            margin-bottom: 14px;
+            white-space: pre-wrap;
+            word-break: break-word;
+            font-size: 14px;
+            line-height: 1.8;
+            max-height: 400px;
+            overflow-y: auto;
+        }
+        .review-content-box::-webkit-scrollbar { width: 6px; }
+        .review-content-box::-webkit-scrollbar-thumb { background: #d0d4dc; border-radius: 3px; }
+
+        .result-label {
+            font-size: 13px;
+            font-weight: 600;
+            color: #555;
+            margin-bottom: 8px;
+            text-transform: uppercase;
+            letter-spacing: 0.5px;
+        }
+
+        /* JSON 查看器 */
+        .json-viewer {
+            background: #1e1e2e;
+            color: #cdd6f4;
+            border-radius: 8px;
+            padding: 16px;
+            overflow-x: auto;
+            font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
+            font-size: 13px;
+            line-height: 1.6;
+            max-height: 400px;
+            overflow-y: auto;
+        }
+        .json-viewer::-webkit-scrollbar { width: 6px; height: 6px; }
+        .json-viewer::-webkit-scrollbar-thumb { background: #45475a; border-radius: 3px; }
+        .json-key { color: #f38ba8; }
+        .json-string { color: #a6e3a1; }
+        .json-number { color: #fab387; }
+        .json-boolean { color: #89b4fa; }
+        .json-null { color: #89b4fa; }
+
+        .placeholder {
+            text-align: center;
+            color: #999;
+            padding: 60px 20px;
+            font-size: 14px;
+        }
+
+        /* 标签页 */
+        .tabs {
+            display: flex;
+            gap: 4px;
+            margin-bottom: 12px;
+            border-bottom: 2px solid #f0f2f5;
+        }
+        .tab {
+            padding: 8px 16px;
+            font-size: 14px;
+            cursor: pointer;
+            border-bottom: 2px solid transparent;
+            margin-bottom: -2px;
+            color: #888;
+            transition: color 0.2s;
+        }
+        .tab:hover { color: #555; }
+        .tab.active {
+            color: #667eea;
+            border-bottom-color: #667eea;
+            font-weight: 600;
+        }
+        .tab-pane { display: none; }
+        .tab-pane.active { display: block; }
+
+        .no-result { color: #999; font-style: italic; }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <header>
+            <h1>语义逻辑审查 — 前端测试</h1>
+            <p>直接调用 SemanticLogicReviewer,无需文件上传流程 | 链路: prompt_loader → format_messages → model_client → ReviewResult</p>
+        </header>
+
+        <!-- 输入区域 -->
+        <section class="input-section">
+            <h2><span class="icon">📝</span> 待审查内容</h2>
+            <textarea id="reviewInput" placeholder="在此输入施工方案内容,点击「执行语义逻辑审查」...&#10;&#10;示例内容:&#10;本工程计划工期为6个月,施工内容包括路基土石方200万方、桥梁5座、隧道3座。&#10;采用先上后下的施工顺序,首先进行桥梁上部结构施工,然后进行桥墩基础施工。&#10;因为天气晴朗,所以混凝土强度不足。&#10;当温度低于5℃时,可正常进行混凝土浇筑施工。"></textarea>
+            <div class="toolbar">
+                <button class="btn btn-primary" id="runBtn" onclick="runSemanticLogicCheck()">
+                    ▶ 执行语义逻辑审查
+                </button>
+                <button class="btn btn-secondary" onclick="clearInput()">清空</button>
+                <button class="btn btn-sample" onclick="loadSample('logic_error')">加载逻辑错误示例</button>
+                <button class="btn btn-sample" onclick="loadSample('normal')">加载正常示例</button>
+                <div class="server-status">
+                    <span class="status-dot offline" id="statusDot"></span>
+                    <span id="statusText">检测中...</span>
+                </div>
+            </div>
+        </section>
+
+        <!-- 结果区域 -->
+        <section class="result-section" id="resultSection" style="display:none;">
+            <h2><span class="icon">📋</span> 审查结果</h2>
+
+            <div class="result-meta" id="resultMeta"></div>
+
+            <div class="tabs">
+                <div class="tab active" onclick="switchTab('response')">模型响应</div>
+                <div class="tab" onclick="switchTab('json')">完整JSON</div>
+            </div>
+
+            <div class="tab-pane active" id="tab-response">
+                <div class="result-label">AI 审查结果</div>
+                <div class="review-content-box" id="responseBox"></div>
+            </div>
+            <div class="tab-pane" id="tab-json">
+                <div class="result-label">原始响应 JSON</div>
+                <pre class="json-viewer" id="jsonBox"></pre>
+            </div>
+        </section>
+
+        <section class="result-section" id="placeholderSection">
+            <div class="placeholder">
+                <div style="font-size:48px; margin-bottom:12px;">🧪</div>
+                <p>输入施工方案内容并点击「执行语义逻辑审查」开始测试</p>
+            </div>
+        </section>
+    </div>
+
+    <!-- 加载遮罩 -->
+    <div class="loading-overlay" id="loadingOverlay">
+        <div class="spinner"></div>
+        <div class="loading-text">语义逻辑审查中,请稍候...</div>
+    </div>
+
+    <script>
+        // ─── 示例数据 ─────────────────────────────────────────────────────────
+        const SAMPLES = {
+            logic_error: `第二章 施工方案
+
+2.1 施工顺序
+本工程采用先上后下的施工顺序,首先进行桥梁上部结构施工,然后进行桥墩基础施工。
+(逻辑错误:应先施工基础再施工上部结构)
+
+2.2 工期与工序矛盾
+本工程计划工期为6个月,施工内容包括路基土石方200万方、桥梁5座、隧道3座。
+(工期与工程量严重不匹配)
+
+2.3 因果错误
+因为天气晴朗,所以混凝土强度不足。
+(因果无关)
+
+2.4 条件结论不匹配
+当温度低于5℃时,可正常进行混凝土浇筑施工。
+(违背常识——低温不宜浇筑混凝土)
+
+2.5 口语化表达
+这事儿吧,咱就这么干,问题不大。
+(施工方案应为严肃规范的书面表达)`,
+            normal: `第一章 工程概况
+
+1.1 项目基本信息
+本工程为四川省会理至禄劝高速公路项目,路线全长约120公里,设计速度80km/h,
+路基宽度24.5米,双向四车道。主要工程内容包括路基工程、桥梁工程、隧道工程等。
+
+1.2 施工范围
+本标段起讫桩号为K0+000~K30+000,主要包括:
+- 路基土石方工程约200万立方米
+- 桥梁工程5座,总长约2000米
+- 涵洞工程20道
+- 路面工程约30公里
+
+1.3 工期安排
+计划工期:24个月
+开工日期:2024年3月1日
+竣工日期:2026年2月28日`,
+        };
+
+        // ─── 工具函数 ─────────────────────────────────────────────────────────
+        function loadSample(type) {
+            document.getElementById('reviewInput').value = SAMPLES[type] || '';
+        }
+        function clearInput() {
+            document.getElementById('reviewInput').value = '';
+        }
+
+        function switchTab(name) {
+            document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+            document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
+            event.target.classList.add('active');
+            document.getElementById('tab-' + name).classList.add('active');
+        }
+
+        function setServerStatus(online) {
+            const dot = document.getElementById('statusDot');
+            const text = document.getElementById('statusText');
+            if (online) {
+                dot.classList.remove('offline');
+                dot.classList.add('online');
+                text.textContent = '服务在线';
+            } else {
+                dot.classList.remove('online');
+                dot.classList.add('offline');
+                text.textContent = '服务离线';
+            }
+        }
+
+        // 检测服务器状态
+        async function checkHealth() {
+            try {
+                const res = await fetch('/api/health', { method: 'GET' });
+                const data = await res.json();
+                setServerStatus(data.status === 'ok');
+            } catch (e) {
+                setServerStatus(false);
+            }
+        }
+        checkHealth();
+        setInterval(checkHealth, 10000);
+
+        // 高亮 JSON
+        function highlightJson(json) {
+            if (typeof json !== 'string') json = JSON.stringify(json, null, 2);
+            return json
+                .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
+                .replace(/("(?:\\.|[^"\\])*")/g, '<span class="json-string">$1</span>')
+                .replace(/\b(true|false)\b/g, '<span class="json-boolean">$1</span>')
+                .replace(/\b(null)\b/g, '<span class="json-null">$1</span>')
+                .replace(/\b(\d+(?:\.\d+)?)\b/g, '<span class="json-number">$1</span>')
+                .replace(/("[^"]*")\s*:/g, '<span class="json-key">$1</span>:');
+        }
+
+        // ─── 执行审查 ──────────────────────────────────────────────────────────
+        async function runSemanticLogicCheck() {
+            const content = document.getElementById('reviewInput').value.trim();
+            if (!content) {
+                alert('请先输入待审查内容');
+                return;
+            }
+
+            const btn = document.getElementById('runBtn');
+            const overlay = document.getElementById('loadingOverlay');
+            btn.disabled = true;
+            overlay.classList.add('active');
+
+            const startTime = performance.now();
+
+            try {
+                const res = await fetch('/api/semantic_logic', {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify({ content }),
+                });
+
+                const data = await res.json();
+                const clientTime = ((performance.now() - startTime) / 1000).toFixed(3);
+
+                if (data.error) {
+                    showError(data.error);
+                    return;
+                }
+
+                showResult(data, clientTime);
+
+            } catch (err) {
+                showError('请求失败: ' + err.message);
+            } finally {
+                btn.disabled = false;
+                overlay.classList.remove('active');
+            }
+        }
+
+        function showResult(data, clientTime) {
+            document.getElementById('placeholderSection').style.display = 'none';
+            document.getElementById('resultSection').style.display = 'block';
+
+            // 元数据卡片
+            const successClass = data.success ? 'success' : 'error';
+            const successText = data.success ? '成功' : '失败';
+            document.getElementById('resultMeta').innerHTML = `
+                <div class="meta-card">
+                    <div class="meta-label">执行状态</div>
+                    <div class="meta-value ${successClass}">${successText}</div>
+                </div>
+                <div class="meta-card">
+                    <div class="meta-label">模型耗时</div>
+                    <div class="meta-value">${(data.model_execution_time || 0).toFixed(3)}s</div>
+                </div>
+                <div class="meta-card">
+                    <div class="meta-label">端到端耗时</div>
+                    <div class="meta-value">${clientTime}s</div>
+                </div>
+                <div class="meta-card">
+                    <div class="meta-label">内容长度</div>
+                    <div class="meta-value">${data.content_length} 字符</div>
+                </div>
+                <div class="meta-card">
+                    <div class="meta-label">Trace ID</div>
+                    <div class="meta-value" style="font-size:12px; word-break:break-all;">${data.trace_id}</div>
+                </div>
+            `;
+
+            // 模型响应内容
+            const responseText = data.details?.response || data.error_message || '(空)';
+            document.getElementById('responseBox').textContent = responseText;
+
+            // JSON 查看器
+            document.getElementById('jsonBox').innerHTML = highlightJson(data);
+        }
+
+        function showError(msg) {
+            document.getElementById('placeholderSection').style.display = 'none';
+            document.getElementById('resultSection').style.display = 'block';
+
+            document.getElementById('resultMeta').innerHTML = `
+                <div class="meta-card" style="border-color:#ea4335;">
+                    <div class="meta-label">执行状态</div>
+                    <div class="meta-value error">请求失败</div>
+                </div>
+            `;
+            document.getElementById('responseBox').innerHTML = `<span style="color:#ea4335;">❌ ${msg}</span>`;
+            document.getElementById('jsonBox').textContent = msg;
+        }
+    </script>
+</body>
+</html>

+ 2 - 21
utils_test/Semantic_Logic_Test/pytest.ini

@@ -11,10 +11,10 @@ markers =
     asyncio: 标记异步测试
     integration: 标记集成测试(需要实际API服务)
     slow: 标记慢速测试
-    unit: 标记��元测试
+    unit: 标记元测试
 
 # 输出选项
-addopts = 
+addopts =
     -v
     --strict-markers
     --tb=short
@@ -22,22 +22,3 @@ addopts =
 
 # 异步测试配置
 asyncio_mode = auto
-
-# 日志配置
-log_cli = false
-log_cli_level = INFO
-log_cli_format = %(asctime)s [%(levelname)8s] %(message)s
-log_cli_date_format = %Y-%m-%d %H:%M:%S
-
-# 覆盖率配置
-[coverage:run]
-source = core.construction_review.component.reviewers
-omit = 
-    */tests/*
-    */test_*.py
-
-[coverage:report]
-precision = 2
-show_missing = True
-skip_covered = False
-

+ 26 - 54
utils_test/Semantic_Logic_Test/run_tests.bat

@@ -1,76 +1,48 @@
 @echo off
-REM Windows 批处理脚本 - 快速运行测试
+REM 语义逻辑审查模块 — 测试启动脚本
 
 echo ========================================
 echo 语义逻辑审查模块测试套件
 echo ========================================
 echo.
-
-cd /d "%~dp0\.."
-
-echo 当前目录: %CD%
+echo 可用命令:
+echo   1. 运行 pytest 单元测试
+echo   2. 启动前端测试服务器 (Web UI)
+echo   3. 运行单元测试 + 启动服务器
+echo   0. 退出
 echo.
 
-REM 检查 Python 是否安装
-python --version >nul 2>&1
-if errorlevel 1 (
-    echo [错误] 未找到 Python,请先安装 Python
-    pause
-    exit /b 1
-)
-
-echo [信息] 检查测试依赖...
-python -c "import pytest" >nul 2>&1
-if errorlevel 1 (
-    echo [警告] 缺少测试依赖,正在安装...
-    pip install -r Semantic_Logic_Test\requirements_test.txt
-)
-
-echo.
-echo ========================================
-echo 请选择要运行的测试:
-echo ========================================
-echo   1. 运行所有测试
-echo   2. 运行所有测试(详细输出)
-echo   3. 运行基础功能测试
-echo   4. 运行边界情况测试
-echo   5. 运行测试并生成覆盖率报告
-echo   6. 运行测试并生成 HTML 报告
-echo   7. 只运行失败的测试
-echo   0. 退出
+cd /d "%~dp0\..\.."
+echo 当前目录: %CD%
 echo.
 
-set /p choice="请输入选项 (0-7): "
+set /p choice="请输入选项 (0-3): "
 
 if "%choice%"=="1" (
-    pytest Semantic_Logic_Test\test_semantic_logic.py -v
+    echo.
+    echo [信息] 运行 pytest 单元测试...
+    set PYTHONPATH=%CD%
+    python -m pytest utils_test\Semantic_Logic_Test\test_semantic_logic.py -v --tb=short
+    echo.
+    pause
 ) else if "%choice%"=="2" (
-    pytest Semantic_Logic_Test\test_semantic_logic.py -v -s
+    echo.
+    echo [信息] 启动前端测试服务器...
+    echo 启动后请访问: http://localhost:8766
+    echo.
+    python utils_test\Semantic_Logic_Test\semantic_logic_server.py --port 8766
 ) else if "%choice%"=="3" (
-    pytest Semantic_Logic_Test\test_semantic_logic.py::TestSemanticLogicReviewer -v
-) else if "%choice%"=="4" (
-    pytest Semantic_Logic_Test\test_semantic_logic.py::TestEdgeCases -v
-) else if "%choice%"=="5" (
-    pytest Semantic_Logic_Test\test_semantic_logic.py --cov=core.construction_review.component.reviewers.semantic_logic --cov-report=html --cov-report=term
     echo.
-    echo [信息] 覆盖率报告已生成到 htmlcov\index.html
-) else if "%choice%"=="6" (
-    pytest Semantic_Logic_Test\test_semantic_logic.py --html=Semantic_Logic_Test\report.html --self-contained-html
+    echo [信息] 先运行单元测试,再启动服务器...
+    set PYTHONPATH=%CD%
+    python -m pytest utils_test\Semantic_Logic_Test\test_semantic_logic.py -v --tb=short
     echo.
-    echo [信息] HTML 报告已生成到 Semantic_Logic_Test\report.html
-) else if "%choice%"=="7" (
-    pytest Semantic_Logic_Test\test_semantic_logic.py --lf -v
+    echo [信息] 测试完成,启动前端服务器...
+    python utils_test\Semantic_Logic_Test\semantic_logic_server.py --port 8766
 ) else if "%choice%"=="0" (
-    echo.
-    echo 再见!
+    echo 再见!
     exit /b 0
 ) else (
-    echo.
     echo [错误] 无效的选项
     pause
-    exit /b 1
 )
-
-echo.
-pause
-

+ 57 - 88
utils_test/Semantic_Logic_Test/run_tests.py

@@ -1,130 +1,99 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 """
-快速运行测试脚本
-提供便捷的测试运行命令
+语义逻辑审查模块 — 测试运行脚本
+提供便捷的测试运行与服务器启动命令
 """
 
 import sys
 import os
 import subprocess
-from pathlib import Path
 
 
 def run_command(cmd, description):
     """运行命令并显示结果"""
-    print(f"\n{'='*60}")
-    print(f"🚀 {description}")
-    print(f"{'='*60}\n")
-    
+    print(f"\n{'=' * 60}")
+    print(f" {description}")
+    print(f"{'=' * 60}\n")
     result = subprocess.run(cmd, shell=True)
-    
+    print(f"\n{'=' * 60}")
     if result.returncode == 0:
-        print(f"\n✅ {description} - 成功")
+        print(f" 成功: {description}")
     else:
-        print(f"\n❌ {description} - 失败")
-    
+        print(f" 失败: {description} (exit code {result.returncode})")
+    print(f"{'=' * 60}\n")
     return result.returncode
 
 
 def main():
-    """主函数"""
-    # 切换到项目根目录
-    project_root = Path(__file__).parent.parent
+    project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
     os.chdir(project_root)
-    
-    print(f"📁 工作目录: {os.getcwd()}")
-    
-    # 检查是否安装了测试依赖
-    print("\n📦 检查测试依赖...")
-    try:
-        import pytest
-        import pytest_asyncio
-        print("✅ 测试依赖已安装")
-    except ImportError:
-        print("❌ 缺少测试依赖,正在安装...")
-        subprocess.run([
-            sys.executable, "-m", "pip", "install", 
-            "-r", "Semantic_Logic_Test/requirements_test.txt"
-        ])
-    
-    # 显示菜单
-    print("\n" + "="*60)
-    print("🧪 语义逻辑审查模块测试套件")
-    print("="*60)
-    print("\n请选择要运行的测试:")
-    print("  1. 运行所有测试")
-    print("  2. 运行所有测试(详细输出)")
-    print("  3. 运行基础功能测试")
-    print("  4. 运行边界情况测试")
-    print("  5. 运行测试并生成覆盖率报告")
-    print("  6. 运行测试并生成 HTML 报告")
-    print("  7. 只运行失败的测试")
-    print("  8. 运行特定测试(手动输入)")
+    print(f"项目根目录: {os.getcwd()}")
+
+    print("\n" + "=" * 60)
+    print(" 语义逻辑审查模块测试套件")
+    print("=" * 60)
+    print("\n请选择要执行的操作:")
+    print("  1. 运行 pytest 单元测试")
+    print("  2. 启动前端测试服务器 (Web UI)")
+    print("  3. 运行单元测试 + 启动服务器")
+    print("  4. 运行单元测试并生成覆盖率报告")
+    print("  5. 只运行失败的测试")
     print("  0. 退出")
-    
-    choice = input("\n请输入选项 (0-8): ").strip()
-    
-    test_file = "Semantic_Logic_Test/test_semantic_logic.py"
-    
+
+    choice = input("\n请输入选项 (0-5): ").strip()
+    test_file = "utils_test/Semantic_Logic_Test/test_semantic_logic.py"
+    server_file = "utils_test/Semantic_Logic_Test/semantic_logic_server.py"
+
     if choice == "1":
+        env = os.environ.copy()
+        env["PYTHONPATH"] = project_root
         return run_command(
-            f"pytest {test_file} -v",
-            "运行所有测试"
+            f'python -m pytest {test_file} -v --tb=short',
+            "运行单元测试"
         )
-    
+
     elif choice == "2":
-        return run_command(
-            f"pytest {test_file} -v -s",
-            "运行所有测试(详细输出)"
-        )
-    
+        print("\n启动前端测试服务器...")
+        print("启动后请访问: http://localhost:8766")
+        return subprocess.run([sys.executable, server_file, "--port", "8766"]).returncode
+
     elif choice == "3":
-        return run_command(
-            f"pytest {test_file}::TestSemanticLogicReviewer -v",
-            "运行基础功能测试"
+        env = os.environ.copy()
+        env["PYTHONPATH"] = project_root
+        ret = run_command(
+            f'python -m pytest {test_file} -v --tb=short',
+            "运行单元测试"
         )
-    
+        if ret == 0:
+            print("\n测试通过,启动前端服务器...")
+            return subprocess.run([sys.executable, server_file, "--port", "8766"]).returncode
+        return ret
+
     elif choice == "4":
+        env = os.environ.copy()
+        env["PYTHONPATH"] = project_root
         return run_command(
-            f"pytest {test_file}::TestEdgeCases -v",
-            "运行边界情况测试"
-        )
-    
-    elif choice == "5":
-        return run_command(
-            f"pytest {test_file} --cov=core.construction_review.component.reviewers.semantic_logic --cov-report=html --cov-report=term",
+            f'python -m pytest {test_file} --cov=core.construction_review.component.reviewers.semantic_logic --cov-report=html --cov-report=term -v',
             "运行测试并生成覆盖率报告"
         )
-    
-    elif choice == "6":
-        return run_command(
-            f"pytest {test_file} --html=Semantic_Logic_Test/report.html --self-contained-html",
-            "运行测试并生成 HTML 报告"
-        )
-    
-    elif choice == "7":
+
+    elif choice == "5":
+        env = os.environ.copy()
+        env["PYTHONPATH"] = project_root
         return run_command(
-            f"pytest {test_file} --lf -v",
+            f'python -m pytest {test_file} --lf -v',
             "只运行失败的测试"
         )
-    
-    elif choice == "8":
-        test_name = input("请输入测试方法名称(例如: test_reviewer_initialization): ").strip()
-        return run_command(
-            f"pytest {test_file}::TestSemanticLogicReviewer::{test_name} -v -s",
-            f"运行测试: {test_name}"
-        )
-    
+
     elif choice == "0":
-        print("\n👋 再见!")
+        print("\n再见!")
         return 0
-    
+
     else:
-        print("\n无效选项")
+        print("\n无效选项")
         return 1
 
 
 if __name__ == "__main__":
     sys.exit(main())
-

+ 166 - 0
utils_test/Semantic_Logic_Test/semantic_logic_server.py

@@ -0,0 +1,166 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+语义逻辑审查 — 前端测试服务器
+提供独立HTTP API,直接调用 SemanticLogicReviewer,无需文件上传流程
+"""
+
+import sys
+import os
+import json
+import time
+import asyncio
+import threading
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+from urllib.parse import parse_qs, urlparse
+
+# 添加项目根目录到路径
+PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+sys.path.insert(0, PROJECT_ROOT)
+os.chdir(PROJECT_ROOT)
+
+from foundation.observability.logger.loggering import review_logger as logger
+from core.construction_review.component.reviewers.semantic_logic import SemanticLogicReviewer
+
+
+# ─── 异步工具 ──────────────────────────────────────────────────────────────────
+
+def run_async(coro):
+    """在同步上下文中运行异步协程"""
+    try:
+        loop = asyncio.get_running_loop()
+        import concurrent.futures
+        with concurrent.futures.ThreadPoolExecutor() as executor:
+            future = executor.submit(asyncio.run, coro)
+            return future.result()
+    except RuntimeError:
+        return asyncio.run(coro)
+
+
+# ─── 审查逻辑 ──────────────────────────────────────────────────────────────────
+
+async def do_semantic_logic_check(review_content: str) -> dict:
+    """
+    执行语义逻辑审查
+    直接调用 SemanticLogicReviewer.check_semantic_logic
+    """
+    trace_id = f"semantic_logic_web_{int(time.time() * 1000)}"
+    reviewer = SemanticLogicReviewer()
+
+    logger.info(f"[语义逻辑Web测试] trace_id={trace_id}, content_length={len(review_content)}")
+
+    start = time.time()
+    result = await reviewer.check_semantic_logic(
+        trace_id=trace_id,
+        review_content=review_content,
+        state=None,
+        stage_name=None,
+    )
+    wall_time = time.time() - start
+
+    return {
+        "trace_id": trace_id,
+        "success": result.success,
+        "details": result.details,
+        "error_message": result.error_message,
+        "model_execution_time": result.execution_time,
+        "wall_time": round(wall_time, 3),
+        "content_length": len(review_content),
+    }
+
+
+# ─── HTTP Handler ──────────────────────────────────────────────────────────────
+
+class SemanticLogicHandler(SimpleHTTPRequestHandler):
+    """HTTP请求处理器"""
+
+    def do_GET(self):
+        parsed = urlparse(self.path)
+
+        if parsed.path == '/api/health':
+            self.send_json_response({"status": "ok"})
+        elif parsed.path in ('', '/', '/index.html'):
+            # 返回前端页面
+            index_path = os.path.join(os.path.dirname(__file__), 'index.html')
+            self.serve_file(index_path, 'text/html')
+        else:
+            # 静态文件服务
+            super().do_GET()
+
+    def do_POST(self):
+        parsed = urlparse(self.path)
+
+        if parsed.path == '/api/semantic_logic':
+            content_length = int(self.headers.get('Content-Length', 0))
+            post_data = self.rfile.read(content_length)
+
+            try:
+                body = json.loads(post_data.decode('utf-8'))
+                review_content = body.get('content', '')
+
+                if not review_content:
+                    self.send_json_response({"error": "请提供 content 参数"}, 400)
+                    return
+
+                print(f"\n[语义逻辑Web测试] 收到请求, content_length={len(review_content)}")
+                result = run_async(do_semantic_logic_check(review_content))
+                print(f"[语义逻辑Web测试] 完成, success={result['success']}, wall_time={result['wall_time']}s")
+
+                self.send_json_response(result)
+
+            except json.JSONDecodeError:
+                self.send_json_response({"error": "JSON解析失败"}, 400)
+            except Exception as e:
+                logger.error(f"[语义逻辑Web测试] 处理失败: {e}", exc_info=True)
+                self.send_json_response({"error": str(e)}, 500)
+        else:
+            self.send_json_response({"error": "Not Found"}, 404)
+
+    def do_OPTIONS(self):
+        self.send_response(200)
+        self.send_header('Access-Control-Allow-Origin', '*')
+        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
+        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
+        self.end_headers()
+
+    def send_json_response(self, data, status=200):
+        self.send_response(status)
+        self.send_header('Content-Type', 'application/json; charset=utf-8')
+        self.send_header('Access-Control-Allow-Origin', '*')
+        self.end_headers()
+        self.wfile.write(json.dumps(data, ensure_ascii=False, indent=2).encode('utf-8'))
+
+    def serve_file(self, filepath: str, content_type: str):
+        if os.path.exists(filepath):
+            self.send_response(200)
+            self.send_header('Content-Type', f'{content_type}; charset=utf-8')
+            self.end_headers()
+            with open(filepath, 'rb') as f:
+                self.wfile.write(f.read())
+        else:
+            self.send_json_response({"error": f"文件不存在: {filepath}"}, 404)
+
+    def end_headers(self):
+        self.send_header('Access-Control-Allow-Origin', '*')
+        super().end_headers()
+
+
+# ─── 启动服务器 ────────────────────────────────────────────────────────────────
+
+def run_server(port=8766):
+    server = HTTPServer(('0.0.0.0', port), SemanticLogicHandler)
+    print(f"\n{'='*70}")
+    print(f" 语义逻辑审查 — 前端测试服务器")
+    print(f"{'='*70}")
+    print(f" 访问地址: http://localhost:{port}")
+    print(f" API端点:  POST /api/semantic_logic")
+    print(f"{'='*70}\n")
+    server.serve_forever()
+
+
+if __name__ == "__main__":
+    import argparse
+    parser = argparse.ArgumentParser(description='语义逻辑审查前端测试服务器')
+    parser.add_argument('--port', type=int, default=8766, help='服务端口 (默认: 8766)')
+    args = parser.parse_args()
+    run_server(args.port)

+ 48 - 107
utils_test/Semantic_Logic_Test/test_data.py

@@ -1,10 +1,11 @@
 """
-测试数据示例
-提供各种测试场景的示例数据
+语义逻辑审查模块 — 测试数据
+提供各种测试场景的示例施工方案内容
 """
 
-# 正常的施工方案内容
-NORMAL_REVIEW_CONTENT = """
+# ─── 正常的施工方案内容 ─────────────────────────────────────────────────────────
+
+NORMAL_CONTENT = """
 第一章 工程概况
 
 1.1 项目基本信息
@@ -24,37 +25,60 @@ NORMAL_REVIEW_CONTENT = """
 竣工日期:2026年2月28日
 """
 
-# 带有逻辑问题的内容
+# ─── 包含语义逻辑问题的内容 ─────────────────────────────────────────────────────
+
 LOGIC_ERROR_CONTENT = """
 第二章 施工方案
 
 2.1 施工顺序
 本工程采用先上后下的施工顺序,首先进行桥梁上部结构施工,然后进行桥墩基础施工。
-(注:这里存在逻辑错误,应该先施工基础再施工上部结构)
+(逻辑错误:应先施工基础再施工上部结构)
+
+2.2 工期与工序矛盾
+本工程计划工期为6个月,施工内容包括路基土石方200万方、桥梁5座、隧道3座。
+(工期与工程量严重不匹配)
+
+2.3 因果错误
+因为天气晴朗,所以混凝土强度不足。
+(因果无关)
+
+2.4 条件结论不匹配
+当温度低于5℃时,可正常进行混凝土浇筑施工。
+(违背常识——低温不宜浇筑混凝土)
+"""
+
+# ─── 无明显问题的内容 ───────────────────────────────────────────────────────────
+
+CLEAN_CONTENT = """
+第三章 质量保证措施
 
-2.2 工期安排
-本工程计划工期为6个月,但根据施工内容分析,实际需要至少12个月才能完成。
-(注:工期安排不合理)
+3.1 质量管理体系
+建立健全质量管理组织机构,实行项目经理负责制,配备专职质检工程师。
+
+3.2 材料质量控制
+所有进场材料必须具有出厂合格证,并按规范要求进行抽样检验,合格后方可使用。
+
+3.3 施工过程控制
+每道工序施工前进行技术交底,施工中严格执行三检制,上道工序不合格不得进入下道工序。
 """
 
-# 空内容
+# ─── 边界情况内容 ──────────────────────────────────────────────────────────────
+
 EMPTY_CONTENT = ""
 
-# 超长内容
-VERY_LONG_CONTENT = """
-第三章 施工技术方案
-""" + "\n".join([f"3.{i} 施工工艺详细说明第{i}条,包含大量技术细节和参数..." * 10 for i in range(1, 1001)])
+LONG_CONTENT = "\n".join(
+    f"3.{i} 施工工艺详细说明第{i}条,包含大量技术细节和参数要求,需严格按照规范执行..."
+    for i in range(1, 501)
+)
 
-# 特殊字符内容
 SPECIAL_CHARS_CONTENT = """
 特殊字符测试:
 - 符号:@#$%^&*()_+-={}[]|\\:;"'<>,.?/~`
 - 数学符号:±×÷≈≠≤≥∞∑∫√
 - 单位符号:℃、㎡、㎥、㎏、㎜
-- 其他:①②③④⑤⑥⑦⑧⑨⑩
+- 编号:①②③④⑤⑥⑦⑧⑨⑩
 """
 
-# Unicode 多语言内容
 UNICODE_CONTENT = """
 多语言测试:
 - 中文:施工方案审查
@@ -66,95 +90,12 @@ UNICODE_CONTENT = """
 - Emoji: 🚧🏗️👷‍♂️📋✅❌
 """
 
-# 参考标准示例
-REFERENCE_STANDARDS = """
-参考标准:
-1. 《公路工程技术标准》JTG B01-2014
-2. 《公路桥涵施工技术规范》JTG/T 3650-2020
-3. 《公路路基施工技术规范》JTG/T 3610-2019
-4. 《公路工程质量检验评定标准》JTG F80/1-2017
-"""
-
-# 审查位置标签示例
-REVIEW_LOCATIONS = [
-    "第一章 工程概况",
-    "第二章 施工方案",
-    "第三章 质量保证措施",
-    "第四章 安全保证措施",
-    "第五章 环境保护措施",
-]
+# ─── 单条施工技术语句 ───────────────────────────────────────────────────────────
 
-# Trace ID 示例
-TRACE_IDS = [
-    "semantic_check_001",
-    "semantic_check_002",
-    "semantic_check_003",
+SINGLE_STATEMENTS = [
+    "本桥采用预应力混凝土连续箱梁结构,跨径布置为30m+40m+30m。",
+    "混凝土浇筑前应对模板、钢筋、预埋件进行检查,符合要求后方可浇筑。",
+    "预应力张拉应在混凝土强度达到设计强度的85%以上时进行。",
+    "高空作业人员必须佩戴安全带,安全带应高挂低用。",
+    "施工现场临时用电应采用TN-S三相五线制系统。",
 ]
-
-# 模拟的 API 响应
-MOCK_API_RESPONSES = {
-    "success": "审查结果:内容逻辑清晰,结构合理,符合规范要求。未发现明显问题。",
-    "with_issues": """
-审查结果:发现以下问题:
-1. 施工顺序存在逻辑错误,应先进行基础施工再进行上部结构施工
-2. 工期安排不合理,建议调整为12个月
-3. 缺少安全措施的详细说明
-    """,
-    "empty": "内容为空,无法进行审查。",
-    "error": "审查失败:API 连接超时",
-}
-
-# 状态字典示例
-def create_mock_state():
-    """创建模拟的状态字典"""
-    from unittest.mock import AsyncMock
-    
-    mock_progress_manager = AsyncMock()
-    mock_progress_manager.update_stage_progress = AsyncMock()
-    
-    return {
-        "progress_manager": mock_progress_manager,
-        "callback_task_id": "test_callback_task_123",
-        "session_id": "test_session_456",
-    }
-
-# 测试场景配置
-TEST_SCENARIOS = {
-    "normal": {
-        "content": NORMAL_REVIEW_CONTENT,
-        "references": REFERENCE_STANDARDS,
-        "location": REVIEW_LOCATIONS[0],
-        "expected_success": True,
-    },
-    "logic_error": {
-        "content": LOGIC_ERROR_CONTENT,
-        "references": REFERENCE_STANDARDS,
-        "location": REVIEW_LOCATIONS[1],
-        "expected_success": True,
-    },
-    "empty": {
-        "content": EMPTY_CONTENT,
-        "references": "",
-        "location": REVIEW_LOCATIONS[0],
-        "expected_success": True,
-    },
-    "long": {
-        "content": VERY_LONG_CONTENT,
-        "references": REFERENCE_STANDARDS,
-        "location": REVIEW_LOCATIONS[2],
-        "expected_success": True,
-    },
-    "special_chars": {
-        "content": SPECIAL_CHARS_CONTENT,
-        "references": "",
-        "location": REVIEW_LOCATIONS[3],
-        "expected_success": True,
-    },
-    "unicode": {
-        "content": UNICODE_CONTENT,
-        "references": "",
-        "location": REVIEW_LOCATIONS[4],
-        "expected_success": True,
-    },
-}
-

+ 348 - 396
utils_test/Semantic_Logic_Test/test_semantic_logic.py

@@ -1,474 +1,426 @@
 """
-语义逻辑审查模块单元测试
-测试 semantic_logic.py 中的 SemanticLogicReviewer 类
+语义逻辑审查模块 — 链路测试
+测试 semantic_logic.py 中的 SemanticLogicReviewer 完整调用链路
+
+链路: prompt_loader → ChatPromptTemplate.format_messages() → generate_model_client.get_model_generate_invoke() → ReviewResult
 """
 
 import pytest
 import asyncio
 import sys
 import os
-from unittest.mock import Mock, patch, AsyncMock, MagicMock
-from typing import Dict, Any
+from unittest.mock import Mock, patch, AsyncMock, MagicMock, PropertyMock
 
-# 添加项目根目录到路径
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
 
 from core.construction_review.component.reviewers.semantic_logic import (
     SemanticLogicReviewer,
     semantic_logic_reviewer,
-    SEMANTIC_LOGIC_MODEL_CONFIG
 )
 from core.construction_review.component.reviewers.base_reviewer import ReviewResult
 
 
-class TestSemanticLogicReviewer:
-    """语义逻辑审查器测试类"""
-    
-    @pytest.fixture
-    def reviewer(self):
-        """创建审查器实例"""
-        return SemanticLogicReviewer()
-    
-    @pytest.fixture
-    def mock_state(self):
-        """创建模拟的状态字典"""
-        mock_progress_manager = AsyncMock()
-        mock_progress_manager.update_stage_progress = AsyncMock()
-        
-        return {
-            "progress_manager": mock_progress_manager,
-            "callback_task_id": "test_task_123"
-        }
-    
-    @pytest.fixture
-    def sample_review_content(self):
-        """示例审查内容"""
-        return """
-        施工方案概述:
-        本工程为高速公路桥梁施工项目,主要包括桥墩基础施工、桥梁上部结构施工等内容。
-        施工工期为12个月,计划2024年3月开工,2025年3月竣工。
-        """
-    
-    @pytest.fixture
-    def sample_review_references(self):
-        """示例审查参考"""
-        return "参考标准:《公路桥涵施工技术规范》JTG/T 3650-2020"
-    
-    def test_reviewer_initialization(self, reviewer):
-        """测试审查器初始化"""
-        assert reviewer is not None
-        assert reviewer.model == SEMANTIC_LOGIC_MODEL_CONFIG["model"]
-        assert reviewer.temperature == SEMANTIC_LOGIC_MODEL_CONFIG["temperature"]
-        assert reviewer.max_tokens == SEMANTIC_LOGIC_MODEL_CONFIG["max_tokens"]
-        assert reviewer.client is not None
-    
-    def test_global_singleton_instance(self):
-        """测试全局单例实例"""
-        assert semantic_logic_reviewer is not None
+# ─── helpers ───────────────────────────────────────────────────────────────────
+
+def _make_mock_prompt_template(messages=None):
+    """构造一个模拟的 ChatPromptTemplate,format_messages() 返回指定消息列表"""
+    from langchain_core.messages import SystemMessage, HumanMessage
+
+    if messages is None:
+        messages = [
+            SystemMessage(content="你是语义逻辑审查专家"),
+            HumanMessage(content="请审查:测试施工方案内容"),
+        ]
+    tmpl = MagicMock()
+    tmpl.format_messages.return_value = messages
+    return tmpl
+
+
+def _make_mock_state():
+    """构造模拟的 state 字典(含 progress_manager)"""
+    pm = AsyncMock()
+    pm.update_stage_progress = AsyncMock()
+    return {
+        "progress_manager": pm,
+        "callback_task_id": "test_callback_001",
+    }
+
+
+# ─── 单元测试:初始化 ──────────────────────────────────────────────────────────
+
+class TestInit:
+    """审查器初始化测试"""
+
+    def test_init_model_client_set(self):
+        """初始化后 model_client 应为 GenerateModelClient 实例"""
+        r = SemanticLogicReviewer()
+        from foundation.ai.agent.generate.model_generate import GenerateModelClient
+        assert isinstance(r.model_client, GenerateModelClient)
+
+    def test_global_singleton(self):
+        """全局单例实例"""
         assert isinstance(semantic_logic_reviewer, SemanticLogicReviewer)
-    
-    def test_model_config(self):
-        """测试模型配置"""
-        assert SEMANTIC_LOGIC_MODEL_CONFIG["base_url"] == "http://192.168.91.253:8003/v1"
-        assert SEMANTIC_LOGIC_MODEL_CONFIG["api_key"] == "sk-123456"
-        assert SEMANTIC_LOGIC_MODEL_CONFIG["model"] == "qwen3-30b"
-        assert SEMANTIC_LOGIC_MODEL_CONFIG["temperature"] == 0.7
-        assert SEMANTIC_LOGIC_MODEL_CONFIG["max_tokens"] == 2000
-    
+
+
+# ─── 单元测试:成功路径 ────────────────────────────────────────────────────────
+
+class TestCheckSemanticLogicSuccess:
+    """检查语义逻辑 — 成功路径"""
+
     @pytest.mark.asyncio
-    async def test_check_semantic_logic_success(
-        self, 
-        reviewer, 
-        sample_review_content, 
-        sample_review_references,
-        mock_state
-    ):
-        """测试语义逻辑检查成功场景"""
-        # 模拟 OpenAI API 响应
-        mock_response = MagicMock()
-        mock_response.choices = [MagicMock()]
-        mock_response.choices[0].message.content = "审查结果:内容逻辑清晰,无明显问题。"
-        
-        # 模拟提示词模板
-        mock_prompt_template = MagicMock()
-        mock_message = MagicMock()
-        mock_message.type = "user"
-        mock_message.content = "请审查以下内容"
-        mock_prompt_template.format_messages.return_value = [mock_message]
-        
-        with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
+    async def test_success_path(self):
+        """
+        验证完整成功链路:
+        prompt_loader.get_prompt_template → format_messages → model_client.get_model_generate_invoke → ReviewResult
+        """
+        reviewer = SemanticLogicReviewer()
+        mock_tmpl = _make_mock_prompt_template()
+        fake_response = '{"issue_point": "无", "location": "", "suggestion": "", "reason": "", "risk_level": ""}'
+
+        with patch.object(reviewer.model_client, 'get_model_generate_invoke', new_callable=AsyncMock) as mock_invoke, \
              patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
-            
-            mock_create.return_value = mock_response
-            mock_get_prompt.return_value = mock_prompt_template
-            
-            # 执行测试
+            mock_invoke.return_value = fake_response
+            mock_get_prompt.return_value = mock_tmpl
+
             result = await reviewer.check_semantic_logic(
-                trace_id="test_trace_001",
-                review_content=sample_review_content,
-                review_references=sample_review_references,
-                review_location_label="第一章",
-                state=mock_state,
-                stage_name="basic_check"
+                trace_id="trace_001",
+                review_content="测试施工方案内容",
             )
-            
-            # 验证结果
+
+            # 链路1: prompt_loader 被正确调用
+            mock_get_prompt.assert_called_once_with(
+                "basic", "semantic_logic_check",
+                review_content="测试施工方案内容",
+                review_references="",
+            )
+
+            # 链路2: format_messages 被调用
+            mock_tmpl.format_messages.assert_called_once()
+
+            # 链路3: model_client 使用 function_name="grammar_check"
+            mock_invoke.assert_awaited_once()
+            call_kwargs = mock_invoke.call_args.kwargs
+            assert call_kwargs["trace_id"] == "trace_001"
+            assert call_kwargs["function_name"] == "grammar_check"
+            assert call_kwargs["messages"] == mock_tmpl.format_messages.return_value
+
+            # 链路4: 返回正确的 ReviewResult
             assert isinstance(result, ReviewResult)
             assert result.success is True
             assert result.details["name"] == "semantic_logic_check"
-            assert "审查结果" in result.details["response"]
+            assert result.details["response"] == fake_response
             assert result.error_message is None
-            assert result.execution_time is not None
             assert result.execution_time > 0
-            
-            # 验证 API 调用
-            mock_create.assert_called_once()
-            call_kwargs = mock_create.call_args.kwargs
-            assert call_kwargs["model"] == "qwen3-30b"
-            assert call_kwargs["temperature"] == 0.7
-            assert call_kwargs["max_tokens"] == 2000
-            
-            # 验证进度管理器被调用
-            mock_state["progress_manager"].update_stage_progress.assert_called()
-    
+
     @pytest.mark.asyncio
-    async def test_check_semantic_logic_without_state(
-        self, 
-        reviewer, 
-        sample_review_content
-    ):
-        """测试没有状态字典的语义逻辑检查"""
-        mock_response = MagicMock()
-        mock_response.choices = [MagicMock()]
-        mock_response.choices[0].message.content = "审查通过"
-        
-        mock_prompt_template = MagicMock()
-        mock_message = MagicMock()
-        mock_message.type = "system"
-        mock_message.content = "系统提示"
-        mock_prompt_template.format_messages.return_value = [mock_message]
-        
-        with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
+    async def test_with_state_triggers_progress(self):
+        """有 state 时触发进度推送"""
+        reviewer = SemanticLogicReviewer()
+        mock_tmpl = _make_mock_prompt_template()
+        fake_response = "无明显问题"
+        state = _make_mock_state()
+
+        with patch.object(reviewer.model_client, 'get_model_generate_invoke', new_callable=AsyncMock) as mock_invoke, \
              patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
-            
-            mock_create.return_value = mock_response
-            mock_get_prompt.return_value = mock_prompt_template
-            
+            mock_invoke.return_value = fake_response
+            mock_get_prompt.return_value = mock_tmpl
+
             result = await reviewer.check_semantic_logic(
-                trace_id="test_trace_002",
-                review_content=sample_review_content,
+                trace_id="trace_002",
+                review_content="施工方案内容...",
+                state=state,
+                stage_name="basic_check",
+            )
+
+            assert result.success is True
+
+            # 进度推送是 fire-and-forget (asyncio.create_task),等待一下
+            await asyncio.sleep(0.1)
+
+            pm = state["progress_manager"]
+            assert pm.update_stage_progress.called
+            call_kwargs = pm.update_stage_progress.call_args.kwargs
+            assert call_kwargs["callback_task_id"] == "test_callback_001"
+            assert call_kwargs["stage_name"] == "basic_check"
+            # issues 中包含审查结果数据
+            assert len(call_kwargs["issues"]) == 1
+            assert call_kwargs["issues"][0]["name"] == "semantic_logic_check"
+            assert call_kwargs["issues"][0]["success"] is True
+
+    @pytest.mark.asyncio
+    async def test_without_state_no_progress_call(self):
+        """无 state 时不触发进度推送,不抛异常"""
+        reviewer = SemanticLogicReviewer()
+        mock_tmpl = _make_mock_prompt_template()
+
+        with patch.object(reviewer.model_client, 'get_model_generate_invoke', new_callable=AsyncMock) as mock_invoke, \
+             patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
+            mock_invoke.return_value = "无明显问题"
+            mock_get_prompt.return_value = mock_tmpl
+
+            result = await reviewer.check_semantic_logic(
+                trace_id="trace_003",
+                review_content="内容",
                 state=None,
-                stage_name=None
+                stage_name=None,
             )
-            
+
             assert result.success is True
-            assert result.details["name"] == "semantic_logic_check"
-    
+            # 不应抛异常
+
+
+# ─── 单元测试:错误路径 ────────────────────────────────────────────────────────
+
+class TestCheckSemanticLogicError:
+    """检查语义逻辑 — 错误路径"""
+
     @pytest.mark.asyncio
-    async def test_check_semantic_logic_api_error(
-        self, 
-        reviewer, 
-        sample_review_content,
-        mock_state
-    ):
-        """测试 API 调用失败场景"""
-        mock_prompt_template = MagicMock()
-        mock_message = MagicMock()
-        mock_message.type = "user"
-        mock_message.content = "测试内容"
-        mock_prompt_template.format_messages.return_value = [mock_message]
-        
-        with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
+    async def test_model_call_failure(self):
+        """模型调用抛异常 → 返回 success=False 的 ReviewResult"""
+        reviewer = SemanticLogicReviewer()
+        mock_tmpl = _make_mock_prompt_template()
+        state = _make_mock_state()
+
+        with patch.object(reviewer.model_client, 'get_model_generate_invoke', new_callable=AsyncMock) as mock_invoke, \
              patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
-            
-            mock_create.side_effect = Exception("API连接失败")
-            mock_get_prompt.return_value = mock_prompt_template
-            
+            mock_invoke.side_effect = Exception("模型服务连接超时")
+            mock_get_prompt.return_value = mock_tmpl
+
             result = await reviewer.check_semantic_logic(
-                trace_id="test_trace_003",
-                review_content=sample_review_content,
-                state=mock_state,
-                stage_name="basic_check"
+                trace_id="trace_err_001",
+                review_content="内容",
+                state=state,
+                stage_name="basic_check",
             )
-            
-            # 验证错误处理
+
             assert isinstance(result, ReviewResult)
             assert result.success is False
-            assert result.error_message is not None
-            assert "API连接失败" in result.error_message
-            assert result.execution_time is not None
-            
-            # 验证错误通知被发送
-            mock_state["progress_manager"].update_stage_progress.assert_called()
-    
+            assert result.details["name"] == "semantic_logic_check"
+            assert "模型服务连接超时" in result.error_message
+            assert result.execution_time > 0
+
+            # 失败也应推送进度
+            await asyncio.sleep(0.1)
+            assert state["progress_manager"].update_stage_progress.called
+
     @pytest.mark.asyncio
-    async def test_check_semantic_logic_empty_content(
-        self, 
-        reviewer,
-        mock_state
-    ):
-        """测试空内容场景"""
-        mock_response = MagicMock()
-        mock_response.choices = [MagicMock()]
-        mock_response.choices[0].message.content = "内容为空,无法审查"
-        
-        mock_prompt_template = MagicMock()
-        mock_message = MagicMock()
-        mock_message.type = "user"
-        mock_message.content = ""
-        mock_prompt_template.format_messages.return_value = [mock_message]
-        
-        with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
+    async def test_prompt_loader_failure(self):
+        """prompt_loader 抛异常 → 应被捕获"""
+        reviewer = SemanticLogicReviewer()
+
+        with patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
+            mock_get_prompt.side_effect = RuntimeError("YAML 解析失败")
+
+            result = await reviewer.check_semantic_logic(
+                trace_id="trace_err_002",
+                review_content="内容",
+            )
+
+            assert result.success is False
+            assert "YAML 解析失败" in result.error_message
+
+    @pytest.mark.asyncio
+    async def test_error_without_state(self):
+        """错误时无 state 也不应抛异常"""
+        reviewer = SemanticLogicReviewer()
+
+        with patch.object(reviewer.model_client, 'get_model_generate_invoke', new_callable=AsyncMock) as mock_invoke, \
              patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
-            
-            mock_create.return_value = mock_response
-            mock_get_prompt.return_value = mock_prompt_template
-            
+            mock_invoke.side_effect = Exception("boom")
+            mock_get_prompt.return_value = _make_mock_prompt_template()
+
             result = await reviewer.check_semantic_logic(
-                trace_id="test_trace_004",
-                review_content="",
-                state=mock_state,
-                stage_name="basic_check"
+                trace_id="trace_err_003",
+                review_content="内容",
+                state=None,
+            )
+
+            assert result.success is False
+            assert "boom" in result.error_message
+
+
+# ─── 链路集成测试:真实 prompt_loader + mock AI 调用 ────────────────────────────
+
+class TestChainIntegration:
+    """集成链路测试:使用真实 prompt_loader 验证完整链路(仅 mock AI 调用)"""
+
+    @pytest.mark.asyncio
+    async def test_full_chain_with_real_prompt_loader(self):
+        """
+        使用真实 prompt_loader 加载 basic/semantic_logic_check 模板,
+        仅 mock 底层 AI 调用,验证完整链路畅通。
+        """
+        reviewer = SemanticLogicReviewer()
+        fake_ai_response = "无明显问题"
+
+        with patch.object(reviewer.model_client, 'get_model_generate_invoke', new_callable=AsyncMock) as mock_invoke:
+            mock_invoke.return_value = fake_ai_response
+
+            result = await reviewer.check_semantic_logic(
+                trace_id="chain_001",
+                review_content="1. 工程概况\n本工程位于四川省,全长120公里。\n2. 施工安排\n先进行基础施工,再进行上部结构施工。",
             )
-            
+
             assert result.success is True
             assert result.details["name"] == "semantic_logic_check"
-    
+            assert result.details["response"] == fake_ai_response
+
+            # 验证 AI 调用确实收到了正确格式的消息
+            call_kwargs = mock_invoke.call_args.kwargs
+            assert call_kwargs["function_name"] == "grammar_check"
+            messages = call_kwargs["messages"]
+            assert len(messages) >= 2  # system + user
+
+            # system message 应包含角色定义
+            system_msg = messages[0]
+            assert "语义逻辑审查" in system_msg.content or "role" in system_msg.content.lower()
+
+            # user message 应包含待审查内容
+            user_msg = messages[-1]
+            assert "工程概况" in user_msg.content or "120公里" in user_msg.content
+
     @pytest.mark.asyncio
-    async def test_check_semantic_logic_with_references(
-        self, 
-        reviewer, 
-        sample_review_content,
-        sample_review_references
-    ):
-        """测试带参考信息的语义逻辑检查"""
-        mock_response = MagicMock()
-        mock_response.choices = [MagicMock()]
-        mock_response.choices[0].message.content = "根据参考标准,内容符合要求"
-        
-        mock_prompt_template = MagicMock()
-        mock_message = MagicMock()
-        mock_message.type = "user"
-        mock_message.content = "审查内容"
-        mock_prompt_template.format_messages.return_value = [mock_message]
-        
-        with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
-             patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
-            
-            mock_create.return_value = mock_response
-            mock_get_prompt.return_value = mock_prompt_template
-            
+    async def test_chain_with_state_progress(self):
+        """集成链路 + 进度推送"""
+        reviewer = SemanticLogicReviewer()
+        state = _make_mock_state()
+
+        with patch.object(reviewer.model_client, 'get_model_generate_invoke', new_callable=AsyncMock) as mock_invoke:
+            mock_invoke.return_value = '{"issue_point": "逻辑矛盾", "location": "第2条", "suggestion": "调整", "reason": "前后矛盾", "risk_level": "中风险"}'
+
             result = await reviewer.check_semantic_logic(
-                trace_id="test_trace_005",
-                review_content=sample_review_content,
-                review_references=sample_review_references
+                trace_id="chain_002",
+                review_content="前文采用A方法。后文说不能采用A方法。",
+                state=state,
+                stage_name="semantic_stage",
             )
-            
+
             assert result.success is True
-            assert "参考标准" in result.details["response"]
-            
-            # 验证提示词模板被正确调用
-            mock_get_prompt.assert_called_once()
-            call_kwargs = mock_get_prompt.call_args.kwargs
-            assert call_kwargs["review_content"] == sample_review_content
-            assert call_kwargs["review_references"] == sample_review_references
-    
+            await asyncio.sleep(0.1)
+            assert state["progress_manager"].update_stage_progress.called
+
+
+# ─── 边界情况测试 ──────────────────────────────────────────────────────────────
+
+class TestEdgeCases:
+    """边界情况测试"""
+
     @pytest.mark.asyncio
-    async def test_message_format_conversion(self, reviewer, sample_review_content):
-        """测试消息格式转换"""
-        mock_response = MagicMock()
-        mock_response.choices = [MagicMock()]
-        mock_response.choices[0].message.content = "测试响应"
-        
-        # 模拟不同类型的消息
-        mock_prompt_template = MagicMock()
-        mock_system_msg = MagicMock()
-        mock_system_msg.type = "system"
-        mock_system_msg.content = "系统提示"
-        
-        mock_user_msg = MagicMock()
-        mock_user_msg.type = "user"
-        mock_user_msg.content = "用户输入"
-        
-        mock_unknown_msg = MagicMock()
-        mock_unknown_msg.type = "unknown"
-        mock_unknown_msg.content = "未知类型"
-        
-        mock_prompt_template.format_messages.return_value = [
-            mock_system_msg, 
-            mock_user_msg, 
-            mock_unknown_msg
-        ]
-        
-        with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
+    async def test_empty_content(self):
+        """空内容 — 链路正常完成"""
+        reviewer = SemanticLogicReviewer()
+
+        with patch.object(reviewer.model_client, 'get_model_generate_invoke', new_callable=AsyncMock) as mock_invoke, \
              patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
-            
-            mock_create.return_value = mock_response
-            mock_get_prompt.return_value = mock_prompt_template
-            
+            mock_invoke.return_value = "内容为空,无法审查"
+            mock_get_prompt.return_value = _make_mock_prompt_template()
+
             result = await reviewer.check_semantic_logic(
-                trace_id="test_trace_006",
-                review_content=sample_review_content
+                trace_id="edge_001",
+                review_content="",
             )
-            
-            # 验证消息格式转换
-            call_kwargs = mock_create.call_args.kwargs
-            messages = call_kwargs["messages"]
-            
-            assert len(messages) == 3
-            assert messages[0]["role"] == "system"
-            assert messages[0]["content"] == "系统提示"
-            assert messages[1]["role"] == "user"
-            assert messages[1]["content"] == "用户输入"
-            assert messages[2]["role"] == "user"  # unknown类型应转为user
-            assert messages[2]["content"] == "未知类型"
-    
+
+            assert result.success is True
+            assert result.details["name"] == "semantic_logic_check"
+
     @pytest.mark.asyncio
-    async def test_execution_time_tracking(self, reviewer, sample_review_content):
-        """测试执行时间跟踪"""
-        mock_response = MagicMock()
-        mock_response.choices = [MagicMock()]
-        mock_response.choices[0].message.content = "测试"
-        
-        mock_prompt_template = MagicMock()
-        mock_message = MagicMock()
-        mock_message.type = "user"
-        mock_message.content = "测试"
-        mock_prompt_template.format_messages.return_value = [mock_message]
-        
-        async def slow_api_call(*args, **kwargs):
-            """模拟慢速API调用"""
-            await asyncio.sleep(0.1)  # 模拟100ms延迟
-            return mock_response
-        
-        with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
+    async def test_long_content(self):
+        """长内容 — 链路正常完成"""
+        reviewer = SemanticLogicReviewer()
+        long_text = "第{}条 施工技术要求详细说明...\n".format
+        content = "\n".join(long_text(i) for i in range(500))
+
+        with patch.object(reviewer.model_client, 'get_model_generate_invoke', new_callable=AsyncMock) as mock_invoke, \
              patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
-            
-            mock_create.side_effect = slow_api_call
-            mock_get_prompt.return_value = mock_prompt_template
-            
+            mock_invoke.return_value = "无明显问题"
+            mock_get_prompt.return_value = _make_mock_prompt_template()
+
             result = await reviewer.check_semantic_logic(
-                trace_id="test_trace_007",
-                review_content=sample_review_content
+                trace_id="edge_002",
+                review_content=content,
             )
-            
-            # 验证执行时间大于100ms
-            assert result.execution_time >= 0.1
-            assert result.execution_time < 1.0  # 应该不会太长
 
+            assert result.success is True
 
-class TestIntegration:
-    """集成测试类"""
-    
     @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_full_workflow(self):
-        """测试完整工作流程(需要实际API可用)"""
-        # 注意:此测试需要实际的API服务可用
-        # 在CI/CD环境中可能需要跳过
-        pytest.skip("需要实际API服务,跳过集成测试")
-        
+    async def test_special_characters(self):
+        """特殊字符内容"""
         reviewer = SemanticLogicReviewer()
-        
-        result = await reviewer.check_semantic_logic(
-            trace_id="integration_test_001",
-            review_content="测试施工方案内容",
-            review_references="测试参考标准"
-        )
-        
-        assert isinstance(result, ReviewResult)
-        assert result.execution_time is not None
-
+        content = "特殊字符:@#$%^&*(){}[]|\\:;\"'<>,.?/~` ±×÷≈≠≤≥∞"
 
-class TestEdgeCases:
-    """边界情况测试类"""
-    
-    @pytest.mark.asyncio
-    async def test_very_long_content(self, reviewer):
-        """测试超长内容"""
-        long_content = "测试内容 " * 10000  # 非常长的内容
-        
-        mock_response = MagicMock()
-        mock_response.choices = [MagicMock()]
-        mock_response.choices[0].message.content = "内容过长"
-        
-        mock_prompt_template = MagicMock()
-        mock_message = MagicMock()
-        mock_message.type = "user"
-        mock_message.content = long_content
-        mock_prompt_template.format_messages.return_value = [mock_message]
-        
-        with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
+        with patch.object(reviewer.model_client, 'get_model_generate_invoke', new_callable=AsyncMock) as mock_invoke, \
              patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
-            
-            mock_create.return_value = mock_response
-            mock_get_prompt.return_value = mock_prompt_template
-            
+            mock_invoke.return_value = "无明显问题"
+            mock_get_prompt.return_value = _make_mock_prompt_template()
+
             result = await reviewer.check_semantic_logic(
-                trace_id="test_edge_001",
-                review_content=long_content
+                trace_id="edge_003",
+                review_content=content,
             )
-            
+
             assert result.success is True
-    
+
     @pytest.mark.asyncio
-    async def test_special_characters(self, reviewer):
-        """测试特殊字符"""
-        special_content = "测试内容包含特殊字符:@#$%^&*(){}[]|\\:;\"'<>,.?/~`"
-        
-        mock_response = MagicMock()
-        mock_response.choices = [MagicMock()]
-        mock_response.choices[0].message.content = "特殊字符处理正常"
-        
-        mock_prompt_template = MagicMock()
-        mock_message = MagicMock()
-        mock_message.type = "user"
-        mock_message.content = special_content
-        mock_prompt_template.format_messages.return_value = [mock_message]
-        
-        with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
+    async def test_unicode_multilang(self):
+        """多语言 Unicode 内容"""
+        reviewer = SemanticLogicReviewer()
+        content = "中文 / English / 日本語 / 한국어 / Русский / العربية / 🚧🏗️"
+
+        with patch.object(reviewer.model_client, 'get_model_generate_invoke', new_callable=AsyncMock) as mock_invoke, \
              patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
-            
-            mock_create.return_value = mock_response
-            mock_get_prompt.return_value = mock_prompt_template
-            
+            mock_invoke.return_value = "无明显问题"
+            mock_get_prompt.return_value = _make_mock_prompt_template()
+
             result = await reviewer.check_semantic_logic(
-                trace_id="test_edge_002",
-                review_content=special_content
+                trace_id="edge_004",
+                review_content=content,
             )
-            
+
             assert result.success is True
-    
+
     @pytest.mark.asyncio
-    async def test_unicode_content(self, reviewer):
-        """测试Unicode字符"""
-        unicode_content = "测试内容包含各种语言:English, 中文, 日本語, 한국어, Русский, العربية, 🚀🎉"
-        
-        mock_response = MagicMock()
-        mock_response.choices = [MagicMock()]
-        mock_response.choices[0].message.content = "Unicode处理正常"
-        
-        mock_prompt_template = MagicMock()
-        mock_message = MagicMock()
-        mock_message.type = "user"
-        mock_message.content = unicode_content
-        mock_prompt_template.format_messages.return_value = [mock_message]
-        
-        with patch.object(reviewer.client.chat.completions, 'create', new_callable=AsyncMock) as mock_create, \
+    async def test_execution_time_tracking(self):
+        """验证执行时间被正确记录"""
+        reviewer = SemanticLogicReviewer()
+
+        async def slow_response(*args, **kwargs):
+            await asyncio.sleep(0.15)
+            return "响应"
+
+        with patch.object(reviewer.model_client, 'get_model_generate_invoke', new_callable=AsyncMock) as mock_invoke, \
              patch('core.construction_review.component.reviewers.semantic_logic.prompt_loader.get_prompt_template') as mock_get_prompt:
-            
-            mock_create.return_value = mock_response
-            mock_get_prompt.return_value = mock_prompt_template
-            
+            mock_invoke.side_effect = slow_response
+            mock_get_prompt.return_value = _make_mock_prompt_template()
+
             result = await reviewer.check_semantic_logic(
-                trace_id="test_edge_003",
-                review_content=unicode_content
+                trace_id="edge_005",
+                review_content="测试",
             )
-            
-            assert result.success is True
+
+            assert result.execution_time >= 0.15
+
+
+# ─── 实际 API 集成测试(需手动开启)─────────────────────────────────────────────
+
+class TestLiveAPI:
+    """实际 API 调用测试(标记为 integration,默认跳过)"""
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_live_api_call(self):
+        """真实调用 AI 接口(需服务可用)"""
+        pytest.skip("需要实际 API 服务,手动运行")
+
+        reviewer = SemanticLogicReviewer()
+        result = await reviewer.check_semantic_logic(
+            trace_id="live_001",
+            review_content="1. 工程概况\n本工程为高速公路桥梁项目。\n2. 施工顺序\n先施工上部结构,再进行基础施工。",
+        )
+
+        assert isinstance(result, ReviewResult)
+        # 不强制断言 success,依赖实际服务状态
+        assert result.execution_time is not None
 
 
 if __name__ == "__main__":
-    # 运行测试
     pytest.main([__file__, "-v", "-s", "--tb=short"])
-

+ 264 - 0
utils_test/Semantic_Logic_Test/测试案例.md

@@ -0,0 +1,264 @@
+# 语义逻辑审查 — 前端测试案例
+
+以下案例可直接复制到前端测试页面(http://localhost:8766)的文本框中执行测试。
+
+---
+
+## 1. 逻辑矛盾 — 施工顺序颠倒
+
+```
+第三章 施工方案
+
+3.1 施工顺序
+本桥上部结构采用预制T梁架设方案,所有T梁在预制场集中预制,达到设计强度后运至现场架设。
+
+3.2 下部结构施工
+本桥下部结构采用现浇混凝土施工,待桥梁上部结构架设完成后,再进行桥墩和基础的现浇施工。
+
+3.3 总结
+上述施工顺序确保了上部结构先行的优势,可有效缩短总工期。
+```
+
+**预期结果**:应识别出"先施工上部结构再施工下部结构"的逻辑矛盾。
+
+---
+
+## 2. 逻辑矛盾 — 安全规定前后冲突
+
+```
+第四章 安全措施
+
+4.1 高空作业规定
+严禁在无防护条件下进行高处作业,所有2米以上作业必须设置安全网和安全带。
+
+4.2 特殊情况处理
+遇大风天气时,为抢抓工期,允许作业人员在做好简要防护后继续高处作业。
+```
+
+**预期结果**:应识别出"严禁无防护作业"与"大风天允许继续高处作业"的矛盾。
+
+---
+
+## 3. 逻辑矛盾 — 环保措施自相矛盾
+
+```
+第五章 环境保护
+
+5.1 水体保护
+施工期间严禁将泥浆、废水直接排入河道,所有废水须经沉淀处理达标后排放。
+
+5.2 施工排水
+钻孔桩施工产生的泥浆水通过排水沟直接引入附近河流,以保障施工场地干燥。
+```
+
+**预期结果**:应识别出"严禁排入河道"与"直接引入河流"的逻辑矛盾。
+
+---
+
+## 4. 因果关系错误 — 晴天导致裂缝
+
+```
+第六章 质量通病防治
+
+6.1 混凝土裂缝原因分析
+由于近期天气持续晴朗,日照充足,导致本标段混凝土出现了多处表面裂缝。
+
+6.2 处理措施
+针对上述原因,采取增加遮阳棚措施,避免阳光直射混凝土表面。
+```
+
+**预期结果**:应识别出"天气晴朗导致混凝土裂缝"的因果错误(正常日照不会导致裂缝,可能是养护不当、温差过大等原因)。
+
+---
+
+## 5. 因果关系错误 — 开会导致进度滞后
+
+```
+第七章 进度管理
+
+7.1 进度滞后原因
+本月施工进度较计划滞后15%,主要原因是项目经理每日坚持召开晨会,导致作业人员工时不足。
+
+7.2 纠偏措施
+取消每日晨会制度,改为每周召开一次例会,以保障现场有效作业时间。
+```
+
+**预期结果**:应识别出"晨会导致进度滞后15%"的因果不合理(晨会耗时通常不会导致如此大幅进度滞后)。
+
+---
+
+## 6. 因果关系错误 — 安全措施导致事故
+
+```
+第八章 安全管理
+
+8.1 事故原因分析
+本次基坑坍塌事故的主要原因是施工现场设置了完善的安全警示标志和围挡设施,导致土方应力集中。
+
+8.2 整改方案
+拆除部分围挡设施,减少应力集中风险。
+```
+
+**预期结果**:应识别出"安全围挡导致基坑坍塌"的因果荒谬(围挡不会导致土方应力集中)。
+
+---
+
+## 7. 条件与结论不匹配 — 低温正常浇筑
+
+```
+第九章 冬季施工措施
+
+9.1 混凝土冬季施工
+当环境温度低于0℃时,混凝土可正常浇筑,无需采取保温措施,因为低温有利于水泥水化反应加速。
+
+9.2 养护要求
+浇筑完成后保持自然养护即可,养护期不少于3天。
+```
+
+**预期结果**:应识别出"0℃以下正常浇筑混凝土"违背工程常识。
+
+---
+
+## 8. 条件与结论不匹配 — 居民区近距离爆破
+
+```
+第十章 爆破作业
+
+10.1 爆破参数
+本工程采用浅孔爆破法,孔深1.5m,单孔装药量5kg。在居民区50m范围内进行爆破时,无需设置警戒区和撤离居民,因为药量较小,不会产生飞石和震动危害。
+
+10.2 安全说明
+爆破作业由持证人员操作即可,无需额外安全措施。
+```
+
+**预期结果**:应识别出"50m内爆破无需警戒"违背安全常识。
+
+---
+
+## 9. 条件与结论不匹配 — 6级风爬模作业
+
+```
+第十一章 高墩施工
+
+11.1 爬模施工规定
+当风力达到6级以上时,爬模平台仍可正常提升作业,因为爬模结构本身具有足够的抗风稳定性,无需停止作业。
+
+11.2 人员要求
+作业人员只需佩戴普通安全帽,无需系挂安全带。
+```
+
+**预期结果**:应识别出"6级风继续爬模作业"和"高墩作业不系安全带"均违背安全规范。
+
+---
+
+## 10. 口语化表达
+
+```
+第十二章 施工组织
+
+12.1 人员安排
+这事儿吧,咱们就这么干,问题不大。班组那边我打过招呼了,他们心里有数,到时候看着办就行。
+
+12.2 材料进场
+钢筋水泥这些东西差不多到了,咱们赶紧使唤,别磨蹭,争取早点弄完收工。
+```
+
+**预期结果**:应识别出口语化表达不符合施工方案严肃规范的要求。
+
+---
+
+## 11. 口语化表达 — 技术交底
+
+```
+第十三章 技术交底
+
+13.1 施工要点
+这个墩子啊,就这么往上浇,注意别漏振,其他的没啥大不了的,大家都干过好几年了,门儿清。
+
+13.2 安全提醒
+到时候多看着点,别出岔子,出了事谁也跑不了,悠着点儿干。
+```
+
+**预期结果**:应识别出"没啥大不了的""门儿清""悠着点儿"等口语化表达。
+
+---
+
+## 12. 混合问题 — 一份文档多种逻辑错误
+
+```
+第一章 编制依据及原则
+
+1.1 编制依据
+本方案严格依据《公路桥涵施工技术规范》JTG/T 3650-2020 编制,所有施工工艺和技术参数均符合现行规范要求。
+
+第二章 工程概况
+
+2.1 工程简介
+本项目为跨江特大桥,主跨280m,采用悬臂浇筑法施工。桥梁设计使用年限100年,设计荷载公路-I级。
+
+第三章 总体施工方案
+
+3.1 施工顺序
+本桥采用从两端向中间同时悬臂浇筑的施工顺序。0#块施工完成后,对称向两侧悬浇,最后在跨中合龙。
+
+3.2 合龙段施工
+合龙温度应选择在当日最低气温时进行,以确保合龙精度。若气温偏高,可通过洒水降温的方式强制降低合龙段温度。
+
+第四章 安全管理
+
+4.1 高空作业
+挂篮平台上作业人员必须全程系挂安全带,安全带应固定在专用锚固点上。平台四周设置防护栏杆和踢脚板。
+
+4.2 夜间施工
+夜间挂篮行走和混凝土浇筑作业时,只需保证平台照明充足即可,作业人员疲劳程度对安全影响不大,无需限制连续作业时间。
+
+第五章 质量保证
+
+5.1 预应力张拉
+预应力钢束张拉应在混凝土强度达到设计强度的85%以上时进行。张拉顺序严格按照设计图纸执行,采用两端对称张拉。
+
+5.2 压浆施工
+孔道压浆应在张拉完成后24小时内进行。压浆材料采用纯水泥浆,水灰比0.35,不掺加减水剂和膨胀剂。
+
+第六章 进度计划
+
+6.1 工期安排
+总工期18个月,其中悬臂浇筑阶段12个月。考虑到雨季影响,悬浇阶段可适当延长,总工期仍可按18个月控制。
+
+6.2 资源投入
+现场配备挂篮4套,塔吊2台,混凝土泵车1台。班组人员按正常班次配置,这事儿差不多就行,到时候看着加人。
+```
+
+**预期结果**:应同时识别出多种问题:
+- **条件结论不匹配**:洒水降温合龙、延长关键工期却保持总工期不变
+- **逻辑矛盾**:前文强调安全,后文忽视疲劳作业风险
+- **口语化**:"这事儿差不多就行,到时候看着加人"
+
+---
+
+## 13. 无明显问题的正常内容(对照组)
+
+```
+第三章 质量保证措施
+
+3.1 质量管理体系
+建立健全质量管理组织机构,实行项目经理负责制,配备专职质检工程师。
+
+3.2 材料质量控制
+所有进场材料必须具有出厂合格证,并按规范要求进行抽样检验,合格后方可使用。
+
+3.3 施工过程控制
+每道工序施工前进行技术交底,施工中严格执行三检制,上道工序不合格不得进入下道工序。
+```
+
+**预期结果**:应返回"无明显问题"。
+
+---
+
+## 14. 空内容(边界测试)
+
+```
+
+```
+
+**预期结果**:应正常返回,不抛异常。