watsonx.aiを使用してLangGraphとMistral Media 3でSQLエージェントを構築する

著者

Anna Gutowska

AI Engineer, Developer Advocate

IBM

このチュートリでは、カスタムSQLiteデータベースのPythonとSQLのクエリーを実行および生成できるAIエージェントを構築します。エージェントは、LangGraphとMistral Medium 3の大規模言語モデル(LLM)を使ってゼロから構築されます。このLLMは、コーディングやマルチモーダル理解など専門的なユースケース向けに最適化されているため、このタスクに適しています。1また、IBM Granite Guardianモデルを使用して、有害なメッセージを検出してブロックします。

LangGraphとは

LangGraphLangChainによって開発された オープンソースAIエージェント・フレームワークで、複雑な生成AIエージェントのワークフローを構築、展開、管理するように設計されています。ユーザーが大規模言語モデルをスケーラブルかつ効率的に作成、実行、最適化できるようにするツールとライブラリーのセットを提供します。LangGraph は本質的に、グラフベースのアーキテクチャーのパワーを活用して、 AIエージェント・ワークフローのさまざまなコンポーネント(マルチエージェント・システムにあるようなコンポーネントを含む)間の複雑な関係をモデル化し、管理します。

IBM watsonx.ai

LangGraphとwatsonx.aiによるデータの洞察

AIエージェントは、自然言語クエリーを受け取って意味のある出力を提供するための処理を、人間に代わってできるのでしょうか。いくつかのオープンソース・テクノロジーとwatsonx.aiの機能を使用して、この真価を問います。

前提条件

  1. watsonx.ai®プロジェクトを作成するには、IBM® Cloudのアカウントが必要です。

  2. このチュートリアルでは、複数のPythonバージョンを使用できます。公開時点では、最新バージョンのPython 3.13をダウンロードすることをお勧めします。

手順

ステップ1. 環境を設定する

いくつかあるツールの中から選択することもできますが、このチュートリアルでは、Jupyter Notebookを使用するためにIBMアカウントを設定する方法について説明します。

  1. IBM Cloud®アカウントを使用してwatsonx.aiにログインします。

  2. watsonx.aiプロジェクトを作成します。

    プロジェクトIDはプロジェクト内から取得できます。[ 管理 ]タブをクリックし、 全般(General) ページの 詳細(Details) セクションからプロジェクトIDをコピーします。このチュートリアルではこのIDが必要になります。

  3. Jupyter Notebookを作成します。

    このステップでは、このチュートリアルからコードをコピーできるJupyter Notebook環境が開きます。あるいは、このノートブックをローカル・システムにダウンロードし、watsonx.aiプロジェクトにアセットとしてアップロードすることもできます。このチュートリアルは、GitHubでも公開されています。

ステップ2. watsonx.ai RuntimeのインスタンスとAPIキーを設定する

  1. watsonx.ai Runtimeサービス・インスタンスを作成します(適切なリージョンを選択し、無料インスタンスであるLiteプランを選択)。

  2. アプリケーション・プログラミング・インターフェース(API)キーを生成します。

  3. watsonx.ai Runtimeサービス・インスタンスを、watsonx.aiで作成したプロジェクトに関連付けます。

ステップ3. 関連ライブラリーをインストールしてインポートし、認証情報を設定する

このチュートリアルには、いくつかのライブラリとモジュールが必要です。以下のコンポーネントを必ずインポートしてください。インストールされていない場合は、pipをクイックインストールすることで問題が解決されます。

#installations
%pip install -qU ibm-watsonx-ai \
    langchain-ibm \
    langgraph \
    langchain_experimental \
    tavily-python \
    langchain-community

端末を再起動し、次のパッケージをインポートします。

# imports
import sqlite3
import getpass

from ibm_watsonx_ai import APIClient, Credentials
from ibm_watsonx_ai.foundation_models.moderations import Guardian
from IPython.display import Image, display
from langchain import hub  
from langchain_ibm import ChatWatsonx
from langgraph.graph.message import add_messages
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langchain_community.utilities.sql_database import SQLDatabase
from langchain_community.agent_toolkits.sql.toolkit import SQLDatabaseToolkit
from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage, AIMessage
from langchain_core.tools import tool
from langchain_experimental.tools.python.tool import PythonREPLTool
from sqlalchemy import create_engine
from typing_extensions import TypedDict
from typing import Annotated

認証情報を設定するには、ステップ1で生成したWATSONX_APIKEYおよびWATSONX_PROJECT_IDが必要です。また、APIのエンドポイントとして機能するURLも設定します。

WATSONX_APIKEY = getpass.getpass("Please enter your watsonx.ai Runtime API key (hit enter): ")

WATSONX_PROJECT_ID = getpass.getpass("Please enter your project ID (hit enter): ")

URL = "https://us-south.ml.cloud.ibm.com"

LLMを初期化する前に、Credentialsクラスを使用して、渡されたAPI認証情報をカプセル化できます。

credentials = Credentials(url=URL, api_key=WATSONX_APIKEY)

ステップ4. チャット・モデルをインスタンス化する

watsonx.ai Runtimeで使用可能なすべてのリソースと対話できるようにするには、APIClientを設定する必要があります。ここで、認証情報とWATSONX_PROJECT_IDを渡します。

client = APIClient(credentials=credentials, project_id=WATSONX_PROJECT_ID)

このチュートリアルでは、ChatWatsonxラッパーを使用してチャット・モデルを設定します。このラッパーは、ツールの呼び出しとチェーンの統合を簡素化します。詳細については、ChatWatsonxの公式文書のAPIリファレンスをご覧ください。Mistral Media 3のmodel_idとクライアントをパラメーターとして渡すことができます。

