快过年了,首先恭贺各位大佬终于不再写bug,回家过年了。祝各位来年事业蒸蒸日上,成就非凡,全家幸福安康。
春联大家都不陌生,虽然现在都不手写了,但是我记得我小时候村里还有手写的。不管了,先往下看。
先来瞅瞅
花了三天时间,从前端ui到后端大模型,全部打通,敬请各位大佬挑逗
这里有个视频,页面很简单,就是前端输入一个主题,然后等待大模型反馈。但是底层所做的事情却很多。
春联大模型
我会将这个大模型分为两大块给大家分享,我觉得画图比讲话要更好一点,所以看图吧
我们先从全局考虑一下,首先这里面最能确认的一点就是要采用长链接的方式,不管是对于讯飞的对接来接受讯飞模型的反馈,亦或是是我们自己的服务和自己的应用页面的保持
(因为这里面但凡一方是要求流式传输,就决定了另一方也必须保持长链接)
这三者关系即是下放这个图,我们自己做的这个服务,姑且称之为chunlian-model,那么他必须扮演两种角色
1、对于讯飞模型来说,我们的服务是客户端,需要向讯飞模型提问问题
2、对于自己的前端应用来说,我们又扮演服务端,来接受应用的询问
看懂上面之后接下来我们考虑下用什么技术方案以及应该考虑的问题点。
先说讯飞
现在可以免费领取有限token,接口地址:讯飞接口地址
我们对接的是web类(接口选择的是v3,并没选择3.5,3.5是刚新出的,很强很强),大家可以自行选择对接类型
在文档的最后会有对接的demo,我选择的java语言。可以参考他的demo,去融合到自己的模型中呢(由于篇幅有限,只贴出关键性代码)
继承的是WebSocketListener,这个在springboot项目中不是首推的,不过不重要,先简单着来。
java复制代码public class XfModel extends WebSocketListener {
// 这里是开始,当用户询问问题
// 会发起一个websocket,这里便会监听到
// 起一个线程去向讯飞模型发送指令
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
MyThread myThread = new MyThread(webSocket);
myThread.start();
}
/**
* 核心代码
* 向讯飞发送指令
*/
class MyThread extends Thread {
private WebSocket webSocket;
public MyThread(WebSocket webSocket) {
this.webSocket = webSocket;
}
/**
* 主要是按照讯飞接口要求,组装各种参数
* 这里有一个我刚才提到的沾包问题,讯飞也有一个方案可以解决
*/
public void run() {
try {
JSONObject requestJson=new JSONObject();
JSONObject header=new JSONObject(); // header参数
header.put("app_id","2f406cc1");
header.put("uid",UUID.randomUUID().toString().substring(0, 10));
JSONObject parameter=new JSONObject(); // parameter参数
JSONObject chat=new JSONObject();
chat.put("domain","generalv3");
chat.put("temperature",0.5);
chat.put("max_tokens",4096);
parameter.put("chat",chat);
JSONObject payload=new JSONObject(); // payload参数
JSONObject message=new JSONObject();
JSONObject functions=new JSONObject();
JSONArray text=new JSONArray();
JSONArray funText=new JSONArray();
JSONArray requiredJsonArray =new JSONArray();
JSONObject funTextJson=new JSONObject();
JSONObject parametersJson =new JSONObject();
JSONObject propertiesJson =new JSONObject();
JSONObject propertiesSLJson = new JSONObject();
propertiesSLJson.put("type","string");
propertiesSLJson.put("description","春联的上联");
JSONObject propertiesXLJson = new JSONObject();
propertiesXLJson.put("type","string");
propertiesXLJson.put("description","春联的下联");
JSONObject propertiesHPJson = new JSONObject();
propertiesHPJson.put("type","string");
propertiesHPJson.put("description","春联的横批联");
propertiesJson.put("sl",propertiesSLJson);
propertiesJson.put("xl",propertiesXLJson);
propertiesJson.put("hp",propertiesHPJson);
parametersJson.put("type","object");
parametersJson.put("properties",propertiesJson);
requiredJsonArray.add("sl");
requiredJsonArray.add("xl");
requiredJsonArray.add("hp");
parametersJson.put("required",requiredJsonArray);
funTextJson.put("name","生成春联");
funTextJson.put("description","根据提示生成一副春联,包含上联、下联以及横批");
funTextJson.put("parameters",parametersJson);
funText.add(funTextJson);
functions.put("text",funText);
// 最新问题
RoleContent roleContent=new RoleContent();
roleContent.role="user";
if (NewQuestion.isEmpty()){
roleContent.content="现在你是春联AI生成高手,帮我生成一个春联,包括上联,下联和横批";
}else {
roleContent.content="现在你是春联AI生成高手,帮我生成一个春联,包括上联,下联和横批,根据关键字:"+NewQuestion;
}
text.add(JSON.toJSON(roleContent));
historyList.add(roleContent);
message.put("text",text);
payload.put("message",message);
payload.put("functions",functions);
requestJson.put("header",header);
requestJson.put("parameter",parameter);
requestJson.put("payload",payload);
System.err.println("requestJson========="+requestJson); // 可以打印看每次的传参明细
webSocket.send(requestJson.toString());
// 等待服务端返回完毕后关闭
while (true) {
Thread.sleep(200);
if (wsCloseFlag) {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
刚才上面讲的是向讯飞发送指令,那么刚才提到一个沾包问题怎么,因为讯飞是流式响应,那势必会出现沾包,按照传统解决方案是没办法的。因为协议不确定,你总不能每次让讯飞给你按照特定协议传输回来。
那么讯飞给出一个方法,Function Call
这玩意主要有俩作用
1、支持自定义返回字段
这个啥意思,就是你请求的时候,传入自定义字段,然后再解释下这个字段的含义,那么讯飞给反馈的时候会把这个字段给你填充上,比如上述代码中(sl:上联; xl:下联; hp:横批)
2、当触发了function_call的情况下,只会返回一帧结果,啥意思,相当于给你拆分好了,不会出现数据沾包。
所以在接收消息的时候,只会收到一次结果,下面这个onmessage 就是当讯飞给我们返回的时候,在这里监听获取。
java复制代码@Override
public void onMessage(WebSocket webSocket, String text) {
//按照响应格式解析
JsonParse myJsonParse = gson.fromJson(text, JsonParse.class);
//这段代码啥意思,这里是区分我们的应用客户端,每个人的id不一样,问的问题要返回给各自的客户端
//不能你问的问题,让我给看到的,这里是tcp长链接,所以必须区分客户端
WebSocketSession userSession = userSessions.get(userId);
//如果发生错误,直接扔给前端
if (myJsonParse.header.code != 0) {
System.out.println("发生错误,错误码为:" + myJsonParse.header.code);
System.out.println("本次请求的sid为:" + myJsonParse.header.sid);
try {
userSession.sendMessage(new TextMessage(myJsonParse.header.message));
} catch (IOException e) {
e.printStackTrace();
}
return;
}
//按照接口响应来返回给前端
List<Text> textList = myJsonParse.payload.choices.text;
FunctionCall callResult = textList.get(0).function_call;
JSONObject result = JSONObject.parseObject(callResult.getArguments());
//返回给前端
try {
userSession.sendMessage(new TextMessage(result.toJSONString()));
} catch (IOException e) {
e.printStackTrace();
}
// status为2代表成功,可以关闭连接,释放资源
if (myJsonParse.header.status == 2) {
System.out.println("*************************************************************************************");
if(canAddHistory()){
RoleContent roleContent=new RoleContent();
roleContent.setRole("assistant");
roleContent.setContent(totalAnswer);
historyList.add(roleContent);
}else{
historyList.remove(0);
RoleContent roleContent=new RoleContent();
roleContent.setRole("assistant");
roleContent.setContent(totalAnswer);
historyList.add(roleContent);
}
}
}
上述代码呢,只是和讯飞的交互,那么接下来看我们自己前后端的的保持,用的是TextWebSocketHandler。
java复制代码@Component
public class MyWebSocketHandler extends TextWebSocketHandler {
@Value("${xfyun.hosturl}")
private String hostUrl;
@Value("${xfyun.apiSecret}")
private String apiSecret;
@Value("${xfyun.apiKey}")
private String apiKey;
//安全线程map 保存用户的客户端与后端session的关联关系
//这样接收到讯飞模型反馈的时候,根据userid,找到相对应WebSocketSession
private final Map<String, WebSocketSession> userSessions = new ConcurrentHashMap<>();
/**
* 这里接收用户在前端问的春联主题信息
* @param session
* @param message
* @throws Exception
*/
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String clientMessage = message.getPayload();
//用户的提问信息
System.out.println("Received message: " + clientMessage);
// 构建鉴权url
String authUrl = getAuthUrl(hostUrl,apiKey , apiSecret);
//用okhttp构建请求
OkHttpClient client = new OkHttpClient.Builder().build();
String url = authUrl.toString().replace("http://", "ws://").replace("https://", "wss://");
Request request = new Request.Builder().url(url).build();
//发起websocket请求
client.newWebSocket(request, new XfModel(session.getUri().getQuery().substring(7), clientMessage,userSessions));
}
//保存用户信息,前端会传用户信息
@Override
public void afterConnectionEstablished(WebSocketSession session) {
userSessions.put(session.getUri().getQuery().substring(7), session);
}
// 结束之后 删除用户信息
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
userSessions.remove(session.getUri().getQuery().substring(7));
}
}
OK,今天的分享先到这里,最主要给大家讲的第一是思路,第二是对接逻辑和过程,包括解决了像沾包等问题,中间自己踩了很多坑,后面有机会再做细致分享。
代码不难,也挺简单,最主要是两段长链接,要互相输出。
你也可以根据自己的想法出一个东西,前端不会的可以让GPT等辅助你做起来
当然,几天就做完的肯定会有很多问题,后面再扣细节,不过把能想到的都想到了,还有就是咱们是免费版本的讯飞,可能流量上会受限制。人多的时候可能会慢。
体验地址:春联生成器