前端开发者做 Agent:别急着堆 Prompt,先用 Tool Calling 跑通 1 个真实工具

5 阅读9分钟

作者:前端转 AI 深度实践者

【省流助手/核心观点】:Agent 的第一步不是让模型“更聪明”,而是让模型能在可控边界内调用工具。模型负责理解用户意图、提出工具调用;程序负责校验工具名、校验参数、检查权限、执行 API、处理错误和返回结果。对前端开发者来说,Tool Calling 很像“自然语言触发的事件分发 + API 调用”。先跑通 1 个真实工具,比一上来讨论多 Agent、长期记忆、自主规划更有价值。


很多人第一次听到 Agent,会下意识想象一个很强的东西:

  • 它能自己规划任务。
  • 它能自己查资料。
  • 它能自己点按钮。
  • 它能自己写代码。
  • 它能自己完成一整套业务流程。

这些方向都没错,但对刚开始做 AI 应用的前端开发者来说,起点不应该这么远。

如果一上来就聊“多 Agent 协作”“自主规划”“长期记忆”“任务分解”,很容易像第一次学前端就直接冲微前端和低代码平台:概念很热闹,手上却没有一个能稳定跑起来的东西。

Agent 的第一块积木其实非常朴素:

让模型会用工具。

也就是 Tool Calling。

1. 痛点:模型很会说,但它不会真的办事

先把一个事实说清楚:

大模型本身不会真的查数据库、调接口、读文件、发邮件、查订单、改库存。

它最擅长的是生成文本。

比如用户问:

帮我查一下订单 A1001 的物流状态。

如果你把这个问题直接丢给普通聊天模型,它可能回答:

订单 A1001 当前正在配送中。

听起来很像真的,但它大概率没有查过你的订单系统。

在真实业务里,这很危险。订单状态、退款金额、库存数量、用户权限、合同条款,这些信息不能靠模型“猜得像”。它们必须来自真实系统。

所以 Agent 的第一步,不是让模型直接回答,而是让模型提出一个工具调用:

{
  "toolName": "getOrderStatus",
  "args": {
    "orderId": "A1001"
  }
}

这句话的意思是:

我判断用户想查订单,所以请程序调用 getOrderStatus,参数是 A1001。

然后程序真的去查订单 API。工具返回真实结果后,模型再基于结果组织用户能读懂的回答。

这才是 Agent 工程的起点。

2. Tool Calling 的本质:模型提意图,程序做执行

Tool Calling 可以拆成一条很清楚的链路:

用户问题
-> 模型判断是否需要工具
-> 模型输出工具名和参数
-> 程序校验工具调用
-> 程序执行真实工具
-> 工具返回结构化结果
-> 模型基于结果生成最终回答

这里最重要的责任边界是:

  • 模型不是执行器。
  • 模型是意图识别器和参数生成器。
  • 程序才是真正的执行器。

如果把模型当执行器,系统会很危险:模型说删就删,模型说发就发,模型说扣款就扣款。

如果把模型当意图识别器,你会自然地设计边界:哪些工具可用,参数怎么校验,哪些动作要用户确认,哪些错误必须拦截。

一句话:

模型可以提出动作,程序负责决定能不能执行。

3. 错误做法:让模型直接“假装调用了工具”

很多早期 Agent demo 会这么写:

async function askAgent(question: string) {
  const prompt = `
你是一个订单助手。
如果用户询问订单状态,请直接回答订单当前状态。

用户问题:${question}
`;

  return llm.chat(prompt);
}

这段代码的问题是:模型没有任何真实数据来源。

它可能回答得很自然,但你不知道答案来自哪里。更糟的是,它会让用户误以为系统真的查过订单。

另一种半吊子写法是让模型输出工具名,然后程序无脑执行:

async function unsafeRunTool(toolCall: any) {
  const tool = tools[toolCall.toolName];
  return tool(toolCall.args);
}

这段代码看起来像 Tool Calling,但生产环境很危险:

  1. 工具名可能不存在。
  2. 参数可能缺失或类型错误。
  3. 用户可能没有权限。
  4. 工具可能是高风险操作。
  5. 执行失败时没有结构化错误。

Agent 不是“模型说什么程序就做什么”。
Agent 是把模型放进一个可控的工具系统里。

4. 正确做法:先做一个最小工具注册表

对前端开发者来说,可以先把 Tool Calling 理解成一个自然语言版事件系统。

以前是按钮触发 handler:

button.addEventListener("click", handleSearchOrder);

现在是用户自然语言触发工具:

我想看看 A1001 到哪了
-> getOrderStatus({ orderId: "A1001" })

第一步,先定义工具调用和工具结果。

type ToolCall = {
  toolName: string;
  args: Record<string, unknown>;
};

