深入 ‘Multi-step Tool Use’:解析 LLM 如何在单次推理中生成一个‘工具调用序列’以提升效率

各位编程爱好者、AI领域的探索者们,大家好!

我是你们今天的讲师,一名深耕编程多年的老兵。今天,我们将深入探讨一个在大型语言模型(LLM)应用中极具颠覆性的技术主题:如何在单次推理中生成一个“工具调用序列”以大幅提升效率,也就是我们常说的“Multi-step Tool Use”的高级形态。

在LLM与外部工具结合的浪潮中,效率始终是核心挑战。传统的工具调用模式虽然强大,但其固有的串行、往复式交互机制,往往导致用户体验不佳,资源消耗巨大。而今天,我们将剖析LLM如何通过一次深思熟虑的“规划”,输出一系列工具操作指令,从而将复杂任务的执行效率推向一个新的高度。


1. 引言:工具使用的演进与效率瓶颈

大型语言模型的能力令人惊叹,它们不仅能理解和生成自然语言,还能通过“工具使用”(Tool Use,或称Function Calling)与外部世界交互。这使得LLM不再是一个孤立的语言处理单元,而是一个能够执行实际任务的智能代理。

最初的工具使用模式相对简单:

  1. 用户提出一个请求。
  2. LLM分析请求,识别出需要调用的工具及其参数。
  3. LLM生成一个单个工具调用指令。
  4. 宿主系统(Host System)接收指令,执行工具。
  5. 工具执行结果返回给宿主系统。
  6. 宿主系统将结果以及原始上下文一并反馈给LLM。
  7. LLM基于新的上下文,决定是继续调用下一个工具,还是生成最终答案。

这种模式的优点是逻辑清晰,易于实现。但随着任务复杂度的提升,其固有的效率瓶颈日益凸显。想象一下,一个需要多个步骤才能完成的任务,例如“查找明天北京的天气,然后如果下雨就发送一封邮件提醒老板,并取消明天的午餐预订”。按照传统模式,这可能需要LLM进行至少3-4次独立的推理,每次推理都需要完整的上下文传输、网络延迟以及模型计算时间。这不仅耗时,而且每次上下文的传递都会增加令牌(token)的使用量,从而带来额外的成本。

我们的目标,正是要打破这种低效的循环。


2. 传统工具使用模式的效率困境

为了更直观地理解传统模式的瓶颈,我们来描绘一下其典型的交互流程。

图1:传统单步工具调用流程

步骤 参与者 操作 描述
1 用户 发送请求 "请帮我查明天北京的天气,如果下雨就发邮件提醒我。"
2 宿主系统 -> LLM 上下文构建与请求发送 将用户请求和可用工具定义发送给LLM。
3 LLM 推理 1 分析请求,识别出需要 get_weather 工具。
4 LLM -> 宿主系统 生成工具调用指令 1 {"tool_name": "get_weather", "arguments": {"city": "北京", "date": "明天"}}
5 宿主系统 执行工具 1 调用 get_weather 函数,获取天气信息。
6 宿主系统 -> LLM 上下文构建与结果反馈 将工具 1 的执行结果(例如:{"weather": "雨夹雪"})和原始上下文发送给LLM。
7 LLM 推理 2 分析天气结果,识别出需要 send_email 工具。
8 LLM -> 宿主系统 生成工具调用指令 2 {"tool_name": "send_email", "arguments": {"to": "[email protected]", "subject": "下雨提醒", "body": "明天北京有雨,请注意出行。"}}
9 宿主系统 执行工具 2 调用 send_email 函数。
10 宿主系统 -> LLM 上下文构建与结果反馈 将工具 2 的执行结果(例如:{"status": "success"})和原始上下文发送给LLM。
11 LLM 推理 3 分析所有信息,确认任务完成。
12 LLM -> 宿主系统 生成最终答案 "已为您查询到明天北京有雨,并已发送邮件提醒。"

为什么低效?

  • 多次LLM API调用: 每次LLM推理都意味着一次API请求,涉及网络传输延迟。
  • 重复上下文传输: 每次调用都需要将完整的会话历史、工具定义以及之前的工具执行结果作为上下文发送给LLM,这会消耗大量的带宽,并增加令牌使用量。
  • 高昂的推理成本: 每次LLM推理都需要计算资源,对于复杂的模型而言,这笔开销不容小觑。
  • 用户感知延迟: 用户必须等待每次LLM的往返响应,这使得整个交互过程显得迟缓。

这些问题在简单的任务中可能不明显,但在需要3个、5个甚至更多工具调用的复杂工作流中,累积的延迟和成本将变得无法接受。


3. 核心概念:单次推理中的多步工具使用

“单次推理中的多步工具使用”旨在解决上述效率问题。其核心思想是:LLM在接收到用户请求和可用工具定义后,通过一次性、深入的“规划”和“推理”,直接生成一个包含多个工具调用的序列,而无需等待任何工具的实际执行结果。

