One能聊天接入百度千帆大模型 —— 文心一言

One能聊天介绍:基于ChatGPT实现的微信小程序,适配H5和WEB端。包含前后端,支持打字效果输出流式输出,支持AI聊天次数限制,支持分享增加次数等功能
One能聊天开源地址:https://github.com/oldinaction/ChatGPT-MP
One能聊天演示环境:可关注【阿壹族】公众号,并回复【One能聊天】查看
下文将介绍在One能聊天项目中接入百度千帆大模型 — 文心一言

开通服务

首先介绍一下百度AI相关产品矩阵
如下图百度力推的千帆大模型超级工厂,他包含

  • 千帆大模型平台:其中大模型开发就是自己训练一个大模型,这种比较有技术含量,少部分企业才会用到;大模型调用则包含百度开放的文心大模型(即文心一言,ERNIE 4.0和ERNIE 3.5为模型版本分类,对标ChatGPT),还包括一些第三方模型供调用
  • 千帆AppBuilder:是提供开发者基于文心大模型可以快速开发出一个AI应用,创建的应用可以集成一些官方的组件(如天气查询、快递查询等),也可以集成自定义组件(通过画布拖拽,自行编排组件逻辑,如调用企业内部API或调用大模型接口),另外还可导入知识库供大模型使用(支持txt/pdf/doc/url等模式)。通过AppBuilder创建的应用官方提供一个访问链接供普通用户使用(界面是通用的AI聊天界面),开发者也可以通过SDK调用创建的AI应用从而集成到实际的业务系统中。这部分会在后续文章中做详细说明
  • 千帆AI原生应用商店:就是百度自己开发的AI应用。如超级助理,下载浏览器插件即可使用,支持划词翻译、网页解读、OCR识别等功能

本文主要对文心大模型ERNIE的API调用做详细说明
image.png
创建应用:进入 https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application 创建,可勾选启用的模型,如ERNIE-3.5-8KERNIE-4.0-8KYi-34B-Chat(免费)等
image.png
部分模型计费说明如下
image.png

单次API调用案例

@RequestMapping("/baidu/ernieBotTurbo")
public Result baiduErnieBotTurbo(@RequestBody Map<String, Object> params) {
    BaiduConfig baiduConfig = SpringU.getBean(BaiduConfig.class);
    BaiduService baiduService = new BaiduService(baiduConfig.getApiKey(), baiduConfig.getApiSecret());
    BaiduChatMessage chatMessage = BaiduChatMessage.builder()
            .content((String) params.get("content"))
            .role("user")
            .build();
    ErnieBotTurboStreamParam postParam = ErnieBotTurboStreamParam.builder()
            .user_id(StpUtil.getLoginIdAsString())
            .messages(MiscU.Instance.toList(chatMessage))
            .build();
    ErnieBotTurboResponse ernieBotTurboResponse = baiduService.ernieBotTurbo(postParam);
    return Result.success(ernieBotTurboResponse);
}

// 该方法是同步请求API,会等大模型将数据完全生成之后,返回响应结果,可能需要等待较长时间,视生成文本长度而定
public ErnieBotTurboResponse ernieBotTurbo(ErnieBotTurboStreamParam param) {
    if (param == null) {
        log.error("参数异常:param不能为空");
        throw new RuntimeException("参数异常:param不能为空");
    }
    if (param.isStream()) {
        param.setStream(false);
    }
    String fullChatUrl = SpringU.getBean(BaiduConfig.class).getFullChatUrl();
    String post = HttpUtil.post(fullChatUrl + BaiduConfig.getToken(appKey, secretKey), JSONUtil.toJsonStr(param));
    return JSONUtil.toBean(post, ErnieBotTurboResponse.class);
}

public class BaiduConfig {
    @Value("${aezo-chat-gpt.baidu.api-key:}")
    private String apiKey;
    @Value("${aezo-chat-gpt.baidu.api-secret:}")
    private String apiSecret;
    @Value("${aezo-chat-gpt.baidu.chat-url:yi_34b_chat}")
    private String chatUrl;

    /**
     * Yi-34B-Chat 免费使用 https://cloud.baidu.com/doc/WENXINWORKSHOP/s/vlpteyv3c
     * 模型对应路径如,更多参考官方文档:
     * Yi-34B-Chat: yi_34b_chat
     * ERNIE-Lite-8K-0922: eb-instant
     * ERNIE-Speed-8K: ernie_speed
     */
    private static final String CHAT_URL_TPL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/%s?access_token=";

    public static String getToken(String appKey, String secretKey) {
        String url = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=" + appKey + "&client_secret=" + secretKey;
        String s = HttpUtil.get(url);
        Token bean = JSONUtil.toBean(s, Token.class);
        return bean.getAccess_token();
    }

    public String getFullChatUrl() {
        return String.format(CHAT_URL_TPL, chatUrl);
    }
}

调用测试
image.png

