1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReActエージェントで書籍検索する機能を実装してみた

Last updated at Posted at 2026-01-17

はじめに

今回はReActエージェントを使って質問内容を推論し回答する機能を紹介していきます。
【参考】こちらの記事で作成したベクトルDBを使った書籍検索ツールです。

概要

対象の書籍をDBに保存して、その内容をチャット形式で検索できる機能。
対象としているのはiOS関連の専門書(約400ページ程)。
書籍:PDFファイル

ReActとは

実装説明に入る前に、まずはReActフレームワークについて軽く説明します。
ReAct(Reason and Act: 推論と行動)とは、与えられたプロンプトについて1~3を繰り返し、複雑な問いに対しても精度高い回答を実現したフレームワークです。

  1. 推論
    プロンプトの内容に対して思考し、Toolsに定義されたどのToolを実行するか判断する。
  2. 行動
    1で判断したToolを実行する。
  3. 観察
    行動の結果(検索内容や計算結果)を確認して観察記録を作成し、別のToolを使用すべきか次の推論が行われる。

ReActの推論は反復最大回数に達するか、最終回答を意味する文字列(ex.'Final Answer')が現れるまで続ける。

image.png

ReActエージェントによる回答生成

回答生成までの流れは以下。

  1. ベクトルDBの読み込み
  2. データ取得方法の定義
  3. Toolsの定義
  4. ReActを実践するプロンプトテンプレートの作成
  5. ReActエージェントの作成と呼び出し

実装例では対象ソースのみ記載します。
もし手元で動かすのであれば以下のimport文は先に定義しておいてください。

from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_classic.tools.retriever import create_retriever_tool
from langchain_core.prompts import PromptTemplate
from langchain_classic.agents import AgentExecutor, create_react_agent
from langchain_core.tools import Tool

1. ベクトルDBの読み込み

保存したベクトルDB(ChromaDB)の読み込みを行う。
検索内容をベクトル化してベクトルDBと数値比較を行うためにOpenAIEmbeddingsでテキスト(検索内容)をベクトル化する。

実装例

# 保存済みChroma DBの読み込み
# OpenAIEmbeddingsでinputの内容をベクトル数値化する
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma(
    persist_directory="./chroma_db", 
    # ベクトル化したinputの数値とDBに保存されている数値を比較する
    embedding_function=embeddings
)

2. データ取得方法の定義

次にベクトルDBからどのように情報を取得するのか取得方法を定義します。

プロンプトに関連する内容を取得する方法をリトリーバル(Retrieval)と言う。RAG(Retrieval Augmented Generation)の「R」の部分ですね。

ベクトル検索の設定一覧

ベクトルDB検索で利用するアルゴリズムを search_type に、パラメータをsearch_kwargs に設定します。

項目 指定値 (例) 概要 設定可能なパラメータ (search_kwargs)
search_type "similarity" 類似度検索(デフォルト)
質問ベクトルに最も近いドキュメントを単純に上位から取得します。
k: 取得するドキュメント数
search_type "mmr" 最大周辺関連性 (MMR)
類似度だけでなく、取得する情報の「多様性」を考慮して、内容の重複を避けます。
k: 最終的な出力数
fetch_k: 最初に抽出する候補数
lambda_mult: 多様性の重み (0~1)
search_type "similarity_score_threshold" 類似度スコアしきい値
設定したスコア以上の類似度を持つドキュメントのみを取得します。
k: 最大取得数
score_threshold: 最小類似度スコア (0~1)

今回はアルゴリズムにMMR(Maximum Marginal Relevance)を使います。
MMRとは
情報検索やレコメンデーションの分野で使われる多様性を意識したランキング調整アルゴリズム。今回の書籍検索に適したアルゴリズムだと思います。

実装例

# 検索の設定
# MMR(Maximum Marginal Relevance)を使用して関連性と多様性のバランスを取る
# - search_type="mmr": 類似性が高く、かつ多様な結果を取得
# - k=10: 最終的に返す文書数
# - fetch_k=20: MMRアルゴリズムが考慮する候補文書数(より広範囲から選択)
# - lambda_mult=0.7: 関連性(1.0)と多様性(0.0)のバランス(0.7は関連性重視)
retriever = vectorstore.as_retriever(
    search_type="mmr",
    search_kwargs={
        "k": 10,
        "fetch_k": 20,
        "lambda_mult": 0.7
    }
)

3. Toolsの定義

ReActフレームワークにおけるToolsとは
推論→行動→観察の流れの中で「行動」フェーズにおいて実行される処理。
推論フェーズでToolsリストの中から実行すべきToolを選択し、行動フェーズで実行されます。
Toolsには実際にエージェントに実行してほしい処理群を定義します。
今回は書籍検索、検索結果の参照元ページの情報を取得するToolを定義します。

実装例

# 標準のMMR検索ツール(多様性重視)
tool_mmr = create_retriever_tool(
    retriever,
    "search_design_pattern_book_mmr",
    "多様な視点でiOSデザインパターン書籍を検索(幅広い情報収集向け)"
)

