从零开始 搭建一个Spring boot程序
- 安装的部分我就不演示了默认大家已经装好配置好环境
新建一个Springboot项目
- 我这里选了Springboot版本2.6.13 这个倒是不会有太大问题根据自己项目来就行
旁边这些依赖自己有需要的就加 我这边不用这里的东西就不选了
然后出来 他就帮我们建立好了 这些依赖跟框架了 可以在pom文件里看到
手动添加的依赖:
在Maven里输入:
<!-- Hutool --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-json</artifactId> <version>5.7.10</version> </dependency> <!-- Alibaba FastJSON --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.78</version> </dependency> <!-- Google Gson --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.9</version> </dependency> <!-- OkHttp --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.9.1</version> </dependency> <!-- Spring Data Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.5.4</version> </dependency> <!-- Spring Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.5.4</version> </dependency>
然后我们新建一个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<RoleContent> 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> 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<RoleContent> 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<Result> get(@RequestParam("question") String question, @RequestParam("id") String id) { //创建了一个DeferredResult<Result>对象,并将其返回给前端。在异步任务执行完毕后, // 通过调用deferredResult.setResult(result)方法将结果设置到DeferredResult对象中,从而实现异步返回结果给前端。 DeferredResult<Result> 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<String>{ 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 < maxWaitTime) { if (bigModelNew.getBotContent().equals("")) { // run() 方法还未执行完毕,可以进行一些其他操作或等待一段时间 Thread.sleep(waitInterval); System.out.println("正在执行线程"+Thread.currentThread().getName()+"...等待时间还剩:"+(maxWaitTime-currentWaitTime)); currentWaitTime += waitInterval; } else { // run() 方法已执行完毕,获取 bot.content 值并进行后续操作 //System.out.println("run执行完毕"); return bigModelNew.getBotContent(); //System.out.println(botText.content); // ... } } } totalFlag=true; return "网络开了点小差 试试重新发送你的消息吧"; } // 构造函数 public BigModelNew(@Value("${userId}") String userId ,@Value("${wsCloseFlag}") Boolean wsCloseFlag ,@Value("${HistoryList}")List<RoleContent> HistoryList ,@Value("${stringRedisTemplate}") StringRedisTemplate stringRedisTemplate) { this.userId = userId; this.wsCloseFlag = wsCloseFlag; this.historyList=HistoryList; this.stringRedisTemplate = stringRedisTemplate; }
这段代码实现了一个基于Spring和WebSocket的异步问答系统。主要的逻辑如下:
- 在get()方法中,接收前端传递的问题和id参数,并创建一个DeferredResult<Result>对象,用于异步返回结果给前端。
- 通过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<Text> 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的交互体验。
- 简化系统配置:定向场景的提示词工程可以帮助系统管理员或开发人员更好地配置和管理问答系统。通过定义和组织不同的场景和相关的提示词,可以使系统的配置更加直观和可控。管理员可以根据实际需求进行提示词的调整和更新,以适应不同的应用场景。