从零开始:构建你的专属Agent工具调用系统,从底层理解

44 阅读31分钟

本文较长,建议点赞收藏。更多AI大模型应用开发学习视频及资料,在智泊AI

🎯 前言

你有没有想过,为什么有些AI助手只能聊天,而有些却能帮你查天气、做计算、甚至操作各种软件?秘密就在于工具调用(Tool Use)技术!

想象一下:当你问"北京今天天气如何?"时,普通的AI只能基于训练数据瞎猜,而掌握了工具调用的Agent却能实时查询天气API,给你最准确的答案。这就像给AI装上了"手脚",让它不再只是个"话痨",而是真正能干活的助手!

今天,我们就从零开始,不是使用框架直接调用,而是使用提示词的方式一步步构建一个完整的工具调用系统。相信我,这比你想象的更简单,也更有趣!

本次代码开源地址:本次代码开源地址:github.com/li-xiu-qi/X…_fc/local_tool_use_implementation.ipynb

🤖 一、认识Agent和工具调用的神奇世界

什么是Agent?就像给AI配个贴心助理

AI Agent 核心能力架构

在AI领域,Agent就像是一个拥有完整"感知-思考-行动"能力的小助手。想象一下,如果AI是个大脑,那Agent就是这个大脑配上了眼睛、耳朵和手脚!

现代AI Agent通常有三个超能力:

  • 👀 感知能力:理解你说的话,看懂环境状态
  • 🧠 思考能力:基于大模型进行推理和规划
  • 🤲 行动能力:执行具体任务,调用各种工具

工具调用到底是个啥?

简单来说,工具调用就是让AI能够使用各种"外挂"功能的技术!这些"工具"本质上是结构化的函数接口,覆盖了超多应用场景:

信息获取类工具

  • 网络搜索API(Google、Bing等)
  • 实时数据查询(天气、股价、新闻)
  • 知识库检索(企业文档、专业数据库)

计算处理类工具

  • 数学计算器(复杂运算、统计分析)
  • 代码执行环境(Python、数据处理)
  • 图像/音频处理工具

交互操作类工具

  • 邮件发送/日历管理
  • 文件操作(读取、写入、转换)
  • 数据库查询与更新

📚 二、工具调用

Function Calling VS Tool Use

最初,OpenAI在2023年推出了Function Calling概念,让模型可以调用预定义的函数。

刚开始国内各大厂商的模型都不支持Function Calling,大家都在观望。但随着技术的发展,慢慢地也都跟上了节奏。

后来,智谱AI在glm3-6b开源的时候提出了Tool Use概念,支持一次调用多个工具。

OpenAI看到这个概念,也赶紧跟进推出了自己的Tool Use版本。不过,原来的Function Calling到现在还是支持的。

不过有的服务商似乎还是使用的是function calling的名字,比如qwen,具体我建议参考实际的厂商而定,qwen的例子在这里:qwen.readthedocs.io/zh-cn/lates…_call.html

引用qwen的function_call链接里面的一段话:这一概念也可能被称为“工具使用” (“tool use”)。虽然有人认为“工具”是“函数”的泛化形式,但在当前,它们的区别仅在技术层面上,表现为编程接口的不同输入输出类型。

🔧 三、工具调用的核心实现原理

那么问题来了:大型语言模型是怎么实现"调用工具"这种看起来超越文本生成的神奇能力呢?让我们一起来揭开这个秘密!

结构化输出的巧妙设计

其实啊,工具调用的核心原理可以用一句话概括:让模型输出结构化的调用指令,而不是直接执行外部操作

听起来有点抽象?没关系,我们来看看具体的流程:

  1. 🎯 意图识别:模型分析你的输入,判断是否需要外部工具
  2. 🎪 工具选择:从工具箱里选择最合适的工具
  3. 📝 参数提取:从你的话里提取工具需要的参数
  4. 🏗️ 结构化输出:生成符合格式的JSON调用指令
  5. ⚡ 执行与反馈:系统执行工具调用并把结果返回给模型

实现架构解析

想象一个典型场景:你问"计算半径为5的圆的面积"。模型的处理过程是这样的:

{  
  "tool_call": {  
    "name": "calculate_area",  
    "arguments": {  
      "shape": "circle",  
      "radius": 5  
    }  
  }  
}

这个JSON结构包含了完整的调用信息,系统解析后动态执行相应函数,再将结果反馈给模型继续处理。 上面的返回结构是我自己设计的,实际上参考qwen的sdk,返回的function call内容是这样的,而且开启parallel_function_calls之后还能连续返回function call的调用结果:

[  
    {'role': 'assistant', 'content': '', 'function_call': {'name': 'get_current_temperature', 'arguments': '{"location""San Francisco, CA, USA""unit""celsius"}'}},  
    {'role': 'assistant', 'content': '', 'function_call': {'name': 'get_temperature_date', 'arguments': '{"location""San Francisco, CA, USA""date""2024-10-01""unit""celsius"}'}},  
]

展开其中的一个你可以发现是这样的结构:

{'role': 'assistant',  
    'content': '',   
    'function_call': {  
        'name': 'get_current_temperature',   
        'arguments':   
        '{"location":   "San Francisco, CA, USA",   
        "unit""celsius"  
        }'  
        }  
    },

💡 核心思想

记住:模型不是真的在"调用工具",而是在"描述如何调用工具"。真正的执行是由系统来完成的!

🚀 四、实战环节:从零搭建工具调用系统

好了,理论说了这么多,是时候撸起袖子干活了!我们来一步步构建一个完整的、非SDK封装的工具调用系统。别担心,我会手把手教你!

