Tool Call の追加

15 minutes  

注意:このセクションでは複数のファイルを変更する必要があります。 変更箇所がわからない場合や、アプリケーションが動作しなくなった場合は、 ~/workshop/agentic-ai/app-with-agents-and-tools フォルダにあるモデルソリューションを参照してください。

前のセクションでは、エージェントが新しい Agents ページや、トレース上部の Agent flow に表示されないことを確認しました。

その理由は、アプリケーションが現在エージェントを使用しておらず、LLM を直接呼び出しているためです。

つまり、現在のアプリケーションは台本のある劇のようなものです。すべてのセリフとアクションがコードに書かれています。LLM を呼び出す際、特定のセリフを読むよう依頼しているだけです。LLM が自ら判断を行っていないため、Observability for AI の計装はそれを自律的なエージェントとして認識しません。

次のセクションでは、LLM にツールとそれらの使用方法を判断する権限を与えます。エージェントモデルに移行することで、LLM は Tool Call を生成するようになります。OpenTelemetry の計装がこれらのインタラクションをキャプチャし、LLM の思考プロセスとツールの使用状況を確認できるようになり、各エージェントが Splunk Observability Cloud に表示されるようになります。

直接呼び出しとエージェントトレースの比較

これらの変更を行う前に、LLM を直接呼び出した場合とエージェント経由で呼び出した場合のトレースのキャプチャ方法の違いを詳しく見てみましょう。

直接呼び出しのトレース

llm.invoke() を呼び出すと、計装は標準的な「Chat」または「Completion」スパンを検出します。プロンプトとレスポンスが記録されます。エージェントフレームワークが管理する「ループ」や「ツール呼び出し」ロジックがないため、Splunk Observability Cloud はスパンを「Agent」として分類するために必要なメタデータを検出できません。

エージェントトレース

エージェント(例create_react_agent)を使用すると、フレームワークは実行を特定の「Agent」および「Tool」スパンでラップします。これらのスパンには、OpenTelemetry に「これは単なるチャットではなく、特定のツールを使用した推論ループです」と伝えるメタデータが含まれています。これが、トレースの可視化における Agents ページと Agent Flow ダイアグラムにデータを表示する仕組みです。

バックアップの作成

Python コードを変更する前に、以下のコマンドで main.py ファイルのバックアップを作成します

cp ~/workshop/agentic-ai/base-app/main.py ~/workshop/agentic-ai/base-app/main.py.bak

Import 文の追加

~/workshop/agentic-ai/base-app/main.py ファイルを編集用に開きます。

Begin: Add Import StatementsEnd: Add Import Statements の間に、以下の import 文を追加します

# Begin: Add Import Statements

from langchain_core.tools import tool
from langchain.agents import (
    create_agent as _create_react_agent,  # type: ignore[attr-defined]
)

# End: Add Import Statements

ツールの追加

同じ main.py ファイルで、Begin: Tool DefinitionsEnd: Tool Definitions の間にツール定義を追加します

# Begin: Tool Definitions

@tool
def mock_search_flights(origin: str, destination: str, departure: str) -> str:
    """Return mock flight options for a given origin/destination pair."""
    # create a local random.Random instance
    seed = hash((origin, destination, departure)) % (2**32)
    rng = random.Random(seed)
    airline = rng.choice(["SkyLine", "AeroJet", "CloudNine"])
    fare = rng.randint(700, 1250)

    return (
        f"Top choice: {airline} non-stop service {origin}->{destination}, "
        f"depart {departure} 09:15, arrive {departure} 17:05. "
        f"Premium economy fare ${fare} return."
    )


@tool
def mock_search_hotels(destination: str, check_in: str, check_out: str) -> str:
    """Return mock hotel recommendation for the stay."""
    seed = hash((destination, check_in, check_out)) % (2**32)
    rng = random.Random(seed)
    name = rng.choice(["Grand Meridian", "Hotel Lumière", "The Atlas"])
    rate = rng.randint(240, 410)

    return (
        f"{name} near the historic centre. Boutique suites, rooftop bar, "
        f"average nightly rate ${rate} including breakfast."
    )


@tool
def mock_search_activities(destination: str) -> str:
    """Return a short list of signature activities for the destination."""
    data = DESTINATIONS.get(destination.lower(), DESTINATIONS["paris"])
    bullets = "\n".join(f"- {item}" for item in data["highlights"])
    return f"Signature experiences in {destination.title()}:\n{bullets}"
    
# End: Tool Definitions

AI Agent Monitoring 用のアプリケーション設定

現在、アプリケーションは以下のように LLM を作成して呼び出しています

def flight_specialist_node(state: PlannerState) -> PlannerState:
    llm = _create_llm(
    "flight_specialist", temperature=0.4, session_id=state["session_id"]
    )
    ...
    result = llm.invoke(messages)
    ...

