Agent 工具调用与 Function Calling:从零设计可控的外部能力接口

深度解析 LLM Agent 工具调用的实现原理:工具描述(Tool Schema)、参数推导、并行调用、结果消化。 涵盖 OpenAI Function Calling、Claude Tool Use、Google Function Declaration 三种主流标准,以及如何在 LangChain 中自定义工具。 附自定义工具注册器、Schema 验证、自动重试的完整代码实现。

工具调用(Function Calling / Tool Use)是 Agent 的"手脚"——它让模型的决策真正落地到可执行的动作。本文拆解工具调用的底层机制、Schema 设计最佳实践,以及跨平台兼容性处理。


1. 工具调用的本质

当用户说 “查一下北京明天天气”,模型需要:

  1. 识别意图:用户想获取天气信息
  2. 匹配工具:存在 get_weather 工具可用
  3. 参数推导:城市 = “北京”,日期 = “明天”
  4. 调用执行:实际调用 API
  5. 结果消化:将 API 返回的 JSON 转化为自然语言回答
用户输入 → 模型推理 → 工具调用决策 → 参数提取 → API 执行 → 结果反馈 → 回答生成

2. 六大工具调用标准对比

标准代表Schema 格式并行调用返回格式
OpenAI Function CallingGPT-4oJSON Schematool_calls id
Claude Tool UseClaude 3.5JSON Schematool_use id
Google Function DeclarationGeminiOpenAPI SchemafunctionCall
Mistral Function CallingMistralJSON Schematool_calls
Llama 3 Tool UseLlama 3.1JSON Schema (简化)⚠️tool_calls
Anthropic Computer UseClaude 3.5伪代码tool_use

2.1 OpenAI Function Calling 详解

from openai import OpenAI
client = OpenAI()

# 定义工具
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的天气信息。当用户询问天气时使用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称"},
                    "date": {"type": "string", "description": "日期(YYYY-MM-DD)"},
                },
                "required": ["city", "date"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "send_email",
            "description": "发送一封邮件。",
            "parameters": {
                "type": "object",
                "properties": {
                    "to": {"type": "string"},
                    "subject": {"type": "string"},
                    "body": {"type": "string"},
                },
                "required": ["to", "subject", "body"],
            },
        },
    },
]

# 第一轮对话:请求工具调用
messages = [{"role": "user", "content": "北京明天天气怎么样?"}]
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice="auto",  # auto | none | {"type": "function", "function": {"name": "xxx"}}
)

# model 决定调用 get_weather
print(response.choices[0].message)  # role=assistant, content=None, tool_calls=[...]

# 解析工具调用
tool_call = response.choices[0].message.tool_calls[0]
print(f"调用的工具: {tool_call.function.name}")
print(f"参数: {tool_call.function.arguments}")

# 执行工具(模拟)
result = {"temp": 28, "weather": "晴", "humidity": 45}

# 第二轮:将结果归还模型
messages.append(response.choices[0].message)  # 把模型请求加回去
messages.append({
    "role": "tool",
    "tool_call_id": tool_call.id,
    "name": tool_call.function.name,
    "content": str(result),
})

final = client.chat.completions.create(model="gpt-4o", messages=messages, tools=tools)
print(final.choices[0].message.content)  # "北京明天晴,28度..."

2.2 Claude Tool Use 详解

from anthropic import Anthropic
client = Anthropic()

# Claude 的工具声明与 OpenAI 几乎相同
tools = [
    {
        "name": "get_weather",
        "description": "获取天气信息",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "date": {"type": "string"},
            },
            "required": ["city"],
        },
    },
]

response = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=1024,
    messages=[{"role": "user", "content": "北京明天天气"}],
    tools=tools,
)

# 检测是否使用了工具
if response.stop_reason == "tool_use":
    tool_use = response.content[-1]  # 最后一条 content block 是 tool_use
    print(f"Claude 想调用: {tool_use.name},参数: {tool_use.input}")

3. Schema 设计最佳实践

3.1 description 是关键

模型通过 description 决定选哪个工具。这 100 字决定了成败:

# ❌ 差的描述
"description": "搜索功能"

# ✅ 好的描述
"description": "在商品数据库中根据关键词搜索商品信息并返回符合条件的 SKU 列表。适用于用户想要查找特定商品的讲候。不支持搜索用户个人信息。"

