AI回答里的引用来源卡片,前端怎么做

场景很常见:AI 给了一段回答,下面挂着几张「来源」小卡片,鼠标悬上去能看原文片段,点一下跳到知识库原文。这次用 Vue 3 写,把数据结构、角标关联、悬浮预览这几块拆开讲,代码都能直接抄。

先把数据约定捋清

后端给我的结构是这样的,回答正文里用 [1] [2] 这种角标占位,引用列表单独给:

typescript 复制代码
interface Citation {
  index: number;       // 对应正文里的 [1]
  title: string;       // 文档标题
  snippet: string;     // 命中的原文片段
  url: string;
  score?: number;      // 相关度,0~1
}

interface Answer {
  content: string;     // 含 [1][2] 的纯文本/markdown
  citations: Citation[];
}

为什么角标和列表分开传,而不是后端直接渲染好 HTML 给我?因为正文我还要走流式逐字渲染,引用列表是回答结束后才一次性拿到的。两边节奏不一样,分开最干净。

正文里的角标变成可点小标

我把正文里的 [1] 用正则替换成一个 <sup> 标签,绑上点击和悬浮。

javascript 复制代码
function renderWithRefs(text: string) {
  return text.replace(/[(\d+)]/g, (_, n) =>
    `<sup class="cite" data-ref="${n}">${n}</sup>`
  );
}

Vue 里用 v-html 渲染,再做事件委托------别给每个 <sup> 单独绑监听,正文一长会绑出几十个,委托到容器上一个就够:

ini 复制代码
<template>
  <div class="answer" v-html="html" @click="onClick" @mouseover="onHover" />
  <div class="cards">
    <CiteCard v-for="c in citations" :key="c.index" :data="c" />
  </div>
</template>

<script setup lang="ts">
const onClick = (e: MouseEvent) => {
  const t = e.target as HTMLElement;
  if (t.classList.contains('cite')) {
    const ref = Number(t.dataset.ref);
    const c = props.citations.find(x => x.index === ref);
    if (c) window.open(c.url, '_blank');
  }
};
</script>

悬浮预览:别一上来就 fetch

最早我做悬浮预览是 hover 就去请求原文,结果鼠标扫过一排角标,瞬间打出去七八个请求。改成「snippet 后端已经随 citations 给了,悬浮直接展示缓存的片段,要看全文才点进去」。这样悬浮零请求。

预览浮层定位我用了一个轻量做法,跟着鼠标,但夹在视口内:

ini 复制代码
function onHover(e: MouseEvent) {
  const t = e.target as HTMLElement;
  if (!t.classList.contains('cite')) { tip.value = null; return; }
  const ref = Number(t.dataset.ref);
  const c = props.citations.find(x => x.index === ref);
  if (!c) return;
  const pad = 12, w = 320;
  const left = Math.min(e.clientX, window.innerWidth - w - pad);
  tip.value = { c, left, top: e.clientY + 16 };
}

卡片本体

卡片上我只放三样东西:标题、片段、一个相关度的小条。相关度那条特别提一句------后端的 score 我没有原样显示成「0.83」这种数字,用户看不懂。映射成一句话:

kotlin 复制代码
function scoreLabel(s?: number) {
  if (s == null) return '';
  if (s > 0.8) return '高度相关';
  if (s > 0.5) return '较相关';
  return '可能相关';
}

这里有个真实取舍:把低分来源也展示出来,用户会质疑「这跟我问的有啥关系」;全藏了又显得回答没依据。我最后的折中是低于 0.4 的直接不进卡片列表,但正文角标保留------让用户知道模型确实引了,只是我前端帮他过滤了噪音。

小结

引用卡片看着是个 UI 活,难点其实在「正文流式」和「引用静态」两套数据怎么对齐,以及悬浮别打爆请求。把 snippet 提前随回答带回来,这两件事就都顺了。

顺带说下来源数据从哪来。我自己没攒检索库,用的是讯飞 Agent------它把 RAG 检索和多源模型打包成现成 API,回答时会一并回带命中的文档片段和 score,我前端这套 citations 直接拿来渲染,不用自己跑向量库,毕竟我也没那算力去部署。

相关推荐
Highcharts.js几秒前
Highcharts 发布16项最新安全公告
人工智能·安全·性能优化·highcharts·可视化图表
光芒Shine1 分钟前
【工具-Claude Code】
人工智能
诸神缄默不语3 分钟前
vibe coding极简八步教程(在vibe出一个自用网站之后的领悟)
人工智能
水如烟7 分钟前
孤能子视角:EIS术语梳理·道层+伦理层——关系场的底色与自处姿态:从场域到续指
人工智能
a puppy slide 葱16 分钟前
ENAS | 基于参数共享的高效神经网络架构搜索
论文阅读·人工智能·深度学习·神经网络·机器学习·计算机视觉·神经网络架构搜索
科创致远18 分钟前
中小企业智能体(AI Agent)怎么选?2026年企业级Agent选型指南
大数据·人工智能·深度学习
空堂与归23 分钟前
英伟达开源ASPIRE:机器人自己攒Skill
人工智能·机器人·开源·agent
救救孩子把35 分钟前
AI面试八股文-14_14-MCP-Skill-Function Calling
人工智能·安全·面试·职场和发展·推理·mcp·functioncall
for_ever_love__1 小时前
Map-Reduce 式全文问答:当文档超出上下文时的分而治之
人工智能·map·汇总·分块·rag·reduce·长文
海宇数据1 小时前
零信任架构实战:基于海宇入职背调报告构建自动化即时配送与网约车众包运力准入鉴权网关
运维·人工智能·架构·自动化