別のAPIプロバイダーを使用する場合は、それに応じてラッパーを変更する必要があります。たとえば、OpenAI APIを使用してGPT-4などのモデルにアクセスするには、openai_api_keyChatOpenAIラッパーが必要です。

model_id = "mistralai/mistral-medium-2505" 
llm = ChatWatsonx(model_id=model_id, watsonx_client=client)

ステップ5. SQLiteデータベースを設定する

このチュートリアルでは、エージェントは使用可能なツールを使ってSQLiteデータベースを操作します。PostgreSQLやSQLiteなどのリレーショナル・データベース管理システムにデータセットが保存されている場合は、この手順をスキップしてください。それ以外の場合は、次のセルを実行して、sql_script変数を、自動車ディーラー販売合成データを含むデータベースを生成するスクリプトに設定します。このデータベースには、個々のディーラー、販売、および車両データを保管するための複数のテーブルが含まれます。

sql_script = """
DROP TABLE IF EXISTS [Dealerships];

DROP TABLE IF EXISTS [Cars];

DROP TABLE IF EXISTS [Sales];

CREATE TABLE [Dealerships] (
    [id] INTEGER PRIMARY KEY,
    [name] TEXT NOT NULL,
    [location] TEXT NOT NULL
);

CREATE TABLE [Cars] (
    [id] INTEGER PRIMARY KEY,
    [make] TEXT NOT NULL,
    [model] TEXT NOT NULL,
    [year] INTEGER NOT NULL,
    [color] TEXT NOT NULL
);

CREATE TABLE [Sales] (
    [id] INTEGER PRIMARY KEY,
    [car_id] INTEGER NOT NULL,
    [dealership_id] INTEGER NOT NULL,
    [sale_date] DATE NOT NULL,
    [sale_price] REAL NOT NULL,
    FOREIGN KEY ([car_id]) REFERENCES [Cars] ([id]),
    FOREIGN KEY ([dealership_id]) REFERENCES [Dealerships] ([id])
);


INSERT INTO [Dealerships] ([id], [name], [location]) VALUES
    (1, 'Dealership A', 'New York'),
    (2, 'Dealership B', 'Los Angeles'),
    (3, 'Dealership C', 'Chicago'),
    (4, 'Dealership D', 'Houston'),
    (5, 'Dealership E', 'Phoenix'),
    (6, 'Dealership F', 'Philadelphia'),
    (7, 'Dealership G', 'San Antonio'),
    (8, 'Dealership H', 'San Diego'),
    (9, 'Dealership I', 'Dallas'),
    (10, 'Dealership J', 'San Jose');

INSERT INTO [Cars] ([id], [make], [model], [year], [color]) VALUES
    (1, 'Toyota', 'Camry', 2020, 'Blue'),
    (2, 'Honda', 'Civic', 2019, 'Red'),
    (3, 'Ford', 'Mustang', 2021, 'Black'),
    (4, 'Chevrolet', 'Silverado', 2018, 'White'),
    (5, 'Nissan', 'Altima', 2020, 'Gray'),
    (6, 'Kia', 'Optima', 2020, 'Silver'),
    (7, 'Hyundai', 'Elantra', 2019, 'Black'),
    (8, 'Volkswagen', 'Golf', 2021, 'Red'),
    (9, 'BMW', '3 Series', 2018, 'White'),
    (10, 'Mercedes-Benz', 'C-Class', 2020, 'Gray'),
    (11, 'Audi', 'A4', 2019, 'Blue'),
    (12, 'Lexus', 'ES', 2021, 'Black'),
    (13, 'Toyota', 'Corolla', 2018, 'White'),
    (14, 'Honda', 'Accord', 2020, 'Gray'),
    (15, 'Ford', 'Fusion', 2019, 'Red');

INSERT INTO [Sales] ([id], [car_id], [dealership_id], [sale_date], [sale_price]) VALUES
    (1, 1, 1, '2022-01-01', 25000.0),
    (2, 2, 2, '2022-02-01', 20000.0),
    (3, 3, 3, '2022-03-01', 30000.0),
    (4, 4, 1, '2022-04-01', 40000.0),
    (5, 5, 2, '2022-05-01', 28000.0),
    (6, 6, 4, '2022-06-01', 22000.0),
    (7, 7, 5, '2022-07-01', 20000.0),
    (8, 8, 6, '2022-08-01', 28000.0),
    (9, 9, 7, '2022-09-01', 35000.0),
    (10, 10, 8, '2022-10-01', 32000.0),
    (11, 11, 9, '2022-11-01', 30000.0),
    (12, 12, 10, '2022-12-01', 38000.0),
    (13, 13, 1, '2023-01-01', 25000.0),
    (14, 14, 2, '2023-02-01', 28000.0),
    (15, 15, 3, '2023-03-01', 22000.0);
    
"""

このスクリプトを実行し、ローカルSQLiteデータベースを作成するには、データベースのConnectionオブジェクトを設定する必要があります。「:memory:」パス名を渡すことで、一時的なメモリー内データベースに接続できます。

connection = sqlite3.connect(":memory:")

次に、データベース・カーソルを作成し、SQLスクリプトを実行するために、executescriptメソッドを使用します。このCursorオブジェクトにより、そのようなスクリプトを実行できます。

connection.executescript(sql_script)

アウトプット:

<sqlite3.Cursor at 0x30c511240>

最後に、次のコマンドを実行して、作業ディレクトリー内のデータベース・ファイルに接続するSQLAlechemy Engineインスタンスを作成します。ファイルにはデータベース・ファイル拡張子が付いたJupyter Notebookと同じ名前が必要です。接続がSQLiteローカル・データベースに行われるように、URLは空欄のままにします。creatorパラメーターにConnectionオブジェクトを指定します。このパラメーターは、データベースへの接続を作成します。

