第二章:动态 Prompt 管理与多科室智能问答系统

Spring AI Alibaba 医疗系统集成完整教程


🎯 学习目标

通过本教程学习,你将掌握:

  1. Spring AI Alibaba 框架的核心使用方法
  2. 动态 Prompt 管理的完整实现(支持 Nacos 和本地两种模式)
  3. 多科室智能问答系统的架构设计与实现]

📖 项目简介

业务场景

本项目是一个基于 Spring AI Alibaba 的医疗信息系统(HIS)AI 集成演示,主要实现:

  • 🏥 多科室智能问答:内科、儿科、外科各自专业的 AI 咨询
  • 📋 动态 Prompt 管理:支持热更新,无需重启即可切换提示词模板
  • 🎯 双模式切换:Nacos 配置中心 + 本地文件模式任意切换

为什么需要动态 Prompt 管理?

在医院 AI 系统中,Prompt 是模型行为的核心驱动

  • 内科问答更关注病因分析与化验指标
  • 儿科问答需避免过度医学术语,用语更加亲切
  • 外科问答重点关注手术适应症和风险评估

如果每次修改 Prompt 都要重启服务,维护成本极高。因此,动态 Prompt 管理成为生产环境的必要能力。


🏗️ 技术架构

核心技术栈

复制代码
Spring Boot 3.2.0
├── Spring AI (1.0.0-M1) - AI 集成框架
├── Spring Cloud Context - 动态刷新支持
├── Nacos Client (2.4.3) - 配置中心
├── Milvus (2.3.5) - 向量数据库
└── 通义千问 API - 底层大模型

系统架构图

复制代码
┌─────────────────┐
│  前端/客户端     │
└────────┬────────┘
         │ HTTP Request
         ▼
┌─────────────────────────────────────┐
│      Controller Layer                │
│  ├─ DepartmentController (多科室)    │
│  ├─ RagController (RAG 问答)         │
│  └─ HisAiController (通用接口)       │
└────────┬────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────┐
│       Service Layer                  │
│  ├─ DepartmentAiService              │
│  ├─ SimpleRagService                 │
│  └─ RagMedicalQaService              │
└────────┬────────────────────────────┘
         │
    ┌────┴────┐
    ▼         ▼
┌─────────┐ ┌──────────────┐
│ Prompt  │ │   ChatClient │
│ Loader  │ │  (Spring AI) │
└────┬────┘ └──────┬───────┘
     │             │
     ▼             ▼
┌─────────┐  ┌──────────┐
│ Nacos   │  │ 通义千问  │
│ Config  │  │   API    │
└─────────┘  └──────────┘

🚀 环境准备

1. 基础环境

bash 复制代码
# Java 17+
java -version

# Maven 3.6+
mvn -version

# Nacos 2.x(可选)
# 下载:https://nacos.io/zh-cn/docs/quick-start.html

2. 获取通义千问 API Key

  1. 访问 阿里云灵积模型平台
  2. 注册并创建 API Key
  3. 设置环境变量:
bash 复制代码
export DASHSCOPE_API_KEY=sk-your-api-key-here

3. 启动 Nacos(可选)

bash 复制代码
# 单机模式启动
cd nacos/bin
sh startup.sh -m standalone

# 访问控制台: http://localhost:8848/nacos
# 默认账号密码: nacos/nacos

📁 项目结构详解

