AIエージェントを「理解する」から「作る」へ
AIエージェントという言葉を毎日のように耳にする。 だが「実際に作ったことがある」と言えるエンジニアは、まだ少数派だ。
概念の解説記事は山ほどある。 ReActパターン、ツール呼び出し、マルチエージェント。 しかし読んだだけでは作れない。 手を動かす段階に入ると、途端にハードルが上がる。
本記事では、LangGraphとClaude APIを使い、実際に動くAIエージェントをゼロから構築する。 完成するのは「Webで情報を収集し、分析し、レポートを自動生成するリサーチエージェント」だ。
前提条件
Python 3.11以上、Claude APIキー(Anthropicアカウント)、基本的なPython知識。 LangGraphやClaude APIの経験は不要——この記事で学べる。
アーキテクチャ設計——ReActパターンを理解する
エージェントの中核にあるのがReAct(Reasoning + Acting)パターンだ。 2022年にPrinceton大学とGoogleの研究チームが発表したこのアプローチは、LLMに「考えてから行動する」能力を与える。
通常のLLMは一問一答で完結する。 エージェントは違う。 「考える→行動する→結果を観察する→また考える」のループを繰り返す。
| フェーズ | やること | 具体例 |
|---|---|---|
| Reasoning(推論) | 現在の状況を分析し、次の行動を決める | 「ユーザーはAIエージェント市場を調べたい。まずWeb検索しよう」 |
| Acting(行動) | 外部ツールを呼び出す | Tavily APIで「AI agent market 2026」を検索 |
| Observation(観察) | ツールの結果を受け取る | 検索結果10件を取得 |
| Loop(ループ) | 十分な情報が集まるまで繰��返す | 3回の検索+2回の要約を経てレポート生成 |
なぜLangGraphを選ぶのか
LangChainのAgentExecutorでもエージェントは作れる。 だがLangGraphには決定的な利点がある。 状態遷移を「グラフ」として明示的に定義できることだ。
AgentExecutorはブラックボックスになりがちで、デバッグが難しい。 LangGraphなら、ノード(処理ステップ)とエッジ(遷移条件)を自分で設計するため、エージェントの振る舞いを完全にコントロールできる。
Step 1——環境構築とClaude API接続
まずプロジェクトをセットアップする。
# プロジェクト作成
mkdir ai-research-agent && cd ai-research-agent
python -m venv .venv && source .venv/bin/activate
# 依存ライブラリをインストール
pip install langgraph langchain-anthropic tavily-python python-dotenv
環境変数を設定する。.envファイルを作成しよう。
# .env
ANTHROPIC_API_KEY=sk-ant-xxxxx
TAVILY_API_KEY=tvly-xxxxx
最小限のClaude API呼び出しで動作確認���る。
from langchain_anthropic import ChatAnthropic
from dotenv import load_dotenv
load_dotenv()
llm = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0)
response = llm.invoke("AIエージェントを一文で説明して")
print(response.content)
これが動けば準備完了だ。 次はエージェントの「手足」となるツールを定義していく。
Step 2——ツールを定義する
エージェントはLLM単体ではない。 外部ツールと組み合わせることで、はじめて「行動」が可能になる。
今回は3つのツールを作成する。
| ツール名 | 役割 | API |
|---|---|---|
| web_search | Web検索で最新���報を取得 | Tavily Search API |
| summarize_text | 長文テキストを要約 | Claude API(内部呼び出し) |
| write_report | 構造化されたレポートを生成 | Claude API(内部呼���出し) |
from langchain_core.tools import tool
from tavily import TavilyClient
import os
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
@tool
def web_search(query: str) -> str:
"""Webを検索して最新情報を取得する"""
results = tavily.search(query=query, max_results=5)
formatted = []
for r in results["results"]:
formatted.append(f"**{r['title']}**
{r['content'][:300]}
URL: {r['url']}")
return "
---
".join(formatted)
@tool
def summarize_text(text: str) -> str:
"""長いテキストを要点に絞って���約する"""
summary_llm = ChatAnthropic(model="claude-haiku-4-5-20251001", temperature=0)
response = summary_llm.invoke(
f"以下のテキストを日本語で300字以内に要約してください:
{text}"
)
return response.content
@tool
def write_report(topic: str, findings: str) -> str:
"""調査結果をもとに構造化レポートを生成する"""
report_llm = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0.3)
response = report_llm.invoke(
f"以下のトピックと調査結果をもとに、h2見出し付きのレポートを作成してください。
"
f"トピック: {topic}
調査結果:
{findings}"
)
return response.content
tools = [web_search, summarize_text, write_report]
ポイントは、各ツールに@toolデコレータとdocstringをつけること。 LLMはこのdocstringを読んで「どのツールをいつ使うか」を判断する。
Step 3——LangGraphでエージェントのグラフを組む
ここが本記事の核心��。 LangGraphでは、エージェントの振る舞いを「状態マシン」として定義する。
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langchain_anthropic import ChatAnthropic
from dotenv import load_dotenv
load_dotenv()
# LLMにツールをバインド
llm = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0)
llm_with_tools = llm.bind_tools(tools)
# エージェントノード: LLMが次の行動を決める
def agent_node(state: MessagesState):
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
# 条件分岐: ツール呼び出しがあるか判定
def should_continue(state: MessagesState):
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tools"
return END
# グラフ構築
graph = StateGraph(MessagesState)
graph.add_node("agent", agent_node)
graph.add_node("tools", ToolNode(tools))
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
graph.add_edge("tools", "agent") # ツール実行後、エージェントに戻る
# コンパイル
app = graph.compile()
このコードが実現するフローはシンプルだ。
- ユーザーの入力を受け取る
- エージェント(LLM)が考え、ツール呼び出しを決定する
- ToolNodeがツールを実行し、結果をメッセージに追加する
- エージェントに戻り、再度判断する
- ツール呼び出しが不要になったら終了
Step 4——実行と動作確認
完成したエージェントを動かしてみよう。
from langchain_core.messages import HumanMessage
result = app.invoke({
"messages": [HumanMessage(
content="2026年のAIエージェント市場について調査し、主要プレイヤーと市場規模をまとめたレポートを作成して"
)]
})
# 最終メッセージを表示
print(result["messages"][-1].content)
実行すると、エージェントは以下のようなステップを踏む。
| ステップ | アクション | 詳細 |
|---|---|---|
| 1 | web_search | 「AI agent market size 2026」で検索 |
| 2 | summarize_text | 検索結果5���を要約 |
| 3 | web_search | 「AI agent major players companies 2026」で追加検索 |
| 4 | write_report | ��集した情報をもとにレポート生成 |
| 5 | 完了 | ユーザーにレポートを返却 |
たった50行ほどのコードで、Web検索→要約→レポート生成を自律的にこなすエージェントが完成した��
ストリーミングで中間過程を見る
エージェントの思考過程をリアルタイムで確認したい場合は、streamを使う。
for event in app.stream(
{"messages": [HumanMessage(content="AIエージェント市場を調査して")]},
stream_mode="updates"
):
for node_name, output in event.items():
print(f"
--- {node_name} ---")
if "messages" in output:
for msg in output["messages"]:
if hasattr(msg, "tool_calls") and msg.tool_calls:
for tc in msg.tool_calls:
print(f"ツール呼び出し: {tc['name']}({tc['args']})")
elif hasattr(msg, "content"):
print(msg.content[:200])
Step 5——本番運用に向けた改善
動くプロトタイプができた。 だが本番環境で使うには、いくつかの改善が必要だ。
コスト最適化
| 手法 | 効果 | 実装方法 |
|---|---|---|
| モデル使い分け | コスト50-70%削減 | 要約にHaiku、最終レポートにSonnetを使��� |
| プロンプトキャッシュ | 繰り返しコスト90%削減 | Anthropicのキャッシュ機能を有効化 |
| 最大ステップ数制限 | 暴走防止 | LangGraphのrecursion_limitを設定 |
# コンパイル時にループ上限を設定
app = graph.compile()
# 実行時にrecursion_limitを指定
result = app.invoke(
{"messages": [HumanMessage(content="調査して")]},
config={"recursion_limit": 15} # 最大15ステップ
)
Human-in-the-Loopの追加
エージェントが重要な判断をする前に、人間の承認を挟むこともできる。 LangGraphのinterrupt_beforeを使えばよい。
app = graph.compile(
interrupt_before=["tools"] # ツール実行前に一時停止
)
これにより、エージェントがツールを呼び出す前に処理が中断される。 開発者が内容を確認し、問題なければapp.invoke(None, config)で続行する。
エラーハンドリング
本番ではAPIの一時的な障害やレート制限に備える必要がある。 ツール関数にリトライロジックを入れておこう。
import time
from functools import wraps
def with_retry(max_retries=3, delay=1.0):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_retries - 1:
raise
time.sleep(delay * (attempt + 1))
return None
return wrapper
return decorator
次に作るべきもの——エージェントは進化を止めない
本記事で作ったのは「単一エージェント」だ。 一つのLLMが考え、一つのツールセットを使い、一つのタスクをこなす。
ここから先には、さらに広い世界がある。
マルチエージェントシステムでは、複数のエージェントが役割分担して協力する。 リサーチャー、ライター、レビュアーの3エージェントがチームとして動く構成は、LangGraphのサブグラフ機能で実現できる。
MCP(Model Context Protocol)との統合も見逃せない。 AnthropicのMCPを使えば、GitHub、Slack、データベースなど外部サービスとの接続がプラグイン感覚で可能になる。
メモリの永続化も実用上は不可欠だ。 LangGraphのMemorySaverや外部ベクトルDBとの連携で、エージェントに長期記憶を持たせられる。
AIエージェントの構築スキルは、2026年のエンジニアにとって最も汎用性の高い武器になりつつある。 まずは本記事のコードを手元で動かしてみてほしい��
そして考えてみよう。あなたの日常業務で、最初にエージェントに任せたいタスクは何だろうか。