前端做 RAG 别只让 AI 会答:用 3 个字段把答案变成“有证据”

5 阅读9分钟

作者:前端转 AI 深度实践者

【省流助手/核心观点】:知识库问答最怕的不是“不回答”,而是 AI 用很自信的语气瞎回答。前端开发者做 RAG(检索增强生成)时,不要只把检索片段丢给模型,还要把答案设计成可校验的数据结构:answercitationsconfidence。这 3 个字段能让 RAG 答案从“看起来会说”,变成“能追溯、能拒答、能排查”。


很多 RAG demo 第一次跑起来时,都会让人兴奋。

用户问一个问题,系统检索资料,再让模型生成回答。页面上出现一段流畅的文字,看起来像一个真正的知识库助手。

但只要进入真实业务,你很快会被一个问题拦住:

这个答案到底有没有依据?

用户问内部制度、产品文档、接口字段、售后政策时,并不是想听 AI “发挥一下”。他真正需要的是:

  • 这个答案是不是来自我的资料?
  • 如果我不信,能不能点回原文?
  • 如果资料里没有,系统会不会硬编?
  • 如果答案错了,开发者能不能定位是哪一层错了?

这篇文章不讲宏大的 RAG 概念,只讲一个很落地的 RAG 优化点:让答案带证据

1. 痛点:为什么“答得像真的”反而更危险?

大模型最迷人的能力,是能把零散信息组织成顺滑的文本。
但在知识库问答里,这也是风险来源。

因为一个没有依据的答案,也可能看起来:

  • 语气很确定
  • 结构很完整
  • 名词很专业
  • 甚至还像是在引用文档

这对前端开发者尤其容易形成错觉:接口返回了、页面渲染了、用户也能读懂,好像功能就完成了。

但 RAG 产品真正要交付的不是“自然语言”,而是“基于资料的回答”。如果系统在资料不足时仍然编出一段漂亮答案,本质上是把“猜测”包装成了“知识”。

2. ❌ 错误做法:只把上下文拼进 Prompt,然后相信模型

很多早期实现都会长这样:

async function askRag(question: string) {
  const chunks = await vectorStore.search(question, { topK: 5 });

  const prompt = `
你是一个知识库助手,请根据以下资料回答用户问题。

资料:
${chunks.map((chunk) => chunk.text).join("\n\n")}

问题:${question}
`;

  return llm.chat(prompt);
}

这段代码可以跑,demo 效果也可能不错,但它有 3 个隐患:

  1. 模型可能引用了资料里没有的信息。
  2. 前端无法知道答案对应哪些原文片段。
  3. 答案错了以后,开发者只能盯着最终文本猜问题。

最要命的是:你没有给系统留下“证据链”。

用户看到的是一段话,开发者拿到的也是一段话。它到底基于哪份资料、哪一段、哪一句生成,没有结构化信息可查。

3. ✅ 正确做法:让模型返回答案、引用和置信边界

更适合生产环境的做法,是把检索片段编号,然后要求模型只返回结构化结果。

type SourceChunk = {
  id: string;
  title: string;
  url: string;
  text: string;
};

type RagAnswer = {
  answer: string;
  citations: Array<{
    chunkId: string;
    quote: string;
  }>;
  confidence: "high" | "medium" | "low";
  refusalReason?: string;
};

然后把 Prompt 从“请回答”改成“请基于证据回答”:

function buildGroundedPrompt(question: string, chunks: SourceChunk[]) {
  const context = chunks
    .map(
      (chunk) => `
[${chunk.id}]
标题:${chunk.title}
链接:${chunk.url}
内容:${chunk.text}
`.trim()
    )
    .join("\n\n");

  return `
你是一个严谨的知识库问答助手。

规则:
1. 只能基于 <context> 中的资料回答。
2. 如果资料不足以回答,answer 必须写“根据当前资料无法确定”,confidence 写 "low"。
3. citations 只能引用真实存在的 chunkId。
4. quote 必须摘自对应 chunk 的原文,不要改写。
5. 不要补充资料中没有出现的信息。

<context>
${context}
</context>

用户问题:${question}

请返回 JSON,格式如下:
{
  "answer": "string",
  "citations": [{ "chunkId": "string", "quote": "string" }],
  "confidence": "high | medium | low",
  "refusalReason": "string | optional"
}
`;
}

这一步的关键,不是让 Prompt 看起来更复杂,而是让 RAG 引用来源变成可被程序消费的数据。

前端可以渲染来源,后端可以做校验,排查问题时也能快速定位到原文。

4. 只让模型“写引用”还不够,你要在代码里校验

很多团队踩过一个坑:模型确实返回了 citations,但引用是假的。

例如检索结果里只有 chunk_1chunk_5,模型却返回了 chunk_8。或者 quote 根本不在原文里,只是模型自己概括出来的一句话。

所以正确姿势是:模型负责生成,代码负责验收

function validateCitations(answer: RagAnswer, chunks: SourceChunk[]) {
  const chunkMap = new Map(chunks.map((chunk) => [chunk.id, chunk]));

  const validCitations = answer.citations.filter((citation) => {
    const source = chunkMap.get(citation.chunkId);
    if (!source) return false;

    return source.text.includes(citation.quote);
  });

  const hasEnoughEvidence =
    answer.confidence !== "low" && validCitations.length > 0;

  return {
    ...answer,
    citations: validCitations,
    answer: hasEnoughEvidence
      ? answer.answer
      : "根据当前资料无法确定",
    confidence: hasEnoughEvidence ? answer.confidence : "low",
    refusalReason: hasEnoughEvidence
      ? answer.refusalReason
      : "未找到足够可靠的来源支撑该回答",
  };
}

