给AI回答加引用角标citation:RAG前端实现

RAG 答案最值钱的部分,是「这句话出自哪」。模型说得头头是道,用户却不敢信,直到你在句末挂个可点的角标 ¹,点开是原文出处------信任感立刻不一样。这个 citation 角标前端怎么做,看着小,流式场景下坑不少。聊聊我的实现。

后端给的是什么

先说数据。我们后端返回的是文本 + 一组引用标记,标记用占位符嵌在文本里,类似:

css 复制代码
根据财报,二季度营收同比增长 18%[cite:3]。利润率持平[cite:1]。

外加一个 sources 数组:

typescript 复制代码
interface Source {
  id: number;
  title: string;
  url: string;
  snippet: string; // 命中的原文片段
}

前端的活就是:把 [cite:N] 替换成可交互的角标,点击/悬浮展示对应 source。

解析:别用 dangerouslySetInnerHTML

最省事的写法是字符串替换成 <sup> 再 innerHTML 塞进去,但 RAG 答案里混着用户/模型生成的内容,直接 innerHTML 等于开 XSS 大门。我把文本切成片段数组,交给框架渲染,安全也可控:

ini 复制代码
function parseCitations(text: string): (string | { cite: number })[] {
  const parts: (string | { cite: number })[] = [];
  const re = /[cite:(\d+)]/g;
  let last = 0, m: RegExpExecArray | null;
  while ((m = re.exec(text))) {
    if (m.index > last) parts.push(text.slice(last, m.index));
    parts.push({ cite: Number(m[1]) });
    last = m.index + m[0].length;
  }
  if (last < text.length) parts.push(text.slice(last));
  return parts;
}

渲染时字符串段走正常文本,{ cite } 段渲染成角标组件,绑上对应 source 的悬浮卡。安全边界守住了,心里踏实。

流式下的坑:标记被切两半

这是 RAG + 流式特有的暗雷。[cite:3] 这七个字符,可能被 SSE 拆成两个 chunk:先来 ...增长 18%[cite: ,下一包才来 3]。如果你每个 chunk 都立刻 parse,第一包会把 [cite: 当普通文本渲染出来,闪一下乱码,第二包再变回去------抖得很难看。

解法是攒一个 buffer,只在「不可能切到标记中间」的安全点才渲染。简单粗暴版:buffer 末尾若有未闭合的 [,就先扣住不渲染,等闭合:

kotlin 复制代码
let buf = '';
function onChunk(delta: string) {
  buf += delta;
  // 找最后一个未闭合的 [,从那截断,剩下的留到下一包
  const open = buf.lastIndexOf('[');
  const safe = open !== -1 && !buf.slice(open).includes(']')
    ? buf.slice(0, open)
    : buf;
  render(parseCitations(safe));
}

这个 buffer 截断逻辑我前后改了三版才稳,核心就一句:标记可能跨包,渲染必须等它齐了。没意识到这点的话,线上就是满屏一闪一闪的 [cite:

角标点击:定位 + 高亮原文

点角标得跳到出处。如果出处在同页的来源列表里,平滑滚过去并高亮两秒,比直接弹新窗口体验好:

javascript 复制代码
function onCiteClick(id: number) {
  const el = document.getElementById(`source-${id}`);
  el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
  el?.classList.add('flash');
  setTimeout(() => el?.classList.remove('flash'), 2000);
}

一个没解决干净的

模型偶尔会幻觉出一个 sources 里根本不存在的编号,比如 [cite:9] 但数组里最大才 5。我加了校验,越界的角标降级成普通文本不渲染成可点链接------但这只是遮羞,根上是检索/生成的对齐问题,前端兜不住。我能做的就是别让一个死链摆在用户面前,治标不治本,老实承认。

RAG 的检索和模型那套我没自己搭,挂在讯飞这类 MaaS 上,场景化检索和来源回传它那边给,前端只负责把引用呈现得可信、可点。

你们的 citation 是悬浮卡还是脚注式?流式切包的坑有没有人也踩过?评论区对个答案。

相关推荐
李昊哲小课14 小时前
fastapi sse websocket 奶茶店实时订单看板
人工智能·python·websocket·网络协议·fastapi·sse
万邦科技Lafite14 小时前
天猫商品评论API:评价内容中的用户情感倾向分析
人工智能·api·电商开放平台·淘宝开放平台·api开放接口
遇码14 小时前
认识 LakeMind:一款本地优先的开源 AI 数据探索工作台
人工智能·开源
格林威14 小时前
多相机微秒级对齐:硬件触发 vs PTP(IEEE 1588)方案实战对比
开发语言·人工智能·数码相机·机器学习·计算机视觉·视觉检测·机器视觉
三江番长 陀舍古帝15 小时前
AI 相关概念之(基础层级):AI、ANI、AGI、ASI
人工智能·agi
加速财经15 小时前
PocketBay探索AI应用部署新方式
人工智能
IT_陈寒15 小时前
Redis缓存击穿把我坑惨了,原来这样设过期时间才靠谱
前端·人工智能·后端
AKAMAI16 小时前
优化AI推理:针对AI工作负载的实时Node Balancers指标
人工智能·云计算
aiblog16 小时前
深度学习中“Transformer”怎么翻译为中文?
人工智能·深度学习·transformer
董员外16 小时前
RAG 系统进化论(八):Agentic RAG(智能体式 RAG),从回答问题到完成任务
人工智能·后端·设计模式