【手搓 Agent 第1关】建立最小 Agent(中):工具调用

普通大模型对话只能依托固有知识应答,无法适配真实场景的复杂需求,而工具调用是区分普通对话模型与智能 Agent 的核心关键。承接上篇 LLM 基础能力,本篇聚焦 Agent「手脚能力」搭建,手把手讲解自定义工具函数、标准化工具描述文档、解析模型工具调用指令、执行本地任务并回传结果的完整流程,让模型突破固有能力限制,自主对接外部场景、完成实操任务。

一、安装手脚

完成目标:定义工具函数、解析 tool call/function call。

  • 学习指南:

    1. 写个本地函数: 比如写一个极简的计算器函数 def add(a, b),或者一个假天气查询 def get_weather(location)
    2. 撰写“工具说明书”(Schema): LLM 看不到你的本地代码,它只能看懂 JSON 格式的说明书。去阅读官方文档中的 Function Calling 部分,学习如何用 JSON Schema 向 LLM 描述你的函数名、描述(description)以及参数类型。
    3. 捕获“动手的意图”: 把你的 Schema 传给大模型。当模型觉得需要调用工具时,它的返回值会发生变化——它不再返回普通的文本回答,而是返回一个 tool_calls 对象。你需要写代码来判断:这次返回的是普通聊天,还是一个工具调用请求?
  • 验收标准: 当你问“北京天气如何”,模型不直接回答,而是返回一个结构化的指令告诉你:“我想调用 get_weather 函数,参数是 location: 北京”。

1. 定义工具

全局列表增加一个工具描述

位置:写在 chat_history后,generate_response

# 工具描述 Schema
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询指定城市的当日天气",
            "parameters": {
                "type": "object",
                "required": ["location"],
                "properties": {
                    "location": {"type": "string", "description": "城市名称,如北京、合肥"}
                }
            }
        }
    },
]

定义工具函数

def get_weather(location: str) -> str:
    """
    通过调用 wttr.in API 查询真实的天气信息。
    """
    # API端点,请求JSON格式的数据
    url = f"https://wttr.in/{location}?format=j1"

    try:
        # 请求天气数据
        response = requests.get(url)
        # 检查请求是否成功(为200)
        response.raise_for_status()
        # 解析JSON响应
        data = response.json()
        # 提取当前天气状况
        current_condition = data['current_condition'][0]
        weather_desc = current_condition['weatherDesc'][0]['value']
        temp_c = current_condition['temp_C']
        
        # 格式化成自然语言返回
        return f"{location}当前天气:{weather_desc},温度:{temp_c}°C"
    
    except Exception as e:
        print(f"Error fetching weather data for {location}: {e}")
        return "抱歉,生成响应时发生错误。"

2. 修改 AI 生成回复函数

第一轮请求大模型(判断是否需要工具)

        chat_history.append({"role": "user", "content": prompt})
        response = client.chat.completions.create(
            model=Model_ID,
            # response_format={"type": "json_object"},
            messages=chat_history,
            tools=tools,
            tool_choice="auto"
        )
        msg = response.choices[0].message

变量解释

  • tools:全局数组,写给 AI 看的函数说明书,AI 不知道你本地代码,只能读这份 JSON 规则
  • msg:OpenAI 内置对象,二选一:
    1. 无工具需求:msg.content 存文字,msg.tool_calls 为空
    2. 需调用工具:msg.tool_calls 存调用指令,msg.content 是空

判断 AI 是否发起工具调用 if msg.tool_calls

        if msg.tool_calls:
            print("AI 调用了工具:", msg.tool_calls[0].function.name)

            # 把模型的工具调用指令存入对话历史
            chat_history.append(msg)

            # 定义可用工具映射
            available_tools = {
                    "get_weather": get_weather
                }

关键说明

  • chat_history.append(msg) 不能省略!OpenAI 规范:后续二次请求 AI 时,AI 需要看到自己刚刚下发了工具调用指令,上下文才完整,否则会逻辑错乱。
  • available_tools是工具映射字典:函数名字符串 → 本地真实函数,作用为让AI只传函数名文本"get_weather",通过字典找到def get_weather()执行