多轮对话流式输出

  • One能聊天中进行接收用户消息处理
private void onMessageBaidu(String msg, Map<String, Object> promptData, String messageContext) {
    BaiduConfig baiduConfig = SpringU.getBean(BaiduConfig.class);
    BaiduService baiduService = new BaiduService(baiduConfig.getApiKey(), baiduConfig.getApiSecret());
    BaiduEventSourceListener baiduEventSourceListener = new BaiduEventSourceListener(this.session);

    List<Message> messages = new ArrayList<>();
    if (StrUtil.isNotBlank(messageContext)) {
        messages = JSONUtil.toList(messageContext, Message.class);
        // 要求最终请求的会话条数必须是奇数,且必须是 U1 A1 U2 A2 U3 A3...的对话形式
        if(messages.size() % 2 != 0) {
            // 原始会话是奇数(加上新的一条输入就变成偶数了)
            int index = 0;
            Iterator<Message> iterator = messages.iterator();
            while (iterator.hasNext()) {
                Message next = iterator.next();
                if(index % 2 == 0) {
                    if(!"user".equals(next.getRole())) {
                        iterator.remove();
                    } else {
                        index++;
                    }
                } else {
                    if(!"assistant".equals(next.getRole())) {
                        iterator.remove();
                    } else {
                        index++;
                    }
                }
            }
        }
        if(messages.size() >= 10) {
            messages.remove(0);
            messages.remove(1);
        }
        Message currentMessage = Message.builder().content(msg).role(Message.Role.USER).build();
        messages.add(currentMessage);
    } else {
        if(promptData != null && ValidU.isNotEmpty(promptData.get("description"))) {
            String prompt = (String) promptData.get("description");
            msg = "请按以下要求和我对话:" + prompt + "(如果前面的提示词中漏掉说明返回的语音,请默认使用中文返回结果即respond in Chinese)。\n我:" + msg;
        }
        Message currentMessage = Message.builder().content(msg).role(Message.Role.USER).build();
        messages.add(currentMessage);
    }

    List<BaiduChatMessage> baiduChatMessages = messages.stream().map(x -> {
        BaiduChatMessage baiduChatMessage = new BaiduChatMessage();
        BeanUtil.copyProperties(x, baiduChatMessage);
        return baiduChatMessage;
    }).collect(Collectors.toList());

    ErnieBotTurboStreamParam postParam = ErnieBotTurboStreamParam.builder()
            .user_id(this.uid)
            .messages(baiduChatMessages)
            .build();

    baiduService.ernieBotTurboStream(postParam, baiduEventSourceListener);
    MessageLocalCache.CACHE.put(uid, JSONUtil.toJsonStr(messages), MessageLocalCache.TIMEOUT);
}

// 该方法是通过流的方式请求API,大模型每生成一些字符,就会通过流的方式相应给客户端,
// 我们是在 BaiduEventSourceListener.java 的 onEvent 方法中获取大模型响应的数据,其中data就是具体的数据,
// 我们获取到数据之后,就可以通过 SSE/webscocket 的方式实时相应给前端页面展示
public void ernieBotTurboStream(ErnieBotTurboStreamParam param, EventSourceListener eventSourceListener) {
    if (Objects.isNull(eventSourceListener)) {
        log.error("参数异常:EventSourceListener不能为空");
        throw new RuntimeException("参数异常:EventSourceListener不能为空");
    }
    if (param == null) {
        log.error("参数异常:param不能为空");
        throw new RuntimeException("参数异常:param不能为空");
    }
    if (!param.isStream()) {
        param.setStream(true);
    }
    try {
        EventSource.Factory factory = EventSources.createFactory(this.okHttpClient);
        ObjectMapper mapper = new ObjectMapper();
        String fullChatUrl = SpringU.getBean(BaiduConfig.class).getFullChatUrl();
        String requestBody = mapper.writeValueAsString(param);
        Request request = new Request.Builder()
                .url(fullChatUrl + BaiduConfig.getToken(appKey, secretKey))
                .post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), requestBody))
                .build();
        //创建事件
        EventSource eventSource = factory.newEventSource(request, eventSourceListener);
    } catch (JsonProcessingException e) {
        log.error("请求参数解析是失败!", e);
        throw new RuntimeException("请求参数解析是失败!", e);
    }
}
  • 将文心一言返回的消息推送给用户
@Slf4j
public class BaiduEventSourceListener extends EventSourceListener {

    private Session session;

    public BaiduEventSourceListener(Session session) {
        this.session = session;
    }

    @SneakyThrows
    @Override
    public void onOpen(EventSource eventSource, Response response) {
        log.info("baidu建立sse连接...");
        session.getBasicRemote().sendText("{\"role\": \"assistant\"}");
    }

    @SneakyThrows
    @Override
    public void onEvent(EventSource eventSource, String id, String type, String data) {
        // {"id":"as-jddwwxm2j3","object":"chat.completion","created":1712913324,"sentence_id":0,"is_end":false,"is_truncated":false,"result":"你好!","need_clear_history":false,"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3}}
        // {"id":"as-jddwwxm2j3","object":"chat.completion","created":1712913325,"sentence_id":1,"is_end":false,"is_truncated":false,"result":"有什么我可以帮助你的吗?","need_clear_history":false,"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3}}
        // {"id":"as-jddwwxm2j3","object":"chat.completion","created":1712913325,"sentence_id":2,"is_end":true,"is_truncated":false,"result":"","need_clear_history":false,"usage":{"prompt_tokens":1,"completion_tokens":8,"total_tokens":9}}
        log.info("baidu返回数据:{}", data);
        String uid = session.getPathParameters().get("uid");
        ObjectMapper mapper = new ObjectMapper();
        // 读取Json
        ErnieBotTurboResponse completionResponse = mapper.readValue(data, ErnieBotTurboResponse.class);
        Message deltaMessage = Message.builder()
                .content(completionResponse.getResult())
                .build();
        String delta = mapper.writeValueAsString(deltaMessage);
        session.getBasicRemote().sendText(delta);

        // 缓存返回消息
        if(!"assistant".equals(deltaMessage.getRole()) && deltaMessage.getContent() != null && !"".equals(deltaMessage.getContent())) {
            StringBuffer msgBuffer = MessageBackLocalCache.CACHE.get(uid);
            if(msgBuffer == null) {
                msgBuffer = new StringBuffer();
                MessageBackLocalCache.CACHE.put(uid, msgBuffer);
            }
            msgBuffer.append(deltaMessage.getContent());
        }
    }

    @SneakyThrows
    @Override
    public void onClosed(EventSource eventSource) {
        log.info("baidu关闭sse连接...");
        session.getBasicRemote().sendText("[DONE]");

        // 记录返回消息
        String uid = session.getPathParameters().get("uid");
        StringBuffer msgBuffer = MessageBackLocalCache.CACHE.get(uid);
        if(msgBuffer != null) {
            JdbcTemplate jdbcTemplate = SpringU.getBean(JdbcTemplate.class);
            List<Map<String, Object>> list = jdbcTemplate.queryForList("select id, create_time " +
                    " from chat_msg_his where user_id = ? and msg_ai is null order by id desc limit 1", uid);
            if(ValidU.isNotEmpty(list)) {
                Map<String, Object> chatInfo = list.get(0);
                Date createTime = (Date) chatInfo.get("create_time");
                Date now = new Date();
                long useTime = (now.getTime() - createTime.getTime()) / 1000;
                jdbcTemplate.update("update chat_msg_his set msg_ai = ?, update_time = ?, use_time = ? where id = ?",
                        msgBuffer.toString(), now, useTime, chatInfo.get("id"));
            }

            // 需要保留原始会话
            String messageStr = (String) MessageLocalCache.CACHE.get(uid);
            List<Message> messages = JSONUtil.toList(messageStr, Message.class);
            messages.add(Message.builder().role("assistant").content(msgBuffer.toString()).build());
            MessageLocalCache.CACHE.put(uid, JSONUtil.toJsonStr(messages), MessageLocalCache.TIMEOUT);
        }
        MessageBackLocalCache.CACHE.remove(uid);
    }

    @SneakyThrows
    @Override
    public void onFailure(EventSource eventSource, Throwable t, Response response) {
        session.getBasicRemote().sendText("机器人出小差了~");
        String uid = session.getPathParameters().get("uid");
        MessageBackLocalCache.CACHE.remove(uid);
        if (Objects.isNull(response)) {
            return;
        }
        ResponseBody body = response.body();
        if (Objects.nonNull(body)) {
            log.error("baidu sse连接异常data:{},异常:{}", body.string(), t);
        } else {
            log.error("baidu sse连接异常data:{},异常:{}", response, t);
        }
        eventSource.cancel();
    }
}

效果展示

image.png

相关推荐

  1. [AI 模型]

    2024-06-14 22:30:03       24 阅读
  2. 模型api 请求错误码 一览表

    2024-06-14 22:30:03       26 阅读
  3. 用python实现调用

    2024-06-14 22:30:03       54 阅读
  4. 下三角矩阵

    2024-06-14 22:30:03       28 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-14 22:30:03       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-14 22:30:03       101 阅读
  3. 在Django里面运行非项目文件

    2024-06-14 22:30:03       82 阅读
  4. Python语言-面向对象

    2024-06-14 22:30:03       91 阅读

热门阅读

  1. 深入解析JVM的GC过程

    2024-06-14 22:30:03       31 阅读
  2. cocos入门11:生命周期

    2024-06-14 22:30:03       29 阅读
  3. Docker相关命令

    2024-06-14 22:30:03       27 阅读
  4. Linux下安装MySQL

    2024-06-14 22:30:03       21 阅读
  5. PostgreSQL 的内置函数

    2024-06-14 22:30:03       29 阅读