はじめに
こんにちは、LLMチャットアプリの完成を目指している初心者エンジニアです。
前回はLangGraphでmemory機能を実装し、会話履歴を保持できました。今回はついに「Tool付きAgent」を作ります!
前回の記事
前回のmemoryのおさらい
checkpointer = InMemorySaver() # 履歴自動保存
app.invoke(..., {"configurable": {"thread_id": "1"}}) # 履歴自動復元
今回:このmemoryをそのまま使いながらTool追加!
Agentとは?Toolとは?
Agent(エージェント)とは
AgentはLLMに判断をさせる
ユーザー質問 → 「どのTool使う?」 → 判断 → Toolに指示
例:
ユーザー: 「東京の天気?」
Agent: 「get_weatherツールを使おう!」 → ToolNodeへパス
Tool(ツール)とは
Tool = LLMが使う部品
@toolで装飾した関数 → LLMが自動実行可能に!
例:
@tool # ←デコレータ(これで装飾するとLLMが使ってくれる)
def get_weather(city: str) -> str:
"""現在の天気をOpenWeatherAPIで取得"""
# API実行 → 「東京:晴れ22℃」
Agent(脳) → Tool(手足) → 外部API/DB/ファイル操作
Agent + Tool + Memoryの全体像
ユーザー → Agent → 「get_weatherツールが必要」 → ToolNode → OpenWeatherAPI
役割分担:
Agent:判断(どのTool?)
Tool:実行(API呼び出し)
Memory:履歴保持
LangGraph:流れ制御
全体のコード
前回のmemory版 → Tool完全対応版へ進化!
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_ollama import ChatOllama
from langchain_core.messages import SystemMessage
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
import requests
import os
@tool
def get_weather(city: str) -> str:
"""指定した都市の現在の天気と気温を取得します(OpenWeather API使用)。"""
api_key = os.getenv("OPENWEATHER_API_KEY")
if not api_key:
return "OPENWEATHER_API_KEY が設定されていません。"
city_map = {
"東京": "Tokyo,JP",
"大阪": "Osaka,JP",
"名古屋": "Nagoya,JP",
"札幌": "Sapporo,JP",
"福岡": "Fukuoka,JP",
}
q = city_map.get(city, city)
url = "https://api.openweathermap.org/data/2.5/weather"
params = {
"q": q,
"appid": api_key,
"units": "metric",
"lang": "ja",
}
resp = requests.get(url, params=params, timeout=10)
if resp.status_code != 200:
return f"OpenWeather APIエラー: {resp.status_code} {resp.text[:200]}"
data = resp.json()
temp = data["main"]["temp"]
desc = data["weather"][0]["description"]
humidity = data["main"]["humidity"]
return f"{city}の現在の天気: {desc}、気温: {temp}℃、湿度: {humidity}%"
tools = [get_weather]
# モデル
model = ChatOllama(model="gpt-oss:20b", temperature=0)
model_with_tools = model.bind_tools(tools)
checkpointer = InMemorySaver()
def agent_node(state: MessagesState):
system_msg = SystemMessage(content="""日本語で簡潔に答えてください。
- 現在の天気・気温: get_weather ツール
- 明日以降の天気予報: get_weather_forecast ツール
都市名は「東京」「大阪」など日本語で指定してください。""")
messages = [system_msg] + state["messages"]
response = model_with_tools.invoke(messages)
if response.tool_calls:
return {"messages": [response]}
return {"messages": [response], "__end__": True}
# グラフ
builder = StateGraph(MessagesState)
builder.add_node("agent", agent_node)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
builder.add_conditional_edges(
"agent",
lambda state: "tools" if state["messages"][-1].tool_calls else END,
{"tools": "tools", END: END}
)
builder.add_edge("tools", "agent")
app = builder.compile(checkpointer=checkpointer)
# デモ
print("=== 1回目: 天気検索 ===")
result1 = app.invoke(
{"messages": [{"role": "user", "content": "今日の東京の気温は?"}]},
{"configurable": {"thread_id": "1"}}
)
print("1回目:", result1["messages"][-1].content[:100] + "...")
print("\n=== 2回目: コーディネート ===")
result2 = app.invoke(
{"messages": [{"role": "user", "content": "今日はどんな服装したらいい?"}]},
{"configurable": {"thread_id": "1"}}
)
print("2回目:", result2["messages"][-1].content)
実際に実行してみます
実行結果:
$ python langchain_sample04.py
=== 1回目: 天気検索 ===
1回目: 東京の現在の気温は **7.35 ℃** です。...
=== 2回目: コーディネート ===
2回目: 晴天で気温は7.35 ℃、湿度が高いので、以下のようにコーディネートすると快適です。
| アイテム | 推奨ポイント |
|----------|--------------|
| **トップス** | 長袖の薄手ニットやカーディガン。外出時は軽いジャケットを羽織ると◎ |
| **ボトムス** | ズボンやスラックス。寒さがあるので、厚手のパンツよりは中程度の素材が良い。 |
| **アウター** | 風が強い場合は防風ジャケット。雨の心配はないので、軽めのコートで十分。 |
| **靴** | 靴下を履き、足元は防水性のあるスニーカーやブーツ。 |
| **アクセサリー** | 風があるときは薄手のマフラーや帽子。 |
**ポイント**
- 7℃前後は肌寒いので、重ね着で体温調節しやすい構成に。
- 湿度が高いので、汗をかきやすいので通気性の良い素材を選ぶと快適です。
以上を参考に、快適に過ごしてください!
memory + Tool両方動作していますね。
かなりチャットボットっぽくなりました。
コードのポイント
1. 前回のmemory継続利用
checkpointer = InMemorySaver()
{"configurable": {"thread_id": "1"}}
2. Tool追加(最大の進化)
@tool # 普通の関数→LLM用API
def get_weather(city: str) -> str:
# OpenWeatherAPI実装
3. model.bind_tools()
model_with_tools = model.bind_tools(tools) # LLMにTool教える
4. ToolNode追加
builder.add_node("tools", ToolNode(tools)) # 自動実行
5. 条件分岐強化
lambda state: "tools" if state["messages"][-1].tool_calls else END
LLMがtool_calls発行→自動ToolNodeへ
6. 日本語都市名対応(city_map)
city_map = {"東京": "Tokyo,JP", "大阪": "Osaka,JP", ...}
q = city_map.get(city, city) # 「東京」→"Tokyo,JP"
なぜ必要?
OpenWeatherAPIは「Tokyo,JP」形式必須
ユーザーは「東京」と入力 → マッピングで変換
未登録都市はそのまま通過(柔軟性確保)
7. paramsで日本語化:
params = {
"q": q, # Tokyo,JP
"lang": "ja", # 説明文を日本語に
"units": "metric" # ℃表示
}
Agent + Tool + Memoryの流れ
- 「今日の東京?」 → Agent → get_weather → 「7 °C、晴天、湿度92 %」 → Memory保存
- 「服装は?」 → Agent → Memory参照 → 「長袖・湿気対策」 →
Memory追加
さいごに
AgentとToolを組み合わせることで、チャットボットが「知りません」と答えるだけでなく、実際に天気APIを自動で呼び出して「東京の天気は晴れ22℃」のようにリアルタイムの情報を返せるようになりました。
LLMが質問内容から適切なToolを選び、API実行や履歴参照も自動化。
日本語の都市名もAPI用に変換し、会話の流れや過去情報も活用できる、実用的なチャットボットが完成しました!
今後の展望
次回ブラウザUI完成:
- Streamlitチャット画面
- 複数スレッド切替
- リアルタイム応答
参考