👻

JavaとSemantic Kernelで始めるAIプロダクト開発入門

2024/09/13に公開

テーマ

Semantic Kernel + javaで作るAIプロダクト作成の基礎

対象とする人

  • 研修等で初めのプログラミング言語としてJavaを学んで、AIプロダクトに興味がある人
  • Javaを長年利用してきたが、AIは未経験の人

Scope

  • 扱うこと

    • Semantic KernelとLLMの基礎的な概念
    • Semantic Kernelを用いたCUIベースのAIプロダクトの実装
  • 扱わないこと

    • AzureやAI Studioの設定方法
    • フロントエンドを含めた製品の構成

Semantic Kernelとは?

Semantic KernelはWindowsが開発した、LLMを用いてAIエージェントを構築するオープンソースソフトウェアです。より詳しく言うと、今までのプログラミング言語でできたこと(計算やアルゴリズムベースの処理、Web APIアクセス等)をLLMに組み込み、LLM自身の判断によってツールとして利用できるようになったり(AIオーケストレーション)、またAIの出力を関数の引数として利用できるように整形したり(Parser)することで、生成AIの力を自分の開発しているアプリケーションに組み込むことができます。

構造

Semantic Kernelの中心になっているのはKernelであり、そこからサービス(LLMなど)やプラグインを利用するという形になっています(Fig. 1)。情報の流れとしては、まずカーネルにユーザーのクエリが送られて、Kernelがそれを受けて判断します。Kernelは情報を受け取ると、適切なツール(サービス)を判断し、そこにクエリを送ります。応答が返ってきたら、その応答をもとに適切なプラグインにクエリを送信するということを繰り返して最終的な出力を得ます。

Fig. 1: Semantic Kernelの概念図[1]

Kernel

KernelはSemantic Kernelの根幹をなす部分であり、中心となって動作する部分です。Semantic KernelではKernelが全てのサービスやプラグイン・クエリといった情報にアクセスできるので、Kernelをきちんと構成することがSemantic Kernelプロジェクトの成功に最も大きく関わってきます。

Service

Semantic KernelにおけるサービスとはLLMのモデルのことです。Javaバージョンで現状で使用できるLLMサービスはAzure OpenAIの他、OpenAI, Gemini (ver 1.2.0~)のようです。
https://learn.microsoft.com/en-us/semantic-kernel/concepts/ai-services/chat-completion/?tabs=csharp-Mistral%2Cpython-AzureOpenAI%2Cjava-AzureOpenAI&pivots=programming-language-java#installing-the-necessary-packages
https://devblogs.microsoft.com/semantic-kernel/announcing-semantic-kernel-for-java-1-2-0/
Semantic KernelにおけるLLMの役割は単に回答を生成するだけでなく、Function Callingの機能を通して、問題解決に必要なツールを選定してクエリを生成することも含まれています。

Plugins

Semantic Kernelにおけるプラグインは、ざっくりというとLLMでないコーディングできるツールのことです。Javaなどのプログラミング言語の標準ライブラリで実装できる機能(計算機能など)やAPIなどで提供されている機能(Web検索など)がここに含まれます。プラグインを接続することによって、LLMが苦手とする部分(計算や専門的な情報や最新の情報を取得すること)を補うことができ、得られる結果の信頼性を高めることができます。

実際に作ってみる

ここからは実際に触ってみて、作ってみましょう。なお以下の実行例ではLLMはAzure OpenAIのGPT-3.5-turboを使っています。

構成

まずはコードの構成について確認しておきます。メインとなるコードの構成は、

  1. 必要なパッケージをインポートする
  2. AIサービスを追加する
  3. Enterprise components (Java未実装)
  4. Kernelを構成する
  5. メモリーを追加する
  6. プラグインを追加する
  7. Create kernel arguments
  8. プロンプトを作成する
  9. Planning
  10. Invoke

となっています(Fig. 2)。このうち、実際にいくつかの例を通して構成を見ていきましょう。

Fig. 2 Semantic Kernelのコードの構成

生のGPTを使ってみる

まずは、最低限の構成としてLLMのみを入れ込んだSemantic Kernelを作ってみましょう。最初に全体のコードを示しておきます。