复制代码
spring-ai-his-demo/
├── src/main/java/com/etong/ai/his/
│   ├── HisAiApplication.java          # 主启动类
│   ├── config/                        # 配置层
│   │   ├── ChatConfig.java            # ChatClient 配置
│   │   ├── LocalPromptLoader.java     # 本地 Prompt 加载器
│   │   └── PromptLoader.java          # Nacos Prompt 加载器
│   ├── controller/                    # 控制器层
│   │   ├── DepartmentController.java  # 科室问答接口
│   │   └── RagController.java         # RAG 问答接口
│   ├── service/                       # 服务层
│   │   ├── DepartmentAiService.java   # 科室 AI 服务
│   │   └── rag/
│   │       ├── SimpleRagService.java  # 简化 RAG 服务
│   │       └── RagMedicalQaService.java # 医学 RAG 服务
│   └── model/                         # 数据模型
├── src/main/resources/
│   ├── application.yml                # 主配置文件
│   ├── bootstrap.yml                  # Bootstrap 配置
│   └── prompts/                       # Prompt 模板目录
│       ├── prompt_his_internist.yaml  # 内科模板
│       ├── prompt_his_pediatrics.yaml # 儿科模板
│       └── prompt_his_surgery.yaml    # 外科模板
└── pom.xml                            # Maven 配置

💻 核心功能实现

功能一:动态 Prompt 加载器

设计思路

系统支持两种 Prompt 加载模式:

  1. Nacos 模式:从配置中心动态加载,支持热更新
  2. 本地模式:从 classpath 加载,适合开发测试
PromptLoader 核心代码
java 复制代码
@Slf4j
@Component
public class PromptLoader {

    @Value("${prompt.source:nacos}")
    private String promptSource;

    @Autowired
    private ConfigService configService;

    private Map<String, String> promptCache = new ConcurrentHashMap<>();

    @PostConstruct
    public void loadPrompts() {
        log.info("开始加载Prompt模板配置,来源:{}", promptSource);

        if ("nacos".equalsIgnoreCase(promptSource)) {
            loadPromptsFromNacos();
        } else {
            loadPromptsFromLocal();
        }
    }

    private void loadPromptFromNacos(String dataId) {
        try {
            // 1. 从 Nacos 获取配置
            String content = configService.getConfig(dataId, group, 5000);
            promptCache.put(dataId, content);

            // 2. 添加配置监听器,实现热更新
            configService.addListener(dataId, group, new Listener() {
                @Override
                public void receiveConfigInfo(String newConfig) {
                    promptCache.put(dataId, newConfig);
                    log.info("✅ Prompt配置已更新: {}", dataId);
                }

                @Override
                public Executor getExecutor() {
                    return null;
                }
            });
        } catch (NacosException e) {
            log.error("从Nacos加载失败,回退到本地: {}", dataId);
            loadPromptFromLocal(dataId);
        }
    }

    public String getPrompt(String dataId) {
        return promptCache.getOrDefault(dataId, 
            "你是一名专业医生,请基于医学常识回答问题。{{question}}");
    }
}

关键点说明:

  • ✅ 使用 ConcurrentHashMap 确保线程安全
  • ✅ Nacos 监听器实现配置热更新
  • ✅ 失败降级机制:Nacos 失败自动切换本地

功能二:多科室智能问答服务

DepartmentAiService 核心实现
java 复制代码
@Slf4j
@Service
public class DepartmentAiService {

    @Autowired
    private LocalPromptLoader promptLoader;

    @Autowired
    private ChatClient chatClient;

    /**
     * 根据科室提供专业问答
     * @param department 科室名称:internist(内科), pediatrics(儿科), surgery(外科)
     * @param question 用户问题
     * @return AI 回答
     */
    public String askByDepartment(String department, String question) {
        log.info("收到{}科室的咨询问题: {}", department, question);

        // 1. 获取科室专属 Prompt 模板
        String dataId = "prompt_his_" + department + ".yaml";
        String promptTemplate = promptLoader.getPrompt(dataId);

        // 2. 替换模板变量
        String prompt = promptTemplate.replace("{{question}}", question);

        // 3. 调用 Spring AI ChatClient
        try {
            String response = chatClient.prompt()
                    .user(prompt)
                    .call()
                    .content();

            log.info("✅ AI回复成功,科室: {}", department);
            return response;

        } catch (Exception e) {
            log.error("❌ AI调用失败", e);
            return "抱歉,系统暂时无法处理您的问题,请稍后再试。";
        }
    }
}

