Spring AI教程(二)Chat API之基于数据库的多Key轮询

基于数据库的多Key轮询

 在之前的文章中我们所使用的Key都是一个,但事实上,官方对Key会有一定的请求限制,在实际业务场景下,我们也不可能通过一个Key来保证我们的系统稳定运行,因为一旦超过请求限制,就会出现无法请求AI的情况,这时,就需要考虑实现一种多Key轮询进行请求,从而保证我们的系统不会出现因为达到请求限制而无法运行的情况。

 可惜的是,Spring AI目前还不支持多Key轮询的方式来调用大语言模型,因此需要我们自己实现。本篇将结合我对Spring AI的源码理解,来实现一个基于数据库的多Key轮询的接口。

6.1 核心类 OpenAiApi

 实现多Key轮询的方式并不难,我们将API和Key的信息存储在数据库中,每次发起请求时,通过向数据库中查询API和Key的数据,手动构建一个ChatClient或StreamChatClient进行调用即可。拿OpenAI为例,就是要手动构建OpenAiChatClient对象,因为OpenAiChatClient实现了ChatClientStreamChatClient接口。

 通过观察OpenAiChatClient的构造方法,发现有一个核心类:OpenAiApi,该类的对象创建恰好就需要API和Key。

 因此我们很容易想通创建OpenAiChatClient的流程:

  • 从数据库中查询API和Key的信息;
  • 利用API和Key构建OpenAiApi对象;
  • 通过OpenAiApi对象再构建OpenAiChatClient;
  • 再根据实际业务场景选择ChatClient和StreamChatClient进行调用;

6.2 环境准备

 为便于演示,我新建了一个spring-ai-key-polling-demo的模块。

  • 数据库使用的是Pgvector,它是Postgresql的扩展,可作为向量数据库,当然,本节还未涉及到向量数据库的使用,因此可以根据自己的喜好选择一个数据库;
  • ORM框架:Spring Data JPA,演示起来比较方便,可根据喜好选择一款ORM框架

核心依赖:

<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
</dependency>
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>

目录结构如下:

6.3 多Key轮询的基本实现

(1) 定义实体类

 这里我定义了一个KeyInfo用来存储我们的Key和API信息,属性较为简单,实际项目中需要结合实际的业务场景添加一些额外的属性,如是否禁用、创建时间等等。

package com.ningning0111.entity;

import jakarta.annotation.Nullable;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table
public class KeyInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String key;
    @Column(nullable = false,columnDefinition = "VARCHAR(50) DEFAULT 'https://api.openai.com'")
    private String api;

    // 描述这个Key干嘛的 可空
    private String description;

}

(2) 定义Repository

 由于使用的是JPA,这里还需要创建Repository,对于Mybatis,需要创建Mapper。

package com.ningning0111.repository;

import com.ningning0111.entity.KeyInfo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface KeyInfoRepository extends JpaRepository<KeyInfo,Long> {
}

(3) 配置文件

 接着我们需要配置application.yml文件:

server:
  port: 8321

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    username: postgres
    password: postgres
    url: jdbc:postgresql://localhost/demo
  jpa:
    hibernate:
      # 自动创建表
      ddl-auto: create
    show-sql: true
    open-in-view: true
    
  autoconfigure:
    exclude: org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration

 因为我们的Key和API都是从数据库中获取的,因此配置文件中就不需要配置了,为了防止相关属性因为无法自动配置而造成Spring启动不起来,就需要将OpenAi的自动装配排除。

(3) Service类
package com.ningning0111.service;

import com.ningning0111.entity.KeyInfo;
import com.ningning0111.repository.KeyInfoRepository;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.StreamingChatClient;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Random;

@RequiredArgsConstructor
@Service
public class ChatService {
    private final KeyInfoRepository repository;

    // 初始化一些key 这些key应该是可调用的
    @PostConstruct
    public void initData() {
        KeyInfo keyInfo = new KeyInfo();
        keyInfo.setKey("sk-W9kYxxxxxxxxxxxxxxxxxxxfAd460353Dc7a");
        keyInfo.setApi("https://api.mnzdna.xyz");
        keyInfo.setDescription("测试API和Key,请填写自己的Key");
        repository.save(keyInfo);
    }

    // 阻塞式
    public ChatClient getChatClient() {
        OpenAiApi openAiApi = randomGetApi();
        assert openAiApi != null;
        return new OpenAiChatClient(openAiApi);
    }

    // 流式
    public StreamingChatClient getStreamChatClient() {
        OpenAiApi openAiApi = randomGetApi();
        assert openAiApi != null;
        return new OpenAiChatClient(openAiApi);
    }

    // 随机获取一个OpenAiApi
    private OpenAiApi randomGetApi(){
        List<KeyInfo> keyInfoList = repository.findAll();
        // 如果数据库中没有KeyInfo对象,则返回null
        if (keyInfoList.isEmpty()) {
            return null;
        }
        // 随机选择一个KeyInfo对象
        Random random = new Random();
        KeyInfo randomKeyInfo = keyInfoList.get(random.nextInt(keyInfoList.size()));
        return new OpenAiApi(randomKeyInfo.getApi(),randomKeyInfo.getKey());
    }
}

(4) Controller类
package com.ningning0111.controller;

import com.ningning0111.service.ChatService;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class ChatController {
    private final ChatService chatService;
    @GetMapping("/demo")
    public String chatDemo(String prompt){
        ChatClient chatClient = chatService.getChatClient();
        String response = chatClient.call(prompt);
        return response;
    }
}

效果如下:

6.4 多Key轮询的优化

 上面的代码是多Key轮询实现的简单方式,但是,由于每次对话都需要从数据库中进行查询,再加上查询到结果后还需要发起网络请求等待响应,因此当Key的数量特别多时,会大大增加响应的时间。为此,我们有必要对其进行优化。这里简单介绍下我使用过的优化方式:

  • 容器启动时,先将数据库里的数据加载到内存中,即:使用List存储;
  • 针对实体KeyInfo的增删改操作,每次操作完成后重新从数据库中加载数据到内存中;
  • 每次轮询都是基于List进行的,而不再是基于数据库;
  • 当轮询到的Key失效时,将其从数据库中删除(删除后会重新加载)
  • 每次轮询前对List进行判空,若空,则重新从数据库加载数据

 具体的实现代码就不展示了,实现逻辑并不难,并且优化策略也有很多,可以结合实际情况实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/564976.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Laravel + ThinkPhP 海报生成

相关资料: 小程序 | EasyWeChat 二维码生成 public function test(){$config [app_id > appid,secret > secret,];$app Factory::miniProgram($config);$response $app->app_code->get(pages/index/index, [//二维码大小width > 150,//二维码颜色line_color…

React自定义Hook函数:高效组件开发的秘密武器

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

恶心透了的小日子,害人终害己,国货呼吁关注抵制日本核废水排放

​|日本排放核废水 日本政府决定将福岛第一核电站的核污染水经过处理后排放入海&#xff0c;这一决定引发了多方面的担忧和反对&#xff0c;特别是在周边国家&#xff0c;包括中国和韩国。关于日本排放核污染水这一新闻事件&#xff0c;我们必须首先认识到&#xff0c;核能利用…

ES源码五:写操作流程(从Es到底层Luence,全网最细的一篇,全是硬货)

今天是玩转es的一天 创建索引 写入文档 入口BaseRestHandler BaseRestHandler是Rest请求的入口&#xff0c;你可以理解为spring mvc里面的controller一样prepareRequest是一个抽象方法&#xff0c;实际上是由各种Rest*Action来重写的&#xff0c;比如这里我们是对索引文档的处…

【C语言】strstr函数刨析-----字符串查找

目录 一、strstr 函数介绍 ✨函数头文件&#xff1a; ✨函数原型&#xff1a; ✨函数解读 ✨功能演示 二、函数的原理以及模拟实现 ✨函数原理 ✨函数的模拟实现 三、strstr函数的注意事项 四、共勉 一、strstr 函数介绍 strstr函数是在一个字符串中查找另一个字符…

K8s: Ingress对象, 创建Ingress控制器, 创建Ingress资源并暴露服务

Ingress对象 1 &#xff09;概述 Ingress 是对集群中服务的外部访问进行管理的 API 对象&#xff0c;典型的访问方式是 HTTPIngress-nginx 本质是网关&#xff0c;当你请求 abc.com/service/a, Ingress 就把对应的地址转发给你&#xff0c;底层运行了一个 nginx但 K8s 为什么不…

GitOps 和 DevOps 有什么区别?

GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab &#xff1a;https://gitlab.cn/install?channelcontent&utm_sourcecsdn 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署…

C语言:数据结构(单链表)

目录 1. 链表的概念及结构2. 实现单链表3. 链表的分类 1. 链表的概念及结构 概念&#xff1a;链表是一种物理存储结构上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表的指针链接次序实现的。 链表的结构跟火车车厢相似&#xff0c;淡季时车次的车厢会相应…

六、项目发布 -- 4. 电子书详情页API开发、电子书列表API开发

电子书详情页API的编写 同理如下app.get中路由、回调&#xff1b;回调中要连接数据库、接收前端传过来的值、到数据库中做查询&#xff0c;然后回调&#xff08;如果回调失败返回什么JSON&#xff0c;如果回调成功返回什么JSON&#xff09;&#xff1b;最后千万别忘记了关闭数…

mapbox控制3D模型旋转

贴个群号 WebGIS学习交流群461555818&#xff0c;欢迎大家 效果 原理与源码 获取角度&#xff0c;然后一直更改角度&#xff0c;角度到达180度后赋值成-180度&#xff0c;然后转到开始获取的角度的角度的时候就停止旋转 function rotateModel(layerID){let bearing map.get…

2024.4.21周报

目录 摘要 Abstract 文献阅读&#xff1a;Next Item Recommendation with Self-Attentive Metric Learning 问题及方法 论文贡献 方法论 序列感知的推荐系统 神经注意模型 模型&#xff1a;ATTREC 序列推荐 基于Self-Attention的用户短期兴趣建模 用户长期兴趣建模…

卷积神经网络CNN入门

卷积神经网络应用领域 因为卷积神经网络主要应用场景就是计算机视觉任务&#xff0c;因此有必要简单介绍一下CV领域发展情况&#xff1a; 可以发现&#xff0c;在 ImageNet 图像数据集中分析图像的错误率十年间已经被深度学习给降低到了比人类&#xff08;HuMan&#xff09;识…

【matlab 代码的python复现】 Matlab实现的滤波器设计实现与Python 的库函数相同实现Scipy

实现一个IIR滤波器的设计 背景 Matlab 设计的滤波器通常封装过于完整,虽然在DSP中能够实现更多功能的滤波器设计但是很难实现Python端口的实现。 我们以一段原始的生物电信号EEG信号进行处理。 EEG信号 1.信号获取 EEG信号通常通过头皮电极,经过多通道采样芯片采样,将获…

35K的鸿蒙音视频开发岗位面经分享~

一个月前&#xff0c;阿里云在官网音视频终端 SDK 栏目发布适配 HarmonyOS NEXT 的操作文档和 SDK&#xff0c;官宣 MediaBox 音视频终端 SDK 全面适配 HarmonyOS NEXT。 此外&#xff0c;阿里云播放器 SDK 也在华为开发者联盟官网鸿蒙生态伙伴 SDK 专区同步上线&#xff0c;面…

OpenTelemetry-1.介绍

目录 1.是什么 2.为什么使用 OpenTelemetry 3.数据类型 Tracing Metrics Logging Baggage 4.架构图 5.核心概念 6.相关开源项目 ​编辑 7.分布式追踪的起源 8.百花齐放的分布式追踪 Zipkin Skywalking Pinpoint Jaeger OpenCensus OpenTracing 9.Openteleme…

「杭州*康恩贝」4月26日PolarDB开源数据库沙龙,开启报名!

4月26日&#xff08;周五&#xff09;&#xff0c;PolarDB开源社区联合康恩贝将共同举办开源数据库技术沙龙&#xff01; 时间&#xff1a;4月26日13:30 地点&#xff1a;浙江省杭州市滨江区滨康路568号康恩贝中心2楼 活动亮点 浙江英诺珐医药有限公司信息经理 朱常青 分享《…

数据结构-二叉树-堆

一、物理结构和逻辑结构 在内存中的存储结构&#xff0c;逻辑结构为想象出来的存储结构。 二、完全二叉树的顺序存储结构 parent (child - 1)/2 leftchild 2*parent 1; rightchild 2*parent 2 上面的顺序结构只适合存储完全二叉树。如果存储&#xff0c;会浪费很多的空…

清华大学:序列推荐模型稳定性飙升,STDP框架惊艳登场

获取本文论文原文PDF&#xff0c;请公众号留言&#xff1a;论文解读 引言&#xff1a;在线平台推荐系统的挑战与机遇 在线平台已成为我们日常生活中不可或缺的一部分&#xff0c;它们提供了丰富多样的商品和服务。然而&#xff0c;如何为用户推荐感兴趣的项目仍然是一个挑战。…

对接浦发银行支付(八)-- 对账接口

一、背景 本文不是要讲述支付服务的对账模块具体怎么做&#xff0c;仅是介绍如何对接浦发银行的对账接口。 也就是说&#xff0c;本文限读取到对账文件的内容&#xff0c;不会进一步去讲述如何与支付平台进行对账。 如果要获取商户的对账单&#xff0c;需要遵循以下步骤&…

使用自购服务器部署RustDesk - 远程桌面服务

服务器官网&#xff1a;雨云 - 新一代云服务提供商 推荐购买宿迁主机&#xff0c;使用NAT网络不购买独立IP&#xff0c;国内主机独立IP价格很贵&#xff0c;这种方式虽然不能省略端口号&#xff0c;但是可以确保访问速度很快&#xff0c;NAT给的10个端口基本够用&#xff1b; …