type ToolResult =
  | {
      ok: true;
      toolName: string;
      data: unknown;
    }
  | {
      ok: false;
      toolName?: string;
      error: string;
    };

然后写一个真实工具:

async function getOrderStatus(args: Record<string, unknown>) {
  const orderId = args.orderId;

  if (typeof orderId !== "string") {
    throw new Error("orderId 必须是字符串");
  }

  const orders = {
    A1001: { status: "运输中", eta: "2026-04-28" },
    A1002: { status: "已签收", eta: null }
  };

  return orders[orderId as keyof typeof orders] ?? {
    status: "not_found"
  };
}

再建立一个工具注册表:

const tools = {
  getOrderStatus
};

这就是最小 Agent 工具系统的第一步。

它不炫,但非常关键:模型不再直接编答案,而是通过工具拿真实结果。

5. 调度器要像网关,而不是传声筒

Tool Calling 的核心不是“能不能调用函数”,而是“能不能安全、稳定、可观察地调用函数”。

所以你需要一个工具调度器。

async function runTool(toolCall: ToolCall): Promise<ToolResult> {
  const tool = tools[toolCall.toolName as keyof typeof tools];

  if (!tool) {
    return {
      ok: false,
      toolName: toolCall.toolName,
      error: `未知工具:${toolCall.toolName}`
    };
  }

  try {
    const data = await tool(toolCall.args ?? {});

    return {
      ok: true,
      toolName: toolCall.toolName,
      data
    };
  } catch (error) {
    return {
      ok: false,
      toolName: toolCall.toolName,
      error: error instanceof Error ? error.message : "工具执行失败"
    };
  }
}

这段代码已经有了 Agent 系统的骨架:

  • 工具注册。
  • 工具分发。
  • 参数传递。
  • 错误处理。
  • 结构化返回。

后续复杂 Agent 系统会继续加权限、确认、日志、重试、工具 Schema、状态管理和评测,但地基仍然是这套东西。

6. 前端页面怎么接 Tool Calling?

前端不一定直接执行工具,但前端必须理解这条链路。否则你很难设计出靠谱的 Agent 交互。

一个最小 API 响应可以这样设计:

type AgentResponse = {
  traceId: string;
  answer: string;
  toolCalls: Array<{
    toolName: string;
    args: Record<string, unknown>;
    result: ToolResult;
  }>;
};

页面可以把工具调用过程展示成“执行记录”:

function AgentMessage({ response }: { response: AgentResponse }) {
  return (
    <article>
      <p>{response.answer}</p>

      {response.toolCalls.length > 0 && (
        <details>
          <summary>本次使用的工具</summary>
          {response.toolCalls.map((call, index) => (
            <div key={`${call.toolName}-${index}`}>
              <strong>{call.toolName}</strong>
              <pre>{JSON.stringify(call.args, null, 2)}</pre>
              <pre>{JSON.stringify(call.result, null, 2)}</pre>
            </div>
          ))}
        </details>
      )}
    </article>
  );
}

这个 UI 不一定要对所有用户开放,但对内部系统、开发环境、运营后台非常有用。

因为 Agent 出问题时,你不能只看最终回答,还要看:

  • 模型选了哪个工具?
  • 参数是否正确?
  • 工具有没有成功?
  • 工具返回了什么?
  • 最终回答有没有正确使用工具结果?

这和 RAG 里要看引用来源是同一个逻辑:AI 系统不能只看最后一句话。

7. Tool Calling 和 RAG 是什么关系?

学完 RAG 再学 Tool Calling,有一个好处:你会发现 RAG 也可以被看作一种工具。

比如:

async function searchDocs(args: Record<string, unknown>) {
  const query = args.query;

  if (typeof query !== "string") {
    throw new Error("query 必须是字符串");
  }

  return [
    {
      title: "报销制度",
      content: "试用期员工不可申请差旅报销。"
    }
  ];
}

当用户问制度问题时,模型调用 searchDocs
当用户问订单问题时,模型调用 getOrderStatus
当用户问退款金额时,模型调用 calculateRefund

这样一看,Agent 就不神秘了:

用户问题
-> 判断需要哪种能力
-> 调用对应工具
-> 整合工具结果
-> 回答用户

RAG 是“查知识”的工具。
订单接口是“查业务状态”的工具。
计算函数是“做确定性计算”的工具。

Agent 的能力来自工具组合,而不是模型单独硬扛所有问题。

8. 高风险工具必须加确认和权限

Tool Calling 最容易让人兴奋,也最需要冷静。

查询订单状态是低风险工具。
取消订单、发邮件、扣款、改权限、删除数据就是高风险工具。

高风险工具不能让模型一句话直接执行。

可以先给工具加一个风险层级:

type ToolRisk = "low" | "medium" | "high";

