查看: 175|回复: 1

Ollama+Python搭建本地AI Agent:工具调用与结构化输出实现

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
AI Agent的核心不是让模型更会聊天,而是让模型根据任务判断是否需要调用外部工具,再基于工具结果生成回答。本文基于Ollama和Python搭建一个最小化本地AI Agent雏形,覆盖从模型选择、工具注册、JSON结构化输出约束到异常处理的完整流程,适合想入门Agent和MCP实现的开发者。

环境准备
需要Python 3.10+、已安装并运行的Ollama,以及一个本地模型(如llama3.2、qwen2.5、gemma3均可)。确认Ollama服务正常:curl http://localhost:11434/api/chat -d '{"model":"llama3.2","messages":[{"role":"user","content":"你好"}],"stream":false}'。返回模型回答则说明环境就绪。

项目结构
创建两个文件:tools.py存放工具函数和注册表,agent.py负责模型调用、意图判断、工具执行和结果汇总。

编写工具函数与注册表
在tools.py中定义三个模拟工具:获取当前时间、计算两数之和、查询本地知识库。
  1. from datetime import datetime
  2. def get_current_time() -> str:
  3.     return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  4. def add_numbers(a: float, b: float) -> str:
  5.     return str(a + b)
  6. def search_local_knowledge(keyword: str) -> str:
  7.     knowledge_base = {
  8.         "ollama": "Ollama 是一个可以在本地运行大语言模型的工具",
  9.         "mcp": "MCP 是 Model Context Protocol,用于连接AI与外部工具",
  10.         "agent": "AI Agent具备任务理解、工具调用和多步骤推理能力"
  11.     }
  12.     keyword = keyword.lower()
  13.     for key, value in knowledge_base.items():
  14.         if key in keyword:
  15.             return value
  16.     return "未找到相关信息"
复制代码
然后建立工具注册表,统一管理工具名称、描述、参数列表和引用。
  1. TOOLS = {
  2.     "get_current_time": {
  3.         "description": "获取当前本地时间",
  4.         "function": get_current_time,
  5.         "args": []
  6.     },
  7.     "add_numbers": {
  8.         "description": "计算两个数字之和",
  9.         "function": add_numbers,
  10.         "args": ["a", "b"]
  11.     },
  12.     "search_local_knowledge": {
  13.         "description": "查询本地知识库",
  14.         "function": search_local_knowledge,
  15.         "args": ["keyword"]
  16.     }
  17. }
复制代码

封装Ollama调用
在agent.py中编写调用Ollama /api/chat接口的函数。
  1. import json, requests
  2. from tools import TOOLS
  3. OLLAMA_URL = "http://localhost:11434/api/chat"
  4. MODEL_NAME = "llama3.2"
  5. def call_ollama(messages):
  6.     payload = {"model": MODEL_NAME, "messages": messages, "stream": False}
  7.     response = requests.post(OLLAMA_URL, json=payload, timeout=120)
  8.     response.raise_for_status()
  9.     return response.json()["message"]["content"]
复制代码

引导模型输出结构化JSON
核心难点是让模型输出可解析的工具调用指令。构造一个强制JSON输出的prompt,明确字段名和格式。
  1. def build_tool_prompt(user_input):
  2.     tool_descriptions = []
  3.     for name, info in TOOLS.items():
  4.         tool_descriptions.append({"name": name, "description": info["description"], "args": info["args"]})
  5.     return f"""
  6. 你是一个本地AI Agent的工具选择器。
  7. 用户问题:{user_input}
  8. 可用工具:{json.dumps(tool_descriptions, ensure_ascii=False, indent=2)}
  9. 判断是否需要调用工具。要求:只能输出JSON,不要Markdown或解释。如果需要工具:{{"need_tool": true, "tool_name": "工具名称", "arguments": {{"参数名": "参数值"}}}}。如果不需要:{{"need_tool": false, "answer": "直接回答"}}
  10. """
复制代码

