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 参数,导致卡在第一步。以下是经过我实测的、最稳妥的步骤:

  1. 创建独立虚拟环境(强烈推荐):

    # 创建名为ai_agent_env的虚拟环境
    python -m venv ai_agent_env
    # 激活它(Windows)
    ai_agent_env\Scripts\activate.bat
    # 激活它(macOS/Linux)
    source ai_agent_env/bin/activate
    
  2. 安装指定版本的依赖: 使用 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() 方法是生成工具描述的核心。

  3. 创建 .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 函数里加入了详细的 print 语句,每一行都标注了 🛠️ 等符号(这些只是视觉标记,不影响功能),目的是让你在终端里能清晰地看到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次迭代后退出,

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