Loading
BLOG 開発者ブログ

2025年4月28日

LangChainのAgent機能を試してみる①:APIを利用したアシスタント制作

こんにちは、shibamです。

LangChainのAgent機能を使うと、自然言語の依頼をもとに、必要なAPIを自動で実行した上でその情報をもとに回答を生成できるアシスタントを作成できます。このシリーズの第一回目として、外部APIと連携した天気情報アシスタントを作成します。

このアシスタントでは、ユーザーが指定した場所の天気情報をOpenWeatherMap APIで取得し、その情報をもとにGPT-4oが最適な服装を提案します。ユーザーは「東京の天気と服装を教えて」と依頼するだけで、システムが自動的に必要な情報を集め、適切な回答を生成します。

目次

  1. プロジェクト概要
  2. 実装方法
  3. コードの解説
  4. 動作確認
  5. まとめ

1. プロジェクト概要

作成するものと機能

今回作成するのは、以下の機能を持つコマンドラインツールです。

  1. ユーザーが選択または入力した地名から緯度・経度情報を取得
  2. 取得した緯度・経度から現在の天気情報をAPI経由で取得
  3. 天気情報に基づいて、GPT-4oが最適な服装を提案

使用技術

  • LangChain: AIアプリケーション開発フレームワーク
  • OpenAI API: GPT-4oの利用(言語理解と服装提案)
  • OpenWeatherMap API: 天気情報の取得
  • Python: 実装言語

※LangChainの基本的なセットアップについては、「LangChainを使ってLLMアプリケーションを作ってみよう」の記事をご参照ください。

2. 実装方法

実装には、LangChainの「Agent」パターンを使用します。このパターンでは、AIが自律的に複数のツールを使い分けながら課題を解決します。

必要なもの

  • OpenAI APIキー
  • OpenWeatherMap APIキー(無料プラン可)
  • 以下のPythonパッケージ
pip install langchain langchain_openai requests python-dotenv rich
# 記事執筆時点でインストールしていたパッケージのバージョン情報
langchain==0.2.17
langchain-openai==0.1.25
requests==2.32.3
python-dotenv==1.0.1
rich==13.9.4

以下はサンプルコードです。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
weather_agent.py

LangChainのAgent機能を使って、指定した場所の天気情報を取得し、
GPT-4oを用いて最適な服装を提案するCLIツール
"""

import os
import sys
import requests
from dotenv import load_dotenv
from rich.console import Console
from rich.markdown import Markdown

from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.schema import SystemMessage
from langchain.agents import Tool, create_react_agent, AgentExecutor

# .envからAPIキーを読み込む
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY")

if OPENAI_API_KEY is None or OPENWEATHER_API_KEY is None:
    print("ERROR: OPENAI_API_KEY もしくは OPENWEATHER_API_KEY が設定されていません。")
    sys.exit(1)

# 基本都市の緯度・経度データ
LOCATIONS = {
    "東京": (35.6895, 139.6917),
    "大阪": (34.6937, 135.5022),
    "京都": (35.0116, 135.7681),
    "札幌": (43.0618, 141.3545),
    "仙台": (38.2682, 140.8694),
    "名古屋": (35.1815, 136.9066),
    "広島": (34.3853, 132.4553),
    "福岡": (33.5902, 130.4017),
    "那覇": (26.2123, 127.6792)
}

@tool
def get_lat_lon(location: str) -> str:
    """
    地名から緯度・経度を取得するツール
    """
    # まず辞書内を検索
    if location in LOCATIONS:
        lat, lon = LOCATIONS[location]
        return f"緯度: {lat}, 経度: {lon}"

    # 辞書になければGeocoding APIで検索
    try:
        url = (
            f"http://api.openweathermap.org/geo/1.0/direct"
            f"?q={location},JP&limit=1&appid={OPENWEATHER_API_KEY}"
        )
        resp = requests.get(url)
        resp.raise_for_status()
        data = resp.json()
        if data:
            lat = data[0]["lat"]
            lon = data[0]["lon"]
            return f"緯度: {lat}, 経度: {lon}"
        else:
            return "指定された地名の緯度・経度を取得できませんでした"
    except Exception as e:
        return f"エラーが発生しました: {e}"

@tool
def get_weather(lat_lon: str) -> str:
    """
    緯度・経度から現在の天気情報を取得するツール
    """
    try:
        # 「緯度: x, 経度: y」形式から数値を抽出
        lat_str = lat_lon.split("緯度: ")[1].split(",")[0].strip()
        lon_str = lat_lon.split("経度: ")[1].strip()
        lat, lon = float(lat_str), float(lon_str)

        url = (
            f"https://api.openweathermap.org/data/2.5/weather"
            f"?lat={lat}&lon={lon}"
            f"&appid={OPENWEATHER_API_KEY}&units=metric&lang=ja"
        )
        resp = requests.get(url)
        resp.raise_for_status()
        data = resp.json()
        w = data["weather"][0]["description"]
        t = data["main"]["temp"]
        f = data["main"]["feels_like"]
        h = data["main"]["humidity"]
        return f"天気: {w}, 気温: {t}°C, 体感温度: {f}°C, 湿度: {h}%"
    except Exception as e:
        return f"エラーが発生しました: {e}"

@tool
def recommend_clothing_llm(weather_info: str) -> str:
    """
    天気情報に基づいてGPT-4oが服装を提案するツール
    """
    try:
        llm = ChatOpenAI(
            temperature=0.7,
            model="gpt-4o",
            openai_api_key=OPENAI_API_KEY
        )
        prompt = f"""
