Эх сурвалжийг харах

v0.0.5-功能优化-专业性审查
- 优化专业性审查父块召回逻辑
- 优化专业性审查提示词

WangXuMing 2 сар өмнө
parent
commit
ec5adf9664

+ 1 - 0
check_data.txt

@@ -0,0 +1 @@
+window.professionalReviewData.review_results[0]

+ 120 - 43
core/construction_review/component/infrastructure/parent_tool.py

@@ -126,20 +126,24 @@ def enhance_with_parent_docs_grouped(
     max_parent_text_length: int = 8000
 ) -> Dict[str, Any]:
     """
-    分组增强 + 按分数筛选 (每个查询对独立处理)
+    分组增强 + 按分数筛选 + 智能扩展(每个查询对独立处理)
 
     核心逻辑:
     1. 每个查询对独立处理,按 bfp_rerank_score 筛选
     2. 只保留并增强高分结果,低分查询对直接跳过
     3. 用父ID从Milvus召回父文档内容
-    4. 将父文档内容拼接到高分结果后(长度受限)
+    4. 根据子块在父文档中的位置进行智能扩展:
+       - Top1: 前后总共扩展 3000 字
+       - Top2: 前后总共扩展 1500 字
+       - Top3: 前后总共扩展 1500 字
+    5. 如果总长度超过 12000 字,按比例压缩所有父块
 
     Args:
         milvus_manager: MilvusManager 实例
         bfp_result_lists: 检索结果列表 (二维,每个子列表对应一个查询对)
         score_threshold: bfp_rerank_score 最低阈值,低于此分数直接跳过 (默认0.5)
-        max_parents_per_pair: 每个查询对最多选取的父文档数量 (默认3个)
-        max_parent_text_length: 单个父文档最大长度限制,单位字符 (默认8000,约5300 tokens)
+        max_parents_per_pair: 每个查询对最多选取的父文档数量 (默认3个,已硬编码为Top3)
+        max_parent_text_length: 单个父文档最大长度限制(已弃用,改为智能扩展)
 
     Returns:
         Dict: 增强结果,包含:
@@ -160,7 +164,7 @@ def enhance_with_parent_docs_grouped(
         if not result_list:
             continue
 
-        # 1. 按分数排序并筛选高分结果
+        # 1. 按分数排序并筛选高分结果片段 
         sorted_results = sorted(result_list, key=lambda x: x.get('bfp_rerank_score', 0), reverse=True)
         high_score_results = [r for r in sorted_results if r.get('bfp_rerank_score', 0) >= score_threshold]
 
@@ -178,71 +182,143 @@ def enhance_with_parent_docs_grouped(
         # 从第一个结果中提取 source_entity(用于日志)
         source_entity = high_score_results[0].get('source_entity', '') if high_score_results else ''
 
-        # 限制父文档数量
-        parent_ids = parent_ids[:max_parents_per_pair]
-        logger.debug(f"实体:{source_entity},[分组增强] 父文档数量限制为 {max_parents_per_pair},实际数量为 {len(parent_ids)},{parent_ids}")
-        # 3. 用 parent_id 召回片段并按 pk 排序拼接
+        # 限制父文档数量为 top3
+        parent_ids = parent_ids[:3]
+        logger.debug(f"实体:{source_entity},[分组增强] 父文档数量限制为 3,实际数量为 {len(parent_ids)},{parent_ids}")
+
+        # 3. 用 parent_id 召回片段并按 pk 排序拼接(优化:根据子块位置扩展)
         parent_id_to_doc = {}
-        for pid in parent_ids:
+        parent_id_to_metadata = {}  # 存储元数据
+
+        # 定义扩展策略:top1=3000字,top2=1500字,top3=1500字
+        expansion_limits = [3000, 1500, 1500]
+
+        for idx, pid in enumerate(parent_ids):
             # 用 parent_id 查询 Milvus,召回该父文档的所有片段
             chunks = fetch_parent_chunks_by_parent_id(milvus_manager, pid)
 
             if chunks:
                 # 按 pk 排序
                 sorted_chunks = sorted(chunks, key=lambda x: x.get('pk', 0))
-                logger.debug(f"排序结果:{pid}:{sorted_chunks}")
+                logger.debug(f"排序结果:{pid}:{[c.get('pk') for c in sorted_chunks]}")
+
                 # 提取元数据(从第一个片段获取)
                 first_chunk = sorted_chunks[0]
                 file_name = first_chunk.get('file_name', '')
                 title = first_chunk.get('title', '')
 
-                # 构建头部信息
-                header = f"【文件】{file_name}\n【标题】{title}\n" if file_name or title else ""
+                # 找到高分子块在父文档中的位置
+                # 从 high_score_results 中找到对应 parent_id 的子块
+                matching_result = next(
+                    (r for r in high_score_results if str(r.get('metadata', {}).get('parent_id')) == str(pid)),
+                    None
+                )
 
-                # 拼接所有片段的 text_content
-                full_text = "\n".join([c.get('text', '') for c in sorted_chunks])
+                if matching_result:
+                    # 获取子块的 text_content(用于定位)
+                    child_text = matching_result.get('text_content', '')
 
-                # 限制父文档长度(从后往前截断,保留尾部内容)
-                if len(full_text) > max_parent_text_length:
-                    # 添加截断提示
-                    truncated_text = f"...[内容过长,已截断,保留尾部{max_parent_text_length}字符]...\n{full_text[-max_parent_text_length:]}"
-                    logger.warning(f"[分组增强] parent_id={str(pid)[:8]}... 文本过长 ({len(full_text)}字符),截断至 {max_parent_text_length} 字符")
-                    combined_text = header + truncated_text
-                else:
-                    combined_text = header + full_text
+                    # 拼接所有片段为完整父文档
+                    full_text = "\n".join([c.get('text', '') for c in sorted_chunks])
+
+                    # 在父文档中找到子块的位置
+                    child_position = full_text.find(child_text)
 
-                parent_id_to_doc[str(pid)] = combined_text
-                logger.debug(f"[分组增强] parent_id={str(pid)[:8]}... 召回并拼接了 {len(sorted_chunks)} 个片段,文件={file_name},最终长度={len(combined_text)}字符")
+                    if child_position >= 0:
+                        # 计算子块中心位置
+                        child_center = child_position + len(child_text) // 2
+
+                        # 根据 top 级别确定扩展长度
+                        expansion_limit = expansion_limits[idx] if idx < len(expansion_limits) else 1500
+                        half_expansion = expansion_limit // 2
+
+                        # 计算扩展范围
+                        start_pos = max(0, child_center - half_expansion)
+                        end_pos = min(len(full_text), child_center + half_expansion)
+
+                        # 提取扩展后的文本
+                        expanded_text = full_text[start_pos:end_pos]
+
+                        # 添加截断提示
+                        if start_pos > 0:
+                            expanded_text = f"...[前文已省略]...\n{expanded_text}"
+                        if end_pos < len(full_text):
+                            expanded_text = f"{expanded_text}\n...[后文已省略]..."
+
+                        logger.info(f"[分组增强] Top{idx+1} parent_id={str(pid)[:8]}... 子块中心位置={child_center}, 扩展范围=[{start_pos}, {end_pos}], 扩展长度={len(expanded_text)}字符")
+                    else:
+                        # 子块未找到,回退到完整父文档
+                        expanded_text = full_text
+                        logger.warning(f"[分组增强] parent_id={str(pid)[:8]}... 子块未在父文档中找到,使用完整父文档")
+                else:
+                    # 没有匹配的高分结果,回退到完整父文档
+                    expanded_text = "\n".join([c.get('text', '') for c in sorted_chunks])
+                    logger.warning(f"[分组增强] parent_id={str(pid)[:8]}... 未找到匹配的高分结果,使用完整父文档")
+
+                # 存储扩展后的文本和元数据
+                parent_id_to_doc[str(pid)] = expanded_text
+                parent_id_to_metadata[str(pid)] = {
+                    'file_name': file_name,
+                    'title': title,
+                    'top_rank': idx + 1,  # Top1/Top2/Top3
+                    'original_length': len("\n".join([c.get('text', '') for c in sorted_chunks])),
+                    'expanded_length': len(expanded_text)
+                }
+                logger.debug(f"[分组增强] Top{idx+1} parent_id={str(pid)[:8]}... 召回了 {len(sorted_chunks)} 个片段,文件={file_name},扩展后长度={len(expanded_text)}字符")
 
         if not parent_id_to_doc:
             logger.warning(f"[分组增强] 查询对 {pair_idx}: 所有父文档召回失败,跳过")
             continue
 
-        # 4. 只保留并增强高分结果(每个结果只用其对应的父文档增强)
+        # 4. 检查总长度并按比例压缩(如果超过 12000 字)
+        total_length = sum(len(text) for text in parent_id_to_doc.values())
+        max_total_length = 12000
+
+        if total_length > max_total_length:
+            logger.warning(f"[分组增强] 查询对 {pair_idx}: 总长度 {total_length} 字符超过限制 {max_total_length},开始按比例压缩")
+
+            # 计算压缩比例
+            compression_ratio = max_total_length / total_length
+
+            # 按比例压缩每个父文档
+            for pid, text in parent_id_to_doc.items():
+                original_length = len(text)
+                target_length = int(original_length * compression_ratio)
+
+                if target_length < original_length:
+                    # 从中间截取,保留核心内容
+                    start_cut = (original_length - target_length) // 2
+                    compressed_text = text[start_cut:start_cut + target_length]
+
+                    # 添加压缩提示
+                    compressed_text = f"...[已按比例压缩{compression_ratio:.2%}]...\n{compressed_text}\n...[已压缩]..."
+                    parent_id_to_doc[pid] = compressed_text
+
+                    logger.info(f"[分组增强] parent_id={str(pid)[:8]}... 压缩前={original_length}字符,压缩后={len(compressed_text)}字符")
+
+            # 重新计算总长度
+            total_length_after = sum(len(text) for text in parent_id_to_doc.values())
+            logger.info(f"[分组增强] 查询对 {pair_idx}: 压缩完成,总长度从 {total_length} 压缩至 {total_length_after} 字符")
+
+        # 5. 只保留并增强高分结果(为每个结果添加头部信息和父文档)
         enhanced_list = []
-        max_total_length = 12000  # 总长度限制(约8000 tokens),为prompt等留出空间
 
         for result in high_score_results:
             parent_id = str(result.get('metadata', {}).get('parent_id'))
             if parent_id in parent_id_to_doc:
-                # 用该结果对应的父文档增强
+                # 获取父文档和元数据
                 parent_text = parent_id_to_doc[parent_id]
-                original_text = result.get('text_content', '')
+                metadata = parent_id_to_metadata.get(parent_id, {})
+                file_name = metadata.get('file_name', '')
+                title = metadata.get('title', '')
+                top_rank = metadata.get('top_rank', 0)
 
-                # 检查总长度
-                total_length = len(original_text) + len(parent_text)
-                if total_length > max_total_length:
-                    # 进一步截断父文档
-                    available_length = max_total_length - len(original_text) - 50  # 留50字符缓冲
-                    if available_length > 500:  # 至少保留500字符
-                        parent_text = parent_text[:available_length] + f"\n...[总长度超限,截断至{available_length}字符]..."
-                        logger.warning(f"[分组增强] 查询对 {pair_idx}: 总长度过长 ({total_length}字符),进一步截断父文档至 {available_length} 字符")
-                    else:
-                        # 父文档太短,不添加父文档
-                        logger.warning(f"[分组增强] 查询对 {pair_idx}: 父文档无法截断(可用空间仅{available_length}字符),跳过父文档增强")
-                        parent_text = ""
+                # 构建头部信息
+                header = f"【Top{top_rank} 参考文档】\n【文件】{file_name}\n【标题】{title}\n" if file_name or title else f"【Top{top_rank} 参考文档】\n"
 
-                enhanced_content = original_text + f"\n【参考文档】\n{parent_text}\n" if parent_text else original_text
+                # 拼接增强内容
+                original_text = result.get('text_content', '')
+                enhanced_content = original_text + f"\n{header}{parent_text}\n"
 
                 enhanced_list.append({
                     'text_content': enhanced_content,
@@ -252,7 +328,8 @@ def enhance_with_parent_docs_grouped(
                     'bfp_rerank_score': result.get('bfp_rerank_score'),
                     'bfp_rerank_parent_id': result.get('bfp_rerank_parent_id', ''),
                     'source_entity': result.get('source_entity', ''),
-                    'enhanced': True if parent_text else False
+                    'enhanced': True,
+                    'parent_top_rank': top_rank  # 新增:记录父文档的 Top 排名
                 })
 
         if enhanced_list:

+ 41 - 72
core/construction_review/component/reviewers/prompt/technical_reviewers.yaml

@@ -3,46 +3,31 @@
 # 非参数合规性检查功能 - 审查 安全相关/强制性条文相关知识库
 non_parameter_compliance_check:
   system_prompt: |
-    # 角色: 
     你是施工方案非参数合规性审查专家,专门负责检查安全相关条文和非参数的强制性标准的符合性。
 
-    ## 审查参考:
-    {review_references}
-
-    ## 审查要求:
-    - 重点检查安全相关非参数性的条文内容
-    - 识别违反安全标准和强制规范的问题
-    - 关注安全防护措施、安全风险评估、安全管理要求
-    - 简明扼要指出违规内容和整改要求
-    - 风险等级分类:
-      * 高风险: 影响审查结论、可能导致法律问题或严重安全隐患
-      * 中风险: 影响专业表达、可能导致理解偏差或一般性问题
-      * 低风险: 形式问题、不影响实质内容和安全
-
   user_prompt_template: |
-    请审查以下内容的安全合规性和非参数性内容(不含数字):
-
+    ## 审查规则
+    1. **核心原则**:仅基于【参考依据】进行审查,若参考依据无法回答待审查问题或相关性过低,必须输出"无明显问题"
+    2. **审查重点**:检查安全相关非参数性条文、安全防护措施、安全风险评估、安全管理要求
+    3. **禁止事项**:
+       - 禁止编造审查依据或引用【参考依据】以外的信息
+       - 禁止对表格制表符进行检查
+       - 禁止曲解术语概念
+       - 禁止对同一内容重复检查
+    4. **风险分级**:
+       - 高风险:影响审查结论、可能导致法律问题或严重安全隐患
+       - 中风险:影响专业表达、可能导致理解偏差或一般性问题
+       - 低风险:形式问题、不影响实质内容和安全
+
+    ## 参考依据
+    {review_references}
 
-    ## 待审查内容:
-    {review_content}  
+    ## 待审查问题
+    {review_content}
 
-    ## 强制标准:
-    1. 审查结果必须基于<审查参考>,如果审查参考不足以对审查内容提供参考时,不予检查,输出无明显问题,不得自行编造审查依据
-    2. 审查依据必须使用<审查参考>中的文件信息不得引用其他外部信息,如果无文件信息,不予检查,输出无明显问题
-    3. 对于表格制表符不需要检查
-    4. 对于术语概念不得曲解
-    5. 没有明显参数问题的内容不予检查,输出无明显问题
-    6. 已检查出的问题项仅输出一次检查结果,禁止对同一内容重复检查
-    7. 若审查参考与审查内容相关性过低,不予检查,输出无明显问题
-    8. 务必注意,只有在审查参考与审查内容相关时才能依据审查参考的内容进行问题检查,否则输出无明显问题
-    
-    ## rules:
-    - 输出格式:务必须严格按照以下标准JSON格式输出审查结果:
-    - 如果未发现问题,请输出:无明显问题
-    - 如果发现问题,请按以下格式输出:
-    - location字段直接输出原字段内容,不得猜测
-    - 务必遵循<强制标准>进行审查
-    - 尤其要注意<强制标准>中的第1条
+    ## 输出结构
+    - 如果【参考依据】无法回答【待审查问题】或相关性过低,输出:无明显问题
+    - 如果发现问题,严格按以下JSON格式输出(location字段直接输出原文,不得猜测):
     ```json
     {{
       "issue_point": "问题标题描述",
@@ -57,48 +42,32 @@ non_parameter_compliance_check:
 # 参数合规性检查功能 - 审查 实体概念/工程术语相关知识库
 parameter_compliance_check:
   system_prompt: |
-    # 角色: 
     你是施工方案参数合规性审查专家,专门负责检查技术参数、实体概念和工程术语的准确性和合理性。
 
-    ## 审查参考:
-    {review_references}
-
-    ## 审查要求:
-    - 重点检查技术参数的准确性和合理性
-    - 识别参数错误或不合理设置
-    - 检查实体概念和工程术语的参数是否正确使用
-    - 验证设计值与标准的符合性
-    - 简明扼要指出参数问题和修正建议
-    - 风险等级分类:
-      * 高风险: 影响审查结论、可能导致法律问题或严重安全隐患
-      * 中风险: 影响专业表达、可能导致理解偏差或一般性问题
-      * 低风险: 形式问题、不影响实质内容和安全
-
-
-
   user_prompt_template: |
-    请审查以下内容的技术参数精确性、实体概念和工程术语的正确性:
-
-    ## 待审查内容:
-    {review_content}  
+    ## 审查规则
+    1. **核心原则**:仅基于【参考依据】进行审查,若参考依据无法回答待审查问题或相关性过低,必须输出"无明显问题"
+    2. **审查重点**:检查技术参数的准确性和合理性、实体概念和工程术语的参数正确性、设计值与标准的符合性
+    3. **禁止事项**:
+       - 禁止编造审查依据或引用【参考依据】以外的信息
+       - 禁止对表格制表符进行检查
+       - 禁止曲解术语概念
+       - 禁止对同一内容重复检查
+       - 禁止检查无明显参数问题的内容
+    4. **风险分级**:
+       - 高风险:影响审查结论、可能导致法律问题或严重安全隐患
+       - 中风险:影响专业表达、可能导致理解偏差或一般性问题
+       - 低风险:形式问题、不影响实质内容和安全
+
+    ## 参考依据
+    {review_references}
 
-    ## 强制标准:
-    1. 审查结果必须基于<审查参考>,如果审查参考不足以对审查内容提供参考时,不予检查,输出无明显问题,不得自行编造审查依据
-    2. 审查依据必须使用<审查参考>中的文件信息不得引用其他外部信息,如果无文件信息,不予检查,输出无明显问题
-    3. 对于表格制表符不需要检查
-    4. 对于术语概念不得曲解
-    5. 没有明显参数问题的内容不予检查,输出无明显问题
-    6. 已检查出的问题项仅输出一次检查结果,禁止对同一内容重复检查
-    7. 若审查参考与审查内容相关性过低,不予检查,输出无明显问题
-    8. 务必注意,只有在审查参考与审查内容相关时才能依据审查参考的内容进行问题检查,否则输出无明显问题
+    ## 待审查问题
+    {review_content}
 
-    ## rules:
-    - 输出格式:务必须严格按照以下标准JSON格式输出审查结果:
-    - 如果未发现问题,请输出:无明显问题
-    - 如果发现问题,请按以下格式输出:
-    - location字段直接输出原字段内容,不得猜测
-    - 务必遵循<强制标准>进行审查
-    - 尤其要注意<强制标准>中的第1条
+    ## 输出结构
+    - 如果【参考依据】无法回答【待审查问题】或相关性过低,输出:无明显问题
+    - 如果发现问题,严格按以下JSON格式输出(location字段直接输出原文,不得猜测):
     ```json
     {{
       "issue_point": "问题标题描述",

+ 4 - 2
foundation/ai/rag/retrieval/query_rewrite.py

@@ -66,7 +66,8 @@ class QueryRewriteManager():
                         self.generate_model_client.get_model_generate_invoke(
                             trace_id=trace_id,
                             task_prompt_info=task_prompt_info,
-                            timeout=60
+                            timeout=60,
+                            model_name="qwen3_30b"  # 修复: 使用正确的模型名称(下划线)
                         )
                     )
                     model_response = future.result()
@@ -75,7 +76,8 @@ class QueryRewriteManager():
                 model_response = asyncio.run(self.generate_model_client.get_model_generate_invoke(
                     trace_id=trace_id,
                     task_prompt_info=task_prompt_info,
-                    timeout=60
+                    timeout=60,
+                    model_name="qwen3_30b"  # 修复: 使用正确的模型名称(下划线)
                 ))
 
             # 格式化模型响应

+ 256 - 0
utils_test/RAG_Test/rag_pipeline_web/PROFESSIONAL_REVIEW_GUIDE.md

@@ -0,0 +1,256 @@
+# 专业性审查测试功能使用指南
+
+## 📋 功能概述
+
+在原有的RAG管道测试工具基础上,新增了**专业性审查完整测试**功能,能够测试从RAG召回到AI审查的完整链路。
+
+## 🎯 功能特性
+
+### 1. **完整的测试链路**
+- ✅ **Step 1: RAG召回** - 查询提取 → 实体增强检索 → 父文档增强 → 结果筛选
+- ✅ **Step 2: 专业性审查** - 非参数合规性检查 + 参数合规性检查
+
+### 2. **灵活的审查类型**
+- **全部审查** - 同时执行非参数和参数审查
+- **非参数审查** - 仅检查安全相关/强制性条文
+- **参数审查** - 仅检查技术参数/实体概念/工程术语
+
+### 3. **详细的结果展示**
+- RAG召回摘要(查询对数量、召回实体数量、审查实体数量)
+- 每个实体的审查结果(包含RAG得分、查询条文、参考来源)
+- 审查问题JSON格式展示(issue_point、location、suggestion、reason、risk_level)
+
+## 🚀 使用方法
+
+### 1. **启动服务**
+
+```bash
+cd D:\wx_work\sichuan_luqiao\LQAgentPlatform\utils_test\RAG_Test\rag_pipeline_web
+python rag_pipeline_server.py
+```
+
+### 2. **访问界面**
+
+浏览器打开:`http://localhost:8765`
+
+### 3. **切换到专业性审查Tab**
+
+点击顶部的 **🎯 专业性审查测试** 标签
+
+### 4. **输入待审查内容**
+
+在文本框中输入待审查的内容,例如:
+
+```
+二)架桥机安装施工
+1、安装准备
+(1)图纸审核
+1)为了使施工人员充分领会设计意图,熟悉设计内容...
+```
+
+### 5. **选择审查类型**
+
+- ⭕ 全部审查(默认)
+- ⭕ 非参数审查
+- ⭕ 参数审查
+
+### 6. **执行审查**
+
+点击 **▶ 执行专业性审查** 按钮
+
+### 7. **查看结果**
+
+结果面板会显示:
+- 📊 RAG召回摘要卡片
+- 📋 每个实体的详细审查结果
+
+## 🔧 技术实现
+
+### 后端API
+
+新增API端点:`POST /api/professional_review`
+
+**请求参数:**
+```json
+{
+  "content": "待审查内容",
+  "check_type": "both"  // "non_parameter", "parameter", "both"
+}
+```
+
+**响应格式:**
+```json
+{
+  "status": "success",
+  "trace_id": "professional_review_1706428800000",
+  "check_type": "both",
+  "rag_summary": {
+    "query_pairs_count": 3,
+    "entity_results_count": 2,
+    "query_pairs": [...],
+    "entity_results": [...]
+  },
+  "review_results": [
+    {
+      "entity": "架桥机",
+      "combined_query": "架桥机 安装施工 图纸审核",
+      "reference_source": "架桥机使用说明书.md",
+      "rag_score": 0.8234,
+      "non_parameter_result": { ... },  // JSON或"无明显问题"
+      "parameter_result": { ... }
+    }
+  ],
+  "total_entities_reviewed": 2
+}
+```
+
+### 前端文件
+
+**新增文件:**
+- `professional_review.js` - 专业性审查逻辑
+
+**修改文件:**
+- `index.html` - 添加专业性审查界面
+- `styles.css` - 添加样式(功能Tab、审查区域、结果展示)
+
+### 核心流程
+
+```
+用户输入
+   ↓
+RAG召回(查询提取 → 实体检索 → 父文档增强)
+   ↓
+对每个召回的实体执行AI审查
+   ├─ 非参数审查(使用优化后的提示词)
+   └─ 参数审查(使用优化后的提示词)
+   ↓
+结果汇总展示
+```
+
+## 📊 结果解读
+
+### RAG得分
+- **>0.5** - 高相关性,建议审查
+- **0.3-0.5** - 中等相关性
+- **<0.3** - 低相关性,已过滤
+
+### 审查结果状态
+
+| 状态标签 | 含义 | 颜色 |
+|---------|------|------|
+| ✅ 无明显问题 | 参考依据无法回答或相关性低 | 绿色 |
+| ⚠️ 发现问题 | 检测到合规性问题 | 橙色 |
+| ❌ 执行失败 | 审查过程出错 | 红色 |
+| ⏸ 未执行 | 该类型审查未启用 | 灰色 |
+
+### 问题JSON字段
+
+```json
+{
+  "issue_point": "问题标题",
+  "location": "问题位置(原文+页码)",
+  "suggestion": "修改建议",
+  "reason": "问题原因和依据",
+  "risk_level": "高风险/中风险/低风险"
+}
+```
+
+## 🎨 界面说明
+
+### 功能Tab
+- 🔍 **RAG召回测试** - 原有的RAG链路测试
+- 🎯 **专业性审查测试** - 新增的完整审查测试
+
+### 输入区域
+- 文本框:输入待审查内容
+- 单选框:选择审查类型
+- 按钮:执行审查 / 清空
+
+### 结果展示
+- **RAG召回摘要** - 3个汇总卡片
+- **审查结果详情** - 每个实体的详细结果
+
+## 🔍 调试建议
+
+### 查看完整数据
+在浏览器控制台查看:
+```javascript
+console.log(window.professionalReviewData)
+```
+
+### 查看RAG召回详情
+```javascript
+console.log(window.professionalReviewData.rag_summary)
+```
+
+### 查看某个实体的审查结果
+```javascript
+console.log(window.professionalReviewData.review_results[0])
+```
+
+## 📝 示例测试用例
+
+### 测试用例1:正常审查
+**输入:**
+```
+主要部件说明
+1、主梁总成
+主梁总成由主梁和导梁构成。主梁单节长12m,共7节,每节重10.87t
+```
+
+**预期结果:**
+- 召回相关的架桥机文档
+- 执行参数和非参数审查
+- 显示具体的审查意见
+
+### 测试用例2:无相关内容
+**输入:**
+```
+这是一段无关的测试内容
+```
+
+**预期结果:**
+- RAG召回为空或低分
+- 返回"无明显问题"
+
+## ⚠️ 注意事项
+
+1. **服务器必须启动** - 确保后端服务运行在 `http://localhost:8765`
+2. **Milvus必须就绪** - 确保向量数据库已正确初始化
+3. **AI模型可用** - 确保 `qwen3_30b` 模型可用
+4. **网络稳定** - 审查过程可能需要较长时间(30-60秒)
+
+## 🐛 常见问题
+
+### Q1: 点击按钮没反应?
+**A:** 检查服务器状态指示器,确保显示"服务正常"
+
+### Q2: 审查一直加载?
+**A:** 查看浏览器控制台,可能是后端超时或错误
+
+### Q3: 结果显示"执行失败"?
+**A:** 检查后端日志,可能是模型调用失败或参数错误
+
+### Q4: RAG召回为空?
+**A:** 可能内容与知识库不相关,尝试输入更具体的施工相关内容
+
+## 📚 相关文件
+
+- **后端**: `rag_pipeline_server.py`
+- **前端HTML**: `index.html`
+- **前端JS**: `professional_review.js`, `app.js`
+- **前端CSS**: `styles.css`
+- **提示词**: `core/construction_review/component/reviewers/prompt/technical_reviewers.yaml`
+
+## 🎯 下一步优化
+
+- [ ] 添加导出结果功能(JSON/Excel)
+- [ ] 支持批量测试(多个内容片段)
+- [ ] 添加历史记录查询
+- [ ] 支持自定义提示词测试
+- [ ] 添加性能分析图表
+
+---
+
+**最后更新时间**: 2026-01-28
+**版本**: v1.0.0

+ 26 - 13
utils_test/RAG_Test/rag_pipeline_web/app.js

@@ -85,40 +85,52 @@ function checkServerStatus() {
  */
 function updateServerStatus(connected, milvusReady) {
     const statusEl = document.getElementById('serverStatus');
+    if (!statusEl) return;
+
     const dot = statusEl.querySelector('.status-dot');
     const text = statusEl.querySelector('.status-text');
-    
+
     dot.className = 'status-dot ' + (connected ? (milvusReady ? 'online' : 'warning') : 'offline');
-    
+
     if (connected) {
         text.textContent = milvusReady ? '服务已连接 (Milvus就绪)' : '服务已连接 (Milvus未就绪)';
     } else {
         text.textContent = '服务未连接 - 请启动 rag_pipeline_server.py';
     }
-    
-    document.getElementById('runRagBtn').disabled = !connected;
+
+    // 保存到全局变量供其他模块使用
+    window.serverConnected = serverConnected;
+    window.milvusReady = milvusReady;
+
+    const runRagBtn = document.getElementById('runRagBtn');
+    if (runRagBtn) {
+        runRagBtn.disabled = !connected;
+    }
 }
 
 /**
  * 执行RAG检索
  */
 function runRAG() {
-    const content = document.getElementById('ragInput').value.trim();
-    
+    const content = document.getElementById('testInput').value.trim();
+
     if (!content) {
         alert('请输入测试文本');
         return;
     }
-    
+
     if (!serverConnected) {
         alert('服务未连接,请先启动 rag_pipeline_server.py');
         return;
     }
-    
+
     // 显示加载状态
-    document.getElementById('loadingOverlay').style.display = 'flex';
+    const loadingOverlay = document.getElementById('loadingOverlay');
+    const loadingText = document.getElementById('loadingText');
+    loadingOverlay.style.display = 'flex';
+    loadingText.textContent = '正在执行RAG检索...';
     document.getElementById('runRagBtn').disabled = true;
-    
+
     fetch(`${API_BASE}/api/rag`, {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
@@ -130,13 +142,14 @@ function runRAG() {
     })
     .then(data => {
         pipelineData = data;
+        window.pipelineData = data; // 同时保存到全局
         renderPipeline(data);
-        document.getElementById('loadingOverlay').style.display = 'none';
+        loadingOverlay.style.display = 'none';
         document.getElementById('runRagBtn').disabled = false;
     })
     .catch(err => {
         alert(`RAG执行失败: ${err.message}`);
-        document.getElementById('loadingOverlay').style.display = 'none';
+        loadingOverlay.style.display = 'none';
         document.getElementById('runRagBtn').disabled = false;
     });
 }
@@ -272,7 +285,7 @@ function loadFromPath(path) {
 function clearData() {
     pipelineData = null;
     currentSelectedStep = null;
-    document.getElementById('ragInput').value = '';
+    document.getElementById('testInput').value = '';
     pipelineOverview.style.display = 'none';
     pipelineFlow.style.display = 'none';
     detailPanel.style.display = 'none';

+ 82 - 8
utils_test/RAG_Test/rag_pipeline_web/index.html

@@ -5,34 +5,74 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>RAG管道测试可视化工具</title>
     <link rel="stylesheet" href="styles.css">
+    <!-- Markdown渲染库 -->
+    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
+    <!-- LaTeX公式渲染库 -->
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
+    <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
 </head>
 <body>
     <div class="container">
         <header class="header">
             <h1>🔍 RAG管道测试可视化工具</h1>
-            <p class="subtitle">RAG链路各环节输入输出数据流转调试工具</p>
+            <p class="subtitle">RAG链路各环节输入输出数据流转调试工具 + 专业性审查完整测试</p>
         </header>
 
-        <!-- RAG测试输入区域 -->
-        <section class="rag-input-section">
-            <h2>🚀 RAG链路测试</h2>
+        <!-- 功能切换Tab -->
+        <section class="function-tabs">
+            <button class="function-tab active" data-function="rag" onclick="switchFunction('rag')">
+                🔍 RAG召回测试
+            </button>
+            <button class="function-tab" data-function="professional" onclick="switchFunction('professional')">
+                🎯 专业性审查测试
+            </button>
+        </section>
+
+        <!-- 统一的测试输入区域 -->
+        <section class="test-input-section">
+            <h2 id="sectionTitle">🚀 RAG链路测试</h2>
             <div class="server-status" id="serverStatus">
                 <span class="status-dot offline"></span>
                 <span class="status-text">服务未连接</span>
             </div>
             <div class="input-area">
-                <textarea id="ragInput" placeholder="输入测试文本,点击执行RAG检索...&#10;&#10;示例:主要部件说明&#10;1、主梁总成&#10;主梁总成由主梁和导梁构成。主梁单节长12m,共7节,每节重10.87t..."></textarea>
+                <textarea id="testInput" placeholder="输入测试文本,点击执行RAG检索...&#10;&#10;示例:主要部件说明&#10;1、主梁总成&#10;主梁总成由主梁和导梁构成。主梁单节长12m,共7节,每节重10.87t..."></textarea>
             </div>
+
+            <!-- 专业性审查类型选择器 -->
+            <div class="review-type-selector" id="reviewTypeSelector" style="display: none;">
+                <label>审查类型:</label>
+                <label class="radio-label">
+                    <input type="radio" name="checkType" value="both" checked><span>全部审查</span>
+                </label>
+                <label class="radio-label">
+                    <input type="radio" name="checkType" value="non_parameter"><span>非参数审查</span>
+                </label>
+                <label class="radio-label">
+                    <input type="radio" name="checkType" value="parameter"><span>参数审查</span>
+                </label>
+            </div>
+
+            <!-- 动作按钮 -->
             <div class="action-buttons">
+                <!-- RAG召回按钮 -->
                 <button class="btn btn-primary" id="runRagBtn" onclick="runRAG()">
                     <span class="btn-icon">▶</span> 执行RAG检索
                 </button>
-                <button class="btn btn-secondary" onclick="loadSampleData()">加载历史数据</button>
-                <button class="btn btn-secondary" onclick="clearData()">清空</button>
+
+                <!-- 专业性审查按钮 -->
+                <button class="btn btn-success" id="runProfessionalBtn" onclick="runProfessionalReview()" style="display: none;">
+                    <span class="btn-icon">▶</span> 执行专业性审查
+                </button>
+
+                <button class="btn btn-secondary" onclick="loadSampleData()" id="loadSampleBtn">加载历史数据</button>
+                <button class="btn btn-secondary" onclick="clearAllData()">清空</button>
             </div>
+
             <div class="loading-overlay" id="loadingOverlay" style="display: none;">
                 <div class="loading-spinner"></div>
-                <p>正在执行RAG检索...</p>
+                <p id="loadingText">正在执行RAG检索...</p>
             </div>
         </section>
 
@@ -109,8 +149,42 @@
                 <!-- 动态生成各阶段详情 -->
             </div>
         </section>
+
+        <!-- 专业性审查结果展示 -->
+        <section class="professional-results" id="professionalResults" style="display: none;">
+            <h2>📊 专业性审查结果</h2>
+
+            <!-- RAG召回摘要 -->
+            <div class="result-section">
+                <h3>🔍 RAG召回摘要</h3>
+                <div class="rag-summary" id="ragSummary">
+                    <!-- 动态生成 -->
+                </div>
+            </div>
+
+            <!-- 审查结果列表 -->
+            <div class="result-section">
+                <h3>📋 审查结果详情</h3>
+                <div class="review-results-list" id="reviewResultsList">
+                    <!-- 动态生成 -->
+                </div>
+            </div>
+        </section>
+    </div>
+
+    <!-- 审查依据侧边栏 -->
+    <div class="reference-sidebar" id="referenceSidebar">
+        <div class="sidebar-header">
+            <h3>📚 审查依据</h3>
+            <button class="sidebar-close-btn" onclick="closeReferenceSidebar()">✕</button>
+        </div>
+        <div class="sidebar-content" id="sidebarContent">
+            <!-- 动态加载Markdown内容 -->
+        </div>
     </div>
+    <div class="sidebar-overlay" id="sidebarOverlay" onclick="closeReferenceSidebar()"></div>
 
     <script src="app.js"></script>
+    <script src="professional_review.js"></script>
 </body>
 </html>

+ 507 - 0
utils_test/RAG_Test/rag_pipeline_web/professional_review.js

@@ -0,0 +1,507 @@
+/**
+ * 专业性审查功能模块
+ */
+
+// ==================== 功能切换 ====================
+
+/**
+ * 切换功能Tab
+ */
+function switchFunction(functionType) {
+    // 切换Tab激活状态
+    document.querySelectorAll('.function-tab').forEach(tab => {
+        tab.classList.remove('active');
+    });
+    document.querySelector(`[data-function="${functionType}"]`).classList.add('active');
+
+    // 获取元素
+    const sectionTitle = document.getElementById('sectionTitle');
+    const testInput = document.getElementById('testInput');
+    const reviewTypeSelector = document.getElementById('reviewTypeSelector');
+    const runRagBtn = document.getElementById('runRagBtn');
+    const runProfessionalBtn = document.getElementById('runProfessionalBtn');
+    const loadSampleBtn = document.getElementById('loadSampleBtn');
+
+    const pipelineOverview = document.getElementById('pipelineOverview');
+    const pipelineFlow = document.getElementById('pipelineFlow');
+    const detailPanel = document.getElementById('detailPanel');
+    const stagesDetail = document.getElementById('stagesDetail');
+    const professionalResults = document.getElementById('professionalResults');
+
+    if (functionType === 'rag') {
+        // 切换到RAG模式
+        sectionTitle.textContent = '🚀 RAG链路测试';
+        testInput.placeholder = '输入测试文本,点击执行RAG检索...\n\n示例:主要部件说明\n1、主梁总成\n主梁总成由主梁和导梁构成。主梁单节长12m,共7节,每节重10.87t...';
+
+        // 显示/隐藏按钮
+        runRagBtn.style.display = 'inline-block';
+        runProfessionalBtn.style.display = 'none';
+        loadSampleBtn.style.display = 'inline-block';
+        reviewTypeSelector.style.display = 'none';
+
+        // 隐藏专业性审查结果
+        professionalResults.style.display = 'none';
+
+        // 如果有RAG数据,显示RAG相关面板
+        if (window.pipelineData) {
+            pipelineOverview.style.display = 'block';
+            pipelineFlow.style.display = 'block';
+            detailPanel.style.display = 'block';
+            stagesDetail.style.display = 'block';
+        }
+    } else if (functionType === 'professional') {
+        // 切换到专业性审查模式
+        sectionTitle.textContent = '🎯 专业性审查完整测试';
+        testInput.placeholder = '输入待审查内容...\n\n示例:\n二)架桥机安装施工\n1、安装准备\n(1)图纸审核...';
+
+        // 显示/隐藏按钮
+        runRagBtn.style.display = 'none';
+        runProfessionalBtn.style.display = 'inline-block';
+        loadSampleBtn.style.display = 'none';
+        reviewTypeSelector.style.display = 'flex';
+
+        // 隐藏RAG相关面板
+        pipelineOverview.style.display = 'none';
+        pipelineFlow.style.display = 'none';
+        detailPanel.style.display = 'none';
+        stagesDetail.style.display = 'none';
+
+        // 如果有专业性审查数据,显示结果面板
+        if (window.professionalReviewData) {
+            professionalResults.style.display = 'block';
+        }
+    }
+}
+
+// ==================== 执行专业性审查 ====================
+
+/**
+ * 执行专业性审查
+ */
+async function runProfessionalReview() {
+    const content = document.getElementById('testInput').value.trim();
+
+    if (!content) {
+        alert('请输入待审查内容');
+        return;
+    }
+
+    if (!window.serverConnected) {
+        alert('服务器未连接,请检查服务是否启动');
+        return;
+    }
+
+    // 获取选中的审查类型
+    const checkType = document.querySelector('input[name="checkType"]:checked').value;
+
+    // 显示加载状态
+    const loadingOverlay = document.getElementById('loadingOverlay');
+    const loadingText = document.getElementById('loadingText');
+    const runBtn = document.getElementById('runProfessionalBtn');
+
+    loadingOverlay.style.display = 'flex';
+    loadingText.textContent = '正在执行专业性审查...';
+    runBtn.disabled = true;
+
+    try {
+        const API_BASE = window.API_BASE || 'http://localhost:8765';
+        const response = await fetch(`${API_BASE}/api/professional_review`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+                content: content,
+                check_type: checkType
+            })
+        });
+
+        if (!response.ok) {
+            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+        }
+
+        const data = await response.json();
+        console.log('📊 收到专业性审查响应:', data);
+
+        if (data.error) {
+            throw new Error(data.error);
+        }
+
+        // 保存数据到全局变量
+        window.professionalReviewData = data;
+
+        // 显示结果
+        renderProfessionalResults(data);
+
+        // 显示结果面板
+        document.getElementById('professionalResults').style.display = 'block';
+
+        console.log('✅ 专业性审查完成:', data);
+
+    } catch (error) {
+        console.error('❌ 专业性审查失败:', error);
+        alert(`专业性审查失败: ${error.message}`);
+    } finally {
+        loadingOverlay.style.display = 'none';
+        runBtn.disabled = false;
+    }
+}
+
+// ==================== 结果渲染 ====================
+
+/**
+ * 渲染专业性审查结果
+ */
+function renderProfessionalResults(data) {
+    // 渲染RAG召回摘要
+    renderRagSummary(data.rag_summary || {});
+
+    // 渲染审查结果列表
+    renderReviewResults(data.review_results || []);
+}
+
+/**
+ * 渲染RAG召回摘要
+ */
+function renderRagSummary(summary) {
+    const ragSummary = document.getElementById('ragSummary');
+
+    const queryCount = summary.query_pairs_count || 0;
+    const entityCount = summary.entity_results_count || 0;
+    const reviewedCount = window.professionalReviewData ? window.professionalReviewData.total_entities_reviewed || 0 : 0;
+
+    ragSummary.innerHTML = `
+        <div class="summary-card">
+            <div class="summary-card-title">查询对数量</div>
+            <div class="summary-card-value">${queryCount}</div>
+        </div>
+        <div class="summary-card">
+            <div class="summary-card-title">召回实体数量</div>
+            <div class="summary-card-value">${entityCount}</div>
+        </div>
+        <div class="summary-card">
+            <div class="summary-card-title">审查实体数量</div>
+            <div class="summary-card-value">${reviewedCount}</div>
+        </div>
+    `;
+}
+
+/**
+ * 渲染审查结果列表
+ */
+function renderReviewResults(results) {
+    const reviewResultsList = document.getElementById('reviewResultsList');
+
+    if (!results || results.length === 0) {
+        reviewResultsList.innerHTML = `
+            <div class="empty-state">
+                <div class="empty-state-icon">📭</div>
+                <div class="empty-state-text">暂无审查结果</div>
+                <div class="empty-state-hint">请检查RAG召回是否成功</div>
+            </div>
+        `;
+        return;
+    }
+
+    reviewResultsList.innerHTML = results.map((result, index) => `
+        <div class="review-result-item">
+            <div class="result-item-header">
+                <div class="result-entity-name">
+                    <span style="color: #888; font-size: 0.9rem; margin-right: 10px;">#${index + 1}</span>
+                    ${result.entity || '未知实体'}
+                </div>
+                <div class="result-rag-score">
+                    RAG得分: ${(result.rag_score || 0).toFixed(4)}
+                </div>
+            </div>
+
+            ${result.combined_query ? `
+                <div class="query-info">
+                    <strong>查询条文:</strong>${escapeHtml(result.combined_query)}
+                </div>
+            ` : ''}
+
+            ${result.reference_source ? `
+                <div class="query-info">
+                    <strong>参考来源:</strong>${escapeHtml(result.reference_source)}
+                </div>
+            ` : ''}
+
+            <div class="result-item-content">
+                ${renderReviewBox('非参数审查', result.non_parameter_result)}
+                ${renderReviewBox('参数审查', result.parameter_result)}
+            </div>
+        </div>
+    `).join('');
+}
+
+/**
+ * 渲染单个审查结果盒子
+ */
+function renderReviewBox(title, resultData) {
+    console.log(`[renderReviewBox] ${title}:`, resultData);
+
+    if (!resultData) {
+        return `
+            <div class="review-result-box">
+                <div class="result-box-title">
+                    ${title === '非参数审查' ? '🔰' : '🔢'} ${title}
+                </div>
+                <div class="result-box-content">
+                    <span class="status-badge status-warning">未执行</span>
+                </div>
+            </div>
+        `;
+    }
+
+    // 处理错误情况
+    if (resultData.error || resultData.error_message) {
+        return `
+            <div class="review-result-box">
+                <div class="result-box-title">
+                    ${title === '非参数审查' ? '🔰' : '🔢'} ${title}
+                </div>
+                <div class="result-box-content">
+                    <span class="status-badge status-error">执行失败</span>
+                    <p style="color: #ff5555; margin-top: 10px;">${escapeHtml(resultData.error || resultData.error_message)}</p>
+                </div>
+            </div>
+        `;
+    }
+
+    // 解析ReviewResult格式: {success: true, details: {response: "...", ...}}
+    let responseText = null;
+    let parsedResult = null;
+
+    if (resultData.details && resultData.details.response) {
+        responseText = resultData.details.response;
+        console.log(`[renderReviewBox] 从details.response提取:`, responseText.substring(0, 100));
+    } else if (typeof resultData === 'string') {
+        responseText = resultData;
+        console.log(`[renderReviewBox] resultData是字符串:`, responseText.substring(0, 100));
+    } else if (resultData.response) {
+        responseText = resultData.response;
+        console.log(`[renderReviewBox] 从response提取:`, responseText.substring(0, 100));
+    } else {
+        console.log(`[renderReviewBox] 无法提取responseText, resultData类型:`, typeof resultData);
+    }
+
+    // 如果有response文本,尝试解析
+    if (responseText) {
+        // 检查是否是"无明显问题"
+        if (responseText.includes('无明显问题')) {
+            console.log(`[renderReviewBox] 检测到"无明显问题"`);
+            return `
+                <div class="review-result-box">
+                    <div class="result-box-title">
+                        ${title === '非参数审查' ? '🔰' : '🔢'} ${title}
+                    </div>
+                    <div class="result-box-content">
+                        <span class="status-badge status-success">✅ 无明显问题</span>
+                    </div>
+                </div>
+            `;
+        }
+
+        // 尝试从Markdown代码块中提取JSON
+        const jsonMatch = responseText.match(/```json\s*\n([\s\S]*?)\n```/);
+        if (jsonMatch) {
+            console.log(`[renderReviewBox] 匹配到Markdown JSON代码块`);
+            try {
+                parsedResult = JSON.parse(jsonMatch[1]);
+                console.log(`[renderReviewBox] JSON解析成功:`, parsedResult);
+            } catch (e) {
+                console.warn('[renderReviewBox] JSON解析失败:', e);
+            }
+        } else {
+            // 尝试直接解析
+            console.log(`[renderReviewBox] 未找到Markdown代码块,尝试直接解析JSON`);
+            try {
+                parsedResult = JSON.parse(responseText);
+                console.log(`[renderReviewBox] 直接JSON解析成功:`, parsedResult);
+            } catch (e) {
+                // 不是JSON格式,直接显示文本
+                console.log(`[renderReviewBox] 不是JSON格式`);
+                parsedResult = null;
+            }
+        }
+    }
+
+    // 如果解析成功,显示格式化的问题详情
+    if (parsedResult && parsedResult.issue_point) {
+        console.log(`[renderReviewBox] 渲染格式化问题详情`);
+
+        // 提取审查依据信息
+        const ragReferences = resultData.details?.rag_review_references || '';
+        const referenceSource = resultData.details?.rag_reference_source || '';
+
+        return `
+            <div class="review-result-box">
+                <div class="result-box-title">
+                    ${title === '非参数审查' ? '🔰' : '🔢'} ${title}
+                </div>
+                <div class="result-box-content">
+                    <span class="status-badge status-warning">⚠️ 发现问题</span>
+                    <div class="issue-details">
+                        <div class="issue-item">
+                            <strong>问题标题:</strong>
+                            <p>${escapeHtml(parsedResult.issue_point)}</p>
+                        </div>
+                        <div class="issue-item">
+                            <strong>问题位置:</strong>
+                            <p>${escapeHtml(parsedResult.location)}</p>
+                        </div>
+                        <div class="issue-item">
+                            <strong>修改建议:</strong>
+                            <p>${escapeHtml(parsedResult.suggestion)}</p>
+                        </div>
+                        <div class="issue-item">
+                            <strong>问题原因:</strong>
+                            <p>${escapeHtml(parsedResult.reason)}</p>
+                        </div>
+                        <div class="issue-item">
+                            <strong>风险等级:</strong>
+                            <span class="risk-badge risk-${parsedResult.risk_level === '高风险' ? 'high' : parsedResult.risk_level === '中风险' ? 'medium' : 'low'}">
+                                ${escapeHtml(parsedResult.risk_level)}
+                            </span>
+                        </div>
+                        ${ragReferences ? `
+                        <div class="issue-item">
+                            <strong>参考依据:</strong>
+                            <button class="view-reference-btn" onclick='showReferenceSidebar(${JSON.stringify(ragReferences).replace(/'/g, "\\'")},"${escapeHtml(referenceSource)}")'>
+                                📖 查看审查依据
+                            </button>
+                        </div>
+                        ` : ''}
+                    </div>
+                </div>
+            </div>
+        `;
+    }
+
+    // 如果有原始response但无法解析为结构化数据,显示JSON
+    if (responseText) {
+        console.log(`[renderReviewBox] 渲染原始文本`);
+        return `
+            <div class="review-result-box">
+                <div class="result-box-title">
+                    ${title === '非参数审查' ? '🔰' : '🔢'} ${title}
+                </div>
+                <div class="result-box-content">
+                    <span class="status-badge status-warning">⚠️ 发现问题</span>
+                    <div class="result-json">
+                        <pre>${escapeHtml(responseText)}</pre>
+                    </div>
+                </div>
+            </div>
+        `;
+    }
+
+    // 默认情况:显示原始数据
+    console.log(`[renderReviewBox] 渲染默认JSON`);
+    return `
+        <div class="review-result-box">
+            <div class="result-box-title">
+                ${title === '非参数审查' ? '🔰' : '🔢'} ${title}
+            </div>
+            <div class="result-box-content">
+                <span class="status-badge status-warning">⚠️ 发现问题</span>
+                <div class="result-json">
+                    <pre>${JSON.stringify(resultData, null, 2)}</pre>
+                </div>
+            </div>
+        </div>
+    `;
+}
+
+// ==================== 工具函数 ====================
+
+/**
+ * HTML转义
+ */
+function escapeHtml(text) {
+    const div = document.createElement('div');
+    div.textContent = text;
+    return div.innerHTML;
+}
+
+/**
+ * 显示审查依据侧边栏
+ */
+function showReferenceSidebar(references, source) {
+    const sidebar = document.getElementById('referenceSidebar');
+    const overlay = document.getElementById('sidebarOverlay');
+    const content = document.getElementById('sidebarContent');
+
+    // 转换Markdown为HTML
+    let htmlContent = '';
+
+    if (source) {
+        htmlContent += `<div class="reference-source">📄 来源:${escapeHtml(source)}</div>`;
+    }
+
+    // 使用marked.js转换Markdown
+    if (typeof marked !== 'undefined') {
+        htmlContent += marked.parse(references);
+    } else {
+        // 如果marked未加载,使用纯文本
+        htmlContent += `<pre>${escapeHtml(references)}</pre>`;
+    }
+
+    content.innerHTML = htmlContent;
+
+    // 渲染LaTeX公式
+    if (typeof renderMathInElement !== 'undefined') {
+        renderMathInElement(content, {
+            delimiters: [
+                {left: '$$', right: '$$', display: true},
+                {left: '$', right: '$', display: false},
+                {left: '\\[', right: '\\]', display: true},
+                {left: '\\(', right: '\\)', display: false}
+            ]
+        });
+    }
+
+    // 显示侧边栏
+    sidebar.classList.add('active');
+    overlay.classList.add('active');
+}
+
+/**
+ * 关闭审查依据侧边栏
+ */
+function closeReferenceSidebar() {
+    const sidebar = document.getElementById('referenceSidebar');
+    const overlay = document.getElementById('sidebarOverlay');
+
+    sidebar.classList.remove('active');
+    overlay.classList.remove('active');
+}
+
+/**
+ * 清空所有数据(统一清空函数)
+ */
+function clearAllData() {
+    document.getElementById('testInput').value = '';
+    document.getElementById('professionalResults').style.display = 'none';
+    window.professionalReviewData = null;
+
+    // 同时清空RAG数据(如果存在clearData函数)
+    if (typeof window.clearData === 'function') {
+        window.clearData();
+    }
+}
+
+/**
+ * 清空专业性审查数据(向后兼容)
+ */
+function clearProfessionalData() {
+    clearAllData();
+}
+
+// ==================== 初始化 ====================
+
+// 页面加载完成后初始化
+document.addEventListener('DOMContentLoaded', () => {
+    console.log('专业性审查模块已加载');
+});

+ 291 - 7
utils_test/RAG_Test/rag_pipeline_web/rag_pipeline_server.py

@@ -28,9 +28,13 @@ from foundation.ai.rag.retrieval.entities_enhance import entity_enhance
 from foundation.ai.rag.retrieval.query_rewrite import query_rewrite_manager
 from foundation.observability.logger.loggering import server_logger as logger
 from foundation.observability.monitoring.rag import rag_monitor
+from core.construction_review.component.ai_review_engine import AIReviewEngine
+from core.base.task_models import TaskFileInfo
 
 # 全局Milvus Manager
 milvus_manager = None
+# 全局AI审查引擎
+ai_review_engine = None
 
 
 def init_milvus():
@@ -43,6 +47,28 @@ def init_milvus():
     return milvus_manager
 
 
+def init_ai_review_engine():
+    """初始化AI审查引擎"""
+    global ai_review_engine
+    if ai_review_engine is None:
+        print("📌 初始化AI审查引擎...")
+        # 创建测试用的TaskFileInfo
+        file_info_dict = {
+            'file_id': "test_file_id",
+            'callback_task_id': "test_task_id",
+            'user_id': "test_user",
+            'file_name': "测试文件.docx",
+            'file_type': 'docx',
+            'file_content': b'',
+            'review_config': [],
+            'review_item_config': {}
+        }
+        task_info = TaskFileInfo(file_info_dict)
+        ai_review_engine = AIReviewEngine(task_info)
+        print("✅ AI审查引擎初始化成功")
+    return ai_review_engine
+
+
 def run_async(coro):
     """在合适的环境中运行异步函数"""
     try:
@@ -55,6 +81,224 @@ def run_async(coro):
         return asyncio.run(coro)
 
 
+async def professional_review_test(review_content: str, check_type: str = "both") -> dict:
+    """
+    专业性审查完整测试 - RAG召回 + AI审查
+
+    Args:
+        review_content: 待审查内容
+        check_type: 审查类型 ("non_parameter", "parameter", "both")
+
+    Returns:
+        完整的审查结果,包含RAG召回和AI审查结果
+    """
+    global milvus_manager, ai_review_engine
+
+    # 初始化
+    if milvus_manager is None:
+        init_milvus()
+    if ai_review_engine is None:
+        init_ai_review_engine()
+
+    trace_id = f"professional_review_{int(time.time() * 1000)}"
+    rag_monitor.start_trace(trace_id, metadata={
+        "content_length": len(review_content),
+        "check_type": check_type,
+        "stage": "professional_review_test"
+    })
+
+    logger.info(f"[专业性审查测试] 开始处理, trace_id: {trace_id}, 类型: {check_type}")
+
+    try:
+        # ==================== Step 1: RAG召回 ====================
+        logger.info("[专业性审查测试] Step 1: 开始RAG召回")
+
+        # 1.1 查询提取
+        query_pairs = query_rewrite_manager.query_extract(review_content)
+
+        # 检查query_pairs是否为None或空
+        if not query_pairs:
+            logger.error("[专业性审查测试] 查询提取失败或返回空结果")
+            return {
+                "status": "error",
+                "trace_id": trace_id,
+                "error": "查询提取失败,请检查模型服务是否正常",
+                "rag_summary": {
+                    "query_pairs_count": 0,
+                    "entity_results_count": 0
+                },
+                "review_results": []
+            }
+
+        logger.info(f"[专业性审查测试] 提取到 {len(query_pairs)} 个查询对")
+
+        # 1.2 实体增强检索
+        bfp_result_lists = entity_enhance.entities_enhance_retrieval(query_pairs)
+
+        if not bfp_result_lists:
+            logger.warning("[专业性审查测试] 实体检索未返回结果")
+            return _build_professional_empty_result(trace_id, "no_rag_results")
+
+        # 1.3 父文档增强
+        try:
+            enhancement_result = enhance_with_parent_docs_grouped(
+                milvus_manager,
+                bfp_result_lists,
+                score_threshold=0.3,
+                max_parents_per_pair=3
+            )
+            enhanced_results = enhancement_result['enhanced_results']
+            logger.info(f"[专业性审查测试] 父文档增强完成: {enhancement_result.get('enhanced_pairs', 0)}/{enhancement_result.get('total_pairs', 0)} 个查询对")
+        except Exception as e:
+            logger.error(f"[专业性审查测试] 父文档增强失败: {e}", exc_info=True)
+            enhanced_results = bfp_result_lists
+
+        # 1.4 提取结果
+        entity_results = extract_query_pairs_results(enhanced_results, query_pairs, score_threshold=0.5) if enhanced_results else []
+
+        if not entity_results:
+            logger.warning("[专业性审查测试] 没有结果通过阈值过滤(得分>0.5)")
+            return _build_professional_empty_result(trace_id, "no_high_score_results")
+
+        logger.info(f"[专业性审查测试] RAG召回完成,获得 {len(entity_results)} 个高分结果")
+
+        # ==================== Step 2: 专业性审查 ====================
+        logger.info("[专业性审查测试] Step 2: 开始专业性审查")
+
+        review_results = []
+
+        for idx, entity_result in enumerate(entity_results):
+            entity = entity_result.get('entity', '')
+            combined_query = entity_result.get('combined_query', '')
+            text_content = entity_result.get('text_content', '')
+            file_name = entity_result.get('file_name', '')
+            final_score = entity_result.get('final_score', 0)
+
+            logger.info(f"[专业性审查测试] 处理实体 [{idx+1}/{len(entity_results)}]: {entity}, 得分: {final_score:.4f}")
+
+            # 准备审查参数
+            trace_id_idx = f"{trace_id}_entity_{idx}"
+            review_references = text_content  # 召回的参考文档
+            reference_source = file_name
+            state = {
+                "callback_task_id": "test_task_id",
+                "progress_manager": None
+            }
+            stage_name = "专业性审查测试"
+
+            # 执行审查
+            entity_review_result = {
+                "entity": entity,
+                "combined_query": combined_query,
+                "reference_source": reference_source,
+                "rag_score": final_score,
+                "non_parameter_result": None,
+                "parameter_result": None
+            }
+
+            # 2.1 非参数合规性检查
+            if check_type in ["non_parameter", "both"]:
+                try:
+                    logger.info(f"[专业性审查测试] 执行非参数审查: {entity}")
+                    non_param_result = await ai_review_engine.check_non_parameter_compliance(
+                        trace_id_idx=trace_id_idx,
+                        review_content=review_content,
+                        review_references=review_references,
+                        reference_source=reference_source,
+                        state=state,
+                        stage_name=stage_name,
+                        entity_query=combined_query  # 传入实体查询条文
+                    )
+                    # 将ReviewResult对象转换为字典
+                    if hasattr(non_param_result, '__dict__'):
+                        entity_review_result["non_parameter_result"] = {
+                            'success': non_param_result.success,
+                            'details': non_param_result.details,
+                            'error_message': non_param_result.error_message,
+                            'execution_time': non_param_result.execution_time
+                        }
+                    else:
+                        entity_review_result["non_parameter_result"] = non_param_result
+                    logger.info(f"[专业性审查测试] 非参数审查完成: {entity}")
+                except Exception as e:
+                    logger.error(f"[专业性审查测试] 非参数审查失败: {e}", exc_info=True)
+                    entity_review_result["non_parameter_result"] = {"error": str(e)}
+
+            # 2.2 参数合规性检查
+            if check_type in ["parameter", "both"]:
+                try:
+                    logger.info(f"[专业性审查测试] 执行参数审查: {entity}")
+                    param_result = await ai_review_engine.check_parameter_compliance(
+                        trace_id_idx=trace_id_idx,
+                        review_content=review_content,
+                        review_references=review_references,
+                        reference_source=reference_source,
+                        state=state,
+                        stage_name=stage_name,
+                        entity_query=combined_query  # 传入实体查询条文
+                    )
+                    # 将ReviewResult对象转换为字典
+                    if hasattr(param_result, '__dict__'):
+                        entity_review_result["parameter_result"] = {
+                            'success': param_result.success,
+                            'details': param_result.details,
+                            'error_message': param_result.error_message,
+                            'execution_time': param_result.execution_time
+                        }
+                    else:
+                        entity_review_result["parameter_result"] = param_result
+                    logger.info(f"[专业性审查测试] 参数审查完成: {entity}")
+                except Exception as e:
+                    logger.error(f"[专业性审查测试] 参数审查失败: {e}", exc_info=True)
+                    entity_review_result["parameter_result"] = {"error": str(e)}
+
+            review_results.append(entity_review_result)
+
+        # ==================== Step 3: 构建结果 ====================
+        final_result = {
+            "status": "success",
+            "trace_id": trace_id,
+            "check_type": check_type,
+            "rag_summary": {
+                "query_pairs_count": len(query_pairs),
+                "entity_results_count": len(entity_results),
+                "query_pairs": query_pairs,
+                "entity_results": entity_results
+            },
+            "review_results": review_results,
+            "total_entities_reviewed": len(review_results)
+        }
+
+        # 专业性审查直接返回结果,不需要包装
+        logger.info(f"✅ 专业性审查测试完成, trace_id: {trace_id}")
+        return final_result
+
+    except Exception as e:
+        logger.error(f"[专业性审查测试] 处理失败: {e}", exc_info=True)
+        return {
+            "status": "error",
+            "trace_id": trace_id,
+            "error": str(e)
+        }
+    finally:
+        rag_monitor.end_trace(trace_id)
+
+
+def _build_professional_empty_result(trace_id: str, reason: str) -> dict:
+    """构建专业性审查的空结果"""
+    return {
+        "status": "no_results",
+        "trace_id": trace_id,
+        "reason": reason,
+        "message": "未获取到有效的RAG召回结果",
+        "rag_summary": {
+            "query_pairs_count": 0,
+            "entity_results_count": 0
+        },
+        "review_results": []
+    }
+
+
 def rag_enhanced_check(query_content: str) -> dict:
     """
     RAG增强检查 - 完整链路(使用装饰器监控版本)
