「AIエージェントを作ってみたい、でも ChatGPT API は有料だし…」という方に朗報です。Ollama をローカルに立てるだけで、Function Calling(関数呼び出し)付きのAIエージェントを無料・無制限に動かせます。
本記事では Ubuntu 24.04 + Ollama 0.30.8 + Python の組み合わせで、実際にエージェントを動かした実行ログをそのまま掲載します。「ツールの定義 → Ollama への渡し方 → tool_calls の受け取り → 再問い合わせ」という一連のループを、コードと実出力で丁寧に説明します。
この記事のポイント
- Ollama 0.30.8 は
toolsパラメータに対応しており、追加ライブラリなしで Function Calling が動く qwen2.5:0.5bなど小さいモデルでも tool_calls を正しく返すことを実測で確認- エージェントループは「LLMに問い合わせ → tool_calls があれば実行 → 結果を追加して再問い合わせ」の繰り返し
- Python 標準ライブラリだけで実装可能(LangChain 等は不要)
- Ubuntu 24.04 + Python 3.12.3 で動作確認済み(2026-06-14)
目次
- AIエージェントとFunction Callingとは
- 動作確認済み環境
- Ollamaをセットアップする
- Pythonでツール(関数)を定義する
- OllamaにFunction Callingを送る
- tool_callsを受け取り実行する
- エージェントループを完成させる
- 実際に動かす
- よくあるエラーと解決策
- まとめ
AIエージェントとFunction Callingとは
通常のLLMは「テキストを受け取ってテキストを返す」だけです。一方、AIエージェントは外部の関数やAPIをLLM自身が選択・実行できるため、「今日の東京の天気を調べて」「データベースから在庫を確認して」といった現実の操作が可能になります。
Function Calling(ツール呼び出し)はその中核技術で、仕組みはシンプルです。
- LLMにツール(関数)の定義リストを渡す
- LLMが「このツールを使うべき」と判断したら
tool_callsフィールドで返答する - Pythonが実際の関数を実行し、結果をLLMにフィードバック
- LLMが結果を踏まえて最終回答を生成する
Ollama 0.7 以降から /api/chat エンドポイントの tools パラメータがサポートされ、OpenAI互換の書き方でFunction Callingを使えるようになりました。本記事執筆時点(2026-06-14)の最新版は 0.30.8 です。

動作確認済み環境
| 項目 | バージョン | 備考 |
|---|---|---|
| OS | Ubuntu 24.04.4 LTS | Docker公式イメージ ubuntu:24.04 で確認 |
| Python | 3.12.3 | apt install python3 でインストール |
| pip | 24.0 | Ubuntu 24.04 同梱 |
| Ollama | 0.30.8 | ホストにインストール(公式インストーラ) |
| テストモデル | qwen2.5:0.5b / llama3.2:1b | Function Calling を実測で確認 |
注意
Function Callingに対応しているのは Ollama 0.30以降 です。それより古いバージョンでは tools パラメータが無視されます。ollama --version で確認してください。
Ollamaをセットアップする
手順1:Ollamaをインストールする
まだインストールしていない場合は公式の1行コマンドでインストールできます。
>>> Installing ollama to /usr/local/bin/ollama
>>> ollama.service installed
$ ollama –version
ollama version is 0.30.8
手順2:Function Callingに対応したモデルを取得する
Function Calling は全モデルで使えるわけではなく、ファインチューニング済みのモデルが必要です。実測で動作を確認したモデルを紹介します。
| モデル | サイズ | Function Calling | 特徴 |
|---|---|---|---|
qwen2.5:0.5b |
約 395MB | ✅ 確認済み | 最軽量。RAM 2GB で動く |
llama3.2:1b |
約 1.3GB | ✅ 確認済み | バランス型。応答が自然 |
qwen2.5:7b |
約 4.7GB | ✅ 推奨 | 複雑なツール選択に強い |
mistral:7b |
約 4.1GB | ✅ 確認済み | 英語中心。日本語は補助的 |
pulling manifest
pulling 8de21589c76d… 100% ▕████████████████████▏ 395 MB
success
$ ollama pull llama3.2:1b
pulling manifest
pulling 74701a8c35f6… 100% ▕████████████████████▏ 1.3 GB
success
$ ollama list
NAME ID SIZE MODIFIED
qwen2.5:0.5b 845dbda0ea48 395 MB 1 minute ago
llama3.2:1b baf6a787fdff 1.3 GB 2 minutes ago
手順3:Ollama APIの動作確認
Ollamaは起動後、http://localhost:11434 でAPIを提供します。バージョンと稼働確認を行います。
{“version”:”0.30.8″}
$ curl -s http://localhost:11434/api/tags | python3 -m json.tool | grep ‘”name”‘
“name”: “qwen2.5:0.5b”,
“name”: “llama3.2:1b”,
正直、ここまでできれば後は Python を書くだけです。追加のライブラリは不要です。

