项目一系列-第9章 集成AI千帆大模型

第9章 集成AI千帆大模型

学习目标

  • 能够说清楚健康评估模块在项目中的作用
  • 能够掌握千帆大模型的开通和对接
  • 能够掌握健康评估模块中的prompt提示词编写
  • 能够自主完成健康评估模块的接口开发

分析设计

需求说明

健康评估是指老人办理入住前需上传体检报告,由AI自动进行分析,并对报告进行总结,同时给出合理的建议;

下图是健康评估列表页,展示了经过健康评估的老人列表。

当点击了上传体检报告 按钮之后会弹窗,效果如下,需要输入信息,需要提前准备好老人的体检报告(PDF格式)

点击确定按钮之后,会使用AI对老人的健康报告进行评估

下图是健康评估的详情页面,是使用AI分析后的结果页,给了很多的数据,可以让护理员或销售人员来查看老人的健康状况,进一步更好的服务老人或者给老人推荐一些护理服务。

表结构说明

基于需求原型,咱们可以确定健康评估表(health_assessment)如下建表语句:

sql 复制代码
CREATE TABLE "health_assessment" (
  "id" bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  "elder_name" varchar(255) DEFAULT NULL COMMENT '老人姓名',
  "id_card" varchar(255) DEFAULT NULL COMMENT '身份证号',
  "birth_date" datetime DEFAULT NULL COMMENT '出生日期',
  "age" int DEFAULT NULL COMMENT '年龄',
  "gender" int DEFAULT NULL COMMENT '性别(0:男,1:女)',
  "health_score" varchar(255) DEFAULT NULL COMMENT '健康评分',
  "risk_level" varchar(255) DEFAULT NULL COMMENT '危险等级(健康, 提示, 风险, 危险, 严重危险)',
  "suggestion_for_admission" int DEFAULT NULL COMMENT '是否建议入住(0:建议,1:不建议)',
  "nursing_level_name" varchar(255) DEFAULT NULL COMMENT '推荐护理等级',
  "admission_status" int DEFAULT NULL COMMENT '入住情况(0:已入住,1:未入住)',
  "total_check_date" varchar(64) DEFAULT NULL COMMENT '总检日期',
  "physical_exam_institution" varchar(255) DEFAULT NULL COMMENT '体检机构',
  "physical_report_url" varchar(255) DEFAULT NULL COMMENT '体检报告URL链接',
  "assessment_time" datetime DEFAULT NULL COMMENT '评估时间',
  "report_summary" text COMMENT '报告总结',
  "disease_risk" text COMMENT '疾病风险',
  "abnormal_analysis" text COMMENT '异常分析',
  "system_score" varchar(255) DEFAULT NULL COMMENT '健康系统分值',
  "create_by" varchar(255) DEFAULT NULL COMMENT '创建者',
  "create_time" datetime DEFAULT NULL COMMENT '创建时间',
  "update_by" varchar(255) DEFAULT NULL COMMENT '更新者',
  "update_time" datetime DEFAULT NULL COMMENT '更新时间',
  "remark" text COMMENT '备注',
  PRIMARY KEY ("id")
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='健康评估表';

特别字段说明:

  • abnormal_analysis(异常分析) 这个字段是字符串类型,会把异常数据转换为json进行存储

接口说明

基于需求说明,在健康评估这个模块中,共有4个接口需要开发,分别是

  • 列表查询-模板生成
  • 上传体检报告-手动编写
  • 智能评测-手动编写
  • 查看详情-模板生成

实现方案

整体实现流程如下:

读取PDF文件内容

市面有很多工具类,咱们可以直接使用,目前选择的是Apache PDFBox

Apache PDFBox库是一个用于处理PDF文档的开源Java工具。该项目允许创建新的PDF文档,编辑现有的文档,以及从文档中提取内容。

导入对应的依赖,在zzyl-common模块中导入以下依赖:

xml 复制代码
<!--pdf工具包-->
<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>2.0.24</version>
</dependency>

创建工具类,通过pdf文件来读取内容,工具类路径:zzyl-common模块下的com.zzyl.common.utils.PDFUtil

java 复制代码
package com.zzyl.common.utils;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

public class PDFUtil {

    public static String pdfToString(InputStream inputStream) {

        PDDocument document = null;

        try {
            // 加载PDF文档
            document = PDDocument.load(inputStream);

            // 创建一个PDFTextStripper实例来提取文本
            PDFTextStripper pdfStripper = new PDFTextStripper();

            // 从PDF文档中提取文本
            String text = pdfStripper.getText(document);
            return text;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭PDF文档
            if (document != null) {
                try {
                    document.close();
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

测试:找一个pdf文档来读取里面的内容为字符串

java 复制代码
package com.nursing.test;

import com.zzyl.common.utils.PDFUtil;

import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class PDFUtilTest {

    public static void main(String[] args) throws FileNotFoundException {

        FileInputStream fileInputStream = new FileInputStream("C:\\tmp\\体检报告-刘爱国-男-69岁.pdf");

        String result = PDFUtil.pdfToString(fileInputStream);
        System.out.println(result);
    }
}

百度智能云AI集成

选择合适的AI

咱们的需求中,prompt是比较多的,也就是说需要更多的token,对话Chat V2支持的模型列表如下:

https://cloud.baidu.com/doc/WENXINWORKSHOP/s/wm7ltcvgc

经过综合评估,咱们本次采用的是ERNIE-4.0-8K

官方地址:https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t

ERNIE 4.0是百度自研的旗舰级超大规模⼤语⾔模型,相较ERNIE 3.5实现了模型能力全面升级,广泛适用于各领域复杂任务场景;支持自动对接百度搜索插件,保障问答信息时效,支持5K tokens输入+2K tokens输出。

集成准备步骤

1)注册和实名认证百度智能云

2)创建应用

2.1)访问千帆大模型,地址:https://qianfan.cloud.baidu.com/

2.2)点击大模型服务与开发平台ModelBuilder**,**可以进入到管理平台

2.3)进入管理平台后,找到应用接入,点击创建应用按钮。

2.4)创建一个新的API Key

