用户问客服:“这件商品能退吗,运费谁承担?”Agent 很快给出一段回答,还在末尾放上了几个引用编号。看起来有出处,但点击以后,可能只是打开了整份文档,甚至指向与结论无关的条款。
带引用的问答,需要能回答两个具体问题:这句话根据哪一段原文,以及那一段是否真的在本轮查询中被取回。一个漂亮的来源列表,不能自动证明回答有依据。
这一篇用 Vue 3 和 TypeScript 做一个最小客服政策助手:调用只读检索工具,取得政策片段;固定模拟模型选择片段;校验通过后展示原文式回答与引用入口。 没有查到依据时,就明确停在“缺少依据”。
所有政策均为专门编写的教学示例,不对应真实商家或法定规则。模型响应用固定模拟,没有调用真实大模型、向量数据库或客服系统。演示可完全离线运行,重点验证引用链路与页面交互,不宣称已经完成真实模型的政策问答评测。

先限定这个最小 Agent 的工作范围
本例只处理三类演示政策:普通商品退货、运费说明和定制商品。用户提交问题后,流程固定为一次检索、一次模拟选择、一次引用校验,不会连续调用未知工具,也不会发起退款或创建售后申请。
检索使用受控关键词匹配,最多返回三个片段,便于重现结果。它没有理解所有自然语言表达,也没有实现向量相似度、重排或多轮追问。因此没有命中时,应说“本次未找到可用依据”,不能把检索失败写成“商城没有这项规定”。
RAG 的基本思路是先检索相关内容,再把内容交给模型作为回答依据。官方介绍也将检索、补充上下文和生成分成不同步骤,并指出来源元数据有助于组织引用。Microsoft Learn:RAG 与索引
本例只实现这条链路的离线、抽取式演示。固定模拟模型负责选择返回片段,不代表真实模型已经学会分析政策或判断用户资格。
片段除了文本,还需要稳定身份
不能只给模型一段没有出处的长文本。检索结果需要携带片段 ID、文档 ID、标题、版本、章节与原文。页面的引用编号则根据本次答案重新排列,不把编号当成永久的文档身份。
type CitationSelection = {
chunkId: string;
quote: string;
};
模拟模型只返回这样的选择,不生成来源标题、版本号和链接。文档信息由应用从检索结果取回,避免模型凭空编造一个看起来可信的文件名或 URL。
三份原始政策文件随源码一起提供。引用展开时,读者既能看到对应片段,也能打开完整演示文件,检查上下文。版本只是本例的固定元数据;真实政策更新后,需要重新建立索引与答案失效规则,不能只换页面上的日期。
片段也应保留适用范围和限制条件。如果把“仅限普通商品”与后面的条件切散,再只引用其中一句,原文真实也可能让回答产生歧义。
引用只允许来自本轮检索
最容易写错的校验,是拿引用 ID 去全库里找:只要这个 ID 存在,就允许显示。这样模型即使没有在当前查询中拿到那段政策,也能引用它。
本例把本轮检索结果变成允许集合,逐条核对模拟输出。除了结构与长度,还需要检查:选择数量有界、ID 不重复、ID 属于本轮返回,以及引文与该片段原文完全一致。
引用 ID 是否在本轮检索结果中?
引文是否逐字等于该片段完整原文?
全部选择是否通过校验后,才构建答案?
如果某一条不通过,就拒绝整份模拟答案,不先显示前半段,再把后半段标成失败。页面清理旧答案,展示具体错误与可重试入口,避免用户把上一次结果误认成本次答案。
这比仅验证 JSON 结构多了一步来源核对,但仍然不是通用的事实判断器。合法引用可以被用在不适用的问题上;引用检查通过,只证明来源和摘录对应,不能独立证明检索相关、政策完整或最终推理成立。
先做抽取式回答,再考虑自由改写
本例不接受模型随意生成“保证退款”“一定包邮”等承诺。校验通过后,应用从受控片段取出原文,用固定前缀组织成“演示政策写明”,并附上引用编号。
这样可以确定回答中展示的政策句子就是被引用的文本,不需要先解决任意自然语言改写的语义验证问题。代价是回答更像政策摘录,而不是流畅的人工客服对话。
如果要增加自由生成的总结,应把“引用有效”与“结论有依据”分开评估。例如模型可能引用一段运费条款,却把责任方说反;也可能逐条引用正确,却遗漏适用条件。这些问题不能靠 ID 存在或原文匹配全部排除。
无命中时,本例不会继续让模拟模型补一个答案,而是显示缺少依据。接真实模型时也应让模型能表达不知道、需要补充条件或转人工,而不是把每次提问都强制变成确定结论。
让用户能从答案回到原文
答案旁的引用按钮打开对应来源区域,并把焦点带到该片段。展开区域显示标题、版本、章节与完整原文,而不是只展示一个外部链接。
这里使用原生 details 与 summary 组织折叠内容。它们提供展开交互和键盘入口,适合这个小示例;引用编号与来源区域 ID 由页面生成,避免让模型决定 DOM 目标。MDN:details 展开元素
原文、问题和状态文案均按普通文本渲染。Vue 会转义普通文本内容,但不能把模型输出拼成模板或随意交给 v-html;来源链接同样来自受控文档配置。Vue:安全实践
页面还展示本轮问题快照、检索到的片段和工具执行记录。用户编辑输入框不会暗中改变已经展示的答案;再次提交时创建新一轮任务,旧响应只有仍属于当前轮次才能更新页面。
用错误引用验证失败路径
只测试正常问题能显示两条来源,覆盖不到引用组件最关键的边界。本例提供故障夹具,让固定模拟模型返回错误选择,直接检查校验层是否拒绝。
| 场景 | 应看到的结果 |
|---|---|
| 正常演示政策问题 | 展示原文式答案,引用可展开核对 |
| 本轮没有检索命中 | 提示缺少依据,不生成政策承诺 |
| 引用一个不存在的 ID | 拒绝答案,不补造来源 |
| 引用全库存在但本轮未返回的 ID | 仍然拒绝 |
| ID 正确但引文被改写 | 拒绝,不把篡改文本当原文 |
| 模型返回错误结构 | 拒绝,不渲染部分答案 |
| 新问题已开始,旧任务才返回 | 旧结果不能覆盖当前轮次 |
核心测试除了检查引用 ID,还要检查全文引文、重复选择、数量限制和任务状态。浏览器测试则实际点击引用、操作折叠区,并在手机视口检查长引文与文档信息是否能读完整。
这些测试验证固定夹具和交互,不等于真实模型的回答正确率。接真实模型后,还要分别测检索命中、问题覆盖、引用对应和答案是否被依据支持,避免把一个指标当成全部质量。
运行示例与查看源码
打开带引用的政策问答演示 · 下载 Vue / TypeScript 完整源码
解压后直接打开 index.html 即可运行。Vue、政策数据和固定模拟逻辑都已打包,不需要模型密钥、服务端或 CDN。若要通过 HTTP 预览静态文件,在解压目录运行:
python3 -m http.server 8092 --bind 127.0.0.1
访问 http://127.0.0.1:8092/。这里的 HTTP 仅用于静态预览,提问仍在浏览器里检索和选择演示片段,没有新增一个真实模型接口。
修改源码时执行:
npm ci
npm run typecheck
npm test
npm run build
先运行普通退货、运费和定制问题,再试无依据与错误引用场景。点击答案中的引用编号,对照展开原文与源文件;不要只看答案末尾是否有几个蓝色数字。
本次类型检查、构建和 19 组核心测试通过,测试结果随源码附带。浏览器已核对三个政策主题、无依据拒答、四种错误引用、来源展开与键盘操作;桌面和手机均无整页横向溢出。验证范围是离线示例,没有调用真实模型或线上客服业务。
接真实模型时保留相同边界
真实模型可以替换固定选择适配器,但本轮检索集合、结构校验与来源信息仍应由程序控制。不要为了让答案更流畅,就放开任意来源 URL 或把没有检索到的文档当作本轮依据。
正式知识库还需要文档生效与失效状态、访问权限、适用人群和版本追踪;若用户条件不完整,应先追问。向量检索或重排可以改进召回,却不能自动解决政策冲突和承诺范围。
这个最小案例先把“带引用”变成一条可检查的链:当前问题找到片段,答案使用这些片段,用户能够回到原文。链路缺了一环,就展示缺少依据或校验失败,而不是生成一个有出处外观的确定答案。