核心优势:

  • 🎯 不同科室使用不同的专业 Prompt
  • 🔄 支持动态切换模板而不重启服务
  • 🛡️ 完善的异常处理和降级机制

功能三:Prompt 模板设计

内科 Prompt 模板
yaml 复制代码
name: internist_qa
version: v1.0
description: 内科医生问答模板
content: |
  你是一名专业的内科医生,请基于医学常识和患者症状,给出可能的诊断建议。
  
  请注意:
  1. 回答要专业、准确
  2. 给出可能的病因分析
  3. 建议相应的检查项目
  4. 必要时建议就医

  当前问题:{{question}}

  请以专业内科医生的口吻回答:
儿科 Prompt 模板
yaml 复制代码
name: pediatrics_qa
version: v1.0
description: 儿科医生问答模板
content: |
  你是一名儿科医生,请用简明易懂的语言回答家长的问题。
  
  请注意:
  1. 使用温和、亲切的语气
  2. 避免使用过度专业的医学术语
  3. 给出实用的家庭护理建议
  4. 明确指出需要立即就医的危险信号

  当前问题:{{question}}

  请以儿科医生的口吻回答:
外科 Prompt 模板
yaml 复制代码
name: surgery_qa
version: v1.0
description: 外科医生问答模板
content: |
  你是一名外科医生,请基于外科专业知识回答患者的问题。
  
  请注意:
  1. 重点关注手术治疗的可能性
  2. 说明手术的适应症和禁忌症
  3. 解释术前准备和术后护理要点
  4. 评估手术风险和预后

  当前问题:{{question}}

  请以外科医生的口吻回答:

模板设计原则:

  • ✅ 明确角色定位
  • ✅ 列出关注重点
  • ✅ 统一使用 {``{question}} 占位符
  • ✅ 符合专业规范

功能四:RESTful API 接口设计

DepartmentController 实现
java 复制代码
@Slf4j
@RestController
@RequestMapping("/ai/department")
@RequiredArgsConstructor
public class DepartmentController {

    private final DepartmentAiService departmentAiService;

    /**
     * 科室问答接口(GET 方式)
     */
    @GetMapping
    public ResponseEntity<String> ask(
            @RequestParam String department, 
            @RequestParam String question) {
        
        log.info("收到科室咨询 - 科室: {}, 问题: {}", department, question);

        try {
            String response = departmentAiService.askByDepartment(department, question);
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            log.error("处理失败", e);
            return ResponseEntity.internalServerError()
                    .body("系统处理失败: " + e.getMessage());
        }
    }

    /**
     * Prompt 热更新接口
     */
    @PostMapping("/refresh")
    public ResponseEntity<String> refreshPrompt(@RequestParam String department) {
        log.info("收到Prompt刷新请求 - 科室: {}", department);

        try {
            String response = departmentAiService.refreshPrompt(department);
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            return ResponseEntity.internalServerError()
                    .body("刷新失败: " + e.getMessage());
        }
    }

    /**
     * 健康检查接口
     */
    @GetMapping("/health")
    public ResponseEntity<String> health() {
        return ResponseEntity.ok("Department AI Service is running");
    }
}

🧪 运行与测试

1. 启动应用

bash 复制代码
# 方式一:使用 Maven
mvn spring-boot:run

# 方式二:打包后运行
mvn clean package
java -jar target/spring-ai-his-demo-1.0.0-SNAPSHOT.jar

2. 测试用例

测试一:内科咨询
bash 复制代码
curl "http://localhost:8080/ai/department?department=internist&question=持续低热三天怎么办"

预期回答:

复制代码
您可能有轻度感染或内分泌问题。建议:
1. 进行血常规检查,了解白细胞情况
2. 检查甲状腺功能
3. 如果伴有咳嗽、乏力,建议胸部CT
4. 建议到内科门诊就诊,明确诊断

