这里写目录标题
条件
需要能支持tool calling的模型,比如我用的qwen3.5
实现原理
- 发请求:把 messages 发给 AI。
- 看返回:
- 如果返回里有 tool_calls 说明 AI 需要帮忙,代码去执行真实函数,把结果追加到 messages,继续下一次循环。
- 如果返回里只有 content(没有 tool_calls)说明 AI 已经拿到了所有信息并整理好了答案,退出循环,打印结果。
代码
python
import requests
import json
# ==========================================
# 1. 预制"真家伙":真实的工具函数
# ==========================================
def get_weather(city: str) -> str:
"""
这是一个模拟查询天气的函数。
在真实场景中,这里会是调用第三方天气API的代码。
"""
# 实际开发中这里可以替换为真实的 HTTP 请求
return f"{city}现在的天气是晴天,气温 25℃。"
# 工具注册表:把函数名和函数本身关联起来,方便后面通过名字调用
TOOL_FUNCS = {"get_weather": get_weather}
# ==========================================
# 2. 预制"说明书":Ollama 格式的工具 Schema
# ==========================================
# 这是给 AI 看的"工具使用手册",告诉它有哪些工具可以用,以及怎么用。
OLLAMA_TOOLS = [
{
"type": "function", # 工具类型:函数
"function": {
"name": "get_weather", # 函数名,必须和 TOOL_FUNCS 里的键一致
"description": "获取指定城市的当前天气情况", # 函数功能描述,AI 会根据这个决定何时调用
"parameters": { # 函数的参数定义,遵循 JSON Schema 规范
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "需要查询天气的城市名称"
}
},
"required": ["city"] # 指明哪些参数是必须的
}
}
}
]
# ==========================================
# 3. 核心:Agent 循环逻辑
# ==========================================
def run_ollama_agent(user_query: str):
# 初始化对话上下文(messages)
# 这是 Agent 的"记忆",它会带着这份记忆和 AI 进行多轮对话
messages = [
{"role": "system", "content": "你是一个有用的助手,需要时使用工具获取信息。"},
{"role": "user", "content": user_query}
]
# Ollama API 地址
url = "http://xxxxx/api/chat"
# 开启 Agent Loop(防止死循环,最多循环10次)
for step in range(10):
print(f"\n--- 第 {step + 1} 步:调用 Ollama 模型 ---")
# 构建发送给 Ollama 的请求体
payload = {
"model": "qwen3:8b", # 指定使用的模型
"messages": messages, # 把完整的对话历史发给模型,让它基于上下文思考
"keep_alive": "1h",
"tools": OLLAMA_TOOLS, # 把"工具说明书"也发给模型
"stream": False # 设置为 False,让 API 一次性返回完整结果(但 Ollama 底层仍是流式)
}
# 发送请求给 Ollama
try:
response = requests.post(url, json=payload, timeout=(60, 1800))
response.raise_for_status() # 检查 HTTP 请求是否成功
# --- 开始处理 Ollama 的流式响应 ---
# 即使 stream=False,Ollama 的 /api/chat 接口返回的仍是一个 JSON 流
# 我们需要逐行读取并拼接,直到收到最后一块数据
full_content = ""
assistant_msg = {"role": "assistant", "content": ""}
for line in response.iter_lines():
if line:
try:
# 将每一行字节流解码为 JSON 对象
json_line = json.loads(line.decode('utf-8'))
# 拼接文本内容(content 字段在每个数据块里都有)
if "message" in json_line and "content" in json_line["message"]:
full_content += json_line["message"]["content"]
# 检查是否是最后一个数据块 (done=True)
if json_line.get("done"):
assistant_msg["content"] = full_content # 保存最终拼接的完整文本
# 关键逻辑:只在最后一个数据块中检查是否有工具调用请求
# tool_calls 字段只会出现在流式响应的最后一个 JSON 对象中
if "message" in json_line and "tool_calls" in json_line["message"]:
assistant_msg["tool_calls"] = json_line["message"]["tool_calls"]
break # 收到最后一块数据,跳出循环
except json.JSONDecodeError:
continue # 跳过无法解析的行
# 将模型的完整回复(可能包含 tool_calls)追加到对话历史中
messages.append(assistant_msg)
except requests.exceptions.ConnectionError:
print("❌ 连接失败,请检查网络或Ollama服务")
break
except requests.exceptions.Timeout:
print("❌ 请求超时")
break
except Exception as e:
print(f"❌ 发生未知错误: {e}")
break
# --- Agent 决策逻辑 ---
# 判定1:如果回复中没有 tool_calls,说明 AI 认为任务已完成,可以直接回答用户
if "tool_calls" not in assistant_msg:
print("✅ 模型完成思考,输出最终回复。")
return assistant_msg["content"]
# 判定2:如果回复中有 tool_calls,说明 AI 需要调用工具来获取信息
for tool_call in assistant_msg["tool_calls"]:
func_name = tool_call["function"]["name"] # 获取要调用的函数名
func_args = tool_call["function"]["arguments"] # 获取函数参数
print(f"🔧 执行工具: {func_name},参数: {func_args}")
# 在工具注册表中找到对应的真实函数并执行
result = TOOL_FUNCS[func_name](**func_args)
print(f"📦 工具返回结果: {result}")
# 将工具的执行结果以 "tool" 角色追加到对话历史中
# 这样,在下一次循环时,AI 就能看到工具返回的数据了
messages.append({"role": "tool", "content": result})
# ==========================================
# 4. 启动 Agent
# ==========================================
if __name__ == "__main__":
# 启动 Agent 并传入用户的问题
final_answer = run_ollama_agent("南京和成都的天气怎么样?")
print("\n最终答案:", final_answer)
特别说明
python
for line in response.iter_lines():
if line:
try:
# 将每一行字节流解码为 JSON 对象
json_line = json.loads(line.decode('utf-8'))
# 拼接文本内容(content 字段在每个数据块里都有)
if "message" in json_line and "content" in json_line["message"]:
full_content += json_line["message"]["content"]
# 检查是否是最后一个数据块 (done=True)
if json_line.get("done"):
assistant_msg["content"] = full_content # 保存最终拼接的完整文本
# 关键逻辑:只在最后一个数据块中检查是否有工具调用请求
# tool_calls 字段只会出现在流式响应的最后一个 JSON 对象中
if "message" in json_line and "tool_calls" in json_line["message"]:
assistant_msg["tool_calls"] = json_line["message"]["tool_calls"]
break # 收到最后一块数据,跳出循环
except json.JSONDecodeError:
continue # 跳过无法解析的行中,详细解释下这个代码 # 拼接文本内容(content 字段在每个数据块里都有)
if "message" in json_line and "content" in json_line["message"]:
full_content += json_line["message"]["content"]
这段代码的核心作用是:在流式(Streaming)响应中,像拼图一样,把 AI 吐出来的一小块一小块的文本,拼凑成一句完整的话。
虽然在请求头里设置了 stream: False,但 Ollama 的底层 API 实际上返回的仍然是一个换行符分隔的 JSON 流(JSON Lines)。这意味着 AI 不会一次性把一大段话发给你,而是像打字机一样,一个字或一个词地往外蹦,每蹦出一个片段,就包装成一个 JSON 发给你。
我们来逐行拆解这段代码:
1. 安全地"拆快递"(防御性编程)
python
if "message" in json_line and "content" in json_line["message"]:
因为 AI 返回的每一个 JSON 数据块,结构可能不一样。有些数据块里只有进度信息,没有 message 字段;有些有 message,但可能只有 thinking(思考过程)而没有 content(最终回复)。
2. 核心动作:拼接文本
python
full_content += json_line["message"]["content"]
- 假设 AI 要回复"你好",它可能会分三次发给你:
* 第 1 个数据块:{"message": {"content": "你"}}
* 第 2 个数据块:{"message": {"content": "好"}}
* 第 3 个数据块:{"message": {"content": "!"}} - 作用: 这行代码就是把这三个片段依次累加到
full_content变量里。经过三次循环,full_content就会变成完整的"你好!"。
这段代码就是一个**"流式文本拼接器"**。它一边接收 AI 吐出的碎片,一边把它们拼成完整的句子,同时还能保证在遇到残缺数据时不崩溃。等最后拼完了,再把完整的 full_content 交给后面的逻辑去处理。