NTTドコモR&Dの技術ブログです。

【初心者向け】LangGraphとGradioで作るAIエージェント検証環境

今回のテーマ

こんにちは!NTTドコモマーケティングメディア部の石塚裕之です! 業務ではAIエージェントアプリの開発をしています。

最近AIエージェントやLLMを使ったアプリやサービスを作るにあたって簡単にWebアプリを作って試す機会が増えてきました。 私が所属するチームではフロントをGradioで構築してAIエージェントの動作を確認することが多いため今回の記事ではそのフロントでAIと会話できる画面を作成した後に、簡単にLanggraphでAIエージェントを開発してみたいと思います!

開発環境構築

フロント側とバックエンド側のコンテナを用意するためにDocker Composeで構築していきます。

Docker での開発環境の構築

仮想環境の作成とライブラリのインストール

python3 -m venv venv
source venv/bin/activate
pip install google-genai langchain langgraph pydantic python-dotenv requests gradio uvicorn logger

pip freeze > requirements.txt

agent Dockerfile

# agent/Dockerfile
FROM python:3.12-slim

COPY ./agent/ /opt/src/
RUN pip install -r /opt/src/requirements.txt
WORKDIR /opt/src

front Dockerfilesの作成

# front/Dockerfile
FROM python:3.12-slim

COPY front/ /opt/src
RUN pip install -r /opt/src/requirements.txt

WORKDIR /opt/src

docker-compose.ymlの作成

# docker-compose.yml
services:
  walk_guide_agent:
    build:
      context: .
      dockerfile: agent/Dockerfile
    image: "walk_guide_agent"
    container_name: "walk_guide_agent"
    command: uvicorn agent_flow:app --reload --host 0.0.0.0 --port 8000
    ports:
      - 8000:8000
    env_file:
      - ./.env
    volumes:
      - ./agent:/opt/src
  

  frontend:
    build:
      context: .
      dockerfile: "front/Dockerfile"
    ports:
      - 8088:8088
    depends_on:
      - walk_guide_agent
    image: "walk_guide_front"
    container_name: "walk_guide_front"
    env_file:
      - ./.env
    command: uvicorn frontend:app --reload --host 0.0.0.0 --port 8088
    volumes:
      - ./front:/opt/src

チャットを試せるフロントをGradioで作成

フロントエンドを用意してAIエージェントの対話のテストをやりやすい環境を作っていきます。Gradioには様々なWebUIが準備されていますが今回は対話画面として使用するためChatbotを使用していきます。

# front/frontend.py
import time
import gradio as gr
from logging import getLogger
from fastapi import FastAPI 
import requests

logger = getLogger(__name__)

BACKEND_URL = "http://walk_guide_agent:8000"

def chat_response(user_input, history):
    logger.info(f"Received user input: {user_input}")
    
    if history is None:
        history = []

    try:
        response = requests.post(
            f"{BACKEND_URL}/chat",
            json={"message": user_input}
        )
        response.raise_for_status()
        bot_response = response.json().get("message", "応答を取得できませんでした。")
    except requests.exceptions.RequestException as e:
        logger.error(f"Backend request failed: {e}")
        bot_response = "エラーが発生しました。もう一度お試しください。"
    
    logger.info(f"Generated response: {bot_response}")
    history.append((user_input, bot_response))
    return history, ""

with gr.Blocks() as demo:
    gr.Markdown("# 散歩サポートAIエージェント")

    with gr.Tab("チャット"):
        chatbot = gr.Chatbot(label="Chat Box")
        msg_input = gr.Textbox(show_label=False, placeholder="メッセージを入力")
        
        msg_input.submit(
            fn=chat_response, 
            inputs=[msg_input, chatbot], 
            outputs=[chatbot, msg_input]
        )

app = FastAPI()

@app.get('/health')
async def healthcheck():
    return 200

app = gr.mount_gradio_app(app, demo, path="", show_api=False)

エージェント側はいったん定型文を返す形にしておきます。

