Skip to content
Published on

LLMエージェントシステムの構築:ツール使用・計画・記憶の完全分析

Authors
  • Name
    Twitter

1. LLMエージェントとは? - ReAct論文の分析

エージェントの定義

LLMエージェントは、単にテキストを生成する言語モデルを超えた存在であり、外部環境と相互作用しながら自律的に意思決定を行うシステムである。従来のLLMが与えられたプロンプトに対して単一の応答を生成するのに対し、エージェントは観察(Observation)、推論(Reasoning)、行動(Action)を繰り返し実行して目標を達成する。

エージェントシステムの中核コンポーネントは以下の通りである:

  • LLM(頭脳):推論と意思決定を担う中核モデル
  • ツール:エージェントが使用できる外部API、データベース、コード実行器などの道具
  • 計画(Planning):複雑なタスクをサブステップに分解する戦略
  • 記憶(Memory):過去のやり取りとコンテキストを維持するメカニズム

ReAct:推論と行動の統合

Yaoらが2022年に発表したReAct(Reasoning and Acting)論文は、LLMエージェントの基盤を確立した画期的な研究である。ICLR 2023に採択されたこの論文は、LLMが推論の軌跡とタスク固有の行動を交互に生成するパラダイムを提案した。

既存のアプローチの限界は明確であった。Chain-of-Thought(CoT)プロンプティングは推論に優れるが外部情報にアクセスできないため、幻覚(ハルシネーション)が生じやすい。逆に、行動のみのアプローチは外部ツールを使用するが推論プロセスがなく、複雑な判断が困難であった。

ReActはこの2つのアプローチを統合する。その中核ループは以下の通りである:

Thought: 現在の状況を分析し、次の行動を計画する。
Action: 外部ツールを呼び出す、または環境と相互作用する。
Observation: Actionの結果を観察する。
...(繰り返し)
Thought: 十分な情報が集まったので、最終回答を生成する。

例えば「Appleの現在の時価総額は日本円でいくらか?」という質問に対して、ReActエージェントは以下のように動作する:

Thought: まず、AppleのUSDでの現在の時価総額を確認する必要がある。
Action: search("Apple current market cap USD")
Observation: Appleの時価総額は約3.4兆ドルである。
Thought: 次に、現在のUSD/JPYの為替レートを確認する必要がある。
Action: search("current USD JPY exchange rate")
Observation: 1 USD = 150 JPY
Thought: 3.4* 150 = 510兆円。最終回答を生成する。
Answer: Appleの時価総額は約510兆円です。

実験結果も印象的であった。HotpotQA(質問応答)とFever(事実検証)タスクでは、ReActはCoTと比較して幻覚を大幅に削減し、ALFWorldやWebShopなどの意思決定ベンチマークでは、既存の模倣学習や強化学習手法に対してそれぞれ34%と10%の成功率向上を示した。


2. ツール/関数呼び出しメカニズム

Anthropic Tool Use公式ドキュメントに基づく分析

AnthropicのClaudeはTool Useという名称でFunction Calling機能を提供している。公式ドキュメントによると、Tool Useは以下のように動作する:

ツールの定義

使用するツールは、APIリクエスト時にJSON Schema形式で定義する。各ツール定義にはnamedescriptioninput_schemaが含まれる。

import anthropic

client = anthropic.Anthropic()

# ツール定義
tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather in a given location",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city and state, e.g. San Francisco, CA"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit"
                }
            },
            "required": ["location"]
        }
    }
]

# API呼び出し
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "What's the weather like in Seoul?"}
    ]
)

Tool Useの動作フロー

  1. リクエストフェーズ:クライアントがツール定義と共にメッセージをAPIに送信する。
  2. 決定フェーズ:Claudeが利用可能なツールから適切なものを選択し、tool_useコンテントブロックを返す。この時点でstop_reason"tool_use"となる。
  3. 実行フェーズ:クライアントが実際にツールを実行する。(Claude自身は実行しない。)
  4. 結果伝達:ツールの実行結果がtool_resultコンテントブロックとしてClaudeに送り返される。
  5. 最終応答:Claudeがツールの結果に基づいて自然言語の応答を生成する。