Main.javaの全体
Main.java
package org.example;

import java.util.Scanner;

import com.azure.ai.openai.OpenAIAsyncClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion;
import com.microsoft.semantickernel.orchestration.InvocationContext;
import com.microsoft.semantickernel.orchestration.ToolCallBehavior;
import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
import com.microsoft.semantickernel.services.chatcompletion.ChatHistory;

public class Main {

    // 環境変数の読み込み
    private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY");
    private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT");
    private static final String MODEL_ID = System.getenv("MODEL_ID");

    public static void main(String[] args) {

        // Azure OpenAIクライアントの作成
        OpenAIAsyncClient client = new OpenAIClientBuilder()
                .credential(new AzureKeyCredential(AZURE_CLIENT_KEY))
                .endpoint(CLIENT_ENDPOINT)
                .buildAsyncClient();

        // ChatCompletionServiceインスタンスを作成
        ChatCompletionService chatCompletionService = OpenAIChatCompletion.builder()
                .withModelId(MODEL_ID)
                .withOpenAIAsyncClient(client)
                .build();
        
        // カーネルの作成
        Kernel kernel = Kernel.builder()
                .withAIService(ChatCompletionService.class, chatCompletionService)
                .build();

        // ChatHistoryインスタンスを作成
        ChatHistory history = new ChatHistory();

        // 会話を開始
        System.out.print("User > ");
        Scanner scanner = new Scanner(System.in);
        String userInput;
        while (!(userInput = scanner.nextLine()).isEmpty()) {
            // ユーザーの入力を会話履歴に追加
            history.addUserMessage(userInput);


            // AIからの応答を取得
            var reply = chatCompletionService.getChatMessageContentsAsync(history, kernel, null)
                    .block();

            StringBuilder message = new StringBuilder();
            reply.forEach(chatMessageContent -> message.append(chatMessageContent.getContent()));

            System.out.println("Assistant > " + message);

            // エージェントからのメッセージを履歴に追加
            history.addAssistantMessage(message.toString());

            // 再度ユーザー入力を取得
            System.out.print("User > ");
        }

        scanner.close();
    }
}

初めに、Mavenを用いて必要なパッケージ(Semantic KernelとAzureOpenAI)をインポートします。

1. パッケージのインポート
pom.xml(一部)
<dependencyManagement>
    <dependency>
        <groupId>com.microsoft.semantic-kernel</groupId>
        <artifactId>semantickernel-bom</artifactId>
        <version>1.2.0</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>com.microsoft.semantic-kernel</groupId>
        <artifactId>semantickernel-api</artifactId>
    </dependency>
    <dependency>
        <groupId>com.microsoft.semantic-kernel</groupId>
        <artifactId>semantickernel-aiservices-openai</artifactId>
    </dependency>
    <dependency>
        <groupId>com.azure</groupId>
        <artifactId>azure-ai-openai</artifactId>
        <version>1.0.0-beta.8</version>
    </dependency>
</dependencies>

メインのコードでも忘れずインポートしましょう。

Main.java
import com.azure.ai.openai.OpenAIAsyncClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.ai.openai.models.ChatChoice;
import com.azure.ai.openai.models.ChatCompletions;
import com.azure.ai.openai.models.ChatCompletionsOptions;
import com.azure.ai.openai.models.ChatRequestAssistantMessage;
import com.azure.ai.openai.models.ChatRequestMessage;
import com.azure.ai.openai.models.ChatRequestSystemMessage;
import com.azure.ai.openai.models.ChatRequestUserMessage;
import com.azure.ai.openai.models.ChatResponseMessage;
import com.azure.core.credential.AzureKeyCredential;

次に、AIサービスを構成しましょう。ここではAzure OpenAIを使います。また、必要な認証情報(APIキーやエンドポイントなど)は環境変数に格納しておきましょう。

2. AIサービスを構成する
Main.java
OpenAIAsyncClient client;
        client = new OpenAIClientBuilder()
                .credential(new AzureKeyCredential(AZURE_CLIENT_KEY))
                .endpoint(CLIENT_ENDPOINT)
                .buildAsyncClient();


