今回のテーマ
こんにちは!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でコンテナを立ち上げてテストします。 メッセージ入力欄から文字を送信することで定型文が帰ってくるようになりました。

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の応答が表示されるようになりました。

お散歩エージェントの構築
グラフとツールの実装
今回は散歩エージェントなので
位置情報から散歩に適切な距離のスポット(ゴール)を設定
↓
経路沿いからスポットを取得
↓
取得した情報からユーザーに応答を生成 の流れを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エージェント開発を始めてみてほしいです。