跳到主要内容

178 篇博文 含有标签「rag」

查看所有标签

被切分边界拦腰截断的关键句,以及随之消失的答案

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的 RAG 流水线将文档切分为 512 个 token 的片段,并带有 50 个 token 的重叠。这是一个标准的行业默认设置。在你的语料库中,有这样一句话——“除非订单来自欧盟地区(在这种情况下监管窗口为 14 天),否则退款将在 5 个工作日内处理”——它恰好跨越了分块边界。分块 N 包含前半部分。分块 N+1 包含后半部分。

用户提问“欧盟退款需要多长时间”。检索系统给分块 N 打分最高,因为查询嵌入与第一段碎片中的“欧盟地区”对齐。而包含唯一实际答案的分块 N+1 排名太低,无法同时被检索到。智能体回答“5 个工作日”,并自信地引用了分块 N。客户人在法兰克福。答案是错误的。流水线完全按照设计运行。

这种故障模式不会出现在你的分块质量评估中。分块是格式良好的。语料库是格式良好的。嵌入模型是格式良好的。分块之间的边界——你在自己文档中划下的那些线——才是答案所在。

悄然渗入你提示词中的评估集

· 阅读需 10 分钟
Tian Pan
Software Engineer

基准测试指标连续四个季度上升。用户满意度却没有。团队中没有人能解释这种差距,直到有人对提示词模板进行了 diff,并注意到 Few-shot 示例正从评估器读取的同一个 CSV 文件中获取。评估集已悄然变成了上下文示例。这个指标不再衡量泛化能力。它衡量的是模型在刚被告知答案的情况下,复制与之最接近问题的能力。

这就是我想命名的失效模式:评估集到提示词的泄露 (eval-to-prompt leakage)。它在结构上与传统机器学习中的测试集污染 (test-set contamination) 完全一致,但它是通过团队刻意构建的后端通道发生的。Few-shot 检索是一个合理的工程举措。评估库是一个合理的工程产物。当两者在没有人划定界限的情况下汇集到同一个存储层时,污染就产生了。

当你的 RAG 流读取时发生的 Wiki 中途编辑问题

· 阅读需 13 分钟
Tian Pan
Software Engineer

你平台团队的一名技术文档工程师正在移动一个段落。这并非比喻——她正真实地从入职指引页剪切一个章节,粘贴到运维手册中,删除第三页上的一个草稿占位符,并修改第四页上的一个弃用警告。整个编辑过程大约花费了她 11 分钟。而你的 RAG 摄取任务每 15 分钟运行一次。恰好在第 6 分钟时,任务启动了。

在接下来的 15 分钟里,你的检索索引包含了一个在她的脑海中从未在任何单一时刻存在过的 Wiki 状态。入职指引页仍然保留着那个章节。运维手册里却还没有。那个草稿占位符在被删除到一半时被捕获了,里面包含了一句她从未打算发布的占位语句。旧的弃用警告仍然被索引着。当一名工程师询问智能体“我们如何在这个服务中处理凭证轮换”时,模型从同一个来源检索到了矛盾的分块,并自信地合成出评分较高的那一个。答案呈现出一种任何人都没写过的错误形态。

这是大多数团队在发布时都没有注意到的失效模式:单一事实来源是事务性的,摄取是轮询的,而两者之间的鸿沟就是“脏读”存在的地方。

被你的 RAG 当成工程规范引用的那张营销页

· 阅读需 10 分钟
Tian Pan
Software Engineer

一位支持工程师把客户工单粘进你内部的 AI 助手。问题很尖锐:"我们的 API 在免费层支持多区域写入吗?"助手秒回,引用了一个余弦相似度 0.91 的片段。答案是肯定的。这个片段来自 2023 年市场部为打赢竞品对比写的落地页。十八个月前,工程团队就把免费层的多区域写入功能下掉了,并发了一份没人在客户页面上链接过的、措辞简短的内部 RFC。这份 RFC 也在向量库里,只拿到了 0.74。

