Skip to content

Repository files navigation

📚 deep-rag

一个把「答案质量」当作一等公民的 chat-with-docs:混合检索(BM25 + dense → RRF 融合 → 可选 cross-encoder 重排 / HyDE 假设文档路)+ 接地引用(回答标注 [1][2] 并校验标号真实性)+ 内建忠实度评测(claim 级 LLM-as-judge:回答拆成原子论断逐条判定,score = 支撑数/总数,本地可审计),并把同一套管线延伸成「任意代码库 → RAG 知识语料」的生成器(rag-corpus skill)。

后端 FastAPI(SSE 流式)+ Streamlit 对话式前端;LLM/Embedding 走 OpenAI 兼容接口,默认智谱 GLM;无 key 时自动进入 MOCK 模式,零配置可跑。

CI


✨ 核心能力

模块 说明
混合检索 BM25(词法,jieba)+ dense(语义余弦)双路并行,RRF 名次融合(对两路分数量纲不敏感);可选 BGE cross-encoder 精排(RERANK=1,失败自动回退 RRF 原序);可选 HyDE 假设文档第三融合路(HYDE=1,对症词法陷阱,见技术要点)
多轮对话 + 查询改写 跟进问题(「那要等多久?」)先由 LLM 改写成独立问题,与原始问题双查询融合检索——改写救回纯指代题,改写丢主题词时原始路兜底,两路互为保险
接地引用 + 标注校验 回答附来源块;[n] 标号与真实引用集双向校验——越界标号(幻觉引用)与从未被引用的检索块都会被标出
claim 级忠实度评测 每条回答生成后跑 LLM-as-judge:回答分解成原子论断逐条判定,score = 支撑数/总论断数(本地计算、可审计,不采信 LLM 拍的整体分);前端绿/红徽章 + 逐条 ✓/✗;judge 畸形输出宽容归一、不可用降级启发式(MOCK 走句子级近似 claim,公式对称)
检索质量评测 Recall@k / Hit@k / MRR / Precision@k 纯函数实现;评测集故意含转述题与词法陷阱样本,暴露真实短板而非自我安慰
持久化 JSONL(零依赖:临时文件 + fsync + 原子替换,逐行容错坏行/坏字节)或 LanceDB(一个环境变量切换);重启免重复 embedding
多项目语料 按 project:relpath 标注入库,sources 参数在两路检索同过滤;UI 侧栏「检索范围」选择器
Provider 无关 + 零配置 换 OpenAI/DeepSeek/本地 vLLM 只改 base_url + 模型名;无 key 进入 MOCK 模式(确定性哈希向量 + 启发式评测),整条链路离线可演示,CI 无 secrets

🏗️ 架构

用户提问(可带多轮 history)
   │
   ▼
┌──────────────────────────────────────────────────────────────┐
│ 多轮改写:跟进问题 → 独立问题(LLM,失败/MOCK 透传)            │ ← app/llm.py rewrite_query
│ 检索:[原始 + 改写 (+HyDE 假设文档)] 多查询                    │ ← app/retrieval.py
│      → 各自 BM25+dense→RRF → 名次级 RRF 融合                  │
│      → (可选)BGE 重排 → top_k 引用                            │
└──────────────────────────────────────────────────────────────┘
   │ context
   ▼
┌──────────────────────────────────────────────────────────────┐
│ 生成:LLM 流式回答(带历史 + [1][2] 引用标注)                 │ ← app/llm.py / main.py (SSE)
└──────────────────────────────────────────────────────────────┘
   │ answer + context
   ▼
┌──────────────────────────────────────────────────────────────┐
│ 评测① 引用校验(毫秒级纯函数)→ event:meta 立刻发出            │ ← app/citations.py
│ 评测② 忠实度:claim 级 judge → supported/total → event:faith  │ ← app/faithfulness.py
│    (尾包分级:快校验不被秒级 judge 绑架在同一尾包)            │
└──────────────────────────────────────────────────────────────┘
   + 离线评测:忠实度 / 检索质量 / 多轮改写 / RAGAS 对照
     (scripts/ 与 eval/ 产出本地报告,见「测试与评估」)

持久化:启动装载 → 仅全新安装种演示语料 → 灌库后落盘          ← app/persist.py

工程细节(围绕 LLM 应用的脚手架)

