内容
# NOEMA-AI
端到端检索增强生成流程,支持从文档中摄入和回答查询。该流程基于内容,并支持**三种检索路径**:**基于向量的检索**(嵌入 + 向量存储)、**无向量检索**(关键词/BM25结构化检索)和**混合检索**(两种检索方式的互惠排名融合)。该流程由 **LangChain** 和 **LangGraph** 编排,由 **LangSmith** 跟踪,并通过 **MCP** (模型上下文协议)暴露给外部客户端。此外,它还支持**多轮对话内存**和**流式答案**,通过服务器发送事件。
```
文档 ─▶ 摄入 ─▶ 分块 ─▶ ┬─▶ 嵌入 ─▶ 向量存储 ─┐
└─▶ BM25 索引(无向量检索) ─┤─▶ LangGraph ─▶ 大型语言模型 ─▶ 答案
│ (检索节点)
查询 ───────────────────────────────────────────────────────────────────┘
```
## 截图
<!--
将截图图片放入 docs/screenshots/ 目录中,使用以下文件名,它们将自动在 GitHub 上渲染。建议使用 1280px 以上的 PNG 图片,暗色模式(与 UI 主题匹配)。
-->
**首页 — NOEMA UI 的登陆视图**

**控制台 — 询问问题,实时查看检索 + 生成跟踪**

**混合检索模式 — 融合向量 + BM25 结果与匹配源**

**聊天 — 多轮对话,带有会话内存**

**API 参考 — 完整端点接口视图**

**流式答案 — 通过 SSE 实时接收令牌**

