内容
# 天文逻辑检索系统
一个自我托管、始终保持更新的 **基于Anthropic GitHub组织的检索增强生成(RAG)流程** ——
基于真实文档、问题、拉取请求和评论(不包括源代码)进行扎根问答。它每小时通过增量摄入刷新
并通过MCP服务器为AI代理提供答案。
我构建这个系统是为了从头到尾学习 [LlamaIndex](https://www.llamaindex.ai/) ——
从抓取和嵌入到混合检索、重新排序、生成、评估和实时服务 ——
在一个具有实际变动部分的问题上,而不是使用玩具数据集。这是一个个人项目,而不是生产软件,
但每个设计决策都是经过深思熟虑和有文档记录的。
## 功能概述
询问它类似 *"Python SDK中是否有关于流媒体的最新拉取请求?"* 的问题,它会:
1. 检索它所摄入的最相关的文档/问题/拉取请求/评论,
2. 重新排序它们,以便最强的几个排在前面,
3. 编写一个仅基于这些来源的答案 —— 并返回源ID,以便可以检查原始内容。
如果语料库无法支持答案,流程会 **拒绝** 而不是编造一个答案。
这种拒绝行为是一个首要目标,并在评估集中明确衡量。
语料库故意狭窄和专注:两个Anthropic SDK仓库
(`anthropic-sdk-python` 和 `anthropic-sdk-typescript`),
并且仅包括它们的 **文档 + 问题/拉取请求/评论** —— 不包括源代码。
摄入整个代码库会使向量存储膨胀,但对问答价值不大;
有用的信号存在于文档和讨论中。
## 架构概览
左侧保持知识更新;右侧根据知识回答问题:
```mermaid
flowchart LR
GH["Anthropic GitHub<br/>(文档 + 问题/拉取请求/评论)"] --> ING["摄入<br/>读取 → 嵌入 → 存储"]
ING --> STORE[("知识存储<br/>向量 + 原始文本")]
Clock["🕐 每小时"] -.->|"仅拉取更改的内容"| ING
Agent["AI 代理<br/>提出问题"] --> ANS["检索 → 重新排序 → 答案"]
STORE --> ANS
ANS -->|"扎根答案 + 源ID"| Agent
```
每个答案都基于从Anthropic的GitHub实际拉取的文档 —— 没有任何内容是虚构的。
## 问题如何变成答案
检索是流程的核心。两个搜索策略并行运行,它们的結果
被融合,并且在LLM看到任何内容之前,更慢但更准确的模型会重新排序:
```mermaid
flowchart LR
Q["问题"] --> Two{"两种搜索方式"}
Two -->|"语义<br/>(向量搜索)"| V["Qdrant<br/>稠密向量"]
Two -->|"关键词<br/>(BM25)"| B["Redis<br/>原始文本"]
V --> Fuse["合并两个列表<br/>(RRF)"]
B --> Fuse
Fuse -->|"~30 个候选项"| RR["重新排序<br/>保留最佳 ~6 个"]
RR --> LLM["从这 6 个中合成答案<br/>"]
LLM --> Out["答案 + 源ID"]
```
将语义(语义)搜索与词汇(关键词)搜索相结合在这里很重要,因为许多
问题依赖于精确的令牌 —— 函数名、拉取请求编号、错误字符串 ——
而仅使用向量搜索会变得模糊。然后,交叉编码器重新排序器确保LLM仅推理
质量最高的几个块。
## 工作范围
LlamaIndex提供了重型、通用的机械设备 ——
摄入管道、密集 + BM25融合检索器、重新排序钩子、响应合成器和评估框架。
该项目中的工程工作围绕这些缝隙:
- **抓取 + 窗口化** —— 拉取文档以及过去7天的issue/PR/评论活动,
过滤掉源代码,并为PR审查评论覆盖读取器间隙。
- **增量新鲜度** —— 水印用于仅获取更改的内容,以及内容哈希用于
仅重新嵌入更改的内容。
- **打包** —— 整个系统作为一个命令的Docker堆栈。
- **MCP表面** —— 将流程公开为AI代理可以调用的工具。
- **评估** —— 精心策划的黄金问答集,包括流程应拒绝的超出范围的问题。
贯穿始终的指导原则:包装,而不是重新实现 —— 保持自定义代码的薄度。
## 保持新鲜
实时流程只有在保持当前状态而无需每小时重新做所有工作时才有用。
两个独立的机制处理这个问题,每个机制解决不同的问题:
```mermaid
flowchart TB
Tick["🕐 每小时触发"] --> WM{"水印<br/>(SQLite)"}
WM -->|"请求仅更新的项目<br/>自上次运行以来"| Fetch["获取更改的活动<br/>+ 当前文档"]
Fetch --> Hash{"内容哈希<br/>(Redis / UPSERTS)"}
Hash -->|"未更改 → 跳过"| Skip["无需操作"]
Hash -->|"已更改 → 嵌入"| Embed["重新嵌入该文档"]
```
- **水印** 缩小了从GitHub下载的内容 —— 仅更新的项目自上次运行以来。
- **内容哈希** 缩小了重新嵌入的内容 —— 如果文档的文本未更改,
则其哈希值匹配并跳过。
两个机制都不能替代对方,它们共同使流程重新运行变得幂等:
立即运行另一个流程会产生 **零** 新的嵌入。
## 技术栈
| 层 | 工具 | 原因 |
|---|---|---|
| 框架 | **LlamaIndex** | 开箱即用的摄入、检索、合成和评估 |
| 提取 | **LlamaHub GitHub 读取器** (仅限文档过滤) | 文档 + 问题/拉取请求,无需手动编写API |
| 嵌入 | **Ollama `nomic-embed-text`** (本地,稠密,768维) | 免费,离线,CPU友好 |
| 生成 + 评估裁判 | **`deepseek-v4-pro:cloud`** 通过Ollama Cloud | 一个强大的远程模型,API密钥控制 |
| 向量存储 | **Qdrant** | 稠密向量 |
| 文档存储 + 重复数据删除 | **Redis** | 原始文档 + 内容哈希重复数据删除(UPSERTS) |
| 水印 | **SQLite** | 持久的每个仓库同步光标 |
| 检索 | **混合稠密 + BM25** ,与RRF融合 | 语义和词汇信号结合 |
| 重新排序器 | **`bge-reranker-base`** 交叉编码器 | 锐化顶部结果,无API依赖 |
| 调度器 | **APScheduler** (每小时,在容器内) | 驱动增量摄入 |
| 服务 | **FastMCP** 通过Streamable HTTP | 将流程公开为代理工具 |
| 评估 | **RAGAS + 原生LlamaIndex + 黄金集** | 三个独立的答案质量视图 |
将本地嵌入与云生成分开意味着 **无需GPU即可托管此系统**:
嵌入器和重新排序器足够小,可以使用CPU,而一个大型模型在云中运行。
## 快速入门(本地Docker)
两个凭证是 **必需的** —— 堆栈如果没有它们将无法工作:
- `GITHUB_TOKEN` —— GitHub [个人访问令牌](https://github.com/settings/tokens)
(经典, `public_repo` 范围足够)—— 用于获取文档 + 问题/拉取请求/评论。
- `OLLAMA_CLOUD_API_KEY` —— [Ollama Cloud](https://ollama.com) API密钥 —— 用于答案
生成和评估裁判。
```bash
cp .env.example .env # 填写 GITHUB_TOKEN + OLLAMA_CLOUD_API_KEY
# (可选)设置 MCP_AUTH_TOKEN 以在MCP端点要求 bearer token
docker compose up -d --build
```
这将启动四个服务:`qdrant`、`redis`、`ollama` (一次性 `ollama-init`
首先将 `nomic-embed-text` 拉取到卷中)和 `app`。在第一次启动时,`app` 容器
**种子一次,然后提供服务** —— 它在MCP服务器接受查询之前会摄入 `config/repos.yaml`
中的仓库,因此第一次启动需要几分钟(嵌入在CPU上运行)。使用 `docker compose logs -f app`
监视进度;当看到 `MCP server starting on 0.0.0.0:8000` 时,服务器已就绪。
> 重新 `up` 很便宜:模型已经缓存在 `./ollama_data` 中,并且内容哈希重复数据删除
> 意味着未更改的文档永远不会重新嵌入。
## 连接AI代理
服务器通过 `http://<host>:8000/mcp` 上的 **MCP over Streamable HTTP** 进行通信。
如果设置了 `MCP_AUTH_TOKEN`,请将其作为 `Authorization: Bearer <token>` 发送。
| 工具 | 功能 |
|------|--------------|
| `answer(query)` | 基于 **所有内容** (文档 + 问题/拉取请求/评论)的扎根答案。 |
| `answer_prs` / `answer_issues` / `answer_comments` / `answer_docs` | 相同,范围限定为一种内容类型。 |
| `list_documents(category=None, repo=None, limit=50, offset=0)` | 浏览原始文档存储 —— 分页,可选类别/仓库过滤。返回 `doc_id`s。 |
| `get_documents(doc_ids)` | 按ID获取原始文档(ID来自 `answer*` 或 `list_documents`)。 |
一个可粘贴的、与代理无关的技能文件,描述何时以及如何使用这些文件,位于
[`anthropic-rag-agent-skill.md`](anthropic-rag-agent-skill.md)。
示例客户端配置(Claude Desktop / Cline / 任何支持HTTP的MCP客户端):
```json
{
"mcpServers": {
"anthropic-rag": {
"type": "streamable-http",
"url": "http://localhost:8000/mcp",
"headers": { "Authorization": "Bearer <MCP_AUTH_TOKEN>" }
}
}
}
```
如果未设置 `MCP_AUTH_TOKEN`,请省略 `headers`。从LAN上的另一台机器替换
`localhost` 为主机IP。
## 设计原则
几个刻意的选择决定了如何构建此系统:
- **本地嵌入,云生成** —— 在容量大的地方优化成本,在可见的地方优化质量。
- **同时存储含义和原始文本** —— 支持向量和关键词检索。
- **混合检索 + 更大的模型重新排序** —— 首先获取正确的几个块。
- **两阶段新鲜度** —— 减少获取的内容,然后减少重新嵌入的内容。
- **评估后再扩展** —— 如果无法衡量,则无法改进。
- **在执行重任的框架上保持薄层** 。
完整的设计原理 —— 每个“为什么选择X而不是Y”的决定 —— 都在 [`docs/SPEC.md`](docs/SPEC.md) 中。
*A个人学习项目,基于 [LlamaIndex](https://www.llamaindex.ai/)、
[Qdrant](https://qdrant.tech/)、 [Redis](https://redis.io/) 和
[FastMCP](https://github.com/jlowin/fastmcp) 构建。不是生产软件。*