AI Agent Monitoring では、代わりにエージェント名のメタデータを含むエージェントを作成し、LLM ではなくエージェントを呼び出す必要があります。

coordinator_nodeflight_specialist_nodehotel_specialist_nodeactivity_specialist_node、および plan_synthesizer_node 関数の定義を以下の内容に置き換えます

ヒントvi エディタで大量の行をまとめて削除するには、Shift + v を押して Visual Line モードにし、下矢印キーで削除したい行をすべて選択してから、d を押して 選択した行を削除します。

def coordinator_node(
    state: PlannerState
) -> PlannerState:
    llm = _create_llm("coordinator", temperature=0.2, session_id=state["session_id"])
    agent = _create_react_agent(llm, tools=[]).with_config(
        {
            "run_name": "coordinator",
            "tags": ["agent", "agent:coordinator"],
            "metadata": {
                "agent_name": "coordinator",
                "session_id": state["session_id"],
            },
        }
    )
    system_message = SystemMessage(
        content=(
            "You are the lead travel coordinator. Extract the key details from the "
            "traveller's request and describe the plan for the specialist agents."
        )
    )

    result = agent.invoke({"messages": [system_message] + list(state["messages"])})
    final_message = result["messages"][-1]
    state["messages"].append(
        final_message
        if isinstance(final_message, BaseMessage)
        else AIMessage(content=str(final_message))
    )
    state["current_agent"] = "flight_specialist"
    return state


def flight_specialist_node(
    state: PlannerState
) -> PlannerState:
    llm = _create_llm(
        "flight_specialist", temperature=0.4, session_id=state["session_id"]
    )
    agent = _create_react_agent(llm, tools=[mock_search_flights]).with_config(
        {
            "run_name": "flight_specialist",
            "tags": ["agent", "agent:flight_specialist"],
            "metadata": {
                "agent_name": "flight_specialist",
                "session_id": state["session_id"],
            },
        }
    )
    step = (
        f"Find an appealing flight from {state['origin']} to {state['destination']} "
        f"departing {state['departure']} for {state['travellers']} travellers."
    )

    # IMPORTANT: pass a proper list of messages (not stringified)
    messages = [
        SystemMessage(content="You are a flight booking specialist. Provide concise options."),
        HumanMessage(content=step),
    ]

    result = agent.invoke({"messages": messages})
    final_message = result["messages"][-1]
    state["flight_summary"] = final_message.content if isinstance(final_message, BaseMessage) else str(final_message)
    state["messages"].append(final_message if isinstance(final_message, BaseMessage) else AIMessage(content=str(final_message)))
    state["current_agent"] = "hotel_specialist"
    return state


def hotel_specialist_node(
    state: PlannerState
) -> PlannerState:
    llm = _create_llm(
        "hotel_specialist", temperature=0.5, session_id=state["session_id"]
    )
    agent = _create_react_agent(llm, tools=[mock_search_hotels]).with_config(
        {
            "run_name": "hotel_specialist",
            "tags": ["agent", "agent:hotel_specialist"],
            "metadata": {
                "agent_name": "hotel_specialist",
                "session_id": state["session_id"],
            },
        }
    )
    step = (
        f"Recommend a boutique hotel in {state['destination']} between {state['departure']} "
        f"and {state['return_date']} for {state['travellers']} travellers."
    )

    # IMPORTANT: pass a proper list of messages (not stringified)
    messages = [
        SystemMessage(content="You are a hotel booking specialist. Provide concise options."),
        HumanMessage(content=step),
    ]

    result = agent.invoke({"messages": messages})

    final_message = result["messages"][-1]
    state["hotel_summary"] = (
        final_message.content
        if isinstance(final_message, BaseMessage)
        else str(final_message)
    )
    state["messages"].append(
        final_message
        if isinstance(final_message, BaseMessage)
        else AIMessage(content=str(final_message))
    )
    state["current_agent"] = "activity_specialist"
    return state


def activity_specialist_node(
    state: PlannerState
) -> PlannerState:
    llm = _create_llm(
        "activity_specialist", temperature=0.6, session_id=state["session_id"]
    )
    agent = _create_react_agent(llm, tools=[mock_search_activities]).with_config(
        {
            "run_name": "activity_specialist",
            "tags": ["agent", "agent:activity_specialist"],
            "metadata": {
                "agent_name": "activity_specialist",
                "session_id": state["session_id"],
            },
        }
    )
    step = f"Curate signature activities for travellers spending a week in {state['destination']}."

    # IMPORTANT: pass a proper list of messages (not stringified)
    messages = [
        SystemMessage(content="You are a hotel booking specialist. Provide concise options."),
        HumanMessage(content=step),
    ]

    result = agent.invoke({"messages": messages})

    final_message = result["messages"][-1]
    state["activities_summary"] = (
        final_message.content
        if isinstance(final_message, BaseMessage)
        else str(final_message)
    )
    state["messages"].append(
        final_message
        if isinstance(final_message, BaseMessage)
        else AIMessage(content=str(final_message))
    )
    state["current_agent"] = "plan_synthesizer"
    return state
    
