wangkangyjy 3 сар өмнө
parent
commit
08433813a1

+ 35 - 19
src/main/java/com/emoon/labreport/config/BailianConfig.java

@@ -25,44 +25,60 @@ public class BailianConfig {
     private String apiKey;
 
     /**
-     * 使用的模型名称
+     * API基础地址
      */
-    private String model;
+    private String baseUrl;
 
     /**
-     * API基础地址
+     * 视觉模型配置
      */
-    private String baseUrl;
+    private ModelConfig vision;
 
     /**
-     * 温度参数,控制输出的随机性
+     * 语言模型配置
      */
-    private Double temperature;
+    private ModelConfig chat;
 
     /**
-     * 最大输出token数
+     * 模型配置内部类
      */
-    private Integer maxTokens;
+    @Data
+    public static class ModelConfig {
+        private String model;
+        private Double temperature;
+        private Integer maxTokens;
+        private Integer timeout = 60;
+    }
 
     /**
-     * 超时时间(秒),默认120秒
+     * 创建视觉模型Bean(用于提取报告数据)
      */
-    private Integer timeout = 120;
+    @Bean("visionModel")
+    public ChatLanguageModel visionModel() {
+        return OpenAiChatModel.builder()
+                .baseUrl(baseUrl)
+                .apiKey(apiKey)
+                .modelName(vision.getModel())
+                .temperature(vision.getTemperature())
+                .maxTokens(vision.getMaxTokens())
+                .timeout(Duration.ofSeconds(vision.getTimeout()))
+                .maxRetries(2)
+                .build();
+    }
 
     /**
-     * 创建ChatLanguageModel Bean
-     * 使用OpenAI兼容接口调用百炼模型
+     * 创建语言模型Bean(用于生成医学解读)
      */
-    @Bean
-    public ChatLanguageModel chatLanguageModel() {
+    @Bean("chatModel")
+    public ChatLanguageModel chatModel() {
         return OpenAiChatModel.builder()
                 .baseUrl(baseUrl)
                 .apiKey(apiKey)
-                .modelName(model)
-                .temperature(temperature)
-                .maxTokens(maxTokens)
-                .timeout(Duration.ofSeconds(timeout))
-                .maxRetries(2) // 失败后重试2次
+                .modelName(chat.getModel())
+                .temperature(chat.getTemperature())
+                .maxTokens(chat.getMaxTokens())
+                .timeout(Duration.ofSeconds(chat.getTimeout()))
+                .maxRetries(2)
                 .build();
     }
 }

+ 72 - 23
src/main/java/com/emoon/labreport/service/ReportInterpretService.java

@@ -9,21 +9,29 @@ import dev.langchain4j.model.chat.ChatLanguageModel;
 import dev.langchain4j.model.output.Response;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
 /**
  * 报告解读服务
  * 负责调用百炼模型进行检验报告的OCR识别和解读
+ * 采用两步流程:
+ * 1. 视觉模型提取结构化数据
+ * 2. 语言模型生成医学解读
  */
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class ReportInterpretService {
 
-    private final ChatLanguageModel chatLanguageModel;
+    @Qualifier("visionModel")
+    private final ChatLanguageModel visionModel;
+
+    @Qualifier("chatModel")
+    private final ChatLanguageModel chatModel;
 
     /**
-     * 解读检验报告
+     * 解读检验报告(两步流程)
      *
      * @param imageBase64 图片的Base64编码(不含data:image前缀)
      * @param promptType  提示词类型
@@ -31,47 +39,88 @@ public class ReportInterpretService {
      */
     public String interpretReport(String imageBase64, String promptType) {
         try {
-            // 1. 加载提示词模板
-            String prompt = loadPrompt(promptType);
+            log.info("开始报告解读流程,类型:{}", promptType);
+
+            // 第一步:使用视觉模型提取结构化数据
+            String extractedData = extractReportData(imageBase64, promptType);
+            log.info("数据提取完成,数据长度:{} 字符", extractedData.length());
+
+            // 第二步:使用语言模型生成医学解读
+            String interpretation = generateInterpretation(extractedData, promptType);
+            log.info("解读生成完成,结果长度:{} 字符", interpretation.length());
+
+            return interpretation;
+
+        } catch (Exception e) {
+            log.error("报告解读失败", e);
+            throw new RuntimeException("报告解读失败:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 第一步:使用视觉模型提取报告数据
+     */
+    private String extractReportData(String imageBase64, String promptType) {
+        try {
+            // 1. 加载数据提取提示词
+            String extractPrompt = loadPrompt(promptType + "-extract");
 
-            // 2. 构建用户消息(包含图片和文本提示词)
-            // 注意:阿里云百炼需要完整的 Base64 URL 格式:data:image/jpeg;base64,{base64_string}
+            // 2. 构建用户消息(包含图片和提取提示词)
             String imageUrl = "data:image/jpeg;base64," + imageBase64;
             UserMessage userMessage = UserMessage.from(
-                    TextContent.from(prompt),
+                    TextContent.from(extractPrompt),
                     ImageContent.from(imageUrl)
             );
 
-            // 3. 调用百炼模型
-            log.info("开始调用百炼模型进行报告解读...");
-            Response<AiMessage> response = chatLanguageModel.generate(userMessage);
+            // 3. 调用视觉模型
+            log.info("调用视觉模型提取数据...");
+            Response<AiMessage> response = visionModel.generate(userMessage);
 
-            // 4. 获取并返回结果
-            String result = response.content().text();
-            log.info("报告解读完成,结果长度:{} 字符", result.length());
+            // 4. 返回提取的JSON数据
+            return response.content().text();
 
-            return result;
+        } catch (Exception e) {
+            log.error("数据提取失败", e);
+            throw new RuntimeException("数据提取失败:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 第二步:使用语言模型生成医学解读
+     */
+    private String generateInterpretation(String extractedData, String promptType) {
+        try {
+            // 1. 加载解读提示词
+            String interpretPrompt = loadPrompt(promptType + "-interpret-prompt");
+
+            // 2. 替换数据占位符
+            String fullPrompt = interpretPrompt.replace("{{EXTRACTED_DATA}}", extractedData);
+
+            // 3. 调用语言模型(语言模型使用纯文本消息,转换为UserMessage)
+            log.info("调用语言模型生成解读...");
+            UserMessage userMessage = UserMessage.from(TextContent.from(fullPrompt));
+            Response<AiMessage> response = chatModel.generate(userMessage);
+
+            // 4. 返回生成的解读
+            return response.content().text();
 
         } catch (Exception e) {
-            log.error("报告解读失败", e);
-            throw new RuntimeException("报告解读失败:" + e.getMessage(), e);
+            log.error("解读生成失败", e);
+            throw new RuntimeException("解读生成失败:" + e.getMessage(), e);
         }
     }
 
     /**
      * 加载提示词模板
-     *
-     * @param promptType 提示词类型
-     * @return 提示词内容
      */
-    private String loadPrompt(String promptType) {
+    private String loadPrompt(String promptName) {
         try {
-            String promptPath = "prompts/" + promptType + "-report-prompt.txt";
+            String promptPath = "prompts/" + promptName + ".txt";
             String prompt = ResourceUtil.readUtf8Str(promptPath);
             log.debug("成功加载提示词:{}", promptPath);
             return prompt;
         } catch (Exception e) {
-            log.warn("加载提示词失败:{},使用默认提示词", promptType, e);
+            log.warn("加载提示词失败:{},使用默认提示词", promptName, e);
             return getDefaultPrompt();
         }
     }
@@ -81,7 +130,7 @@ public class ReportInterpretService {
      */
     private String getDefaultPrompt() {
         return """
-                你是一位专业的医学检验报告解读专家。请识别这张血常规检验报告,并提供详细的解读。
+                你是一位专业的医学检验报告解读专家。请识别这张检验报告,并提供详细的解读。
 
                 要求:
                 1. 提取所有检验数据

+ 12 - 4
src/main/resources/application.yaml

@@ -18,11 +18,19 @@ spring:
 # 百炼平台配置
 bailian:
   api-key: ${BAILIAN_API_KEY:sk-fa9452b62d504a21b4ed595be3bf3881}
-  model: qwen-vl-plus  # 使用plus版本,速度更快
   base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
-  temperature: 0.3  # 低温度,快速输出
-  max-tokens: 2800  # 允许更丰富的内容
-  timeout: 60  # 超时时间(秒)
+  # 视觉模型配置(用于提取报告数据)
+  vision:
+    model: qwen-vl-plus
+    temperature: 0.1  # 极低温度,确保准确提取
+    max-tokens: 2000
+    timeout: 60
+  # 语言模型配置(用于生成医学解读)
+  chat:
+    model: qwen-plus
+    temperature: 0.3
+    max-tokens: 3000
+    timeout: 60
 
 # 日志配置
 logging:

+ 5 - 3
src/main/resources/prompts/blood-report-prompt.txt

@@ -12,8 +12,10 @@
 每项:名称、结果、单位、参考值、状态(✅⚠️↑↓)、一句话解读
 
 **重要:判断指标是否异常时,必须严格按照参考范围判断**
-- 只有当结果超出参考范围时才标记为异常(⚠️↑↓)
-- 结果在参考范围内必须标记为正常(✅)
+- 结果 < 参考范围下限:标记为 ↓ 降低(异常)
+- 结果 > 参考范围上限:标记为 ↑ 升高(异常)
+- 结果在参考范围内(包括边界值):标记为 ✅ 正常
+- 例如:MCHC 结果315,参考范围316~354,315<316,必须标记为↓降低
 - 例如:NEUT% 结果61.50%,参考范围40~75%,在范围内,必须标记为✅正常
 
 ## 重点异常分析
@@ -22,4 +24,4 @@
 ## 健康建议
 整体结论+3-5条具体建议(饮食/作息/复查等)
 
-要求:表格形式、严格按参考范围判断异常、简洁明了
+要求:表格形式、严格按参考范围判断异常、注意边界值、简洁明了