# paper-qa-lang
**Repository Path**: bllose/paper-qa-lang
## Basic Information
- **Project Name**: paper-qa-lang
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2026-05-07
- **Last Updated**: 2026-05-13
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# paper-qa-lang
PaperQA 的 LangChain + LangGraph + MCP 重构版本 — 一个面向学术论文的 RAG 问答系统。

## 架构
``` text
┌─────────────────────────────────────────────────┐
│ 前端 (frontend/) │
│ 纯 HTML/CSS/JS 单页应用 │
└─────────────────────┬───────────────────────────┘
│ SSE streaming
┌─────────────────────▼───────────────────────────┐
│ API 层 (api/) │
│ FastAPI — 文件上传 / 聊天接口 │
└─────────────────────┬───────────────────────────┘
│
┌─────────────────────▼───────────────────────────┐
│ 核心库 (src/paper_qa_lang/) │
│ ┌──────────┬──────────┬──────────┬──────────┐ │
│ │ parsing │ store │ graph │ chat │ │
│ │ PDF解析 │ChromaDB │LangGraph │ 对话引擎 │ │
│ │ 分块增强 │ SQLite │ 工作流 │ CLI/API │ │
│ └──────────┴──────────┴──────────┴──────────┘ │
└─────────────────────┬───────────────────────────┘
│ MCP protocol
┌─────────────────────▼───────────────────────────┐
│ MCP 服务层 (mcp/) │
│ ┌──────────────────────┬─────────────────────┐ │
│ │ paper-metadata-mcp │ paper-download-mcp │ │
│ │ Crossref / OpenAlex │ ArXiv / Unpaywall │ │
│ │ Semantic Scholar │ Semantic Scholar │ │
│ └──────────────────────┴─────────────────────┘ │
└─────────────────────────────────────────────────┘
```
## 项目结构
``` text
paper-qa-lang/
├── src/paper_qa_lang/ # 核心库
│ ├── chat/ # 对话引擎 + CLI
│ ├── config/ # 配置系统 (Pydantic Settings)
│ ├── embeddings/ # Embedding 模型 (Qwen)
│ ├── graph/ # LangGraph 工作流 (ingestion / ReAct)
│ ├── helper/ # 工具 (proxy 检测等)
│ ├── ingestion/ # 论文识别与导入
│ ├── models/ # 数据模型 (Paper, Chunk 等)
│ ├── parsing/ # PDF 解析 / 分块 / 上下文增强
│ ├── prompts/ # Prompt 模板
│ ├── store/ # ChromaDB 向量存储 + PaperLibrary
│ └── utils/ # 工具函数
├── mcp/
│ ├── paper-metadata-mcp/ # MCP: 元数据查询 (6 tools)
│ └── paper-download-mcp/ # MCP: 论文下载 (4 tools)
├── api/ # FastAPI Web 服务
├── frontend/ # Web 前端
└── tests/ # 测试
```
## 快速开始
### 1. 环境准备
Python >= 3.11
### 2. 安装
```bash
# 核心依赖
pip install -e .
# 完整安装 (含所有功能)
pip install -e ".[all]"
# 按需安装
pip install -e ".[parsing,store]" # PDF 解析 + 向量存储
pip install -e ".[chat]" # Web API + CLI
pip install -e ".[llm]" # LLM 集成
```
### 3. 配置
```bash
cp .env.example .env
```
按需编辑 `.env`,至少配置一个 LLM provider 的 API Key:
```env
ANTHROPIC_API_KEY=sk-your-api-key-here
ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
```
支持三个 LLM provider:
| Provider | 环境变量 |
|----------|----------|
| Anthropic (含 DeepSeek 兼容) | `ANTHROPIC_API_KEY` + `ANTHROPIC_BASE_URL` |
| OpenAI (含 Qwen 兼容) | `OPENAI_API_KEY` + `OPENAI_BASE_URL` |
| Google Gemini | `GOOGLE_API_KEY` |
### 4. 运行
**CLI 对话:**
```bash
python -m paper_qa_lang.chat.cli
```
**Web API:**
```bash
uvicorn api.main:app --reload --port 8000
# 打开 http://localhost:8000
```
**MCP 服务:**
```bash
cd mcp/paper-metadata-mcp
python server.py
```
## MCP 工具
### paper-metadata-mcp (元数据查询)
| 工具 | 功能 | 数据源 |
|------|------|--------|
| `query_by_doi` | DOI 查完整元数据 | 聚合所有源 |
| `query_by_title` | 标题查元数据 | Crossref + OpenAlex |
| `get_bibtex` | BibTeX 引用格式 | Crossref |
| `get_citation_count` | 引用数 | Semantic Scholar |
| `get_open_access_url` | 开放获取 PDF 链接 | Unpaywall + OpenAlex |
| `bulk_query` | 批量查询 | 聚合所有源 |
### paper-download-mcp (论文下载)
| 工具 | 功能 | 数据源 |
|------|------|--------|
| `download_by_doi` | DOI 下载论文 PDF | ArXiv + Unpaywall |
| `download_by_title` | 标题搜索并下载 | ArXiv + Semantic Scholar |
| `check_availability` | 检查可下载来源 | 所有源 |
| `get_download_status` | 查询下载进度 | 本地缓存 |
### 配置到 Claude Desktop
```json
{
"mcpServers": {
"paper-metadata": {
"command": "python",
"args": ["mcp/paper-metadata-mcp/server.py"],
"env": {
"UNPAYWALL_EMAIL": "your@email.com"
}
}
}
}
```
## 模块概览
| 模块 | 文件 | 职责 |
|------|------|------|
| `parsing` | `pdf_parser.py` / `chunking.py` / `enrichment.py` | PDF 文本提取、智能分块、元数据增强 |
| `store` | `chroma_store.py` / `paper_library.py` | ChromaDB 向量存储、SQLite 论文库管理 |
| `graph` | `ingestion.py` / `react.py` | LangGraph 工作流:论文摄入、ReAct 问答 |
| `chat` | `engine.py` / `cli.py` | 对话引擎、CLI 交互界面 |
| `config` | `settings.py` | 统一配置 (Pydantic Settings + .env) |
## 问题分类与路由
ChatEngine 在收到用户消息后,先通过嵌入式模型进行零样本分类,再根据类别走不同的回答路线。
### 分类架构
```text
用户问题
→ BGE Embedding 编码(与 exemplar 使用同一编码方法,无检索前缀)
→ 与预计算的 exemplar 向量逐一计算余弦相似度
→ 按类别取 Top-K 均值 + 边距判断
→ 三类输出:
├─ 打招呼/自我介绍 → 小模型直接回答(不搜知识库,不统计 token)
├─ AI技术/论文 → 知识库搜索 + 主模型 RAG 回答
└─ 其他 → 主模型直接回答(不搜知识库)
```
### 术语表
分类依赖预定义的 exemplar 列表,存储在 `src/paper_qa_lang/data/` 下:
| 文件 | 类别 | 例句数 |
|------|------|--------|
| `greetings.json` | 打招呼/自我介绍 | 28 |
| `ai_tech.json` | AI技术/论文 | 40 |
Agent 启动时加载所有 exemplar,通过 `BgeEmbedding.embed_documents()` 批量预计算向量。运行时对外部输入仅做一次 `embed_documents()` 推理,开销恒定。
### 算法:Top-K 均值 + 边距
单条 exemplar 匹配容易被噪音误导(BGE 空间中短中文对话句天然聚集)。改为类别级判断:
1. **Top-K 均值** — 与每类别相似度最高的 K 条 exemplar 取平均,作为该类别得分
2. **绝对阈值** — 最高类别得分 ≥ `threshold` 才有效
3. **边距检查** — 最佳类别必须领先 runner-up ≥ `margin`,否则归"其他"
```
"今天星期几" → greetings Top5均值=0.62 AI_tech Top5均值=0.58
→ 边距 0.04 < margin(0.06) → "其他" ✓
```
### 配置参数
```python
# settings.py → ClassifierSettings
threshold: float = 0.55 # 最低 Top-K 均值
margin: float = 0.06 # 必须领先 runner-up 的边距
top_k: int = 5 # 每类别取前 K 条均值
```
### 小模型配置
打招呼/自我介绍走 `SmallChatSettings` 独立配置,凭据解析优先级:
```
显式赋值 → SMALL_{PROVIDER}_API_KEY → {PROVIDER}_API_KEY(回退共享)
```
```bash
# .env 示例
SMALL_MODEL_NAME=gpt-4o-mini
SMALL_OPENAI_API_KEY=sk-xxx # 不设则回退到 OPENAI_API_KEY
SMALL_OPENAI_BASE_URL=https://api.openai.com/v1
```
### 新增文件
| 文件 | 职责 |
|------|------|
| `chat/classifier.py` | `QuestionClassifier` — 零样本分类,Top-K 均值 + 边距 |
| `data/greetings.json` | 打招呼/自我介绍术语例句 |
| `data/ai_tech.json` | AI 技术/论文术语例句 |
| `config/settings.py` | 新增 `SmallChatSettings`、`ClassifierSettings` |
## 上下文宽度监控
### 数据流
```text
用户输入
→ engine.py: 构建 messages = [system(+检索chunks), ...history, user]
→ llm.get_num_tokens_from_messages(messages) ← 预计算 input_tokens
→ SSE → 前端即时显示「输入: N tokens」
→ llm.astream(messages) ← 流式调用
→ chunk.usage_metadata 捕获最后一个 chunk 的用量 ← 实际 output + input
→ SSE → 前端累加 output,修正 input,显示累计总消耗
```
### 上下文的三部分构成
每条 LLM 调用的 `input_tokens` 由以下三部分拼接而成:
| 来源 | 内容 | 变化规律 |
| ---- | ---- | -------- |
| System Prompt | 基础指令 + 论文检索结果(chunks 需通过余弦相似度阈值 `score_threshold=0.3` 才会纳入) | 每次检索后重新构建 |
| 对话历史 | 历史 user / assistant 消息(受 `max_history` 截断) | 逐轮累积增长 |
| 当前消息 | 用户最新输入 | 每条不同 |
`messages = [SystemMessage(含检索结果), *历史, HumanMessage(当前)]`
### 预计算与修正
`get_num_tokens_from_messages()` 是 LLM provider 的客户端/服务端 tokenizer 估算值。流式结束后,从最后一个 `AIMessageChunk` 的 `usage_metadata` 中提取 LLM 真实计费数据,与预估值对比并自动修正。
- **Anthropic** (`ChatAnthropic`): 调用官方 `messages.count_tokens` API,服务端精确计数,`stream_usage=True` 默认开启
- **OpenAI** (`ChatOpenAI`): 使用 `tiktoken` 客户端估算,`stream_usage` 默认开启
两个关键参数:
| 参数 | 位置 | 用途 |
| ---- | ---- | ---- |
| `stream_usage=True` | LLM 构造参数 | 流式最后一个 chunk 携带 `usage_metadata` |
| `score_threshold=0.3` | `ChatEngine` | 低于此值的 chunks 不纳入上下文 |
### SSE 事件协议
后端 `astream_chat` 产出的结构化事件经 `api/main.py` 透传为 SSE:
| 事件 type | JSON 结构 | 前端行为 |
| --------- | --------- | -------- |
| `input_tokens` | `{"type":"input_tokens", "count": N}` | 刷新 `#input-tokens`,累加入累计输入 |
| `token` | `{"type":"token", "content":"..."}` | 流式拼接 render markdown |
| `usage` | `{"type":"usage", "input_tokens":N, "output_tokens":M}` | 累加 output,修正 input 差值 |
### 页面展示
stats 栏同一行显示:
```html
输入: 0
累计消耗: 0
```
前端维护的状态由 [app.js](frontend/app.js) 的 `ChatApp` 管理:
```text
#lastInputTokens → 最新一次输入宽度 (每次发送消息时刷新)
#totalInputTokens → 历史累计输入
#totalOutputTokens → 历史累计输出
totalConsumedTokens → getter: input + output
```
每次发送新消息时,`input-tokens` 立刻刷新为当次上下文宽度,`total-tokens` 持续累加——用户可同时看到单次开销和会话总计。
## 测试
```bash
pip install -e ".[all]"
pytest tests/ -v
```
## 技术栈
- **LLM 框架** — LangChain 1.2 / LangGraph 1.1
- **向量数据库** — ChromaDB
- **PDF 解析** — PyMuPDF
- **Web** — FastAPI + SSE streaming
- **协议** — MCP (Model Context Protocol)
- **配置** — Pydantic Settings + python-dotenv
- **Embedding** — HuggingFace Transformers (BGE / Qwen)