# 類似度スコアフィルタリング付きツール(精度重視)
search_func = create_enhanced_search_function(vectorstore)
tool_scored = Tool(
    name="search_design_pattern_book",
    func=search_func,
    description="""「iOSデザインパターン」の書籍内容を高精度で検索する専用ツールです。

【必ず使用すべき状況】
- iOSデザインパターン(Factory、Observer、Singleton、MVC、MVVMなど)に関する質問
- 本書の特定の章・セクション・ページの内容確認
- コード例、実装方法、ベストプラクティスの確認
- パターンのメリット・デメリット、使い分けの説明
- 図表やサンプルコードの詳細
- 書籍に記載されている具体的な情報が必要な場合

【入力】ユーザーの質問文をそのまま渡してください。
【出力】類似度スコアでフィルタリングされた関連性の高いテキストと参照元ページ情報が返されます。
【重要】このツールから得られた情報のみを使用して正確に回答してください。書籍に記載がない情報は推測せず、「書籍に記載がありません」と明示してください。"""
)

tools = [tool_mmr,tool_scored]
def create_enhanced_search_function(vectorstore):
"""
検索結果を品質チェックして整形するカスタム検索関数
類似度スコアが低い結果を除外し、参照元を明示
"""
def search_with_score(query: str) -> str:
    # 類似度スコア付きで検索(MMRではなく類似度検索を使用)
    results = vectorstore.similarity_search_with_score(query, k=10)

    if not results:
        return "関連する情報が見つかりませんでした。質問を変えてみてください。"

    # 類似度スコアでフィルタリング(スコアが低いほど類似度が高い)
    # 閾値を0.5に設定(調整可能)
    filtered_results = [(doc, score) for doc, score in results if score < 0.5]

    if not filtered_results:
        return f"関連性の高い情報が見つかりませんでした(最高類似度スコア: {results[0][1]:.3f})。より具体的な質問をお願いします。"

    # 結果を整形(上位5件のみ使用、類似度順)
    formatted_results = []
    for i, (doc, score) in enumerate(filtered_results[:5], 1):
        content = doc.page_content
        # メタデータから参照元情報を取得
        metadata = doc.metadata
        source_info = f"[参照元: ページ {metadata.get('page', 'N/A')}]" if 'page' in metadata else ""

        formatted_results.append(
            f"--- 検索結果 {i} (類似度スコア: {score:.3f}) {source_info} ---\n{content}\n"
        )

    return "\n".join(formatted_results)

return search_with_score

4. ReActを実践するプロンプトテンプレートの作成

ReActを実践するためのプロンプトテンプレートをLangChainHubから取得し設定します。
筆者の環境だとLangChainHubからgit pullで取得できなかったので以下からテンプレートを直接取得しローカルにtemplatesフォルダを作り、そこから読み込みさせました。
https://smith.langchain.com/hub/hwchase17/react

with open("templates/hwchase17_react.txt", "r", encoding="utf-8") as f:
        pdf_path = f.read().strip()  # strip()で改行などを除去
    prompt = PromptTemplate.from_template(pdf_path)

5. ReActエージェントの作成

さあ、いよいよReActエージェントの作成です。
エージェント作成にあたりまずはLLMを生成します。
その後、Tools(3で作成)とプロンプト(4で作成)を含めてエージェント作成します。

llm = ChatOpenAI(model="gpt-4o", temperature=0)
# tools: 3で作成
# prompt: 4で作成
agent = create_react_agent(llm, tools, prompt)

エージェント作成した後はエージェントが動く実行環境を定義します。
ここまで定義して初めてReActエージェントが使える状態になります。

# 実行環境(Executor)の構築
# Agentの実際に動くものであるのに対して、AgentExecutorは実行するための環境
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    # AIの処理プロセスを出力(True推奨:デバッグと精度検証に必須)
    verbose=True,
    # 最大試行回数を定義(無限ループ防止)
    max_iterations=10,
    # 出力時にパースエラーになった際には再処理を行う
    handle_parsing_errors=True,
    # タイムアウト設定(秒):長時間実行の防止
    max_execution_time=120,
)

これまでに定義したエージェントを実行

# エージェント定義
agent_executor = create_book_agent()
# input に検索内容を記載してexecutorを実行する。
response = agent_executor.invoke({"input": "◯◯について簡潔に教えて"})

補足情報

以下は実装していて筆者が純粋に気になったので参考までに
なぜagentでtoolsを定義したにAgentExecutorで再度定義するのか

  • agentのtools
    ツール説明として使われる。
    AIはこの説明を読んで、「この質問に答えるには search_design_pattern_book のツールが必要だな」とExecutorに伝える。

  • AgentExecutorのtools
    実際の実行コードとして使われる

    1. Agent が「search_design_pattern_book を実行せよ」と伝える
    2. AgentExecutor が tools から該当する名前の関数を探す
    3. AgentExecutor が実際にその関数を動かし、結果(Observation)を Agent に返却する

まとめ

これまでに記載していた実装をまとめたものが以下になります。

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_classic.tools.retriever import create_retriever_tool
from langchain_core.prompts import PromptTemplate
from langchain_classic.agents import AgentExecutor, create_react_agent
from langchain_core.tools import Tool

