我的 AI Agent 直接拿大模型生成的参数去调用工具执行,结果模型一"幻觉"出个不存在的参数,工具就报错把整个任务带崩了,我对着把模型输出当可信数据直接执行这个坑排查了大半天的复盘
这是我做"能调用工具的 AI Agent"时,栽的一个关于"信任边界"的大跟头。它让我明白:大模型生成的东西,无论看起来多么言之凿凿,都只是一个"建议/猜测",绝不能当成"可信的事实"直接拿去执行。
需求是做一个能帮用户操作系统的 Agent:用户用自然语言说需求,Agent 决定调用哪个工具、生成调用参数,然后执行。我的实现很"直接"——把大模型吐出的工具调用(函数名 + 参数),解析出来就原样拿去执行:
# Agent 执行工具调用(有问题的版本)
def run_agent(user_input):
# 让大模型决定调用什么工具、生成什么参数
response = llm.chat(user_input, tools=TOOL_DEFINITIONS)
if response.tool_call:
tool_name = response.tool_call.name # 模型说要调的工具名
args = response.tool_call.arguments # ★ 模型生成的参数(JSON)
# ★★★ 致命: 直接信任并执行模型生成的工具名和参数 ★★★
tool_func = TOOLS[tool_name] # 万一 tool_name 是模型瞎编的呢?
result = tool_func(**args) # 万一 args 是幻觉/格式错/越界呢?
return result
这段代码,在模型"表现正常"时跑得好好的。可大模型不是永远正常的——某次,用户的需求有点模糊,模型在生成工具参数时"幻觉"了:它给一个"查询用户"的工具,编造了一个数据库里根本不存在的字段名当参数;还有一次,它给一个工具传了个类型完全不对的参数(该传数字传了句描述文字);更吓人的一次,它生成的参数,差点触发了一个删除范围过大的危险操作。轻则工具直接抛异常、把整个 Agent 任务带崩(且没有任何容错),重则可能执行了不该执行的操作。我盯着代码里那行 tool_func(**args),意识到问题的本质:我把大模型的输出,当成了"绝对正确、可以直接执行"的指令,而它其实只是一个"可能对、也可能错得离谱"的概率性建议。
第一件事:看清真相——LLM 的输出是"概率性建议",必须当不可信输入对待
我重新理解了大模型的本质,以及 Agent 架构里的"信任边界",才彻底想明白这个坑——大模型本质是一个"根据概率生成下一个 token"的系统,它会"一本正经地胡说八道(幻觉)";它生成的工具调用参数,是不可信的、可能出错的输入,必须像对待"用户输入"一样去校验,而不能直接信任执行。
LLM 输出与 Agent 信任边界的真相
# 1. 大模型的本质: 它是一个"概率性的文本生成器"——
# 根据训练和上下文, 生成"看起来最合理"的下一段文本。
# - 它【不保证正确】, 它生成的是"统计上最可能", 不是"事实上正确"。
# - 它会【幻觉(hallucination)】: 一本正经地编造不存在的东西
# (不存在的字段/API/ID/文件路径), 且语气和真的一模一样。
# 2. 所以"模型生成的工具调用参数", 本质是:
# 一个【不可信的、可能包含错误/幻觉/恶意(若被注入)的】输入!
# - 参数值可能是幻觉的(不存在的ID/字段)
# - 参数类型/格式可能不对(该数字给了文字, JSON结构错)
# - 参数可能越界/越权(删除范围过大, 访问不该访问的资源)
# - 工具名可能是模型瞎编的(调一个不存在的工具)
# 3. Agent 架构里的【信任边界】:
# - 大模型 = 一个"聪明但不可靠的助手", 它给出"建议"(调什么工具/什么参数)
# - 你的代码 = 真正执行操作的人, 你必须【对每一个要执行的操作负责】
# - ★ 大模型的输出, 到你的执行代码之间, 是一道【信任边界】:
# 跨过这道边界去【真实执行】之前, 必须【校验、约束、把关】!
# - 把"模型说要这么做"直接等同于"那就这么做", 是把"建议"当成了"命令",
# 把"不可信来源"当成了"可信指令"——这是Agent最危险的设计错误之一。
# 4. 类比: 这就像一个新来的、很热心但常出错的实习生(LLM)给你递操作建议;
# 你(代码)绝不能闭着眼睛照他说的就去删库、转账;
# 你必须 review 他的建议(校验), 在你的权限和规则内执行。
# 核心: LLM输出是概率性的、会幻觉、不可信; Agent中"模型生成的工具调用/参数"必须当作
# 不可信输入来校验(类型/范围/权限/白名单), 在执行的信任边界处严格把关, 绝不直接信任执行。
真相大白,我幡然醒悟。原来大模型的本质,是一个"概率性的文本生成器"——它生成的是"统计上最可能"的文本,而不保证事实上正确;它会"幻觉",一本正经地编造不存在的字段、API、ID,语气还和真的一模一样。所以"模型生成的工具调用参数",本质是一个不可信的、可能包含错误/幻觉的输入:参数值可能是幻觉的、类型格式可能不对、可能越界越权、工具名可能是瞎编的。而 Agent 架构里有一道关键的信任边界:大模型是"聪明但不可靠的助手",它给出的是"建议"(调什么工具、什么参数);而我的代码才是"真正执行操作的人",必须对每一个要执行的操作负责——从模型输出到执行代码之间,是一道信任边界,跨过它去真实执行之前,必须校验、约束、把关。我把"模型说要这么做"直接等同于"那就这么做",是把"建议"当成了"命令"、把"不可信来源"当成了"可信指令"。这就像一个热心但常出错的实习生给你递操作建议,你绝不能闭着眼照他说的去删库、转账——你必须 review 他的建议,在你的权限和规则内执行。
第二件事:正解——在执行前校验工具名与参数,最小权限 + 危险操作把关
搞懂了原理,正解就清晰了:在"模型输出"到"真实执行"的信任边界处,严格校验工具名(白名单)、校验参数(schema/类型/范围)、限制权限(最小权限)、对危险操作加确认/兜底。
from pydantic import BaseModel, ValidationError
# ====== 为每个工具定义参数的 schema(用于校验) ======
class QueryUserArgs(BaseModel):
user_id: int # 必须是 int
fields: list[str] = [] # 字段名列表
ALLOWED_FIELDS = {"name", "email", "created_at"} # 字段白名单
def run_agent(user_input):
response = llm.chat(user_input, tools=TOOL_DEFINITIONS)
if not response.tool_call:
return response.text
tool_name = response.tool_call.name
raw_args = response.tool_call.arguments
# ★ 1. 工具名白名单校验: 模型可能瞎编一个不存在的工具
if tool_name not in TOOLS:
return f"模型请求了未知工具 {tool_name}, 已拒绝" # 优雅处理, 不崩
# ★ 2. 参数校验: 用 schema 校验类型/必填/结构(模型参数可能幻觉/格式错)
try:
args = ARG_SCHEMAS[tool_name](**raw_args) # pydantic 校验, 不符就抛错
except ValidationError as e:
# 把校验错误【反馈给模型】, 让它修正后重试(而不是直接崩!)
return ask_llm_to_retry(user_input, error=str(e))
# ★ 3. 业务规则/越界校验: 字段白名单、范围、数量上限等
if tool_name == "query_user":
bad = set(args.fields) - ALLOWED_FIELDS
if bad:
return ask_llm_to_retry(user_input, error=f"不允许的字段: {bad}")
# ★ 4. 最小权限: Agent/工具只能访问它该访问的资源(按用户身份限权)
# 工具内部再做一层权限校验, 别让Agent能越权操作
# ★ 5. 危险操作把关: 删除/转账/批量修改等, 加二次确认或人工审批
if is_dangerous(tool_name, args):
return request_human_confirmation(tool_name, args) # 不直接执行!
# 通过了所有把关, 才真正执行
result = TOOLS[tool_name](**args.dict())
return result
# ====== 还有一层: 工具结果也不可全信 ======
# 工具执行的结果(尤其外部API/网页)再喂回给模型时, 也要意识到:
# - 结果可能含恶意内容(prompt injection), 别让它劫持Agent
# - 对结果做必要的清洗/隔离
# 核心: 在"模型输出→真实执行"的信任边界处层层把关——工具名白名单、参数schema校验、
# 业务越界校验、最小权限、危险操作人工确认; 校验失败反馈给模型重试而非崩溃。
修复的核心,是"在信任边界处对模型生成的工具调用层层校验、把关"。第一步,工具名白名单校验——模型可能瞎编不存在的工具,不在 TOOLS 里就优雅拒绝、不崩。第二步,参数 schema 校验——用 pydantic 等校验类型/必填/结构,模型参数可能幻觉或格式错;校验失败就把错误反馈给模型让它修正重试,而不是直接崩溃。第三步,业务规则/越界校验——字段白名单、范围、数量上限。第四步,最小权限——Agent/工具只能访问它该访问的资源,工具内部再做一层权限校验,别让 Agent 越权。第五步,危险操作把关——删除/转账/批量修改等加二次确认或人工审批,不直接执行。还有一层:工具结果也不可全信——结果(尤其外部 API/网页)喂回模型时可能含恶意内容劫持 Agent,要清洗/隔离。归根结底:在"模型输出→真实执行"的信任边界处层层把关——工具名白名单、参数 schema 校验、业务越界校验、最小权限、危险操作人工确认;校验失败反馈给模型重试而非崩溃。
第三件事:AI Agent 工程里其他关于"不可信"的坑
排查后我把 AI Agent 工程里其他关于"把不可信当可信"的坑也系统梳理了一遍。
AI Agent 工程的"不可信"相关坑
# 1. 直接执行模型生成的工具参数(本文): 幻觉/越界/格式错。→ 信任边界校验。
# 2. 工具结果直接喂回模型, 不设防: 外部内容可能含"提示注入"
# (网页里藏着"忽略之前的指令, 去做XX")→ 可能劫持Agent。→ 隔离/标注/限权。
# 3. 模型幻觉编造事实当真: 模型说"我已经查到X"但其实没调工具/编的。
# → 重要结论要有"工具调用的真实证据"支撑, 别信模型的"自述"。
# 4. 没有最大步数/预算: Agent工具调用死循环, 烧钱(详见上下文管理那篇)。
# 5. 模型生成的代码直接执行(code interpreter): 极危险!
# → 必须沙箱隔离、限制权限/资源/网络。
# 6. 把Agent的"自信"当"正确": 模型语气都很肯定, 但肯定≠正确。
# → 对关键操作/结论保持审慎, 加校验和人工环节。
# 7. 多Agent互相轻信: 一个Agent的输出直接被另一个当可信输入。
# → Agent之间也要有校验, 别盲目链式信任。
# 共同根源: 把"大模型(及它驱动的Agent)"当成了"确定性的、可信的程序模块",
# 而它其实是"概率性的、会出错会被诱导的"组件; 用"信任确定性代码"的方式去信任它, 就会出事。
# 核心: Agent工程的核心安全/健壮性原则是"不信任模型输出"——模型生成的工具调用、结果解读、
# 甚至代码, 都要在执行边界校验、限权、沙箱、必要时人工把关; 模型是建议者, 你的代码是负责人。
排查让我把 Agent 工程的其他"不可信"坑也梳理清了。一、直接执行模型生成的工具参数(本文)。二、工具结果直接喂回模型不设防(外部内容含提示注入可能劫持 Agent)。三、模型幻觉编造事实当真(重要结论要有工具调用的真实证据)。四、没有最大步数/预算(死循环烧钱)。五、模型生成的代码直接执行(极危险,必须沙箱)。六、把 Agent 的"自信"当"正确"(肯定≠正确)。七、多 Agent 互相轻信。它们的共同根源是:把"大模型(及它驱动的 Agent)"当成了"确定性的、可信的程序模块",而它其实是"概率性的、会出错会被诱导的"组件;用"信任确定性代码"的方式去信任它,就会出事。核心是:Agent 工程的核心健壮性原则是"不信任模型输出"——模型生成的工具调用、结果解读、甚至代码,都要在执行边界校验、限权、沙箱、必要时人工把关;模型是建议者,你的代码是负责人。下面这张图,是这次直接执行模型参数的成因与解法:
第四件事:模型输出各环节的"可信度"与应对速查表
这次踩坑后,我把 Agent 流程里各个"模型相关环节"的可信度和应对整理成一张表。
| 环节 | 可信度 | 风险 | 应对 |
|---|---|---|---|
| 模型选哪个工具 | 低 | 选错/编造工具 | 白名单 + 工具描述清晰 |
| 模型生成的参数 | 低 | 幻觉/类型错/越界 | schema校验+业务校验 |
| 模型对结果的解读 | 中 | 误读/夸大 | 关键结论要有证据支撑 |
| 模型的"自述已完成" | 低 | 没做却说做了 | 用工具调用记录核实 |
| 模型生成的代码 | 低 | 危险/有bug | 沙箱+限权+审查 |
| 工具真实执行的结果 | 高 | 外部内容可能注入 | 清洗/隔离后再用 |
这张表把"哪些环节可信、哪些不可信"钉死了。核心是:凡是"模型生成/模型判断"的环节(选工具、生成参数、解读结果、自述完成、生成代码),可信度都低,需要校验把关;只有"工具真实执行的结果"可信度高(但外部内容仍要防注入)。它给我的最大启发是:构建一个可靠的 Agent 系统,关键在于清醒地区分系统里的"确定性部分"和"概率性部分"——工具的执行、代码的逻辑是确定性的(可信、可控);而大模型的每一次输出都是概率性的(不可全信);可靠的系统,应该把"关键的、不容出错的逻辑"放在确定性的代码里,而把大模型用在它擅长的"理解意图、规划、生成候选"上,并在二者交界处严格校验。这其实是 LLM 应用架构的一个核心原则:"用确定性的代码,约束和兜底概率性的模型"——别指望模型永远正确,而是用你能控制的、确定的代码,为模型可能犯的错误兜底、设立护栏;模型负责"灵活和智能",代码负责"可靠和安全",两者各司其职。清醒区分确定性与概率性部分、用确定的代码为概率的模型设护栏——是构建可靠 Agent 的架构基石。
第五件事:为什么"信任边界"是工程中的核心概念
这次让我把"信任边界"这个概念,从安全领域延伸到了更广的工程认知里。
| 场景 | 不可信的输入来自 | 边界处该做的把关 |
|---|---|---|
| Web后端 | 用户请求/表单 | 校验、转义、鉴权 |
| API接口 | 调用方传参 | 参数校验、限流、认证 |
| 读外部数据 | 第三方API/文件 | schema校验、容错 |
| AI Agent(本文) | 模型生成的工具调用 | 白名单、校验、限权、确认 |
| 插件/扩展系统 | 第三方插件 | 沙箱、权限隔离 |
| 微服务间调用 | 上游服务 | 别盲信, 做防御性校验 |
这张表把"信任边界"这个概念铺开了。核心是:几乎每个系统都存在"信任边界"——它把"我能控制、可信任的内部"和"我无法控制、不可信的外部"分隔开;而所有的安全和健壮性问题,都集中爆发在"没有在信任边界上做好把关"的地方;AI Agent 的"模型输出→执行",只是这条普遍原则在新场景下的又一次体现。它给我的深刻启发是:"识别系统中的信任边界、并在边界上做好校验和防护",是一项贯穿所有软件工程领域的核心能力;每当数据/指令从一个"信任域"流入另一个时,你都要问:"这个来源可信吗?如果它是错的、恶意的、畸形的,我的系统会怎样?我在这里把关了吗?"。这让我对"不信任原则"有了更体系化的认识:成熟的工程思维,有一种健康的"偏执"——它默认"任何来自我控制范围之外的东西(用户输入、外部数据、第三方、乃至 AI 模型)都可能出错或有害",并主动在每一个信任边界上设立防线;这不是杞人忧天,而是对"真实世界充满不确定与恶意"的清醒认知;而 AI 时代的新课题,正是要把大模型也纳入这套"信任边界"的思维框架——把它当成一个强大但不可全信的外部组件来对待。识别并守护每一处信任边界、对边界外的一切保持健康的不信任——这,是这个 Agent 坑教给我的、贯穿一切工程的核心安全与健壮性思维。
第六件事:设计 Agent 工具调用时,我现在的判断习惯
现在每当我给 Agent 加一个工具、设计它的调用流程,我都会按这张图先想清楚:
这张图的精髓,是"模型的每个工具调用,都要经过白名单→参数校验→业务校验→权限→危险操作把关,才能执行"。工具名先过白名单(未知拒绝),参数过 schema 校验(失败反馈模型重试),再过业务越界校验,在最小权限下执行;危险操作必须二次确认/人工审批,结果清洗隔离后再用。这套习惯,让我设计 Agent 时,从"模型说啥就执行啥"变成了"模型是建议者、我的代码层层把关"——核心始终是:模型输出不可信,在执行的信任边界处校验、限权、把关;模型负责智能,代码负责可靠安全。
我立下的几条规矩
这场"直接执行模型参数把任务带崩"的事故,换来了我做 AI Agent 时,刻进骨子里的几条铁律:
- LLM 输出是概率性建议,不是可信指令。它会幻觉,绝不直接执行。
- 模型输出到执行之间是信任边界。跨过去前必须校验把关。
- 工具名走白名单,参数走 schema 校验。未知工具拒绝,畸形参数拦截。
- 校验失败反馈给模型重试,别崩。把错误信息喂回去让它修正。
- 最小权限 + 危险操作人工确认。删除/转账绝不让模型说了算。
- 工具结果也防注入。外部内容可能劫持 Agent,清洗隔离。
- 用确定性代码为概率性模型兜底。模型管智能,代码管护栏。
附:一个可复用的"安全工具执行器"封装
这次踩坑后,我把"校验 + 限权 + 把关 + 容错"的逻辑,封装成了一个可复用的"安全工具执行器",所有 Agent 的工具调用都走它,从根上堵住"无脑执行模型输出"的口子:
from pydantic import BaseModel, ValidationError
from typing import Callable
class Tool:
"""一个工具的完整定义: 含参数schema、是否危险、权限要求、执行函数"""
def __init__(self, name, arg_schema: type[BaseModel],
func: Callable, dangerous=False, required_perm=None):
self.name = name
self.arg_schema = arg_schema # 参数校验用的 pydantic 模型
self.func = func
self.dangerous = dangerous # 是否危险操作(需确认)
self.required_perm = required_perm
class SafeToolExecutor:
"""安全工具执行器: 统一对模型生成的工具调用做校验、限权、把关、容错"""
def __init__(self, tools: dict[str, Tool], user_perms: set):
self.tools = tools
self.user_perms = user_perms # 当前用户/会话的权限集
def execute(self, tool_name: str, raw_args: dict):
# 1. 工具名白名单
if tool_name not in self.tools:
return {"ok": False, "error": f"未知工具: {tool_name}", "retry": True}
tool = self.tools[tool_name]
# 2. 权限检查(最小权限)
if tool.required_perm and tool.required_perm not in self.user_perms:
return {"ok": False, "error": f"无权限执行 {tool_name}", "retry": False}
# 3. 参数 schema 校验
try:
args = tool.arg_schema(**raw_args)
except ValidationError as e:
# retry=True: 让上层把错误反馈给模型, 重新生成参数
return {"ok": False, "error": f"参数校验失败: {e}", "retry": True}
# 4. 危险操作: 不直接执行, 返回"待确认"
if tool.dangerous:
return {"ok": False, "need_confirm": True,
"preview": {"tool": tool_name, "args": args.dict()}}
# 5. 通过所有把关, 在 try 中执行(执行失败也优雅返回, 不让Agent崩)
try:
result = tool.func(**args.dict())
return {"ok": True, "result": result}
except Exception as e:
return {"ok": False, "error": f"工具执行出错: {e}", "retry": False}
# 用法: Agent 拿到模型的工具调用后, 一律交给 executor
# outcome = executor.execute(model_tool_name, model_raw_args)
# if outcome.get("retry"): ask_llm_to_fix(outcome["error"]) # 反馈模型重试
# elif outcome.get("need_confirm"): ask_human(outcome["preview"]) # 人工确认
# elif outcome["ok"]: use(outcome["result"])
# 核心: 把"工具调用的所有把关(白名单/权限/参数校验/危险确认/执行容错)"收敛进一个执行器;
# 所有Agent工具调用都走它, 让"安全执行"成为唯一的、不可绕过的通道。
这个安全工具执行器,是我这次踩坑后最有价值的工程沉淀。它把我用一次事故换来的所有教训——"工具名白名单、权限检查、参数校验、危险操作确认、执行容错"——全部收敛进了一个统一的、不可绕过的执行通道;此后 Agent 拿到模型的任何工具调用,都必须经过它,再也不会有"某个地方图省事直接 func(**args) 执行了模型输出"的漏洞。它还把每种结果都设计成了结构化的、可处理的返回(而非直接抛异常崩溃):retry=True 让上层把错误反馈给模型重新生成、need_confirm 触发人工确认、ok 才是真正成功——让 Agent 在面对模型的各种错误时,能优雅地应对、修正、或求助,而不是崩溃。这正是我想分享的核心思想:对于"安全和健壮性"这种至关重要、又容易在某个角落被遗漏的关注点,最可靠的做法,是把它收敛进一个"唯一正确的通道",并让这个通道成为不可绕过的——而不是依赖"每个调用工具的地方都记得自己去做校验"(总会有人忘、有地方漏)。因为"安全"和"健壮"如果是"可选的、需要自觉的",那它迟早会在某处被省略;只有把它做成"默认的、强制的、走这条路就自动拥有的",它才真正可靠;把关键的横切防护收敛成"必经之路",是用架构而非纪律来保证安全的根本之道。把工具执行的所有防护收敛进一个不可绕过的安全执行器、用架构保证"安全是默认的"——这,是我用一次 Agent 事故,换来的、关于"如何为 AI Agent 系统建立可靠安全底座"的实用架构智慧。
写在最后
回头看,这场由"直接执行模型生成的参数"引发的、任务被幻觉带崩的事故,真正教给我的,远不止"校验工具参数"这一个技巧。它让我对"如何与大模型这种全新的、概率性的组件协作"这件事,有了一次根本性的认知升级。我栽跟头,根源是我下意识地用"对待传统程序模块"的方式,去对待大模型。在传统编程里,我调用一个函数、一个模块,它的行为是确定的、可预期的、可信的——我不需要怀疑"这个排序函数会不会突然给我返回一段乱码"。可大模型是一种全新性质的组件:它极其强大、极其灵活,但同时概率性、不确定、会犯错、会幻觉、还可能被诱导;我却把它当成了一个"确定性的、永远会给出正确答案的"模块来无脑信任。用旧的信任模型,去信任一个性质完全不同的新组件——这是我犯错的认知根源。这让我领悟到一个深刻的认知:大模型给软件工程带来的最大挑战之一,是它引入了一种我们过去不熟悉的"概率性的、不可靠的、但又很强大的"组件;与它协作,需要我们建立一套新的心智模型——既要善用它的强大(理解、生成、规划),又要时刻警惕它的不可靠(幻觉、出错、被诱导),并用确定性的工程手段(校验、限权、沙箱、人工兜底)去驾驭和约束它。这其实是 AI 时代工程师的一项核心新素养:学会"在不可靠之上构建可靠"——把大模型当成一个"能力超强但需要被监督的下属/外部服务",既充分发挥它的价值,又用严密的工程把关,确保整个系统在它出错时依然安全、可控、不崩溃;不盲目神化它(以为它无所不能、永远正确),也不因噎废食(因为它会错就不用),而是清醒地、有边界地、有护栏地使用它。用"对待强大但不可靠的概率性组件"的新心智去驾驭大模型、在它的不确定性之上用确定性工程构建可靠——这,是我用一次 Agent 被幻觉带崩的事故,换来的、关于 AI Agent、也关于 AI 时代工程范式的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次写下"拿模型输出去执行"的代码前,先停一下、问自己"它要是瞎编的,我这儿拦得住吗?",那我对着那个被幻觉带崩的 Agent 排查的这大半天,就值了。
—— 别看了 · 2026