3. 工具调用基础

好,咱们进入第三章。这一章讲的是工具调用的核心流程。说白了,就是 Agent 怎么决定用哪个工具、怎么把参数填对、怎么处理返回结果。

我在项目里见过不少同学,模型选得挺好,Prompt 写得也漂亮,结果卡在工具调用这一步——要么参数传错了,要么工具选得驴唇不对马嘴。嗯,今天咱们就把这块彻底掰开揉碎。

3.1 请求-响应模式:一次完整的工具调用长什么样

先看最基础的模式。一次工具调用,本质上就是一次请求-响应循环。我习惯把它拆成四个阶段:

  1. 意图识别:LLM 分析用户输入,判断要不要调工具、调哪个。
  2. 参数构建:LLM 生成工具需要的参数(JSON 格式)。
  3. 工具执行:系统拿到参数,调用真实 API 或函数。
  4. 结果回填:把工具返回的结果塞回 LLM 的上下文,让它继续生成回答。

核心要点:这四个阶段不是串行的,而是嵌套在 LLM 的推理循环里。每次工具调用,LLM 都会“暂停”一下,等结果回来再继续。

举个例子。用户说:“帮我查一下北京的天气。” 流程是这样的:

  • LLM 识别出“查天气”这个意图 → 匹配到 get_weather 工具。
  • LLM 生成参数:{"city": "北京"}。
  • 系统调用天气 API,返回 {"temp": 22, "condition": "晴"}。
  • LLM 拿到结果,组织成自然语言:“北京今天 22 度,晴天。”

你想想看,这个循环里最容易出问题的是哪一步?我个人经验是参数构建。模型经常把参数名写错,或者漏掉必填字段。

3.2 工具选择策略:基于 LLM 的意图识别

工具选择,说白了就是让 LLM 从一堆工具里挑一个最合适的。这不是简单的关键词匹配,而是语义级别的意图识别。

我常用的策略有三种,咱们用表格对比一下:

策略 原理 适用场景 缺点
函数描述匹配 给每个工具写一段自然语言描述,LLM 根据描述选 工具数量少(<10 个),描述清晰 工具多了容易混淆
嵌入向量检索 把工具描述转成向量,用语义相似度召回 工具数量多(几十上百个) 需要维护向量库,有延迟
两阶段混合 先向量检索粗筛,再让 LLM 精排 大规模工具库,要求高准确率 实现复杂,成本高

我的建议:刚开始做项目,先用函数描述匹配。等工具超过 15 个、出现选择错误时,再上向量检索。别一上来就搞复杂的。

这里有个关键点——工具描述怎么写。我踩过坑。以前我写工具描述特别简略,比如“查询订单状态”。结果 LLM 经常把“查询物流”也匹配到这个工具上。

后来我改了写法:

// 不好的写法
"description": "查询订单状态"

// 好的写法
"description": "查询用户订单的当前处理状态,包括待支付、已支付、已发货、已完成。不包含物流轨迹查询。"

你看,加上边界说明(“不包含物流轨迹查询”),模型就很少选错了。这叫负例描述,很管用。

3.3 参数填充与校验:别让模型乱填参数

参数填充是工具调用里最细碎的活,也是最容易出 bug 的地方。我把它分成三步:

3.3.1 参数定义要严格

每个参数都要说清楚:

  • 类型:string、number、boolean、array、object。
  • 是否必填:required 字段里列清楚。
  • 枚举值:如果参数只能取固定值,用 enum 限制。
  • 描述:写清楚这个参数是干什么的,格式要求。

举个例子,一个完整的参数定义:

{
  "name": "create_order",
  "parameters": {
    "type": "object",
    "properties": {
      "user_id": {
        "type": "string",
        "description": "用户 ID,格式为 'U' 开头加 8 位数字"
      },
      "items": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "product_id": { "type": "string" },
            "quantity": { "type": "integer", "minimum": 1 }
          },
          "required": ["product_id", "quantity"]
        }
      },
      "payment_method": {
        "type": "string",
        "enum": ["alipay", "wechat", "card"]
      }
    },
    "required": ["user_id", "items"]
  }
}