以下の天気情報に基づいて、適切な服装のアドバイスを具体的に提案してください:

{weather_info}

【服装の要素】
1. 上着(シャツ、セーター、コートなど)
2. 下着(ズボン、スカートなど)
3. 必要ならアクセサリー(帽子、マフラー、手袋など)
4. その他推奨事項(傘、日焼け止めなど)

日本語でわかりやすくお願いします。
"""
        response = llm.invoke(prompt)
        return response.content
    except Exception as e:
        return f"服装の提案中にエラーが発生しました: {e}"

def get_location_from_user() -> str:
    """
    ユーザーに地名を選択または入力してもらう関数
    """
    print("場所を選択または入力してください:")
    cities = list(LOCATIONS.keys())[:5]
    for i, city in enumerate(cities, 1):
        print(f"{i}. {city}")
    print(f"{len(cities)+1}. その他(直接入力)")

    while True:
        choice = input("選択してください(番号): ").strip()
        if not choice.isdigit():
            print("数字を入力してください。")
            continue
        idx = int(choice)
        if 1 <= idx <= len(cities):
            return cities[idx-1]
        elif idx == len(cities) + 1:
            return input("場所を入力してください: ").strip()
        else:
            print("無効な選択です。もう一度お試しください。")

def weather_clothing_agent():
    """
    メインのAgent実行関数
    """
    # ユーザー入力
    location = get_location_from_user()

    # Agent用ツールリスト
    tools = [
        Tool(name="GetLatLon",
             func=get_lat_lon,
             description="地名から緯度・経度を取得する"),
        Tool(name="GetWeather",
             func=get_weather,
             description="緯度・経度から天気情報を取得する"),
        Tool(name="RecommendClothing",
             func=recommend_clothing_llm,
             description="天気情報から服装を提案する"),
    ]

    # システムメッセージ
    template = """あなたは天気と服装のアドバイザーです。以下の手順に従ってください:
1. 地名から緯度・経度を取得
2. 緯度・経度から現在の天気を取得
3. 天気情報に基づいて服装を提案
回答は日本語でわかりやすく提供してください。

利用可能なツール:
{tools}

ツール名: {tool_names}

以下の形式を守ってください:

Question: 質問
Thought: 何をすべきか考える
Action: 使用するツール名
Action Input: ツールへの入力
Observation: ツールからの出力
...(Thought/Action/Action Input/Observationを繰り返し)...
Thought: これで最終的な答えがわかりました
Final Answer: 質問への最終回答

Begin!