# 2. Claudeの応答からtool_useブロックを抽出
tool_use_block = next(
    block for block in response.content if block.type == "tool_use"
)
tool_name = tool_use_block.name        # "get_weather"
tool_input = tool_use_block.input      # {"location": "Seoul, South Korea"}
tool_use_id = tool_use_block.id        # ユニーク識別子

# 3. 実際にツールを実行(開発者が実装)
weather_result = call_weather_api(tool_input["location"])

# 4. ツール結果をClaudeに送信
follow_up = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "What's the weather like in Seoul?"},
        {"role": "assistant", "content": response.content},
        {
            "role": "user",
            "content": [
                {
                    "type": "tool_result",
                    "tool_use_id": tool_use_id,
                    "content": weather_result
                }
            ]
        }
    ]
)

2025年に追加された高度な機能

Anthropicは2025年に3つの重要な機能を追加した:

  • Tool Search Tool:すべてのツール定義を事前にロードする代わりに、必要に応じてツールを動的に発見する。これによりコンテキストウィンドウの効率的な使用が可能になる。
  • Programmatic Tool Calling:コード実行環境でツールが呼び出され、コンテキストウィンドウの負担を軽減する。
  • Structured Outputs:ツール定義にstrict: trueオプションを追加すると、Claudeのツール呼び出しが定義されたスキーマに常に正確に従うことが保証される。

3. OpenAI Function Calling vs Anthropic Tool Useの比較

両プラットフォームはLLMが外部関数を呼び出すための構造化データを生成するメカニズムを提供するが、実装と設計思想に違いがある。

項目OpenAI Function CallingAnthropic Tool Use
名称Function Calling(またはTool Calling)Tool Use
ツール定義の位置toolsパラメータtoolsパラメータ
スキーマ形式JSON Schema(parametersJSON Schema(input_schema
応答形式tool_calls配列(メッセージ内)tool_useコンテントブロック
並列呼び出しparallel_tool_callsで制御対応(複数のtool_useブロック)
厳密モードstrict: true(Structured Outputs)strict: true(2025年追加)
サーバー側ツールWeb検索、Code InterpreterなどWeb検索、コード実行など
結果伝達toolロールのメッセージtool_resultコンテントブロック

OpenAIアプローチの特徴

OpenAIはtool_choiceパラメータを通じてモデルのツール使用を細かく制御できる。オプションには"auto"(モデルが自律的に判断)、"required"(必ずツールを使用)、"none"(ツール使用禁止)、または特定の関数を指定する方法がある。parallel_tool_calls: falseを設定すると、モデルは一度に1つのツールのみを呼び出すよう制限される。

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Compare the weather in Seoul and Tokyo"}],
    tools=[{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get weather for a location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string"}
                },
                "required": ["location"]
            }
        }
    }],
    parallel_tool_calls=True  # ソウルと東京を同時に呼び出し
)

Anthropicアプローチの特徴

AnthropicのClaudeはツール使用時に思考プロセスを自然に露出する傾向があり、エージェントの意思決定プロセスを透明に理解しやすい。tool_choiceもサポートしており、"auto""any"(少なくとも1つのツールを使用する必要がある)、または特定のツールを指定できる。

重要な違いはアーキテクチャの設計思想にある。OpenAIはメッセージレベルでツール呼び出しを処理するのに対し、Anthropicはコンテントブロックレベルで処理する。これによりAnthropicは単一の応答内でテキストとツール呼び出しをより柔軟に混在させることができる。


4. 計画戦略

**計画(Planning)**は、複雑なタスクを実行するエージェントにとって重要な能力である。主要な戦略を見ていこう。

Plan-and-Executeパターン

Plan-and-Executeは、まず全体の計画(Plan)を立て、次に各ステップを順次実行(Execute)するパターンである。LangChainブログで紹介されたこのアプローチは、Wangらの Plan-and-Solve Prompting論文とYohei NakajimaのBabyAGIプロジェクトに基づいている。

[ユーザークエリ]
     |
     v
