1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Agentとtoolの基礎 - Streamlitを用いた外部検索付きLLMのチャットデモまでの道のり③

Last updated at Posted at 2025-12-16

はじめに

こんにちは、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の流れ

  1. 「今日の東京?」 → Agent → get_weather → 「7 °C、晴天、湿度92 %」 → Memory保存
  2. 「服装は?」 → Agent → Memory参照 → 「長袖・湿気対策」 →
    Memory追加

さいごに

AgentとToolを組み合わせることで、チャットボットが「知りません」と答えるだけでなく、実際に天気APIを自動で呼び出して「東京の天気は晴れ22℃」のようにリアルタイムの情報を返せるようになりました。
LLMが質問内容から適切なToolを選び、API実行や履歴参照も自動化。
日本語の都市名もAPI用に変換し、会話の流れや過去情報も活用できる、実用的なチャットボットが完成しました!

今後の展望
次回ブラウザUI完成:

  • Streamlitチャット画面
  • 複数スレッド切替
  • リアルタイム応答

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?