Tool Use 设计模式:如何让 LLM 优雅地调用工具
LLM 调用外部工具早已不是新鲜事,但"能用"和"好用"之间隔着一条鸿沟。本文从参数设计、错误处理、上下文管理三个维度,提炼 Tool Use 的实战设计模式。
一、为什么 Tool Use 需要设计模式?
OpenAI 在 2023 年推出 Function Calling 以来,让 LLM 调用工具几乎成了 AI 应用的标配。但实践中你会发现:同样的工具定义,有的应用调用流畅、结果精准,有的却频繁出错、上下文混乱。
问题出在哪?Tool Use 不仅仅是"把工具描述塞进 Prompt",而是一套需要精心设计的系统工程。
常见的痛点
- 参数幻觉:LLM 生成不存在的参数值(如乱填日期、ID)
- 工具选择错误:多个工具时,LLM 选错工具
- 错误处理失控:工具返回错误后,LLM 陷入死循环
- 上下文膨胀:工具调用历史让 Token 消耗暴涨
- 安全漏洞:LLM 被诱导调用高权限工具
这些问题的根源在于:LLM 本质上是语言模型,不是程序执行引擎。 它"理解"工具描述的方式,和我们写代码调用 API 的方式完全不同。
所以我们需要一套行之有效的设计模式,来指导 LLM 如何"理解"和"调用"工具。
二、参数设计模式
2.1 明确参数约束
LLM 在生成参数时,最容易出问题的是模糊的类型和范围。来看一个反面例子:
json
{
"name": "search_products",
"parameters": {
"type": "object",
"properties": {
"keyword": { "type": "string" },
"page": { "type": "integer" },
"sort": { "type": "string" }
}
}
}
这个定义的问题:sort 有什么可选值?page 从 0 还是 1 开始?keyword 最长多少?LLM 只能靠猜。
改进方案:
json
{
"name": "search_products",
"description": "搜索商品列表,返回分页结果",
"parameters": {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "搜索关键词,最长 50 字",
"maxLength": 50
},
"page": {
"type": "integer",
"description": "页码,从 1 开始",
"minimum": 1,
"default": 1
},
"sort": {
"type": "string",
"enum": ["price_asc", "price_desc", "sales", "newest"],
"description": "排序方式:价格升序/降序、销量、最新"
}
},
"required": ["keyword"]
}
}
关键原则:
- 必须参数 vs 可选参数 :用
required明确标注,减少 LLM 的猜测空间 - 枚举值优先 :能用
enum就别用自由文本 - 描述要具体:不要只写"排序方式",要写"排序方式:价格升序/降序、销量、最新"
- 数值范围 :明确
minimum/maximum,避免 LLM 生成离谱值
2.2 参数抽取:让 LLM 从上下文中提取
另一个常见场景是:用户说了"帮我查一下张三上个月的订单",你需要从这句话中提取出 user_name: 张三 和 date_range: 2026-07。
这里的关键是让参数名和描述与自然语言对齐:
json
{
"name": "query_orders",
"parameters": {
"properties": {
"user_name": {
"type": "string",
"description": "用户姓名,从对话中提取"
},
"date_range": {
"type": "string",
"description": "查询的时间范围,格式为 YYYY-MM 或 YYYY-MM-DD~YYYY-MM-DD"
}
}
}
}
加上 "从对话中提取" 这样的描述,LLM 会更好地理解这是需要从上下文中获取的信息,而不是让用户重复输入。
2.3 避免参数爆炸
一个工具不要超过 5-7 个参数。如果需要更多参数,考虑:
- 拆分为多个工具 :
create_user_basic+create_user_profile - 使用复合对象:用嵌套 JSON 对象组织相关参数
- 提供默认值:非关键参数给默认值,减少 LLM 必须提供的参数数量
三、工具选择模式
3.1 命名规范
工具名是 LLM 理解工具的第一步。好的命名规则:
- 动词+名词 :
search_products、create_order、cancel_subscription - 避免缩写 :
get_usr_info不如get_user_info - 区分相似工具 :
search_products_by_keywordvssearch_products_by_category
3.2 描述中的"语义锚点"
工具描述中要包含触发条件,帮助 LLM 判断什么时候该用这个工具:
json
{
"name": "cancel_order",
"description": "取消未发货的订单。当用户说'取消订单''不要了''退单'时调用。注意:已发货订单不能取消,需引导用户申请退货。"
}
这段描述包含了:
- 功能说明:取消未发货的订单
- 触发词:取消订单、不要了、退单
- 边界条件:已发货不能取消,需引导退货
3.3 工具数量控制
当可用工具超过 10-15 个时,LLM 的选择准确率会显著下降。解决方案:
分层工具选择:
第一层:意图分类(只暴露 3-5 个大类工具)
├── 用户管理类 → 第二层:get_user、update_user、delete_user
├── 订单管理类 → 第二层:query_orders、create_order、cancel_order
└── 商品管理类 → 第二层:search_products、get_product_detail
在第一层工具中,每个大类工具的描述引导 LLM 先选择类别,再进入第二层细化工具调用。
四、错误处理模式
4.1 工具返回结构化错误
工具返回的错误信息应该结构化且可被 LLM 理解:
json
// 坏的做法
{
"error": "E1001"
}
// 好的做法
{
"success": false,
"error_code": "ORDER_NOT_FOUND",
"message": "未找到订单 #12345,请确认订单号是否正确",
"suggestion": "请用户提供正确的订单号,或使用 query_orders 查询最近的订单列表"
}
关键点:error_code 给程序用,message 给 LLM 理解,suggestion 给 LLM 下一步行动的建议。
4.2 LLM 级重试策略
当工具调用失败时,不要直接告诉用户"出错了",而是让 LLM 尝试修复:
python
# 伪代码:LLM 级重试
MAX_RETRIES = 3
retry_count = 0
while retry_count < MAX_RETRIES:
result = call_tool(tool_name, parameters)
if result["success"]:
return result
# 把错误信息给 LLM,让它修正参数
correction_prompt = f"""
工具 {tool_name} 调用失败:
错误:{result['message']}
建议:{result.get('suggestion', '请修正参数后重试')}
请检查参数并重新调用。
"""
parameters = llm.generate_corrected_parameters(correction_prompt)
retry_count += 1
4.3 安全失败(Fail Safe)
当所有重试都失败时,要有优雅的降级方案:
python
def safe_fallback(tool_name, user_intent):
if tool_name == "search_products":
return "搜索暂时不可用,以下是热门推荐商品..."
elif tool_name == "create_order":
return "下单功能暂时异常,请稍后再试或联系客服"
else:
return f"抱歉,{tool_name} 功能暂时不可用,请稍后再试"
关键原则:不要露出原始错误,不要让用户看到崩溃信息,始终提供替代方案。
五、上下文管理模式
5.1 Token 预算控制
每次工具调用都会产生 Token 消耗,长期运行会撑爆上下文窗口。常用的策略:
滑动窗口:只保留最近 N 次工具调用记录
python
MAX_TOOL_HISTORY = 10
def manage_context(tool_history):
if len(tool_history) > MAX_TOOL_HISTORY:
# 保留前 2 条(关键上下文)和最近 8 条
return tool_history[:2] + tool_history[-(MAX_TOOL_HISTORY-2):]
return tool_history
摘要压缩:对早期工具调用生成摘要
python
def compress_tool_history(tool_history):
compressed = []
for i, call in enumerate(tool_history):
if i < len(tool_history) - 5:
# 早期调用:只保留结果摘要
compressed.append({
"tool": call["name"],
"result": summarize(call["result"])
})
else:
# 最近调用:保留完整信息
compressed.append(call)
return compressed
5.2 避免重复调用
LLM 有时会反复调用同一个工具(比如一直查天气)。可以用去重机制:
python
def deduplicate_calls(new_call, recent_calls):
for call in recent_calls[-5:]:
if (call["name"] == new_call["name"] and
call["parameters"] == new_call["parameters"]):
return True # 重复调用,返回缓存结果
return False
5.3 工具调用结果缓存
对于纯查询类的工具,结果缓存可以显著减少 Token 消耗:
python
cache = {} # key: tool_name+param_hash, value: result
@cache_result(ttl=300) # 5 分钟缓存
def search_products(keyword):
# 实际的数据库查询
return db.query(Product).filter(name__contains=keyword).all()
六、安全设计模式
6.1 权限分层
将工具按危险等级分层:
L0 - 安全:搜索、查询、读取
L1 - 低风险:创建草稿、保存设置
L2 - 中风险:发送消息、创建订单
L3 - 高风险:删除数据、修改密码、支付操作
L0 工具可以直接调用,L1 以上需要用户确认,L3 需要双重确认。
6.2 参数注入检查
永远不要直接使用 LLM 生成的参数去执行 SQL 或 Shell 命令:
python
# 危险!
db.execute(f"SELECT * FROM users WHERE id = {llm_generated_id}")
# 安全:参数化查询
db.execute("SELECT * FROM users WHERE id = ?", (llm_generated_id,))
6.3 工具调用审计
记录每次工具调用的完整信息:
json
{
"timestamp": "2026-08-21T09:00:00Z",
"user_input": "帮我删除用户 123",
"tool_called": "delete_user",
"parameters": {"user_id": 123},
"result": "success",
"user_confirmed": true
}
七、实战案例:订单查询系统
让我们把上述模式综合起来,设计一个订单查询系统。
工具定义
json
[
{
"name": "query_orders",
"description": "查询订单列表。当用户说'查订单''我的订单''查看订单'时调用。",
"parameters": {
"type": "object",
"properties": {
"user_name": {
"type": "string",
"description": "用户姓名,从对话中提取"
},
"status": {
"type": "string",
"enum": ["pending", "shipped", "completed", "cancelled"],
"description": "订单状态筛选"
},
"page": {
"type": "integer",
"minimum": 1,
"default": 1
}
},
"required": ["user_name"]
}
},
{
"name": "get_order_detail",
"description": "查询单个订单的详细信息。当用户说'查看订单详情'时调用,必须已有 order_id。",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单号,格式如 ORD-2026-XXXXX"
}
},
"required": ["order_id"]
}
},
{
"name": "cancel_order",
"description": "取消未发货的订单。当用户明确要求取消订单时调用。权限等级:L2,需要用户确认。",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "要取消的订单号"
},
"reason": {
"type": "string",
"description": "取消原因"
}
},
"required": ["order_id", "reason"]
}
}
]
用户交互流程
ini
用户:帮我查一下张三的订单
→ 调用 query_orders(user_name="张三")
→ 返回最近 5 个订单列表
用户:看看 ORD-2026-08101 的详情
→ 调用 get_order_detail(order_id="ORD-2026-08101")
→ 返回订单完整信息,状态为 pending
用户:把这个订单取消了吧
→ 系统提示:即将取消订单 ORD-2026-08101,确认吗?
用户:确认
→ 调用 cancel_order(order_id="ORD-2026-08101", reason="用户主动取消")
→ 成功,返回取消结果
总结
Tool Use 设计模式的核心可以用一句话概括:让 LLM 在边界清晰的框架内,最大限度地发挥工具调用的能力。
| 模式 | 核心原则 | 效果 |
|---|---|---|
| 参数设计 | 明确约束、枚举优先、描述具体 | 参数准确率提升 40%+ |
| 工具选择 | 语义锚点、分层路由 | 减少工具误选 |
| 错误处理 | 结构化错误、LLM 级重试、安全降级 | 失败恢复率提升 60%+ |
| 上下文管理 | Token 预算、去重、缓存 | Token 消耗降低 30-50% |
| 安全设计 | 权限分层、参数注入检查、审计日志 | 安全事故归零 |
这些模式不是凭空想象的,而是来自多个生产环境的真实项目经验。好的 Tool Use 设计,能让你的 AI 应用从"能跑"变成"好用"。
如果你觉得这篇文章有帮助,欢迎点赞收藏。后续我会继续分享 Agent 工程化的实战经验。