目录
- 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 测试
- 3. 扩展
- 3.1 自定义OpenAiProxyService
- 3 AI助手展示
源码及更详细的介绍说明参见Git上的README.md文档
https://github.com/asleepyfish/chatgpt
本文结合SpringBoot的Demo地址:https://github.com/asleepyfish/chatgpt-demo
流式输出结合Vue的Demo地址:https://github.com/asleepyfish/chatgpt-vue
另外一篇博客有基于此api的产品。https://blog.csdn.net/qq_41821963/article/details/130918024
1. 配置阶段
1.1 依赖引入
pom.xml
中引入依赖(当前最新版本为1.2.1,可前往Github页面查看当前最新版本)
<dependency>
<groupId>io.github.asleepyfish</groupId>
<artifactId>chatgpt</artifactId>
<version>1.2.1</version>
</dependency>
1.2 配置application.yml文件
在application.yml
文件中配置相关参数(Optional为可选参数)
参数 | 解释 |
---|---|
token | 申请的API KEYS |
proxy-host | 代理的ip |
proxy-port | 代理的端口 |
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的会话永不销毁(增加请求消耗) |
例:
chatgpt:
token: sk-xxxxxxxxxxxxxxx
proxy-host: 127.0.0.1
proxy-port: xxxx
session-expiration-time: 30
其中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
可以一次性返回多条不同的解答(n
为ChatCompletionRequest
类中一个参数)。
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
表示生成图片的格式,格式中分为url
和b64_json
两种,如果希望返回的是url,则返回的url会在生成一个小时后消失,默认值是url
。
2.2.1 测试
测试代码
@Test
public void testGenerateImg() {
OpenAiUtils.createImage("英短").forEach(System.out::println);
}
结果
默认情况下会生成一个url,点击去就可以看到图片。
2.3 下载图片
在3.2的基础上做了优化,直接使用responseFormat
为b64_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 生成流式回答
生成流式回答的方法是OpenAiUtils
的createStreamChatCompletion
方法,本工具类重载了同名的多个参数的方法,其中最通用的方法是
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
方法的入参中。同时,为了避免结果输出到浏览器产生乱码和支持流式输出,需要ContentType
和CharacterEncoding
。
具体代码如下:
@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) {...}
其中startDate
和endDate
区间范围不超过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)
3. 扩展
3.1 自定义OpenAiProxyService
由于之前的版本中使用@Bean的方式初始化OpenAiProxyService
和OpenAiUtils
,导致一个SpringBoot中实例是唯一的。
但是有时候需要在项目里自定义多个OpenAiProxyService
实例,来装配不同的ChatGPTProperties
信息(可以实例化多个Token(sk-xxxxxxxxxxx)使用)。
所以在1.1.6
版本中新增了自定义OpenAiProxyService
功能。在维持原有SpringBoot项目中全局的一个OpenAiUtils
实例的基础上,现在可以自定义不同的OpenAiProxyService
实例,并且实例之间的属性是完全隔离的。
下面是一个Demo用来展示使用方法。
@GetMapping("/customToken")
public void customToken() {
ChatGPTProperties chatGPTProperties = new ChatGPTProperties();
chatGPTProperties.setToken("sk-002xxxxxxxxxxxxxxxxxxxxxxxxx");
chatGPTProperties.setProxyHost("127.0.0.1");
chatGPTProperties.setProxyPort(7890);
OpenAiProxyService openAiProxyService = new OpenAiProxyService(chatGPTProperties, Duration.ZERO);
// 直接使用new出来的openAiProxyService来调用方法,每个OpenAiProxyService都拥有自己的Token。
// 这样在一个SpringBoot项目中,就可以有多个Token,可以有更多的免费额度供使用了
openAiProxyService.createStreamChatCompletion("Java的三大特性是什么");
}
在上述方法中,新new了一个ChatGPTProperties
对象,并且set了token
为sk-002xxxxxxxxxxxxxxxxxxxxxxxxx
(这里不需要设置除了token
、proxyHost
和proxyPort
以外的其他属性,因为ChatGPTProperties
的其他属性拥有默认值,如果需要对其他属性做修改,可以自行设置。注意:sessionExpirationTime没有默认值,表示会话没有过期时间,如果需要设置会话过期时间,请set该值。)
而在application.yml
中设置的token
为sk-001xxxxxxxxxxxxxxxxxxxxxxxxx
,这个token是给全局唯一的OpenAitils
用的,这样就可以通过OpenAiProxyService
的构造方法new出来一个新的OpenAiProxyService
实例,其中构造方法的第二个参数直接填Duration.ZERO
就好,表示Http调用请求没有超时时间,后续版本更新中,我会新增一个只有一个入参的构造方法。
这样直接使用new出来的openAiProxyService
来调用方法,每个OpenAiProxyService
都拥有自己的Token。
在一个SpringBoot项目中,就可以有多个Token
,可以有更多的免费额度供使用了。
3 AI助手展示
接入微信公众号,AI助手可自动回复。