多工具 Agent 实战:当 Function Calling 不够用时,如何编排复杂任务?

4 阅读7分钟

一、引子:一个工具搞不定的场景

上周我遇到一个真实需求:"帮我把明天的会议安排发邮件给团队,同时查一下北京明天的天气,如果下雨就在邮件里加一句提醒带伞。"

这个需求拆开来看很简单——会议安排查日历、天气查 API、邮件发通知,每个都是 Function Calling 的标准用例。但问题在于:这三个工具怎么配合?

单工具 Function Calling 的逻辑是"用户说一句话 → 模型判断调哪个函数 → 返回结果"。但当任务需要多个工具按顺序或条件协作时,简单的 function call 就不够用了。

二、Function Calling 的两层局限

第一层:一次只能调一个工具

大多数 LLM 的 Function Calling 实现,每次请求最多让模型调用一个函数。即使你注册了十几个工具,模型也只能选一个执行。对于"先查天气、再决定邮件内容、最后发送"这种多步流程,就需要多次往返。

第二层:没有"计划"能力

Function Calling 本质是即时响应模式:用户问一句,模型答一句。模型不会主动说:"我需要分三步完成这个任务——第一步查天气,第二步查日程,第三步根据前两步的结果决定邮件内容。"

特性单 Function Calling多工具编排
每次调用数最多 1 个函数可串联/并联多个
执行顺序用户隐式决定Agent 自主规划
中间结果传递不保留可传递到下一步
条件分支不支持根据上一步结果决策
循环/重试不支持可自动重试

多工具 Agent 的核心就是把"即时响应"升级为"任务编排"——让模型不仅知道"调什么",还知道"什么时候调、调完怎么用"。

三、核心实现:一个简单的多工具编排框架

下面我用 Python 实现一个极简多工具编排框架,核心思路是 ReAct 模式(思考-行动-观察循环)

import json
from typing import Any, Callable


class Tool:
    """一个可被 Agent 调用的工具"""
    def __init__(self, name: str, desc: str, fn: Callable, params: dict):
        self.name = name
        self.desc = desc
        self.fn = fn
        self.params = params  # JSON Schema 格式的参数定义


class MultiToolAgent:
    """多工具编排 Agent"""
    def __init__(self, tools: list[Tool]):
        self.tools = {t.name: t for t in tools}

    def plan(self, task: str) -> list[dict]:
        """让 LLM 把任务拆解为多步工具调用计划"""
        prompt = self._build_plan_prompt(task)
        response = call_llm(prompt)  # 调用你的 LLM API
        return json.loads(response)

    def execute(self, plan: list[dict]) -> list[Any]:
        """按计划顺序执行工具调用"""
        results = []
        for step in plan:
            tool = self.tools[step["tool"]]
            result = tool.fn(**step["args"])
            results.append(result)
        return results

    def _build_plan_prompt(self, task: str) -> str:
        tools_desc = "\n".join(
            f"- {t.name}: {t.desc}" for t in self.tools.values()
        )
        return f"""你是一个任务编排助手。以下工具可用:
{tools_desc}

用户任务:{task}

请将任务分解为工具调用步骤,以 JSON 数组返回:
[{{"tool": "工具名", "args": {{"参数名": "值"}}, "depends_on": []}}]

规则:
1. 需要前一步结果的步骤在 depends_on 中标记前一步索引
2. 无依赖的步骤可并行执行
3. 每个步骤说明为何需要这个工具"""

3.1 注册三个业务工具

def get_weather(city: str, date: str) -> dict:
    """模拟天气查询"""
    return {"city": city, "date": date, "condition": "rainy", "temp": 22}


def get_calendar_events(date: str) -> list:
    """模拟日历查询"""
    return [{"time": "10:00", "title": "团队周会"}]


def send_email(to: str, subject: str, body: str) -> dict:
    """模拟发送邮件"""
    print(f"[邮件] 收件人:{to}")
    print(f"[邮件] 主题:{subject}")
    print(f"[邮件] 正文:{body}")
    return {"status": "sent", "to": to}


# 注册工具
tools = [
    Tool("get_weather", "查指定城市某日天气", get_weather,
         {"city": "string", "date": "string"}),
    Tool("get_calendar", "查指定日期的日程安排", get_calendar_events,
         {"date": "string"}),
    Tool("send_email", "发送邮件", send_email,
         {"to": "string", "subject": "string", "body": "string"}),
]

agent = MultiToolAgent(tools)

# Agent 自动生成的计划示例
plan = agent.plan("明天有团队周会,查北京天气,下雨就在邮件里提醒带伞")
# 输出:
# [
#   {"tool": "get_calendar", "args": {"date": "2026-07-08"}, "depends_on": []},
#   {"tool": "get_weather", "args": {"city": "北京", "date": "2026-07-08"}, "depends_on": []},
#   {"tool": "send_email", "args": {"to": "team@company.com", "subject": "明日会议提醒", "body": "..."}, "depends_on": [0, 1]}
# ]