助手并没有幻觉。它检索到了得分最高的文档,然后忠实地把答案锚定在那段文本上。检索器尽到了职责。只是,那份职责本身就是错的。

你的 Embedding 并不知晓外包人员已离职

· 阅读需 11 分钟
Tian Pan
Software Engineer

一名外包人员在上个季度结束了为期六个月的聘期。人力资源部门执行了离职清单:禁用 SSO、擦除笔记本电脑数据、移除 GitHub 席位、归档 Slack、撤销 Notion 访问权限。合规部门签字确认。六周后,一个内部 RAG 助手在回答问题时引用了该外包人员编写的一份机密战略文档——而引用的数据块在向量数据库的白名单中仍标记着该外包人员的用户 ID。事实来源(source-of-truth)的访问日志中没有任何读取记录,因为根本没有发生读取。检索来自一份从未被纳入离职流程的数据副本。

这是没人会画在架构图上的结构性问题。你的向量索引不仅仅是一个相似度搜索引擎。它是一个权限缓存——一个关于“谁能看到什么”的派生存储,冻结在你运行嵌入任务的那一刻——而且几乎没有人像失效其他内容那样去失效它。

那个把上周 Slack 消息当成昨天消息来读的智能体

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的运营 Agent 通过引用一条内容为 “我们明天发版” 的 Slack 消息,回答了一个关于即将到来的发布会的问题。Agent 将其视为当前的计划,并开始撰写沟通稿。然而,这条消息是六周前发布的。发版早就完成了。检索流水线(retrieval pipeline)根据你衡量的每一项指标——与 “发布日期” 的语义相似度、高于阈值的 top-1 置信度、与项目匹配的来源频道——抓取到了正确的文本块(chunk),而 Agent 基于一句仅在编写时的会议语境下才有意义的话制定了计划。

这里的 Bug 不在模型本身。Bug 在于,“明天” 并不是一个日期。它是一个指向时钟的指针,而该消息编写时的时钟并不是 Agent 阅读时的时钟。你的检索流水线索引了消息的正文,却丢弃了其框架(frame)。

语义过时的 Embedding:当向量不再理解当下

· 阅读需 10 分钟
Tian Pan
Software Engineer

你曾在十八个月前嵌入了知识库。模型没变。分块(chunks)没变。索引很健康,延迟也正常,召回率仪表盘是一条 0.86 的水平线。然而,客服团队正悄无声息地在工单回复中粘贴错误的文章链接,销售机器人在潜在客户询问新产品时不断翻出已弃用的 SKU,而一名内部用户刚告诉你助手“感觉变笨了”,却说不出具体原因。

一切都没坏。是你的嵌入(embeddings)老了。在你的领域中,“post”一词以前指的是博客文章;现在,语料库中有一半的地方用它指代 Slack 帖子、论坛帖子和职位发布(job posting),而你那十八个月前的向量仍将其视为同一个概念。编码这些向量的模型从未见过这些新含义,从未见过新的产品名称,从未见过品牌重塑,也从未见过引入了三个新术语的监管规定——而你的客户现在正不假思索地使用这些术语。检索系统回答了它知道如何回答的问题,但这已不再是你的用户正在提出的问题。

Token 预算是产品决策,而非配置值

· 阅读需 12 分钟
Tian Pan
Software Engineer

在你的代码库某处,有一行代码看起来像 retriever.search(query, top_k=8)。某位工程师在某个下午写下了那个 8。它从未被团队以外的任何人评审过,从未出现在规范文档中,也从未被重新审视。这一个整数决定了你的上下文窗口中有多少配额分配给了检索到的文档而非对话历史,决定了每次请求的成本,决定了响应速度的感官体验,并且——由于语言模型在长文本下的实际表现——还决定了答案的准确性。

这是一个产品决策。它却被硬编码在一个 f-string 里。

当 RAG 本该是一次 JOIN 时

· 阅读需 10 分钟
Tian Pan
Software Engineer