[Planner LLM] --> ステップ1、ステップ2、ステップ3...
     |
     v
[Executor] --> ステップ1を実行 --> ステップ2を実行 --> ステップ3を実行
     |
     v
[Re-planner] --> 必要に応じて計画を修正
     |
     v
[最終回答]

Plan-and-ExecuteがReActに対して持つ利点は以下の通りである:

  • 速度:サブタスクの実行ごとに大規模なPlanner LLMを呼び出す必要がない。より小さなモデルで個別のステップを実行できる。
  • 推論品質:Plannerがタスク全体を明示的に分解するため、ステップの見落としが少ない。
  • コスト効率:Plannerには高性能モデル、Executorには軽量モデルを使用することでコストを削減できる。

Tree of Thoughts (ToT)

Yaoらが2023年にNeurIPSで発表したTree of Thoughtsは、Chain-of-Thoughtの限界を克服するために設計されたフレームワークである。その中核的なアイデアは、LLMの推論プロセスをツリー構造に展開し、複数の思考パスを探索・評価することにある。

                    [問題]
                   /  |  \
              [思考1] [思考2] [思考3]    <-- 複数のパスを生成
              /  \      |      \
         [1-a] [1-b]  [2-a]   [3-a]    <-- 各パスを展開
           |     |      |       |
        [評価] [評価]  [評価]   [評価]   <-- 自己評価
           |            |
        [選択]         [選択]            <-- 最適パスを選択

ToTの中核コンポーネントは以下の通りである:

  1. 思考の分解:問題を中間的な「思考」単位に分解する。
  2. 思考の生成:各ステップで複数の候補思考を生成する(サンプリングまたは提案による)。
  3. 状態の評価:LLM自身が各状態の有望性を評価する。
  4. 探索アルゴリズム:BFS(幅優先探索)またはDFS(深さ優先探索)でツリーを探索する。

Game of 24タスクでは、GPT-4 + CoTがわずか4%の成功率であったのに対し、ToTは74%を達成した。これは探索とバックトラックを可能にする構造的推論の力を示している。


5. 記憶システム

記憶(Memory)システムは、エージェントが長期的に効果的に動作するために不可欠である。記憶は大きく3つのタイプに分類される。

短期記憶

LLMのコンテキストウィンドウ自体が短期記憶として機能する。これには現在の会話履歴と直近のツール呼び出し結果が含まれる。

# 短期記憶:メッセージとして会話履歴を管理
messages = [
    {"role": "user", "content": "私の名前はキムヨンジュです"},
    {"role": "assistant", "content": "こんにちは、キムヨンジュさん!"},
    {"role": "user", "content": "私の名前は何と言いましたか?"},
    # コンテキストウィンドウ内で以前の会話を参照可能
]

限界は明確である。コンテキストウィンドウにはサイズ制限があり(Claude: 200Kトークン、GPT-4o: 128Kトークン)、セッションが終了すると記憶は消失する。

長期記憶

外部ストレージ(Vector DB、Key-Valueストアなど)に情報を永続的に保存し、必要に応じて検索するアプローチである。RAG(Retrieval-Augmented Generation)が最も代表的な実装パターンである。

from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

# 長期記憶:Vector DBに保存
vectorstore = Chroma(
    collection_name="agent_memory",
    embedding_function=OpenAIEmbeddings()
)

# 過去のやり取りを保存
vectorstore.add_texts([
    "ユーザーはPythonとデータ分析に関心がある。",
    "ユーザーはソウル在住で日本語を好む。",
    "前回のセッションでユーザーはpandas DataFrameについて質問した。"
])

# 関連する記憶を検索
relevant_memories = vectorstore.similarity_search(
    "ユーザーのプログラミングに対する関心", k=3
)

エンティティ記憶

会話に登場する特定のエンティティ(人物、場所、概念など)に関する情報を抽出・更新するメカニズムである。会話の進行に伴い、各エンティティに関する知識が蓄積されていく。

