☰
用 SpringBoot + SSE 自制网页,接入本地 Ollama 大模型对话:TaoToken 统一 Key 配置骨架
2026/9/27 22:15:24 网站建设 项目流程

1. 本地 Ollama 对话网页:为什么值得自己搭一套

Ollama 装好之后,命令行里ollama run就能聊天,但每次都要开终端、复制粘贴,体验很割裂。更实际的问题是:如果你想把本地模型接进自己的工具链、给团队做个内网问答页、或者单纯想练手 SSE 流式推送,命令行是满足不了的。这时候用 SpringBoot 搭一个网页后端,把 Ollama 的流式输出通过 SSE 推到浏览器,就是一条很顺的路径。

这篇要解决的就是这条完整链路:本地 Ollama 跑起来 → SpringBoot 提供 SSE 接口 → 前端网页逐字显示回复 → 同时留出 TaoToken 统一 Key 的配置骨架,方便你以后把请求切到云端模型或做多模型路由。适合有 Java 基础、想跑通「网页访问本地大模型」的开发者,也适合正在找 SSE 实战案例的人。

我试过把 Ollama 的/api/generate直接暴露给前端,结果跨域和流式解析两头卡,最后还是回到 SpringBoot 中转的方案。下面按可复制的顺序来,每一步都有命令和配置。

2. 前置准备:Ollama 运行与 TaoToken 通道骨架

2.1 确认 Ollama 服务在跑

Ollama 安装后默认监听11434端口。启动项目之前,先确认服务活着:

ollama serve

如果提示端口被占用,说明后台已经在跑了,直接验证模型列表:

ollama list

拉一个轻量模型做测试,1.5b 在普通笔记本上也能跑:

ollama run deepseek-r1:1.5b

能进对话就说明本地推理链路通了。按Ctrl+D退出,服务不会停。

注意:SpringBoot 项目启动前必须保证 Ollama 在运行,否则 SSE 请求会直接报连接拒绝。

2.2 TaoToken 统一 Key 的定位

本地 Ollama 不需要 Key,但一旦你想把同一套后端代码切到云端模型、或者做本地+云端的混合路由,就需要一个统一的 API 通道。TaoToken 在这里扮演的是「统一 Key + 统一入口」的角色:你拿一个 Key,通过https://taotoken.net/api这个基地址去调不同模型,后端代码不用为每个厂商写一套鉴权。

配置骨架放在application.yml里,用环境变量注入,别硬编码:

taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY:} model: deepseek-r1:1.5b

对应的settings.json示例(如果你用支持该格式的客户端或工具链):

{ "provider": "taotoken", "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的Key", "model": "deepseek-r1:1.5b", "stream": true }

Key 的获取入口在控制台的 API Keys 页面,建议单独建一个项目 Key,方便按项目停用。接入文档里有各语言的最小请求示例,排障时对照着看比猜快。

3. 可复制配置:SpringBoot SSE 后端三件套

3.1 依赖与项目结构

创建 SpringBoot 项目时勾选 Spring Web 和 WebFlux(WebClient 在 WebFlux 里)。目录结构:

src/main/java/com/example/ ├── config/WebConfig.java ├── controller/ChatController.java └── service/OllamaService.java src/main/resources/static/index.html

pom.xml关键依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>

3.2 跨域配置

前端页面和后端同源时其实不需要,但开发阶段用文件方式打开 HTML 会触发跨域,先配上:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("*"); } }

3.3 SSE 控制器

@RestController @RequestMapping("/api") public class ChatController { @Autowired private OllamaService ollamaService; @GetMapping("/chat-stream") public SseEmitter streamChat(@RequestParam String message) { SseEmitter emitter = new SseEmitter(0L); ollamaService.streamResponse(message, emitter); return emitter; } }

new SseEmitter(0L)表示不设超时,长回复不会被截断。默认超时是 30 秒,大模型吐字慢的时候很容易踩这个坑。

3.4 服务层:把 Ollama 流式输出转成 SSE

@Service public class OllamaService { private static final String OLLAMA_API_URL = "http://localhost:11434/api/generate"; private final ObjectMapper objectMapper = new ObjectMapper(); public void streamResponse(String message, SseEmitter emitter) { WebClient.create() .post() .uri(OLLAMA_API_URL) .contentType(MediaType.APPLICATION_JSON) .bodyValue(Map.of( "model", "deepseek-r1:1.5b", "prompt", message, "stream", true )) .retrieve() .bodyToFlux(String.class) .doOnComplete(() -> sendCompletionSignal(emitter)) .subscribe( data -> processData(data, emitter), error -> handleError(emitter, error) ); } private void processData(String data, SseEmitter emitter) { try { JsonNode json = objectMapper.readTree(data); String response = json.get("response").asText(); emitter.send(SseEmitter.event().name("message").data(response)); } catch (Exception e) { handleError(emitter, new RuntimeException("数据解析失败: " + data, e)); } } private void sendCompletionSignal(SseEmitter emitter) { try { emitter.send(SseEmitter.event().name("done").data("COMPLETED")); emitter.complete(); } catch (IOException e) { emitter.completeWithError(e); } } private void handleError(SseEmitter emitter, Throwable error) { try { emitter.send(SseEmitter.event().name("error").data("服务请求失败: " + error.getMessage())); emitter.completeWithError(error); } catch (IOException e) { emitter.completeWithError(e); } } }

