工具失败、空结果、重复调用与证据管理

搜索没有匹配结果,与搜索服务连接失败,需要采取不同的下一步。若都只返回空字符串,模型就失去了判断依据。下面设计能够说明成功、失败和部分完成的结果,让重试与补充检索有根据。

1. 区分失败与空结果

以下状态语义完全不同:

  • success_with_data:成功且有数据;
  • success_empty:查询成功但没有匹配;
  • not_found:指定资源不存在;
  • invalid_argument:调用参数错误;
  • permission_denied:策略阻止;
  • timeout:结果未知或未完成;
  • partial:返回了部分数据;
  • internal_error:工具内部异常。

如果都变成“没有结果”,模型容易错误扩大结论或无意义重复调用。

2. 恢复策略

  • 空搜索:改写关键词、放宽过滤、换数据源;
  • 参数错误:给出字段级提示;
  • 超时:缩小范围或按幂等规则重试;
  • 部分结果:继续分页并保留已得数据;
  • 权限拒绝:停止该动作,选择允许的读取或生成步骤;
  • 内部异常:记录关联 ID,避免把完整栈泄漏到对话。

每类重试都要有次数和总预算。

3. 重复调用检测

保存最近 N 次:

Python
from dataclasses import dataclass from typing import Any @dataclass(frozen=True) class InvocationFingerprint: tool: str canonical_arguments: Any result_digest: str state_digest: str
Rust
use serde_json::Value; struct InvocationFingerprint { tool: String, canonical_arguments: Value, result_digest: String, state_digest: String, }
JavaScript
/** * @typedef {{ * tool: string, * canonicalArguments: unknown, * resultDigest: string, * stateDigest: string * }} InvocationFingerprint */
TypeScript
{ tool: string canonicalArguments: unknown resultDigest: string stateDigest: string }

若参数、状态和结果摘要都相同,继续调用几乎不会产生新信息。Harness 可以注入显式观察,要求模型改变策略或结束。

4. EvidenceItem

Python
from dataclasses import dataclass from typing import Literal @dataclass(frozen=True) class EvidenceItem: id: str source_type: Literal["url", "file", "query", "screenshot", "test"] locator: str retrieved_at: str tool_call_id: str excerpt: str | None = None content_hash: str | None = None
Rust
enum SourceType { Url, File, Query, Screenshot, Test, } struct EvidenceItem { id: String, source_type: SourceType, locator: String, excerpt: Option<String>, retrieved_at: String, tool_call_id: String, content_hash: Option<String>, }
JavaScript
/** * @typedef {{ * id: string, * sourceType: 'url'|'file'|'query'|'screenshot'|'test', * locator: string, * excerpt?: string, * retrievedAt: string, * toolCallId: string, * contentHash?: string * }} EvidenceItem */
TypeScript
type EvidenceItem = { id: string sourceType: 'url' | 'file' | 'query' | 'screenshot' | 'test' locator: string excerpt?: string retrievedAt: string toolCallId: string contentHash?: string }

EvidenceItem 与对话文本分开保存。最终答案引用证据 ID,渲染层再转换为链接、文件行号或截图。这样可以检查引用是否真实来自工具结果,而不是模型生成的相似 URL。

5. 引用验证

  1. 引用目标是否在 EvidenceItem 集合;
  2. 引用内容是否支持对应陈述;
  3. 来源时间和版本是否适用;
  4. 截断内容是否遗漏相反信息;
  5. 多来源结论是否分别标注事实与推断。

对 RAG 和研究 Agent,引用完整性是最终成功条件的一部分。

6. 统一结果的三值语义

调用结果至少要区分:

  1. success_with_data:成功且存在结果;
  2. success_empty:查询成功,当前条件下确实没有匹配;
  3. failure 或 unknown:请求失败、超时,或副作用状态待确认。

把后两者都转换成 [] 会让 Agent 形成错误事实。证据项可使用 source、locator、version、captured_at、content_hash 和 excerpt,并保留从哪次 call 得到。

flowchart LR T[ToolResult] --> K{kind} K -->|data| E[抽取 EvidenceItem] K -->|empty| N[记录查询范围与时间] K -->|retryable error| R[消耗重试预算] K -->|unknown side effect| Q[查询外部状态] E --> V[最终答案验证] N --> V

最终答案中的关键事实应能映射到证据;模型综合或推断的内容要与直接来源事实区分。大结果经过截断时必须携带 truncated: true 和继续读取方式,否则模型会误以为已看到全集。

把“没查到”与“没查成”分开

查询工单时,空数组可能表示真的没有匹配项,也可能是你把网络错误吞掉后返回了 []。后一种实现会让模型很自然地编出“系统里没有这张工单”的结论。

先设计四个假结果:查询成功且有数据、查询成功但为空、没有权限、连接超时。让调用方先判断结果类型,再决定回答或恢复;不要只判断 items.length。模型看到的错误保留原因和下一步即可,详细堆栈留在受控日志里。

goose 的架构文档说明了把执行错误作为工具响应回传的做法。这能让模型根据失败调整动作,但不意味着任何错误都应该重试。权限拒绝不能靠换个工具绕过去;创建动作超时则要先核对目标状态。

本节的核验依据

2026-10-25 查阅。下面的链接支持本节的机制说明;练习场景和选型建议是本教程的教学设计。