engine = create_engine(
    "sqlite://",
    creator=lambda: connection
)

このチュートリアルでは、ローカルに保存されていない別のデータベースを使用したい場合は、空のパスを「sqlite:///Chinook.db」に置き換えるだけで、サンプルのデジタル・メディア・ストア・データベースにアクセスできます。

ステップ6. ツールを設定する

このチュートリアルでは、LangChainの事前構築済みSQLDatabaseToolkitを使用します。このツールキットには、SQLAlchemy Engineをパラメーターとして受け取るSQLDatabaseオブジェクトと、任意で選択するLLMが必要です。ツールキットをインスタンス化すると、そのツールを取得できます。ツールを出力して詳細を確認しましょう。

db = SQLDatabase(engine)
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
tools = toolkit.get_tools()
tools

アウトプット:

[QuerySQLDatabaseTool(description="Input to this tool is a detailed and correct SQL query, output is a result from the database. If the query is not correct, an error message will be returned. If an error is returned, rewrite the query, check the query, and try again. If you encounter an issue with Unknown column 'xxxx' in 'field list', use sql_db_schema to query the correct table fields.", db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x30c3ecd10>),
InfoSQLDatabaseTool(description='Input to this tool is a comma-separated list of tables, output is the schema and sample rows for those tables. Be sure that the tables actually exist by calling sql_db_list_tables first! Example Input: table1, table2, table3', db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x30c3ecd10>),
ListSQLDatabaseTool(db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x30c3ecd10>),
QuerySQLCheckerTool(description='Use this tool to double check if your query is correct before executing it. Always use this tool before executing a query with sql_db_query!', db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x30c3ecd10>, llm=ChatWatsonx(model_id='mistralai/mistral-medium-2505', apikey=SecretStr('**********'), params={}, watsonx_model=<ibm_watsonx_ai.foundation_models.inference.model_inference.ModelInference object at 0x309c34690>, watsonx_client=<ibm_watsonx_ai.client.APIClient object at 0x30c3e3250>), llm_chain=LLMChain(verbose=False, prompt=PromptTemplate(input_variables=['dialect', 'query'], input_types={}, partial_variables={}, template='\n{query}\nDouble check the {dialect} query above for common mistakes, including:\n- Using NOT IN with NULL values\n- Using UNION when UNION ALL should have been used\n- Using BETWEEN for exclusive ranges\n- Data type mismatch in predicates\n- Properly quoting identifiers\n- Using the correct number of arguments for functions\n- Casting to the correct data type\n- Using the proper columns for joins\n\nIf there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\n\nOutput the final SQL query only.\n\nSQL Query: '), llm=ChatWatsonx(model_id='mistralai/mistral-medium-2505', apikey=SecretStr('**********'), params={}, watsonx_model=<ibm_watsonx_ai.foundation_models.inference.model_inference.ModelInference object at 0x309c34690>, watsonx_client=<ibm_watsonx_ai.client.APIClient object at 0x30c3e3250>), output_parser=StrOutputParser(), llm_kwargs={}))]

ツールキットの一部として4つのツールが利用できることがわかります。各ツールは、ツールの説明にあるように、独自の目的を果たします。これらのツールは、データベースを一覧表示し、クエリーを実行し、テーブル・スキーマを返し、実行前にSQLクエリーを検証するように構築されています。

エージェントにPythonコードを生成および実行する機能を提供するために、LangChainのPythonREPLToolクラスを利用できます。このコードは、Python REPL(Read-Eval-Print Loop)ツールを設定し、その機能を定義し、後で使用するためにツールのリストに追加します。

python_repl = PythonREPLTool()
tools.append(python_repl)

SQLツールとデータベースに合わせて調整されたシステム・プロンプトがエージェントに提供されるように、LangChain Hubによって提供されるプロンプトを取得します。プロンプトの詳細を確認するために、system_messageを出力してみましょう。

chatprompttemplate = hub.pull("langchain-ai/sql-agent-system-prompt")  
system_message = chatprompttemplate.format(dialect="SQLite", top_k=5)  
print(system_message)

アウトプット:

System: You are an agent designed to interact with SQL databases.
Given an input question, create a syntactically correct SQLite query to run, then look at the results of the query and return the answer.
Unless the user specifies a specific number of examples they wish to obtain, always limit your query to at most 5 results.
You can order the results by a relevant column to return the most interesting examples in the database.
Never query for all the columns from a specific table, only ask for the relevant columns given the question.
You have access to tools for interacting with the database.
Only use the below tools. Only use the information returned by the below tools to construct your final answer.
You MUST double check your query before executing it. If you get an error while executing a query, rewrite the query and try again.
    
DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.
    
To start you should ALWAYS look at the tables in the database to see what you can query.
Do NOT skip this step.
Then you should query the schema of the most relevant tables.

ステップ7. エージェントの状態を定義する

LangGraph には、ReAct(推論とアクション)アーキテクチャーに準拠したエージェント・グラフを生成する、事前構築されたcreate_react_agent関数があります。このアーキテクチャーにより、エージェントは停止条件が満たされるまでループ内のツールを繰り返し呼び出すことができます。

ReActエージェントのフロー図 Reactエージェントのフロー

より実践的なアプローチを提供するために、このチュートリアルではReActエージェントをゼロから構築します。最初のステップとして、ユーザー、ツール、エージェント自体からのメッセージのコンテキストを保管するためのAgentStateクラスを作成します。ここでは、メッセージが適切な辞書形式であることを確認するために、PythonのTypedDictクラスが使用されます。LangGraphのTypedDictリデューサー関数を使用して、既存のメッセージ・リストに新しいメッセージを追加することもできます。

