跳转至

API 编排与工具选择

引言

当 Agent 拥有大量工具时,如何选择正确的工具、以正确的顺序调用、并处理调用中的错误,成为一个关键的工程问题。本节探讨工具选择算法、编排模式和错误处理策略。

工具选择问题

挑战

  • 工具数量过多:上百个工具时,LLM 难以在上下文中处理所有工具定义
  • 工具语义重叠:多个工具功能相似,需要精确区分
  • 上下文消耗:每个工具定义消耗数百 token
  • 选择准确性:选错工具导致任务失败

工具选择策略

1. 全量加载(适合工具少的场景)

# 工具 < 20 个时,直接全部传递给 LLM
response = llm.chat(
    messages=messages,
    tools=all_tools,  # 全部工具定义
)

2. 语义路由(Semantic Routing)

基于用户查询的语义,预先筛选相关工具:

class ToolRouter:
    def __init__(self, tools, embedding_model):
        self.tools = tools
        self.embed = embedding_model

        # 预计算所有工具描述的嵌入
        self.tool_embeddings = {
            tool["name"]: self.embed(tool["description"])
            for tool in tools
        }

    def select_tools(self, query, top_k=5):
        """基于语义相似度选择最相关的工具"""
        query_embedding = self.embed(query)

        scores = {}
        for name, emb in self.tool_embeddings.items():
            scores[name] = cosine_similarity(query_embedding, emb)

        # 返回最相关的 top_k 个工具
        sorted_tools = sorted(scores.items(), key=lambda x: x[1], reverse=True)
        selected_names = [name for name, score in sorted_tools[:top_k]]

        return [t for t in self.tools if t["name"] in selected_names]

# 使用
router = ToolRouter(all_tools, embedding_model)
relevant_tools = router.select_tools("帮我查一下北京的天气")
response = llm.chat(messages=messages, tools=relevant_tools)

3. 分类路由

使用 LLM 先分类,再加载对应类别的工具:

TOOL_CATEGORIES = {
    "information_retrieval": ["web_search", "knowledge_base_query", "database_query"],
    "data_analysis": ["execute_python", "create_chart", "statistical_test"],
    "communication": ["send_email", "send_slack", "create_document"],
    "file_operations": ["read_file", "write_file", "list_directory"],
}

def category_routing(query, llm):
    """先分类再选择工具"""
    category = llm.classify(
        f"将以下查询分类到工具类别中:{list(TOOL_CATEGORIES.keys())}\n"
        f"查询: {query}\n类别:"
    )
    return TOOL_CATEGORIES.get(category, [])

4. 两阶段选择

先用轻量模型筛选,再用强模型精确选择:

def two_stage_selection(query, all_tools):
    # Stage 1: 轻量模型快速筛选(成本低)
    candidates = light_model.select(
        query=query,
        tools=[{"name": t["name"], "description": t["description"][:100]} for t in all_tools],
        top_k=10,
    )

    # Stage 2: 强模型精确选择(带完整定义)
    selected = strong_model.select(
        query=query,
        tools=[t for t in all_tools if t["name"] in candidates],
    )
    return selected

工具链模式(Tool Chaining)

顺序链(Sequential Chain)

# 工具 A 的输出作为工具 B 的输入
async def sequential_chain(query):
    # Step 1: 搜索
    search_results = await search_tool(query)

    # Step 2: 用搜索结果查数据库
    db_results = await database_query(extract_entities(search_results))

    # Step 3: 分析
    analysis = await code_executor(generate_analysis_code(db_results))

    return analysis

并行扇出(Fan-out)

import asyncio

async def fan_out(query):
    """并行调用多个工具,汇总结果"""
    tasks = [
        web_search(query),
        knowledge_base_search(query),
        database_query(query),
    ]

    results = await asyncio.gather(*tasks, return_exceptions=True)

    # 过滤错误
    valid_results = [r for r in results if not isinstance(r, Exception)]

    return merge_results(valid_results)

条件分支

async def conditional_routing(query, context):
    """根据条件选择不同的工具链"""
    intent = classify_intent(query)

    if intent == "factual_question":
        # 知识检索链
        docs = await knowledge_base_search(query)
        return generate_answer(query, docs)

    elif intent == "data_analysis":
        # 数据分析链
        data = await database_query(extract_sql(query))
        code = generate_analysis_code(data)
        return await code_executor(code)

    elif intent == "action_request":
        # 执行操作链
        plan = plan_actions(query)
        results = []
        for step in plan:
            result = await execute_action(step)
            results.append(result)
            if result.get("error"):
                break
        return summarize_results(results)

