原生对接
- 申请密钥(5 分钟)
- 注册/登录火山引擎 → 进入「火山方舟」控制台
- 左侧「模型推理」→「在线推理」→ 创建接入点(记下模型名,如
ep-2025xxxx-xxxxx) - 右上角头像 → 「API 访问密钥」→ 创建并保存
API Key(格式:Bearer xxxxx)
- 项目结构
demo
├─ src/main/java/com/example/demo
│ ├─ DemoApplication.java
│ ├─ controller/DoubaoController.java
│ └─ service/DoubaoService.java
└─ application.yml- 依赖(pom.xml 片段)
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>- 配置(application.yml)
yaml
doubao:
api-key: Bearer 这里填你的APIKey
model: ep-2025xxxx-xxxxx # 接入点模型名称
url: https://ark.cn-beijing.volces.com/api/v3/chat/completions- Service 层
java
package com.example.demo.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class DoubaoService {
private final RestTemplate restTemplate = new RestTemplate();
@Value("${doubao.url}")
private String url;
@Value("${doubao.api-key}")
private String apiKey;
@Value("${doubao.model}")
private String model;
/**
* 单次对话:用户输入 -> AI 回答
*/
public String chat(String userText) {
// 1. 拼装请求体
Map<String, Object> body = Map.of(
"model", model,
"messages", List.of(
Map.of("role", "user", "content", userText)
),
"stream", false
);
// 2. 头信息
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", apiKey);
// 3. 发请求
ResponseEntity<Map> resp = restTemplate.exchange(
url,
HttpMethod.POST,
new HttpEntity<>(body, headers),
Map.class
);
// 4. 解析结果
if (resp.getStatusCode() != HttpStatus.OK || resp.getBody() == null) {
throw new RuntimeException("豆包接口异常:" + resp.getStatusCode());
}
List<Map<String, Object>> choices = (List<Map<String, Object>>) resp.getBody().get("choices");
if (choices == null || choices.isEmpty()) {
return "";
}
Map<String, Object> msg = (Map<String, Object>) choices.get(0).get("message");
return (String) msg.get("content");
}
}- Controller 层
java
package com.example.demo.controller;
import com.example.demo.service.DoubaoService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/ai")
@RequiredArgsConstructor
@CrossOrigin
public class DoubaoController {
private final DoubaoService doubaoService;
/**
* POST /ai/chat
* 请求体:{"q":"你好,豆包!"}
*/
@PostMapping("/chat")
public String chat(@RequestBody Map<String,String> param) {
return doubaoService.chat(param.getOrDefault("q", "你好"));
}
}- 启动类
java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}- 运行 & 测试
- 启动
DemoApplication - 终端验证
bash
curl -X POST http://localhost:8080/ai/chat \
-H "Content-Type: application/json" \
-d '{"q":"用一句话介绍你自己"}'流式输出
- 项目依赖(与上一版相同,不再赘述)
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>- 配置(application.yml)
yaml
doubao:
api-key: Bearer 你的APIKey
model: ep-2025xxxx-xxxxx # 接入点名称
url: https://ark.cn-beijing.volces.com/api/v3/chat/completions- Service 层(流式核心)
java
package com.example.demo.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tools.jackson.databind.ObjectMapper;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
@Service
@RequiredArgsConstructor
public class DoubaoStreamService {
private final RestTemplate restTemplate = new RestTemplate();
private final ExecutorService pool = Executors.newCachedThreadPool();
@Value("${doubao.url}")
private String url;
@Value("${doubao.api-key}")
private String apiKey;
@Value("${doubao.model}")
private String model;
/**
* 流式对话:返回 SseEmitter,前端订阅即可
*/
public SseEmitter stream(String userText) {
SseEmitter emitter = new SseEmitter(0L); // 0 表示永不超时
pool.execute(() -> {
try {
// 1. 请求体(stream=true 是关键)
Map<String, Object> body = Map.of(
"model", model,
"messages", List.of(Map.of("role", "user", "content", userText)),
"stream", true
);
RequestCallback requestCallback = request -> {
request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
request.getHeaders().set("Authorization", apiKey);
// 关键:用 Jackson 把 Map 转成字节数组
new ObjectMapper()
.writeValue(request.getBody(), body);
};
ResponseExtractor<Void> responseExtractor = response -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
// 火山方舟 SSE 格式:data: {...} 或 data: [DONE]
if (line.startsWith("data:")) {
String json = line.substring(5).trim();
if ("[DONE]".equals(json)) {
emitter.send(SseEmitter.event().name("stop").data(""));
break;
}
// 只取 delta.content
String content = parseDelta(json);
if (content != null) {
emitter.send(SseEmitter.event().data(content));
}
}
}
}
return null;
};
restTemplate.execute(url, HttpMethod.POST, requestCallback, responseExtractor);
} catch (Exception e) {
log.error("流式调用异常", e);
try {
emitter.send(SseEmitter.event().name("error").data("exception"));
} catch (Exception ignore) {
}
} finally {
try {
emitter.complete();
} catch (Exception ignore) {
}
}
});
return emitter;
}
/* 简易解析:只提取 choices[0].delta.content */
private String parseDelta(String json) {
try {
ObjectMapper mapper = new ObjectMapper();
Map<?,?> root = mapper.readValue(json, Map.class);
List<?> choices = (List<?>) root.get("choices");
if (choices != null && !choices.isEmpty()) {
Map<?,?> delta = (Map<?,?>) ((Map<?,?>) choices.get(0)).get("delta");
if (delta != null && delta.containsKey("content")) {
return (String) delta.get("content");
}
}
} catch (Exception ignore) {
}
return null;
}
}- Controller 层
java
package com.example.demo.controller;
import com.example.demo.service.DoubaoStreamService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RestController
@RequestMapping("/ai")
@RequiredArgsConstructor
@CrossOrigin
public class StreamController {
private final DoubaoStreamService streamService;
/**
* GET /ai/stream?msg=你好
* 前端:new EventSource("/ai/stream?msg=你好")
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream(@RequestParam String msg) {
return streamService.stream(msg);
}
}- 前端最简示例(纯 HTML)
html
<!doctype html>
<html>
<body>
<input id="q" placeholder="输入问题"/>
<button onclick="ask()">发送</button>
<pre id="ans"></pre>
<script>
function ask(){
const q = document.getElementById('q').value.trim();
if(!q) return;
document.getElementById('ans').textContent = '';
const evt = new EventSource('/ai/stream?msg='+encodeURIComponent(q));
evt.onmessage = e => document.getElementById('ans').textContent += e.data;
evt.addEventListener('stop', () => evt.close());
evt.addEventListener('error', () => { evt.close(); console.error('SSE error') });
}
</script>
</body>
</html>