第02章:工具调用工程——Function Calling与工具链设计

第02章:工具调用工程——Function Calling与工具链设计

工具是Agent触手可及的世界。工具链设计的好坏,决定了Agent能做什么,以及做得有多可靠。


2.1 Function Calling的底层机制

Function Calling(现在也叫Tool Use)是LLM与外部世界交互的标准接口。

理解底层机制比记住API文档更重要:

# Function Calling的完整请求/响应周期

import openai
import json

client = openai.OpenAI()

# 第1步:发送工具定义 + 用户消息给LLM
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "user", "content": "今天北京的天气怎么样?"}
    ],
    tools=[
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "获取指定城市的当前天气",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": {"type": "string", "description": "城市名"},
                        "unit": {
                            "type": "string",
                            "enum": ["celsius", "fahrenheit"],
                            "description": "温度单位"
                        }
                    },
                    "required": ["city"]
                }
            }
        }
    ],
    tool_choice="auto"
)

# 第2步:检查LLM的决定
message = response.choices[0].message
print(f"finish_reason: {response.choices[0].finish_reason}")
# → "tool_calls" 表示LLM决定调用工具
# → "stop" 表示LLM直接回答

if response.choices[0].finish_reason == "tool_calls":
    tool_call = message.tool_calls[0]
    print(f"工具名称: {tool_call.function.name}")
    print(f"工具参数: {tool_call.function.arguments}")
    # → {"city": "北京", "unit": "celsius"}
    
    # 第3步:执行工具(你的代码)
    args = json.loads(tool_call.function.arguments)
    weather_result = get_weather(**args)  # 实际调用天气API
    
    # 第4步:把工具结果返回给LLM
    final_response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "user", "content": "今天北京的天气怎么样?"},
            message,  # LLM的工具调用请求
            {
                "role": "tool",
                "content": json.dumps(weather_result),
                "tool_call_id": tool_call.id  # 必须对应!
            }
        ]
    )
    print(final_response.choices[0].message.content)

关键理解:LLM本身不执行工具。LLM只是告诉你"我想调用X工具,参数是Y",然后去执行,把结果再喂回给LLM。LLM是指挥官,你的代码是执行者。


2.2 工具设计原则

工具的描述质量直接影响LLM的使用准确性:

# ======= 坏的工具定义 =======
BAD_TOOL = {
    "type": "function",
    "function": {
        "name": "search",                      # 太模糊
        "description": "搜索东西",              # 太模糊,LLM不知道什么时候用
        "parameters": {
            "type": "object",
            "properties": {
                "q": {"type": "string"}        # 变量名不清晰
            },
            "required": ["q"]
        }
    }
}

# ======= 好的工具定义 =======
GOOD_TOOL = {
    "type": "function",
    "function": {
        "name": "search_web",
        "description": """搜索互联网获取最新信息。
        
        适合使用的场景:
        - 需要当前/最新数据(新闻、价格、时间敏感信息)
        - 需要事实核查
        - 需要了解特定网站的信息
        
        不适合使用的场景:
        - 问题可以从已知知识回答
        - 数学计算
        - 代码生成
        """,
        "parameters": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "搜索查询词。建议使用英文以获得更好结果。例如:'Bitcoin price today' 而不是 '比特币今天价格'"
                },
                "time_range": {
                    "type": "string",
                    "enum": ["day", "week", "month", "year"],
                    "description": "时间范围限制。用于获取特定时间段的信息"
                },
                "max_results": {
                    "type": "integer",
                    "description": "返回结果数量,1-10之间,默认3",
                    "minimum": 1,
                    "maximum": 10,
                    "default": 3
                }
            },
            "required": ["query"]
        }
    }
}

工具设计的7个原则

TOOL_DESIGN_PRINCIPLES = [
    "1. 名称要自解释:search_web比search好,send_email比email好",
    "2. 描述要说明用途和边界:什么时候用,什么时候不用",
    "3. 参数名称要清晰:query比q好,recipient_email比to好",
    "4. 枚举类型用enum字段:让LLM知道有哪些合法值",
    "5. 可选参数提供默认值:减少LLM的决策负担",
    "6. 工具职责单一:一个工具做一件事,不做两件事",
    "7. 错误返回要有意义:让LLM知道出了什么问题,而不只是'error'"
]

