AI Agent 证据仲裁工程实践:别让检索结果按自信程度撒谎

很多团队第一次把 AI Agent 接进内部系统时,最先做的是两件事:接知识库,接工具。知识库负责回答制度、流程、产品说明,工具负责查订单、查余额、查审批状态。Demo 看起来很顺,用户问一句,Agent 检索几段材料,再调用一两个接口,最后给一个完整答案。

真正上线后,问题往往不是"没有答案"。更麻烦的是"证据太多,而且互相打架"。

制度正文说差旅餐补以城市级标准为准;一份旧 FAQ 写着统一 80 元;财务接口返回北京 100 元、苏州 60 元;用户历史报销单里又出现过 90 元。普通 RAG 会把最像问题的 FAQ 排在前面,普通 Agent 会把接口结果和文档摘几句拼起来,最后用非常确定的语气告诉用户"餐补上限 80 元"。

这类错误很隐蔽。HTTP 状态码是 200,工具调用成功,检索也有命中,日志里看不到异常。用户发现报销被驳回,才知道 Agent 答错了。

我现在更愿意把这类能力叫做"证据仲裁",而不是简单的"检索增强"或"幻觉检测"。检索只负责把可能相关的材料找出来,仲裁要回答另一个问题:当多份材料都看似相关时,哪一份应该被信任,哪一份应该被降权,哪一种冲突必须触发拒答或追问。

这篇文章不做工具清单,也不讲泛泛的 RAG 搭建。我们用一个可复现的小案例,把生产 Agent 里证据仲裁器该怎么设计讲清楚。

一、相关不等于可信,命中不等于支持

很多线上事故都来自一个很朴素的误会:相似度高的证据就是好证据。

向量检索的目标是找语义接近的片段,rerank 的目标是把更符合 query 的片段排前面。它们解决的是"像不像",不是"该不该信"。旧 FAQ 里如果刚好写了"餐补上限是多少",它当然和用户问题很像;制度正文可能写成"补贴标准由财务系统按城市等级动态生效",反而没那么像用户的自然语言问题。

于是系统会出现一个很荒诞的现象:越像用户问题的旧材料,越容易盖过真正权威但措辞不同的新材料。

公开的 Agent 可观测性资料里反复提到一个点:传统 APM 能告诉你一次请求是否成功、耗时多少、调用了哪些服务,但很难告诉你 Agent 为什么选了这条路径。RAG 观测资料也强调,检索质量、上下文缺失、材料过期、生成是否忠于材料,这些都不是 HTTP 成功率能覆盖的指标。

证据仲裁器要补上的就是这一层。它不替代检索,也不替代生成模型。它夹在检索和生成之间,把"候选材料"变成"可被引用的证据决策"。

二、一个最小故障样本

先构造一个小数据集。假设我们有一个内部报销政策 Agent,它能读取三类数据。

来源 内容摘要 更新时间 权威等级
company_policy_2026 差旅餐补以财务系统中城市级标准为准,制度页面只说明计算原则 2026-06-01 0.95
hr_faq_2024 国内出差餐补统一为每天 80 元 2024-03-10 0.45
finance_api 北京每天 100 元,苏州每天 60 元,三线城市每天 50 元 实时 1.00
reimbursement_case 某员工去年在杭州报销过每天 90 元 2025-09-12 0.30

用户问:

我下周去苏州出差,餐补一天最多能报多少?

一个普通 top-k 流程可能拿到 FAQ 和历史报销单,因为"餐补""一天""报多少"这些词都很接近。财务接口虽然最权威,但如果它作为工具结果在后一步出现,模型很可能已经被前面的 80 元锚定。最后答案就会摇摆,甚至直接错。

我们希望正确答案是:

按当前财务系统,苏州出差餐补上限为每天 60 元。旧 FAQ 中的 80 元是过期统一标准,不能作为最终依据;历史报销单不是制度来源,只能用于排查异常,不能用于当前政策判断。

这句话背后有四个动作。

  1. 把用户问题拆成 claim:地点是苏州、费用类型是餐补、需要当前上限。
  2. 判断每条证据对每个 claim 是支持、反驳还是无关。
  3. 结合来源权威性、更新时间、工具实时性,做冲突仲裁。
  4. 如果冲突无法解决,禁止模型硬答,转为追问或人工升级。

这就是证据仲裁器的核心。

三、Evidence Envelope:别把裸文本扔给模型

很多系统的问题从数据结构就埋下了。检索阶段返回一组字符串,然后直接拼进 prompt:

text 复制代码
Context:
1. 国内出差餐补统一为每天 80 元。
2. 苏州每天 60 元。
3. 差旅餐补以财务系统中城市级标准为准。

这对模型很不公平。它看到的是三个句子,不知道哪个来自制度、哪个来自旧 FAQ、哪个是实时接口,也不知道更新时间、权限边界和适用范围。

正确做法是给每条证据包一层 envelope。

ts 复制代码
type EvidenceSourceType = 'policy' | 'faq' | 'api' | 'case' | 'user_input';

type EvidenceEnvelope = {
  id: string;
  sourceType: EvidenceSourceType;
  sourceName: string;
  text: string;
  updatedAt: string;
  authority: number;      // 0~1,制度/实时系统通常更高
  freshness: number;      // 0~1,按业务半衰期计算
  accessScope: 'public' | 'internal' | 'restricted';
  retrievedBy: 'vector' | 'keyword' | 'tool' | 'manual';
  retrievalScore?: number;
  metadata?: Record<string, string>;
};

这里最重要的是 authorityfreshness。它们不应该交给模型临场判断,而应该由业务系统显式给出。

比如制度库可以规定:

ts 复制代码
const SOURCE_AUTHORITY: Record<EvidenceSourceType, number> = {
  api: 1.0,
  policy: 0.95,
  faq: 0.45,
  case: 0.30,
  user_input: 0.20,
};

这不是为了做一个完美评分系统,而是为了把组织内早就存在的信任关系写进 runtime。财务接口本来就比员工历史报销单权威,正式制度本来就比 FAQ 权威。Agent 不应该重新发明这套规则。

freshness 也一样。不同材料的过期速度不同。价格、库存、政策额度可能按天变化;产品手册可能按月变化;法律条款可能按版本变化。可以先用一个简单半衰期函数:

ts 复制代码
function freshness(updatedAt: string, halfLifeDays: number, now = new Date()): number {
  const ageMs = now.getTime() - new Date(updatedAt).getTime();
  const ageDays = Math.max(0, ageMs / 86400000);
  return Math.pow(0.5, ageDays / halfLifeDays);
}

政策 FAQ 的半衰期可以设成 90 天,实时接口直接设成 1。这样旧 FAQ 即使语义很像,也不会在仲裁时天然占优。

四、Claim-Support Matrix:先拆主张,再看证据

下一步不要急着生成答案。先把用户问题和候选答案拆成 claim。

在报销案例里,至少有三个 claim:

ts 复制代码
type Claim = {
  id: string;
  text: string;
  kind: 'fact' | 'policy' | 'calculation' | 'recommendation';
  risk: 'low' | 'medium' | 'high';
};

const claims: Claim[] = [
  { id: 'c1', text: '用户出差地点是苏州', kind: 'fact', risk: 'low' },
  { id: 'c2', text: '餐补上限需要按当前政策判断', kind: 'policy', risk: 'high' },
  { id: 'c3', text: '苏州当前餐补上限为每天 60 元', kind: 'calculation', risk: 'high' },
];

然后让一个轻量判定器或规则模块判断每条证据和每个 claim 的关系。

ts 复制代码
type SupportLabel = 'support' | 'contradict' | 'irrelevant' | 'partial';

type ClaimSupport = {
  claimId: string;
  evidenceId: string;
  label: SupportLabel;
  confidence: number;
  quote: string;
};

矩阵可能长这样:

claim company_policy_2026 hr_faq_2024 finance_api reimbursement_case
c1 苏州 无关 无关 支持 无关
c2 按当前政策 支持 部分支持但过期 支持 无关
c3 上限 60 元 间接支持 反驳 支持 反驳或无关

这个矩阵的价值很大。它把"文档看起来相关"变成了"这条证据具体支持哪个主张"。生成模型最后只能使用被矩阵确认支撑的 claim,而不是把所有 retrieved chunk 都当成事实来源。

如果你担心 模型辅助评审 成本,可以先做混合方案。结构化字段、金额、日期、城市名用规则判断;语义支撑关系再交给小模型或主模型批处理。大多数业务场景里,最危险的 claim 往往就是金额、日期、权限、状态这几类,规则反而更可靠。

五、仲裁器:把冲突变成可执行决策

有了 evidence envelope 和 support matrix,就可以写仲裁器了。

ts 复制代码
type ArbitrationDecision = {
  claimId: string;
  verdict: 'accepted' | 'rejected' | 'needs_clarification' | 'escalate';
  answerable: boolean;
  winningEvidenceIds: string[];
  rejectedEvidenceIds: string[];
  conflictType?: 'none' | 'soft' | 'hard';
  score: number;
  rationale: string;
};