递归工具使用

Agent 在执行中发现需要额外工具调用:

async def recursive_tool_use(query, tools, llm, depth=0, max_depth=5):
    """递归工具调用"""
    if depth >= max_depth:
        return "达到最大递归深度"

    response = await llm.chat(
        messages=[{"role": "user", "content": query}],
        tools=tools
    )

    if not response.tool_calls:
        return response.content

    # 执行工具调用
    results = []
    for tc in response.tool_calls:
        result = await execute_tool(tc.name, tc.arguments)
        results.append(result)

    # 将结果反馈给 LLM,可能触发更多工具调用
    follow_up = format_results(results)
    return await recursive_tool_use(follow_up, tools, llm, depth + 1, max_depth)

错误处理与重试

错误分类

错误类型 示例 处理策略
参数错误 缺少必填参数 让 LLM 修正参数
认证失败 API Key 过期 刷新认证后重试
速率限制 429 Too Many Requests 指数退避重试
服务不可用 500 Server Error 等待后重试或降级
逻辑错误 查询无结果 改写查询或换工具
超时 执行时间过长 超时重试或简化请求

重试策略

import asyncio
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=1, max=10),
)
async def resilient_tool_call(tool_name, arguments):
    """带重试的工具调用"""
    return await execute_tool(tool_name, arguments)

class SmartRetry:
    def __init__(self, llm):
        self.llm = llm

    async def execute_with_recovery(self, tool_name, arguments, error=None):
        """智能错误恢复"""
        if error:
            # 让 LLM 分析错误并修正参数
            fix = await self.llm.generate(
                f"工具 {tool_name} 调用失败。\n"
                f"参数: {arguments}\n"
                f"错误: {error}\n"
                f"请修正参数或建议替代方案。"
            )
            arguments = fix.get("corrected_arguments", arguments)

        try:
            return await execute_tool(tool_name, arguments)
        except Exception as e:
            if self.is_retryable(e):
                return await self.execute_with_recovery(tool_name, arguments, str(e))
            raise

    def is_retryable(self, error):
        retryable_codes = [429, 500, 502, 503, 504]
        return getattr(error, 'status_code', None) in retryable_codes

降级策略

class ToolWithFallback:
    def __init__(self, primary_tool, fallback_tools):
        self.primary = primary_tool
        self.fallbacks = fallback_tools

    async def execute(self, arguments):
        """主工具失败时尝试备用工具"""
        try:
            return await self.primary(arguments)
        except Exception as primary_error:
            for fallback in self.fallbacks:
                try:
                    return await fallback(arguments)
                except:
                    continue
            raise primary_error

# 示例:搜索工具的降级链
search = ToolWithFallback(
    primary_tool=google_search,
    fallback_tools=[bing_search, brave_search, duckduckgo_search]
)

工具使用决策框架

何时使用何种工具

TOOL_DECISION_TREE = """
用户查询 → 
├─ 需要最新信息? → web_search
├─ 需要内部知识? → knowledge_base_search  
├─ 需要精确计算? → code_interpreter
├─ 需要数据分析? → code_interpreter + data_tools
├─ 需要执行操作? → 
│  ├─ 发送消息? → email/slack_tool
│  ├─ 文件操作? → file_tools
│  └─ 系统操作? → bash_tool
├─ 需要多步推理? → 组合多个工具
└─ 纯知识问答? → 不使用工具,直接回答
"""

实践建议

编排清单

  • [ ] 实现工具路由减少无关工具的干扰
  • [ ] 为每个工具定义清晰的使用场景和排除条件
  • [ ] 实现错误处理和重试机制
  • [ ] 设置工具调用的超时和资源限制
  • [ ] 添加工具使用的日志和监控
  • [ ] 对高风险工具添加人工确认步骤

性能优化

  • 并行调用独立的工具减少延迟
  • 缓存常用工具的结果
  • 使用轻量模型做预筛选
  • 对工具结果进行截断避免上下文浪费

延伸阅读

  • 框架选型综述 - Agent 框架如何处理工具编排
  • Qin, Y., et al. (2024). "Tool Learning with Large Language Models: A Survey"
  • Hao, S., et al. (2024). "ToolkenGPT: Augmenting Frozen Language Models with Massive Tools via Tool Embeddings"

评论 #