def plan_synthesizer_node(state: PlannerState) -> PlannerState:
    llm = _create_llm(
        "plan_synthesizer", temperature=0.3, session_id=state["session_id"]
    )

    agent = _create_react_agent(llm, tools=[]).with_config(
        {
            "run_name": "plan_synthesizer",
            "tags": ["agent", "agent:plan_synthesizer"],
            "metadata": {
                "agent_name": "plan_synthesizer",
                "session_id": state["session_id"],
            },
        }
    )

    system_content = (
        "You are the travel plan synthesiser. Combine the specialist insights into a "
        "concise, structured itinerary covering flights, accommodation and activities."
    )

    content = json.dumps(
        {
            "flight": state["flight_summary"],
            "hotel": state["hotel_summary"],
            "activities": state["activities_summary"],
        },
        indent=2,
    )

    out = agent.invoke(
        {
            "messages": [
                SystemMessage(content=system_content),
                HumanMessage(
                    content=(
                        f"Traveller request: {state['user_request']}\n\n"
                        f"Origin: {state['origin']} | Destination: {state['destination']}\n"
                        f"Dates: {state['departure']} to {state['return_date']}\n\n"
                        f"Specialist summaries:\n{content}"
                    )
                ),
            ]
        }
    )
    # 1) Extract the assistant's final text
    final_msg = next(m for m in reversed(out["messages"]) if isinstance(m, AIMessage))
    state["final_itinerary"] = final_msg.content

    # 2) Append the new messages to your ongoing conversation
    state["messages"].extend(out["messages"])  # or append just final_msg

    state["current_agent"] = "completed"
    return state

フライト、ホテル、アクティビティのスペシャリストエージェントを作成する際にツールを渡していることに注目してください。エージェントが呼び出されると、LLM はリクエストを処理するためにツールを呼び出すべきかどうかを判断します。

ヒント:以下のコマンドを実行して、変更内容をモデルソリューションと比較できます

diff ~/workshop/agentic-ai/base-app/main.py ~/workshop/agentic-ai/app-with-agents-and-tools/main.py

更新された Docker イメージのビルド

新しいタグで更新された Docker イメージをビルドします

docker build --platform linux/amd64 -t localhost:9999/agentic-ai-app:app-with-agents-and-tools .
docker push localhost:9999/agentic-ai-app:app-with-agents-and-tools

Kubernetes マニフェストの更新

~/workshop/agentic-ai/base-app/k8s.yaml ファイルを編集用に開き、エージェントとツールを含むイメージを使用するようにイメージを更新します

          image: localhost:9999/agentic-ai-app:app-with-agents-and-tools

更新されたアプリケーションのデプロイ

以下のようにマニフェストファイルを使用して、更新されたアプリケーションをデプロイできます

kubectl apply -f ~/workshop/agentic-ai/base-app/k8s.yaml

Kubernetes でのアプリケーションテスト

新しいアプリケーション Pod が正常に起動し、古い Pod が存在しないことを確認します

kubectl get pods -n travel-agent
NAME                                        READY   STATUS    RESTARTS   AGE
travel-planner-langchain-68977dc5c4-4w7p9   1/1     Running   0          41s

次に、以下のコマンドを実行してアプリケーションをテストします

curl http://travel-planner.localhost/travel/plan \
  -H "Content-Type: application/json" \
  -d '{
    "origin": "Seattle",
    "destination": "Tokyo",
    "user_request": "We are planning a week-long trip to Seattle from Tokyo. Looking for boutique hotel, business-class flights and unique experiences.",
    "travelers": 2
  }'

Splunk Observability Cloud でデータを表示する

Splunk Observability Cloud に戻って、トレースがどのように表示されるか確認しましょう。

APM に移動し、AI agents を選択します。環境名が選択されていることを確認してください(例agentic-ai-$INSTANCE)。ページにデータが表示されるようになったことがわかります!

Agents Agents

APM -> AI trace data に移動します。これは AI 関連のコンテンツを含むトレースを検索できる新しいページです

Agents Agents

環境名が選択されていることを確認してください(例agentic-ai-$INSTANCE)。 新しいトレースの1つを選択します。Agent flow にすべてのエージェントが表示されるようになりました!

Trace Trace

Tool Call も確認できます

Trace Trace