一、引子:一个工具搞不定的场景
上周我遇到一个真实需求:"帮我把明天的会议安排发邮件给团队,同时查一下北京明天的天气,如果下雨就在邮件里加一句提醒带伞。"
这个需求拆开来看很简单——会议安排查日历、天气查 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 不是复杂的技术发明,而是把"一次调用"扩展为"一个流程"。核心三点:
- 任务分解:让 LLM 把大任务拆解为多个工具调用步骤
- 依赖管理:明确步骤间的先后关系和条件分支
- 结果传递:上一步的输出作为下一步的上下文输入
如果你的场景只是"问一个简单问题,返回一个答案",Function Calling 完全够用。但一旦涉及多步操作、条件判断、数据流转——就需要一个编排框架来接管。
下篇预告:当 Agent 里的工具超过 10 个时,如何让模型快速找到最合适的工具?我会介绍 Tool Retrieval(工具检索增强)的思路和实现。
关注我,不错过后续内容。欢迎在评论区聊聊你的 Agent 踩坑经历!
这种"问一句答一句"的模式在简单场景下够用,但你有没有遇到过这种情况:你想让 AI 先查一下数据库里有没有某个用户的订单,有的话再调用物流 API 查快递状态,最后把结果汇总成一条消息发到企业微信。如果用纯 Function Calling,你需要写三段 Prompt 分三次调用,中间还得手动传参。而多工具编排 Agent 可以在一次对话中自动完成这个链条——这就是它真正的价值所在。