function supportScore(e: EvidenceEnvelope, s: ClaimSupport): number {
  const labelWeight =
    s.label === 'support' ? 1 :
    s.label === 'partial' ? 0.45 :
    s.label === 'contradict' ? -1 : 0;

  return labelWeight * s.confidence * (
    0.50 * e.authority +
    0.30 * e.freshness +
    0.20 * (e.retrievalScore ?? 0.5)
  );
}

function arbitrateClaim(
  claim: Claim,
  evidence: EvidenceEnvelope[],
  supports: ClaimSupport[]
): ArbitrationDecision {
  const rows = supports.filter(s => s.claimId === claim.id);
  const byId = new Map(evidence.map(e => [e.id, e]));

  const scored = rows.map(s => {
    const e = byId.get(s.evidenceId)!;
    return { support: s, evidence: e, score: supportScore(e, s) };
  });

  const positive = scored.filter(x => x.score > 0).sort((a, b) => b.score - a.score);
  const negative = scored.filter(x => x.score < 0).sort((a, b) => a.score - b.score);

  const bestPositive = positive[0];
  const strongestNegative = negative[0];

  if (!bestPositive) {
    return {
      claimId: claim.id,
      verdict: claim.risk === 'high' ? 'needs_clarification' : 'rejected',
      answerable: false,
      winningEvidenceIds: [],
      rejectedEvidenceIds: negative.map(x => x.evidence.id),
      conflictType: negative.length ? 'hard' : 'none',
      score: 0,
      rationale: '没有足够证据支撑该主张',
    };
  }

  const hasHardConflict = strongestNegative && Math.abs(strongestNegative.score) > bestPositive.score * 0.8;

  if (hasHardConflict && claim.risk === 'high') {
    const positiveAuthority = bestPositive.evidence.authority;
    const negativeAuthority = strongestNegative.evidence.authority;

    if (positiveAuthority - negativeAuthority < 0.25) {
      return {
        claimId: claim.id,
        verdict: 'escalate',
        answerable: false,
        winningEvidenceIds: [bestPositive.evidence.id],
        rejectedEvidenceIds: [strongestNegative.evidence.id],
        conflictType: 'hard',
        score: bestPositive.score,
        rationale: '高风险主张存在强冲突,且权威差不足以自动仲裁',
      };
    }
  }

  return {
    claimId: claim.id,
    verdict: 'accepted',
    answerable: true,
    winningEvidenceIds: positive.slice(0, 3).map(x => x.evidence.id),
    rejectedEvidenceIds: negative.map(x => x.evidence.id),
    conflictType: negative.length ? 'soft' : 'none',
    score: bestPositive.score,
    rationale: negative.length
      ? '存在冲突证据,但胜出证据在权威性和时效性上明显更强'
      : '主张被可信证据支撑,未发现有效反证',
  };
}

这段代码故意没有写得很"智能"。生产系统里,第一版仲裁器越朴素越好,因为它必须可解释、可调参、可回放。你要能回答用户和业务方:为什么 Agent 没信 FAQ?为什么这次没有直接回答?为什么某个工具结果压过了文档?

模型可以参与判断"这句话是否支持那个 claim",但最终是否采纳,最好由一个确定性仲裁层决定。

六、四个接入点:不要只在生成后查幻觉

很多团队把"幻觉检测"放在最终输出之后。答案生成完,再跑一个检查器,看是否忠于上下文。这个步骤有用,但太晚了。

证据仲裁至少有四个接入点。

接入点 目标 失败时动作
检索后 过滤过期、低权威、权限不匹配材料 降权或移除
工具结果后 合并实时接口与文档证据 重新仲裁相关 claim
生成前 只把 accepted claim 和证据交给模型 不可回答时追问或升级
输出前 检查最终答案是否引入未支撑 claim 阻断、重写或人工确认

关键是生成前这一刀。不要把所有冲突材料都塞进 prompt,然后指望模型自己"综合判断"。模型很擅长写出流畅解释,但不应该承担组织权威规则的最终裁判。

生成 prompt 可以变成这样:

text 复制代码
你只能基于 accepted_claims 回答。
不得使用 rejected_evidence 中的信息作为结论依据。
如果用户问题涉及 blocked_claims,必须说明当前证据冲突,建议查询人工或最新系统。

accepted_claims:
- c1: 用户问的是苏州出差餐补
- c2: 餐补上限以财务系统城市级标准为准
- c3: 苏州当前餐补上限为每天 60 元

rejected_evidence:
- hr_faq_2024: 旧 FAQ,统一 80 元标准已过期
- reimbursement_case: 历史个案,不代表当前制度