Schema Tips:

  • 在 description 中明确包含"何时用这个工具"(触发条件)
  • 提供约束条件:“日期格式必须是 YYYY-MM-DD”
  • 提供枚举值:如果 city 只能是几个明确值,用 enum
  • 给出典型例子:使用 examples 字段辅助理解

3.2 参数命名

# ❌ 模糊命名
"from": str    # 从哪里?时间?地点?

# ✅ 自解释命名
"start_date": str
"origin_city": str

3.3 幂等工具

标记哪些工具是幂等的(可安全重复调用):

{
    "name": "get_user_profile",
    "description": "获取用户信息(幂等操作)。",
    "parameters": {...},
    "strict": True,  # OpenAI 参数校验强化
}

3.4 限制并发

from typing import Callable, Dict
import asyncio

class RateLimitedTool:
    def __init__(self, fn: Callable, max_concurrent: int = 5):
        self.fn = fn
        self.semaphore = asyncio.Semaphore(max_concurrent)

    async def __call__(self, **kwargs):
        async with self.semaphore:
            return await self.fn(**kwargs)

4. 跨平台兼容层

class ToolRegistry:
    """跨 LLM 平台的工具注册器"""
    def __init__(self):
        self._tools: Dict[str, Callable] = {}

    def register(self, name: str, description: str, fn: Callable, schema: dict):
        self._tools[name] = {
            "fn": fn,
            "description": description,
            "schema": schema,
        }
        return self

    def to_openai_format(self):
        return [
            {
                "type": "function",
                "function": {
                    "name": name,
                    "description": meta["description"],
                    "parameters": meta["schema"],
                },
            }
            for name, meta in self._tools.items()
        ]

    def to_claude_format(self):
        return [
            {
                "name": name,
                "description": meta["description"],
                "input_schema": meta["schema"],
            }
            for name, meta in self._tools.items()
        ]

    def execute(self, tool_name: str, arguments: dict):
        if tool_name not in self._tools:
            raise ValueError(f"未知工具: {tool_name}")
        # Schema 校验
        import jsonschema
        jsonschema.validate(arguments, self._tools[tool_name]["schema"])
        return self._tools[tool_name]["fn"](**arguments)


# 使用
registry = ToolRegistry()
registry.register(
    name="get_weather",
    description="获取城市天气(当用户询问天气时使用)",
    fn=lambda city, date: {"temp": 28, "weather": "晴"},
    schema={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
)

# OpenAI 格式
openai_tools = registry.to_openai_format()
# Claude 格式
claude_tools = registry.to_claude_format()

5. 高级:自动工具发现

不需要手动注册所有工具——让 Agent 自己发现:

import inspect

class AutoToolDiscovery:
    def __init__(self):
        self.registry = {}

    def discover(self, module):
        """扫描模块中所有 @tool 装饰的函数"""
        for name, obj in inspect.getmembers(module, inspect.isfunction):
            if hasattr(obj, "_tool_schema"):
                self.registry[name] = {
                    "fn": obj,
                    "schema": obj._tool_schema,
                    "desc": obj._tool_desc,
                }

# 装饰器用法
def tool(description, schema):
    def decorator(fn):
        fn._tool_schema = schema
        fn._tool_desc = description
        return fn
    return decorator

# 应用
@tool(
    description="获取天气信息",
    schema={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
)
def get_weather(city: str):
    return f"{city} 今天晴,25度"

6. 错误处理与降级

class ResilientToolExecutor:
    def execute(self, tool_name, arguments, fallback=None):
        try:
            result = self.registry.execute(tool_name, arguments)
            return {"status": "success", "data": result}
        except jsonschema.ValidationError as e:
            return {
                "status": "validation_error",
                "message": f"参数错误: {e.message}",
                "suggestion": f"请确保按此格式调用: {json.dumps(self.registry.get_schema(tool_name))}",
            }
        except Exception as e:
            if fallback:
                return fallback(arguments)
            return {"status": "error", "message": str(e)}

7. 总结与选型建议

场景推荐标准理由
快速开发 MVPOpenAI Function Calling文档最全、生态最广
长上下文 + 多模态Claude Tool Use200K 上下文,多模态优秀
控制成本 + 多 providerMistral Tool UseAPI 成本最低
私有化部署Llama 3.1 + JSON Schema开源,离线运行

📂 继续阅读:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「llm」更多文章