type ToolMeta = {
  risk: ToolRisk;
  requiresConfirmation: boolean;
};

const toolMeta: Record<string, ToolMeta> = {
  getOrderStatus: {
    risk: "low",
    requiresConfirmation: false
  },
  cancelOrder: {
    risk: "high",
    requiresConfirmation: true
  }
};

调度器在执行前先检查:

function shouldAskConfirmation(toolName: string) {
  const meta = toolMeta[toolName];
  return meta?.requiresConfirmation === true;
}

当前端收到需要确认的工具调用时,不要直接执行,而是展示确认态:

function ToolConfirmation({
  toolName,
  args,
  onConfirm,
  onCancel
}: {
  toolName: string;
  args: Record<string, unknown>;
  onConfirm: () => void;
  onCancel: () => void;
}) {
  return (
    <section>
      <p>需要确认后才能执行:{toolName}</p>
      <pre>{JSON.stringify(args, null, 2)}</pre>
      <button type="button" onClick={onConfirm}>
        确认执行
      </button>
      <button type="button" onClick={onCancel}>
        取消
      </button>
    </section>
  );
}

这里的原则很简单:

模型可以建议动作,用户和程序共同决定是否执行。

9. 生产环境避坑指南

1. 查询类工具和写入类工具分开设计

查询类工具通常可以自动执行,比如查订单、查知识库、查库存。

写入类工具要谨慎,比如取消订单、创建工单、发送邮件、修改配置。
这类工具至少要有权限校验、二次确认和操作日志。

2. 不要把原始模型输出直接当参数

模型输出是不可信输入。
即使只是查订单,也要校验参数类型、必填字段和格式。

function assertOrderId(value: unknown): asserts value is string {
  if (typeof value !== "string" || !/^A\d{4}$/.test(value)) {
    throw new Error("orderId 格式不合法");
  }
}

3. 每次工具调用都要有 traceId

Tool Calling 出问题时,最终回答不够排查。

你需要记录:

  • 用户问题
  • 模型输出的 toolCall
  • 工具执行入参
  • 工具返回结果
  • 耗时和错误信息
  • 最终回答

这些数据要通过同一个 traceId 串起来。

4. 工具数量不要一开始就堆太多

工具越多,模型越容易选错。

初期建议只开放 3 到 5 个边界清晰的工具:查文档、查订单、查库存、计算价格、创建草稿工单。

等工具选择稳定后,再逐步扩展。

5. 工具结果要结构化,不要只返回一段文本

坏的返回:

订单正在运输中,预计明天到。

更好的返回:

{
  "status": "shipping",
  "eta": "2026-04-28",
  "carrier": "SF Express"
}

结构化结果更容易被模型二次组织,也更容易被前端展示、日志记录和自动评测。

10. 常见误区

误区 1:Agent 就是更强的 Prompt

不是。Prompt 很重要,但 Agent 的关键是模型、工具、状态、权限、日志、错误处理的组合。

误区 2:模型能输出工具名,就算 Tool Calling 完成了

不算。程序还要校验工具名、校验参数、执行工具、处理错误、记录日志。

误区 3:工具越多越好

不一定。工具太多,模型更容易选错。初期工具要少而清晰,逐步扩展。

误区 4:只要模型会调用工具,就可以自动执行所有操作

很危险。查询类工具和写入类工具要分开看。涉及删除、支付、发送、修改权限的工具,都应该有更严格的确认和权限控制。

11. 给前端开发者的落地清单

如果你准备从 RAG 走向 Agent,可以按这个顺序练:

  1. 先选 1 个真实但低风险的工具,比如 getOrderStatus
  2. 定义 ToolCallToolResult
  3. 写工具注册表。
  4. runTool 调度器。
  5. 处理未知工具、参数错误和工具异常。
  6. 在前端展示工具调用过程。
  7. 给高风险工具加确认态和权限校验。
  8. 再接真实模型,让模型生成 tool call。

注意这个顺序。

不要一上来就接真实数据库、真实写入权限和一堆复杂工具。
先把工程骨架跑通,再把模型接进来。

这就是前端开发者熟悉的方式:先 mock 数据,跑通交互,再接真实接口。

结语

Agent 的第一步,不是幻想模型变成全能员工。

更稳的起点是:让模型提出工具调用意图,让程序在可控边界内执行真实工具。

这听起来没有“自主智能体”那么炫,但更接近工程现实。

前端开发者其实非常适合理解这件事。你早就熟悉事件、接口、状态、权限和错误处理。Tool Calling 只是把自然语言放进这套系统里,让用户不必点对每一个按钮,也能触发正确能力。

当你能讲清楚模型和工具的责任边界,能写出一个稳定的工具调度器,能处理错误、权限和确认,你就已经不只是在“调用 AI”。

你开始在做 AI 工程。