3)项目集成

3.1)在父工程pom.xml文件中统一管理依赖的版本:

xml 复制代码
<properties>
    <openai-java.version>2.8.1</openai-java.version>
    <kotlin.version>1.9.23</kotlin.version>
    <jackson.version>2.16.1</jackson.version>
    <okhttp.version>4.12.0</okhttp.version>
</properties>

<dependencies>
    <!-- 千帆AI -->
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-bom</artifactId>
        <version>${kotlin.version}</version>
        <scope>import</scope>
        <type>pom</type>
    </dependency>

    <!-- Kotlin Standard Library -->
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib</artifactId>
        <version>${kotlin.version}</version>
    </dependency>

    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-reflect</artifactId>
        <version>${kotlin.version}</version>
    </dependency>

    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib-jdk8</artifactId>
        <version>${kotlin.version}</version>
    </dependency>

    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib-jdk7</artifactId>
        <version>${kotlin.version}</version>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>${jackson.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>${jackson.version}</version>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jdk8</artifactId>
        <version>${jackson.version}</version>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jsr310</artifactId>
        <version>${jackson.version}</version>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.module</groupId>
        <artifactId>jackson-module-kotlin</artifactId>
        <version>${jackson.version}</version>
    </dependency>

    <dependency>
        <groupId>com.openai</groupId>
        <artifactId>openai-java</artifactId>
        <version>${openai-java.version}</version>
    </dependency>

    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>okhttp</artifactId>
        <version>${okhttp.version}</version>
    </dependency>
</dependencies>

3.2)在common模块的pom.xml文件中引入openai-java的依赖,并排除有版本冲突的依赖然后重新引入

xml 复制代码
<!-- 千帆AI -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jdk8</artifactId>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-kotlin</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-reflect</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk7</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!--MyBatisPlus再AI集成中要排除有版本冲突的依赖-->
<!--MyBatisPlus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-reflect</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk7</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
</dependency>

<dependency>
    <groupId>com.openai</groupId>
    <artifactId>openai-java</artifactId>
    <exclusions>
        <exclusion>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.squareup.okio</groupId>
            <artifactId>okio</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk7</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-reflect</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib-jdk7</artifactId>
</dependency>

<!-- Kotlin Standard Library -->
<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib</artifactId>
</dependency>

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-reflect</artifactId>
</dependency>

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib-jdk8</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk7</artifactId>
        </exclusion>
    </exclusions>
</dependency>