# agent/app.py
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.post("/chat")
async def chat():
    return {"message": "こんにちわ"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

こちらをdocker compose up -dでコンテナを立ち上げてテストします。 メッセージ入力欄から文字を送信することで定型文が帰ってくるようになりました。

Gradio 対話画面

AIエージェントの実装

Gemini API Keyを発行し.envファイルに設定します。

# .env
GOOGLE_API_KEY=your_google_api_key_here
# agent/app.py

from fastapi import FastAPI
import uvicorn
import os
from dotenv import load_dotenv
from pydantic import BaseModel
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.graph import StateGraph, MessagesState, START, END

# 環境変数の読み込み (.envからAPIキーを取得)
load_dotenv()

# Geminiモデルの初期化
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")

# ノードの定義
def call_model_node(state: MessagesState):
    messages = state["messages"]
    response = llm.invoke(messages)
    return {"messages": [response]}

# グラフの構築
workflow = StateGraph(MessagesState)

# ノードの追加
workflow.add_node("agent", call_model_node)

# エッジ(流れ)の定義: スタート -> agent -> 終了
workflow.add_edge(START, "agent")
workflow.add_edge("agent", END)

# グラフのコンパイル
agent_app = workflow.compile()

app = FastAPI()

# リクエストボディの定義
class ChatRequest(BaseModel):
    message: str

@app.post("/chat")
async def chat(request: ChatRequest):
    # ユーザーの入力をLangGraph用のメッセージ形式に変換
    input_message = HumanMessage(content=request.message)
    
    # グラフを実行
    result = agent_app.invoke({"messages": [input_message]})
    agent_message = result["messages"][-1].content
    
    return {"message": agent_message}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

agent/app.pyの変更後にdocker compose down docker compose up -d で再起動します。

これでGradioからチャットを送るとグラフが実行されGeminiの応答が表示されるようになりました。

LLMとの対話

お散歩エージェントの構築

グラフとツールの実装

今回は散歩エージェントなので

位置情報から散歩に適切な距離のスポット(ゴール)を設定

経路沿いからスポットを取得

取得した情報からユーザーに応答を生成 の流れをLangGraphで実装していきます。

位置情報から適切な距離のスポットを設定する箇所は最初APIでスポット取得してから距離測定して適切な距離のスポットを選定していたのですが、うまくいかなかったのでここをAIエージェントにしています。

スポット取得と距離測定をツール化してGeminiに投げている形です。 またツールを実装するにあたってGeminiには厳格なメッセージ順序があり、そこでエラー発生することがあるので(Humanの次はAI、AIの次はToolなど)Stateに新たにToolMessageを追加してツールの実行結果をそこにためていきます。

@tool
def search_places_tool(latitude: float, longitude: float, radius: float = 4000):
    """
    指定された座標(現在地)の周辺にある観光スポット、公園、神社などを検索し、
    現在地からの距離(km)も計算して返します。
    """
    # 1. 実行開始ログ(引数の確認)
    logger.info(f"search_places_tool called: lat={latitude}, lng={longitude}, radius={radius}")

    if radius < 100:
        logger.warning(f"Radius {radius} seems too small. Converting km to meters.")
        radius = radius * 1000

    url = GPLACES_API_URL
    headers = {
        "Content-Type": "application/json",
        "X-Goog-Api-Key": GPLACES_API_KEY,
        "X-Goog-FieldMask": "places.displayName,places.location,places.formattedAddress,places.types"
    }
    payload = {
        "includedTypes": ["tourist_attraction", "park"],
        "maxResultCount": 20,
        "locationRestriction": {
            "circle": {
                "center": {"latitude": latitude, "longitude": longitude},
                "radius": radius
            }
        }
    }

    try:
        # リクエスト送信
        response = requests.post(url, json=payload, headers=headers)
        
        if response.status_code != 200:
            logger.error(f"Google API Error: Status={response.status_code}, Body={response.text}")
            response.raise_for_status()

        data = response.json()
        places = data.get("places", [])
        
        logger.info(f"Google API Success: Found {len(places)} places.")
        
        if not places:
            logger.warning(f"No places found in response. Raw data: {data}")

        simplified_places = []
        for p in places:
            p_lat = p["location"]["latitude"]
            p_lng = p["location"]["longitude"]
            distance = geodesic((latitude, longitude), (p_lat, p_lng)).km
            
            simplified_places.append({
                "name": p.get("displayName", {}).get("text", "Unknown"),
                "location": p.get("location", {}),
                "distance_km": round(distance, 2),
                "types": p.get("types", [])
            })
            
        return json.dumps(simplified_places, ensure_ascii=False)

    except requests.exceptions.RequestException as e:
        error_msg = f"API Request Error in search_places_tool: {e}"
        logger.error(error_msg)
        return error_msg
        
    except Exception as e:
        logger.exception("Unexpected error in search_places_tool")
        return f"Unexpected Error: {e}"

@tool
def get_distance_tool(start_lat: float, start_lng: float, end_lat: float, end_lng: float) -> float:
    """2点間の距離(km)を正確に計算します。"""
    try:
        dist = geodesic((start_lat, start_lng), (end_lat, end_lng)).km
        return dist
    except Exception as e:
        logger.error(f"Distance calculation error: {e}")
        return -1.0

試しに東京駅の座標を指定して実行してみます。

散歩エージェント結果画面

これで一通りAIエージェントにツールを持たせて応答を得ることができました。ここから検証を重ねて必要なツールを追加したりプロンプトをチューニングしたりして肉付けをしていくイメージです。

簡単に自分の環境で効果や性能を試しながら、役立つサービスを作っていきます。最近はAI駆動開発やレビューなどにもAIと共に効率化を図れる箇所が多くあると感じています。最新の機能やツールを活用してこういった検証を加速させていければと思います。ぜひAIエージェント開発を始めてみてほしいです。