当前位置:首页 > 工具 > 正文内容

医疗 AI 分诊助手开发:基于 Spring Boot 与 Ollama 的 SSE 流式对话系统实现

访客 工具 2026年9月2日 2

项目架构与依赖管理

在构建医疗 AI 分诊助手时,后端核心基于 Spring Boot 3.x 结合 Spring AI 框架,前端则采用 Vue 3 配合 Element Plus。后端采用多模块 Maven 结构,核心服务模块 triage-service-provider 的依赖配置如下:

<dependencies>
    <!-- 标准 Web 启动器,用于 MVC 架构和 SseEmitter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI 集成 Ollama -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
    </dependency>

    <!-- 参数校验 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>

值得注意的是,虽然 Spring AI 底层使用了 Project Reactor,但本项目并未引入 spring-boot-starter-webflux。我们选择了传统的 Servlet 栈(Spring MVC),并通过 SseEmitter 实现流式响应。这种选型能够更好地兼容现有的 JDBC/JPA 等阻塞式数据库驱动,同时降低了响应式编程带来的调试难度。由于 AI 推理延迟(秒级)远大于网络传输损耗,WebFlux 的高并发吞吐优势在单纯的对话场景中并不显著。

后端抽象层设计

为了提高系统的灵活性,我们定义了 MedAiClient 接口,屏蔽底层 AI 供应端的差异(如 Ollama 或 Mock 实现)。

public interface MedAiClient {
    /**
     * 同步阻塞对话
     */
    String generateResponse(String prompt);

    /**
     * 携带系统上下文的对话
     */
    String generateResponse(String context, String prompt);

    /**
     * SSE 流式对话
     */
    void streamResponse(String prompt, Consumer<String> chunkHandler, Runnable completionHandler, Consumer<Throwable> exceptionHandler);
}

Ollama 实现类

利用 Spring AI 自动装配的 StreamingChatModel,我们可以轻松实现流式输出:

@Component
@Primary
@ConditionalOnProperty(name = "triage.ai.mode", havingValue = "ollama", matchIfMissing = true)
public class OllamaClientImpl implements MedAiClient {
    private final ChatModel chatModel;
    private final StreamingChatModel streamingModel;
    private final String systemInstruction;

    public OllamaClientImpl(ChatModel chatModel, 
                            StreamingChatModel streamingModel,
                            @Qualifier("medSystemPrompt") String systemInstruction) {
        this.chatModel = chatModel;
        this.streamingModel = streamingModel;
        this.systemInstruction = systemInstruction;
    }

    @Override
    public void streamResponse(String prompt, Consumer<String> chunkHandler, Runnable completionHandler, Consumer<Throwable> exceptionHandler) {
        Prompt messagePrompt = new Prompt(systemInstruction + "\n用户咨询: " + prompt);
        
        streamingModel.stream(messagePrompt)
                .subscribe(
                        resp -> {
                            String text = resp.getResult().getOutput().getText();
                            if (text != null) chunkHandler.accept(text);
                        },
                        exceptionHandler::accept,
                        completionHandler
                );
    }
    // 省略其他同步方法实现...
}

SSE 流式控制器实现

在控制器层,我们使用 SseEmitter 来管理长连接。AI 推理时间较长,建议将超时时间设置为 5 分钟(300,000ms),防止前端过早断开。

@RestController
@RequestMapping("/api/v1/triage")
public class TriageController {
    private final TriageService triageService;

    @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter handleStreamChat(@RequestBody @Valid TriageRequest req) {
        SseEmitter emitter = new SseEmitter(300_000L);
        
        triageService.executeStreamProcess(req.getUserInput(), emitter);
        
        emitter.onTimeout(() -> emitter.complete());
        emitter.onError((ex) -> emitter.completeWithError(ex));
        
        return emitter;
    }
}

在 Service 层,我们将 AI 生成的每一个字符包装成 SSE 的 data 格式推送到前端,并在生成结束后追加法律免责声明:

public void executeStreamProcess(String input, SseEmitter emitter) {
    aiClient.streamResponse(input,
            chunk -> {
                try {
                    emitter.send(SseEmitter.event().data(chunk));
                } catch (IOException e) {
                    emitter.completeWithError(e);
                }
            },
            () -> {
                try {
                    emitter.send(SseEmitter.event().data("\n\n---\n*提醒:AI 建议仅供参考,危急情况请立即就医。*"));
                    emitter.complete();
                } catch (IOException e) {
                    emitter.completeWithError(e);
                }
            },
            emitter::completeWithError
    );
}

前端流式解析处理