3.3)参考官方示例(https://cloud.baidu.com/doc/qianfan-docs/s/nm9l6oc8e)编写测试方法来学习

java 复制代码
package com.zzyl;

import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.ResponseFormatJsonObject;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionCreateParams;

public class QianfanAIModelTest {
    public static void main(String[] args) {
        OpenAIClient client = OpenAIOkHttpClient.builder()
                .apiKey("bce-v3/ALTAK-6TGebWXMraNYDUsJdvwoh/61483266642b4a18108057ce85a900850d4b7715") //将your_APIKey替换为真实值,如何获取API Key请查看https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Um2wxbaps#步骤二-获取api-key
                .baseUrl("https://qianfan.baidubce.com/v2/") //千帆ModelBuilder平台地址
                .build();

        ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
                .addUserMessage("你是谁?") // 对话messages信息
                .model("ernie-4.0-8k") // 模型对应的model值,请查看支持的模型列表:https://cloud.baidu.com/doc/WENXINWORKSHOP/s/wm7ltcvgc
//                .responseFormat(ChatCompletionCreateParams.ResponseFormat.ofJsonObject(ResponseFormatJsonObject.builder().build()))
                .build();

        ChatCompletion chatCompletion = client.chat().completions().create(params);
        System.out.println(chatCompletion.choices().get(0).message().content().orElseGet(() -> ""));
    }
}

代码的apiKey要换成自己的

后续再接口开发中就要将AI集成到项目中。

接口开发

先用若依跟据表结构生成基础代码,便于后续代码的改造开发。

该项目主要有两个接口需要开发,上传体检报告保存智能评测(AI生成评测数据)

上传体检报告

为什么要有单独的文件上传接口,因为这个接口还有将数据添加到缓存的步骤。

可以由原先的通用上传接口改造。

在功能模块的pom.xml文件下:

xml 复制代码
<!-- 阿里云OSS -->
<dependency>
    <groupId>com.zzyl</groupId>
    <artifactId>zzyl-oss</artifactId>
</dependency>

工具类要存在common模块的utils包下:

java 复制代码
package com.zzyl.common.utils;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

public class PDFUtil {

    public static String pdfToString(InputStream inputStream) {

        PDDocument document = null;

        try {
            // 加载PDF文档
            document = PDDocument.load(inputStream);

            // 创建一个PDFTextStripper实例来提取文本
            PDFTextStripper pdfStripper = new PDFTextStripper();

            // 从PDF文档中提取文本
            String text = pdfStripper.getText(document);
            return text;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭PDF文档
            if (document != null) {
                try {
                    document.close();
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

该工具类要生效确保pdf工具包依赖已经引入。

在控制层:

java 复制代码
@Autowired
private AliyunOSSOperator aliyunOSSOperator;

@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 通用上传请求(单个)
*/             
@ApiOperation("上传体检报告")
@PostMapping("/upload")
public AjaxResult uploadFile(MultipartFile file, String idCardNo) throws Exception
{
    try {
        // 上传到OSS
        String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename());

        AjaxResult ajax = AjaxResult.success();
        ajax.put("url", url);
        ajax.put("fileName", url);
        ajax.put("newFileName", url.substring(url.lastIndexOf("/")));
        ajax.put("originalFilename", file.getOriginalFilename());
        // PDF文件内容读取为字符串
        String content = PDFUtil.pdfToString(file.getInputStream());
        // 临时存储到redis中
        redisTemplate.opsForHash().put(CacheConstants.HEALTH_REPORT_ALL_KEY, idCardNo, content);

        return ajax;
    } catch (Exception e) {
        return AjaxResult.error(e.getMessage());
    }
}
保存智能评测(AI生成评测数据)
设计Prompt

Prompt的构成部分:

  • 角色:给 AI 定义一个最匹配任务的角色,比如:「你是一位软件工程师」「你是一位小学老师」

  • 指示:对任务进行描述

  • 上下文:给出与任务相关的其它背景信息(尤其在多轮交互中)

  • 例子:必要时给出举例,[实践证明例子对输出正确性有帮助]

  • 输入:任务的输入信息;在提示词中明确的标识出输入

  • 输出:输出的格式描述,以便后继模块自动解析模型的输出结果,比如(JSON、Java)

    设计Prompt提示词,因为涉及了大量的专业名词和健康指标,实际开发中需要找产品经理协助

    js 复制代码
    请以一个专业医生的视角来分析这份体检报告,报告中包含了一些异常数据,我需要您对这些数据进行解读,并给出相应的健康建议。
    体检内容如下:
        内容略....
    
    要求:
    1. 提取体检报告中的"总检日期";
    2. 通过临床医学、疾病风险评估模型和数据智能分析,给该用户的风险等级和健康指数给出结果。风险等级分为:健康、提示、风险、危险、严重危险。健康指数范围为0至100分;
    3. 根据用户身体各项指标数据,详细说明该用户各项风险等级的占比是多少,最多保留两位小数。结论格式:该用户健康占比20.00%,提示占比20.00%,风险占比20%,危险占比20%,严重危险占比20%;
    4. 对于体检报告中的异常数据,请列出(异常数据的结论、体检项目名称、检查结果、参考值、单位、异常解读、建议)这7字段。解读异常数据,解决这些数据可能代表的健康问题或风险。分析可能的原因,包括但不限于生活习惯、饮食习惯、遗传因素等。基于这些异常数据和可能的原因,请给出具体的健康建议,包括饮食调整、运动建议、生活方式改变以及是否需要进一步检查或治疗等。
    结论格式:异常数据的结论:肥胖,体检项目名称:体重指数BMI,检查结果:29.2,参考值>24,单位:-。异常解读:体重超标包括超重与肥胖。体重指数(BMI)=体重(kg)/身⾼(m)的平⽅,BMI≥24为超重,BMI≥28为肥胖;男性腰围≥90cm和⼥性腰围≥85cm为腹型肥胖。体重超标是⼀种由多因素(如遗传、进⻝油脂较多、运动少、疾病等)引起的慢性代谢性疾病,尤其是肥胖,已经被世界卫⽣组织列为导致疾病负担的⼗⼤危险因素之⼀。AI建议:采取综合措施预防和控制体重,积极改变⽣活⽅式,宜低脂、低糖、⾼纤维素膳⻝,多⻝果蔬及菌藻类⻝物,增加有氧运动。若有相关疾病(如⾎脂异常、⾼⾎压、糖尿病等)应积极治疗。
    5. 根据这个体检报告的内容,分别给人体的8大系统打分,每项满分为100分,8大系统分别为:呼吸系统、消化系统、内分泌系统、免疫系统、循环系统、泌尿系统、运动系统、感官系统
    6. 给体检报告做一个总结,总结格式:体检报告中尿蛋⽩、癌胚抗原、⾎沉、空腹⾎糖、总胆固醇、⽢油三酯、低密度脂蛋⽩胆固醇、⾎清载脂蛋⽩B、动脉硬化指数、⽩细胞、平均红细胞体积、平均⾎红蛋⽩共12项指标提示异常,尿液常规共1项指标处于临界值,⾎脂、⾎液常规、尿液常规、糖类抗原、⾎清酶类等共43项指标提示正常,综合这些临床指标和数据分析:肾脏、肝胆、⼼脑⾎管存在隐患,其中⼼脑⾎管有"⾼危"⻛险;肾脏部位有"中危"⻛险;肝胆部位有"低危"⻛险。
    
    输出要求:
    最后,将以上结果输出为纯JSON格式,不要包含其他的文字说明,也不要出现Markdown语法相关的文字,所有的返回结果都是json,详细格式如下:
    
    {
      "totalCheckDate": "YYYY-MM-DD",
      "healthAssessment": {
        "riskLevel": "healthy/caution/risk/danger/severeDanger",
        "healthIndex": XX.XX
      },
      "riskDistribution": {
        "healthy": XX.XX,
        "caution": XX.XX,
        "risk": XX.XX,
        "danger": XX.XX,
        "severeDanger": XX.XX
      },
      "abnormalData": [
        {
          "conclusion": "异常数据的结论",
          "examinationItem": "体检项目名称",
          "result": "检查结果",
          "referenceValue": "参考值",
          "unit": "单位",
          "interpret":"对于异常的结论进一步详细的说明",
          "advice":"针对于这一项的异常,给出一些健康的建议"
        }
      ],
      "systemScore": {
        "breathingSystem": XX,
        "digestiveSystem": XX,
        "endocrineSystem": XX,
        "immuneSystem": XX,
        "circulatorySystem": XX,
        "urinarySystem": XX,
        "motionSystem": XX,
        "senseSystem": XX
      },
      "summarize": "体检报告的总结"
    }
功能编写步骤

当用户上传体检报告之后,点击确定,就会触发智能评测的调用。

1)一些可变的参数,在application.yml文件中定义:

yaml 复制代码
# 百度千帆大模型配置
baidu:
  qianfan:
    apiKey: bce-v3/ALTAK-6H6k7kuLyTmZPBmOJ2ajC/2520d4ad36847715153ca819932f1d854bb9e71f(换成自己的apiKey)
    baseUrl: https://qianfan.baidubce.com/v2/
    model: ernie-4.0-8k

2)在zzyl-common模块下定义工具类:

2.2)在zzyl-common模块下定义BaiduAIProperties配置类和AIModelInvoker工具类:

com.zzyl.common.ai.BaiduAIProperties配置类:

java 复制代码
package com.zzyl.common.ai;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "baidu.qianfan")
public class BaiduAIProperties {
    private String apiKey;
    private String baseUrl;
    private String model;
}

com.zzyl.common.ai.AIModelInvoker工具类:

java 复制代码
package com.zzyl.common.ai;

import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.ResponseFormatJsonObject;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class AIModelInvoker {
    @Autowired
    private BaiduAIProperties baiduAIProperties;

    public String qianfanInvoker(String prompt) {
        OpenAIClient client = OpenAIOkHttpClient.builder()
                .apiKey(baiduAIProperties.getApiKey())
                .baseUrl(baiduAIProperties.getBaseUrl())
                .build();

        ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
                .addUserMessage(prompt)
                .model(baiduAIProperties.getModel())
                .responseFormat(ChatCompletionCreateParams.ResponseFormat.ofJsonObject(ResponseFormatJsonObject.builder().build()))
                .build();

        ChatCompletion chatCompletion = client.chat().completions().create(params);
        return chatCompletion.choices().get(0).message().content().orElseGet(() -> "");
    }
}

2.2)跟据身份证提取个人信息的IDCardUtils工具类