这个序列,本质上是LLM对完成用户请求所需步骤的“预判”和“编排”。宿主系统收到这个序列后,可以按照LLM的规划,依次或并行地执行这些工具,并在序列执行完毕后,再将所有结果汇总,决定是否需要再次与LLM交互。

工作原理简述:

  1. 用户查询与工具定义作为输入: 宿主系统将用户请求和所有可用工具的定义(包括它们的名称、描述和参数)发送给LLM。
  2. LLM的“全局规划”: LLM不只是考虑下一步,而是基于其训练数据中包含的复杂任务分解模式,尝试对整个任务进行端到端规划。它会预测完成任务可能需要哪些工具,它们的调用顺序,以及每个工具所需的参数。
  3. 生成结构化输出: LLM生成一个结构化的输出,通常是一个JSON数组,其中每个元素代表一个独立的工具调用。这个数组就是LLM预先规划好的“工具调用序列”。
  4. 宿主系统执行: 宿主系统接收到这个JSON数组后,解析它,然后按照序列中定义的顺序,依次调用相应的工具。
  5. 结果处理: 所有工具调用执行完毕后,宿主系统将所有中间结果收集起来。此时,它可以选择:
    • 直接基于这些结果生成一个最终的、完整的回复。
    • 将这些结果作为新的上下文,再次发送给LLM,让LLM进行最终的总结或进一步的决策(这通常是当原始规划未能完全覆盖所有情况,或需要LLM对复杂结果进行解释时)。

关键在于,在步骤1到步骤4之间,LLM只进行了一次推理。这极大地减少了与LLM的往返次数,是效率提升的核心所在。


4. 实现多步工具使用的关键技术与设计原则

要让LLM具备在单次推理中生成工具调用序列的能力,我们需要从数据、提示工程和输出格式约定等多个层面进行精心的设计和准备。

4.1 训练数据:能力的基石

这是最关键的一环。LLM之所以能进行复杂规划,是因为它在海量的训练数据中“见过”并“学会了”这种模式。为了让LLM能够输出工具调用序列,训练数据中必须包含大量的多步任务分解和工具编排的示例

训练数据示例结构:

[
  {
    "user_query": "查找明天上海的天气,如果气温低于10度,就给我发邮件提醒。",
    "tool_calls_sequence": [
      {
        "tool_name": "get_weather",
        "arguments": {"city": "上海", "date": "明天"}
      },
      {
        "tool_name": "check_temperature_and_send_email",
        "arguments": {
          "min_temp": 10,
          "to": "[email protected]",
          "subject_template": "上海明天低温提醒",
          "body_template": "明天上海气温预计低于{min_temp}度,请注意保暖。当前天气:{weather_report}"
        },
        "depends_on": "get_weather.temperature" // 隐式或显式地表达依赖
      }
    ],
    "final_answer_template": "已为您查询到明天上海天气,并根据气温情况执行了邮件发送操作。{email_status}"
  },
  {
    "user_query": "查询我的日程表,找到所有与'项目启动'相关的会议,然后将这些会议的详细信息汇总并发送给我的团队。",
    "tool_calls_sequence": [
      {
        "tool_name": "search_calendar",
        "arguments": {"keyword": "项目启动", "time_range": "upcoming"}
      },
      {
        "tool_name": "summarize_events",
        "arguments": {"events_data": "{search_calendar_result}"}
      },
      {
        "tool_name": "send_team_message",
        "arguments": {
          "to_group": "my_team_id",
          "subject": "项目启动会议汇总",
          "body": "{summarize_events_result}"
        }
      }
    ],
    "final_answer_template": "已为您查找并汇总了所有项目启动会议,并已发送给您的团队。"
  }
]

这些数据需要人工标注或通过其他自动化方法生成。它们教会LLM:

  • 如何将一个高层级请求分解成一系列原子操作。
  • 操作之间可能存在的逻辑顺序和依赖关系。
  • 如何从前一个工具的输出中提取信息作为后一个工具的输入。
  • 期望的输出格式。

4.2 提示工程:引导LLM的“思维”

即使是经过训练的模型,也需要清晰的提示来引导其生成预期的多步序列。系统提示(System Prompt)在这里扮演着至关重要的角色。

系统提示的核心要素:

  1. 角色设定: 明确LLM的身份和职责,例如“你是一个能够根据用户请求规划并执行多步工具调用的智能助手。”
  2. 可用工具定义: 清晰地描述每个工具的功能、名称、参数及其类型。这与传统工具调用无异。
  3. 输出格式规范: 这是最重要的一点。 必须明确告知LLM,当需要多步操作时,它应该生成一个JSON数组,每个元素是一个工具调用对象。