解析与执行工具
模型返回的文本可能包含多余前缀,容错解析JSON。
  1. def parse_json_safely(text):
  2.     try:
  3.         return json.loads(text)
  4.     except json.JSONDecodeError:
  5.         start = text.find("{")
  6.         end = text.rfind("}") + 1
  7.         if start != -1 and end != -1:
  8.             return json.loads(text[start:end])
  9.         raise ValueError(f"非法JSON:{text}")
复制代码
执行工具时注意参数校验,比如数字可能被模型输出为字符串,在工具函数内部做类型转换。
  1. def execute_tool(tool_name, arguments):
  2.     if tool_name not in TOOLS:
  3.         raise ValueError(f"未知工具:{tool_name}")
  4.     return TOOLS[tool_name]["function"](**arguments)
复制代码

结果汇总与主循环
工具执行结果不应直接返回,而是交给模型重新总结成自然语言。
  1. def summarize_with_tool_result(user_input, tool_name, tool_result):
  2.     messages = [
  3.         {"role": "system", "content": "你是一个严谨的技术助手,请根据工具结果回答用户问题。"},
  4.         {"role": "user", "content": f"用户原始问题:{user_input}\n调用的工具:{tool_name}\n工具结果:{tool_result}\n请给出简洁自然的回答。"}
  5.     ]
  6.     return call_ollama(messages)
复制代码
完整主函数和交互循环:
  1. def run_agent(user_input):
  2.     tool_prompt = build_tool_prompt(user_input)
  3.     messages = [{"role": "system", "content": "你只输出JSON。"}, {"role": "user", "content": tool_prompt}]
  4.     decision = parse_json_safely(call_ollama(messages))
  5.     if not decision.get("need_tool"):
  6.         return decision.get("answer", "无回答")
  7.     tool_result = execute_tool(decision["tool_name"], decision.get("arguments", {}))
  8.     return summarize_with_tool_result(user_input, decision["tool_name"], tool_result)
  9. if __name__ == "__main__":
  10.     while True:
  11.         user_input = input("\n你:").strip()
  12.         if user_input.lower() in ["exit","quit","q"]: break
  13.         try:
  14.             print(f"\nAgent:{run_agent(user_input)}")
  15.         except Exception as e:
  16.             print(f"出错:{e}")
复制代码

运行测试
安装requests后执行python agent.py。测试问题如“现在几点?”、“计算18.5+23.7”、“MCP是什么?”可验证工具调用与结果总结。

常见问题排查
连接不上:检查Ollama是否运行(curl http://localhost:11434/)。模型不存在:ollama pull llama3.2。模型输出非JSON:强化system prompt或增加JSON截取逻辑。参数类型错误:在工具函数内做类型转换,例如def add_numbers(a,b): return str(float(a)+float(b))。

与真正MCP的区别
本文实现的是手动工具注册与调用,而MCP标准协议提供JSON-RPC通信、工具发现、权限控制等规范。理解此雏形后再学习MCP会更容易。

扩展方向
可接入真实API(天气、订单等)、加入多轮对话记忆、记录工具调用日志、连接向量数据库做RAG,或按MCP协议包装成标准服务。

此Demo虽然简单,但覆盖了Agent核心环节:任务理解→工具选择→参数生成→工具执行→结果总结→异常处理。跑通这个最小闭环,才能真正理解AI Agent落地的工程细节。
回复

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Ollama+Python搭建本地AI Agent:工具调用与结构化输出实现

楼主这篇文章太及时了,最近刚好在研究怎么让本地模型具备调用工具的能力。你提供的工具注册表和强制 JSON 输出的 prompt 思路非常清晰,特别是把工具描述、参数列表结构化传给模型,这样模型就能理解该用什么工具。我有个小问题:当模型返回的 JSON 里 `need_tool` 为 `false` 时,你是怎么直接让模型回答的?是直接复用 `call_ollama` 还是另外做一次普通对话?另外,如果工具参数填写错误或者模型输出的 JSON 格式不完整(比如少了一个花括号),你在异常处理上有什么经验?期待后续分享更完整的错误处理逻辑。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-12 11:09 , Processed in 0.026721 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部