环境准备:工欲善其事,必先利其器

首先,我们需要搭建基础环境。在环境中配置API密钥和基础URL:

API_KEY=你的apikeyBASE_URL=https://ark.cn-beijing.volces.com/api/v3

📌 温馨提示

我这里用的是火山引擎,你也可以换成硅基流动、智谱清言或者任何一家服务商。因为这里的标准是我们自己设计的,所以可以兼容任何一家,需要注意的是,我们的实现不是官方实现方案……

下面是我们需要的依赖项和环境变量导入:

import json  
import re  
import asyncio  
from datetime import datetime  
from typing import Dict, List, Any, Optional, Callable  
from dataclasses import dataclass  
import math  
  
import os  
from dotenv import load_dotenv  
from fallback_openai_client import AsyncFallbackOpenAIClient  
  
load_dotenv()  
  
DEEPSEEK_API_KEY = os.getenv("API_KEY")  
DEEPSEEK_BASE_URL = os.getenv("BASE_URL")  
DEEPSEEK_MODEL = "deepseek-v3-250324"  
  
print("🚀 本地工具调用实现初始化完成")

这里我们使用Deepseek模型作为推理引擎,通过封装的异步客户端AsyncFallbackOpenAIClient实现稳定的API调用。

💡 关于客户端

AsyncFallbackOpenAIClient其实和OpenAI的标准客户端是一样的,只是我做了一些封装。你完全可以用OpenAIAsyncOpenAI替换!

定义工具函数:给AI准备"工具箱"

接下来,我们定义几个典型的工具函数,展示不同类型的功能实现:

# 工具函数定义  
def get_current_time() -> str:  
    """获取当前时间"""  
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")  
  
def calculate_area(shape: str, **kwargs) -> dict:  
    """计算几何图形面积"""  
    if shape == "circle":  
        radius = kwargs.get("radius"0)  
        if radius <= 0:  
            return {"error""圆的半径必须大于0"}  
        return {"shape""circle""area": math.pi * radius ** 2}  
    elif shape == "rectangle":  
        width = kwargs.get("width"0)  
        height = kwargs.get("height"0)  
        if width <= 0or height <= 0:  
            return {"error""矩形的宽度和高度必须大于0"}  
        return {"shape""rectangle""area": width * height}  
    else:  
        return {"error"f"不支持的图形类型: {shape}"}  
  
def get_weather(city: str) -> str:  
    """获取城市天气信息(模拟)"""  
    weather_data = {  
        "北京""晴天,气温 15°C,微风",  
        "上海""多云,气温 18°C,东南风",  
        "广州""雨天,气温 22°C,南风",  
        "深圳""晴天,气温 25°C,无风",  
        "杭州""阴天,气温 16°C,西北风"  
    }  
    return weather_data.get(city, f"抱歉,暂时无法获取 {city} 的天气信息")  
  
def search_knowledge(query: str) -> str:  
    """搜索知识库(模拟)"""  
    knowledge_base = {  
        "python""Python 是一种解释型、面向对象、动态数据类型的高级程序设计语言。",  
        "机器学习""机器学习是人工智能的一个分支,让计算机通过数据学习模式。",  
        "深度学习""深度学习是机器学习的子集,使用神经网络模拟人脑学习过程。",  
        "ai""人工智能(AI)是计算机科学的一个领域,致力于创建智能机器。"  
    }  
      
    query_lower = query.lower()  
    for key, value in knowledge_base.items():  
        if key in query_lower:  
            return value  
      
    returnf"抱歉,没有找到关于 '{query}' 的相关信息"  
  
print("✅ 工具函数定义完成")

工具函数设计的小心思

这些看似简单的工具函数,其实蕴含着几个重要的设计思路:

🎯 功能多样性原则

我故意设计了四种不同类型的工具:

  • 无参数工具get_current_time):最简单的调用方式
  • 条件参数工具calculate_area):通过**kwargs处理复杂参数
  • 单参数工具get_weathersearch_knowledge):最常见的调用模式

🛡️ 错误处理机制

注意calculate_area函数中的参数验证:

if radius <= 0:    return {"error": "圆的半径必须大于0"}

这不是偶然的设计!这是为了测试工具层面的错误处理如何与系统层面的错误处理配合。

🎭 模拟真实API

虽然get_weathersearch_knowledge用的是模拟数据,但接口设计完全模仿真实API的行为。这让你可以轻松地将模拟工具替换为真实的外部服务!写后端的同学一定很熟悉!

🤓 设计思维

记住一个重要原则:接口优先设计!先定义工具的行为边界,再实现具体功能。就像写后端接口一样,不能功能写好了就直接对接前端,一定要有固定的接口规范!

# 工具函数定义  
def get_current_time() -> str:  
    """获取当前时间"""  
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")  
  
def calculate_area(shape: str, **kwargs) -> dict:  
    """计算几何图形面积"""  
    if shape == "circle":  
        radius = kwargs.get("radius"0)  
        if radius <= 0:  
            return {"error""圆的半径必须大于0"}  
        return {"shape""circle""area": math.pi * radius ** 2}  
    elif shape == "rectangle":  
        width = kwargs.get("width"0)  
        height = kwargs.get("height"0)  
        if width <= 0or height <= 0:  
            return {"error""矩形的宽度和高度必须大于0"}  
        return {"shape""rectangle""area": width * height}  
    else:  
        return {"error"f"不支持的图形类型: {shape}"}  
  
