Forráskód Böngészése

Merge branch 'dev' into dev-planWrite

tangle 1 hónapja
szülő
commit
62c706b76e

BIN
config/.DS_Store


+ 1 - 1
config/model_setting.yaml

@@ -150,4 +150,4 @@ model_settings:
 # 默认配置(当功能未指定时使用)
 default:
   model: shutian_qwen3_5_122b
-  enable_thinking: true
+  enable_thinking: false

+ 3 - 18
core/base/task_models.py

@@ -35,20 +35,8 @@ class TaskFileInfo:
         self.file_content = file_info.get('file_content', b'')
 
         # 审查配置信息(使用 .get() 提供默认值,支持键不存在的情况)
-        review_config_raw = file_info.get('review_config')
         review_item_config_raw = file_info.get('review_item_config')
 
-        # 类型校验:确保review_config是列表
-        if isinstance(review_config_raw, list):
-            self.review_config = review_config_raw
-        else:
-            # 如果不是列表,记录警告并使用默认空列表
-            logger.warning(
-                f"review_config类型错误,期望list,实际{type(review_config_raw).__name__},"
-                f"值: {review_config_raw},将使用空列表"
-            )
-            self.review_config = []
-
         # 类型校验:确保review_item_config是列表
         if isinstance(review_item_config_raw, list):
             self.review_item_config = review_item_config_raw
@@ -76,8 +64,9 @@ class TaskFileInfo:
         return self._file_info.copy()
 
     def get_review_config_list(self) -> list:
-        """获取审查配置列表"""
-        return self.review_config.copy()
+        """[已废弃] 获取审查配置列表 — 旧 review_config 已移除,仅保留兼容性"""
+        logger.warning("get_review_config_list() 已废弃,请使用 get_review_item_config_list()")
+        return []
 
     def get_review_item_config_list(self) -> list:
         """获取审查项配置列表(章节_审查维度格式)"""
@@ -95,10 +84,6 @@ class TaskFileInfo:
         """获取测试定位标志符"""
         return self.test_designation_chunk_flag
 
-    def has_review_type(self, review_type: str) -> bool:
-        """检查是否包含指定的审查类型"""
-        return review_type in self.review_config
-
     def get_file_size(self) -> int:
         """获取文件大小(字节)"""
         return len(self.file_content) if self.file_content else 0

+ 11 - 99
core/construction_review/component/ai_review_engine.py

