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

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

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


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クライアントの初期化 (ANTHROPIC_API_KEY 環境変数、または claude login 済みの認証情報を使用)
# 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 = [{"role": "user", "content": user_question}]
    tools = [
        {
          "name": "search_wikipedia",
          "description": "Wikipedia で特定のキーワードに関する情報を検索します。事実確認や一般的な知識の取得に役立ちます。",
          "input_schema": {
            "type": "object",
            "properties": {
              "query": {
                "type": "string",
                "description": "検索したいキーワード。"
              }
            },
            "required": ["query"]
          }
        }
    ]

    for i in range(max_iterations):
        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=1000,
            messages=conversation_history,
            tools=tools,
            tool_choice={"type": "auto"}
        )

        # LLMの応答(思考+ツール呼び出し等)を会話履歴に必ず追加する
        conversation_history.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "tool_use":
            # content 内の tool_use ブロックをすべて処理する(content[0]決め打ちにしない)
            tool_results = []
            for block in response.content:
                if block.type != "tool_use":
                    continue
                tool_name = block.name
                tool_input = block.input
                print(f"Thought: LLM decided to use tool '{tool_name}' with input {tool_input}")

                try:
                    observation = execute_tool(tool_name, tool_input)
                    print(f"Observation: Tool returned '{observation}'")
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": observation,
                    })
                except Exception as e:
                    print(f"Observation: Tool execution failed with error: {e}")
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": f"Error: {e}",
                        "is_error": True,
                    })

            conversation_history.append({"role": "user", "content": tool_results})

        elif response.stop_reason == "end_turn":
            # LLMが最終回答を生成したと判断(text ブロックを連結、content[0]決め打ちにしない)
            final_answer = "".join(
                block.text for block in response.content if block.type == "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 エージェント完走 — 実装と動作確認の図解

関連動画

Claudeが全アプリで情報共有!新AIツールの力とは? #shorts #AI

【超実践】Claude初心者必見!Claudeの最新基本活用と神活用4選を紹介するで!

参考リンク


全レッスン無料公開中 — クイズ・WHY深掘り・実践演習付きのフル版を開けます。フル版で開く →
最小 QA エージェント完走 — 実装と動作確認 | AIエージェント活用実践編 第1章 - AI研修