【第4回】Function Calling実践編:外部APIと連携する自律型AIエージェント
はじめに
LLMアプリケーション開発連載、第4回へようこそ。これまでの連載で、私たちは独自の知識を持つRAGチャットボットを構築し、モダンなチャットUIからストリーミングで対話できるようになりました。
しかし、このチャットボットはまだ「知っていること」しか話せません。リアルタイムの情報にアクセスしたり、外部システムを操作したりすることはできません。そこで登場するのが、LLMに「行動」する能力を与える「Function Calling」と、それを活用した「AIエージェント」です。
本記事では、以下の技術を組み合わせ、LLMが外部ツールを自律的に利用するAIエージェントを構築する具体的な手順をハンズオン形式で解説します。
- OpenAI Function Calling: LLMが外部関数を呼び出すための強力な機能。
- LangChain Agent: LLMを頭脳として、次に何をすべきかを推論し、ツールを実行するフレームワーク。
- FastAPI: バックエンドAPIとして、Agentの機能を公開。
この記事を読み終える頃には、あなたのLLMアプリは単なるQ&Aボットから、実世界とインタラクションできる強力な「AIエージェント」へと進化しているでしょう。
1. Function Callingとは何か? LLMが「行動」する仕組み
LLMは膨大なテキストデータで学習していますが、リアルタイムの情報や、外部システムを操作する能力は持っていません。Function Callingは、このギャップを埋めるための仕組みです。
- 役割: LLMがユーザーの意図を理解し、外部ツール(関数)を呼び出すべきかを判断し、そのための引数を生成します。LLMは関数を直接実行するわけではなく、「この関数を、この引数で呼び出すべきだ」というJSON形式の指示を出力します。
- AIエージェントにおける位置づけ: AIエージェントは、LLMを「頭脳」として、目標達成のために自律的に計画を立て、ツールを使い、行動します。Function Callingは、このエージェントが「行動」するための具体的な「手足」となります。
2. LangChain Agentの基本構造
LangChainのAgentは、LLMとTool(ツール)を組み合わせて、複雑なタスクを自律的に実行するためのフレームワークです。
- Agent: LLMを頭脳として、ユーザーの入力と利用可能なツールを考慮し、次に何をすべきか(ツールを呼び出すか、直接回答するか)を推論します。
- Tools: Agentが利用できる外部の機能(Python関数、API呼び出しなど)です。各ツールには、その機能と引数に関する説明が与えられます。
- Agent Executor: Agentの推論とToolの実行を繰り返すループです。AgentがToolの呼び出しを決定すると、ExecutorがそのToolを実行し、結果をAgentにフィードバックします。このプロセスは、目標が達成されるか、エラーが発生するまで繰り返されます。
3. 【実践】天気情報APIと連携するAIエージェントの構築
今回は、ユーザーの質問に応じて天気情報APIを呼び出すAIエージェントを構築します。
3.1 開発環境のセットアップ
前回のプロジェクトに引き続き、必要なライブラリを追加インストールします。
cd llm-rag-api # バックエンドプロジェクトのディレクトリ
source venv/bin/activate
pip install langchain-community # Toolの定義に必要
.env
ファイルにOpenWeatherMapのAPIキーを追加します。
OPENAI_API_KEY="sk-..."
OPENWEATHERMAP_API_KEY="your_openweathermap_api_key"
3.2 Toolの定義
tools.py
ファイルを作成し、天気情報取得のToolを定義します。
“`python:tools.py
import os
import requests
from langchain.tools import tool
OpenWeatherMap APIキー
OPENWEATHERMAP_API_KEY = os.getenv(“OPENWEATHERMAP_API_KEY”)
@tool
def get_current_weather(location: str) -> str:
“””
指定された場所の現在の天気情報を取得します。
location: 天気情報を取得したい都市名(例: Tokyo, London)
“””
if not OPENWEATHERMAP_API_KEY:
return “OpenWeatherMap APIキーが設定されていません。”
base_url = "http://api.openweathermap.org/data/2.5/weather"
params = {
"q": location,
"appid": OPENWEATHERMAP_API_KEY,
"units": "metric", # 摂氏で取得
"lang": "ja" # 日本語で取得
}
try:
response = requests.get(base_url, params=params)
response.raise_for_status() # HTTPエラーがあれば例外を発生
data = response.json()
if data["cod"] == 200:
weather_description = data["weather"][0]["description"]
temperature = data["main"]["temp"]
return f"{location}の現在の天気は{weather_description}、気温は{temperature}℃です。"
else:
return f"天気情報を取得できませんでした: {data.get("message", "不明なエラー")}"
except requests.exceptions.RequestException as e:
return f"天気情報APIへのリクエスト中にエラーが発生しました: {e}"
`@tool`デコレータは、Python関数をLangChainのToolとして認識させるためのものです。関数のdocstringと引数の型ヒントが、LLMがToolの使い方を理解するための重要な情報となります。
### 3.3 Agentの構築と実行
`main.py`を修正し、Agentの機能を追加します。
```python:main.py
import os
import asyncio
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
from rag_chain import create_rag_chain # RAG機能も引き続き利用
from tools import get_current_weather # 新しく定義したTool
# .envファイルを読み込む
load_dotenv()
app = FastAPI()
# --- RAGチェーンの初期化(前回と同じ)---
# RAGチェーンをグローバルに保持(本番ではより良い方法を検討)
# documents/my-knowledge.pdf が存在しない場合はコメントアウトするか、適切なパスを設定してください
# rag_chain = create_rag_chain("./documents/my-knowledge.pdf")
# --- Agentの初期化 ---
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# Agentが利用できるツールをリストアップ
agent_tools = [
get_current_weather,
# ここに他のツールを追加できます
]
# Agentのプロンプト定義
prompt = ChatPromptTemplate.from_messages(
[
("system", "あなたは親切なAIアシスタントです。ユーザーの質問に答え、必要に応じてツールを使用してください。"),
MessagesPlaceholder(variable_name="chat_history"), # 過去の会話履歴
("human", "{input}"), # 現在のユーザー入力
MessagesPlaceholder(variable_name="agent_scratchpad"), # Agentの思考プロセス
]
)
# Agentの作成
agent = create_openai_tools_agent(llm, agent_tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=agent_tools, verbose=True)
class ChatRequest(BaseModel):
message: str
chat_history: list = [] # 過去の会話履歴を保持
@app.post("/chat")
async def chat_with_agent(request: ChatRequest):
async def stream_generator():
# LangChainのメッセージ形式に変換
lc_chat_history = []
for msg in request.chat_history:
if msg["type"] == "human":
lc_chat_history.append(HumanMessage(content=msg["content"]))
elif msg["type"] == "ai":
lc_chat_history.append(AIMessage(content=msg["content"]))
# ToolMessageも必要に応じて追加
try:
# Agentの実行をストリーム
async for chunk in agent_executor.astream(
{"input": request.message, "chat_history": lc_chat_history}
):
if "messages" in chunk:
for message in chunk["messages"]:
if isinstance(message, AIMessage):
# Agentの思考や最終回答
if message.content:
yield f"data: {message.content}\n\n"
if message.tool_calls:
# Agentがツール呼び出しを決定した場合
for tool_call in message.tool_calls:
yield f"data: Tool Call: {tool_call.name}({tool_call.args})\n\n"
elif isinstance(message, ToolMessage):
# ツールの実行結果
yield f"data: Tool Output: {message.content}\n\n"
elif "output" in chunk:
# Agentの最終出力(通常はAIMessageに含まれるが念のため)
if chunk["output"]:
yield f"data: Final Output: {chunk['output']}\n\n"
await asyncio.sleep(0.05) # ストリーミングのリアルタイム感を出すための遅延
except Exception as e:
yield f"data: Error during agent execution: {str(e)}\n\n"
print(f"Agent streaming error: {e}")
return StreamingResponse(stream_generator(), media_type="text/event-stream")
@app.get("/")
def read_root():
return {"message": "LLM Agent API is running."}
ポイント: /chat
エンドポイントでは、ユーザーのメッセージだけでなく、過去の会話履歴も受け取るようにしています。これにより、Agentは文脈を理解した上で適切なToolを呼び出したり、会話を継続したりできます。
4. 動作確認
-
FastAPIサーバーの起動:
llm-rag-api
ディレクトリでFastAPIサーバーを起動します。bash
cd llm-rag-api # プロジェクトルートから移動
source venv/bin/activate
uvicorn main:app --reload --port 8000 -
curl
でテスト: 別のターミナルからcurl
コマンドでAPIを叩いてみましょう。bash
curl -X POST "http://127.0.0.1:8000/chat" \
-H "Content-Type: application/json" \
-d '{"message": "今日の東京の天気は?", "chat_history": []}' --no-bufferAgentが
get_current_weather
ツールを呼び出し、天気情報を取得して回答する様子がストリーミングで確認できるはずです。
まとめ
今回は、OpenAI Function CallingとLangChain Agentを組み合わせることで、LLMが外部ツールを自律的に利用し、より複雑なタスクをこなせるAIエージェントを構築しました。
これにより、LLMアプリは単なるQ&Aボットから、リアルタイムの情報にアクセスし、実世界とインタラクションできる強力なツールへと進化します。Agentの思考プロセスをストリームで確認できることで、その「賢さ」をより実感できたのではないでしょうか。
いよいよ次回は連載最終回、【第5回】デプロイと収益化編です。今回作成したLLMアプリケーションをクラウドにデプロイし、Stripeを連携させて収益化するまでの道筋を解説します。お楽しみに!
“`
コメント