プライベートなローカルAIスタックを構築する(エンドツーエンド)
ここまでで各パーツを個別に見てきました: ローカルモデル、ローカルエージェントループ、MCP 経由で公開されるツール、そして Claude+ローカルのハイブリッドパターン。これは 集大成 — それらを配線して 自分のマシン上で動く 1 つの実用的なプライベートアシスタント にまとめ上げるページです: ローカルで動くオープンウェイトモデル、ツールを呼び出せるモデル非依存のエージェントループ、それらのツールをローカル MCP サーバー経由で公開する仕組み、危険なツールの前に立つガードレール、そして — オプションで — 最も難しい 5% のステップのためのオプトインな「スマートレイヤー」としての Claude。一貫したテーマは: 機密なものはすべてデバイス上にとどまり、クラウドはオプションで、難しい少数のためだけに予約される。
- スタック全体を 1 つの図として見る: ローカルモデル + エージェントループ + ローカル MCP ツール + ガードレール(+ オプションの Claude)
- オープンウェイトモデルをローカルで動かし、ツール呼び出しができることを確認する
- モデル非依存の最小限のエージェントループを立ち上げる — 同じループのままエンドポイントだけを差し替える
- いくつかのツールをローカル MCP サーバー経由で公開し、エージェントに呼び出させる
- ガードレールを 1 つ追加する: 破壊的アクションへの承認、ループ/予算の上限、信頼できない結果の取り扱い
- オプションで、最も難しい推論だけを Claude にルーティングし、デフォルトの経路は完全にローカルに保つ
スタック全体を 1 枚の絵で
メンタルモデルは少数のボックスで、それぞれは姉妹ページですでに出会ったものです。アシスタントはこれらのボックスを配線したものにすぎません:
これをループとして読んでください。エージェントはローカルモデルに次に何をすべきか尋ねます。モデルは答えを返すか、ツール呼び出しを発行します。すべてのツール呼び出しは、実際に作業を行う(ファイルを読む、コマンドを実行する、ノートを検索する)ローカル MCP サーバーに到達する前に ガードレール を通過し、結果を返します。エージェントはその結果をモデルにフィードバックし、タスクが完了するまで繰り返します。Claude への点線の経路はオプトインです: エージェントはローカルモデルが処理できないステップだけを、あなたが許可したときにのみエスカレートします。
このスタックを構築する価値を生む 3 つの特性があります:
- デフォルトでローカル。 モデル、ループ、ツール、そしてあなたのデータはすべてあなたのハードウェア上に存在します。オプションの Claude 経路が発火しない限り何もボックスの外には出ません — そして発火する場合でも、送るのはあなたが選んだものだけです。
- モデル非依存のループ。 エージェントは OpenAI 形式のチャットエンドポイントと話します。今日は Ollama のローカルエンドポイントに向け、明日はループを書き直さずに別のプロバイダーに向けられます。
- 1 つの標準の背後にあるツール。 機能はループにハードコードされるのではなく、MCP サーバーの中に存在します。ツールを一度作れば、MCP を話すあらゆるクライアント(あなたのエージェント、Claude Code、別のアプリ)がそれを使えます。
ステップバイステップの構築
- Ollama をインストールし、ツール呼び出しに対応したモデルを起動します。ollama run は初回利用時にダウンロードを行い、localhost:11434 で OpenAI 互換のローカル API を公開します。これがデフォルトの『脳』です — プライベートでオフライン。(完全なセットアップは『モデルをローカルで動かす』ページを参照。)
- 小さなループを書きます: メッセージ + ツールスキーマをチャットエンドポイントに送り、返信を読み、tool_calls が含まれていればそれらを実行し、結果を追記し、モデルが最終回答を返すまでループします。このループはどのモデルと話しているかについては何も知りません — OpenAI のチャット形式だけを知っています。
- 実際の機能(ファイルを読む、コマンドを実行する、ノートを検索する)をハードコードする代わりに、stdio 経由のローカル MCP サーバーに置きます。エージェントはサーバーのツールを一覧取得し、それらをモデルのツールスキーマにマッピングし、必要に応じて呼び出します。一度作ってクライアント間で再利用できます。
- どのツールも実行される前にゲートをかけます: 読み取り専用ツールは自動許可し、破壊的なもの(run_shell、write_file、delete)には明示的な承認を要求し、ループの反復回数と総トークン数に上限を設け、すべてのツール結果をモデルを誘導しようとしうる信頼できない入力として扱います。
- ローカル経路をデフォルトのままにします。あるステップが本当に難しいとき — 込み入った多段階の推論や、ローカルモデルが何度も失敗するプラン — そのステップだけを Claude API にエスカレートさせ、その後ローカルループに戻します。これはハイブリッドのページにあるルーター/ドラフトしてから精緻化のアイデアを、一度に 1 ステップずつ適用したものです。
1. ローカルモデル(デフォルトの脳)
モデルを起動し、ローカルエンドポイントが立ち上がっていることを確認します。ツール呼び出し を謳うモデルを選んでください — エージェントループはそれに依存します。
ツール対応のローカルモデルを動かす + API を確認する
# Start a model that supports tool/function calling
ollama run llama3.1
# In another terminal, confirm the local OpenAI-compatible endpoint is live.
# Ollama serves it at http://localhost:11434/v1 — no internet required.
curl http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.1",
"messages": [{"role": "user", "content": "Reply with the single word: ready"}]
}'2. モデル非依存のエージェントループ
このループは意図的に単純です: メッセージとツールスキーマをチャットエンドポイントに転送し、モデルがツールを呼び出すよう求めるたびにそのツールを実行し、結果をフィードバックします。OpenAI のチャット形式しか話さないため、同じループ が今はローカルエンドポイントに対して、後で別のプロバイダーに対して動作します — 変えるのは base_url であってロジックではありません。
from openai import OpenAI
# Point at the LOCAL model. Swap base_url/api_key later to change providers —
# the loop below does not change. That is what "model-agnostic" means here.
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
MODEL = "llama3.1"
MAX_STEPS = 8 # hard cap on loop iterations (a guardrail — see step 4)
def run_agent(user_goal, tool_schemas, dispatch):
messages = [
{"role": "system", "content": "You are a local assistant. Use tools when needed."},
{"role": "user", "content": user_goal},
]
for _ in range(MAX_STEPS):
resp = client.chat.completions.create(
model=MODEL, messages=messages, tools=tool_schemas,
)
msg = resp.choices[0].message
if not msg.tool_calls:
return msg.content # model gave a final answer
messages.append(msg)
for call in msg.tool_calls:
result = dispatch(call) # runs through the guardrail + MCP server
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result,
})
return "Stopped: hit the step cap." # never loop forever
tool_schemas はツールのリスト(OpenAI の関数呼び出し形式)で、dispatch は要求されたツールを実際に実行するかどうかと、どう実行するかを決める唯一の関数です — そこにガードレールと MCP サーバーが存在します。
3. ローカル MCP サーバー経由のツール
ツールをループ内にハードコードするのではなく、ローカル MCP サーバー 経由で公開します。MCP は AI クライアントを外部ツールに接続するためのオープン標準です。ローカルサーバーはあなたのマシン上で小さなプログラムとして動き、クライアントと stdio 経由で話すため、あなたのデータとアクションはボックス内にとどまります。(なぜこれが正しい境界なのか、そしてサーバーの作り方は MCP で Claude をローカルツールに接続する で扱っています。)
安全で読み取り専用のツールを 1 つ公開する最小限の Python MCP サーバー:
# server.py — a tiny local MCP server exposing one read-only tool.
# Run it over stdio; an MCP client (your agent, Claude Code, ...) connects to it.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("local-tools")
@mcp.tool()
def search_notes(query: str) -> str:
"""Search the user's local notes folder and return matching snippets."""
# ... read from a LOCAL directory only; never reach outside it ...
return f"(stub) matches for: {query}"
if __name__ == "__main__":
mcp.run() # stdio transport by default — local, no network
エージェントはこのサーバーに接続し、ツールの 一覧 を求め、それぞれをループがすでに理解している OpenAI のツールスキーマに変換し、モデルのツール呼び出しをサーバーにルーティングします。同じループ、実際の機能 — そしてサーバーは MCP を話すあらゆるクライアントから再利用可能です。
4. ガードレール(これは飛ばさないこと)
これがおもちゃと、自分のマシンで信頼できるものとの違いです。ステップ 2 の dispatch 関数は、すべてのツール呼び出しが実行される 前 に検査される唯一のチョークポイントです。3 つの役割:
READ_ONLY = {"search_notes", "read_file", "list_dir"}
def dispatch(call):
name = call.function.name
args = call.function.arguments
# 1) APPROVAL: read-only tools auto-run; everything else asks a human first.
if name not in READ_ONLY:
if not human_approves(name, args): # destructive => require consent
return "DENIED by user."
# 2) The MCP server does the actual work (it, too, is sandboxed to safe paths).
result = call_mcp_tool(name, args)
# 3) UNTRUSTED RESULT: a tool result is data, not instructions. Do not let it
# silently become a new command to the model (prompt-injection defense).
return f"<tool_result name={name}>\n{result}\n</tool_result>"
それをループにすでに組み込まれている ループ/予算の上限(MAX_STEPS、加えて実行ごとに追跡するトークンの上限)と組み合わせれば、重要な 3 つのコントロールが揃います: 破壊的なことには人間をループに入れる、エージェントが永遠に回り続けたり浪費し続けたりしないようにするハードストップ、そしてツール出力を信頼できないテキストとして扱う習慣です。
5. オプション — スマートレイヤーとしての Claude
デフォルトでは、決してクラウドを呼びません。しかし一部のステップは本当に小さなローカルモデルの手に余ります — やっかいな多段階の計画、正確でなければならないリファクタリング、長いコンテキストにまたがる統合。それらのステップだけ に対して、エージェントは Claude API にエスカレートし、より良い回答を得て、ローカルループに戻ることができます。これは Claude + ローカルモデル にある ルーター / ドラフトしてから精緻化 のアイデアを、一度に 1 ステップずつ適用したものです。
import anthropic
cloud = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env
def hard_step(prompt, allow_cloud=False):
"""Escalate ONE hard step to Claude — only when explicitly allowed."""
if not allow_cloud:
return None # default: stay fully local, send nothing off-device
msg = cloud.messages.create(
model="claude-sonnet-4-5", # check current model ids before pinning
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return msg.content[0].text
2 つのルールがこれを誠実に保ちます: クラウド経路は オプトイン(デフォルトでオフ)であること、そしてその単一のステップが必要とするものだけを送ること — コンテキスト全体ではありません。ローカルモデルが働き者であり続け、Claude は難しい 5% のために呼ぶスペシャリストです。正確な現在のモデル ID と料金については、以下の確認ノートを参照してください。
- ローカルエージェントは依然としてあなたのマシン上で実際のアクションを実行します — ツールをサンドボックス化し、破壊的なステップには承認を要求し、ループ/予算に上限を設け、ツール結果を信頼できないものとして扱ってください(プロンプトインジェクション)。
理解度チェック
理解度チェック
0/4- プライベートアシスタントはループに配線された 4 つのボックスである: ローカルモデル + モデル非依存のエージェント + ローカル MCP ツール + ガードレール — オプションの 5 つ目のボックスとしての Claude を伴う
- ローカルがデフォルトでありプライバシーの保証である: モデル、ループ、ツール、そしてあなたのデータはすべて、あなたがクラウド経路にオプトインしない限りあなたのマシンにとどまる
- ループは単純でモデル非依存に保ち(OpenAI のチャット形式)、実際の機能はローカル MCP サーバーの背後に置く — 一度作って、クライアント間で再利用する
- ガードレールは飛ばせない部分である: 破壊的ステップを承認し、ループ/予算に上限を設け、ツールをサンドボックス化し、ツール結果を信頼できないものとして扱う
- Claude は難しい 5% のためのオプトインなスマートレイヤーである — 一度に 1 ステップずつエスカレートし、そのステップが必要とするものだけを送る
- 移ろいやすい具体(モデル名、ID、価格、SDK の API)は確認ノートの背後に置く; アーキテクチャは永続的、数字はそうではない