☰
Spring Boot 集成 AI API 实战:从零实现流式对话与联网搜索
2026/10/1 20:28:21 网站建设 项目流程

前言

2026 年,AI 大模型已经不是什么新鲜事了。DeepSeek、GPT、Claude 各家模型百花齐放,但在实际项目中,怎么把 AI API 稳定地接到自己的 Spring Boot 项目里,仍然是很多开发者头疼的问题——尤其是当你需要同时兼容多种 API 协议、支持流式输出、甚至调用联网搜索能力时。

本文将以一个真实项目为例,手把手教你:

  1. 同时兼容OpenAI Chat Completions 协议和Anthropic Messages 协议

  2. 实现流式(SSE)和非流式两种调用方式

  3. 调用 Anthropic 的web_search 工具实现真正的联网搜索

  4. 不引入 OkHttp/WebFlux,仅用 JDK 原生HttpURLConnection搞定一切

所有代码来自实际生产环境,不是玩具 Demo。

SpringBoot合集_1416套免费下载

技术选型:为什么不用 SDK?

市面上有 LangChain4j、Spring AI 等框架,但在实际项目中我选择手写 HTTP 调用,原因很简单:

  • 轻量:不需要额外引入一堆依赖

  • 可控:不同 API 协议的请求头、响应格式差异很大,手写更灵活

  • 兼容:一个项目可能同时对接 DeepSeek 官网、第三方网关、OpenAI 兼容接口,每家的"方言"不同

技术栈:Spring Boot 2.x + Java 8 + Hutool JSON 工具库。

一、双通道架构设计

实际项目中,AI 的用途不止一种。以我自己的项目为例,AI 被用在两个场景:

场景协议用途特点
采集通道Anthropic Messages联网搜索热点资讯需要web_search工具
写作通道OpenAI Chat Completions生成 SEO 文案、违规检测纯文本生成

两个通道各自独立配置(API Key、Base URL、模型名),互不影响。这种设计的好处是:采集用的模型可以换,写作的不会跟着挂。

1.1 配置存储

配置信息存在数据库的system_config表里(键值对),而不是application.yml,原因是:不同环境的 API Key 不同,后台改完不用重启。

@Autowired private SystemConfigMapper systemConfigMapper; ​ private String cfg(String key, String def) { SystemConfig c = systemConfigMapper.selectOne( new LambdaQueryWrapper<SystemConfig>() .eq(SystemConfig::getConfigKey, key) .last("limit 1")); if (c == null || StrUtil.isBlank(c.getConfigValue())) { return def; } return c.getConfigValue().trim(); }

1.2 Base URL 归一化

不同用户填的地址五花八门:有人填https://api.deepseek.com/anthropic,有人填完整路径。需要做一个归一化:

private String anthropicUrl(String baseUrl) { String b = baseUrl == null ? "" : baseUrl.trim(); if (StrUtil.isBlank(b)) { b = "https://api.deepseek.com/anthropic"; } while (b.endsWith("/")) { b = b.substring(0, b.length() - 1); } if (b.endsWith("/v1/messages")) return b; if (b.endsWith("/v1")) return b + "/messages"; if (b.endsWith("/anthropic")) return b + "/v1/messages"; if (b.equals("https://api.deepseek.com")) { return b + "/anthropic/v1/messages"; } if (b.contains("/anthropic")) return b + "/v1/messages"; return b + "/anthropic/v1/messages"; }

这种"防御性归一化"在实际项目中非常实用——用户填什么格式都能兼容。

二、非流式调用:最简单的方式

先从最基础的非流式调用说起。以生成 SEO 文案为例,请求发出后等 AI 完整返回,一次性拿到结果。

2.1 构造请求体(OpenAI 协议)

JSONObject req = new JSONObject(); req.set("model", model); // 如 "mimo-v2.5" req.set("temperature", 0.7); req.set("stream", false); ​ // 系统提示词 List<Map<String, String>> messages = new ArrayList<>(); Map<String, String> sys = new HashMap<>(); sys.put("role", "system"); sys.put("content", "你是资深中文SEO文案专家..."); messages.add(sys); ​ // 用户消息 Map<String, String> usr = new HashMap<>(); usr.put("role", "user"); usr.put("content", "文章标题:" + title + "\n文章内容:" + plain); messages.add(usr); req.set("messages", messages);

2.2 发送请求并解析响应

