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中定义三个模拟工具:获取当前时间、计算两数之和、查询本地知识库。- from datetime import datetime
- def get_current_time() -> str:
- return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- def add_numbers(a: float, b: float) -> str:
- return str(a + b)
- def search_local_knowledge(keyword: str) -> str:
- knowledge_base = {
- "ollama": "Ollama 是一个可以在本地运行大语言模型的工具",
- "mcp": "MCP 是 Model Context Protocol,用于连接AI与外部工具",
- "agent": "AI Agent具备任务理解、工具调用和多步骤推理能力"
- }
- keyword = keyword.lower()
- for key, value in knowledge_base.items():
- if key in keyword:
- return value
- return "未找到相关信息"
复制代码 然后建立工具注册表,统一管理工具名称、描述、参数列表和引用。- TOOLS = {
- "get_current_time": {
- "description": "获取当前本地时间",
- "function": get_current_time,
- "args": []
- },
- "add_numbers": {
- "description": "计算两个数字之和",
- "function": add_numbers,
- "args": ["a", "b"]
- },
- "search_local_knowledge": {
- "description": "查询本地知识库",
- "function": search_local_knowledge,
- "args": ["keyword"]
- }
- }
复制代码
封装Ollama调用
在agent.py中编写调用Ollama /api/chat接口的函数。- import json, requests
- from tools import TOOLS
- OLLAMA_URL = "http://localhost:11434/api/chat"
- MODEL_NAME = "llama3.2"
- def call_ollama(messages):
- payload = {"model": MODEL_NAME, "messages": messages, "stream": False}
- response = requests.post(OLLAMA_URL, json=payload, timeout=120)
- response.raise_for_status()
- return response.json()["message"]["content"]
复制代码
引导模型输出结构化JSON
核心难点是让模型输出可解析的工具调用指令。构造一个强制JSON输出的prompt,明确字段名和格式。- def build_tool_prompt(user_input):
- tool_descriptions = []
- for name, info in TOOLS.items():
- tool_descriptions.append({"name": name, "description": info["description"], "args": info["args"]})
- return f"""
- 你是一个本地AI Agent的工具选择器。
- 用户问题:{user_input}
- 可用工具:{json.dumps(tool_descriptions, ensure_ascii=False, indent=2)}
- 判断是否需要调用工具。要求:只能输出JSON,不要Markdown或解释。如果需要工具:{{"need_tool": true, "tool_name": "工具名称", "arguments": {{"参数名": "参数值"}}}}。如果不需要:{{"need_tool": false, "answer": "直接回答"}}
- """
复制代码
解析与执行工具
模型返回的文本可能包含多余前缀,容错解析JSON。- def parse_json_safely(text):
- try:
- return json.loads(text)
- except json.JSONDecodeError:
- start = text.find("{")
- end = text.rfind("}") + 1
- if start != -1 and end != -1:
- return json.loads(text[start:end])
- raise ValueError(f"非法JSON:{text}")
复制代码 执行工具时注意参数校验,比如数字可能被模型输出为字符串,在工具函数内部做类型转换。- def execute_tool(tool_name, arguments):
- if tool_name not in TOOLS:
- raise ValueError(f"未知工具:{tool_name}")
- return TOOLS[tool_name]["function"](**arguments)
复制代码
结果汇总与主循环
工具执行结果不应直接返回,而是交给模型重新总结成自然语言。- def summarize_with_tool_result(user_input, tool_name, tool_result):
- messages = [
- {"role": "system", "content": "你是一个严谨的技术助手,请根据工具结果回答用户问题。"},
- {"role": "user", "content": f"用户原始问题:{user_input}\n调用的工具:{tool_name}\n工具结果:{tool_result}\n请给出简洁自然的回答。"}
- ]
- return call_ollama(messages)
复制代码 完整主函数和交互循环:- def run_agent(user_input):
- tool_prompt = build_tool_prompt(user_input)
- messages = [{"role": "system", "content": "你只输出JSON。"}, {"role": "user", "content": tool_prompt}]
- decision = parse_json_safely(call_ollama(messages))
- if not decision.get("need_tool"):
- return decision.get("answer", "无回答")
- tool_result = execute_tool(decision["tool_name"], decision.get("arguments", {}))
- return summarize_with_tool_result(user_input, decision["tool_name"], tool_result)
- if __name__ == "__main__":
- while True:
- user_input = input("\n你:").strip()
- if user_input.lower() in ["exit","quit","q"]: break
- try:
- print(f"\nAgent:{run_agent(user_input)}")
- except Exception as e:
- 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落地的工程细节。 |