由于标准的 axios 对流式 ReadableStream 支持不佳,前端推荐使用原生的 fetch API。为了避免中文字符分块导致的乱码(一个汉字占 3 字节,可能被分割在两个数据块中),必须在 TextDecoder 中开启 stream: true 模式。

async function fetchAiStream(content: string, onUpdate: (val: string) => void) {
    const response = await fetch('/api/v1/triage/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userInput: content })
    });

    if (!response.body) return;

    const reader = response.body.getReader();
    const decoder = new TextDecoder('utf-8');
    let isDone = false;

    while (!isDone) {
        const { value, done } = await reader.read();
        isDone = done;
        
        // 关键:开启流模式解码,防止中文乱码
        const rawChunk = decoder.decode(value, { stream: !done });
        
        // 解析 SSE 协议格式 "data:xxxx\n\n"
        const lines = rawChunk.split('\n');
        lines.forEach(line => {
            if (line.startsWith('data:')) {
                const dataText = line.replace('data:', '');
                onUpdate(dataText);
            }
        });
    }
}

技术要点总结

  1. 模型配置:在 Spring AI 中,Ollama 的模型名称应通过配置文件(spring.ai.ollama.chat.model)指定,而非直接在代码中硬编码。
  2. Mock 测试:在没有本地 GPU 环境时,可以通过自定义 MockClient 并利用 Thread.sleep 模拟逐字输出的效果,以便进行 UI 联调。
  3. 资源管理SseEmitter 返回后,Servlet 线程会立即释放,实际的写操作在异步线程中完成。务必注册 onTimeoutonError 回调,确保服务端连接能正常关闭,避免内存泄露。
  4. 安全性提示:医疗场景下,系统提示词(System Prompt)应强制 AI 不得给出具体的药物剂量建议,并由后端代码层级强制注入免责声明,确保合规性。

相关文章

Trojan服务器搭建与配置

一、整体架构(先对齐认知)Clash Meta (PC / iOS / Android)        ↓ TLS   Trojan Server (443)        ↓     InternetTrojan 的核心是: TLS + HTTPS 流量伪装 看起来像正常网站 非常适合...

Tailscale 的详细用法

Tailscale 是一种基于 WireGuard 协议 的 零配置 VPN(虚拟私有网络)服务,让设备之间能够 安全、加密地直接连接,就像它们在同一个本地网络一样。它的核心特点是 简单、安全、跨平台。Tailscale 非常适合 没有公网 IP、两台电脑不在同一局域网 的场景。 简单来说,Tailscale 是什么?Tailscale 是一款让你的各种设备(电脑、服务器、手机...

深入解析Node.js运行环境与异步I/O架构

深入解析Node.js运行环境与异步I/O架构

核心定义与价值Node.js本质上是一个JavaScript运行环境,而非编程语言或应用框架。它赋予了JavaScript脱离浏览器在服务端、命令行工具及网络应用中执行的能力。其核心意义在于:用单一语言打通前后端开发壁垒。基于事件驱动与非阻塞I/O的架构特性,Node.js在处理API网关、实时通信及微服务等I/O密集型场景时表现卓越,已成为现代后端工程的主流选择。浏览器沙箱限制1995年Java...

ADO.NET SQL参数化查询的最佳实践

在 ADO.NET 中执行 SQL 查询时,参数化查询是一种关键的安全措施和性能优化手段。它通过将 SQL 命令和用户提供的数据分开处理,有效防止了 SQL 注入攻击,并有助于数据库缓存执行计划。下面总结了几种常用的参数化查询方式。 1. 使用 SqlParameter 对象(推荐) 这是最推荐的参数化查询方式。通过显式创建 SqlParameter 对象,您可以精确控制参数的类...

基于ELK的日志集中化分析系统搭建

构建统一日志管理平台的必要性 在分布式架构中,各服务节点独立运行,日志分散存储于不同主机。传统通过命令行工具如grep、awk逐个检索日志的方式,在数据量庞大时效率极低,难以实现快速定位问题。为提升运维效率,需建立集中式日志处理体系,具备日志采集、传输、存储、分析与告警能力。 ELK技术栈核心组件解析 Elasticsearch:分布式搜索引擎,支持全文检索、实时数据分析和高可用集群部署,...

企业级 Oracle 数据库部署与初始化实战

系统环境规划与前置条件检查 在着手部署 Oracle RDBMS 之前,必须对底层基础设施进行严格评估。该企业级关系型数据库管理系统常用于处理高并发业务逻辑,因此对计算资源、存储 I/O 及网络稳定性有较高标准。 1. 资源规格定义 为确保实例稳定运行,建议满足以下基准配置: 计算单元:双核或以上处理器,主频不低于 2.0GHz。 内存容量:物理内存至少 2GB,生产环境建议扩容至 4GB 以...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。