Agent开发 5.3 - 工具执行防御与防造假机制:触发门禁、权威溯源与 Fail-Fast 显式抛错

2026-10-29 | 76分钟 | yrobot | Agent开发, 工具工程, Fail-Fast, Gate门禁, Grounding, 事实溯源, 假数据防御, 可靠性设计

在构建严肃的商业级 Agent 应用(如留学申请规划、金融投研、法律合同审查)时,系统不仅要学会调用外部工具获取数据,更要时刻面临真实物理网络环境的不确定性:目标网页可能因网站改版返回 404、可能因防爬拦截返回 403,也可能因为外部接口变动解析不出任何有效字段。

许多习惯传统 Web 接口开发的工程师,往往会下意识地在工具代码里写一个通配 catch 块:为了保证前端页面不白屏、接口不抛错,随手返回一个默认对象,甚至塞入一组静态 Mock 数据作为兜底。

但在 Agent 系统的闭环推演中,这种看似贴心的兜底,实际上是在向系统的状态机注入不可逆的虚假信息。


从单点响应到长程推演:兜底假数据的灾难

在传统的 Web 请求中,用户发起一次查询,后端返回一次响应。如果某个侧边栏数据源出错,返回一个默认空对象,前端少渲染一个卡片,用户感知通常较为平滑。

然而,Agent 的运行本质是长程链式推演(Chained Decision Making)。大模型把每一次工具调用的返回内容,当成客观世界的真实反馈(Observation),并基于这个反馈决定下一步的规划与行动:

