🎉🎉欢迎光临🎉🎉
🏅我是苏泽,一位对技术充满热情的探索者和分享者。🚀🚀
🌟特别推荐给大家我的最新专栏《Spring 狂野之旅:底层原理高级进阶》 🚀
本专栏纯属为爱发电永久免费!!!
这是苏泽的个人主页可以看到我其他的内容哦👇👇
努力的苏泽http://suzee.blog.csdn.net/
大家好这里还是苏泽,关于我的Spring狂野之旅已经出了5期,基本都是从Spring的底层源码去读它,不仅能学会使用方法又能理解其工作机制以及原理,我认为这是非常美妙的一件事,这几期反响都还行,
于是这一章专门出一期运用前面所讲过的知识 自己从0开始搭建一个后台程序 能够对接市面上绝大多数的ai对话api(不同公司的具体的鉴权方法你们找官网文档copy就行了)
那么正片开始
目录
大家好这里还是苏泽,关于我的Spring狂野之旅已经出了5期,基本都是从Spring的底层源码去读它,不仅能学会使用方法又能理解其工作机制以及原理,我认为这是非常美妙的一件事,这几期反响都还行,
于是这一章专门出一期运用前面所讲过的知识 自己从0开始搭建一个后台程序 能够对接市面上绝大多数的ai对话api(不同公司的具体的鉴权方法你们找官网文档copy就行了)
从零开始 搭建一个Spring boot程序
确保你已经安装了Java开发工具(JDK)。你可以在命令行中输入java -version来验证是否已安装Java,并确保版本符合Spring Boot的要求。编辑安装的部分我就不演示了默认大家已经装好配置好环境
创建一个新的Spring Boot项目。你可以使用编辑新建一个Springboot项目编辑
手动添加的依赖:
统一封装一下返回结果
开干:
1.构建好机器人需要的常量:
下面的部分就是存放对应模型的url和Serverid以及秘钥 这些是根据你们要选定的模型去改的
存历史记录 以及查询历史记录的方法:
下面是获取ai对话的回答内容的部分 (这里是有线程优化的处理的)
这段代码实现了一个基于Spring和WebSocket的异步问答系统。主要的逻辑如下:
在并发场景下,这样优化的优势在于:
这一段代码是可以应用在其他使用WebSocket连接ai的业务上的
是基于WebSocket的与远程服务进行问答的功能。主要的逻辑如下:
最后 就是鉴权方法 这个内容 作为api的调用者 我们是不需要理解的(并不影响使用),当然如果对网络协议以及加密感兴趣的伙伴可以细看 我就直接放上来了,因为每家公司的鉴权方法都不一样,而且文档中会直接给出鉴权方法 直接复制到代码里面就可以的
实现效果
如果朋友对定向提示词工程有兴趣的话可以专门出一期这个模块的博客
提示词工程主要的优势在于以下:
从零开始 搭建一个Spring boot程序
-
确保你已经安装了Java开发工具(JDK)。你可以在命令行中输入
java -version
来验证是否已安装Java,并确保版本符合Spring Boot的要求。
安装的部分我就不演示了默认大家已经装好配置好环境
-
创建一个新的Spring Boot项目。你可以使用
新建一个Springboot项目
我这里选了Springboot版本2.6.13 这个倒是不会有太大问题根据自己项目来就行
旁边这些依赖自己有需要的就加 我这边不用这里的东西就不选了
然后出来 他就帮我们建立好了 这些依赖跟框架了 可以在pom文件里看到
手动添加的依赖:
在Maven里输入:
cn.hutool
hutool-json
5.7.10
com.alibaba
fastjson
1.2.78
com.google.code.gson
gson
2.8.9
com.squareup.okhttp3
okhttp
4.9.1
org.springframework.boot
spring-boot-starter-data-redis
2.5.4
org.springframework.boot
spring-boot-starter-web
2.5.4
然后我们新建一个class文件
在里面加入导包:
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.gson.Gson;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
配置了那么多。终于可以开始项目了…
统一封装一下返回结果
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/*/**
*@author suze
*@date 2023-10-25
*@time 15:19
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Boolean success;
private String errorMsg;
private Object data;
private Long total;
public static Result ok(){
return new Result(true, null, null, null);
}
public static Result ok(Object data){
return new Result(true, null, data, null);
}
public static Result ok(List> data, Long total){
return new Result(true, null, data, total);
}
public static Result fail(String errorMsg){
return new Result(false, errorMsg, null, null);
}
// public static Result fail(int errorCode, String errorMsg){
// return new Result(false, errorMsg, null, null);
// }
}
然后用到的地方就导入Result的包即可
import TopOne.dto.Result;
开干:
为了方便演示我把Controller和Server放在一个文件里 大家根据自己需要去调整
1.构建好机器人需要的常量:
public static final Gson gson = new Gson();
// 个性化参数
private String userId;
private Boolean wsCloseFlag;
private static Boolean totalFlag=true; // 控制提示用户是否输入
public List historyList=new ArrayList(); // 这个是临时的 每次用完在线程中要注销他
//写一个结构专门来存机器的回答
public static class BotText{
String content=" ";
}
public static BotText botText=new BotText();
//告诉控制器输出完没有
public static Boolean outputFlag=true;
private String botContent = "";
//返回的json结果拆解
class JsonParse {
Header header;
Payload payload;
}
class Header {
int code;
int status;
String sid;
}
class Payload {
Choices choices;
}
class Choices {
List text;
}
class Text {
String role;
String content;
}
class RoleContent{
String role;
String content;
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
下面的部分就是存放对应模型的url和Serverid以及秘钥 这些是根据你们要选定的模型去改的
public static final String hostUrl = "";
public static final String appid = "";
public static final String apiSecret = "";
public static final String apiKey = "";
存历史记录 以及查询历史记录的方法:
我这里做的缓存是2h 用的是Redis,一开始构建项目的时候可以用一个简单的Map来代替Redis 自行替换掉Redis部分代码即可
// 将对话历史存储到 Redis
public void saveHistory(String history,String id) {
// 设置有效期为 2 小时
stringRedisTemplate.opsForValue().set("id:" + id + ":history", history, 2, TimeUnit.HOURS);
}
@RequestMapping("/SaveHistory")
public Result SaveHistory(@RequestParam("userId") String id, @RequestBody String history) {
saveHistory(history,id);
return Result.ok("保存历史记录成功有效时间:2h");
}
// 从 Redis 中获取对话历史
public List getHistory(String userId) {
String historyStr = stringRedisTemplate.opsForValue().get("id:" + userId + ":history");
if (historyStr==null){
return null;
}
return JSONUtil.toList(JSONUtil.parseArray(historyStr), RoleContent.class);
}
//用于获取历史聊天记录
@RequestMapping("/history")
public Result history(@RequestParam("userId") String id) {
String history = stringRedisTemplate.opsForValue().get("id:" + id + ":history");
if (history == null) {
return Result.fail("没有找到历史记录");
}
JSONArray jsonObject = JSON.parseArray(history);
// String jsonString = JSON.toJSONString(jsonObject);
return Result.ok(jsonObject);
}
下面是获取ai对话的回答内容的部分 (这里是有线程优化的处理的)
public static String totalAnswer=""; // 大模型的答案汇总
// 可以写原始问题
public static String NewQuestion = "";
public WordUtils wordUtils=new WordUtils();
//线程池
public ThreadPoolExecutor pool = new ThreadPoolExecutor(13, 13, 1,
TimeUnit.MINUTES, new ArrayBlockingQueue(6),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
@RequestMapping("/get")
public DeferredResult get(@RequestParam("question") String question, @RequestParam("id") String id) {
//创建了一个DeferredResult对象,并将其返回给前端。在异步任务执行完毕后,
// 通过调用deferredResult.setResult(result)方法将结果设置到DeferredResult对象中,从而实现异步返回结果给前端。
DeferredResult deferredResult = new DeferredResult();
CompletableFuture.supplyAsync(() -> {
try {
callable callable = new callable(question, id);
String answer = callable.call();
Result result = Result.ok(answer);
return result;
} catch (Exception e) {
Result result = Result.fail(e.getMessage());
return result;
}
}, pool).whenComplete((result, throwable) -> {
if (throwable != null) {
result = Result.fail(throwable.getMessage());
deferredResult.setResult(result);
} else {
deferredResult.setResult(result);
}
});
return deferredResult;
}
public class callable implements Callable{
private String id;
private String question;
public callable(String question,String id) {
//这里处理一下userId的长度 因为讯飞那边限制了
if (id.length() >= 30) {
id= id.substring(0, 30);
}
this.question = question;
this.id=id;
}
@Override
public String call() throws Exception {
String answer =main(question,id);
//System.out.println(answer);
answer = JSONUtil.toJsonStr(answer);
System.out.println("call");
botText.content="";//清空
//缓存历史对话 时效两小时
//String historyStr = JSONUtil.toJsonStr();
//stringRedisTemplate.opsForValue().set("id:"+id+":history", historyStr,2,TimeUnit.HOURS);
return answer;
}
}
// 主函数
public String main(String newQuestion,String userid) throws Exception {
// 个性化参数入口,如果是并发使用,可以在这里模拟
System.out.println(totalFlag);
if(totalFlag){
totalFlag=false;
NewQuestion=newQuestion;
// 构建鉴权url
String authUrl = getAuthUrl(hostUrl, apiKey, apiSecret);
OkHttpClient client = new OkHttpClient.Builder().build();
String url = authUrl.toString().replace("http://", "ws://").replace("https://", "wss://");
Request request = new Request.Builder().url(url).build();
totalAnswer="";
// WebSocket webSocket = client.newWebSocket(request, new BigModelNew(i + "",false));
//这里创建了大模型的新对象 实际上那些发送请求获取答案的操作都是在这个线程中做的
BigModelNew bigModelNew = null;
if (getHistory(userid)!=null){
bigModelNew=new BigModelNew(userid, false,getHistory(userid),stringRedisTemplate);
}
else {
bigModelNew=new BigModelNew(userid, false,historyList,stringRedisTemplate);
}
// 等待 WebSocket 的 run() 方法执行完毕
int maxWaitTime = 10000; // 最大等待时间,单位:毫秒
int currentWaitTime = 0; // 当前已等待的时间,单位:毫秒
int waitInterval = 1000;// 每次等待的时间间隔,单位:毫秒
WebSocket webSocket = client.newWebSocket(request, bigModelNew);
System.out.println(maxWaitTime);
while (currentWaitTime HistoryList
,@Value("${stringRedisTemplate}") StringRedisTemplate stringRedisTemplate) {
this.userId = userId;
this.wsCloseFlag = wsCloseFlag;
this.historyList=HistoryList;
this.stringRedisTemplate = stringRedisTemplate;
}
这段代码实现了一个基于Spring和WebSocket的异步问答系统。主要的逻辑如下:
-
在
get()
方法中,接收前端传递的问题和id参数,并创建一个DeferredResult
对象,用于异步返回结果给前端。 -
通过
CompletableFuture.supplyAsync()
方法创建一个异步任务,该任务会在一个线程池中执行。 -
异步任务的具体逻辑在
callable
类的call()
方法中实现。在该方法中,调用main()
方法进行问题回答,并将结果转换为JSON格式。 -
异步任务执行完毕后,通过
deferredResult.setResult(result)
方法将结果设置到DeferredResult
对象中,实现异步返回结果给前端。 -
在
main()
方法中,判断是否是并发场景下的第一个请求。如果是第一个请求,则创建一个WebSocket连接,并等待run()
方法执行完毕。 -
在
run()
方法中,通过WebSocket与远程服务进行通信,获取问题的回答。 -
在并发场景下,如果有多个请求同时到达,只有第一个请求会创建WebSocket连接,后续的请求会等待第一个请求的回答结果,并共享同一个
totalAnswer
。
在并发场景下,这样优化的优势在于:
-
使用线程池和异步任务可以提高并发处理能力,减少请求的等待时间。通过异步任务,可以将耗时的操作(如远程服务调用)放在后台线程中执行,而不会阻塞主线程。
-
使用
DeferredResult
对象可以实现异步返回结果给前端。每个请求都会得到一个独立的DeferredResult
对象,通过设置结果到该对象中,可以实现异步返回给前端。 -
在并发场景下,只有第一个请求会创建WebSocket连接,后续的请求会等待第一个请求的回答结果。这样可以减少对远程服务的重复请求,节省资源和提高性能。
然后我们把外面的调用的功能构建好了 接下来就是WebSocket内部当中 重写的具体方法 下面是重写部分的方法:
//一个很关键的函数 用于得到botContent
public String getBotContent() {
return botContent;
}
public boolean canAddHistory(){ // 由于历史记录最大上线1.2W左右,需要判断是能能加入历史
int history_length=0;
for(RoleContent temp:historyList){
history_length=history_length+temp.content.length();
}
if(history_length>12000){
historyList=new ArrayList();
return false;
}else{
return true;
}
}
// 线程来发送音频与参数
class MyThread extends Thread {
private WebSocket webSocket;
private String newAnswer;
public MyThread(WebSocket webSocket) {
this.webSocket = webSocket;
}
public void run() {
try {
JSONObject requestJson=new JSONObject();
JSONObject header=new JSONObject(); // header参数
header.put("app_id",appid);
header.put("uid",userId);//这里放userId
JSONObject parameter=new JSONObject(); // parameter参数
JSONObject chat=new JSONObject();
chat.put("domain","generalv3");
chat.put("temperature",0.6);
chat.put("max_tokens",8192);
parameter.put("chat",chat);
JSONObject payload=new JSONObject(); // payload参数
JSONObject message=new JSONObject();
JSONArray text=new JSONArray();
// 历史问题获取
if(historyList.size()>0){
for(RoleContent tempRoleContent:historyList){
text.add(JSON.toJSON(tempRoleContent));
}
}
// 最新问题
RoleContent roleContent=new RoleContent();
roleContent.role="user";
roleContent.content=NewQuestion;
text.add(JSON.toJSON(roleContent));
// System.err.println("text:");
historyList.add(roleContent);
// historyList.forEach(System.out::println);
//saveHistory(historyList);//在这里就把历史记录存到Redis了 就可以清空该线程的历史记录List了
//historyList.clear();
message.put("text",text);
payload.put("message",message);
requestJson.put("header",header);
requestJson.put("parameter",parameter);
requestJson.put("payload",payload);
// System.err.println(requestJson); // 可以打印看每次的传参明细
webSocket.send(requestJson.toString());
// 等待服务端返回完毕后关闭
while (true) {
// System.err.println(wsCloseFlag + "---");
Thread.sleep(200);
if (wsCloseFlag) {
break;
}
}
webSocket.close(1000, "");
// System.out.println("answer"+botText.content);
} catch (Exception e) {
e.printStackTrace();
}finally {
botContent=botText.content;
}
}
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
//System.out.print("AI:");
MyThread myThread = new MyThread(webSocket);
myThread.start();
}
@Override
public void onMessage(WebSocket webSocket, String text) {
// System.out.println(userId + "用来区分那个用户的结果" + text);
JsonParse myJsonParse = gson.fromJson(text, JsonParse.class);
//System.out.println("AI:" + gson.toJson(myJsonParse.payload));
if (myJsonParse.header.code != 0) {
System.out.println("发生错误,错误码为:" + myJsonParse.header.code);
System.out.println("本次请求的sid为:" + myJsonParse.header.sid);
webSocket.close(1000, "");
}
List textList = myJsonParse.payload.choices.text;
for (Text temp : textList) {
//System.out.println("这里存了机器的话"+temp.content);
botText.content=botText.content+temp.content;//这里存机器的话
totalAnswer=totalAnswer+temp.content;
}
//System.out.println("answeraaaa:"+botText.content);//这里能够打印 但是打印很多次说明他调用了很多次
// botText.content=totalAnswer;
if (myJsonParse.header.status == 2) {
// 可以关闭连接,释放资源
// System.out.println();
// System.out.println("*************************************************************************************");
if(canAddHistory()){
RoleContent roleContent=new RoleContent();
roleContent.setRole("assistant");
roleContent.setContent(totalAnswer);
historyList.add(roleContent);
//System.out.println("OnMessage:"+historyList.toString());
//String jsonString = JSON.toJSONString(historyList, SerializerFeature.PrettyFormat);
//history.text=jsonString;//这里已经把历史记录给保存了
//在这里答案已经输出完了,就应该outputFinished = true;
// output=botText.content;
// myHistory=history.text;
outputFlag=true;
System.out.println("AI:answer"+botText.content);
}else{
RoleContent roleContent=new RoleContent();
roleContent.setRole("assistant");
roleContent.setContent(totalAnswer);
historyList.add(roleContent);
}
//saveHistory(historyList);//在这里就把历史记录存到Redis了 就可以清空该线程的历史记录List了
historyList.clear();
wsCloseFlag = true;//只有等消息传过来了 才能够结束
totalFlag=true;
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
if (null != response) {
int code = response.code();
System.out.println("onFailure code:" + code);
try {
System.out.println("onFailure body:" + response.body().string());
} catch (IOException e) {
throw new RuntimeException(e);
}
if (101 != code) {
System.out.println("connection failed");
System.exit(0);
}
}
}
这一段代码是可以应用在其他使用WebSocket连接ai的业务上的
是基于WebSocket的与远程服务进行问答的功能。主要的逻辑如下:
MyThread
类是一个继承自Thread
的线程类,用于发送问答请求和接收回答。在run()
方法中,首先构建请求的JSON对象,包括头部参数、参数和载荷参数。然后通过WebSocket发送该JSON对象。接着,在一个循环中等待服务端返回结果,并将返回的结果拼接到botText.content
和totalAnswer
中。最后,关闭WebSocket连接,并将botText.content
赋值给botContent
。
onOpen()
方法在WebSocket连接建立时被调用。在该方法中,创建一个MyThread
对象并启动线程。
onMessage()
方法在接收到WebSocket消息时被调用。该方法首先解析收到的消息,并判断是否存在错误。如果没有错误,则将回答文本拼接到botText.content
和totalAnswer
中,并根据返回的状态码进行相应的处理。如果状态码为2,表示回答已经完整返回,此时可以关闭连接并进行一些后续处理,如将回答文本添加到历史记录中。
onFailure()
方法在WebSocket连接失败时被调用。在该方法中,可以根据失败的原因进行相应的处理。
最后 就是鉴权方法 这个内容 作为api的调用者 我们是不需要理解的(并不影响使用),当然如果对网络协议以及加密感兴趣的伙伴可以细看 我就直接放上来了,因为每家公司的鉴权方法都不一样,而且文档中会直接给出鉴权方法 直接复制到代码里面就可以的
// 鉴权方法
public static String getAuthUrl(String hostUrl, String apiKey, String apiSecret) throws Exception {
URL url = new URL(hostUrl);
// 时间
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("GMT"));
String date = format.format(new Date());
// 拼接
String preStr = "host: " + url.getHost() + "n" +
"date: " + date + "n" +
"GET " + url.getPath() + " HTTP/1.1";
// System.err.println(preStr);
// SHA256加密
Mac mac = Mac.getInstance("hmacsha256");
SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "hmacsha256");
mac.init(spec);
byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8));
// Base64加密
String sha = Base64.getEncoder().encodeToString(hexDigits);
// System.err.println(sha);
// 拼接
String authorization = String.format("api_key="%s", algorithm="%s", headers="%s", signature="%s"", apiKey, "hmac-sha256", "host date request-line", sha);
// 拼接地址
HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder().//
addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorization.getBytes(StandardCharsets.UTF_8))).//
addQueryParameter("date", date).//
addQueryParameter("host", url.getHost()).//
build();
// System.err.println(httpUrl.toString());
return httpUrl.toString();
}
实现效果
跟一个搞小程序的朋友借了个模板 套了一下前端 目前在小程序端实现了
响应的内容:
这是获取聊天记录的响应:
具体的业务我还做了提示词工程,让ai具备一点定向场景的倾向了,就像如下
具体的业务截图回应效果
如果朋友对定向提示词工程有兴趣的话可以专门出一期这个模块的博客
提示词工程主要的优势在于以下:
提高问答准确性:通过给用户提供定向场景的提示词,可以引导用户在特定领域或场景下提问。这样做可以减少模糊或不相关的问题,从而提高问答的准确性。提示词可以限制用户的问题范围,使得AI能够更好地理解用户的意图并给出相关的回答。
加速问题解决:定向场景的提示词工程可以帮助用户快速定位到他们感兴趣的领域或问题类型,并提供相关的问题模板或关键词。这样可以节省用户在描述问题上的时间和精力,使得问题能够更快地得到解答,提高问题解决的效率。
提升用户体验:通过为用户提供定向场景的提示词,可以使用户感到更加舒适和自信。用户知道他们在与AI进行交互时所处的场景,并且可以根据提示词的指引进行提问。这种引导性的交互方式可以减少用户的迷茫和犹豫,提升用户与AI的交互体验。
简化系统配置:定向场景的提示词工程可以帮助系统管理员或开发人员更好地配置和管理问答系统。通过定义和组织不同的场景和相关的提示词,可以使系统的配置更加直观和可控。管理员可以根据实际需求进行提示词的调整和更新,以适应不同的应用场景。
有兴趣的小伙伴可以先关注一下我 下一期出的话我会发粉丝通告哦