LLM 工具失败:仅三个根本原因 – Value、Condition、Intent
摘要
文章介绍了 'execution-state-preflight',这是一个针对 LLM 工具调用的验证层规范,定义了三个失败根本原因,并提出了两个检查点与清单,以确保安全和可审计的执行。
查看缓存全文
缓存时间: 2026/08/24 07:47
Jang-woo-AnnaSoft/执行状态预检
来源:https://github.com/Jang-woo-AnnaSoft/execution-state-preflight
执行状态预检
一个在 MCP 工具调用之前运行的验证层。 三个检查清单规定了调用发出前必须满足的条件。 两个门控机制负责执行这些检查。 这两个门控都不会产生值或理由——它们只负责查询,并且任意一个都可以阻止调用。 这并不是阻挡在您代理程序前的一堵墙。它为猜测所留下的空白填充了明确的来源信息。 执行仍然是目标。
状态: 这是一份规范,而非库。
createPreflight 拒绝在缺少六个注入钩子的情况下构建。
参见 (execution-state-preflight (https://github.com/Jang-woo-AnnaSoft/execution-state-preflight/blob/main/execution-state-preflight.js))。
从这里开始。 谁来填写表单 是论证部分——解释了为什么检查清单必须置于模型之外,以及这样做会带来哪些变化。 design.md 是实现说明:结构、钩子契约,以及此规范涵盖和未涵盖的内容。 本 README 描述的是参考骨架本身。 如果您正在决定是否值得这样做,请按此顺序阅读。 如果您已经决定,请直接从此处开始。
八个问题
达成三个检查清单和两个门控机制,需要解决八个问题。
- 将执行与验证分离——并分离验证方(系统 / 提供者 / 用户)
- 验证条件,而不仅仅是数值
- 由系统而非模型决定什么构成“未知”
- 通过结构而非良好意图来保证人为参与
- 逐字段的来源记录,作为审计和责任划分的原始材料
- 规则变成附着在工具上的数据而非代码,从而无需部署即可更改
- 失败有其名称——指令差距(用户指令不完整)和动作定义差距(模型选择了错误的工具,或选择了尚未存在的工具)
- 当前无法运行的内容被保留而非丢弃
三个检查清单
一个动作所需的规则根据其定义者进行划分。这种划分是整个设计的核心——以下所有内容都是执行此划分的机制。
固定检查清单 —— 我们选择哪个工具,执行条件是否满足(when/case)?
工具无关,且每次执行时都相同。在记录中,它们是 c1_when_case、c2_user_action_name、c3_provider_action_name。
提供者检查清单 —— 必填字段、类型与格式、执行前检查、禁止条件、额外确认条件。每个工具都不同。在可强制性上再次细分:inputSchema.required 可以被门控,而 description 是自然语言无法强制,因此记录为建议并作为上下文传递给模型。
用户检查清单 —— 用户意图、当前上下文、执行限制、执行前检查、偏好。随用户环境而非工具变化。每个条目需要一个稳定的 id;没有 id 就无法将答案重新关联,运行将被挂起。
固定检查清单是门控1询问的内容。 提供者和用户检查清单是门控2询问的内容。
两个门控
指令 (受信任标签段)
│
├─ 门控1:固定检查清单 → tool_undetermined → ask_user
│ 这是正确的工具吗?它何时运行?
│
├─ 门控2:提供者 + 用户检查清单 → unknown_fields → ask_user
│ 每个值来自哪里?unverified_checklist
│ 用户条件已验证吗?
│
└─ 两者都通过 → execute → executed
门控1位于提供者提供的所有内容之上。如果将其下移,一个未确定工具的 required 字段和 description 会随它进入门控——您将为根本不该发生的调用验证参数。
门控1 —— 固定检查清单
工具选择准确率永远无法达到100%。错误选择是不可避免的,因此首要任务是构建一个结构,使得错误选择无法到达执行阶段。
confirmToolNameMatchesIntent 将用户称呼动作的方式 (c2) 与所选工具 (c3) 进行比较。任何不是明确 { approved: true } 的结果都会在此停止——返回 undefined、抛出异常或省略该字段的钩子都表示未批准。沉默不代表批准。
{
"schema_version": "1.4",
"action_key": "u_01:clean-up",
"phase": "at_trigger",
"fixed": {
"c1_when_case": "immediate",
"c2_user_action_name": "clean up the old invoices",
"c3_provider_action_name": "records.delete_all"
},
"fields": null,
"advisory_notes": "",
"unknown_count": null,
"gate": {
"kind": "tool_undetermined",
"user_message": "I could not determine which tool to use. Please restate what you want to do.",
"_diag": {
"candidate_tool": "records.delete_all",
"user_action": "clean up the old invoices",
"reason": "scope mismatch: user action is bounded, tool is unbounded"
}
},
"execution_decision": "ask_user",
"reason": "ask_user: tool undetermined (scope mismatch: user action is bounded, tool is unbounded)"
}
该记录中有四点是刻意设计的:
用户消息不包含候选工具名称。 向某人展示 records.delete_all,问题就不再是“你想要什么”,而是“批准这个吗?”——人们会选择他们看到的东西。候选工具存在于 _diag 中,它只进入日志,绝不传达给用户。
是 ask_user,而非 hold。 未确定的工具不是缺陷。这是一个需要询问的事项。
fields 是 null,而非 []。 null 意味着未做决定;[] 则意味着查询已运行但结果为空。unknown_count 同理。这就是为什么调用方的约定是 if (decision !== "execute") 而永远不是 if (unknown_count > 0)——null > 0 为 false,一个尚未计算的状态会悄然通过。
重新输入是替换工具,而非答案。 对 tool_undetermined 的响应不会进入 userAnswers。您替换 mcpTool 并再次调用。骨架有意不读取“用户已重新选择”标志,因为读取它会将其变成一个旁路开关;只有名称是固定的 (input.tool_reselected_by_user),以便采用的系统可以一致地实现它。
限制重试次数——在同一个 action_key 下两到三次,然后挂起。
如果您有足够少的工具可以提供一个列表,请直接列出。无默认选择,无“推荐”标记。
固定检查清单的另一半是 c1_when_case,它决定阶段:immediate 立即运行后续内容,其他任何值则将其推迟到触发时间。超出枚举范围的值会挂起而非直接执行——参见值立即获取,条件在触发时确定。
门控2 —— 提供者和用户检查清单
以下内容仅在工具确定后才会被触及。
每个值来自何处
验证器无法区分用户输入的账户号和模型编造的账户号。更糟的是,必填字段会给模型带来压力去生成某些内容。因此,这一层不验证参数——它查询每个参数的来源。
| # | 来源 | 含义 |
|---|---|---|
| 0 | user_answer | 用户在 ask_user 后回答 |
| 1 | instruction | 取自受信任的片段,带有 span |
| 2 | pre_set_data | 先前通过决策路径确定 |
| 3 | measured_data | 从环境中观测到 |
| 4 | prior_state | 继承自先前的 executed 记录 |
这是一个查找顺序,而非按可信度排名。如果更高级别已有答案,则该值已确定;如果没有,则下移一级。所有五个都会被检查。全部五个为空则意味着 unknown。对于不完整的指令,unknown 不是错误。它是正确的输出。
用户条件是否成立
用户检查清单在同一次运行中验证,就在执行之前,钩子未主动确认的任何内容都会返回 unverified。默认钩子不确认任何内容——它也不信任传入的状态,因此一个预先标记为 verified 的条目仍然会失败。只有当两个计数都为零时,门控才会通过。
{
"schema_version": "1.4",
"action_key": "u_01:bank.transfer",
"phase": "at_trigger",
"fixed": {
"c1_when_case": "immediate",
"c2_user_action_name": "send money to my landlord",
"c3_provider_action_name": "bank.transfer"
},
"fields": [
{
"name": "from_account",
"value": "1102534471",
"status": "known",
"source": "pre_set_data",
"origin_source": "pre_set_data",
"resolved_at": "2026-08-11T09:12:03.114Z"
},
{
"name": "amount",
"value": 500000,
"status": "known",
"source": "instruction",
"origin_source": "instruction",
"resolved_at": "2026-08-11T09:12:03.118Z"
},
{
"name": "to_account",
"status": "unknown",
"source": null,
"origin_source": null,
"resolved_at": "2026-08-11T09:12:03.121Z"
}
],
"advisory_notes": "Transfers are final. Confirm the recipient before calling.",
"user_checklist": [
{
"id": "chk_limit",
"description": "within daily transfer limit",
"status": "verified",
"source": "measured_data"
}
],
"unknown_count": 1,
"unverified_checklist_count": 0,
"gate": {
"unknown_fields": [{ "name": "to_account", "note": null }],
"unverified_checklist": []
},
"execution_decision": "ask_user",
"reason": "ask_user: unknown_fields=1, unverified_checklist=0"
}
advisory_notes 承载提供者的 description。它被记录并传递给模型,并且不是门控的一部分——自然语言无法强制执行,假装可以会将不可验证的条件置于验证位置。
整个链中有三个原则成立:
值是被读取的,而非产生的。 条件是否成立由观察而非模型推理来回答。
来源不是自报的。 一个预执行步骤会查询定义的来源并填充值。如果依赖自报,编造的值也会被附上来源。模型绝不能制造其自身执行的依据。
第一个来源永不擦除。 第4级用 prior_state 覆盖 source,但继承 origin_source。同时覆盖两者会打开一个洗白路径:询问一次,执行一次,此后任何值都可以声称拥有清白的血统。
记录在一个 action_key 下累积:ask_user → execute → executed。只有 executed 成为下次运行时第4级的基线。
快速开始
const { createPreflight } = require("./execution-state-preflight");
const preflight = createPreflight({
hooks: {
classifyWhenCase, // → "immediate" | "scheduled" | "conditional" | "recurring"
extractUserActionName, // → 用户对此动作的称呼
confirmToolNameMatchesIntent, // → { approved, reason }
extractFromInstruction, // → { value, segment_index, span } | undefined
measureFromEnvironment, // → { valid, value } | undefined
buildActionKey, // → 不透明字符串;设置 prior_state 的影响范围
},
storage, // { persist, load } —— 仅追加,或单独保存 `executed`
strict: true, // 同时拒绝三个未验证的默认值
});
const state = await preflight.runPreflightAndRecord({
userId: "u_01",
instruction: [
{ text: "send 500,000 won to my landlord", trust: "user" },
{ text: "", trust: "untrusted", origin: "gmail:msg_881" },
],
mcpTool,
preSetData,
measured_data,
priorExecutionState,
userChecklist,
});
// 根据允许条件做决定,而非根据计数。
if (state.execution_decision === "execute") {
await preflight.executeIfReady(state, mcpTool, callMcpTool);
} else {
// state.gate 精确说明了缺少什么,以及是什么形式。
}
instruction 是一个带信任标签的段数组,而非字符串。传递字符串会导致第1级关闭——所有字段都保持 unknown。这是刻意的:到达上下文(转发的电子邮件、工具结果、抓取的页面)的不受信任文本,无法自行产生 known 值。要使用其中的某些内容,请提问,并将答案作为 user_answer 取回。
一个声称来自指令的值必须指明其坐标——哪个片段,哪个字符范围。没有 span,或 span 超出范围,将被拒绝。
构建后检查 preflight.unsafeDefaults。如果它非空,则门控正在弱模式下运行。
您必须实现什么
构建时需要六个钩子。
四个会抛出 not implemented。
另外两个有函数体,但 extractFromInstruction 总是返回 undefined(第1级关闭),measureFromEnvironment 是对 ctx.measured_data 的一个简单透传——如果您不提供自己的实现,两者仍然会使构建失败。
| 钩子 | 返回值 | 如果实现错误 |
|---|---|---|
classifyWhenCase | 四种情况之一 | 在无法决定时回退到 immediate 会导致不可逆执行 |
extractUserActionName | 用户对此动作的称呼 | 填充 action_key,这是 prior_state 的查找键 |
confirmToolNameMatchesIntent | { approved, reason } | 这是门控1;不符合的返回不是批准 |
extractFromInstruction | { value, segment_index, span } | 错误报告受信任片段将破坏信任检查 |
measureFromEnvironment | { valid, value } | valid: false 绝不能变成 known;此处基于 LLM 的钩子不是测量 |
buildActionKey | 不透明字符串 | 键的设计就是第4级的影响范围 |
另有三个带有默认值且不验证任何内容的钩子:applyFieldPolicy、validateFieldSchema(完全没有格式或类型检查)和 verifyUserChecklistItem(所有内容都返回未验证)。strict: true 会拒绝它们。
层次边界是刻意不规定的。它相对于您代理循环的位置由您决定。
值立即获取,条件在触发时确定
并非所有操作都立即运行。当 c1_when_case 不是 immediate 时,运行会在同一 action_key 下分为两个阶段。
在指令阶段,值在用户仍在场时被解析——这是您能提问的最后时刻。一个合法地尚不存在的值(余额、当前时间)会由您的字段策略标记为 pending_at_trigger,只有此标记才能为未知开脱。其他所有内容都会阻止调度本身。
在触发阶段,整个预检在延迟的输入上再次运行,pending_at_trigger 不能为任何内容开脱。用户检查清单仅在此处验证——在指令阶段检查的条件到调用触发时可能已过时。
您向前传递的内容是一种选择。意图应被保留(指令、检查清单、答案)。现实应被重新获取(schema、预设数据、策略)。测量值不得被传递——被保留的测量值就是过时的测量值。并且保留意味着冻结:您在指令阶段过度询问得到的答案会落入第0级,并将在触发时击败新的测量值。
此规范不做什么
- 不进行脱敏。
fields[].value按原样持久化——账户号、金额、收件人、令牌。延迟的记录从指令时间到触发时间都以明文形式存在。脱敏、访问控制和仅追加强制执行属于您的存储适配器。 - 不对其接收的状态进行完整性检查。
executeIfReady读取您给它的对象。如果您传递一个手工构建的对象,门控将被绕过。如果决策和执行跨越信任边界,请签名。 - 不进行重试。 工具调用抛出异常并不意味着提供者端没有发生任何事情。对于支付,请使用幂等键并通过测量来确认。
- 不进行每工具风险加权。
delete_all_records和list_records通过相同的门控。工具选择本身位于此结构之外:无法选择一个虚构的工具,但在多个可能都能完成任务的工具中选择错误的仍然会通过。 - 不进行锁定。 在同一
action_key上并发预检和执行是调用方的问题。 - 假定参数是扁平的。 字段名等于参数键。嵌套 schema 和键映射工具需要适配器。
组合
仅将此应用于不可逆的动作。 并非所有内容都必须到位。 并非所有部署都需要所有三个检查清单。 仅立即执行,单个工具,无需检查用户条件——取用匹配的部分。 如果代理和工
相似文章
通过溯源分析防范LLM代理失对齐
本文提出了一种基于溯源的框架和多阶段流水线\tool,用于在LLM代理执行工具调用前检测失对齐,与基于LLM作为裁判的基线相比,显著降低了错误率。
少推理,多验证:确定性门控机制修复工具使用型LLM智能体中的静默策略违规故障模式
本文识别了工具使用型LLM智能体中的一种静默故障模式,其中策略违规发生时既无工具错误,也无智能体自我报告。作者提出并评估了轻量级确定性预执行门控机制,该机制在τ²-bench航空领域显著减少了此类故障。
LLMTest
LLMTest 是一个帮助开发者在应用中使用合适的 LLM 并设置回退方案的工具。
LLM生成代码中的拼凑问题
本文形式化描述了'拼凑问题'——即LLM生成的代码在局部正确但在整个代码库中结构上不连贯的现象,提出了一个八类故障分类法和一个混合验证框架,并证明许多故障能够避开现有工具。
走向安全的LLM代理:关于规范、验证与执行的综述
这篇综述论文回顾了38项关于安全LLM代理的研究,强调了关键挑战,如规范翻译瓶颈、运行时监控等执行方法的不完全安全保障,以及阻碍安全任务完成的验证者税。