ChatCompletionService chatCompletion = new OpenAIChatCompletion.builder()
                .withModelId(MODEL_ID)
                .withOpenAIAsyncClient(client)
                .build();

ChatCompletionServiceはLLMとチャットを行うためのオブジェクトです。
次に、カーネルを構成します。今回は、このLLMだけを入れ込めば大丈夫です。

4. Kernelを構成する
Main.java
Kernel kernel = new Kernel.builder()
                .withAIService(ChatCompletionService.class, chatCompletion)
                .build();

GPTを利用するだけなら、ステップ5 ~ 9は必要ありません。最後に、結果を生成できるようにします。

10. Invoke
Main.java
userInput = scanner.nextLine();
// Add user input
history.addUserMessage(userInput);

// Prompt AI for response to users input
List<ChatMessageContent<?>> results = chatCompletionService
    .getChatMessageContentsAsync(history, kernel, null)
    .block();

ここでchatのデータ(ユーザーの入力とLLMの出力は)はHistoryオブジェクトに格納されます。これで、最低限の構成でGPTを使うSemantic Kernelができました。次に、このコードを実行してみましょう。すると、

User > 昼ご飯の献立候補を出して
Assistant > 以下は、昼ご飯の献立候補です:

1. からあげとサラダ - 揚げたてのからあげを主菜に、サラダや野菜のサイドディッシュを添えます。
2. 野菜カレーとナン - 野菜たっぷりのカレーを作り、インド風のナンと一緒にいただきます。
3. パスタとグリーンサラダ - お好みのパスタソース(ミートソース、ペスカトーレなど)と、シンプルなグリーンサラダを組み合わせます。
4. オムライスと味噌汁 - オムライスにはお好きな具材を使い、味噌汁は具材やお好みでアレンジします。
5. 冷やし中華とギョーザ - 夏にぴったりの冷やし中華と、焼き餃子をセットで楽しめます。

これらの候補からお好きなものを選んで、昼ご飯の献立にしてください。
User > 

というようにChatGPTが回答してくれます。これで、最低限のSemantic Kernelの作成ができました。

プロンプトを追加した制御

実際に、AIプロダクトを使いたいとなった場合にはただチャットがしたいというよりも「何のために使いたい」という目的がはっきりしている場合が多いと思います。 このような場合は、あらかじめ情報をプロンプトとして入力しておくことができます。 プロンプトを追加するには、ChatHistoryオブジェクトにプロンプトを追加するだけです。

8. プロンプトを追加する
Main.java
history.addSystemMessage("""
                あなたは日本料理のスペシャリストです。あなたの役割は献立アドバイザーとして
                お客様に昼ご飯の献立を提案することです。ユーザーが材料を指定するので、その材料を使った日本の家庭料理を提案してください。
                """);

全体のコードはこのようになります。材料を入力すると、それに合わせた献立を提案してくれます。

Main.javaの全体
Main.java
package org.example;

import java.util.Scanner;

import com.azure.ai.openai.OpenAIAsyncClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion;
import com.microsoft.semantickernel.orchestration.InvocationContext;
import com.microsoft.semantickernel.orchestration.ToolCallBehavior;
import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
import com.microsoft.semantickernel.services.chatcompletion.ChatHistory;

public class Main {

    // 環境変数の読み込み
    private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY");
    private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT");
    private static final String MODEL_ID = System.getenv("MODEL_ID");

