JavaでGenAIアプリケーションをテストするための有望な方法論

プログラミングの広大な世界において、生成人工知能(GenAI)の時代はターニングポイントとなり、開発者に多くの可能性を開きました。

LangChain4j や Spring AI などのツールは、Java での GenAI アプリケーションの作成へのアクセスを民主化し、Java 開発者がこの魅力的な世界に飛び込むことを可能にしました。 例えば、Langchain4jでは、大規模言語モデル(LLM)の設定と操作が非常に簡単になりました。 次のJavaコード・スニペットについて考えてみます。

public static void main(String[] args) {
    var llm = OpenAiChatModel.builder()
            .apiKey("demo")
            .modelName("gpt-3.5-turbo")
            .build();
    System.out.println(llm.generate("Hello, how are you?"));
}

この例は、開発者がJavaアプリケーション内でLLMをすばやくインスタンス化する方法を示しています。 APIキーを使用してモデルを構成し、モデル名を指定するだけで、開発者はすぐにテキスト応答の生成を開始できます。 このアクセシビリティは、Javaコミュニティ内でイノベーションと探求を促進するために極めて重要です。 それ以上に、ローカルで実行できる幅広いモデルや、埋め込みを保存したり、セマンティック検索を実行したりするためのさまざまなベクターデータベースがあります。

しかし、このような進歩にもかかわらず、人工知能を組み込んだアプリケーションのテストの難しさという永続的な課題に直面しています。 この点については、まだまだ探求・発展の余地がある分野のようです。

この記事では、GenAIアプリケーションのテストに有望な方法論を紹介します。

2400x1260 2024 genai

プロジェクト概要

サンプル プロジェクトは、質問に答えることができる 2 つの AI エージェントと対話するための API を提供するアプリケーションに焦点を当てています。 

AIエージェントは、人工知能を使用して人間のような相互作用と応答をシミュレートし、自律的にタスクを実行するように設計されたソフトウェアエンティティです。 

このプロジェクトでは、1 人のエージェントが LLM にすでに含まれている直接的な知識を使用し、もう 1 人のエージェントが内部ドキュメントを活用して、検索拡張生成 (RAG) を通じて LLM を強化します。 このアプローチにより、エージェントは受け取った入力に基づいて、正確でコンテキストに関連する回答を提供できます。

RAGに関する技術的な詳細は、他の場所で十分な情報が得られるため、省略することを好みます。 この例では、RAG の特定のバリアントを使用しており、情報検索用の埋め込みを生成して保存する従来のプロセスを簡略化しています。

このプロジェクトでは、ドキュメントをチャンクに分割してそれらのチャンクを埋め込む代わりに、LLMを使用してドキュメントの要約を生成します。 埋め込みは、その概要に基づいて生成されます。

ユーザーが質問を書くと、質問の埋め込みが生成され、要約の埋め込みに対してセマンティック検索が実行されます。 一致するものが見つかった場合、ユーザーのメッセージは元のドキュメントで拡張されます。

この方法では、ドキュメント チャンクの構成に対処したり、取得するチャンクの数を設定したり、ユーザーのメッセージを補強する方法が理にかなっているかどうかを心配したりする必要はありません。 ユーザが求めている内容が記載されたドキュメントがある場合、それはLLMに送信されるメッセージに含まれます。

テクニカルスタック

このプロジェクトはJavaで開発されており、 Testcontainers とLangChain4jを備えたSpring Bootアプリケーションを利用しています。

プロジェクトの設定については、「 テストコンテナを使用したローカル開発環境 」および「 Spring Bootアプリケーションのテスト」および「テストコンテナを使用した開発」で概説されている手順に従いました。

また、 Tescontainers Desktop を使用して、データベースへのアクセスを容易にし、生成された埋め込みを検証し、コンテナログを確認します。

テストの課題

本当の課題は、言語モデルによって生成された応答をテストしようとするときに発生します。 従来は、応答に特定のキーワードが含まれていることを確認することで解決できましたが、これは不十分でエラーが発生しやすくなっています。

static String question = "How I can install Testcontainers Desktop?";
@Test
    void verifyRaggedAgentSucceedToAnswerHowToInstallTCD() {
        String answer  = restTemplate.getForObject("/chat/rag?question={question}", ChatController.ChatResponse.class, question).message();
        assertThat(answer).contains("https://testcontainers.com/desktop/");
    }

このアプローチは脆弱であるだけでなく、応答の関連性や一貫性を評価する能力も欠いています。

別の方法としては、コサイン類似性を使用して「参照」応答の埋め込みと実際の応答を比較し、より意味的な形式の評価を提供することです。 

この方法では、2つのベクトル/埋め込み間の角度の余弦を計算することにより、それらの間の類似度を測定します。 両方のベクトルが同じ方向を向いている場合、「参照」応答は実際の応答と意味的に同じであることを意味します。

static String question = "How I can install Testcontainers Desktop?";
static String reference = """
       - Answer must indicate to download Testcontainers Desktop from https://testcontainers.com/desktop/
       - Answer must indicate to use brew to install Testcontainers Desktop in MacOS
       - Answer must be less than 5 sentences
       """;
@Test
    void verifyRaggedAgentSucceedToAnswerHowToInstallTCD() {
        String answer  = restTemplate.getForObject("/chat/rag?question={question}", ChatController.ChatResponse.class, question).message();
        double cosineSimilarity = getCosineSimilarity(reference, answer);
        assertThat(cosineSimilarity).isGreaterThan(0.8);
    }