# エンティティ記憶の構造例
entity_store = {
    "キムヨンジュ": {
        "occupation": "データエンジニア",
        "interests": ["LLM", "データパイプライン", "Kubernetes"],
        "preferred_language": "Python",
        "recent_question_topic": "LangGraphエージェントの構築"
    },
    "Project_A": {
        "status": "進行中",
        "tech_stack": ["LangGraph", "Claude API", "PostgreSQL"],
        "goal": "社内データ分析エージェントの構築"
    }
}

実際には、これら3種類の記憶は組み合わせて使用される。LangGraphでは、MemorySaver(短期)と外部Store(長期)を.compile()時に注入することで、統合された記憶システムを構築できる。


6. LangGraph公式ドキュメントに基づく分析

LangGraphの中核概念

LangGraphは、LangChainチームが開発したエージェントオーケストレーションフレームワークであり、エージェントのワークフローを有向グラフとしてモデル化する。公式ドキュメントによると、中核コンポーネントは以下の通りである:

StateGraph

StateGraphはLangGraphの中心的なクラスである。ユーザー定義のStateオブジェクトでパラメータ化され、グラフ内のすべてのノードがこのステートの読み書きを行う。

from langgraph.graph import StateGraph, START, END
from langgraph.graph import MessagesState

# MessagesStateはメッセージリストを管理する組み込みState
graph = StateGraph(MessagesState)

カスタムStateを定義することも可能である:

from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    plan: list[str]
    current_step: int
    final_answer: str

Annotatedadd_messagesを使用してリデューサー関数を指定できる。これはノードがステートを返す際に、既存の値をどのように更新するかを決定する。add_messagesは新しいメッセージを既存のメッセージリストに追加する操作を行う。

ノード

ノードはエージェントの実際のロジックを実行する関数である。現在のStateを入力として受け取り、更新されたStateを返す。

from langchain_anthropic import ChatAnthropic

model = ChatAnthropic(model="claude-sonnet-4-20250514")
model_with_tools = model.bind_tools(tools)

def call_model(state: AgentState):
    """LLMを呼び出すノード"""
    response = model_with_tools.invoke(state["messages"])
    return {"messages": [response]}

def execute_tool(state: AgentState):
    """ツールを実行するノード"""
    last_message = state["messages"][-1]
    results = []
    for tool_call in last_message.tool_calls:
        result = tool_map[tool_call["name"]].invoke(tool_call["args"])
        results.append(
            ToolMessage(content=str(result), tool_call_id=tool_call["id"])
        )
    return {"messages": results}

# ノードの追加
graph.add_node("agent", call_model)
graph.add_node("tools", execute_tool)

エッジ

エッジはノード間の実行フローを定義する。LangGraphは3種類のエッジを提供する:

1. 通常エッジ:常に固定された次のノードに移動する。

graph.add_edge(START, "agent")     # 開始 -> agentノード
graph.add_edge("tools", "agent")   # tools -> agentノード(結果を伝達)

2. 条件付きエッジ:条件に基づいて異なるノードに分岐する。

def should_continue(state: AgentState):
    """ツール呼び出しが必要かどうかを判定"""
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tools"
    return END

graph.add_conditional_edges("agent", should_continue)

3. エントリーポイントSTART定数を使用してグラフの開始点を定義する。

コンパイルと実行

グラフを定義した後、実行前にコンパイルが必要である。

from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)

# 実行
result = app.invoke(
    {"messages": [("user", "ソウルの現在の天気を教えてください")]},
    config={"configurable": {"thread_id": "session-001"}}
)

thread_idにより会話セッションが区別され、Checkpointerが各スーパーステップでステートを自動保存する。


7. Human-in-the-Loopパターン

LangGraphのInterruptメカニズム

LangGraphの公式ドキュメントはHuman-in-the-Loopをファーストクラスの機能としてサポートしている。その主要コンポーネントはCheckpointerInterrupt関数である。

基本原理

LangGraphのすべての実行は、Checkpointerを通じて各スーパーステップでステートが保存される。これに基づき、特定のノード実行の前後でグラフを一時停止し、ユーザーの入力を待ち、その後再開することができる。

静的Interrupt

特定のノード実行の前後にブレークポイントを設定する。

