Java后端
pom.xml添加ChatGPT的依赖
<dependency><groupId>com.unfbx</groupId><artifactId>chatgpt-java</artifactId><version>1.0.10</version><!--排除子依赖 slf4j-simple 不然会有冲突 --><exclusions><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-simple</artifactId></exclusion></exclusions>
</dependency>
SpringBootApplication启动文件配置
@SpringBootApplication
public class NotesApplication implements CommandLineRunner {public static void main(String[] args) {SpringApplication.run(NotesApplication.class, args);}@Overridepublic void run(String... args) throws Exception {}@Value("${chatgpt.apiKey}")private List<String> apiKey;@Value("${chatgpt.apiHost}")private String apiHost;@Beanpublic OpenAiStreamClientSub openAiStreamClient() {//本地开发调试条件: 1. 配置科学上网 2.配置IP代理Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 17890));HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger());//!!!!!!测试或者发布到服务器千万不要配置Level == BODY!!!!httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);OkHttpClient okHttpClient = new OkHttpClient.Builder().proxy(proxy).addInterceptor(httpLoggingInterceptor).connectTimeout(30, TimeUnit.SECONDS).writeTimeout(60, TimeUnit.SECONDS).readTimeout(600, TimeUnit.SECONDS).build();System.out.println("使用的key:" + apiKey);return OpenAiStreamClientSub.builder().apiHost(apiHost).apiKey(apiKey)//自定义key使用策略 默认随机策略.keyStrategy(new KeyRandomStrategy()).okHttpClient(okHttpClient).build();}
}
注意本地开发配置条件:.配置IP代理 很关键,缺一不可
控制器Controller
/*** AI助手端 SSE 服务端 推送 客户端* @param headers*/@RequestMapping("/v1/chat/completions")@CrossOriginpublic SseEmitter sseCompletions(@RequestHeader Map<String, String> headers) {String uuid = headers.get("uuid");//解码 内容部分String promptContent = headers.get("promptcontent");String content = null;try {content = URLDecoder.decode(promptContent, "utf-8");} catch (UnsupportedEncodingException e) {e.printStackTrace();}log.info("content:{}", content);//默认30秒超时,设置为0L则永不超时SseEmitter sseEmitter = new SseEmitter(0L);long startTime = System.currentTimeMillis();customChatGpt.sseCompletions(sseEmitter, uuid, content);long endTime = System.currentTimeMillis();log.info("请求耗时:{} ms", (endTime-startTime)/1000);return sseEmitter;
}
- 需要配置 @CrossOrigin 注解: 全称"跨域资源共享"(Cross-origin resource sharing)
解决跨域问题,实现CORS的关键在于服务器,只要服务器实现CORS接口,就可以实现跨域通信
返回值为SseEmitter ,此处采用SSE推送技术实现,SseEmitter简介:SpringMVC提供的一种技术,可以实现服务端向客户端实时推送数据的功能,用法在Contorller中提供一个接口,返回SseEmitter对象,发送数据可以在另一个接口调用其send方法发送数据,SpringBoot已经集成这个功能,因此直接使用
生成ChatGPT喜欢的聊天列表,用于ChatGPT更好的理解与用户聊天的上下文
public void sseCompletions(SseEmitter sseEmitter, String uuid, String question) {String key = String.format(messagesKey, uuid);List<Message> messages = redisCache.getCacheObject(key);if (StringUtils.isNotNull(messages)) {if (messages.size() >= 5) {messages = messages.subList(1, 5);}Message currentMessage = Message.builder().content(question).role(Message.Role.USER).build();messages.add(currentMessage);} else {messages = new ArrayList<>();Message currentMessage = Message.builder().content(question).role(Message.Role.USER).build();messages.add(currentMessage);}OpenAISSEEventSourceListener eventSourceListener = new OpenAISSEEventSourceListener(sseEmitter);openAiStreamClientSub.streamChatCompletion(messages, eventSourceListener);redisCache.setCacheObject(key, messages, 50, TimeUnit.MINUTES );
}1.3 重载 EventSourceListener 监听器 重写 onEvent 回调接口
@SneakyThrows@Overridepublic void onEvent(EventSource eventSource, String id, String type, String data) {log.info("OpenAI返回数据:{}", data);if (data.equals("[DONE]")) {log.info("OpenAI返回数据结束了");sseEmitter.send(SseEmitter.event().id("[DONE]").data("[DONE]").reconnectTime(3000));return;}ObjectMapper mapper = new ObjectMapper();// 读取JsonChatCompletionResponse completionResponse = mapper.readValue(data, ChatCompletionResponse.class);sseEmitter.send(SseEmitter.event().id(completionResponse.getId()).data(completionResponse.getChoices().get(0).getDelta()).reconnectTime(3000));}
构造http请求 v1/chat/completions 接口,发起对话
public void streamChatCompletion(ChatCompletion chatCompletion, EventSourceListener eventSourceListener) {if (Objects.isNull(eventSourceListener)) {log.error("参数异常:EventSourceListener不能为空,可以参考:com.unfbx.chatgpt.sse.ConsoleEventSourceListener");throw new BaseException(CommonError.PARAM_ERROR);} else {if (!chatCompletion.isStream()) {chatCompletion.setStream(true);}try {EventSource.Factory factory = EventSources.createFactory(this.okHttpClient);ObjectMapper mapper = new ObjectMapper();String requestBody = mapper.writeValueAsString(chatCompletion);Request request = (new okhttp3.Request.Builder()).url(this.apiHost + "v1/chat/completions").post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), requestBody)).build();factory.newEventSource(request, eventSourceListener);} catch (JsonProcessingException var8) {log.error("请求参数解析异常:{}", var8);var8.printStackTrace();} catch (Exception var9) {log.error("请求参数解析异常:{}", var9);var9.printStackTrace();}}}
等待ChatGPT流式回答,效果如下
H5前端
下载依赖: npm install event-source-polyfill
创建SSE连接
createSSE(item){let that = thisconst itemAss = {type: 'chat',content: '回答中...',my: false,loading: true,};that.messages.push(itemAss)that.disableInput()that.setScroll()if(window.EventSource){this.eventSource = new EventSourcePolyfill(//${config.baseUrl}/sse/openApi/chatGPT/v1/chat/sseCompletions`http://localhost:7091/openApi/chatGPT/v1/chat/completions`, {// 设置重连时间heartbeatTimeout: 60 * 60 * 1000,// 添加tokenheaders: {'Authorization': `Bearer `,'uuid': that.userName,'promptcontent': encodeURIComponent(item.content)},});this.eventSource.onopen = (e) => {console.log("已建立SSE连接~")}this.eventSource.onmessage = (e) => {//隐藏加载框// uni.hideLoading();let item = that.messages[that.messages.length - 1]console.log("消息入栈:", e.data)let result = e.data//数据流开始标记if(result === '{"role":"assistant"}'){item.content = ''}//数据流结束标记if(result === '[DONE]'){uni.setStorageSync('isOpen', true)that.closeSSE()}//内容if(result.includes('content')){let jsonResult = JSON.parse(result)//内容进行拼接item.content += jsonResult.contentthat.setScroll()}}this.eventSource.onerror = (e) => {if (e.readyState == EventSource.CLOSED) {console.log("SSE连接关闭", JSON.stringify(this.eventSource))} else if (this.eventSource.readyState == EventSource.CONNECTING) {console.log("SSE正在重连", JSON.stringify(this.eventSource))} else {console.log('error', e);}};} else {console.log("你的浏览器不支持SSE~")}
},
问题及解决办法
配置本地代理