注意:持续发热超过3天应及时就医。
测试二:儿科咨询
bash 复制代码
curl "http://localhost:8080/ai/department?department=pediatrics&question=孩子晚上咳嗽厉害怎么办"

预期回答:

复制代码
家长您好,孩子夜间咳嗽可能是以下原因:

护理建议:
• 保持室内湿度在50-60%
• 睡前可以用温水蒸气熏一下
• 抬高枕头,帮助呼吸顺畅
• 避免接触烟雾和刺激性气味

⚠️ 需要立即就医的情况:
- 呼吸急促或困难
- 伴有高热(38.5°C以上)
- 咳嗽超过一周未缓解
- 出现喘息声音

建议先观察1-2天,如症状加重请及时就医。
测试三:外科咨询
bash 复制代码
curl "http://localhost:8080/ai/department?department=surgery&question=阑尾炎一定要手术吗"

预期回答:

复制代码
作为外科医生,我来为您解答:

手术适应症:
1. 急性阑尾炎伴腹膜刺激征
2. 阑尾脓肿形成
3. 保守治疗无效的慢性阑尾炎

非手术治疗情况:
- 早期单纯性阑尾炎可尝试抗生素治疗
- 阑尾周围脓肿可先引流后择期手术

手术优势:
✓ 根治性治疗,避免复发
✓ 微创腹腔镜手术创伤小
✓ 术后恢复快,一般3-5天出院

建议:最终治疗方案需面诊后由外科医生评估决定。
测试四:Prompt 热更新
bash 复制代码
# 刷新配置
curl -X POST "http://localhost:8080/ai/department/refresh?department=internist"

# 响应
{
  "success": true,
  "message": "Prompt配置刷新成功: prompt_his_internist.yaml"
}

📊 配置说明

application.yml 核心配置

yaml 复制代码
spring:
  ai:
    openai:
      api-key: ${DASHSCOPE_API_KEY}
      base-url: https://dashscope.aliyuncs.com/compatible-mode
      chat:
        options:
          model: qwen-turbo        # 模型选择
          temperature: 0.7         # 创造性(0-1)
          max-tokens: 1000         # 最大输出长度

# Prompt 配置来源
prompt:
  source: local  # nacos 或 local

# Nacos 配置
spring:
  cloud:
    nacos:
      config:
        enabled: true
        server-addr: localhost:8848
        namespace: dev
        group: DEFAULT_GROUP
        refresh-enabled: true      # 启用动态刷新

配置参数说明

参数 说明 推荐值
temperature 回答的随机性 0.7(专业)/ 0.9(创意)
max-tokens 回答最大长度 1000-2000
prompt.source Prompt 来源 local(开发)/ nacos(生产)
refresh-enabled 是否热更新 true

🎓 核心知识点总结

1. Spring AI Alibaba 核心概念

复制代码
ChatClient
├── prompt() - 设置提示词
├── user() - 设置用户消息
├── system() - 设置系统消息
├── call() - 同步调用
└── stream() - 流式调用

2. Prompt 管理最佳实践

DO(推荐做法):

  • 使用 YAML 格式管理 Prompt 模板
  • 区分系统提示词和用户提示词
  • 为不同场景设计专属模板
  • 使用 Nacos 实现生产环境热更新

DON'T(避免做法):

  • 不要在代码中硬编码 Prompt
  • 不要在一个模板中处理所有场景
  • 不要忽略异常处理和降级机制

3. 医疗 AI 系统设计原则

  1. 专业性:使用专业医学术语,但要分场景调整
  2. 安全性:明确声明"建议就医",避免承担诊断责任
  3. 可追溯:记录每次问答的 Prompt 版本和模型响应
  4. 可监控:集成日志和指标监控
  5. 可降级:AI 服务不可用时的兜底方案

🚀 生产级扩展建议

1. 添加缓存层

java 复制代码
@Cacheable(value = "aiResponses", key = "#department + '_' + #question")
public String askByDepartment(String department, String question) {
    // ... 原有逻辑
}