Question: {input}
{agent_scratchpad}"""

    # PromptTemplate を生成
    prompt = PromptTemplate.from_template(template)
    llm = ChatOpenAI(
        temperature=0,
        model="gpt-4o",
        openai_api_key=OPENAI_API_KEY
    )

    agent = create_react_agent(
        llm=llm,
        tools=tools,
        prompt=prompt,
    )
    executor = AgentExecutor(
        agent=agent,
        tools=tools,
        verbose=True,
        max_iterations=5,
        handle_parsing_errors=True,
    )

    query = f"{location}の現在の天気と適切な服装を教えてください。"
    result = executor.invoke({"input": query})

    # 結果整形
    output = result.get("output") if isinstance(result, dict) else str(result)
    console = Console(width=100)
    console.print(Markdown(output))

if __name__ == "__main__":
    weather_clothing_agent()

3. コードの解説

全体の構成

コードは以下の部分で構成されています。

  1. 都市データの定義
  2. ツール(機能)の定義
    • 地名から緯度・経度取得
    • 緯度・経度から天気情報取得
    • 天気情報から服装提案
  3. ユーザーインターフェース
  4. Agent実行部分

主要部分の解説

1. 都市データの定義

# 主要都市の緯度・経度データを辞書で用意
LOCATIONS = {
    "東京": (35.6895, 139.6917),
    "大阪": (34.6937, 135.5022),
    "京都": (35.0116, 135.7681),
    "札幌": (43.0618, 141.3545),
    "仙台": (38.2682, 140.8694),
    "名古屋": (35.1815, 136.9066),
    "広島": (34.3853, 132.4553),
    "福岡": (33.5902, 130.4017),
    "那覇": (26.2123, 127.6792)
}

2. ツールの定義

@tool
def get_lat_lon(location: str) -> str:
    """地名から緯度・経度を取得する関数"""
    # 辞書に地名があればその緯度・経度を返す
    if location in LOCATIONS:
        lat, lon = LOCATIONS[location]
        return f"緯度: {lat}, 経度: {lon}"
    
    # 辞書に地名がなければOpenWeatherMapのGeocoding APIで検索
    try:
        api_key = WEATHER_API_KEY
        url = f"http://api.openweathermap.org/geo/1.0/direct?q={location},JP&limit=1&appid={api_key}"
        response = requests.get(url)
        data = response.json()
        
        if data and len(data) > 0:
            lat = data[0]["lat"]
            lon = data[0]["lon"]
            return f"緯度: {lat}, 経度: {lon}"
        else:
            return "指定された地名の緯度・経度を取得できませんでした"
    except Exception as e:
        return f"エラーが発生しました: {str(e)}"

@tool
def get_weather(lat_lon: str) -> str:
    """緯度・経度から現在の天気を取得する関数"""
    try:
        # 緯度・経度を抽出
        lat_str = lat_lon.split("緯度: ")[1].split(",")[0].strip()
        lon_str = lat_lon.split("経度: ")[1].strip()
        lat, lon = float(lat_str), float(lon_str)
        
        # OpenWeatherMap APIで天気を取得
        api_key = WEATHER_API_KEY
        url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={api_key}&units=metric&lang=ja"
        response = requests.get(url)
        data = response.json()
        
        if response.status_code == 200:
            weather_desc = data["weather"][0]["description"]
            temp = data["main"]["temp"]
            feels_like = data["main"]["feels_like"]
            humidity = data["main"]["humidity"]
            
            return f"天気: {weather_desc}, 気温: {temp}°C, 体感温度: {feels_like}°C, 湿度: {humidity}%"
        else:
            return "天気情報を取得できませんでした"
    except Exception as e:
        return f"エラーが発生しました: {str(e)}"

@tool
def recommend_clothing_llm(weather_info: str) -> str:
    """天気情報に基づいて服装を提案する関数"""
    try:
        # LLMを初期化
        llm = ChatOpenAI(temperature=0.7, model="gpt-4o")
        
        # LLMへのプロンプト
        prompt = f"""
以下の天気情報に基づいて、適切な服装のアドバイスを具体的に提案してください:

{weather_info}

服装のアドバイスには、以下の要素を含めてください:
1. 上着(シャツ、セーター、コートなど)
2. 下着(ズボン、スカートなど)
3. 必要な場合は、アクセサリー(帽子、マフラー、手袋など)
4. その他の推奨事項(傘、日焼け止めなど)

