Groundline:一个自己计算省下多少钱的 RAG 服务

用 FastAPI 和 LangGraph 做的个人项目:混合检索、重排序、自检,以及覆盖 60 到 65 个百分点重复提问的语义缓存。省下的钱直接显示在界面上。

Groundline:会计算自身成本与节省额的文档检索
把每个答案的价格摆出来的 RAG

最近几天我做了一个名叫 Groundline 的个人项目,想聊聊为什么把它做成这个样子,以及它解决了哪些问题。

为什么还要再做一个 RAG

如今搭一个基础的文档检索并不难,教程成千上万。但大多数练手和演示项目停在「太好了,它总算能回答」这一步。真实业务关心的是另一个问题:运行它要花多少钱。

每一次调用语言模型都在花真金白银和等待时间。如果用户反复用不同措辞问同一件事,而系统每次都重新跑完整的向量检索并从零生成答案,预算就消耗在重复已经做过的工作上。

所以我决定做一个服务,它不只是解决文档问答,还要把省下的钱用诚实的数字直接摆出来。

用户看到的样子

上传 PDF、TXT 或 Markdown 文档然后提问。得到的是一个清晰的答案,并附带精确的来源指引:文件、具体片段和页码。模型被严格禁止凭空发挥,只能使用文本中真正检索到的内容。

Groundline 的文档页面,带有文件上传区域
上传文档:PDF、TXT 和 Markdown

揭开盖子:一个答案的七个步骤

问题到来时,系统不会直接冲向模型,而是老老实实走完整条链路。

  1. 语义缓存。先检查是否已经回答过类似的问题。如果有,就立刻返回答案,一个 token 也不花。
  2. 改写。缓存没命中时,模型清理问题中的冗余,把缩写展开成规整的检索语句。
  3. 混合检索。按语义的向量检索和按精确词的全文检索并行启动。前者很擅长捕捉同义词,后者不会丢掉料号、术语和编码。
  4. 重排序。一个专门的模型给检索到的片段打分,并不按形式上的相似度排列,而是按片段究竟在多大程度上回答了问题。
  5. 自检。模型判断上下文是否足够。如果不够,就带着「究竟缺什么」的说明再检索一次,最多两次额外尝试。
  6. 生成。答案逐词流式传给用户。
  7. 保存与缓存。答案写入历史,并送进缓存,供将来类似的问题使用。

整件事的起点:缓存

缓存的相似度阈值不是拍脑袋定的。每一次请求都会记录它与最接近的已存问题之间的相似度,即使缓存没有命中也照样记录。我依据这些指标把阈值调到既能抓住真正的换一种说法,又不会把不同的问题混成一堆。

在我这份包含重复问题的测试数据上,缓存覆盖了 60 到 65 个百分点的请求。在真正独一无二的用户场景里这个数字当然会更低,但经济收益依然实实在在。

而且省下的部分在运行过程中就能看到:

  • 当前正在执行流水线的哪一步,它消耗了多少毫秒或多少 token;
  • 一张实时图表,把模型花费和缓存省下的钱放在一起对比。
Groundline 的聊天界面,显示答案、展开的来源和流水线面板
流水线面板展示每一步的耗时

踩过的坑

  1. 处理器争抢。负责嵌入和重排序的本地模型与新文件的后台索引在抢 CPU。峰值时,一个普通请求不是几百毫秒,而是挂了 83 秒。我用严格的串行化解决了它:后台索引和问题处理被彻底分开,各自排队访问模型。
  2. 数据库连接泄漏。如果用户正好在生成答案时关掉标签页,连接就会卡死在连接池里。在连接数上限小得可怜的免费主机上,这很快会让服务瘫痪。修复办法是让写入数据库的最后一步不受取消影响。

技术栈

  • 后端:FastAPI、LangGraph、Python。
  • 数据库:带 pgvector 扩展的 Postgres 负责向量检索,SQLAlchemy 和 Alembic 负责迁移。
  • 安全:用户之间的数据隔离不只依赖代码,还依赖数据库里的行级安全策略。查询里漏写一个过滤条件也不会泄露别人的文件。
  • 模型与监控:Groq 提供快速回答,LangFuse 追踪每一步,ragas 库评估质量,包括准确率、上下文召回和有无幻觉。
Groundline 展开的额度面板,可见多个配额
配额摆在用户眼前,而不是藏在报错里

这样一条链路长什么样

同样的七个步骤,看一遍比读一遍更容易记住。一张可交互的图会把一次查询带过整条链路,并显示每个阶段发生了什么变化:

模型如何依据你的文档作答

阅读更多