2. 添加限流保护

java 复制代码
@RateLimiter(name = "aiService", fallbackMethod = "askFallback")
public String askByDepartment(String department, String question) {
    // ... 原有逻辑
}

3. 添加可观测性

java 复制代码
@Timed(value = "ai.department.ask", description = "科室问答耗时")
@Counted(value = "ai.department.requests", description = "科室问答请求数")
public String askByDepartment(String department, String question) {
    // ... 原有逻辑
}

4. 添加审计日志

java 复制代码
@Aspect
@Component
public class AuditAspect {
    @Around("@annotation(Auditable)")
    public Object audit(ProceedingJoinPoint jp) throws Throwable {
        // 记录请求
        Object result = jp.proceed();
        // 记录响应
        return result;
    }
}

🔍 常见问题排查

Q1: Nacos 连接失败

症状: 启动时报 NacosException: Unable to connect

解决:

yaml 复制代码
# 检查 Nacos 是否启动
curl http://localhost:8848/nacos

# 修改配置切换到本地模式
prompt:
  source: local

Q2: API Key 无效

症状: 请求返回 401 Unauthorized

解决:

bash 复制代码
# 检查环境变量
echo $DASHSCOPE_API_KEY

# 重新设置
export DASHSCOPE_API_KEY=sk-your-new-key

Q3: Prompt 模板不生效

症状: 修改了 YAML 但回答没变化

解决:

bash 复制代码
# 手动刷新配置
curl -X POST "http://localhost:8080/ai/department/refresh?department=internist"

# 或重启应用

✅ 复习要点

  • 理解 Spring AI ChatClient 的基本用法
  • 掌握 Prompt 模板的设计原则
  • 理解 Nacos 配置监听器的工作机制
  • 掌握多科室问答的实现思路
  • 了解医疗 AI 系统的安全考虑
  • 掌握配置热更新的实现方式

🎯 实践练习

练习一:新增妇产科问答

  1. 创建 prompt_his_obstetrics.yaml 模板
  2. 在 PromptLoader 中添加加载逻辑
  3. 测试妇产科专业问答效果

练习二:添加多轮对话支持

  1. 使用 Spring Session 存储会话历史
  2. 在 Prompt 中注入上下文信息
  3. 实现连续问答能力

练习三:集成 Prometheus 监控

  1. 添加 Micrometer 依赖
  2. 暴露 AI 服务的关键指标
  3. 使用 Grafana 可视化

📚 参考资料


👨‍💻 关于作者

Mr.Krab 🦀


📄 许可证

本项目仅供学习和演示使用,请勿用于商业用途。


感谢阅读!如有问题欢迎提 Issue 或 PR!

相关推荐
码界奇点3 小时前
2025时序数据库选型指南从架构基因到AI赋能的深度解析
人工智能·ai·架构·时序数据库
IT_陈寒3 小时前
Python 3.11性能翻倍秘诀:7个你从未注意过的隐藏优化点!
前端·人工智能·后端
aneasystone本尊4 小时前
深入 Dify 应用的会话流程之文件上传
人工智能
不吃鱼的羊4 小时前
Autosar OS简介
人工智能
Kingsdesigner4 小时前
从AI画稿到3D虚拟时装:Illustrator与Substance 3D的服装设计工作流
人工智能·3d·illustrator·substance 3d·sampler·stager·数字时尚
ezl1fe4 小时前
RAG 每日一技(十九):当文本遇上表格,如何拿下“半结构化”PDF
人工智能·后端·算法
后端小肥肠4 小时前
公众号对标账号文章总错过?用 WeWe-RSS+ n8n,对标文章定时到你的邮箱(上篇教程)
人工智能·agent
说私域5 小时前
开源AI智能名片赋能下微商商业模式的创新路径研究——以链动2+1模式与S2B2C商城小程序融合为例
人工智能·小程序·开源
技术猴小猴5 小时前
如何使用Python实现LRU缓存
python·spring·缓存