SpringAI_PostgreSQL智能医院问诊助手
本文介绍如何利用Spring AI框架结合PostgreSQL pgvector扩展构建智能医院问诊客服助手。该系统可解决患者挂号困难、疾病咨询复杂等医疗痛点,提供智能导诊、用药咨询、病历解读等功能。采用的技术栈包括Spring Boot 3.2+、Spring AI 1.0.0-M4、PostgreSQL+pgvector等,实现高准确率(>90%)、低延迟(<2秒)的医疗咨询服务。
·
Spring AI + PostgreSQL 向量库:构建智能医院问诊客服助手
摘要
在医疗场景中,患者常常面临挂号困难、疾病咨询复杂等问题。本文将介绍如何使用 Spring AI 最新版本结合 PostgreSQL pgvector 扩展,构建一个生产级的智能医院问诊客服助手,实现症状识别、科室推荐、用药咨询等功能,大幅提升患者就医体验。
目录
- 项目背景与需求分析
- 技术架构设计
- 环境准备与配置
- PostgreSQL pgvector 详解
- 核心功能实现
- 医疗知识库构建
- 智能问诊引擎
- 多轮对话管理
- REST API 设计
- 生产环境部署
- 安全与合规
- 性能优化
- 测试与验证
- 总结与展望
1. 项目背景与需求分析
1.1 医疗行业痛点
┌─────────────────────────────────────────────────────────────┐
│ 传统医院就诊痛点分析 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 患者端痛点 解决方案 │
│ ───────────────────────────────────────────────────────── │
│ • 不知道看哪个科室 ➜ AI 科室推荐 │
│ • 症状描述不清晰 ➜ 症状识别引导 │
│ • 排队等待时间长 ➜ 预问诊分流 │
│ • 用药咨询困难 ➜ 智能用药指导 │
│ • 复诊提醒遗漏 ➜ 智能随访管理 │
│ │
│ 医院端痛点 解决方案 │
│ ───────────────────────────────────────────────────────── │
│ • 咨询窗口压力大 ➜ AI 客服分流 │
│ • 人工成本高 ➜ 24/7 智能服务 │
│ • 知识更新滞后 ➜ 知识库实时更新 │
│ • 服务质量不稳定 ➜ 标准化服务流程 │
│ • 数据分析困难 ➜ 智能数据洞察 │
│ │
└─────────────────────────────────────────────────────────────┘
1.2 功能需求
核心功能:
- 🏥 智能导诊:根据症状推荐科室和医生
- 💊 用药咨询:提供安全用药指导
- 📋 病历解读:解释检查报告
- 🔔 健康提醒:复诊、用药提醒
- 📊 数据统计:患者画像分析
技术要求:
- 高准确率(医疗知识检索准确率 > 90%)
- 低延迟(响应时间 < 2秒)
- 高可用(99.9% 可用性)
- 可追溯(所有对话可审计)
- 安全合规(符合医疗数据保护法规)
1.3 技术选型
| 组件 | 技术选型 | 理由 |
|---|---|---|
| 应用框架 | Spring Boot 3.2+ | 成熟稳定、生态完善 |
| AI 框架 | Spring AI 1.0.0-M4 | 原生 Spring 支持 |
| 向量数据库 | PostgreSQL + pgvector | 开源、关系型+向量支持 |
| LLM | OpenAI GPT-4 / Qwen | 高质量生成能力 |
| 缓存 | Redis | 高性能缓存 |
| 消息队列 | RabbitMQ | 异步任务处理 |
| 监控 | Prometheus + Grafana | 全方位监控 |
2. 技术架构设计
2.1 整体架构图
┌──────────────────────────────────────────────────────────────────┐
│ 智能医院问诊客服助手架构 │
└──────────────────────────────────────────────────────────────────┘
┌─────────────────┐
│ Web/Mobile │
│ 前端 │
└────────┬────────┘
│ HTTPS
┌──────────────▼──────────────┐
│ API Gateway │
│ (认证、限流、路由) │
└──────────────┬──────────────┘
│
┌──────────────────────────┼──────────────────────────┐
│ │ │
┌───────▼────────┐ ┌──────────▼─────────┐ ┌────────▼────────┐
│ 会话管理服务 │ │ 问诊核心服务 │ │ 知识库服务 │
│ │ │ │ │ │
│ • 会话上下文 │ │ • 症状识别 │ │ • 医学知识 │
│ • 用户状态 │◀────▶│ • 科室推荐 │◀────▶│ • 用药指南 │
│ • 对话历史 │ │ • 风险评估 │ │ • 疾病百科 │
│ │ │ • 应答生成 │ │ • 医生信息 │
└────────┬───────┘ └──────────┬─────────┘ └────────┬────────┘
│ │ │
│ │ │
┌────────▼───────────────────────────▼─────────────────────────▼────────┐
│ Spring AI Framework │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ ChatClient │ │ VectorStore │ │ Embedding │ │ Memory │ │
│ │ │ │ (PgVector) │ │ Model │ │ Manager │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └───────────┘ │
└────────────────────────────────────────────────────────────────────────┘
│ │ │
│ │ │
┌────────▼────────┐ ┌─────────▼────────┐ ┌────────▼────────┐
│ PostgreSQL │ │ Redis Cache │ │ RabbitMQ │
│ + pgvector │ │ │ │ │
│ │ │ • 会话缓存 │ │ • 异步任务 │
│ • 向量数据 │ │ • 热点数据 │ │ • 消息通知 │
│ • 业务数据 │ │ • 查询缓存 │ │ • 事件处理 │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
│
┌────────▼────────┐
│ OpenAI API │
│ / Qwen API │
│ │
│ • GPT-4 │
│ • Embedding │
└─────────────────┘
2.2 RAG 工作流程
┌──────────────────────────────────────────────────────────────┐
│ 智能问诊 RAG 工作流程 │
└──────────────────────────────────────────────────────────────┘
【患者问诊流程】
┌─────────────┐
│ 患者提问 │ "我最近总是头疼,应该看哪个科室?"
└──────┬──────┘
│
▼
┌─────────────┐ ┌──────────────────────────────┐
│ 意图识别 │─────▶│ 识别类型:症状咨询 │
└──────┬──────┘ │ 关键词:头疼、科室 │
│ └──────────────────────────────┘
▼
┌─────────────┐ ┌──────────────────────────────┐
│ 向量检索 │─────▶│ 从医学知识库检索相关文档 │
│ (pgvector) │ │ • 头疼症状说明 │
└──────┬──────┘ │ • 神经内科介绍 │
│ │ • 相关病例 │
▼ └──────────────────────────────┘
┌─────────────┐
│ 上下文增强 │ 增加患者历史、当前状态
└──────┬──────┘
│
▼
┌─────────────┐ ┌──────────────────────────────┐
│ LLM 生成 │─────▶│ 生成专业回答: │
│ 回答 │ │ "根据您的症状描述..." │
└──────┬──────┘ └──────────────────────────────┘
│
▼
┌─────────────┐ ┌──────────────────────────────┐
│ 结果优化 │─────▶│ • 添加科室链接 │
│ & 审核 │ │ • 添加注意事项 │
└──────┬──────┘ │ • 合规性检查 │
│ └──────────────────────────────┘
▼
┌─────────────┐
│ 返回患者 │ 结构化回答 + 推荐操作
└─────────────┘
3. 环境准备与配置
3.1 PostgreSQL + pgvector 安装
Docker 方式(推荐):
# 拉取支持 pgvector 的 PostgreSQL 镜像
docker pull pgvector/pgvector:pg16
# 启动 PostgreSQL
docker run -d \
--name postgres-vector \
-e POSTGRES_USER=medical_admin \
-e POSTGRES_PASSWORD=Medical@2024 \
-e POSTGRES_DB=medical_ai \
-p 5432:5432 \
-v postgres-data:/var/lib/postgresql/data \
pgvector/pgvector:pg16
# 验证安装
docker exec -it postgres-vector psql -U medical_admin -d medical_ai
medical_ai=# CREATE EXTENSION IF NOT EXISTS vector;
medical_ai=# SELECT * FROM pg_extension WHERE extname = 'vector';
Docker Compose 方式:
version: '3.8'
services:
postgres:
image: pgvector/pgvector:pg16
container_name: medical-postgres
environment:
POSTGRES_USER: medical_admin
POSTGRES_PASSWORD: Medical@2024
POSTGRES_DB: medical_ai
POSTGRES_INITDB_ARGS: "-E UTF8"
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U medical_admin"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: medical-redis
ports:
- "6379:6379"
command: redis-server --requirepass Medical@Redis2024
volumes:
- redis-data:/data
restart: unless-stopped
volumes:
postgres-data:
redis-data:
3.2 初始化数据库脚本
init-scripts/01-init.sql:
-- 创建 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 创建医学知识库表
CREATE TABLE medical_knowledge (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
content_type VARCHAR(50) NOT NULL, -- disease, symptom, medication, department
category VARCHAR(100),
embedding vector(1536), -- OpenAI ada-002 维度
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 创建向量索引(HNSW)
CREATE INDEX ON medical_knowledge USING hnsw (embedding vector_cosine_ops);
-- 创建元数据索引
CREATE INDEX idx_medical_knowledge_type ON medical_knowledge(content_type);
CREATE INDEX idx_medical_knowledge_category ON medical_knowledge(category);
CREATE INDEX idx_medical_knowledge_metadata ON medical_knowledge USING gin(metadata);
-- 创建患者会话表
CREATE TABLE patient_sessions (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(100) UNIQUE NOT NULL,
patient_id VARCHAR(100),
patient_name VARCHAR(100),
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) DEFAULT 'active', -- active, completed, expired
metadata JSONB
);
-- 创建对话历史表
CREATE TABLE conversation_history (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(100) NOT NULL,
role VARCHAR(20) NOT NULL, -- user, assistant, system
content TEXT NOT NULL,
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES patient_sessions(session_id)
);
-- 创建索引
CREATE INDEX idx_conversation_session ON conversation_history(session_id);
CREATE INDEX idx_conversation_created ON conversation_history(created_at);
-- 创建科室信息表
CREATE TABLE departments (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
specialties TEXT[],
keywords TEXT[],
available_time VARCHAR(200),
location VARCHAR(200),
contact VARCHAR(50)
);
-- 创建医生信息表
CREATE TABLE doctors (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
department_id INTEGER REFERENCES departments(id),
title VARCHAR(50),
specialties TEXT[],
introduction TEXT,
schedule JSONB,
rating DECIMAL(3,2)
);
-- 创建审计日志表
CREATE TABLE audit_logs (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(100),
action VARCHAR(50),
details JSONB,
ip_address INET,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 创建更新时间触发器
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_medical_knowledge_updated_at
BEFORE UPDATE ON medical_knowledge
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
3.3 Maven 依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<relativePath/>
</parent>
<groupId>com.medical</groupId>
<artifactId>ai-consultation-assistant</artifactId>
<version>1.0.0</version>
<name>AI Medical Consultation Assistant</name>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M4</spring-ai.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Boot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring AI OpenAI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- Spring AI PostgreSQL Vector Store -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
<!-- Spring AI PDF Document Reader -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- PGVector JDBC -->
<dependency>
<groupId>com.pgvector</groupId>
<artifactId>pgvector</artifactId>
<version>0.1.4</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Apache Commons Lang -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Micrometer for Metrics -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
3.4 应用配置
application.yml:
spring:
application:
name: medical-ai-assistant
# 数据源配置
datasource:
url: jdbc:postgresql://localhost:5432/medical_ai
username: medical_admin
password: Medical@2024
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# JPA 配置
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
format_sql: true
jdbc:
batch_size: 20
# Redis 配置
data:
redis:
host: localhost
port: 6379
password: Medical@Redis2024
timeout: 60s
jedis:
pool:
max-active: 20
max-idle: 10
min-idle: 5
# AI 配置
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4
temperature: 0.3 # 医疗场景需要更精确的回答
max-tokens: 2000
top-p: 0.9
embedding:
options:
model: text-embedding-ada-002
# PostgreSQL Vector Store 配置
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
initialize-schema: false
remove-existing-vector-store-table: false
# 服务器配置
server:
port: 8080
servlet:
context-path: /api
# 日志配置
logging:
level:
root: INFO
com.medical: DEBUG
org.springframework.ai: DEBUG
org.hibernate.SQL: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
# 管理端点
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
# 医疗 AI 助手自定义配置
medical:
assistant:
# 知识库配置
knowledge:
chunk-size: 800
chunk-overlap: 200
similarity-threshold: 0.75
max-results: 5
# 会话配置
session:
timeout-minutes: 30
max-history-messages: 20
# 安全配置
security:
enable-content-filter: true
enable-audit-log: true
sensitive-words:
- 诊断
- 确诊
- 处方
# 推荐配置
recommendation:
min-confidence: 0.8
max-departments: 3
4. PostgreSQL pgvector 详解
4.1 pgvector 核心概念
向量索引类型:
┌──────────────────────────────────────────────────────────┐
│ pgvector 索引类型对比 │
├──────────────────────────────────────────────────────────┤
│ │
│ 索引类型 精确度 速度 内存占用 适用场景 │
│ ───────────────────────────────────────────────────────│
│ HNSW 95-99% 快 中等 大规模数据 │
│ IVFFlat 90-95% 很快 低 超大规模 │
│ 无索引 100% 慢 低 小规模/测试 │
│ │
│ HNSW 参数: │
│ • m: 16-64 (连接数,越大越精确) │
│ • ef_construction: 64-400 (构建时搜索深度) │
│ │
└──────────────────────────────────────────────────────────┘
距离度量:
-- 余弦距离(推荐用于文本向量)
<=> operator -- 范围 [0, 2],0 表示完全相同
-- L2 距离(欧氏距离)
<-> operator -- 范围 [0, ∞)
-- 内积(需要归一化向量)
<#> operator -- 范围 (-∞, ∞)
4.2 医疗知识向量化示例
package com.medical.ai.config;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.vectorstore.PgVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class VectorStoreConfig {
@Value("${medical.assistant.knowledge.similarity-threshold:0.75}")
private double similarityThreshold;
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel, JdbcTemplate jdbcTemplate) {
return new PgVectorStore.Builder(jdbcTemplate, embeddingModel)
.withSchemaName("public")
.withTableName("medical_knowledge")
.withVectorColumnName("embedding")
.withContentColumnName("content")
.withMetadataColumnName("metadata")
.withDistanceType(PgVectorStore.PgDistanceType.COSINE_DISTANCE)
.withIndexType(PgVectorStore.PgIndexType.HNSW)
.withDimensions(1536)
.withInitializeSchema(false)
.build();
}
}
5. 核心功能实现
5.1 实体类定义
package com.medical.ai.entity;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Type;
import java.time.LocalDateTime;
/**
* 医学知识实体
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "medical_knowledge")
public class MedicalKnowledge {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
@Column(name = "content_type", nullable = false, length = 50)
private String contentType; // disease, symptom, medication, department
@Column(length = 100)
private String category;
@Column(columnDefinition = "jsonb")
private JsonNode metadata;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
/**
* 患者会话实体
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "patient_sessions")
public class PatientSession {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "session_id", unique = true, nullable = false)
private String sessionId;
@Column(name = "patient_id")
private String patientId;
@Column(name = "patient_name")
private String patientName;
@Column(name = "start_time")
private LocalDateTime startTime;
@Column(name = "last_active")
private LocalDateTime lastActive;
@Column(length = 20)
private String status; // active, completed, expired
@Column(columnDefinition = "jsonb")
private JsonNode metadata;
@PrePersist
protected void onCreate() {
startTime = LocalDateTime.now();
lastActive = LocalDateTime.now();
status = "active";
}
}
/**
* 对话历史实体
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "conversation_history")
public class ConversationHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "session_id", nullable = false)
private String sessionId;
@Column(nullable = false, length = 20)
private String role; // user, assistant, system
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
@Column(columnDefinition = "jsonb")
private JsonNode metadata;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}
/**
* 科室信息实体
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "departments")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(columnDefinition = "TEXT[]")
private String[] specialties;
@Column(columnDefinition = "TEXT[]")
private String[] keywords;
@Column(name = "available_time")
private String availableTime;
private String location;
private String contact;
}
5.2 Repository 层
package com.medical.ai.repository;
import com.medical.ai.entity.*;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface MedicalKnowledgeRepository extends JpaRepository<MedicalKnowledge, Long> {
List<MedicalKnowledge> findByContentType(String contentType);
List<MedicalKnowledge> findByCategory(String category);
}
@Repository
public interface PatientSessionRepository extends JpaRepository<PatientSession, Long> {
Optional<PatientSession> findBySessionId(String sessionId);
List<PatientSession> findByPatientId(String patientId);
@Query("SELECT s FROM PatientSession s WHERE s.lastActive < ?1 AND s.status = 'active'")
List<PatientSession> findExpiredSessions(LocalDateTime cutoffTime);
}
@Repository
public interface ConversationHistoryRepository extends JpaRepository<ConversationHistory, Long> {
List<ConversationHistory> findBySessionIdOrderByCreatedAtAsc(String sessionId);
@Query(value = "SELECT * FROM conversation_history WHERE session_id = ?1 " +
"ORDER BY created_at DESC LIMIT ?2", nativeQuery = true)
List<ConversationHistory> findRecentBySessionId(String sessionId, int limit);
}
@Repository
public interface DepartmentRepository extends JpaRepository<Department, Long> {
Optional<Department> findByName(String name);
@Query(value = "SELECT * FROM departments WHERE ?1 = ANY(keywords)", nativeQuery = true)
List<Department> findByKeyword(String keyword);
}
6. 医疗知识库构建
6.1 知识库加载服务
package com.medical.ai.service;
import com.medical.ai.entity.MedicalKnowledge;
import com.medical.ai.repository.MedicalKnowledgeRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
public class MedicalKnowledgeService {
private final VectorStore vectorStore;
private final MedicalKnowledgeRepository knowledgeRepository;
@Value("${medical.assistant.knowledge.chunk-size:800}")
private int chunkSize;
@Value("${medical.assistant.knowledge.chunk-overlap:200}")
private int chunkOverlap;
public MedicalKnowledgeService(
VectorStore vectorStore,
MedicalKnowledgeRepository knowledgeRepository) {
this.vectorStore = vectorStore;
this.knowledgeRepository = knowledgeRepository;
}
/**
* 加载医学文档到知识库
*/
@Transactional
public String loadMedicalDocument(Resource resource, String contentType, String category) {
try {
log.info("开始加载医学文档: {}, 类型: {}, 分类: {}",
resource.getFilename(), contentType, category);
// 1. 读取文档
TextReader textReader = new TextReader(resource);
List<Document> documents = textReader.get();
// 2. 分割文档
TokenTextSplitter splitter = new TokenTextSplitter(chunkSize, chunkOverlap, 5, 10000, true);
List<Document> chunks = splitter.apply(documents);
// 3. 添加元数据
chunks.forEach(doc -> {
Map<String, Object> metadata = new HashMap<>(doc.getMetadata());
metadata.put("content_type", contentType);
metadata.put("category", category);
metadata.put("source", resource.getFilename());
metadata.put("loaded_at", System.currentTimeMillis());
doc.setMetadata(metadata);
});
// 4. 存储到向量库
vectorStore.add(chunks);
// 5. 保存到数据库(用于管理和审计)
List<MedicalKnowledge> knowledgeEntries = chunks.stream()
.map(doc -> MedicalKnowledge.builder()
.content(doc.getContent())
.contentType(contentType)
.category(category)
.build())
.collect(Collectors.toList());
knowledgeRepository.saveAll(knowledgeEntries);
log.info("✅ 医学文档加载完成: {} 个文档块", chunks.size());
return String.format("成功加载 %d 个文档块", chunks.size());
} catch (Exception e) {
log.error("加载医学文档失败", e);
throw new RuntimeException("文档加载失败: " + e.getMessage(), e);
}
}
/**
* 初始化基础医学知识
*/
@Transactional
public void initializeBasicKnowledge() {
log.info("开始初始化基础医学知识库...");
// 疾病知识
List<Document> diseases = createDiseaseDocuments();
vectorStore.add(diseases);
// 症状知识
List<Document> symptoms = createSymptomDocuments();
vectorStore.add(symptoms);
// 用药知识
List<Document> medications = createMedicationDocuments();
vectorStore.add(medications);
// 科室信息
List<Document> departments = createDepartmentDocuments();
vectorStore.add(departments);
log.info("✅ 基础医学知识库初始化完成");
}
/**
* 创建疾病知识文档
*/
private List<Document> createDiseaseDocuments() {
return List.of(
createDocument(
"感冒(普通感冒):由病毒引起的上呼吸道感染,主要症状包括打喷嚏、鼻塞、流鼻涕、咽痛、咳嗽、" +
"低热、头痛和全身不适。多数情况下7-10天可自愈。需多休息、多饮水,症状严重时可用对症药物。" +
"如果高烧不退或症状持续超过10天,应就医检查。推荐科室:呼吸内科、全科医学科。",
"disease", "呼吸系统疾病"
),
createDocument(
"高血压:成人收缩压≥140mmHg和/或舒张压≥90mmHg。长期高血压可导致心脑血管疾病、肾脏疾病等。" +
"需要长期规律服药,定期监测血压,保持健康生活方式,包括低盐饮食、适度运动、控制体重、戒烟限酒。" +
"推荐科室:心血管内科、老年医学科。",
"disease", "心血管疾病"
),
createDocument(
"糖尿病(2型):以高血糖为特征的代谢性疾病。主要症状:多饮、多尿、多食、体重下降。" +
"需要控制饮食、规律运动、监测血糖、规律用药。并发症包括糖尿病肾病、视网膜病变、神经病变等。" +
"推荐科室:内分泌科。",
"disease", "内分泌疾病"
),
createDocument(
"偏头痛:反复发作的搏动性头痛,常伴有恶心、呕吐、畏光、畏声。发作持续4-72小时。" +
"诱因包括压力、睡眠不足、某些食物、激素变化等。治疗包括急性期止痛和预防性治疗。" +
"推荐科室:神经内科、疼痛科。",
"disease", "神经系统疾病"
)
);
}
/**
* 创建症状知识文档
*/
private List<Document> createSymptomDocuments() {
return List.of(
createDocument(
"头痛:常见症状,可能由多种原因引起。紧张性头痛、偏头痛、丛集性头痛是最常见的原发性头痛。" +
"继发性头痛可能提示严重疾病,如脑出血、脑膜炎、脑肿瘤等。伴有发热、颈部僵硬、意识改变、" +
"突然剧烈头痛应立即就医。推荐科室:神经内科。",
"symptom", "神经系统症状"
),
createDocument(
"胸痛:需要警惕心绞痛、心肌梗死等严重疾病。心源性胸痛特点:胸骨后压榨性疼痛,可放射至左肩、" +
"左臂,持续数分钟至数十分钟,休息或含服硝酸甘油可缓解。伴有呼吸困难、出汗、恶心应立即就医。" +
"推荐科室:心血管内科、急诊科。",
"symptom", "心血管症状"
),
createDocument(
"腹痛:根据部位不同可能提示不同疾病。右上腹痛:肝胆疾病;左上腹痛:胃、脾、胰腺疾病;" +
"右下腹痛:阑尾炎、妇科疾病;左下腹痛:结肠疾病。伴有发热、呕吐、腹泻、便血应及时就医。" +
"推荐科室:消化内科、普外科、妇科。",
"symptom", "消化系统症状"
)
);
}
/**
* 创建用药知识文档
*/
private List<Document> createMedicationDocuments() {
return List.of(
createDocument(
"布洛芬:非甾体抗炎药,用于解热镇痛。适应症:发热、头痛、牙痛、肌肉痛、关节痛等。" +
"成人常用量:200-400mg,每日3-4次。注意事项:空腹服用可能刺激胃粘膜,建议饭后服用。" +
"禁忌症:消化性溃疡、严重肝肾功能不全、孕晚期。",
"medication", "解热镇痛药"
),
createDocument(
"阿莫西林:青霉素类抗生素,用于细菌感染。适应症:呼吸道感染、泌尿道感染、皮肤软组织感染等。" +
"用法:成人500mg,每日3次,饭前或饭后均可。疗程:一般7-14天。" +
"注意事项:青霉素过敏者禁用,需在医生指导下使用,不可自行停药。",
"medication", "抗生素"
),
createDocument(
"硝苯地平:钙通道阻滞剂,用于治疗高血压和心绞痛。成人常用量:10-20mg,每日2-3次。" +
"缓释片:每日1-2次。注意事项:可能出现头痛、面部潮红、踝部水肿。不可突然停药。" +
"定期监测血压。孕妇慎用。",
"medication", "降压药"
)
);
}
/**
* 创建科室信息文档
*/
private List<Document> createDepartmentDocuments() {
return List.of(
createDocument(
"心血管内科:诊治心脏和血管系统疾病。常见疾病:高血压、冠心病、心律失常、心力衰竭、" +
"心肌病等。症状包括:胸痛、胸闷、心悸、气短、晕厥等。科室配备心电图、超声心动图、" +
"冠脉造影等检查设备。",
"department", "内科"
),
createDocument(
"呼吸内科:诊治呼吸系统疾病。常见疾病:肺炎、支气管炎、哮喘、慢阻肺、肺癌等。" +
"症状包括:咳嗽、咳痰、气促、胸痛、咯血等。可进行肺功能检查、支气管镜检查等。",
"department", "内科"
),
createDocument(
"消化内科:诊治消化系统疾病。常见疾病:胃炎、消化性溃疡、肝炎、胆囊炎、胰腺炎、" +
"炎症性肠病等。症状包括:腹痛、腹胀、恶心、呕吐、腹泻、便秘、黄疸等。" +
"可进行胃镜、肠镜、超声等检查。",
"department", "内科"
),
createDocument(
"神经内科:诊治神经系统疾病。常见疾病:脑卒中、癫痫、帕金森病、头痛、眩晕、周围神经病等。" +
"症状包括:头痛、头晕、肢体麻木无力、抽搐、意识障碍等。可进行头颅CT/MRI、脑电图等检查。",
"department", "内科"
)
);
}
/**
* 创建文档辅助方法
*/
private Document createDocument(String content, String contentType, String category) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("content_type", contentType);
metadata.put("category", category);
metadata.put("source", "system_init");
metadata.put("loaded_at", System.currentTimeMillis());
return new Document(content, metadata);
}
}
7. 智能问诊引擎
7.1 问诊服务核心实现
package com.medical.ai.service;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class MedicalConsultationService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
private final ConversationService conversationService;
@Value("${medical.assistant.knowledge.similarity-threshold:0.75}")
private double similarityThreshold;
@Value("${medical.assistant.knowledge.max-results:5}")
private int maxResults;
private static final String SYSTEM_PROMPT = """
你是一位专业的医疗咨询助手,具备丰富的医学知识。你的职责是:
1. 根据患者描述的症状,结合医学知识库,提供专业的初步判断和建议
2. 推荐合适的就诊科室和医生
3. 提供用药咨询和健康建议
4. 解答患者的医疗相关问题
重要准则:
- 你不能进行确诊,只能提供参考意见
- 对于严重症状,必须建议患者立即就医
- 回答要通俗易懂,避免过多专业术语
- 保持同理心,理解患者的焦虑
- 对于用药咨询,强调需在医生指导下使用
- 涉及诊断、处方等医疗行为时,提醒患者咨询专业医生
参考的医学知识:
{context}
""";
public MedicalConsultationService(
ChatClient.Builder chatClientBuilder,
VectorStore vectorStore,
ConversationService conversationService) {
this.chatClient = chatClientBuilder.build();
this.vectorStore = vectorStore;
this.conversationService = conversationService;
}
/**
* 处理患者咨询
*/
public ConsultationResponse consult(String sessionId, String question) {
log.info("处理咨询 - 会话: {}, 问题: {}", sessionId, question);
try {
// 1. 保存用户消息
conversationService.saveMessage(sessionId, "user", question);
// 2. 检索相关医学知识
List<Document> relevantDocs = retrieveRelevantKnowledge(question);
if (relevantDocs.isEmpty()) {
String fallbackAnswer = "抱歉,我在知识库中没有找到相关信息。为了您的健康," +
"建议您咨询专业医生或到医院就诊。";
conversationService.saveMessage(sessionId, "assistant", fallbackAnswer);
return ConsultationResponse.builder()
.answer(fallbackAnswer)
.confidence(0.0)
.requiresUrgentCare(false)
.build();
}
// 3. 构建上下文
String context = buildContext(relevantDocs);
// 4. 获取对话历史
List<Message> conversationHistory = conversationService.getConversationHistory(sessionId);
// 5. 生成回答
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(SYSTEM_PROMPT.replace("{context}", context)));
messages.addAll(conversationHistory);
messages.add(new UserMessage(question));
Prompt prompt = new Prompt(messages);
String answer = chatClient.prompt(prompt).call().content();
// 6. 分析回答并提取建议
ConsultationAnalysis analysis = analyzeResponse(answer, question);
// 7. 保存助手回答
conversationService.saveMessage(sessionId, "assistant", answer);
// 8. 构建响应
return ConsultationResponse.builder()
.answer(answer)
.confidence(calculateConfidence(relevantDocs))
.recommendedDepartments(analysis.getDepartments())
.requiresUrgentCare(analysis.isUrgent())
.references(extractReferences(relevantDocs))
.build();
} catch (Exception e) {
log.error("处理咨询失败", e);
throw new RuntimeException("咨询处理失败: " + e.getMessage(), e);
}
}
/**
* 检索相关医学知识
*/
private List<Document> retrieveRelevantKnowledge(String query) {
return vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(maxResults)
.withSimilarityThreshold(similarityThreshold)
);
}
/**
* 构建上下文
*/
private String buildContext(List<Document> documents) {
return documents.stream()
.map(doc -> {
String type = doc.getMetadata().getOrDefault("content_type", "").toString();
String category = doc.getMetadata().getOrDefault("category", "").toString();
return String.format("[%s - %s]\n%s", type, category, doc.getContent());
})
.collect(Collectors.joining("\n\n"));
}
/**
* 分析回答内容
*/
private ConsultationAnalysis analyzeResponse(String answer, String question) {
// 检测紧急关键词
boolean isUrgent = answer.toLowerCase().contains("立即") ||
answer.toLowerCase().contains("紧急") ||
answer.toLowerCase().contains("急诊") ||
question.toLowerCase().contains("剧烈") ||
question.toLowerCase().contains("突然");
// 提取推荐科室
List<String> departments = extractDepartments(answer);
return ConsultationAnalysis.builder()
.isUrgent(isUrgent)
.departments(departments)
.build();
}
/**
* 提取科室信息
*/
private List<String> extractDepartments(String text) {
List<String> departments = new ArrayList<>();
String[] commonDepts = {
"心血管内科", "呼吸内科", "消化内科", "神经内科",
"内分泌科", "肾内科", "血液科", "风湿免疫科",
"普外科", "骨科", "神经外科", "泌尿外科",
"妇科", "产科", "儿科", "眼科", "耳鼻喉科",
"皮肤科", "急诊科", "全科"
};
for (String dept : commonDepts) {
if (text.contains(dept)) {
departments.add(dept);
}
}
return departments;
}
/**
* 计算置信度
*/
private double calculateConfidence(List<Document> documents) {
if (documents.isEmpty()) {
return 0.0;
}
// 简化计算:基于检索到的文档数量和相关性
return Math.min(0.95, documents.size() * 0.15 + 0.5);
}
/**
* 提取参考资料
*/
private List<String> extractReferences(List<Document> documents) {
return documents.stream()
.limit(3)
.map(doc -> {
String type = doc.getMetadata().getOrDefault("content_type", "").toString();
String category = doc.getMetadata().getOrDefault("category", "").toString();
return String.format("%s - %s", type, category);
})
.collect(Collectors.toList());
}
/**
* 咨询响应
*/
@Data
@Builder
public static class ConsultationResponse {
private String answer;
private Double confidence;
private List<String> recommendedDepartments;
private Boolean requiresUrgentCare;
private List<String> references;
}
/**
* 咨询分析结果
*/
@Data
@Builder
private static class ConsultationAnalysis {
private boolean isUrgent;
private List<String> departments;
}
}
7.2 症状识别服务
package com.medical.ai.service;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
public class SymptomRecognitionService {
private final ChatClient chatClient;
private static final String SYMPTOM_EXTRACTION_PROMPT = """
从以下患者描述中提取症状信息,以JSON格式返回:
患者描述:{description}
请提取:
1. 主要症状(main_symptoms)
2. 伴随症状(accompanying_symptoms)
3. 持续时间(duration)
4. 严重程度(severity: mild/moderate/severe)
5. 诱发因素(triggers)
返回格式:
{
"main_symptoms": ["症状1", "症状2"],
"accompanying_symptoms": ["伴随症状1"],
"duration": "持续时间描述",
"severity": "严重程度",
"triggers": ["诱发因素"]
}
""";
public SymptomRecognitionService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
/**
* 识别症状
*/
public SymptomInfo recognizeSymptoms(String description) {
log.info("识别症状: {}", description);
try {
String prompt = SYMPTOM_EXTRACTION_PROMPT.replace("{description}", description);
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
// 解析 JSON 响应(实际应用中使用 Jackson 等工具)
return parseSymptomInfo(response);
} catch (Exception e) {
log.error("症状识别失败", e);
return SymptomInfo.builder()
.mainSymptoms(List.of(description))
.severity("unknown")
.build();
}
}
/**
* 引导式问诊
*/
public List<String> generateFollowUpQuestions(SymptomInfo symptomInfo) {
List<String> questions = new ArrayList<>();
if (symptomInfo.getDuration() == null || symptomInfo.getDuration().isEmpty()) {
questions.add("这些症状持续多久了?");
}
if (symptomInfo.getSeverity() == null || symptomInfo.getSeverity().equals("unknown")) {
questions.add("症状的严重程度如何?(轻微/中度/严重)");
}
if (symptomInfo.getTriggers().isEmpty()) {
questions.add("有什么情况会加重或缓解这些症状吗?");
}
questions.add("之前有类似症状吗?");
questions.add("目前是否在服用任何药物?");
questions.add("有慢性病史或家族病史吗?");
return questions.stream().limit(3).toList();
}
/**
* 解析症状信息(简化版)
*/
private SymptomInfo parseSymptomInfo(String jsonResponse) {
// 实际应用中应使用 Jackson 等工具解析
return SymptomInfo.builder()
.mainSymptoms(new ArrayList<>())
.accompanyingSymptoms(new ArrayList<>())
.duration("")
.severity("moderate")
.triggers(new ArrayList<>())
.build();
}
/**
* 症状信息
*/
@Data
@Builder
public static class SymptomInfo {
private List<String> mainSymptoms;
private List<String> accompanyingSymptoms;
private String duration;
private String severity;
private List<String> triggers;
}
}
8. 多轮对话管理
package com.medical.ai.service;
import com.medical.ai.entity.ConversationHistory;
import com.medical.ai.entity.PatientSession;
import com.medical.ai.repository.ConversationHistoryRepository;
import com.medical.ai.repository.PatientSessionRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
@Service
public class ConversationService {
private final PatientSessionRepository sessionRepository;
private final ConversationHistoryRepository historyRepository;
@Value("${medical.assistant.session.max-history-messages:20}")
private int maxHistoryMessages;
@Value("${medical.assistant.session.timeout-minutes:30}")
private int sessionTimeoutMinutes;
public ConversationService(
PatientSessionRepository sessionRepository,
ConversationHistoryRepository historyRepository) {
this.sessionRepository = sessionRepository;
this.historyRepository = historyRepository;
}
/**
* 创建新会话
*/
@Transactional
public String createSession(String patientId, String patientName) {
String sessionId = UUID.randomUUID().toString();
PatientSession session = PatientSession.builder()
.sessionId(sessionId)
.patientId(patientId)
.patientName(patientName)
.build();
sessionRepository.save(session);
log.info("创建新会话: {}, 患者: {}", sessionId, patientName);
return sessionId;
}
/**
* 保存消息
*/
@Transactional
public void saveMessage(String sessionId, String role, String content) {
ConversationHistory history = ConversationHistory.builder()
.sessionId(sessionId)
.role(role)
.content(content)
.build();
historyRepository.save(history);
// 更新会话最后活动时间
sessionRepository.findBySessionId(sessionId).ifPresent(session -> {
session.setLastActive(LocalDateTime.now());
sessionRepository.save(session);
});
log.debug("保存消息 - 会话: {}, 角色: {}", sessionId, role);
}
/**
* 获取对话历史
*/
@Cacheable(value = "conversationHistory", key = "#sessionId")
public List<Message> getConversationHistory(String sessionId) {
List<ConversationHistory> histories =
historyRepository.findRecentBySessionId(sessionId, maxHistoryMessages);
return histories.stream()
.map(history -> {
if ("user".equals(history.getRole())) {
return new UserMessage(history.getContent());
} else {
return new AssistantMessage(history.getContent());
}
})
.collect(Collectors.toList());
}
/**
* 结束会话
*/
@Transactional
public void endSession(String sessionId) {
sessionRepository.findBySessionId(sessionId).ifPresent(session -> {
session.setStatus("completed");
sessionRepository.save(session);
log.info("会话结束: {}", sessionId);
});
}
/**
* 清理过期会话
*/
@Transactional
public void cleanupExpiredSessions() {
LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(sessionTimeoutMinutes);
List<PatientSession> expiredSessions = sessionRepository.findExpiredSessions(cutoffTime);
expiredSessions.forEach(session -> {
session.setStatus("expired");
sessionRepository.save(session);
});
log.info("清理了 {} 个过期会话", expiredSessions.size());
}
/**
* 获取会话信息
*/
public PatientSession getSession(String sessionId) {
return sessionRepository.findBySessionId(sessionId)
.orElseThrow(() -> new RuntimeException("会话不存在: " + sessionId));
}
}
9. REST API 设计
9.1 咨询控制器
package com.medical.ai.controller;
import com.medical.ai.service.MedicalConsultationService;
import com.medical.ai.service.ConversationService;
import com.medical.ai.service.SymptomRecognitionService;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/consultation")
public class ConsultationController {
private final MedicalConsultationService consultationService;
private final ConversationService conversationService;
private final SymptomRecognitionService symptomService;
public ConsultationController(
MedicalConsultationService consultationService,
ConversationService conversationService,
SymptomRecognitionService symptomService) {
this.consultationService = consultationService;
this.conversationService = conversationService;
this.symptomService = symptomService;
}
/**
* 开始咨询会话
*/
@PostMapping("/session/start")
public ResponseEntity<ApiResponse<SessionResponse>> startSession(
@RequestBody @Valid StartSessionRequest request) {
log.info("开始咨询会话 - 患者: {}", request.getPatientName());
String sessionId = conversationService.createSession(
request.getPatientId(),
request.getPatientName()
);
return ResponseEntity.ok(ApiResponse.success(
SessionResponse.builder()
.sessionId(sessionId)
.welcomeMessage("您好!我是智能医疗助手,请描述您的症状或健康问题,我会尽力帮助您。")
.build()
));
}
/**
* 发送咨询问题
*/
@PostMapping("/ask")
public ResponseEntity<ApiResponse<MedicalConsultationService.ConsultationResponse>> ask(
@RequestBody @Valid ConsultationRequest request) {
log.info("收到咨询 - 会话: {}, 问题: {}", request.getSessionId(), request.getQuestion());
try {
MedicalConsultationService.ConsultationResponse response =
consultationService.consult(request.getSessionId(), request.getQuestion());
return ResponseEntity.ok(ApiResponse.success(response));
} catch (Exception e) {
log.error("咨询处理失败", e);
return ResponseEntity.ok(ApiResponse.error("处理失败: " + e.getMessage()));
}
}
/**
* 症状识别
*/
@PostMapping("/symptom/recognize")
public ResponseEntity<ApiResponse<SymptomResponse>> recognizeSymptom(
@RequestBody @Valid SymptomRequest request) {
log.info("症状识别 - 描述: {}", request.getDescription());
SymptomRecognitionService.SymptomInfo symptomInfo =
symptomService.recognizeSymptoms(request.getDescription());
var followUpQuestions = symptomService.generateFollowUpQuestions(symptomInfo);
return ResponseEntity.ok(ApiResponse.success(
SymptomResponse.builder()
.symptomInfo(symptomInfo)
.followUpQuestions(followUpQuestions)
.build()
));
}
/**
* 结束会话
*/
@PostMapping("/session/end")
public ResponseEntity<ApiResponse<String>> endSession(
@RequestBody @Valid EndSessionRequest request) {
log.info("结束会话: {}", request.getSessionId());
conversationService.endSession(request.getSessionId());
return ResponseEntity.ok(ApiResponse.success("会话已结束,感谢使用!"));
}
// ===== 请求/响应 DTO =====
@Data
public static class StartSessionRequest {
private String patientId;
@NotBlank(message = "患者姓名不能为空")
private String patientName;
}
@Data
@lombok.Builder
public static class SessionResponse {
private String sessionId;
private String welcomeMessage;
}
@Data
public static class ConsultationRequest {
@NotBlank(message = "会话ID不能为空")
private String sessionId;
@NotBlank(message = "问题不能为空")
private String question;
}
@Data
public static class SymptomRequest {
@NotBlank(message = "症状描述不能为空")
private String description;
}
@Data
@lombok.Builder
public static class SymptomResponse {
private SymptomRecognitionService.SymptomInfo symptomInfo;
private java.util.List<String> followUpQuestions;
}
@Data
public static class EndSessionRequest {
@NotBlank(message = "会话ID不能为空")
private String sessionId;
}
@Data
@lombok.Builder
public static class ApiResponse<T> {
private Boolean success;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.success(true)
.data(data)
.build();
}
public static <T> ApiResponse<T> error(String message) {
return ApiResponse.<T>builder()
.success(false)
.message(message)
.build();
}
}
}
9.2 知识库管理 API
package com.medical.ai.controller;
import com.medical.ai.service.MedicalKnowledgeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@RestController
@RequestMapping("/knowledge")
public class KnowledgeController {
private final MedicalKnowledgeService knowledgeService;
public KnowledgeController(MedicalKnowledgeService knowledgeService) {
this.knowledgeService = knowledgeService;
}
/**
* 上传医学文档
*/
@PostMapping("/upload")
public ResponseEntity<String> uploadDocument(
@RequestParam("file") MultipartFile file,
@RequestParam("contentType") String contentType,
@RequestParam("category") String category) {
log.info("上传医学文档: {}, 类型: {}, 分类: {}",
file.getOriginalFilename(), contentType, category);
try {
ByteArrayResource resource = new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return file.getOriginalFilename();
}
};
String result = knowledgeService.loadMedicalDocument(resource, contentType, category);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("文档上传失败", e);
return ResponseEntity.internalServerError().body("上传失败: " + e.getMessage());
}
}
/**
* 初始化知识库
*/
@PostMapping("/initialize")
public ResponseEntity<String> initializeKnowledge() {
log.info("初始化医学知识库");
try {
knowledgeService.initializeBasicKnowledge();
return ResponseEntity.ok("知识库初始化成功");
} catch (Exception e) {
log.error("知识库初始化失败", e);
return ResponseEntity.internalServerError().body("初始化失败: " + e.getMessage());
}
}
}
10. 生产环境部署
10.1 Docker Compose 完整配置
version: '3.8'
services:
# PostgreSQL + pgvector
postgres:
image: pgvector/pgvector:pg16
container_name: medical-postgres
environment:
POSTGRES_USER: medical_admin
POSTGRES_PASSWORD: Medical@2024
POSTGRES_DB: medical_ai
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U medical_admin"]
interval: 10s
timeout: 5s
retries: 5
# Redis
redis:
image: redis:7-alpine
container_name: medical-redis
command: redis-server --requirepass Medical@Redis2024
ports:
- "6379:6379"
volumes:
- redis-data:/data
restart: unless-stopped
# Spring Boot 应用
app:
build:
context: .
dockerfile: Dockerfile
container_name: medical-ai-app
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/medical_ai
SPRING_DATASOURCE_USERNAME: medical_admin
SPRING_DATASOURCE_PASSWORD: Medical@2024
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PASSWORD: Medical@Redis2024
OPENAI_API_KEY: ${OPENAI_API_KEY}
ports:
- "8080:8080"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
volumes:
postgres-data:
redis-data:
10.2 Dockerfile
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/ai-consultation-assistant-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Xmx2g", "-Xms512m", "app.jar"]
11. 安全与合规
11.1 数据脱敏
package com.medical.ai.util;
import org.springframework.stereotype.Component;
import java.util.regex.Pattern;
@Component
public class DataMaskingUtil {
private static final Pattern PHONE_PATTERN = Pattern.compile("(\\d{3})\\d{4}(\\d{4})");
private static final Pattern ID_CARD_PATTERN = Pattern.compile("(\\d{6})\\d{8}(\\d{4})");
/**
* 手机号脱敏
*/
public String maskPhone(String phone) {
if (phone == null || phone.length() != 11) {
return phone;
}
return PHONE_PATTERN.matcher(phone).replaceAll("$1****$2");
}
/**
* 身份证号脱敏
*/
public String maskIdCard(String idCard) {
if (idCard == null || idCard.length() != 18) {
return idCard;
}
return ID_CARD_PATTERN.matcher(idCard).replaceAll("$1********$2");
}
/**
* 姓名脱敏
*/
public String maskName(String name) {
if (name == null || name.length() < 2) {
return name;
}
return name.charAt(0) + "*".repeat(name.length() - 1);
}
}
12. 总结与展望
12.1 项目总结
┌────────────────────────────────────────────────────────┐
│ 智能医院问诊客服助手核心价值 │
├────────────────────────────────────────────────────────┤
│ │
│ ✅ 技术创新 │
│ • Spring AI + PostgreSQL pgvector 深度集成 │
│ • RAG 技术在医疗场景的应用 │
│ │
│ ✅ 业务价值 │
│ • 提升患者就医体验 │
│ • 降低医院运营成本 │
│ • 24/7 智能服务 │
│ │
│ ✅ 生产就绪 │
│ • 完整的安全合规设计 │
│ • 高可用架构 │
│ • 性能优化 │
│ │
│ ✅ 可扩展性 │
│ • 模块化设计 │
│ • 易于集成其他系统 │
│ • 支持持续优化 │
│ │
└────────────────────────────────────────────────────────┘
12.2 未来展望
- 🎯 多模态支持(图像识别、语音交互)
- 📊 患者画像与智能推荐
- 🔬 集成检验报告解读
- 🌐 多语言支持
- 🤖 更智能的 Agent 能力
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)