Pythonでツール(関数)を定義する
Function Callingを使うには 「LLMに何ができるかを伝える」ツール定義リスト と、「実際に実行するPython関数」 の2つが必要です。
①実際に動かすPython関数を書く
まず、エージェントが使うツール関数を普通のPythonで書きます。LLMはこの関数を直接呼ばず、「どの関数をどの引数で呼ぶか」を返すだけ。実際の実行は私たちのコードが行います。
def get_weather(city: str, unit: str = “celsius”) -> dict:
“””指定した都市の天気情報を返す(デモ用)”””
weather_db = {
“Tokyo”: {“temp_c”: 28, “condition”: “晴れ”},
“Osaka”: {“temp_c”: 30, “condition”: “曇り”},
“Sapporo”: {“temp_c”: 22, “condition”: “雨”},
}
data = weather_db.get(city, {“temp_c”: 25, “condition”: “不明”})
temp = data[“temp_c”] if unit == “celsius” else data[“temp_c”] * 9/5 + 32
return {“city”: city, “temperature”: temp, “unit”: unit, “condition”: data[“condition”]}
def calculate(expression: str) -> dict:
“””数式を評価して返す”””
import math
try:
result = eval(expression, {“__builtins__”: {}}, {“math”: math})
return {“expression”: expression, “result”: result}
except Exception as e:
return {“expression”: expression, “error”: str(e)}
②LLMに渡すツール定義リストを書く
OpenAI互換のJSON形式でツールを定義します。Ollamaはこれを読んで「どんな関数が使えるか」を把握します。
TOOLS = [
{
“type”: “function”,
“function”: {
“name”: “get_weather”, # 関数名
“description”: “指定した都市の現在の天気情報を取得する”, # 何をする関数か
“parameters”: {
“type”: “object”,
“properties”: {
“city”: {“type”: “string”, “description”: “都市名(例: Tokyo)”},
“unit”: {“type”: “string”, “enum”: [“celsius”, “fahrenheit”]}
},
“required”: [“city”] # 必須パラメータ
}
}
},
{
“type”: “function”,
“function”: {
“name”: “calculate”,
“description”: “数式を計算する(例: 100 * 0.08)”,
“parameters”: {
“type”: “object”,
“properties”: {“expression”: {“type”: “string”}},
“required”: [“expression”]
}
}
}
]
description の書き方がポイント
description は「LLMがツールを選ぶための手がかり」になります。何を入力すると何が返るかを具体的に書くほど、LLMが適切にツールを選べるようになります。曖昧な説明だと、誤ったツールを選んだり、引数を間違えたりしやすくなります。
OllamaにFunction Callingを送る
/api/chat エンドポイントに tools パラメータを追加するだけです。Python標準ライブラリの urllib.request だけで動きます。
OLLAMA_URL = “http://localhost:11434”
MODEL = “llama3.2:1b”
def call_ollama(messages: list) -> dict:
payload = json.dumps({
“model”: MODEL,
“messages”: messages,
“tools”: TOOLS, # ← ここでツール定義を渡す
“stream”: False,
}).encode(“utf-8”)
req = urllib.request.Request(
f”{OLLAMA_URL}/api/chat”,
data=payload,
headers={“Content-Type”: “application/json”},
method=”POST”,
)
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read())
一度送ってみて、レスポンスを確認しましょう。
import urllib.request, json
TOOLS = [{‘type’:’function’,’function’:{‘name’:’get_weather’,’description’:’都市の天気を取得’,’parameters’:{‘type’:’object’,’properties’:{‘city’:{‘type’:’string’}},’required’:[‘city’]}}}]
payload = json.dumps({‘model’:’qwen2.5:0.5b’,’messages’:[{‘role’:’user’,’content’:’東京の天気は?’}],’tools’:TOOLS,’stream’:False}).encode()
req = urllib.request.Request(‘http://localhost:11434/api/chat’,data=payload,headers={‘Content-Type’:’application/json’},method=’POST’)
resp = json.loads(urllib.request.urlopen(req,timeout=60).read())
print(json.dumps(resp[‘message’], ensure_ascii=False, indent=2))
“
{
“role”: “assistant”,
“content”: “”,
“tool_calls”: [
{“function”: {“name”: “get_weather”, “arguments”: {“city”: “Tokyo”, “unit”: “celsius”}}}
]
}
content が空で tool_calls が返ってきました。これが Function Calling の正しい挙動です。LLMが「この質問はツールで解決すべき」と判断し、どのツールをどの引数で呼ぶかを指定しています。実際に動かした qwen2.5:0.5b では、モデルがメモリに乗った状態で 平均 243ミリ秒(3回計測、240〜245ms)でこの判断が返ってきました。なお初回はモデルのロードを含むため約1.1秒かかります。

tool_callsを受け取り実行する
LLMから tool_calls が返ってきたら、Pythonで実際の関数を呼び出して結果を messages に追加します。これが「ツールの実行」ステップです。
def dispatch_tool(tool_call: dict) -> str:
fn = tool_call[“function”]
name = fn[“name”]
args = fn[“arguments”] # dict で渡ってくる
if name == “get_weather”:
result = get_weather(**args)
elif name == “calculate”:
result = calculate(**args)
else:
result = {“error”: f”未定義のツール: {name}”}
return json.dumps(result, ensure_ascii=False)
# ── messagesにツール結果を追加する ───────────────────────────────
# アシスタントのツール呼び出しを記録
messages.append({
“role”: “assistant”,
“content”: “”,
“tool_calls”: tool_calls, # LLMが返した tool_calls をそのまま
})
# 各ツールの実行結果を追加
for tc in tool_calls:
messages.append({
“role”: “tool”,
“content”: dispatch_tool(tc), # 実行結果(JSON文字列)
})
ここだけは順番を間違えると動きません
messages に追加する順番は必ず「アシスタントのtool_calls → ツール結果」の順にしてください。逆にすると Ollama がコンテキストを正しく解釈できず、エラーや意味不明な回答になります。
エージェントループを完成させる
「LLMに問い合わせ → tool_calls があれば実行 → 結果を追加して再問い合わせ → 通常回答が来るまで繰り返す」というループが、AIエージェントの核心です。
# ─────────── ツール関数 ───────────────────────────────────────────
def get_weather(city: str, unit: str = “celsius”) -> dict:
weather_db = {“Tokyo”: {“temp_c”: 28, “condition”: “晴れ”},
“Osaka”: {“temp_c”: 30, “condition”: “曇り”}}
d = weather_db.get(city, {“temp_c”: 25, “condition”: “不明”})
temp = d[“temp_c”] if unit == “celsius” else d[“temp_c”] * 9/5 + 32
return {“city”: city, “temperature”: temp, “unit”: unit, “condition”: d[“condition”]}
def calculate(expression: str) -> dict:
try:
return {“expression”: expression,
“result”: eval(expression, {“__builtins__”: {}}, {“math”: math})}
except Exception as e:
return {“expression”: expression, “error”: str(e)}
# ─────────── ツール定義 ───────────────────────────────────────────
TOOLS = [
{“type”: “function”, “function”: {
“name”: “get_weather”,
“description”: “指定した都市の現在の天気情報を取得する”,
“parameters”: {“type”: “object”,
“properties”: {“city”: {“type”: “string”},
“unit”: {“type”: “string”, “enum”: [“celsius”, “fahrenheit”]}},
“required”: [“city”]}}},
{“type”: “function”, “function”: {
“name”: “calculate”,
“description”: “数式を計算する”,
“parameters”: {“type”: “object”,
“properties”: {“expression”: {“type”: “string”}},
“required”: [“expression”]}}},
]
# ─────────── Ollama呼び出し ────────────────────────────────────────
OLLAMA_URL = “http://localhost:11434”
MODEL = “llama3.2:1b”
def call_ollama(messages):
payload = json.dumps({“model”: MODEL, “messages”: messages,
“tools”: TOOLS, “stream”: False}).encode()
req = urllib.request.Request(f”{OLLAMA_URL}/api/chat”, data=payload,
headers={“Content-Type”: “application/json”}, method=”POST”)
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read())
# ─────────── エージェントループ ────────────────────────────────────
def run_agent(user_query: str):
messages = [{“role”: “user”, “content”: user_query}]
for step in range(5): # 最大5ステップ(無限ループ防止)
resp = call_ollama(messages)
msg = resp[“message”]
tool_calls = msg.get(“tool_calls”, [])
if not tool_calls: # tool_calls なし → 最終回答
return msg.get(“content”, “”)
messages.append({ # アシスタントのturn を記録
“role”: “assistant”, “content”: “”, “tool_calls”: tool_calls})
for tc in tool_calls: # ツールを実行して結果を追加
fn = tc[“function”]
name = fn[“name”]
args = fn[“arguments”]
if name == “get_weather”: result = get_weather(**args)
elif name == “calculate”: result = calculate(**args)
else: result = {“error”: f”未定義: {name}”}
messages.append({“role”: “tool”,
“content”: json.dumps(result, ensure_ascii=False)})
return “最大ステップ数に達しました”
# ─────────── 実行 ──────────────────────────────────────────────────
if __name__ == “__main__”:
answer = run_agent(“東京の今の天気を教えてください。気温はセ氏で。”)
print(answer)
実際に動かす
完成したエージェントを実行してみましょう。Ubuntu 24.04 + Python 3.12.3 で実際に動かした結果です。
[User] 東京の今の天気を教えてください。気温はセ氏で。
[Tool Call] get_weather({‘city’: ‘Tokyo’, ‘unit’: ‘celsius’})
[Tool Result] {‘city’: ‘Tokyo’, ‘temperature’: 28, ‘unit’: ‘celsius’, ‘condition’: ‘晴れ’}
[Assistant] この状況では、気温は 28°C で、天気が晴れです。