    public static void main(String[] args) {

        // Azure OpenAIクライアントの作成
        OpenAIAsyncClient client = new OpenAIClientBuilder()
                .credential(new AzureKeyCredential(AZURE_CLIENT_KEY))
                .endpoint(CLIENT_ENDPOINT)
                .buildAsyncClient();

        // ChatCompletionServiceインスタンスを作成
        ChatCompletionService chatCompletionService = OpenAIChatCompletion.builder()
                .withModelId(MODEL_ID)
                .withOpenAIAsyncClient(client)
                .build();

        // カーネルの作成
        Kernel kernel = Kernel.builder()
                .withAIService(ChatCompletionService.class, chatCompletionService)
                .build();

        // ChatHistoryインスタンスを作成
        ChatHistory history = new ChatHistory();

+       // システムプロンプトの追加
+       history.addSystemMessage("""
+               あなたは日本料理のスペシャリストです。あなたの役割は献立アドバイザーとして
+               お客様に昼ご飯の献立を提案することです。ユーザーが材料を指定するので、その材料を使った日本の家庭料理を提案してください。
+               """);

+       System.out.println("献立を提案します。材料を入力してください。");
        // 会話を開始
        System.out.print("User > ");
        Scanner scanner = new Scanner(System.in);
        String userInput;
        while (!(userInput = scanner.nextLine()).isEmpty()) {
            // ユーザーの入力を会話履歴に追加
            history.addUserMessage(userInput);


            // AIからの応答を取得
            var reply = chatCompletionService.getChatMessageContentsAsync(history, kernel, null)
                    .block();

            StringBuilder message = new StringBuilder();
            reply.forEach(chatMessageContent -> message.append(chatMessageContent.getContent()));

            System.out.println("Assistant > " + message);

            // エージェントからのメッセージを履歴に追加
            history.addAssistantMessage(message.toString());

            // 再度ユーザー入力を取得
            System.out.print("User > ");
        }

        scanner.close();
    }
}

実行してみると、以下のようになります。

献立を提案します。材料を入力してください。
User > にんじん
Assistant > お客様がにんじんを使いたいということですね。にんじんは日本料理でよく使われる具材の一つです。以下の献立を提案させていただきます。

1. にんじんサラダ:にんじんを千切りにし、醤油、ごま油、酢などで和えたサラダです。爽やかな味わいで、食欲をそそります。

2. 天ぷら:にんじんを薄くスライスし、衣をつけて揚げた天ぷらです。サクサクとした食感が楽しめます。

3. にんじんと鶏肉の煮物:にんじんを薄い輪切りにし、鶏肉と一緒に煮込んだ煮物です。優しい味わいで、栄養もたっぷり摂ることができます。

4. 野菜炒め:にんじんと他のお好みの野菜を炒める料理です。ソースや醤油で味付けし、ご飯と一緒にいただくとおいしいです。

以上の提案はいかがでしょうか?他にもにんじんを使った様々な料理がありますので、お好みに合わせてお選びください。
User > 

Pluginの追加

ここまでは、GPTのみを使ったSemantic Kernelを作成しました。しかし、GPTはあくまで自然言語処理に特化したモデルであり、計算やデータベースアクセスなどの処理には向いていません。 そこで、プラグインを追加して、GPTの弱点を補いましょう。 ここでは、プラグインとして、簡単な計算を行うプラグインを追加してみます。
まずはプラグインとなる関数を作成します。Calculatorクラスを作成し、四則演算のメソッドを追加しましょう。

プラグインの作成
Calculator.java
package plugins;

import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction;

public class Calculator {

    @DefineKernelFunction(name = "Add", description = "A function that adds two numbers.")
    public double add(double a, double b) {
        return a + b;
    }

    @DefineKernelFunction(name = "Subtract", description = "A function that subtracts two numbers.")
    public double subtract(double a, double b) {
        return a - b;
    }

    @DefineKernelFunction(name = "Multiply", description = "A function that multiplies two numbers.")
    public double multiply(double a, double b) {
        return a * b;
    }

    @DefineKernelFunction(name = "Divide", description = "A function that divides two numbers.")
    public double divide(double a, double b) {
        return a / b;
    }
}

プラグイン用の関数を作る際には、通常通りメソッドやクラスを実装するのに加えて、@DefineKernelFunctionアノテーションを追加します。このアノテーションは、Semantic Kernelがプラグインとして認識するために必要です。アノテーションの中には関数の名前nameと関数の説明descriptionを指定します。こうすることでGPTがどの関数を使うべきなのかを判断してくれます。こうして作ったプラグインクラスをMainクラスに追加します。

6. プラグインの追加
Main.java
+ var calculatorPlugin = KernelPluginFactory.createFromObject(new Calculator(), "Calculator");
  
  // カーネルの作成
  Kernel kernel = Kernel.builder()
          .withAIService(ChatCompletionService.class, chatCompletionService)
+         .withPlugin(calculatorPlugin)
          .build();