java 复制代码
package com.zzyl.common.utils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.time.format.DateTimeFormatter;

public final class IDCardUtils {

    // 私有构造器,防止实例化
    private IDCardUtils() {
        throw new AssertionError("Cannot instantiate static class IDCardUtils");
    }

    /**
     * 根据身份证号提取个人年龄。
     *
     * @param idCard 身份证号码
     * @return 个人年龄,数值类型
     */
    public static int getAgeByIdCard(String idCard) {
        LocalDate birthDate = extractBirthDate(idCard);
        if (birthDate != null) {
            LocalDate currentDate = LocalDate.now();
            return Period.between(birthDate, currentDate).getYears();
        }
        throw new IllegalArgumentException("Invalid ID card number.");
    }

    /**
     * 根据身份证号提取出生日期。
     *
     * @param idCard 身份证号码
     * @return 出生日期,LocalDateTime类型
     */
    public static LocalDateTime getBirthDateByIdCard(String idCard) {
        String birthDateString = idCard.substring(6, 14); // 身份证号码中的出生日期部分
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        LocalDate birthDate = LocalDate.parse(birthDateString, formatter);
        return LocalDateTime.of(birthDate, LocalTime.MIDNIGHT);
    }

    /**
     * 根据身份证号提取性别。
     *
     * @param idCard 身份证号码
     * @return 性别,0表示女性,1表示男性
     */
    public static int getGenderFromIdCard(String idCard) {
        if (idCard == null || idCard.length() != 18) {
            throw new IllegalArgumentException("Invalid ID card number");
        }

        String genderCode = idCard.substring(16, 17);
        int gender = Integer.parseInt(genderCode);

        return gender % 2 == 0 ? 0 : 1;
    }