@@ -101,7 +101,6 @@ class Stage(Enum):
     """工作流状态"""
     BASIC = {
         'reviewer_type':'basic',
-        'grammar': 'sensitive_word_check',
         'sensitive':'sensitive_check',
         'semantic': 'semantic_logic_check',
         'completeness': 'completeness_check',
@@ -145,7 +144,6 @@ class AIReviewEngine(BaseReviewer):
         self.file_id = task_file_info.file_id
         self.callback_task_id = task_file_info.callback_task_id
         self.user_id = task_file_info.user_id
-        self.review_config = task_file_info.review_config
         self.project_plan_type = task_file_info.project_plan_type
         self.tendency_review_role = task_file_info.tendency_review_role
 
@@ -601,36 +599,6 @@ class AIReviewEngine(BaseReviewer):
         logger.info(f"[技术审查] 成功处理 {len(flattened_results)} 个审查结果")
         return flattened_results
 
-    async def sensitive_word_check(self, trace_id_idx: str, review_content: str,
-                          state: str, stage_name: str) -> Dict[str, Any]:
-        """
-        敏感词 LLM 审查
-
-        Args:
-            trace_id_idx: 追踪ID索引
-            review_content: 审查内容
-            state: 状态字典
-            stage_name: 阶段名称
-
-        Returns:
-            ReviewResult: 敏感词审查结果
-        """
-        from core.construction_review.component.reviewers.sensitive_word_check import sensitive_word_check_reviewer
-
-        # 构造trace_id
-        prompt_name = Stage.BASIC.value['grammar']
-        trace_id = prompt_name + trace_id_idx
-
-        # 调用敏感词 LLM 审查模块
-        result = await sensitive_word_check_reviewer.check_sensitive_word(
-            trace_id=trace_id,
-            review_content=review_content,
-            state=state,
-            stage_name=stage_name
-        )
-
-        return result
-
     async def grammar_check(self, trace_id_idx: str, review_content: str,
                           state: str, stage_name: str) -> Dict[str, Any]:
         """
@@ -1066,74 +1034,18 @@ class AIReviewEngine(BaseReviewer):
         Returns:
             Dict[str, Any]: 敏感信息检查结果
         """
-        from core.construction_review.component.reviewers.utils import (
-            check_sensitive_words_async,
-            format_check_results
-        )
-        from foundation.observability.logger.loggering import review_logger as logger
-        import time
-        
-        start_time = time.time()
+        from core.construction_review.component.reviewers.sensitive_check_reviewer import sensitive_check_reviewer
+
         trace_id = "sensitive_check" + trace_id_idx
-        first_results = await check_sensitive_words_async(review_content) # 先使用关键词匹配式审查
-        
-        # 判断是否检测到敏感词
-        if first_results:
-            logger.info(f"检测到 {len(first_results)} 个敏感词,准备送入大模型二审")
-            # 有敏感词,拼接原文与敏感词列表,进入大模型二审
-            # 格式化敏感词列表
-            sensitive_words_info = []
-            for item in first_results:
-                sensitive_words_info.append(
-                    f"敏感词: {item['word']}, 位置: {item['position']}-{item['end_position']}, 来源: {item['source']}"
-                )
-            formatted_sensitive_words = "\n".join(sensitive_words_info)
-            
-            logger.info(f"格式化后的敏感词信息:\n{formatted_sensitive_words}")
-            
-            # 调用大模型得到敏感词审查结果(通过 function_name 从 model_setting.yaml 加载模型配置)
-            return await self.review("sensitive_check", trace_id, "basic", "sensitive_word_check",
-                                   review_content, formatted_sensitive_words,
-                                   None, state, stage_name, timeout=60, function_name="sensitive_check")
-        else:
-            # 没有检测到敏感词,构造返回体
-            logger.info("没有检测到敏感词,未进入二审")
-            from core.construction_review.component.reviewers.base_reviewer import ReviewResult
-            
-            execution_time = time.time() - start_time
-            result = ReviewResult(
-                success = True,
-                details = {"name": "sensitive_check", "response": "无明显问题"},
-                error_message = None,
-                execution_time = execution_time
-            )
-            
-            # 推送审查完成信息
-            if state and state.get("progress_manager"):
-                import asyncio
-                review_result_data = {
-                    'name': 'sensitive_check',
-                    'success': result.success,
-                    'details': result.details,
-                    'error_message': result.error_message,
-                    'execution_time': result.execution_time,
-                    'timestamp': time.time()
-                }
-                asyncio.create_task(
-                    state["progress_manager"].update_stage_progress(
-                        callback_task_id=state["callback_task_id"],
-                        stage_name=stage_name,
-                        current=None,
-                        status="processing",
-                        message=f"sensitive_check 审查完成,未检测到敏感词,耗时: {result.execution_time:.2f}s",
-                        issues=[review_result_data],
-                        event_type="processing"
-                    )
-                )
-            
-            logger.info(f"sensitive_check 审查完成,未检测到敏感词,耗时: {result.execution_time:.2f}s")
-            
-            return result
+
+        result = await sensitive_check_reviewer.check_sensitive(
+            trace_id=trace_id,
+            review_content=review_content,
+            state=state,
+            stage_name=stage_name
+        )
+
+        return result
 
     async def check_non_parameter_compliance(self, trace_id_idx: str, review_content: str, review_references: str,
                                          reference_source: str, state: str, stage_name: str,

+ 1 - 9
core/construction_review/component/minimal_pipeline/catalog_reviewer.py

@@ -374,7 +374,7 @@ check_result 中必须包含以下字段:
 """
 
     def _extract_json(self, content: str) -> Optional[Dict[str, Any]]:
-        """从LLM响应中提取JSON,增强健壮性(支持思考模式输出)"""
+        """从LLM响应中提取JSON,增强健壮性"""
         try:
             # 清理内容:移除 markdown 代码块标记
             content = content.strip()
@@ -385,14 +385,6 @@ check_result 中必须包含以下字段:
             content = re.sub(r'\s*```\s*$', '', content, flags=re.MULTILINE)
             content = re.sub(r'^```\s*', '', content, flags=re.MULTILINE)
 
-            # 处理思考模式输出:跳过思考部分,提取最终答案
-            # 检查 <think>...</think> 标签 (Qwen3.5 思考模式标准格式)
-            think_end = content.find("</think>")
-            if think_end != -1:
-                # 提取 </think> 之后的内容
-                content = content[think_end + len("</think>"):].strip()
-                logger.debug(f"[CatalogReviewer] 检测到 <think> 标签,从 </think> 后提取内容,长度: {len(content)}")
-
             # 找到第一个 { 开始的位置
             json_start = content.find('{')
             if json_start == -1:

+ 2 - 2
core/construction_review/component/reviewers/grammar_check_reviewer.py

@@ -75,7 +75,7 @@ class GrammarCheckReviewer:
             result = ReviewResult(
                 success=True,
                 details={
-                    "name": "grammar_check",
+                    "name": "sensitive_word_check",
                     "response": model_response
                 },
                 error_message=None,
@@ -117,7 +117,7 @@ class GrammarCheckReviewer:
             # 返回失败结果
             result = ReviewResult(
                 success=False,
-                details={"name": "grammar_check"},
+                details={"name": "sensitive_word_check"},
                 error_message=error_msg,
                 execution_time=execution_time
             )

+ 135 - 39
core/construction_review/component/reviewers/prompt/basic_reviewers.yaml

@@ -5,7 +5,7 @@ grammar_check:
   system_prompt: |
     system
     # role
-    你是词句语法审查专家
+    你是词句语法审查专家,专注于文字的**书写正确性**和**语法规范性**。
 
     ## workflow
     - 负责检查文本中的错别字和重复字词等语法问题。
@@ -20,10 +20,10 @@ grammar_check:
     ## example
     1. 出现了明显错字,出现了的地得使用混乱。
     2. 如果出现了错字,大概率是因为拼音拼写错误导致的错字,请根据其相似读音推测正确的字,如果拿不准请不给出修改为xx的建议。
-    3. 如“供气”错打为了“供器”,应当结合上下文推测“供qi”应该为是什么。
+    3. 如”供气”错打为了”供器”,应当结合上下文推测”供qi”应该为是什么。
     4. 对于条款编号而言,'一)'这样的结构是正确的,符合中文规范
-    5. 所有建议都要基于审查内容给出忠实建议,禁止给出不符合逻辑的错误建议,如建议将A修改成A,例如:"将'确定的'改为'确定的'(原字为'确',应为'确')",这种建议就是错误的
-    6. 请对汉语中经典易错字如“辩”与“辨”等等的混用请多加注意。
+    5. 所有建议都要基于审查内容给出忠实建议,禁止给出不符合逻辑的错误建议,如建议将A修改成A,例如:”将'确定的'改为'确定的'(原字为'确',应为'确')”,这种建议就是错误的
+    6. 请对汉语中经典易错字如”辩”与”辨”等等的混用请多加注意。
 
     ## rule
     - 不需要强求要输出问题,除非是非常明显的错误。小问题可以忽略。
@@ -44,6 +44,22 @@ grammar_check:
         6. 统一解释:如果表格中出现了多列相同的表头标题,不是错误,而是解析时这几个是合并的表头。
         7. 所有建议都要基于审查内容给出忠实建议,禁止给出不符合逻辑的错误建议,如建议将A修改成A,例如:将'若'改为'若'(应为'若'),这种建议就是错误的
 
+    ## ⚠️ 严格禁止审查以下内容(与词句语法无关)
+    **你只负责检查词句语法层面的问题**,以下类型的问题**一律跳过,不输出任何 issue**:
+    1. **逻辑矛盾**:如前后陈述不一致、数据前后冲突等(例:前文说”采用A方法”,后文说”不采用A方法”)
+    2. **时间/日期错误**:如开工日期晚于竣工日期、时间顺序倒置等(例:”计划2026年开工,2025年竣工”——这是逻辑错误,不是词句错误)
+    3. **因果关系错误**:如原因与结果不匹配(例:”因为天气晴朗,所以混凝土强度不足”)
+    4. **条件与结论不匹配**:如条件无法推导出结论(例:”当温度低于5℃时,可正常施工”)
+    5. **常识/事实错误**:如技术参数不合理、施工方法选择错误等
+    6. **语义矛盾**:如表达意思前后冲突、语义歧义
+    7. **数值计算错误**:如加法结果不对、数据不一致
+    8. **业务逻辑问题**:如施工方案不合理、工艺流程错误
+    9. **数据单位/格式**:如单位使用不当、数字格式问题
+    10. **语义逻辑问题**:任何与语义、逻辑、事实相关的内容
+
+    **你的职责范围仅限于**:错别字(如”混泥土”→”混凝土”)、多字/少字、重复字词(如”公司公司”)、标点符号错误、”的地得”混用、明显的语法结构错误。
+    超出以上范围的所有问题,请忽略并输出”无明显问题”。
+
     ## output
     ```json
         {{
@@ -60,10 +76,18 @@ grammar_check:
     - 低风险:形式问题、不影响实质内容的词句错误。
 
   user_prompt_template: |
-    请审查以下内容的词句语法错误,重点关注错别字、重复字词和语法结构:
+    请审查以下内容的词句语法错误,**仅限**错别字、多字、少字、重复字词、标点符号错误、"的地得"混用、明显的语法结构错误。
+
     【待检查文本】
     {review_content}
 
+    ⚠️ 重要提醒:以下类型的问题**不属于本次审查范围,请跳过不输出**:
+    - 逻辑矛盾(如前后数据不一致、时间顺序倒置、因果关系错误)
+    - 事实/常识错误(如施工方案不合理、技术参数错误)
+    - 数值计算错误、单位格式问题
+    - 语义歧义、条件结论不匹配
+    - 任何与逻辑、语义、事实相关的问题
+
     输出格式:务必须严格按照以下标准json格式输出审查结果
     如果未发现明显的词句语法错误,请输出:无明显问题。
     **禁止**输出建议类似于"将'设'改为'设'(原字为'设',应为'设')",没有问题就是没有问题,不可造假,谢谢。
@@ -121,21 +145,32 @@ semantic_logic_check:
     4. 禁止对表格格式、制表符进行检查
     5. 禁止对同一问题重复输出
     6. 禁止在没有明确逻辑错误时输出问题
-    7. 禁止将"表达不够完美"当作"逻辑错误"
-    8. 禁止将"可以优化"当作"必须修改"
-    9. 禁止对所谓的"表述不恰当"、"表述过于严格"当做错误点,如你不能将“禁止夜间施工”改为“应在增加照明条件下允许夜间施工”这样的建议端上来
+    7. 禁止将”表达不够完美”当作”逻辑错误”
+    8. 禁止将”可以优化”当作”必须修改”
+    9. 禁止对所谓的”表述不恰当”、”表述过于严格”当做错误点,如你不能将”禁止夜间施工”改为”应在增加照明条件下允许夜间施工”这样的建议端上来
     10. **禁止审查过于专业的知识**,你只是审查通用的语义逻辑关系,而并非需要你根据你的知识去审查过多的部分如涉及到架桥参数、施工参数等等一些列的问题,这些问题有后续流程会处理,你暂且跳过
     11. **禁止对专业知识进行点评**,如技术参数、技术规范、技术条文,你对这方面知识还是较为落后的,你不需要对这方便进行涉猎
-    12. 所有建议都要基于审查内容给出忠实建议,禁止给出不符合逻辑的错误建议,如建议将A修改成A,例如:"将'确定的'改为'确定的'(原字为'确',应为'确')",这种建议就是错误的
+    12. 所有建议都要基于审查内容给出忠实建议,禁止给出不符合逻辑的错误建议,如建议将A修改成A,例如:”将'确定的'改为'确定的'(原字为'确',应为'确')”,这种建议就是错误的
     13. **禁止回复**如A修改为A这种假回答,文段没有问题必须直接输出:无明显问题
+    14. **禁止审查词句语法问题**:错别字、多字、少字、重复字词、标点符号错误、”的地得”混用、语法结构等,这些问题由词句语法审查模块负责,不属于你的职责范围
+    15. **禁止审查敏感词问题**:政治敏感、商业机密、表述适宜性等,这些问题由敏感词审查模块负责,不属于你的职责范围
 
     必须遵守:
     1. 必须基于客观事实和逻辑规则判断
     2. 必须确保问题的真实性和严重性
-    3. 必须区分"逻辑错误"与"表达习惯"
-    4. 必须在不确定时选择"无明显问题"
+    3. 必须区分”逻辑错误”与”表达习惯”
+    4. 必须在不确定时选择”无明显问题”
     5. 必须保持高标准:宁缺毋滥
-    6. 给出的上下文可能不完整,你需要注意不同序号下,有些地方可能来源于不同模块,如前面4条是第(2)节内容,后面有明确申明如“(3)xxx规范”这样的那么前后文就没有参考价值,属于前后文不相关
+    6. 给出的上下文可能不完整,你需要注意不同序号下,有些地方可能来源于不同模块,如前面4条是第(2)节内容,后面有明确申明如”(3)xxx规范”这样的那么前后文就没有参考价值,属于前后文不相关
+
+    ## ⚠️ 严格禁止审查以下内容(非语义逻辑范畴)
+    你只负责检查语义逻辑层面的问题,以下类型的问题**一律跳过,不输出任何 issue**:
+    1. **词句语法问题**:错别字、多字/少字、重复字词、标点符号错误、”的地得”混用、语法结构错误 —— 这些由**词句语法审查模块**负责
+    2. **敏感词问题**:政治敏感、商业机密、表述不当、工程绝对化用语 —— 这些由**敏感词审查模块**负责
+    3. **专业参数/技术问题**:架桥参数、施工参数、技术规范 —— 由专业技术审查流程处理
+    4. **格式/排版问题**:段落缩进、字体大小、制表符等
+
+    遇到以上类型的问题,请忽略,仅审查逻辑矛盾、因果关系错误、条件结论不匹配、口语化表达四类问题。
     
     # 风险等级判定标准 (Risk Level)
     
@@ -162,8 +197,17 @@ semantic_logic_check:
 
   user_prompt_template: |
     # 审查任务
-    请对以下施工方案内容进行语义逻辑审查,严格按照系统提示词中的三类问题范围进行检查。
-    
+    请对以下施工方案内容进行语义逻辑审查,**仅**检查以下四类问题:
+    1. **逻辑矛盾** - 前后相互冲突的陈述
+    2. **因果关系错误** - 原因与结果之间不存在合理的逻辑关联
+    3. **条件与结论不匹配** - 给定条件无法推导出所述结论
+    4. **口语化表达** - 不符合施工方案严肃规范的口语化用语
+
+    ⚠️ **以下问题不属于本次审查范围,请跳过不输出**:
+    - 错别字、重复字词、标点符号错误、语法结构(由词句语法审查模块负责)
+    - 政治敏感、商业机密、表述不当(由敏感词审查模块负责)
+    - 技术参数、施工规范(由专业技术审查流程处理)
+
     ## 待审查内容:
     {review_content}
 
@@ -171,12 +215,12 @@ semantic_logic_check:
     1. 仅识别明确的逻辑矛盾、因果错误、条件结论不匹配问题
     2. 必须确保问题的真实性,不得对正确内容提出修改
     3. 不确定是否为问题时,必须输出"无明显问题"
-    
+
     ## 输出格式:
-    
+
     **情况1:未发现明确的语义逻辑问题**
     直接输出:无明显问题
-    
+
     **情况2:发现明确的语义逻辑问题**
     严格按照以下JSON格式输出(仅输出确认无误的问题):
     ```json
@@ -188,10 +232,10 @@ semantic_logic_check:
       "risk_level": "高风险/中风险/低风险(严格按照系统提示词中的标准判定)"
     }}
     ```
-    
+
     ## 特别提醒:
     - 如果内容表达虽不完美但逻辑正确,输出"无明显问题"
-    - 如果是专业术语的标准表达,输出"无明显问题"  
+    - 如果是专业术语的标准表达,输出"无明显问题"
     - 如果只是表达习惯差异,输出"无明显问题"
     - 保持高标准:宁可漏报,不可误报
 
@@ -314,34 +358,86 @@ sensitive_word_check:
   system_prompt: |
     你是施工方案敏感词审查专家,负责检查政治敏感和表述适宜性问题。
 
-    审查要求:
-    - 重点关注政治敏感、商业机密、表述不当、工程绝对化用语(如绝对不会出现事故、绝对不会有污染等等)
+    ## 核心原则
     - 你只需要考虑初筛找到的敏感词与原文,不需要你自行去找敏感词
-    - 通过给出的敏感词初筛内容,并联系上下文,确定初筛的关键词匹配的敏感词是否合理,如果合理则给出issue,如果初筛的敏感词在原文中的语义并无恶意则直接输出:无明显问题
-    - 例如:原文“应禁止工人在宿舍中赌博”,初筛内容:“赌博”;解释:原文中“赌博”二字无恶意;结论:无明显问题
-    - 风险等级分类:
-      * 高风险:影响审查结论、可能导致法律问题或严重安全隐患
-      * 中风险:影响专业表达、可能导致理解偏差或一般性问题
-      * 低风险:形式问题、不影响实质内容和安全
-    你需要注意:不要过于应激,不要反馈“提及xxx敏感词会导致不良反应”,不要没错找错,无中生有。请还需多多注意。
+    - **默认原则:敏感词出现在文档中本身就是问题,必须输出 issue。仅当上下文明确是”禁止/反对/防范”该敏感事物时才可豁免。**
+    - 初筛已经用 AC 自动机精确匹配了敏感词库,你不需要质疑初筛结果的准确性,你的任务是结合上下文判断该敏感词是否需要修正。
+
+    ## 敏感词分类与判定规则
+
+    ### 第一类:绝对敏感词(必须拦截,不可豁免)
+    以下类别敏感词无论上下文如何,均属于严重问题,**必须输出 issue**:
+    - **政治敏感**:国家领导人姓名、政治口号、政治运动名称、反动言论
+    - **暴力恐怖**:恐怖组织名称、暴力行为描述、武器/爆炸物交易信息
+    - **色情淫秽**:色情内容、淫秽描述
+    - **违法信息**:毒品制作、赌博平台、诈骗方法
+    - **分裂言论**:危害国家统一、民族团结的言论
+    - 示例:
+      - 原文中出现”打倒共产主义” → **必须报 issue**,这是政治敏感,不可豁免
+      - 原文中出现”出售雷管” → **必须报 issue**,这是危险品交易信息
+      - 原文中出现”习近平” → **必须报 issue**,领导人姓名不应出现在施工方案中
+      - 原文中出现”硝铵炸药配方” → **必须报 issue**,这是危险品制作信息
+
+    ### 第二类:上下文敏感词(需结合语境判断)
+    以下类别敏感词需要根据上下文判断是否豁免:
+    - **工程绝对化用语**:”绝对不会出现事故”、”绝对不会有污染”、”百分百安全”等
+    - **一般性违禁行为**:赌博、吸毒、酗酒等(当上下文是禁止/防范时放行)
+
+    ### 豁免条件(仅适用于第二类上下文敏感词)
+    仅当敏感词出现在以下语境中时才可放行:
+    - 明确的安全规范/禁止条款(如”严禁赌博”、”禁止酗酒”)
+    - 风险防范措施说明(如”制定应急预案防止XX”)
+    - 示例:
+      - 原文”应禁止工人在宿舍中赌博” → 赌博出现在禁止语境中 → **无明显问题**
+      - 原文”工人休息时可以打牌” → 初筛命中”赌博”近义词但上下文无禁止 → **应报 issue**
+
+    ## 风险等级分类
+    - **高风险**: 政治敏感、暴力恐怖、色情淫秽、分裂言论 —— 必须删除或彻底改写
+    - **中风险**: 违法信息、危险品交易、工程绝对化用语 —— 需要整改
+    - **低风险**: 一般性表述不当,不影响实质安全 —— 建议修改
+
+    ## 重要提醒
+    - **不要自行降级**:绝对敏感词即使出现在引用/举例中也是问题,施工方案不是讨论敏感话题的场所
+    - **不要漏报**:初筛已经帮你定位了敏感词,你只需确认是否属于豁免情形,不属于豁免就必须报 issue
+    - **不要误报**:非敏感词范畴的问题(语法、逻辑、技术参数)不属于你的职责
+
+    ## ⚠️ 严格禁止审查以下内容(非敏感词范畴)
+    你只负责检查敏感词层面的问题,以下类型的问题**一律跳过,不输出任何 issue**:
+    1. **词句语法问题**:错别字、多字/少字、重复字词、标点符号错误、”的地得”混用、语法结构错误 —— 这些由**词句语法审查模块**负责
+    2. **语义逻辑问题**:逻辑矛盾、因果关系错误、条件结论不匹配、事实错误 —— 这些由**语义逻辑审查模块**负责
+    3. **专业参数/技术问题**:架桥参数、施工参数、技术规范 —— 由专业技术审查流程处理
+
+    遇到以上类型的问题,请忽略,仅审查敏感词层面的问题。
 
   user_prompt_template: |
-    以下我初步查找到的敏感词内容:
+    以下我通过敏感词库初筛找到的敏感词内容:
     {review_references}
 
-    请根据以下内容作为上下文,确定敏感词是否合理:
+    请根据以下原文内容作为上下文,判断每个初筛命中的敏感词是否构成问题:
     {review_content}
 
-    输出格式:务必须严格按照以下标准JSON格式输出审查结果:
-    如果初筛敏感词在原文中并无恶意,请输出:无明显问题
-    如果发现初筛出的敏感词确实存在问题,请按以下格式输出:
-    location字段直接输出原字段内容,不得猜测
+    ## 判定规则
+    1. **绝对敏感词**(政治、暴力恐怖、色情、违法、分裂言论、领导人姓名)→ 无论上下文,**必须报 issue**
+    2. **上下文敏感词**(工程绝对化用语、一般违禁行为)→ 仅当在”禁止/防范/反对”语境中才可豁免,否则报 issue
+    3. 初筛结果是 AC 自动机精确匹配的,不需要你质疑命中是否正确
+
+    ⚠️ **以下问题不属于本次审查范围,请跳过不输出**:
+    - 错别字、重复字词、标点符号错误、语法结构(由词句语法审查模块负责)
+    - 逻辑矛盾、因果错误、条件结论不匹配(由语义逻辑审查模块负责)
+
+    ## 输出格式
+    **情况1:所有敏感词均属于豁免情形**
+    直接输出:无明显问题
+
+    **情况2:存在需要处理的敏感词**
+    严格按照以下JSON格式输出(一个敏感词对应一个 issue):
+    location字段直接输出包含该敏感词的原文段落,不得猜测
     ```json
     {{
-      "issue_point": "问题标题描述",
-      "location": "当前问题对应的原始条款内容及位置,如六、验收标准 (页码: 85),以及其语境上下文",
-      "suggestion": "具体的修改建议内容",
-      "reason": "问题的原因分析和依据说明",
-      "risk_level": ""
+      “issue_point”: “[敏感词类别]具体问题描述”,
+      “location”: “包含敏感词的原文内容及位置,如六、验收标准 (页码: 85),以及其语境上下文”,
+      “suggestion”: “具体的修改建议(如删除、替换、或改写)”,
+      “reason”: “说明为何这是敏感问题(注明敏感词类别和风险)”,
+      “risk_level”: “高风险/中风险/低风险”
     }}
     ```

+ 39 - 58
core/construction_review/component/reviewers/sensitive_word_check.py → core/construction_review/component/reviewers/sensitive_check_reviewer.py

@@ -1,25 +1,20 @@
 """
-敏感词查模块
-使用通用模型底座进行敏感词上下文审查
+敏感词查模块
+两阶段审查:关键词初筛 + LLM 二审
 """
 
 import time
 import asyncio
 from typing import Dict, Any
-from core.construction_review.component.reviewers.base_reviewer import ReviewResult
-from core.construction_review.component.reviewers.utils.prompt_loader import prompt_loader
-from foundation.ai.agent.generate.model_generate import generate_model_client
+from core.construction_review.component.reviewers.base_reviewer import ReviewResult, BaseReviewer
+from core.construction_review.component.reviewers.utils import check_sensitive_words_async
 from foundation.observability.logger.loggering import review_logger as logger
 
 
-class SensitiveWordLLMReviewer:
-    """敏感词 LLM 审查器"""
+class SensitiveCheckReviewer(BaseReviewer):
+    """敏感词审查器 — 关键词初筛 + LLM 审"""
 
-    def __init__(self):
-        """初始化敏感词 LLM 审查器"""
-        self.model_client = generate_model_client
-
-    async def check_sensitive_word(
+    async def check_sensitive(
         self,
         trace_id: str,
         review_content: str,
@@ -27,7 +22,7 @@ class SensitiveWordLLMReviewer:
         stage_name: str = None
     ) -> ReviewResult:
         """
-        执行敏感词 LLM 审查
+        执行敏感词审查
 
         Args:
             trace_id: 追踪ID
@@ -41,106 +36,93 @@ class SensitiveWordLLMReviewer:
         start_time = time.time()
 
         try:
-            logger.info(f"开始敏感词 LLM 审查,trace_id: {trace_id}, 内容长度: {len(review_content)}")
-
-            # 构造提示词参数
-            prompt_kwargs = {}
-            prompt_kwargs["review_content"] = review_content
-            prompt_kwargs["review_references"] = ""  # 添加空字符串,满足模板要求
-
-            # 获取提示词模板
-            prompt_template = prompt_loader.get_prompt_template(
-                "basic",
-                "sensitive_word_check",
-                **prompt_kwargs
-            )
+            logger.info(f"开始敏感词审查,trace_id: {trace_id}, 内容长度: {len(review_content)}")
 
-            # 格式化提示词消息
-            messages = prompt_template.format_messages()
+            first_results = await check_sensitive_words_async(review_content)
 
-            logger.info("调用敏感词 LLM 审查模型")
+            if first_results:
+                logger.info(f"检测到 {len(first_results)} 个敏感词,送入大模型二审")
 
-            # 使用 function_name 从 model_setting.yaml 加载模型配置
-            model_response = await self.model_client.get_model_generate_invoke(
-                trace_id=trace_id,
-                messages=messages,
-                function_name="grammar_check"
-            )
+                sensitive_words_info = []
+                for item in first_results:
+                    sensitive_words_info.append(
+                        f"敏感词: {item['word']}, 位置: {item['position']}-{item['end_position']}, 来源: {item['source']}"
+                    )
+                formatted_sensitive_words = "\n".join(sensitive_words_info)
 
-            logger.info(f"敏感词 LLM 审查模型响应成功,响应长度: {len(model_response)}")
+                logger.info(f"格式化敏感词信息:\n{formatted_sensitive_words}")
 
-            # 计算执行时间
-            execution_time = time.time() - start_time
+                result = await self.review(
+                    "sensitive_check", trace_id, "basic", "sensitive_word_check",
+                    review_content, formatted_sensitive_words,
+                    None, state, stage_name, timeout=60, function_name="sensitive_check"
+                )
 
-            # 构造审查结果
+                logger.info(f"sensitive_check 审查完成(已二审),耗时: {time.time() - start_time:.2f}s")
+                return result
+
+            # 未检测到敏感词
+            logger.info("未检测到敏感词,跳过二审")
+            execution_time = time.time() - start_time
             result = ReviewResult(
                 success=True,
-                details={
-                    "name": "sensitive_word_check",
-                    "response": model_response
-                },
+                details={"name": "sensitive_check", "response": "无明显问题"},
                 error_message=None,
                 execution_time=execution_time
             )
 
-            # 推送审查完成信息
             if state and state.get("progress_manager"):
                 review_result_data = {
-                    'name': 'sensitive_word_check',
+                    'name': 'sensitive_check',
                     'success': result.success,
                     'details': result.details,
                     'error_message': result.error_message,
                     'execution_time': result.execution_time,
                     'timestamp': time.time()
                 }
-
                 asyncio.create_task(
                     state["progress_manager"].update_stage_progress(
                         callback_task_id=state["callback_task_id"],
                         stage_name=stage_name,
                         current=None,
                         status="processing",
-                        message=f"sensitive_word_check 审查完成,耗时: {result.execution_time:.2f}s",
+                        message=f"sensitive_check 审查完成,未检测到敏感词,耗时: {result.execution_time:.2f}s",
                         issues=[review_result_data],
                         event_type="processing"
                     )
                 )
 
-            logger.info(f"sensitive_word_check 审查完成,耗时: {result.execution_time:.2f}s")
-
+            logger.info(f"sensitive_check 审查完成,未检测到敏感词,耗时: {execution_time:.2f}s")
             return result
 
         except Exception as e:
             execution_time = time.time() - start_time
-            error_msg = f"敏感词 LLM 审查失败: {str(e)}"
+            error_msg = f"敏感词审查失败: {str(e)}"
             logger.error(error_msg, exc_info=True)
 
-            # 返回失败结果
             result = ReviewResult(
                 success=False,
-                details={"name": "sensitive_word_check"},
+                details={"name": "sensitive_check"},
                 error_message=error_msg,
                 execution_time=execution_time
             )
 
-            # 推送失败信息
             if state and state.get("progress_manager"):
                 review_result_data = {
-                    'name': 'sensitive_word_check',
+                    'name': 'sensitive_check',
                     'success': False,
                     'details': result.details,
                     'error_message': error_msg,
                     'execution_time': execution_time,
                     'timestamp': time.time()
                 }
-
                 asyncio.create_task(
                     state["progress_manager"].update_stage_progress(
                         callback_task_id=state["callback_task_id"],
                         stage_name=stage_name,
                         current=None,
                         status="processing",
-                        message=f"sensitive_word_check 审查失败: {error_msg}",
+                        message=f"sensitive_check 审查失败: {error_msg}",
                         issues=[review_result_data],
                         event_type="processing"
                     )
@@ -149,5 +131,4 @@ class SensitiveWordLLMReviewer:
             return result
 
 
-# 全局单例实例
-sensitive_word_check_reviewer = SensitiveWordLLMReviewer()
+sensitive_check_reviewer = SensitiveCheckReviewer()

+ 1 - 2
core/construction_review/workflows/ai_review_workflow.py

@@ -312,8 +312,7 @@ class AIReviewWorkflow:
 
             # 2. 解析审查项配置
             review_func_mapping: Dict[str, Union[str, List[str]]] = {
-                'sensitive_word_check': 'sensitive_word_check',
-                'grammar_check': 'grammar_check',
+                'sensitive_word_check': 'grammar_check',  # 前端接口:sensitive_word_check = 词句语法审查
                 'semantic_logic_check': 'check_semantic_logic',
                 'completeness_check': 'check_completeness',
                 'timeliness_check': 'timeliness_reviewer',  # 统一入口

+ 1 - 15
core/construction_review/workflows/core_functions/ai_review_core_fun.py

@@ -92,7 +92,6 @@ class AIReviewCoreFun:
         self.file_id = task_file_info.file_id
         self.callback_task_id = task_file_info.callback_task_id
         self.user_id = task_file_info.user_id
-        self.review_config = task_file_info.review_config
         self.project_plan_type = task_file_info.project_plan_type
         
         self.max_concurrent = 20
@@ -381,20 +380,7 @@ class AIReviewCoreFun:
         logger.debug(f"执行审查: {trace_id} -> {func_name}")
 
         # 根据func_name构建对应的参数并调用
-        if func_name == "sensitive_word_check" and not is_complete_field:
-            raw_result = await method(trace_id, review_content, state, stage_name)
-            # 基础审查方法,放入 basic_compliance
-            return UnitReviewResult(
-                unit_index=chunk_index,
-                unit_content=chunk,
-                basic_compliance={func_name: raw_result},
-                technical_compliance={},
-                rag_enhanced={},
-                overall_risk=self._calculate_single_result_risk(raw_result),
-                is_sse_push=True
-            )
-
-        elif func_name == "grammar_check" and not is_complete_field:
+        if func_name == "grammar_check" and not is_complete_field:
             raw_result = await method(trace_id, review_content, state, stage_name)
             # 基础审查方法,放入 basic_compliance
             return UnitReviewResult(

+ 113 - 3
foundation/ai/agent/generate/model_generate.py

@@ -13,10 +13,114 @@ from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage
 from foundation.ai.models.model_handler import model_handler
 from foundation.observability.logger.loggering import review_logger as logger
 import asyncio
+import re
 import time
 from typing import Optional, Callable, Any, List, Union
 
 
+# ============================================================
+# 思考内容过滤(统一收敛在调用层)
+#
+# Qwen3.5 等模型在 enable_thinking=True 时会先输出 <think>...</think>
+# 块再给出最终答案。所有业务方都不需要思考过程,统一在此处去除,
+# 避免每个调用点重复实现,也防止漏处理导致思考内容污染输出。
+# ============================================================
+_THINK_BLOCK_PATTERN = re.compile(r"<think>.*?</think>\s*", re.DOTALL)
+_DANGLING_THINK_PATTERN = re.compile(r"<think>[\s\S]*$")
+
+
+def _strip_thinking_content(content: str) -> str:
+    """去除完整响应中的 <think>...</think> 块。
+
+    - 完整闭合块:整段去除(含尾随空白)
+    - 仅 <think> 无 </think>(被截断):从 <think> 起全部丢弃,记录警告
+    - 不含思考标签:原文返回
+    """
+    if not content:
+        return content
+    cleaned = _THINK_BLOCK_PATTERN.sub("", content)
+    if "<think>" in cleaned:
+        cleaned = _DANGLING_THINK_PATTERN.sub("", cleaned)
+        logger.warning("[模型调用] 响应包含未闭合的 <think> 块,已截断丢弃")
+    return cleaned.strip()
+
+
+class _ThinkingBlockStreamFilter:
+    """流式响应中过滤 <think>...</think> 块的状态机。
+
+    处理 chunk 边界穿过标签的情况(如先收到 "<thi"、下次再到 "nk>正文"),
+    保证调用方拿到的流不会泄漏任何思考片段。
+
+    用法:
+        flt = _ThinkingBlockStreamFilter()
+        for chunk in stream:
+            cleaned = flt.feed(chunk)
+            if cleaned:
+                yield cleaned
+        tail = flt.flush()
+        if tail:
+            yield tail
+    """
+
+    _OPEN = "<think>"
+    _CLOSE = "</think>"
+
+    def __init__(self):
+        self._buf = ""
+        self._inside = False
+
+    def feed(self, chunk: str) -> str:
+        """喂入一个 chunk,返回此刻应输出的内容(可能为空字符串)。"""
+        if not chunk:
+            return ""
+        self._buf += chunk
+        out = []
+        while True:
+            if self._inside:
+                idx = self._buf.find(self._CLOSE)
+                if idx == -1:
+                    keep_len = self._partial_match_len(self._buf, self._CLOSE)
+                    self._buf = self._buf[-keep_len:] if keep_len else ""
+                    break
+                self._buf = self._buf[idx + len(self._CLOSE):].lstrip()
+                self._inside = False
+            else:
+                idx = self._buf.find(self._OPEN)
+                if idx == -1:
+                    keep_len = self._partial_match_len(self._buf, self._OPEN)
+                    if keep_len:
+                        out.append(self._buf[:-keep_len])
+                        self._buf = self._buf[-keep_len:]
+                    else:
+                        out.append(self._buf)
+                        self._buf = ""
+                    break
+                if idx > 0:
+                    out.append(self._buf[:idx])
+                self._buf = self._buf[idx + len(self._OPEN):]
+                self._inside = True
+        return "".join(out)
+
+    def flush(self) -> str:
+        """流结束时调用,返回缓冲区剩余可输出内容。"""
+        if self._inside:
+            logger.warning("[模型流式调用] 流结束时仍在 <think> 块内,已丢弃尾部")
+            self._buf = ""
+            return ""
+        result = self._buf
+        self._buf = ""
+        return result
+
+    @staticmethod
+    def _partial_match_len(buf: str, tag: str) -> int:
+        """返回 buf 末尾匹配 tag 前缀的最大长度(避免标签被切断后误输出)。"""
+        max_n = min(len(tag) - 1, len(buf))
+        for n in range(max_n, 0, -1):
+            if buf[-n:] == tag[:n]:
+                return n
+        return 0
+
+
 def _sync_retry_with_backoff(
     func: Callable,
     *args,
@@ -260,7 +364,7 @@ class GenerateModelClient:
 
             elapsed_time = time.time() - start_time
             logger.info(f"[模型调用] 成功 trace_id: {trace_id}, 耗时: {elapsed_time:.2f}s")
-            return response.content
+            return _strip_thinking_content(response.content)
 
         except asyncio.TimeoutError:
             elapsed_time = time.time() - start_time
@@ -452,7 +556,7 @@ class GenerateModelClient:
 
             elapsed_time = time.time() - start_time
             logger.info(f"[模型调用-同步] 成功 trace_id: {trace_id}, 耗时: {elapsed_time:.2f}s")
-            return response.content
+            return _strip_thinking_content(response.content)
 
         except Exception as e:
             elapsed_time = time.time() - start_time
@@ -538,12 +642,18 @@ class GenerateModelClient:
             response = llm_to_use.stream(final_messages)
 
             chunk_count = 0
+            think_filter = _ThinkingBlockStreamFilter()
             for chunk in response:
                 chunk_count += 1
                 if hasattr(chunk, 'content') and chunk.content:
-                    yield chunk.content
+                    cleaned = think_filter.feed(chunk.content)
+                    if cleaned:
+                        yield cleaned
                 elif chunk:
                     yield chunk
+            tail = think_filter.flush()
+            if tail:
+                yield tail
 
             elapsed_time = time.time() - start_time
             logger.info(f"[模型流式调用] 成功 trace_id: {trace_id}, 生成块数: {chunk_count}, 耗时: {elapsed_time:.2f}s")

+ 3 - 0
foundation/ai/models/model_handler.py

@@ -933,6 +933,7 @@ class ModelHandler:
                 model=model_id,
                 api_key="dummy",  # 本地模型使用虚拟API key
                 timeout=self.REQUEST_TIMEOUT,
+                tiktoken_enabled=False,
             )
 
             logger.info(f"本地Qwen3-Embedding-8B模型初始化成功: {model_id}")
@@ -979,6 +980,7 @@ class ModelHandler:
                 model=model_id,
                 api_key=api_key,
                 timeout=self.REQUEST_TIMEOUT,
+                tiktoken_enabled=False,
             )
 
             logger.info(f"硅基流动Embedding模型初始化成功: {model_id} (dimensions: {dimensions})")
@@ -1126,6 +1128,7 @@ class ModelHandler:
                 model=model_id,
                 api_key=api_key,
                 timeout=self.REQUEST_TIMEOUT,
+                tiktoken_enabled=False,
             )
 
             logger.info(f"蜀天Qwen3-Embedding-8B模型初始化成功: {model_id}")

+ 391 - 0
utils_test/Model_Test/test_thinking_filter_and_concurrency.py

@@ -0,0 +1,391 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+单元测试:思考内容过滤 & 并发安全验证
+
+无网络依赖,使用 stub + mock 模拟模型调用,覆盖以下能力:
+1. _strip_thinking_content:完整内容过滤的各种边界
+2. _ThinkingBlockStreamFilter:流式过滤(含 chunk 边界穿过 <think>/</think>)
+3. GenerateModelClient 端到端:function_name → 思考开关 → 返回过滤
+4. 并发场景:50 个混合模式请求,验证缓存实例不被污染、内容不串话
+
+运行:
+    python utils_test/Model_Test/test_thinking_filter_and_concurrency.py
+"""
+
+import sys
+import asyncio
+import types
+import importlib.util
+import unittest
+from pathlib import Path
+from typing import List, Tuple
+from unittest.mock import MagicMock
+
+
+PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
+
+
+# ============================================================
+# Step 1: 注入 stub 依赖(本地环境无 langchain/yaml 等包)
+# ============================================================
+
+def _make_module(name: str, **attrs) -> types.ModuleType:
+    mod = types.ModuleType(name)
+    for k, v in attrs.items():
+        setattr(mod, k, v)
+    sys.modules[name] = mod
+    return mod
+
+
+_make_module("langchain_core")
+_make_module("langchain_core.prompts",
+             ChatPromptTemplate=type("ChatPromptTemplate", (), {}))
+
+
+class BaseMessage:
+    def __init__(self, content=""):
+        self.content = content
+
+
+class SystemMessage(BaseMessage):
+    pass
+
+
+class HumanMessage(BaseMessage):
+    pass
+
+
+_make_module("langchain_core.messages",
+             BaseMessage=BaseMessage,
+             SystemMessage=SystemMessage,
+             HumanMessage=HumanMessage)
+
+_make_module("foundation")
+_make_module("foundation.ai")
+_make_module("foundation.ai.models")
+_make_module("foundation.ai.agent")
+_make_module("foundation.ai.agent.generate")
+_make_module("foundation.observability")
+_make_module("foundation.observability.logger")
+
+
+class _SilentLogger:
+    def info(self, *a, **k): pass
+    def debug(self, *a, **k): pass
+    def warning(self, *a, **k): pass
+    def error(self, *a, **k): pass
+
+
+_make_module("foundation.observability.logger.loggering",
+             review_logger=_SilentLogger())
+
+
+# Mock LangChain ChatOpenAI:bind() 返回新实例(与真实 RunnableBinding 一致),
+# ainvoke/stream 根据当前 extra_body 决定是否吐 <think> 块(模拟蜀天后端默认开启思考)
+class MockChatOpenAI:
+    def __init__(self, model_name="mock", extra_body=None):
+        self.model_name = model_name
+        self.extra_body = dict(extra_body or {})
+
+    def bind(self, **kwargs):
+        new_eb = dict(self.extra_body)
+        if "extra_body" in kwargs:
+            new_eb.update(kwargs["extra_body"])
+        return MockChatOpenAI(self.model_name, extra_body=new_eb)
+
+    def _is_thinking_on(self) -> bool:
+        ck = self.extra_body.get("chat_template_kwargs", {})
+        return ck.get("enable_thinking", True)  # 模拟服务端默认 True
+
+    async def ainvoke(self, messages, **kwargs):
+        if self._is_thinking_on():
+            content = (
+                "<think>\n推理过程:分析问题 X 的步骤...\n经过推导...\n</think>\n\n"
+                f"答案:模型 {self.model_name} 处理 {len(messages)} 条消息"
+            )
+        else:
+            content = f"答案:模型 {self.model_name} 处理 {len(messages)} 条消息"
+        await asyncio.sleep(0.01)  # 制造并发竞态机会
+        return MagicMock(content=content)
+
+    def invoke(self, messages, **kwargs):
+        if self._is_thinking_on():
+            content = (
+                "<think>推理过程</think>\n\n"
+                f"同步答案:{self.model_name}"
+            )
+        else:
+            content = f"同步答案:{self.model_name}"
+        return MagicMock(content=content)
+
+    def stream(self, messages, **kwargs):
+        if self._is_thinking_on():
+            full = "<think>\n推理过程\n</think>\n\n这是流式答案"
+        else:
+            full = "这是流式答案"
+        # 故意切到 4 字符一组,逼出 chunk 边界穿越 <think>/</think> 标签
+        for i in range(0, len(full), 4):
+            chunk = MagicMock()
+            chunk.content = full[i:i + 4]
+            yield chunk
+
+
+class MockModelHandler:
+    def __init__(self):
+        self._cache = {}
+
+    def get_model_by_name(self, model_type=None):
+        key = model_type or "default"
+        if key not in self._cache:
+            self._cache[key] = MockChatOpenAI(model_name=key)
+        return self._cache[key]
+
+    def get_models(self):
+        return self.get_model_by_name("default")
+
+
+mock_handler = MockModelHandler()
+_make_module("foundation.ai.models.model_handler",
+             model_handler=mock_handler,
+             ModelHandler=MockModelHandler,
+             get_models=lambda: mock_handler.get_models())
+
+# 配置 stub:模拟 model_setting.yaml 的关键映射
+_FAKE_THINKING = {
+    "outline_chapter_revise": False,        # 编写:非思考
+    "catalog_integrity_review": True,       # 目录审查:思考
+    "doc_classification_tertiary": False,
+    "default": True,                        # 危险默认值(云端真实情况)
+}
+_FAKE_MODEL = {
+    "outline_chapter_revise": "shutian_qwen3_5_122b",
+    "catalog_integrity_review": "shutian_qwen3_5_122b",
+    "doc_classification_tertiary": "shutian_qwen3_5_122b",
+    "default": "shutian_qwen3_5_122b",
+}
+_make_module("foundation.ai.models.model_config_loader",
+             get_model_for_function=lambda n: _FAKE_MODEL.get(n, _FAKE_MODEL["default"]),
+             get_thinking_mode_for_function=lambda n: _FAKE_THINKING.get(n, _FAKE_THINKING["default"]))
+
+
+# ============================================================
+# Step 2: 加载 model_generate.py(被测对象)
+# ============================================================
+_target = PROJECT_ROOT / "foundation" / "ai" / "agent" / "generate" / "model_generate.py"
+_spec = importlib.util.spec_from_file_location("mg_under_test", _target)
+mg = importlib.util.module_from_spec(_spec)
+_spec.loader.exec_module(mg)
+
+
+# ============================================================
+# Step 3: 测试用例
+# ============================================================
+
+class TestStripThinkingContent(unittest.TestCase):
+    """_strip_thinking_content 单元测试"""
+
+    def test_complete_block(self):
+        s = "<think>推理过程</think>\n\n最终答案"
+        self.assertEqual(mg._strip_thinking_content(s), "最终答案")
+
+    def test_multiple_blocks(self):
+        s = "<think>think1</think>段A<think>think2</think>段B"
+        self.assertEqual(mg._strip_thinking_content(s), "段A段B")
+
+    def test_dangling_block(self):
+        s = "正文段\n<think>推理被截断"
+        self.assertEqual(mg._strip_thinking_content(s), "正文段")
+
+    def test_no_thinking(self):
+        self.assertEqual(mg._strip_thinking_content("纯回答内容"), "纯回答内容")
+
+    def test_empty_and_none(self):
+        self.assertEqual(mg._strip_thinking_content(""), "")
+        self.assertIsNone(mg._strip_thinking_content(None))
+
+    def test_multiline_block(self):
+        s = "<think>\n第一行\n第二行\n第三行\n</think>\n\n答案"
+        self.assertEqual(mg._strip_thinking_content(s), "答案")
+
+    def test_block_at_end(self):
+        s = "答案先行<think>反思</think>"
+        self.assertEqual(mg._strip_thinking_content(s), "答案先行")
+
+
+class TestStreamFilter(unittest.TestCase):
+    """_ThinkingBlockStreamFilter 流式过滤测试"""
+
+    def _drive(self, chunks: List[str]) -> str:
+        flt = mg._ThinkingBlockStreamFilter()
+        out = []
+        for c in chunks:
+            r = flt.feed(c)
+            if r:
+                out.append(r)
+        tail = flt.flush()
+        if tail:
+            out.append(tail)
+        return "".join(out)
+
+    def test_single_chunk_with_block(self):
+        self.assertEqual(self._drive(["<think>x</think>正文"]), "正文")
+
+    def test_split_open_tag(self):
+        # chunk 边界切到 <thi|nk>
+        self.assertEqual(self._drive(["<thi", "nk>推理</think>正文"]), "正文")
+
+    def test_split_close_tag(self):
+        # chunk 边界切到 </thi|nk>
+        self.assertEqual(self._drive(["<think>推理</thi", "nk>正文"]), "正文")
+
+    def test_split_in_middle_of_block(self):
+        chunks = ["<think>", "推理1", "推理2", "</think>", "答案"]
+        self.assertEqual(self._drive(chunks), "答案")
+
+    def test_no_thinking_passes_through(self):
+        self.assertEqual(self._drive(["普通", "答案", "内容"]), "普通答案内容")
+
+    def test_dangling_flush_drops(self):
+        flt = mg._ThinkingBlockStreamFilter()
+        first = flt.feed("正文")          # 应输出 "正文"
+        second = flt.feed("<think>未完成")  # 进入 think 内
+        tail = flt.flush()                # 在 think 内 → 丢弃
+        self.assertEqual(first, "正文")
+        self.assertEqual(second, "")
+        self.assertEqual(tail, "")
+
+    def test_multiple_blocks_streamed(self):
+        s = "<think>a</think>段1<think>b</think>段2"
+        self.assertEqual(self._drive([s]), "段1段2")
+
+    def test_realistic_3char_chunks(self):
+        full = "<think>\n推理过程\n</think>\n\n这是答案"
+        chunks = [full[i:i + 3] for i in range(0, len(full), 3)]
+        result = self._drive(chunks)
+        self.assertIn("这是答案", result)
+        self.assertNotIn("<think>", result)
+        self.assertNotIn("</think>", result)
+        self.assertNotIn("推理过程", result)
+
+
+class TestEndToEndInvoke(unittest.IsolatedAsyncioTestCase):
+    """通过 mock 验证 GenerateModelClient 端到端"""
+
+    async def test_thinking_off_via_function_name(self):
+        client = mg.GenerateModelClient()
+        result = await client.get_model_generate_invoke(
+            trace_id="t1",
+            prompt="2+2",
+            function_name="outline_chapter_revise",  # config: false
+        )
+        self.assertNotIn("<think>", result)
+        self.assertNotIn("</think>", result)
+        self.assertNotIn("推理过程", result)
+        self.assertIn("答案", result)
+
+    async def test_thinking_on_block_stripped(self):
+        client = mg.GenerateModelClient()
+        result = await client.get_model_generate_invoke(
+            trace_id="t2",
+            prompt="解释勾股定理",
+            function_name="catalog_integrity_review",  # config: true
+        )
+        # 即使开启思考,底层也会过滤
+        self.assertNotIn("<think>", result)
+        self.assertNotIn("</think>", result)
+        self.assertNotIn("推理过程", result)
+        self.assertIn("答案", result)
+
+    async def test_default_config_thinking_still_filtered(self):
+        """函数名未匹配 → 走 default(true),但仍然被过滤"""
+        client = mg.GenerateModelClient()
+        result = await client.get_model_generate_invoke(
+            trace_id="t3",
+            prompt="X",
+            function_name="not_in_yaml_xxx",  # 未匹配 → 走 default
+        )
+        self.assertNotIn("<think>", result)
+        self.assertIn("答案", result)
+
+
+class TestStreamEndToEnd(unittest.TestCase):
+    """流式调用端到端"""
+
+    def test_stream_thinking_on_filtered(self):
+        client = mg.GenerateModelClient()
+        chunks = list(client.get_model_generate_stream(
+            trace_id="s1",
+            prompt="X",
+            function_name="catalog_integrity_review",
+        ))
+        joined = "".join(c if isinstance(c, str) else "" for c in chunks)
+        self.assertNotIn("<think>", joined)
+        self.assertNotIn("</think>", joined)
+        self.assertNotIn("推理过程", joined)
+        self.assertIn("流式答案", joined)
+
+
+class TestConcurrentNoCrosstalk(unittest.IsolatedAsyncioTestCase):
+    """并发场景:思考/非思考混合不串话,缓存实例不被污染"""
+
+    async def test_50_concurrent_mixed_modes(self):
+        client = mg.GenerateModelClient()
+        N = 50
+
+        async def run(i: int) -> Tuple[int, str, str]:
+            fn = "outline_chapter_revise" if i % 2 == 0 else "catalog_integrity_review"
+            r = await client.get_model_generate_invoke(
+                trace_id=f"concur-{i}",
+                prompt=f"请求 {i}",
+                function_name=fn,
+            )
+            return (i, fn, r)
+
+        results = await asyncio.gather(*(run(i) for i in range(N)))
+
+        leaks = [(i, fn) for i, fn, r in results
+                 if "<think>" in r or "</think>" in r or "推理过程" in r]
+        self.assertEqual(leaks, [], f"思考内容泄漏到 {len(leaks)} 个返回中: {leaks[:3]}")
+        self.assertEqual(len(results), N)
+        no_answer = [(i, r) for i, fn, r in results if "答案" not in r]
+        self.assertEqual(no_answer, [], f"个别结果丢失答案: {no_answer[:3]}")
+
+    async def test_cached_instance_not_polluted(self):
+        """关键:bind() 不能污染 model_handler 缓存的 ChatOpenAI 实例"""
+        client = mg.GenerateModelClient()
+        cached = mock_handler.get_model_by_name("shutian_qwen3_5_122b")
+        # 跑 30 个混合并发
+        await asyncio.gather(*(
+            client.get_model_generate_invoke(
+                trace_id=f"iso-{i}",
+                prompt="x",
+                function_name="catalog_integrity_review" if i % 2 else "outline_chapter_revise"
+            ) for i in range(30)
+        ))
+        # cached 实例的 extra_body 应当始终为空 —— bind 走的是 RunnableBinding 副本
+        self.assertEqual(
+            cached.extra_body, {},
+            f"缓存实例被污染,extra_body={cached.extra_body}(这意味着并发请求会串话)"
+        )
+
+
+# ============================================================
+# 入口
+# ============================================================
+def run_all():
+    loader = unittest.TestLoader()
+    suite = unittest.TestSuite()
+    for cls in (TestStripThinkingContent,
+                TestStreamFilter,
+                TestEndToEndInvoke,
+                TestStreamEndToEnd,
+                TestConcurrentNoCrosstalk):
+        suite.addTests(loader.loadTestsFromTestCase(cls))
+    runner = unittest.TextTestRunner(verbosity=2)
+    result = runner.run(suite)
+    return 0 if result.wasSuccessful() else 1
+
+
+if __name__ == "__main__":
+    sys.exit(run_all())

+ 6 - 79
views/construction_review/launch_review.py

@@ -46,13 +46,9 @@ class LaunchReviewRequest(BaseModel):
         ...,
         description="倾向性审查角色,暂定为 default_role"
     )
-    review_config: Optional[List[str]] = Field(
-        None,
-        description="审查配置列表,包含的项为启用状态(审查维度枚举值)。与review_item_config互斥"
-    )
     review_item_config: Optional[List[str]] = Field(
         None,
-        description="审查项配置列表,格式为「章节code_审查维度」(如 basis_sensitive_word_check)。与review_config互斥"
+        description="审查项配置列表,格式为「章节code_审查维度」(如 basis_sensitive_word_check)"
     )
     project_plan_type: str = Field(
         ...,
@@ -74,63 +70,6 @@ class LaunchReviewResponse(BaseModel):
     data: dict
 
 
-def validate_review_config(review_config: List[str]) -> None:
-    """验证审查配置参数"""
-    # 检查review_config是否为空
-    logger.info(f"审查配置: {review_config}")
-    if not review_config or len(review_config) == 0:
-        raise LaunchReviewErrors.enum_type_cannot_be_null()
-
-    # 支持的审查项枚举值
-    supported_review_items = {
-        'sensitive_word_check',       # 敏感词审查
-        'grammar_check',              # 词句语法检查
-        'semantic_logic_check',       # 语义逻辑审查
-        'completeness_check',         # 条文完整性审查
-        'timeliness_check',           # 时效性审查
-        'reference_check',            # 规范性审查
-        'sensitive_check',            # 敏感信息检查
-        'non_parameter_compliance_check',  # 非参数合规性检查功能
-        'parameter_compliance_check', # 参数合规性检查功能
-    }
-
-    # 检查是否包含不支持的审查项
-    unsupported_items = set(review_config) - supported_review_items
-    if unsupported_items:
-        raise LaunchReviewErrors.enum_type_invalid()
-
-def validate_review_config_mutually_exclusive(
-    review_config: Optional[List[str]],
-    review_item_config: Optional[List[str]]
-) -> None:
-    """
-    验证 review_config 与 review_item_config 互斥性
-
-    规则:
-    1. 两者不能同时提供(互斥)
-    2. 两者不能同时为空(必须提供其中一个)
-
-    Args:
-        review_config: 审查配置列表(审查维度枚举值)
-        review_item_config: 审查项配置列表(章节_审查维度格式)
-
-    Raises:
-        HTTPException: 当两者同时提供或同时为空时抛出异常
-    """
-    # 判断字段是否存在(只判断是否为 None,不判断长度)
-    has_review_config = review_config is not None
-    has_review_item_config = review_item_config is not None
-
-    # 情况1:两者都提供了 - 互斥错误(包括空列表的情况)
-    if has_review_config and has_review_item_config:
-        raise LaunchReviewErrors.mutually_exclusive_config()
-
-    # 情况2:两者都为空 - 必须提供一个
-    if not has_review_config and not has_review_item_config:
-        raise LaunchReviewErrors.review_config_required()
-
-    logger.info(f"审查配置验证通过: 使用{'review_config' if has_review_config else 'review_item_config'}")
-
 def validate_review_item_config(review_item_config: List[str]) -> None:
     """
     验证审查项配置参数合法性
@@ -161,7 +100,7 @@ def validate_review_item_config(review_item_config: List[str]) -> None:
             "acceptance", "other"
         },
         "review_dimensions": {
-            "sensitive_word_check", "grammar_check", "semantic_logic_check", "completeness_check",
+            "sensitive_word_check", "semantic_logic_check", "completeness_check",
             "timeliness_check", "reference_check", "sensitive_check",
             "non_parameter_compliance_check", "parameter_compliance_check"
         }
@@ -271,7 +210,6 @@ async def launch_review_sse(request_data: LaunchReviewRequest):
     callback_task_id = request_data.callback_task_id
     TraceContext.set_trace_id(callback_task_id)
     user_id = request_data.user_id
-    review_config = request_data.review_config
     review_item_config = request_data.review_item_config
     project_plan_type = request_data.project_plan_type
     tendency_review_role = request_data.tendency_review_role
@@ -282,20 +220,10 @@ async def launch_review_sse(request_data: LaunchReviewRequest):
     # 验证用户标识
     validate_user_id(user_id)
 
-    # 验证审查配置互斥性(优先验证互斥关系)
-    validate_review_config_mutually_exclusive(review_config, review_item_config)
-
-    # 根据提供的配置类型进行相应验证(只判断是否为 None)
-    if review_config is not None:
-        # 使用 review_config 时的验证
-        if len(review_config) == 0:
-            raise LaunchReviewErrors.review_config_required()  # 提供了空列表,提示必须提供有效值
-        validate_review_config(review_config)
-    elif review_item_config is not None:
-        # 使用 review_item_config 时的验证
-        if len(review_item_config) == 0:
-            raise LaunchReviewErrors.review_config_required()  # 提供了空列表,提示必须提供有效值
-        validate_review_item_config(review_item_config)
+    # 验证审查项配置
+    if review_item_config is None or len(review_item_config) == 0:
+        raise LaunchReviewErrors.review_config_required()
+    validate_review_item_config(review_item_config)
 
     # 验证工程方案类型
     validate_project_plan_type(project_plan_type)
@@ -367,7 +295,6 @@ async def launch_review_sse(request_data: LaunchReviewRequest):
                 # 添加审查配置到文件信息,并确保使用当前正确的callback_task_id
                 file_info.update({
                     'user_id': user_id,
-                    'review_config': review_config,
                     'review_item_config': review_item_config,
                     'project_plan_type': project_plan_type,
                     'tendency_review_role': tendency_review_role,

+ 8 - 8
views/construction_review/schemas/error_schemas.py

@@ -203,14 +203,14 @@ class ErrorCodes:
     QDSC018 = {
         "code": "QDSC018",
         "error_type": "MUTUALLY_EXCLUSIVE_CONFIG",
-        "message": "review_config与review_item_config互斥,只能提供其中一个",
+        "message": "[已废弃] review_config已移除,仅支持review_item_config",
         "status_code": 400
     }
 
     QDSC019 = {
         "code": "QDSC019",
         "error_type": "REVIEW_CONFIG_REQUIRED",
-        "message": "必须提供review_config或review_item_config中的其中一个",
+        "message": "必须提供review_item_config(章节_审查维度列表)",
         "status_code": 400
     }
 
@@ -492,16 +492,16 @@ class LaunchReviewErrors:
 
     @staticmethod
     def mutually_exclusive_config():
-        """review_config与review_item_config互斥"""
-        logger.error("review_config与review_item_config同时提供,但两者互斥")
-        message = "参数错误:review_config(审查维度列表)与review_item_config(章节_审查维度列表)互斥,只能提供其中一个,不能同时提供。"
+        """[已废弃] review_config已移除,仅支持review_item_config"""
+        logger.error("review_config已废弃,请使用review_item_config")
+        message = "参数错误:review_config 已废弃,请使用 review_item_config(章节_审查维度列表)。"
         return create_http_exception(ErrorCodes.QDSC018, message)
 
     @staticmethod
     def review_config_required():
-        """必须提供review_config或review_item_config"""
-        logger.error("未提供review_config或review_item_config")
-        message = "参数错误:必须提供review_config(审查维度列表)或review_item_config(章节_审查维度列表)中的其中一个。"
+        """必须提供review_item_config"""
+        logger.error("未提供review_item_config")
+        message = "参数错误:必须提供 review_item_config(章节_审查维度列表)。"
         return create_http_exception(ErrorCodes.QDSC019, message)
 
     @staticmethod