层 实现 代码
检索正确性 RRF 与标准式一致;平局次序确定化(dense 名次 → bm25 名次 → chunk_id,不依赖容器迭代序);多查询融合的展示分即融合分(排序-分数不矛盾);top_k 双向夹取 app/retrieval.py
并发安全 store 全部读写持 RLock(FastAPI sync 端点在线程池真并发共享实例);持久化在锁内做快照序列化 app/store.py
启动数据安全 仅「全新安装」才种演示语料;加载失败以空库服务且不落盘(一写就覆盖用户索引);坏行跳过后装回按位置重编号 chunk_id app/persist.py · app/main.py lifespan
失败降级 重排失败回退 RRF 原序;改写失败检索原问题;HyDE 失败跳过该路;judge 失败回退句子级启发式;生成中途失败显式发 event: error(与用户主动停止可区分);流开始前失败返回结构化 502 app/rerank.py · app/llm.py · app/main.py
LLM 调用层 进程内复用客户端(连接池)+ 显式超时/重试;流式用 with 块关连接;embedding 自动分批 ≤32 条/请求(智谱单次 64 条上限,带真实回归测试);响应缺向量 fail-fast app/llm.py
输入守卫 上传扩展名白名单 + 2 MiB 上限 + UTF-8→gb18030 严格解码(二进制 422 拒绝);向量维度一致性校验(换 embedding 模型混库即报错,杜绝静默垃圾相似度) app/main.py · app/store.py
前端安全 语料侧字符串(引用原文/来源/判分理由)全量 html.escape——unsafe_allow_html 只包自产 markup;失败轮次不进对话历史 ui/streamlit_app.py

📁 目录结构

app/
  config.py            # 环境驱动配置(.env / RERANK / PERSIST_* / SEED_DEMO)
  store.py             # 混合存储(BM25 + dense 余弦 + RLock)+ 项目过滤 + save/load
  persist.py           # 持久化:JSONL(原子写+逐行容错)/ LanceDB(可选)
  llm.py               # Provider 无关 LLM 客户端 + query 改写 + MOCK fallback
  ingestion.py         # 滑窗切分 + ingest_files 批量入库(一次 embed/一次重建)
  retrieval.py         # hybrid_retrieve + multi_query_retrieve(双查询融合)
  rerank.py            # 可选 BGE 重排(惰性加载、失败回退、分数回写)
  citations.py         # 引用标注校验(纯函数)
  faithfulness.py      # claim 级忠实度分解(judge 逐条判定 + 句子级启发式 fallback)
  retrieval_metrics.py # Recall/Precision/Hit/MRR(纯函数)
  schemas.py / main.py # Pydantic 模型 / FastAPI(SSE、结构化错误、上传守卫)
ui/streamlit_app.py    # 对话式前端(可打断生成、错误/停止可区分、检索范围过滤)
skills/rag-corpus/     # 项目→语料 skill(SKILL.md + 6 个规则文件,含程序化质量门 G1–G10)
eval/                  # 评测集:忠实度 / 检索 / 多轮 + RAGAS 对照脚本
scripts/               # ingest_corpus.py + 评测脚本(支持任意项目语料)
tests/                 # 单元 + API 集成测试(TestClient,离线;计数见「测试与评估」)
data/sample.md         # 演示语料(虚构 NovaCloud 帮助文档,14 节)

🚀 快速开始

要求 Python ≥ 3.10。

pip install -r requirements.txt

cp .env.example .env    # (可选)填 LLM_API_KEY;不填走 MOCK,零配置可跑

python -m app.main                   # 后端 :8000(启动装载持久化索引,全新安装才种演示语料)
streamlit run ui/streamlit_app.py    # 前端 :8501

打开前端即是对话式界面:灌文档(可选)→ 提问 → 追问(「那要等多久?」)→ 看流式回答 + 忠实度徽章 + 引用校验 + 改写说明。生成中可随时停止,已生成部分保留。

🔌 主要 API

方法 路径 说明
POST /ingest 灌文本 {text, source};失败返回结构化 502
POST /ingest/file 上传 .txt/.md(白名单 + 2 MiB 上限;UTF-8 优先、gb18030 兜底,均失败 422)
POST /ingest/batch 批量入库 {project, files:[{path,text}]};source 存 project:path
GET /sources 项目列表与块数(检索范围选择器数据源)
DELETE /ingest/project/{name} 删除某项目全部块(重灌防重复;chunk_id 重编号)
POST /query SSE:citations → token 流 → meta(引用+引用校验+改写/HyDE,毫秒级先行)→ faith(claim 级忠实度,judge 完成即发);失败时 error 事件(与用户停止可区分);body 可带 history 与 sources(项目过滤)
POST /eval 批量忠实度评测
GET /health 状态(mock / chunks / rerank / persist)