    /**
     * 内部辅助方法,从身份证号中提取出生日期的LocalDate对象。
     *
     * @param idCard 身份证号码
     * @return 出生日期的LocalDate对象,或null(如果身份证号码无效)
     */
    private static LocalDate extractBirthDate(String idCard) {
        if (idCard == null || idCard.length() != 18) {
            return null;
        }
        try {
            String birthStr = idCard.substring(6, 14);
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
            return LocalDate.parse(birthStr, formatter);
        } catch (Exception e) {
            return null;
        }
    }

    public static void main(String[] args) {
        System.out.println(getAgeByIdCard("330103199001011234"));
        System.out.println(getBirthDateByIdCard("330103199001011234"));
    }
}

3)改造MVC代码

3.1)修改IHealthAssessmentService中的insertHealthAssessment方法,把返回值类型替换为Long

java 复制代码
/**
 * 新增健康评估
 * 
 * @param healthAssessment 健康评估
 * @return 结果
 */
public Long insertHealthAssessment(HealthAssessment healthAssessment);

3.2)实现类中的返回值类型也要改为Long,并且按照思路来编写业务代码

最终代码如下:

java 复制代码
@Autowired
private RedisTemplate<String, String> redisTemplate;

@Autowired
private AIModelInvoker aiModelInvoker;