## 功能
- **向量 RAG** — 分块 → 嵌入 → 持久向量存储(Chroma)→ 相似性搜索。默认情况下,配备零依赖的离线哈希嵌入,并提供真正的本地语义嵌入选项(`EMBEDDING_PROVIDER=sentence-transformers`),用于实际语义搜索
- **无向量 RAG** — 在原始分块上进行 BM25 关键词排名,无需嵌入模型
- **混合 RAG** — 使用向量和 BM25 结果列表的互惠排名融合(RRF),因此在*任一*检索器中排名良好的分块都会被优先考虑,而在*两者*中排名良好的分块则会优先出现
- **多轮对话内存** — `POST /chat` 保留每个会话的滚动窗口,因此后续问题(“第二个怎么办?”)会根据早期回合进行解析,而不是在真空中回答
- **流式答案** — `GET /query/stream` (服务器发送事件)以令牌为单位流式传输答案,而不是等待完整响应
- **文档管理** — `GET /documents` 列出每个索引源;`DELETE /documents/{source}` 同时从每个检索路径中删除它
- **查询结果缓存** — 短期内存 TTL 缓存为重复的相同问题提供服务,无需重新运行检索 + 大型语言模型;自动在摄入/删除时失效
- **重启后持久性** — BM25 语料库和摄入文档清单保存到 `PERSIST_DIR` 并在启动时重新加载,因此重启不会无声地清除已摄入的内容
- **LangGraph 流程** — 摄入、检索(向量/无向量/混合)和生成建模为可组合的图形节点,并具有从向量到无向量的自动回退
- **LangSmith 跟踪** — 每个大型语言模型调用和检索步骤在配置时都是可跟踪/可评估的
- **大型语言模型网关/中间件** — 单一层路由请求、日志和速率限制,然后再调用大型语言模型
- **防护栏** — 输入/输出检查,带有规范化(空白/零宽度字符抵抗性)提示注入模式匹配;仍然基于模式,而不是完整的分类器 — 请参见下面的注释
- **API 密钥身份验证** — 在 `/ingest` 、 `/query/*` 、 `/documents/*` 和 `/chat/*` 上可选地强制执行 `X-API-Key` 头,默认关闭,以便应用程序仍然可以零设置运行
- **MCP 服务器** — 通过 MCP 工具向外部大型语言模型客户端公开 `ingest_document` 、 `delete_document` 、 `list_documents` 、 `query_vector_rag` 、 `query_vectorless_rag` 、 `query_hybrid_rag` 和 `chat_with_memory`
- **Docker 支持** — 一个 `Dockerfile` 和 `docker-compose.yml` ,用于一键容器化部署,带有持久卷
- **52 个单元/API 测试** ,涵盖分块器、防卫桩(包括逃避尝试)、BM25 存储、混合 RRF 融合、查询缓存、聊天内存和 FastAPI 端点(包括身份验证强制、文档删除和聊天会话)
## 项目布局
```
noema/
├── app/
│ ├── config.py # 环境/设置
│ ├── main.py # FastAPI 入口点(REST API)
│ ├── middleware.py # 大型语言模型网关/日志/速率限制中间件
│ ├── guardrails.py # 输入和输出防护栏
│ ├── cache.py # TTL 查询结果缓存
│ ├── chat.py # 基于会话的对话内存
│ ├── ingestion/
│ │ ├── loader.py # 加载 .txt / .pdf / .docx 到原始文本
│ │ └── chunker.py # 带重叠的滑动窗口分块
│ ├── retrieval/
│ │ ├── embeddings.py # 嵌入提供者(API 或本地备用)
│ │ ├── vector_store.py # Chroma 支持的向量 RAG
│ │ ├── vectorless_store.py # BM25 支持的无向量 RAG(+ 持久性)
│ │ └── hybrid.py # 向量 + BM25 的互惠排名融合
│ ├── llm/
│ │ └── client.py # 大型语言模型客户端(Groq / LLaMA 3.3 70B,可模拟,流式 + 历史感知)
│ ├── graph/
│ │ └── pipeline.py # LangGraph 状态机连接所有组件
│ ├── mcp/
│ │ └── server.py # MCP 服务器将流程公开为工具
│ └── static/ # Web UI(在 "/" 处由 app/main.py 提供)
│ ├── index.html
│ └── assets/
│ ├── style.css
│ └── app.js
├── docs/screenshots/ # 放置截图图片的地方(参见上面的截图)
├── data/sample_docs/sample.txt # 示例文档,用于摄入
├── scripts/ingest.py # 命令行界面:从命令行摄入文档
├── tests/ # 52 个测试:分块器、防卫桩、存储、混合、缓存、聊天、API
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── .env.example
```
## 设置
```bash
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # 然后填写您的密钥
```
### 环境变量(`.env`)
| 变量 | 目的 |
|---|---|
| `GROQ_API_KEY` | Groq-hosted LLaMA 3.3 70B 调用密钥(留空则使用内置模拟大型语言模型进行本地测试) |
| `LANGCHAIN_TRACING_V2` | `true` 启用 LangSmith 跟踪 |
| `LANGCHAIN_API_KEY` | LangSmith API 密钥 |
| `LANGCHAIN_PROJECT` | LangSmith 项目名称,例如 `noema` |
| `VECTOR_DB_PATH` | 持久 Chroma 存储的本地路径,默认为 `./chroma_db` |
| `EMBEDDING_PROVIDER` | `hash` (默认,确定性离线词袋嵌入,零依赖) · `sentence-transformers` (真正的本地语义嵌入 — `pip install sentence-transformers` ,首次使用时下载小模型) · `remote` (您连接的托管嵌入 API 的占位符) |
| `EMBEDDING_MODEL_NAME` | `EMBEDDING_PROVIDER=sentence-transformers` 时使用的模型名称,默认为 `all-MiniLM-L6-v2` |
| `API_KEY` | 留空则以开放开发模式运行 API。设置一个值,则在 `/ingest` 和 `/query/*` 上需要匹配的 `X-API-Key` 头 |
| `PERSIST_DIR` | 保存 BM25 语料库和摄入文档清单的位置,默认为 `./storage`。在启动时恢复意味着重启不会无声地清除已摄入的内容 |
| `QUERY_CACHE_TTL_SECONDS` / `QUERY_CACHE_MAX_ENTRIES` | 相同 `(mode, query, top_k)` 请求从缓存中提供服务的时间长度,以及缓存达到多少条目后被淘汰 |
| `CHAT_HISTORY_MAX_TURNS` / `CHAT_SESSION_TTL_SECONDS` | `/chat` 在每个会话中保留多少轮对话,以及空闲会话在被清除之前保留多长时间 |
| `HYBRID_RRF_K` | `/query/hybrid` 的互惠排名融合常数;较高的值减少排名位置 1 与 2 的重要性差异 |
## 运行
**1. 摄入文档**
```bash
python scripts/ingest.py data/sample_docs/sample.txt
```
**2. 启动 API(和 UI)**
```bash
uvicorn app.main:app --reload
```
在浏览器中打开 **http://127.0.0.1:8000** — 现在可以直接从 FastAPI 应用程序 (`app/static/`) 提供 NOEMA Web UI。 在摄入面板上放置一个文件,选择一种检索模式,然后提出一个问题;当请求通过防护栏 → 检索 → 生成时,管道跟踪条带会亮起,结果显示哪个通道(向量/无向量)回答了问题,以及检索到的块及其分数。
端点:
- `GET /health` — 存活 + 正常运行时间
- `GET /stats` — 索引文档列表,BM25 分块计数,向量存储可用性,活动聊天会话,查询缓存统计
- `GET /documents` — 列出每个索引源及其分块计数
- `DELETE /documents/{source}` — 从每个检索路径中删除文档
- `POST /ingest` — 上传/摄入文档
- `POST /query/vector` — 使用向量 RAG 回答查询
- `POST /query/vectorless` — 使用无向量(BM25)RAG 回答查询
- `POST /query/hybrid` — 使用两种方式的互惠排名融合回答查询
- `POST /query/graph` — 运行完整的 LangGraph 流程(向量/无向量/混合,带有回退)
- `GET /query/stream` — 服务器发送事件:按令牌流式传输答案
- `POST /chat` — 带会话内存的多轮对话端点
- `GET /chat/{session_id}/history` — 读取会话的回合
- `DELETE /chat/{session_id}` — 清除会话
UI 是纯 HTML/CSS/JS(无构建步骤),因此它与 `uvicorn` 进程一起运行,无需额外工具。如果您想单独托管它,请在 `app.js` 加载之前在 `app/static/index.html` 中设置 `window.__DOCUMIND_API_BASE__` ,并指向 API 运行的位置 — API 端已打开 CORS。
**3. 运行 MCP 服务器** (以便 MCP 兼容的客户端可以将此流程作为工具调用)
```bash
python -m app.mcp.server
```
**4. 或在 Docker 中运行**
```bash
docker compose up --build
```
这会构建镜像,在 `/data` 处挂载命名卷以持久保存 BM25 语料库/向量存储,并在 `http://localhost:8000` 上提供 API + UI。配置仍然来自 `.env` (通过 `docker-compose.yml` 中的 `env_file` 引用)。
## 当前状态说明
这是一个可用的框架,而不是一个黑盒演示:分块器、防卫桩、BM25 检索、混合融合、查询缓存、聊天内存和 FastAPI 端点(共 52 个测试)完全离线运行,并经过单元测试。向量存储、大型语言模型客户端和 MCP 服务器使用真实集成连接,但需要 `GROQ_API_KEY` (和可选的 `chromadb`/LangSmith 凭证)才能访问真实 API。没有密钥,大型语言模型客户端会回退到模拟响应程序,因此即使在开发和演示中,流程仍然可以端到端运行。
已知、如实声明的限制:
- **嵌入**:默认的 `EMBEDDING_PROVIDER=hash` 是一个词袋哈希,不是真正的语义搜索 — 适合在离线情况下连接/测试流程,不适合检索质量。设置 `EMBEDDING_PROVIDER=sentence-transformers` 以获取真正的语义嵌入(需要安装 + 一次模型下载)。
- **防护栏** 是基于正则表达式/模式的提示注入检查,而不是经过训练的分类器 — 它们可以捕获常见的措辞和简单的间距/ unicode 逃避,而不是一个决心对抗的对手。 换入一个托管的防护栏服务,以适应任何超出演示的需求。
- **持久性基于文件,而不是真正的数据库**:BM25 语料库和摄入文档清单现在保存到 `PERSIST_DIR` 并在启动时重新加载(以前它们只存在于内存中,重启时会无声地清除它们),但这仍然是一个本地磁盘上的 JSON 文件,而不是一个适当的存储 — 它在多个工作进程或主机后面无法正常工作。 聊天会话和速率限制器仍然仅限于内存,设计为短暂的、基于会话的状态,不需要生存重启。
- **API 身份验证** 是一个单一的共享 `X-API-Key` ,而不是每个用户的身份验证 — 足够用于演示或内部工具,不适合多租户部署中的真实身份验证。
- **查询缓存是一个简单的内存 TTL 字典** ,不是分布式缓存 — 足够用于单进程部署,并且在每次摄入/删除时失效,因此它不能在文档更改后提供过时的答案,但它不会在多个工作进程之间共享。