示例工具定义 (JSON Schema 格式):

[
  {
    "name": "get_weather",
    "description": "获取指定城市和日期的天气信息。",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {"type": "string", "description": "城市名称,如'北京'"},
        "date": {"type": "string", "description": "日期,如'今天', '明天', '2023-10-27'"}
      },
      "required": ["city", "date"]
    }
  },
  {
    "name": "send_email",
    "description": "向指定收件人发送邮件。",
    "parameters": {
      "type": "object",
      "properties": {
        "to": {"type": "string", "description": "收件人邮箱地址"},
        "subject": {"type": "string", "description": "邮件主题"},
        "body": {"type": "string", "description": "邮件内容"}
      },
      "required": ["to", "subject", "body"]
    }
  },
  {
    "name": "search_calendar",
    "description": "搜索用户的日程表,查找指定关键词的会议。",
    "parameters": {
      "type": "object",
      "properties": {
        "keyword": {"type": "string", "description": "搜索关键词"},
        "time_range": {"type": "string", "description": "时间范围,如'upcoming', 'today', 'next week'"}
      },
      "required": ["keyword", "time_range"]
    }
  }
]

示例系统提示 (System Prompt):

你是一个高级AI助手,能够通过调用外部工具来完成用户的复杂请求。
当一个请求需要多个工具协调工作时,你需要在一次响应中规划并生成一个包含所有必要工具调用的JSON数组。

可用工具列表(请严格遵守它们的名称、描述和参数定义):
{
    "tools": [
        {
            "name": "get_weather",
            "description": "获取指定城市和日期的天气信息。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称,如'北京'"},
                    "date": {"type": "string", "description": "日期,如'今天', '明天', '2023-10-27'"}
                },
                "required": ["city", "date"]
            }
        },
        {
            "name": "send_email",
            "description": "向指定收件人发送邮件。",
            "parameters": {
                "type": "object",
                "properties": {
                    "to": {"type": "string", "description": "收件人邮箱地址"},
                    "subject": {"type": "string", "description": "邮件主题"},
                    "body": {"type": "string", "description": "邮件内容"}
                },
                "required": ["to", "subject", "body"]
            }
        },
        {
            "name": "search_calendar",
            "description": "搜索用户的日程表,查找指定关键词的会议。",
            "parameters": {
                "type": "object",
                "properties": {
                    "keyword": {"type": "string", "description": "搜索关键词"},
                    "time_range": {"type": "string", "description": "时间范围,如'upcoming', 'today', 'next week'"}
                },
                "required": ["keyword", "time_range"]
            }
        }
    ]
}

当需要调用工具时,请生成一个符合以下JSON Schema的数组。如果只需一个工具,也请将其包装在数组中。
如果任务无法通过工具完成,或可以直接回答,请直接生成自然语言回复。

```json
[
  {
    "tool_name": "string", // 必须是上述可用工具列表中的名称
    "arguments": {
      // JSON对象,键是参数名,值是参数值,必须符合工具定义中的参数类型和要求
    }
  },
  // ... 更多工具调用对象
]

Few-shot 示例: 在系统提示后,提供几个用户请求及其对应的多步工具调用序列的例子,可以进一步强化LLM对输出格式和规划逻辑的理解。

User: 帮我查一下明天北京的天气,如果下雨,就给我的邮箱 [email protected] 发一封主题为“雨天提醒”的邮件,内容是“明天北京有雨,请注意带伞。”
Assistant:
```json
[
  {
    "tool_name": "get_weather",
    "arguments": {
      "city": "北京",
      "date": "明天"
    }
  },
  {
    "tool_name": "send_email",
    "arguments": {
      "to": "[email protected]",
      "subject": "雨天提醒",
      "body": "明天北京有雨,请注意带伞。"
    }
  }
]

#### 4.3 模型架构考量(简述)

核心LLM架构本身无需改变,但为了支持这种复杂的规划能力,模型在训练时需要特别关注:
*   **长依赖建模:** 确保模型能够理解用户请求与多个未来步骤之间的长距离依赖关系。
*   **结构化输出生成:** 模型需要被优化以稳定地生成符合特定JSON Schema的结构化文本。这通常通过在微调阶段使用高质量的结构化数据和适当的损失函数来实现。
*   **规划与推理能力:** 模型需要在训练过程中学习如何进行任务分解、子目标设定以及资源分配(即选择合适的工具和参数)。

---

### 5. 生成工具调用序列的机制与宿主系统编排

现在,我们来详细探讨从LLM接收请求到最终工具执行的全过程。

#### 5.1 输入上下文的构建

宿主系统首先需要构建发送给LLM的完整输入上下文。这包括:
*   **用户原始请求:** `user_query`
*   **系统提示:** 包含LLM的角色、多步调用说明和输出格式要求。
*   **工具定义:** 所有可用工具的详细JSON Schema描述。
*   **(可选)Few-shot 示例:** 提供给LLM的示范性多步调用例子。