/**
 * 新增健康评估
 * 
 * @param healthAssessment 健康评估
 * @return 结果
 */
@Override
public Long insertHealthAssessment(HealthAssessment healthAssessment)
{
    // 1.设计Prompt提示词(需要从Redis中读取当前身份证号对应的体检报告)
    String prompt = getPrompt(healthAssessment.getIdCard());

    // 2.调用百度千帆大模型,分析体检报告,获取分析结果
    String qianfanResult = aiModelInvoker.qianfanInvoker(prompt);

    // 3.将分析结果保存到数据库中,并返回保存的这条记录的id
    // 将大模型返回的字符串解析为对象,方便取数据
    HealthReportVo healthReportVo = JSON.parseObject(qianfanResult, HealthReportVo.class);

    return saveHealthAssessment(healthReportVo, healthAssessment);
}

/**
 * 保存大模型返回的结果和前端传递的老人信息到数据库
 * @param healthReportVo        千帆大模型返回的结果
 * @param healthAssessment      老人基本信息
 * @return  记录的id
 */
private Long saveHealthAssessment(HealthReportVo healthReportVo, HealthAssessment healthAssessment) {
    // 老人身份证号
    String idCard = healthAssessment.getIdCard();

    healthAssessment.setBirthDate(IDCardUtils.getBirthDateByIdCard(idCard));
    healthAssessment.setAge(IDCardUtils.getAgeByIdCard(idCard));
    healthAssessment.setGender(IDCardUtils.getGenderFromIdCard(idCard));

    // 健康评分
    double healthScore = healthReportVo.getHealthAssessment().getHealthIndex();
    healthAssessment.setHealthScore(String.valueOf(healthScore));

    // 风险等级
    healthAssessment.setRiskLevel(healthReportVo.getHealthAssessment().getRiskLevel());

    // 通过健康评分判断是否建议入住
    healthAssessment.setSuggestionForAdmission(healthScore >= 60 ? 0 : 1);

    // 通过健康评分计算一个推荐的护理等级
    String nursingLevelName = getLevelNameByHealthScore(healthScore);
    healthAssessment.setNursingLevelName(nursingLevelName);

    // 统一先设置未入住
    healthAssessment.setAdmissionStatus(1);

    // 总检日期
    healthAssessment.setTotalCheckDate(healthReportVo.getTotalCheckDate());

    healthAssessment.setAssessmentTime(LocalDateTime.now());

    // 报告总结
    healthAssessment.setReportSummary(healthReportVo.getSummarize());

    // 疾病风险分布
    healthAssessment.setDiseaseRisk(JSON.toJSONString(healthReportVo.getRiskDistribution()));

    // 异常分析
    healthAssessment.setAbnormalAnalysis(JSON.toJSONString(healthReportVo.getAbnormalData()));

    // 八大系统评分
    healthAssessment.setSystemScore(JSON.toJSONString(healthReportVo.getSystemScore()));

    healthAssessmentMapper.insert(healthAssessment);
    return healthAssessment.getId();
}

private String getLevelNameByHealthScore(double healthScore) {
    if (healthScore > 100 || healthScore < 0) {
        throw new BaseException("健康评分值不合法");
    }

    if (healthScore >= 90) {
        return "四级护理等级";
    } else if (healthScore >= 80) {
        return "三级护理等级";
    } else if (healthScore >= 70) {
        return "二级护理等级";
    } else if (healthScore >= 60) {
        return "一级护理等级";
    } else {
        return "特级护理等级";
    }
}

/**
 * 获取Prompt提示词
 * @param idCard    身份证号
 * @return  提示词
 */
