搭建私有本地 AI 栈(端到端)
你已经分别见过各个部件:一个本地模型、一个本地智能体循环、通过 MCP 暴露的工具,以及 Claude+ 本地混合模式。这是收官之作——把它们连接成一个运行在你自己机器上的可用私有助手的页面:一个在本地运行的开放权重模型、一个能调用工具的模型无关智能体循环、通过本地 MCP 服务器暴露的那些工具、一道挡在危险工具前面的护栏,以及——可选地——把 Claude 作为一个可选启用的“智能层”,用于最难的 5% 步骤。贯穿始终的主线是:一切敏感内容都留在设备上;云是可选的,只留给少数难题。
- 把整个栈看作一张图:本地模型 + 智能体循环 + 本地 MCP 工具 + 护栏(+ 可选的 Claude)
- 在本地运行一个开放权重模型,并确认它能进行工具调用
- 搭起一个最小的、模型无关的智能体循环——同一个循环,换掉端点即可
- 通过本地 MCP 服务器暴露几个工具,让智能体去调用它们
- 加一道护栏:对破坏性操作要求审批、设置循环/预算上限,以及处理不可信的结果
- 可选地只把最难的推理路由给 Claude,让默认路径保持完全本地
整个栈,一张图说清
这个心智模型由为数不多的几个方框组成,每一个你都已在某个姊妹页面见过。这个助手不过是把这些方框连接起来:
把它当作一个循环来读。智能体问本地模型下一步该做什么。模型要么直接回答,要么发出一个工具调用。每个工具调用在抵达本地 MCP 服务器之前都会经过一道护栏,MCP 服务器真正去干活(读一个文件、运行一条命令、搜索你的笔记)并返回结果。智能体把结果反馈给模型,如此反复,直到任务完成。通往 Claude 的虚线路径是可选启用的:智能体只把本地模型处理不了的步骤上升,而且只在你允许时才这么做。
三条特性让这个栈值得搭建:
- 默认本地。 模型、循环、工具,以及你的数据全都待在你自己的硬件上。除非可选的 Claude 路径被触发,否则没有任何东西离开这台机器——而即便触发,也只有你选择发送的内容才会出去。
- 模型无关的循环。 智能体与一个 OpenAI 风格的对话端点通信。今天把它指向 Ollama 的本地端点;明天不用重写循环就能把它指向另一家提供商。
- 工具统一于一个标准之后。 各种能力存在于 MCP 服务器里,而不是硬编码进循环。工具只需构建一次,任何会说 MCP 的客户端(你的智能体、Claude Code、另一个应用)都能使用它。
分步搭建
- 安装 Ollama 并启动一个支持工具调用的模型。ollama run 会在首次使用时下载,并在 localhost:11434 上暴露一个本地的、与 OpenAI 兼容的 API。这就是你默认的“大脑”——私有且离线。(完整安装:本地运行模型页面。)
- 写一个小小的循环:把消息 + 一份工具 schema 发给对话端点,读取回复,如果回复里包含 tool_calls 就执行它们,把结果追加进去,然后循环,直到模型返回最终答案。这个循环对它与哪个模型通信一无所知——只知道 OpenAI 的对话形状。
- 把你真正的能力(读文件、运行命令、搜索笔记)放进一个基于 stdio 的本地 MCP 服务器,而不是硬编码它们。智能体列出服务器的工具,把它们映射进模型的工具 schema,并按需调用。构建一次,跨客户端复用。
- 在任何工具运行之前对它把关:自动放行只读工具,对破坏性工具(run_shell、write_file、delete)要求明确审批,限制循环迭代次数和总 token 数,并把每个工具结果都当作可能试图操纵模型的不可信输入。
- 把本地路径保持为默认。当某一步确实很难时——棘手的多步推理、一个本地模型总是搞砸的计划——让智能体只把那一步上升到 Claude API,然后回到本地循环。这就是混合页面里的路由器 / 先起草再精修的思路,只不过一次只应用于一步。
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. 模型无关的智能体循环
这个循环故意做得很笨:它把消息和一份工具 schema 转发给对话端点,每当模型要求调用工具时,它就运行工具并把结果反馈回去。因为它只会说 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 连接到本地工具。)
一个最小的 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 工具 schema,并把模型的工具调用路由到该服务器。同一个循环,真实的能力——而且这个服务器可被任何会说 MCP 的客户端复用。
4. 护栏(千万别跳过这一步)
这是玩具和你愿意在自己机器上信任的东西之间的分界。第 2 步里的 dispatch 函数是那个唯一的咽喉要道,每个工具调用在运行之前都会在这里被检查。三项职责:
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,加上你按每次运行跟踪的 token 上限)结合起来,你就有了三项真正重要的控制:对任何破坏性操作让人类参与其中、一个硬性停止让智能体无法永远打转或花钱、以及一种把工具输出当作不可信文本对待的习惯。
5. 可选——Claude 作为智能层
默认情况下,永远不要调用云。但有些步骤确实超出了一个小型本地模型的能力——盘根错节的多步规划、一次必须正确的重构、一次跨长上下文的综合。只对那些步骤,智能体可以上升到 Claude API,拿到一个更好的答案,然后落回本地循环。这就是来自 Claude + 本地模型的路由器 / 先起草再精修思路,一次只应用于一步。
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
两条规则让这件事保持诚实:云路径是可选启用的(默认关闭),而且你只发送那一步所需要的内容——不是你的整个上下文。本地模型始终是主力;Claude 是你为难题 5% 而请来的专家。关于确切的当前模型 id 和定价,见下面的核验说明。
- 本地智能体仍然会在你的机器上采取真实操作——给工具做沙箱、对破坏性步骤要求审批、给循环/预算设上限,并把工具结果当作不可信内容对待(提示注入)。
自我检测
自我检测
0/4- 一个私有助手就是连成循环的四个方框:本地模型 + 模型无关的智能体 + 本地 MCP 工具 + 一道护栏——加上 Claude 作为可选的第五个方框
- 本地是默认,也是隐私保证:模型、循环、工具和你的数据全都留在你的机器上,除非你主动选用云路径
- 让循环保持笨拙且模型无关(OpenAI 对话形状),把真实能力放在一个本地 MCP 服务器之后——构建一次,跨客户端复用
- 护栏是你不能跳过的部分:审批破坏性步骤、给循环/预算设上限、给工具做沙箱,并把工具结果当作不可信内容对待
- Claude 是难题 5% 的可选启用智能层——一次上升一步,只发送那一步所需要的内容
- 易变的具体信息(模型名称、id、价格、SDK API)藏在核验说明背后;架构是持久的,数字则不是