class AgentState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

ステップ8. ReActAgentクラスを定義する

次に、ReActAgentクラスを作成します。このクラスは、環境の変化に対応できるエージェントを作成するための基本構造を提供します。ReActAgentクラスの関数では、グラフの状態に応じてツールを繰り返し呼び出すことができます。

__init__関数は、大規模言語モデル、ツール、システム・メッセージのクラスの属性をパラメーターとして初期化します。このコンストラクターは、ガーディアン・モデル、LLM、およびツールのノードを含む状態図を作成します。状態図はguardianノードから始まり、guardianノードはLLMとデータベースに到達する前に有害なコンテンツを検知するguardian_moderationメソッドを呼び出します。guardianノードとllmノード間の条件付きエッジは、図の状態をllmノードまたは終端のいずれかにルーティングします。これは、guardian_moderation関数の出力によって決まります。安全なメッセージはllmノードに渡され、call_llmメソッドが実行されます。また、メッセージを適切にルーティングするために、llmノードとtoolsノードの間に条件付きエッジを追加します。LLMがツール呼び出しを返すと、should_call_toolsメソッドはTrueブールを返します。そうでない場合、Falseが返され、図は末尾にルーティングされます。このステップはReActエージェント・アーキテクチャーの一部であり、エージェントがツールの出力を受信し、状態の変化に反応して次のアクションを決定できるようにしたいと考えています。

次に、図をコンパイルすると、後のステップでエージェントを呼び出すことができます。メッセージを固定化するには、MemorySaverチェックポイントを使用します。このメソッドの最後の2行では、クラス・インスタンスのtoolsllm属性を初期化します。tools属性は、ツール名をtoolオブジェクトにマッピングする辞書です。llm属性はLLMであり、bind_toolsメソッドを使用してツールにバインドされます。

class ReActAgent:
    
    def __init__(self, llm, tools, system_message=""):
        memory = MemorySaver()
        graph = StateGraph(AgentState)
        graph.add_node("guardian", self.guardian_moderation)
        graph.add_node("llm", self.call_llm)
        graph.add_node("tools", self.call_tools)
        graph.add_node("block_message", self.block_message)
        graph.add_conditional_edges(
            "guardian",
            lambda state: state["moderation_verdict"],  
            {
                "inappropriate": "block_message",  
                "safe": "llm"           
            }
        )
        graph.add_edge("block_message", END)
        graph.add_conditional_edges(
            "llm",
            self.should_call_tools,
            ["tools", END]
        )
        graph.add_edge("tools", "llm")
        graph.add_edge(START, "guardian") self.system_message= system_message
        self.graph = graph.compile(checkpointer=memory)
        self.tools = {t.name: t for t in tools}
        self.llm = llm.bind_tools(tools)

ReActAgentクラスの次の関数はcall_llmです。この関数は、状態からメッセージを取得してLLMを呼び出します。システム・メッセージが存在する場合、メソッドはそれをメッセージ・リストの先頭に追加します。その後、メッセージを使用してLLMが呼び出され、LLM応答を含む新しい状態が返されます。

def call_llm(self, state: AgentState):
    messages = state['messages']
    if self.system_message:
        messages = [SystemMessage(content=self.system_message)] + messages
    message = self.llm.invoke(messages)
    return {'messages': [message]}

call_tools関数は、ReActAgentクラスの次の関数です。このメソッドは、状態内の最後のメッセージからツールの呼び出しを取得し、それを反復処理して、指定された引数を使用して各ツールを呼び出します。次に、各ツール呼び出しの結果が、resultsというリストに保管されます。最後に、この新しい状態は辞書の形式で返され、メッセージ・キーが結果リストにマッピングされます。

def call_tools(self, state: AgentState):
    tool_calls = state['messages'][-1].tool_calls
    results = []
    for t in tool_calls:
        result = self.tools[t['name']].invoke(t['args'])
        results.append(ToolMessage(tool_call_id=t['id'], 
                                    name=t['name'], 
                                    content=str(result)))
    return {'messages': results}

ReActAgentクラスの次の関数はshould_call_toolsです。この関数は、状態から以前のLLM応答を取得し、ツール呼び出しが含まれているかどうかを確認することで、状態に基づいてツールを呼び出すかどうかを決定します。

def should_call_tools(self, state: AgentState):
    result = state['messages'][-1]
    return "tools" if len(result.tool_calls) > 0 else END

guardrainノードで実行されるguardian_moderation関数は、不要なコンテンツや機密性の高いコンテンツを検出してブロックするために、guardianシステムを使用してメッセージをモダナイズするように設計されています。まず、最新のメッセージが取得されます。次に、detectorという名前の辞書が定義されます。これには、検出器の構成とそのしきい値が含まれています。これらの検出器は、個人情報(PII)やヘイト・スピーチ、侮辱や冒涜(HAP)など、メッセージ内の特定の種類のコンテンツを識別します。次に、Guardianクラスのインスタンスが作成され、clientというapi_clientオブジェクトとdetector辞書が渡されます。Guardianインスタンスのdetectメソッドが呼び出され、最後のメッセージの内容とdetectors辞書が渡されます。このメソッドは、Granite Guardianモデルの出力に応じて、moderation_verdictキーが「safe」または「inappropriate」のいずれかの値を保管する辞書を返します。

def guardian_moderation(self, state: AgentState):
    message = state['messages'][-1]  
    detectors = {
        "granite_guardian": {"threshold": 0.4},
        "hap": {"threshold": 0.4},
        "pii": {},
    }
    guardian = Guardian(
        api_client=client,  
        detectors=detectors 
    )
    response = guardian.detect(
        text=message.content,
        detectors=detectors
    )
    if len(response['detections']) != 0 and response['detections'][0]['detection'] == "Yes":
        return {"moderation_verdict": "inappropriate"}
    else:
        return {"moderation_verdict": "safe"}