这里的关键点:Ollama 的流式响应是一行一个 JSON,每个 JSON 里response字段是增量文本。bodyToFlux(String.class)按行拆开,逐条解析后通过SseEmitter.event().name("message")推给前端。前端监听message事件累加文本,监听done事件收尾。

4. 验证请求:curl 与网页双通道确认

4.1 先用 curl 验证 Ollama 本身

在启动 SpringBoot 之前,直接打 Ollama 的接口,确认流式返回正常:

curl http://localhost:11434/api/generate -d '{ "model": "deepseek-r1:1.5b", "prompt": "用一句话解释什么是SSE", "stream": true }'

你会看到一行行 JSON 往外冒,每行都有response字段。如果这里卡住不动,问题在 Ollama 侧,不用往下查 SpringBoot。

4.2 再验证 SpringBoot 的 SSE 接口

项目启动后,用 curl 打自己的接口:

curl -N "http://localhost:8080/api/chat-stream?message=你好"

-N关闭缓冲,能实时看到event:message和data:...交替出现。看到event:done就说明整条链路通了。

4.3 网页端确认

index.html放在src/main/resources/static/下,启动后直接访问http://localhost:8080/index.html。输入问题,AI 回复应该逐字出现,末尾有光标闪烁,完成后光标消失。

前端核心逻辑是EventSource:

const apiUrl = `http://localhost:8080/api/chat-stream?message=${encodeURIComponent(message)}`; currentEventSource = new EventSource(apiUrl); currentEventSource.addEventListener('message', (event) => { fullResponse += event.data; updateAIReply(fullResponse); }); currentEventSource.addEventListener('done', () => { finalizeAIReply(fullResponse); resetState(); });

EventSource默认只支持 GET,所以消息通过 query 参数传。消息长了 URL 会超长,生产环境建议改成 POST + fetch 的 ReadableStream 方案,但作为跑通链路,GET 足够。

5. 本篇常见错排查

5.1 连接被拒绝:Connection refused

报错Connection refused: localhost/127.0.0.1:11434,说明 Ollama 没跑。执行ollama serve后重试。如果是远程访问,把OLLAMA_API_URL里的localhost改成 Ollama 所在机器的 IP,同时确认 Ollama 监听了外部地址。

5.2 SSE 请求 30 秒后断开

默认SseEmitter超时 30 秒,长回复会被截断。改成new SseEmitter(0L)或设置一个足够大的值。另外 Nginx 反代时proxy_read_timeout也要调大,否则网关层先断。

5.3 前端收不到流式数据,一次性全出来

检查两处:一是curl -N是否实时输出,如果 curl 也是攒着一起出,问题在后端缓冲;二是 Nginx 是否开了proxy_buffering off。SpringBoot 侧确认返回的 Content-Type 是text/event-stream。

5.4 中文乱码

SseEmitter默认用 UTF-8,但如果你手动设置了produces = "text/event-stream;charset=ISO-8859-1"就会乱。检查@GetMapping有没有多余的 produces 声明,去掉即可。

5.5 模型名写错导致 404

Ollama 返回model not found,说明bodyValue里的model字段和ollama list里的名字不一致。注意带 tag 的完整名称,比如deepseek-r1:1.5b不能简写成deepseek-r1。

5.6 TaoToken Key 未生效

如果切到 TaoToken 通道后返回 401,先确认TAOTOKEN_API_KEY环境变量真的注入了,可以在启动日志里打印一下长度(别打印完整 Key)。再对照接入文档检查请求头格式,通常是Authorization: Bearer sk-xxx。排障阶段建议先用模型对话页面手动发一条,确认 Key 本身可用,再回到代码里查。

6. 把链路固定下来:下一步怎么走

跑通之后,建议把 Ollama 地址、模型名、TaoToken 的 base-url 和 Key 全部抽到application.yml,用@ConfigurationProperties绑定,这样切换本地/云端只改配置不改代码。长期做编码辅助或 Agent 的话,可以考虑 Coding Plan 那类按量方案,把 Key 管理和额度控制交给平台,自己专注在业务逻辑上。

最后留一个实用技巧:SSE 接口加一个@GetMapping("/health")返回 Ollama 的连通状态,前端页面加载时先探一下,连不上就直接提示「本地模型未启动」,比让用户对着转圈的光标猜要友好得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询