private String getPrompt(String idCard) {
    // 获取文件中的内容
    String content = (String) redisTemplate.opsForHash().get("healthReport", idCard);

    // 判断是否为空
    if (StringUtils.isEmpty(content)) {
        throw new BaseException("文件提取内容失败,请重新上传提交报告");
    }

    String prompt = "请以一个专业医生的视角来分析这份体检报告,报告中包含了一些异常数据,我需要您对这些数据进行解读,并给出相应的健康建议。\n" +
            "体检内容如下:\n" +
            content + "    \n" +
            "\n" +
            "要求:\n" +
            "1. 提取体检报告中的"总检日期";\n" +
            "2. 通过临床医学、疾病风险评估模型和数据智能分析,给该用户的风险等级和健康指数给出结果。风险等级分为:健康、提示、风险、危险、严重危险。健康指数范围为0至100分;\n" +
            "3. 根据用户身体各项指标数据,详细说明该用户各项风险等级的占比是多少,最多保留两位小数。结论格式:该用户健康占比20.00%,提示占比20.00%,风险占比20%,危险占比20%,严重危险占比20%;\n" +
            "4. 对于体检报告中的异常数据,请列出(异常数据的结论、体检项目名称、检查结果、参考值、单位、异常解读、建议)这7字段。解读异常数据,解决这些数据可能代表的健康问题或风险。分析可能的原因,包括但不限于生活习惯、饮食习惯、遗传因素等。基于这些异常数据和可能的原因,请给出具体的健康建议,包括饮食调整、运动建议、生活方式改变以及是否需要进一步检查或治疗等。\n" +
            "结论格式:异常数据的结论:肥胖,体检项目名称:体重指数BMI,检查结果:29.2,参考值>24,单位:-。异常解读:体重超标包括超重与肥胖。体重指数(BMI)=体重(kg)/身⾼(m)的平⽅,BMI≥24为超重,BMI≥28为肥胖;男性腰围≥90cm和⼥性腰围≥85cm为腹型肥胖。体重超标是⼀种由多因素(如遗传、进⻝油脂较多、运动少、疾病等)引起的慢性代谢性疾病,尤其是肥胖,已经被世界卫⽣组织列为导致疾病负担的⼗⼤危险因素之⼀。AI建议:采取综合措施预防和控制体重,积极改变⽣活⽅式,宜低脂、低糖、⾼纤维素膳⻝,多⻝果蔬及菌藻类⻝物,增加有氧运动。若有相关疾病(如⾎脂异常、⾼⾎压、糖尿病等)应积极治疗。\n" +
            "5. 根据这个体检报告的内容,分别给人体的8大系统打分,每项满分为100分,8大系统分别为:呼吸系统、消化系统、内分泌系统、免疫系统、循环系统、泌尿系统、运动系统、感官系统\n" +
            "6. 给体检报告做一个总结,总结格式:体检报告中尿蛋⽩、癌胚抗原、⾎沉、空腹⾎糖、总胆固醇、⽢油三酯、低密度脂蛋⽩胆固醇、⾎清载脂蛋⽩B、动脉硬化指数、⽩细胞、平均红细胞体积、平均⾎红蛋⽩共12项指标提示异常,尿液常规共1项指标处于临界值,⾎脂、⾎液常规、尿液常规、糖类抗原、⾎清酶类等共43项指标提示正常,综合这些临床指标和数据分析:肾脏、肝胆、⼼脑⾎管存在隐患,其中⼼脑⾎管有"⾼危"⻛险;肾脏部位有"中危"⻛险;肝胆部位有"低危"⻛险。\n" +
            "\n" +
            "输出要求:\n" +
            "最后,将以上结果输出为纯JSON格式,不要包含其他的文字说明,也不要出现Markdown语法相关的文字,所有的返回结果都是json,详细格式如下:\n" +
            "\n" +
            "{\n" +
            "  \"totalCheckDate\": \"YYYY-MM-DD\",\n" +
            "  \"healthAssessment\": {\n" +
            "    \"riskLevel\": \"healthy/caution/risk/danger/severeDanger\",\n" +
            "    \"healthIndex\": XX.XX\n" +
            "  },\n" +
            "  \"riskDistribution\": {\n" +
            "    \"healthy\": XX.XX,\n" +
            "    \"caution\": XX.XX,\n" +
            "    \"risk\": XX.XX,\n" +
            "    \"danger\": XX.XX,\n" +
            "    \"severeDanger\": XX.XX\n" +
            "  },\n" +
            "  \"abnormalData\": [\n" +
            "    {\n" +
            "      \"conclusion\": \"异常数据的结论\",\n" +
            "      \"examinationItem\": \"体检项目名称\",\n" +
            "      \"result\": \"检查结果\",\n" +
            "      \"referenceValue\": \"参考值\",\n" +
            "      \"unit\": \"单位\",\n" +
            "      \"interpret\":\"对于异常的结论进一步详细的说明\",\n" +
            "      \"advice\":\"针对于这一项的异常,给出一些健康的建议\"\n" +
            "    }\n" +
            "  ],\n" +
            "  \"systemScore\": {\n" +
            "    \"breathingSystem\": XX,\n" +
            "    \"digestiveSystem\": XX,\n" +
            "    \"endocrineSystem\": XX,\n" +
            "    \"immuneSystem\": XX,\n" +
            "    \"circulatorySystem\": XX,\n" +
            "    \"urinarySystem\": XX,\n" +
            "    \"motionSystem\": XX,\n" +
            "    \"senseSystem\": XX\n" +
            "  },\n" +
            "  \"summarize\": \"体检报告的总结\"\n" +
            "}";
    return prompt;
}