2.3 构建生产可用的工具框架

from typing import Callable, Any
from dataclasses import dataclass, field
import functools
import time
import logging

logger = logging.getLogger(__name__)

@dataclass
class ToolResult:
    """工具执行结果的标准格式"""
    success: bool
    data: Any = None
    error: str = None
    metadata: dict = field(default_factory=dict)
    
    def to_string(self) -> str:
        if self.success:
            return str(self.data)
        return f"工具执行失败:{self.error}"


class ToolRegistry:
    """
    工具注册表:管理所有工具的注册、查找和执行
    
    支持:
    - 工具注册(装饰器模式)
    - 重试机制
    - 执行日志
    - 权限控制
    """
    
    def __init__(self):
        self._tools: dict[str, Callable] = {}
        self._tool_definitions: list[dict] = []
        self._requires_confirmation: set[str] = set()
    
    def register(
        self, 
        name: str, 
        description: str, 
        parameters: dict,
        requires_confirmation: bool = False,
        max_retries: int = 3
    ):
        """注册工具的装饰器"""
        def decorator(func: Callable):
            @functools.wraps(func)
            def wrapper(*args, **kwargs) -> ToolResult:
                for attempt in range(max_retries):
                    try:
                        start_time = time.time()
                        result = func(*args, **kwargs)
                        elapsed = time.time() - start_time
                        
                        logger.info(f"工具 {name} 执行成功,耗时 {elapsed:.2f}s")
                        
                        return ToolResult(
                            success=True,
                            data=result,
                            metadata={"elapsed_seconds": elapsed, "attempts": attempt + 1}
                        )
                    except Exception as e:
                        logger.warning(f"工具 {name} 第{attempt+1}次失败:{e}")
                        if attempt == max_retries - 1:
                            return ToolResult(
                                success=False,
                                error=str(e),
                                metadata={"attempts": attempt + 1}
                            )
                        time.sleep(2 ** attempt)  # 指数退避
            
            self._tools[name] = wrapper
            self._tool_definitions.append({
                "type": "function",
                "function": {
                    "name": name,
                    "description": description,
                    "parameters": parameters
                }
            })
            
            if requires_confirmation:
                self._requires_confirmation.add(name)
            
            return wrapper
        return decorator
    
    def execute(self, tool_name: str, tool_args: dict, 
                confirmation_callback=None) -> ToolResult:
        """执行工具"""
        if tool_name not in self._tools:
            return ToolResult(success=False, error=f"工具 {tool_name} 不存在")
        
        # 需要确认的工具
        if tool_name in self._requires_confirmation:
            if confirmation_callback:
                confirmed = confirmation_callback(tool_name, tool_args)
                if not confirmed:
                    return ToolResult(
                        success=False, 
                        error="用户拒绝了操作"
                    )
        
        return self._tools[tool_name](**tool_args)
    
    @property
    def definitions(self) -> list[dict]:
        """获取所有工具的OpenAI格式定义"""
        return self._tool_definitions


# ====== 使用示例:注册工具 ======

registry = ToolRegistry()

@registry.register(
    name="web_search",
    description="搜索互联网获取最新信息",
    parameters={
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "搜索查询词"},
            "max_results": {"type": "integer", "default": 3}
        },
        "required": ["query"]
    }
)
def web_search(query: str, max_results: int = 3) -> dict:
    """实际的搜索实现"""
    import requests
    response = requests.post(
        "https://api.tavily.com/search",
        json={"api_key": "...", "query": query, "max_results": max_results}
    )
    return response.json()


@registry.register(
    name="send_email",
    description="发送电子邮件给指定收件人",
    parameters={
        "type": "object",
        "properties": {
            "to": {"type": "string", "description": "收件人邮箱"},
            "subject": {"type": "string", "description": "邮件主题"},
            "body": {"type": "string", "description": "邮件正文"}
        },
        "required": ["to", "subject", "body"]
    },
    requires_confirmation=True  # 发邮件需要用户确认!
)
def send_email(to: str, subject: str, body: str) -> str:
    """实际的发邮件实现"""
    # 使用Resend/SendGrid等发送
    return f"邮件已发送给 {to}"

2.4 并行工具调用

GPT-4o支持在一次响应中调用多个工具(并行执行):