# コンパイル時にブレークポイントを設定
app = graph.compile(
    checkpointer=checkpointer,
    interrupt_before=["execute_tool"],   # ツール実行前にInterrupt
    # interrupt_after=["execute_tool"],  # ツール実行後にInterrupt
)

# execute_toolノードの直前で実行が停止
result = app.invoke(
    {"messages": [("user", "このデータを削除してください")]},
    config={"configurable": {"thread_id": "session-002"}}
)

# ユーザーの承認後に再開
app.invoke(None, config={"configurable": {"thread_id": "session-002"}})

動的Interrupt(interrupt関数)

LangGraphはノード内で動的にInterruptするinterrupt()関数を提供する。このアプローチはより柔軟である。

from langgraph.types import interrupt, Command

def sensitive_tool_node(state: AgentState):
    """センシティブな操作の前にユーザーの承認を求めるノード"""
    last_message = state["messages"][-1]

    for tool_call in last_message.tool_calls:
        if tool_call["name"] in ["delete_data", "send_email", "execute_query"]:
            # 実行を中断してユーザーの承認を要求
            user_response = interrupt(
                f"'{tool_call['name']}'ツールを実行しようとしています。"
                f"入力: {tool_call['args']}。承認しますか?"
            )

            if user_response != "approved":
                return {"messages": [
                    ToolMessage(
                        content="ユーザーが実行を拒否しました。",
                        tool_call_id=tool_call["id"]
                    )
                ]}

        # 承認された場合は実行
        result = tool_map[tool_call["name"]].invoke(tool_call["args"])
        return {"messages": [
            ToolMessage(content=str(result), tool_call_id=tool_call["id"])
        ]}

Interruptされたグラフを再開するには、Command(resume=...)を使用する:

# Interrupt状態からユーザーが承認
app.invoke(
    Command(resume="approved"),
    config={"configurable": {"thread_id": "session-002"}}
)

使用シナリオ

  • 危険な操作の承認:データ削除、メール送信、決済処理の前にユーザーの確認を取る
  • 曖昧なリクエストの明確化:エージェントがユーザーの意図を正確に判断できない場合に追加質問する
  • 実行計画のレビュー:Plan-and-Executeパターンの計画フェーズ後にユーザーが計画をレビュー/修正する

8. マルチエージェントシステムの構築

Supervisorアーキテクチャ

LangGraph公式ドキュメントおよびlanggraph-supervisorライブラリで提示されているSupervisorパターンは、マルチエージェントシステムの中核アーキテクチャである。中央のSupervisorエージェントが専門化されたWorkerエージェントを調整する。

                    [ユーザークエリ]
                         |
                         v
                   [Supervisorエージェント]
                   /       |        \
                  v        v         v
          [Research    [Code       [Data
           Agent]      Agent]      Agent]
              |           |           |
              v           v           v
         [Web検索]    [コード実行] [SQLクエリ]

実装

from langgraph.graph import StateGraph, MessagesState, START, END

# 各専門エージェントの定義
def research_agent(state: MessagesState):
    """Web検索専門エージェント"""
    model = ChatAnthropic(model="claude-sonnet-4-20250514")
    model_with_search = model.bind_tools([web_search_tool])
    response = model_with_search.invoke(state["messages"])
    return {"messages": [response]}

def code_agent(state: MessagesState):
    """コード作成・実行専門エージェント"""
    model = ChatAnthropic(model="claude-sonnet-4-20250514")
    model_with_code = model.bind_tools([code_execution_tool])
    response = model_with_code.invoke(state["messages"])
    return {"messages": [response]}

def data_agent(state: MessagesState):
    """データ分析専門エージェント"""
    model = ChatAnthropic(model="claude-sonnet-4-20250514")
    model_with_data = model.bind_tools([sql_query_tool, chart_tool])
    response = model_with_data.invoke(state["messages"])
    return {"messages": [response]}

