作者复盘真实文档场景下 RAG 的三类故障:语义缓存返回过期答案、纯向量检索漏掉精确关键词,以及脏数据摄入。改进方案包括基于评测集调缓存阈值、混合稠密与稀疏检索,以及事件驱动管线。
大多数 GitHub 上的 RAG 演示都在做同一件事:对一些文本块进行 embedding,用余弦相似度搜索,把 top-k 结果塞进 prompt,完成。我一开始也做了一个这样的版本。但当我真正尝试把它用在混乱的真实文档上时,它在三个具体方面彻底崩溃了。于是,我把它从线性脚本重构成了事件驱动的 pipeline,最终诞生了 Project Aether。
Semantic cache 返回了过期的垃圾内容。 按照完全一致的查询字符串缓存 LLM 响应毫无意义,因为没有人会把同一个问题一模一样地输入两遍。我需要基于相似度的缓存机制——这条新查询和某条已缓存查询是否“足够接近”?但这又引出了一个更棘手的问题:相似度达到什么阈值才算“足够接近”,同时又不会因为问题之间存在细微差异,而返回一个错误但看起来很合理的缓存答案?最后,我用一个小型 eval 数据集来调优这个阈值,而不是拍脑袋猜一个余弦相似度阈值,然后祈祷它能正常工作。
朴素的向量搜索会漏掉需要精确匹配的词。 纯 dense embedding 不擅长处理精确关键词、零件编号、错误代码这类内容。有人搜索某个具体的 SKU,纯 dense 搜索却返回了五个语义相似但实际错误的产品。我用 dense+sparse 混合检索取代了纯 dense 检索,从而解决了这个问题。
摄取真实文档,意味着某个人的 PII 最终会进入你的向量数据库。 这是 RAG 教程里几乎没人讨论的问题。如果你正在对真实的客服工单或合同进行分块和 embedding,那么其中包含的任何 PII 也会一起被 embedding,并永久存入一个你可能无法完全控制访问权限的向量存储中。因此,摄取流程必须先执行一次 PII 脱敏,再进行任何分块处理,而不是等分块之后才处理。
Project Aether 是一个事件驱动的 RAG 搜索引擎:
使用 FastAPI 构建 API 层,全链路异步。
使用 LlamaIndex Workflows,把摄取和查询建模为真正的事件图,而不是“先调用这个、再调用那个”的脚本。这样一来,重试和局部失败就容易理解和处理得多。
使用 Redis 实现 semantic caching,也就是上面问题 1 对应的部分。
使用 Chroma Cloud 实现 dense+sparse 混合向量搜索,并采用服务端 Qwen/Splade embedding,这是对问题 2 的修复。
在摄取侧使用 RQ 处理后台任务。
使用 Groq 运行 Llama 3.3 70B 完成生成,主要是因为我希望获得快速推理能力,同时又不想为空闲的 GPU 时间付费。
摄取 pipeline 会在分块之前对 PII 进行脱敏,并使用 LLM 生成的元数据来丰富文本块。查询侧则会先进行查询转换,再通过相关性判断 refinement loop 反复优化结果,最后才进入生成阶段。此外,还有一个可选的 cross-encoder 重排序阶段(BAAI/bge-reranker-v2-m3)。默认情况下我会将它关闭,因为对于内存受限的托管环境来说,它实在太重了。
在线演示运行在 Render 的免费套餐上,因此只支持查询——摄取以本地任务的形式运行,并未对外开放——而且第一次请求时的冷启动一定会很慢。我不打算假装事实并非如此,免费托管就是这样。如果你访问演示,发现第一次响应需要等待一段时间,那是容器正在唤醒,并不是 RAG pipeline 本身很慢。
这是一个个人项目,没有团队。因此,如果某些地方看起来只完成了一半,那是因为我只能根据自己的时间限制来构建它,而不是因为我没有更多想法了。
Repo 在这里:https://github.com/gabaoun/Project-Aether
说实话,比起一个没有任何反馈的 star,我更希望收到“这个检索方案是错的,因为 X”这样的评论。不过,如果你觉得它有用,点一个 star 也能帮助更多人发现它。我很好奇,是否还有其他人也曾为 semantic-cache 的阈值调优苦苦挣扎。感觉这是目前 RAG 领域最少被讨论的难题之一。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。