Browse Source

舌象模型适配

zhaohan 3 weeks ago
parent
commit
302773295d

+ 1 - 0
emoon-extend/emoon-tongue/src/main/java/com/emoon/tongue/controller/TesttongueController.java

@@ -537,6 +537,7 @@ public class TesttongueController {
         messages.add(userMsg);
 
         requestBody.put("messages", messages);
+        requestBody.put("temperature", 0);
         return requestBody;
     }
 

+ 10 - 0
emoon-extend/emoon-tongue/src/main/java/com/emoon/tongue/mapper/MedicalDataMapper.java

@@ -0,0 +1,10 @@
+package com.emoon.tongue.mapper;
+
+import com.emoon.common.mybatis.core.mapper.BaseMapperPlus;
+import com.emoon.tongue.domain.entity.MedicalData;
+
+/**
+ * 病-证-药关联数据 Mapper
+ */
+public interface MedicalDataMapper extends BaseMapperPlus<MedicalData, MedicalData> {
+}

+ 0 - 3
emoon-extend/emoon-tongue/src/main/java/com/emoon/tongue/mapper/TongueDiagnosisMapper.java

@@ -3,15 +3,12 @@ package com.emoon.tongue.mapper;
 import com.emoon.common.mybatis.core.mapper.BaseMapperPlus;
 import com.emoon.tongue.domain.entity.TongueDiagnosis;
 import com.emoon.tongue.domain.vo.TongueDiagnosisDetailVo;
-import org.apache.ibatis.annotations.Mapper;
-
 /**
  * 舌诊任务Mapper接口
  *
  * @author destiny
  * @date 2025-12-09
  */
-@Mapper
 public interface TongueDiagnosisMapper extends BaseMapperPlus<TongueDiagnosis, TongueDiagnosisDetailVo> {
 
 }

+ 72 - 157
emoon-extend/emoon-tongue/src/main/java/com/emoon/tongue/service/impl/TongueAiLocalDiagnosisService.java

@@ -19,7 +19,6 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
-import java.math.RoundingMode;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -27,12 +26,19 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * 本地API舌诊诊断实现(骨架)
+ * 本地API舌诊诊断实现
  *
- * <p>当前已按你提供的本地模型协议对齐:返回为 OpenAI ChatCompletions 结构,
- * 从 {@code choices[0].message.content} 取自然语言内容,再拆分为结构化字段。</p>
+ * <p>当前已按本地模型协议对齐:返回为 OpenAI ChatCompletions 结构,
+ * 从 {@code choices[0].message.content} 取结构化文本,再拆分为结构化字段。</p>
  *
- * <p>额外保留一个兼容分支:如果未来本地接口直接返回结构化 JSON(包含 tongueGeneralDesc/keyFeatures 等字段),
+ * <p>模型 content 输出格式:
+ * <pre>
+ * 舌苔厚薄:薄苔;
+ * 舌苔颜色:黄;
+ * 舌质颜色:红舌
+ * </pre>
+ *
+ * <p>额外保留一个兼容分支:如果本地接口直接返回结构化 JSON(包含 tongueGeneralDesc/keyFeatures 等字段),
  * 也能直接映射;否则最终兜底返回 mock。</p>
  */
 @Slf4j
@@ -85,32 +91,23 @@ public class TongueAiLocalDiagnosisService implements TongueAiDiagnosisProvider
         }
     }
 
