我给 Agent 写了个"清理 N 天前数据"的工具,模型某次把 N 填成了 0,工具没校验就照单执行,把全部数据都删了:一次 Agent 工具参数未校验的深度复盘
那次数据被误删是触目惊心的:我给 AI Agent 写了个工具 cleanup_old_data(days)——"清理 days 天前的数据"。我把工具描述写得清清楚楚,Agent 平时用得也挺好。可某次,Agent 在处理一个模糊的清理请求时,把参数 days 填成了 0;而我的工具拿到 days=0 后没做任何校验,直接执行了 DELETE WHERE created_at < now() - 0 天——也就是删除了"0 天前"(即此刻)之前的全部数据。我盯着这个由"一个被填错的参数"引发的删库,后背发凉:问题出在我把"模型填进工具的参数"当成了"绝对可信、绝对合法"的输入,直接拿来执行,没有做任何校验。我犯的根本错误是:我潜意识里把 LLM 当成了一个"聪明、可靠、不会填错参数"的调用者;可 LLM 是概率性的——它完全可能填出不合理、非法、甚至危险的参数(把 N 填成 0、负数、超大值,把 ID 填成不存在的,把范围填错);而我的工具,对这个"不可信来源"填来的参数,毫无防备地照单全收、直接执行,于是一个 days=0 就酿成了删库。根本原因是:工具没有校验模型填来的参数——而模型填的参数,本质上是一种"不可信的输入",必须像对待任何外部输入(用户输入、API 入参)一样严格校验。问题的根,是把 LLM 填的工具参数当成可信合法输入直接执行、没校验;而 LLM 是概率性的、会填出非法/危险参数。这篇就把这次"Agent 工具参数未校验"的坑,从头到尾复盘一遍。
故障现场:模型把参数填成 0,工具直接删库
问题在于工具对模型填来的参数不做校验、直接执行:
# ✗ 出问题的工具: 对模型填的参数不校验, 直接执行
def cleanup_old_data(days: int):
# ✗ 直接拿模型填的 days 去删, 没校验它合不合理!
cutoff = now() - timedelta(days=days)
db.execute("DELETE FROM records WHERE created_at < ?", cutoff)
return f"已清理 {days} 天前的数据"
# Agent调用这个工具时, 模型(概率性地)把 days 填成了 0:
# cleanup_old_data(days=0)
# → cutoff = now() - 0天 = 此刻
# → DELETE WHERE created_at < 此刻 → 删除了【全部历史数据】! (删库)
# 为什么会这样? 因为我把"模型填的参数"当成了可信、合法的输入:
# 1. 我潜意识假设"模型很聪明, 不会填出 days=0 这种离谱的值";
# 2. 但LLM是【概率性】的, 它完全可能:
# - 把N填成0、负数、超大值; 把ID填成不存在的/猜的; 把枚举值填成没定义的;
# - 在请求模糊、上下文误导、或纯粹"抽风"时, 填出各种非法/边界/危险的参数;
# 3. 而我的工具对这些参数【毫无校验】, 照单全收、直接执行 → 一个days=0就删库。
# 类比: 这就像写一个Web接口, 直接拿用户传的参数去执行危险操作而不做任何校验——
# 只不过这里的"用户", 换成了"会犯错的、概率性的LLM"。
# 而且LLM填参数, 比人填表单更不可控(人填错有限, LLM可能填出任何东西)。
# 关键: 模型填给工具的参数, 是"不可信的输入"(LLM概率性、会填非法/危险值); 工具若不校验就直接执行,
# 一个被填错的参数(如days=0)就可能酿成删库等灾难 —— 工具必须校验模型填的参数。
第一次想明白"是我把模型当成了不会出错的调用者、对它填的参数照单全收"时,我又懊恼又后怕:"我费心把工具描述写清楚,以为模型就会乖乖填对参数;完全没想到它会填出 days=0,更没想到我的工具对此毫无防备、直接就把库删了。"这个坑最危险的地方在于:它把"模型偶尔填错一个参数"这种本该被工具挡下的小概率失误,直接放大成了删库这样的灾难;而且 days 参数平时填得都对(给了你工具很可靠的错觉),偶发一次填错就出大事。下面就来拆解,Agent 的工具该怎么对待模型填的参数。
第一件事:搞懂"模型填的参数是不可信输入"
我顺着这次事故,把 Agent 工具与参数校验的关系彻底理清了。
为什么"模型填给工具的参数"必须当成不可信输入来校验?
【核心: LLM是概率性的、会填出非法/边界/危险参数; 模型填的参数=不可信输入, 工具(尤其有副作用的)必须像校验用户输入一样校验它, 危险操作加确认】
1. 谁在"调用"你的工具? —— 一个概率性的、会犯错的 LLM
- Agent工具的参数, 是LLM根据上下文"生成"的(本质是预测token);
- LLM不是确定性程序, 它【可能】填出: 0/负数/超大值、不存在的ID、非法枚举、格式错的、甚至危险的内容;
- → 它是一个"大概率填对、但小概率填错"的调用者。
2. 关键认知: 模型填的参数 = 不可信输入
- 就像"用户通过表单/API传来的参数"是不可信的(可能恶意/错误)一样;
- "LLM填给工具的参数"也是不可信的(LLM会犯错、会被误导、会抽风);
- → 凡是处理不可信输入的铁律(校验、边界检查、白名单), 都适用于工具参数。
3. 不校验的后果(尤其有副作用的工具):
- 删除类: days=0/负数 → 删了不该删的(本文);
- 数量类: size=100万 → 拖垮系统/OOM;
- ID类: 不存在的/别人的ID → 操作错对象/越权;
- 金额/范围类: 负数/超大 → 业务错乱;
- → 一个非法参数 + 一个有副作用的工具 = 真实灾难。
4. 正确做法: 工具内部严格校验参数
- 范围校验: days必须 >= 1(且 <= 合理上限); size有上限; 金额非负;
- 合法性校验: ID必须存在、属于当前用户; 枚举值在白名单内;
- 边界/危险拦截: 对"会影响大量数据/不可逆"的操作, 额外严格(甚至要人工确认, 同541篇);
- 校验不过: 返回清晰的错误给模型(同553篇), 让它知道参数错了、能改, 而非默默执行。
5. 更深: 把LLM放在"不可信"的位置, 在它和"真实副作用"之间设防护
- LLM负责"决策/填参数", 但它的输出要经过"校验层"才能触达真实操作;
- 这层校验, 是"不可信的LLM"和"真实的、有副作用的执行"之间的安全闸门。
一句话: LLM是概率性的、会填非法/危险参数, 模型填给工具的参数是"不可信输入"; 工具(尤其有副作用的)必须
像校验用户输入一样严格校验它(范围/合法性/白名单)、危险操作加确认、校验不过返回错误给模型, 别照单执行。
这套认知,是整个坑的根。谁在调用你的工具:一个概率性、会犯错的 LLM——工具参数是 LLM 根据上下文生成的,它可能填出 0/负数/超大值、不存在的 ID、非法枚举、危险内容,是"大概率填对、小概率填错"的调用者。关键认知:模型填的参数=不可信输入——就像用户/API 传来的参数不可信一样,LLM 填的参数也不可信(会犯错、被误导、抽风);处理不可信输入的铁律(校验、边界检查、白名单)都适用。不校验的后果(尤其有副作用工具):删除类(days=0 删库)、数量类(size 百万拖垮)、ID 类(操作错对象/越权)、金额类(业务错乱)。正确做法:工具内部严格校验(范围、合法性、白名单)、危险/不可逆操作额外严格甚至人工确认、校验不过返回清晰错误给模型让它能改。更深:把 LLM 放在"不可信"位置,在它和真实副作用之间设"校验层"安全闸门。一句话:LLM 是概率性的、会填非法/危险参数,模型填给工具的参数是"不可信输入";工具(尤其有副作用的)必须像校验用户输入一样严格校验它(范围/合法性/白名单)、危险操作加确认、校验不过返回错误给模型,别照单执行。
第二件事:正解——工具内严格校验模型填的参数、危险操作加确认
搞懂了原理,正解就清晰了:工具内部把模型填的参数当不可信输入严格校验(范围/合法性/白名单),校验不过返回清晰错误给模型;有副作用/不可逆的操作再加人工确认。
# ====== 正解: 工具内严格校验参数 + 危险操作加确认 ======
def cleanup_old_data(days):
# ★ 校验1: 类型和范围(days必须是合理的正整数)
if not isinstance(days, int) or days < 1:
return {"success": False,
"error": f"days 必须是 >= 1 的整数, 收到的是 {days!r}",
"hint": "请确认要清理多少天前的数据, 至少 1 天"} # 返回错误给模型(同553篇)
if days > 3650: # ★ 校验2: 合理上限(防超大值)
return {"success": False, "error": "days 不能超过 3650(10年)"}
# ★ 校验3: 危险操作先算影响范围, 太大就要人工确认
cutoff = now() - timedelta(days=days)
affected = db.count("SELECT count(*) FROM records WHERE created_at < ?", cutoff)
if affected > 10000: # 影响过多, 不让Agent自己拍板
if not request_human_confirmation(f"将删除 {affected} 条数据(days={days}), 确认?").approved:
return {"success": False, "error": "影响数据量大, 用户未确认, 已取消"}
db.execute("DELETE FROM records WHERE created_at < ?", cutoff)
return {"success": True, "message": f"已清理 {days} 天前的数据, 共 {affected} 条"}
# → days=0 进来: 校验1直接拦下, 返回错误给模型(模型会知道填错了、可改), 不会再删库。
# ====== 校验工具参数的要点 ======
# 1. 把模型填的参数当"不可信输入": 像校验用户输入/API入参一样校验, 别假设模型填得对;
# 2. 范围/类型校验: 数字的上下界(days>=1、size<=上限)、类型对不对、非负等;
# 3. 合法性校验: ID存在且属于当前用户(防越权)、枚举值在白名单、格式合法;
# 4. 危险/不可逆操作额外防护: 先算影响范围, 过大/不可逆就要人工确认(同541篇), 或做软删除可回滚;
# 5. 校验不过返回清晰错误(同553篇): 让模型知道参数错了、错在哪、怎么改, 而非默默执行或崩;
# 6. 用schema约束工具参数: 很多框架支持给工具参数定义JSON Schema(类型/范围/枚举), 让框架先校验一道。
# ====== 一个心智模型 ======
# - 把Agent的工具, 当成一个【对外暴露的、会被不可信调用者(LLM)调用的API】来设计;
# - API怎么防御不可信调用(校验、限权、限流、确认), 工具就怎么防御LLM;
# - LLM比普通用户更不可预测(可能填出任何东西), 所以工具的防御要【更严】, 不能更松。
# 核心: 工具把模型填的参数当不可信输入严格校验(范围/合法性/白名单)、危险操作加人工确认、
# 校验不过返回错误给模型; 像设计"对不可信调用者开放的API"那样设计Agent工具, 别照单执行。
修复的核心,是"工具把模型填的参数当不可信输入严格校验,危险操作加确认"。正解:工具内严格校验 + 危险操作加确认——校验类型范围(days>=1 且有上限)、合法性、危险操作先算影响范围过大就人工确认;days=0 进来被校验直接拦下、返回错误给模型(它会知道填错可改),不再删库。校验要点:把参数当不可信输入、范围/类型校验、合法性校验(ID 存在且属当前用户防越权、白名单)、危险/不可逆操作额外防护(算影响范围+人工确认+软删除)、校验不过返回清晰错误、用 JSON Schema 约束参数让框架先校验一道。心智模型:把 Agent 工具当成"对外暴露的、会被不可信调用者(LLM)调用的 API"来设计;API 怎么防御不可信调用,工具就怎么防御 LLM;LLM 比普通用户更不可预测,防御要更严。归根结底:工具把模型填的参数当不可信输入严格校验(范围/合法性/白名单)、危险操作加人工确认、校验不过返回错误给模型;像设计"对不可信调用者开放的 API"那样设计 Agent 工具,别照单执行。
第三件事:AI Agent 工具安全与健壮性的其他要点
排查后我把 Agent 工具的安全、健壮性相关的其他要点也系统梳理了一遍。
AI Agent 工具安全与健壮性的其他要点
# 1. 参数未校验(本文): 模型填非法参数照单执行。→ 当不可信输入严格校验。
# 2. 工具错误没回传(同553篇): 失败模型不知情谎报成功。→ 如实回传错误。
# 3. 有副作用工具没幂等(同541篇): 重试/重复调重复执行。→ 幂等键+确认。
# 4. 工具权限过大: 给工具的权限超出任务所需(能删能改一切)。→ 最小权限。
# 5. 危险/不可逆操作Agent自己拍板: 删库/转账没人把关。→ human-in-the-loop确认。
# 6. 没有审计: Agent调了什么工具、传了什么参数没记录。→ 全程审计可追溯。
# 7. 没有沙箱/限制: 让Agent执行任意代码/命令而不隔离。→ 沙箱、白名单、资源限制。
# 8. 工具返回过大: 海量结果塞爆context。→ 分页/摘要/截断/存引用按需取。
# 共同根源: Agent是"用一个不可信、概率性的LLM, 去驱动一组能操作真实世界的工具"; 这个组合的安全性,
# 不能寄望于"LLM总是做对的事", 而要靠"工具这一侧的严格防御"——把每个工具都当成"面对一个会犯错、
# 甚至可能被诱导做坏事的调用者"来加固(校验、限权、确认、审计、沙箱)。
# 核心: Agent的安全防线在"工具侧"而非"模型侧"——把模型当不可信调用者, 工具做严格校验、最小权限、
# 危险操作确认、全程审计、必要时沙箱; 别指望LLM永远填对、做对, 要让工具挡住它的错误和风险。
排查让我把 Agent 工具安全与健壮性的其他要点也梳理清了。一、参数未校验(本文)。二、工具错误没回传。三、有副作用工具没幂等。四、工具权限过大。五、危险操作 Agent 自己拍板。六、没有审计。七、没有沙箱/限制。八、工具返回过大。它们的共同根源是:Agent 是"用一个不可信、概率性的 LLM,去驱动一组能操作真实世界的工具";这个组合的安全性,不能寄望于"LLM 总是做对的事",而要靠"工具这一侧的严格防御"——把每个工具都当成"面对一个会犯错、甚至可能被诱导做坏事的调用者"来加固。核心是:Agent 的安全防线在"工具侧"而非"模型侧"——把模型当不可信调用者,工具做严格校验、最小权限、危险操作确认、全程审计、必要时沙箱;别指望 LLM 永远填对、做对,要让工具挡住它的错误和风险。下面这张图,是这次参数未校验坑的成因与解法:
第四件事:不可信输入来源对比表
这次踩坑后,我把几种"不可信输入来源"对比成一张表,把 LLM 也明确列了进去。
| 输入来源 | 为什么不可信 | 可预测性 | 防御 |
|---|---|---|---|
| 用户表单/API 入参 | 可能错误/恶意 | 较可预测(范围有限) | 校验/限权 |
| 外部系统返回 | 可能异常/变更 | 较可预测 | 校验/容错 |
| LLM 填的工具参数(本文) | 概率性、会犯错/被诱导 | 更不可预测(可能填任何东西) | 更严格校验+确认 |
| 配置/环境 | 可能配错 | 较可预测 | 校验/默认值 |
这张表把 LLM 在"不可信输入"里的位置钉清了。核心是:我们早就习惯了"用户输入不可信、要校验"这条铁律;而 LLM 填的参数,是一种"新的、甚至更不可信的"输入来源——用户填错有限(受表单约束、有常识),而 LLM 是概率性的、可能填出任何东西(包括人类绝不会填的离谱值),它的不可预测性更高;可我们却容易因为"它很聪明"而不自觉地信任它、放松了对它输出的校验。它给我的最大启发是:随着 LLM 进入系统,我们的"不可信输入"清单上,多了一个新成员——LLM 的输出;"用户输入不可信"这条老智慧,要扩展成"包括 LLM 在内的、一切'系统边界之外/由不确定来源产生'的输入都不可信";而且因为 LLM "看起来聪明",它的不可信反而更容易被忽视——这让它比传统不可信输入更危险。这给了我一种构建 AI 系统时的根本边界意识:在引入 LLM 的系统里,要清醒地划出"信任边界"——把 LLM(及其输出)放在边界的"不可信"一侧,它的任何输出(填的参数、给的内容、做的决策)在被系统"当真"执行前,都要经过校验这道关;"把 LLM 明确纳入'不可信输入'的范畴、对其输出一律校验、别因它'聪明'就给它特权",是构建安全 AI 系统的基本边界纪律。认清 LLM 是更不可信的输入来源、把它纳入不可信范畴一律校验——是这个坑带给我的认知。
第五件事:这次事故暴露的"信任带来的疏于防范"
这次让我反思更深一层:我之所以不校验,根上是因为我"信任"了那个聪明的模型。我把"信任它"和"防范它"对比成表。
| 维度 | 信任模型(我当时的心态) | 防范模型(正确的) |
|---|---|---|
| 对模型填的参数 | 觉得它会填对, 照单执行 | 当不可信输入, 一律校验 |
| 对危险操作 | 让它自己拍板 | 加确认/限权 |
| 出错时 | 毫无防备, 直接酿祸 | 工具挡下, 不出事 |
| 心态根源 | "它很聪明, 应该没问题" | "它会犯错, 我要兜住" |
| 本质 | 用信任代替了防范 | 不因信任而放弃防范 |
这张表道出了最深的教训。核心是:我栽跟头的心态根源,是"因为觉得它聪明可靠, 就用'信任'代替了'防范'"——我想"模型这么聪明, 应该不会填出 days=0 这种离谱值吧", 于是省掉了本该有的校验;可"聪明"不等于"不会犯错",尤其是概率性的 LLM;我的信任, 恰恰让我在它犯错时毫无防备。它给我的深刻启发是:"信任"是一种宝贵的东西,但在系统安全/健壮性的语境里,"信任不该代替防范"——哪怕你信任一个组件/人/模型很可靠,也不该因此省掉"万一它出错"的防护;真正健壮的系统, 不是"建立在'各部分都可靠'的信任之上", 而是"即使某部分不可靠/出错, 也能兜住";"用信任省掉防范", 是脆弱系统的常见根源。这给了我一种设计健壮系统的根本原则:设计系统时,要把"信任"和"防范"分开——可以在'正常协作'上信任各方, 但在'关键的、有严重后果的'环节, 必须保留"不信任的防范"(校验、确认、限权、兜底),假设"任何一方都可能出错";"不因信任而放弃防范、为'万一出错'守住关键防线",是构建一个不会被'某一方的失误'轻易击垮的健壮系统的核心原则——对 LLM 尤其如此。认清信任不该代替防范、对关键环节保留不信任的防护——是这个参数校验坑带给我的工程态度。
第六件事:给 Agent 写工具时,我现在的自检习惯
现在每当我给 AI Agent 写一个工具,我都会先按这张图问自己:
这张图的精髓,是"把模型参数当不可信输入校验,危险操作加确认,校验不过回传错误"。模型参数当不可信输入校验、有副作用算影响+人工确认、校验不过返回错误给模型、再加最小权限+审计。这套习惯,让我从"信任模型填的参数照单执行"变成了"把工具当面向不可信调用者的 API 加固"——核心始终是:把模型填的工具参数当不可信输入严格校验(范围/合法性/白名单),危险操作加人工确认,校验不过返回错误给模型,别照单执行。
我立下的几条规矩
这场"模型把 days 填成 0、工具直接删库"的事故,换来了我给 Agent 写工具时,刻进骨子里的几条铁律:
- LLM 是概率性的,会填出非法/边界/危险的参数。别假设它永远填对。
- 模型填给工具的参数 = 不可信输入,必须像校验用户输入一样校验。
- 校验范围/类型/合法性:数字有上下界、ID 存在且属当前用户、枚举在白名单。
- 有副作用/不可逆的操作额外防护:算影响范围、过大要人工确认、软删除可回滚。
- 校验不过返回清晰错误给模型,让它知道错在哪、能改,而非默默执行。
- 把 Agent 工具当成"面向不可信调用者的 API"来加固:校验、限权、确认、审计、沙箱。
- Agent 的安全防线在工具侧,别因为模型"聪明"就用信任代替防范。
写在最后
回头看,这场由"没校验模型填的参数"引发的、一个 days=0 删库的事故,真正教给我的,远不止"工具要校验参数"这一个技巧。它让我对"我们越是觉得某个'执行者'聪明、可靠, 就越容易放松对它的防范; 而恰恰是这份'因信任而放下的防备', 让它偶尔的一次失误, 变成了无人能挡的灾难",有了一次刻骨的体会。我栽跟头,最深的根源,是我被 LLM 的"聪明"麻痹了——它平时表现得那么智能、那么"善解人意",以至于我下意识地把它当成了一个'比普通程序/用户更可靠'的存在,对它格外地放心、格外地不设防;我给普通用户的输入都会老老实实校验,却偏偏对这个"聪明的 LLM"填的参数, 免去了校验——仿佛它的"聪明"给了它一张"免检通行证";可这张通行证是我臆想出来的, LLM 从未保证过它"不会填错", 它本质上比普通用户更不可预测; 我对它越信任、防备越松, 它那偶发的 days=0 也就越畅通无阻地酿成了删库。这让我领悟到一个关于"能力、信任与防范"的深刻认知:一个执行者"看起来越能干、越聪明",我们就越倾向于信任它、给它更大的权限、放松对它的防范——而这恰恰是危险的:能力强 ≠ 不会犯错;给一个"能力强但仍会犯错"的执行者过度的信任和不设防的权限, 意味着它一旦犯错, 破坏力也更大;"越是强大的执行者, 越需要配套强大的约束和防范", 而非相反。这给了我一种对待"强大执行者(尤其 AI)"的根本清醒:面对一个强大、智能的执行者(LLM、自动化系统、乃至能干的人),不要让"它很能干"成为"放松防范、给予无约束权限"的理由——反而要意识到"它越能干, 它的失误/失控后果越严重, 越需要校验、限权、确认、审计这些约束来兜住它";"不被能力和聪明麻痹、对强大的执行者配套以相称的约束与防范",是安全地驾驭强大能力(尤其是 AI)的根本原则。认清能力强不等于不会错、越强大的执行者越需要相称的约束防范、别被聪明麻痹而放松防备——这,是我用一次 days=0 删库的事故,换来的、关于 AI Agent、也关于如何安全驾驭强大执行者的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次给 Agent 写工具时,把模型填的每个参数都老老实实校验一遍、给删除这类操作加道确认,那我对着那被 days=0 删空的数据复盘的这段时间,就值了。
—— 别看了 · 2026