ただし、この方法では、評価プロセスの不透明度に加えて、応答の受容性を判断するための適切なしきい値を選択するという問題が生じます。

より効果的な方法を目指して

ここでの本当の問題は、LLMによって提供される答えが自然言語であり、非決定論的であるという事実から生じます。 このため、現在のテスト方法を使用して検証することは困難ですが、これらの方法は予測可能な値のテストに適しているためです。 

しかし、自然言語で非決定論的な答えを理解するための優れたツール、つまりLLM自体はすでにあります。 したがって、鍵は、1つのLLMを使用して、別のLLMによって生成された応答の妥当性を評価することにある可能性があります。 

この提案では、詳細な検証基準を定義し、LLMを「バリデータエージェント」として使用して、応答が指定された要件を満たしているかどうかを判断する必要があります。 このアプローチは、一般的な知識と専門的な情報の両方を利用して、特定の質問に対する回答を検証するために適用できます

詳細な指示と例を組み込むことで、Validator Agent は正確で正当な評価を提供し、応答が正しいか正しくないかを明確にすることができます。

static String question = "How I can install Testcontainers Desktop?";
    static String reference = """
            - Answer must indicate to download Testcontainers Desktop from https://testcontainers.com/desktop/
            - Answer must indicate to use brew to install Testcontainers Desktop in MacOS
            - Answer must be less than 5 sentences
            """;

    @Test
    void verifyStraightAgentFailsToAnswerHowToInstallTCD() {
        String answer  = restTemplate.getForObject("/chat/straight?question={question}", ChatController.ChatResponse.class, question).message();
        ValidatorAgent.ValidatorResponse validate = validatorAgent.validate(question, answer, reference);
        assertThat(validate.response()).isEqualTo("no");
    }

    @Test
    void verifyRaggedAgentSucceedToAnswerHowToInstallTCD() {
        String answer  = restTemplate.getForObject("/chat/rag?question={question}", ChatController.ChatResponse.class, question).message();
        ValidatorAgent.ValidatorResponse validate = validatorAgent.validate(question, answer, reference);
        assertThat(validate.response()).isEqualTo("yes");
    }

LLMがユーザーの質問に対してより良い代替案を提案する必要がある、より複雑な応答をテストすることもできます。

static String question = "How I can find the random port of a Testcontainer to connect to it?";
    static String reference = """
            - Answer must not mention using getMappedPort() method to find the random port of a Testcontainer
            - Answer must mention that you don't need to find the random port of a Testcontainer to connect to it
            - Answer must indicate that you can use the Testcontainers Desktop app to configure fixed port
            - Answer must be less than 5 sentences
            """;

    @Test
    void verifyRaggedAgentSucceedToAnswerHowToDebugWithTCD() {
        String answer  = restTemplate.getForObject("/chat/rag?question={question}", ChatController.ChatResponse.class, question).message();
        ValidatorAgent.ValidatorResponse validate = validatorAgent.validate(question, answer, reference);
        assertThat(validate.response()).isEqualTo("yes");
    }

バリデーターエージェント

バリデーターエージェントの設定は、他のエージェントの設定と変わりません。 これは、LangChain4j AI Serviceと特定の手順のリストを使用して構築されています。

public interface ValidatorAgent {
    @SystemMessage("""
                ### Instructions
                You are a strict validator.
                You will be provided with a question, an answer, and a reference.
                Your task is to validate whether the answer is correct for the given question, based on the reference.
                
                Follow these instructions:
                - Respond only 'yes', 'no' or 'unsure' and always include the reason for your response
                - Respond with 'yes' if the answer is correct
                - Respond with 'no' if the answer is incorrect
                - If you are unsure, simply respond with 'unsure'
                - Respond with 'no' if the answer is not clear or concise
                - Respond with 'no' if the answer is not based on the reference
                
                Your response must be a json object with the following structure:
                {
                    "response": "yes",
                    "reason": "The answer is correct because it is based on the reference provided."
                }
                
                ### Example
                Question: Is Madrid the capital of Spain?
                Answer: No, it's Barcelona.
                Reference: The capital of Spain is Madrid
                ###
                Response: {
                    "response": "no",
                    "reason": "The answer is incorrect because the reference states that the capital of Spain is Madrid."
                }
                """)
    @UserMessage("""
            ###
            Question: {{question}}
            ###
            Answer: {{answer}}
            ###
            Reference: {{reference}}
            ###
            """)
    ValidatorResponse validate(@V("question") String question, @V("answer") String answer, @V("reference") String reference);

    record ValidatorResponse(String response, String reason) {}
}

ご覧のとおり、 Few-Shot Prompting を使用して、予想される応答についてLLMをガイドしています。 また、応答をオブジェクトに解析しやすくするために応答の JSON 形式を要求し、判定の根拠をよりよく理解するために、回答の理由を含める必要があることを指定します。

結論

GenAIアプリケーションの進化に伴い、高度な人工知能によって生成される応答の複雑さと微妙さを効果的に評価できるテスト方法を開発するという課題が生じています。 

LLMをバリデーターエージェントとして使用するという提案は、人工知能の分野におけるソフトウェア開発と評価の新時代への道を開く有望なアプローチを表しています。 時間の経過とともに、現在の課題を克服し、これらの革新的なテクノロジーの可能性を最大限に引き出すことができるイノベーションがさらに増えることを期待しています。

さらに詳しく