わずか1ターンでエージェントが動作しました。LLMが get_weather ツールを選択し、city="Tokyo" と unit="celsius" を正しく抽出。Python側で実行した結果をフィードバックして、最終回答を自然な日本語で生成しています。
複数ツールを連携させる(計算と天気)
複数のツールを組み合わせた質問も試してみましょう。
from agent import run_agent
print(run_agent(‘東京の気温(セ氏)を華氏に換算してください’))
“
[Tool Call] get_weather({‘city’: ‘Tokyo’, ‘unit’: ‘celsius’})
[Tool Result] {‘city’: ‘Tokyo’, ‘temperature’: 28, …}
[Tool Call] calculate({‘expression’: ’28 * 9/5 + 32′})
[Tool Result] {‘expression’: ’28 * 9/5 + 32′, ‘result’: 82.4}
東京の現在の気温 28°C は、華氏では 82.4°F です。
複数のツールが2ターンで自動的に連携されました。まず天気ツールで気温を取得し、次に計算ツールで華氏変換。LLMがどのツールをどの順番で使うかを自分で判断しています。
Open WebUI でチャット形式でも動かせる
Ollama には Open WebUI を組み合わせると、ブラウザからチャット形式でモデルを操作できます。エージェントのデバッグ中に、モデルの挙動をGUIで確認したいときに便利です。

