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 Schema✅tool_calls id
Claude Tool UseClaude 3.5JSON Schema✅tool_use id
Google Function DeclarationGeminiOpenAPI Schema✅functionCall
Mistral Function CallingMistralJSON Schema✅tool_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()

4.5 多工具并行调用编排

import asyncio
from typing import Any

class ParallelToolExecutor:
    """并行执行多个无依赖工具,有依赖时按 DAG 拓扑排序执行。"""

    async def execute_parallel(self, tool_calls: list[dict]) -> list[dict]:
        """并行执行所有工具调用。"""
        tasks = [
            self._execute_single(call["name"], call["arguments"])
            for call in tool_calls
        ]
        results = await asyncio.gather(*tasks, return_exceptions=True)

        return [
            {"tool": call["name"], "result": res if not isinstance(res, Exception) else {"error": str(res)}}
            for call, res in zip(tool_calls, results)
        ]

    async def execute_dag(self, dag: dict[str, Any]) -> dict[str, Any]:
        """按依赖关系图执行工具(简化版)。"""
        # dag = {
        #   "weather": {"tool": "get_weather", "args": {...}, "deps": []},
        #   "route": {"tool": "plan_route", "args": {...}, "deps": ["weather"]},
        # }
        completed = {}
        pending = set(dag.keys())

        while pending:
            # 找到所有依赖已满足的任务
            ready = {
                k for k in pending
                if all(d in completed for d in dag[k].get("deps", []))
            }
            if not ready:
                raise ValueError("DAG 存在循环依赖或缺失依赖")

            # 并行执行就绪任务
            tasks = {
                k: asyncio.create_task(
                    self._execute_single(dag[k]["tool"], dag[k]["args"])
                )
                for k in ready
            }
            for k, task in tasks.items():
                completed[k] = await task
                pending.remove(k)

        return completed

    async def _execute_single(self, name: str, args: dict) -> Any:
        # 实际执行...
        pass

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)}

6.5 ReAct 循环:推理+行动+观察

class ReActAgent:
    """
    ReAct (Reasoning + Acting) 循环:
    Thought → Action → Observation → Thought → ... → Answer
    """
    def __init__(self, llm_client, tools: ToolRegistry):
        self.llm = llm_client
        self.tools = tools
        self.max_iterations = 10

    async def run(self, query: str) -> str:
        history = []
        for i in range(self.max_iterations):
            # 构造 ReAct Prompt
            prompt = self._build_react_prompt(query, history)
            response = await self.llm.complete(prompt)

            # 解析 Thought 和 Action
            thought, action = self._parse_response(response)

            if action is None:
                # 没有 Action,直接给出最终答案
                return thought

            # 执行工具
            tool_name = action["name"]
            tool_args = action["arguments"]
            try:
                observation = self.tools.execute(tool_name, tool_args)
            except Exception as e:
                observation = f"Error: {e}"

            history.append({
                "thought": thought,
                "action": action,
                "observation": observation,
            })

        return "达到最大迭代次数,未找到答案。"

    def _build_react_prompt(self, query: str, history: list) -> str:
        tool_desc = "\n".join(
            f"- {name}: {meta['description']}"
            for name, meta in self.tools._tools.items()
        )

        history_str = "\n".join(
            f"Thought: {h['thought']}\n"
            f"Action: {json.dumps(h['action'])}\n"
            f"Observation: {h['observation']}"
            for h in history
        )

        return f"""你是一个智能助手,可以使用以下工具:
{tool_desc}

请按照以下格式回答问题:
Thought: 你的思考过程
Action: {{"name": "工具名", "arguments": {{...}}}}
Observation: 工具执行结果
...(重复直到找到答案)

问题:{query}
{history_str}
Thought:"""

    def _parse_response(self, response: str) -> tuple[str, dict | None]:
        # 简化解析:提取 Action JSON
        import re
        action_match = re.search(r'Action:\s*(\{.*?\})', response, re.DOTALL)
        if action_match:
            thought = response.split("Action:")[0].replace("Thought:", "").strip()
            return thought, json.loads(action_match.group(1))
        return response, None

6.6 人类在环确认(Human-in-the-Loop)

class HumanConfirmedToolExecutor:
    """
    高风险工具(如发送邮件、转账、删除数据)需要人类确认。
    """
    HIGH_RISK_TOOLS = {"send_email", "transfer_money", "delete_user", "deploy_production"}

    async def execute(self, tool_name: str, arguments: dict, user_session: str) -> dict:
        if tool_name in self.HIGH_RISK_TOOLS:
            # 暂停执行,等待人类确认
            confirmation_id = await self.request_human_confirmation(
                user_session, tool_name, arguments
            )
            return {
                "status": "awaiting_confirmation",
                "confirmation_id": confirmation_id,
                "message": f"请确认是否执行 {tool_name}:{json.dumps(arguments, ensure_ascii=False)}",
            }

        # 低风险工具直接执行
        return await self._execute_tool(tool_name, arguments)

    async def confirm(self, confirmation_id: str, approved: bool) -> dict:
        pending = self.pending_confirmations.pop(confirmation_id, None)
        if not pending:
            return {"status": "error", "message": "确认请求已过期"}

        if approved:
            return await self._execute_tool(pending["tool"], pending["args"])
        return {"status": "denied", "message": "用户拒绝了操作"}

7. 总结与选型建议

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

📂 继续阅读:

继续阅读

探索更多技术文章

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

全部文章 返回首页

「llm」更多文章

  1. AI Agent 产品设计方法论:从交互范式到产品落地的完整框架
  2. 大模型本地部署与私有托管:从 Ollama 到 vLLM 生产级推理集群
  3. LLM 商业化应用开发与产品化方法论:从价值验证到规模化盈利