はじめに
今回はAWSから少しはなれてOpenAI関連です。
OpenAIのFunction Callingを使って、LLMに「天気取得」「計算」「現在時刻取得」の3つのツールを持たせ、Streamlit でチャット形式のUIを作るところまで試してみました。
作成したリポジトリは以下になります。
Function Calling とは
Function Calling とは、AIがユーザーの質問に答えるために「どの関数を呼ぶべきか」を自分で判断し、その引数を返す仕組みです。
OpenAIの公式ドキュメントでは、次の3要素で説明されています。
| 要素 | 説明 |
|---|---|
| ツール定義 | アプリがAIに提供する機能の一覧(天気取得・計算・DBアクセスなど) |
| ツール呼び出し | AIが「このツールを使いたい」と判断した応答 |
| ツール出力 | 実際に関数を実行した結果をAIに返す情報 |
通常のAPI呼び出しとの違い
【通常の会話】
ユーザー: 「東京の天気は?」
AI: 「東京は晴れです(※これは学習済みデータに基づく推測です)」
【Function Calling あり】
ユーザー: 「東京の天気は?」
AI: 「get_weather(city="東京") を呼んでください」 ← ここがFunction Calling
アプリ: get_weather を実行して結果を取得
AI: 「東京は現在22℃で晴れです」 ← 実データを元に回答
まあ、要は言い方が違うだけでAnthropicが提供するtool_useと大体同じです(厳密にはそれぞれの意味合いが若干違うらしい)
公式が示す5ステップのフロー
公式ドキュメントでは「高レベルの5ステップ」として以下が定義されています。
① 使用可能なツール情報を含めてAPIにリクエスト
↓
② AIからツール呼び出し(関数名 + 引数)を受け取る
↓
③ アプリ側で実際に関数を実行し、結果を取得
↓
④ 結果をAPIに送信(会話履歴に追加)
↓
⑤ 最終回答を受け取る(または追加ツール呼び出しがあれば②へ戻る)
図で表すと公式ではこうなっています。
AIは関数を実行しているわけではなく、「どの関数をどの引数で呼ぶか」を指示しているだけという点がポイントです。実行するのはあくまでアプリ側です。
tool_choice でAIの行動を制御する
tool_choice パラメータでAIのツール選択動作を4段階で制御できます。
| 値 | 動作 |
|---|---|
"auto"(デフォルト) |
AIが必要に応じてツールを0〜複数個使う |
"required" |
必ず1つ以上のツールを呼ぶことを強制 |
{"type": "function", "name": "xxx"} |
特定のツールを強制的に呼ばせる |
"none" |
ツールを使わず通常の回答のみ |
今回の実装では "auto" を使い、AIに判断を任せています。
並列ツール呼び出し(Parallel Tool Calls)
1つの質問に対して、AIは複数のツールをまとめて呼び出せます。たとえば「大阪の天気と現在時刻を教えて」と聞くと、get_weather と get_current_time を同時に呼び出します。無効化したい場合は parallel_tool_calls=False を指定します。
実装していく
作ったもの
Streamlit で作ったシンプルなチャットUIです。
- 左サイドバーに使えるツールの一覧とツール呼び出し履歴を表示
- チャットでAIに質問すると、必要に応じてツールを呼び出して回答してくれる
- AIがどのツールを呼んだか、引数と結果も画面上に表示される
フォルダ構成
OpenAI-api-functioncalling/
├── .env # APIキー(Gitには含めない)
├── .gitignore
├── requirements.txt # 依存パッケージ
├── tools.py # ツール定義と実装
└── app.py # Streamlit UI + APIループ
役割の分担はシンプルです。tools.py にツールの定義と実装をまとめ、app.py でUIとAPIとのやり取りを担います。
環境構築
Python と仮想環境のセットアップ
python -m venv .venv
source .venv/bin/activate # Windows は .venv\Scripts\activate
pip install -r requirements.txt
requirements.txt の内容:
openai>=1.0.0
streamlit>=1.35.0
python-dotenv>=1.0.0
pytz>=2024.1
APIキーの設定
.env ファイルを作成して OpenAI の API キーを書きます。
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
.gitignore に .env を追加して、APIキーが誤って公開されないようにします
.env
1. ツールを定義する(tools.py)
tools.py では2つのことをやっています。
- TOOLS リスト:OpenAI API に「こういう関数が使えます」と伝えるための JSON スキーマ
- 実際の関数:AIが「使う」と判断したら実行される Python 関数
ツール定義(JSONスキーマ)
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "指定された都市の現在の天気情報を取得します。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "天気を調べたい都市名(例: 東京、大阪、New York)",
}
},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "数式を計算して結果を返します。四則演算や累乗などに対応しています。",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "計算したい数式(例: 123 * 456、(10 + 5) / 3、2 ** 10)",
}
},
"required": ["expression"],
},
},
},
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "指定されたタイムゾーンの現在時刻を取得します。",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "タイムゾーン名(例: Asia/Tokyo、America/New_York、UTC)",
}
},
"required": [],
},
},
},
]
description ではツールに対する説明を追加します。
AIはこの説明文を読んで「この質問にはどのツールが必要か」を判断します。曖昧な説明だと、AIが適切なツールを選べなくなります。
実際の関数実装
# 天気取得(モックデータ)
MOCK_WEATHER = {
"東京": {"condition": "晴れ", "temp": 22, "humidity": 55},
"大阪": {"condition": "曇り", "temp": 20, "humidity": 65},
# ...
}
def get_weather(city: str) -> dict:
key = city.lower().strip()
data = MOCK_WEATHER.get(key)
if data:
return {
"city": city,
"condition": data["condition"],
"temperature": f"{data['temp']}°C",
"humidity": f"{data['humidity']}%",
}
return {"city": city, "error": f"「{city}」の天気情報は見つかりませんでした。"}
# 計算
def calculate(expression: str) -> dict:
expr = expression.strip()
# 安全な文字だけを許可する正規表現チェック
if not re.compile(r"^[\d\s\+\-\*\/\.\(\)\*\*\%]+$").match(expr):
return {"error": f"安全でない数式です: {expression}"}
result = eval(expr, {"__builtins__": {}})
return {"expression": expr, "result": result}
# 現在時刻
def get_current_time(timezone: str = "UTC") -> dict:
tz = pytz.timezone(timezone or "UTC")
now = datetime.now(tz)
return {
"timezone": timezone,
"datetime": now.strftime("%Y年%m月%d日 %H:%M:%S"),
"weekday": ["月", "火", "水", "木", "金", "土", "日"][now.weekday()] + "曜日",
}
ディスパッチャ関数
ツール名と引数(JSON文字列)を受け取り、対応する関数を呼び出して結果をJSON文字列で返します。
def execute_tool(name: str, arguments: str) -> str:
args = json.loads(arguments)
if name == "get_weather":
result = get_weather(**args)
elif name == "calculate":
result = calculate(**args)
elif name == "get_current_time":
result = get_current_time(**args)
else:
result = {"error": f"未知のツール: {name}"}
return json.dumps(result, ensure_ascii=False)
2. APIの呼び出しループを実装する
Function Calling の肝となる部分です。app.py の中でこのループが動いています。
while True:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=build_api_messages(st.session_state.messages),
tools=TOOLS,
tool_choice="auto", # AIが必要に応じてツールを選ぶ
)
choice = response.choices[0]
finish_reason = choice.finish_reason
msg = choice.message
if finish_reason == "tool_calls":
# AIがツールを呼ぶと判断した
for tc in msg.tool_calls:
result = execute_tool(tc.function.name, tc.function.arguments)
# ツール結果をメッセージ履歴に追加してもう一度APIを呼ぶ
st.session_state.messages.append(
{"role": "tool", "tool_call_id": tc.id, "content": result}
)
else:
# ツール不要 or ツール結果を元に最終回答が生成された
final_content = msg.content or ""
st.session_state.messages.append(
{"role": "assistant", "content": final_content}
)
break
ポイント:なぜループが必要なのか?
1回のAPI呼び出しで完結しないためです。AIが「ツールを使いたい」と返してきたとき、アプリ側でツールを実行してその結果をまたAPIに送らなければなりません。この往復を finish_reason == "tool_calls" が "stop" になるまで繰り返します。
ユーザー → API(ツール定義付き)
↓
tool_calls(どのツールをどう呼ぶか)
↓
ツールを実行
↓
ツール結果を追加して再度API呼び出し
↓
最終回答(finish_reason == "stop")
また、APIに渡すメッセージは整形が必要です。Streamlit のセッション状態には表示用の情報も入っているため、APIに渡す前に適切な形式に変換します。
def build_api_messages(messages):
api_msgs = []
for m in messages:
role = m["role"]
if role == "user":
api_msgs.append({"role": "user", "content": m["content"]})
elif role == "assistant":
entry = {"role": "assistant"}
if m.get("content"):
entry["content"] = m["content"]
if m.get("tool_calls"):
entry["tool_calls"] = m["tool_calls"]
api_msgs.append(entry)
elif role == "tool":
api_msgs.append({
"role": "tool",
"tool_call_id": m["tool_call_id"],
"content": m["content"],
})
return api_msgs
3. Streamlit で画面を作る
UIはシンプルに、チャット形式+サイドバーの構成です。
# サイドバー:使えるツール一覧
with st.sidebar:
st.title("🛠️ 使えるツール")
tool_info = [
("🌤️ get_weather", "都市名を伝えると天気を教えます", "「東京の天気は?」"),
("🧮 calculate", "数式を計算します", "「123 × 456 は?」"),
("🕐 get_current_time", "現在時刻を返します", "「今何時?」"),
]
for name, desc, example in tool_info:
with st.expander(name):
st.write(desc)
st.caption(f"例: {example}")
st.subheader("📊 ツール呼び出し履歴")
for log in st.session_state.tool_call_log[-10:]:
st.markdown(f"- **{log['tool']}**({log['args_summary']})")
AIがツールを呼んだときは、引数と結果を st.expander で表示して「AIが何をしたか」が見えるようにしました。
with st.chat_message("assistant"):
with st.expander(f"🔧 ツール呼び出し: `{fn_name}`", expanded=True):
st.write("**引数:**")
st.json(args_dict)
st.write("**結果:**")
st.json(json.loads(result))
動かしてみる
streamlit run app.py
こんな感じで動いてくれました。
試せる質問の例:
| 質問 | 使われるツール |
|---|---|
| 「東京の天気は?」 | get_weather |
| 「2の10乗は?」 | calculate |
| 「今東京は何時?」 | get_current_time |
| 「大阪の天気と、気温を2倍したら何度?」 | get_weather → calculate(複数ツール) |
このように二つの質問で複数のツール予備部出すことも確認できます。
ここまでの学び
1. AIは「関数を実行する」のではなく「関数を指示する」だけ
Function Calling という名前だと「AIが関数を呼ぶ」ようなイメージを持ちがちですが、実際にはAIは関数名と引数を返すだけです。関数を実行するのはアプリ側の責任です。この分離を意識すると設計がクリアになります。
2. description の質がAIの判断精度を左右する
ツール定義の description は英語でも日本語でも機能しますが、具体的な使用例を書くほど精度が上がる印象でした。「何をするか」だけでなく「どんな質問で使うか」まで書くのが効果的です。
3. Function Callingを利用したループ設計
「ツールを呼んだら結果を追加して再度API呼び出し」というループを正しく実装するのが最大のポイントです。finish_reason を見てループを抜けるタイミングを判断するのがミソです。
さいごに
Function Calling 便利やなあと思いつつ、Strandsとも少し似ています。
Strandsの場合SDkなので若干領域が違いますが、動的にツールをお手軽に呼び出せるのはすごい似ています。
私は今までAWSゴリ押し勢でしたが、OpenAI APIでも似たようなことができると知ることができたのは大きな収穫でした。
現場や顧客にそれぞれ制約なんかがあるのはよくあるケースですから、そこに最適なAIエージェントをササっと構築提案できるようにAWSに限らず今後も幅広く知見を深めていきたいですね。
参考



