Agent 与工具调用实战:从 Function Calling 到 ReAct 工作流

从工具 schema 设计、调用循环、失败处理到评测闭环,建立一套可落地的 Agent 与工具调用实践框架。

难度

进阶

阅读时长

约 135 分钟

更新日期

2026/03/24

主题

Agent / 工具调用 / 结构化输出 / Prompting / 评测

先修知识

Prompt Engineering 系统指南JSON Schema 或函数参数基础HTTP API 与异常处理常识

学习目标

读完这篇教程后,你应该能:

  1. 区分 function calling、固定工作流和 Agent 三种实现思路的边界。
  2. 为工具设计更稳定的 schema,而不是把业务动作直接交给模型自由发挥。
  3. 搭建一个最小的“思考 - 调用 - 观察 - 收敛”循环,并控制步数、权限和错误处理。
  4. 为 Agent 建立一套够用的评测指标,知道问题出在工具选择、参数填写还是工作流收敛。

如果你已经读过 Prompt Engineering 系统指南ReAct:Synergizing Reasoning and Acting in Language ModelsToolformer:Language Models Can Teach Themselves to Use Tools,这篇文章会把相关概念真正落到工程实践。

先分清三件事:工具调用、工作流和 Agent 不是同一个东西

现在很多产品只要接了一个外部 API,就会自称“Agent”。但工程上最好先分清三层能力:

  • 工具调用:模型根据 schema 选择某个工具,并填好参数。
  • 工作流编排:步骤顺序由工程代码预先写死,模型只负责其中某个环节。
  • Agent:模型在循环里决定下一步做什么,直到完成任务、失败退出或转人工。

这三者最大的差别,不是“听起来谁更高级”,而是:

  • 决策权到底掌握在模型手里还是代码手里
  • 出错后是可定位的单步问题,还是开放式循环问题
  • 延迟、成本、可解释性会不会明显上升

很多团队真正需要的,其实是“带工具调用的工作流”,而不是一个完全开放的 Agent。默认原则应该是:

能用固定流程解决,就不要过早引入开放式循环。

一个最小 Agent 系统至少包含哪些部件

从系统视角看,一个最小 Agent 往往由下面几部分组成:

组件作用常见问题
模型负责理解任务、选择动作、整合结果乱调工具、漏掉关键条件
工具注册表暴露可用工具及 schema描述含糊、参数约束过松
执行器真正调用 API、数据库、检索系统异常未捕获、超时失控
状态存储保存历史消息、工具结果、临时计划上下文越来越乱,无法收敛
安全护栏控制权限、确认高风险动作、限制步数写操作被误触发
评测与日志记录调用轨迹、失败样本、完成率只能凭感觉调 prompt

很多 Agent 项目失败,不是因为“模型不够聪明”,而是因为后面四项做得太弱。

如果把这些部件连成一张运行图,会更容易看清一个可控 Agent 到底靠什么稳定下来:

Agent 与工具调用的可控闭环图 用户请求进入模型决策层,模型参考工具注册表选择动作;工具调用先经过 schema 校验与高风险确认,再进入执行器,工具结果作为 observation 回写上下文,循环直到回复用户或达到步数上限。 一个靠谱的 Agent,不是“会想”,而是每一跳都被系统护栏包住
  <g>
    <rect x="36" y="92" width="130" height="66" rx="16" fill="#e8f1ff" stroke="#98b7e1" />
    <text x="101" y="130" text-anchor="middle" font-size="18" font-weight="700">用户请求</text>
  </g>

  <g>
    <rect x="204" y="76" width="220" height="98" rx="20" fill="#f8fbf4" stroke="#d5e4c6" />
    <text x="314" y="108" text-anchor="middle" font-size="20" font-weight="700">模型决策层</text>
    <text x="314" y="132" text-anchor="middle" font-size="13" fill="#4b5563">回复用户 / 选择工具 / 继续循环</text>
    <text x="314" y="153" text-anchor="middle" font-size="13" fill="#4b5563">受 system prompt、历史状态约束</text>
  </g>

  <g>
    <rect x="462" y="76" width="176" height="98" rx="20" fill="#fff4dc" stroke="#e2c36f" />
    <text x="550" y="108" text-anchor="middle" font-size="20" font-weight="700">工具注册表</text>
    <text x="550" y="132" text-anchor="middle" font-size="13" fill="#4b5563">名称 / 描述 / schema</text>
    <text x="550" y="153" text-anchor="middle" font-size="13" fill="#4b5563">读写动作显式分离</text>
  </g>

  <g>
    <rect x="676" y="64" width="248" height="122" rx="22" fill="#f9fafb" stroke="#d7e2f1" />
    <text x="800" y="96" text-anchor="middle" font-size="20" font-weight="700">执行前护栏</text>
    <text x="800" y="120" text-anchor="middle" font-size="13" fill="#4b5563">schema 校验</text>
    <text x="800" y="141" text-anchor="middle" font-size="13" fill="#4b5563">高风险写操作确认</text>
    <text x="800" y="162" text-anchor="middle" font-size="13" fill="#4b5563">最大步数 / 重复动作检测</text>
  </g>

  <g>
    <rect x="700" y="238" width="198" height="74" rx="18" fill="#fce7ef" stroke="#e2a8bd" />
    <text x="799" y="268" text-anchor="middle" font-size="19" font-weight="700">工具执行器</text>
    <text x="799" y="291" text-anchor="middle" font-size="13" fill="#4b5563">API / DB / 检索 / 工作流</text>
  </g>

  <g>
    <rect x="420" y="250" width="224" height="62" rx="18" fill="#dff4f0" stroke="#8dc7bd" />
    <text x="532" y="277" text-anchor="middle" font-size="18" font-weight="700">Observation 回写</text>
    <text x="532" y="298" text-anchor="middle" font-size="13" fill="#4b5563">结构化工具结果进入上下文</text>
  </g>

  <g>
    <rect x="154" y="250" width="216" height="62" rx="18" fill="#ede9fe" stroke="#b7a8ea" />
    <text x="262" y="277" text-anchor="middle" font-size="18" font-weight="700">最终回答或转人工</text>
    <text x="262" y="298" text-anchor="middle" font-size="13" fill="#4b5563">收敛、失败退出、升级处理</text>
  </g>

  <line x1="166" y1="125" x2="204" y2="125" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
  <line x1="424" y1="125" x2="462" y2="125" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
  <line x1="638" y1="125" x2="676" y2="125" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
  <line x1="799" y1="186" x2="799" y2="238" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
  <line x1="700" y1="275" x2="644" y2="281" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
  <path d="M 420 281 C 388 281, 366 226, 366 182" fill="none" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
  <path d="M 314 174 L 314 228" fill="none" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
  <line x1="314" y1="250" x2="370" y2="281" stroke="#5b6b7f" stroke-width="3" marker-end="url(#agent-arrow)" />
  <text x="58" y="356" font-size="13" fill="#64748b">读操作可以更自由探索,写操作必须额外经过确认与审计,这就是“工具调用”与“业务动作”之间的安全边界。</text>
</g>
真正让 Agent 稳下来的是“模型决策 + 后端护栏 + observation 回写”三者配合,而不是单纯堆更长的推理链。

第一步:把工具定义成稳定接口,而不是自然语言愿望

模型能否稳定调用工具,关键不在模型名字,而在工具定义是否清晰。一个好的工具 schema 至少要满足四件事:

  1. 工具职责单一,一个工具只做一件明确的业务动作。
  2. 参数边界清楚,能用枚举就不用自由文本,能填 ID 就不要让模型写整段描述。
  3. 高风险写操作与低风险读操作分离,不要把“查询”和“提交”混成一个工具。
  4. 认证、权限、租户信息尽量由后端注入,不要依赖模型自己补全。

下面用一个客服支持场景举例。假设我们有三个工具:

[
  {
    "name": "search_help_center",
    "description": "查询帮助中心文章,返回最相关的证据片段。",
    "input_schema": {
      "type": "object",
      "properties": {
        "query": { "type": "string" },
        "top_k": { "type": "integer", "enum": [3, 5] }
      },
      "required": ["query"]
    }
  },
  {
    "name": "check_order_status",
    "description": "根据订单号查询订单状态。",
    "input_schema": {
      "type": "object",
      "properties": {
        "order_id": { "type": "string" }
      },
      "required": ["order_id"]
    }
  },
  {
    "name": "create_support_ticket",
    "description": "在确认需要人工介入时创建工单。",
    "input_schema": {
      "type": "object",
      "properties": {
        "order_id": { "type": "string" },
        "issue_type": {
          "type": "string",
          "enum": ["refund", "payment", "delivery", "account", "other"]
        },
        "summary": { "type": "string" }
      },
      "required": ["issue_type", "summary"]
    }
  }
]