block_message関数は通知メカニズムとして機能し、入力されたクエリーに不適切なコンテンツが含まれており、ブロックされたことをユーザーに通知します。

def block_message(self, state: AgentState):
    return {"messages": [AIMessage(content="This message has been blocked due to inappropriate content.")]}

これらすべてのコードをまとめて、次のセルを実行できます。

class ReActAgent:
    
    def __init__(self, llm, tools, system_message=""):
        memory = MemorySaver()
        graph = StateGraph(AgentState)
        graph.add_node("guardian", self.guardian_moderation)
        graph.add_node("llm", self.call_llm)
        graph.add_node("tools", self.call_tools)
        graph.add_node("block_message", self.block_message)
        graph.add_conditional_edges(
            "guardian",
            lambda state: state["moderation_verdict"],  
            {
                "inappropriate": "block_message",  
                "safe": "llm"           
            }
        )
        graph.add_edge("block_message", END)
        graph.add_conditional_edges(
            "llm",
            self.should_call_tools,
            ["tools", END]
        )
        graph.add_edge("tools", "llm")
        graph.add_edge(START, "guardian")
        self.system_message = system_message
        self.graph = graph.compile(checkpointer=memory)
        self.tools = {t.name: t for t in tools}
        self.llm = llm.bind_tools(tools)
    
    def call_llm(self, state: AgentState):
        messages = state['messages']
        if self.system_message:
            messages = [SystemMessage(content=self.system_message)] + messages
        message = self.llm.invoke(messages)
        return {'messages': [message]}
    
    def call_tools(self, state: AgentState):
        tool_calls = state['messages'][-1].tool_calls
        results = []
        for t in tool_calls:
            result = self.tools[t['name']].invoke(t['args'])
            results.append(ToolMessage(tool_call_id=t['id'], 
                                       name=t['name'], 
                                       content=str(result)))
        return {'messages': results}
    
    def should_call_tools(self, state: AgentState):
        result = state['messages'][-1]
        return "tools" if len(result.tool_calls) > 0 else END
    
    def guardian_moderation(self, state: AgentState):
        message = state['messages'][-1]  
        detectors = {
            "granite_guardian": {"threshold": 0.4},
            "hap": {"threshold": 0.4},
            "pii": {},
        }
        guardian = Guardian(
            api_client=client,  
            detectors=detectors 
        )
        response = guardian.detect(
            text=message.content,
            detectors=detectors
        )
        if len(response['detections']) != 0 and response['detections'][0]['detection'] == "Yes":
            return {"moderation_verdict": "inappropriate"}
        else:
            return {"moderation_verdict": "safe"}
        
    def block_message(self, state: AgentState):
        return {"messages": [AIMessage(content="This message has been blocked due to inappropriate content.")]}

ステップ9. ReActAgentオブジェクトを作成して呼び出す

次のコード・ブロックの1行目は、ReActAgentクラスのインスタンスを作成し、LLM、SQLツール、システム・メッセージをパラメーターとして渡します。次に、図の状態をメモリーに保管するスレッドを指定します。各thread_idは、新しいチャット・ウィンドウを表すものだと考えてください。また、ユーザー入力を任意の文字列に定義することもできます。次に、ユーザー入力で構成されるリストをHumanMessageタイプの形式で渡して、エージェントを呼び出します。

まず、Granite Guardianモデルによってブロックされる必要があるプロンプトを試してみましょう。

agent = ReActAgent(llm, tools, system_message=system_message)

config = {"configurable": {"thread_id": "1"}}

user_input = "What is the home address of the customer who purchased the most expensive car last month?"

result = agent.graph.invoke({'messages': [HumanMessage(content=user_input)]}, config)

for message in result["messages"]:
    message.pretty_print()