プラグインを追加するには、KernelPluginFactory.createFromObjectメソッドを使います。このメソッドは、プラグインとなるオブジェクトとプラグインの名前を引数に取ります。このメソッドで作ったプラグインオブジェクトをKernelに追加しておきましょう。
さらに、プラグインを使えるようにするには、GPTの応答にプラグインを使う旨を伝える必要があります。これは、InvocationContextオブジェクトを使って行います。

7. Create kernel arguments
Main.java
+ InvocationContext invocationContext = InvocationContext.builder()
+         .withPluginName("Calculator")
+         .build();

忘れずに、getChatMessageContentsAsyncメソッドにInvocationContextオブジェクトを追加しておきましょう。

10. Invoke
Main.java
- var reply = chatCompletionService.getChatMessageContentsAsync(history, kernel, null)
+ var reply = chatCompletionService.getChatMessageContentsAsync(history, kernel, invocationContext)
        .block();

これで、プラグインを使ったSemantic Kernelが完成しました。試しに難しい計算をしてみましょう。

プラグインなし
User > 452908*31998
Assistant > The product of 452908 and 31998 is 14,498,807,784.
User > 
プラグインあり
User > 452908*31998
Assistant > null1.4492150184E10The result of multiplying 452,908 by 31,998 is 14,492,150,184.
User > 

(https://storage.googleapis.com/zenn-user-upload/ea58d93e893e-20240913.png)
Fig. 3 452908×31998の正しい計算結果
プラグインを使うことで、GPTの出力が正しくなりました(Fig. 3)。ただ、回答の前に謎の文字列"null1.4492150184E10"が入っています。これは一体何なのでしょうか?
AIの出力をロールとともに出力するようにするため、次のように変更します。

Main.java
  StringBuilder message = new StringBuilder();
- reply.forEach(chatMessageContent -> message.append(chatMessageContent.getContent()));
+ reply.forEach(chatMessageContent -> message.append(chatMessageContent.getAuthorRole().toString() + " " + chatMessageContent.getContent() + "\n"));

すると、出力は以下のように変化します。

User > 452908*31998
Assistant > assistant null
tool 1.4492150184E10
assistant The result of multiplying 452,908 by 31,998 is 14,492,150,184.

User > 

これは、getAuthorRole()メソッドを使って、AIの出力がどのプラグインによるものかを示すようにしたものです。これをみる限り、Kernelを通してAssistant(LLM)とtoolがやりとりしており、Semantic Kernelが確かにFig. 1のような形で動作していることがわかりました。
なお、最終的な結果だけが必要なら

Main.java
- StringBuilder message = new StringBuilder();
- reply.forEach(chatMessageContent -> message.append(chatMessageContent.getAuthorRole().toString() + " " + chatMessageContent.getContent() + "\n"));

+ String message = reply.get(reply.size() - 1).getContent();

とすれば、最終的な結果だけが得られます。

外部APIの利用

プラグインとして、外部のAPIを利用することもできます。今回は、Wolfram Alpha APIのShort Answers APIを使って、数式を計算するプラグインを作成してみましょう。まずは、Wolfram AlphaのAPIを使うためのクラスを作成します。

WolframAlphaAPI.java
WolframAlphaAPI.java
package Plugins;

import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction;
import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.*;
import java.nio.charset.StandardCharsets;

public class WolframAlphaAPI {

    private static final String APP_ID = System.getenv("WOLFRAM_ALPHA_APP_ID");
    private static final String API_ENDPOINT = "https://api.wolframalpha.com/v1/result";


    @DefineKernelFunction(name = "wolfram_alpha", description = "A function that asks the Wolfram Alpha API a question. You can use it for advanced mathematical calculation and/or obtain scientific knowledge.")
    public static String getAnswer(
            @KernelFunctionParameter(name = "question", description = "The question to ask the Wolfram Alpha API.") String question
    ) throws IOException {
        String encodedQuestion = URLEncoder.encode(question, StandardCharsets.UTF_8.toString());
        String urlString = API_ENDPOINT + "?appid=" + APP_ID + "&i=" + encodedQuestion;

        URL url = new URL(urlString);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");

        BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        String inputLine;
        StringBuilder response = new StringBuilder();

        while ((inputLine = in.readLine()) != null) {
            response.append(inputLine);
        }
        in.close();

        return response.toString();
    }
}

同様に、プラグインを追加することで、Wolfram AlphaのAPIを使うことができます。

Main.java
        // ChatCompletionServiceインスタンスを作成
        ChatCompletionService chatCompletionService = OpenAIChatCompletion.builder()
                .withModelId(MODEL_ID)
                .withOpenAIAsyncClient(client)
                .build();

        var calculatorPlugin = KernelPluginFactory.createFromObject(new Calculator(), "Calculator");
+       var wolframPlugin = KernelPluginFactory.createFromObject(new WolframAlphaAPI(), "Wolfram");

        // InvocationContextインスタンスを作成
        InvocationContext invocationContext = InvocationContext.builder()
                .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true))
                .build();

        // カーネルの作成
        Kernel kernel = Kernel.builder()
                .withAIService(ChatCompletionService.class, chatCompletionService)
                .withPlugin(calculatorPlugin)