@@ -308,6 +552,39 @@ class RAGPipelineHandler(SimpleHTTPRequestHandler):
                 self.send_json_response({'status': 'ok', 'message': 'Milvus初始化成功'})
             except Exception as e:
                 self.send_json_response({'error': str(e)}, 500)
+
+        elif parsed.path == '/api/professional_review':
+            # 新增:专业性审查测试API
+            content_length = int(self.headers['Content-Length'])
+            post_data = self.rfile.read(content_length)
+
+            try:
+                body = json.loads(post_data.decode('utf-8'))
+                review_content = body.get('content', '')
+                check_type = body.get('check_type', 'both')  # "non_parameter", "parameter", "both"
+
+                if not review_content:
+                    self.send_json_response({'error': '请提供content参数'}, 400)
+                    return
+
+                if check_type not in ['non_parameter', 'parameter', 'both']:
+                    self.send_json_response({'error': 'check_type必须是 non_parameter, parameter 或 both'}, 400)
+                    return
+
+                print(f"\n📝 收到专业性审查测试请求, 类型: {check_type}, 内容长度: {len(review_content)}")
+
+                # 运行异步函数
+                result = run_async(professional_review_test(review_content, check_type))
+
+                print(f"✅ 专业性审查测试完成")
+                self.send_json_response(result)
+
+            except json.JSONDecodeError:
+                self.send_json_response({'error': 'JSON解析失败'}, 400)
+            except Exception as e:
+                logger.error(f"专业性审查测试失败: {e}", exc_info=True)
+                self.send_json_response({'error': str(e)}, 500)
+
         else:
             self.send_json_response({'error': 'Not Found'}, 404)
 