注意:参数描述里不要写模糊的词,比如“合适的数量”。模型不知道什么叫“合适”。要写“最小 1,最大 99”。

3.3.2 参数填充的常见错误

我曾经遇到过这么个情况:模型把用户说的“帮我查一下张三的订单”,填成了 {"user_name": "张三"},但工具接口要求的是 user_id。结果查不到数据。

这类问题我总结了一下:

错误类型 表现 解决办法
字段名映射错误 用自然语言里的词代替参数名 在描述里写明“参数名必须严格使用 xxx”
类型错误 数字填成字符串,数组填成逗号分隔 在描述里给示例值,比如 "example": 100
遗漏必填参数 required 字段没填全 在描述里强调“此参数必填,不可省略”
枚举值超出范围 填了 enum 里没有的值 用 enum 限制,并在描述里列出所有可选值

3.3.3 校验机制:别信模型,要校验

模型生成的参数,一定要做服务端校验。我见过太多人直接把模型输出的 JSON 扔给 API,结果出各种问题。

校验分两层:

  1. 格式校验:JSON 是否合法?字段类型对不对?必填字段有没有?
  2. 业务校验:用户有没有权限?数据存不存在?值在不在合理范围?

格式校验可以用 JSON Schema 自动做。业务校验得自己写逻辑。

// 伪代码示例
function validateAndCall(toolName, params) {
  // 1. 格式校验
  const schema = getToolSchema(toolName);
  const valid = validateJsonSchema(params, schema);
  if (!valid) {
    return { error: "参数格式错误,请重新生成" };
  }

  // 2. 业务校验
  if (toolName === "create_order") {
    if (!userExists(params.user_id)) {
      return { error: "用户不存在" };
    }
    if (params.items.length === 0) {
      return { error: "订单不能为空" };
    }
  }

  // 3. 调用工具
  return callTool(toolName, params);
}

小技巧:校验失败时,把具体的错误信息返回给 LLM,让它重新生成参数。比如“参数 user_id 格式错误,应为 U 开头加 8 位数字”。模型看到这个,通常能自己修正。

3.4 完整流程:一张图看懂

下面这张图把整个工具调用流程串起来了。我建议你仔细看看,尤其是校验失败后的重试循环——这是很多系统忽略的。

工具调用完整流程图 工具调用完整流程图 用户输入 意图识别(LLM 选择工具) 参数构建(LLM 生成 JSON) 格式校验(JSON Schema) 失败 返回错误信息 重试 通过 业务校验 业务校验失败,返回错误 工具执行 结果回填 LLM 最终回答 关键设计点 • 校验失败自动重试 • 错误信息要具体 • 业务校验不可跳过

3.5 避坑指南:我踩过的几个坑

最后分享几个实战中遇到的坑,你遇到了能少走弯路。

坑一:模型生成参数时,会把用户输入里的同义词直接填进去

比如用户说“用微信支付”,模型填了 "payment_method": "微信支付",但枚举值是 "wechat"。解决办法:在参数描述里写明“必须使用英文枚举值,不要用中文”。

坑二:工具返回结果太大,把 LLM 的上下文撑爆了

我曾经查一次订单,返回了 200 条记录,直接把 干满了。解决办法:在工具描述里写明“默认只返回前 10 条”,或者让工具支持分页参数。

坑三:模型在工具调用失败后,会自己编一个结果

嗯,你没看错。如果校验失败后没有把错误信息返回给 LLM,模型可能会说“好的,已为您创建订单”,实际上啥也没干。所以一定要把错误信息明确返回,并且让模型知道“工具调用失败,请重试”。

好了,这一章的内容就这些。工具调用流程看着简单,但每个环节都有细节。你只要把意图识别、参数构建、校验、结果回填这四个环节做扎实了,大部分问题都能避免。