flowchart TD
    subgraph 伪降级故障传导
        A1["调用院系导师爬虫"] --> B1["网络 404 或反爬拦截"]
        B1 --> C1["[错误做法] 吞掉异常,返回 Mock 假数据
(如假名字与 @institution.edu 假邮箱)"] C1 --> D1["Agent 误判为已成功获取有效导师"] D1 --> E1["下游环节基于虚构教授生成套磁信与申请方案"] E1 --> F1["向最终用户交付严重错误的决策结果"] end

在上述链路中,一旦工具为了避免报错而返回了虚假数据,外层的调度器与大模型就会误以为该步骤已经圆满达成,进而把虚假实体写入共享状态黑板。整条推演链随之全线失真。

在严肃的生产级业务中,系统的设计原则必须非常明确:宁可显式中断并明确告知用户当前数据源查无结果、转入人工核验,也绝不能为了流程表面的顺畅而编造任何一条虚假凭据。

开源教学演示(如单脚本玩具项目)通常只展示所有 API 畅通无阻的理想情况;而在真实的工程落地上,业界主流方案(如 Google Vertex AI 的事实溯源机制与标准微内核运行时的断点协同)普遍依赖三项核心机制:

  1. 执行前置门禁(Precondition Gate):在重型工具运行前,先核查上下文依赖是否齐备;
  2. 异常快速失败(Fail-Fast):拒绝静默吞错,将客观异常转化为语义明确的错误观察,赋予系统策略转移的依据;
  3. 权威事实溯源(Grounding Attribution):所有输出的事实字段必须附带自证清白的证据链,并通过切面拦截假数据。
flowchart LR
    A["Agent 执行上下文"] --> B{"1. 前置门禁
(Gate Evaluator)"} B -- "前置依赖缺失" --> C["阻断执行 Block / 重定向 Redirect"] B -- "校验通过 Proceed" --> D["执行外部工具 (抓取 / 检索 / 计算)"] D --> E{"数据源响应结果"} E -- "网络中断 / 404 / 403" --> F["2. 显式快速失败 (Fail-Fast)
抛出 ToolExecutionError 触发策略切换"] E -- "成功获取原始数据" --> G["3. 事实溯源与审计 (Grounding)
注入证据链,拦截无凭据与假数据"] G --> H["交付真实有效实体至下游与用户界面"]

一、 执行前置门禁:避免无效空转与越级调用

调用工具是有成本的——不仅消耗网络带宽与调用配额,更会占用模型的上下文窗口。在许多复合业务流中,后置工具通常对前置状态有严格的依赖。

例如,在留学申请推演中,如果用户尚未筛选出任何意向候选院校(院校池为空),调度器若盲目听从大模型的自由发挥,直接调用“导师学术背调”或“院系招生资助透视”,工具必然因缺少基本参数而报错或抓取空页面。

前置门禁(Precondition Gate) 的职责,就是在工具被实际派发之前,以纯函数或规则的形式对当前上下文进行严格核验:

// 门禁判定决策类型
export type GateDecision =
  | { action: "proceed" } // 前置条件就绪,放行执行
  | { action: "block"; reason: string } // 前置依赖严重缺失,坚决阻断
  | { action: "redirect"; targetStage: string; reason: string }; // 条件不足,重定向到前置准备节点

// 通用轻量级门禁评估器
export class GateEvaluator<TContext> {
  private checks: Array<(ctx: TContext) => GateDecision | Promise<GateDecision>> = [];

  addCheck(name: string, check: (ctx: TContext) => GateDecision | Promise<GateDecision>): this {
    this.checks.push(check);
    return this;
  }

  async evaluate(ctx: TContext): Promise<GateDecision> {
    for (const check of this.checks) {
      const decision = await check(ctx);
      if (decision.action !== "proceed") {
        return decision; // 一旦未通过即刻阻断或重定向
      }
    }
    return { action: "proceed" };
  }
}

在调度切面中使用门禁,可以把这类不合理的调用拦截在最前端:

const advisorDiscoveryGate = new GateEvaluator<{ candidateSchoolIds: string[] }>()
  .addCheck("候选院校非空检查", (ctx) => {
    if (!ctx.candidateSchoolIds || ctx.candidateSchoolIds.length === 0) {
      return {
        action: "block",
        reason: "候选院校列表为空,无法执行导师挖掘。请先引导用户选定目标院校。",
      };
    }
    return { action: "proceed" };
  });

// 派发前校验
const gateResult = await advisorDiscoveryGate.evaluate(currentContext);
if (gateResult.action === "block") {
  // 直接向模型回灌系统提示,修正其行动意图
  return `[Gate Blocked] ${gateResult.reason}`;
}

通过门禁的前置拦截,系统无需等到外部网络超时或数据库抛出外键错误,就能直接让模型意识到“依赖未就绪”,从而自主调整执行次序。


二、 异常处理哲学:Fail-Fast 显式抛错与策略转移

在工具的实际执行代码中,必须严格区分两类性质完全不同的“空”:

  1. 合法的空结果:爬虫顺利访问了官方院系主页,页面结构完整,且官方明确写明该专业今年不招收博士生。这是具备真实证据支撑的有效事实,工具应当返回带有权威凭据的空数组;
  2. 执行层的客观失败:目标 URL 返回 404 Not Found、遭遇反爬防火墙 403 阻断、或者页面结构改版导致正则表达式解析落空。这是执行异常,工具绝不能私自伪造一个备选名单,必须显式快速失败(Fail-Fast)。

1. 结构化错误设计

为了让外层的 Event Loop 能够做出针对性的响应,工具抛出的错误不能只是简单的字符串,而应当携带语义化的排错元信息:

// 具备策略指导意义的工具执行错误
export class ToolExecutionError extends Error {
  constructor(
    message: string,
    public readonly code: "NOT_FOUND" | "NETWORK_BLOCKED" | "PARSE_ERROR",
    public readonly retryableWithAltSource: boolean, // 明确告知 Agent 是否可切换到其他渠道重试
  ) {
    super(message);
    this.name = "ToolExecutionError";
  }
}

// 爬取工具实现:坚决拒绝伪降级
export async function scrapeFacultyDirectory(url: string) {
  const response = await fetchWithTimeout(url);

  if (response.status === 404) {
    // 明确提示主页不存在,建议切换渠道
    throw new ToolExecutionError(`院系主页不存在 (404): ${url}`, "NOT_FOUND", true);
  }

  if (response.status === 403) {
    // 明确提示遇到反爬,不可盲目换 URL 重试,需切换无头浏览器或转人工
    throw new ToolExecutionError(`主页受到防爬安全策略拦截 (403)`, "NETWORK_BLOCKED", false);
  }

  return parseFacultyList(response.text);
}

2. 从错误反馈到策略转移(Strategy Pivoting)

在 Agent 的认知闭环中,一个明确且语义清晰的错误,本身就是极高价值的环境反馈。

当外层的状态机接收到 ToolExecutionError,且标识为 retryableWithAltSource = true 时,大模型获得的观察不再是“执行失败了”,而是“该特定主页不存在”。这促使模型做出智能的策略转移:

  • 路径 A(原路死磕,错误方式):如果错误信息模糊,模型可能会不断变换参数、反复尝试访问同一个失效链接;
  • 路径 B(策略转移,正确方式):模型意识到院系官网主页无法访问,主动将检索渠道切换为公认的学术文献库(如 OpenAlex 或 Google Scholar),通过检索该校近两年的论文通讯作者,反向挖掘在任教授名单;
  • 路径 C(转交人工):若全自动渠道均宣告失败,系统优雅地将推演挂起(Suspend),在界面向用户输出一张“待核验卡片”,由人工协助确认主页网址。

三、 事实权威溯源:让输出数据自证清白

解决了门禁与异常处理后,我们还要面对一个更隐蔽的问题:工具返回的数据看起来很完整,但它到底是真的抓到了数据,还是在某个环节被混入了未经验证的猜测?

为了彻底杜绝假数据污染,必须在数据实体契约中强制引入 GroundingSource(事实溯源凭据)。

1. 事实溯源凭据契约设计

Grounding(事实溯源)的核心思想,是要求系统输出的每一个关键事实(如教授职称、截止日期、申请硬性门槛),都必须携带可供人类与审计程序双向核验的物理证据:

// 事实溯源凭证契约
export interface GroundingSource {
  id: string;
  label: string; // 证据标签,如 "MIT EECS 官网手册 p.14"、"OpenAlex 学术数据库"
  sourceType: "official_handbook" | "academic_db" | "web_search" | "community";
  url?: string; // 原始数据来源可点击链接
  snippetText: string; // 抓取到的真实原文快照(用于对照核对)
  confidenceScore: number; // 抓取与解析置信度 (0.0 ~ 1.0)
}

// 业务实体强制绑定溯源凭证
export interface AdvisorEntity {
  name: string;
  title: string;
  email?: string;
  researchFocus: string[];
  groundingSources: GroundingSource[]; // 核心证据链,严禁无凭据空跑
}

前端界面可以根据 GroundingSource 直接为关键属性渲染“来源徽章(Attribution Badge)”。当用户对某个要求产生疑问时,点击即可展开原文快照与官方链接,建立端到端的可信度。

2. 运行时防造假审计切面

定义了数据契约后,需要在工具执行管道的后置处理中加入审计切面,对返回的数据进行自动化体检:

export function auditToolOutput(advisors: AdvisorEntity[]): AdvisorEntity[] {
  return advisors.map((advisor) => {
    // 审计检查 1:拦截缺失证据链的虚构实体
    if (!advisor.groundingSources || advisor.groundingSources.length === 0) {
      throw new Error(`[安全审计拦截] 导师实体 "${advisor.name}" 缺少任何权威溯源凭据`);
    }

    // 审计检查 2:识别并清洗典型合成假数据
    // 许多简易爬虫在未提取到邮箱时,会拼接假域名如 name@institution.edu
    if (advisor.email && advisor.email.includes("@institution.edu")) {
      console.warn(`[安全审计清洗] 剔除检测到的合成假邮箱: ${advisor.email}`);
      const { email: _, ...cleanAdvisor } = advisor;
      return cleanAdvisor;
    }

    return advisor;
  });
}

通过这一切面,带有造假痕迹的字段在入库与进入黑板之前就被剔除或阻断,无法对后续推理环节造成干扰。


四、 多源冲突仲裁:基于权威层级(Authority Tier)的硬性仲裁

在面对真实外部环境时,我们还会遇到另一个普遍的工程挑战:当系统从多个不同渠道拿到了彼此矛盾的事实信息时,应该听谁的?

例如在留学申请场景中:

  • 院校官方招生页写着:“申请截止日期为 12 月 15 日”;
  • 留学交流社区的讨论帖声称:“该项目今年截止日延期到了 1 月 15 日”;
  • 泛搜索引擎的旧网页快照显示:“截止日期为 11 月 30 日”。

如果把这三份冲突数据直接全量推给大模型,模型极易产生幻觉,甚至在不同的对话轮次中随机采纳不同的时间。

解决多数据源冲突的工程准则,是建立清晰的权威层级(Authority Tier)进行硬性仲裁,而不是依赖大模型凭空猜测:

flowchart TD
    T1["Tier 1: 官方一手源 (official_handbook)
最高权威,硬性门槛拥有一票否决权"] --> T2["Tier 2: 权威专业数据库 (academic_db)
如 OpenAlex / CSRankings,硬指标数据高度可信"] T2 --> T3["Tier 3: 泛互联网检索 (web_search)
时效性好,但易混入过时快照,仅作补充线索"] T3 --> T4["Tier 4: 社区交流论坛 (community)
民间经验与软性参考,严禁推翻官方一手规则"]

仲裁代码实现

// 权威层级定义:数字越小,权重越高
export const SOURCE_AUTHORITY_RANK: Record<GroundingSource["sourceType"], number> = {
  official_handbook: 1, // 官方一手手册与招生简章
  academic_db: 2,       // 权威学术数据库
  web_search: 3,        // 泛搜索引擎
  community: 4,         // 社区经验与论坛交流
};

// 事实冲突仲裁纯函数
export function arbitrateConflictingFacts<T>(
  factA: { value: T; source: GroundingSource },
  factB: { value: T; source: GroundingSource }
): { resolvedValue: T; reason: string; primaryGrounding: GroundingSource } {
  const rankA = SOURCE_AUTHORITY_RANK[factA.source.sourceType];
  const rankB = SOURCE_AUTHORITY_RANK[factB.source.sourceType];

  // 规则 1:权威层级严格压制(一手官方源绝对优先)
  if (rankA < rankB) {
    return {
      resolvedValue: factA.value,
      reason: `采纳高权威源 [${factA.source.label}],覆盖低权威源 [${factB.source.label}]`,
      primaryGrounding: factA.source,
    };
  }
  if (rankB < rankA) {
    return {
      resolvedValue: factB.value,
      reason: `采纳高权威源 [${factB.source.label}],覆盖低权威源 [${factA.source.label}]`,
      primaryGrounding: factB.source,
    };
  }

  // 规则 2:同级数据源发生冲突,按置信度权重裁决
  if (factA.source.confidenceScore >= factB.source.confidenceScore) {
    return {
      resolvedValue: factA.value,
      reason: `同属 ${factA.source.sourceType} 层级,按较高置信度采纳`,
      primaryGrounding: factA.source,
    };
  }

  return {
    resolvedValue: factB.value,
    reason: `同属 ${factB.source.sourceType} 层级,按较高置信度采纳`,
    primaryGrounding: factB.source,
  };
}

如果两个冲突来源处于同一权威层级且置信度完全一致(例如官网两份不同页面更新不一致),系统不应当强行二选一,而应当将两份证据并列标注,并在交互界面生成核验提示,交由人类决策。


全景总结与选型指南

在设计高可靠的 Agent 工具层时,防御体系的建立不是单一维度的修补,而是环环相扣的工程组合:

防御环节 核心工程手段 解决的本质问题 错误反例对比
执行前(Pre-execution) 前置门禁 (Gate Evaluator) 上下文依赖不全时提前阻断,防止无意义的网络开销与脏数据渗透 不做依赖校验,盲目调用重型爬虫导致空转报错
执行中(In-execution) 快速失败 (Fail-Fast) 抛出结构化 ToolExecutionError,为模型提供明确反馈以触发渠道切换 通配 catch 吞掉异常并返回 Mock 假数据,误导下游决策
执行后(Post-execution) 事实溯源 (Grounding Attribution) 强制绑定 GroundingSource 证据链,配合审计切面剔除假数据 返回没有任何来源快照的孤立字段,真伪无法验证
数据合并(Arbitration) 权威层级仲裁 (Authority Tier) 官方一手凭证压制软性第三方信息,防止多源冲突时模型随机站队 把多份相互矛盾的数据丢给大模型自己碰运气

工具不是向大模型随意开放的裸露脚本,而是具备严谨生命周期的受信管道。在环境不确定性面前,坚决摒弃伪降级、坦诚向系统暴露客观事实,是保障 Agent 长期推演不失真的关键底线。