这比"下面是检索内容,请回答"安全得多。

七、把证据决策写进 Trace

没有可观测性,仲裁器上线后很快会变成另一个黑盒。用户投诉"Agent 答错了",你只看到最终答案和几个 chunk,仍然不知道当时为什么选了 A 没选 B。

我建议每个高风险回答都记录下面这些字段:

ts 复制代码
type EvidenceTrace = {
  traceId: string;
  userQuery: string;
  claims: Claim[];
  evidence: Array<{
    id: string;
    sourceType: EvidenceSourceType;
    sourceName: string;
    authority: number;
    freshness: number;
    retrievalScore?: number;
  }>;
  supports: ClaimSupport[];
  decisions: ArbitrationDecision[];
  finalAnswerHash: string;
  blocked: boolean;
};

如果你们已经接了 开放遥测,可以把关键字段打到 span attribute 或 event 里:

ts 复制代码
span.setAttribute('agent.evidence.count', evidence.length);
span.setAttribute('agent.claim.count', claims.length);
span.setAttribute('agent.evidence.blocked', decisions.some(d => !d.answerable));

for (const d of decisions) {
  span.addEvent('evidence.arbitration', {
    'claim.id': d.claimId,
    'claim.verdict': d.verdict,
    'claim.conflict_type': d.conflictType ?? 'none',
    'claim.score': d.score,
    'evidence.winners': d.winningEvidenceIds.join(','),
    'evidence.rejected': d.rejectedEvidenceIds.join(','),
  });
}

注意不要把敏感原文全量写进日志,尤其是合同、财务、客户资料。存 evidence id、hash、来源、分数、裁决理由就够排障了。需要复盘时,再通过权限系统回查原文。

这也是很多 Agent 可观测性实践里的核心思想:trace 不只是"记录发生了什么",还要能把生产样本转成测试集。一次证据仲裁失败,应该沉淀成一条 regression case,而不是在群里吵完就过去。

八、一个小实验:top-k 为什么会输给仲裁器

我们用前面的四条材料做一个最小实验。假设用户问题是"苏州出差餐补一天最多能报多少"。

普通 top-k 的排序可能是:

  1. hr_faq_2024:国内出差餐补统一为每天 80 元
  2. reimbursement_case:杭州历史报销每天 90 元
  3. finance_api:苏州每天 60 元
  4. company_policy_2026:以财务系统城市级标准为准

如果直接生成,模型很容易回答 80 元,或者写成"通常 80 元,但可能按城市调整"。这对用户没有帮助,因为用户要的是可执行结论。

加入仲裁器后,排序变成决策:

证据 相似度 权威 时效 对 c3 的关系 仲裁结果
hr_faq_2024 0.91 0.45 0.08 反驳 rejected
reimbursement_case 0.78 0.30 0.45 无关/反驳 rejected
finance_api 0.72 1.00 1.00 支持 accepted
company_policy_2026 0.66 0.95 0.85 间接支持 accepted

这个实验不证明某个固定公式永远正确,它证明的是一件工程事实:相似度只能作为证据候选的入口信号,不能作为最终信任信号。信任必须引入业务权威、时效、冲突关系和风险等级。

九、Eval 数据集怎么建

证据仲裁器最怕"看起来合理,线上失效"。所以第一版上线前,我会做一个很小但尖锐的数据集,不追求数量,追求冲突质量。

建议从生产或准生产里挑 20 类样本:

  1. 新旧政策冲突。
  2. FAQ 与正式制度冲突。
  3. 历史个案与当前规则冲突。
  4. 用户输入与系统记录冲突。
  5. 两个工具返回不同状态。
  6. 权限不足导致证据缺失。
  7. 检索命中相似但业务对象不同。
  8. 同名产品不同版本。
  9. 金额、日期、地区这类结构化字段冲突。
  10. 文档说"以系统为准",系统结果又为空。

每条样本至少标注三个东西:期望答案、必须采纳的证据、必须拒绝的证据。

json 复制代码
{
  "query": "我下周去苏州出差,餐补一天最多能报多少?",
  "must_accept": ["finance_api", "company_policy_2026"],
  "must_reject": ["hr_faq_2024", "reimbursement_case"],
  "expected_claims": [
    "苏州当前餐补上限为每天 60 元",
    "旧 FAQ 不作为当前依据"
  ],
  "should_block": false
}

评估指标也不要只看最终答案相似度。至少要看四个:

指标 含义
accepted evidence precision 被采纳证据里有多少确实该采纳
rejected evidence recall 应拒绝证据有多少被系统拒绝
unsupported claim count 最终答案里有多少 claim 没有证据支撑
necessary block recall 该拒答或升级的样本里,系统拦住了多少

这四个指标比"回答打分 4.3/5"更适合工程迭代。因为它们能定位问题发生在检索、支撑判断、仲裁阈值还是生成约束。

十、常见坑

第一,把 rerank 分数当可信度。 rerank 只能说明材料和问题贴近,不说明材料权威。旧文档、论坛回答、历史个案都可能非常贴近。

第二,让模型自己决定哪份证据可信。 模型可以解释规则,但组织里的权威关系必须外置。正式制度、实时系统、用户口述、历史记录,这些信任等级应该由业务配置决定。

第三,只保存最终答案。 只存答案无法复盘。至少要保存 claim、evidence id、support label、仲裁结果和拒绝理由。

第四,所有冲突都继续回答。 高风险 claim 一旦出现强冲突,应该追问或升级。比如金额、权限、合规、医疗、法律、合同条款,不要为了"体验流畅"硬答。

第五,证据粒度太粗。 一整篇文档命中并不代表整篇都支持结论。最好把文档切到条款级、段落级,最终引用也落到 claim 级。

第六,没有权限边界。 有些证据对系统可见,但对当前用户不可见。仲裁器不能把 restricted 证据变相泄露进答案。它可以说"当前权限下无法确认",不能说出受限内容。

十一、小团队一周落地版本

如果你没有时间做完整平台,可以按这个顺序上第一版。

第一天,改数据结构。所有检索和工具结果都包成 EvidenceEnvelope,至少补齐 sourceType、updatedAt、authority、retrievedBy。

第二天,挑 20 条冲突样本。不要挑简单问答,要挑新旧政策、FAQ 与系统冲突、历史个案误导这类样本。

第三天,做 claim 拆分。先只覆盖高风险字段:金额、日期、地区、状态、权限。能规则拆就规则拆,别一开始就追求通用。

第四天,实现仲裁器。公式简单一点没关系,但要能输出 accepted、rejected、needs_clarification、escalate。

第五天,改生成 prompt。只允许模型使用 accepted claim。blocked claim 必须追问或升级。

第六天,把仲裁结果写进 trace。注意脱敏,记录 id 和 hash,不要把敏感原文全塞日志。

第七天,跑 eval,调阈值。重点看 unsupported claim 和 necessary block,而不是只看答案流畅度。

一周后你不会得到一个完美的"事实系统",但会得到一个很关键的能力:当 Agent 不确定时,它知道自己为什么不确定;当证据冲突时,它不会因为某段文本更像问题就自信撒谎。

结语

AI Agent 的生产可靠性,不只取决于模型多强、检索多准、工具接得多全。真正困难的是把这些不稳定信号合成一个可解释、可回放、可阻断的决策。

证据仲裁器听起来像一个中间层小模块,但它改变了系统默认行为:从"找到相似材料就回答",变成"只有被可信证据支撑的 claim 才能回答"。

这就是很多 Agent 从 Demo 走向生产时缺的那一层刹车。

如果你现在的系统已经有 RAG、有工具调用、有 trace,但用户仍然偶尔反馈"它说得很像真的,但就是错了",不要急着换模型。先检查一件事:你的 Agent 到底是在检索证据,还是在仲裁证据。

相关推荐
头发够用的程序员43 分钟前
Nsight Systems 零基础入门:Trace采集与基础操作
人工智能·深度学习·神经网络·计算机视觉·边缘计算·分析工具
neocheng_52244 分钟前
怎么评判 AI 证书的社会认可度?证书价值要结合实际场景判断
人工智能
卷无止境1 小时前
FastAPI 权限管理实战:从 ACL 到 RBAC 的那些门道
后端·python·fastapi
搜yyzk6685 小时前
智能电话机器人:1天千通电话,高效低成本
人工智能
程序员黑豆8 小时前
Java类型推断完全指南:从var到菱形运算符,掌握使用限制与最佳实践
java·前端·ai编程
fthux9 小时前
装闭 RenoPit 源码解析(09):AnalysisEngine装修闭坑分析主流程
人工智能·ai·开源·github·open source·renopit
敏编程9 小时前
开源项目 NextBand:探索融合健康传感、语音交互与 AI Agent 的下一代可穿戴设备
人工智能
GreenTea9 小时前
深度解读 Anthropic 多智能体报告:更强的模型 ≠ 更好的协调
前端·后端·算法
风流 少年9 小时前
Spring AI 2.0:Memory
java·后端·spring