手把手搭建可调试AI Agent:OpenAI工具调用核心原理与工程实践
1. 项目概述:为什么一个“简单AI Agent”值得你花两小时亲手搭一遍
我第一次在OpenAI官方文档里看到“tool calling”这个词时,下意识划了过去——不就是函数调用嘛,Python里 func() 敲一下的事。直到上周帮朋友调试一个客服对话系统,客户问“上个月订单总金额是多少”,后端要查数据库、算折扣、再格式化返回,而大模型卡在“我不知道订单数据在哪”这一步,反复兜圈子。那一刻我才真正意识到: 不是模型不够聪明,而是它被关在语言的玻璃房里,看不见、摸不着真实世界的数据和动作。 工具调用(tool calling)不是给API加个装饰,它是给大模型装上手脚,让它能真正“做事”。这篇文章讲的,就是一个最精简、最透明、最可复现的AI Agent骨架——它只有两个工具:乘法计算器和天气查询(虽然是模拟的),但它的结构、错误处理逻辑、消息流转机制,和你未来接入支付网关、CRM系统、甚至控制IoT设备的Agent完全一致。它不依赖任何框架封装,所有代码都在你眼皮底下运行;它不回避真实问题,比如模型胡乱调用工具、参数解析失败、无限循环调用;它甚至把“为什么用 pydantic.BaseModel 而不是直接写dict”这种新手常踩的坑,掰开揉碎讲清楚。如果你刚接触Agent开发,它能让你30分钟内跑通第一个可交互的Agent;如果你已在用LangChain或LlamaIndex,它会帮你看清那些抽象层之下,到底发生了什么。关键词: OpenAI tool calling、AI Agent、函数调用、Pydantic Schema、消息历史管理、工具执行循环 。适合所有想亲手造轮子、不想被黑盒框架牵着鼻子走的开发者。
2. 整体设计思路:从“模型输出JSON”到“系统完成任务”的完整闭环
2.1 核心矛盾:模型不执行,只“建议”——这是设计起点
很多初学者最大的误解,是以为调用 client.chat.completions.create(..., tools=tools) 后,OpenAI服务器会自动帮你执行 multiply(5, 6) 。事实截然相反: 模型只负责“说”,绝不“做”。 它的输出永远是一段JSON字符串,内容类似 {"name": "multiply", "arguments": {"a": 5, "b": 6}} 。这个JSON是模型基于你的提示词(system prompt)和对话历史,经过推理后,“认为”此刻应该调用哪个工具、传什么参数。执行权100%在你手里。这个设计看似麻烦,实则是安全与可控的基石。想象一下,如果模型能自动调用银行转账API,一个prompt注入攻击就能卷走你的钱。所以整个Agent的骨架,本质是一个“决策-执行-反馈”的三步闭环:模型决策(输出tool call JSON)→ 你执行(解析JSON,调用本地函数)→ 你反馈(把执行结果塞回消息流,让模型看见)→ 模型再决策……直到它决定“stop”。这个闭环的每一步,都必须由你显式控制,没有魔法。
2.2 为什么必须用Pydantic定义输入模型?手写JSON Schema的代价
原文中 InputMultiply(BaseModel) 那段代码,新手常觉得多此一举:“我直接写个字典描述参数不行吗?” 行,但代价巨大。我们来对比两种方式:
-
手写Schema(危险且易错):
multiply_tool = { "type": "function", "function": { "name": "multiply", "description": "Multiply two integers and returns the result integer", "parameters": { "type": "object", "properties": { "a": {"type": "integer", "description": "first value"}, "b": {"type": "integer", "description": "second value"} }, "required": ["a", "b"] # 这个字段你敢漏吗? } } }问题来了:
"required"数组必须和properties里的key严格一致,漏一个,模型就可能传空值;"type"写成"int"还是"integer"?OpenAI API只认"integer";如果参数是嵌套对象,比如{"user": {"id": 123, "profile": {"age": 25}}},手写Schema的缩进、引号、逗号,任何一个字符错误都会导致API报400错误,而错误信息往往只说“invalid schema”,你得逐行肉眼排查。 -
Pydantic自动生成功能(安全且省心):
class InputMultiply(BaseModel): a: int = Field(description="first value") b: int = Field(description="second value")model_json_schema()方法会自动生成包含"required"、正确"type"、甚至支持复杂嵌套结构的完整JSON Schema。它还做了类型校验:如果你传a="abc",Pydantic会在生成Schema前就抛出ValidationError,把错误拦截在API调用之前。这就像给你的工具描述加了一道编译器检查。我试过在生产环境里,因为手写Schema少了一个"required"字段,导致模型在用户没提供必填参数时,静默地传了null,最终在数据库写入时报NOT NULL constraint failed,而日志里根本找不到源头。用Pydantic,这种问题在开发阶段就被掐死了。
2.3 消息历史(Messages)是Agent的“记忆”与“上下文”——它必须精确管理
Agent不是单次问答,而是一场多轮对话。模型要理解“它刚才让我查了北京天气,现在又让我把温度乘以2”,就必须看到完整的对话历史。 messages 列表就是这个历史的载体。关键点在于它的 结构和追加时机 :
- 初始
messages包含system(角色设定)和user(用户第一句话)。 - 当模型返回
finish_reason == "tool_calls"时,你必须将 整个response.choices[0].message对象 (它包含了tool_calls字段)追加到messages里。这一步极其重要!它告诉模型:“我收到了你的调用指令,并已记录在案。” - 然后,你执行工具,得到结果
function_response,再将这个结果以role: "tool"、tool_call_id(必须和原tool_call.id一致)、name(工具名)的格式,作为一条新消息追加进去。tool_call_id是关联键,模型靠它知道“这个结果对应我刚才哪条指令”。
如果漏掉第一步(不追加 tool_calls 消息),模型下次看到 messages 里只有原始提问和你的工具结果,会一脸懵:“谁让你查天气的?我怎么不记得?” 如果 tool_call_id 不匹配,模型会直接忽略这条工具结果,因为它无法建立关联。我踩过的最深的坑,就是把 tool_call.id 误写成 tool_call.function.name ,结果模型永远收不到工具返回值,死循环10次后报错退出。 messages 不是日志,它是Agent认知世界的唯一依据,必须像维护数据库事务一样严谨。
2.4 MAX_ITERATIONS 不是可有可无的保险丝——它是防止“AI发疯”的物理开关
MAX_ITERATIONS = 10 这行代码,表面看是防bug,实则是防灾难。设想一个场景:你的天气工具函数 get_current_weather 内部有个bug,总是返回空字符串 "" 。模型收到空响应,无法生成答案,于是再次尝试调用天气工具……如此循环。没有 MAX_ITERATIONS ,它会一直调用下去,直到耗尽你的API配额,或者触发OpenAI的速率限制(rate limit),返回 429 Too Many Requests 。更糟的是,如果工具执行本身有副作用(比如发邮件、扣款),无限循环就是一场事故。 MAX_ITERATIONS 是硬性熔断器,它强制在第10次迭代后停止,并返回当前状态(通常是未完成的中间结果)。我在调试一个电商比价Agent时,就遇到过模型在“获取商品A价格”和“获取商品B价格”之间反复横跳,因为两个工具返回的格式不一致,模型始终无法拼出最终结论。 MAX_ITERATIONS 让我能快速捕获这个现象,而不是等它跑满一小时。它不是一个“优雅降级”的软开关,而是一根实实在在的保险丝,该烧断时必须烧断。
3. 核心细节解析:从工具定义到消息解析的每一处魔鬼
3.1 工具定义的黄金法则:函数、输入模型、描述三者必须严丝合缝
一个可用的工具,由三个部分构成,缺一不可,且必须一一对应:
- 函数本身(
multiply): 执行实际逻辑的Python函数。它必须是纯函数(无副作用,或副作用可控),返回值最好是JSON序列化的对象(如json.dumps({"output": 30})),方便后续解析。 - 输入模型(
InputMultiply): 继承BaseModel的类,用Field(description=...)精确描述每个参数。description字段会被模型读取,用于理解参数含义,所以不能写“数字a”,而要写“第一个参与乘法运算的整数”。 - 工具描述(
multiply_tool): 由generate_tool_description生成的字典,它把函数名、描述、输入模型的Schema三者打包。这个字典最终会通过tools参数传给OpenAI API。
三者脱节的后果很直接:
- 如果函数签名是
def multiply(a: int, b: int),但InputMultiply里定义了c: str,generate_tool_description会报错,因为inspect.signature拿到的参数和BaseModel的字段对不上。 - 如果
InputMultiply里a的description写成“乘数”,而函数文档字符串写“被乘数”,模型可能混淆参数顺序。 - 如果函数返回
30(int),但你的代码期望json.loads(function_response)得到一个字典,就会抛JSONDecodeError。
我坚持一个实践: 写完函数,立刻写 InputMultiply ,再立刻写函数文档字符串,三者同步更新。 在Git提交时,这三行代码必须出现在同一个commit里。这听起来教条,但能避免90%的工具调用失败。
3.2 available_functions 字典:Agent的“工具箱索引”——名字必须零误差
available_functions = {"multiply": multiply_executor, "get_current_weather": get_current_weather_executor} 这行代码,是Agent执行环节的“大脑”。当模型返回 {"name": "multiply", ...} 时,你靠这个字典找到对应的 multiply_executor 函数去调用。这里的名字( "multiply" )必须和 multiply_tool["function"]["name"] 里的名字 完全一致,包括大小写和下划线 。OpenAI API返回的 tool_call.function.name 是字符串,Python字典查找是精确匹配。我曾把工具名写成 "multiply_tool" ,而字典里是 "multiply" ,结果每次调用都报 KeyError ,花了半小时才定位到这个拼写错误。更隐蔽的坑是:如果你的函数名是 getWeather ,而 InputGetweather 的类名是 InputGetweather , generate_tool_description 会用函数名 getWeather ,但你字典里写了 "get_weather" ,那就永远对不上。我的经验是: 工具名统一用小写字母+下划线(snake_case),和函数名保持绝对一致,不要做任何转换。 这样, multiply 函数 → multiply 工具名 → multiply 字典键,形成一条清晰、无歧义的链条。
3.3 get_response_from_openai 函数:隐藏的重试与错误处理战场
原文的 get_response_from_openai 函数过于简化,只做了基础调用。在真实环境中,网络抖动、API临时故障、token超限都是家常便饭。一个健壮的Agent必须内置重试逻辑。我推荐的增强版如下:
import time
import random
from openai import APIConnectionError, RateLimitError, APIStatusError
def get_response_from_openai(tools, messages, client=OpenAI(), max_retries=3):
for attempt in range(max_retries):
try:
response = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=tools,
tool_choice="auto",
timeout=30 # 设置超时,避免挂起
)
return response
except APIConnectionError as e:
print(f"网络连接错误,{2 ** attempt}秒后重试... ({e})")
time.sleep(2 ** attempt + random.uniform(0, 1))
except RateLimitError as e:
print(f"速率限制,等待1分钟... ({e})")
time.sleep(60)
except APIStatusError as e:
if e.status_code == 400:
print(f"请求错误,请检查tools或messages格式: {e}")
raise e
elif e.status_code == 429:
print(f"配额超限,等待1分钟... ({e})")
time.sleep(60)
else:
print(f"API服务端错误: {e}")
raise e
except Exception as e:
print(f"未知错误: {e}")
raise e
raise Exception("重试3次后仍失败")
这个版本加入了:
- 指数退避重试(Exponential Backoff): 第一次失败等1秒,第二次等2秒,第三次等4秒,避免雪崩式重试。
- 随机抖动(Jitter):
random.uniform(0, 1)加一点随机时间,防止大量Agent同时重试,撞上同一波高峰。 - 精准异常分类: 区分网络错误、限流、配额、格式错误,不同错误采取不同策略。
- 超时设置:
timeout=30防止请求无限挂起,拖垮整个Agent。
没有这个重试层,你的Agent在生产环境里会像纸糊的一样脆弱。我见过一个金融分析Agent,因为一次偶然的网络超时,整个批处理流程就中断了,而它本可以安静地重试并继续。
3.4 chat 函数中的消息追加: tool_call_id 是生命线,绝不能错
chat 函数里,工具执行后的消息追加是核心操作:
messages.append({
"tool_call_id": tool_call.id, # 关键!必须和原tool_call.id完全一致
"role": "tool",
"name": function_name,
"content": function_response,
})
tool_call.id 是一个由OpenAI生成的唯一字符串,形如 "call_abc123xyz" 。它不是你自己生成的,也不是函数名,它是OpenAI为这次调用分配的“工单号”。模型靠它把 tool_calls 指令和 tool 响应关联起来。如果这里写错了,比如写成 tool_call.function.name ,或者漏掉了 tool_call.id ,模型就会“失联”,它看到一条 role: "tool" 的消息,但找不到对应的 tool_call ,于是这条消息被忽略, messages 历史里就缺失了关键一环,后续所有推理都基于错误的前提。我建议在调试时,把 tool_call.id 和追加的 tool_call_id 都打印出来,肉眼确认它们是否一模一样。这是一个“一错全错”的关键点,值得你花10秒去验证。
4. 实操过程:从零开始搭建,每一步都附带现场记录与参数说明
4.1 环境准备与依赖安装:一个干净的虚拟环境是成功的开始
不要跳过这一步。我见过太多人因为全局Python环境混乱, pydantic 版本冲突(v1和v2不兼容),或者 openai 包版本太老不支持 tool_choice 参数,导致卡在第一步。以下是经过我实测的、最稳妥的步骤:
-
创建独立虚拟环境(强烈推荐):
# 创建名为ai_agent_env的虚拟环境 python -m venv ai_agent_env # 激活它(Windows) ai_agent_env\Scripts\activate.bat # 激活它(macOS/Linux) source ai_agent_env/bin/activate -
安装指定版本的依赖: 使用
pip install时,明确指定版本号,避免自动升级引入不兼容变更。# 安装OpenAI官方SDK(确保>=1.0.0) pip install openai==1.35.11 # 安装Pydantic(v2.x,v1.x语法不同) pip install pydantic==2.7.1 # 安装python-dotenv(读取.env文件) pip install python-dotenv==1.0.0 # 验证安装 pip list | grep -E "(openai|pydantic|dotenv)"提示:
openai==1.35.11是我当前测试稳定的版本。如果你用更新的版本,API调用方式(如client.chat.completions.create)基本不变,但某些高级参数可能有调整。pydantic==2.7.1是v2系列的稳定版,其model_json_schema()方法是生成工具描述的核心。 -
创建
.env文件并配置API Key: 在项目根目录下创建一个名为.env的纯文本文件,内容只有一行:OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx注意:
OPENAI_API_KEY必须是你的 Secret Key ,不是Organization ID或Project ID。它在OpenAI官网的 API Keys页面 生成。生成后, 立即复制并保存好 ,页面刷新后就再也看不到了。把它放在.env里,比硬编码在Python文件里安全得多,也方便在不同环境(开发/测试/生产)间切换。
4.2 编写 utils.py : generate_tool_description 函数的完整实现与原理
创建 utils.py 文件,将原文的辅助函数补全。这里的关键是理解 inspect.signature(func) 和 input_model.model_json_schema() 如何协同工作。
import json
import inspect
from pydantic import BaseModel
def generate_tool_description(func, input_model: BaseModel):
"""
生成OpenAI工具调用所需的JSON描述。
原理:利用Python反射(inspect)获取函数签名,获取函数文档,
利用Pydantic的model_json_schema()自动推导输入参数的JSON Schema。
"""
# 1. 获取函数签名,用于提取参数名
signature = inspect.signature(func)
# 2. 获取函数文档字符串,作为工具描述
func_doc = func.__doc__ or f"Function to {func.__name__}"
# 3. 调用Pydantic方法,生成完整的JSON Schema
# model_json_schema()返回一个dict,其中"properties"是参数定义
schema = input_model.model_json_schema()
# 4. 提取"properties"部分,这是OpenAI需要的参数定义
properties = schema.get("properties", {})
# 5. 构建最终的工具描述字典
# 注意:OpenAI要求"parameters"下的"type"必须是"object"
tool_description = {
"type": "function",
"function": {
"name": func.__name__,
"description": func_doc,
"parameters": {
"type": "object",
"properties": properties,
# 6. 关键!Pydantic的schema里包含了"required"字段,
# 我们必须把它提上来,否则OpenAI不知道哪些是必填项
"required": schema.get("required", [])
}
}
}
return tool_description
实操心得:
schema.get("required", [])这一行是原文缺失的,但至关重要。如果你不手动加上"required",OpenAI会认为所有参数都是可选的,模型可能只传一个参数就调用函数,导致你的函数因缺少参数而崩溃。Pydantic的model_json_schema()方法会自动分析Field(default=...)和Field(default_factory=...)来确定哪些是必填项,schema.get("required")就是把它准确地提取出来。
4.3 定义工具:乘法与天气——从模拟到真实的平滑演进路径
我们按原文定义两个工具,但我会展示如何从“模拟”走向“真实”的扩展思路。
乘法工具( multiply ):
# tools.py
from pydantic import BaseModel, Field
import json
class InputMultiply(BaseModel):
a: int = Field(description="第一个参与乘法运算的整数")
b: int = Field(description="第二个参与乘法运算的整数")
def multiply(a: int, b: int) -> str:
"""将两个整数相乘,并返回JSON格式的结果。"""
result = a * b
# 返回JSON字符串,便于后续json.loads解析
return json.dumps({"output": result, "operation": "multiply", "inputs": {"a": a, "b": b}})
def multiply_executor(args):
"""执行乘法工具的包装器。"""
# args是json.loads后的dict,直接解包
return multiply(args["a"], args["b"])
实操心得:
multiply函数返回json.dumps(...),而不是原始int。这是因为chat函数里function_response会被当作字符串处理,json.loads(function_response)需要一个合法的JSON字符串。如果直接返回30,json.loads("30")会得到30(int),但json.loads("{'output': 30}")会报错,因为单引号不是JSON标准。所以, 所有工具函数的返回值,都应该是json.dumps({...})生成的、双引号包裹的、符合JSON标准的字符串。
天气工具( get_current_weather ):
# tools.py (续)
import requests
from typing import Optional
class InputGetweather(BaseModel):
location: str = Field(description="要查询天气的城市名称,例如'Beijing'或'Shanghai'")
def get_current_weather(location: str) -> str:
"""
获取指定城市的当前天气。
注意:这是一个模拟函数。在生产环境中,应替换为真实的API调用。
"""
# 模拟真实API的响应结构
mock_data = {
"Beijing": {"temperature": "28 degree", "type": "cloudy", "humidity": "65%"},
"Shanghai": {"temperature": "32 degree", "type": "sunny", "humidity": "78%"},
"Guangzhou": {"temperature": "35 degree", "type": "rainy", "humidity": "92%"}
}
# 返回模拟数据,或调用真实API
weather = mock_data.get(location.capitalize(), {"temperature": "unknown", "type": "unknown"})
return json.dumps({"location": location, "weather": weather})
def get_current_weather_executor(args):
"""执行天气工具的包装器。"""
return get_current_weather(args["location"])
# 可选:替换为真实API(例如OpenWeatherMap)
# def get_current_weather(location: str) -> str:
# API_KEY = "your_openweather_api_key"
# url = f"http://api.openweathermap.org/data/2.5/weather?q={location}&appid={API_KEY}&units=metric"
# try:
# response = requests.get(url, timeout=10)
# response.raise_for_status()
# data = response.json()
# temp = data['main']['temp']
# desc = data['weather'][0]['description']
# return json.dumps({"location": location, "temperature": f"{temp}°C", "type": desc})
# except Exception as e:
# return json.dumps({"error": f"Failed to fetch weather: {str(e)}"})
实操心得:模拟数据
mock_data的键名用了location.capitalize(),这样用户输入beijing或BEIJING都能匹配到Beijing。这是一个很小但很实用的用户体验优化。当你准备接入真实API时,注释掉模拟部分,取消注释真实API部分即可,其他代码(InputGetweather,get_current_weather_executor,available_functions)完全不用改。这就是良好设计的威力: 接口(interface)稳定,实现(implementation)可替换。
4.4 主程序 main.py :整合所有模块,启动交互式Agent
现在,把所有碎片组装起来。 main.py 是整个Agent的入口。
# main.py
import os
import json
from pydantic import BaseModel, Field
from dotenv import load_dotenv
from openai import OpenAI
from utils import generate_tool_description
from tools import multiply_executor, get_current_weather_executor, InputMultiply, InputGetweather
# 1. 加载环境变量
load_dotenv()
assert os.environ.get("OPENAI_API_KEY"), "请在.env文件中设置OPENAI_API_KEY"
# 2. 初始化客户端和常量
client = OpenAI()
MODEL = "gpt-3.5-turbo"
SYSTEM = "你是一个乐于助人的AI助手,能够使用乘法计算器和天气查询工具来回答用户的问题。"
MAX_ITERATIONS = 10
# 3. 定义工具描述
multiply_tool = generate_tool_description(multiply_executor, InputMultiply)
get_weather_tool = generate_tool_description(get_current_weather_executor, InputGetweather)
# 4. 构建工具映射字典
available_functions = {
"multiply": multiply_executor,
"get_current_weather": get_current_weather_executor
}
tools = [multiply_tool, get_weather_tool]
# 5. 核心API调用函数(含重试)
def get_response_from_openai(tools, messages, client=client, max_retries=3):
# 此处插入上一节的增强版get_response_from_openai函数
pass
# 6. 核心聊天循环
def chat(user_input):
messages = [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": user_input}
]
for i in range(MAX_ITERATIONS):
print(f"\n--- 第{i+1}轮迭代 ---")
try:
response = get_response_from_openai(tools=tools, messages=messages)
except Exception as e:
print(f"API调用失败: {e}")
return
message = response.choices[0].message
finish_reason = response.choices[0].finish_reason
print(f"模型结束原因: {finish_reason}")
if finish_reason == "stop":
print(f"\n✅ 最终答案: {message.content}")
break
elif finish_reason == "tool_calls":
print(f"🛠️ 模型建议调用工具:")
for tool_call in message.tool_calls:
print(f" - 工具名: {tool_call.function.name}")
print(f" - 参数: {tool_call.function.arguments}")
# 将模型的tool_calls消息追加到历史
messages.append(message)
# 逐一执行工具调用
for tool_call in message.tool_calls:
function_name = tool_call.function.name
function_to_call = available_functions.get(function_name)
if not function_to_call:
print(f"❌ 错误: 未找到工具 '{function_name}' 的执行器")
continue
try:
# 解析参数
function_args = json.loads(tool_call.function.arguments)
print(f" → 执行 {function_name}({function_args})")
function_response = function_to_call(function_args)
print(f" ← 工具返回: {function_response}")
# 将工具执行结果追加为tool消息
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": function_response,
})
except json.JSONDecodeError as e:
print(f"❌ 参数解析错误: {e}")
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": json.dumps({"error": "Invalid JSON arguments"}),
})
except Exception as e:
print(f"❌ 工具执行错误: {e}")
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": json.dumps({"error": str(e)}),
})
else:
print(f"⚠️ 未知结束原因: {finish_reason}")
break
# 7. 用户交互主循环
def main():
print("🚀 简易AI Agent已启动!")
print(" 输入 'stop' 退出对话。")
print(" 尝试问:'5乘以6等于多少?'")
print(" 或者:'上海今天的天气怎么样?'")
print("-" * 50)
while True:
try:
user_input = input("\n💬 请输入您的问题: ").strip()
if not user_input:
continue
if user_input.lower() == "stop":
print("👋 再见!")
break
chat(user_input)
except KeyboardInterrupt:
print("\n\n👋 强制退出。再见!")
break
except EOFError:
print("\n\n👋 输入结束。再见!")
break
if __name__ == "__main__":
main()
实操心得:我在
chat函数里加入了详细的✅、🛠️、❌等符号(这些只是视觉标记,不影响功能),目的是让你在终端里能清晰地看到Agent的“思考”和“行动”轨迹。当你第一次运行时,你会看到它如何一步步解析、调用、反馈。这种透明度是调试的基石。另外,try...except块包围了json.loads和工具执行,捕获了最常见的两类错误:参数JSON格式错误和工具函数内部异常,并将错误信息以JSON格式返回给模型,让模型知道“这一步失败了”,而不是让它在错误数据上继续推理。
5. 常见问题与排查技巧实录:那些让你抓狂的Bug,我都替你踩过了
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查与解决方法 |
|---|---|---|
KeyError: 'multiply' |
available_functions 字典里没有 "multiply" 这个键,或者键名拼写错误(大小写、下划线)。 |
1. 检查 available_functions 定义,确认键名和 multiply_tool["function"]["name"] 完全一致。 2. 在 chat 函数里, print(available_functions.keys()) ,确认字典里确实有这个键。 |
JSONDecodeError: Expecting property name enclosed in double quotes |
工具函数返回的不是合法JSON字符串(例如用了单引号,或返回了原始 int / dict )。 |
1. 确保所有工具函数都用 json.dumps({...}) 返回。 2. 在 multiply_executor 里, print(repr(function_response)) ,检查输出是否是 '{"output": 30}' (字符串)而不是 {"output": 30} (字典)。 |
模型反复调用同一个工具,永不 stop |
工具返回的结果格式不符合模型预期,模型无法从中提取答案;或 SYSTEM 提示词不够强,没告诉模型“得到工具结果后要总结”。 |
1. 检查工具返回的JSON结构,确保包含模型能理解的字段(如 "temperature" 、 "output" )。 2. 强化 SYSTEM 提示词,例如加上:“你必须在获得工具返回结果后,用自己的话总结答案,并给出最终回复。” |
400 Bad Request 错误,提示 tools 格式无效 |
tools 列表里的某个字典, "parameters" 下缺少 "type": "object" ,或 "required" 字段缺失/格式错误。 |
1. 打印 multiply_tool ,检查其结构是否和OpenAI文档要求一致。 2. 确保 generate_tool_description 函数里包含了 "required": schema.get("required", []) 。 |
429 Rate Limit Error |
在短时间内发送了过多请求,超过了OpenAI的免费额度或你的账户配额。 | 1. 检查OpenAI平台的Usage Dashboard,确认配额。 2. 在 get_response_from_openai 里加入 RateLimitError 的重试逻辑(见4.3节)。 3. 降低 MAX_ITERATIONS ,或增加 time.sleep(1) 在循环内。 |
5.2 我踩过的最深的三个坑与独家避坑技巧
坑一: tool_call.id 的“幽灵”副本 现象:工具调用成功,结果也返回了,但模型下一轮还是说“请调用天气工具”,仿佛没看到结果。 排查:我打印了所有 messages ,发现 tool 消息的 tool_call_id 和 tool_calls 消息里的 id 不一致。后来发现, tool_call 对象在循环中被多次引用,我误用了 tool_call.id 的副本,而那个副本在某次操作后被修改了。
独家技巧: 永远直接使用
tool_call.id,不要赋值给中间变量再用。如果必须赋值,用tool_call_id = tool_call.id,然后立刻用。或者,最保险的做法是:messages.append({"tool_call_id": tool_call.id, ...}),把tool_call.id的读取和使用放在同一行,杜绝中间变量污染。
坑二: system 消息的“隐形诅咒” 现象:Agent在 multiply 工具返回 {"output": 30} 后,回答“结果是30”,但紧接着又说“我还可以帮你做别的事”。用户感觉很啰嗦。 原因: SYSTEM 提示词里写了“你是一个乐于助人的AI助手”,模型过度解读了“乐于助人”,在完成任务后还想主动提供额外帮助。
独家技巧:
SYSTEM提示词要“冷酷无情”。改成:“你是一个AI助手,你的唯一任务是准确、简洁地回答用户的问题。你只能使用提供的工具。得到工具结果后,立即给出最终答案,不要添加任何额外解释、问候或主动提供帮助。” 这句话我反复测试过,能显著提升回答的精准度和简洁度。
坑三: MAX_ITERATIONS 的“假死”陷阱 现象:Agent在第10次迭代后退出,
更多推荐
所有评论(0)