这段逻辑非常适合前端同学理解:

  • citations 就像组件 props,不能随便信。
  • chunkId 就像列表 key,必须能对应真实数据。
  • quote 就像用户输入,必须经过校验再展示。

做完这一步,你的知识库问答系统就不再只是“会说”,而是开始具备基本的可追溯能力。

5. 前端页面不要只展示答案,要展示“证据入口”

如果 UI 只展示一段回答,用户只能选择相信或不相信。

更好的交互是:答案下面展示来源卡片,让用户能点回原文。

function RagAnswerView({ result, sources }: {
  result: RagAnswer;
  sources: SourceChunk[];
}) {
  const sourceMap = new Map(sources.map((source) => [source.id, source]));

  return (
    <section>
      <p>{result.answer}</p>

      {result.citations.length > 0 && (
        <div>
          <h3>回答依据</h3>
          {result.citations.map((citation) => {
            const source = sourceMap.get(citation.chunkId);
            if (!source) return null;

            return (
              <a key={citation.chunkId} href={source.url}>
                <strong>{source.title}</strong>
                <blockquote>{citation.quote}</blockquote>
              </a>
            );
          })}
        </div>
      )}

      {result.confidence === "low" && (
        <p>当前资料不足,建议补充文档或换一种问法。</p>
      )}
    </section>
  );
}

这类设计对真实落地很重要,因为它给 RAG 产品补上了一个常被忽略的能力:用户可验证

RAG 的信任感不是靠“AI 味很浓”的回答建立的,而是靠“我能看到它依据什么说”建立的。

6. 生产环境避坑指南

1. 不要把所有检索结果都当来源

检索到 5 个片段,不代表这 5 个片段都支撑最终答案。

错误做法是把所有 chunks 都挂到答案后面,制造一种“引用很多,所以可信”的错觉。正确做法是只展示模型实际使用、并且通过校验的来源。

2. 拒答不是失败,是边界

资料里没有答案时,最负责任的输出不是“我猜一下”,而是:

根据当前资料无法确定。

这句话看起来不够惊艳,但在企业知识库、客服知识库、内部文档问答里非常重要。一个敢拒答的系统,通常比一个永远自信的系统更可靠。

3. 低相似度结果不要强行生成

如果检索阶段最高相似度很低,或者 Rerank 分数明显不达标,应该提前触发拒答,而不是继续让模型编。

function shouldAnswer(topScore: number, rerankScore?: number) {
  if (topScore < 0.35) return false;
  if (rerankScore !== undefined && rerankScore < 0.45) return false;

  return true;
}

分数阈值没有统一标准,需要结合你的数据集评估。但原则很明确:没有足够证据,就不要生成确定答案。

4. 引用粒度不要太粗

只引用“某某文档”通常不够。用户真正需要的是能定位到段落、标题、页面锚点,最好还能看到原文摘录。

建议保存这些 metadata:

type ChunkMetadata = {
  docId: string;
  titlePath: string[];
  url: string;
  startLine?: number;
  endLine?: number;
  updatedAt?: string;
};

这样答案出错时,你能快速判断:是文档过期、切块太粗、检索偏了,还是模型理解错了。

5. 把“引用异常”记进日志

生产环境不要只记录最终答案,至少记录:

  • 用户问题
  • 命中的 chunkId
  • 检索分数和排序分数
  • 模型返回的 citations
  • 被过滤掉的无效引用
  • 是否触发拒答

这些日志会直接决定你后续能不能做有效的 RAG 优化。

7. 常见误区

误区 1:只要用了 RAG,答案就天然可信

不一定。RAG 只是把资料放进上下文,不代表模型一定严格基于资料回答。生成阶段仍然需要边界、结构和校验。

误区 2:引用来源只是 UI 装饰

不是。引用来源既是用户信任入口,也是开发排查入口。没有来源,你只能猜;有来源,你至少知道该查检索、生成还是文档本身。

误区 3:每句话都要做复杂引用

也不必一上来就做论文级引用。对多数业务系统来说,先做到“答案有来源卡片、来源能点回原文、引用能通过校验”,就已经超过大量 demo。

误区 4:Prompt 写了“不要编造”就够了

不够。Prompt 是约束,不是保险。真正可靠的系统,一定要在代码层做结构化输出、引用校验和拒答兜底。

8. 给前端开发者的落地清单

下次你做 RAG 答案页时,不要只问“它回答得自然吗”,而要检查这 6 件事:

  1. 答案是否只基于检索片段生成?
  2. 每个引用是否能对应真实 chunkId
  3. 引用原文是否真的出现在 source chunk 里?
  4. 资料不足时是否能明确拒答?
  5. UI 是否给用户提供原文入口?
  6. 日志是否能帮助你复盘检索、排序和生成链路?

只要这 6 件事做好,你的 RAG 系统就会从“能演示”往“能交付”迈一大步。

结语

RAG 最让人兴奋的地方,不是让模型说得更像专家,而是让模型能基于你的资料回答。

真正可信的知识库问答,至少要做到三点:

  • 知道自己依据什么回答
  • 能让用户追溯到原文
  • 资料不足时敢于说不知道

前端开发者做 AI 应用,不只是把模型结果渲染出来。更重要的是设计一条完整的证据链:从检索片段,到结构化答案,再到页面引用和日志排查。

RAG 的目标不是让模型更会编,而是让它更少乱编。


点赞 + 收藏不迷路,下一篇继续拆 RAG 从 demo 到生产的工程细节。