
最近不少朋友问我怎么在本地搭一个真正能干活的 AI Agent而不是只会聊天的对话框。我自己摸索了一段时间最后选定的方案是Ollama 跑本地模型再写一套轻量的调度逻辑把模型变成一个能查资料、能调工具、能按流程执行任务的智能代理。整套下来不花一分钱 API 费用数据全在自己机器上隐私和可控性都拿捏得很稳。这篇文章就把我完整踩过的坑、选型的逻辑、以及可以直接照抄的搭建流程全部写出来适合有一定编程基础、想摆脱云 API 依赖的开发者参考。1. 搭一个本地 AI Agent 前先想清楚三件事1.1 Agent 到底是什么和聊天机器人有什么区别先说个基础认知问题。很多人把能连续对话误当成 Agent其实完全不是一回事。普通的聊天机器人是你说一句我回一句它没有目标也不会主动调用外部工具。而 AI Agent 的核心是给定一个目标它能自己规划步骤、调用工具、分析结果、调整策略直到把任务完成。举个生活化的例子。你让聊天机器人帮我查一下明天的天气并规划出行它只会回复一段建议。但一个合格的 Agent 会这样做先调用天气 API 获取数据判断是否需要带伞再调用地图 API 规划路线最后把结果汇总成一段可读的答复。每一步都是模型在思考但每一步的执行都依赖外部工具。所以搭建 Agent 的关键不只是选一个大模型而是要把模型推理和工具执行这两条线拼接起来。Ollama 解决的是推理这一半另一半需要我们自己用代码搭一个调度器这也是这篇文章里最核心的内容。1.2 为什么选 Ollama 而不是云 API你可以用任何模型服务来搭 Agent但我最终选择 Ollama原因有三个。第一是成本。我做的项目里有大量测试和反复调优的场景每天可能要发几百次请求。如果用云 API这些请求都是真金白银。Ollama 跑在自己的 GPU 或 CPU 上没有调用次数限制随便刷。对于个人开发者、学习项目、原型验证来说免费这个优势是压倒性的。第二是隐私。很多企业场景或者个人敏感数据根本不适合发到云端。Ollama 的所有推理都在本地完成模型文件、中间结果、日志全部留在自己手里。这一点在做私有化知识库、内部文档问答、代码辅助工具时特别重要。第三是可控性。云 API 的版本更新、模型下架、限流策略都不由你控制。Ollama 装好之后模型文件就在你的磁盘里你可以随意切换版本、调整参数、做微调没有任何外部约束。当然Ollama 也有代价。你没有云端那么强的算力所以跑不了超大参数模型模型效果也不一定比顶级商业 API 好。但对我这种方式是先用起来、跑通流程的开发阶段来说Ollama 是性价比最高的起点。1.3 整体方案本地模型 调度脚本 工具接口我先说一下最终架构让你心里有个地图。用户输入 - 主调度循环 - Ollama 本地模型 - 输出意图判断 ↓ 需要工具 解析出工具名和参数 - 执行函数 - 返回结果给模型 - 继续推理 ↓ 不需要工具 直接输出最终回答整个系统分成三层模型层Ollama 加载本地开源模型负责理解意图、生成文本、决定是否调用工具。调度层一段 Python 脚本负责循环调用模型、解析输出、维护多轮对话记忆、判断任务是否结束。工具层一组普通的 Python 函数比如查询天气、读写文件、计算表达式、搜索本地文档。每个函数就对应 Agent 的一只手。这个架构看起来简单但很实用。好处是每一层都能独立替换今天用 Ollama 跑一个 7B 模型明天换更大的模型调度层完全不用改工具层加一个函数Agent 就多一项能力。下面我按这个框架逐步拆解搭建过程。2. 环境搭建装好 Ollama 并选对模型2.1 安装 Ollama3 分钟跑起第一个模型Ollama 的安装是我见过的最省心的 AI 工具之一。在 macOS 和 Windows 上直接去官网下载安装包双击安装即可。在 Linux 上执行一句脚本就能装好curl -fsSL https://ollama.com/install.sh | sh装好之后验证是否成功ollama --version然后拉取一个模型试试。我用的主力模型是 Qwen2.5 7B Instruct这个模型中文效果好、工具调用能力也不错体积适中适合大多数人的硬件。拉取命令ollama pull qwen2.5:7b第一次拉取会下载几个 GB 的文件等它跑完然后启动一个简单对话ollama run qwen2.5:7b 你好做个自我介绍如果能看到正常回复就说明 Ollama 已经跑通了。这里有个值得多说一句的点Ollama 安装完成后会默认在后台启动一个本地服务监听11434端口。也就是说你用命令行能对话其实底层就是通过 HTTP API 在调用后面我们用 Python 写 Agent 调度器也是走这个端口。2.2 模型量化与显存需求怎么算很多新手卡在这一步模型下载好了但跑起来特别慢或者直接报内存不足。这涉及模型量化和硬件配置的关系。模型文件有很多种精度格式最常见的是 FP16 和量化版。一个 7B 参数的模型FP16 格式大约占用 14GB 显存很多人的显卡根本装不下。量化指的是把模型里的浮点数精度降低比如从 16 位降到 4 位模型体积直接缩到四分之一效果损失却控制在可接受范围内。Ollama 上默认拉取的模型都自带量化版本你不需要手动处理。以 Qwen2.5 7B 为例量化格式模型文件大小运行所需内存估算推荐硬件q4_K_M约 4.7GB6-8GB8GB 显存或 16GB 内存q8_0约 8.2GB10-12GB12GB 显存或 32GB 内存FP16约 15GB16GB24GB 显存或 64GB 内存我自己的机器是 8GB 显存的显卡跑 q4_K_M 版本很流畅生成速度大约每秒 40 个 token。如果你没有独立显卡只有 CPU 和 16GB 内存也能跑就是速度慢一些大概每秒几个 token做实验够用但聊天的体验会打折扣。注意如果你的机器内存或显存小于模型所需Ollama 会优先保证模型加载然后挤压其他程序的内存空间结果就是电脑卡死。所以选模型之前先看一眼自己的硬件再决定用哪个量化版本。2.3 调好关键运行参数Ollama 模型的运行参数可以通过命令行设置也可以直接在启动时写好。这些参数对 Agent 的稳定性影响非常大我逐个说。温度temperature控制生成随机性。0 表示每次输出最确定的答案1 表示最大随机。做 Agent 任务我强烈建议把温度调到 0 到 0.3 之间。因为 Agent 需要稳定地输出工具调用指令温度太高它就会开始自由发挥格式一乱调度器就解析不了。上下文长度num_ctx这个决定模型能记住多少前面的对话。默认值通常是 2048 或 4096。做 Agent 任务时因为要包含工具返回的长文本、多轮对话历史上下文很容易被打满。建议调大比如 8192 或 16384但注意这会增加显存消耗。重复惩罚repeat_penalty防止模型重复同样的句子。默认值 1.1 就行不用动。top_p核采样参数默认 0.9 附近一般不需要调。如果发现输出质量不稳定可以降到 0.8。在实际项目中我通常把这些参数写进启动脚本不手动改全局配置。比如ollama run qwen2.5:7b --temperature 0.1 --num_ctx 8192这里要注意--num_ctx如果设得比模型默认值大会显著增加显存占用。比如 7B 模型 q4 量化本身只需 5GB但把上下文提到 16K 之后可能需要再多 3-4GB。根据你的显存情况量力而行。3. 手把手让 Agent 学会干活3.1 用 API 跟本地模型对话命令行只是用来测试的要搭建 Agent真正的核心是调用 Ollama 的 HTTP API。它提供了一个很简洁的接口直接发 POST 请求就行。先看一个简单的对话请求curl http://localhost:11434/api/chat -d { model: qwen2.5:7b, messages: [ {role: user, content: 1 1 等于几} ], stream: false }响应的 JSON 里有个message.content字段就是模型的回答。用 Python 调用更直观import requests def chat_with_model(messages, temperature0.2): response requests.post( http://localhost:11434/api/chat, json{ model: qwen2.5:7b, messages: messages, stream: False, options: { temperature: temperature } } ) return response.json()[message][content]注意stream参数。默认情况下模型是逐字输出流式返回的对 Agent 这种机器对机器的场景来说流式反而增加解析复杂度所以直接设成false一次性拿完整结果简单省事。3.2 让模型输出工具调用指令Agent 的关键一步是让模型决定要不要调工具以及调哪个工具、传什么参数。这本质上是一个提示词工程问题。我的做法是在系统提示词里给模型一个明确的行为规则。下面是我真实用过的一套提示词模板你是任务规划助手。你有一个可以使用的工具列表 - weather(city: string): 查询城市天气 - calculator(expression: string): 计算数学表达式 - current_time(): 获取当前时间 规则 1. 如果用户问题需要工具才能回答必须输出严格的 JSON格式为 {tool: 工具名, params: {参数名: 参数值}, thought: 简短推理说明} 2. 如果不需要工具直接输出纯文本回答。 3. 判断要准确不能把简单问题复杂化。然后把这段提示词和用户消息一起发给模型。模型在推理时要么输出普通文本要么输出 JSON 格式的工具调用指令。调度器拿到输出后先用正则表达式提取 JSON再根据tool字段匹配对应的 Python 函数完成调用。这里有一个很重要的经验模型输出 JSON 时经常带一些额外的文字比如好的我来查一下天气JSON 如下{...}。如果你是直接json.loads整个字符串一定会报错。所以必须用正则把大括号里的内容提取出来再去解析。import re import json def extract_json(raw_text): pattern r\{[^{}]*\} match re.search(pattern, raw_text, re.DOTALL) if match: return json.loads(match.group(0)) return None注册工具函数的时候用字典映射def weather(city): # 这里可以接真实天气 API演示时返回模拟数据 return f{city}今天晴气温 22-28 度 def calculator(expression): return str(eval(expression)) def current_time(): from datetime import datetime return datetime.now().strftime(%Y-%m-%d %H:%M) TOOLS { weather: weather, calculator: calculator, current_time: current_time, }新增工具的时候只需要写一个普通 Python 函数然后在系统提示词的工具列表里加上描述再把这个函数注册进TOOLS字典Agent 就能用了。3.3 搭建工具调度主循环核心调度逻辑其实就是一个 while 循环把消息发给模型解析输出如果输出是工具调用指令就执行工具把结果加进消息历史再发给模型如果输出是普通回答就结束并把回答展示给用户。直接看我写的一个完整骨架def run_agent(user_input): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input}, ] max_iterations 5 # 防止死循环 for _ in range(max_iterations): response_text chat_with_model(messages) tool_call extract_json(response_text) if tool_call and tool in tool_call: tool_name tool_call[tool] params tool_call.get(params, {}) if tool_name in TOOLS: # 执行工具 result TOOLS[tool_name](**params) # 把工具结果追加到对话历史 messages.append({role: assistant, content: response_text}) messages.append({role: tool, content: str(result)}) continue else: return f错误未知工具 {tool_name} else: # 没有工具调用这就是最终回答 return response_text return 达到最大循环次数任务未完成这段代码有几个值得注意的点。第一为什么工具结果要作为消息的一部分发回去而不是直接在系统外部拼接因为模型需要看到工具返回的数据才能决定下一步是继续调用工具还是生成最终回答。这本质上是一个多轮推理的过程。如果你把工具结果存在变量里下一轮请求时模型根本不知道你查到了什么。第二max_iterations上限很重要。新手经常踩这个坑模型在循环里反复调用同一个工具或者调用一个不存在工具导致无限循环程序卡死。设置一个上限最好打印日志这样你能看到模型每一步在想什么、要调什么方便调试。第三消息历史不能无限增长。每次工具返回结果都会追加两条消息时间一长上下文会被撑爆。所以在实战版本里我会加一个裁剪逻辑只保留最近 N 轮的消息丢弃更早的历史。3.4 用 Open WebUI 给 Agent 一个可视界面调度脚本写完之后你其实已经拥有了一个能跑的 Agent 核心。但纯命令行交互不太友好我建议加一层可视界面这里推荐开源项目 Open WebUI。Open WebUI 是一个很成熟的本地模型聊天界面支持与 Ollama 对接还能额外挂载知识库、自定义工具。安装最简单的方式是用 Dockerdocker run -d \ -p 3000:8080 \ -v open-webui:/app/backend/data \ --name open-webui \ --restart always \ ghcr.io/open-webui/open-webui:main启动后浏览器访问http://localhost:3000注册一个本地账号然后在设置里把 Ollama 地址填成http://host.docker.internal:11434就能在网页里和模型聊天了。不过要提醒的是Open WebUI 本身默认走的是聊天逻辑不是我们上面写的调度逻辑。如果你想在网页上体验完整的工具调用 Agent需要再花点功夫把自定义工具注册进 Open WebUI或者直接把调度脚本封装成一个轻量 API再用一个简单前端页面去调用。我个人的建议是先靠脚本验证 Agent 逻辑确认稳定后再去折腾界面不要一开始就陷入前端的泥潭。4. 优化、扩展与安全细节4.1 提示词怎样写Agent 才稳定提示词对 Agent 的稳定性影响怎么说都不为过。同样的模型提示词写得模糊它就会频繁误判、乱调工具写清楚了它能连续处理非常复杂的任务。我总结几个实战原则。第一工具描述必须具体到输入参数是什么、返回值是什么、什么场景下使用。比如天气工具不要只写查询天气要写输入城市名返回该城市当日天气状况适合用户询问天气时使用。模型是靠描述来匹配意图的描述越具体准确率越高。第二必须在提示词里说清楚什么时候不要用工具。如果不加这一条模型可能看到任何问题都先输出工具指令。加上如果用户只是闲聊、问看法、做总结请不要调用工具之后误调用率会大幅下降。第三给模型一些示例。如果某个工具容易误匹配可以在提示词里加一个示例比如用户问明天需要带伞吗你应该调用 weather 工具查询城市天气再根据结果回答。模型对示例的模仿能力非常强给一个例子胜过一百句规则。注意每次修改提示词后建议跑一遍同一批测试用例对比输出变化。我自己的经验是做一个简单的回归测试脚本把 10 个典型问题跑一遍检查工具调用是否正确避免改好一处、弄坏三处。4.2 跑得更快显存、上下文和并发调优本地模型最难的就是性能。我把性能问题分为三个维度分别说。显存层面最有效的做法是先把显存占用降下来。优先选择量化程度高的模型版本比如 q4_K_M。如果还嫌不够换一个更小的模型比如把 7B 换成 3B 甚至 1.8B。模型小了速度会快但推理质量也会下降需要自己权衡。上下文层面上下文长度直接决定模型能吃多少内容。很多 Agent 任务其实不需要非常长的上下文尤其是工具结果要精简。我在设计工具返回值时会刻意让工具返回简短结果而不是一股脑输出一大段。比如天气工具返回晴22-28 度而不是返回一整篇天气报告。这不仅省上下文还能减少模型的理解负担。并发层面Ollama 默认是串行处理请求的一个模型同时只能服务一个请求。如果有多个人要同时用最简单的方式是多加载几个模型实例但显存成本翻倍。实战里我更推荐加一层请求队列让 Agent 系统排队处理任务而不是同时强推多个请求导致内存爆炸。另外可以开启OLLAMA_NUM_PARALLEL环境变量允许一个模型并发处理多个请求。这个配置会显著提高吞吐量但会同步增加显存和内存消耗适合显存充足的情况。4.3 接入外部工具与私有数据Agent 的价值上限取决于你给它接了多少外挂。我建议优先接这三类。第一类是 API 型工具。比如查天气、查物流、翻译、搜索网页。实现方式就是写一个函数内部用requests请求外部接口返回 JSON 结果。注意要设置请求超时避免模型等一个迟迟不响应的接口导致整个调度卡住。第二类是本地文件工具。比如让 Agent 读取、搜索、总结某个目录下的文档。做法是封装一个read_file(path)和search_files(keyword)函数让模型可以自己翻文件、提取信息。我在做一个内部知识库助手时就是靠这种工具让 Agent 直接查询本地文档效果比预训练知识更准。第三类是代码执行工具。让 Agent 写 Python 代码、执行代码、返回运行结果。这个能力非常强大但风险也最高因为模型生成的代码不一定安全可能执行破坏性命令。我的建议是只在沙箱环境里开放这个工具比如 Docker 容器内别在宿主机上裸跑。私有数据这块除了上面的文件工具还可以用向量数据库做语义检索。把文档切成小块用本地 embedding 模型转成向量存进向量库然后给 Agent 一个search_vectors(query)工具它就能基于本地资料回答问题。整个链路全部在本地数据不出机器。用户问我们项目的部署文档里数据库连接配置怎么写 Agent 判断需要查资料 - 调用 search_vectors(数据库连接配置) 工具返回相关文档片段 - Agent 总结片段 - 输出最终回答这个架构扩展性很强后续想把 Agent 从助手升级成自动执行任务的角色只需要继续加工具比如加定时任务工具、邮件发送工具、数据报表工具它就能完成更加复杂的组合操作。5. 常见问题速查搭建中一个坑一个坑踩过来这套系统我前前后后改了三版踩过不少坑。把最典型的几个问题整理成一个速查表你大概率也会遇到。症状可能原因解决办法模型启动后一直显示loading模型文件过大或显存不足Ollama 在反复交换内存换更小量化模型关闭其他占用显存的程序加--num_ctx缩小上下文Agent 乱调用工具明明问题很简单却查了天气提示词里没有不需要工具时直接回答的规则在系统提示词中补充否定规则并加示例模型输出的 JSON 解析失败模型在 JSON 前后加了自然语言描述用正则提取大括号内容再解析 JSON不要直接json.loads对话轮数多了之后模型开始失忆上下文长度被打满最早的历史被丢弃加大num_ctx或精简工具返回内容限制消息历史条数请求报connection refusedOllama 服务没启动或者端口被占用执行ollama serve启动服务查看 11434 端口占用情况第一次调用模型特别慢模型正在加载进内存Ollama 有冷启动延迟等待几秒或者在服务启动前用预热脚本发一个空请求提前加载模型工具函数报错说参数不匹配模型传了错误的参数名或参数个数给工具函数自定义一个 dict 描述参数格式并在提示词里写清楚5.1 Ollama 服务起不来怎么办这个坑我踩得最狠。有一次我把模型参数调大之后运行ollama run直接报错服务一直起不来查了半天发现是显存不足导致 Ollama 进程崩溃。解决办法是检查一下nvidia-smi看显存占用再确认模型文件的量化格式和显存是否匹配。如果确认是显存问题最稳妥的方法是换小模型。当时我把 7B 模型换成 3B一切恢复正常。又或者在启动前释放一下显存关掉其他占用显存的进程。另一个常见原因是端口被其他程序占用。Ollama 默认监听11434如果你机器上恰好有其他服务占了同一个端口服务就会启动失败。排查方法lsof -i :11434如果有结果说明端口被占用要么停掉那个程序要么修改 Ollama 的端口配置。5.2 模型一直处于上下文加载状态对话半天没响应很多人遇到过第一次发消息后模型要卡很久才有反应。这是因为 Ollama 第一次调用时需要把整个模型文件从磁盘加载到内存这个过程取决于模型大小和磁盘速度。7B 模型大概需要几十秒。解决办法有两个。第一设置环境变量OLLAMA_KEEP_ALIVE1h表示模型加载后在一小时内保持驻留内存避免每轮对话都重新加载。这个配置非常有效我设完之后第二轮对话的响应时间从几十秒降到了即时。第二做一个预热调用。在 Agent 系统启动的时候主动给模型发一个简单请求让它提前加载完成。这样用户真正使用时不会感受到冷启动的延迟。5.3 工具调用输出格式不稳定同一个问题这次能用下次不能用这种情况通常不是模型本身的错而是提示词写得不够严格。模型在输出 JSON 时有时候会用单引号有时候会多一个逗号有时候用中文冒号。这些都会导致解析失败。除了用正则提取之外我还有一个纠错策略如果 JSON 解析失败就把错误信息和原始输出反馈给模型让模型重新输出一遍。相当于给模型一次自我修正的机会。代码如下if tool_call is None: messages.append({role: user, content: 你的输出包含无效的 JSON请重新输出只输出 JSON不要有其他文字。}) continue实测下来这个纠错策略能让成功率从 80% 提升到 95% 以上。偶尔出错没关系关键是系统能自我修正而不是一错到底。5.4 API 请求耗时太长有没有办法优化如果你的工具环节涉及外部网络请求比如天气 API、搜索 API这些请求的耗时可能比模型推理还长。我建议给每个外部工具设置 10 秒超时并且在工具层做结果缓存。同一个城市查天气短时间内不会有变化可以直接返回上次的结果大幅减少重复请求。另外Ollama 的num_predict参数也可以限制模型单次生成的最大 token 数。Agent 场景下模型很多输出都是简短的 JSON 指令把这个值设成 512 就能满足需求不用让它生成到默认的 2048。限制 token 数可以明显提升响应速度。options: { temperature: 0.2, num_predict: 512 }最后还有一个调优思路如果任务简单用一个小模型做判断用一个大模型做总结。比如意图识别阶段直接用 3B 模型判断是否要调工具速度快了好几倍等真正需要生成长回答的时候再切换到 7B 模型。这个双模型架构虽然复杂一点但效果和成本的平衡非常好。我个人在这套系统上花的时间大概有一半是在调提示词和做异常兜底而不是在写核心调度代码。这也印证了一个观点Agent 能不能稳定干活差的不是模型的智力而是你给它的规则、工具和容错机制。当你把提示词写清楚、把工具接口设计好、把异常处理补齐之后Ollama 跑出来的 Agent 完全可以胜任很多日常自动化任务。最后分享一个小技巧每次给 Agent 新增工具之前先在提示词里手动描述这个工具的输入输出格式跑通一个任务之后再把工具注册到代码里。这样增量式开发排查问题会轻松很多。