GPT-OSS × OpenAI Python SDKで学ぶマルチツールエージェント構築
GPT-OSS × OpenAI Python SDKで学ぶマルチツールエージェント構築
近年の生成AI活用において「エージェント」という言葉を耳にする機会が増えました。エージェントとは単にユーザーの入力に対して応答するだけでなく、自律的にタスクを分解し、外部のツールやAPIを呼び出して複雑な処理を実現できる仕組みを指します。
本記事で扱うのは、OSSモデルである「gpt-oss:20b」をローカルで稼働させ、それを Ollama 経由で OpenAI Python SDK から呼び出す という構成です。
通常であれば OpenAI のクラウドモデルを利用しますが、ここではローカル推論基盤を前提にしています。そのため、OSSモデルの柔軟さと公式SDKの書き心地の良さを“いいとこ取り”できる点が特徴です。
OSSモデルをエージェント化する場合、フレームワーク(LangChain や LlamaIndex など)に依存せずとも 公式SDKを共通インターフェースとして使える ため、学習コストを最小化しながら堅牢な開発が可能です。
Ollamaによるローカル環境構築
Ollamaとは
Ollama はローカルで大規模言語モデルを実行できるランタイムです。REST API を提供しているため、エンドポイント http://localhost:11434/v1
を備えています。
インストール
macOS / Linux の場合は以下のように導入します。
# macOS
brew install ollama
# Linux
curl -fsSL https://ollama.com/install.sh | sh
モデルの取得
今回利用するのは OSS モデル「gpt-oss:20b」です。以下のコマンドでダウンロード&起動します。
ollama pull gpt-oss:20b
サーバ起動
Ollama はデフォルトでバックグラウンド常駐し、APIを http://localhost:11434/v1
から利用可能です。これで準備完了です。
OpenAI SDK からの接続設定
まず、OpenAI SDKを入れましょう。
pip install -U openai
これで準備は完了です。
次にPython側を定義し、gpt-oss:20b
からレスポンスを受け取るための設定を行います。
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="dummy" # 何でもOK
)
OpenAI()
の引数のapi_key
は利用しませんので、どんな値を入れても構いません。
上記の設定で、 client.chat.completions.create()
を呼び出すと、ollama
でインストールしたLLMが応答してくれるようになります。
例:
resp = client.chat.completions.create(
model="gpt-oss:20b",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "フランスの首都は?"}
]
)
print(resp.choices[0].message.content)
ここからエージェント的な挙動を行えるようにカスタマイズをしていきたいと思います。
エージェントの構築
1. エージェントの基本構造
準備が整いましたので、早速エージェントの作成を・・といきたいところですが、その前に、エージェントの構成要素を整理しておきましょう。
エージェントには、大きく分けると以下の4つの構成要素があります。
-
言語モデル(LLM)
自然言語処理と推論を担当する中核。ここでは OpenAI SDK から呼び出す GPT モデルを利用します。 -
プロンプト設計
エージェントがどのように振る舞うかを決定するルール。システムプロンプトや指示文として与えます。 -
ツール(外部関数やAPI)
天気情報を取得するAPIや計算関数、翻訳サービスなど、LLMだけでは実現できない処理を外部に委任します。 -
制御ループ
LLMが「ツールを使うべき」と判断したとき、実際に関数を呼び出して結果を返し、再度LLMに渡す一連の仕組みです。
今回紹介する実装のポイントは、この「制御ループ」をPythonコードで明示的に記述する点にあります。
2. ツール呼び出し
先ほど挙げたOpenAI Python SDKの例では、単純にLLMからメッセージを受け取るだけでした。ツールや制御ループといった仕組みはまだ実装されていません。これを入れるためには、SDKのtool
パラメータを使用します。
ツール呼び出しのフローは次の通りです。
-
モデルが「ツールを使うべき」と判断すると、
choices[0].message
のcontent
は空文字になり、代わりにtool_calls
が返る。 -
開発者は
tool_calls
を読み取り、実際にPython関数を実行する。 -
実行結果を
role="tool"
メッセージとして会話履歴に追加する。 -
再度
client.chat.completions.create()
を呼び出すと、モデルがツールの返り値を考慮して最終回答を生成する。
つまり、**「LLM → ツール呼び出し指示 → 実関数実行 → 結果差し戻し → 再度LLM」**というループを回す必要があります。
ちなみに、これを忘れると、いつまでも content=''
のまま結果が得られなくなってしまいます。
3. 複数ツールを使うサンプル実装
ここからは実際に動作するマルチツールエージェントの例を示します。
用意するツールは以下の3つです。
-
天気取得(get_weather)
引数の都市名に対して天気情報を返す。 -
計算機(calculator)
数式を受け取り評価する。 -
翻訳(translate_to_japanese)
英文を日本語に翻訳する(ここではダミー実装)。
import json
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:11434/v1",
api_key=""
)
# --- 実体関数 ---
def get_weather(city: str) -> str:
return f"The weather in {city} is sunny, 25°C."
def calculator(expression: str) -> str:
return str(eval(expression))
def translate_to_japanese(text: str) -> str:
return f"{text}(日本語に翻訳された体)"
tool_impl = {
"get_weather": lambda args: get_weather(**args),
"calculator": lambda args: calculator(**args),
"translate_to_japanese": lambda args: translate_to_japanese(**args),
}
# --- 会話 ---
messages = [
{"role": "user", "content": "今日の東京の天気を調べて、それを日本語で説明し、最後に 12*(3+7) を計算して"}
]
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "指定都市の天気を取得する",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
},
},
{
"type": "function",
"function": {
"name": "calculator",
"description": "数式を計算する",
"parameters": {"type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"]},
},
},
{
"type": "function",
"function": {
"name": "translate_to_japanese",
"description": "テキストを日本語に翻訳する",
"parameters": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]},
},
},
]
# --- ツール呼び分けループ ---
while True:
resp = client.chat.completions.create(
model="gpt-oss:20b",
messages=messages,
tools=tools,
tool_choice="auto",
# 必要なら parallel_tool_calls=False で逐次化
)
msg = resp.choices[0].message
# 1) ツール呼び出しがあれば実行して role="tool" を追加、再ループ
if msg.tool_calls:
messages.append({"role": "assistant", "content": "", "tool_calls": [
tc.model_dump() for tc in msg.tool_calls]})
for tc in msg.tool_calls:
fn_name = tc.function.name
args = json.loads(tc.function.arguments or "{}")
result = tool_impl[fn_name](args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"name": fn_name,
"content": result,
})
continue
# 2) ツール呼び出しが無ければ最終回答
print(msg.content) # ここで本文が出る
break
実行結果
本日の東京の天気は晴れで、気温は約25 ℃です。
12 × (3 + 7) の計算結果は **120** です。
ここで注目すべきは、モデルが自律的に 「get_weather → translate_to_japanese → calculator」 の順でツールを呼び出し、最終的に日本語のまとめを返している点です。
4. 実務での応用シナリオ
この仕組みを拡張すれば、以下のようなエージェントがすぐに作れます。
-
営業支援アシスタント
顧客情報をCRMから取得 → 最新の売上データを集計 → 提案資料を生成。 -
開発者支援エージェント
GitHub APIからPRを取得 → 静的解析ツールを走らせる → 結果をMarkdownにまとめてコメント投稿。 -
ライフログアシスタント
Google Calendarから予定を取得 → 天気APIを組み合わせ → 「今日は雨だから出発時間を10分早めましょう」と通知。
ポイントは、LLMがツールの選択と実行順序を判断するため、ユーザーが自然言語で曖昧に依頼しても実現できる ことです。
7. 7. まとめ
本記事では、OpenAI Python SDKを用いて以下を解説しました。
-
エージェントの基本構造と制御ループの考え方
-
tools 機能を使ったツール呼び出しの仕組み
-
複数ツールを連携させたマルチツールエージェントの実装例
今回選んだ gpt-oss:20b は、あくまで筆者の主観ですが「他のOSSモデルに比べて安定性がピカイチ」で、長めの対話や複雑な指示にも比較的落ち着いた挙動を見せてくれます。さらに Ollama 経由で動かすことで OpenAI SDK との相性も抜群。余計なラッパーを挟まずに「公式SDKの書き心地」でローカルモデルを扱えるのはとても快適です。
個人的には LangChain のようなフレームワークは便利である一方、独自DSLや多層抽象のせいでコードが読みにくく、一貫性や可読性の面でも課題があります。開発がコミュニティ主導なため、設計の統一感にばらつきがあり、依存関係も肥大化しやすいという評価が散見されます。公式SDKを使ったシンプルな制御ループ構築のほうが、自分の頭にもコードにも優しいと感じます。
そのため筆者としては、まずは OpenAI SDKを素直に使って制御ループを自前で組み、仕組みを完全に理解したうえで必要に応じてフレームワークを検討する、というアプローチを推したいと考えています。
OSSモデルと公式SDKを組み合わせれば、シンプルで理解しやすく、かつ強力なエージェント構築が可能になると思います。
Discussion