作者:前端转 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,但生产环境很危险:
- 工具名可能不存在。
- 参数可能缺失或类型错误。
- 用户可能没有权限。
- 工具可能是高风险操作。
- 执行失败时没有结构化错误。
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 个真实但低风险的工具,比如
getOrderStatus。 - 定义
ToolCall和ToolResult。 - 写工具注册表。
- 写
runTool调度器。 - 处理未知工具、参数错误和工具异常。
- 在前端展示工具调用过程。
- 给高风险工具加确认态和权限校验。
- 再接真实模型,让模型生成 tool call。
注意这个顺序。
不要一上来就接真实数据库、真实写入权限和一堆复杂工具。
先把工程骨架跑通,再把模型接进来。
这就是前端开发者熟悉的方式:先 mock 数据,跑通交互,再接真实接口。
结语
Agent 的第一步,不是幻想模型变成全能员工。
更稳的起点是:让模型提出工具调用意图,让程序在可控边界内执行真实工具。
这听起来没有“自主智能体”那么炫,但更接近工程现实。
前端开发者其实非常适合理解这件事。你早就熟悉事件、接口、状态、权限和错误处理。Tool Calling 只是把自然语言放进这套系统里,让用户不必点对每一个按钮,也能触发正确能力。
当你能讲清楚模型和工具的责任边界,能写出一个稳定的工具调度器,能处理错误、权限和确认,你就已经不只是在“调用 AI”。
你开始在做 AI 工程。