# 環境変数の読み込み
load_dotenv()

def create_enhanced_search_function(vectorstore):
    """
    検索結果を品質チェックして整形するカスタム検索関数
    類似度スコアが低い結果を除外し、参照元を明示
    """
    def search_with_score(query: str) -> str:
        # 類似度スコア付きで検索(MMRではなく類似度検索を使用)
        results = vectorstore.similarity_search_with_score(query, k=10)

        if not results:
            return "関連する情報が見つかりませんでした。質問を変えてみてください。"

        # 類似度スコアでフィルタリング(スコアが低いほど類似度が高い)
        # 閾値を0.5に設定(調整可能)
        filtered_results = [(doc, score) for doc, score in results if score < 0.5]

        if not filtered_results:
            return f"関連性の高い情報が見つかりませんでした(最高類似度スコア: {results[0][1]:.3f})。より具体的な質問をお願いします。"

        # 結果を整形(上位5件のみ使用、類似度順)
        formatted_results = []
        for i, (doc, score) in enumerate(filtered_results[:5], 1):
            content = doc.page_content
            # メタデータから参照元情報を取得
            metadata = doc.metadata
            source_info = f"[参照元: ページ {metadata.get('page', 'N/A')}]" if 'page' in metadata else ""

            formatted_results.append(
                f"--- 検索結果 {i} (類似度スコア: {score:.3f}) {source_info} ---\n{content}\n"
            )

        return "\n".join(formatted_results)

    return search_with_score

def create_book_agent():
    # 保存済みChroma DBの読み込み
    # OpenAIEmbeddingsでinputの内容をベクトル数値化する
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    vectorstore = Chroma(
        persist_directory="./chroma_db", 
        # ベクトル化したinputの数値とDBに保存されている数値を比較する
        embedding_function=embeddings
    )
    
    # 検索の設定
    # MMR(Maximum Marginal Relevance)を使用して関連性と多様性のバランスを取る
    # - search_type="mmr": 類似性が高く、かつ多様な結果を取得
    # - k=10: 最終的に返す文書数
    # - fetch_k=20: MMRアルゴリズムが考慮する候補文書数(より広範囲から選択)
    # - lambda_mult=0.7: 関連性(1.0)と多様性(0.0)のバランス(0.7は関連性重視)
    retriever = vectorstore.as_retriever(
        search_type="mmr",
        search_kwargs={
            "k": 10,
            "fetch_k": 20,
            "lambda_mult": 0.7
        }
    )

    # 標準のMMR検索ツール(多様性重視)
    tool_mmr = create_retriever_tool(
        retriever,
        "search_design_pattern_book_mmr",
        "多様な視点でiOSデザインパターン書籍を検索(幅広い情報収集向け)"
    )

    # 類似度スコアフィルタリング付きツール(精度重視)
    search_func = create_enhanced_search_function(vectorstore)
    tool_scored = Tool(
        name="search_design_pattern_book",
        func=search_func,
    description="""「iOSデザインパターン」の書籍内容を高精度で検索する専用ツールです。

【必ず使用すべき状況】
- iOSデザインパターン(Factory、Observer、Singleton、MVC、MVVMなど)に関する質問
- 本書の特定の章・セクション・ページの内容確認
- コード例、実装方法、ベストプラクティスの確認
- パターンのメリット・デメリット、使い分けの説明
- 図表やサンプルコードの詳細
- 書籍に記載されている具体的な情報が必要な場合

【入力】ユーザーの質問文をそのまま渡してください。
【出力】類似度スコアでフィルタリングされた関連性の高いテキストと参照元ページ情報が返されます。
【重要】このツールから得られた情報のみを使用して正確に回答してください。書籍に記載がない情報は推測せず、「書籍に記載がありません」と明示してください。"""
)

    tools = [tool_mmr,tool_scored]

    with open("templates/hwchase17_react.txt", "r", encoding="utf-8") as f:
        pdf_path = f.read().strip()  # strip()で改行などを除去
    prompt = PromptTemplate.from_template(pdf_path)

    llm = ChatOpenAI(model="gpt-4o", temperature=0)
    # tools: 3で作成
    # prompt: 4で作成
    agent = create_react_agent(llm, tools, prompt)

    # 実行環境(Executor)の構築
    # Agentの実際に動くものであるのに対して、AgentExecutorは実行するための環境
    agent_executor = AgentExecutor(
        agent=agent,
        tools=tools,
        # AIの処理プロセスを出力(True推奨:デバッグと精度検証に必須)
        verbose=True,
        # 最大試行回数を定義(無限ループ防止)
        max_iterations=10,
        # 出力時にパースエラーになった際には再処理を行う
        handle_parsing_errors=True,
        # タイムアウト設定(秒):長時間実行の防止
        max_execution_time=120,
    )
    
    return agent_executor

# --- 実行セクション ---
if __name__ == "__main__":
    agent_executor = create_book_agent()

    response = agent_executor.invoke({"input": "◯◯について簡潔に教えて"})

最後に

今回は保存したデータをReActエージェントを使ってどうやって検索するのかまとめました。皆様の実装の参考になれば幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?