Browse Source

质控正反例修改

zhaohan 7 months ago
parent
commit
fad14d4e5d

+ 36 - 30
ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/xinference/RerankUtil.java

@@ -41,36 +41,42 @@ public class RerankUtil {
                 .build();
 
         // 3. 调用接口
-        Response response = client.newCall(request).execute();
-        if (!response.isSuccessful()) {
-            throw new RuntimeException("Rerank API 调用失败: " + response);
-        }
-
-        // 4. 解析返回 JSON
-        String responseStr = response.body().string();
-        JsonNode root = objectMapper.readTree(responseStr);
-        JsonNode results = root.get("results"); // 假设返回里有 data 字段 [{index:0, score:xx}, ...]
-
-        // 5. 排序 nearestList
-        List<Map<String, Object>> sortedList = new ArrayList<>();
-        List<Map<String, Object>> rerankResults = new ArrayList<>();
-
-        for (JsonNode node : results) {
-            Map<String, Object> item = new HashMap<>();
-            item.put("index", node.get("index").asInt());
-            item.put("score", node.get("relevance_score").asDouble());
-            rerankResults.add(item);
-        }
-
-        rerankResults.stream()
+        try (Response response = client.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                // 获取错误详情
+                String errorBody = response.body() != null ? response.body().string() : "无响应体";
+                throw new RuntimeException(String.format(
+                    "Rerank API 调用失败: code=%d, message=%s, url=%s, errorBody=%s",
+                    response.code(), response.message(), rerankUrl, errorBody
+                ));
+            }
+
+            // 4. 解析返回 JSON
+            String responseStr = response.body().string();
+            JsonNode root = objectMapper.readTree(responseStr);
+            JsonNode results = root.get("results"); // 假设返回里有 data 字段 [{index:0, score:xx}, ...]
+
+            // 5. 排序 nearestList
+            List<Map<String, Object>> sortedList = new ArrayList<>();
+            List<Map<String, Object>> rerankResults = new ArrayList<>();
+
+            for (JsonNode node : results) {
+                Map<String, Object> item = new HashMap<>();
+                item.put("index", node.get("index").asInt());
+                item.put("score", node.get("relevance_score").asDouble());
+                rerankResults.add(item);
+            }
+
+            rerankResults.stream()
 //                .filter(r -> (Double) r.get("score") >= 0.5)  // 过滤:只保留 score >= 5 的结果
-                .sorted((a, b) -> Double.compare((Double) b.get("score"), (Double) a.get("score"))) // 按 score 降序排序
-                .forEach(r -> {
-                    int idx = (Integer) r.get("index");
-                    Map<String, Object> original = nearestList.get(idx);
-                    sortedList.add(original);
-                });
-
-        return sortedList;
+                    .sorted((a, b) -> Double.compare((Double) b.get("score"), (Double) a.get("score"))) // 按 score 降序排序
+                    .forEach(r -> {
+                        int idx = (Integer) r.get("index");
+                        Map<String, Object> original = nearestList.get(idx);
+                        sortedList.add(original);
+                    });
+
+            return sortedList;
+        }
     }
 }

+ 252 - 37
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/MedicalRecordQc123Service.java

@@ -13,10 +13,13 @@ import org.ruoyi.chat.util.HttpUtils;
 import org.ruoyi.domain.MedicalRecordResult;
 import org.ruoyi.domain.PatientRecord;
 import org.ruoyi.domain.bo.QualityControlRuleBo;
+import org.ruoyi.domain.bo.QualityControlRuleExampleBo;
 import org.ruoyi.domain.vo.QualityControlRuleVo;
+import org.ruoyi.domain.vo.QualityControlRuleExampleVo;
 import org.ruoyi.mapper.MedicalRecordResultMapper;
 import org.ruoyi.mapper.PatientRecordMapper;
 import org.ruoyi.service.IQualityControlRuleService;