+               .withPlugin(wolframPlugin)
                .build();

        // ChatHistoryインスタンスを作成
        ChatHistory history = new ChatHistory();

これで、Wolfram AlphaのAPIを通して、それらの知識を利用できるようになりました。試しに、以下のようなタスクを入力してみましょう。

User > e^x sin(x) を x = 0から 2π まで積分してください
Assistant > e^x sin(x)をx=0から2πまで積分した結果は、-e^π sinh(π)です。
User > ニューヨークと東京の距離は?
Assistant > ニューヨークと東京の距離は約10879キロメートルです。

以上のようにWolfram Alphaを用いて正しく計算や知識の獲得ができています。これで、外部APIを使ったSemantic Kernelの作成ができました。

Azure AI Search を活用したRAGの実装

最後に、Azure AI Searchを使って、Retrieval Augmented Generation (RAG)を実装してみましょう。
RAGは、GPTのような生成モデルと、Azure AI Searchのような検索モデルを組み合わせたモデルで、より高度な情報検索が可能です。例えば、社内文章や昨日発表されたプレスリリースなどにLLMはアクセスすることができません。RAGでは自分で作成したデータベースを検索してからその情報を元にLLMが回答を生成することで、自分たちが必要な情報を精度高く抽出できます。
まずは、Azure AI Searchのインデックスを作成する必要がありますが、その方法は同僚の西村さんが書かれた記事に詳しく書かれています。
https://zenn.dev/givery_ai_lab/articles/e8c81625f3d0e3
ここでは、Azure AI Searchのインデックスを作成したとして、そのインデックスを使ってRAGを実装してみましょう。
まずは、Azure AI Searchを使うためのクラスを作成します。

AzureAISearch.java
AzureAISearch.java
package Plugins;

import java.util.ArrayList;
import java.util.List;
import com.azure.core.credential.AzureKeyCredential;
import com.azure.search.documents.SearchClient;
import com.azure.search.documents.SearchClientBuilder;
import com.azure.search.documents.SearchDocument;
import com.azure.search.documents.models.SearchResult;
import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction;
import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter;

public class AzureAISearch {

    private static final String SEARCH_API_KEY = System.getenv("SEARCH_API_KEY");
    private static final String SEARCH_URL_ENDPOINT = System.getenv("SEARCH_URL_ENDPOINT");
    private static final String SEARCH_INDEX_NAME = System.getenv("SEARCH_INDEX_NAME");