✅ 测试与评估

pytest                                            # 82 项:切分/overlap、RRF/融合/平局、维度守卫、
                                                  # 坏行重编号、并发冒烟、rerank 契约、claim 级判分公式、
                                                  # HyDE 降级契约、SSE 事件序与错误契约、
                                                  # lifespan 数据安全、上传编码兜底…
python scripts/run_eval.py                        # 忠实度评测(claim 级 judge,模式自动标注;
                                                  #   评测集含有据/编造/混合三种样本)
python scripts/run_retrieval_eval.py              # 检索质量(Recall/Hit/MRR/Precision + 多轮改写对照
                                                  #   + HyDE A/B 对照)
python scripts/run_retrieval_eval.py --corpus <目录> --evalset <目录>/evalset.json   # 任意自定义语料
python eval/ragas_eval.py                         # RAGAS 标准对照(pip install ragas langchain-openai)

评测报告(RESULTS*.md / RETRIEVAL_RESULTS*.md)为本地运行时产物,不入库——数字永远可由上述命令在你自己的环境复现;评测脚本的模式标注(MOCK/启发式/真实)与指标聚合逻辑由测试离线覆盖。

CI(.github/workflows/ci.yml):ruff + mypy 静态检查,pytest 跑 3.10–3.13 全矩阵;显式 LLM_API_KEY="" 强制 MOCK,零 secrets。

📦 把任意代码库变成知识库(rag-corpus)

