首页 前端知识 Java调用ChatGPT(基于SpringBoot和Vue)实现连续对话、流式输出和自定义baseUrl

Java调用ChatGPT(基于SpringBoot和Vue)实现连续对话、流式输出和自定义baseUrl

2024-01-30 10:01:16 前端知识 前端哥 956 55 我要收藏

目录

  • 版本更新说明
  • 1. 配置阶段
    • 1.1 依赖引入
    • 1.2 配置application.yml文件
    • 1.3 注解添加
  • 2. 使用
    • 2.1 生成回答
      • 2.1.1 测试
    • 2.2 生成图片
      • 2.2.1 测试
    • 2.3 下载图片
      • 2.3.1 测试
    • 2.4 生成流式回答
      • 2.4.1 流式回答输出到IDEA控制台
      • 2.4.2 流式回答输出到浏览器页面
      • 2.4.3 流式回答结合Vue输出到前端界面
    • 2.5 查询账单和订阅
      • 2.5.1 测试
    • 2.6 自定义baseUrl
    • 2.7 模型(model)
    • 2.8 编辑(edit)
    • 2.9 向量(embeddings)
    • 2.10 转录(transcription)
    • 2.11 翻译(translation)
    • 2.12 自定义OkHttpClient
      • 2.12.1 基于SpringBoot指定OkHttpClient
      • 2.12.2 基于Main方法指定OkHttpClient
    • 2.13 设置系统级提示信息
    • 2.14 计算输入prompt消耗token
    • 2.15 自定义每次请求使用token
  • 3. 扩展
    • 3.1 自定义OpenAiProxyService
  • 4 自定义OpenAi服务端
    • 4.1 介绍
    • 4.2 测试
      • 4.2.1 基于SpringBoot使用OpenAiUtils
      • 4.2.2 基于Main方法使用OpenAiProxyService
  • 5 AI助手展示

源码及更详细的介绍说明参见Git上的README.md文档 https://github.com/asleepyfish/chatgpt

本文Demo(SpringBoot和Main方法Demo均包括)的Git地址:https://github.com/asleepyfish/chatgpt-demo

流式输出结合Vue前端的Demo的Git地址:https://github.com/asleepyfish/chatgpt-vue

后续使用方法和api版本更新均在Github的README.md文档更新,本博客暂停更新~

2023-05-28更新

制作了一个基于本文sdk包的对话网站,可前往http://chatgpt.alpacos.cn/体验。
[注]: 由于本人的服务器的带宽较低,首次加载页面可能会略有些慢,请耐心等待,详细使用说明请查看另一篇博客~ 👇
ChatGPT网页版(基于SpringBoot和Vue)

2023-07-17更新

所有的测试Demo均包括结合Spring框架的(ChatGPTController类中)和main方法使用(MainTest类中)Demo地址:https://github.com/asleepyfish/chatgpt-demo

版本更新说明

  • 1.1.5 增加查询账单功能billingUsage(单位:美元),可以选择传入开始和结束日期查询(最多100天),或者不传入参,此时表示查询所有日期账单。
  • 1.1.6 增加自定义OpenAiProxyService功能,支持单个SpringBoot中添加多个OpenAiProxyService实例,每个实例可以拥有个性化的参数;查询账单功能优化。
  • 1.2.0 增加subscription方法(查询订阅信息,包括订阅到期日和账号额度等信息,但是没有使用量情况,使用通过billingUsage方法查询使用量),增加billing方法,整合了subscriptionbillingUsage方法,出参包括订阅到期日、额度、使用量、余量等信息。增加对内部cache的多种操作,包括获取,赋值等操作。
  • 1.2.1 billing方法中出参dueDate取值逻辑修改,ChatGPTProperties类支持build链式创建对象。
  • 1.3.0 新增以下方法,每种方法均包含多种重载方法,具体使用请参考:https://github.com/asleepyfish/chatgpt-demo
    • listModelsgetModel
    • edit
    • embeddings
    • transcriptiontranslation
    • createImageEditcreateImageVariation
    • listFilesuploadFiledeleteFileretrieveFileretrieveFileContent
    • createFineTunecreateFineTuneCompletionlistFineTunesretrieveFineTunecancelFineTunelistFineTuneEventsdeleteFineTune
    • createModeration
  • 1.3.1 支持自定义baseUrl,默认为 https://api.openai.com/ ,配置参数在ChatGPTProperties类中,可通过application.yml配置。
  • 1.3.2 修复自定义baseUrl后无法访问bug,自定义baseUrl必须以/结尾。新增自定义属于自己的baseUrl示例。
  • 1.3.3 支持自定义OkHttpClient,解决OkHttpClient默认并发数无法指定问题,解决部分Proxy登录需要账号密码问题,详细介绍见2.12节。
  • 1.3.4 修复指定baseUrl后未切换baseUrlbug,1.3.3版本不建议使用。
  • 1.3.5 支持SpringBoot3引用(SpringBoot3所需JDK最低是17),代码Demo见https://github.com/asleepyfish/chatgpt-demo,分支为dev-springboot3
  • 1.3.6 新增以下功能
    • ① 新增设置系统级别提示功能
    • ② 新增计算输入prompt消耗token功能
    • ③ 新增备用token,可自定义每次请求使用token

1. 配置阶段

1.1 依赖引入

pom.xml中引入依赖(当前最新版本为1.3.6,可前往Github页面查看当前最新版本)

        <dependency>
            <groupId>io.github.asleepyfish</groupId>
            <artifactId>chatgpt</artifactId>
            <version>1.3.6</version>
        </dependency>

1.2 配置application.yml文件

application.yml文件中配置相关参数(Optional为可选参数)

参数解释
token申请的API KEYS
proxy-host (Optional)代理的ip
proxy-port (Optional)代理的端口
model (Optional)model可填可不填,默认即text-davinci-003
chat-model (Optional)可填可不填,默认即gpt-3.5-turbo (ChatGPT当前最强模型,生成回答使用的就是这个模型)
retries (Optional)指的是当chatgpt第一次请求回答失败时,重新请求的次数(增加该参数的原因是因为大量访问的原因,在某一个时刻,chatgpt服务将处于无法访问的情况,不填的默认值为5)
session-expiration-time (Optional)(单位(min))为这个会话在多久不访问后被销毁,这个值不填的时候,即表示所有问答处于同一个会话之下,相同user的会话永不销毁(增加请求消耗)
base-url (Optional)默认为 https://api.openai.com/ ,可不填
token-strategy-impl (Optional)默认策略为RandomTokenStrategy,可不填
alter-tokens (Optional)备选token,可以和token-strategy-impl结合,每次请求按策略分配token,可不填

例:

chatgpt:
  token: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx #必填
#  proxy-host: 127.0.0.1 #需要代理时必填
#  proxy-port: 7890 #需要代理时必填
#  model: text-davinci-003 #可选
#  chat-model: gpt-3.5-turbo #可选
#  retries: 10 #可选,默认为5
#  session-expiration-time: 30 #可选,不填则会话永不过期
#  base-url: https://apps.ichati.cn/1d6f32f8-b59d-46f8-85e9-7d434bxxxxxx/ #可选,默认为https://api.openai.com/
#  token-strategy-impl: io.github.asleepyfish.strategy.DefaultTokenStrategy #可选,默认为RandomTokenStrategy(随机)
#  alter-tokens: #可选,备选tokens,可以和token-strategy-impl结合,每次请求按策略分配token
#    - sk-xxx2
#    - sk-xxx3

其中token、proxy-host、proxy-port是必填的

上面的session-expiration-time参数很重要,是用来表示这个会话在多久不访问后被销毁,从而实现联系上下文的连续对话。

实现方式是通过ChatCompletionRequest中的user来区分某个会话,而session-expiration-time表示这个会话在多久不访问后被销毁。

如果这里看不懂请看2.1节示例

1.3 注解添加

启动类上加入图中的注解则将服务注入到Spring中。
在这里插入图片描述

2. 使用

2.1 生成回答

提供了工具类OpenAiUtils,里面提供了相关方法进行调用。
其中最简单的使用方法是:

OpenAiUtils.createChatCompletion(content);// 不建议使用

入参content即输入的问题的字符串。但是不建议使用。

这里建议使用下面的方式,通过传入user的值,再结合session-expiration-time参数,可以实现指定某次会话,或者某个用户的连续对话。

OpenAiUtils.createChatCompletion(content, user);// 建议使用

还提供一个通用的静态方法是

public static List<String> createChatCompletion(ChatCompletionRequest chatCompletionRequest) {...}

入参ChatCompletionRequest 里包含模型的一些可调参数。

OpenAiUtils类中还提供了多个可供选择的静态方法,可以自行查看。

上述方法的返回参数是一个list,是因为调整参数返回答案n可以一次性返回多条不同的解答(nChatCompletionRequest类中一个参数)。

2.1.1 测试

测试代码:

@PostMapping("/chatTest")
public List<String> chatTest(String content) {
    return OpenAiUtils.createChatCompletion(content, "testUser");
}

Post请求

入参输入:Java序列化的方式

返回结果:

[
  "\n\nJava序列化是将Java对象转换为字节序列的过程,以便在网络上传输或将其保存到磁盘上。Java提供了两种序列化方式:\n\n1. 基于Serializable接口的序列化\n\nSerializable接口是Java提供的一个标记接口,用于标记一个类可以被序列化。如果一个类实现了Serializable接口,那么它的所有非瞬态字段都会被序列化。序列化的过程可以通过ObjectOutputStream类来实现,反序列化的过程可以通过ObjectInputStream类来实现。\n\n2. 基于Externalizable接口的序列化\n\nExternalizable接口也是Java提供的一个标记接口,用于标记一个类可以被序列化。与Serializable接口不同的是,Externalizable接口需要实现writeExternal和readExternal方法,这两个方法分别用于序列化和反序列化。在序列化的过程中,只有被writeExternal方法显式写入的字段才会被序列化,而在反序列化的过程中,只有被readExternal方法显式读取的字段才会被反序列化。\n\n总的来说,基于Serializable接口的序列化更加简单,但是它会序列化所有非瞬态字段,包括一些不需要序列化的字段,而基于Externalizable接口的序列化可以更加灵活地控制序列化的过程。"
]

再次输入:有没有更加高效的序列化框架

返回结果:

[
  "是的,Java中有很多高效的序列化框架,以下是一些常用的序列化框架:\n\n1. Protobuf\n\nProtobuf是Google开发的一种高效的序列化框架,它可以将结构化数据序列化为二进制格式,支持多种编程语言。相比于Java自带的序列化方式,Protobuf序列化后的数据更小,解析速度更快。\n\n2. Kryo\n\nKryo是一个快速、高效的Java序列化框架,它可以将Java对象序列化为字节数组,支持多种数据类型。Kryo序列化的速度比Java自带的序列化方式快很多,序列化后的数据也更小。\n\n3. FST\n\nFST是一个高性能的Java序列化框架,它可以将Java对象序列化为字节数组,支持多种数据类型。FST序列化的速度比Java自带的序列化方式快很多,序列化后的数据也更小。\n\n4. Avro\n\nAvro是一个高效的数据序列化系统,它可以将结构化数据序列化为二进制格式,支持多种编程语言。Avro序列化后的数据比Java自带的序列化方式更小,解析速度也更快。\n\n总的来说,这些高效的序列化框架都比Java自带的序列化方式更快、更小、更灵活,可以根据具体的需求选择合适的框架。"
]

可以看出上述两次问答是在一次会话中的,而前面所说的参数session-expiration-time即这个user所代表的会话多久没被继续访问时的销毁时间。单位(min)

2.2 生成图片

最简单的使用方式是

OpenAiUtils.createImage(prompt);

入参表示生成图片的描述文字,还提供了一个通用的静态方法

public static List<String> createImage(CreateImageRequest createImageRequest) {...}

入参CreateImageRequest中有一些可以使用的参数,其中n表示生成图片的数量,responseFormat表示生成图片的格式,格式中分为urlb64_json两种,如果希望返回的是url,则返回的url会在生成一个小时后消失,默认值是url

2.2.1 测试

测试代码

    @Test
    public void testGenerateImg() {
        OpenAiUtils.createImage("英短").forEach(System.out::println);
    }

结果
默认情况下会生成一个url,点击去就可以看到图片。
在这里插入图片描述

2.3 下载图片

在3.2的基础上做了优化,直接使用responseFormatb64_json然后解析成图片返回。简单使用方式如下:

OpenAiUtils.downloadImage(prompt, response);

通用方式如下:

public static void downloadImage(CreateImageRequest createImageRequest, HttpServletResponse response) {...}

CreateImageRequest对象中设置的返回参数n大于1时,会将图片打包成一个zip包返回,当n等于1时直接返回图片。

2.3.1 测试

测试代码

@RestController
public class ChatGPTController {
    @GetMapping("/downloadImage")
    public void downloadImage(String prompt, HttpServletResponse response) {
        OpenAiUtils.downloadImage(prompt, response);
    }
}

发送get请求,然后选择Send and Download
在这里插入图片描述

我用的get 工具是idea里面下载的插件Fast Request的,用Postman也是可以的,但是要选择 Send and Download,上图中绿色的箭头是Send,蓝色的是Send and Download。

在这里插入图片描述

2.4 生成流式回答

生成流式回答的方法是OpenAiUtilscreateStreamChatCompletion方法,本工具类重载了同名的多个参数的方法,其中最通用的方法是

public static void createStreamChatCompletion(ChatCompletionRequest chatCompletionRequest, OutputStream os) {...}

最简单的方法是

public static void createStreamChatCompletion(String content) {...}

其中的content即本次对话的问题。

这里需要主义的是,上述第一个方法中的OutputStream os其实是一个必传的对象,上述的最简单的方法实际上是默认传递的System.out这个os对象,也就是将流式问答的结果显示到IDEA的控制台。

如果需要将流式问答的结果显示到其他界面可以自发的传入OutputStream os对象,这里有一个简便的方法是

public static void createStreamChatCompletion(String content, OutputStream os) {...}

只需要输入问题,和输出流对象即可。

下面将举例具体说明。(本文所有Demo的示例地址: https://github.com/asleepyfish/chatgpt-demo)

2.4.1 流式回答输出到IDEA控制台

代码如下:

@GetMapping("/streamChat")
public void streamChat(String content) {
    // OpenAiUtils.createStreamChatCompletion(content, System.out);
    // 下面的默认和上面这句代码一样,是输出结果到控制台
    OpenAiUtils.createStreamChatCompletion(content);
}

然后使用Postman或者其他可以发送Get请求的工具发送请求。

本次测试的结果如下面的Gif图所示

在这里插入图片描述

2.4.2 流式回答输出到浏览器页面

上述的方法中输出流传入的是System.out对象,该对象实际上就是一个PrintStream对象,会把输出结果展示到控制台。

如果需要将输出结果在浏览器展示,可以从前端传入一个HttpServletResponse response对象,拿到这个response以后将response.getOutputStream()这个输出流对象传入createStreamChatCompletion方法的入参中。同时,为了避免结果输出到浏览器产生乱码和支持流式输出,需要ContentTypeCharacterEncoding

具体代码如下:

@GetMapping("/streamChatWithWeb")
public void streamChatWithWeb(String content, HttpServletResponse response) throws IOException {
    // 需要指定response的ContentType为流式输出,且字符编码为UTF-8
    response.setContentType("text/event-stream");
    response.setCharacterEncoding("UTF-8");
    // 禁用缓存
    response.setHeader("Cache-Control", "no-cache");    
    OpenAiUtils.createStreamChatCompletion(content, response.getOutputStream());
}

测试结果过程的Gif图如下所示:

在这里插入图片描述

2.4.3 流式回答结合Vue输出到前端界面

调用的后端方法同2.4.2节方法streamChatWithWeb,前端只需要在界面传入问题,点击提问按钮即可返回结果流式输出到文本框中。

测试结果过程的Gif图如下所示:

在这里插入图片描述
Vue3 Demo的Git地址在文章开头有~

2.5 查询账单和订阅

查询账单提供了两个方法,金额单位均为美元(USD),且均未对小数位截取,可以根据需要自行选择保留结果小数点位数。

第一个是可以传入开始和结束日期,按照指定日期区间查询的方法:

public String billingUsage(String startDate, String endDate) {...}

其中startDateendDate区间范围不超过100天。

第二个方法是一个入参为可变参数的方法,当不传入参时,查询从2023年1月1日距今的账单的方法,如果有人的订阅日早于2023年1月1日可以传入自定义账单起始日期:

public String billingUsage(String... startDate) {...}

查询订阅提供了一个方法,这个方法的出参中包括了订阅到期日,总额度等信息:

public Subscription subscription() {...}

由于查询总额度和查询使用量是两个接口,这里封装了一个方法来将几个比较有用的参数统一返回的方法,方法如下:

public Billing billing(String... startDate) {...}

这个方法的入参也是一个可变入参,不传参时,startDate默认为2023-01-01,如果账单开始日早于该天,可以传入指定的startDate。出参Billing中有四个参数:dueDate(额度到期日),total(额度总量),usage(已使用量),balance(余额)。

2.5.1 测试

测试代码如下:

@GetMapping("/billing")
public void billing() {
    String monthUsage = OpenAiUtils.billingUsage("2023-04-01", "2023-05-01");
    log.info("四月使用:{}美元", monthUsage);
    String totalUsage = OpenAiUtils.billingUsage();
    log.info("一共使用:{}美元", totalUsage);
    String stageUsage = OpenAiUtils.billingUsage("2023-01-31");
    log.info("自从2023/01/31使用:{}美元", stageUsage);
    Subscription subscription = OpenAiUtils.subscription();
    log.info("订阅信息(包含到期日期,账户总额度等信息):{}", subscription);
    // dueDate为到期日,total为总额度,usage为使用量,balance为余额
    Billing totalBilling = OpenAiUtils.billing();
    log.info("历史账单信息:{}", totalBilling);
    // 默认不传参的billing方法的使用量usage从2023-01-01开始,如果用户的账单使用早于该日期,可以传入开始日期startDate
    Billing posibleStartBilling = OpenAiUtils.billing("2022-01-01");
    log.info("可能的历史账单信息:{}", posibleStartBilling);
}

测试结果如下:

四月使用:0.9864320000000001美元
一共使用:2.2074280000000002美元
自从2023/01/31使用:2.2074280000000001美元
订阅信息(包含到期日期,账户总额度等信息):Subscription(object=billing_subscription, hasPaymentMethod=false, canceled=false, canceledAt=null, delinquent=null, accessUntil=1688169600, softLimit=66667, hardLimit=83334, systemHardLimit=83334, softLimitUsd=4.00002, hardLimitUsd=5.00004, systemHardLimitUsd=5.00004, plan=Plan(title=Explore, id=free), accountName=Leo Mikey, poNumber=0, billingEmail=null, taxIds=null, billingAddress=null, businessAddress=null)
历史账单信息:Billing(dueDate=2023-07-01, total=5.00004, usage=2.2074280000000002, balance=2.7926119999999998)
可能的历史账单信息:Billing(dueDate=2023-07-01, total=5.00004, usage=2.2074280000000002, balance=2.7926119999999998)

2.6 自定义baseUrl

由于部分原因,用户可以选择搭建好的代理服务作为baseUrl,而不需要使用官方的https://api.openai.com/

这部分可以和第4节放在一起看

如果用户基于SpringBoot使用自定义baseUrl,可以参考下面的application.yml,配置chatgpt.base-url,如果不配置,则表示默认使用官方的https://api.openai.com/

chatgpt:
  token: sk-xxxxxxxxxxxxxxxxxx
  proxy-host: 127.0.0.1 #可选
  proxy-port: 7890 #可选
  model: text-davinci-003 #可选
  chat-model: gpt-3.5-turbo #可选
  retries: 10 #可选,默认为5
  session-expiration-time: 30 #可选,不填则会话永不过期
  base-url: https://openai.api2d.net/ #可选,默认为https://api.openai.com/

然后相关调用代码如下:

@PostMapping("/baseUrl")
public void baseUrl() {
    // 先在application.yml中配置chatgpt.base-url
    System.out.println("models列表:" + OpenAiUtils.listModels());
}

如果用户不想和Spring集成,则可以使用main方法调用的方式。

下面为了测试方便性,均以main方法做示例,结合SpringBoot使用OpenAiUtils调用的方法请参考github上的demo示例。

main方法中直接使用请参考第3节扩展部分自定义OpenAiProxyService

如下所示:

public static void main(String[] args) {
    ChatGPTProperties properties = ChatGPTProperties.builder().token("sk-xxx")
        .proxyHost("127.0.0.1")
        .proxyPort(7890)
        // 自定义baseUrl,若不需要自定义baseUrl,下一行可以去掉
        .baseUrl("https://openai.api2d.net/")
        .build();
    OpenAiProxyService openAiProxyService = new OpenAiProxyService(properties);
    System.out.println("models列表:" + openAiProxyService.listModels());
}

其中自定义了baseUrl为 https://openai.api2d.net/ ,并调用了listModels方法。

2.7 模型(model)

提供了两个与模型相关的方法:

// 列出全部models
List<Model> listModels() {...}
// 获取指定model信息
Model getModel(String model) {...}

测试代码如下:

ChatGPTProperties properties = ChatGPTProperties.builder().token("sk-xxx")
    .proxyHost("127.0.0.1")
    .proxyPort(7890)
    .build();
OpenAiProxyService openAiProxyService = new OpenAiProxyService(properties);
System.out.println("models列表:" + openAiProxyService.listModels());
System.out.println("=============================================");
System.out.println("text-davinci-003信息:" + openAiProxyService.getModel("text-davinci-003"));

测试结果(截取如下):

models列表:[Model(id=whisper-1, object=model, ownedBy=openai-internal, permission=[Permission(id=modelperm-KlsZlfft3Gma8pI6A8rTnyjs,.....
=============================================
text-davinci-003信息:Model(id=text-davinci-003, object=model, ownedBy=openai-internal, permission=[Permission(id=modelperm-jepinXYt59ncUQrjQEIUEDyC, object=model_permission, created=1688551385, allowCreateEngine=false, allowSampling=true, allowLogProbs=false, allowSearchIndices=false, allowView=true, allowFineTuning=false, organization=*, group=null, isBlocking=false)], root=text-davinci-003, parent=null)

2.8 编辑(edit)

edit功能可以用来检查语句的拼写错误,代码的正确性等,api中重载了多种edit相关的方法,这里介绍最简单的一种,其他重载方法,可通过源码查看。

String edit(String input, String instruction) {...}

测试代码如下:

ChatGPTProperties properties = ChatGPTProperties.builder().token("sk-xxx")
    .proxyHost("127.0.0.1")
    .proxyPort(7890)
    .build();
OpenAiProxyService openAiProxyService = new OpenAiProxyService(properties);
String input = "What day of the wek is it?";
String instruction = "Fix the spelling mistakes";
System.out.println("编辑前:" + input);
// 下面这句和openAiProxyService.edit(input, instruction, EditModelEnum.TEXT_DAVINCI_EDIT_001);是一样的,默认使用模型TEXT_DAVINCI_EDIT_001
System.out.println("编辑后:" + openAiProxyService.edit(input, instruction));
System.out.println("=============================================");
input = "    public static void mian([String] args) {\n" +
    "        system.in.println(\"hello world\");\n" +
    "    }";
instruction = "Fix the code mistakes";
System.out.println("修正代码前:\n" + input);
System.out.println("修正代码后:\n" + openAiProxyService.edit(input, instruction, EditModelEnum.CODE_DAVINCI_EDIT_001));

测试结果如下:

编辑前:What day of the wek is it?
编辑后:What day of the week is it today?

=============================================
修正代码前:
    public static void mian([String] args) {
        system.in.println("hello world");
    }
修正代码后:
    public static void main(String[] args) {
        System.out.println("hello world");
    }

2.9 向量(embeddings)

有时候需要将文本内容转化成计算机可以识别到的向量形式,可以使用这个方法。api中重载了多种embeddings相关的方法,这里介绍最简单的一种,其他重载方法,可通过源码查看。

List<Double> embeddings(String text) {...}

测试代码如下:

ChatGPTProperties properties = ChatGPTProperties.builder().token("sk-xxx")
    .proxyHost("127.0.0.1")
    .proxyPort(7890)
    .build();
OpenAiProxyService openAiProxyService = new OpenAiProxyService(properties);
// 单文本
String text = "Once upon a time";
System.out.println("文本:" + text);
System.out.println("文本的嵌入向量:" + openAiProxyService.embeddings(text));
System.out.println("=============================================");
// 文本数组
String[] texts = {"Once upon a time", "There was a princess"};
System.out.println("文本数组:" + Arrays.toString(texts));
EmbeddingRequest embeddingRequest = EmbeddingRequest.builder()
    .model(EmbeddingModelEnum.TEXT_EMBEDDING_ADA_002.getModelName()).input(Arrays.asList(texts)).build();
System.out.println("文本数组的嵌入向量:" + openAiProxyService.embeddings(embeddingRequest));

测试结果截取如下:

文本:Once upon a time
文本的嵌入向量:[0.02122687, -0.02013961, -0.004614537, -0.011599556, -0.011213957, 0.0019817224, 0.0023404553, ...
文本数组:[Once upon a time, There was a princess]
文本数组的嵌入向量:EmbeddingResult(model=text-embedding-ada-002-v2, object=list, data=[Embedding(object=embedding, embedding=[0.02122687, -0.02013961, -0.004614537, -0.011599556, -0.011213957, 0.0019817224...

2.10 转录(transcription)

转录功能可以将诸如mp3mp4等格式的音频文件转化成文字输出。

最简单的一种使用方法如下,其他重载方法可通过代码查看。

String transcription(String filePath, AudioResponseFormatEnum audioResponseFormatEnum) {...}

下面是一首许嵩的《想象之中》的转录测试。

ChatGPTProperties properties = ChatGPTProperties.builder().token("sk-xxx")
    .proxyHost("127.0.0.1")
    .proxyPort(7890)
    .build();
OpenAiProxyService openAiProxyService = new OpenAiProxyService(properties);
String filePath = "src/main/resources/audio/想象之中-许嵩.mp3";
System.out.println("语音文件转录后的json文本是:" + openAiProxyService.transcription(filePath, AudioResponseFormatEnum.JSON));
// File file = new File("src/main/resources/audio/想象之中-许嵩.mp3");
// System.out.println("语音文件转录后的json文本是:" + openAiProxyService.transcription(file, AudioResponseFormatEnum.JSON));
    

测试结果截取如下:

语音文件转录后的json文本是:{"text":"想像之中 想像之中 雨過一道彩虹 抬起了頭 瑟瑟灰色天空 想像之中 付出會有結果 毫無保留 信奉你的承諾 想像之中 這次要愛很久 我領略過 你眼裡的溫柔 熱戀以後 你忽然的冰凍 但若兩人丟給我去承受 想像中 很不同 想像中 一切都和後來不同 我承認 曾經那麼心動 你沒想像中那麼戀舊 回憶換不回你的溫柔 最後也不是故作冷漠 轉過頭 我怎麼又一滴淚落 我沒想像中那麼脆弱 分開後 形容也沒消瘦 一起踏過了幾座春秋 過了愛不是追逐佔有 想像之中 這次要愛很久 我領略過 你眼裡的溫柔 熱戀以後 你忽然的冰凍 但若兩人丟給我去承受 想像中 很不同 想像中 一切都和後來不同 我承認 曾經那麼心動 你沒想像中那麼戀舊 回憶換不回你的溫柔 最後也不是故作冷漠 轉過頭 我怎麼又一滴淚落 我沒想像中那麼脆弱 分開後 形容也沒消瘦 一起踏過了幾座春秋 過了愛不是追逐佔有 你沒想像中那麼戀舊 回憶換不回你的溫柔 最後也不是故作冷漠 轉過頭 我怎麼又一滴淚落 我沒想像中那麼脆弱 分開後 形容也沒消瘦 一起踏過了幾座春秋 領悟了愛不是追逐佔有"}

上述文字输出的时候是繁体,具体使用可以使用重载方法中一个代码TranscriptionRequest入参的方法,其中的language可以指定返回的语言,具体使用可以前往官网查看。

2.11 翻译(translation)

翻译功能可以将任意的音频文件翻译成英文。

最简单的一种使用方法如下,其他重载方法可通过代码查看。

String translation(String filePath, AudioResponseFormatEnum audioResponseFormatEnum) {...}

下面是一首许嵩的《想象之中》的翻译测试。

ChatGPTProperties properties = ChatGPTProperties.builder().token("sk-xxx")
    .proxyHost("127.0.0.1")
    .proxyPort(7890)
    .build();
OpenAiProxyService openAiProxyService = new OpenAiProxyService(properties);
String filePath = "src/main/resources/audio/想象之中-许嵩.mp3";
System.out.println("语音文件翻译成英文后的json文本是:" + openAiProxyService.translation(filePath, AudioResponseFormatEnum.JSON));
// File file = new File("src/main/resources/audio/想象之中-许嵩.mp3");
// System.out.println("语音文件翻译成英文后的json文本是:" + openAiProxyService.translation(file, AudioResponseFormatEnum.JSON));
    

测试结果截取如下:

语音文件翻译成英文后的json文本是:{"text":"Translated by Hua Chenyu English Subs In my imagination, there will be results Without keeping your promise In my imagination, this time I will love for a long time I have experienced the tenderness in your eyes After falling in love, you suddenly freeze But if two people are thrown to me to bear In my imagination, it's very different In my imagination, everything is different from the later I admit that I used to be so heartbroken You are not as long-lasting as I imagined Memories can't exchange your tenderness In the end, it's not lonely and lonely Why am I crying again? ..."}

2.12 自定义OkHttpClient

注意:如果没有个性化定义OkHttpClient的需求,请忽略本节,系统会有一个默认的OkHttpClient实现

由于部分用户有自定义OkHttpClient默认并发数和设置Proxy的账号密码的需求,为了同时解决这两个问题,做了自定义OkHttpClient的改造,具体使用方法可以分为结合SpringBoot使用和Main方法使用两种。

2.12.1 基于SpringBoot指定OkHttpClient

默认情况下不需要指定OkHttpClient,有一默认实现,如果想要修改OkHttpClient,满足个性化的需求,可以创建一个@Bean,系统会自动使用该Client实例。

如下定义了一个ClientConfiguration类,指定了Client默认并发数,且指定了Proxy的账号密码:

@Configuration
public class ClientConfiguration {

    @Resource
    private ChatGPTProperties properties;

    @Bean
    public OkHttpClient okHttpClient() {
        Dispatcher dispatcher = new Dispatcher();
        dispatcher.setMaxRequests(100);
        dispatcher.setMaxRequestsPerHost(10);
        return new OkHttpClient.Builder()
                .addInterceptor(new AuthenticationInterceptor(properties.getToken()))
                .connectionPool(new ConnectionPool(100, 10, TimeUnit.SECONDS))
                .readTimeout(Duration.ZERO.toMillis(), TimeUnit.MILLISECONDS)
                .connectTimeout(Duration.ZERO.toMillis(), TimeUnit.MILLISECONDS)
                .hostnameVerifier((hostname, session) -> true)
                .proxy(new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(properties.getProxyHost(), properties.getProxyPort())))
                .proxyAuthenticator((route, response) -> {
                    String credential = Credentials.basic("proxyUsername", "proxyPassword");
                    return response.request().newBuilder()
                            .header("Proxy-Authorization", credential)
                            .build();
                })
                .dispatcher(dispatcher)
                .build();
    }
}

2.12.2 基于Main方法指定OkHttpClient

直接在创建OpenAiProxyService对象的时候指定OkHttpClient,代码如下:

ChatGPTProperties properties = ChatGPTProperties.builder().token("sk-xxx")
    .proxyHost("127.0.0.1")
    .proxyPort(7890)
    .build();
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(100);
dispatcher.setMaxRequestsPerHost(10);
// 自定义okHttpClient
OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .addInterceptor(new AuthenticationInterceptor(properties.getToken()))
    .connectionPool(new ConnectionPool(100, 10, TimeUnit.SECONDS))
    .readTimeout(Duration.ZERO.toMillis(), TimeUnit.MILLISECONDS)
    .connectTimeout(Duration.ZERO.toMillis(), TimeUnit.MILLISECONDS)
    .hostnameVerifier((hostname, session) -> true)
    .proxy(new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(properties.getProxyHost(), properties.getProxyPort())))
    .proxyAuthenticator((route, response) -> {
        String credential = Credentials.basic("proxyUsername", "proxyPassword");
        return response.request().newBuilder()
            .header("Proxy-Authorization", credential)
            .build();
    })
    .dispatcher(dispatcher)
    .build();
// 下面的openAiProxyService使用自定义的okHttpClient
OpenAiProxyService openAiProxyService = new OpenAiProxyService(properties, okHttpClient);
System.out.println("models列表:" + openAiProxyService.listModels());

2.13 设置系统级提示信息

使用main方法的测试代码如下:

ChatGPTProperties properties = ChatGPTProperties.builder().token("sk-xxx")
        // 自定义baseUrl
        .proxyHost("127.0.0.1")
        .proxyPort(7890)
        .build();
OpenAiProxyService openAiProxyService = new OpenAiProxyService(properties);
System.out.println("初始系统级提示信息为:" + openAiProxyService.getSystemPrompt());
openAiProxyService.setSystemPrompt("我是一个Java开发工程师,所有的代码请求都请用Java给我生成。");
openAiProxyService.createStreamChatCompletion("写一个迭代器模式的代码");
// System.out.println("当前的系统级信息提示为:" + openAiProxyService.getSystemPrompt());
// 清理系统级提示信息
// openAiProxyService.cleanUpSystemPrompt();
// System.out.println("清理后的系统级提示信息为:" + openAiProxyService.getSystemPrompt());

结合SpringBoot的使用方法请参照demo中ChatGPTController类中方法systemPrompt

测试效果参考下面GIF图(结合SpringBoot实现带系统级提示的流式输出):
在这里插入图片描述

2.14 计算输入prompt消耗token

使用main方法的测试代码如下:

ChatGPTProperties properties = ChatGPTProperties.builder().token("sk-xxx")
        // 自定义baseUrl
        .proxyHost("127.0.0.1")
        .proxyPort(7890)
        .build();
OpenAiProxyService openAiProxyService = new OpenAiProxyService(properties);
String text = "Hello World!";

System.out.println("当前输入文字使用模型[gpt-3.5-turbo] token总数为:" + openAiProxyService.countTokens(text));
ModelType modelType = ModelType.GPT_4_32K;
// 实际上单就计算的token的数目上来说3.5和4是一样的
System.out.println("当前输入文字使用模型[gpt-4-32k] token总数为:" + openAiProxyService.countTokens(text, modelType));

输出为:

当前输入文字使用模型[gpt-3.5-turbo] token总数为:3
当前输入文字使用模型[gpt-4-32k] token总数为:3

2.15 自定义每次请求使用token

问题背景:在之前的SpringBoot项目中只能指定一个token,当项目的使用人数增加的时候会出现一个问题,就是单token无法支持很多人同时请求,且使用完token的免费额度的时候,需要手动更换下一个token,很多时候如果不及时更换已经用完额度的token会造成服务无法访问的情况。
解决:基于上述问题,现增加alterTokens参数,可以在原先token的基础上,支持在服务中定义更多的token(但是需要注意的是原先的token选项仍是必填选项,而alterTokens则是选填的)。同时增加TokenStrategy策略,来在众多token中选择使用某个token来完成一次请求。当某个token额度使用完或者过期时,会自动在服务中移除该token

application.yml配置如下:

在这里插入图片描述
如上图所示,tokenalter-tokens加起来总共有3个token可以使用,而我们指定的DefaultTokenStrategy每次都会取第一个token使用,当第一个token额度用完,会自动删除,然后第二个token就变成第一个token了,继续使用。如果不指定TokenStrategy,则默认是RandomTokenStrategy,即随机选择token的策略,我推荐大家使用这种策略。当然,如果用户有更个性化的获取Token的策略,也可以通过实现TokenStrategy的方法指定获取token的策略。
例如下面的策略,每次取token中第二个用:

public class SelectSecondStrategy implements TokenStrategy {

    @Override
    public String getToken(List<String> tokens) {
        if (!CollectionUtils.isEmpty(tokens) && tokens.size() > 1) {
            return tokens.get(1);
        }
        throw new ChatGPTException(ChatGPTErrorEnum.NO_AVAILABLE_TOKEN_ERROR);
    }
}

使用main方法的测试代码:

ChatGPTProperties properties = ChatGPTProperties.builder().token("sk-xxx1")
        // 自定义baseUrl
        .proxyHost("127.0.0.1")
        .proxyPort(7890)
        .alterTokens(Arrays.asList("sk-xxx2", "sk-xxx3"))
        .tokenStrategyImpl(SelectSecondStrategy.class)
        .build();
OpenAiProxyService openAiProxyService = new OpenAiProxyService(properties);
System.out.println("models列表:" + openAiProxyService.listModels());

因为指定获取token的策略为SelectSecondStrategy类,所以每次都选择第二个token,即sk-xxx2

输出结果如下:

models列表:[Model(id=text-search-babbage-doc-001, object=model, ownedBy=openai-dev, permission=null, root=null, parent=null), Model(id=curie-search-query, object=model, ownedBy=openai-dev, permission=null, root=null, parent=null), Model(id=text-davinci-003, object=model, ownedBy=openai-internal, permission=null, root=null, parent=null), Model(id=text-search-babbage-query-001, object=model, ownedBy=openai-dev, permission=null, root=null, parent=null),...

3. 扩展

3.1 自定义OpenAiProxyService

由于之前的版本中使用@Bean的方式初始化OpenAiProxyServiceOpenAiUtils,导致一个
SpringBoot中实例是唯一的。

但是有时候需要在项目里自定义多个OpenAiProxyService实例,来装配不同的ChatGPTProperties信息(可以实例化多个Token(sk-xxxxxxxxxxx)使用)。

所以在1.1.6版本中新增了自定义OpenAiProxyService功能。在维持原有SpringBoot项目中全局的一个OpenAiUtils实例的基础上,现在可以自定义不同的OpenAiProxyService实例,并且实例之间的属性是完全隔离的。

下面是一个Demo用来展示使用方法。

ChatGPTProperties properties = ChatGPTProperties.builder().token("sk-002xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
    .proxyHost("127.0.0.1")// 代理ip
    .proxyHost("7890")// 代理端口
    .build();
OpenAiProxyService openAiProxyService = new OpenAiProxyService(properties);
// 直接使用new出来的openAiProxyService来调用方法,每个OpenAiProxyService都拥有自己的Token。
// 这样在一个SpringBoot项目中,就可以有多个Token,可以有更多的免费额度供使用了
openAiProxyService.createStreamChatCompletion("Java的三大特性是什么");

在上述方法中,新new了一个ChatGPTProperties对象,并且set了tokensk-002xxxxxxxxxxxxxxxxxxxxxxxxx(这里不需要设置除了tokenproxyHostproxyPort以外的其他属性,因为ChatGPTProperties的其他属性拥有默认值,如果需要对其他属性做修改,可以自行设置。注意:sessionExpirationTime没有默认值,表示会话没有过期时间,如果需要设置会话过期时间,请set该值。

而在application.yml中设置的tokensk-001xxxxxxxxxxxxxxxxxxxxxxxxx,这个token是给全局唯一的OpenAitils用的,这样就可以通过OpenAiProxyService的构造方法new出来一个新的OpenAiProxyService实例,其中构造方法的第二个参数直接填Duration.ZERO就好,表示Http调用请求没有超时时间,后续版本更新中,我会新增一个只有一个入参的构造方法。

这样直接使用new出来的openAiProxyService来调用方法,每个OpenAiProxyService都拥有自己的Token。

在一个SpringBoot项目中,就可以有多个Token,可以有更多的免费额度供使用了。

4 自定义OpenAi服务端

4.1 介绍

默认使用OpenAi服务的baseUrlhttps://api.openai.com/ ,众所周知的原因,很多人网络无法直连。如果服务是部署在国外服务器,或者拥有代理,配置proxy-hostproxy-port依旧可以直接访问 https://api.openai.com/ ,这个也是比较推荐的方式,毕竟这是直连openai的服务,最安全,提供的服务接口也最全。

但是很多人没用国外的服务器,也没有自己的代理,在这里提供一种解决方法,通过使用这种方法,来实现无需国外服务器,也无需代理即可访问OpenAi服务。

在这里先简单介绍一下反向代理的概念。

反向代理是一种用于隐藏服务端真实地址的技术。比如说真实地址是B,但是我们不希望用户直连B,让B的地址暴露,我们就可以使用反向代理,让A的请求转发到B。

在这里,我们可以将B理解成 https://api.openai.com/ ,由于我们不能直连 https://api.openai.com/ ,但是我们可以在一台可以直连 https://api.openai.com/ 的服务器上反向代理,给我们暴露一个网址A,我们将baseUrl设置为A,向A发送请求,服务器接受到后自动将请求转发到 https://api.openai.com/ ,并且将结果返回。这样的话,我们只需要指定baseUrl,都不需要设置代理proxy-hostproxy-port即可访问OpenAi的服务了。那现在的问题就是怎么简单的获取到这样的一个baseUrl了。

这里我找到了一个可以提供OpenAi代理的服务。访问https://www.ichati.cn/dashboard/products/tools,注册登录后选择白泽,点击立即使用。

image-20230718104126500

然后输入API-Key,点击开始部署(这里我测试只需要长度保持一致,后面几位的api-key可以随便填)。

image-20230718104238571

然后如下图所示生成了我们的baseUrl(注意,这里生成的网页最后没用斜杠/,使用服务的使用baseUrl需要在最后带/,例如 https://apps.ichati.cn/0a7699ac-bc73-4732-9263-55c1c01f56e3/ )。

image-20230718104412550

4.2 测试

在4.1节中已经生成了我们项目的baseUrl,且因为它给我们转发,我们可以任务,他的服务部署在一台可以直连 https://api.openai.com/ 的服务器上,也就自然不需要设置proxy-hostproxy-port了。

4.2.1 基于SpringBoot使用OpenAiUtils

先设置application.yml,只需要设置tokenbaseUrl即可。

image-20230718111150784

启动SpringBoot项目。

使用Post工具调用下面的接口生成图片。

@PostMapping("/createImage")
public List<String> createImage(String prompt) {
        List<String> imageList = OpenAiUtils.createImage(prompt);
        System.out.println(imageList);
        return imageList;
        }

image-20230718115911134

结果如下:

image-20230718115949339

访问该链接,就可以得到一个生成的图片~

4.2.2 基于Main方法使用OpenAiProxyService

下面是使用Main方法进行调用的具体代码,只需要设置tokenbaseUrl即可。

ChatGPTProperties properties = ChatGPTProperties.builder().token("sk-xxxx")
        .baseUrl("https://apps.ichati.cn/0a7699ac-bc73-4732-9263-55c1c01f56e3/") // 这里需要填自己的baseUrl
        .build();
        OpenAiProxyService openAiProxyService = new OpenAiProxyService(properties);
        System.out.println(openAiProxyService.chatCompletion("Go写个Hello World程序"));

输入结果如下:

[package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}]

5 AI助手展示

接入微信公众号,AI助手可自动回复。
在这里插入图片描述

转载请注明出处或者链接地址:https://www.qianduange.cn//article/843.html
标签
springboot
评论
会员中心 联系我 留言建议 回顶部
复制成功!