Jakub-Syrek/GenericRagGenerator
GitHub: Jakub-Syrek/GenericRagGenerator
一个完全本地化的RAG服务,通过RESTful API对文档、代码仓库和多源项目进行索引、检索与基于引用的智能问答。
Stars: 0 | Forks: 0
# GenericRagGenerator
[](#)
[](#rest-api)
[](#)
[](#)
[](#)
[](https://github.com/Jakub-Syrek/GenericRagGenerator/actions/workflows/ci.yml)
[](https://github.com/Jakub-Syrek/GenericRagGenerator/tree/main/tests)
[](https://github.com/Jakub-Syrek/GenericRagGenerator/blob/main/eval/sample-result.md)
[](https://www.python.org/)
[](https://fastapi.tiangolo.com/)
[](https://ollama.com/)
[](https://docs.llamaindex.ai/)
[](https://www.trychroma.com/)
[](https://github.com/astral-sh/ruff)
[](http://mypy-lang.org/)
[](https://github.com/PyCQA/bandit)
[](https://github.com/Yelp/detect-secrets)
[](https://pre-commit.com/)
[](Dockerfile)
[](scripts/install-windows-service.ps1)
本地检索增强生成 (RAG) 服务,以**完全符合 RESTful 标准的 HTTP API** 形式提供(22 个 endpoint,OpenAPI / Swagger 文档位于 `/docs`)。
上传文档、整个代码仓库的 ZIP 压缩包,或是多来源的项目,然后即可通过相同的接口从 curl、Postman、IDE 插件或内置浏览器 UI 中**查询 / 聊天 / 搜索**该索引。无论你是将其作为前台进程 (`run.ps1`)、Windows 服务 (NSSM) 还是 Docker compose 堆栈启动,提供的 API 都是完全相同的——
没有“服务”与“API”的区别,每一次安装都是一个具备网络通信能力的 HTTP 服务器。
回答严格基于已索引的内容生成,并会引用确切的源文件(对于代码,还会包含行号范围)。
## 可导入的内容
- **普通文档**:`.pdf`、`.txt`、`.md` / `.markdown`、`.rst`、`.html` /
`.htm`、`.docx`。
- **源代码**(约 30 种语言):Python、TypeScript / JavaScript / TSX /
JSX、Java、Kotlin、Scala、Go、Rust、C / C++ / headers、C#、Ruby、PHP、
Swift、shell(bash / zsh / sh)、PowerShell、SQL,以及常见的配置
格式(YAML、TOML、JSON、XML、INI、CSS、SCSS)。
- **整个代码仓库**:拖入一个 `.zip` 压缩包;安全的提取器会拒绝
路径穿越、软链接、超大文件,并自动跳过诸如
`.git/`、`node_modules/`、`__pycache__/`、`dist/`、`build/`、`target/`、
`vendor/` 和 IDE 缓存等冗余文件。
每个 chunk 都附带了 `kind` (`code`/`doc`)、`language`、可选的
`line_start`/`line_end` 以及 `repository_id` 元数据。聊天 UI 中的来源标签会为代码渲染出 `repo/path/to/file.py:42-101` 的格式,而对于文档则显示纯路径。
## 技术栈
| 层级 | 选择 |
|-------------------|-------------------------------------------------------------------|
| 后端 | Python 3.11 + FastAPI |
| LLM 运行时 | [Ollama](https://ollama.com)(本地、开源) |
| 聊天模型 | `llama3.1:8b`(可通过 `CHAT_MODEL` 配置) |
| Embedding 模型 | `nomic-embed-text`,带有 `search_query:` / `search_document:` 前缀 |
| RAG 编排 | [LlamaIndex](https://docs.llamaindex.ai) |
| 向量数据库 | [ChromaDB](https://www.trychroma.com)(内置、磁盘存储) |
| Chunking | `MarkdownNodeParser` (md) / `CodeChunker` (code) / `SentenceSplitter` (默认) |
| 前端 | 纯 HTML + CSS + ES6(符合 CSP 标准,无内联脚本) |
| 安全 | bandit + detect-secrets + pip-audit、安全响应头、slowapi、可选 API key |
| CI | GitHub Actions (ruff + mypy + bandit + pytest + pip-audit) |
## 前置条件
1. Python 3.11+
2. 在本地运行 Ollama:`ollama serve`
3. 拉取所需的模型(只需一次):
ollama pull llama3.1:8b
ollama pull nomic-embed-text
## 本地运行
```
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
Copy-Item .env.example .env
.\run.ps1
```
然后打开 。从侧边栏上传文档*或*项目 ZIP,接着在聊天面板中提问即可。助手会逐 token 流式输出回答,并为它从向量数据库中提取的每一个 chunk 显示引用标签。
## 可选:安装为 Windows 服务
NSSM ([nssm.cc](https://nssm.cc)) 将 `.venv` 中的 uvicorn 进程封装为一个标准的 Windows 服务,具备自动启动、日志轮转和故障重启功能。
`scripts/` 目录下的两个 PowerShell 辅助脚本可实现此操作:
```
# 1. 安装 NSSM 一次(管理员 PowerShell):
winget install NSSM.NSSM
# 或:choco install nssm
# 或:手动将 nssm.exe 放到 PATH 中的某个位置
# 2. 从项目根目录开始,在*提权的* PowerShell 中:
.\scripts\install-windows-service.ps1
```
该脚本会注册名为 `GenericRagGenerator` 的服务,
绑定到 `127.0.0.1:8000`(默认的 `local` 模式),将 stdout/stderr 捕获并轮转记录到 `.\logs\service-*.log`(10 MB 轮转)中,并在发生故障 5 秒后重启进程。
通过 `-Mode` 提供两种安装配置:
| 模式 | 绑定 | 适用场景 |
|-----------|--------------|--------------------------------------------------------|
| `local` | `127.0.0.1` | 单用户 / 个人电脑。API 仅可在本地访问。 |
| `public` | `0.0.0.0` | 共享主机 / 企业部署。除非在 `.env` 中设置了 `API_KEY`(或 `AUTH_PASSWORD` + `JWT_SECRET`),否则将拒绝安装。 |
```
# 仅本地(默认):API 在 http://127.0.0.1:8000
.\scripts\install-windows-service.ps1
# 在自定义端口上公开绑定(必须先在 .env 中设置 auth)
.\scripts\install-windows-service.ps1 -Mode public -Port 9000
# 公开 + 隐藏 Swagger / Redoc / OpenAPI
.\scripts\install-windows-service.ps1 -Mode public -DisableDocs
```
运维命令:
```
Get-Service GenericRagGenerator # status
Restart-Service GenericRagGenerator # reload after .env / code changes
Stop-Service GenericRagGenerator # graceful stop (15 s window)
.\scripts\uninstall-windows-service.ps1
```
生产环境注意事项:
- Ollama 本身通过其安装程序作为服务运行;两者可以在同一台机器上
并行运行。
- 当主机为共享状态时,请在最低权限的本地账户下运行该服务
(`nssm set GenericRagGenerator ObjectName .\rag-user `)
而不是使用 `LocalSystem`。
- 在安装服务前于 `.env` 中设置 `API_KEY`,从而确保公开接口
从首次启动起即受保护。
## 在 Docker 中运行(适合企业环境)
```
docker compose up -d
docker exec ggrag-ollama ollama pull llama3.1:8b
docker exec ggrag-ollama ollama pull nomic-embed-text
```
该 compose 堆栈在运行应用和 Ollama 时遵循以下原则:
- 应用镜像中使用非 root 用户(`UID 10001`)且不含 shell;
- `read_only: true` 只读根文件系统,仅为 `/tmp` 和
`/home/app/.cache` 挂载 tmpfs;
- 两个服务均配置了 `cap_drop: ALL` 和 `no-new-privileges: true`;
- 在 `/api/health` 上配置了健康检查;
- 为 Chroma 数据和 Ollama 模型缓存使用独立的命名卷。
有关完整的威胁模型和企业部署指南,请参阅 [`SECURITY.md`](SECURITY.md)。
## REST API
无论你选择哪种运行模式(`run.ps1`、Windows 服务、Docker compose 堆栈),相同的接口都会暴露在相同的端口上——不存在“服务”与“API”的区别。交互式 OpenAPI 文档位于 `/docs` (Swagger UI) 和 `/redoc`;在生产环境中设置 `DOCS_ENABLED=false` 可隐藏这两者以及 `/openapi.json`。
| Method | Path | 用途 |
|--------|-------------------------------------|--------------------------------------------------------|
| GET | `/api/health` | 服务 + Ollama 可达性(始终无需认证) |
| POST | `/api/auth/login` | 验证凭据,颁发 JWT bearer |
| GET | `/api/auth/whoami` | 回显已认证的主体 + 权限范围 |
| POST | `/api/admin/reset` | 清除索引中的所有 chunk(仅限管理员权限) |
| GET | `/api/documents` | 列出已索引的文档 |
| POST | `/api/documents` | 上传单个文档(multipart) |
| GET | `/api/documents/{id}` | 文档详情(类型、语言、预览) |
| GET | `/api/documents/{id}/chunks` | 列出从单个文档生成的所有 chunk |
| DELETE | `/api/documents/{id}` | 移除文档及其所有 chunk |
| GET | `/api/repositories` | 列出已索引的代码仓库 |
| POST | `/api/repositories` | 上传项目 ZIP (multipart) |
| GET | `/api/repositories/{id}` | 代码仓库详情及其逐文件导入列表 |
| GET | `/api/repositories/{id}/files` | 列出从单个代码仓库导入的所有文件 |
| DELETE | `/api/repositories/{id}` | 移除代码仓库及其所有 chunk |
| GET | `/api/projects` | 列出已索引的多来源项目 |
| POST | `/api/projects` | 将多个原始文件作为一个项目上传 (multipart) |
| GET | `/api/projects/{id}` | 项目详情及其逐文件导入列表 |
| GET | `/api/projects/{id}/files` | 列出导入到单个项目中的所有文件 |
| DELETE | `/api/projects/{id}` | 移除项目及其所有 chunk |
| POST | `/api/search` | 仅检索的相似度搜索(不调用 LLM) |
| POST | `/api/query` | 同步 RAG 回答(单个 JSON,非流式) |
| POST | `/api/chat` | 流式 RAG 回答 (NDJSON) |
### 认证
在所有受保护的路由上均支持以下两种流程:
- **静态 `X-API-Key`** —— 在环境中设置 `API_KEY`,然后
在每次请求中发送 `X-API-Key: `。使用常数时间进行比较
(`hmac.compare_digest`);适合服务间通信。
- **交互式 JWT bearer** —— 在环境中设置 `AUTH_PASSWORD` *和* `JWT_SECRET`,然后执行:
TOKEN=$(curl -s -X POST http://127.0.0.1:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"…"}' | jq -r .access_token)
curl http://127.0.0.1:8000/api/auth/whoami -H "Authorization: Bearer $TOKEN"
Bearer token 使用 HS256 签名,携带 `sub` + `scopes` 声明,并在 `JWT_EXPIRES_MINUTES`(默认 60)后过期。从
`/api/auth/login` 获取的 token 携带 `admin` 权限范围,这是调用
`/api/admin/reset` 所必需的。
如果未配置 `API_KEY` 或 `JWT_SECRET`,认证将被禁用,所有 endpoint 将以 `anonymous` 主体的身份运行——这适用于本地开发,但在生产环境中绝对不要使用。
### 资源负载
```
POST /api/projects // multipart
files=&files=&name=
```
```
POST /api/query
{
"messages": [{"role": "user", "content": "How does slugify work?"}],
"document_ids": ["..."],
"repository_ids": ["..."],
"project_ids": ["..."]
}
// returns { "answer": "...", "sources": [...] }
```
```
POST /api/search
{
"query": "how does slugify work?",
"top_k": 10,
"document_ids": ["..."],
"repository_ids": ["..."],
"kinds": ["code", "doc"]
}
// returns { "query": "...", "results": [...], "total": N }
```
```
POST /api/admin/reset // Authorization: Bearer
// returns { "chunks_removed": N }
```
## 架构
经过几次抽象提取使得代码更加实用——其余部分仅是原生的 FastAPI / Pydantic
/ `logging`:
| 位置 | 为什么不直接写成内联代码 |
|---------------------------------|----------------------------------------------------------------------------------------------------------|
| `services/chunking.py` | `Chunker` 协议 + `ChunkerRegistry`。另一种选择是采用 `if kind == "code" elif language == ...` 链式判断,并且每次增加新格式都要扩充代码。 |
| `services/index_catalog` | `IndexCatalog` 是唯一一个导入 `chromadb` 的类。将 ChromaDB 替换为 Qdrant/pgvector 只需修改这一个文件。 |
| `services/rag_service.py` | 组合根。Handler 依赖于外观接口,而不是直接依赖底层的 LlamaIndex/Chroma/Ollama 三件套。 |
| `security/_PrefixedOllamaEmbedding` | nomic-embed-text 需要 `search_query:` / `search_document:` 前缀;这是唯一知道该逻辑的类。 |
## 可选的检索功能
默认全部关闭——开启环境开关即可启用。当设置为 `false` 时,每个开关都会保持仅使用密集检索的单进程默认路径不受影响。
| 标志 | 默认值 | 增加的功能 |
|----------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------|
| `RETRIEVAL_MODE=hybrid` | `vector`| BM25 词法检索通过与密集命中结果结合 Reciprocal Rank Fusion 进行融合。有助于查询罕见词汇(函数名、错误码)。 |
| `CACHE_ENABLED=true` | `false` | 在 `/api/search` 和 `/api/query` 上使用 LRU + TTL 响应缓存。每次导入 / 删除 / 清除操作都会使其自动失效。 |
| `RERANKER_ENABLED=true` | `false` | FlashRank ONNX cross-encoder(约 80 MB,一次性下载)在检索后对候选列表进行重新排序。 |
| `PARSER_SANDBOX_ENABLED=true` | `false` | PDF / DOCX / HTML 解析器在子进程中运行并设有时间限制。负载格式错误只会导致该 worker 崩溃。 |
| `OLLAMA_RETRY_*` | 开启 | 针对瞬态 Ollama 错误(`ConnectError`、`ReadTimeout`、`RemoteProtocolError`)使用由 Tenacity 驱动的指数退避重试。 |
其他内置功能(始终开启,无需设置标志):
- **导入时基于内容哈希去重。** 每次上传均以原始负载的 SHA-256
作为键值;重新上传相同字节会在调用 Ollama embedding 前直接短路返回,并在响应中返回之前的记录且附带
`deduplicated=true`。
- **基于主体的 ACL。** 当配置了 `API_KEY` / `JWT_SECRET` 时,
每个 chunk 都会标记上上传者的主体名称。读取、
列表和删除路径均受所有者权限范围限制,因此 JWT 用户只能看到其自己的文档 / 代码仓库 / 项目。静态 API key 和
任何携带 `admin` 权限范围的 JWT 可绕过此过滤器。匿名
模式(未配置凭据)保持单租户状态。
聊天流每行发出一个 JSON 事件:
- `{"type": "sources", "sources": [...]}` 位于开头且仅输出一次,其中每个
source 包含 `document_id`、`filename`、`kind`、`language`、
`repository_name`(如果是通过 repo 导入的),以及针对代码 chunk 的 `line_start` /
`line_end`。
- `{"type": "delta", "content": "..."}` 对应每一批生成的 token。
- 成功时输出 `{"type": "done"}`,如果 Ollama 或 Chroma 在流式传输过程中失败,则输出 `{"type": "error", "message": "..."}`。
## 项目结构
```
backend/app/
api/ HTTP routes (auth, admin, documents, repository,
projects, search, query, chat, health)
services/ RagService (facade), chunking (Strategy/Registry),
index_catalog (Repository), document_loader
models/ Pydantic schemas
security/ Headers middleware, API-key + JWT auth, rate limiter
config.py Settings (env-driven)
dependencies.py FastAPI DI providers (cached singletons via lru_cache)
main.py FastAPI entry, middleware wiring, static frontend mount
frontend/ Static UI (documents + repository forms, source chips)
eval/ RAG quality eval (corpus + runner + sample report)
sample_repo/ Synthetic mini_parser fixture (code + HTML + Markdown)
tests/ Pytest suite (unit + API integration with TestClient)
data/ Runtime (uploads + Chroma persistence) - gitignored
logs/ Windows-service stdout / stderr (NSSM-managed) - gitignored
scripts/ PowerShell helpers (install / uninstall Windows service)
Dockerfile Multi-stage, non-root, healthcheck
docker-compose.yml App + Ollama + hardening
SECURITY.md Threat model and deployment guidance
```
## 质量评估
```
.\.venv\Scripts\python.exe -m eval.run_eval
```
下载四篇维基百科文章的引言(RAG、Vector database、Word
embedding、Photosynthesis),将 `eval/sample_repo/` 打包为 ZIP,通过现有的 API 推送两套语料库,在
`/api/chat` 上运行 24 个预设问题,并从以下四个维度对每个回答进行评分:
- **`retrieval_top1_precision`** —— Top-1 来源与预期文件匹配。
- **`answer_substring_match`** —— 回答包含任何预期
子字符串(不区分大小写)。
- **`kind_precision`** —— 对于标记为代码/文档类型的问题,Top-1 来源属于
预期的类型。
- **`ooc_refusal_rate`** —— 正确拒绝对语料库范围之外的试探。
结果保存在 `eval/results/` 下(带时间戳的 JSON + Markdown)。已提交的基准线位于 [`eval/sample-result.md`](eval/sample-result.md)。
在 `llama3.1:8b` + `nomic-embed-text` 下的最新本地运行结果:
- **24/24** 综合通过
- retrieval_top1 1.0, answer_match 1.0, kind 1.0, ooc_refusal 1.0
- 预热后平均延迟约为每轮 0.6 秒
## 开发工作流
```
# 一次性设置
pip install -r requirements.txt
pre-commit install
# 每次更改时
pre-commit run --all-files
pytest
```
`pre-commit` 会运行:
- 空格 / EOL / 大文件规范检查、
- `ruff` (代码检查 + 格式化)、
- 对 `backend/` 运行 `mypy`、
- `bandit`(Python 安全隐患排查)、
- 基于文件 `.secrets.baseline` 运行 `detect-secrets`。
CI 在 push 和 PR 时会运行相同的 pre-commit 套件,并加上 `pip-audit`
(仅记录安全公告但不阻断;详见 `SECURITY.md`)。
针对实时 Ollama 运行的集成式端到端检查不包含在
CI 中,而是存放在 `smoke_test.py` 和 `eval/` 包中。
## 配置项
| 变量 | 默认值 | 用途 |
|-----------------------------|-------------------------------|-------------------------------------------------------------|
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama 的访问地址。 |
| `CHAT_MODEL` | `llama3.1:8b` | 聊天补全模型。 |
| `EMBEDDING_MODEL` | `nomic-embed-text` | Embedding 模型。 |
| `EMBEDDING_QUERY_PREFIX` | `"search_query: "` | 添加到查询 embedding 前的前缀(nomic 非对称机制)。 |
| `EMBEDDING_DOCUMENT_PREFIX` | `"search_document: "` | 添加到文档 embedding 前的前缀。 |
| `CHUNK_SIZE` / `CHUNK_OVERLAP` | `800` / `120` | 按字符计算的句子分割器窗口大小。 |
| `TOP_K` | `8` | 检索器 top-k。仅在语料库较小或为合成数据时才需调高;在真实索引上操作只会过度填充 prompt。 |
| `API_KEY` | *(未设置)* | 设置后,将使用 `X-API-Key` 对 `/api/documents`、`/api/repositories`、`/api/chat` 进行访问限制。 |
| `CORS_ORIGINS` | `["http://localhost:8000", ...]` | 针对浏览器的严格白名单。 |
| `RATE_LIMIT_CHAT` | `30/minute` | 对 `/api/chat` 应用的基于单个 IP 的 slowapi 限制。 |
| `RATE_LIMIT_UPLOADS` | `10/minute` | 供反向代理使用(slowapi 无法包装 `UploadFile`)。 |
| `AUTH_USERNAME` | `admin` | `POST /api/auth/login` 接受的用户名。 |
| `AUTH_PASSWORD` | *(未设置)* | 登录时接受的密码。启用 bearer 流程时必填。 |
| `JWT_SECRET` | *(未设置)* | 用于签发 JWT bearer 的 HS256 签名密钥。启用登录时必填。 |
| `JWT_EXPIRES_MINUTES` | `60` | 签发的 bearer token 的有效期。 |
| `DOCS_ENABLED` | `true` | 在生产环境中设置为 `false` 可隐藏 `/docs`、`/redoc` 和 `/openapi.json`。 |
| `RETRIEVAL_MODE` | `vector` | `vector` (仅密集检索) 或 `hybrid` (密集检索 + BM25 通过 RRF 融合)。 |
| `CACHE_ENABLED` | `false` | 用于 `/api/search` 和 `/api/query` 的 LRU+TTL 响应缓存。 |
| `CACHE_MAX_ENTRIES` | `256` | LRU 驱逐前的最大缓存响应数。 |
| `CACHE_TTL_SECONDS` | `300` | 缓存响应的 TTL(以秒为单位)。 |
| `RERANKER_ENABLED` | `false` | 在检索出的候选列表上运行 FlashRank cross-encoder。 |
| `RERANKER_MODEL` | `ms-marco-MiniLM-L-12-v2` | FlashRank ONNX 模型名称。 |
| `RERANKER_TOP_K` | `5` | 重新排序后保留的命中结果数。 |
| `PARSER_SANDBOX_ENABLED` | `false` | 在子进程沙箱中运行 PDF / DOCX / HTML 解析器。 |
| `PARSER_SANDBOX_TIMEOUT_SECONDS` | `10.0` | 每个沙箱解析在被杀掉之前的时钟时间限制。 |
| `OLLAMA_RETRY_ATTEMPTS` | `3` | 遇到 Ollama 瞬态错误时的总尝试次数(包含首次)。 |
| `OLLAMA_RETRY_BACKOFF_MIN_SECONDS` | `1.0` | 初始指数退避窗口时间。 |
| `OLLAMA_RETRY_BACKOFF_MAX_SECONDS` | `8.0` | 指数退避窗口时间的上限。 |
## 安全
完整的威胁模型、范围内/范围外的保证、部署指南以及公开的依赖安全公告均位于 [`SECURITY.md`](SECURITY.md)。简而言之:
- 优先考虑本地 / 企业网络环境目标;在没有反向代理的情况下
不适合直接暴露在公共互联网中。
- 将 Bandit + detect-secrets + pip-audit 集成到了 pre-commit / CI 中。
- 加固响应标头(CSP、XFO DENY、HSTS、Referrer-Policy、
Permissions-Policy)、严格的 CORS、可选的 API key、基于 IP 的速率
限制。
- ZIP 导入强制执行路径穿越 / 软链接 / 大小限制,并将特定的命名
领域错误映射到具体的 HTTP 状态码。
## 提交与风格规范
- 在代码、注释、提交记录和标识符中仅使用英语。
- 公共函数需记录 `@param` / `@returns`(JSDoc 风格的文档字符串)。
- SOLID 原则 + DI:依赖通过 FastAPI 的 `Depends` 和
构造函数参数注入,严禁使用模块全局变量。
- 函数规模应保持在 30 行左右的上限;ruff 会强制执行 pylint 的语句 / 分支
限制。
- 错误在每个外部边界处都会被转换为明确的领域
异常(`EmbeddingError`、`ChatGenerationError`、`VectorStoreError`、
`StorageError`、`UnsafeArchiveError`、`RepositoryError`、
`EmptyDocumentError`、`UnsupportedFormatError`)。
- 提交是原子性的并会立即推送;`main` 分支上的 CI 必须保持通过状态。
标签:AI风险缓解, AV绕过, ChromaDB, FastAPI, LlamaIndex, LLM评估, Ollama, RAG, RESTful API, 大模型, 提示词优化, 本地部署, 请求拦截, 逆向工具