    @DefineKernelFunction(name = "Search", description = "A function that searches internal documents about press data of Givery Inc. using Azure AI Search.")
    public String searchAsync(@KernelFunctionParameter(name = "userQuery", description = "The string type query used for the search") String userQuery) {

        // SearchClientを作成
        SearchClient searchClient = new SearchClientBuilder()
                .endpoint(SEARCH_URL_ENDPOINT)
                .credential(new AzureKeyCredential(SEARCH_API_KEY))
                .indexName(SEARCH_INDEX_NAME)
                .buildClient();

        SearchResults results = new SearchResults();

        try {
            for (SearchResult result : searchClient.search(userQuery)) {
                results.addResult(result.getDocument(SearchDocument.class));
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "Error occurred during search: " + e.getMessage();
        }

        return results.toString();
    }

    public class SearchResults {

        private List<SearchDocument> results = new ArrayList<>();

        public List<SearchDocument> getResults() {
            return this.results;
        }

        public void addResult(SearchDocument result) {
            this.results.add(result);
        }

        @Override
        public String toString() {
            if (results.isEmpty()) {
                return "No results found.";
            }
            StringBuilder result = new StringBuilder();
            for (SearchDocument document : results) {
                result.append("[").append(document.get("chunk")).append("]");
            }
            return result.toString();
        }
    }

    public static void main(String[] args) {
        AzureAISearch azureAISearch = new AzureAISearch();
        System.out.println(azureAISearch.searchAsync("Tech Innovators Inc."));
    }
}

注意すべき点としては@DefineKernelFunctionアノテーション内のdescriptionにはどんな文書が入っているかをある程度説明しておくことをおすすめします。
今回はギブリーのプレスリリースをpdf化してindexに格納しておきます。

次に、このプラグインをMainクラスに追加します。追加方法は今までと全く同じです。

Main.java
Main.java
        var calculatorPlugin = KernelPluginFactory.createFromObject(new Calculator(), "Calculator");
        var wolframPlugin = KernelPluginFactory.createFromObject(new WolframAlphaAPI(), "Wolfram");
+       var aiSearchPlugin = KernelPluginFactory.createFromObject(new AzureAISearch(), "AzureAISearch");

        // InvocationContextインスタンスを作成
        InvocationContext invocationContext = InvocationContext.builder()
                .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true))
                .build();

        // カーネルの作成
        Kernel kernel = Kernel.builder()
                .withAIService(ChatCompletionService.class, chatCompletionService)
                .withPlugin(calculatorPlugin)
                .withPlugin(wolframPlugin)
+               .withPlugin(aiSearchPlugin)
                .build();

これで、Azure AI Searchを使ったRAGを実装することができました。
ここで、Semantic Kernelの仕組み上、AI searchで帰ってきた検索結果全てをLLMにインプットして答えを返してくれます。検索結果はドキュメントそのものが帰ってくるので結構長い文章です。そのため、gpt-35-turboではコンテクストウィンドウがしばしば不足してしまい、ちゃんとした答えを返してくれないので、比較的コンテクストウィンドウの大きいモデル(gpt-4oなど)を用いることをおすすめします。今回はこのセクションでだけgpt-4oを用いています。
試しに、以下のようなタスクを入力してみましょう。

User > マーケGAIのマルチLLMで使用可能なLLMモデルを教えて
Assistant > マーケGAI(マーケジーエーアイ)の「マルチLLM」機能は、以下のLLMモデルを利用可能としています:

### 現在利用可能なLLMモデル
1. **GPT-4o**
2. **GPT-4o mini**
3. **Claude 3.5 sonnet**

### 今後対応予定のLLMモデル
1. **Gemini**
2. **Azure OpenAI Service**
3. **OpenAI o1**

この機能により、ユーザーはコンテンツ生成業務に最適なモデルを選択することができます。

以上のように、プレスリリースの情報をもとに結果を返してくれるようになりました。

まとめ

Semantic Kernel for Javaでは、Javaを用いてGPTなどの生成モデルとAI Searchや計算機などのプラグインを簡単に結合してユースケースに応じたAIを作成することができます。この記事では、Semantic Kernelの基本的な使い方からプラグインの追加、外部APIの利用、RAGの実装までを紹介しました。Semantic Kernel for Javaは現在も開発が進められており、頻繁に機能が更新・追加されて使いやすくなっています。これを使うことで、Javaでもより高度なAIを作成することができるので、ぜひ活用してみてください。
また、Semantic Kernelについてより詳しく知りたい方はMicrosoftの開発者の方が詳細を書かれたブログがありますので、ぜひ参考にしてください。
https://qiita.com/takashiuesaka/items/299c64bb3c5873a470b9

また、生成AIを活用したPoCや支援にご興味があれば、以下リンクよりお問い合わせください。
https://givery.co.jp/lp/ai-lab/

脚注
  1. カーネルについて ↩︎

Givery AI Lab

Discussion