この記事は虎の穴ラボ Advent Calendar 2024の18日目の記事です。
こんにちは、虎の穴ラボのH.Hです。
今日は最近使っているSpringAIというライブラリの中で対応しているFunctionCallingの実装と実際の結果についてまとめました。
FunctionCallingとは何か
OpenAIのAPIのリファレンスでは以下の説明されています。
抜粋
Function calling enables developers to connect language models to external data and systems. You can define a set of functions as tools that the model has access to, and it can use them when appropriate based on the conversation history. You can then execute those functions on the application side, and provide results back to the model. Learn how to extend the capabilities of OpenAI models through function calling in this guide.
簡単に書くと独自の処理を行う関数を定義して、AI側では判別できない情報を付与した結果を求める機能です。例えば現在の日時や天気情報、都市の気温といったデータを活用して回答を出してもらうような使い方です。
また、どの関数を使用するかもAI側で判断させることができる点も特徴的な機能です。
今日の日付が欲しい時には日付の判別する関数を呼び出すが、日付を必要としない場合には関数を呼び出さないというような処理を切り分けされます。
SpringAIのFunctionCallingの実装について
SpringAIのFunctionCallingのページはこちらになります。
こちらではサンプルとして、都市の名前が入力されたら関数の中で定義された都市の気温が固定で返されるものが書かれています。
パリは15度、東京は10度、サンフランシスコは30度という値が返される関数を事前に準備して、入力内容から都市の名前を取得して気温を判断しています。
実装サンプル
実装のサンプルはドキュメントのコードのサンプルとSpringAIのGithubに公開されているテストコードを見ることで、全体像を見ることができます。
テストコード
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*") public class OpenAiFunctionCallbackIT { private final Logger logger = LoggerFactory.getLogger(OpenAiFunctionCallbackIT.class); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"), "spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test void functionCallTest() { this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.temperature=0.1").run(context -> { OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");//各都市の気温を聞くプロンプトを定義 ChatResponse response = chatModel.call( new Prompt(List.of(userMessage), OpenAiChatOptions.builder().withFunction("WeatherInfo").build())); logger.info("Response: {}", response); assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15"); }); } @Test void streamFunctionCallTest() { this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.temperature=0.1").run(context -> { OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); UserMessage userMessage = new UserMessage( "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'WeatherInfo'"); Flux<ChatResponse> response = chatModel.stream( new Prompt(List.of(userMessage), OpenAiChatOptions.builder().withFunction("WeatherInfo").build())); String content = response.collectList() .block() .stream() .map(ChatResponse::getResults) .flatMap(List::stream) .map(Generation::getOutput) .map(AssistantMessage::getContent) .collect(Collectors.joining()); logger.info("Response: {}", content); assertThat(content).containsAnyOf("30.0", "30"); assertThat(content).containsAnyOf("10.0", "10"); assertThat(content).containsAnyOf("15.0", "15"); }); } @Configuration static class Config { //※ただしテストコードのコードをそのまま使用すると私の環境ではエラーが出たので別の記述方法を使用しています。 @Bean public FunctionCallback weatherFunctionInfo() { return FunctionCallback.builder() .function("WeatherInfo", new MockWeatherService()) .description("Get the weather in location") .inputType(MockWeatherService.Request.class) .build(); } } }
実際のコードで使用したConfigのコード
@Configuration static class Config { @Bean @Description("Get the weather in location") public MyBiFunction weatherFunctionWithClassBiFunction() { return new MyBiFunction(); } @Bean @Description("Get the weather in location") public BiFunction<MockWeatherService.Request, ToolContext, MockWeatherService.Response> weatherFunctionWithContext() { return (request, context) -> new MockWeatherService().apply(request); } @Bean @Description("Get the weather in location") public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction() { return new MockWeatherService(); } @Bean public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunctionTwo() { MockWeatherService weatherService = new MockWeatherService(); return (weatherService::apply); } } public static class MyBiFunction implements BiFunction<MockWeatherService.Request, ToolContext, MockWeatherService.Response> { @Override public MockWeatherService.Response apply(MockWeatherService.Request request, ToolContext context) { return new MockWeatherService().apply(request); } }
都市名から気温を返す部分のサンプルコード
public class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> { @Override public Response apply(Request request) { double temperature = 10; if (request.location().contains("Paris")) { temperature = 15; } else if (request.location().contains("Tokyo")) { temperature = 10; } else if (request.location().contains("San Francisco")) { temperature = 30; } return new Response(temperature, 15, 20, 2, 53, 45, Unit.C); } /** * Temperature units. */ public enum Unit { /** * Celsius. */ C("metric"), /** * Fahrenheit. */ F("imperial"); /** * Human readable unit name. */ public final String unitName; Unit(String text) { this.unitName = text; } } /** * Weather Function request. */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonClassDescription("Weather API request") public record Request(@JsonProperty(required = true, value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location, @JsonProperty(required = true, value = "lat") @JsonPropertyDescription("The city latitude") double lat, @JsonProperty(required = true, value = "lon") @JsonPropertyDescription("The city longitude") double lon, @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) { } /** * Weather Function response. */ public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity, Unit unit) { } }
今回検証したいこと
1.複数の関数が必要な場合はどう実行すれば良いのか
サンプルは関数が一つだけの実装だったので、複数の関数が必要な場合にはどのように実装するのかを実装して検証してみました。
検証として行ったのは以下のとおりです。
1)入力された地名からその地名が属する都道府県を特定し、都道府県の県庁所在地の気温を特定
※実際の気温ではなく今回は北から都道府県を並べたときにそれぞれの都市に対応した固定の-20度から26度の値を返却するような関数を準備しました。
2)現在の日時を用いて日本の四季を判別
1と2を踏まえて外出するときのアドバイスを出してもらうようにしました。
実装のコード
ChatModel chatModel = new OpenAiChatModel(openAiApi); List<Message> messageList = new ArrayList<>(); SystemMessage systemMessage=new SystemMessage("地名を入力するので県庁所在地を日本語で判別してください。東京都内の地名は県庁所在地は東京としてください。県庁所在地から気温を判別し、現在の日付から日本の季節を踏まえて外出するときのアドバイスをしてください。ただし、-10度以下と35度以上の場合は外出を止めてください。"); messageList.add(systemMessage); UserMessage userMessage = new UserMessage( testBean.getTestString()); messageList.add(userMessage); var promptOptions = OpenAiChatOptions.builder() .withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService()) .withName("CurrentWeatherService") .withDescription("Get the weather in location") .withResponseConverter(response -> "" + response.temp() + response.unit()) .build(), FunctionCallbackWrapper.builder(new YearService()) .withName("CurrentYearService") .withDescription("What is today's date?") .withResponseConverter(response -> "" + response.day()) .build())).withModel("gpt-4o") .build(); //複数の関数を設定する場合は、List.ofに入れることで実行されます。 ChatResponse response = chatModel.call(new Prompt(messageList, promptOptions)); Config部分 @Configuration static class Config { @Bean @Description("Get the weather in location") public MyBiFunction weatherFunctionWithClassBiFunction() { return new MyBiFunction(); } @Bean @Description("Get the weather in location") public BiFunction<MockWeatherService.Request, ToolContext, MockWeatherService.Response> weatherFunctionWithContext() { return (request, context) -> new MockWeatherService().apply(request); } @Bean @Description("Get the weather in location") public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction() { return new MockWeatherService(); } @Bean public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunctionTwo() { MockWeatherService weatherService = new MockWeatherService(); return (weatherService::apply); } @Bean @Description("Get this year in the Western calendar year") public MyBiFunction1 yearFunctionWithClassBiFunction() { return new MyBiFunction1(); } @Bean @Description("Get this year in the Western calendar year") public BiFunction<YearService.Request, ToolContext, YearService.Response> yearFunctionWithContext() { return (request, context) -> new YearService().apply(request); } @Bean @Description("Get this year in the Western calendar year") public Function<YearService.Request, YearService.Response> yearFunction() { return new YearService(); } @Bean public Function<YearService.Request, YearService.Response> yearFunctionTwo() { YearService yearService = new YearService(); return (yearService::apply); } } public static class MyBiFunction implements BiFunction<MockWeatherService.Request, ToolContext, MockWeatherService.Response> { @Override public MockWeatherService.Response apply(MockWeatherService.Request request, ToolContext context) { return new MockWeatherService().apply(request); } } public static class MyBiFunction1 implements BiFunction<YearService.Request, ToolContext, YearService.Response> { @Override public YearService.Response apply(YearService.Request request, ToolContext context) { return new YearService().apply(request); } }
関数1(MockWeatherService) 県庁所在地から気温を返すクラス
public class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> { @Override public Response apply(Request request) { double temperature = -273; System.out.println(request.location()); if (request.location().contains("札幌")){temperature = -20;} else if (request.location().contains("青森")){temperature = -19;} else if (request.location().contains("盛岡")){temperature = -18;} else if (request.location().contains("仙台")){temperature = -17;} else if (request.location().contains("秋田")){temperature = -16;} else if (request.location().contains("山形")){temperature = -15;} else if (request.location().contains("福島")){temperature = -14;} else if (request.location().contains("水戸")){temperature = -13;} else if (request.location().contains("宇都宮")){temperature = -12;} else if (request.location().contains("前橋")){temperature = -11;} else if (request.location().contains("さいたま")){temperature = -10;} else if (request.location().contains("千葉")){temperature = -9;} else if (request.location().contains("東京")){temperature = -8;} else if (request.location().contains("横浜")){temperature = -7;} else if (request.location().contains("新潟")){temperature = -6;} else if (request.location().contains("富山")){temperature = -5;} else if (request.location().contains("金沢")){temperature = -4;} else if (request.location().contains("福井")){temperature = -3;} else if (request.location().contains("甲府")){temperature = -2;} else if (request.location().contains("長野")){temperature = -1;} else if (request.location().contains("岐阜")){temperature = 0;} else if (request.location().contains("静岡")){temperature = 1;} else if (request.location().contains("名古屋")){temperature = 2;} else if (request.location().contains("津")){temperature = 3;} else if (request.location().contains("大津")){temperature = 4;} else if (request.location().contains("京都")){temperature = 5;} else if (request.location().contains("大阪")){temperature = 6;} else if (request.location().contains("神戸")){temperature = 7;} else if (request.location().contains("奈良")){temperature = 8;} else if (request.location().contains("和歌山")){temperature = 9;} else if (request.location().contains("鳥取")){temperature = 10;} else if (request.location().contains("松江")){temperature = 11;} else if (request.location().contains("岡山")){temperature = 12;} else if (request.location().contains("広島")){temperature = 13;} else if (request.location().contains("山口")){temperature = 14;} else if (request.location().contains("徳島")){temperature = 15;} else if (request.location().contains("高松")){temperature = 16;} else if (request.location().contains("松山")){temperature = 17;} else if (request.location().contains("高知")){temperature = 18;} else if (request.location().contains("福岡")){temperature = 19;} else if (request.location().contains("佐賀")){temperature = 20;} else if (request.location().contains("長崎")){temperature = 21;} else if (request.location().contains("熊本")){temperature = 22;} else if (request.location().contains("大分")){temperature = 23;} else if (request.location().contains("宮崎")){temperature = 24;} else if (request.location().contains("鹿児島")){temperature = 25;} else if (request.location().contains("那覇")){temperature = 26;} return new Response(temperature, 15, 20, 2, 53, 45, Unit.C); } /** * Temperature units. */ public enum Unit { /** * Celsius. */ C("metric"), /** * Fahrenheit. */ F("imperial"); /** * Human readable unit name. */ public final String unitName; Unit(String text) { this.unitName = text; } } /** * Weather Function request. */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonClassDescription("Weather API request") public record Request(@JsonProperty(required = true, value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location, @JsonProperty(required = true, value = "lat") @JsonPropertyDescription("The city latitude") double lat, @JsonProperty(required = true, value = "lon") @JsonPropertyDescription("The city longitude") double lon, @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) { } /** * Weather Function response. */ public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity, Unit unit) { } }
関数2(YearService) 現在日時を取得してYYYY/MM/DDの形式で返却する
public class YearService implements Function<YearService.Request, YearService.Response> { @Override public Response apply(Request request) { LocalDateTime localDateTime=LocalDateTime.now(); String day=localDateTime.getYear()+"/"+localDateTime.getMonthValue()+"/"+localDateTime.getDayOfMonth(); return new Response(day, 15, 20, 2, 53, 45, Unit.C); } /** * Temperature units. */ public enum Unit { /** * Celsius. */ C("metric"), /** * Fahrenheit. */ F("imperial"); /** * Human readable unit name. */ public final String unitName; Unit(String text) { this.unitName = text; } } /** * Weather Function request. */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonClassDescription("Year API request") public record Request( @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) { } /** * Weather Function response. */ public record Response(String day, double feels_like, double temp_min, double temp_max, int pressure, int humidity, Unit unit) { } }
実行結果 以下は2024/12/8に実行した結果です。実験の結果、二つの関数は正しく判別されて使用されましたが、言語設定に関連していくつかの問題が確認されました。ただし、システムプロンプトで定義した日本語を使用する指定が不安定で、データをアルファベットで取得してしまい、目的の判定が正しく行われないなどが発生しました。
池袋を入力した場合
現在、東京の気温は約-8°Cです。今日は2024年12月8日で、日本では冬の季節にあたります。非常に寒い気温なので、外出する際は以下のアドバイスを参考にしてください:\n\n1. **暖かい服装**: 厚手のコートやセーター、手袋、マフラー、帽子などを着用して体を温かく保ちましょう。\n \n2. **重ね着**: 重ね着をすることで、屋内外の温度差に対応しやすくなります。\n\n3. **防寒アイテム**: ホッカイロなどの防寒アイテムを持ち歩くと便利です。\n\n4. **保湿ケア**: 冷たい風で肌が乾燥しやすいため、保湿クリームを使用することをおすすめします。\n\n5. **体を温める飲み物**: 温かい飲み物を持参することで、体を内側から温めることができます。\n\n外出の際には十分に寒さ対策をして、安全に過ごしてください。
秋葉原を入力した場合
秋葉原(東京)の現在の気温は-8.0°Cです。現在の日付は2024年12月8日で、日本では冬の季節にあたります。\n\n### 外出時のアドバイス\n- **防寒対策**: 気温が非常に低いので、厚手のコート、マフラー、手袋、帽子などを着用して防寒対策をしっかり行ってください。\n- **乾燥対策**: 冬は空気が乾燥しているため、保湿クリームを使用して肌を保護することをおすすめします。\n- **体調管理**: 寒さで体調を崩しやすいので、温かい飲み物を摂取して体を温めるようにしましょう。\n\n暖かくしてお出かけください!
函館を入力した場合
日本語で県庁所在地を取得を指定していますが、Sapporoが取得されていたことで判別できていませんでした。
現在、札幌市の気温データにエラーがあるようです。しかし、現在の日付は2024年12月8日です。通常、この時期は冬にあたり、札幌では非常に寒くなることが予想されます。以下のアドバイスを参考にしてください:\n\n- 暖かい衣服を着用し、特に防寒対策として手袋や帽子、マフラーをお持ちください。\n- 足元が滑りやすい場合があるので、滑りにくい靴を履いてください。\n- 外出前に最新の天気予報を確認し、積雪や凍結に注意してください。\n\n安全に外出するために、気温が非常に低い場合は無理せず室内で過ごすことをお勧めします。
札幌を入力した場合
札幌の現在の気温は-20.0度です。非常に寒いため、外出は控えることをお勧めします。安全を第一に考え、屋内で過ごすようにしてください。
那覇を入力した場合
那覇市の現在の気温は26°Cです。今日は2024年12月8日で、日本の季節は冬です。しかし、沖縄は比較的温暖な気候のため、冬でも気温が高くなることがあります。\n\n外出する際のアドバイスは以下の通りです:\n\n- 日中は軽装でも問題ないですが、風が強い場合や夕方以降は少し肌寒く感じることもあるので、薄手の上着を持参すると良いでしょう。\n- 紫外線対策として、帽子や日焼け止めを使用することをお勧めします。\n- 水分補給を忘れずに行いましょう。\n\n安全に楽しい時間をお過ごしください。
2.必要のない場合、不要な関数はどのような扱いになるのか
複数の関数が定義されていた場合、不要な関数も呼び出されるのかを確かめてみました。 呼び出されないと思いますが、万が一呼び出されてしまうとエラーが起きてしまう可能性もあるので念のために確認を行いました。
実行結果 今日の日付を聞いた場合
今日の日付は2024年12月8日です。季節は冬です。外出時の服装や注意点は、気温によって異なりますので、具体的な地名を教えていただければ、さらに詳しいアドバイスを提供します。
実行したのが12/8なので、その日を正しく取得し応答を返してくれることが確認できました。
まとめ
今回SpringAI経由でOpenAIの準備しているAPIを使用してFunctionCallingの実装を試しました。
結果としては複数の関数を使用しての判定も行えることが確認できて、リアルタイムの情報が付与できることで使い方の幅が広まりました。ただし、プロンプトの構成によっては、期待していたデータが得られないなど、より明確に回答を出してくれるようにする指定をすることと意図しない結果が返却された場合の対応も考えておく必要があると実装をしてみて感じました。
Fantia開発採用情報
虎の穴ラボでは現在、一緒にFantiaを開発していく仲間を積極募集中です!
多くのユーザーに使っていただけるtoCサービスの開発をやってみたい方は、ぜひ弊社の採用情報をご覧ください。
toranoana-lab.co.jp