这里最重要的设计点,不是格式好看,而是:

  • search_help_center 只负责找证据
  • check_order_status 只负责读订单状态
  • create_support_ticket 是明确的写动作,应该受额外确认保护

如果你把这三件事混在一个“solve_customer_problem”工具里,模型就很难学会稳定决策,日志也会失去可分析性。

第二步:不要只教模型“怎么调工具”,还要教它“什么时候不该调”

工具调用最常见的失败,不是模型不会填参数,而是它总想调工具,或者在不该调的时候调错工具。一个实用的系统提示,最好同时说明:

  • 什么情况下可以直接回答
  • 什么情况下必须先检索证据
  • 缺少关键参数时是否应该追问
  • 涉及写操作时是否必须先征得用户确认
  • 工具失败时应该降级、重试还是转人工

例如,你可以把策略写成这样:

你是订单支持助手。

决策规则:
1. 如果问题可以仅凭已有上下文回答,不要调用工具。
2. 如果回答需要订单状态,必须先调用 check_order_status。
3. 如果问题涉及规则说明,优先调用 search_help_center 获取证据。
4. 如果缺少订单号,不要猜测,先向用户追问。
5. create_support_ticket 属于高风险写操作,只有在信息充足且用户明确同意后才能调用。
6. 连续两次工具失败后,停止循环并建议人工处理。

这个阶段的重点不是“写得像论文一样复杂”,而是让模型知道哪些动作属于默认动作,哪些属于受限动作。

第三步:搭建一个最小可控的 Agent 循环

一个常见误解是,Agent 必须无限自由地“自主思考”。实际上,工程里更稳的做法是给它一个有限循环:

  1. 读取当前任务和历史状态
  2. 让模型决定回复用户还是调用工具
  3. 如果调用工具,先做 schema 校验,再执行
  4. 把工具结果作为 observation 写回上下文
  5. 直到完成任务、达到最大步数或命中护栏

下面是一个足够说明问题的最小伪代码:

import json

MAX_STEPS = 6

def run_agent(user_query: str) -> str:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_query},
    ]

    for step in range(MAX_STEPS):
        result = call_model(messages, tools=TOOLS)

        if result["type"] == "message":
            return result["content"]

        if result["type"] == "tool_call":
            tool_name = result["name"]
            arguments = validate_tool_args(tool_name, result["arguments"])

            if needs_confirmation(tool_name, arguments):
                return "这个操作需要你确认后才能继续。"

            observation = execute_tool(tool_name, arguments)

            messages.append(
                {
                    "role": "assistant",
                    "tool_calls": [result],
                }
            )
            messages.append(
                {
                    "role": "tool",
                    "name": tool_name,
                    "content": json.dumps(observation, ensure_ascii=False),
                }
            )

    return "已达到最大处理步数,建议转人工继续。"

这个循环里最关键的不是 for step in range(MAX_STEPS) 这句代码,而是三层控制:

  • 参数校验:模型输出不等于可信输入
  • 高风险确认:写操作不能无条件执行
  • 步数上限:Agent 必须能被收住

如果缺了这三层,系统就很容易出现“明明只是一个小任务,却无限循环或误操作”的问题。

ReAct 思路真正落地时,重点是 Observation,不是长篇推理

ReAct 的核心价值,在于把“思考”和“行动”放进同一个闭环里。但产品工程里,不一定要把模型的完整推理链都暴露出来。更实用的做法通常是:

  • 让模型生成一句简短的行动计划,而不是长篇 reasoning
  • 保留结构化 observation,确保每次工具返回都能被后续步骤可靠消费
  • 把循环日志记录在系统内部,用于排障和评测,而不是直接回显给用户

换句话说,ReAct 的工程重点更像是:

关注点更推荐的做法
计划表达一句简短行动意图或结构化状态
工具结果严格结构化,方便后续消费
最终回答面向用户重新组织,不直接转储中间轨迹
排障方式看调用轨迹、步数、参数与 observation,而不是只看最终一句回复

真正让 Agent 变稳的,往往不是更长的“思维链”,而是更干净的状态和 observation。

第四步:为失败设计兜底,而不是等失败出现后再补洞

工具调用系统最容易出现的错误,大致可以分成下面几类:

失败类型典型表现常见防护
选错工具该检索时去创建工单强化决策规则,增加 few-shot 反例
参数缺失漏掉订单号、日期、用户 IDschema 校验 + 缺参追问
参数格式错日期格式、枚举值不合法解析前校验,失败后让模型修复
工具超时API 卡住,循环停滞超时控制、熔断、降级回复
幻觉 observation模型编造工具结果工具结果只能来自执行器,不允许模型自己写
写操作误触发未确认就发起退款或建单高风险动作二次确认
无限循环一直重复检索或重复同一个动作最大步数、重复动作检测

很多时候,Agent 的“稳定性”不是靠更强模型自然涌现出来的,而是靠你是否把这些失败路径提前堵住。

一个好用的经验法则:把读操作和写操作分成两条思维路径

在工程里,你最好默认:

  • 读操作:允许模型较自由地探索、检索、总结
  • 写操作:需要更强约束、更窄 schema、更明确确认

例如:

  • 查知识库、查订单、查库存,通常属于低风险读操作
  • 创建工单、发起退款、修改日程、发送邮件,通常属于高风险写操作

一旦进入写操作,你应该额外补上:

  1. 用户确认
  2. 参数回显
  3. 幂等保护
  4. 审计日志

否则用户一句模糊表达,就有可能被系统错误解释成真实业务动作。

第五步:不要只看 demo 效果,要建立 Agent 评测闭环

Agent 系统如果没有评测,通常会出现一个错觉:

演示时看起来很聪明,线上一跑就到处漏水。

最小可用的评测维度,建议至少覆盖下面这些:

  • 工具选择准确率:是否选对了工具
  • 参数填写准确率:是否填对必填字段、枚举和值格式
  • 任务完成率:最终是否解决了问题
  • 平均步数:是否经常走太多步
  • 升级率:何时选择转人工,是否过度保守或过度激进
  • 错误恢复能力:遇到工具失败后是否会合理降级

如果你要做自动评测,可以把样本设计成下面这种结构:

{
  "input": "我的订单已经扣款,但后台还是显示未支付。",
  "expected_tool_path": ["check_order_status", "create_support_ticket"],
  "required_args": {
    "issue_type": "payment"
  },
  "success_criteria": [
    "先查询订单状态",
    "写操作前要求用户确认或已满足预设确认条件",
    "最终回复解释清楚下一步"
  ]
}

评测数据不一定要很多,但必须覆盖真实失败类型。否则你会不停优化看起来“更聪明”的案例,却对高频坏案例没有任何约束。

什么场景适合 Agent,什么场景不适合

下面这个判断框架通常很有用:

场景更适合什么方案
固定表单抽取、固定 JSON 输出结构化输出,不一定要 Agent
明确 DAG 的审批或 ETL 流程工作流编排
需要查多个工具、根据中间结果继续决策Agent 或半 Agent
强合规、低容错写操作更强人工审核或传统流程
强实时、超低延迟要求尽量减少循环,优先固定流程

真正成熟的系统往往不是“全站 Agent 化”,而是:

  • 能不用 Agent 的环节不用
  • 必须开放决策的环节才放模型进循环
  • 一旦进入高风险区域,随时可以退回确定性工作流

常见误区

1. 工具越多越强

工具过多会显著增加选择难度和歧义。先把最核心的 3 到 5 个工具做稳定,通常比一口气接 20 个工具更有效。

2. 只测最终回答,不看中间轨迹

最终答案看起来像是对的,不代表路径是对的。线上事故往往藏在“工具其实调错了,但刚好结果还能圆回来”的轨迹里。

3. 把权限控制交给模型

模型可以辅助判断,但权限校验必须在后端做。特别是涉及用户资产、隐私和外部系统写入时,不能只靠提示词约束。

4. 追求完整自主,而忽略人工接管

一个会在关键时刻优雅转人工的 Agent,通常比一个永远想自己硬做到底的 Agent 更可靠。

练习与思考题

  1. 选一个你熟悉的业务场景,拆出 3 个读工具和 1 个写工具,并解释为什么要这样分层。
  2. 为一个“会议安排助手”设计最小 schema,要求能处理查空闲时间、创建会议和发送确认三类动作。
  3. 自己写 10 条评测样本,覆盖缺参、工具超时、错误工具选择和高风险写操作四类失败路径。

延伸阅读

相关阅读

从相近主题继续深入,建立连续学习链路。