-    /**
-     * 构建本地API请求体(按你的本地协议改这里)
-     *
-     * <p>当前默认发送:imageUrl + chiefComplaint</p>
-     */
     private Map<String, Object> buildLocalRequest(TongueDiagnosisDetailVo detail) {
         Map<String, Object> requestBody = new HashMap<>();
 
-        // 你提供的本地接口协议:{ model, messages: [...] }
         requestBody.put("model", localModel);
 
         List<Map<String, Object>> messages = new ArrayList<>();
 
-        // system
         Map<String, Object> systemMsg = new HashMap<>();
         systemMsg.put("role", "system");
         systemMsg.put("content", systemPrompt);
         messages.add(systemMsg);
 
-        // user (multimodal content)
         Map<String, Object> userMsg = new HashMap<>();
         userMsg.put("role", "user");
 
         List<Map<String, Object>> content = new ArrayList<>();
 
-        // 本地模型无法访问外网/MinIO URL 时,优先传 imagePath(服务器落盘路径);没有再回退到 imageUrl
         String imageUrl = null;
         if (detail.getTongueImage() != null) {
             imageUrl = detail.getTongueImage().getPath();
@@ -139,6 +136,7 @@ public class TongueAiLocalDiagnosisService implements TongueAiDiagnosisProvider
         messages.add(userMsg);
 
         requestBody.put("messages", messages);
+        requestBody.put("temperature", 0);
         return requestBody;
     }
 
@@ -152,11 +150,8 @@ public class TongueAiLocalDiagnosisService implements TongueAiDiagnosisProvider
     /**
      * 解析本地API响应
      *
-     * <p>优先解析你当前本地模型的返回格式:OpenAI ChatCompletions 结构,
-     * 取 {@code choices[0].message.content} 并拆分为 tongueManifestation/syndrome/prescription/keyFeatures。</p>
-     *
-     * <p>仅当本地接口未来改为直接返回结构化 JSON(包含 tongueGeneralDesc/keyFeatures 等字段)时,
-     * 才会走结构化映射分支。</p>
+     * <p>优先走 OpenAI ChatCompletions 结构 → choices[0].message.content → 按行解析 kv。</p>
+     * <p>兼容分支:直接返回结构化 JSON(包含 tongueGeneralDesc/keyFeatures 等字段)时走 mapStructuredPayload。</p>
      */
     private TongueDiagnosisInfo parseLocalResponse(String raw, TongueDiagnosisDetailVo detail) {
         if (raw == null || raw.trim().isEmpty()) {
@@ -167,13 +162,11 @@ public class TongueAiLocalDiagnosisService implements TongueAiDiagnosisProvider
         try {
             JsonNode root = objectMapper.readTree(raw);
 
-            // 1) 如果本地接口返回也是 OpenAI 结构,则取 choices[0].message.content
             String assistantContent = extractAssistantContent(root);
             if (assistantContent != null && !assistantContent.trim().isEmpty()) {
-                return parseAssistantNaturalLanguage(assistantContent);
+                return parseAssistantStructuredContent(assistantContent);
             }
 
-            // 2) 否则尝试兼容:直接返回结构化 JSON(保留之前的通用实现)
             JsonNode payload = extractPayload(root);
             if (payload != null && !payload.isMissingNode() && !payload.isNull()
                     && (payload.has("tongueGeneralDesc") || payload.has("keyFeatures"))) {
@@ -207,7 +200,6 @@ public class TongueAiLocalDiagnosisService implements TongueAiDiagnosisProvider
             diagnosisInfo.setHealthAdvice(payload.get("healthAdvice").asText());
         }
 
-        // keyFeatures: [{feature, confidence}]
         List<KeyFeatures> keyFeatures = new ArrayList<>();
         String tongueColorResult = null;
         String tongueCoatingResult = null;
@@ -259,151 +251,78 @@ public class TongueAiLocalDiagnosisService implements TongueAiDiagnosisProvider
     }
 
     /**
-     * 将本地模型返回的自然语言 content 拆分成:
-     * - tongueManifestation(舌象)
-     * - keyFeatures(从“现象为”里拆)
-     * - syndrome(“诊断为”后的证型)
-     * - prescription(“处方:”后的文本)
-     * - diagnosis(按需求先空着)
+     * 解析本地舌象专精模型返回的结构化 content。
+     *
+     * <p>模型输出格式示例:
+     * <pre>
+     * 舌苔厚薄:薄苔;
+     * 舌苔颜色:黄;
+     * 舌质颜色:红舌
+     * </pre>
+     * 按"key:value"逐行解析,映射到 TongueDiagnosisInfo 对应字段。
      */
-    private TongueDiagnosisInfo parseAssistantNaturalLanguage(String content) {
+    private TongueDiagnosisInfo parseAssistantStructuredContent(String content) {
         String normalized = content.replace("\r\n", "\n").replace("\r", "\n").trim();
 
-        String manifestation = extractBetween(normalized, "现象为", "诊断为");
-        String syndrome = extractAfterLabel(normalized, "诊断为");
-        syndrome = firstSentence(syndrome);
-        String prescription = extractAfterLabel(normalized, "处方");
-
-        TongueDiagnosisInfo info = new TongueDiagnosisInfo();
-        info.setTongueManifestation(manifestation);
-        // 兼容旧字段:让旧前端/旧落库字段还能看见
-        info.setTongueGeneralDesc(manifestation);
-        info.setSyndrome(syndrome);
-        info.setDiagnosis(""); // 按需求先空着
-        info.setPrescription(prescription);
-
-        List<KeyFeatures> features = parseFeaturesFromManifestation(manifestation);
-        info.setKeyFeatures(features);
-
-        // 尝试从特征里填三个老字段(如果你后续不用也没关系)
-        fillLegacyTongueFields(info, features);
-
-        // overallConfidence 这里没给,先用平均固定值兜底
-        if (features != null && !features.isEmpty()) {
-            BigDecimal avg = BigDecimal.ZERO;
-            for (KeyFeatures f : features) {
-                if (f.getConfidence() != null) avg = avg.add(f.getConfidence());
+        Map<String, String> kvMap = new HashMap<>();
+        for (String line : normalized.split("\n")) {
+            String trimmed = line.trim();
+            if (trimmed.isEmpty()) continue;
+            if (trimmed.endsWith(";") || trimmed.endsWith(";")) {
+                trimmed = trimmed.substring(0, trimmed.length() - 1).trim();
+            }
+            int colonIdx = trimmed.indexOf(":");
+            if (colonIdx < 0) colonIdx = trimmed.indexOf(":");
+            if (colonIdx > 0 && colonIdx < trimmed.length() - 1) {
+                String key = trimmed.substring(0, colonIdx).trim();
+                String value = trimmed.substring(colonIdx + 1).trim();
+                kvMap.put(key, value);
             }
-            avg = avg.divide(BigDecimal.valueOf(features.size()), 2, RoundingMode.HALF_UP);
-            info.setOverallConfidence(avg);
         }
 
-        return info;
-    }
+        String coatingThickness = kvMap.get("舌苔厚薄");
+        String coatingColor = kvMap.get("舌苔颜色");
+        String tongueColor = kvMap.get("舌质颜色");
 
-    private List<KeyFeatures> parseFeaturesFromManifestation(String manifestation) {
-        if (manifestation == null) return new ArrayList<>();
-
-        // 去掉前缀描述,只保留“厚苔, 润苔...”这段
-        String s = manifestation;
-        int idx = s.indexOf(":");
-        if (idx >= 0) s = s.substring(idx + 1);
-
-        s = s.replace("。", "").replace(";", ",").replace(",", ",").replace("、", ",");
-        String[] parts = s.split(",");
-
-        List<KeyFeatures> list = new ArrayList<>();
-        // 给一个“固定但合理”的置信度梯度:0.92 -> 0.85
-        BigDecimal[] confs = new BigDecimal[] {
-                new BigDecimal("0.92"),
-                new BigDecimal("0.89"),
-                new BigDecimal("0.87"),
-                new BigDecimal("0.86"),
-                new BigDecimal("0.85"),
-                new BigDecimal("0.85"),
-                new BigDecimal("0.85"),
-                new BigDecimal("0.85")
-        };
-
-        int i = 0;
-        for (String p : parts) {
-            String feature = p == null ? "" : p.trim();
-            if (feature.isEmpty()) continue;
-            KeyFeatures k = new KeyFeatures();
-            k.setFeature(feature);
-            k.setConfidence(confs[Math.min(i, confs.length - 1)]);
-            list.add(k);
-            i++;
-        }
-        return list;
-    }
-
-    private void fillLegacyTongueFields(TongueDiagnosisInfo info, List<KeyFeatures> features) {
-        if (info == null || features == null) return;
+        TongueDiagnosisInfo info = new TongueDiagnosisInfo();
 
-        for (KeyFeatures f : features) {
-            if (f == null || f.getFeature() == null) continue;
-            String t = f.getFeature().trim();
-            // 很粗的启发式:命中几个常见词就填到老字段里
-            if (info.getTongueShapeResult() == null && (t.contains("齿痕") || t.contains("裂纹") || t.contains("胖大") || t.contains("瘦薄"))) {
-                info.setTongueShapeResult(t);
-            }
-            if (info.getTongueColorResult() == null && (t.contains("淡红") || t.contains("淡白") || t.contains("红") || t.contains("青") || t.contains("黯") || t.contains("紫"))) {
-                info.setTongueColorResult(t);
-            }
-            if (info.getTongueCoatingResult() == null && (t.contains("苔") || t.contains("黄") || t.contains("白") || t.contains("灰") || t.contains("黑") || t.contains("腻") || t.contains("润") || t.contains("燥"))) {
-                info.setTongueCoatingResult(t);
-            }
+        if (tongueColor != null && !tongueColor.isEmpty()) {
+            info.setTongueColorResult(tongueColor);
         }
-    }
 
-    private String extractBetween(String text, String leftLabel, String rightLabel) {
-        if (text == null) return null;
-        int l = text.indexOf(leftLabel);
-        if (l < 0) return null;
-        int start = text.indexOf(":", l);
-        if (start < 0) start = l + leftLabel.length();
-        else start = start + 1;
-        int r = text.indexOf(rightLabel, start);
-        String out = (r > start) ? text.substring(start, r) : text.substring(start);
-        return (leftLabel.contains("现象") ? "病人舌苔呈现的现象为:" : "") + cleanup(out);
-    }
+        StringBuilder coatingDesc = new StringBuilder();
+        if (coatingThickness != null && !coatingThickness.isEmpty()) {
+            coatingDesc.append(coatingThickness);
+        }
+        if (coatingColor != null && !coatingColor.isEmpty()) {
+            if (!coatingDesc.isEmpty()) coatingDesc.append(",");
+            coatingDesc.append(coatingColor);
+        }
+        if (!coatingDesc.isEmpty()) {
+            info.setTongueCoatingResult(coatingDesc.toString());
+        }
 
-    private String extractAfterLabel(String text, String label) {
-        if (text == null) return null;
-        int l = text.indexOf(label);
-        if (l < 0) return null;
-        int start = text.indexOf(":", l);
-        if (start < 0) start = l + label.length();
-        else start = start + 1;
-        return cleanup(text.substring(start));
-    }
+        String generalDesc = normalized.replace("\n", ";").replace(";;", ";");
+        info.setTongueGeneralDesc(generalDesc);
+        info.setTongueManifestation(generalDesc);
+
+        List<KeyFeatures> features = new ArrayList<>();
+        BigDecimal fixedConfidence = new BigDecimal("0.90");
+        for (Map.Entry<String, String> entry : kvMap.entrySet()) {
+            KeyFeatures kf = new KeyFeatures();
+            kf.setFeature(entry.getKey() + ":" + entry.getValue());
+            kf.setConfidence(fixedConfidence);
+            features.add(kf);
+        }
+        info.setKeyFeatures(features);
 
-    private String cleanup(String s) {
-        if (s == null) return null;
-        return s.replace("\n", " ").trim();
-    }
+        if (!features.isEmpty()) {
+            info.setOverallConfidence(fixedConfidence);
+        }
 
-    private String firstSentence(String s) {
-        if (s == null) return null;
-        int idx = s.indexOf("。");
-        if (idx >= 0) return s.substring(0, idx + 1).trim();
-        idx = s.indexOf("\n");
-        if (idx >= 0) return s.substring(0, idx).trim();
-        return s.trim();
+        return info;
     }
 
-    /**
-     * 从不同形态的响应中提取真正的诊断payload(你本地接口若不同,改这里最方便)
-     *
-     * <p>支持:
-     * <ul>
-     *   <li>直接返回 TongueDiagnosisInfo JSON</li>
-     *   <li>包一层 data: {...}</li>
-     *   <li>包一层 result: {...}</li>
-     * </ul>
-     * </p>
-     */
     private JsonNode extractPayload(JsonNode root) {
         if (root == null) return null;
         if (root.has("tongueGeneralDesc") || root.has("keyFeatures")) return root;
@@ -430,7 +349,6 @@ public class TongueAiLocalDiagnosisService implements TongueAiDiagnosisProvider
             }
 
             String json = objectMapper.writeValueAsString(requestBody);
-            // 记录喂给模型前的完整提示词/请求体(包含 system/user/messages 以及 image_path)
             log.info("本地模型请求: url={}, body={}", url, json);
             httpPost.setEntity(new StringEntity(json, StandardCharsets.UTF_8));
 
@@ -438,7 +356,6 @@ public class TongueAiLocalDiagnosisService implements TongueAiDiagnosisProvider
                 HttpEntity entity = response.getEntity();
                 String content = entity != null ? EntityUtils.toString(entity, StandardCharsets.UTF_8) : null;
                 int status = response.getStatusLine() != null ? response.getStatusLine().getStatusCode() : -1;
-                // 记录模型返回的原始结果(完整响应)
                 log.info("本地模型响应: url={}, status={}, body={}", url, status, content);
                 if (status < 200 || status >= 300) {
                     throw new RuntimeException("本地API HTTP状态码异常: " + status + ", body=" + content);
@@ -448,5 +365,3 @@ public class TongueAiLocalDiagnosisService implements TongueAiDiagnosisProvider
         }
     }
 }
-
-