
SpringBoot前后端分离项目使用大模型API并配置知识库(dify)
想要使用大模型或者dify的api,大部分都是通过访问网页给出的url,在请求头中携带获取到的API keys,然后在POST请求里的body参数中携带对应的信息即可,若是不考虑安全性,直接在前端使用fetch请求即可获取对应的流式数据或者块级数据,但是若不想把API keys暴露在前端,则需要在后端再做一次转发操作。在访问api页面,点击右上角的API密钥来创建密钥,至此,api获取成功,下文的
引言
本文主要讲述从获取大模型API开始,到运用到springboot前后端分离项目中的整个流程,若是已经获取了API以及对应的API keys,想要直接在项目中使用,可以直接跳转四、在项目中使用API。
一、获取大模型API
1.1、使用在线API
若想使用在线大模型的api,需要先去对应大模型的在线api网站上获取对应的api,以deepseek为例,需要先去它的api开放平台,进入API keys页面,然后创建一个属于自己的api,注意,api仅能在创建时看到和复制,创建之后无法获取,如果创建时忘了保存api,可以删掉再创建一个。
1.2、使用本地API
使用本地api需要先去ollama官网下载ollama,下载完成后在cmd命令行窗口输入“ollama”命令,若有出现信息则说明下载成功。
下载完ollama之后需要拉取对应的本地大模型,以deepseek为例,在ollama官网的搜索栏搜索deepseek,进入对应模型的下载页面,并选择下载对应量级的模型,建议下载8b模型,如果本地配置高,可以自行选择更高量级。在进入对应模型的页面并选择好量级后,复制右上角的命令,并在cmd控制台运行并等待下载完成即可。这里建议再下载一个embedding大模型nomic-embed-text:latest。
使用ollama list命令可看到本地下载的所有大模型。
二、下载应用开发模型(dify)
若要使用知识库,比较简单的方法便是利用dify进行配置,其它的诸如RAGFlow、FastGPT也是一样的效果。dify的配置教程网上有很多,大家可以自行找一个适合自己的,比如可以参考这篇博客保姆教程篇:手把手教你从零开始本地部署Dify工作流-CSDN博客,只要保证最后dify抛出的api能够被自己项目的运行环境访问到即可,这里就不过多赘述。
博主本人是在windows10环境下配置的ollama,然后用VMware虚拟机安装Ubuntu24.04.2配置的docker,建议大家还是把ollama和docker配置到同一环境下,如果有和博主一样情况的,设置一下主机和虚拟机的端口映射即可。
三、配置dify信息
3.1、进入dify主页
如果前面的步骤顺利完成,那么打开浏览器,输入dify默认的地址http://localhost:80,然后便可进入dify初始的账号密码设置页面,设置完成便可进入dify的主页, 在此,可点击左侧的创建空白应用创建一个自己的应用。
3.2、配置dify模型信息
先在右上角的头像处进入设置页面
接着选择模型供应商,在此选择所使用api对应的厂家,如使用deepseek的在线api则选择深度求索,如果使用的是ollama本地大模型则使用ollama。
在下载好对应的插件之后,配置对应的API key即可,deepseek的api配置如下
若是使用ollama,则可以进行如下配置。
embedding的模型配置也是同理。
3.3、配置知识库
在配置完对应的大模型之后,便可以开始构建我们的知识库以及对应的工作流了,在知识库页面选择创建知识库,然后上传自己的知识库文件/选择web站点。
点击下一步,索引方式选择高质量,embedding模型选择我们刚才配置的nomic-embed-text:latest模型 (若是使用在线api,可自行选择对应的模型,若无embedding模型,索引方式选择经济即可),配置完成后选择保存并处理。
3.4、配置对话助手
在配置完知识库之后,来到工作室,选择创建空白应用。
在空白应用处选择需要的应用类型,这里以聊天助手为例。
在具体设置页面,可通过提示词来设置模型回答的偏好,内容等,在下方知识库模块则可以设置我们刚才配置好的知识库。
3.5、获取api
在设置完应用之后,在右上角发布并获取对应的api。如果不需要自定义前端页面或后端的信息,其实直接点击“嵌入网站”即可,但是本文的主要目的就是讲springboot的前后端分离项目如何调用大模型api,因此统一采用访问api的方式进行开发。
在访问api页面,点击右上角的API密钥来创建密钥,至此,api获取成功,下文的访问方式均采用dify的API进行开发,但也会提到不使用dify配置知识库,仅采用在线大模型API的访问方式。
四、在项目中使用API
想要使用大模型或者dify的api,大部分都是通过访问网页给出的url,在请求头中携带获取到的API keys,然后在POST请求里的body参数中携带对应的信息即可,若是不考虑安全性,直接在前端使用fetch请求即可获取对应的流式数据或者块级数据,但是若不想把API keys暴露在前端,则需要在后端再做一次转发操作。
4.1、后端
后端使用 WebClient 进行请求的发送,在使用前,注意在pom.xml文件中引入相关依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
接着,在实体类中定义API接口所需要的参数类,以dify为例
entity/ChatRequest.java
//ChatRequest.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatRequest {
private Object inputs;
private String query;
private String response_mode;
private String conversation_id;
private String user;
private List<Object> files;
}
然后在controller层添加对应的接口,其中,分为两种情况,一种是AI应用较为常见的流式数据,即请求发送后,响应的数据是持续返回的,可以减少用户的初次等待时间,一种是块级数据,即当AI应用处理完所有信息后再全部返回。
controller/AgentController.java
//AgentController.java
@RestController
@RequestMapping("/agent")
public class AgentController {
public static final String CHAT_URL = "http://localhost/v1/chat-messages";
public static final String WORKFLOW_URL = "http://localhost/v1/workflows/run";
public static final String CHAT_APIKEY = "xxxxxxxxx";//获取的应用API key
public static final String ADVICE_APIKEY = "xxxxxxxxx";获取的应用API key
private final WebClient chatClient;
private final WebClient adviceClient;
public AgentController() {
this.chatClient = WebClient.builder()
.baseUrl(CHAT_URL)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + CHAT_APIKEY)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
this.adviceClient = WebClient.builder()
.baseUrl(WORKFLOW_URL)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + ADVICE_APIKEY)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
//接收并返回流式数据
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> agentChat(@RequestBody ChatRequest request) {
//request即为发送给大模型api的请求体,由前端传入,也可自行在后端编辑信息
return chatClient.post()
.bodyValue(request)
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(String.class);
//自定义响应体
// return chatClient.post()
// .bodyValue(request)
// .accept(MediaType.TEXT_EVENT_STREAM)
// .retrieve()
// .bodyToFlux(String.class)
// .map(data->{
// return "data: "+data;
// });
}
//接收并返回块级数据
@PostMapping("/advice")
public Mono<String> agentAdvice(@RequestBody ChatRequest request) {
return adviceClient.post()
.bodyValue(request)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class);
}
}
若是直接使用的大模型API,则同理,修改一下url和API key,再将ChatRequest类中的参数修改为对应api需要的参数即可,以deepseek为例。
@Data
public class ChatRequest {
private List<Msg> messages;
private String model = "deepseek-chat";
private Integer max_tokens = 4096;
private Boolean stream = true;
private Object stream_options = null;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Msg {
private String content;
private String role;
}
}
4.2、前端
后端写好接口之后,前端直接用fetch请求访问即可,或者由于后端是直接用的controller接口,并且可以自定义响应体,也可以直接用自己自定义的axios请求,但若是遇到流式数据,需要自己自行处理。这里的请求体的构建以dify的接口需要为例。
接收流式数据,这里展示如何发送fetch请求给后端并处理返回的流式数据,完整的代码在下文给出。
const [controls, setControls] = useState<ChatControls>({
abortController: null,
currentAiMessageId: null
});
//对话的id,用于区分不同的对话
const conversationId = useRef('')
//用户信息,替换为自己的,或者直接={username:"root"}也行
const userInfo = useUserInfo()
// 发送消息处理
const sendMsg = async () => {
if (!input.trim() || controls.currentAiMessageId) return;
// 创建新的AbortController,用于中断fetch请求
const abortController = new AbortController();
setControls(c => ({...c, abortController}));
//发送给AI的信息,测试的话直接const userMsg = "你好"
const userMsg = input.trim()
const obj = {
"inputs": "",
"query": userMsg,
"response_mode": "streaming",
"conversation_id": conversationId.current,
"user": userInfo.username
};
try {
//请求的发送,这里的url即为后端的地址http://xxxxxx:xxxx/agent/chat
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(obj),
signal: abortController.signal
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (reader) {
const {done, value} = await reader.read();
if (done || end) {
// 处理最后剩余的数据
if (buffer.trim()) {
console.log(buffer)
}
break
}
const chunk=decoder.decode(value, {stream: true});
//处理sse流式响应的封装信息
buffer += chunk.replace(/^data: /, '');
let lineEndIndex;
while ((lineEndIndex = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, lineEndIndex).trim();
buffer = buffer.slice(lineEndIndex + 1);
if (line.startsWith('data:')) {
console.log(line.slice(5)); // 去掉"data:"
}else{
console.log(line);
}
}
}
} catch (error: any) {
if (error.name === 'AbortError') {
console.log('请求已被中止');
} else {
console.error('请求失败:', error);
}
} finally {
setControls(c => ({...c, abortController: null, currentAiMessageId: null}));
}
};
如果是接收块级数据,直接接收即可
//该处的url为后端返回块级数据的接口http://xxxxxx:xxxx/agent/advice
const response=await fetchWithTimeout(url,{
method: 'POST',
headers: headers,
body: JSON.stringify(obj),
},30000)
const data = await response.json();
console.log(data)
其中fetchWithTimeout函数为自定义的带超时控制的fetch函数,防止块级数据响应时间过长,直接使用fetch也是一样的,函数如下
/**
* 带有超时控制的fetch函数
* @param resource
* @param options
* @param timeout
* @param doTask
*/
export const fetchWithTimeout=async (resource: RequestInfo, options: RequestInit = {}, timeout = 8000,doTask?:()=>void): Promise<Response>=>{
const controller = new AbortController();
const id = setTimeout(() => {
controller.abort()
doTask&&doTask();
}, timeout);
try {
const response = await fetch(resource, {
...options,
signal: controller.signal
});
clearTimeout(id);
return response;
} catch (error) {
throw error;
}
}
至此,大模型API返回流式数据和块级数据的前后端联调方式已经展示完毕。
五、聊天界面搭建示例
聊天界面的前端代码以React为例,但由于请求的发送和接收是用的JS,因此Vue的用户改一下一下响应式变量,或者直接让AI将React代码转成Vue代码即可,逻辑是一样的。
页面示例
完整代码如下
chatPage/index.tsx
import {memo, useCallback, useEffect, useRef, useState} from 'react'
import type {ReactNode, FC} from 'react'
import {marked} from "marked";
import {useTranslation} from "react-i18next";
import {IconButton} from "@/components/icon";
import {MinusOutlined} from "@ant-design/icons";
import {Button, Input} from "antd";
import {themeVars} from "@/theme/theme.css.ts";
import {useUserInfo} from "@/store/userStore.ts";
import * from "./style.ts"
interface IProps {
children?: ReactNode,
toggleChat: () => void
}
// 类型定义
type Message = {
id: string;
role: 'user' | 'ai';
content: string;
thinkContent?: string;
isComplete: boolean;
thinkExpanded?: boolean;
thinkTime?: number;
isThinking?: boolean;
};
type ChatControls = {
abortController: AbortController | null;
currentAiMessageId: string | null;
};
const ChatPage: FC<IProps> = (props) => {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [showScrollButton, setShowScrollButton] = useState(false);
const [controls, setControls] = useState<ChatControls>({
abortController: null,
currentAiMessageId: null
});
const messagesEndRef = useRef<HTMLDivElement>(null);
const messagesWrapperRef = useRef<HTMLDivElement>(null);
const isAutoScrolling = useRef(true);
//对话的id,用于区分不同的对话
const conversationId = useRef('')
//是否为带思考的信息
const isThink = useRef(false);
const {toggleChat} = props
const {t} = useTranslation();
const userInfo = useUserInfo()
useEffect(() => {
// 滚动事件处理
const wrapper = messagesWrapperRef.current;
const handleScroll = () => {
if (!wrapper) return;
const {scrollTop, scrollHeight, clientHeight} = wrapper;
const isNearBottom = scrollHeight - (scrollTop + clientHeight) < 50;
setShowScrollButton(!isNearBottom);
isAutoScrolling.current = isNearBottom;
};
wrapper?.addEventListener('scroll', handleScroll);
return () => wrapper?.removeEventListener('scroll', handleScroll);
}, []);
// 自动滚动处理
const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
messagesEndRef.current?.scrollIntoView({behavior});
setShowScrollButton(false)
}, []);
// 消息更新时自动滚动
useEffect(() => {
if (isAutoScrolling.current) {
scrollToBottom('auto');
}
}, [messages, scrollToBottom]);
// 新建对话处理
const handleNewChat = useCallback(() => {
// 中止正在进行的请求
if (controls.abortController) {
controls.abortController.abort();
}
// 重置所有状态
setMessages([]);
setInput('');
setControls({abortController: null, currentAiMessageId: null});
setShowScrollButton(false)
conversationId.current = ''
isThink.current = false;
}, [controls.abortController]);
// 停止生成处理
const handleStopGeneration = useCallback(() => {
if (controls.abortController) {
controls.abortController.abort();
}
}, [controls.abortController]);
const url = import.meta.env.VITE_APP_CHAT_URL;
const headers = new Headers({
"Content-type": "application/json",
});
// 发送消息处理
const sendMsg = async () => {
if (!input.trim() || controls.currentAiMessageId) return;
// 创建新的AbortController
const abortController = new AbortController();
setControls(c => ({...c, abortController}));
const userMsg = input.trim()
// 添加用户消息
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: userMsg,
isComplete: true,
};
// 添加初始AI消息
const aiMessage: Message = {
id: `ai_${Date.now()}`,
role: 'ai',
content: '',
isComplete: false,
thinkExpanded: true,
isThinking: true
};
setMessages(prev => [...prev, userMessage, aiMessage]);
setInput('');
const obj = {
"inputs": "",
"query": userMsg,
"response_mode": "streaming",
"conversation_id": conversationId.current,
"user": userInfo.username
};
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(obj),
signal: abortController.signal
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let buffer = '';
let end = false
//回复的信息
let accumulatedData=''
//思考的信息
let thinkData=''
//思考时间
let thinkTime = 0
//是否正在思考
let isThinking=true
//是否展开思考信息
let thinkExpanded=true
//对流式数据进行数据处理
const handleDataLine = (jsonStr: string) => {
try {
const data = JSON.parse(jsonStr);
if (data.event === 'message_end') {
end = true
}
if (data.conversation_id) {
conversationId.current = data.conversation_id
}
if (data.event === 'message' && data.answer) {
if (data.answer === "<think>") {
isThinking = true
isThink.current = true
thinkExpanded=true
thinkTime = new Date().getTime()
} else if (data.answer === "</think>") {
isThinking = false
thinkExpanded=false
thinkTime = Number(((new Date().getTime() - thinkTime) / 1000).toFixed(2))
}
if (isThinking) {
thinkData += data.answer
} else {
accumulatedData += data.answer;
}
}
} catch (e) {
console.error('解析错误:', jsonStr, e);
}
}
while (reader) {
const {done, value} = await reader.read();
if (done || end) {
// 处理最后剩余的数据
if (buffer.trim()) {
handleDataLine(buffer.trim());
}
break
}
const chunk=decoder.decode(value, {stream: true});
buffer += chunk.replace(/^data: /, '');
let lineEndIndex;
while ((lineEndIndex = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, lineEndIndex).trim();
buffer = buffer.slice(lineEndIndex + 1);
if (line.startsWith('data:')) {
handleDataLine(line.slice(5)); // 去掉"data:"
}else{
handleDataLine(line);
}
}
// 更新AI消息内容
setMessages(prev => prev.map(msg => {
if (msg.id === aiMessage.id) {
return {
...msg,
content: accumulatedData,
thinkContent: thinkData,
thinkExpanded: thinkExpanded,
isComplete: false,
isThinking: isThinking,
};
}
return msg;
}));
}
// 标记消息完成
setMessages(prev => prev.map(msg => {
if (msg.id === aiMessage.id) {
return {...msg,
isComplete: true,
thinkTime:thinkTime};
}
return msg;
}));
} catch (error: any) {
if (error.name === 'AbortError') {
console.log('请求已被中止');
// 更新最后一条AI消息为完成状态
setMessages(prev => prev.map(msg =>
msg.id === controls.currentAiMessageId
? {...msg, isComplete: true}
: msg
));
} else {
console.error('请求失败:', error);
}
} finally {
setControls(c => ({...c, abortController: null, currentAiMessageId: null}));
}
};
// 处理思考信息展开和收缩事件
const handleThinkClick = (messageId: string) => {
setMessages(prev => prev.map(msg =>
msg.id === messageId
? {...msg, thinkExpanded: !msg.thinkExpanded}
: msg
));
};
return (
<>
<Container
style={{
backgroundColor: themeVars.colors.background.paper,
}}>
<ButtonWrapper>
<Button type="primary" onClick={handleNewChat}>
{t('sys.chat.newChat')}
</Button>
<IconButton onClick={toggleChat}>
<MinusOutlined/>
</IconButton>
</ButtonWrapper>
<MessagesWrapper ref={messagesWrapperRef}>
{messages.length === 0 &&
<span className='tips' style={{color: themeVars.colors.text.primary}}>
{t('sys.chat.tips')}
</span>
}
{messages.map((message) => {
return (
<MessageItem key={message.id} $isUser={message.role === 'user'}>
<Avatar/>
<Bubble
$isUser={message.role === 'user'}
style={{
backgroundColor: message.role === 'user' ? themeVars.colors.palette.primary.default : themeVars.colors.background.neutral,
color: message.role === 'user' ? themeVars.colors.common.white : themeVars.colors.text.primary
}}>
{isThink.current && message.role !== 'user' &&
<ThinkBg
style={{
backgroundColor: themeVars.colors.background.neutral,
color: themeVars.colors.text.secondary
}}
$isExpanded={message.thinkExpanded || false}
onClick={() => {
handleThinkClick(message.id)
}}>
{message.thinkExpanded ? (
// 展开状态显示原始内容
<div
dangerouslySetInnerHTML={{__html: marked.parse(message.thinkContent || '')}}/>
) : (
// 收缩状态显示自定义内容
!message.isThinking ? (
<div className="custom-content">
<span>▶ {t('sys.chat.thingEnd')} {message.thinkTime} s</span>
<small>{t('sys.chat.openDetails')}</small>
</div>
) : null
)}
</ThinkBg>
}
<div dangerouslySetInnerHTML={{__html: marked.parse(message.content)}}/>
</Bubble>
</MessageItem>
);
})}
<div ref={messagesEndRef}/>
{showScrollButton && (
<ScrollToBottomButton
style={{
backgroundColor: themeVars.colors.palette.primary.default,
color: themeVars.colors.common.white
}}
onClick={() => scrollToBottom()}
>
↓
</ScrollToBottomButton>
)}
</MessagesWrapper>
<InputWrapper style={{backgroundColor: themeVars.colors.background.neutral}}>
<Input
style={{
flex: 1,
padding: "12px",
border: "1px solid #e0e0e0",
outline: "none",
}}
placeholder={t('sys.chat.input')}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMsg()}
/>
{controls.abortController ? (
<StopButton onClick={handleStopGeneration}>
{t('sys.chat.stop')}
</StopButton>
) : (
<SendButton
style={{
backgroundColor: themeVars.colors.palette.primary.default,
color: themeVars.colors.common.white
}}
onClick={sendMsg}>
{t('sys.chat.send')}
</SendButton>
)}
</InputWrapper>
</Container>
</>
)
}
export default memo(ChatPage)
样式代码,chatPage/style.ts
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
padding: 16px;
`;
export const ButtonWrapper = styled.div`
display: flex;
justify-content: space-between;
`;
export const MessagesWrapper = styled.div`
flex: 1;
overflow-y: auto;
padding: 20px;
position: relative;
.tips {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
user-select: none;
}
`;
export const MessageItem = styled.div<{ $isUser: boolean }>`
display: flex;
flex-direction: ${({$isUser}) => ($isUser ? 'row-reverse' : 'row')};
margin-bottom: 20px;
align-items: flex-start;
gap: 12px;
`;
export const Avatar = styled.div`
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #e0e0e0;
flex-shrink: 0;
`;
export const Bubble = styled.div<{ $isUser: boolean }>`
max-width: 70%;
padding: 12px 16px;
border-radius: ${({$isUser}) =>
$isUser ? '12px 12px 0 12px' : '12px 12px 12px 0'};
position: relative;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
`;
export const ThinkBg = styled.div<{ $isExpanded: boolean }>`
padding: 5px;
margin-bottom: 10px;
max-height: ${({$isExpanded}) => ($isExpanded ? '300px' : '30px')};
overflow: ${({$isExpanded}) => ($isExpanded ? 'auto' : 'hidden')};
transition: all 0.3s ease;
cursor: pointer;
position: relative;
&:hover {
filter: brightness(0.95);
}
`;
export const ScrollToBottomButton = styled.button`
position: sticky;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 8px 16px;
border: none;
border-radius: 20px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
`;
export const InputWrapper = styled.div`
display: flex;
gap: 12px;
padding: 16px;
background-color: white;
border-radius: 8px;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
`;
export const SendButton = styled.button`
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
&:hover {
opacity: 0.8;
}
`;
export const StopButton = styled.button`
padding: 12px 24px;
background-color: #ff4d4f;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
}
`;
至此,所有流程已打通,若要更多自定义需求自行处理数据格式以及前端样式即可。
更多推荐
所有评论(0)