+import org.ruoyi.service.IQualityControlRuleExampleService;
 import org.ruoyi.service.VectorStoreService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -54,9 +57,14 @@ public class MedicalRecordQc123Service {
     @Autowired
     private MedicalRecordResultMapper resultMapper;
 
+
+
     @Autowired
     private IQualityControlRuleService qualityControlRuleService;
 
+    @Autowired
+    private IQualityControlRuleExampleService qualityControlRuleExampleService;
+
     @Resource
     private VectorStoreService vectorStoreService;
 
@@ -338,7 +346,8 @@ public class MedicalRecordQc123Service {
 
                 for (QualityControlRuleVo rule : rules) {
                     try {
-                        List<Map<String, Object>> maps = fetchMatchingRules(rule.getRuleContent());
+                        // 用病历原文检索相关正反例
+                        List<Map<String, Object>> maps = fetchMatchingRules(rule.getRuleCode(), step.content);
                         List<Object> ruleFailures = batchQcForRule(recordId, type, rule, step.content, batchSize, maps, record);
                         allFailures.addAll(ruleFailures);
                         log.info("✅ 规则 [{}] type={} 分批质控完成(批次大小={}),不通过 {} 条", rule.getRuleCode(), type, batchSize, ruleFailures.size());
@@ -346,7 +355,8 @@ public class MedicalRecordQc123Service {
                         log.error("❌ 规则 [{}] type={} 分批质控失败: {}", rule.getRuleCode(), type, e.getMessage());
                         // ✅ 如果是模型崩溃异常,立即终止整个病历的质控
                         if (e.getMessage() != null && (e.getMessage().contains("模型服务崩溃") || e.getMessage().contains("模型返回非JSON格式,可能服务异常"))) {
-                            e.printStackTrace();
+                            log.error("🛑 检测到模型服务崩溃,立即停止病历 {} 的质控", recordId);
+                            throw new RuntimeException("模型服务崩溃,停止质控", e);
                         }
                     }
                 }
@@ -371,8 +381,8 @@ public class MedicalRecordQc123Service {
 
                 for (QualityControlRuleVo rule : rules) {
                     try {
-                        // 用 rule.ruleContent 查询知识库获取正反例
-                        List<Map<String, Object>> maps = fetchMatchingRules(rule.getRuleContent());
+                        // 用病历原文检索相关正反例
+                        List<Map<String, Object>> maps = fetchMatchingRules(rule.getRuleCode(), step.content);
 
 
                         // 构造提示词(传入病历部分名称)
@@ -384,7 +394,26 @@ public class MedicalRecordQc123Service {
 
                         // 收集不通过项
                         try {
-                            JSONArray arr = JSON.parseArray(processedOutput);
+                            // 预处理:移除 </think> 标签及之前的内容,提取纯 JSON
+                            String cleanedOutput = processedOutput;
+                            
+                            // 移除 </think> 标签及之前的内容
+                            if (cleanedOutput.contains("</think>")) {
+                                cleanedOutput = cleanedOutput.substring(cleanedOutput.indexOf("</think>") + 8).trim();
+                            }
+                            
+                            // 移除其他可能的思考标签
+                            cleanedOutput = cleanedOutput.replaceAll("<think>.*?</think>", "").trim();
+                            
+                            // 提取第一个 JSON 数组 (从 [ 到最后一个 ])
+                            int firstBracket = cleanedOutput.indexOf('[');
+                            int lastBracket = cleanedOutput.lastIndexOf(']');
+                            
+                            if (firstBracket >= 0 && lastBracket > firstBracket) {
+                                cleanedOutput = cleanedOutput.substring(firstBracket, lastBracket + 1);
+                            }
+                            
+                            JSONArray arr = JSON.parseArray(cleanedOutput);
                             if (arr != null && !arr.isEmpty()) {
                                 for (int i = 0; i < arr.size(); i++) {
                                     Object item = arr.get(i);
@@ -399,7 +428,7 @@ public class MedicalRecordQc123Service {
                                 }
                             }
                         } catch (Exception e) {
-                            log.warn("⚠️ 规则 [{}] 返回非JSON格式,按文本纳入聚合", rule.getRuleCode());
+                            log.warn("⚠️ 规则 [{}] 返回非JSON格式,原始内容: {}", rule.getRuleCode(), processedOutput);
                             allFailures.add(processedOutput);
                         }
 
@@ -409,7 +438,8 @@ public class MedicalRecordQc123Service {
                         log.error("❌ 规则 [{}] type={} 质控失败: {}", rule.getRuleCode(), type, e.getMessage());
                         // ✅ 如果是模型崩溃异常,立即终止整个病历的质控
                         if (e.getMessage() != null && (e.getMessage().contains("模型服务崩溃") || e.getMessage().contains("模型返回非JSON格式,可能服务异常"))) {
-                            e.printStackTrace();
+                            log.error("🛑 检测到模型服务崩溃,立即停止病历 {} 的质控", recordId);
+                            throw new RuntimeException("模型服务崩溃,停止质控", e);
                         }
                     }
                 }
@@ -455,7 +485,8 @@ public class MedicalRecordQc123Service {
                 log.error("❌ 规则 [{}] specific 质控失败: {}", rule.getRuleCode(), e.getMessage());
                 // ✅ 如果是模型崩溃异常,立即终止整个病历的质控
                 if (e.getMessage() != null && (e.getMessage().contains("模型服务崩溃") || e.getMessage().contains("模型返回非JSON格式,可能服务异常"))) {
-                    e.printStackTrace();
+                    log.error("🛑 检测到模型服务崩溃,立即停止病历 {} 的质控", recordId);
+                    throw new RuntimeException("模型服务崩溃,停止质控", e);
                 }
             }
         }
@@ -490,8 +521,8 @@ public class MedicalRecordQc123Service {
 
         log.info("📝 规则 [{}] 开始 specific 质控,合并 types={}", rule.getRuleCode(), validTypes);
 
-        // 用 rule.ruleContent 查询知识库获取正反例
-        List<Map<String, Object>> maps = fetchMatchingRules(rule.getRuleContent());
+        // 用病历原文(合并内容)检索相关正反例
+        List<Map<String, Object>> maps = fetchMatchingRules(rule.getRuleCode(), mergedContent.toString());
 
         // 构造提示词(合并多个部分名称)
         String combinedSectionName = validTypes.stream().map(this::getTypeName).collect(Collectors.joining(" + "));
@@ -537,7 +568,26 @@ public class MedicalRecordQc123Service {
 
             // 合并新的 specific 规则结果
             try {
-                JSONArray newArray = JSON.parseArray(processedOutput);
+                // 预处理:移除 </think> 标签及之前的内容,提取纯 JSON
+                String cleanedOutput = processedOutput;
+                
+                // 移除 </think> 标签及之前的内容
+                if (cleanedOutput.contains("</think>")) {
+                    cleanedOutput = cleanedOutput.substring(cleanedOutput.indexOf("</think>") + 8).trim();
+                }
+                
+                // 移除其他可能的思考标签
+                cleanedOutput = cleanedOutput.replaceAll("<think>.*?</think>", "").trim();
+                
+                // 提取第一个 JSON 数组 (从 [ 到最后一个 ])
+                int firstBracket = cleanedOutput.indexOf('[');
+                int lastBracket = cleanedOutput.lastIndexOf(']');
+                
+                if (firstBracket >= 0 && lastBracket > firstBracket) {
+                    cleanedOutput = cleanedOutput.substring(firstBracket, lastBracket + 1);
+                }
+                
+                JSONArray newArray = JSON.parseArray(cleanedOutput);
                 if (newArray != null) {
                     for (int i = 0; i < newArray.size(); i++) {
                         Object item = newArray.get(i);
@@ -565,6 +615,29 @@ public class MedicalRecordQc123Service {
             log.info("✅ 规则 [{}] specific 质控完成,已聚合到已有记录(总计 {} 条)", rule.getRuleCode(), allFailures.size());
         } else {
             // 没有已有记录,新增一条
+            // 预处理:移除 </think> 标签及之前的内容,提取纯 JSON
+            String cleanedOutput = processedOutput;
+            
+            try {
+                // 移除 </think> 标签及之前的内容
+                if (cleanedOutput.contains("</think>")) {
+                    cleanedOutput = cleanedOutput.substring(cleanedOutput.indexOf("</think>") + 8).trim();
+                }
+                
+                // 移除其他可能的思考标签
+                cleanedOutput = cleanedOutput.replaceAll("<think>.*?</think>", "").trim();
+                
+                // 提取第一个 JSON 数组 (从 [ 到最后一个 ])
+                int firstBracket = cleanedOutput.indexOf('[');
+                int lastBracket = cleanedOutput.lastIndexOf(']');
+                
+                if (firstBracket >= 0 && lastBracket > firstBracket) {
+                    cleanedOutput = cleanedOutput.substring(firstBracket, lastBracket + 1);
+                }
+            } catch (Exception e) {
+                log.warn("⚠️ 新建记录预处理失败,使用原始输出: {}", e.getMessage());
+            }
+            
             MedicalRecordResult result = new MedicalRecordResult();
             result.setPatientRecordId(recordId);
             result.setQcType(targetType);
@@ -572,7 +645,7 @@ public class MedicalRecordQc123Service {
             result.setQcEndTime(LocalDateTime.now());
             result.setQcResult(qcResult);
             result.setQcScore("85");
-            result.setQcComments(processedOutput);
+            result.setQcComments(cleanedOutput);
             result.setCreateDate(LocalDateTime.now());
             resultMapper.insert(result);
 
@@ -644,7 +717,8 @@ public class MedicalRecordQc123Service {
             log.info("🧪 执行批次 {}/{},日期: {}", batchNo, totalBatches, batchDates);
 
             // 调用大模型质控(使用融合质控专用提示词)
-            List<Map<String, Object>> maps = fetchMatchingRules(rule.getRuleContent());
+            // 用批次内容检索相关正反例
+            List<Map<String, Object>> maps = fetchMatchingRules(rule.getRuleCode(), batchContent.toString());
             String dateRange = batchDates.get(0) + " ~ " + batchDates.get(batchDates.size() - 1);
             String prompt = buildPromptForMergedRecords(rule, dateRange, batchContent.toString(), maps, record);
             String modelOutput = callLLM(prompt);
@@ -652,7 +726,26 @@ public class MedicalRecordQc123Service {
 
             // 收集不通过项
             try {
-                JSONArray arr = JSON.parseArray(processedOutput);
+                // 预处理:移除 </think> 标签及之前的内容,提取纯 JSON
+                String cleanedOutput = processedOutput;
+                
+                // 移除 </think> 标签及之前的内容
+                if (cleanedOutput.contains("</think>")) {
+                    cleanedOutput = cleanedOutput.substring(cleanedOutput.indexOf("</think>") + 8).trim();
+                }
+                
+                // 移除其他可能的思考标签
+                cleanedOutput = cleanedOutput.replaceAll("<think>.*?</think>", "").trim();
+                
+                // 提取第一个 JSON 数组 (从 [ 到最后一个 ])
+                int firstBracket = cleanedOutput.indexOf('[');
+                int lastBracket = cleanedOutput.lastIndexOf(']');
+                
+                if (firstBracket >= 0 && lastBracket > firstBracket) {
+                    cleanedOutput = cleanedOutput.substring(firstBracket, lastBracket + 1);
+                }
+                
+                JSONArray arr = JSON.parseArray(cleanedOutput);
                 if (arr != null && !arr.isEmpty()) {
                     int beforeSize = aggregatedFailures.size();
                     for (int j = 0; j < arr.size(); j++) {
@@ -907,7 +1000,26 @@ public class MedicalRecordQc123Service {
                 String processedOutput = processModelOutput(batchOutput);
 
                 try {
-                    JSONArray arr = JSON.parseArray(processedOutput);
+                    // 预处理:移除 </think> 标签及之前的内容,提取纯 JSON
+                    String cleanedOutput = processedOutput;
+                    
+                    // 移除 </think> 标签及之前的内容
+                    if (cleanedOutput.contains("</think>")) {
+                        cleanedOutput = cleanedOutput.substring(cleanedOutput.indexOf("</think>") + 8).trim();
+                    }
+                    
+                    // 移除其他可能的思考标签
+                    cleanedOutput = cleanedOutput.replaceAll("<think>.*?</think>", "").trim();
+                    
+                    // 提取第一个 JSON 数组 (从 [ 到最后一个 ])
+                    int firstBracket = cleanedOutput.indexOf('[');
+                    int lastBracket = cleanedOutput.lastIndexOf(']');
+                    
+                    if (firstBracket >= 0 && lastBracket > firstBracket) {
+                        cleanedOutput = cleanedOutput.substring(firstBracket, lastBracket + 1);
+                    }
+                    
+                    JSONArray arr = JSON.parseArray(cleanedOutput);
                     if (arr != null && !arr.isEmpty()) {
                         int before = aggregated.size();
                         for (int j = 0; j < arr.size(); j++) {
@@ -931,7 +1043,8 @@ public class MedicalRecordQc123Service {
             log.error("❌ 规则 [{}] type={} 分批质控失败", rule.getRuleCode(), qcType, ex);
             // ✅ 如果是模型崩溃异常,立即重新抛出
             if (ex.getMessage() != null && (ex.getMessage().contains("模型服务崩溃") || ex.getMessage().contains("模型返回非JSON格式,可能服务异常"))) {
-                ex.printStackTrace();
+                log.error("🛑 检测到模型服务崩溃,立即停止质控");
+                throw new RuntimeException("模型服务崩溃,停止质控", ex);
             }
             return new ArrayList<>();
         }
@@ -967,16 +1080,16 @@ public class MedicalRecordQc123Service {
 
             for (Map<String, Object> item : ragResult) {
                 String type = (String) item.get("type");
-                String exampleContent = (String) item.get("content");
+                String exampleContent = (String) item.get("exampleContent");
 
-                if ("正例".equals(type)) {
-                    positive.add(content);
-                } else if ("反例".equals(type)) {
+                if ("positive".equals(type)) {
+                    positive.add(exampleContent);
+                } else if ("negative".equals(type)) {
                     negative.add(exampleContent);
                 }
             }
 
-            sb.append("【参考内容】\n");
+            sb.append("***【参考内容】***\n");
             if (!positive.isEmpty()) {
                 sb.append("正例(符合要求的写法):\n");
                 positive.stream().limit(5).forEach(e -> sb.append("• ").append(e).append("\n"));
@@ -992,7 +1105,9 @@ public class MedicalRecordQc123Service {
         sb.append("以下是 ").append(date).append(" 时间段内的病程记录和医嘱记录的融合内容。\n");
         sb.append("病程记录和医嘱记录是同一时间段的临床记录,存在时间和逻辑上的关联关系。\n");
         sb.append("质控时需要综合考虑病程记录中的病情描述与医嘱记录中的医疗处置是否匹配、是否合理。\n");
-        sb.append("根据给出的参考内容进行判断,正例是符合改规则质控要求,反例为不符合质控要求。\n");
+        if (!ragResult.isEmpty()) {
+            sb.append("根据给出的参考内容进行判断,正例是符合该规则质控要求,反例为不符合质控要求。\n");
+        }
         sb.append("请针对这两部分的融合内容进行质控审查。\n\n");
 
         // 添加病人基本信息
@@ -1026,7 +1141,9 @@ public class MedicalRecordQc123Service {
         sb.append("- 禁止输出 Markdown 代码块符号(如 ```json)\n");
         sb.append("- 只针对当前规则进行质控,不要关联其他规则\n");
         sb.append("- 所有结论必须严格基于病历原文记载\n");
-        sb.append("- 输出中不要出现参考内容\n");
+        if (!ragResult.isEmpty()) {
+            sb.append("- 输出中不要出现参考内容\n");
+        }
         sb.append("- 质控时要综合考虑病程记录和医嘱记录的关联性和一致性\n");
 
         return sb.toString();
@@ -1058,7 +1175,9 @@ public class MedicalRecordQc123Service {
 
 
         sb.append("你是一名专业的医疗病历质控员,需要依据【质控规则】对病历内容进行审核。\n");
-        sb.append("若提供了【参考内容】,仅用于理解规则,不得直接引用或照抄。\n");
+        if (!ragResult.isEmpty()) {
+            sb.append("若提供了【参考内容】,仅用于理解规则,不得直接引用或照抄。\n");
+        }
         sb.append("请严格基于【病历原文】判断是否违反该规则。\n\n");
 
         // === 质控规则 ===
@@ -1077,16 +1196,16 @@ public class MedicalRecordQc123Service {
 
             for (Map<String, Object> item : ragResult) {
                 String type = (String) item.get("type");
-                String exampleContent = (String) item.get("content");
+                String exampleContent = (String) item.get("exampleContent");
 
-                if ("正例".equals(type)) {
-                    positive.add(content);
-                } else if ("反例".equals(type)) {
+                if ("positive".equals(type)) {
+                    positive.add(exampleContent);
+                } else if ("negative".equals(type)) {
                     negative.add(exampleContent);
                 }
             }
 
-            sb.append("【参考内容】\n");
+            sb.append("***【参考内容】***\n");
             if (!positive.isEmpty()) {
                 sb.append("正例(符合要求的写法):\n");
                 positive.forEach(e -> sb.append("• ").append(e).append("\n"));
@@ -1150,7 +1269,9 @@ public class MedicalRecordQc123Service {
         sb.append("===【注意事项】===\n");
         sb.append("- 严禁输出 Markdown 代码块(如 ```json)\n");
         sb.append("- 严禁输出解释性文字、分析过程或无关说明\n");
-        sb.append("- 不得引用或复述【参考内容】中的文字\n");
+        if (!ragResult.isEmpty()) {
+            sb.append("- 不得引用或复述【参考内容】中的文字\n");
+        }
         sb.append("- 若规则不适用于当前病历内容,应视为通过\n");
         sb.append("- 所有结论必须严格基于【病历原文】内容\n");
 
@@ -1171,14 +1292,98 @@ public class MedicalRecordQc123Service {
     }
 
     /**
-     * 调用 RAG 获取正反例
+     * 调用 RAG 获取正反例(数据库 + 向量库组合)
+     * 
+     * @param ruleCode 规则编码(用于数据库查询)
+     * @param medicalContent 病历原文内容(用于向量检索)
+     * @return 匹配到的正反例列表(数据库结果在前,向量库结果在后)
      */
-    private List<Map<String, Object>> fetchMatchingRules(String sectionLabel) throws IOException {
-        Map<String, Object> param = new HashMap<>();
-        param.put("messages", sectionLabel);  // ✅ 动态传入中文标签
-        param.put("maxDistance", 0.75);
-        param.put("kid", "Quality_control");
-        return vectorStoreService.getQualityRuleVector(sectionLabel, 0.75, 20);
+    private List<Map<String, Object>> fetchMatchingRules(String ruleCode, String medicalContent) throws IOException {
+        List<Map<String, Object>> result = new ArrayList<>();
+
+        // 1️⃣ 先查数据库:根据规则编码精确匹配正反例
+        QualityControlRuleExampleBo queryBo = new QualityControlRuleExampleBo();
+        queryBo.setRuleCode(ruleCode);
+        List<QualityControlRuleExampleVo> dbExamples = qualityControlRuleExampleService.queryList(queryBo);
+        
+        // 🚫 如果数据库中没有正反例,直接返回空,不使用向量库
+        if (dbExamples == null || dbExamples.isEmpty()) {
+            log.warn("规则 [{}] 数据库中无正反例,不返回任何参考内容", ruleCode);
+            return result; // 返回空列表
+        }
+        
+        // 转换数据库结果
+        for (QualityControlRuleExampleVo example : dbExamples) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("type", example.getType()); // positive 或 negative
+            item.put("exampleContent", example.getExampleContent());
+            item.put("exampleDesc", example.getExampleDesc());
+            item.put("source", "database"); // 标记来源为数据库
+            result.add(item);
+        }
+        
+        log.debug("规则 [{}] 从数据库查询到 {} 条正反例", ruleCode, dbExamples.size());
+        
+        // 2️⃣ 再查向量库:用病历原文检索相关的参考内容
+        try {
+            List<Map<String, Object>> vectorResults = vectorStoreService.getQualityRuleVector(medicalContent, 0.75, 20);
+            
+            // 重排序(拼接完整的 rerank API endpoint)
+            String rerankUrl = baseUrl + "/v1/rerank";
+            List<Map<String, Object>> rerankList = RerankUtil.rerankNearestList(medicalContent, vectorResults, rerankUrl, "bge-reranker-v2-m3");
+            
+            // 截取重排序后的前10条
+            List<Map<String, Object>> top10Results = rerankList.stream()
+                .limit(10)
+                .collect(Collectors.toList());
+            
+            log.debug("规则 [{}] 向量库检索到 {} 条,重排序后截取前10条", ruleCode, vectorResults.size());
+
+            // 第一步:筛选向量库中属于当前规则的项
+            List<Map<String, Object>> sameRuleItems = new ArrayList<>();
+            for (Map<String, Object> vectorItem : top10Results) {
+                String vectorRuleCode = (String) vectorItem.get("ruleCode");
+                if (ruleCode.equals(vectorRuleCode)) {
+                    sameRuleItems.add(vectorItem);
+                }
+            }
+            
+            log.debug("规则 [{}] 向量库中找到 {} 个当前规则的项", ruleCode, sameRuleItems.size());
+            
+            // 第二步:找出数据库和向量库的内容交集
+            Set<String> dbContents = dbExamples.stream()
+                .map(QualityControlRuleExampleVo::getExampleContent)
+                .collect(Collectors.toSet());
+            
+            List<Map<String, Object>> intersectionItems = new ArrayList<>();
+            for (Map<String, Object> vectorItem : sameRuleItems) {
+                String vectorContent = (String) vectorItem.get("exampleContent");
+                // 如果向量库的内容在数据库中存在,则为交集项
+                if (vectorContent != null && dbContents.contains(vectorContent)) {
+                    vectorItem.put("source", "intersection"); // 标记来源为交集
+                    intersectionItems.add(vectorItem);
+                }
+            }
+
+
+
+            // 第三步:如果有交集,清空数据库结果,只保留交集;否则只保留数据库结果
+            if (!intersectionItems.isEmpty()) {
+                result.clear(); // 清空之前添加的数据库结果
+                result.addAll(intersectionItems);
+                log.info("规则 [{}] 数据库和向量库有 {} 个交集项,只保留交集", ruleCode, intersectionItems.size());
+            } else {
+                log.info("规则 [{}] 数据库和向量库无交集,只保留数据库 {} 条结果", ruleCode, dbExamples.size());
+            }
+            
+        } catch (Exception e) {
+            log.warn("规则 [{}] 向量库检索失败,仅使用数据库结果: {}", ruleCode, e.getMessage());
+        }
+        
+        log.info("规则 [{}] 总计获取 {} 条参考内容(数据库 {} 条 + 向量库 {} 条)", 
+            ruleCode, result.size(), dbExamples.size(), result.size() - dbExamples.size());
+        
+        return result;
     }
 
 
@@ -1218,6 +1423,16 @@ public class MedicalRecordQc123Service {
         if (response.contains("address=") && response.contains("pid=")) {
             throw new RuntimeException("⚠️ 模型服务崩溃,响应异常: " + response);
         }
+        
+        // ✅ 检测模型未找到错误
+        if (response.contains("Model not found") || response.contains("model not found")) {
+            throw new RuntimeException("⚠️ 模型服务崩溃,模型未找到: " + response);
+        }
+        
+        // ✅ 检测 detail 字段的错误响应
+        if (response.contains("\"detail\"") && !response.contains("\"choices\"")) {
+            throw new RuntimeException("⚠️ 模型服务崩溃,返回错误详情: " + response);
+        }
 
         // ✅ 检测其他异常响应格式
         if (!response.trim().startsWith("{") && !response.trim().startsWith("[")) {