def get_weather(city: str) -> str:  
    """获取城市天气信息(模拟)"""  
    weather_data = {  
        "北京""晴天,气温 15°C,微风",  
        "上海""多云,气温 18°C,东南风",  
        "广州""雨天,气温 22°C,南风",  
        "深圳""晴天,气温 25°C,无风",  
        "杭州""阴天,气温 16°C,西北风"  
    }  
    return weather_data.get(city, f"抱歉,暂时无法获取 {city} 的天气信息")  
  
def search_knowledge(query: str) -> str:  
    """搜索知识库(模拟)"""  
    knowledge_base = {  
        "python""Python 是一种解释型、面向对象、动态数据类型的高级程序设计语言。",  
        "机器学习""机器学习是人工智能的一个分支,让计算机通过数据学习模式。",  
        "深度学习""深度学习是机器学习的子集,使用神经网络模拟人脑学习过程。",  
        "ai""人工智能(AI)是计算机科学的一个领域,致力于创建智能机器。"  
    }  
      
    query_lower = query.lower()  
    for key, value in knowledge_base.items():  
        if key in query_lower:  
            return value  
      
    returnf"抱歉,没有找到关于 '{query}' 的相关信息"  
  
print("✅ 工具函数定义完成")

工具函数设计的深层思考

这个看似简单的工具函数层实际上体现了工具调用系统的几个关键设计原则

1. 功能多样性原则我们故意设计了四种不同类型的工具:

  • 无参数工具get_current_time):验证系统对简单调用的处理能力
  • 条件参数工具calculate_area):通过**kwargs展示复杂参数处理
  • 单参数工具get_weathersearch_knowledge):最常见的调用模式

2. 错误处理机制注意calculate_area函数中的参数验证:

if radius <= 0:    return {"error": "圆的半径必须大于0"}

这不是偶然的设计,而是为了测试工具层面的错误处理如何与系统层面的错误处理相配合。

虽然get_weathersearch_knowledge使用的是模拟数据,但它们的接口设计完全模仿真实API的行为模式。这让用户可以轻松地将模拟工具替换为真实的外部服务调用。

每个工具函数都有清晰的输入输出契约,这为后续的工具注册和管理奠定了基础。我建议读者应该学会接口优先的设计思维——先定义工具的行为边界,再实现具体功能。就像是你写后端的时候不能先写好功能了就直接接到前端吧,一定是有一个固定值的状态。

学习目标:通过这个环节,用户应该掌握如何设计具有良好接口的工具函数,理解不同参数模式的处理策略,以及错误处理在工具层面的重要性。

工具注册与管理:给AI配个"工具管家"

需要注意的是,如果是本地的模型,其实工具的传递方式不是使用这种直接放到prompt里面的,而是放到一个特殊的工具标签里面包围的,从而实现模型能够更好的在推理的时候选择合适的工具。因为电脑配置的原因,我这里没法演示,使用本地推理的模型进行工具配置。但是我们本次要讲解的重点是如何实现Agent调用函数,所以问题也不大,感兴趣的同学可以当作扩展知识学习了。

为了实现灵活的工具管理,我们需要构建一个注册系统。这就像给AI配了个专业的"工具管家",负责管理所有可用的工具!

@dataclass  
class ToolParameter:  
    """工具参数定义"""  
    name: str  
    type: str  
    description: str  
    required: boolTrue  
  
@dataclass  
class ToolDefinition:  
    """工具定义"""  
    name: str  
    function: Callable  
    description: str  
    parameters: List[ToolParameter]  
  
class LocalToolRegistry:  
    """本地工具注册表"""  
      
    def __init__(self):  
        self.tools: Dict[str, ToolDefinition] = {}  
      
    def register_tool(self, tool_def: ToolDefinition):  
        """注册工具"""  
        self.tools[tool_def.name] = tool_def  
        print(f"✅ 注册工具: {tool_def.name}")  
      
    def get_tool(self, name: str) -> Optional[ToolDefinition]:  
        """获取工具定义"""  
        return self.tools.get(name)  
      
    def list_tools(self) -> List[str]:  
        """列出所有工具名称"""  
        return list(self.tools.keys())  
      
    def generate_tool_prompt(self) -> str:  
        """生成工具描述的 prompt"""  
        ifnot self.tools:  
            return"当前没有可用的工具。"  
          
        prompt = "你有以下工具可以使用:\n\n"  
          
        for tool_name, tool_def in self.tools.items():  
            prompt += f"**{tool_name}**: {tool_def.description}\n"  
            prompt += "参数:\n"  
              
            ifnot tool_def.parameters:  
                prompt += "  - 无需参数\n"  
            else:  
                for param in tool_def.parameters:  
                    required_str = "必需"if param.required else"可选"  
                    prompt += f"  - {param.name} ({param.type}{required_str}): {param.description}\n"  
            prompt += "\n"  
          
        prompt += """当你需要调用工具时,请严格按照以下JSON格式输出:  
  
\```json  
{  
  "tool_call": {  
    "name": "工具名称",  
    "arguments": {  
      "参数名": "参数值"  
    }  
  }  
}  
\```  
  
如果不需要调用工具,请直接回答问题。  
"""  
  
        return prompt  
  
# 创建工具注册表并注册工具  
  
tool_registry = LocalToolRegistry()  
  
# 注册工具  
  
tool_registry.register_tool(ToolDefinition(  
    name="get_current_time",  
    function=get_current_time,  
    description="获取当前的日期和时间",  
    parameters=[]  
))  
  
tool_registry.register_tool(ToolDefinition(  
    name="calculate_area",  
    function=calculate_area,  
    description="计算几何图形的面积(支持圆形和矩形)",  
    parameters=[  
        ToolParameter("shape""string""图形类型,可选值:circle(圆形)、rectangle(矩形)"),  
        ToolParameter("radius""number""圆的半径(当shape为circle时必需)", required=False),  
        ToolParameter("width""number""矩形的宽度(当shape为rectangle时必需)", required=False),  
        ToolParameter("height""number""矩形的高度(当shape为rectangle时必需)", required=False),  
    ]  
))  
  
