1. 项目概述当本地大模型遇上智能体框架最近在折腾本地大模型应用开发的朋友估计都绕不开一个核心问题如何让一个“聪明”的模型不仅能回答问题还能像真正的助手一样自主调用工具、处理复杂任务这正是智能体Agent技术要解决的。今天要聊的这个项目NN-Studio/agent_by_ollama就是一个非常典型的、基于Ollama本地大模型服务来构建智能体系统的实践方案。简单来说它不是一个全新的、从零开始的框架而更像是一个“粘合剂”和“脚手架”。它的核心价值在于将Ollama提供的强大本地模型能力比如 Llama 3、Mistral、Qwen 等与成熟的智能体开发框架如 LangChain、LlamaIndex 或自定义框架高效地连接起来让你能在自己的电脑或服务器上快速搭建一个具备规划、工具调用、记忆等能力的智能体应用。对于个人开发者、研究者和中小团队来说这意味着你可以用极低的成本无需昂贵的 API 调用费和完全可控的数据隐私来探索和实现各种 AI 自动化场景。我自己在尝试将本地模型用于自动化工作流时就遇到过几个痛点一是模型服务Ollama和智能体逻辑之间的接口调用不够优雅每次都要写一堆重复的 HTTP 请求代码二是工具调用的标准化和错误处理很繁琐三是缺乏一个清晰的项目结构来管理提示词、工具集和对话历史。agent_by_ollama这个项目从命名和设计思路上看正是瞄准了这些痛点提供了一个开箱即用、结构清晰的起点。接下来我们就深入拆解一下如何基于这个思路构建一个属于自己的、功能完备的本地智能体系统。2. 核心架构与设计思路拆解在动手写代码之前理清架构是避免后期陷入混乱的关键。一个基于 Ollama 的智能体系统其核心组件和交互逻辑可以归纳为下图所示的几个部分它们共同构成了一个闭环的工作流注此处用文字描述架构图实际项目中可用 PlantUML 或 draw.io 绘制整个系统围绕智能体执行引擎运转。用户输入一个任务Query比如“帮我总结一下昨天项目会议记录的核心内容并给张三发一封邮件概要”。这个任务首先被送到任务规划与拆解模块。该模块的核心是一个大模型通过 Ollama 调用它根据预设的提示词Prompt将复杂任务分解为一系列可执行的子步骤。例如分解为1. 从文件系统读取“会议记录.txt”2. 调用文本摘要工具总结内容3. 调用邮件发送工具填入收件人“张三”和摘要内容。接下来工具调用与执行模块登场。智能体会根据规划按顺序选择并调用相应的工具Tool。每个工具都是一个独立的函数比如read_file(path)、summarize_text(text)、send_email(to, subject, body)。智能体需要生成符合工具调用规范的参数通常是 JSON 格式然后由框架去实际执行这个函数。执行的结果成功或失败附带返回数据会被反馈给智能体。然后进入记忆与上下文管理模块。智能体并非“金鱼记忆”它需要记住之前的对话历史、工具调用结果和系统状态。这部分通常通过维护一个“对话历史”列表来实现每次与模型的交互都会将新的用户输入、工具调用、工具结果、模型回复追加到这个历史中。当下一次模型调用时会将相关的历史记录作为上下文一并送入从而实现连贯的多轮对话和任务执行。最后所有的交互都通过Ollama 模型服务层完成。Ollama 在这里扮演了“大脑”的角色负责所有的理解、规划、决策和生成任务。框架需要与其进行稳定的 HTTP/gRPC 通信处理模型流式输出、调整生成参数如 temperature, top_p等。为什么选择这样的架构解耦与灵活性将模型服务Ollama、智能体逻辑、工具实现分离开任何一部分都可以独立升级或替换。比如你可以轻松地从 Llama 3 切换到 Qwen 模型而无需重写智能体代码。可观测性与调试清晰的模块划分使得日志记录和调试变得容易。你可以清楚地看到任务是如何被规划的、调用了哪些工具、返回了什么结果这对于优化智能体行为至关重要。易于扩展当需要增加新功能时比如接入一个新的数据库查询工具你只需要按照规范实现这个工具函数并将其注册到工具库中即可核心框架无需改动。3. 环境准备与 Ollama 模型部署工欲善其事必先利其器。在开始构建智能体之前一个稳定、高效的本地模型环境是基石。3.1 Ollama 的安装与基础配置Ollama 的安装极其简单这也是它广受欢迎的原因之一。根据你的操作系统选择对应方式macOS/Linux: 通常一行命令搞定curl -fsSL https://ollama.ai/install.sh | shWindows: 直接从官网下载安装程序图形化安装。安装完成后在终端运行ollama serve即可启动服务。默认情况下Ollama 的 API 服务器会运行在http://localhost:11434。你可以通过curl http://localhost:11434/api/tags来验证服务是否正常它会列出你已经拉取的模型。第一个核心操作拉取模型。这是最关键的一步模型的选择直接决定了智能体的“智商”上限。对于智能体任务推荐使用经过指令精调、且在工具调用或推理能力上表现突出的模型。# 拉取一个推荐的轻量级通用模型例如 Llama 3 的 8B 指令版 ollama pull llama3:8b-instruct-q4_K_M # 或者对于中文任务支持更好的 Qwen 系列 ollama pull qwen2.5:7b-instruct # 如果你想追求更强的推理和工具调用能力可以尝试 DeepSeek 的最新版本 ollama pull deepseek-r1:7b注意模型名称后的标签如:8b-instruct-q4_K_M指定了模型的版本、类型和量化等级。instruct表示指令微调版本更适合对话和任务执行。q4_K_M是一种4位量化格式能在几乎不损失精度的情况下大幅减少内存占用和提升推理速度。对于大多数本地智能体应用7B-14B 参数范围的 4-bit 或 5-bit 量化模型是性价比最高的选择。3.2 模型选择与性能调优实战拉取模型只是开始如何让它跑得又快又好需要一些调优技巧。1. 量化等级的选择Ollama 支持多种量化格式如 q4_0, q4_K_M, q5_K_M, q8_0。数字越小量化越激进模型体积越小速度越快但可能损失更多精度。我的经验是q4_K_M: 在精度和速度间取得了很好的平衡是大多数场景的默认推荐。q5_K_M: 如果显存或内存充足追求更高精度可以选择这个。q8_0: 几乎无损但模型体积大速度慢除非对精度有极端要求否则不推荐。2. 上下文长度Context Length设置智能体需要处理长对话历史和复杂任务规划足够的上下文窗口是必须的。许多新模型原生支持 128K 甚至更长的上下文。在 Ollama 中你可以在运行时通过参数指定# 运行模型时指定上下文长度 ollama run llama3.2:3b-instruct --num-ctx 131072或者在创建模型 Modelfile 时定义。对于智能体应用建议至少设置 8192如果处理长文档则可能需要 32768 或更高。3. GPU 与 CPU 的取舍如果你有 NVIDIA GPUOllama 会自动利用 CUDA 加速。使用ollama ps可以查看模型运行时的 GPU 内存占用。如果 GPU 内存不足Ollama 会自动将部分层卸载到 CPU 内存但这会显著降低速度。技巧对于 7B 模型至少需要 6-8GB 的 GPU 显存才能流畅运行 4-bit 量化版。如果没有独立显卡纯 CPU 推理也是可行的尤其是对于 3B 左右的更小模型但需要耐心等待响应。4. 实践建议搭建一个基准测试在正式开发前我强烈建议你对候选模型进行一个简单的基准测试。创建一个benchmark.py脚本import requests import time def test_model(model_name, prompt): url http://localhost:11434/api/generate payload { model: model_name, prompt: prompt, stream: False, options: {num_predict: 512, temperature: 0.1} } start time.time() response requests.post(url, jsonpayload) elapsed time.time() - start return elapsed, len(response.json()[response]) # 测试一个需要规划和工具调用的提示词 test_prompt 你是一个智能助手。用户说告诉我北京和上海今天的天气然后比较一下。 请规划你需要调用的工具假设有天气查询工具并以JSON格式输出你的思考过程和工具调用计划。 models [llama3.2:3b-instruct, qwen2.5:7b-instruct, deepseek-r1:7b] for model in models: try: time_taken, tokens test_model(model, test_prompt) print(fModel: {model:30} Time: {time_taken:.2f}s, Tokens: {tokens}) except Exception as e: print(fModel: {model:30} Error: {e})这个测试能帮你直观感受不同模型的速度和输出质量为项目选型提供依据。4. 智能体框架核心模块实现有了稳定的模型服务我们就可以着手构建智能体框架的核心了。这里我们不会完全照搬某个现有框架而是借鉴agent_by_ollama的思路实现一个简约但功能完整的核心系统。4.1 智能体状态机与对话管理智能体的核心是一个状态机它管理着一次对话或任务执行的生命周期。我们定义一个Agent基类from typing import Dict, List, Any, Optional, Callable from dataclasses import dataclass, field import json dataclass class AgentMessage: 定义智能体交互中的消息单元 role: str # user, assistant, tool content: str name: Optional[str] None # 工具调用时的工具名 class AgentState: 管理智能体的状态主要是对话历史 def __init__(self): self.message_history: List[AgentMessage] [] self.max_history_tokens 8000 # 历史上下文最大长度近似 def add_message(self, message: AgentMessage): self.message_history.append(message) # 简单的历史长度管理如果估计的token数超限移除最早的一些消息 # 实际生产环境应使用准确的tokenizer计算 if len(self.message_history) 20: # 简单按条数裁剪 self.message_history.pop(0) def get_context_for_model(self) - str: 将消息历史格式化为模型可以理解的提示上下文 context_lines [] for msg in self.message_history: if msg.role user: context_lines.append(fHuman: {msg.content}) elif msg.role assistant: context_lines.append(fAssistant: {msg.content}) elif msg.role tool: context_lines.append(fTool ({msg.name}): {msg.content}) return \n.join(context_lines)这个AgentState类负责维护对话的“记忆”。它并没有直接存储原始 token而是用一个列表保存结构化的消息。在需要发送给模型前get_context_for_model方法会将其格式化为一个连续的文本对话。这是一种简化实现高级实现会精确计算 tokens 并做智能裁剪。4.2 工具系统设计与动态注册工具是智能体的“手脚”。一个好的工具系统应该支持动态发现和调用。class Tool: 工具基类所有具体工具都继承于此 def __init__(self, name: str, description: str, func: Callable): self.name name self.description description self.func func # 自动从函数签名生成参数模式简化版实际可用inspect模块 self.parameters {} # 可以存放JSON Schema def execute(self, **kwargs) - str: 执行工具返回结果字符串。处理异常。 try: result self.func(**kwargs) return str(result) except Exception as e: return fError executing tool {self.name}: {str(e)} class ToolRegistry: 工具注册表全局单例管理所有可用工具 _instance None def __new__(cls): if cls._instance is None: cls._instance super().__new__(cls) cls._instance.tools: Dict[str, Tool] {} return cls._instance def register(self, tool: Tool): self.tools[tool.name] tool def get_tool(self, name: str) - Optional[Tool]: return self.tools.get(name) def get_tools_description(self) - str: 生成给模型看的工具描述文本用于提示词 desc [] for name, tool in self.tools.items(): desc.append(f- {name}: {tool.description}) return \n.join(desc) # 工具使用装饰器方便注册 def register_tool(name: str, description: str): def decorator(func): tool Tool(namename, descriptiondescription, funcfunc) ToolRegistry().register(tool) return func return decorator # 示例定义几个常用工具 register_tool( nameget_weather, description获取指定城市的当前天气。参数city (字符串城市名) ) def get_weather(city: str) - str: # 这里应该是真实的API调用例如调用和风天气、OpenWeatherMap等 # 为示例我们返回模拟数据 weather_data { 北京: 晴25°C北风2级, 上海: 多云28°C东南风3级, 深圳: 阵雨30°C南风1级 } return weather_data.get(city, f未找到{city}的天气信息) register_tool( namecalculate, description执行数学计算。参数expression (字符串数学表达式如 3 5 * 2) ) def calculate(expression: str) - str: try: # 警告使用eval有安全风险仅作示例。生产环境应用安全库如ast.literal_eval或专用计算库。 result eval(expression) return str(result) except Exception as e: return f计算错误: {e}工具系统设计的关键点声明式描述每个工具都有清晰的名称和自然语言描述模型依靠这些描述来理解工具的功能。统一接口所有工具都通过execute方法调用输入输出标准化便于框架管理。集中注册ToolRegistry作为中心化的目录方便智能体查询和调用。安全隔离工具函数应被设计为无副作用的、或副作用可控的。像calculate中的eval在真实场景中必须被替换为安全的解析器这是非常重要的安全实践。4.3 与 Ollama API 的稳健通信层这是连接我们框架和“大脑”的桥梁。我们需要一个健壮的客户端来处理与 Ollama 的交互支持同步、异步、流式输出等。import requests import json from typing import Iterator class OllamaClient: def __init__(self, base_url: str http://localhost:11434): self.base_url base_url.rstrip(/) self.session requests.Session() def generate(self, model: str, prompt: str, system: str None, **options) - str: 同步生成文本 url f{self.base_url}/api/generate payload { model: model, prompt: prompt, system: system, stream: False, options: options } # 清理空值 payload {k: v for k, v in payload.items() if v is not None} try: resp self.session.post(url, jsonpayload, timeout60) resp.raise_for_status() return resp.json()[response] except requests.exceptions.RequestException as e: raise ConnectionError(fFailed to call Ollama API: {e}) def generate_stream(self, model: str, prompt: str, system: str None, **options) - Iterator[str]: 流式生成文本用于实时输出体验 url f{self.base_url}/api/generate payload { model: model, prompt: prompt, system: system, stream: True, options: options } payload {k: v for k, v in payload.items() if v is not None} try: resp self.session.post(url, jsonpayload, streamTrue, timeout60) resp.raise_for_status() for line in resp.iter_lines(): if line: try: data json.loads(line.decode(utf-8)) if response in data: yield data[response] except json.JSONDecodeError: continue except requests.exceptions.RequestException as e: yield f[ERROR] API调用失败: {e} def list_models(self) - list: 获取本地可用的模型列表 url f{self.base_url}/api/tags try: resp self.session.get(url, timeout10) resp.raise_for_status() return resp.json().get(models, []) except requests.exceptions.RequestException: return []关键细节与避坑指南超时设置务必设置合理的timeout参数。模型生成时间不定对于同步调用可以设置长一些如60秒对于流式调用需要设置一个较长的读超时。错误处理网络波动、模型未加载、参数错误等都可能导致 API 调用失败。必须用try-except包裹并给出友好的错误信息而不是让整个智能体崩溃。流式输出generate_stream返回一个生成器。这对于构建聊天界面非常重要可以实现打字机效果提升用户体验。在智能体内部循环中我们通常使用同步generate来获取完整响应后再处理。系统提示词System Prompt这是控制模型行为的关键。通过system参数传入的提示词比在普通对话历史中更有效力常用于设定智能体的角色、行为规范和输出格式要求。5. 任务规划与执行引擎的实现这是智能体的“中枢神经系统”它协调模型、工具和状态完成从用户输入到最终输出的闭环。5.1 提示词工程让模型学会规划和调用模型本身并不知道如何调用工具我们需要通过精心设计的提示词来引导它。提示词通常包含以下几个部分系统角色设定告诉模型它现在是一个可以调用工具的智能体。工具描述动态插入当前可用的工具列表和描述。输出格式指令严格要求模型以特定格式如 JSON回复便于程序解析。示例Few-shot提供一两个完整的“用户输入-模型思考-工具调用-最终回答”的例子让模型通过示例学习。def build_agent_prompt(user_input: str, tools_description: str, history_context: str) - str: 构建给模型的完整提示词 system_instruction 你是一个有帮助的AI助手可以调用工具来解决问题。请遵循以下步骤 1. 理解用户请求。 2. 如果需要使用工具请以如下JSON格式输出你的思考和计划 { thought: 你的推理过程分析需要用什么工具以及为什么, action: { name: 工具名称, args: {arg1: value1, arg2: value2} } } 3. 如果不需要工具或工具调用已完成请直接给出最终答案。 记住一次只调用一个工具。工具调用结果会以Tool (工具名): 结果的形式提供给你。 # 如果存在对话历史将其作为上下文 history_part f之前的对话\n{history_context}\n if history_context else full_prompt f{system_instruction} 当前可用的工具 {tools_description} {history_part} Human: {user_input} Assistant: return full_prompt5.2 主循环逻辑解析、调用、响应的闭环现在我们将所有模块串联起来实现智能体的主执行循环。import re import json class SimpleAgent: def __init__(self, model_name: str llama3:8b-instruct): self.model_name model_name self.state AgentState() self.ollama OllamaClient() self.tool_registry ToolRegistry() # 预编译正则表达式用于从模型回复中提取JSON self.json_pattern re.compile(r\{.*?action.*?\}, re.DOTALL) def process_user_input(self, user_input: str) - str: 处理单轮用户输入的核心方法 # 1. 将用户输入加入历史 self.state.add_message(AgentMessage(roleuser, contentuser_input)) # 2. 构建提示词 tools_desc self.tool_registry.get_tools_description() history_ctx self.state.get_context_for_model() prompt build_agent_prompt(user_input, tools_desc, history_ctx) max_attempts 5 # 防止无限循环 for attempt in range(max_attempts): # 3. 调用模型获取回复 raw_response self.ollama.generate( modelself.model_name, promptprompt, options{temperature: 0.2, num_predict: 1024} # 低温度使输出更确定 ) # 4. 尝试解析模型回复看是否是工具调用 tool_call self._parse_tool_call(raw_response) if tool_call: # 5. 执行工具调用 tool_name tool_call[action][name] tool_args tool_call[action][args] tool self.tool_registry.get_tool(tool_name) if tool: # 将模型的“思考”和“工具调用指令”加入历史可选有助于模型理解上下文 self.state.add_message(AgentMessage(roleassistant, contentraw_response)) # 执行工具 tool_result tool.execute(**tool_args) print(f[Tool Call] {tool_name}({tool_args}) - {tool_result[:100]}...) # 日志 # 将工具执行结果加入历史 self.state.add_message(AgentMessage(roletool, contenttool_result, nametool_name)) # 更新上下文准备下一轮模型根据工具结果决定下一步 history_ctx self.state.get_context_for_model() prompt f工具调用结果如上。请根据这个结果继续分析或回答用户最初的问题。如果任务已完成请给出最终答案。\n\nHuman: {user_input}\nAssistant: # 循环继续模型将基于新历史包含工具结果再次生成 else: # 工具不存在将错误信息反馈给模型 error_msg fTool {tool_name} not found. self.state.add_message(AgentMessage(roletool, contenterror_msg, namesystem)) history_ctx self.state.get_context_for_model() prompt f{error_msg} 请检查工具名称是否正确或换一种方式解决问题。\n\nHuman: {user_input}\nAssistant: else: # 6. 模型回复不是工具调用即最终答案 self.state.add_message(AgentMessage(roleassistant, contentraw_response)) return raw_response # 返回最终答案循环结束 # 如果超过最大尝试次数 return 抱歉我在处理您的请求时遇到了一些困难未能完成所有步骤。 def _parse_tool_call(self, response: str) - Optional[Dict]: 从模型回复中尝试解析JSON格式的工具调用指令 # 简单的正则匹配提取JSON块 match self.json_pattern.search(response) if match: try: data json.loads(match.group()) # 验证基本结构 if action in data and name in data[action] and args in data[action]: return data except json.JSONDecodeError: pass return None # 使用示例 if __name__ __main__: agent SimpleAgent(model_nameqwen2.5:7b-instruct) # 模拟对话 queries [ 北京现在的天气怎么样, 那上海呢, 帮我计算一下(15 27) * 3 等于多少 ] for query in queries: print(f\n[User]: {query}) response agent.process_user_input(query) print(f[Agent]: {response})这个SimpleAgent类实现了一个基础的 ReAct (Reasoning and Acting) 风格智能体。它的工作流程是典型的“思考-行动-观察”循环接收输入用户提问。思考与规划模型根据提示词、历史、工具列表决定是直接回答还是调用工具。行动如果决定调用工具框架解析出工具名和参数并执行对应的函数。观察工具执行的结果被反馈给模型作为新的上下文。循环或结束模型根据工具结果决定是继续调用下一个工具还是给出最终答案。实操心得JSON 解析的鲁棒性上述_parse_tool_call方法使用正则表达式匹配 JSON这在模型输出格式比较规整时有效但不够健壮。在实际项目中我推荐两种更稳健的方法使用模型的内置 JSON 模式许多新版 Ollama 模型支持format参数可以强制模型输出 JSON。在调用 API 时设置format: json并提供一个完整的 JSON Schema这样模型输出就是标准 JSON无需复杂解析。使用“后备解析器”如果正则匹配失败可以尝试寻找响应中的json ...标记或者使用更宽松的 JSON 解析库如json5来解析。同时在提示词中反复强调输出格式要求并给出更清晰的示例能大幅提高模型输出格式的合规率。6. 高级特性与性能优化实战一个基础的智能体跑起来后我们会面临更多实际挑战如何让它更可靠、更高效、更强大6.1 处理复杂任务与多步规划上面的简单循环一次只处理一个工具调用。对于“查天气然后比较”这类多步任务模型需要在一次规划中就安排好所有步骤或者具备更强的自主规划能力。我们可以升级提示词和解析逻辑来支持多步规划。def build_advanced_prompt(user_input: str, tools_desc: str, history: str) - str: 支持多步规划的提示词 return f你是一个高级规划助手。请为以下任务创建一个分步计划并可以调用工具。 任务{user_input} 可用工具 {tools_desc} 请输出一个JSON数组每个元素是一个步骤。每个步骤可以是 1. 一个工具调用{{type: tool, name: ..., args: {{...}}}} 2. 一个推理步骤{{type: reasoning, content: ...}} 3. 一个最终回答{{type: answer, content: ...}} 请现在输出完整的计划数组 class AdvancedAgent(SimpleAgent): def process_complex_task(self, user_input: str) - str: prompt build_advanced_prompt(user_input, self.tool_registry.get_tools_description(), ) plan_json_str self.ollama.generate(modelself.model_name, promptprompt, options{temperature: 0.1}) try: plan json.loads(plan_json_str) if not isinstance(plan, list): return 模型未能生成有效的计划。 results [] for step in plan: if step.get(type) tool: tool_name step[name] tool_args step.get(args, {}) tool self.tool_registry.get_tool(tool_name) if tool: result tool.execute(**tool_args) results.append(f步骤【{tool_name}】结果{result}) else: results.append(f步骤错误工具{tool_name}未找到。) elif step.get(type) reasoning: # 记录推理步骤可用于后续解释 pass elif step.get(type) answer: # 收集所有结果合成最终答案 context \n.join(results) final_prompt f基于以下步骤执行结果\n{context}\n\n请生成对用户任务{user_input}的完整、友好的回答。 final_answer self.ollama.generate(modelself.model_name, promptfinal_prompt) return final_answer return 计划执行完毕但未生成最终答案。 except json.JSONDecodeError: return 无法解析模型生成的计划。这种方式的优点是规划一步到位减少了模型调用次数理论上一次规划一次总结。缺点是对模型的规划能力要求高且计划可能因实际情况变化如工具失败而需要调整。更成熟的框架如 LangChain会采用更动态的规划-执行-调整策略。6.2 记忆优化与上下文长度管理随着对话进行历史记录会越来越长最终会超出模型的上下文窗口。我们必须实施有效的记忆管理策略。摘要式记忆当历史过长时不是简单丢弃旧消息而是调用模型对之前的对话进行摘要。def summarize_history(self, long_history: List[AgentMessage]) - str: 使用模型将长对话历史总结成一段摘要 history_text self.state.get_context_for_model() summary_prompt f请将以下对话历史浓缩成一个简洁的摘要保留关键事实、决策和用户偏好。 对话历史 {history_text} 摘要 summary self.ollama.generate(modelself.model_name, promptsummary_prompt, options{num_predict: 256}) return f【对话历史摘要】{summary}在AgentState.add_message中可以加入逻辑当历史消息条数或估计 token 数超过阈值时将最早的一部分消息替换为它们的摘要。向量记忆对于需要长期记忆和知识检索的场景如记住用户喜好、项目细节可以将历史信息存入向量数据库如 Chroma, FAISS。当需要相关信息时通过语义搜索检索出来再注入上下文。这超出了基础智能体的范围但这是构建强大个人助手的关键。6.3 性能监控与调试技巧开发智能体时调试比传统程序更复杂。你需要知道模型“想”了什么为什么做出某个决定。结构化日志不要只用print。使用logging模块为不同级别INFO, DEBUG, WARNING和不同组件AGENT, TOOL, MODEL配置日志。import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s) agent_logger logging.getLogger(agent) agent_logger.info(fUser input: {user_input}) agent_logger.debug(fModel raw response: {raw_response})可视化流程可以考虑将每个循环的“用户输入 - 模型思考 - 工具调用 - 工具结果”记录到一个结构化的文件如 JSONL中方便事后分析和重现问题。超时与重试模型 API 调用和工具调用都可能超时或失败。必须为这些操作添加重试机制和超时控制。from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max10)) def call_model_with_retry(client, model, prompt): return client.generate(modelmodel, promptprompt)7. 常见问题排查与实战心得在实际部署和运行agent_by_ollama这类项目时你会遇到各种各样的问题。下面是我踩过的一些坑和解决方案。7.1 模型相关问题问题1模型输出格式不符合预期无法解析 JSON。原因提示词不够清晰模型能力不足温度temperature参数过高导致输出随机。解决在提示词中提供更精确的示例Few-shot Learning示例的输入输出格式必须严格符合你的要求。使用format: json参数如果模型支持。降低temperature如设为 0.1 或 0使输出更确定。在代码中添加更强大的后处理如果解析失败可以尝试将错误信息和原始响应再次发送给模型要求它纠正格式。问题2模型响应速度慢尤其是第一次调用。原因Ollama 首次加载模型需要时间模型参数过大硬件资源不足。解决确保 Ollama 服务已启动且模型已拉取。可以预先运行ollama run model-name一次来触发加载。换用更小的量化模型如从 7B 的 q8_0 换到 q4_K_M。检查 CPU/GPU 和内存使用情况。对于 CPU 推理确保有足够的内存和较好的单核性能。考虑使用num_ctx参数减少上下文长度这能显著影响速度。问题3模型“胡言乱语”或无法理解工具描述。原因工具描述写得太复杂或太模糊系统提示词角色设定不明确。解决用最简洁、无歧义的语言描述工具。例如“get_weather(city)获取城市的当前天气参数city是城市名称字符串。”在系统提示词中强化角色“你是一个必须严格按照指定JSON格式回复的助手只能使用下面列出的工具。”尝试不同的模型。某些模型在工具调用和指令跟随上表现更好如 DeepSeek-R1, Qwen2.5-Instruct。7.2 框架与工具问题问题4工具执行失败但模型无法从错误中恢复。原因工具返回的错误信息太技术化模型无法理解框架没有将错误有效地反馈给模型。解决工具函数应返回对人类和模型都友好的错误信息。例如不要返回KeyError: city而是返回“错误缺少必要的参数‘city’。请提供城市名。”在智能体主循环中将工具错误信息明确地格式化为模型上下文的一部分例如“工具调用失败错误信息。请检查参数或尝试其他方法。”问题5智能体陷入死循环不断调用同一个工具。原因模型陷入了错误的推理循环工具结果没有提供新的信息。解决在主循环中设置最大迭代次数如我们代码中的max_attempts强制跳出。在提示词中增加约束“注意不要重复调用同一个工具除非有新的理由。”改进工具的设计确保相同的输入能产生确定的输出避免让模型觉得“可能下次结果会不同”。问题6如何增加新的工具解决这是本框架设计优势所在。只需两步用register_tool装饰器定义一个新的工具函数。确保函数有清晰的文档字符串或通过description参数提供清晰的描述。 无需修改核心的Agent或ToolRegistry类实现了开闭原则。7.3 部署与运维问题问题7如何将智能体封装成 API 服务解决使用 FastAPI 或 Flask 快速包装。from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI() agent SimpleAgent() # 全局智能体实例 class QueryRequest(BaseModel): question: str session_id: str None # 用于支持多会话 app.post(/chat) async def chat(request: QueryRequest): try: answer agent.process_user_input(request.question) return {answer: answer, session_id: request.session_id} except Exception as e: raise HTTPException(status_code500, detailstr(e))使用session_id来区分不同用户的对话状态可以为每个会话维护一个独立的AgentState。问题8如何评估智能体的表现解决建立简单的测试集。准备一系列典型问题单步工具调用、多步规划、需要推理的问题然后运行智能体人工或通过规则判断其回答是否正确、工具调用是否合理。记录成功率、平均响应时间等指标。这是迭代优化提示词、工具设计和模型选择的基础。构建一个基于 Ollama 的本地智能体系统是一个不断迭代和调优的过程。从NN-Studio/agent_by_ollama这样的项目出发理解其核心思想然后根据你自己的具体需求——无论是做一个自动化的数据分析助手一个智能的文档处理机器人还是一个个性化的聊天伴侣——去扩展工具集、优化提示词、完善交互逻辑。最关键的是开始动手在真实的问题和反馈中你的智能体会变得越来越聪明、可靠。