新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring Boot 集成 AI API 实战:从零实现流式对话与联网搜索

发布时间:2026/10/1 20:28:22来源:尧图网络
Spring Boot 集成 AI API 实战:从零实现流式对话与联网搜索
前言2026 年AI 大模型已经不是什么新鲜事了。DeepSeek、GPT、Claude 各家模型百花齐放但在实际项目中怎么把 AI API 稳定地接到自己的 Spring Boot 项目里仍然是很多开发者头疼的问题——尤其是当你需要同时兼容多种 API 协议、支持流式输出、甚至调用联网搜索能力时。本文将以一个真实项目为例手把手教你同时兼容OpenAI Chat Completions 协议和Anthropic Messages 协议实现流式SSE和非流式两种调用方式调用 Anthropic 的web_search 工具实现真正的联网搜索不引入 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 LambdaQueryWrapperSystemConfig() .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); ​ // 系统提示词 ListMapString, String messages new ArrayList(); MapString, String sys new HashMap(); sys.put(role, system); sys.put(content, 你是资深中文SEO文案专家...); messages.add(sys); ​ // 用户消息 MapString, 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-01Content-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_formatJSONObject 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_8conn.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核心就是三件事选协议OpenAI Chat Completions 通用性好Anthropic Messages 支持联网搜索做流式SSE 是标配用户体验好后端逐行转发就行容错解析AI 返回的不一定干净JSON 提取要做防御性编程不需要框架不需要复杂架构HttpURLConnection 几个工具方法就够了。适合中小项目快速落地。如果需要高并发再考虑连接池和异步化改造。环境说明Spring Boot 2.3.12.RELEASEJava 8JSON 工具Hutool JSONUtilAI 接口DeepSeek 官网 Anthropic API OpenCode Go API无额外依赖纯 JDK HttpURLConnection
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Android系统五层架构全解析:从Linux内核到应用层 2026/10/1 22:32:59

Android系统五层架构全解析:从Linux内核到应用层

我第一次正儿八经研究Android,不是从写Hello World开始的,而是被网上各种零散概念弄懵之后,翻到了官方那张分层架构图。当时盯着看了很久,每一层都是英文,每一层都似懂非懂。后来做了几年客户端开发,再回头…

阅读更多 →
顺序表与链表全面拆解:内存模型、代码实操与选型指南 2026/10/1 22:32:58

顺序表与链表全面拆解:内存模型、代码实操与选型指南

每次带新人做数据结构入门,我第一句话基本都是:线性表这个概念,你哪怕毕业十年也躲不开。面试官爱问数组和链表的区别,刷题网站天天考反转链表,业务代码里面试者纠结用ArrayList还是LinkedList——说到底就是“顺序表”…

阅读更多 →
AI开发必备的Python实战技巧:环境配置、数据处理与训练优化 2026/10/1 22:32:58

AI开发必备的Python实战技巧:环境配置、数据处理与训练优化

做AI开发这几年,我见过太多项目卡在Python这个环节。模型结构设计得挺漂亮,结果数据读不动、环境装不上、训练跑几个epoch就崩,最后花几天时间排查,发现只是一行不起眼的代码在捣乱。这篇文章想聊聊我在AI方向真正高频用到的Pytho…

阅读更多 →
500行C代码实现微型解释器:词法、语法与求值全解析 2026/10/1 22:32:58

500行C代码实现微型解释器:词法、语法与求值全解析

简介:这是一份面向编译器与解释器入门学习者的实践型资源,围绕「用C语言在500多行内实现一个微型解释器」展开,适合已掌握C语言基础语法、希望理解词法分析、语法分析与AST构建等编译原理核心概念的开发者练手。资源包共10个文件,…

阅读更多 →
BitLocker解密中调整分区导致卡死的修复方法 2026/10/1 22:32:51

BitLocker解密中调整分区导致卡死的修复方法

先交代一下我自己的经历。上个月帮人处理一台 Windows 10 笔记本,情况和你标题里写的几乎一模一样:系统盘开了 BitLocker,觉得没必要想关掉,控制面板点了“关闭 BitLocker”,后台开始解密。跑到一半,用户想…

阅读更多 →
Python LSTM气温预测实战:从数据构造到可视化评估 2026/10/1 22:32:51

Python LSTM气温预测实战:从数据构造到可视化评估

简介:这份资源面向计算机、人工智能及相关专业的在校学生与教师,也适合希望入门时间序列预测的开发者,提供一套基于LSTM的气温预测与可视化完整项目。项目通过bs4从中国天气网爬取北京、上海、广州、郑州四城2011至2021年共3652条天气数据&am…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