AIエージェント活用実践編単一エージェント実装 — ReAct ループの基本

最小 QA エージェント完走 — 実装と動作確認

無料公開レッスン / 読了目安 13


search_wikipedia ツールとメインループの構造

まず、search_wikipedia ツールの定義を考えましょう。これは、第2レッスンで学んだ Tool 定義の原則に従います。

{
  "name": "search_wikipedia",
  "description": "Wikipedia で特定のキーワードに関する情報を検索します。事実確認や一般的な知識の取得に役立ちます。",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "検索したいキーワード。"
      }
    },
    "required": ["query"]
  }
}

次に、このツールと LLM を連携させるメインループの基本的な構造を考えます。 ここでは、Anthropic Claude の tool_use 機能を利用することを想定します。

import anthropic
import json

# Claude APIクライアントの初期化 (ch01で設定済みを想定)
# client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

def execute_tool(tool_name: str, tool_input: dict) -> str:
    """
    ツール実行をシミュレートする関数。
    実際にはここでWikipedia APIなどを呼び出す。
    """
    if tool_name == "search_wikipedia":
        query = tool_input.get("query")
        # ここで実際のWikipedia API呼び出しを行う
        # 例: wikipedia-apiライブラリを使う
        # import wikipedia
        # try:
        #     page = wikipedia.page(query, auto_suggest=False)
        #     return page.summary
        # except wikipedia.exceptions.PageError:
        #     return "Wikipediaで該当するページが見つかりませんでした。"
        # 簡単のため、ここではダミーの応答を返す
        if "ReAct" in query:
            return "ReActは、LLMが推論(Reasoning)と行動(Acting)を組み合わせることで、複雑なタスクをより効果的に解決するためのフレームワークです。Thought-Action-Observationのループを繰り返します。"
        elif "東京タワー" in query:
            return "東京タワーは、東京都港区芝公園にある総合電波塔です。1958年に竣工し、高さは333メートルです。主にテレビやラジオの電波を送信しています。"
        else:
            return "検索結果はありません。"
    else:
        raise ValueError(f"Unknown tool: {tool_name}")

def run_qa_agent(user_question: str, max_iterations: int = 5):
    conversation_history = []
    tools = [
        {
          "name": "search_wikipedia",
          "description": "Wikipedia で特定のキーワードに関する情報を検索します。事実確認や一般的な知識の取得に役立ちます。",
          "input_schema": {
            "type": "object",
            "properties": {
              "query": {
                "type": "string",
                "description": "検索したいキーワード。"
              }
            },
            "required": ["query"]
          }
        }
    ]

    for i in range(max_iterations):
        messages = conversation_history + [{"role": "user", "content": user_question}] if not conversation_history else conversation_history

        response = client.messages.create(
            model="claude-3-opus-20240229", # または Sonnet, Haiku
            max_tokens=1000,
            messages=messages,
            tools=tools,
            tool_choice={"type": "auto"}
        )

        if response.stop_reason == "tool_use":
            tool_use = response.content[0]
            tool_name = tool_use.name
            tool_input = tool_use.input

            print(f"Thought: LLM decided to use tool '{tool_name}' with input {tool_input}")
            
            # 会話履歴にLLMのツール使用の意図を追加
            conversation_history.append({"role": "assistant", "content": response.content})

            try:
                observation = execute_tool(tool_name, tool_input)
                print(f"Observation: Tool returned '{observation}'")
                
                # 会話履歴にツールの実行結果を追加
                conversation_history.append({"role": "user", "content": [{"type": "tool_result", "tool_use_id": tool_use.id, "content": observation}]})
            except Exception as e:
                print(f"Observation: Tool execution failed with error: {e}")
                conversation_history.append({"role": "user", "content": [{"type": "tool_result", "tool_use_id": tool_use.id, "content": f"Error: {e}"}]})
                # エラー発生時はエージェントを停止するか、ユーザーに報告する
                return f"エラーが発生しました: {e}"

        elif response.stop_reason == "end_turn":
            # LLMが最終回答を生成したと判断
            final_answer = response.content[0].text
            print(f"Final Answer: {final_answer}")
            return final_answer
        else:
            print(f"Unexpected stop reason: {response.stop_reason}")
            print(f"LLM content: {response.content}")
            return "申し訳ありませんが、質問に答えることができませんでした。"

    return "最大イテレーション数に達しました。途中までの情報しか提供できません。"

# 実際の使い方
# print(run_qa_agent("ReActとは何ですか?"))
# print(run_qa_agent("東京タワーの高さは何メートルですか?"))

上記のコードは、client.messages.createtool_use を返した場合、そのツールを実行し、結果を tool_result として LLM に返すという ReAct ループの基本的な流れを示しています。 max_iterations は、無限ループを防ぐための重要な安全装置です。

正常系の動作確認

このエージェントが意図した通りに動作するかを確認するには、いくつかの質問を投げかけてみましょう。

  • 「ReActとは何ですか?」
    • search_wikipedia が呼び出され、その結果に基づいて回答が生成されることを期待します。
  • 「東京タワーの高さは何メートルですか?」
    • 同様に search_wikipedia が呼び出され、回答が生成されることを期待します。
  • 「存在しない概念について教えてください」
    • search_wikipedia が呼び出されるが、適切な結果が得られず、LLM がその状況を認識して「見つかりませんでした」と回答するか、別の対応をとることを期待します。

これらのテストを通じて、エージェントがツールを適切に選択し、その結果を解釈してユーザーに分かりやすい形で回答できるかを確認します。

まとめ

本レッスンでは、ReAct ループの知識を統合し、Wikipedia 検索と要約を行う最小限の QA エージェントの設計と実装の概略を学びました。 search_wikipedia のような外部ツールと LLM の推論能力を組み合わせることで、エージェントは自律的に情報を収集し、ユーザーの質問に答えることができます。 この経験は、より複雑なエージェントを構築するための重要な第一歩となるでしょう。

参考リンク


最小 QA エージェント完走 — 実装と動作確認 | AIエージェント活用実践編 第1章 - AI研修