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 是悬浮卡还是脚注式?流式切包的坑有没有人也踩过?评论区对个答案。