回答は、日本語でまとめてください。
"""
        # LLMを呼び出し
        response = llm.invoke(prompt)
        return response.content
    except Exception as e:
        return f"服装の提案中にエラーが発生しました: {str(e)}"

3. ユーザーインターフェース

def get_location_from_user():
    """ユーザーから場所を選択または入力してもらう関数"""
    print("場所を選択または入力してください:")
    print("利用可能な都市:")
    
    # 利用可能な都市の一覧を表示(簡略化版)
    cities = list(LOCATIONS.keys())[:5]  # 最初の5都市だけ表示
    for i, city in enumerate(cities, 1):
        print(f"{i}. {city}")
    
    print(f"{len(cities) + 1}. その他(直接入力)")
    
    while True:
        try:
            choice = int(input("選択してください(番号): "))
            if 1 <= choice <= len(cities):
                # リストから選択された都市
                return cities[choice - 1]
            elif choice == len(cities) + 1:
                # 直接入力
                location = input("場所を入力してください: ")
                return location
            else:
                print("無効な選択です。もう一度お試しください。")
        except ValueError:
            print("数字を入力してください。")

4. Agent設定と実行

def weather_clothing_agent():
    """メイン処理を行う関数"""
    # ユーザーからの入力を受け取る
    location = get_location_from_user()
    
    # ツールの定義
    tools = [
        Tool(name="GetLatLon", func=get_lat_lon, description="地名から緯度・経度を取得する"),
        Tool(name="GetWeather", func=get_weather, description="緯度・経度から天気情報を取得する"),
        Tool(name="RecommendClothing", func=recommend_clothing_llm, description="天気情報から適切な服装を提案する")
    ]
    
    # システムメッセージ
    system_message = SystemMessage(
        content="""あなたは天気と服装のアドバイザーです。ユーザーが指定した場所の天気情報を取得し、
適切な服装を提案してください。必ず次の手順に従ってください:

1. まず地名から緯度・経度を取得する
2. 次に緯度・経度から現在の天気情報を取得する
3. 最後に天気情報に基づいて適切な服装を提案する

回答は日本語でわかりやすく提供してください。"""
    )
    
    # プロンプトテンプレート
    template = """以下の質問に最善を尽くして答えてください。以下のツールを使用できます:

{tools}

ツール名:{tool_names}

以下の形式を使用してください:

Question: あなたが答えるべき入力質問
Thought: 何をすべきか考えます
Action: 使用するツール名
Action Input: ツールへの入力
Observation: ツールからの出力
... (このThought/Action/Action Input/Observationのサイクルは複数回繰り返すことができます)
Thought: これで最終的な答えがわかりました
Final Answer: 元の質問に対する最終的な答え

Begin!

Question: {input}
{agent_scratchpad}"""

    prompt = PromptTemplate.from_template(template)
    
    # ChatGPTモデルの初期化
    llm = ChatOpenAI(temperature=0, model="gpt-4o")
    
    # Agentの作成
    agent = create_react_agent(llm, tools, prompt)
    
    # Agentの実行
    agent_executor = AgentExecutor(
        agent=agent,
        tools=tools,
        verbose=True,
        max_iterations=5,
        handle_parsing_errors=True
    )
    
    # クエリの実行
    query = f"{location}の現在の天気と適切な服装を教えてください。"
    response = agent_executor.invoke({"input": query})
    
    # 結果の表示
    if isinstance(response, dict) and "output" in response:
        final_message = response["output"]
    else:
        final_message = str(response)
    
    print("\n最終的な回答:")
    # Markdown形式の出力を表示
    from rich.markdown import Markdown
    from rich.console import Console
    
    console = Console(width=100)
    console.print(Markdown(final_message))

if __name__ == "__main__":
    weather_clothing_agent()

プロンプト設計のポイント

Agentを正しく動作させるためには、プロンプト設計が非常に重要です。以下に、今回のプロンプト設計のポイントを解説します。

1. システムメッセージでの明確な指示

    # システムメッセージ
    system_message = SystemMessage(
        content="""あなたは天気と服装のアドバイザーです。ユーザーが指定した場所の天気情報を取得し、
適切な服装を提案してください。必ず次の手順に従ってください:

1. まず地名から緯度・経度を取得する
2. 次に緯度・経度から現在の天気情報を取得する
3. 最後に天気情報に基づいて適切な服装を提案する

回答は日本語でわかりやすく提供してください。"""
    )

ここでは、Agentに「何者か」「何をすべきか」「どのような順序で処理すべきか」を明確に指示しています。特に処理の順序を明示することで、Agentが適切なツールを順番に使用するよう誘導しています。

2. ReActフォーマットの活用

    # プロンプトテンプレート
    template = """以下の質問に最善を尽くして答えてください。以下のツールを使用できます:

{tools}

ツール名:{tool_names}

以下の形式を使用してください:

Question: あなたが答えるべき入力質問
Thought: 何をすべきか考えます
Action: 使用するツール名
Action Input: ツールへの入力
Observation: ツールからの出力
... (このThought/Action/Action Input/Observationのサイクルは複数回繰り返すことができます)
Thought: これで最終的な答えがわかりました
Final Answer: 元の質問に対する最終的な答え

Begin!

Question: {input}
{agent_scratchpad}"""

このプロンプトは「ReAct」(Reasoning and Acting)と呼ばれる形式を採用しています。Agentに「考え(Thought)」→「行動(Action)」→「観察(Observation)」のサイクルを繰り返させることで、複雑なタスクを段階的に解決できるようにしています。

特に重要なのは以下の点です:

  • Thought: Agentに次に何をすべきか考えさせる
  • Action: 使用するツール名を明示させる
  • Action Input: ツールへの入力を具体的に指定させる
  • Observation: ツールからの出力を確認させる

この形式により、Agentは自分の思考プロセスを明示的に示しながら、適切なタイミングで適切なツールを使用できるようになります。

なお、今回ご紹介したReAct形式に加えて、LangGraphを利用してAgentの思考フローをより細かく制御することも可能です。

LangChainは現在、LangGraphによるAgentの実装を推奨しているため、次回以降の記事ではその点についても詳しく紹介したいと思います。

3. Agent実行部分

    # ChatGPTモデルの初期化
    llm = ChatOpenAI(temperature=0, model="gpt-4o")
    
    # Agentの作成
    agent = create_react_agent(llm, tools, prompt)
    
    # Agentの実行
    agent_executor = AgentExecutor(
        agent=agent,
        tools=tools,
        verbose=True,
        max_iterations=5,
        handle_parsing_errors=True
    )

Agent実行時の設定も重要です。以下は基本的な設定になります。

  • temperature=0: 決定論的な応答を得るために設定
  • verbose=True: Agentの思考プロセスを表示
  • max_iterations=5: 無限ループを防ぐための最大反復回数
  • handle_parsing_errors=True: Agentの出力形式エラーを自動修正

4. 動作確認

以下の手順で作成したCLIツールを動作確認します。

  1. スクリプトの実行
    python weather_agent.py

  2. プロンプトに従って場所を選択または入力
    場所を選択または入力してください:
    1. 東京
    2. 大阪
    3. 札幌
    4. 福岡
    5. 名古屋
    6. その他(直接入力)
    選択してください(番号): 1

実行結果の例:

Thought: ツールget_locationを呼び出します  
Action: get_location  
Action Input: 東京  
Observation: 緯度: 35.6895, 経度: 139.6917  

Thought: ツールget_weatherを呼び出します  
Action: get_weather  
Action Input: 緯度: 35.6895, 経度: 139.6917  
Observation: 天気: 晴れ, 気温: 20°C, 体感温度: 19°C, 湿度: 50%  

Thought: ツールrecommend_clothing_llmを呼び出します  
Action: recommend_clothing_llm  
Action Input: 天気: 晴れ, 気温: 20°C, 体感温度: 19°C, 湿度: 50%  
Observation: (省略)  

Final Answer:  
東京の現在の天気は晴れ、気温は20°C、体感温度は19°C、湿度は50%です。おすすめの服装は、薄手のシャツに軽めのジャケット、ジーンズ、帽子とサングラスです。

5. まとめ

今回は、LangChainのAgent機能を使って、自然言語の指示からAPIを自動実行し、その結果をもとに回答を生成するアシスタントを作成しました。

このようなAgentベースのアプローチの最大の利点は、ユーザーが複雑な処理手順を意識することなく、自然な形で情報を得られる点です。システムが自動的に必要なAPIを呼び出し、情報を収集・処理して、最終的な回答を生成します。

プロンプト設計は、Agentの動作を制御する上で非常に重要です。特に、処理の順序や使用すべきツールを明確に指示することで、より信頼性の高いAgentを構築できます。

 

 

shibamのブログ