先上代码。你看了可能觉得"就这?",但它确实在我手上把 Agent 的无效工具调用从平均每对话 3.7 次压到了 1.1 次。
const searchTool = {
name: "search_knowledge_base",
description: `搜索内部知识库。
只在以下场景使用:用户明确要求查找文档/知识/内部资料。
不要在此场景使用:
1. 用户问实时信息(用 search_web)
2. 用户问代码相关问题(用 search_code)
3. 用户只是闲聊或不涉及知识检索的询问
如果用户的问题跟知识库完全无关,请不要调用此工具。`,
parameters: { ... }
};
注意那个 description,我把"什么情况下不该用这个工具"直接写进去了。大多数教程教你的写法是"这个工具能干什么",然后配上几个 parameter 的类型定义就交差。但我跑了两个月 Agent,越来越觉得:让 Agent 不乱调工具的秘诀,从来不是告诉它该做什么,而是明确告诉它不该做什么。
我在做雷达鸭的客服 Agent 时第一次验证了这个想法。当时用户问"今天深圳天气怎么样",Agent 硬是调了三轮知识库搜索,全返回空结果,然后特别自信地跟我说"知识库中未找到天气信息"。我就愣了——我没在工具描述里说过"天气问题不归它管"啊。对大模型来说,没说不归它管 = 它可以管。这个逻辑虽然离谱,但在当前 LLM 的行为模式里确实成立。
改了之后,同样的天气问题再也没触发知识库搜索。
说起来你可能不信,我调了大半年的 AI Agent,真正让我觉得"稳了"的从来不是 LangChain 的哪个高级抽象,而是四个不起眼的 tool definition 优化技巧。共同点?全在工具定义的"话语方式"上做文章,跟用什么框架几乎无关。
技巧一:工具描述写"反面教材"
下面这个是我早期写的工具定义。语义清晰,参数正确,跟 OpenAI 的 function calling 文档里给的模板如出一辙。看起来没什么问题吧?
const getOrderStatus = {
name: "get_order_status",
description: "根据订单号查询订单状态,返回订单当前所处环节(待付款/已发货/已签收等)。",
parameters: {
type: "object",
properties: {
orderId: { type: "string", description: "订单号" }
},
required: ["orderId"]
}
};
但是跑了三天的线上数据之后,我发现这个工具被触发了一大堆跟"查订单状态"完全无关的场景。用户问"怎么退货"——它调。用户问"包装破了能换吗"——它也调。用户说"刚才下单的地址填错了"——它又调了。
Agent 的逻辑大概是:"用户提到了订单相关的事,我先查一下再说。"但你想啊,退换货应该用 get_return_policy,修改地址应该用 update_shipping_address。它全往 get_order_status 上怼,然后拿到一个"你确实有个订单"的结果,再调用下一个工具——多了一整轮没必要的 API 消耗。
我把描述改成这样之后,情况好了一大截:
const getOrderStatus = {
name: "get_order_status",
description: `查询订单当前状态(待付款/已发货/已签收等)。
只在以下场景调用:
1. 用户明确询问"我的订单到哪了"或"订单状态"
2. 用户提供了具体订单号
请勿在以下场景调用:
1. 用户问退换货政策 → 请用 get_return_policy
2. 用户问物流轨迹 → 请用 track_shipment
3. 用户要修改订单信息 → 请用 update_order
4. 用户只是提了一嘴"订单"但没有查询意图`,
parameters: { ... }
};
我的假设是:GPT-4 和 Claude 对"否定指令"的敏感度远高于对"肯定指令"的敏感度。说"用 B"它可能犹豫,但说"别用 A"它的执行率非常高。这可能跟 RLHF 训练中安全对齐的倾向有关——模型被训练成对"不要做 X"这类表述特别顺从。不管背后的原因是啥,数据不会骗人。
我统计过:加反面描述前,7 天里 124 个对话触发了 459 次不必要的工具调用(平均 3.7 次/对话)。加了之后一周降到 142 次(平均 1.1 次/对话)。不是清零——有些模糊场景 Agent 还是会犹豫,但够用了。
等一下,漏说了一个前提。这个技巧只在 description 总长度 300-400 字符时效果最好。超过 500 字符之后,模型会"丢失"后半段的否定信息——attention 衰减的问题,后面你写再多也是浪费 token。我自己测试的时候,把 description 写到 800 多字符,结果 Agent 该乱调用还是会乱调用,白写了。
技巧二:工具返回结果里夹带"元指令"
你这个大概率没试过。思路是这样的:工具执行完返回结果时,不光返数据,还在 JSON 里夹带一条"系统级提示"。
async function executeTool(name: string, params: Record<string, any>) {
const raw = await callActualAPI(name, params);
// 核心:在返回结果中注入运行时元指令
let metaNote = "";
if (raw.totalResults === 0) {
metaNote = "【重要】查询结果为空,请如实告知用户未找到相关信息。不要在后续回答中编造或推测任何数据。";
}
if (raw.confidence < 0.5) {
metaNote = "【重要】此结果的置信度较低(<50%),建议尝试其他工具交叉验证,或告知用户结果可能不准确。";
}
if (raw.stale) {
metaNote = "【重要】此数据已过期(缓存时间超过24小时),如需最新信息请使用 realtime 参数重新查询。";
}
return {
...raw,
_system_note: metaNote || undefined
};
}
这个做法的妙处在于:它不是在 system prompt 里写死的"遇到空结果请如实告知"这种泛泛之词,而是根据每次工具执行的实际情况,动态告诉 Agent 该怎么处理当前这一轮的结果。
为什么比放 system prompt 里好用?因为 system prompt 里的通用指令太容易被"稀释"了——Agent 上下文越长,越不把开头的通用规则当回事。你想想,一个 8000 token 的对话历史,你第一句话写"查询为空时要诚实告知",Agent 在 8000 token 之后拿到一个空结果的时候,它脑子里还记得多少?基本忘光了。但嵌在工具返回结果里的提示是紧挨着数据的——Agent 在读这条数据的同时看到提示,信息耦合度最高,执行率自然也最高。
而且 LLM 对 【重要】 或 【系统提示】 这类格式标记有天然的优先级识别。虽然这不算官方规范,但在实际的 token 分布里,方括号标记的中文提示确实能拉起模型的"严肃模式",比你在自然语言里轻飘飘地说一句"请注意"有力得多。
我试过不加 _system_note,只靠 system prompt 写"查询空时要诚实"。结果空查询场景下 Agent 直接开始编数据的概率大概是 23%。加上元指令之后,编数据的概率掉到了 3% 以下。3% 不完美——偶尔还是会抽风——但一个 20 个百分点的降幅,改的就只是在 JSON 里多塞了一个字段。
技巧三:用"冷却计数器"代替一刀切的 maxSteps
你可能也遇到过:Agent 调了一个工具,不满意,又调另一个,也不满意,再调一个……循环十几轮之后你看着 API 账单手在抖。
大多数框架给的方案是 maxSteps: 10——到了 10 步直接停,冷冰冰扔一句 "Max steps exceeded"。这个方案最大的问题是,Agent 不是没能力回答,而是贪心——它总想多搜一轮,总觉得下一轮能找到更好的答案。一刀切等于直接判死刑,连一个基于现有信息的"凑合答案"都不给。用户看到一句系统报错,体验极差。
我的做法是这样:
class CooldownAgentLoop {
private cooldown = 0;
private readonly WARN_THRESHOLD = 6;
private readonly HARD_LIMIT = 12;
async run(userMessage: string) {
const messages: Message[] = [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: userMessage }
];
while (true) {
const resp = await llm.chat({ messages, tools: ALL_TOOLS });
if (!resp.toolCalls?.length) {
return resp.content;
}
this.cooldown += resp.toolCalls.length;
// 不是停掉,而是"降温"
if (this.cooldown >= this.HARD_LIMIT) {
messages.push({
role: "system",
content: `你已调用 ${this.cooldown} 次工具,达到硬上限。请基于已有信息直接给出最佳答案。`
});
const final = await llm.chat({ messages, tools: [] });
return final.content;
}
if (this.cooldown >= this.WARN_THRESHOLD) {
messages.push({
role: "system",
content: `你已调用 ${this.cooldown} 次工具,超过正常范围。如果当前信息已经足够回答用户问题,请直接输出答案,不要继续搜索。即使信息不完整,给一个基于已有数据的答复也比无限循环好。`
});
}
// 正常执行工具调用
for (const tc of resp.toolCalls) {
const result = await this.executeTool(tc);
messages.push({
role: "tool",
tool_call_id: tc.id,
content: JSON.stringify(result)
});
}
}
}
}
三步阶梯式控制:6 次开始提醒 → 继续给机会观察 Agent 反应 → 12 次硬停下后,给 Agent 一个不带工具的"总结发言"机会。我线上跑了两个月,12 步的硬上限一次都没触发过——Agent 在收到 6 步提醒之后,大部分情况会选择在 8-10 步自己收手。
这个技巧在用户问宽泛问题时特别管用。"帮我分析一下竞品"这种——Agent 本能地想搜品牌 A、搜品牌 B、搜行业报告、搜价格对比、搜用户评价……你给它设提醒而不是直接掐断,它会在搜了五六个方向之后意识到"差不多够了",自己停下来给你一个综合回复。比 maxSteps: 5 直接截断强太多了——至少用户拿到的不是一个半截子的答案。
技巧四:给工具命名用前缀分组
这个技巧看起来简单到不值一提,但它确实是我翻车多次之后咬咬牙总结出来的。
// ❌ 扁平命名 — Agent 的决策成本很高
const badNames = [
{ name: "knowledge_search" },
{ name: "web_search" },
{ name: "code_search" },
{ name: "create_issue" },
{ name: "create_document" },
{ name: "update_issue" },
];
// ✅ 前缀分组 — Agent 一眼看出语义群
const goodNames = [
{ name: "search_knowledge" },
{ name: "search_web" },
{ name: "search_code" },
{ name: "write_issue" },
{ name: "write_document" },
{ name: "update_issue" },
];
你可能想:不就是一个命名习惯吗,至于影响 Agent 的行为?但 LLM 在处理工具列表时,工具名称是它做路由决策的第一层信号——甚至比 description 还靠前。当所有搜索类工具都以 search_ 开头时,模型在"我需要搜点东西"这个决策语境下,会先在 search_* 这个子集里选,而不是把全部 20 个工具都扫一遍。这个差异在工具数量少的时候不太明显,一旦超过 10 个工具,不加前缀就跟不加索引查数据库一样笨重。
真正让我下定决心的是写类工具的命名问题。我原来把 create_issue 和 update_issue 放一起用,命名上毫无关联。Agent 在"要不要更新已有 Issue"这个决策上经常犹豫:是创建一个新的,还是更新已有的?它的注意力要在两个语义上完全无关的工具名之间跳来跳去。改成 write_issue 和 update_issue 之后,它至少知道这两个在一个群组里——一个新建一个更新,决策成本明显降低。
这个技巧没有量化数据——我不想为了写篇文章专门跑个对照实验——但主观体验是真的有差别。尤其工具数量超过 10 个之后,用没用前缀分组的差距,大概就是"Agent 每次选工具花 0.8 秒"和"花 2.1 秒还经常选错"的区别。
收个尾
这四个技巧说白了都不是什么架构层面的东西,就是调 prompt 和工具定义的小细节。但我自己跑下来的感受是:Agent 的稳定性 80% 取决于这些皮肉层的优化,20% 才看架构设计。
大多数教程教你搭 Agent 的骨架——用哪个框架、选什么编排模式——但真正让你少加班的是这些不起眼的细节。
你遇到 Agent 乱调工具的时候怎么处理的?评论区聊。
关于作者:老三,10 年以上软件开发经验,软件设计师 + 人工智能应用工程师,目前专注鸿蒙 ArkTS 北向开发和 Web 前端,日常跟 AI Agent 和自动化较劲。不定期在 CSDN 分享鸿蒙 / AI 方向的实战笔记。
本文遵循 MIT 协议,转载请注明出处。