HttpURLConnection conn = null; try { conn = (HttpURLConnection) new URL(baseUrl + "/chat/completions").openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Authorization", "Bearer " + apiKey); conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); conn.setDoOutput(true); conn.setConnectTimeout(15000); conn.setReadTimeout(120000); conn.getOutputStream().write(req.toString().getBytes(StandardCharsets.UTF_8)); ​ int status = conn.getResponseCode(); if (status != 200) { // 读取错误流 String err = readStream(conn.getErrorStream()); return fail("AI 服务错误(HTTP " + status + "):" + err); } ​ // 解析响应 String aiText = JSONUtil.parseObj(readStream(conn.getInputStream())) .getJSONArray("choices") .getJSONObject(0) .getJSONObject("message") .getStr("content"); return aiText; } catch (Exception e) { return fail("请求异常:" + e.getMessage()); } finally { if (conn != null) conn.disconnect(); }

关键点:

  • setConnectTimeout(15000):连接超时 15 秒,网络不好时快速失败

  • setReadTimeout(120000):读取超时 2 分钟,AI 生成长文时需要足够时间

  • 必须加User-Agent,部分 API 网关会拦截无 UA 的请求

2.3 读取流的工具方法

private String readStream(InputStream in) throws Exception { if (in == null) return ""; ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buf = new byte[4096]; int n; while ((n = in.read(buf)) != -1) { out.write(buf, 0, n); } return new String(out.toByteArray(), StandardCharsets.UTF_8); }

三、流式调用(SSE):用户体验的关键

非流式的问题是:用户点"生成"后要等十几秒才能看到结果。流式输出(Server-Sent Events)可以让 AI 的文字逐步出现在页面上,体验好得多。

3.1 后端:读取 SSE 流并转发

conn = (HttpURLConnection) new URL(baseUrl + "/chat/completions").openConnection(); // ... 基础配置同上 ... req.set("stream", true); // 关键:开启流式 conn.setRequestProperty("Accept", "text/event-stream"); ​ // 读取 SSE 流 BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); OutputStream out = response.getOutputStream(); StringBuilder collected = new StringBuilder(); ​ String line; while ((line = reader.readLine()) != null) { // 原样转发给前端 out.write((line + "\n").getBytes(StandardCharsets.UTF_8)); out.flush(); ​ // 同时收集完整文本用于后续处理 String t = line.trim(); if (!t.startsWith("data:")) continue; String data = t.substring(5).trim(); if (data.isEmpty() || "[DONE]".equals(data)) continue; ​ try { JSONObject obj = JSONUtil.parseObj(data); String piece = obj.getJSONArray("choices") .getJSONObject(0) .getJSONObject("delta") .getStr("content"); if (piece != null) { collected.append(piece); } } catch (Exception ignored) { } }

核心思路:每一行 SSE 数据都原样转发给浏览器(前端用EventSource接收),同时把所有文本片段收集起来。收集完毕后可以做进一步处理(比如存库)。

3.2 前端:EventSource 接收

const evtSource = new EventSource("/admin/seo/generate-sse", { method: "POST", // EventSource 不支持 POST,需要改用 fetch }); ​ // 实际用 fetch + ReadableStream: const resp = await fetch("/admin/seo/generate-sse", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, content }) }); ​ const reader = resp.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value); // 解析 SSE 行 for (const line of text.split("\n")) { if (line.startsWith("data: ")) { const data = line.slice(6); if (data === "[DONE]") break; const obj = JSON.parse(data); if (obj.result) { // 最终结果,回填表单 document.getElementById("seoTitle").value = obj.result.seoTitle; } else if (obj.ai_error) { alert("错误:" + obj.ai_error); } } } }

3.3 后端 SSE 写入工具

private void sseWrite(HttpServletResponse response, String data) throws IOException { response.getOutputStream().write( ("data: " + data + "\n\n").getBytes(StandardCharsets.UTF_8)); response.getOutputStream().flush(); }

注意:SSE 格式是data: xxx\n\n,结尾必须有两个换行符。

四、Anthropic Messages 协议:真正的联网搜索

OpenAI 协议不支持联网搜索,而 DeepSeek 官网的 Anthropic Messages API 支持web_search工具,可以让 AI 实时搜索互联网。

4.1 请求体构造(Anthropic 格式)

与 OpenAI 协议的主要区别:

JSONObject body = new JSONObject(); body.set("model", "deepseek-flash"); body.set("temperature", 0.3); body.set("max_tokens", 8000); body.set("stream", true); // 工具定义:联网搜索 JSONArray tools = new JSONArray(); JSONObject ws = new JSONObject(); ws.set("type", "web_search_20250305"); // Anthropic 专用工具类型 ws.set("name", "web_search"); ws.set("max_uses", 5); // 最多搜索 5 次 tools.add(ws); body.set("tools", tools); // 消息格式也不同(没有 system role,直接 user) JSONArray messages = new JSONArray(); JSONObject msg = new JSONObject(); msg.set("role", "user"); msg.set("content", "请搜索最新AI资讯..."); messages.add(msg); body.set("messages", messages);

4.2 请求头差异

头部OpenAI 协议Anthropic 协议
认证Authorization: Bearer xxxx-api-key: xxx
版本无anthropic-version: 2023-06-01
Content-Type相同相同
conn.setRequestProperty("x-api-key", apiKey); conn.setRequestProperty("anthropic-version", "2023-06-01"); conn.setRequestProperty("Content-Type", "application/json");

4.3 解析 Anthropic 响应

Anthropic 的流式响应格式与 OpenAI 不同,delta 字段是text而不是content:

// Anthropic 格式 JSONObject evt = JSONUtil.parseObj(data); Object delta = evt.get("delta"); if (delta instanceof String) { // 直接是文本 collected.append((String) delta); } else if (delta instanceof JSONObject) { // 或者在 delta.text 里 String text = ((JSONObject) delta).getStr("text"); if (text != null) { collected.append(text); } }

非流式响应的解析也不一样:

// Anthropic 非流式:内容在 content 数组里 JSONObject root = JSONUtil.parseObj(resp); JSONArray content = root.getJSONArray("content"); StringBuilder sb = new StringBuilder(); for (int i = 0; i < content.size(); i++) { JSONObject c = content.getJSONObject(i); if ("text".equals(c.getStr("type"))) { sb.append(c.getStr("text")); } } return sb.toString();

五、实用技巧:让 AI 返回结构化数据

项目中大量场景需要 AI 返回 JSON,但 AI 不一定每次都乖乖输出纯 JSON。几个实用技巧:

5.1 强制 JSON 输出

OpenAI 协议支持response_format:

JSONObject rf = new JSONObject(); rf.set("type", "json_object"); req.set("response_format", rf);

5.2 容错解析

AI 可能在 JSON 前后加一些说明文字,需要提取:

private JSONObject extractJson(String aiJson) { String text = aiJson.trim(); int si = text.indexOf('{'); int ei = text.lastIndexOf('}'); if (si >= 0 && ei > si) { text = text.substring(si, ei + 1); } try { return JSONUtil.parseObj(text); } catch (Exception e) { return null; } }

5.3 System Prompt 技巧

让 AI 只输出 JSON 的 prompt 写法:

严格只输出一个JSON对象,不要输出JSON以外的任何文字。JSON格式: {"status":0或1,"reply":"审核意见"}

关键在于明确告诉 AI 不要输出其他内容,并且给出具体的字段和格式。

六、踩坑记录

6.1 超时问题

AI 生成长文可能需要 30 秒以上,setReadTimeout至少设 120 秒。如果是联网搜索 + 生成的流式调用,建议设 180-300 秒。

6.2 连接复用

HttpURLConnection不支持连接池,每次请求都会新建连接。对于低频场景(比如后台手动触发)够用了。如果高频调用,建议换 HttpClient。

6.3 字符编码

务必用StandardCharsets.UTF_8:

conn.getOutputStream().write(req.toString().getBytes(StandardCharsets.UTF_8));

不要用getBytes()(默认编码),中文内容会乱码。

6.4 网关特殊要求

部分 AI 网关(如 OpenCode)要求自定义请求头做会话路由:

private static final String SESSION_ID = UUID.randomUUID().toString(); conn.setRequestProperty("x-opencode-session", SESSION_ID); conn.setRequestProperty("User-Agent", "Mozilla/5.0 ..."); // 伪装浏览器

七、完整调用链路

以"AI 生成 SEO 文案"为例,完整流程:

浏览器 → POST /admin/seo/generate-sse (title + content) ↓ Spring Controller → 构造请求体 → HttpURLConnection → AI API ↓ AI API (SSE 流) → Controller 逐行读取 → 转发给浏览器 ↓ 浏览器显示生成进度 → 结束后解析 JSON → 回填表单

以"联网采集热点"为例:

浏览器 → POST /admin/news/scan-sse ↓ Service → 构造 Anthropic 请求体(带 web_search 工具) ↓ AI 执行联网搜索 → 返回 JSON 数组 ↓ Service 解析热点 → 存入 hot_news 表 ↓ 返回采集结果(总数、批次号、耗时)

总结

Spring Boot 集成 AI API,核心就是三件事:

  1. 选协议:OpenAI Chat Completions 通用性好,Anthropic Messages 支持联网搜索

  2. 做流式:SSE 是标配,用户体验好,后端逐行转发就行

  3. 容错解析:AI 返回的不一定干净,JSON 提取要做防御性编程

不需要框架,不需要复杂架构,HttpURLConnection+ 几个工具方法就够了。

适合中小项目快速落地。如果需要高并发,再考虑连接池和异步化改造。

环境说明

  • Spring Boot 2.3.12.RELEASE

  • Java 8

  • JSON 工具:Hutool JSONUtil

  • AI 接口:DeepSeek 官网 Anthropic API + OpenCode Go API

  • 无额外依赖,纯 JDK HttpURLConnection

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

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

立即咨询