@@ -352,17 +629,24 @@ def run_server(port=8765):
             return os.path.join(web_dir, path)
 
     server = HTTPServer(('0.0.0.0', port), CustomHandler)
-    print(f"\n{'='*60}")
+    print(f"\n{'='*70}")
     print(f"🚀 RAG管道测试服务器已启动")
-    print(f"{'='*60}")
+    print(f"{'='*70}")
     print(f"📍 访问地址: http://localhost:{port}")
     print(f"📍 工作目录: {project_root}")
     print(f"📍 API端点:")
-    print(f"   POST /api/rag     - 执行RAG检索")
-    print(f"   POST /api/init    - 初始化Milvus")
-    print(f"   GET  /api/data    - 获取最新数据")
-    print(f"   GET  /api/health  - 健康检查")
-    print(f"{'='*60}\n")
+    print(f"   POST /api/rag                    - 执行RAG检索")
+    print(f"   POST /api/professional_review    - 专业性审查完整测试(RAG+AI审查)")
+    print(f"   POST /api/init                   - 初始化Milvus")
+    print(f"   GET  /api/data                   - 获取最新数据")
+    print(f"   GET  /api/health                 - 健康检查")
+    print(f"{'='*70}")
+    print(f"\n💡 专业性审查测试示例:")
+    print(f"   curl -X POST http://localhost:{port}/api/professional_review \\")
+    print(f"     -H \"Content-Type: application/json\" \\")
+    print(f"     -d '{{\"content\": \"待审查内容\", \"check_type\": \"both\"}}'")
+    print(f"\n   check_type 可选值: non_parameter, parameter, both")
+    print(f"{'='*70}\n")
 
     # 预初始化Milvus
     try:

+ 592 - 18
utils_test/RAG_Test/rag_pipeline_web/styles.css

@@ -87,28 +87,62 @@ body {
 
 /* 按钮样式 */
 .btn {
-    padding: 10px 20px;
+    padding: 12px 28px;
     border: none;
-    border-radius: 6px;
+    border-radius: 8px;
     cursor: pointer;
-    font-size: 0.9rem;
+    font-size: 1rem;
+    font-weight: 600;
     transition: all 0.3s ease;
+    display: inline-flex;
+    align-items: center;
+    gap: 8px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
 }
 
 .btn-primary {
-    background: linear-gradient(135deg, #00d4ff, #0099cc);
+    background: linear-gradient(135deg, #0099ff, #0066cc);
     color: white;
 }
 
+.btn-primary:hover {
+    background: linear-gradient(135deg, #00b3ff, #0077dd);
+    transform: translateY(-2px);
+    box-shadow: 0 6px 20px rgba(0, 153, 255, 0.4);
+}
+
 .btn-secondary {
-    background: rgba(255, 255, 255, 0.1);
+    background: rgba(255, 255, 255, 0.08);
     color: #e0e0e0;
-    border: 1px solid rgba(255, 255, 255, 0.2);
+    border: 1px solid rgba(255, 255, 255, 0.15);
 }
 
-.btn:hover {
+.btn-secondary:hover {
+    background: rgba(255, 255, 255, 0.12);
+    border-color: rgba(255, 255, 255, 0.25);
     transform: translateY(-2px);
-    box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
+    box-shadow: 0 4px 12px rgba(255, 255, 255, 0.1);
+}
+
+.btn-success {
+    background: linear-gradient(135deg, #00d98e, #00a86b);
+    color: white;
+}
+
+.btn-success:hover {
+    background: linear-gradient(135deg, #00ff9f, #00bf7d);
+    transform: translateY(-2px);
+    box-shadow: 0 6px 20px rgba(0, 217, 142, 0.4);
+}
+
+.btn:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+    transform: none !important;
+}
+
+.btn-icon {
+    font-size: 1.1rem;
 }
 
 /* 概览卡片 */
@@ -576,31 +610,36 @@ body {
 }
 
 .input-area {
-    margin-bottom: 15px;
+    margin-bottom: 20px;
 }
 
 .input-area textarea {
     width: 100%;
-    min-height: 150px;
-    background: rgba(0, 0, 0, 0.3);
-    border: 1px solid rgba(255, 255, 255, 0.1);
-    border-radius: 8px;
-    padding: 15px;
-    color: #e0e0e0;
-    font-family: inherit;
+    min-height: 180px;
+    background: rgba(0, 0, 0, 0.4);
+    border: 2px solid rgba(255, 255, 255, 0.15);
+    border-radius: 10px;
+    padding: 18px;
+    color: #e8e8e8;
+    font-family: 'Consolas', 'Monaco', monospace;
     font-size: 0.95rem;
-    line-height: 1.6;
+    line-height: 1.7;
     resize: vertical;
-    transition: border-color 0.3s ease;
+    transition: all 0.3s ease;
+    box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
 }
 
 .input-area textarea:focus {
     outline: none;
     border-color: #00d4ff;
+    background: rgba(0, 0, 0, 0.5);
+    box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3),
+                0 0 0 3px rgba(0, 212, 255, 0.15);
 }
 
 .input-area textarea::placeholder {
     color: #666;
+    font-style: italic;
 }
 
 .action-buttons {
@@ -671,3 +710,538 @@ body {
     color: #ffb86c;
     margin-top: 5px;
 }
+
+/* ==================== 功能切换Tab样式 ==================== */
+.function-tabs {
+    display: flex;
+    gap: 10px;
+    margin-bottom: 30px;
+    border-bottom: 2px solid rgba(255, 255, 255, 0.1);
+    padding-bottom: 10px;
+}
+
+.function-tab {
+    padding: 12px 24px;
+    border: none;
+    background: rgba(255, 255, 255, 0.05);
+    color: #888;
+    border-radius: 8px 8px 0 0;
+    cursor: pointer;
+    font-size: 1rem;
+    transition: all 0.3s ease;
+    border-bottom: 3px solid transparent;
+}
+
+.function-tab:hover {
+    background: rgba(0, 212, 255, 0.1);
+    color: #00d4ff;
+}
+
+.function-tab.active {
+    background: rgba(0, 212, 255, 0.2);
+    color: #00d4ff;
+    border-bottom-color: #00d4ff;
+}
+
+/* ==================== 专业性审查区域样式 ==================== */
+.professional-review-section {
+    background: rgba(255, 255, 255, 0.03);
+    border-radius: 12px;
+    padding: 25px;
+    margin-bottom: 30px;
+    position: relative;
+}
+
+.review-type-selector {
+    display: flex;
+    align-items: center;
+    gap: 25px;
+    margin: 15px 0 20px 0;
+    padding: 18px 20px;
+    background: linear-gradient(135deg, rgba(0, 153, 255, 0.08), rgba(0, 212, 255, 0.05));
+    border: 1px solid rgba(0, 212, 255, 0.2);
+    border-radius: 10px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+.review-type-selector > label:first-child {
+    color: #00d4ff;
+    font-weight: 600;
+    font-size: 0.95rem;
+}
+
+.radio-label {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    cursor: pointer;
+    color: #e0e0e0 !important;
+    font-weight: normal !important;
+    padding: 6px 12px;
+    border-radius: 6px;
+    transition: all 0.2s ease;
+}
+
+.radio-label:hover {
+    background: rgba(0, 212, 255, 0.1);
+}
+
+.radio-label input[type="radio"] {
+    width: 18px;
+    height: 18px;
+    cursor: pointer;
+    accent-color: #00d4ff;
+}
+
+.radio-label input[type="radio"]:checked + span {
+    color: #00ff88;
+    font-weight: 500;
+}
+
+/* ==================== 专业性审查结果样式 ==================== */
+.professional-results {
+    margin-top: 30px;
+}
+
+.result-section {
+    background: rgba(255, 255, 255, 0.03);
+    border-radius: 12px;
+    padding: 20px;
+    margin-bottom: 20px;
+}
+
+.result-section h3 {
+    color: #00d4ff;
+    margin-bottom: 15px;
+    font-size: 1.2rem;
+}
+
+/* RAG召回摘要卡片 */
+.rag-summary {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+    gap: 15px;
+}
+
+.summary-card {
+    background: rgba(0, 212, 255, 0.1);
+    border: 1px solid rgba(0, 212, 255, 0.3);
+    border-radius: 8px;
+    padding: 15px;
+}
+
+.summary-card-title {
+    color: #888;
+    font-size: 0.85rem;
+    margin-bottom: 8px;
+}
+
+.summary-card-value {
+    color: #00d4ff;
+    font-size: 1.5rem;
+    font-weight: bold;
+}
+
+/* 审查结果项 */
+.review-results-list {
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+}
+
+.review-result-item {
+    background: rgba(255, 255, 255, 0.05);
+    border-left: 4px solid #00d4ff;
+    border-radius: 8px;
+    padding: 20px;
+}
+
+.result-item-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 15px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.result-entity-name {
+    color: #00d4ff;
+    font-size: 1.1rem;
+    font-weight: bold;
+}
+
+.result-rag-score {
+    background: rgba(255, 182, 108, 0.2);
+    color: #ffb86c;
+    padding: 4px 12px;
+    border-radius: 12px;
+    font-size: 0.85rem;
+}
+
+.result-item-content {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 20px;
+}
+
+.review-result-box {
+    background: rgba(255, 255, 255, 0.03);
+    border-radius: 8px;
+    padding: 15px;
+}
+
+.result-box-title {
+    color: #ffb86c;
+    font-size: 0.9rem;
+    font-weight: bold;
+    margin-bottom: 10px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.result-box-content {
+    color: #e0e0e0;
+    font-size: 0.9rem;
+    line-height: 1.6;
+}
+
+.result-json {
+    background: rgba(0, 0, 0, 0.3);
+    border-radius: 6px;
+    padding: 12px;
+    max-height: 300px;
+    overflow-y: auto;
+}
+
+.result-json pre {
+    margin: 0;
+    color: #e0e0e0;
+    font-size: 0.85rem;
+    white-space: pre-wrap;
+    word-wrap: break-word;
+}
+
+/* 状态标签 */
+.status-badge {
+    display: inline-block;
+    padding: 4px 10px;
+    border-radius: 12px;
+    font-size: 0.8rem;
+    font-weight: bold;
+}
+
+.status-success {
+    background: rgba(0, 255, 136, 0.2);
+    color: #00ff88;
+}
+
+.status-warning {
+    background: rgba(255, 182, 108, 0.2);
+    color: #ffb86c;
+}
+
+.status-error {
+    background: rgba(255, 85, 85, 0.2);
+    color: #ff5555;
+}
+
+/* 问题详情样式 */
+.issue-details {
+    margin-top: 15px;
+    padding: 15px;
+    background: rgba(0, 0, 0, 0.2);
+    border-radius: 8px;
+    border-left: 3px solid #ffb86c;
+}
+
+.issue-item {
+    margin-bottom: 15px;
+}
+
+.issue-item:last-child {
+    margin-bottom: 0;
+}
+
+.issue-item strong {
+    display: block;
+    color: #00d4ff;
+    font-size: 0.9rem;
+    margin-bottom: 6px;
+}
+
+.issue-item p {
+    margin: 0;
+    color: #e0e0e0;
+    line-height: 1.6;
+    padding-left: 10px;
+    border-left: 2px solid rgba(255, 255, 255, 0.1);
+}
+
+/* 风险等级标签 */
+.risk-badge {
+    display: inline-block;
+    padding: 4px 12px;
+    border-radius: 12px;
+    font-size: 0.85rem;
+    font-weight: bold;
+    margin-left: 10px;
+}
+
+.risk-badge.risk-high {
+    background: rgba(255, 85, 85, 0.2);
+    color: #ff5555;
+    border: 1px solid #ff5555;
+}
+
+.risk-badge.risk-medium {
+    background: rgba(255, 182, 108, 0.2);
+    color: #ffb86c;
+    border: 1px solid #ffb86c;
+}
+
+.risk-badge.risk-low {
+    background: rgba(80, 250, 123, 0.2);
+    color: #50fa7b;
+    border: 1px solid #50fa7b;
+}
+
+/* 查看审查依据按钮 */
+.view-reference-btn {
+    background: linear-gradient(135deg, #00d4ff, #0099cc);
+    color: #ffffff;
+    border: none;
+    padding: 8px 16px;
+    border-radius: 6px;
+    font-size: 0.9rem;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    box-shadow: 0 2px 6px rgba(0, 212, 255, 0.3);
+    margin-left: 10px;
+}
+
+.view-reference-btn:hover {
+    background: linear-gradient(135deg, #00e5ff, #00aadd);
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0, 212, 255, 0.4);
+}
+
+/* 审查依据侧边栏 */
+.reference-sidebar {
+    position: fixed;
+    top: 0;
+    right: -600px;
+    width: 600px;
+    height: 100vh;
+    background: rgba(20, 20, 30, 0.98);
+    backdrop-filter: blur(10px);
+    border-left: 1px solid rgba(0, 212, 255, 0.3);
+    box-shadow: -5px 0 30px rgba(0, 0, 0, 0.5);
+    transition: right 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+    z-index: 1000;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+}
+
+.reference-sidebar.active {
+    right: 0;
+}
+
+.sidebar-header {
+    padding: 20px 25px;
+    background: linear-gradient(135deg, rgba(0, 153, 255, 0.15), rgba(0, 212, 255, 0.1));
+    border-bottom: 2px solid rgba(0, 212, 255, 0.3);
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.sidebar-header h3 {
+    margin: 0;
+    color: #00d4ff;
+    font-size: 1.3rem;
+}
+
+.sidebar-close-btn {
+    background: rgba(255, 85, 85, 0.2);
+    border: 1px solid #ff5555;
+    color: #ff5555;
+    width: 32px;
+    height: 32px;
+    border-radius: 6px;
+    font-size: 1.2rem;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.sidebar-close-btn:hover {
+    background: rgba(255, 85, 85, 0.3);
+    transform: scale(1.1);
+}
+
+.sidebar-content {
+    flex: 1;
+    padding: 25px;
+    overflow-y: auto;
+    color: #e0e0e0;
+    line-height: 1.8;
+}
+
+/* Markdown内容样式 */
+.sidebar-content h1, .sidebar-content h2, .sidebar-content h3 {
+    color: #00d4ff;
+    margin-top: 20px;
+    margin-bottom: 12px;
+    border-bottom: 1px solid rgba(0, 212, 255, 0.2);
+    padding-bottom: 8px;
+}
+
+.sidebar-content h1 { font-size: 1.6rem; }
+.sidebar-content h2 { font-size: 1.3rem; }
+.sidebar-content h3 { font-size: 1.1rem; }
+
+.sidebar-content p {
+    margin: 10px 0;
+}
+
+.sidebar-content code {
+    background: rgba(0, 0, 0, 0.3);
+    padding: 2px 6px;
+    border-radius: 3px;
+    color: #ffb86c;
+    font-family: 'Consolas', monospace;
+}
+
+.sidebar-content pre {
+    background: rgba(0, 0, 0, 0.4);
+    padding: 15px;
+    border-radius: 8px;
+    overflow-x: auto;
+    border-left: 3px solid #00d4ff;
+}
+
+.sidebar-content pre code {
+    background: none;
+    padding: 0;
+}
+
+.sidebar-content ul, .sidebar-content ol {
+    padding-left: 25px;
+    margin: 10px 0;
+}
+
+.sidebar-content li {
+    margin: 5px 0;
+}
+
+.sidebar-content blockquote {
+    border-left: 4px solid #00d4ff;
+    padding-left: 15px;
+    margin: 15px 0;
+    color: #b8b8b8;
+    font-style: italic;
+}
+
+.sidebar-content table {
+    width: 100%;
+    border-collapse: collapse;
+    margin: 15px 0;
+}
+
+.sidebar-content th, .sidebar-content td {
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    padding: 10px;
+    text-align: left;
+}
+
+.sidebar-content th {
+    background: rgba(0, 212, 255, 0.1);
+    color: #00d4ff;
+    font-weight: 600;
+}
+
+.sidebar-content td {
+    background: rgba(0, 0, 0, 0.2);
+}
+
+.reference-source {
+    background: rgba(0, 212, 255, 0.1);
+    border: 1px solid rgba(0, 212, 255, 0.3);
+    padding: 12px 15px;
+    border-radius: 8px;
+    margin-bottom: 20px;
+    color: #00d4ff;
+    font-weight: 500;
+}
+
+/* 侧边栏遮罩层 */
+.sidebar-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.7);
+    backdrop-filter: blur(4px);
+    opacity: 0;
+    visibility: hidden;
+    transition: all 0.3s ease;
+    z-index: 999;
+}
+
+.sidebar-overlay.active {
+    opacity: 1;
+    visibility: visible;
+}
+
+/* LaTeX公式样式优化 */
+.sidebar-content .katex {
+    color: #e0e0e0;
+}
+
+.sidebar-content .katex-display {
+    margin: 20px 0;
+}
+
+/* 查询对信息 */
+.query-info {
+    background: rgba(0, 212, 255, 0.05);
+    border: 1px solid rgba(0, 212, 255, 0.2);
+    border-radius: 6px;
+    padding: 10px;
+    margin-top: 10px;
+    font-size: 0.85rem;
+    color: #888;
+}
+
+.query-info strong {
+    color: #00d4ff;
+}
+
+/* 空状态提示 */
+.empty-state {
+    text-align: center;
+    padding: 60px 20px;
+    color: #666;
+}
+
+.empty-state-icon {
+    font-size: 4rem;
+    margin-bottom: 20px;
+}
+
+.empty-state-text {
+    font-size: 1.1rem;
+    margin-bottom: 10px;
+}
+
+.empty-state-hint {
+    font-size: 0.9rem;
+    color: #555;
+}