「JSON modeで構造化出力はできた。次はLLMに道具を持たせて複合タスクを自動化したい」
そんな悩みを抱えるLinuxエンジニアは多いはずです。この記事では、OllamaのFunction Calling(ツール呼び出し)機能を使い、ローカルLLMに外部ツールを実行させる自律型AIエージェントをUbuntu Serverに構築する手順を解説します。curl・Pythonの両方で実装例を示し、ファイル読み取り・シェルコマンド実行・内部APIとの連携まで一通り動かします。機密データをクラウドに送らず、オンプレミスだけで完結するエージェント環境を整えたいエンジニアに向けた内容です。
この記事のポイント
・/api/chat の tools パラメータにJSONスキーマでツールを定義し、tool_callsを検出して実行する
・Function Callingに対応したモデルはllama3.3:70b-instruct-q4_0やmistral:7b-instruct-v0.3-q8_0が代表例
・Pythonのollamaライブラリを使うと数十行でツール連携エージェントを実装できる
・ツール実行可能範囲はコード側のホワイトリストで制限し、LLMに任意実行させない設計が基本
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
Function Callingとは何か — JSON modeとの本質的な違い
Function Calling(ツール呼び出し)は、LLMが「外部の関数を呼び出す必要がある」と判断したとき、その呼び出し指示を構造化データとして返す仕組みです。アプリ側はその指示を受け取り、実際に関数を実行してから結果をLLMに渡します。LLMはツールの実行結果を踏まえた自然言語の最終回答を生成します。JSON modeとの違いを明確にしておきます。JSON modeは「LLMの出力フォーマットをJSONに固定する」だけで、外部との通信は発生しません。Function Callingは「LLMが道具を呼び出すかどうかを自分で判断し、呼び出す場合はその指示を返す」という双方向の仕組みです。出力形式の制御と外部連携は、目的がまったく異なります。
具体的なフローは次の5ステップです。
1. アプリがツール定義(関数名・説明・パラメータのJSONスキーマ)とユーザーのクエリをLLMへ送信する
2. LLMが「ツールを呼ぶ必要あり」と判断した場合、
tool_callsを含む応答を返す3. アプリ側が
tool_callsを検出し、指定された関数を実際に実行する4. 実行結果を
role: "tool"のメッセージとして会話履歴に追加し、再度LLMへ送信する5. LLMがツール結果を踏まえた最終回答を生成する
「ツールを呼ぶかどうか」の判断はLLMが行う点が重要です。アプリ側はツールを用意するだけで、どのタイミングで何を使うかはモデルが自律的に決定します。この自律性が、単純なAPI連携やJSON modeとの本質的な差です。タスクによってはLLMが複数のツールを連続して呼び出す場合もあり、その場合はステップ2~4をループします。
Function Callingに対応したOllamaモデルを確認する
すべてのOllamaモデルがFunction Callingをサポートしているわけではありません。非対応モデルにtoolsパラメータを渡してもエラーにはならず、tool_callsなしで平文の応答が返ってくる場合が多いため、事前の確認が必要です。
2026年7月時点でFunction Callingに対応している主なモデルは次の通りです。
・llama3.3:70b-instruct-q4_0(Llama 3.3 70Bの標準量子化。精度は高いが42GBのVRAM/ストレージが必要)
・mistral:7b-instruct-v0.3-q8_0(Mistral 7B v0.3以降が対象。v0.2以前は非対応。VRAMが少ない環境の第一候補)
・phi4:14b-instruct-q4_0(Phi-4 14B。8GB程度のVRAMで動作し、Function Callingの精度も実用レベル)
・gemma3:27b-instruct-q4_0(Gemma 3 27B。日本語タスクとの相性が良い)
モデルがFunction Callingに対応しているかどうかは、Modelfileのテンプレート定義を確認するのが正確です。次のコマンドで取得済みモデルのModelfileを表示し、
Toolsやtool_callsに関する記述があるかを確認します。
# 取得済みモデルの一覧を確認する $ ollama list NAME ID SIZE MODIFIED llama3.3:70b-instruct-q4_0 2df51abc23f4 42 GB 2 days ago mistral:7b-instruct-v0.3-q8_0 a8e15a1a0e7f 7.7 GB 5 days ago phi4:14b-instruct-q4_0 8f2c3d1e9a12 8.1 GB 3 days ago # Modelfileを表示してツール対応記述を確認する $ ollama show llama3.3:70b-instruct-q4_0 --modelfile | head -40 FROM /root/.ollama/models/blobs/sha256-... TEMPLATE """{{- if .System }}<|start_header_id|>system<|end_header_id|> ... {{- if .Tools }} <|start_header_id|>tools<|end_header_id|> {{ .Tools }} {{- end }}"""
{{ .Tools }}の記述があればFunction Callingに対応しています。Ollamaの基本的なセットアップとモデル取得の手順は、「Ubuntu ServerでローカルLLMを構築する方法」で解説しています。まだOllamaをセットアップしていない場合はそちらを参照してください。
ツール定義のJSONスキーマを書く — パラメータと型の設定
Function Callingでは、LLMへのリクエストにtools配列を追加します。各ツールはOpenAI互換のJSONスキーマ形式で記述します。この定義の書き方がFunction Callingの精度に直結するため、丁寧に設計する必要があります。
ツール定義の基本構造は次の通りです。
{ "type": "function", "function": { "name": "read_file", "description": "指定したパスのテキストファイルを読み取る。ログ確認・設定ファイルの参照が必要なときに使う", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "読み取るファイルの絶対パス(例: /var/log/syslog)" }, "max_lines": { "type": "integer", "description": "読み取る最大行数。省略した場合は全行を返す(例: 50)" } }, "required": ["path"] } } }
・
name: アプリ側の関数名と完全に一致させる。LLMが返すtool_callsにもこの名前が入るため、スペルミスがあるとディスパッチに失敗する・
description: LLMが「いつこのツールを使うか」を判断する最重要フィールド。「○○が必要なときに使う」と使用条件を明記すると精度が上がる。説明が短すぎるとツールが呼ばれないか誤った場面で呼ばれる・
parameters.propertiesの各description: 括弧内に具体的な値の例を書くと、LLMが適切な形式で引数を生成しやすくなる・
parameters.required: 必須パラメータを必ず指定する。設定漏れがあるとLLMが省略したまま返してくるケースがある複数のツールを渡す場合は
tools配列に並べます。LLMはその中から適切なものを選んで呼び出します。ツール数が多すぎる(目安として15個以上)と選択精度が下がるため、1回のリクエストでは用途ごとに絞り込んで渡す設計が実用的です。
curlでFunction Callingを試す — /api/chatの4ステップフロー
実装前に、curlで動作確認します。/api/generateではなく/api/chatエンドポイントを使う点に注意してください。toolsパラメータは/api/chat専用です。
1. ツール定義付きリクエストを送信する
# ツール定義を含むリクエストをcurlで送信する(stream=falseを必ず指定) $ curl -s http://localhost:11434/api/chat \ -H "Content-Type: application/json" \ -d '{ "model": "llama3.3:70b-instruct-q4_0", "stream": false, "messages": [ {"role": "user", "content": "/var/log/syslogの最新行を確認してください"} ], "tools": [ { "type": "function", "function": { "name": "read_file", "description": "指定したパスのテキストファイルを読み取る。ログ確認が必要なときに使う", "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "ファイルの絶対パス(例: /var/log/syslog)"}, "max_lines": {"type": "integer", "description": "最大行数(例: 20)"} }, "required": ["path"] } } } ] }' | python3 -m json.tool
2. tool_callsレスポンスを確認する
LLMがツールを呼ぶ必要があると判断した場合、次のような応答が返ります。contentが空文字列になっているのは正常な動作です。
# LLMがtool_callsを返した場合の応答例 { "message": { "role": "assistant", "content": "", "tool_calls": [ { "function": { "name": "read_file", "arguments": { "path": "/var/log/syslog", "max_lines": 20 } } } ] }, "done": true }
3. ツールを実行して結果をLLMに返す
tool_callsの内容をもとにアプリ側でファイルを読み取り、結果をrole: "tool"メッセージとして会話履歴に追加して再送信します。
# ツール実行結果を role: "tool" として会話履歴に追加して再送信する $ curl -s http://localhost:11434/api/chat \ -H "Content-Type: application/json" \ -d '{ "model": "llama3.3:70b-instruct-q4_0", "stream": false, "messages": [ {"role": "user", "content": "/var/log/syslogの最新行を確認してください"}, {"role": "assistant", "content": "", "tool_calls": [{"function": {"name": "read_file", "arguments": {"path": "/var/log/syslog", "max_lines": 20}}}]}, {"role": "tool", "content": "Jul 8 10:00:01 server systemd[1]: Started Daily apt download activities.\nJul 8 10:01:00 server CRON[23456]: (root) CMD (run-parts /etc/cron.hourly)"} ] }' | python3 -m json.tool
4. LLMが最終回答を生成する
今度はtool_callsなしでmessage.contentに自然言語の回答が返ります。この4ステップが1サイクルです。タスクに応じて複数のツール呼び出しが必要な場合は、tool_callsがなくなるまでステップ2~3をループします。
Pythonで実装する — tool_callsの検出とエージェントループ
curlで動作を確認できたら、Pythonでエージェントループを実装します。ollamaライブラリを使うと、ツール呼び出しの検出とループ処理をシンプルに記述できます。
1. ollamaライブラリをインストールする
# ollamaライブラリをインストールする $ pip install ollama # バージョン確認 $ python3 -c "import ollama; print(ollama.__version__)" 0.4.4
2. エージェントループを実装する
#!/usr/bin/env python3 import subprocess import ollama MODEL = "llama3.3:70b-instruct-q4_0" TOOLS = [ { "type": "function", "function": { "name": "run_command", "description": "許可されたLinuxコマンドを実行して標準出力を返す。サーバー状態の確認が必要なときに使う", "parameters": { "type": "object", "properties": { "command": { "type": "string", "description": "実行するコマンド(例: df -h /var)" } }, "required": ["command"] } } } ] # 実行を許可するコマンドのホワイトリスト ALLOWLIST = ["df", "free", "uptime", "hostname", "uname", "cat", "head", "tail"] def run_command(command: str) -> str: cmd_name = command.strip().split()[0] if cmd_name not in ALLOWLIST: return f"ERROR: {cmd_name} の実行は許可されていません" try: result = subprocess.run( command, shell=True, capture_output=True, text=True, timeout=10 ) return result.stdout or result.stderr except subprocess.TimeoutExpired: return "ERROR: タイムアウトしました" def agent_loop(user_message: str, max_iterations: int = 10) -> str: messages = [{"role": "user", "content": user_message}] for _ in range(max_iterations): response = ollama.chat(model=MODEL, messages=messages, tools=TOOLS) msg = response.message tool_calls_data = [tc.model_dump() for tc in (msg.tool_calls or [])] messages.append({ "role": "assistant", "content": msg.content or "", "tool_calls": tool_calls_data }) if not msg.tool_calls: return msg.content for tc in msg.tool_calls: if tc.function.name == "run_command": result = run_command(tc.function.arguments.get("command", "")) else: result = f"ERROR: 未定義のツール {tc.function.name}" messages.append({"role": "tool", "content": result}) return "ERROR: 最大反復回数に達しました" if __name__ == "__main__": answer = agent_loop("サーバーのディスク使用量とメモリ残量を確認してまとめてください") print(answer)
3. 実行結果を確認する
# スクリプトを実行する(LLMがdf -hとfree -hを自律的に呼び出して回答を生成する) $ python3 ollama_agent.py 現在のサーバー状態をまとめます。 【ディスク使用量(df -h)】 / (ルート): 120GB中45GB使用 (37%) /var/log: 8.2GB でやや大きめです。ローテーション設定を確認してください。 【メモリ残量(free -h)】 合計64GB、使用中22GB、バッファ込みで39GBが空きです。 現状はいずれも問題ないレベルですが、/var/log の肥大化には注意してください。
stream=True)ではtool_callsの扱いが変わります。チャンクを結合してパースする処理が別途必要になるため、まずstream=Falseで動作を確認してから必要に応じてストリーミング対応を追加する順序が無難です。
実務で使えるツールの実装例 — ファイル・コマンド・内部API
前のセクションでシェルコマンド実行ツールを示しました。ここでは業務でよく使う他の2種類を追加します。セキュリティ面の基本原則として、LLMは渡したツールを「呼び出す指示」を出すだけで、実際にコードを実行するのはアプリ側です。許可する操作の範囲をコード側で明示的にコントロールする設計は必須です。ローカルLLMを社内環境で使う判断基準や情報セキュリティポリシーとの整合については、「社内でChatGPTが使えないときの代替手段」も参考になります。
1. ファイル内容を読み取るツール
読み取りを許可するディレクトリをプレフィックスで制限します。LLMが/etc/shadowのような機密ファイルを読もうとした場合に弾けます。
import os # 読み取りを許可するディレクトリのプレフィックス一覧 ALLOWED_READ_DIRS = ["/var/log/", "/etc/nginx/", "/home/deploy/config/"] def read_file(path: str, max_lines: int = None) -> str: # パスがホワイトリストのいずれかで始まるか確認 if not any(path.startswith(d) for d in ALLOWED_READ_DIRS): return f"ERROR: {path} は読み取り許可外のパスです" if not os.path.isfile(path): return f"ERROR: {path} が見つかりません" try: with open(path, "r", encoding="utf-8", errors="replace") as f: lines = f.readlines() if max_lines: lines = lines[:max_lines] return "".join(lines) except PermissionError: return f"ERROR: {path} の読み取り権限がありません"
2. 内部HTTPエンドポイントへリクエストするツール
社内の監視APIや在庫管理APIなどからデータを取得するツールです。リクエスト先URLもホワイトリストで制限し、外部インターネットへのリクエストを防ぎます。import requests # リクエストを許可する内部URLのプレフィックス一覧 ALLOWED_HOSTS = [ "http://internal-monitoring.example.local", "http://inventory-api.example.local" ] def http_get(url: str, params: dict = None) -> str: if not any(url.startswith(h) for h in ALLOWED_HOSTS): return f"ERROR: {url} はリクエスト許可外のURLです" try: resp = requests.get(url, params=params or {}, timeout=10) resp.raise_for_status() # レスポンスが大きい場合は先頭4,000文字に切り詰める return resp.text[:4000] except requests.RequestException as e: return f"ERROR: リクエスト失敗 - {e}"
TOOLS配列に並べると、LLMが「どれを使えばタスクをこなせるか」を自律的に判断するようになります。Function Callingで使うモデルの選択基準について迷いがある場合は、「ローカルLLMのモデルを比較する方法」でLlama3.3・Mistral・Gemma 3・Phi-4の使い分け方を解説しています。
Function Callingでハマりやすいトラブルと対処法
1. tool_callsが返ってこない
最も多い原因は2つです。1つ目は非対応モデルへのリクエストで、前述の対応モデル一覧とModelfileの確認を先に行ってください。2つ目はdescriptionが短すぎてLLMがツールの使いどころを判断できないケースです。「○○の情報が必要なときに呼び出す」「○○を確認したい場合に使う」と具体的な使用条件を明記した説明文に書き直すと改善します。また、ユーザーのクエリが曖昧すぎる場合もLLMがツールを呼ばないことがあります。「/var/logのsyslogを確認して」のように明示的な指示にすると呼び出される確率が上がります。
2. argumentsが不完全なまま返ってくる
requiredフィールドの設定漏れが原因のことが多いです。LLMが任意項目と誤解して省略するケースがあります。必須パラメータはrequired配列に必ず含めてください。加えて、descriptionに「(例: /var/log/syslog)」のように具体的な値のサンプルを含めると、LLMが適切な形式で引数を生成しやすくなります。
3. ツール呼び出しが繰り返されて終わらない
LLMが同じツールを繰り返し呼び出し続けてループするケースがあります。エージェントループにmax_iterationsのハードリミットを設定し、上限に達したら処理を打ち切る実装を必ず入れてください。本記事の実装例では10回に設定しています。業務用途では5回前後が適切です。終了条件として「ツール結果が前回と同一だった場合はループを打ち切る」ロジックを加えるとさらに堅牢になります。
4. /api/generateと/api/chatの混同
Function Callingは必ず/api/chatエンドポイントを使います。/api/generateはtoolsパラメータを受け付けません。エンドポイントを誤るとtoolsが無視されて平文応答が返ってきます。同じ誤りはPythonライブラリでも起きます。ollama.generate()ではなくollama.chat()を使ってください。
5. stream=Trueでtool_callsが取得できない
stream=Trueの場合、tool_callsはストリームチャンクとして分割されて届きます。チャンクを結合してからJSONとして解析する処理が必要です。Function Callingの初期実装では必ずstream=Falseを指定し、動作確認が取れてからストリーミング対応を追加する順序をすすめます。
本記事のまとめ
OllamaのFunction Callingを使うと、ローカルLLMに外部ツールを呼び出させる自律型AIエージェントをLinuxサーバー上に構築できます。機密データをクラウドに送らず、ファイル読み取り・コマンド実行・内部API連携を組み合わせたタスク自動化を社内環境で実現できます。実装のポイントは、ツール定義のdescriptionを丁寧に書くこと、ホワイトリストで実行範囲を制限すること、エージェントループに反復上限を設けること、の3点です。
| 操作 | コマンド・設定 |
|---|---|
| 対応モデル確認 | ollama show llama3.3:70b-instruct-q4_0 --modelfile | head -40 |
| ツール付きリクエスト | curl http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{"model":"llama3.3:70b-instruct-q4_0","stream":false,"tools":[...]}' |
| Pythonライブラリ導入 | pip install ollama |
| tool_calls検出 | response.message.tool_calls がNoneでなければツール呼び出しあり |
| ツール結果の返し方 | messages.append({"role": "tool", "content": "実行結果文字列"}) |
| ループ上限の設定 | for _ in range(max_iterations): のmax_iterationsを10以下に設定する |
ローカルLLMのFunction Callingを2日間で実装する
Ollamaのツール呼び出しをアーキテクチャ設計から実装まで手を動かして習得したい方向けに、「ローカルAIマスターセミナー」を開催しています。
少人数(最大8名)ZOOMハンズオン形式で実施しています。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。
登録無料・いつでも解除できます