# Supervisorルーティング関数
def supervisor(state: MessagesState):
    """タスクをどのエージェントに委任するかを決定"""
    model = ChatAnthropic(model="claude-sonnet-4-20250514")
    system_prompt = """あなたはSupervisorです。ユーザーのリクエストを分析し、
    適切な専門エージェントに委任してください。
    - research: 情報検索が必要な場合
    - code: コードの作成/実行が必要な場合
    - data: データ分析/可視化が必要な場合
    - FINISH: タスクが完了した場合"""

    response = model.invoke([
        {"role": "system", "content": system_prompt},
        *state["messages"]
    ])
    return {"messages": [response]}

def route_supervisor(state: MessagesState):
    """Supervisorの決定に基づいてルーティング"""
    last_message = state["messages"][-1].content
    if "research" in last_message.lower():
        return "research_agent"
    elif "code" in last_message.lower():
        return "code_agent"
    elif "data" in last_message.lower():
        return "data_agent"
    return END

# グラフの構成
workflow = StateGraph(MessagesState)
workflow.add_node("supervisor", supervisor)
workflow.add_node("research_agent", research_agent)
workflow.add_node("code_agent", code_agent)
workflow.add_node("data_agent", data_agent)

workflow.add_edge(START, "supervisor")
workflow.add_conditional_edges("supervisor", route_supervisor)
workflow.add_edge("research_agent", "supervisor")
workflow.add_edge("code_agent", "supervisor")
workflow.add_edge("data_agent", "supervisor")

app = workflow.compile()

階層構造

より複雑なシステムでは、階層型マルチエージェント構造が使用される。トップレベルのSupervisorが中間レベルのSupervisorを管理し、中間レベルのSupervisorが実際のWorkerを管理する。各レイヤーは独立してテストでき、既存のシステムに影響を与えずに新しいドメインを追加できる。


9. MCP(Model Context Protocol)概要

Anthropicが提案するオープン標準

Model Context Protocol(MCP)は、Anthropicが2024年11月に発表したオープンプロトコルであり、LLMアプリケーションと外部データソースやツールとの間に標準化された接続を提供する。USBが様々な周辺機器をコンピュータに接続するための標準インターフェースであるように、MCPはAIモデルを外部システムに接続するための標準インターフェースである。

アーキテクチャ

MCPはクライアント-サーバーアーキテクチャに従う。

[LLMアプリケーション(MCPクライアント)]
         |
    [MCPプロトコル]
         |
[MCPサーバーA]  [MCPサーバーB]  [MCPサーバーC]
     |               |               |
[GitHub API]   [データベース]   [ファイルシステム]
  • MCPホスト:Claude Desktop、IDEなどLLMが組み込まれたアプリケーション
  • MCPクライアント:MCPサーバーと通信するホスト内のコンポーネント
  • MCPサーバー:標準化されたプロトコルを通じて特定の機能(ツール、データ)を公開するサーバー

コア機能

MCPサーバーは3種類の機能を公開できる:

  1. ツール:エージェントが呼び出せる関数(例:ファイル読み取り、API呼び出し)
  2. リソース:コンテキストとして使用できるデータ(例:ドキュメント、設定ファイル)
  3. プロンプト:再利用可能なプロンプトテンプレート

2025年の状況

MCP仕様の最新バージョンは2025年11月25日に公開され、現在10,000を超える公開MCPサーバーが稼働している。ChatGPT、Cursor、Gemini、Microsoft Copilot、Visual Studio Codeなどの主要AI製品がMCPを採用している。

2025年6月には重要なセキュリティアップデートがあった。MCPサーバーがOAuth 2.0リソースサーバーとして分類され、構造化JSON出力(structuredContent)のサポートと、セッション中にユーザー入力を要求するElicitation機能が追加された。

AnthropicはMCPをAgentic AI Foundationに寄贈し、コミュニティ主導のガバナンス体制に移行した。


10. エージェント評価手法

エージェントシステムは、従来のLLM評価とは異なる次元での評価が必要である。主要な評価軸は以下の通りである:

タスク完了率

与えられた目標が実際に達成されたかどうかを測定する。ベンチマークによって評価基準が異なる。

  • WebArena:Webブラウジングタスクの成功/失敗
  • SWE-bench:実際のGitHub Issueが解決されたかどうか
  • HumanEval:コード生成の正確性

