LLM 工具失败:仅三个根本原因 – Value、Condition、Intent

Hacker News Top 工具

摘要

文章介绍了 '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 描述的是参考骨架本身。 如果您正在决定是否值得这样做,请按此顺序阅读。 如果您已经决定,请直接从此处开始。


八个问题

达成三个检查清单和两个门控机制,需要解决八个问题。

  1. 将执行与验证分离——并分离验证方(系统 / 提供者 / 用户)
  2. 验证条件,而不仅仅是数值
  3. 由系统而非模型决定什么构成“未知”
  4. 通过结构而非良好意图来保证人为参与
  5. 逐字段的来源记录,作为审计和责任划分的原始材料
  6. 规则变成附着在工具上的数据而非代码,从而无需部署即可更改
  7. 失败有其名称——指令差距(用户指令不完整)和动作定义差距(模型选择了错误的工具,或选择了尚未存在的工具)
  8. 当前无法运行的内容被保留而非丢弃

三个检查清单

一个动作所需的规则根据其定义者进行划分。这种划分是整个设计的核心——以下所有内容都是执行此划分的机制。

固定检查清单 —— 我们选择哪个工具,执行条件是否满足(when/case)? 工具无关,且每次执行时都相同。在记录中,它们是 c1_when_casec2_user_action_namec3_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 未确定的工具不是缺陷。这是一个需要询问的事项。 fieldsnull,而非 [] null 意味着未做决定;[] 则意味着查询已运行但结果为空。unknown_count 同理。这就是为什么调用方的约定是 if (decision !== "execute") 而永远不是 if (unknown_count > 0)——null > 0false,一个尚未计算的状态会悄然通过。 重新输入是替换工具,而非答案。tool_undetermined 的响应不会进入 userAnswers。您替换 mcpTool 并再次调用。骨架有意不读取“用户已重新选择”标志,因为读取它会将其变成一个旁路开关;只有名称是固定的 (input.tool_reselected_by_user),以便采用的系统可以一致地实现它。

限制重试次数——在同一个 action_key 下两到三次,然后挂起。 如果您有足够少的工具可以提供一个列表,请直接列出。无默认选择,无“推荐”标记。

固定检查清单的另一半是 c1_when_case,它决定阶段:immediate 立即运行后续内容,其他任何值则将其推迟到触发时间。超出枚举范围的值会挂起而非直接执行——参见值立即获取,条件在触发时确定


门控2 —— 提供者和用户检查清单

以下内容仅在工具确定后才会被触及。

每个值来自何处

验证器无法区分用户输入的账户号和模型编造的账户号。更糟的是,必填字段会给模型带来压力去生成某些内容。因此,这一层不验证参数——它查询每个参数的来源。

#来源含义
0user_answer用户在 ask_user 后回答
1instruction取自受信任的片段,带有 span
2pre_set_data先前通过决策路径确定
3measured_data从环境中观测到
4prior_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_userexecuteexecuted。只有 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级的影响范围

另有三个带有默认值且不验证任何内容的钩子:applyFieldPolicyvalidateFieldSchema(完全没有格式或类型检查)和 verifyUserChecklistItem(所有内容都返回未验证)。strict: true 会拒绝它们。

层次边界是刻意不规定的。它相对于您代理循环的位置由您决定。


值立即获取,条件在触发时确定

并非所有操作都立即运行。当 c1_when_case 不是 immediate 时,运行会在同一 action_key 下分为两个阶段。

在指令阶段,值在用户仍在场时被解析——这是您能提问的最后时刻。一个合法地尚不存在的值(余额、当前时间)会由您的字段策略标记为 pending_at_trigger,只有此标记才能为未知开脱。其他所有内容都会阻止调度本身。

在触发阶段,整个预检在延迟的输入上再次运行,pending_at_trigger 不能为任何内容开脱。用户检查清单仅在此处验证——在指令阶段检查的条件到调用触发时可能已过时。

您向前传递的内容是一种选择。意图应被保留(指令、检查清单、答案)。现实应被重新获取(schema、预设数据、策略)。测量值不得被传递——被保留的测量值就是过时的测量值。并且保留意味着冻结:您在指令阶段过度询问得到的答案会落入第0级,并将在触发时击败新的测量值。


此规范不做什么

  • 不进行脱敏。 fields[].value 按原样持久化——账户号、金额、收件人、令牌。延迟的记录从指令时间到触发时间都以明文形式存在。脱敏、访问控制和仅追加强制执行属于您的存储适配器。
  • 不对其接收的状态进行完整性检查。 executeIfReady 读取您给它的对象。如果您传递一个手工构建的对象,门控将被绕过。如果决策和执行跨越信任边界,请签名。
  • 不进行重试。 工具调用抛出异常并不意味着提供者端没有发生任何事情。对于支付,请使用幂等键并通过测量来确认。
  • 不进行每工具风险加权。 delete_all_recordslist_records 通过相同的门控。工具选择本身位于此结构之外:无法选择一个虚构的工具,但在多个可能都能完成任务的工具中选择错误的仍然会通过。
  • 不进行锁定。 在同一 action_key 上并发预检和执行是调用方的问题。
  • 假定参数是扁平的。 字段名等于参数键。嵌套 schema 和键映射工具需要适配器。

组合

仅将此应用于不可逆的动作。 并非所有内容都必须到位。 并非所有部署都需要所有三个检查清单。 仅立即执行,单个工具,无需检查用户条件——取用匹配的部分。 如果代理和工

相似文章

通过溯源分析防范LLM代理失对齐

arXiv cs.CL

本文提出了一种基于溯源的框架和多阶段流水线\tool,用于在LLM代理执行工具调用前检测失对齐,与基于LLM作为裁判的基线相比,显著降低了错误率。

LLMTest

Product Hunt

LLMTest 是一个帮助开发者在应用中使用合适的 LLM 并设置回退方案的工具。

LLM生成代码中的拼凑问题

arXiv cs.AI

本文形式化描述了'拼凑问题'——即LLM生成的代码在局部正确但在整个代码库中结构上不连贯的现象,提出了一个八类故障分类法和一个混合验证框架,并证明许多故障能够避开现有工具。