Vo可跟据Json格式生成并编写严格的提示词生成。

HealthReportVo值对象:

java 复制代码
package com.zzyl.nursing.vo;

import lombok.Data;

import java.util.List;

/**
 * 体检报告
 * @author itheima
 */
@Data
public class HealthReportVo {

    /**
     * 体检日期
     */
    private String totalCheckDate;
    /**
     * 健康评估
     */
    private HealthAssessmentVo healthAssessment;
    /**
     * 风险分布
     */
    private RiskDistributionVo riskDistribution;
    /**
     * 异常数据列表
     */
    private List<AbnormalDataVo> abnormalData;

    /**
     * 健康系统分值
     */
    private SystemScore systemScore;

    /**
     * 综合总结
     */
    private String summarize;
}

HealthAssessmentVo值对象:

java 复制代码
package com.zzyl.nursing.vo;

import lombok.Data;

/**
 *  健康评估类
 * @author itheima
 */
@Data
public class HealthAssessmentVo {
    /**
     * 健康风险等级
     */
    private String riskLevel;
    /**
     * 健康指数
     */
    private double healthIndex;

}

AbnormalDataVo值对象:

java 复制代码
package com.zzyl.nursing.vo;

import lombok.Data;

/**
 * 异常数据类
 * @author itheima
 */
@Data
public class AbnormalDataVo {
    /**
     * 结论
     */
    private String conclusion;
    /**
     * 检查项目
     */
    private String examinationItem;
    /**
     * 结果
     */
    private String result;
    /**
     * 参考值
     */
    private String referenceValue;
    /**
     * 单位
     */
    private String unit;
    /**
     * 结果解释
     */
    private String interpret;
    /**
     * 建议
     */
    private String advice;

}

RiskDistributionVo值对象:

java 复制代码
package com.zzyl.nursing.vo;

import lombok.Data;

/**
 * 风险分布类
 * @author itheima
 */
@Data
public class RiskDistributionVo {
    /**
     * 健康
     */
    private double healthy;
    /**
     * 警告
     */
    private double caution;
    /**
     * 风险
     */
    private double risk;
    /**
     * 危险
     */
    private double danger;
    /**
     * 严重危险
     */
    private double severeDanger;

}

SystemScore值对象:

java 复制代码
package com.zzyl.nursing.vo;

import lombok.Data;

@Data
public class SystemScore {

    /**
     * 呼吸系统
     */
    private int breathingSystem;

    /**
     * 消化系统
     */
    private int digestiveSystem;

    /**
     * 内分泌系统
     */
    private int endocrineSystem;

    /**
     * 免疫系统
     */
    private int immuneSystem;

    /**
     * 循环系统
     */
    private int circulatorySystem;

    /**
     * 泌尿系统
     */
    private int urinarySystem;

    /**
     * 感觉系统
     */
    private int motionSystem;

    /**
     * 感官系统
     */
    private int senseSystem;
}