ツール選択精度

エージェントが適切なツールを選択したかどうかを評価する。

# 評価指標の例
evaluation = {
    "correct_tool_selected": True,      # 正しいツールが選択されたか?
    "correct_parameters": True,         # パラメータは正確か?
    "unnecessary_tool_calls": 0,        # 不要な呼び出しの回数
    "total_tool_calls": 3,             # 総呼び出し回数
    "optimal_tool_calls": 2,           # 最適な呼び出し回数
    "efficiency": 2/3                  # 効率性
}

軌跡の品質

最終結果だけでなく、プロセスの効率性を評価する。

  • 不要なステップなく目標に到達したか?
  • エラー発生時に適切にリカバリーしたか?
  • 同じ操作を繰り返していないか?

安全性とガードレール

  • 未許可のツールの呼び出しを回避したか?
  • センシティブな情報を適切に処理したか?
  • Human-in-the-Loopが必要な場面で正しく中断したか?

コストとレイテンシ

  • 総API呼び出し回数とトークン使用量
  • タスク完了までの総経過時間
  • モデルサイズに対する性能効率

11. 実践例:データ分析エージェント

以下は、LangGraphを使用してCSVデータ分析エージェントを構築する完全なコードである。LLMとしてAnthropic Claudeを使用し、Tool Useと条件付きエッジを活用する。

import pandas as pd
from typing import TypedDict, Annotated
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import ToolMessage, HumanMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import ToolNode, tools_condition


# ============================================
# 1. ツール定義
# ============================================

@tool
def load_csv(file_path: str) -> str:
    """CSVファイルを読み込み、基本情報を返す。"""
    df = pd.read_csv(file_path)
    info = {
        "shape": df.shape,
        "columns": list(df.columns),
        "dtypes": df.dtypes.to_dict(),
        "head": df.head().to_string(),
        "describe": df.describe().to_string()
    }
    return str(info)


@tool
def run_analysis(file_path: str, query: str) -> str:
    """pandasを使用してCSVデータに対する分析クエリを実行する。

    Args:
        file_path: CSVファイルのパス
        query: 実行するpandasクエリ(例: 'df.groupby("category").mean()')
    """
    df = pd.read_csv(file_path)
    try:
        result = eval(query, {"df": df, "pd": pd})
        if isinstance(result, pd.DataFrame):
            return result.to_string()
        elif isinstance(result, pd.Series):
            return result.to_string()
        return str(result)
    except Exception as e:
        return f"クエリ実行エラー: {str(e)}"


@tool
def generate_summary(analysis_results: str, user_question: str) -> str:
    """分析結果をユーザーフレンドリーなサマリーに変換する。

    Args:
        analysis_results: 分析結果のテキスト
        user_question: 元のユーザーの質問
    """
    summary = f"""
    ## 分析サマリー

    **質問**: {user_question}

    **結果**:
    {analysis_results}
    """
    return summary


# ============================================
# 2. エージェント設定
# ============================================

# ツールリスト
tools = [load_csv, run_analysis, generate_summary]

# LLM設定
model = ChatAnthropic(
    model="claude-sonnet-4-20250514",
    temperature=0,
    max_tokens=4096
)
model_with_tools = model.bind_tools(tools)


# エージェントノードの定義
def agent_node(state: MessagesState):
    """エージェントが推論し、必要に応じてツールを呼び出す。"""
    system_message = {
        "role": "system",
        "content": """あなたはデータ分析専門エージェントです。
        ユーザーの質問に答えるため、以下のステップに従ってください:
        1. まずload_csvでデータを読み込み、構造を把握する。
        2. run_analysisで適切なpandasクエリを実行する。
        3. generate_summaryで結果をまとめる。
        常にステップバイステップで考え、必要なツールを呼び出してください。"""
    }
    messages = [system_message] + state["messages"]
    response = model_with_tools.invoke(messages)
    return {"messages": [response]}


# ============================================
# 3. グラフ構築
# ============================================

# StateGraphの作成
workflow = StateGraph(MessagesState)

