Skip to main content

08、插件开发

ChatGpt在早些时候推出function call功能,这个在我看来其实就是插件的本质,通过自定义function实现插件的功能,SDK在1.0.14+版本的时候已经支持了原生的function call调用。但是function call调用逻辑比较复杂难懂,很多小伙伴反应不太会使用。于是我基于function call做了下定制封装实现Plugin功能。(可能存在不合理的地方欢迎指正)

3.2、使用步骤

想看示例的朋友可以直接看:PluginTest

3.2.1、简介——插件抽象类

插件抽象类,定义了插件的必须参数:插件名称,方法,描述,参数,必须参数,插件的请求值R,插件的返回值T。 需要重点关注R和T两个泛型。

插件抽象类还包含两个重要的抽象方法: func和content方法。这两个方法需要自己实现。

方法功能
public abstract T func(R args);接受一个插件参数,返回插件返回值。后面有示例演示。
public abstract String content(T t);构建给gpt的参数信息

@Data
@AllArgsConstructor
public abstract class PluginAbstract<R extends PluginParam, T> {

private Class<?> R;
private String name;
private String function;
private String description;
private List<Arg> args;
private List<String> required;
private Parameters parameters;
public PluginAbstract(Class<?> r) {
R = r;
}

public void setRequired(List<String> required) {
if (CollectionUtil.isEmpty(required)) {
this.required = this.getArgs().stream().filter(e -> e.isRequired()).map(Arg::getName).collect(Collectors.toList());
return;
}
this.required = required;
}

private void setRequired() {
if (CollectionUtil.isEmpty(required)) {
this.required = this.getArgs().stream().filter(e -> e.isRequired()).map(Arg::getName).collect(Collectors.toList());
}
}

private void setParameters() {
JSONObject properties = new JSONObject();
args.forEach(e -> {
JSONObject param = new JSONObject();
param.putOpt("type", e.getType());
param.putOpt("enum", e.getEnumDictValue());
param.putOpt("description", e.getDescription());
properties.putOpt(e.getName(), param);
});
this.parameters = Parameters.builder()
.type("object")
.properties(properties)
.required(this.getRequired())
.build();
}

public void setArgs(List<Arg> args) {
this.args = args;
setRequired();
setParameters();
}

@Data
public static class Arg {
private String name;
private String type;
private String description;
@JsonIgnore
private boolean enumDict;
@JsonProperty("enum")
private List<String> enumDictValue;
@JsonIgnore
private boolean required;
}

public abstract T func(R args);

public abstract String content(T t);
}

3.2.2、创建插件

创建自定义插件继承PluginAbstract抽象类。 WeatherReq,WeatherResp在这省略 。完整测试源码请看:https://github.com/Grt1228/chatgpt-java test包目录。

举例实现天气查询插件。

public class WeatherPlugin extends PluginAbstract<WeatherReq, WeatherResp> {
public WeatherPlugin(Class<?> r) {
super(r);
}

@Override
public WeatherResp func(WeatherReq args) {
//模拟天气查询,真实使用场景需要调用第三方接口查询真实天气
WeatherResp weatherResp = new WeatherResp();
weatherResp.setTemp("25到28摄氏度");
weatherResp.setLevel(3);
return weatherResp;
}
@Override
public String content(WeatherResp weatherResp) {
//构建chatgpt需要的content参数
return "当前天气温度:" + weatherResp.getTemp() + ",风力等级:" + weatherResp.getLevel();
}
}

3.2.3、使用插件

插件使用同样支持阻塞输出和流式输出两种方式,可以自己根据实际场景使用。

创建OpenAi客户端

客户端的创建和原来保持一致


/**
* 描述: 插件测试类
*
* @author https:www.unfbx.com
* 2023-08-18
*/
@Slf4j
public class PluginTest {

private OpenAiClient openAiClient;
private OpenAiStreamClient openAiStreamClient;

@Before
public void before() {
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger());
//!!!!千万别再生产或者测试环境打开BODY级别日志!!!!
//!!!生产或者测试环境建议设置为这三种级别:NONE,BASIC,HEADERS,!!!
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient okHttpClient = new OkHttpClient
.Builder()
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(new OpenAiResponseInterceptor())
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
openAiClient = OpenAiClient.builder()
.okHttpClient(okHttpClient)
.apiKey(Arrays.asList("sk-********************************"))
.apiHost("https://dgr.life/")
.build();
openAiStreamClient = OpenAiStreamClient.builder()
//支持多key传入,请求时候随机选择
.apiKey(Arrays.asList("sk-********************************"))
.apiHost("https://dgr.life/")
.build();
}
}

流式输出

@Test
public void streamPlugin() {
WeatherPlugin plugin = new WeatherPlugin(WeatherReq.class);
plugin.setName("知心天气");
plugin.setFunction("getLocationWeather");
plugin.setDescription("提供一个地址,方法将会获取该地址的天气的实时温度信息。");
PluginAbstract.Arg arg = new PluginAbstract.Arg();
arg.setName("location");
arg.setDescription("地名");
arg.setType("string");
arg.setRequired(true);
plugin.setArgs(Collections.singletonList(arg));

// Message message1 = Message.builder().role(Message.Role.USER).content("秦始皇统一了哪六国。").build();
Message message2 = Message.builder().role(Message.Role.USER).content("获取上海市的天气现在多少度,然后再给出3个推荐的户外运动。").build();
List<Message> messages = new ArrayList<>();
// messages.add(message1);
messages.add(message2);
//默认模型:GPT_3_5_TURBO_16K_0613
//有四个重载方法,都可以使用
openAiStreamClient.streamChatCompletionWithPlugin(messages, new ConsoleEventSourceListener(), plugin);
CountDownLatch countDownLatch = new CountDownLatch(1);
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

阻塞输出


@Test
public void plugin() {
WeatherPlugin plugin = new WeatherPlugin(WeatherReq.class);
plugin.setName("知心天气");
plugin.setFunction("getLocationWeather");
plugin.setDescription("提供一个地址,方法将会获取该地址的天气的实时温度信息。");
PluginAbstract.Arg arg = new PluginAbstract.Arg();
arg.setName("location");
arg.setDescription("地名");
arg.setType("string");
arg.setRequired(true);
plugin.setArgs(Collections.singletonList(arg));

// Message message1 = Message.builder().role(Message.Role.USER).content("秦始皇统一了哪六国。").build();
Message message2 = Message.builder().role(Message.Role.USER).content("获取上海市的天气现在多少度,然后再给出3个推荐的户外运动。").build();
List<Message> messages = new ArrayList<>();
// messages.add(message1);
messages.add(message2);
//默认模型:GPT_3_5_TURBO_16K_0613
//有四个重载方法,都可以使用
ChatCompletionResponse response = openAiClient.chatCompletionWithPlugin(messages, plugin);
log.info("自定义的方法返回值:{}", response.getChoices().get(0).getMessage().getContent());
}

四、完结

上面已经完整介绍了整个插件的使用过程,方案不一定是最合理的,也是beat版本,欢迎交流。 如果觉的文章对你有帮助帮忙点个赞。