先说结论
做了半年 AI Coding Agent,踩的坑比写的功能还多。
掘金上"从零开发 Coding Agent"系列很火,但大部分文章止步在"工具调用闭环"就结束了------好像能调工具就万事大吉。
实际上,工具调通只是 Day 1。真正让你半夜爬起来修 bug 的,是那些**"看起来设计没问题,但跑起来会坑死人"的决策**。
今天不写工具调用怎么实现,写 5 个我在生产环境踩过的设计陷阱。
陷阱一:以为 LLM 不会在循环里撒野
最经典的坑。
Agent 拿到一个任务,调了工具 A,结果不对,再调一次,还是不对,再调一次......
你以为最多重试 3 次就停了,实际上 LLM 会换 5 种不同的方式调同一个工具,每次微调参数,直到 token 烧光。
我亲眼见过一个 Agent 为了解析一个 CSV 文件,连续调了 17 次 read_file,每次传不同的 encoding 参数,最后 token 账单比这个 CSV 的业务价值还高。
修复方案:
python
# 不是限制重试次数,是限制同一个工具的调用窗口
from collections import defaultdict
from datetime import datetime, timedelta
tool_call_log = defaultdict(list)
TOOL_WINDOW = 60 # 60秒内同一工具最多调3次
TOOL_MAX_PER_WINDOW = 3
def check_tool_rate(tool_name):
now = datetime.now()
window_start = now - timedelta(seconds=TOOL_WINDOW)
# 清理过期记录
tool_call_log[tool_name] = [
t for t in tool_call_log[tool_name] if t > window_start
]
if len(tool_call_log[tool_name]) >= TOOL_MAX_PER_WINDOW:
return False # 拒绝调用
tool_call_log[tool_name].append(now)
return True
关键不是"限制重试",是限制同一个工具在时间窗口内的调用频次。LLM 换参数重试也算一次。
陷阱二:工具返回的 JSON 你信了
LLM 调工具的时候,你的工具返回了一个 JSON,然后 LLM 把这个 JSON 的字段拿去用。
看起来没问题对吧?
问题在于:工具的返回值可能比 LLM 预期的长得多。
我有个查数据库的工具,返回了 2000 行结果。LLM 拿到后,试图把全部数据塞进它的上下文,然后基于这些数据做分析。
结果就是:上下文直接爆了,后面的对话全部丢失,Agent 开始"失忆"。
你以为你返回的是数据,LLM 以为它拿到的是整个世界。
修复方案:
python
def query_database(sql, limit=20):
"""工具函数:查数据库,但强制分页"""
results = execute_sql(sql)
if len(results) > limit:
return {
"data": results[:limit],
"total": len(results),
"truncated": True,
"hint": f"结果被截断,共{len(results)}行,只返回前{limit}行。如需更多请用 OFFSET 分页。",
"next_action": "建议先用 COUNT(*) 确认总量,再决定是否分页查询"
}
return {"data": results, "total": len(results), "truncated": False}
核心思路:工具永远不要返回超过 LLM 能消化的数据量。截断不算什么,给个提示让 LLM 知道数据被截了,它会自己分页。
陷阱三:多步任务中途断了,你以为能续上
Agent 执行一个 10 步的任务,跑到第 7 步断了(网络问题、超时、进程重启)。
你的第一反应是:把上下文存下来,重启后恢复。
但实际恢复的时候你会发现:
- 第 4 步的副作用已经发生了(比如已经给数据库写了一条记录)
- 第 5 步和第 6 步的结果不在上下文里(因为中间断了没存)
- LLM 重新执行的时候不知道哪些步骤已经做了,很可能重复执行
重复执行数据库写入的后果不用我多说了吧。
修复方案不是"更好地保存上下文",是给每一步加幂等性:
python
def agent_step(step_id, task, context):
"""每一步都先检查是否已完成"""
# 检查这一步的结果是否已经在状态存储里
existing = state_store.get(step_id)
if existing and existing["status"] == "completed":
return existing["result"] # 直接返回,不重新执行
# 标记为执行中
state_store.set(step_id, {"status": "running", "started_at": time.time()})
try:
result = execute(task, context)
state_store.set(step_id, {"status": "completed", "result": result})
return result
except Exception as e:
state_store.set(step_id, {"status": "failed", "error": str(e)})
raise
每一步的副作用操作都要设计成可重入 的。比如数据库写入用 INSERT ... ON CONFLICT 或者先查再写。
Agent 断了不可怕,可怕的是断了之后重新跑一遍,把已经做过的事情又做了一遍。
陷阱四:把工具描述写给人看,不是写给 LLM 看
这个坑是最隐蔽的。
你的工具有个 description:"查询用户信息"。
你觉得描述得很清楚。但 LLM 看到的是:
- 什么时候该用这个工具?
- 返回什么格式?
- 参数
user_id是字符串还是数字? - 查不到用户会返回什么?空对象还是报错?
LLM 不知道,于是它会猜。猜错了,调出来的结果不对,它再换一种方式猜。
这就是为什么很多人的 Agent"看起来能用但偶尔抽风"------不是模型不行,是工具描述写得太含糊。
好的工具描述应该长这样:
python
tools = [
{
"name": "query_user_info",
"description": "根据用户ID查询用户基本信息。当用户询问'某某的资料''查一下这个用户'时使用。返回用户对象,包含name/email/role字段。如果用户不存在,返回空对象{},不会报错。",
"parameters": {
"user_id": {
"type": "string",
"description": "用户的唯一标识符,格式为'U'开头加6位数字,如'U123456'"
}
},
"returns": "JSON对象 {name: string, email: string, role: string}",
"errors": "用户不存在时返回空对象{},不抛异常",
"example": "query_user_info(user_id='U123456') → {name: '张三', email: 'zhangsan@xx.com', role: 'admin'}"
}
]
对比一下你的工具描述,差了多远?
工具描述不是文档,是 prompt。你写给 LLM 看的每一行字,都在影响它的决策质量。
陷阱五:没有"我不知道"这个选项
Agent 最危险的行为不是做错,是不知道自己不知道。
用户问了一个超出工具能力范围的问题,Agent 没有"我不知道"的选项,于是它硬着头皮调工具,用不完整的数据拼出一个看起来像样的答案。
这个答案可能是错的,但用户不知道,因为 Agent 说得很自信。
修复方案:在系统 prompt 里强制加一条:
arduino
如果你无法通过现有工具获取足够信息来回答问题,必须直接回复"当前工具无法获取足够信息来回答这个问题"。
不要猜测,不要用不完整的数据拼凑答案,不要编造。
并在工具层面加一个 fallback:
python
def agent_route(task, tools):
"""路由前先判断:有没有工具能处理这个任务?"""
capabilities = "\n".join(f"- {t['name']}: {t['description']}" for t in tools)
judge = call_model(f"""判断以下任务是否能用现有工具完成。
可用工具:
{capabilities}
任务:{task}
只回答 YES 或 NO,以及一句话理由。""")
if judge.startswith("NO"):
return "当前工具无法获取足够信息来回答这个问题。建议:[根据任务给出人工处理建议]"
# 正常路由
return route_to_tool(task, tools)
给 Agent 一个体面认输的选项,比让它硬撑好得多。
总结
这 5 个坑的共同特征是:在设计阶段看起来都没问题,只在生产环境的边缘 case 里爆雷。
如果你正在做 AI Coding Agent,逐一对照检查:
- 同一工具的时间窗口调用限制
- 工具返回值的截断和分页
- 多步任务的幂等性设计
- 工具描述是写给 LLM 的 prompt 不是写给人的文档
- Agent 必须有"我不知道"的退路
工具调用闭环是 Day 1 的事,上面这些才是 Day 30 的事。
数据出处:掘金热门榜 2026-08-16,"从零开发 Coding Agent"系列在热门 30 篇中占 2 篇。文中代码示例来自生产环境实际使用,已脱敏处理。