四、关键升级:依赖图与条件执行

上面的框架有一个明显短板:所有步骤都是线性串行,不支持条件分支。 如果天气是晴天,就不需要在邮件里加"带伞提醒"。真正的 Agent 需要动态 Workflow:

class ConditionalStep:
    """条件分支步骤——根据上一步结果决定走哪条路"""
    def __init__(self, condition_fn, if_true: list, if_false: list):
        self.condition_fn = condition_fn
        self.if_true = if_true
        self.if_false = if_false


def execute_with_context(agent, plan, context=None):
    """带上下文传递和条件分支的执行引擎"""
    context = context or {}
    for step in plan:
        if isinstance(step, ConditionalStep):
            result = step.condition_fn(context)
            branch = step.if_true if result else step.if_false
            context.update(execute_with_context(agent, branch, context))
        else:
            tool = agent.tools[step["tool"]]
            # 自动填充依赖步骤的结果到参数
            resolved_args = {}
            for k, v in step["args"].items():
                if isinstance(v, str) and v.startswith("$"):
                    # 从 context 中引用前一步结果
                    resolved_args[k] = context.get(v[1:], v)
                else:
                    resolved_args[k] = v
            result = tool.fn(**resolved_args)
            context[step["tool"]] = result
    return context

条件逻辑方案对比

方案实现成本灵活性适合场景
Prompt 让 LLM 决策简单二选一
硬编码条件分支固定业务流程
动态 Workflow 引擎复杂业务编排
状态机驱动状态确定的流程
规则引擎(如 Drools)大规模业务规则

五、一个月踩坑总结

坑 1:工具返回格式不统一

不同工具的返回值结构五花八门——有的返回 dict、有的返回 list、有的直接 print。下游步骤解析时频繁报错。

解法:统一返回值包装器。

class ToolResult:
    def __init__(self, success: bool, data=None, error=None):
        self.success = success
        self.data = data
        self.error = error

def safe_call(fn, **kwargs):
    try:
        result = fn(**kwargs)
        return ToolResult(success=True, data=result)
    except Exception as e:
        return ToolResult(success=False, error=str(e))

坑 2:工具调用死循环

Agent 在复杂任务中可能反复调用同一个工具(比如查了 5 次天气),浪费 token 和时间。更严重的是,两个工具互相依赖时可能陷入死循环。

解法:添加调用缓存 + 最大步数限制。

class CachedTool(Tool):
    def __init__(self, *args, max_cache_age=300, **kwargs):
        super().__init__(*args, **kwargs)
        self._cache = {}

    def call(self, **params):
        key = json.dumps(params, sort_keys=True)
        if key not in self._cache:
            self._cache[key] = self.fn(**params)
        return self._cache[key]


# 执行时限制最大步数
MAX_STEPS = 20
step_count = 0
while not task_complete and step_count < MAX_STEPS:
    step_count += 1
    # 执行下一步...
if step_count >= MAX_STEPS:
    print("警告:超出最大执行步数,可能陷入循环")

坑 3:幻觉参数

Agent 有时会自行编造不存在的工具参数。比如工具要求参数 city,Agent 传了 location。这在 Prompt 做了详细说明后仍会发生。

解法:在调用前做严格参数校验。

def validate_params(tool: Tool, args: dict) -> bool:
    """校验参数是否符合工具的 JSON Schema"""
    required = [k for k, v in tool.params.items() if v.get("required")]
    for key in required:
        if key not in args:
            print(f"参数校验失败:缺少必填参数 {key}")
            return False
    return True

六、总结

多工具 Agent 不是复杂的技术发明,而是把"一次调用"扩展为"一个流程"。核心三点:

  1. 任务分解:让 LLM 把大任务拆解为多个工具调用步骤
  2. 依赖管理:明确步骤间的先后关系和条件分支
  3. 结果传递:上一步的输出作为下一步的上下文输入

如果你的场景只是"问一个简单问题,返回一个答案",Function Calling 完全够用。但一旦涉及多步操作、条件判断、数据流转——就需要一个编排框架来接管。

下篇预告:当 Agent 里的工具超过 10 个时,如何让模型快速找到最合适的工具?我会介绍 Tool Retrieval(工具检索增强)的思路和实现。


关注我,不错过后续内容。欢迎在评论区聊聊你的 Agent 踩坑经历!

这种"问一句答一句"的模式在简单场景下够用,但你有没有遇到过这种情况:你想让 AI 先查一下数据库里有没有某个用户的订单,有的话再调用物流 API 查快递状态,最后把结果汇总成一条消息发到企业微信。如果用纯 Function Calling,你需要写三段 Prompt 分三次调用,中间还得手动传参。而多工具编排 Agent 可以在一次对话中自动完成这个链条——这就是它真正的价值所在。