这些元素组合成一个完整的`prompt`字符串或结构化消息体,发送给LLM API。

#### 5.2 LLM的单次推理

LLM接收到这个包含所有必要信息的提示后,开始进行推理。在这个阶段,LLM会:
1.  **理解用户意图:** 解析用户请求,识别核心任务和子任务。
2.  **规划工具链:** 根据其对可用工具的理解和训练中学习到的任务分解模式,构思一个完成任务的工具调用序列。这包括确定需要哪些工具、它们的逻辑顺序以及如何将信息从一个工具传递到下一个工具(尽管在这一步中,LLM只是预测参数,而不是实际传递数据)。
3.  **生成结构化输出:** LLM生成一个符合预定义JSON Schema的文本序列,这个序列就是我们期待的工具调用数组。

**LLM输出示例:**

假设用户请求是:"帮我查明天北京的天气,如果气温低于5度,就给我发邮件[email protected]提醒我,主题是'低温预警',内容是'明天北京气温很低,注意保暖!'"

LLM可能生成如下JSON:

```json
[
  {
    "tool_name": "get_weather",
    "arguments": {
      "city": "北京",
      "date": "明天"
    }
  },
  {
    "tool_name": "send_email",
    "arguments": {
      "to": "[email protected]",
      "subject": "低温预警",
      "body": "明天北京气温很低,注意保暖!"
    }
  }
]

注意: 在这个例子中,LLM直接规划了send_email,但其body内容是固定的。更高级的规划可能涉及在get_weather结果后,根据实际气温动态生成send_emailbody。这需要宿主系统在执行时进行条件判断和参数填充,或者LLM在规划时就预留了结果插槽。我们这里主要关注序列的生成。

5.3 宿主系统侧的编排与执行

这是将LLM的“计划”付诸实践的关键环节。宿主系统需要一个健壮的执行器(Orchestrator)。

执行流程:

  1. 解析LLM输出: 宿主系统接收到LLM生成的JSON字符串后,首先进行解析。
    • 检查是否是有效的JSON。
    • 检查JSON是否是一个数组。
    • 检查数组中的每个元素是否符合工具调用对象的结构(tool_namearguments)。
  2. 迭代执行工具调用: 宿主系统按照解析出的工具调用序列,逐一执行每个工具。
    • 动态参数填充: 对于需要依赖前一个工具结果的参数,宿主系统需要在执行时进行动态填充。例如,如果 send_emailbody 需要包含 get_weather 的实际天气描述,宿主系统会在 get_weather 执行完成后,提取相关信息,然后构造 send_email 的参数。
    • 条件执行: 某些高级场景下,LLM的规划可能暗示了条件逻辑(例如“如果天气是雨天,则发送邮件”)。宿主系统需要有能力识别并实现这种条件判断。
    • 错误处理: 每个工具调用都可能失败。宿主系统需要捕获这些错误,并决定如何响应(例如,记录日志,跳过后续步骤,或向用户报告)。
  3. 收集中间结果: 将每个工具调用的结果存储起来,以便后续步骤使用或用于最终答案的生成。
  4. 最终答案生成或回馈LLM:
    • 直接生成: 如果整个任务的最终答案可以完全基于收集到的工具结果来构建(例如,一个预设的模板),宿主系统可以直接生成。
    • 回馈LLM: 对于更复杂的总结或需要LLM进行深度理解的场景,宿主系统可以将所有工具的执行结果(以及原始请求)重新打包成一个新的上下文,再次发送给LLM,让LLM生成最终的自然语言回复。这是“单次推理”的边界:LLM一次性生成了执行计划,但可能需要第二次推理来总结执行结果。

Python代码示例:宿主系统编排

首先定义我们的工具:

import json
from typing import Dict, Any, List, Callable
import datetime

# 模拟的外部工具
def get_weather(city: str, date: str) -> Dict[str, Any]:
    """
    获取指定城市和日期的天气信息。
    Args:
        city (str): 城市名称。
        date (str): 日期,如 '今天', '明天', '2023-10-27'。
    Returns:
        Dict[str, Any]: 包含天气信息的字典。
    """
    print(f"DEBUG: 调用 get_weather(city='{city}', date='{date}')")
    # 实际中会调用外部API
    if city == "北京" and date == "明天":
        return {"city": city, "date": date, "temperature": "3°C", "condition": "雨夹雪", "description": "明天北京有雨夹雪,气温较低。"}
    elif city == "上海" and date == "今天":
        return {"city": city, "date": date, "temperature": "15°C", "condition": "多云", "description": "今天上海多云。"}
    else:
        return {"city": city, "date": date, "error": "无法获取天气信息。"}

def send_email(to: str, subject: str, body: str) -> Dict[str, Any]:
    """
    向指定收件人发送邮件。
    Args:
        to (str): 收件人邮箱地址。
        subject (str): 邮件主题。
        body (str): 邮件内容。
    Returns:
        Dict[str, Any]: 邮件发送状态。
    """
    print(f"DEBUG: 调用 send_email(to='{to}', subject='{subject}', body='{body}')")
    # 实际中会调用邮件API
    print(f"邮件已发送至 {to},主题:{subject}n内容:n{body}n---")
    return {"status": "success", "message": f"邮件已发送至 {to}"}

def search_calendar(keyword: str, time_range: str) -> List[Dict[str, Any]]:
    """
    搜索用户的日程表,查找指定关键词的会议。
    Args:
        keyword (str): 搜索关键词。
        time_range (str): 时间范围,如 'upcoming', 'today', 'next week'。
    Returns:
        List[Dict[str, Any]]: 匹配的会议列表。
    """
    print(f"DEBUG: 调用 search_calendar(keyword='{keyword}', time_range='{time_range}')")
    # 模拟数据
    if keyword == "项目启动" and time_range == "upcoming":
        return [
            {"id": "c1", "title": "项目启动会 - 前端", "date": "2023-11-15", "time": "10:00", "location": "会议室A"},
            {"id": "c2", "title": "项目启动会 - 后端", "date": "2023-11-16", "time": "14:00", "location": "在线会议"}
        ]
    elif keyword == "午餐" and time_range == "明天":
         return [
            {"id": "c3", "title": "与张三的午餐", "date": "2023-11-10", "time": "12:30", "location": "公司餐厅"}
        ]
    return []

# 注册可用工具
available_tools: Dict[str, Callable] = {
    "get_weather": get_weather,
    "send_email": send_email,
    "search_calendar": search_calendar,
}

# 模拟LLM客户端
class MockLLMClient:
    def generate(self, prompt: str) -> str:
        print("n--- LLM 输入 Prompt ---")
        print(prompt)
        print("----------------------n")

        # 根据prompt内容模拟LLM的响应
        if "明天北京的天气" in prompt and "低于5度" in prompt and "邮件" in prompt:
            # 模拟LLM规划的序列
            return json.dumps([
                {
                    "tool_name": "get_weather",
                    "arguments": {"city": "北京", "date": "明天"}
                },
                {
                    "tool_name": "send_email",
                    "arguments": {
                        "to": "[email protected]",
                        "subject": "低温预警",
                        # LLM在此处可能还无法知道确切的温度,但可以规划一个模板
                        # 宿主系统需要在执行时根据get_weather结果填充
                        "body": "明天北京气温预计{temperature},请注意保暖!天气状况:{condition}" 
                    }
                }
            ], indent=2, ensure_ascii=False)
        elif "查找我的日程表" in prompt and "项目启动" in prompt and "团队" in prompt:
            return json.dumps([
                {
                    "tool_name": "search_calendar",
                    "arguments": {"keyword": "项目启动", "time_range": "upcoming"}
                },
                {
                    "tool_name": "send_email",
                    "arguments": {
                        "to": "[email protected]",
                        "subject": "项目启动会议汇总",
                        "body": "以下是即将到来的项目启动会议:n{calendar_summary}"
                    }
                }
            ], indent=2, ensure_ascii=False)
        elif "取消明天的午餐预订" in prompt:
            # 这是一个无法通过现有工具直接完成的任务,LLM可能会尝试规划,或者直接回复
            # 这里模拟LLM无法规划出直接取消的工具,但会先查一下
            return json.dumps([
                {
                    "tool_name": "search_calendar",
                    "arguments": {"keyword": "午餐", "time_range": "明天"}
                }
            ], indent=2, ensure_ascii=False)

        return "很抱歉,我无法理解您的请求或执行所需的多步操作。"

# 宿主系统编排器
def execute_multi_step_tool_calls(user_query: str, llm_client: MockLLMClient) -> str:
    # 1. 构建完整的LLM输入Prompt (简化,实际会包含工具schemas和few-shot)
    system_prompt = """
你是一个高级AI助手,能够通过调用外部工具来完成用户的复杂请求。
当一个请求需要多个工具协调工作时,你需要在一次响应中规划并生成一个包含所有必要工具调用的JSON数组。

可用工具列表(请严格遵守它们的名称、描述和参数定义):
""" + json.dumps({
        "tools": [
            {"name": "get_weather", "description": "获取指定城市和日期的天气信息。", "parameters": {"type": "object", "properties": {"city": {"type": "string"}, "date": {"type": "string"}}, "required": ["city", "date"]}},
            {"name": "send_email", "description": "向指定收件人发送邮件。", "parameters": {"type": "object", "properties": {"to": {"type": "string"}, "subject": {"type": "string"}, "body": {"type": "string"}}, "required": ["to", "subject", "body"]}},
            {"name": "search_calendar", "description": "搜索用户的日程表,查找指定关键词的会议。", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}, "time_range": {"type": "string"}}, "required": ["keyword", "time_range"]}}
        ]
    }, indent=2, ensure_ascii=False) + """

当需要调用工具时,请生成一个符合以下JSON Schema的数组。如果只需一个工具,也请将其包装在数组中。
如果任务无法通过工具完成,或可以直接回答,请直接生成自然语言回复。

```json
[
  {
    "tool_name": "string", 
    "arguments": {
      // JSON对象,键是参数名,值是参数值,必须符合工具定义中的参数类型和要求
    }
  },
  // ... 更多工具调用对象
]

