java调用公司Ollama服务实现流式效果(全栈篇)
·
后端
pom.xml
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M5</spring-ai.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 公司内部Ollama模型 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
注意:此时的jdk版本需要17及以上!
application.yaml
server:
port: 8080
spring:
application:
name: ai-demo
ai:
ollama:
base-url: http://服务ip:11434
chat:
model: deepseek-r1:7b
AIChatController.java
@Resource
private OllamaChatModel ollamaChatModel;
/**
* 全文本输出格式
*
* @param message
* @return
*/
@GetMapping("/chat")
public String chat(String message) {
Prompt prompt = new Prompt(new UserMessage(message));
ChatResponse chatResponse = ollamaChatModel.call(prompt);
String content = chatResponse.getResult().getOutput().getContent();
content = content.replace("<think>", "").replace("</think>", "").trim();
log.info("输出结果: {}", content);
return content;
}
/**
* 流式输出格式
*
* @param message
* @return
*/
@GetMapping("/stream")
public SseEmitter streamChat(@RequestParam String message) {
// 60秒超时时间
SseEmitter emitter = new SseEmitter(60_000L);
// 使用异步线程处理流式响应
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
// 创建流式Prompt
Prompt prompt = new Prompt(new UserMessage(message));
// 获取响应流
Flux<ChatResponse> responseFlux = ollamaChatModel.stream(prompt);
responseFlux.subscribe(
chunk -> {
// 处理每个响应块
// String content = processChunk(chunk);
String content = chunk.getResult().getOutput().getContent();
log.info("content: {}", content);
try {
// 发送数据到客户端
emitter.send(content);
} catch (IOException e) {
throw new RuntimeException(e);
}
},
error -> {
emitter.completeWithError(error);
log.error("流式输出异常: {}", error.getMessage());
},
() -> {
emitter.complete();
log.info("流式输出完成");
}
);
} catch (Exception e) {
emitter.completeWithError(e);
log.error("流式处理初始化失败: {}", e.getMessage());
}
});
executor.shutdown();
return emitter;
}
前端
package.json
{
"name": "chat",
"private": true,
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"lucide-vue-next": "^0.263.0",
"marked": "^4.2.12",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15",
"@vitejs/plugin-vue": "^4.2.3",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"vite": "^4.4.9"
}
}
核心代码
const sendMessage = async () => {
const message = userMessage.value.trim()
if (!message || isTyping.value) return
messages.value.push({
text: message,
sender: 'user',
timestamp: new Date().toISOString()
})
userMessage.value = ''
isTyping.value = true
scrollToBottom()
try {
const response = await fetch(
// `${baseUrl}/ollama/chat?message=${encodeURIComponent(message)}`,
`${baseUrl}/ollama/stream?message=${encodeURIComponent(message)}`,
{ method: 'GET' }
)
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`)
// 创建新的消息对象并立即添加到消息列表
let botMessage = ref({
text: '',
sender: 'bot',
timestamp: new Date().toISOString(),
})
messages.value.push(botMessage.value) // 立即添加消息对象
// 创建 ReadableStream 的读取器
const reader = response.body.getReader()
const decoder = new TextDecoder()
// 循环读取流数据
while (true) {
const { done, value } = await reader.read()
if (done) break
// 解码并处理文本
const text = decoder.decode(value)
// 处理 SSE 格式,移除 "data: " 前缀和 think 标签
const cleanText = text
.replace(/^data:\s*/gm, '')
.replace(/<think>|<\/think>/g, '')
.trim()
console.log('cleanText---', cleanText)
if (cleanText) {
// 直接更新消息对象的文本内容
botMessage.value.text += cleanText
newMessage.value += cleanText
scrollToBottom()
}
}
} catch (error) {
messages.value.push({
text: '请求失败,请稍后再试',
sender: 'bot',
timestamp: new Date().toISOString()
})
} finally {
isTyping.value = false
scrollToBottom()
}
}
目录结构
运行效果

至此,流式效果已实现,需要代码的同学可以加入技术群获取!
本人正在打造技术交流群,欢迎志同道合的朋友一起探讨,一起努力,通过自己的努力,在技术岗位这条道路上走得更远。QQ群号:925317809 备注:技术交流 即可通过!
加入技术群可以获取资料,含AI资料、Spring AI中文文档等,等你加入~
更多推荐

所有评论(0)