アウトプット:

    ================================ [1m Human Message  [0m=================================
    
    What is the home address of the customer who purchased the most expensive car last month?
    ================================== [1m Ai Message  [0m==================================
    
    This message has been blocked due to inappropriate content.

Granite Guardianモデルは、ユーザーが機密のクライアント情報を要求することを適切にブロックすることができました。会話が終了する前にグラフがLLMノードに到達しなかったことがわかります。次に、別のスレッドで適切な質問をしてみましょう。例えば、「2022年の業績上位5社の総売上高はいくらか」というユーザー・インプットを試します。

user_input = "What is the total sales revenue for the top 5 performing dealerships in the year 2022?"

config2 = {"configurable": {"thread_id": "2"}}

result = agent.graph.invoke({'messages': [HumanMessage(content=user_input)]}, config2)

for message in result["messages"]:
    message.pretty_print()

アウトプット:

    ================================ [1m Human Message  [0m=================================

What is the total sales revenue for the top 5 performing dealerships in the year 2022?
================================== [1m Ai Message  [0m==================================
Tool Calls:
  sql_db_list_tables (H2irrlsPM)
 Call ID: H2irrlsPM
  Args:
    tool_input:
================================= [1m Tool Message  [0m=================================
Name: sql_db_list_tables

Cars, Dealerships, Sales
================================== [1m Ai Message  [0m==================================
Tool Calls:
  sql_db_schema (ILZWiZvGX)
 Call ID: ILZWiZvGX
  Args:
    table_names: Cars, Dealerships, Sales
================================= [1m Tool Message  [0m=================================
Name: sql_db_schema


CREATE TABLE "Cars" (
	id INTEGER, 
	make TEXT NOT NULL, 
	model TEXT NOT NULL, 
	year INTEGER NOT NULL, 
	color TEXT NOT NULL, 
	PRIMARY KEY (id)
)

/*
3 rows from Cars table:
id	make	model	year	color
1	Toyota	Camry	2020	Blue
2	Honda	Civic	2019	Red
3	Ford	Mustang	2021	Black
*/


CREATE TABLE "Dealerships" (
	id INTEGER, 
	name TEXT NOT NULL, 
	location TEXT NOT NULL, 
	PRIMARY KEY (id)
)

/*
3 rows from Dealerships table:
id	name	location
1	Dealership A	New York
2	Dealership B	Los Angeles
3	Dealership C	Chicago
*/


CREATE TABLE "Sales" (
	id INTEGER, 
	car_id INTEGER NOT NULL, 
	dealership_id INTEGER NOT NULL, 
	sale_date DATE NOT NULL, 
	sale_price REAL NOT NULL, 
	PRIMARY KEY (id), 
	FOREIGN KEY(dealership_id) REFERENCES "Dealerships" (id), 
	FOREIGN KEY(car_id) REFERENCES "Cars" (id)
)

/*
3 rows from Sales table:
id	car_id	dealership_id	sale_date	sale_price
1	1	1	2022-01-01	25000.0
2	2	2	2022-02-01	20000.0
3	3	3	2022-03-01	30000.0
*/
================================== [1m Ai Message  [0m==================================
Tool Calls:
  sql_db_query_checker (yIZ0tk4VP)
 Call ID: yIZ0tk4VP
  Args:
    query: SELECT Dealerships.name, SUM(Sales.sale_price) AS total_sales_revenue FROM Dealerships INNER JOIN Sales ON Dealerships.id = Sales.dealership_id WHERE strftime('%Y', Sales.sale_date) = '2022' GROUP BY Dealerships.id ORDER BY total_sales_revenue DESC LIMIT 5;
================================= [1m Tool Message  [0m=================================
Name: sql_db_query_checker

SELECT Dealerships.name, SUM(Sales.sale_price) AS total_sales_revenue
FROM Dealerships
INNER JOIN Sales ON Dealerships.id = Sales.dealership_id
WHERE strftime('%Y', Sales.sale_date) = '2022'
GROUP BY Dealerships.id
ORDER BY total_sales_revenue DESC
LIMIT 5;
================================== [1m Ai Message  [0m==================================
Tool Calls:
  sql_db_query (cTzJFfvTl)
 Call ID: cTzJFfvTl
  Args:
    query: SELECT Dealerships.name, SUM(Sales.sale_price) AS total_sales_revenue FROM Dealerships INNER JOIN Sales ON Dealerships.id = Sales.dealership_id WHERE strftime('%Y', Sales.sale_date) = '2022' GROUP BY Dealerships.id ORDER BY total_sales_revenue DESC LIMIT 5;
================================= [1m Tool Message  [0m=================================
Name: sql_db_query

[('Dealership A', 65000.0), ('Dealership B', 48000.0), ('Dealership J', 38000.0), ('Dealership G', 35000.0), ('Dealership H', 32000.0)]
================================== [1m Ai Message  [0m==================================

The total sales revenue for the top 5 performing dealerships in the year 2022 are:

1. Dealership A: $65,000
2. Dealership B: $48,000
3. Dealership J: $38,000
4. Dealership G: $35,000
5. Dealership H: $32,000

================================ [1m Human Message  [0m=================================

What is the total sales revenue for the top 5 performing dealerships in the year 2022?
================================== [1m Ai Message  [0m==================================
Tool Calls:
  sql_db_list_tables (H2irrlsPM)
 Call ID: H2irrlsPM
  Args:
    tool_input:
================================= [1m Tool Message  [0m=================================
Name: sql_db_list_tables

Cars, Dealerships, Sales
================================== [1m Ai Message  [0m==================================
Tool Calls:
  sql_db_schema (ILZWiZvGX)
 Call ID: ILZWiZvGX
  Args:
    table_names: Cars, Dealerships, Sales
================================= [1m Tool Message  [0m=================================
Name: sql_db_schema


CREATE TABLE "Cars" (
	id INTEGER, 
	make TEXT NOT NULL, 
	model TEXT NOT NULL, 
	year INTEGER NOT NULL, 
	color TEXT NOT NULL, 
	PRIMARY KEY (id)
)

/*
3 rows from Cars table:
id	make	model	year	color
1	Toyota	Camry	2020	Blue
2	Honda	Civic	2019	Red
3	Ford	Mustang	2021	Black
*/


CREATE TABLE "Dealerships" (
	id INTEGER, 
	name TEXT NOT NULL, 
	location TEXT NOT NULL, 
	PRIMARY KEY (id)
)

/*
3 rows from Dealerships table:
id	name	location
1	Dealership A	New York
2	Dealership B	Los Angeles
3	Dealership C	Chicago
*/


CREATE TABLE "Sales" (
	id INTEGER, 
	car_id INTEGER NOT NULL, 
	dealership_id INTEGER NOT NULL, 
	sale_date DATE NOT NULL, 
	sale_price REAL NOT NULL, 
	PRIMARY KEY (id), 
	FOREIGN KEY(dealership_id) REFERENCES "Dealerships" (id), 
	FOREIGN KEY(car_id) REFERENCES "Cars" (id)
)

/*
3 rows from Sales table:
id	car_id	dealership_id	sale_date	sale_price
1	1	1	2022-01-01	25000.0
2	2	2	2022-02-01	20000.0
3	3	3	2022-03-01	30000.0
*/
================================== [1m Ai Message  [0m==================================
Tool Calls:
  sql_db_query_checker (yIZ0tk4VP)
 Call ID: yIZ0tk4VP
  Args:
    query: SELECT Dealerships.name, SUM(Sales.sale_price) AS total_sales_revenue FROM Dealerships INNER JOIN Sales ON Dealerships.id = Sales.dealership_id WHERE strftime('%Y', Sales.sale_date) = '2022' GROUP BY Dealerships.id ORDER BY total_sales_revenue DESC LIMIT 5;
================================= [1m Tool Message  [0m=================================
Name: sql_db_query_checker

SELECT Dealerships.name, SUM(Sales.sale_price) AS total_sales_revenue
FROM Dealerships
INNER JOIN Sales ON Dealerships.id = Sales.dealership_id
WHERE strftime('%Y', Sales.sale_date) = '2022'
GROUP BY Dealerships.id
ORDER BY total_sales_revenue DESC
LIMIT 5;
================================== [1m Ai Message  [0m==================================
Tool Calls:
  sql_db_query (cTzJFfvTl)
 Call ID: cTzJFfvTl
  Args:
    query: SELECT Dealerships.name, SUM(Sales.sale_price) AS total_sales_revenue FROM Dealerships INNER JOIN Sales ON Dealerships.id = Sales.dealership_id WHERE strftime('%Y', Sales.sale_date) = '2022' GROUP BY Dealerships.id ORDER BY total_sales_revenue DESC LIMIT 5;
================================= [1m Tool Message  [0m=================================
Name: sql_db_query

[('Dealership A', 65000.0), ('Dealership B', 48000.0), ('Dealership J', 38000.0), ('Dealership G', 35000.0), ('Dealership H', 32000.0)]
================================== [1m Ai Message  [0m==================================

The total sales revenue for the top 5 performing dealerships in the year 2022 are:

1. Dealership A: $65,000
2. Dealership B: $48,000
3. Dealership J: $38,000
4. Dealership G: $35,000
5. Dealership H: $32,000

成功です。エージェントは、複数のツールを呼び出してSQLテーブル・スキーマを抽出すること、新しいSQLクエリーを生成すること、および実行前にその正確性を検証することを含む一連のタスクを正常に実行しました。その結果、エージェントは2022年のデータベース内の業績上位5社の自動車販売店の正しい総売上高を返します。エージェントが生成した各SQL Queryを印刷する際に、エージェントのマルチステップの推論を確認できます。応答では、Granite Guardianモデルがユーザーのクエリーが適切であると判断したこともわかります。

フォローアップ質問をしてみましょう。今回は、エージェントにPythonコードを生成して実行し、結果の棒グラフを表示するように指示します。同じthread_idを使用しているため、エージェントは以前のメッセージを取得する必要があります。

user_input = "Write Python code to plot these results on a bar graph. Then, you must execute the code and display the bar graph."

result = agent.graph.invoke({'messages': [HumanMessage(content=user_input)]}, config2)

for message in result["messages"]:
    message.pretty_print()

アウトプット:

================================ [1m Human Message  [0m=================================
    
    What is the total sales revenue for the top 5 performing dealerships in the year 2022?
================================== [1m Ai Message  [0m==================================
    Tool Calls:
      sql_db_list_tables (H2irrlsPM)
     Call ID: H2irrlsPM
      Args:
        tool_input:
================================= [1m Tool Message  [0m=================================
    Name: sql_db_list_tables
    
    Cars, Dealerships, Sales
================================== [1m Ai Message  [0m==================================
    Tool Calls:
      sql_db_schema (ILZWiZvGX)
     Call ID: ILZWiZvGX
      Args:
        table_names: Cars, Dealerships, Sales
================================= [1m Tool Message  [0m=================================
    Name: sql_db_schema
    
    
    CREATE TABLE "Cars" (
    	id INTEGER, 
    	make TEXT NOT NULL, 
    	model TEXT NOT NULL, 
    	year INTEGER NOT NULL, 
    	color TEXT NOT NULL, 
    	PRIMARY KEY (id)
    )
    
    /*
    3 rows from Cars table:
    id	make	model	year	color
    1	Toyota	Camry	2020	Blue
    2	Honda	Civic	2019	Red
    3	Ford	Mustang	2021	Black
    */
    
    
    CREATE TABLE "Dealerships" (
    	id INTEGER, 
    	name TEXT NOT NULL, 
    	location TEXT NOT NULL, 
    	PRIMARY KEY (id)
    )
    
    /*
    3 rows from Dealerships table:
    id	name	location
    1	Dealership A	New York
    2	Dealership B	Los Angeles
    3	Dealership C	Chicago
    */
    
    CREATE TABLE "Sales" (
    	id INTEGER, 
    	car_id INTEGER NOT NULL, 
    	dealership_id INTEGER NOT NULL, 
    	sale_date DATE NOT NULL, 
    	sale_price REAL NOT NULL, 
    	PRIMARY KEY (id), 
    	FOREIGN KEY(dealership_id) REFERENCES "Dealerships" (id), 
    	FOREIGN KEY(car_id) REFERENCES "Cars" (id)
    )
    
    /*
    3 rows from Sales table:
    id	car_id	dealership_id	sale_date	sale_price
    1	1	1	2022-01-01	25000.0
    2	2	2	2022-02-01	20000.0
    3	3	3	2022-03-01	30000.0
    */
================================== [1m Ai Message  [0m==================================
    Tool Calls:
      sql_db_query_checker (yIZ0tk4VP)
     Call ID: yIZ0tk4VP
      Args:
        query: SELECT Dealerships.name, SUM(Sales.sale_price) AS total_sales_revenue FROM Dealerships INNER JOIN Sales ON Dealerships.id = Sales.dealership_id WHERE strftime('%Y', Sales.sale_date) = '2022' GROUP BY Dealerships.id ORDER BY total_sales_revenue DESC LIMIT 5;
================================= [1m Tool Message  [0m=================================
    Name: sql_db_query_checker
    
    SELECT Dealerships.name, SUM(Sales.sale_price) AS total_sales_revenue
    FROM Dealerships
    INNER JOIN Sales ON Dealerships.id = Sales.dealership_id
    WHERE strftime('%Y', Sales.sale_date) = '2022'
    GROUP BY Dealerships.id
    ORDER BY total_sales_revenue DESC
    LIMIT 5;
================================== [1m Ai Message  [0m==================================
    Tool Calls:
      sql_db_query (cTzJFfvTl)
     Call ID: cTzJFfvTl
      Args:
        query: SELECT Dealerships.name, SUM(Sales.sale_price) AS total_sales_revenue FROM Dealerships INNER JOIN Sales ON Dealerships.id = Sales.dealership_id WHERE strftime('%Y', Sales.sale_date) = '2022' GROUP BY Dealerships.id ORDER BY total_sales_revenue DESC LIMIT 5;
================================= [1m Tool Message  [0m=================================
    Name: sql_db_query
    
    [('Dealership A', 65000.0), ('Dealership B', 48000.0), ('Dealership J', 38000.0), ('Dealership G', 35000.0), ('Dealership H', 32000.0)]
================================== [1m Ai Message  [0m==================================
    
    The total sales revenue for the top 5 performing dealerships in the year 2022 are:
    
    1. Dealership A: $65,000
    2. Dealership B: $48,000
    3. Dealership J: $38,000
    4. Dealership G: $35,000
    5. Dealership H: $32,000
================================ [1m Human Message  [0m=================================
    
    Write Python code to plot these results on a bar graph. Then, you must execute the code and display the bar graph.
================================== [1m Ai Message  [0m==================================
    Tool Calls:
      Python_REPL (5X1fPIODL)
     Call ID: 5X1fPIODL
      Args:
        query: import matplotlib.pyplot as plt
    
    # Data for total sales revenue
    dealerships = ['Dealership A', 'Dealership B', 'Dealership J', 'Dealership G', 'Dealership H']
    sales_revenue = [65000, 48000, 38000, 35000, 32000]
    
    # Create a bar graph
    plt.bar(dealerships, sales_revenue, color='blue')
    
    # Customize the plot
    plt.title('Total Sales Revenue for Top 5 Performing Dealerships in 2022')
    plt.xlabel('Dealerships')
    plt.ylabel('Total Sales Revenue ($)')
    plt.xticks(rotation=45)
    plt.grid(axis='y')
    
    # Display the plot
    plt.tight_layout()
    plt.show()
================================= [1m Tool Message  [0m=================================
    Name: Python_REPL
    
    
================================== [1m Ai Message  [0m==================================
    
    Your requirements have been fulfilled. Code has been executed in repl.
    The bar graph has been plotted and shown in the python REPL.
    The graph can also be seen below:
2022年の業績上位5社に位置するディーラーの総売上高

予想したとおり、エージェントはpython_replツールを正常に呼び出してPythonコードを生成、実行し、結果をグラフィカルに表現しました。エージェントが適切なx軸とy軸の値、ラベル、タイトルを決定することもできたことを確認してください。これは、AIエージェントを従来のLLMチャットボットと区別するエージェント的な推論を強調するものです。

エージェントのグラフを視覚的に表現するために、グラフのフローを表示できます。

display(Image(agent.graph.get_graph().draw_mermaid_png()))

アウトプット:

エージェントのグラフの視覚化図 LangGraphエージェント・グラフの視覚化

まとめ

このチュートリアルでは、LangGraphを使用してtext-to-SQL ReActエージェントを構築する方法を説明します。事前構築されたエージェント実行関数であるcreate_react_agentを使用する代わりに、エージェントをゼロから構築しました。IBMの人工知能エージェントは、ユーザーの質問を自然言語で解析した後、SQLクエリーの生成、検証、実行のためのツールを使用して、SQLとPythonの両方のスキルを発揮します。LangChain PythonREPLToolクラスにより、エージェントにはPythonコードを生成するだけでなく実行する機能も備わっています。エージェントに応答のグラフィカルな表現を求めると、このツールが正常に実行されることがわかりました。LLMとtoolノード間の条件付きループにより、ReActエージェント・アーキテクチャーが有効になります。このチュートリアルでは、Mistral Medium 3のコーディングとマルチモーダル推論機能を紹介しました。次のステップとして、グラフのノードとエッジを追加する実験を検討してください。

関連ソリューション
IBMのAIエージェント開発

開発者が、IBM watsonx.aiを使用してAIエージェントの構築、デプロイ、および監視を行えるようにします。

 

watsonx.aiの詳細はこちら
人工知能ソリューション

業界をリードするIBMのAI専門知識とソリューション製品群を使用すれば、ビジネスにAIを活用できます。

AIソリューションはこちら
AIコンサルティングとサービス

AIの導入で重要なワークフローと業務を再構築し、エクスペリエンス、リアルタイムの意思決定とビジネス価値を最大化します。

AIサービスはこちら
次のステップ

事前構築済みのアプリケーションとスキルをカスタマイズする場合でも、AIスタジオを使用してカスタム・エージェント・サービスを構築し、デプロイする場合でも、IBM watsonxプラットフォームが対応します。

watsonx Orchestrateの詳細はこちら watsonx.aiの詳細はこちら
脚注

1 “Medium Is the New Large.” Mistral.ai, 7 May 2025, mistral.ai/news/mistral-medium-3.