# ノードの追加
workflow.add_node("agent", agent_node)
workflow.add_node("tools", ToolNode(tools))

# エッジの追加
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
    "agent",
    tools_condition,  # tool_callsがあれば"tools"、なければEND
)
workflow.add_edge("tools", "agent")  # ツール結果をエージェントに伝達

# コンパイル
checkpointer = MemorySaver()
app = workflow.compile(checkpointer=checkpointer)


# ============================================
# 4. 実行
# ============================================

def run_data_agent(question: str, file_path: str, thread_id: str = "default"):
    """データ分析エージェントを実行する。"""
    config = {"configurable": {"thread_id": thread_id}}

    initial_message = HumanMessage(
        content=f"ファイルパス: {file_path}\n\n質問: {question}"
    )

    result = app.invoke(
        {"messages": [initial_message]},
        config=config
    )

    # 最終応答の抽出
    final_message = result["messages"][-1]
    return final_message.content


# 使用例
if __name__ == "__main__":
    answer = run_data_agent(
        question="月別の平均売上高はいくらで、最も売上が高かった月はどこですか?",
        file_path="./data/sales.csv",
        thread_id="analysis-001"
    )
    print(answer)

このエージェントはReActパターンに従って動作する。LLMが推論を行い(agent_node)、必要なツールを選択し(tools_conditionによる分岐)、ツールの結果をLLMに送り返す(tools -> agentエッジ)というループが繰り返される。MemorySaverが各ステップでステートを保存するため、同じthread_idによる後続の質問は以前の分析コンテキストを維持する。


12. References

クイズ

Q1: 「LLMエージェントシステムの構築:ツール使用・計画・記憶の完全分析」の主なトピックは何ですか?

LLMエージェントの中核概念であるツール使用(Tool Use)、計画(Planning)、記憶(Memory)を、LangGraphとAnthropic公式ドキュメントに基づいて分析し、実践的なエージェントを構築する。

Q2: エージェントの定義とは何ですか? LLMエージェントは、単にテキストを生成する言語モデルを超えた存在であり、外部環境と相互作用しながら自律的に意思決定を行うシステムである。従来のLLMが与えられたプロンプトに対して単一の応答を生成するのに対し、エージェントは観察(Observation)、推論(Reasoning)、行動(Action)を繰り返し実行して目標を達成する。

Q3: ReAct:推論と行動の統合の核心的な概念を説明してください。 Yaoらが2022年に発表したReAct(Reasoning and Acting)論文は、LLMエージェントの基盤を確立した画期的な研究である。ICLR 2023に採択されたこの論文は、LLMが推論の軌跡とタスク固有の行動を交互に生成するパラダイムを提案した。 既存のアプローチの限界は明確であった。Chain-of-Thought(CoT)プロンプティングは推論に優れるが外部情報にアクセスできないため、幻覚(ハルシネーション)が生じやすい。逆に、行動のみのアプローチは外部ツールを使用するが推論プロセスがなく、複雑な判断が困難であった。

Q4: Anthropic Tool Use公式ドキュメントに基づく分析の主な特徴は何ですか? AnthropicのClaudeはTool Useという名称でFunction Calling機能を提供している。公式ドキュメントによると、Tool Useは以下のように動作する: ツールの定義 使用するツールは、APIリクエスト時にJSON Schema形式で定義する。各ツール定義にはname、description、input_schemaが含まれる。 Tool Useの動作フロー リクエストフェーズ:クライアントがツール定義と共にメッセージをAPIに送信する。

Q5: Plan-and-Executeパターンはどのように機能しますか? Plan-and-Executeは、まず全体の計画(Plan)を立て、次に各ステップを順次実行(Execute)するパターンである。LangChainブログで紹介されたこのアプローチは、Wangらの Plan-and-Solve Prompting論文とYohei NakajimaのBabyAGIプロジェクトに基づいている。 Plan-and-ExecuteがReActに対して持つ利点は以下の通りである: 速度:サブタスクの実行ごとに大規模なPlanner LLMを呼び出す必要がない。より小さなモデルで個別のステップを実行できる。