一个支持团队向他们的新 AI 助手提了一个简单的问题:“上周有哪些企业客户提交了工单?”助手给出了一个自信、流利的回答,列出了六个账户。其中五个是正确的。有一个客户在两个月前就已经流失了,而另一个提交了三个工单的企业账户却完全遗漏了。直到一次续约会议谈崩了,才有人发现这个问题。

错误不在于模型,而在于架构。在设计评审的某个环节,一个带有硬性谓词(如方案层级、日期范围、工单计数)的问题被路由到了向量索引。团队因为拥有检索系统,所以就选择了检索。他们将工单记录向量化,将问题向量化,然后试图让余弦相似度(cosine similarity)来完成 WHERE 子句的工作。它做不到,也从来都做不到。

这是生产环境 AI 系统中最常见且讨论最少的故障模式之一:当真正的查询是关系型时,却动用了语义搜索。数据明明整齐地存储在带有外键的行中,答案本可以通过一个 JOIN 轻松获得。结果却经过了嵌入模型,导致精度化为乌有。

那些在悄无声息中重新排列你整个语料库的嵌入模型升级

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个新的嵌入模型登上了排行榜。它比你 18 个月前发布的模型得分更高,API 只需更改一行代码,甚至维度也一致。有人提了一个工单:“升级嵌入模型”。这看起来就像更换一个日志库一样简单。

事实并非如此。嵌入模型并不是你检索系统的一个普通组件——它就是你检索系统所处的坐标系。更换它并不会改进你的索引,而是会让索引失效。而最残酷的地方在于,系统并不会崩溃。没有异常,没有失败的健康检查。你的搜索只是开始返回微妙不同的结果,而在 RAG 管道中,“微妙不同”意味着不同的文档被喂给了模型,也就意味着不同的答案最终传达给用户。

自信地返回错误答案的语义缓存

· 阅读需 11 分钟
Tian Pan
Software Engineer

两个支持用户在短短一分钟内向你的智能体提出了几乎相同的问题。第一个用户问:“我们针对 EU 订单的退款期限是多久?”第二个用户问:“我们针对 US 订单的退款期限是多久?”这两个句子的嵌入向量(embeddings)仅有一丝之差——长度相同,结构相同,只有一个两字母 token 的区别。你的语义缓存经过了相似度阈值的微调(在演示中看起来非常合理),将它们判定为匹配。于是,第二个用户得到了第一个用户的答案。EU 的 14 天冷静期被当作事实,以流利的文字呈现给了一位 US 客户,且没有任何补充说明。

没有人会因此收到报警。缓存返回了 200。延迟表现优异。成本看板显示了一次命中,这正是每个人想要的结果。唯一能说明出了问题的信号是客户根据一项不适用于他们的政策行事——而这个信号在几天后才会通过退款纠纷传来,而不是通过你的监控系统。

正是这种故障模式,让语义缓存与你之前构建过的任何缓存都截然不同。精确匹配缓存可能会过期,但它永远不会 错误——key 要么匹配,要么不匹配。语义缓存则是有意放弃了这种保证。它被设计为能针对从未见过的 key 返回答案,而这种延迟优势的代价是正确性风险,大多数团队从未对这一风险进行过量化。

你的向量索引是一个没有失效策略的缓存

· 阅读需 11 分钟
Tian Pan
Software Engineer

向量索引感觉就像一个数据库。你向其中写入文档,查询它,它返回结果。但它并不是数据库——它是存储在其他地方的数据的“派生、非规范化副本”。你的事实来源是 wiki、工单系统、CRM 或 PDF 文件夹。嵌入 (embeddings) 是这些事实的投影,冻结在你运行摄取任务 (ingestion job) 的那一刻。

这使得你的向量索引变成了一个缓存。就像所有缓存一样,它会失效。不同之处在于,大多数团队是有意识地构建缓存层,带有 TTL 和失效钩子 (invalidation hook),而几乎没有人会有意识地将向量索引作为缓存来构建。他们将其构建为“知识库”,然后在它提供三周前就已经过时的知识时感到惊讶。