halil-ucar/enterprise-rag-assistant
GitHub: halil-ucar/enterprise-rag-assistant
面向企业的 RAG 知识助手,融合混合检索与严格的数据安全控制,提供带有来源验证的高质量问答。
Stars: 0 | Forks: 0
# enterprise-rag-assistant
[](https://github.com/halil-ucar/enterprise-rag-assistant/actions/workflows/ci.yml)
一款面向企业的基于 RAG 的知识助手:支持**混合搜索**(向量 + 全文)并带有 **reranking** 功能,提供带有**经验证的来源引用**的回答。机密数据**完全由本地模型**处理;通过 Postgres **行级安全**进行访问控制,针对 **prompt injection** 进行了强化。
FastAPI · PostgreSQL/pgvector · Redis · LangGraph · BGE-M3 · OpenAI/Azure OpenAI/Ollama
**▶ [在线演示 —— 一键访问,无需登录,所有数据均为虚构](https://halil-ucar.github.io/enterprise-rag-assistant/demo.html)** ——
完整的白盒 UI,以静态方式提供:每个响应均在浏览器中进行模拟(预设回答,无实时推理),包括检索轨迹以及 Anna/Ben 的行级安全切换。
截图展示了演示语料库(所有数据均为虚构)——更多视图见[docs/screenshots/](docs/screenshots/)。
## 快速开始
```
cp .env.example .env # fill keys — or run fully local, see below
make up # full container stack (CPU inference)
make seed-container # generate + ingest the 12-doc corpus inside the api container
open http://localhost:8000
```
`make seed`(代替 `seed-container`)是原生模式的变体——它在宿主机上进行 embed,并且需要 `uv sync --extra ml`,该操作没有 macOS x86_64 的 torch wheel。在容器模式下,请始终使用 `make seed-container`。
**原生开发模式**(Apple Silicon → MPS 推理):运行 `make dev`,然后在两个终端中分别运行 `make dev-api` 和 `make dev-worker`。**完全离线**(完全不进行云端调用):运行 `make demo-offline`——需要本地 [Ollama](https://ollama.com) 并执行 `ollama pull qwen3:8b`。
两个演示用户让权限变得直观可见:**Anna (IT)** 和 **Ben (HR)**——可在 UI 中切换。向两人询问 „In welcher Spanne liegt das Gehaltsband E3?“ 并观察行级安全如何给出不同的回答。
## 白盒机制
每个回答都会显示其路由(直接/Agentic)、提供商 + 模型层级、数据分类、缓存状态、TTFT 和 token 计数——以及一个可折叠的**检索调试面板**,其中包含每个候选者的密集排序、全文排序、RRF 分数和 rerank 分数。你可以观察一个在单一搜索中未脱颖而出的文档,如何通过融合过程逐渐上升,以及 reranker 如何对列表顶部进行重新排序。
## 架构
```
flowchart LR
Q[Frage] --> CR["condense + route\n(EIN Mini-Call)"]
CR -->|direct| R
CR -->|agentic| LOOP
subgraph Retrieval
R[Hybrid: HNSW + FTS german/simple] --> F[RRF k=60 in SQL] --> RR[Cross-Encoder Rerank] --> CTX[Kontext top 3-5]
end
LOOP["CRAG-Loop: grade → rewrite → retrieve\n(max 2 Iterationen + Token-Budget)"] --> R
CTX --> GEN["Generation (Tier je Route,\nDatenklasse: confidential ⇒ lokal)"]
GEN --> A["Antwort mit validierten Zitaten [S#]\n+ Glass-Box-Trace"]
```
完整的决策日志(包括被拒绝的替代方案):**[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** ·
扩展阶段:**[docs/SCALING.md](docs/SCALING.md)** · macOS 分步设置:
**[docs/SETUP-MACOS.md](docs/SETUP-MACOS.md)**。
## 安全模型(威胁 → 对策 → 证明)
| 威胁 | 对策 | 证明 |
|---|---|---|
| 跨部门泄露 | RLS(`FORCE`、独立应用角色、每个事务 `SET LOCAL`) | 集成测试 |
| 缓存绕过权限 | 权限范围缓存键(租户+部门+分类+版本) | 单元测试 |
| 机密数据流出 | 确定性分类路由,失败即关闭,无云端兜底 | 单元测试 |
| 间接 prompt injection | 边界标记 + 指令层级,无工具生成 | 黄金集用例 |
| 通过文档存储的 XSS | UI 仅通过 `textContent` 进行渲染 | 黄金集用例 |
| 删除后的残留数据 | 级联:chunks/vectors → 缓存 → 引用会话消息 → 反馈;用户自助服务(`DELETE /me/data`)+ 保留策略定时任务 | CI 集成测试 |
| 滥用/DoS | 每身份速率限制(token bucket)+ 请求大小上限 | 单元 + 集成测试 |
| 未经审计的访问 | 仅追加审计追踪(仅授予 INSERT 权限、代理 ID、独立保留策略) | 集成测试 |
| 通过 XSS 窃取 token | BFF:token 永不到达浏览器;HttpOnly 会话 cookie + CSRF token | 单元 + Playwright 测试 |
语料库中包含一份**预设的注入文档**(嵌入指令 + markup payload)——询问 „Wann ist das Wartungsfenster des Altsystems?“ 并观察它如何被作为数据进行处理。
## 评估
```
make eval # retrieval ablation (0 LLM cost): dense → hybrid → +rerank
uv run python eval/run_eval.py --answers # + answer checks, latency SLOs per run mode
uv run python eval/run_eval.py --answers --judge # + faithfulness (RAGAS definition)
```
Recall@5 和 MRR@5 锚定在文档+章节上(独立于分块);回答检查包括拒答契约、注入用例以及**引用有效性比率**(指每个内联标记都能解析到所提供来源的回答比例);延迟 SLO 因运行模式(原生/MPS 与 容器/CPU)而异,并且在违规时**判定运行失败**。`--judge` 严格依据生成器所见的精确上下文,根据 RAGAS 的定义添加**忠实度**评估(声明分解 + 针对每项声明的 NLI 判定,指令原文取自 ragas 0.4.3)。
判定器**根据数据分类执行策略控制**(与生成矩阵相同):云端判定器(Anthropic,商业无训练条款)可以对公开/内部回答进行评分,而机密集合仅由本地判定器评分——判定器在模型家族上不同于生成器,拒答标记为 N/A,会被报告但绝不会作为 CI 门控。结果存放在 `eval/runs/` 中。
语料库是**故意设计为两层结构**的(参见 [seed/CORPUS-DESIGN.md](seed/CORPUS-DESIGN.md)):
包含一个 12 文档的*冒烟*语料库用于 CI 管道,以及一个**精心策划的 42 个核心文档,它们被构建为设计好的困难负样本**——版本孪生(当前版本 vs. DEPRECATED)、位置孪生(Hagen vs. Köln)、系统易混淆项(不同的错误代码表)、FAQ/文本重复项——通过 37 个问题的黄金集进行评分(`make seed-core && make check-golden && make eval-core`):
| 配置 | Recall@5 | MRR@5 | n |
|---|---|---|---|
| dense | 0.969 | 0.898 | 32 |
| hybrid | 0.969 | 0.840 | 32 |
| hybrid+rerank | **1.000** | **0.945** | 32 |
测量于 2026-07-14 · 容器/CPU · BGE-M3 + bge-reranker-v2-m3 · 核心语料库 42 文档 / 214 chunks。
12 文档的冒烟测试语料库在*所有*配置下的 Recall@5 均为 1.000——这是一种**天花板效应**:使得区分不同配置变得过于容易。困难负样本核心集恢复了信号,而按类别进行的分解展示了每个部分*在哪里*发挥了作用:
| 类别 | dense | hybrid | hybrid+rerank |
|---|---|---|---|
| paraphrase | 0.500 | 0.444 | **0.750** |
| error_code | 0.917 | 0.917 | **1.000** |
| location | 1.000 | 1.000 | **1.000** |
| version | **0.900** | 0.740 | 0.800 |
各类别的 MRR@5。完整表格及延迟见 `eval/runs/`。
**回答质量**(`--answers --judge`,核心集,2026-07-14):36/37 项确定性检查通过 ·
**引用有效性 31/31 (100%)**——每个内联 `[S#]` 标记都能解析到提供的来源 ·
**忠实度 1.000**(RAGAS 定义——声明分解 + 针对每项声明的 NLI 判定;评分 n=31,6 项拒答标记为 N/A,判定器 `gemini-flash-lite` ≠ 生成器)。完美的分数并不是盲目通过的:在更广泛的冒烟测试集上,*同一个*指标在一个开放的多文档综合问题上得分为 **0.20**,因此它确凿地捕捉到了无根据的声明——核心集的 1.0 反映了简短、带有引用的回答以及 Agentic 的自检扎根性。诚实的警告:小型判定模型和简短的事实性回答是忠实度评估中较为容易的范畴。唯一一项失败的检查是检索器遗漏某个改写内容时的诚实拒答——这是检索遗漏,而非幻觉。
这次消融实验已经证明了其价值:最初融合是将 `ts_rank_cd(german) + ts_rank_cd(simple)` 合并到一个词法列表中,并且“标识符”部分接受任何长单词——词汇重叠噪声在它们各自的列表中排名高于精确的代码命中(此处的 hybrid MRR 为 0.846,*低于* dense)。将各部分拆分为**独立的排序列表**,并将 simple 部分限制为仅标识符,极大地修复了这个问题——带有诊断记录的决策日志位于 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#retrieval-chain)。依然坦诚的是:reranker 是主力(它提升了最难的类别 paraphrase 的分数),但在*版本孪生*上,管道仍略有退化(0.900 → 0.800)——因为对于 cross-encoder 来说,当前策略及其被废弃的孪生策略看起来几乎相同。这种权衡始终暴露在外,没有被假装掩盖掉。
### 相同系统在规模化下的表现(5 826 个 chunks)
核心语料库回答了*质量*问题;但它无法回答*当答案隐藏在数以千计的文档中时,hybrid 是否依然重要?* 因此,相同的 37 个问题针对**完整索引**进行了重新评分——即 42 个核心文档加上一个确定性的 **1500 篇文档的 Haystack**(`make seed-full FILL=1500`,总计 5826 个 chunks):
| 配置 | Recall@5 | MRR@5 | n |
|---|---|---|---|
| dense | 0.250 | 0.234 | 32 |
| hybrid | 0.906 | 0.793 | 32 |
| hybrid+rerank | **0.969** | **0.953** | 32 |
测量于 2026-07-12 · 容器/CPU · 完整索引 1542 文档 / 5826 chunks · 在部分拆分修复之后(修复前:hybrid 为 0.844/0.680,端到端 MRR 为 0.938)。
这**推翻了小语料库的结论**,而这正是通过实际测量而非臆断的全部意义所在。在 38 个 chunks 时,词法部分*造成了负面影响*(dense 已经是完美的,融合只会增加噪声)。而在 5826 个 chunks 时,**dense 检索的召回率骤降至 25%**——单一的 dense 向量无法从 1500 个看似合理的相邻项中分离出正确的策略——而词法部分将**召回率挽救至 91%**,reranker 提升至 97%。按类别来看,这种效应在精确 token 或近似重复项占主导地位的地方最为显著:`rls` 和 `version` 从(dense 的)MRR 0.000 提升至(hybrid+rerank 的)1.000 / 0.900;`error_code` 从 0.333 提升至 1.000。**hybrid 检索的价值是语料库规模的函数**——在此处可被证明,而非断言。
延迟突显了顶层架构的成本:CPU cross-encoder 主导了端到端时间(容器模式下 rerank 的 p50 ≈ 28 秒,而 dense 检索 ≈ 5 毫秒,hybrid ≈ 19 毫秒)——这就是为什么它是按运行模式设定的 SLO 和一个已记录的扩展杠杆(GPU 服务、缩小候选集),而不是一个始终开启的默认设置。
## 仓库结构
```
src/rag_assistant/ core library (ports, pipeline, providers, stores)
db/init/ schema + RLS policies (applied on first compose start)
config/collections.yaml declarative tenant/collection registry
seed/ corpus generator + golden set + ingest script
eval/run_eval.py ablation matrix, answer checks, SLOs
ui/index.html glass-box chat (single static page)
docs/demo.html static click-through demo (baked by scripts/build_demo.py)
tests/ unit (fakes, no services) + integration (marked)
```
## 开发
```
make test
make test-all
make lint
make type
```
`make test` 运行单元测试层——纯函数加上 `Fake*` 适配器,无需任何服务。
`make test-all` 增加了针对真实 Postgres/Redis 的集成测试层(需要 `make dev` 基础设施)。CI 在每次推送到 `main` 分支以及每个 pull request 时都会运行这两者:
lint、类型检查和单元测试,外加一个在真实 Postgres/pgvector 和 Redis 上进行的集成任务,该任务会测试 RLS 策略、删除级联和数据库恢复检查。
## 许可证
MIT — 详见 [LICENSE](LICENSE)。
标签:AI风险缓解, AV绕过, FastAPI, LLM应用开发, PostgreSQL, RAG, 人工智能, 企业级知识库, 搜索引擎查询, 测试用例, 混合检索, 用户模式Hook绕过, 请求拦截, 逆向工具