循环处理所有要调用的工具(支持一次性调用多个函数)

            # 遍历所有要调用的工具
            for tool in msg.tool_calls:
                tool_id = tool.id 
                func_name = tool.function.name
                func_args = json.loads(tool.function.arguments)
                
                # 打印调用详情,可视化
                print(f"调用工具:{func_name}, 参数:{func_args}")

                # 执行本地工具,动态分发调用
                tool_func = available_tools.get(func_name)
                if tool_func:
                    tool_result = tool_func(**func_args)
                else:
                    tool_result = "不存在该工具"
                print(f"工具返回结果:{tool_result}")

                # 存入工具执行结果
                chat_history.append({
                    "role": "tool",
                    "tool_call_id": tool_id,
                    "name": func_name,
                    "content": tool_result
                })

重点难懂语法拆解

  • json.loads(tool.function.arguments):AI 传的参数是一段 JSON 文本字符串,Python 无法直接读取,必须转成字典:'{"location":"合肥"}'{"location": "合肥"}
  • tool_func(**func_args)** 字典解包语法:func_args = {"location": "合肥"}等价于 get_weather(location="合肥")

第二轮请求大模型(结合工具结果生成最终回答)

            # 工具执行完,二次请求大模型,生成最终回答
            
            print("\n正在结合工具结果,生成最终回复...")
            
            second_resp = client.chat.completions.create(
                model=Model_ID,
                messages=chat_history,
            )
            final_content = second_resp.choices[0].message.content.strip()
            chat_history.append({"role": "assistant", "content": final_content})
            print("===== 工具流程结束 =====\n")
            return final_content

作用

  • 第一轮 AI 只下发调用指令,不会生成答案;
  • 这一轮把查到的天气数据全部传给 AI,让 AI 按照 system_prompt 规则整理输出 JSON。

else 分支(无需调用工具,普通闲聊)

        else:
            # 无工具调用,直接返回普通文本
            print("❌ 模型无需调用工具,直接文字回答")
            content = msg.content.strip()
            chat_history.append({"role": "assistant", "content": content})
            return content

3. 示例输出

注:此处为了让 AI 能够输出自然语言,把强制 AI 输出 JSON 格式的system prompt 和相关代码删去。

AI对话程序,输入 quit 结束对话

你:你好,我叫阿言。
❌ 模型无需调用工具,直接文字回答
AI: 你好,阿言!很高兴认识你 😊 我是你的 AI 助手,有什么我可以帮助你的吗?

你:帮我查询合肥明日的天气
AI 调用了工具: get_weather
调用工具:get_weather, 参数:{'location': '合肥'}
工具返回结果:合肥当前天气:Partly Cloudy ,温度:29°C

正在结合工具结果,生成最终回复...
===== 工具流程结束 =====

AI: 你好阿言!我已经查询了这合肥的天气信息:

**合肥** 📍
- 天气状况:局部多云 (Partly Cloudy)
- 温度:29°C

⚠️ **温馨提示:** 以上为当前实时天气数据。如果你需要了解**明日具体天气预报**(包括详细温度范围、降水概率、风力风向等),建议你查看专业天气应用或网站,比如中国气象局官网、墨迹天气或手机自带天气功能,这样能获得更精准的预报信息。

需要我帮你做其他事情吗?😊

你:quit
结束对话。

二、本篇总结 & 下期预告

本篇我们实现了单次工具调用的完整流程,让模型具备了调用外部工具的能力,但目前仍无法自主多轮推理、迭代完成复杂任务,且缺乏工程防护机制。

下一篇 Stage1-下,我们将搭建完整的 Agent 自主循环逻辑,同时新增步数限制、超时控制、分层错误处理三大安全护栏,最终落地一套稳定、可复用、适配线上场景的最小完整 Agent。

posted @ 2026-07-23 21:26  Alkaid2077  阅读(4)  评论(0)    收藏  举报