Tool Use / Function Calling(工具调用)模式
概述
Tool Use(也称 Function Calling)是 Agent 的基础能力模式。Agent 不再仅仅生成文本,而是能够识别何时需要调用外部工具,并正确构建调用参数,将工具返回的结果融入后续推理中。这是 Agent 区别于普通 LLM 的核心能力。
原理
flowchart TD
用户输入 --> LLM判断["LLM 判断"]
LLM判断 -->|"需要工具?"| 判断结果{ }
判断结果 -->|"No"| 直接回复
判断结果 -->|"Yes"| 选择工具["选择工具 + 构建参数"]
选择工具 --> 执行工具["执行工具调用"]
执行工具 --> 工具结果["工具返回结果 + LLM 继续推理"]
工具结果 --> 更多工具{需要更多工具?}
更多工具 -->|"Yes"| 选择工具
更多工具 -->|"No"| 生成回复["生成最终回复"]
核心机制:
- 工具定义(Schema):用结构化格式(JSON Schema)描述每个工具的名称、功能、参数
- 工具选择(Selection):LLM 根据用户意图决定调用哪个工具
- 参数提取(Argument Extraction):LLM 从用户输入中提取工具参数
- 结果整合(Integration):将工具返回结果作为上下文,继续推理
使用场景
- 实时信息查询:天气、股价、新闻等需要外部 API 的数据
- 数据库操作:CRUD 操作、SQL 查询
- 数学计算:精确计算(LLM 数学能力不稳定)
- 代码执行:在沙箱中运行代码并获取结果
- 文件操作:读写本地或远程文件
- 第三方服务集成:发送邮件、创建日历事件、操作 IoT 设备
- 知识库 RAG:从向量数据库检索相关文档
示例代码
import json
from typing import List, Dict, Any, Callable, Optional
from dataclasses import dataclass, field
from enum import Enum
class ToolChoice(Enum):
"""工具选择策略"""
AUTO = "auto" # LLM 自动决定
REQUIRED = "required" # 必须使用工具
NONE = "none" # 不使用工具
@dataclass
class ToolParameter:
"""工具参数定义"""
name: str
type: str # "string", "number", "boolean", "object", "array"
description: str
required: bool = True
enum: Optional[List[str]] = None
@dataclass
class Tool:
"""工具定义"""
name: str
description: str
parameters: List[ToolParameter]
function: Callable
category: str = "general" # 工具分类
def to_openai_schema(self) -> Dict:
"""转换为 OpenAI Function Calling 格式"""
properties = {}
required = []
for param in self.parameters:
prop = {
"type": param.type,
"description": param.description,
}
if param.enum:
prop["enum"] = param.enum
properties[param.name] = prop
if param.required:
required.append(param.name)
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": properties,
"required": required,
}
}
}
class ToolRegistry:
"""工具注册中心"""
def __init__(self):
self._tools: Dict[str, Tool] = {}
def register(self, tool: Tool) -> None:
"""注册工具"""
self._tools[tool.name] = tool
print(f"[ToolRegistry] 注册工具: {tool.name}")
def get_schemas(self) -> List[Dict]:
"""获取所有工具的 schema"""
return [tool.to_openai_schema() for tool in self._tools.values()]
def execute(self, name: str, arguments: Dict) -> str:
"""执行工具调用"""
if name not in self._tools:
return json.dumps({"error": f"未知工具: {name}"})
tool = self._tools[name]
try:
result = tool.function(**arguments)
# 确保返回值是字符串
if not isinstance(result, str):
result = json.dumps(result, ensure_ascii=False)
return result
except Exception as e:
return json.dumps({"error": str(e)})
class ToolUseAgent:
"""支持工具调用的 Agent"""
def __init__(self, llm, tool_registry: ToolRegistry):
"""
Args:
llm: 支持 Function Calling 的 LLM
tool_registry: 工具注册中心
"""
self.llm = llm
self.tool_registry = tool_registry
def run(
self,
user_message: str,
system_prompt: str = "",
max_tool_calls: int = 10,
) -> str:
"""
执行 Agent 对话,支持多轮工具调用
"""
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": user_message})
tool_call_count = 0
while tool_call_count < max_tool_calls:
# 调用 LLM(带工具定义)
response = self.llm.chat(
messages=messages,
tools=self.tool_registry.get_schemas(),
tool_choice=ToolChoice.AUTO.value,
)
# LLM 直接回复(不需要工具)
if response.content and not response.tool_calls:
return response.content
# LLM 请求工具调用
if response.tool_calls:
# 将 LLM 的响应加入历史
messages.append({
"role": "assistant",
"content": response.content,
"tool_calls": response.tool_calls,
})
for tool_call in response.tool_calls:
tool_name = tool_call["function"]["name"]
tool_args = json.loads(tool_call["function"]["arguments"])
print(f"[ToolCall] {tool_name}({tool_args})")
# 执行工具
result = self.tool_registry.execute(tool_name, tool_args)
# 将工具结果加入历史
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": result,
})
tool_call_count += 1
return "达到最大工具调用次数限制"
# ========== 工具定义示例 ==========
def search_web(query: str, num_results: int = 5) -> str:
"""模拟网页搜索"""
return json.dumps({
"results": [
{"title": f"搜索结果 {i}: {query}", "url": f"https://example.com/{i}"}
for i in range(num_results)
]
})
def get_weather(city: str, unit: str = "celsius") -> str:
"""获取天气信息"""
# 模拟天气 API
return json.dumps({
"city": city,
"temperature": 22,
"unit": unit,
"condition": "晴天",
"humidity": "65%",
})
def execute_python(code: str) -> str:
"""在沙箱中执行 Python 代码"""
import subprocess
try:
result = subprocess.run(
["python", "-c", code],
capture_output=True,
text=True,
timeout=5,
)
return json.dumps({
"stdout": result.stdout,
"stderr": result.stderr,
"returncode": result.returncode,
})
except subprocess.TimeoutExpired:
return json.dumps({"error": "代码执行超时"})
def query_database(sql: str) -> str:
"""查询数据库"""
# 模拟数据库查询
return json.dumps({
"columns": ["id", "name", "age"],
"rows": [
[1, "张三", 28],
[2, "李四", 35],
],
"row_count": 2,
})
# ========== 组装 Agent ==========
# 创建工具注册中心
registry = ToolRegistry()
registry.register(Tool(
name="search_web",
description="搜索互联网获取实时信息",
parameters=[
ToolParameter("query", "string", "搜索关键词"),
ToolParameter("num_results", "number", "返回结果数量", required=False),
],
function=search_web,
category="search",
))
registry.register(Tool(
name="get_weather",
description="获取指定城市的天气信息",
parameters=[
ToolParameter("city", "string", "城市名称,如 Beijing"),
ToolParameter("unit", "string", "温度单位:celsius 或 fahrenheit", required=False, enum=["celsius", "fahrenheit"]),
],
function=get_weather,
category="utility",
))
registry.register(Tool(
name="execute_python",
description="在安全沙箱中执行 Python 代码并返回结果",
parameters=[
ToolParameter("code", "string", "要执行的 Python 代码"),
],
function=execute_python,
category="code",
))
registry.register(Tool(
name="query_database",
description="执行 SQL 查询",
parameters=[
ToolParameter("sql", "string", "SQL 查询语句"),
],
function=query_database,
category="data",
))
# 创建 Agent
agent = ToolUseAgent(llm=YourLLM(), tool_registry=registry)
# 运行
result = agent.run("北京今天天气怎么样?适合出门吗?")
print(result)
工具设计最佳实践
1. 工具描述要精准
# ❌ 差的描述
Tool(name="search", description="搜索东西", ...)
# ✅ 好的描述
Tool(
name="search_web",
description="使用 Google 搜索引擎搜索互联网上的实时信息,返回相关网页的标题、摘要和 URL",
...
)
2. 参数设计原则
# 使用 enum 约束可选值
ToolParameter("sort_order", "string", "排序方式",
enum=["asc", "desc"], required=False)
# 提供合理的默认值
ToolParameter("page_size", "integer", "每页条数", required=False) # 默认 20
# 参数名用 snake_case,避免歧义
ToolParameter("start_date", "string", "开始日期,格式 YYYY-MM-DD")
3. 工具返回值规范
def my_tool(param: str) -> str:
"""工具函数,始终返回 JSON 字符串"""
return json.dumps({
"success": True, # 明确标识是否成功
"data": {...}, # 实际数据
"error": None, # 错误信息(如有)
"metadata": { # 元数据(可选)
"timestamp": "2024-01-01T00:00:00",
"source": "API",
}
}, ensure_ascii=False)
4. 工具粒度控制
# 避免过于细粒度
Tool(name="set_x", ...)
Tool(name="set_y", ...)
# 也不要过于粗粒度
Tool(name="do_everything", ...)
# 合理的粒度
Tool(name="create_user", ...) # 创建一个用户
Tool(name="update_user_profile", ...) # 更新用户资料
多工具编排模式
class ToolChain:
"""工具链:按顺序执行多个工具"""
def __init__(self, registry: ToolRegistry):
self.registry = registry
def execute(self, chain: List[Dict[str, Any]]) -> List[str]:
results = []
for step in chain:
result = self.registry.execute(step["tool"], step["args"])
results.append(result)
return results
class ToolRouter:
"""工具路由:根据条件选择工具"""
def route(self, intent: str, query: str) -> Optional[str]:
routes = {
"weather": "get_weather",
"search": "search_web",
"code": "execute_python",
"data": "query_database",
}
return routes.get(intent)
优点与局限
| 优点 | 局限 |
|---|---|
| 突破 LLM 的知识截止时间限制 | 工具选择可能出错(幻觉) |
| 执行精确计算和确定性操作 | 参数提取可能不准确 |
| 与现有系统无缝集成 | 工具过多时选择困难 |
| 支持复杂的多步工作流 | 工具返回结果可能被 LLM 误解 |
| 安全可控(沙箱执行) | 需要额外的工具开发和维护成本 |
评论
评论加载中…