deep-rag 不只吃文档——用 rag-corpus skill 把任意代码仓库转成 RAG 优化的语料(模块手册 manual/*.md + 问答层 faq.md + 检索评测集 evalset.json),然后直接提问你的项目:

# ① 生成语料:在目标项目里对 Claude Code 说「用 rag-corpus 把这个项目做成知识库」
#    (skill 正本在 skills/rag-corpus/,可复制到 ~/.claude/skills/ 对任意项目使用;
#     生成走「清点 → 分区全量阅读 → 合并 → 程序化质量门 G1–G10」流程)
#    产出 <项目>/deeprag-corpus/:manual/*.md + faq.md + evalset.json + manifest.json

# ② 入库(服务运行中)
python scripts/ingest_corpus.py <项目>/deeprag-corpus --project <项目名> --api http://localhost:8000

# ③ 验证检索质量(配了真实 key 则用真实 embedding)
python scripts/run_retrieval_eval.py --corpus <项目>/deeprag-corpus --evalset <项目>/deeprag-corpus/evalset.json --project <项目名>

多项目入库后,UI 侧栏「检索范围」可按项目过滤。自举验证:对 deep-rag 自身完整跑过这套管线(手册锚点均经程序化门禁校验)——生成的语料目录与评测报告同属本地运行产物,按惯例不入库,任何项目都能用上述命令自行复现。

🛡️ 可靠性与安全要点

  • 密钥仅存 .env(已 gitignore);api_key 字段 repr=False,不出现在日志与调试输出
  • 上传:扩展名白名单 + 2 MiB 上限 + 严格解码——二进制/坏编码文件 422 显式拒绝,绝不乱码入库
  • 前端把语料内容视为不可信输入:所有动态字符串过 html.escape 再进 HTML 模板
  • 持久化:临时文件 + fsync + os.replace 原子替换;逐行容错坏行/坏字节;启动加载失败时空库服务且不写盘(保护用户索引不被覆盖)
  • 容器:非 root 用户运行 + HEALTHCHECK 探活;.dockerignore 把 .env、本地工作区挡在构建上下文外
  • 可选重排的版本坑:FlagEmbedding(1.4) 需 transformers<5——5.x 移除 prepare_for_model 后打分抛错并静默回退 RRF 原序(重排失效而非崩溃,符合降级契约但等于白开)
  • 公网部署须知:默认无鉴权且 CORS 全开(演示定位)——公网部署并填真实 key 时请置于鉴权/反向代理之后,或使用 MOCK 模式

📈 路线图

  • 检索主干:BM25+dense 混合(RRF)+ 可选重排 + 引用接地 + SSE 流式
  • 质量链路:忠实度评测 + 引用标注校验 + 检索质量评测 + RAGAS 标准对照
  • 多轮:query 改写 + 双查询融合(改写前后对照评测)
  • 持久化:JSONL 默认 / LanceDB 可选,重启免重复 embedding
  • 项目 → 知识库管线:rag-corpus skill + 多项目入库/来源过滤 + 任意语料评测
  • 加固轮:SSE 错误契约、store 并发锁、向量维度守卫、启动数据安全、上传白名单、CI 静态检查
  • claim 级忠实度分解(score = supported/total 本地计算;混合样本实证整体打分的稀释盲区后与 RAGAS 对照对齐)
  • HyDE 词法陷阱检索增强(先跑 rerank/HyDE 排除实验定位病灶再实现第三融合路;A/B 见检索评测 HyDE 段)
  • 流式校验的尾包分级:引用校验先行(meta)+ judge 异步补发(faith);句级流式 judge 经成本论证否决(论证见上)

🏆 技术要点

  • RRF 名次融合:每个片段按名次贡献 1/(k + rank),对 BM25 分数与余弦分数的量纲差异完全免疫;实现上预计算 cid→rank 字典避免 list.index() 的 O(N²),平局次序显式策略化(dense 名次 → bm25 名次 → chunk_id),可复现、可解释。
  • claim 级忠实度而非整体打分:混合回答(有据句 + 编造句同段)会被有据句稀释——整体打分对此系统性失明,与 RAGAS 标准对照时出现过「整体打分满分、标准对照不及格」的真实样本。改为单次结构化调用做分解 + 逐条判定,score = 支撑数/总论断数 由本地计算(可审计),与 RAGAS 的数学一致但延迟不翻倍;MOCK 降级为句子级近似 claim,两条路径公式对称。混合样本上的对照结论一致,具体数字由 run_eval.py / ragas_eval.py 本地复现。
  • HyDE 前先做排除实验:词法陷阱(查询词在不相关章节逐字共现)是 query 侧问题——排除实验证明 cross-encoder 重排救不了(正确块不在候选池,BGE 反而更偏爱逐字共现的干扰块),于是按 rerank 同款开关范式实现 HyDE 假设文档第三融合路(答案侧词汇替代问题侧陷阱词):词法陷阱样本从零命中救回到 top-k 内,整体均值亦有提升;代价同样如实记录——个别题的正确块名次后移(仍在 top-k)。逐题 A/B 由 run_retrieval_eval.py 的 HyDE 段产出,本地可复现。「先测量、先排除便宜方案、再对症实现」比 HyDE 这个名词本身更有区分度。
  • SSE 尾包分级:毫秒级的引用校验(纯函数)曾与秒级的忠实度 judge(实测单次 ~2.5s)阻塞在同一个 meta 尾包里——拆成 meta(校验先行)+ faith(judge 完成即发)两个事件后,快校验立刻可见。句级流式 judge 经成本论证否决(每句一次带全量 context 的判定 × 5–15 句、同步生成器需线程化、MOCK 离线不可演示;完整决策记录在本地 OPTIMIZATIONS.md 工作文件,按评测产物惯例不入库)。
  • 改写 + 双查询融合而非只信改写:LLM 改写偶尔丢主题词(曾观测到把「错误」改写成「系统或设备」),融合让改写路与原始路互为兜底——改写有效则提升,改写失效则原始路救回。
  • MOCK 模式是一等工程决策:确定性哈希假向量让「共享 token 的文本相似度更高」,零配置下检索链路有语义可演示;CI 全矩阵无 secrets 跑真实代码路径;所有评测产物如实标注 MOCK/启发式/真实三模式。
  • 评测集故意包含难题:转述题(无词面重叠)、纯指代跟进、词法陷阱(查询词在不相关章节逐字共现)——能区分好坏检索的评测才是评测。
  • chunk_id 是位置下标的显式契约:删除/装回都会重编号,引用只活在单次请求内;这换来了 get O(1) 直取与无需外部 ID 服务的简单性。
  • 持久化的取舍:JSONL 零依赖 + 原子替换 + 逐行容错,demo→中型语料够用;检索侧内存余弦在万级 chunk 内正确且快,换 ANN 索引的时机是「暴力扫描变慢」,由数据驱动而非提前设计。

🐳 部署

单容器(FastAPI:8000 + Streamlit:7860),对外 7860。

docker build -t deep-rag .
docker run -p 7860:7860 -e LLM_API_KEY=你的智谱key deep-rag

HF Spaces(Docker Space,push 仓库后在 Settings 加 LLM_API_KEY secret)与 Render(Web Service 指向 7860)均可直接跑;容器内已含就绪门(API 健康后才起 UI)与存活监督(API 退出即重启容器)。

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages