如何让 Agent 学会使用复杂工具:API 抽象与工具链封装技巧
关键词
AI Agent, API抽象, 工具链封装, 工具使用, 智能体开发, LLM集成, 自动化工作流
摘要
随着大型语言模型(LLM)的快速发展,如何让AI Agent有效地使用复杂工具已成为AI应用开发的关键挑战。本文将深入探讨Agent学习使用工具的核心原理,重点介绍API抽象与工具链封装的高级技巧。我们将从基础概念开始,逐步深入到技术实现、应用案例和未来展望,为读者提供全面而实用的指导。通过本文,你将了解如何构建能够灵活使用多种工具的智能Agent系统,掌握将复杂API转化为Agent可理解接口的方法,并学会设计高效的工具链来解决实际问题。
1. 背景介绍
1.1 主题背景和重要性
在人工智能的发展历程中,我们经历了从规则驱动到数据驱动,再到现在的能力驱动的转变。大型语言模型(LLMs)的出现,特别是GPT-4、Claude等模型的问世,标志着AI进入了一个新的时代。这些模型展现出了惊人的语言理解和生成能力,但它们也存在明显的局限性:缺乏实时信息、无法直接访问外部系统、难以执行复杂的多步骤任务。
这就引出了一个关键问题:如何让这些强大的语言模型能够"动手做事"?答案就是让它们学会使用工具。就像人类通过使用工具极大地扩展了自身能力一样,AI Agent通过调用外部工具也能突破自身的局限。
工具使用能力的重要性体现在多个方面:
- 扩展知识边界:通过搜索引擎、数据库查询等工具,Agent可以获取最新信息
- 增强行动能力:通过API调用、代码执行等工具,Agent可以直接影响现实世界
- 解决复杂问题:通过组合使用多种工具,Agent可以处理单靠模型自身无法完成的任务
- 提高可靠性:工具的使用可以减少模型幻觉,提供更准确的结果
1.2 目标读者
本文主要面向以下读者群体:
- AI应用开发者:希望构建具备工具使用能力的智能Agent系统
- 机器学习工程师:对LLM应用和工具集成感兴趣的技术人员
- 产品经理:希望理解AI Agent能力边界,规划智能产品的人员
- 研究人员:探索工具学习和Agent架构的学术研究者
- 技术爱好者:对AI前沿技术有浓厚兴趣的开发者
阅读本文需要具备一定的编程基础(特别是Python),对API调用有基本了解,对LLM和AI Agent概念有初步认识。
1.3 核心问题或挑战
让Agent学会使用复杂工具面临着多个核心挑战:
- 工具理解挑战:如何让Agent准确理解工具的功能、参数和使用场景?
- 工具选择挑战:面对多个可用工具,Agent如何根据当前任务选择最合适的工具?
- 多步骤规划挑战:复杂任务通常需要多个工具调用,Agent如何规划合理的执行顺序?
- 错误处理挑战:工具调用失败时,Agent如何诊断问题并调整策略?
- 工具组合挑战:如何将多个工具组合成高效的工作流,实现1+1>2的效果?
- 抽象层次挑战:如何在保持工具灵活性的同时,提供足够的抽象以简化Agent的使用?
本文将围绕这些挑战,探讨API抽象与工具链封装的解决方案。
2. 核心概念解析
2.1 基础概念:从工具到Agent
在深入探讨技术细节之前,让我们先通过生活化的比喻来理解一些核心概念。
2.1.1 Agent是什么?
我们可以把Agent想象成一位多才多艺的助手。这位助手有很好的理解能力(感谢LLM),但手脚不太灵活,也没有随身携带各种工具。不过,这位助手非常聪明,只要给它合适的工具和清晰的使用说明,它就能完成各种各样的任务。
从技术角度定义,Agent是一个能够感知环境、做出决策并采取行动的自主实体。在LLM时代,我们通常所说的Agent是指以大型语言模型为核心,结合各种工具和记忆系统的智能实体。
2.1.2 工具是什么?
工具可以被看作是Agent的"手脚"和"感官"延伸。就像人类使用锤子敲钉子、使用望远镜看远处一样,Agent使用工具来完成它自身无法直接完成的任务。
在数字世界中,工具通常表现为API(应用程序接口)、函数、服务或其他可调用的功能模块。它们可以是简单的数学计算,也可以是复杂的数据库查询、网页浏览、文件操作等。
2.1.3 工具使用的基本流程
让我们用一个日常生活中的例子来说明Agent使用工具的基本流程:
假设你让一位助手(Agent)帮你安排一次商务会议。这位助手需要:
- 理解任务:明确会议的目的、参与人员、大致时间等要求
- 确定所需工具:日历查询工具、日程协调工具、会议邀请发送工具等
- 规划步骤:先查询所有参与者的可用时间,然后找到共同空闲时间,最后发送邀请
- 执行工具调用:按顺序使用各个工具
- 处理结果:整合工具返回的信息,形成最终方案
- 反馈与调整:如果某个步骤失败,分析原因并尝试其他方法
这个流程虽然看似简单,但每个环节都包含着复杂的技术挑战,我们将在后续章节逐一探讨。
2.2 API抽象:让复杂变简单
2.2.1 什么是API抽象?
API抽象是将复杂的底层API接口转化为更高级、更易于理解和使用的接口的过程。这就像是给一个复杂的机器设备加装一个用户友好的控制面板,让用户不需要了解机器内部的工作原理,只需按几个按钮就能完成操作。
在Agent工具使用的场景中,API抽象主要解决以下问题:
- 简化参数:将复杂的参数结构转化为Agent容易理解的形式
- 隐藏细节:封装认证、错误处理、重试逻辑等底层细节
- 统一接口:为不同的API提供一致的调用方式
- 增强语义:添加更丰富的描述信息,帮助Agent理解工具用途
2.2.2 API抽象的层次
我们可以将API抽象分为不同的层次,从低到高依次是:
- 原始API层:直接暴露第三方服务的原始接口
- 封装层:添加基本的错误处理、认证和参数验证
- 语义层:添加功能描述、使用示例和参数说明
- 组合层:将多个相关API组合成更高级的功能
- 领域特定层:针对特定领域优化的高度抽象接口
让我们用一个天气预报API的例子来说明这些层次:
- 原始API层:需要经纬度坐标、API密钥,返回包含大量技术参数的JSON
- 封装层:接受城市名称,自动处理坐标转换和认证,返回基本天气信息
- 语义层:明确描述这是"获取天气信息"的工具,说明输入输出格式
- 组合层:结合地理编码和天气预报,提供"获取任意城市未来7天天气预报"的功能
- 领域特定层:为旅行规划提供"评估目的地天气是否适合户外活动"的专门功能
2.2.3 好的API抽象的特点
一个设计良好的API抽象应该具备以下特点:
- 高内聚低耦合:每个抽象工具功能明确,相互依赖少
- 语义清晰:工具名称、描述和参数都能准确传达其功能
- 容错性强:能处理各种异常情况,提供有用的错误信息
- 可组合:可以方便地与其他工具组合使用
- 可扩展:易于添加新功能或修改现有功能
- 文档完善:提供清晰的使用说明和示例
2.3 工具链封装:连接工具的桥梁
2.3.1 什么是工具链?
工具链是一系列工具的有序组合,这些工具协同工作以完成一个复杂任务。如果说单个工具是乐器,那么工具链就是整个乐队,它们按照乐谱(工作流)协调演奏,创造出美妙的音乐(解决方案)。
在Agent场景中,工具链封装是指将多个工具按照特定逻辑组织起来,形成一个更大的"超级工具",Agent可以像使用单个工具一样使用这个工具链。
2.3.2 工具链的类型
工具链可以按照不同的维度进行分类:
-
按控制流类型:
- 线性链:工具按照固定顺序依次执行
- 分支链:根据条件选择不同的工具路径
- 循环链:重复执行某些工具直到满足条件
- 混合链:结合以上多种控制结构
-
按数据流向:
- 管道模式:前一个工具的输出作为后一个工具的输入
- 扇出/扇入模式:一个工具的输出分发给多个工具,再聚合结果
- 共享存储模式:多个工具通过共享数据存储交换信息
-
按决策方式:
- 预定义链:工具执行顺序和逻辑在设计时固定
- 动态链:Agent根据情况实时决定工具使用顺序
- 混合链:部分预定义,部分动态决策
让我们用Mermaid图表来可视化这些不同类型的工具链:
2.3.3 工具链封装的价值
工具链封装为Agent工具使用带来多方面的价值:
- 降低复杂度:将多步骤操作简化为单一步骤
- 提高可靠性:预定义的执行流程减少了Agent决策失误的可能
- 优化性能:可以预先优化工具间的数据传递和执行顺序
- 增强可维护性:工具链逻辑集中管理,便于修改和优化
- 促进复用:常用的工具组合可以被封装后在多个场景复用
2.4 概念间的关系与对比
2.4.1 核心概念对比
让我们通过一个表格来对比这些核心概念在不同维度上的特点:
| 概念 | 主要目标 | 抽象级别 | 灵活性 | 可靠性 | 适用场景 |
|---|---|---|---|---|---|
| 原始API | 暴露功能 | 低 | 高 | 低 | 简单直接任务 |
| API抽象 | 简化使用 | 中 | 中 | 中 | 通用工具需求 |
| 单个工具 | 完成特定功能 | 中高 | 中 | 中高 | 单一功能任务 |
| 工具链 | 完成复杂任务 | 高 | 低 | 高 | 固定流程复杂任务 |
| 动态Agent | 自主决策完成任务 | 最高 | 最高 | 视情况而定 | 开放复杂任务 |
2.4.2 概念间的实体关系
现在让我们用ER图来展示这些概念之间的关系:
2.4.3 概念交互关系
最后,让我们来看一下这些概念在实际运行中的交互关系:
2.5 核心要素组成
无论是API抽象还是工具链封装,都包含一些核心要素:
-
描述层:
- 名称和唯一标识符
- 功能描述(帮助Agent理解用途)
- 使用示例(展示典型用法)
- 分类标签(便于组织和检索)
-
接口层:
- 输入模式(定义参数格式和约束)
- 输出模式(定义返回结果格式)
- 错误模式(定义可能的错误情况)
-
实现层:
- 核心功能实现
- 参数验证逻辑
- 错误处理机制
- 性能优化策略
-
元数据层:
- 使用统计信息
- 性能指标
- 依赖关系
- 版本信息
这些要素共同构成了一个完整的工具或工具链,使其能够被Agent理解和使用。在下一节中,我们将深入探讨这些概念的技术原理和实现方法。
3. 技术原理与实现
3.1 Agent工具使用的基本原理
3.1.1 工具调用的核心机制
让我们从最基本的问题开始:Agent是如何知道要调用哪个工具以及如何调用它的?
在底层,这通常通过以下几种机制实现:
-
提示工程(Prompt Engineering):
- 在提示中包含工具的描述和使用说明
- 引导模型生成特定格式的工具调用请求
- 解析模型输出,提取工具调用信息
-
函数调用(Function Calling):
- 许多现代LLM(如GPT-4、Claude)原生支持函数调用功能
- 通过结构化输入描述工具的名称、参数和功能
- 模型直接生成结构化的工具调用请求
-
微调(Fine-tuning):
- 对基础模型进行微调,使其学会特定的工具使用模式
- 这通常需要大量的工具使用示例数据
- 适合有大量特定工具使用需求的场景
无论使用哪种机制,核心流程都是相似的:
3.1.2 ReAct模式:推理与行动的结合
ReAct(Reasoning + Acting)是一种流行的Agent架构,它将推理和行动有机结合起来。在ReAct模式中,Agent交替进行思考和行动,形成一个迭代过程。
让我们用数学公式来形式化ReAct过程:
假设我们有:
- 任务输入 xxx
- 一系列思考步骤 s1,s2,...,sns_1, s_2, ..., s_ns1,s2,...,sn
- 一系列行动步骤 a1,a2,...,ana_1, a_2, ..., a_na1,a2,...,an
- 一系列观察结果 o1,o2,...,ono_1, o_2, ..., o_no1,o2,...,on
ReAct过程可以表示为:
s1∼pS(s1∣x)
s_1 \sim p_S(s_1 | x)
s1∼pS(s1∣x)
a1∼pA(a1∣x,s1)
a_1 \sim p_A(a_1 | x, s_1)
a1∼pA(a1∣x,s1)
o1=execute(a1)
o_1 = \text{execute}(a_1)
o1=execute(a1)
s2∼pS(s2∣x,s1,a1,o1)
s_2 \sim p_S(s_2 | x, s_1, a_1, o_1)
s2∼pS(s2∣x,s1,a1,o1)
a2∼pA(a2∣x,s1,a1,o1,s2)
a_2 \sim p_A(a_2 | x, s_1, a_1, o_1, s_2)
a2∼pA(a2∣x,s1,a1,o1,s2)
o2=execute(a2)
o_2 = \text{execute}(a_2)
o2=execute(a2)
...
...
...
y=generate(x,s1,a1,o1,...,sn,an,on)
y = \text{generate}(x, s_1, a_1, o_1, ..., s_n, a_n, o_n)
y=generate(x,s1,a1,o1,...,sn,an,on)
其中,yyy 是最终生成的答案。
这个过程可以理解为Agent在每一步都会:
- 思考当前情况(生成 sis_isi)
- 决定采取什么行动(生成 aia_iai)
- 执行行动并观察结果(获取 oio_ioi)
- 重复以上步骤直到任务完成
3.2 API抽象的技术实现
3.2.1 设计良好的工具接口
设计一个好的工具接口是API抽象的第一步。让我们探讨一些关键原则和技术:
-
清晰的命名:
- 工具名称应该清楚地表达其功能
- 例如:
search_web比tool1好得多
-
结构化的输入输出:
- 使用JSON Schema等方式明确定义输入输出格式
- 这有助于模型理解如何使用工具,也便于验证输入
-
适当的抽象级别:
- 既不要过于底层(需要Agent处理太多细节)
- 也不要过于高层(失去灵活性)
-
完善的错误处理:
- 定义清晰的错误类型和错误消息
- 提供有助于调试的信息
让我们用一个简单的Python示例来展示这些原则:
from typing import Dict, Any, List, Optional
from pydantic import BaseModel, Field
from enum import Enum
# 定义工具输入模型
class WeatherUnits(str, Enum):
CELSIUS = "celsius"
FAHRENHEIT = "fahrenheit"
class GetWeatherInput(BaseModel):
"""获取天气信息的输入参数"""
location: str = Field(..., description="城市名称或地址,例如'北京'或'纽约,NY'")
units: WeatherUnits = Field(default=WeatherUnits.CELSIUS, description="温度单位")
days: int = Field(default=1, ge=1, le=7, description="预报天数,最多7天")
# 定义工具输出模型
class WeatherCondition(str, Enum):
SUNNY = "sunny"
CLOUDY = "cloudy"
RAINY = "rainy"
SNOWY = "snowy"
class DailyForecast(BaseModel):
date: str
high_temp: float
low_temp: float
condition: WeatherCondition
precipitation_chance: float = Field(..., ge=0, le=1)
class GetWeatherOutput(BaseModel):
"""获取天气信息的输出结果"""
location: str
units: WeatherUnits
forecasts: List[DailyForecast]
disclaimer: Optional[str] = None
# 定义错误模型
class WeatherErrorType(str, Enum):
LOCATION_NOT_FOUND = "location_not_found"
API_ERROR = "api_error"
INVALID_REQUEST = "invalid_request"
class WeatherError(BaseModel):
error_type: WeatherErrorType
message: str
suggestion: Optional[str] = None
这种结构化的定义方式有几个优点:
- 提供了清晰的文档
- 可以自动验证输入输出
- 便于LLM理解和使用
- 类型安全,减少错误
3.2.2 封装底层API
现在,让我们来看如何实际封装一个底层API。假设我们有一个原始的天气API,它需要经纬度坐标,返回复杂的JSON数据。我们的目标是将其封装成上面定义的GetWeather工具。
import requests
from typing import Tuple, Optional
import json
# 原始API客户端
class RawWeatherAPIClient:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.rawweather.com/v1"
self.geocoding_url = "https://api.geocoding.com/v1"
def geocode(self, location: str) -> Tuple[float, float]:
"""将地址转换为经纬度坐标"""
params = {
"q": location,
"api_key": self.api_key
}
response = requests.get(f"{self.geocoding_url}/search", params=params)
response.raise_for_status()
data = response.json()
if not data.get("results"):
raise ValueError(f"无法找到位置: {location}")
return (
data["results"][0]["latitude"],
data["results"][0]["longitude"]
)
def get_weather(self, lat: float, lon: float, days: int = 1) -> Dict[str, Any]:
"""获取原始天气数据"""
params = {
"lat": lat,
"lon": lon,
"days": days,
"api_key": self.api_key
}
response = requests.get(f"{self.base_url}/forecast", params=params)
response.raise_for_status()
return response.json()
# 封装后的天气工具
class WeatherTool:
def __init__(self, api_key: str):
self.raw_client = RawWeatherAPIClient(api_key)
def _convert_condition(self, raw_condition: str) -> WeatherCondition:
"""将原始天气状况转换为我们的标准格式"""
condition_map = {
"clear": WeatherCondition.SUNNY,
"partly_cloudy": WeatherCondition.CLOUDY,
"mostly_cloudy": WeatherCondition.CLOUDY,
"overcast": WeatherCondition.CLOUDY,
"light_rain": WeatherCondition.RAINY,
"rain": WeatherCondition.RAINY,
"heavy_rain": WeatherCondition.RAINY,
"snow": WeatherCondition.SNOWY,
"light_snow": WeatherCondition.SNOWY,
}
return condition_map.get(raw_condition, WeatherCondition.CLOUDY)
def _convert_temp(self, temp: float, units: WeatherUnits) -> float:
"""根据需要转换温度单位"""
if units == WeatherUnits.FAHRENHEIT:
return temp * 9/5 + 32
return temp # 原始API返回摄氏度
def execute(self, input_data: GetWeatherInput) -> GetWeatherOutput:
"""执行天气查询工具"""
try:
# 获取经纬度
lat, lon = self.raw_client.geocode(input_data.location)
# 获取原始天气数据
raw_data = self.raw_client.get_weather(lat, lon, input_data.days)
# 转换为我们的格式
forecasts = []
for day_data in raw_data["daily"]:
forecast = DailyForecast(
date=day_data["date"],
high_temp=self._convert_temp(day_data["temp_max"], input_data.units),
low_temp=self._convert_temp(day_data["temp_min"], input_data.units),
condition=self._convert_condition(day_data["condition"]),
precipitation_chance=day_data["precipitation_probability"] / 100 # 转换为0-1范围
)
forecasts.append(forecast)
return GetWeatherOutput(
location=input_data.location,
units=input_data.units,
forecasts=forecasts,
disclaimer="天气数据仅供参考,请谨慎使用"
)
except ValueError as e:
return WeatherError(
error_type=WeatherErrorType.LOCATION_NOT_FOUND,
message=str(e),
suggestion="请尝试使用更具体的位置名称"
)
except requests.exceptions.HTTPError as e:
return WeatherError(
error_type=WeatherErrorType.API_ERROR,
message=f"天气服务暂时不可用: {str(e)}",
suggestion="请稍后再试"
)
except Exception as e:
return WeatherError(
error_type=WeatherErrorType.INVALID_REQUEST,
message=f"处理请求时出错: {str(e)}",
suggestion="请检查输入参数是否正确"
)
这个例子展示了几个重要的封装技术:
- 隐藏复杂性:将地理编码和天气查询两个步骤合并为一个工具
- 数据转换:将原始数据转换为更有意义的格式
- 单位转换:根据用户需要自动转换单位
- 错误处理:捕获各种错误并提供有意义的错误信息和建议
- 输入验证:使用Pydantic模型确保输入数据的正确性
3.2.3 为LLM优化工具描述
仅仅实现工具逻辑是不够的,我们还需要为LLM提供足够的信息,使其能够理解和正确使用工具。这通常通过提供良好的工具描述和使用示例来实现。
让我们继续扩展我们的天气工具,添加LLM友好的描述:
class ToolMetadata(BaseModel):
"""工具元数据,用于帮助LLM理解工具"""
name: str
description: str
input_schema: Dict[str, Any]
output_schema: Dict[str, Any]
examples: List[Dict[str, Any]]
category: str
tags: List[str]
# 为天气工具创建元数据
weather_tool_metadata = ToolMetadata(
name="get_weather",
description="""
获取指定地点的天气预报信息。可以查询当前天气和未来最多7天的预报。
支持摄氏度和华氏度两种温度单位。
""",
input_schema=GetWeatherInput.model_json_schema(),
output_schema=GetWeatherOutput.model_json_schema(),
examples=[
{
"name": "查询北京当前天气",
"input": {
"location": "北京",
"units": "celsius",
"days": 1
},
"explanation": "查询北京的当前天气,使用摄氏度单位"
},
{
"name": "查询纽约一周天气",
"input": {
"location": "纽约,NY",
"units": "fahrenheit",
"days": 7
},
"explanation": "查询纽约未来7天的天气预报,使用华氏度单位"
}
],
category="环境信息",
tags=["天气", "预报", "温度", "地理信息"]
)
这些元数据可以被格式化为LLM容易理解的提示词部分。例如,使用OpenAI的函数调用格式:
def format_tool_for_openai(metadata: ToolMetadata) -> Dict[str, Any]:
"""将工具元数据格式化为OpenAI函数调用格式"""
return {
"type": "function",
"function": {
"name": metadata.name,
"description": metadata.description,
"parameters": metadata.input_schema
}
}
# 格式化天气工具
openai_weather_tool = format_tool_for_openai(weather_tool_metadata)
print(json.dumps(openai_weather_tool, indent=2, ensure_ascii=False))
3.3 工具链封装的技术实现
3.3.1 定义工具链工作流
现在我们已经了解了如何封装单个工具,让我们探讨如何将多个工具组合成工具链。
首先,我们需要定义一种方式来描述工具链的工作流。这里有几种常见的方法:
- 基于代码的工作流定义:直接用代码编写工作流逻辑
- 声明式工作流定义:使用YAML、JSON等格式定义工作流
- 图形化工作流定义:通过图形界面设计工作流
让我们首先创建几个不同的工具,然后展示如何将它们组合成工具链:
# 首先,让我们定义几个简单的工具
# 1. 搜索工具
class SearchInput(BaseModel):
query: str = Field(..., description="搜索查询")
num_results: int = Field(default=5, ge=1, le=10, description="返回结果数量")
class SearchResult(BaseModel):
title: str
url: str
snippet: str
class SearchOutput(BaseModel):
results: List[SearchResult]
class SearchTool:
name = "search"
def execute(self, input_data: SearchInput) -> SearchOutput:
# 实际实现中这里会调用真实的搜索API
# 这里我们只是模拟一些结果
mock_results = [
SearchResult(
title=f"关于 {input_data.query} 的结果 1",
url=f"https://example.com/result1",
snippet=f"这是关于 {input_data.query} 的第一篇搜索结果摘要..."
),
SearchResult(
title=f"关于 {input_data.query} 的结果 2",
url=f"https://example.com/result2",
snippet=f"这是关于 {input_data.query} 的第二篇搜索结果摘要..."
)
]
return SearchOutput(results=mock_results[:input_data.num_results])
# 2. 网页内容提取工具
class ExtractContentInput(BaseModel):
url: str = Field(..., description="要提取内容的网页URL")
class ExtractContentOutput(BaseModel):
title: str
content: str
url: str
class ExtractContentTool:
name = "extract_content"
def execute(self, input_data: ExtractContentInput) -> ExtractContentOutput:
# 实际实现中这里会抓取并解析网页内容
return ExtractContentOutput(
title=f"网页标题: {input_data.url}",
content=f"这是从 {input_data.url} 提取的网页内容...",
url=input_data.url
)
# 3. 文本摘要工具
class SummarizeInput(BaseModel):
text: str = Field(..., description="要摘要的文本")
max_length: int = Field(default=200, ge=50, le=1000, description="摘要的最大长度")
class SummarizeOutput(BaseModel):
summary: str
original_length: int
summary_length: int
class SummarizeTool:
name = "summarize"
def execute(self, input_data: SummarizeInput) -> SummarizeOutput:
# 实际实现中这里会调用LLM进行摘要
summary = f"这是文本的摘要(长度不超过{input_data.max_length}字符):{input_data.text[:input_data.max_length]}..."
return SummarizeOutput(
summary=summary,
original_length=len(input_data.text),
summary_length=len(summary)
)
现在我们有了三个工具:搜索、提取内容和摘要。让我们将它们组合成一个"研究主题"的工具链:
from typing import List, Dict, Any, Callable
# 定义工作流步骤
class WorkflowStep:
def __init__(self, tool, input_transform: Callable = None, output_transform: Callable = None):
self.tool = tool
self.input_transform = input_transform or (lambda x, context: x)
self.output_transform = output_transform or (lambda x, context: x)
# 定义工具链
class ToolChain:
def __init__(self, name: str, description: str, steps: List[WorkflowStep]):
self.name = name
self.description = description
self.steps = steps
def execute(self, initial_input: Any) -> Dict[str, Any]:
"""执行工具链"""
context = {
"initial_input": initial_input,
"steps": []
}
current_input = initial_input
for i, step in enumerate(self.steps):
# 转换输入(使用上下文)
step_input = step.input_transform(current_input, context)
# 执行工具
step_output = step.tool.execute(step_input)
# 转换输出(使用上下文)
transformed_output = step.output_transform(step_output, context)
# 保存到上下文
context["steps"].append({
"step_index": i,
"tool_name": step.tool.name,
"input": step_input,
"output": step_output,
"transformed_output": transformed_output
})
# 更新当前输入为下一步准备
current_input = transformed_output
# 返回最终结果和完整上下文
return {
"final_result": current_input,
"context": context
}
现在让我们使用这个框架来创建我们的研究工具链:
# 定义研究主题工具链的输入和输出模型
class ResearchTopicInput(BaseModel):
topic: str = Field(..., description="要研究的主题")
num_sources: int = Field(default=3, ge=1, le=5, description="要研究的来源数量")
class ResearchSource(BaseModel):
title: str
url: str
summary: str
class ResearchTopicOutput(BaseModel):
topic: str
sources: List[ResearchSource]
overall_summary: str
# 创建研究主题工具链
def create_research_toolchain():
# 步骤1: 搜索相关网页
def search_input_transform(input_data, context):
return SearchInput(query=input_data.topic, num_results=input_data.num_sources)
def search_output_transform(output, context):
return output.results
# 步骤2: 提取每个网页的内容
def extract_input_transform(search_results, context):
# 这里我们将并行处理多个URL,但在简单示例中我们只处理第一个
# 实际实现中可以并行处理所有URL
if search_results:
return ExtractContentInput(url=search_results[0].url)
return None
def extract_output_transform(output, context):
return {
"search_results": context["steps"][0]["transformed_output"],
"extracted_content": output
}
# 步骤3: 摘要内容
def summarize_input_transform(data, context):
return SummarizeInput(text=data["extracted_content"].content, max_length=300)
def summarize_output_transform(summary, context):
data = context["steps"][1]["transformed_output"]
search_results = data["search_results"]
extracted_content = data["extracted_content"]
# 为简化起见,我们只为第一个搜索结果创建研究来源
# 实际实现中应该为所有结果创建
sources = [
ResearchSource(
title=extracted_content.title,
url=extracted_content.url,
summary=summary.summary
)
]
return ResearchTopicOutput(
topic=context["initial_input"].topic,
sources=sources,
overall_summary=f"关于 {context['initial_input'].topic} 的研究摘要..."
)
# 创建工作流步骤
steps = [
WorkflowStep(
tool=SearchTool(),
input_transform=search_input_transform,
output_transform=search_output_transform
),
WorkflowStep(
tool=ExtractContentTool(),
input_transform=extract_input_transform,
output_transform=extract_output_transform
),
WorkflowStep(
tool=SummarizeTool(),
input_transform=summarize_input_transform,
output_transform=summarize_output_transform
)
]
# 创建工具链
return ToolChain(
name="research_topic",
description="研究一个主题,搜索相关网页,提取内容并生成摘要",
steps=steps
)
# 使用工具链
research_chain = create_research_toolchain()
result = research_chain.execute(ResearchTopicInput(topic="人工智能", num_sources=3))
print(result)
这个例子展示了如何将多个工具组合成一个更复杂的工具链。每个步骤都可以访问之前步骤的结果,并且可以根据需要转换输入和输出数据。
3.3.2 高级工具链模式
上面的例子是一个简单的线性工具链。在实际应用中,我们经常需要更复杂的工作流模式。让我们探讨一些高级工具链模式:
- 分支与条件执行:
class ConditionalStep(WorkflowStep):
def __init__(self, condition: Callable, true_step: WorkflowStep, false_step: WorkflowStep = None):
self.condition = condition
self.true_step = true_step
self.false_step = false_step
# 这些是为了兼容基类,但我们会覆盖execute方法
super().__init__(tool=None)
def execute(self, input_data, context):
if self.condition(input_data, context):
return self._execute_step(self.true_step, input_data, context)
elif self.false_step:
return self._execute_step(self.false_step, input_data, context)
else:
# 没有false步骤,直接返回输入
return input_data
def _execute_step(self, step, input_data, context):
step_input = step.input_transform(input_data, context)
step_output = step.tool.execute(step_input)
return step.output_transform(step_output, context)
- 并行执行:
class ParallelStep:
def __init__(self, steps: List[WorkflowStep]):
self.steps = steps
def execute(self, input_data, context):
import concurrent.futures
# 使用线程池并行执行步骤
results = [None] * len(self.steps)
def execute_step(i, step):
step_input = step.input_transform(input_data, context)
step_output = step.tool.execute(step_input)
results[i] = step.output_transform(step_output, context)
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = [executor.submit(execute_step, i, step) for i, step in enumerate(self.steps)]
concurrent.futures.wait(futures)
return results
- 循环执行:
class LoopStep:
def __init__(self, step: WorkflowStep, condition: Callable, max_iterations: int = 10):
self.step = step
self.condition = condition
self.max_iterations = max_iterations
def execute(self, input_data, context):
current_input = input_data
iterations = 0
all_outputs = []
while self.condition(current_input, context, iterations) and iterations < self.max_iterations:
# 执行步骤
step_input = self.step.input_transform(current_input, context)
step_output = self.step.tool.execute(step_input)
transformed_output = self.step.output_transform(step_output, context)
# 保存结果
all_outputs.append({
"iteration": iterations,
"input": step_input,
"output": step_output,
"transformed_output": transformed_output
})
# 更新状态
current_input = transformed_output
iterations += 1
return {
"iterations": iterations,
"all_outputs": all_outputs,
"final_output": current_input
}
这些高级模式使我们能够构建更加灵活和强大的工具链,能够处理复杂的多步骤任务。
3.4 实现智能工具选择与动态规划
到目前为止,我们主要讨论了预定义的工具链。但在许多实际场景中,我们需要Agent能够根据具体情况智能地选择工具和规划执行顺序。这就是动态工具使用的用武之地。
3.4.1 基于LLM的工具选择
最直接的方法是利用LLM自身的推理能力来选择工具。我们可以为LLM提供可用工具的描述,让它决定使用哪个工具以及如何使用它。
让我们看一个简单的实现:
import openai
import json
class LLMToolSelector:
def __init__(self, model: str = "gpt-4", tools: List = None):
self.model = model
self.tools = tools or []
self.openai_client = openai.OpenAI() # 假设已设置API密钥
def register_tool(self, tool, metadata: ToolMetadata):
"""注册一个工具及其元数据"""
self.tools.append({
"tool": tool,
"metadata": metadata,
"openai_format": format_tool_for_openai(metadata)
})
def select_and_execute(self, task: str, max_iterations: int = 5) -> Dict[str, Any]:
"""根据任务选择工具并执行,可能进行多轮迭代"""
messages = [
{"role": "system", "content": "你是一个智能助手,能够使用各种工具来完成任务。当你收到用户请求时,请决定是否需要使用工具,或者是否可以直接回答。"},
{"role": "user", "content": task}
]
iteration = 0
final_answer = None
while iteration < max_iterations and final_answer is None:
# 准备工具列表
tools = [t["openai_format"] for t in self.tools]
# 调用LLM
response = self.openai_client.chat.completions.create(
model=self.model,
messages=messages,
tools=tools,
tool_choice="auto" # 让模型决定是否使用工具
)
response_message = response.choices[0].message
messages.append(response_message) # 保存模型的响应
# 检查是否需要调用工具
if response_message.tool_calls:
for tool_call in response_message.tool_calls:
# 找到对应的工具
tool_name = tool_call.function.name
tool_info = next((t for t in self.tools if t["metadata"].name == tool_name), None)
if tool_info:
# 解析工具参数
tool_args = json.loads

365

被折叠的 条评论
为什么被折叠?