User: """ + user_query

# 2. 调用LLM进行单次推理
llm_output_str = llm_client.generate(system_prompt)

# 3. 解析LLM的输出
try:
    tool_calls_sequence = json.loads(llm_output_str)
    if not isinstance(tool_calls_sequence, list):
        return f"LLM返回了非工具调用序列的JSON,内容:{llm_output_str}"
except json.JSONDecodeError:
    return f"LLM返回的不是有效的JSON格式,内容:{llm_output_str}"
except Exception as e:
    return f"解析LLM输出时发生未知错误:{e}, 输出内容:{llm_output_str}"

if not tool_calls_sequence:
    return f"LLM返回了空的工具调用序列,或者直接回复了:n{llm_output_str}"

print("n--- LLM 生成的工具调用序列 ---")
print(json.dumps(tool_calls_sequence, indent=2, ensure_ascii=False))
print("-----------------------------n")

# 4. 迭代执行工具调用并收集结果
execution_results = {} # 存储所有工具调用的结果,供后续步骤或最终回复使用
final_response_parts = [] # 收集宿主系统生成的响应片段

for i, call in enumerate(tool_calls_sequence):
    tool_name = call.get("tool_name")
    arguments = call.get("arguments", {})

    if not tool_name or tool_name not in available_tools:
        final_response_parts.append(f"错误: 发现未知工具或工具名称缺失: {tool_name}")
        continue

    tool_func = available_tools[tool_name]

    # 动态参数填充逻辑 (此处简化,实际可能更复杂)
    # 例如,如果body中包含占位符,尝试用之前的结果填充
    processed_arguments = {}
    for arg_name, arg_value in arguments.items():
        if isinstance(arg_value, str) and "{" in arg_value and "}" in arg_value:
            # 尝试从之前的结果中填充
            # 这是一个非常简化的处理方式,实际应该有更严谨的模板引擎和数据路径解析
            filled_value = arg_value
            for res_key, res_val in execution_results.items():
                if isinstance(res_val, dict):
                    for sub_key, sub_val in res_val.items():
                        filled_value = filled_value.replace(f"{{{res_key}.{sub_key}}}", str(sub_val))
                        filled_value = filled_value.replace(f"{{{sub_key}}}", str(sub_val)) # 兼容直接用子key
                elif isinstance(res_val, list):
                    # 对于列表结果,可能需要特殊处理,比如汇总成字符串
                    if f"{{{res_key}}}" in filled_value:
                        summary = "n".join([f"- {item['title']} ({item['date']} {item['time']})" for item in res_val if 'title' in item])
                        filled_value = filled_value.replace(f"{{{res_key}}}", summary)
            processed_arguments[arg_name] = filled_value
        else:
            processed_arguments[arg_name] = arg_value

    print(f"执行第 {i+1} 步: 调用 {tool_name},参数:{processed_arguments}")
    try:
        result = tool_func(**processed_arguments)
        execution_results[tool_name] = result # 存储结果以备后续使用
        final_response_parts.append(f"工具 '{tool_name}' 执行成功,结果:{result}")

        # 针对特定工具结果的条件判断和响应
        if tool_name == "get_weather" and "temperature" in result:
            temp_str = result["temperature"].replace("°C", "").strip()
            if temp_str.isdigit() and int(temp_str) < 5:
                final_response_parts.append(f"检测到低温({temp_str}°C),已根据LLM规划发送邮件。")

        elif tool_name == "search_calendar" and result:
             final_response_parts.append(f"已找到 {len(result)} 条相关日程。")

    except Exception as e:
        final_response_parts.append(f"工具 '{tool_name}' 执行失败,错误:{e}")
        # 如果某个关键步骤失败,可能需要中断整个序列
        break 

# 5. 总结结果或回馈LLM生成最终答案
# 简单的总结逻辑:
if "send_email" in execution_results and execution_results["send_email"].get("status") == "success":
    final_answer = f"任务完成!{user_query} 已处理完毕。"
    if "get_weather" in execution_results and "temperature" in execution_results["get_weather"]:
         final_answer += f" 天气查询结果:{execution_results['get_weather']['description']}"
    if "search_calendar" in execution_results and execution_results["search_calendar"]:
         final_answer += f" 日程查询结果:共找到 {len(execution_results['search_calendar'])} 条匹配项。"
    final_answer += f" 邮件发送状态:{execution_results['send_email']['message']}"
elif tool_calls_sequence:
     final_answer = f"部分任务已执行,但可能未完全满足请求:n" + "n".join(final_response_parts)
else:
    # 如果LLM根本没返回工具调用序列,直接返回LLM的原始输出
    final_answer = llm_output_str

# 实际应用中,这里可以将 execution_results 再次传给 LLM 进行更智能的总结
# final_summary_prompt = f"基于以下执行结果:{json.dumps(execution_results, ensure_ascii=False)},请总结用户的请求:'{user_query}' 的处理情况。"
# final_answer = llm_client.generate(final_summary_prompt)

return final_answer

— 测试用例 —

llm = MockLLMClient()

print("n— 任务 1: 查天气并根据条件发邮件 —")
user_q1 = "帮我查明天北京的天气,如果气温低于5度,就给我的邮箱 [email protected] 发一封主题为“低温预警”的邮件,内容是“明天北京气温很低,注意保暖!!”"
response1 = execute_multi_step_tool_calls(user_q1, llm)
print("n— 最终响应 1 —")
print(response1)

print("nn— 任务 2: 查日程并汇总发送团队 —")
user_q2 = "查找我的日程表,找到所有与’项目启动’相关的会议,然后将这些会议的详细信息汇总并发送给我的团队 [email protected]。"
response2 = execute_multi_step_tool_calls(user_q2, llm)
print("n— 最终响应 2 —")
print(response2)

print("nn— 任务 3: 无法直接完成但能部分规划 —")
user_q3 = "取消明天的午餐预订。"
response3 = execute_multi_step_tool_calls(user_q3, llm)
print("n— 最终响应 3 —")
print(response3)


**代码解释:**

1.  **`available_tools`:** 模拟了宿主系统中可用的实际工具函数。
2.  **`MockLLMClient`:** 这是一个关键的模拟器。它扮演了LLM的角色,根据输入的`prompt`内容,**一次性**返回一个预设的JSON字符串,这个字符串包含了多个工具调用对象。注意其中的`body`字段如何包含`{temperature}`、`{condition}`等占位符,这是LLM规划时预留的,需要宿主系统在执行时进行填充。
3.  **`execute_multi_step_tool_calls` 函数:** 这是宿主系统的编排核心。
    *   它首先构建一个详细的`system_prompt`,告诉LLM如何响应多步任务。
    *   调用`llm_client.generate(system_prompt)`,**仅此一次**与LLM交互。
    *   解析LLM返回的JSON工具调用序列。
    *   使用一个循环遍历序列中的每个工具调用。
    *   在循环内部,`processed_arguments` 逻辑展示了宿主系统如何根据前一个工具的执行结果(存储在`execution_results`中)来动态填充当前工具的参数。这是一个简化的模板替换。
    *   调用实际的工具函数(如 `get_weather`)。
    *   存储每个工具的执行结果。
    *   最后,基于所有执行结果生成一个最终的回复。

这个例子清晰地展示了“单次推理”的核心:LLM只被调用一次来规划整个序列。后续的执行、参数填充和条件判断都发生在宿主系统侧,大大减少了与LLM的往返次数。

---

### 6. 单次推理多步工具使用的优势

这种高级的多步工具使用模式带来了显著的改进:

1.  **显著降低延迟:** 这是最直接和最重要的优势。将多次LLM推理合并为一次,消除了中间的网络传输和LLM计算等待时间。对于用户来说,这意味着任务响应速度更快,用户体验得以极大提升。
2.  **提高效率与降低成本:**
    *   **更少的LLM API调用:** 减少了API请求次数,直接降低了按调用次数计费的成本。
    *   **优化令牌使用:** 每次LLM调用都需传输上下文。一次性规划意味着避免了在每次工具调用结果返回后重复传输整个历史上下文,从而减少了总令牌使用量,降低了按令牌计费的成本。
3.  **更强的任务理解与规划能力:** LLM被训练和提示去进行“全局规划”,这意味着它在最初就对整个任务有了一个宏观的理解和分解。这通常能带来更合理、更连贯的工具使用策略,减少“短视”决策。
4.  **简化宿主系统编排(对于LLM交互部分):** 对于宿主系统而言,它从LLM那里得到的是一个完整的“执行计划”,而不是一个需要反复询问的交互式代理。宿主系统可以更专注于执行、错误处理和结果整合,而不是复杂的LLM交互逻辑。
5.  **支持更复杂的应用场景:** 能够处理涉及多个子任务、数据传递和条件判断的复杂用户请求,从而扩展了LLM驱动代理的应用范围。

---

### 7. 挑战与考量

尽管优势显著,但单次推理多步工具使用并非没有挑战。

1.  **训练数据复杂性:** 构建高质量的多步工具调用训练数据非常困难,需要大量的人工标注和领域知识。数据中的任务分解、参数依赖和条件逻辑的表达是关键。
2.  **LLM规划的鲁棒性:**
    *   **“幻觉”问题:** LLM可能规划出一个逻辑上看似合理但实际上无法执行的序列,例如调用不存在的工具、使用错误的参数或预测不符合实际的条件。
    *   **对不可预测结果的适应性:** 如果LLM规划时对某个中间工具的执行结果做出了错误假设,而实际结果与假设不符,那么后续的工具调用可能就会出错。
3.  **宿主系统的智能与容错:**
    *   **动态参数填充:** 如何优雅、健壮地将前一个工具的输出映射并填充到后一个工具的输入参数中,是一个复杂的问题。这需要宿主系统具备强大的模板引擎、数据解析和转换能力。
    *   **条件执行与分支:** LLM生成的序列通常是线性的。如果需要根据中间结果进行条件分支(例如“如果A成功,则执行B;否则执行C”),宿M主系统需要识别这种意图并在执行时实现它。这通常需要LLM在输出格式中明确表达条件逻辑,或者宿主系统自行判断。
    *   **错误处理与恢复:** 如果序列中的某个工具调用失败,宿主系统如何处理?是中断整个序列,还是尝试跳过并继续?是否需要将失败信息反馈给LLM进行重新规划?
4.  **实时反馈的缺失:** 在一次性生成序列时,LLM无法获得任何实时反馈。这意味着它不能像传统模式那样,在每个步骤后根据实际结果调整后续计划。这在某些高度动态或不确定性的任务中可能是一个限制。
5.  **模型规模与推理成本:** 虽然减少了API调用次数,但让LLM进行更复杂的规划可能意味着需要更大的模型或更长的推理时间才能生成高质量的序列。

---

### 8. 高级概念与未来方向

为了克服上述挑战并进一步提升能力,多步工具使用正在向更高级的形态演进。

1.  **条件性工具执行与分支逻辑:** LLM输出的JSON不再是一个简单的线性数组,而是包含条件判断(`if/else`)、循环(`for each`)等控制流的“迷你程序”。
    ```json
    [
      {
        "tool_name": "get_weather",
        "arguments": {"city": "北京", "date": "明天"},
        "result_variable": "weather_data"
      },
      {
        "if": "weather_data.temperature < 5",
        "then": [
          {"tool_name": "send_email", "arguments": {"to": "[email protected]", "subject": "低温预警", "body": "明天北京气温很低!"}},
          {"tool_name": "cancel_meeting", "arguments": {"meeting_id": "123"}}
        ],
        "else": [
          {"tool_name": "send_sms", "arguments": {"to": "user_phone", "message": "明天北京天气不错。"}}
        ]
      }
    ]
这种输出格式将LLM的规划能力提升到了一个新的维度,但同时也对宿主系统的执行器提出了更高的要求。
  1. 并行工具执行: 如果序列中的某些工具调用之间没有数据依赖,LLM可以在规划时指示它们可以并行执行,进一步缩短总执行时间。这需要LLM在输出格式中明确标记可并行执行的组。
  2. 工具增强生成(Tool-Augmented Generation, TAG): 将工具调用无缝地嵌入到LLM的自然语言生成中。例如,LLM在生成一段文字时,某个词语或句子会触发一个工具调用,并将工具结果直接融入到后续的文本中。
  3. 结合反馈的动态规划: 尽管我们强调“单次推理”,但对于极端复杂或不确定性高的任务,纯粹的单次规划可能不足。一个混合模型可能是,LLM先进行一个多步规划,宿主系统执行其中的一部分,然后将部分结果反馈给LLM,让LLM基于实际情况进行动态的再规划。这是一种介于纯单步和纯多步之间的折衷方案,旨在兼顾效率和鲁棒性。
  4. 自省与纠错: LLM被训练去“反思”自己生成的工具调用序列,识别潜在的错误或遗漏,并在执行之前进行自我修正。

9. 结语

单次推理中生成工具调用序列,是LLM与外部工具结合领域向前迈出的重要一步。它将LLM从一个简单的“工具选择器”提升为能够进行复杂“任务编排”的智能规划器。通过赋能LLM更强的预见性和规划能力,我们能够构建出响应更迅速、效率更高、用户体验更流畅的AI代理系统。这不仅是技术层面的突破,更是将LLM能力从对话助手推向真正意义上的智能工作流执行者的关键路径。随着模型能力的不断提升和工程实践的日益成熟,我们有理由相信,这种高效的工具使用模式将成为未来AI应用的标准范式。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注