import asyncio
from openai import AsyncOpenAI

async def execute_parallel_tools(tool_calls: list, registry: ToolRegistry):
    """并行执行多个工具调用"""
    
    async def execute_single(tool_call):
        tool_name = tool_call.function.name
        tool_args = json.loads(tool_call.function.arguments)
        
        # 在线程池中执行(避免阻塞事件循环)
        result = await asyncio.to_thread(
            registry.execute, tool_name, tool_args
        )
        
        return {
            "tool_call_id": tool_call.id,
            "result": result
        }
    
    # 所有工具并行执行
    results = await asyncio.gather(*[
        execute_single(tc) for tc in tool_calls
    ])
    
    return results


class AsyncAgent:
    """支持并行工具调用的Agent"""
    
    def __init__(self, registry: ToolRegistry, model: str = "gpt-4o"):
        self.client = AsyncOpenAI()
        self.registry = registry
        self.model = model
    
    async def run(self, user_message: str) -> str:
        messages = [{"role": "user", "content": user_message}]
        
        for _ in range(10):  # max iterations
            response = await self.client.chat.completions.create(
                model=self.model,
                messages=messages,
                tools=self.registry.definitions,
                tool_choice="auto"
            )
            
            message = response.choices[0].message
            
            if not message.tool_calls:
                return message.content
            
            messages.append(message)
            
            # 并行执行所有工具调用
            tool_results = await execute_parallel_tools(
                message.tool_calls, self.registry
            )
            
            # 把所有工具结果追加到对话
            for tr in tool_results:
                messages.append({
                    "role": "tool",
                    "content": tr["result"].to_string(),
                    "tool_call_id": tr["tool_call_id"]
                })
        
        return "达到最大迭代次数"

2.5 常用工具库:开箱即用的工具集

# 高价值的工具组合

ESSENTIAL_TOOLS = {
    "web": {
        "tavily_search": "最适合Agent的搜索API(专为LLM优化的结果格式)",
        "firecrawl": "网页全文抓取(Markdown格式,适合RAG)",
        "playwright": "浏览器自动化(填表、点击、截图)"
    },
    "code": {
        "e2b": "云端代码执行沙箱(安全的代码运行环境)",
        "local_python": "本地Python执行(需要严格输入验证)"
    },
    "data": {
        "pandas": "数据分析和处理",
        "sql_executor": "执行SQL查询(连接到你的数据库)"
    },
    "communication": {
        "resend": "发送邮件",
        "slack_api": "发送Slack消息",
        "telegram_bot": "Telegram消息"
    },
    "files": {
        "read_file": "读取文件内容",
        "write_file": "写入文件",
        "pdf_parser": "解析PDF(PyMuPDF/pdfplumber)"
    }
}

# 使用E2B安全执行代码的示例
from e2b_code_interpreter import Sandbox

def safe_code_execution(code: str, timeout: int = 30) -> str:
    """
    在隔离沙箱中执行Python代码
    E2B提供完全隔离的云端Python环境
    """
    with Sandbox() as sandbox:
        execution = sandbox.run_code(code, timeout=timeout)
        
        if execution.error:
            return f"代码执行错误:{execution.error.name}\n{execution.error.value}"
        
        output_parts = []
        for output in execution.results:
            if hasattr(output, 'text'):
                output_parts.append(output.text)
            elif hasattr(output, 'png'):
                # 图表输出
                output_parts.append("[图表已生成,保存为PNG]")
        
        if execution.logs.stdout:
            output_parts.extend(execution.logs.stdout)
        
        return "\n".join(output_parts) if output_parts else "代码执行完成(无输出)"

本章小结

  1. Function Calling的底层:LLM只是"声明"要调用什么,你的代码才是执行者
  2. 工具描述质量直接影响LLM的使用准确性:描述用途、边界、参数含义
  3. 生产工具框架需要:注册表管理、重试机制、执行日志、权限控制
  4. 并行工具调用可以显著减少延迟(GPT-4o支持一次响应包含多个工具调用)
  5. 危险操作(发邮件/删除数据)必须有用户确认步骤

行动项:用ToolRegistry注册3个工具(搜索、计算器、文件读取),构建一个可以回答"分析这个CSV文件的销售数据趋势"的Agent。