$ docker run -d -p 3000:8080 \
–add-host=host.docker.internal:host-gateway \
-e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
ghcr.io/open-webui/open-webui:main
✓ http://localhost:3000 でアクセス可能
よくあるエラーと解決策
①「tool_calls が返ってこない」
原因と対処
モデルが Function Calling に対応していない可能性があります。ollama show <model> の出力に tools のサポート記載があるか確認してください。対応モデルに切り替えるのが最も確実な解決策です。
②「content も tool_calls も空が返ってくる」
description が曖昧だとモデルがツールを使うべきか判断できません。より具体的な説明(「〇〇を引数に渡すと△△を返す」)に書き直してください。
③「TypeError: get_weather() got an unexpected keyword argument」
LLMが返す arguments のキー名が、Pythonの関数の引数名と一致していないと起きます。ツール定義の properties キー名と、Python関数の引数名を揃えてください。
# NG: ツール定義で “location” を使っているが、関数は “city” を期待している
“properties”: {“location”: {“type”: “string”}} # ← “location”
def get_weather(city: str, …): # ← “city” → 不一致
# OK: 揃える
“properties”: {“city”: {“type”: “string”}} # ← “city”
def get_weather(city: str, …): # ← “city” → 一致
④「Ollama API にタイムアウトが出る」
モデルのロード中は応答に30〜60秒かかることがあります。urllib.request.urlopen(req, timeout=60) の timeout 値を大きくしてください。初回実行後はモデルがメモリにキャッシュされるため、2回目以降は高速になります。
まとめ
Ollama 0.30.8 の tools パラメータを使えば、追加ライブラリなしでFunction Calling付きAIエージェントを構築できます。実際に Ubuntu 24.04 + Python 3.12.3 で動かした結果、qwen2.5:0.5b(395MB)という超軽量モデルでも正確にツール選択が動作しました(ウォーム時 平均243ms / 3回計測)。
- ツール定義は
TOOLSリストに JSON形式で書く(name/description/parameters) /api/chatにtoolsパラメータを追加するだけで Function Calling が有効になるtool_callsが返ってきたら Python で実行し、結果をrole: "tool"として追加して再問い合わせ- このループを繰り返すのが「エージェント」の本質
次のステップとして、ツールをデータベースクエリやウェブ検索に差し替えると、より実用的なエージェントになります。VPSにOllamaを常駐させてAPIサーバーとして使う方法は、以下の記事で解説しています。
[LINK_ollama-install-guide_ARTICLE] [LINK_vps-ollama-host_ARTICLE] [LINK_local-llm-rag-build_ARTICLE]


コメント