tool_registry.register_tool(ToolDefinition(  
    name="get_weather",  
    function=get_weather,  
    description="获取指定城市的天气信息",  
    parameters=[  
        ToolParameter("city""string""城市名称,如:北京、上海、广州、深圳、杭州")  
    ]  
))  
  
tool_registry.register_tool(ToolDefinition(  
    name="search_knowledge",  
    function=search_knowledge,  
    description="在知识库中搜索相关信息",  
    parameters=[  
        ToolParameter("query""string""搜索查询词")  
    ]  
))  
  
print(f"\n📋 已注册 {len(tool_registry.tools)} 个工具")  
print("工具列表:", tool_registry.list_tools())

注册系统的结构设计

这个工具注册系统体现了现代软件架构的几个重要设计理念:

🎯 元数据驱动设计

我们用ToolParameterToolDefinition数据类来描述工具的"身份证":

@dataclass  
class ToolParameter:  
    name: str          # 参数名  
    type: str          # 参数类型    
    description: str   # 参数说明  
    required: boolTrue  # 是否必需

这种设计的妙处在于:

  • 自动生成文档(通过generate_tool_prompt
  • 运行时验证参数
  • 动态构建界面
  • 支持版本控制

🗂️ 注册表模式

LocalToolRegistry实现了经典的注册表模式,好处多多:

  • 解耦合:工具定义与使用完全分离
  • 易扩展:新增工具不用改现有代码
  • 可配置:不同场景动态加载不同工具

📖 自文档化系统

generate_tool_prompt方法很巧妙——让系统自己告诉AI怎么用:

这种自文档化确保AI模型始终获得最新、最准确的使用说明!

💡 设计智慧

注意calculate_area工具的参数定义技巧:通过required=False但在描述中说明条件性要求,实现了灵活的参数验证。这在复杂工具中很常用!

解析器的实现

工具调用的关键挑战之一是从模型的自然语言输出中准确提取结构化信息。我们的解析器采用了多模式匹配策略:

class ToolCallParser:  
    """工具调用解析器"""  
      
    @staticmethod  
    def extract_json_from_text(text: str) -> Optional[dict]:  
        """从文本中提取JSON"""  
        patterns = [  
            r'```json\s*(\{.*?\})\s*```',  # 标准代码块格式  
            r'```\s*(\{.*?\})\s*```',      # 简化代码块格式  
            r'\{[^{}]*"tool_call"[^{}]*\{[^{}]*\}[^{}]*\}',  # 直接匹配结构  
        ]  
          
        for pattern in patterns:  
            matches = re.findall(pattern, text, re.DOTALL | re.IGNORECASE)  
            for match in matches:  
                try:  
                    return json.loads(match)  
                except json.JSONDecodeError:  
                    continue  
          
        # 尝试直接解析整个文本  
        try:  
            return json.loads(text.strip())  
        except json.JSONDecodeError:  
            pass  
          
        returnNone  
      
    @staticmethod  
    def parse_tool_call(text: str) -> Optional[dict]:  
        """解析工具调用"""  
        json_data = ToolCallParser.extract_json_from_text(text)  
          
        ifnot json_data:  
            returnNone  
          
        # 检查是否包含tool_call结构  
        if"tool_call"in json_data:  
            tool_call = json_data["tool_call"]  
            if"name"in tool_call:  
                return {  
                    "name": tool_call["name"],  
                    "arguments": tool_call.get("arguments", {})  
                }  
          
        # 检查是否直接是工具调用格式  
        if"name"in json_data and"arguments"in json_data:  
            return {  
                "name": json_data["name"],  
                "arguments": json_data.get("arguments", {})  
            }  
          
        returnNone  
  
# 测试解析器  
parser = ToolCallParser()  
  
test_cases = [  
    '```json\n{"tool_call": {"name": "get_current_time", "arguments": {}}}\n```',  
    '{"tool_call": {"name": "get_weather", "arguments": {"city": "北京"}}}',  
    '我需要调用工具:```json\n{"tool_call": {"name": "calculate_area", "arguments": {"shape": "circle", "radius": 5}}}\n```',  
    '普通回答,不需要调用工具'  
]  
  
print("🧪 测试工具调用解析器:")  
for i, case in enumerate(test_cases, 1):  
    result = parser.parse_tool_call(case)  
    print(f"测试 {i}{'✅ 解析成功' if result else '❌ 无工具调用'}")  
    if result:  
        print(f"   工具: {result['name']}, 参数: {result['arguments']}")

多模式解析策略的深度分析

解析器可能是整个工具调用系统中最容易被低估但最关键的组件。它的设计体现了处理不确定性输入的工程智慧:

1. 层次化匹配策略

我们的正则表达式按优先级排列:

patterns = [  
    r'```json\s*(\{.*?\})\s*```',  # 最标准的格式  
    r'```\s*(\{.*?\})\s*```',      # 省略json标识的格式    
    r'\{[^{}]*"tool_call"[^{}]*\{[^{}]*\}[^{}]*\}',  # 应急匹配  
]

这种设计考虑了AI模型输出的三种典型变化

  • 完全遵循规范:包含完整的markdown代码块标记
  • 部分遵循规范:有代码块但缺少语言标识
  • 偏离规范但结构正确:直接输出JSON但可能混合其他文本

2. 容错性设计

注意这个关键的循环结构:

for match in matches:  
    try:  
        return json.loads(match)  
    except json.JSONDecodeError:  
        continue  # 继续尝试下一个匹配

这种设计处理了一个现实问题:正则表达式可能匹配到看似正确但实际无效的JSON。通过立即尝试解析并捕获异常,我们确保只返回真正有效的数据。 不过有的时候模型还是有可能存在输出不能解析的内容,那个时候我们应该是需要涉及到请求重试的机制,我们这里是没有设计这种情况的。

3. 结构兼容性处理

解析器支持两种JSON结构:

# 标准结构  
{"tool_call": {"name""工具名""arguments": {...}}}  
  
# 简化结构    
{"name""工具名""arguments": {...}}

这种兼容性设计体现了向后兼容渐进式增强的原则。

4. 防御性编程实践

每个可能失败的操作都被包装在try-catch中:

try:  
    return json.loads(text.strip())  
except json.JSONDecodeError:  
    pass  # 优雅地失败,返回None

这种优雅降级确保解析失败不会马上导致整个系统崩溃。

学习目标:读者应该学会如何设计健壮的文本解析器,理解正则表达式在复杂场景中的应用技巧,掌握容错性编程的实践方法,以及如何处理不确定性输入。

执行引擎

工具执行器负责安全地调用外部函数并处理各种异常情况:

class LocalToolExecutor:  
    """本地工具执行器"""  
      
    def __init__(self, tool_registry: LocalToolRegistry):  
        self.tool_registry = tool_registry  
      
    asyncdef execute_tool(self, tool_name: str, arguments: dict) -> dict:  
        """执行工具调用"""  
        try:  
            tool_def = self.tool_registry.get_tool(tool_name)  
            ifnot tool_def:  
                return {  
                    "success"False,  
                    "error"f"工具 '{tool_name}' 不存在",  
                    "available_tools": self.tool_registry.list_tools()  
                }  
              
            # 验证必需参数  
            missing_params = []  
            for param in tool_def.parameters:  
                if param.required and param.name notin arguments:  
                    missing_params.append(param.name)  
              
            if missing_params:  
                return {  
                    "success"False,  
                    "error"f"缺少必需参数: {', '.join(missing_params)}"  
                }  
              
            # 执行工具函数  
            if arguments:  
                result = tool_def.function(**arguments)  
            else:  
                result = tool_def.function()  
              
            return {  
                "success"True,  
                "tool_name": tool_name,  
                "arguments": arguments,  
                "result": result  
            }  
              
        except Exception as e:  
            return {  
                "success"False,  
                "error"f"工具执行失败: {str(e)}",  
                "tool_name": tool_name,  
                "arguments": arguments  
            }  
  
# 创建执行器  
executor = LocalToolExecutor(tool_registry)  
  
# 测试执行器  
print("🧪 测试工具执行器:")  
  
test_executions = [  
    ("get_current_time", {}),  
    ("calculate_area", {"shape""circle""radius"5}),  
    ("get_weather", {"city""北京"}),  
    ("search_knowledge", {"query""Python"}),  
    ("nonexistent_tool", {})  # 测试不存在的工具  
]  
  
for tool_name, args in test_executions:  
    result = await executor.execute_tool(tool_name, args)  
    status = "✅"if result["success"else"❌"  
    print(f"{status} {tool_name}{result}")

执行引擎的安全性与健壮性设计

执行器是工具调用系统的安全边界,它的设计必须考虑到各种潜在的失败场景:

1. 多层次的错误分类

我们的错误处理策略包含三个层次:

工具不存在错误

if not tool_def:  
    return {  
        "success"False,  
        "error"f"工具 '{tool_name}' 不存在",  
        "available_tools": self.tool_registry.list_tools()  # 友好的提示  
    }

这不仅报告了错误,还主动提供了可选的工具列表,帮助调试。

参数验证错误

if missing_params:  
    return {  
        "success"False,  
        "error"f"缺少必需参数: {', '.join(missing_params)}"  
    }

这种预验证避免了在函数执行时才发现参数错误,提升了错误信息的准确性。

执行时异常

except Exception as e:  
    return {  
        "success"False,  
        "error"f"工具执行失败: {str(e)}",  
        "tool_name": tool_name,  
        "arguments": arguments  
    }

最后的兜底机制确保任何未预料的异常都能被优雅地处理。

2. 统一的返回格式

注意所有的返回结果都遵循统一的格式:

{  
    "success": bool,        # 明确的成功/失败标识  
    "tool_name": str,       # 便于日志记录和调试  
    "arguments": dict,      # 便于错误重现  
    "result": any,          # 成功时的实际结果  
    "error": str,           # 失败时的错误描述  
    "available_tools": list # 可选的帮助信息  
}

这种结构化的错误处理使得上层系统可以智能地处理不同类型的失败。相信写过后端的同学,应该很熟悉这种格式。

3. 异步设计的考量

尽管当前的工具函数都是同步的,但执行器设计为异步方法:

async def execute_tool(self, tool_name: str, arguments: dict) -> dict:

这种面向未来的设计为后续集成真实的异步API(如网络请求、数据库查询)预留了空间。

安全性考虑

  • 所有异常都被捕获,防止系统崩溃
  • 参数通过**kwargs传递,避免了代码注入风险 -返回的错误信息经过控制,不暴露系统内部细节
  • 如果是实际的生产场景,还需要限制模型能够运行的权限,否则有一个工具是使用终端,运行命令"rm -rf ~/"怎么办?

学习目标:用户应该掌握多层次错误处理的设计模式,理解参数验证的重要性,学会设计统一的返回格式,以及如何在执行器中平衡安全性与灵活性。

完整对话系统的构建

最后,我们将所有组件整合成一个完整的对话系统:

class LocalToolUseChat:  
    """本地工具调用聊天系统"""  
      
    def __init__(self, api_key: str, base_url: str, model: str, tool_registry: LocalToolRegistry):  
        self.client = AsyncFallbackOpenAIClient(  
            primary_api_key=api_key,  
            primary_base_url=base_url,  
            primary_model_name=model  
        )  
        self.tool_registry = tool_registry  
        self.executor = LocalToolExecutor(tool_registry)  
        self.parser = ToolCallParser()  
      
    def build_system_prompt(self) -> str:  
        """构建系统提示词"""  
        returnf"""你是一个智能助手,可以使用以下工具来帮助用户:  
  
{self.tool_registry.generate_tool_prompt()}  
  
请注意:  
1. 只有在确实需要调用工具时才使用工具  
2. 严格按照JSON格式输出工具调用  
3. 如果不需要工具,直接回答问题  
4. 每次只调用一个工具  
"""  
      
    asyncdef chat(self, user_message: str, conversation_history: List[dict] = None) -> dict:  
        """进行对话,支持工具调用"""  
        if conversation_history isNone:  
            conversation_history = []  
          
        # 构建消息  
        messages = [  
            {"role""system""content": self.build_system_prompt()}  
        ]  
        messages.extend(conversation_history)  
        messages.append({"role""user""content": user_message})  
          
        max_tool_calls = 3# 防止无限循环  
        tool_call_count = 0  
          
        while tool_call_count < max_tool_calls:  
            try:  
                # 调用模型  
                response = await self.client.chat_completions_create(  
                    messages=messages,  
                    max_tokens=1000,  
                    temperature=0.7  
                )  
                  
                assistant_response = response.choices[0].message.content  
                  
                # 解析工具调用  
                tool_call = self.parser.parse_tool_call(assistant_response)  
                  
                if tool_call:  
                    print(f"🔧 检测到工具调用: {tool_call['name']}")  
                    print(f"📝 参数: {tool_call['arguments']}")  
                      
                    # 执行工具  
                    tool_result = await self.executor.execute_tool(  
                        tool_call['name'],   
                        tool_call['arguments']  
                    )  
                      
                    print(f"✅ 工具执行结果: {tool_result}")  
                      
                    # 将工具调用和结果添加到对话历史  
                    messages.append({  
                        "role""assistant",   
                        "content"f"我将调用工具 {tool_call['name']} 来帮助你。"  
                    })  
                    messages.append({  
                        "role""user",   
                        "content"f"工具调用结果: {json.dumps(tool_result, ensure_ascii=False)}"  
                    })  
                      
                    tool_call_count += 1  
                    continue# 继续下一轮对话  
                else:  
                    # 返回最终响应  
                    return {  
                        "response": assistant_response,  
                        "tool_calls_used": tool_call_count,  
                        "conversation": messages,  
                        "success"True  
                    }  
                      
            except Exception as e:  
                return {  
                    "error"f"对话失败: {str(e)}",  
                    "tool_calls_used": tool_call_count,  
                    "conversation": messages,  
                    "success"False  
                }  
          
        # 达到最大工具调用次数  
        return {  
            "response""已达到最大工具调用次数限制",  
            "tool_calls_used": tool_call_count,  
            "conversation": messages,  
            "success"True  
        }  
      
    asyncdef close(self):  
        """关闭客户端"""  
        await self.client.close()  
  
# 创建本地工具调用聊天系统  
local_chat = LocalToolUseChat(  
    api_key=DEEPSEEK_API_KEY,  
    base_url=DEEPSEEK_BASE_URL,  
    model=DEEPSEEK_MODEL,  
    tool_registry=tool_registry  
)  
  
print("✅ 本地工具调用聊天系统初始化完成")

系统整合的架构设计

系统流程图

对话系统是所有组件的指挥中心,它的设计体现了复杂系统整合的关键原则:

1. 状态管理与对话连续性

系统通过conversation_history维护对话状态:

messages = [  
    {"role""system""content": self.build_system_prompt()}  
]  
messages.extend(conversation_history)  
messages.append({"role""user""content": user_message})

这种设计确保了:

  • 上下文保持:AI能够理解之前的对话内容
  • 工具调用历史:之前的工具调用结果可以影响后续决策
  • 系统提示词注入:确保AI始终知道可用的工具

2. 多轮交互的循环控制

核心的while循环体现了工具调用的迭代特性

while tool_call_count < max_tool_calls:  
    # 调用模型 -> 解析响应 -> 执行工具 -> 更新对话历史

这个循环处理了一个重要场景:一个用户查询可能需要多次工具调用。例如,"先查询北京天气,然后搜索穿衣建议"。

3. 防护机制设计

max_tool_calls = 3  # 防止无限循环

这个看似简单的限制实际上是系统稳定性的重要保障,防止AI陷入无限的工具调用循环。

4. 智能的对话历史构建

注意工具调用后的对话历史更新策略:

# messages.append({  
#     "role": "assistant",   
#     "content": f"我将调用工具 {tool_call['name']} 来帮助你。"  
# }) # 这里可以不要……  
messages.append({  
    "role""user",   
    "content": f"工具调用结果: {json.dumps(tool_result, ensure_ascii=False)}"  
})

这种设计让AI模型感知到用户调用了工具,并给他答案,并且能够基于工具结果进行推理。这是实现真正智能对话的关键。

有一个角色是tool,一般通过sdk封装返回来的结果调用工具的角色一般就叫tool,至少智谱的是这样。

🚀运行系统

让我们通过一系列测试来验证我们的工具调用系统的表现:

async def test_local_tool_use():  
    """测试本地工具调用系统"""  
      
    test_cases = [  
        "你好,你能做什么?",  
        "现在几点了?",  
        "帮我计算半径为8的圆的面积",  
        "北京今天天气怎么样?",  
        "搜索一下关于机器学习的信息",  
        "先告诉我现在时间,然后查询上海的天气",  
        "计算一个长5宽3的矩形面积"  
    ]  
      
    print("🚀 开始测试本地工具调用系统")  
    print("=" * 70)  
      
    for i, question in enumerate(test_cases, 1):  
        print(f"\n📋 测试 {i}{question}")  
        print("-" * 50)  
          
        try:  
            result = await local_chat.chat(question)  
              
            if result["success"]:  
                print(f"🤖 助手回复: {result['response']}")  
                print(f"📊 使用工具次数: {result['tool_calls_used']}")  
            else:  
                print(f"❌ 错误: {result.get('error''未知错误')}")  
                  
        except Exception as e:  
            print(f"❌ 测试失败: {e}")  
          
        print("-" * 50)  
      
    await local_chat.close()  
    print("\n✅ 测试完成")  
  
# 运行测试  
await test_local_tool_use()

测试系统的设计策略

1. 测试用例的分层设计

我们的测试用例按复杂度递增:

基础交互测试

  • "你好,你能做什么?" - 测试系统的基本响应能力和自我介绍

单工具调用测试

  • "现在几点了?" - 无参数工具
  • "帮我计算半径为8的圆的面积" - 有参数工具
  • "北京今天天气怎么样?" - 字符串参数工具
  • "搜索一下关于机器学习的信息" - 模糊查询工具

复杂场景测试

  • "先告诉我现在时间,然后查询上海的天气" - 多工具调用
  • "计算一个长5宽3的矩形面积" - 复杂参数工具

从最后的结果来看,我们的工具都能够被调用到,并作为提示信息让模型输出准确的结果。

输出:

🚀 开始测试本地工具调用系统  
======================================================================  
  
📋 测试 1: 你好,你能做什么?  
--------------------------------------------------  
🤖 助手回复: 你好!我是一个智能助手,可以帮助你完成多种任务,包括但不限于以下内容:  
  
1. **时间查询**:可以告诉你当前的日期和时间。  
2. **数学计算**:比如计算几何图形的面积(圆形、矩形等)。  
3. **天气查询**:可以获取指定城市的天气信息。  
4. **知识搜索**:在知识库中搜索相关信息来回答你的问题。  
5. **日常问答**:回答各种问题,提供建议和信息。  
  
如果你有任何需要,随时告诉我!  
📊 使用工具次数: 0  
--------------------------------------------------  
  
📋 测试 2: 现在几点了?  
--------------------------------------------------  
🤖 助手回复: 你好!我是一个智能助手,可以帮助你完成多种任务,包括但不限于以下内容:  
  
1. **时间查询**:可以告诉你当前的日期和时间。  
2. **数学计算**:比如计算几何图形的面积(圆形、矩形等)。  
3. **天气查询**:可以获取指定城市的天气信息。  
4. **知识搜索**:在知识库中搜索相关信息来回答你的问题。  
5. **日常问答**:回答各种问题,提供建议和信息。  
  
如果你有任何需要,随时告诉我!  
📊 使用工具次数: 0  
--------------------------------------------------  
  
📋 测试 2: 现在几点了?  
--------------------------------------------------  
🔧 检测到工具调用: get_current_time  
📝 参数: {}  
✅ 工具执行结果: {'success': True'tool_name''get_current_time''arguments': {}, 'result''2025-05-27 14:08:08'}  
🔧 检测到工具调用: get_current_time  
📝 参数: {}  
✅ 工具执行结果: {'success': True'tool_name''get_current_time''arguments': {}, 'result''2025-05-27 14:08:08'}  
🤖 助手回复: 现在是2025527日,下午208分。  
📊 使用工具次数: 1  
--------------------------------------------------  
  
📋 测试 3: 帮我计算半径为8的圆的面积  
--------------------------------------------------  
🤖 助手回复: 现在是2025527日,下午208分。  
📊 使用工具次数: 1  
--------------------------------------------------  
  
📋 测试 3: 帮我计算半径为8的圆的面积  
--------------------------------------------------  
🔧 检测到工具调用: calculate_area  
📝 参数: {'shape''circle''radius': 8}  
✅ 工具执行结果: {'success': True'tool_name''calculate_area''arguments': {'shape''circle''radius': 8}, 'result': {'shape''circle''area': 201.06192982974676}}  
🔧 检测到工具调用: calculate_area  
📝 参数: {'shape''circle''radius': 8}  
✅ 工具执行结果: {'success': True'tool_name''calculate_area''arguments': {'shape''circle''radius': 8}, 'result': {'shape''circle''area': 201.06192982974676}}  
🤖 助手回复: 半径为8的圆的面积是201.06(四舍五入到小数点后两位)。  
📊 使用工具次数: 1  
--------------------------------------------------  
  
📋 测试 4: 北京今天天气怎么样?  
--------------------------------------------------  
🤖 助手回复: 半径为8的圆的面积是201.06(四舍五入到小数点后两位)。  
📊 使用工具次数: 1  
--------------------------------------------------  
  
📋 测试 4: 北京今天天气怎么样?  
--------------------------------------------------  
🔧 检测到工具调用: get_weather  
📝 参数: {'city''北京'}  
✅ 工具执行结果: {'success': True'tool_name''get_weather''arguments': {'city''北京'}, 'result''晴天,气温 15°C,微风'}  
🔧 检测到工具调用: get_weather  
📝 参数: {'city''北京'}  
✅ 工具执行结果: {'success': True'tool_name''get_weather''arguments': {'city''北京'}, 'result''晴天,气温 15°C,微风'}  
🤖 助手回复: 北京今天的天气是晴天,气温15°C,有微风。天气不错,适合外出活动!  
📊 使用工具次数: 1  
--------------------------------------------------  
  
📋 测试 5: 搜索一下关于机器学习的信息  
--------------------------------------------------  
🤖 助手回复: 北京今天的天气是晴天,气温15°C,有微风。天气不错,适合外出活动!  
📊 使用工具次数: 1  
--------------------------------------------------  
  
📋 测试 5: 搜索一下关于机器学习的信息  
--------------------------------------------------  
🔧 检测到工具调用: search_knowledge  
📝 参数: {'query''机器学习'}  
✅ 工具执行结果: {'success': True'tool_name''search_knowledge''arguments': {'query''机器学习'}, 'result''机器学习是人工智能的一个分支,让计算机通过数据学习模式。'}  
🔧 检测到工具调用: search_knowledge  
📝 参数: {'query''机器学习'}  
✅ 工具执行结果: {'success': True'tool_name''search_knowledge''arguments': {'query''机器学习'}, 'result''机器学习是人工智能的一个分支,让计算机通过数据学习模式。'}  
🤖 助手回复: 机器学习是人工智能的一个重要分支,它使计算机能够通过分析数据来自动学习和改进,而无需进行明确的编程。机器学习的核心思想是通过算法从数据中提取模式,并利用这些模式进行预测或决策。  
  
主要特点包括:  
1. 数据驱动 - 依赖大量数据进行训练  
2. 自动改进 - 随着数据量的增加,性能会提升  
3. 模式识别 - 能够发现数据中的复杂关系  
  
常见的机器学习类型包括监督学习、无监督学习和强化学习。它在图像识别、自然语言处理、推荐系统等领域有广泛应用。  
  
需要了解更多具体方面的信息吗?比如算法类型、应用案例或学习资源等。  
📊 使用工具次数: 1  
--------------------------------------------------  
  
📋 测试 6: 先告诉我现在时间,然后查询上海的天气  
--------------------------------------------------  
🤖 助手回复: 机器学习是人工智能的一个重要分支,它使计算机能够通过分析数据来自动学习和改进,而无需进行明确的编程。机器学习的核心思想是通过算法从数据中提取模式,并利用这些模式进行预测或决策。  
  
主要特点包括:  
1. 数据驱动 - 依赖大量数据进行训练  
2. 自动改进 - 随着数据量的增加,性能会提升  
3. 模式识别 - 能够发现数据中的复杂关系  
  
常见的机器学习类型包括监督学习、无监督学习和强化学习。它在图像识别、自然语言处理、推荐系统等领域有广泛应用。  
  
需要了解更多具体方面的信息吗?比如算法类型、应用案例或学习资源等。  
📊 使用工具次数: 1  
--------------------------------------------------  
  
📋 测试 6: 先告诉我现在时间,然后查询上海的天气  
--------------------------------------------------  
🔧 检测到工具调用: get_current_time  
📝 参数: {}  
✅ 工具执行结果: {'success': True'tool_name''get_current_time''arguments': {}, 'result''2025-05-27 14:08:27'}  
🔧 检测到工具调用: get_current_time  
📝 参数: {}  
✅ 工具执行结果: {'success': True'tool_name''get_current_time''arguments': {}, 'result''2025-05-27 14:08:27'}  
🔧 检测到工具调用: get_weather  
📝 参数: {'city''上海'}  
✅ 工具执行结果: {'success': True'tool_name''get_weather''arguments': {'city''上海'}, 'result''多云,气温 18°C,东南风'}  
🔧 检测到工具调用: get_weather  
📝 参数: {'city''上海'}  
✅ 工具执行结果: {'success': True'tool_name''get_weather''arguments': {'city''上海'}, 'result''多云,气温 18°C,东南风'}  
🤖 助手回复: 现在是202552714:08:27,上海的天气是多云,气温18°C,东南风。  
📊 使用工具次数: 2  
--------------------------------------------------  
  
📋 测试 7: 计算一个长53的矩形面积  
--------------------------------------------------  
🤖 助手回复: 现在是202552714:08:27,上海的天气是多云,气温18°C,东南风。  
📊 使用工具次数: 2  
--------------------------------------------------  
  
📋 测试 7: 计算一个长53的矩形面积  
--------------------------------------------------  
🔧 检测到工具调用: calculate_area  
📝 参数: {'shape''rectangle''width': 5'height': 3}  
✅ 工具执行结果: {'success': True'tool_name''calculate_area''arguments': {'shape''rectangle''width': 5'height': 3}, 'result': {'shape''rectangle''area': 15}}  
🔧 检测到工具调用: calculate_area  
📝 参数: {'shape''rectangle''width': 5'height': 3}  
✅ 工具执行结果: {'success': True'tool_name''calculate_area''arguments': {'shape''rectangle''width': 5'height': 3}, 'result': {'shape''rectangle''area': 15}}  
🤖 助手回复: 这个矩形的面积是15平方单位。  
📊 使用工具次数: 1  
--------------------------------------------------  
  
✅ 测试完成

学习资源推荐

如果你想更深入地学习大模型,以下是一些非常有价值的学习资源,这些资源将帮助你从不同角度学习大模型,提升你的实践能力。

本文较长,建议点赞收藏。更多AI大模型应用开发学习视频及资料,在智泊AI