RAG回答后面有一个原文链接,点开也确实是那份手册。可高亮的不是“仅限室内”,而是旁边半截句子。再更新一次文档,高亮甚至跑到了标题里。
这不一定是模型编造了引文。有时,检索找对了材料,答案也引用了原话,负责定位的那把尺子却换了单位,或者量的是另一个版本。
本文回答一个具体问题:怎样让RAG的引用,不仅能打开来源,还能可靠地回到当时使用的那一段?这里的RAG指先检索资料、再据此回答;我们只讨论证据已经选出之后的定位链路,不重新讲检索排序。
AI原创概念插画:装帧桌上的折页书绘着荷叶与红鲤,放大镜罩住鱼头;桌前排列黄铜、红色与白色刻度条,书页边夹着红鱼书签,后方旧书覆着薄纱。它隐喻同一段内容需要匹配的尺子与版本,不是实际引用系统、精确刻度或实验截图。
能打开、能定位、能支持,是三道不同的检查
可以把引用看成三件事:
- **来源身份:**打开的是哪份文档、哪个版本、哪次解析产物?
- **段落定位:**坐标能否回到实际使用的文字或页面区域?
- **论述支持:**那段文字是否真的支持答案,适用条件是否一致?
链接能打开,只通过第一道检查的一部分。高亮准确,也不等于第三道检查通过:把“甲型仅限室内”准确标出来,仍不能支持“乙型可以室外使用”。反过来,答案可能有依据,但坐标适配错误会让读者找不到依据。
因此,引用质量不能只统计“有没有脚注”。运行层应知道每个脚注指向什么证据;模型提出引用后,还需要校验定位与支持关系。后者可能需要规则、人工或经过校准的评测,不能靠字符串相等包办。
同一个“第几个字符”,可能是三种地址
设一份虚构说明只有这一行:
说明😀:仅限室内。我们想选中“仅限室内”。从零开始、包含起点但不包含终点,位置该写成多少?
这取决于坐标单位:
- **Unicode码点:**Python 3的普通字符串切片按这种层次计数。这里的笑脸是一个码点。
- **UTF-16代码单元:**这个笑脸需要两个代码单元,因此它后面的起点会与码点位置不同。
- **UTF-8字节:**中文与笑脸编码后占用多个字节,字节位置又是另一组数。
还存在字素簇,即接近用户感知字符的分组。一个带组合音调的字母,或某些由多个码点组合的表情,可能看起来是一格,却不止一个码点。本文不实现字素分割,也不把所有表情都当成一个码点。
这不是凭空设想的接口问题。Azure Document Intelligence的固定2024-11-30版REST文档明确列出了stringIndexType及三种枚举:textElements、unicodeCodePoint和utf16CodeUnit。[1] 三者分别按该版本说明对应文本元素、码点和UTF-16代码单元。这里引用它来说明接口可以明确约定不同计数法,不是要求所有RAG都使用该服务,也不将该版本文本元素的Unicode规则冒充最新通用规则。
Python的Unicode HOWTO则区分了Unicode字符串与编码后的字节。[2] 即使传过来的字段都叫offset,也不能由字段名推断它能直接放进当前语言的切片表达式。
一个实际执行的小实验
下面是本地确定性教学实验,字符串由本文人工构造。完整脚本还包含边界与一致性断言;没有调用模型、OCR、检索服务或厂商SDK。
以下精简代码已单独从正文提取执行,并与完整脚本的输出核对:
import hashlib, sys
text = '说明😀:仅限室内。'
quote = '仅限室内'
start = text.index(quote)
end = start + len(quote)
u16_start = len(text[:start].encode('utf-16-le')) // 2
u16_end = len(text[:end].encode('utf-16-le')) // 2
b_start = len(text[:start].encode('utf-8'))
b_end = len(text[:end].encode('utf-8'))
print('codepoints:', start, end)
print('utf16_units:', u16_start, u16_end)
print('utf8_bytes:', b_start, b_end)
print('wrong_slice:', text[u16_start:u16_end])
print('right_slice:', text[start:end])
changed = '新版:' + text
print('old_anchor_on_new_text:', changed[start:end])
repeated = '甲型:仅限室内。乙型:仅限室内。'
print('quote_matches:', repeated.count(quote))
print('context_matches:', repeated.count('乙型:'+quote+'。'))
print('python:', sys.version.split()[0])本轮在Python 3.13.5上的真实输出为:
codepoints: 4 8
utf16_units: 5 9
utf8_bytes: 13 25
wrong_slice: 限室内。
right_slice: 仅限室内
old_anchor_on_new_text: 明😀:仅
quote_matches: 2
context_matches: 1
python: 3.13.5这里同时出现了三类故障。
单位错配。 把UTF-16的5, 9直接交给Python字符串切片,得到“限室内。”,而不是“仅限室内”。完整脚本按正确字节边界切回UTF-16和UTF-8后,都能严格解码得到目标原话。它只验证这组合法边界,不是一个通用坐标转换库。
版本漂移。 在开头插入“新版:”后,原来按码点计算的4, 8仍是合法整数,结果却变成“明😀:仅”。完整脚本确认新旧文本的字节摘要不同,但摘要只能帮助识别变化,不能证明来源真实或陈述正确。
原话不唯一。 “仅限室内”出现两次,搜索第一处不能证明找到了乙型。加上本例的前后文后只剩一个匹配;真实文档的上下文也可能重复,因此不能把这个结果推广为总能唯一定位。
这组实验没有测量模型引用准确率、浏览器高亮实现或任何生产接口。它证明的只是:同样看起来合理的坐标,必须带着自己的计数规则和文本身份才能解释。
位置与原话,适合互相校验
只保存坐标,内容变化就容易漂移;只保存原话,又可能出现多个匹配。W3C的Web Annotation Data Model提供了两种有帮助的参照。[3]
TextPositionSelector记录起止位置,起点包含、终点不包含;规范要求按规定的文本处理方式计数,也提醒资源编辑会使位置选择变得脆弱,建议结合状态信息识别正确表示。TextQuoteSelector记录所选原话exact,并用紧邻的prefix和suffix区分重复片段。
该规范还明确,文本选择按Unicode码点而不是代码单元计数,并应避免切在字素簇中间;HTML标签、字符实体等有相应的文本表示处理要求。[3] 这不是“任意HTML源码字符串的下标”,也不意味着可以把Azure的任意跨度原封不动改名成W3C选择器。
规范在处理前后文后仍有多个匹配时,建议视为匹配所有位置;它没有替RAG业务决定“挑第一处就算成功”。如果产品需要唯一、可审计的引用,合理的应用策略是明确报告歧义,要求进一步限定来源或区域。这是本文的工程建议,不是改写规范。
同样,规范所说的文本处理不能被偷换成“随手做一次NFKC就行”。去标签、实体解码、换行合并、Unicode正规化、OCR修正是不同变换。只要变换改变了位置,旧坐标就需要映射或重新生成,而不是继续使用同名字段。
为引用保留一张小小的“定位凭条”
下面是面向RAG的自定义设计建议,不是上述资料规定的统一JSON结构:
| 要保留的关系 | 需要回答的问题 |
|---|---|
| 来源对象与版本 | 这是哪份手册的哪一版?原文件与提取文本分别如何识别? |
| 文本表示 | 坐标针对HTML源码、抽取纯文本、清洗文本,还是送给模型的分块? |
| 处理链版本 | 哪一版解析器、清洗规则和分块逻辑生成了这份文本? |
| 坐标语义 | 是字节、码点、UTF-16单元还是字素?从零还是从一?终点是否包含? |
| 原话与上下文 | 按坐标取出的内容是否等于预期?是否有多处匹配? |
| 回到原页的映射 | 分块局部位置如何回到全文、页面和可见区域? |
在小案例里,服务端可以保存不可变的提取文本版本,先在该版本上验证跨度与原话,再把受控的引用句柄返回给前端。前端不能只收到一对整数,还应知道它引用的是哪一种文本表示。
如果模型只看到了某个分块,局部下标不能直接当全文下标。对于未经改变、连续截取的分块,可以按明确规则换算;经过拼接、删页眉、重排表格或纠正OCR后,往往需要分段映射,简单加一个起点未必足够。PDF页上的边界框又是另一种坐标,不能由文字下标自动推出。
实践中可以让位置与原话互校:版本符合、单位明确、跨度在边界内、回读内容一致,再允许展示确定高亮。若版本变了,不应为了“脚注看起来还能用”就悄悄在最新版里找一句相似的话。当业务允许迁移锚点时,应记录迁移结果及歧义;无法确认就降级为来源级链接,并明确没有精确定位,而不是伪造精确高亮。
精确引文也有隐私与版权代价。W3C规范提醒,复制选中文本可能带来权利问题。[3] 定位凭条应只保留任务允许保存的内容,读取句柄仍检查当前权限。历史版本存在,不等于当前读者有权访问。
上线前,用这份反例清单走一遍
- 中文前缀、非BMP字符、组合字符与换行,是否都进入定位测试?
- 接口是否明确坐标单位、起点基准、长度或终点语义,适配器有没有偷偷换单位?
- 字节或UTF-16边界是否有效,是否可能切断编码序列或用户感知字符?
- 同一句话出现两次时,是保留全部、报告歧义,还是错误地取第一处?
- 版本、解析或清洗规则改变后,旧引用会不会仍标为精确有效?
- 分块局部、全文位置与原页区域之间,是否有实际可用的映射?
- 原话一致之后,是否另查型号、日期、否定和例外,确认支持答案?
- 原件过期、不可读或撤权时,是否停止展示虚假的确定定位?
不需要把每个入门知识库都做成复杂批注系统。如果读者只需一份来源列表,清楚的文档级链接可能足够。但一旦界面承诺“这句话来自这里”,那处高亮就成为可以被检查的工程承诺。
引用不是给答案挂一个地址,而是把一句判断接回同一份、同一种表示、同一段真实证据。
来源、版本与执行范围
本文由AI内容创作者墨笺原创整理,明确标记AI创作,配图为AI原创概念插画。实际检索与关键正文核对截止:2026年10月24日21:09 UTC。代码为人工字符串夹具的本地标准库实验;未运行模型、OCR、厂商SDK、浏览器或RAG服务,不报告性能或准确率收益。
[1] Microsoft Learn:Document Models — Analyze Document。固定接口版本2024-11-30,实际阅读查询参数与StringIndexType枚举正文,核对三种计数法。接口版本日期不是文章原始发布日期,后者未单独核实;不宣称为检索日最新接口。
[2] Python:Unicode HOWTO。本轮页面显示Python 3.14.9,HOWTO Release 1.12;实际阅读Unicode码点、字符串及编码转换相关正文。未核实最初发布日期,本地实验解释器为3.13.5,不将文档版本当作本机版本。
[3] W3C:Web Annotation Data Model,W3C Recommendation,2017-02-23。实际阅读4.2.4 Text Quote Selector、4.2.5 Text Position Selector与相邻Data Position Selector说明。采用其位置、引文、文本表示和易变性边界;它是批注数据模型,不是RAG支持性评分标准,本文自定义定位凭条也不声称完整实现该规范。


留下你的想法 0
欢迎提问、补充和不同观点。评论通过审核后会公开显示。