1. 项目概述当本地大模型遇上智能体框架最近在折腾本地AI应用的朋友可能都绕不开两个名字Ollama和智能体Agent。前者让你能在自己的电脑上轻松跑起各种开源大语言模型后者则代表了让AI自主完成任务的新范式。当我把这两个东西放在一起捣鼓出“agent_by_ollama”这个项目时感觉就像给本地AI装上了“大脑”和“手脚”让它从一个单纯的聊天伙伴变成了一个能帮你查资料、写代码、处理文件的“数字助理”。简单来说agent_by_ollama是一个基于 Ollama 本地大模型服务构建的智能体Agent框架。它的核心目标是让你无需依赖OpenAI、Claude等云端API完全在本地环境中就能创建出具备规划、执行、反思能力的AI智能体。想象一下你告诉它“帮我总结一下上个月的工作报告并生成下周的计划”它就能自动调用文件读取工具、文本分析模型最终给你一个结构化的输出。整个过程数据不出你的电脑隐私和安全完全可控而且几乎没有使用成本除了电费。这个项目特别适合几类人一是对数据隐私有极高要求的开发者或企业希望将AI能力集成到内部系统二是AI爱好者或研究者想深入理解智能体的工作原理并动手实践三是那些受限于网络环境或预算但又想体验最新AI智能体能力的用户。它把看似高深的智能体技术用一套相对清晰的代码结构封装起来降低了上手门槛。接下来我就带你深入拆解这个项目的设计思路、核心实现以及那些我踩过坑才总结出的实操要点。2. 核心架构与设计思路拆解要理解agent_by_ollama得先弄明白它要解决的核心矛盾强大的智能体能力与完全的本地化、轻量化部署之间的矛盾。市面上成熟的智能体框架如 LangChain、AutoGen其默认设计往往围绕云服务API展开。而Ollama虽然让本地运行大模型变得简单但它本质上是一个模型服务不直接提供智能体所需的任务分解、工具调用等高层逻辑。2.1 为什么选择“Ollama 自研框架”的路径项目选择自研一个轻量级框架来桥接Ollama而非直接魔改LangChain主要基于以下几点考量极致的轻量与可控LangChain功能强大但体系庞大依赖众多。对于目标明确的本地智能体场景很多组件是冗余的。自研框架可以只保留最核心的“大脑规划- 执行工具- 反思评估”循环让代码更简洁依赖更少调试也更方便。与Ollama的深度集成Ollama提供了简洁的REST API和模型管理功能。自研框架可以更灵活地利用这些特性比如动态切换不同的本地模型用小巧的phi3做简单分类用强大的llama3做复杂推理或者直接处理Ollama返回的原始流式响应实现更流畅的交互体验。规避复杂依赖与版本冲突在本地部署环境中Python包依赖冲突是常见噩梦。一个尽可能纯净的环境能显著提升项目的可移植性和稳定性。agent_by_ollama的核心依赖可能只有requests调用Ollama API、pydantic数据验证等少数几个库。项目的顶层设计通常遵循一个经典的智能体工作流我将其概括为“感知-规划-行动-反思”循环并在此基础上做了本地化适配用户输入 - 智能体接收感知- 模型规划任务规划- 调用本地工具行动- 评估结果并循环反思- 输出最终结果这个循环的每一次迭代都通过向本地的Ollama服务发送请求利用大语言模型的理解和推理能力来驱动。2.2 核心模块职责划分一个典型的agent_by_ollama项目会包含以下几个核心模块理解它们的关系至关重要智能体核心Agent Core这是项目的大脑。它维护着智能体的状态如对话历史、已执行步骤、持有对工具集的引用并控制着主循环流程。它负责将用户的请求和上下文组织成给Ollama的提示词Prompt。工具抽象层Tool Abstraction定义智能体可以使用的“技能”。每个工具都是一个独立的函数或类有明确的名称、描述和参数。例如read_file、web_search可能需要本地化的替代方案如调用duckduckgo-search、execute_python等。框架需要提供一套标准接口让智能体能“知道”有哪些工具可用并“学会”如何调用它们。Ollama 客户端Ollama Client一个封装好的类负责与本地Ollama服务的HTTP API进行通信。它处理模型的加载、对话生成、参数如温度temperature、最大令牌数max_tokens的设置。这是与本地大模型交互的唯一通道。提示词工程Prompt Engineering这是本地智能体性能好坏的关键。由于本地模型能力通常弱于GPT-4精心设计的提示词尤为重要。这部分代码会定义系统指令System Prompt告诉模型“你是一个AI助手可以调用以下工具...”以及用户请求的格式化模板确保模型能稳定输出结构化的响应如JSON格式便于程序解析出要执行的动作。任务解析与分发器Parser Dispatcher负责解析Ollama返回的文本。理想的响应是类似{action: call_tool, tool_name: search, arguments: {...}}的JSON。解析器需要提取这些信息然后分发给对应的工具执行并将执行结果返回给智能体核心进入下一轮循环。注意在本地环境下工具的设计需要格外小心。像“发送邮件”、“调用第三方API”这类涉及网络或系统高级权限的工具必须经过严格的安全审查避免智能体被恶意指令利用对你的系统造成风险。建议初期只开放只读、无副作用的工具。3. 环境搭建与核心配置详解理论说得再多不如动手搭一个。下面我就以在Linux/macOS系统上从零开始搭建一个基础版的agent_by_ollama环境为例带你走一遍流程。3.1 基础环境准备Ollama的安装与模型拉取智能体的“大脑”依赖于Ollama提供的模型服务所以这是第一步。安装Ollama 访问 Ollama 官网根据你的操作系统选择安装方式。对于Linux通常是一行命令curl -fsSL https://ollama.ai/install.sh | sh安装完成后运行ollama serve启动服务。默认会在11434端口启动一个HTTP服务。拉取合适的模型 Ollama 支持众多模型选择取决于你的硬件和需求。对于智能体任务需要模型有较强的指令遵循和推理能力。轻量级尝试llama3:8b或qwen2.5:7b。在16GB内存的机器上可以流畅运行适合学习和简单任务。性能优先llama3.1:70b或qwen2.5:32b。需要强大的GPU和足够的内存但能力接近第一梯队闭源模型。# 例如拉取 Llama3 8B 模型 ollama pull llama3:8b # 拉取完成后可以测试一下模型是否正常工作 ollama run llama3:8b Hello, world!3.2 项目结构与依赖安装假设我们的项目目录结构如下这有助于保持代码清晰agent_by_ollama/ ├── main.py # 主程序入口 ├── agent_core.py # 智能体核心逻辑 ├── tools/ # 工具目录 │ ├── __init__.py │ ├── file_tool.py # 文件操作工具 │ └── calculator.py # 计算工具 ├── ollama_client.py # Ollama 客户端封装 ├── prompts.py # 提示词模板 └── requirements.txt # Python依赖requirements.txt文件内容可以非常简单pydantic2.0 requests2.28使用pip安装依赖pip install -r requirements.txt3.3 核心模块代码实现要点接下来我们看看几个核心文件的关键代码片段。注意以下代码是概念性示例展示了核心逻辑。1. Ollama 客户端 (ollama_client.py) 这是与模型交互的桥梁必须稳定可靠。import requests import json from typing import Iterator class OllamaClient: def __init__(self, base_url: str http://localhost:11434): self.base_url base_url def generate(self, model: str, prompt: str, system: str None, **kwargs) - str: 发送请求并获取完整响应 payload { model: model, prompt: prompt, stream: False, options: { temperature: kwargs.get(temperature, 0.1), # 智能体任务需要较低随机性 num_predict: kwargs.get(max_tokens, 2048), } } if system: payload[system] system try: resp requests.post(f{self.base_url}/api/generate, jsonpayload, timeout60) resp.raise_for_status() return resp.json()[response] except requests.exceptions.ConnectionError: raise Exception(无法连接到Ollama服务请确保 ollama serve 已启动。) except requests.exceptions.Timeout: raise Exception(请求Ollama服务超时可能是模型加载过慢或提示词过长。) def generate_stream(self, model: str, prompt: str, **kwargs) - Iterator[str]: 流式生成用于实现打字机效果 payload {**kwargs, model: model, prompt: prompt, stream: True} with requests.post(f{self.base_url}/api/generate, jsonpayload, streamTrue) as resp: for line in resp.iter_lines(): if line: data json.loads(line) yield data.get(response, )实操心得在智能体循环中我通常使用非流式streamFalse生成因为需要完整解析模型的输出。流式生成更适合最终与用户对话的环节。另外将超时时间timeout设置得长一些如60秒非常必要复杂任务下模型推理可能需要较长时间。2. 工具定义 (tools/file_tool.py) 工具的设计要遵循单一职责原则描述清晰准确这直接影响到模型能否正确调用它。from pydantic import BaseModel, Field from typing import Optional class ReadFileInput(BaseModel): 读取文件内容的工具输入参数 file_path: str Field(description需要读取的文件的绝对路径或相对于当前工作目录的路径) class FileTool: name read_file description 读取指定文本文件的内容。 args_schema ReadFileInput def run(self, file_path: str) - str: try: with open(file_path, r, encodingutf-8) as f: content f.read() return f文件 {file_path} 读取成功内容如下\n\n{content[:2000]}...\n # 限制返回长度 except FileNotFoundError: return f错误找不到文件 {file_path}。 except PermissionError: return f错误没有权限读取文件 {file_path}。 except Exception as e: return f读取文件时发生未知错误{str(e)}注意事项工具函数的返回值应该是字符串因为需要反馈给语言模型。务必做好异常处理并将错误信息清晰地返回这样智能体才能根据错误进行“反思”和调整。对于文件操作路径安全是重中之重要避免目录遍历攻击可以考虑将工具的工作目录限制在某个安全范围内。3. 提示词工程 (prompts.py) 这是项目的灵魂。一个优秀的系统提示词能极大提升本地模型的工具调用能力。SYSTEM_PROMPT 你是一个运行在本地环境中的AI智能体。你的目标是理解用户的请求并通过调用合适的工具来完成任务。 你可以使用的工具如下 {tools_list} 请严格按照以下步骤和格式思考并回应 1. 分析用户请求的目标。 2. 从可用工具中选择最合适的一个。如果当前步骤无需调用工具例如直接回答用户问题则选择 final_answer。 3. 如果需要调用工具请以严格的JSON格式输出且只输出这个JSON对象不要有任何其他解释。 JSON格式必须为{{action: call_tool, tool_name: 工具名, arguments: {{arg1: value1, ...}}}} 4. 如果直接回答请输出{{action: final_answer, content: 你的回答内容}} 记住每次只执行一个步骤。你将收到上一步工具的执行结果然后继续分析下一步。 def format_user_prompt(user_input: str, conversation_history: list None) - str: 格式化用户输入结合历史对话 history_text if conversation_history: for msg in conversation_history[-5:]: # 限制历史长度避免上下文溢出 history_text f{msg[role]}: {msg[content]}\n prompt f{history_text}用户: {user_input}\n智能体: return prompt核心技巧系统提示词中明确要求模型输出严格的JSON格式这是实现程序自动解析的关键。对于能力较弱的模型你甚至可以在提示词中给出多个例子Few-shot Learning。此外管理好对话历史的长度conversation_history[-5:]因为Ollama模型的上下文窗口是有限的如8K超出部分会被丢弃。4. 智能体主循环与任务执行流程有了上面的积木我们就可以把它们组装成智能体的核心引擎。主循环的逻辑是项目的骨架。4.1 主循环逻辑实现在agent_core.py中我们会构建一个Agent类其run方法实现了核心循环import json import re from .ollama_client import OllamaClient from .prompts import SYSTEM_PROMPT, format_user_prompt class Agent: def __init__(self, model: str llama3:8b, tools: list None): self.model model self.ollama OllamaClient() self.tools {tool.name: tool for tool in (tools or [])} self.conversation_history [] def run(self, user_input: str, max_steps: int 10) - str: 执行智能体任务直到返回最终答案或达到最大步数 self.conversation_history.append({role: user, content: user_input}) for step in range(max_steps): # 1. 准备提示词 tools_list_str \n.join([f- {name}: {tool.description} for name, tool in self.tools.items()]) system_prompt SYSTEM_PROMPT.format(tools_listtools_list_str) user_prompt format_user_prompt(user_input, self.conversation_history) # 2. 调用模型获取决策 raw_response self.ollama.generate( modelself.model, promptuser_prompt, systemsystem_prompt ) # 3. 解析模型响应 action_dict self._parse_response(raw_response) # 4. 执行动作 if action_dict[action] final_answer: final_answer action_dict[content] self.conversation_history.append({role: assistant, content: final_answer}) return final_answer # 任务完成返回最终答案 elif action_dict[action] call_tool: tool_name action_dict[tool_name] tool_args action_dict.get(arguments, {}) if tool_name not in self.tools: tool_result f错误未知工具 {tool_name}。 else: # 实际调用工具 tool self.tools[tool_name] try: tool_result tool.run(**tool_args) except Exception as e: tool_result f调用工具 {tool_name} 时发生错误{str(e)} # 将工具执行结果作为新一轮的“用户输入”加入历史继续循环 self.conversation_history.append({role: assistant, content: f[调用工具 {tool_name}]}) self.conversation_history.append({role: user, content: f工具执行结果{tool_result}}) # 下一轮循环会基于这个新的“用户输入”即工具结果继续决策 else: # 解析失败将错误信息反馈给模型 error_msg f无法解析你的响应{raw_response}。请严格按照指定的JSON格式输出。 self.conversation_history.append({role: user, content: error_msg}) return f达到最大执行步数{max_steps}任务未完成。4.2 响应解析的关键细节_parse_response方法是连接非结构化文本和结构化程序逻辑的脆弱环节必须足够健壮。def _parse_response(self, response: str) - dict: 尝试从模型响应中解析出JSON动作指令 # 方法1尝试直接查找JSON块常见于模型将JSON包裹在json 中 json_match re.search(r(?:json)?\s*({.*?})\s*, response, re.DOTALL) if json_match: json_str json_match.group(1) else: # 方法2尝试将整个响应解析为JSON模型可能直接输出JSON json_str response.strip() try: action_dict json.loads(json_str) # 验证必需字段 if action not in action_dict: raise ValueError(响应中缺少 action 字段) return action_dict except (json.JSONDecodeError, ValueError) as e: # 解析失败返回一个要求模型重试的默认动作 return { action: call_tool, tool_name: final_answer, # 假设我们有一个直接回答的工具 arguments: { content: f我无法理解你的指令格式。请确保输出有效的JSON。原始响应{response[:200]} } }避坑指南模型输出不稳定是本地智能体最大的挑战之一。你可能会遇到1) 输出非JSON的废话2) JSON格式错误如缺少引号3) 工具参数类型错误。因此解析逻辑必须有多层降级策略先尝试提取代码块内的JSON再尝试解析整个响应最后要有兜底的错误处理将错误信息反馈给模型让它有机会在下一次循环中纠正。5. 实战演练构建一个本地文件分析助手现在让我们把上面所有的部分组合起来创建一个可以实际运行的例子一个能读取本地文件并总结其内容的智能体。5.1 工具集扩展除了之前的read_file我们再增加一个简单的文本分析工具。tools/summarizer_tool.py:from pydantic import BaseModel, Field class SummarizeInput(BaseModel): text: str Field(description需要总结的文本内容) max_length: int Field(default100, description总结文本的最大长度字符数) class SummarizerTool: name summarize_text description 对一段文本进行摘要总结提取核心内容。 args_schema SummarizeInput def run(self, text: str, max_length: int 100) - str: # 这是一个极其简单的总结逻辑实际应用中可以用更复杂的算法或调用另一个专门的总结模型 if len(text) max_length: return text # 简单截取前N句作为“总结”仅作演示 sentences text.replace(。, 。).split(。) summary for s in sentences: if len(summary) len(s) max_length: summary s 。 else: break return summary if summary else text[:max_length] ...5.2 主程序集成main.py:from agent_core import Agent from tools.file_tool import FileTool from tools.summarizer_tool import SummarizerTool def main(): # 1. 初始化智能体并注册工具 my_agent Agent( modelllama3:8b, # 指定使用的本地模型 tools[FileTool(), SummarizerTool()] # 注册工具实例 ) # 2. 运行一个示例任务 user_query 请帮我读取 /home/user/documents/report.txt 这个文件然后总结一下它的主要内容。 print(f用户: {user_query}) print(\n--- 智能体开始执行 ---\n) final_result my_agent.run(user_query, max_steps6) print(\n--- 执行结束 ---\n) print(f最终答案: {final_result}) if __name__ __main__: main()5.3 运行与观察运行python main.py你会看到类似下面的输出具体内容取决于你的文件和模型用户: 请帮我读取 /home/user/documents/report.txt 这个文件然后总结一下它的主要内容。 --- 智能体开始执行 --- 内部循环 步骤1: 模型分析请求决定调用 read_file 工具参数为 file_path。 步骤2: 执行 read_file成功读取文件内容返回文件文本。 步骤3: 模型收到文件内容决定调用 summarize_text 工具参数为刚读取的文本。 步骤4: 执行 summarize_text生成摘要。 步骤5: 模型收到摘要认为任务完成输出 final_answer。 --- 执行结束 --- 最终答案: 文件 report.txt 的主要内容是关于2024年第三季度的项目进展总结。报告指出A项目已按计划完成前端界面开发B项目在算法优化上遇到延迟团队计划在下月初进行集中攻关。总体预算执行情况良好略有结余。这个简单的例子展示了智能体如何将复杂任务“读文件并总结”自动分解为两个顺序执行的子任务“读文件” - “总结”并通过与本地模型的交互来协调整个过程。6. 性能调优与常见问题排查在实际使用中你一定会遇到各种问题。下面是我在开发和使用过程中积累的一些经验。6.1 如何提升智能体的可靠性本地模型的“智商”和“服从性”波动较大以下是几个提升稳定性的关键点提示词优化是重中之重结构化输出要求必须在系统提示词中强制要求JSON格式并给出明确示例。分步思考Chain-of-Thought鼓励模型“一步一步思考”在提示词中加入Lets think step by step.或中文的“请按步骤思考”能显著提升复杂任务下的推理质量。工具描述清晰具体工具的名称、描述、参数说明要尽可能无歧义。避免使用“处理数据”这种模糊描述改用“计算CSV文件的行数”这种具体描述。模型选择与参数调整温度Temperature智能体任务需要确定性高的输出建议设置为较低值如0.1-0.3。过高的温度会导致输出随机工具调用格式错误。重复惩罚Repeat Penalty可以适当调高如1.1防止模型陷入重复循环。尝试不同模型如果llama3:8b在工具调用上表现不佳可以试试qwen2.5:7b或command-r不同模型对指令的遵循能力有差异。实现“反思”机制 基础循环中工具执行失败后只是简单地将错误信息反馈。更高级的做法是让模型进行“反思”。例如当read_file返回“文件未找到”时系统可以提示模型“工具执行失败原因是文件路径错误。请检查你提供的路径是否正确或尝试使用list_directory工具先查看目录内容。” 这需要更复杂的提示词设计和状态管理。6.2 常见问题与解决方案速查表问题现象可能原因排查步骤与解决方案连接Ollama失败报ConnectionError1. Ollama服务未启动。2. 端口被占用或防火墙阻止。1. 终端运行ollama serve并确保无报错。2. 检查http://localhost:11434能否访问。使用curl http://localhost:11434/api/tags测试。模型响应速度极慢1. 模型过大硬件RAM/VRAM不足。2. 提示词过长接近模型上下文窗口。1. 换用更小的模型如phi3:mini。使用ollama ps查看模型加载状态。2. 精简对话历史在format_user_prompt中限制历史消息条数。模型不输出JSON而是输出自然语言提示词中结构化输出的指令不够强或模型未遵循。1. 强化系统提示词使用“你必须输出JSON且只能是JSON”等强硬措辞。2. 在提示词中提供2-3个完整的输入输出示例Few-shot。3. 在解析失败时将错误和正确示例反馈给模型让它重试。工具调用参数总是错误1. 模型不理解参数格式。2. 参数验证失败。1. 在工具描述中明确每个参数的类型和示例如file_path: (字符串) 文件路径例如 /home/user/data.txt。2. 在调用工具前使用args_schema(Pydantic模型) 对输入参数进行强制验证和类型转换。智能体陷入死循环不断调用同一个工具1. 工具执行结果未能让模型意识到任务已完成。2. 最大步数max_steps设置过高。1. 检查工具返回的结果是否清晰。例如总结工具完成后应返回“总结已完成内容是...”。2. 设置合理的max_steps如5-10步并添加超时中断逻辑。在提示词中提醒模型“如果任务看似无法完成或陷入循环请输出最终答案说明情况。”内存占用越来越高对话历史不断累积未做清理。实现一个简单的历史窗口管理只保留最近N轮对话。对于长会话可以定期让模型自己总结之前的对话摘要然后用摘要替代详细历史节省上下文空间。6.3 进阶扩展方向当基础版本跑通后你可以考虑以下几个方向进行深化支持并行工具调用让模型可以同时发出多个工具调用指令提升效率。这需要扩展动作解析格式如{action: parallel_call, calls: [...]}并实现并发执行逻辑。集成向量数据库与记忆使用chroma或faiss本地向量库让智能体能够读取超出上下文窗口的长文档并拥有长期记忆能力。实现Web UI使用Gradio或Streamlit快速构建一个图形界面方便非技术用户与你的本地智能体交互。工具生态扩展接入更多本地化工具如execute_shell谨慎、query_database连接本地SQLite、generate_image调用本地Stable Diffusion等打造真正强大的个人AI工作流。构建本地智能体的过程是一个与模型“斗智斗勇”、不断优化提示词和流程的工程。它没有云API那样开箱即用的稳定性但带来的数据自主权和可定制化程度是无可比拟的。每一次成功让智能体完成一个复杂任务都像教会了一个数字伙伴一项新技能这种成就感正是开源和本地化AI的魅力所在。