内容
# AskRepo
本地代码库助手。将指向git仓库并提出问题,例如 *"身份验证在哪里处理?"* 或 *"这个函数的作用是什么?"* - 它会用精确的 `文件:行号` 位置引用来回答。
<p align="center">
<img src="docs/demo.svg" alt="askrepo ask, answering a question with a code citation" width="820">
</p>
使其工作良好的两个关键因素:
- **AST分块**。代码通过tree-sitter按结构划分 - 每个函数、方法或类一个分块 - 而不是固定的行窗口。分块在语义上保持完整,每个分块都准确知道其行范围,因此引用是精确的。
- **确定性引用**。模型仅引用编号的代码段(`[1]`, `[2]`)。每个代码段的 `文件:行号` 从分块元数据中解析,而不是由模型编写,因此它不会虚构行号。未被引用的代码段从答案中删除。
所有操作都在本地运行:
- 通过 [Ollama](https://ollama.com) 的 `qwen3:14b` 生成答案,
- 通过 [fastembed](https://github.com/qdrant/fastembed) (ONNX)的 `jina-embeddings-v2-base-code` 嵌入代码 - 无需torch,无需transformers,
- `jina-reranker-v2-base-multilingual`(跨编码器,ONNX)作为可选的重新排序步骤 - 默认关闭,因为消融实验表明它在这里有害,
- [LanceDB](https://lancedb.com) 存储向量并进行混合密集+全文搜索。
## 安装
```bash
python -m venv venv && source venv/bin/activate
pip install -e . # 或:pip install -r requirements.txt
```
您需要Ollama运行并已拉取答案模型:
```bash
ollama pull qwen3:14b
```
嵌入和重新排序模型将在首次使用时自动下载。
## 使用
```bash
# 索引仓库(增量 - 仅更改的文件重新嵌入)
askrepo index /path/to/repo
# 提问
askrepo ask "身份验证在哪里处理?" --repo /path/to/repo
askrepo ask "登录函数的作用是什么?" --repo /path/to/repo
```
如果您使用 `pip install -e .` 安装,则 `askrepo` 命令在您的PATH上;否则,请使用 `python -m askrepo ...`。
### 搜索内容
测试和文档默认情况下从结果中排除。它们使用与问题相同的词语描述实现,因此它们在标记的查询集中排名靠前,在 **36%** 的查询中占据第一名,并占据近三分之一的 top-5 位置。将其排除是堆栈中最大的排名改进(参见消融实验)。
过滤器在问题关于测试或文档时自动关闭,因此仍然有效:
```bash
askrepo ask "引用如何映射到文件和行号?" # 实现
askrepo ask "引用映射在哪里测试?" # 自动保留测试
askrepo ask "..." --include-tests --include-docs # 强制所有内容都包含在内
```
### 作为MCP工具使用(Claude Code等)
AskRepo可以作为 [MCP](https://modelcontextprotocol.io) 服务器运行,暴露一个只读的 `search_code` 工具。将其指向一个已经索引的仓库,代理可以通过语义搜索您的代码库,获得精确的 `文件:行号` 匹配,这与普通的grep互补。
```bash
pip install -e ".[mcp]"
askrepo index /path/to/repo # 首先索引
```
将其添加到您的MCP客户端。对于Claude Code,在 `.mcp.json` 中:
```json
{
"mcpServers": {
"askrepo": {
"command": "askrepo-mcp",
"env": { "ASKREPO_MCP_REPO": "/path/to/repo" }
}
}
}
```
代理然后调用
`search_code(query, k=8, rerank=..., expand=..., include_tests=..., include_docs=...)`;
`expand=true` 还会返回匹配函数调用的定义。该工具永远不会写入任何内容。
每个标志默认为服务器环境,代理可以为每个搜索覆盖它。搜索成本约为20毫秒,这使其成为代理重复使用的实用工具。
### 评估检索
```bash
askrepo eval --repo /path/to/repo --k 5
```
评估检索堆栈 - `dense`, `fts`, `hybrid`, `hybrid+scope`, `+rerank` - 并报告hit@1, hit@k, recall@k, MRR, symbol-hit@k,测试/文档浪费的槽位比例和延迟,针对标记的查询集(`<repo>/eval/queries.json`)。它仅进行检索(无LLM),因此速度快且确定性。 `--with-llm` 还消融了HyDE。有关格式,请参见 `eval/queries.json`。
此仓库的结果(35个查询,k=5),显示每个阶段的改进:
| 配置 | hit@1 | hit@5 | MRR | 测试% | 延迟 |
|---|---|---|---|---|---|
| dense | 0.657 | 0.857 | 0.744 | 38% | 11 毫秒 |
| fts | 0.371 | 0.743 | 0.499 | 22% | 5 毫秒 |
| hybrid | 0.600 | 0.914 | 0.726 | 23% | 17 毫秒 |
| hybrid+rerank | 0.514 | 0.914 | 0.695 | 15% | 约5.3秒 |
| **hybrid+scope** | **0.829** | **0.943** | **0.863** | **5%** | **18 毫秒** |
| hybrid+scope+rerank | 0.743 | 0.943 | 0.833 | 6% | 约2.7秒 |
混合检索优于单独的检索器。最大的排名改进是作用域过滤:保持测试和文档排除在候选集之外,将hit@1从0.60提升到0.83,并将浪费的槽位从23%减少到5%,无需额外成本。
交叉编码器重新排序器 **默认关闭**。它不会改变召回率(hit@5 仍然为0.943),但会错误地移动排名第一的位置:在35个查询中,它修复了2个,破坏了5个。七个不一致的查询不足以称为回归(确切的McNemar p = 0.45),但这就是重点 - 它无法证明收益,而且每次查询的成本约为2.7秒。
失败模式一致,足以命名。每次重新排序错误时,它都会提升一个 *更长的* 分块,而不是更短的分块 - 平均1034个字符,而不是491个字符 - 而被提升的分块通常是提及主题词汇的分块,而不是实现主题的分块:
| 问题 | 重新排序器降级 | 重新排序器提升 |
|---|---|---|
| fastembed如何生成代码嵌入? | `embedder.py` `Embedder` | `config.py` `Config` (列出每个`ASKREPO_*`名称) |
| 引用如何映射到文件和行号? | `answer.py` `_cited_indices` | `chunker.py` `_line_fallback` |
在QA场景中,受过训练的交叉编码器奖励表面重叠和长度偏差,因此大型枚举分块胜过实际执行工作的较小函数。它还丢弃RRF排序,而是完全替换它,因此弱信号覆盖正确的先验。换成更轻量的 `ms-marco-MiniLM` 模型不会改变这一点(hit@1 0.714,MRR 0.807)- 这是重新排序步骤,而不是模型。
它仍然可以通过 `--rerank` / `ASKREPO_RERANK=1` 为某些语料库提供服务。这是删除旧符号增强启发式的方法 - 消融实验决定。
## 工作原理
```
git仓库 -> 导入 -> AST分块 -> 嵌入器 -> LanceDB
|
问题 -> 检索器(密集+全文,RRF,作用域过滤)-> 上下文 -> qwen3 -> 答案 + 引用
```
| 模块 | 责任 |
|--------|----------------|
| `ingest.py` | 遍历仓库,遵循嵌套 `.gitignore`,检测语言,哈希文件以进行增量索引。跳过符号链接和超大/二进制文件。 |
| `chunker.py` | 使用tree-sitter进行AST分块;对于不支持的文件,使用重叠的行窗口回退。 |
| `embedder.py` | jina(fastembed/ONNX)或ollama/nomic嵌入。 |
| `store.py` | LanceDB表 + 文件哈希清单。 |
| `retriever.py` | 混合检索:RRF超过密集+全文,作用域过滤,调用图展开。 |
| `reranker.py` | 可选的候选集交叉编码器重新排序(默认关闭)。 |
| `answer.py` / `llm.py` | 提示组装,生成,确定性 `[n]` 引用映射。 |
| `cli.py` | `index` / `ask` / `eval`。 |
| `mcp_server.py` | 代理的只读 `search_code` MCP工具。 |
## 配置
所有设置都可以通过环境变量覆盖:
| 变量 | 默认值 | 含义 |
|----------|---------|---------|
| `ASKREPO_EMBED_BACKEND` | `jina` | `jina`(fastembed)或 `ollama`(nomic) |
| `ASKREPO_LLM` | `qwen3:14b` | Ollama答案模型 |
| `ASKREPO_RERANK` | `0` | 交叉编码器重新排序器(启用为 `1`;请先查看消融实验) |
| `ASKREPO_RERANKER` | `jinaai/jina-reranker-v2-base-multilingual` | 重新排序器模型(更轻量:`Xenova/ms-marco-MiniLM-L-6-v2`) |
| `ASKREPO_TOPK` | `8` | 放入答案上下文的分块数 |
| `ASKREPO_EXCLUDE_TESTS` | `1` | 从结果中删除测试文件(当查询关于测试时自动关闭) |
| `ASKREPO_EXCLUDE_DOCS` | `1` | 从结果中删除markdown/config文件(对文档问题自动关闭) |
| `ASKREPO_ENRICH` | `1` | 为每个分块嵌入路径/符号头部 |
| `ASKREPO_CONTEXTUAL` | `0` | 在索引时为每个分块添加LLM单行摘要(慢) |
| `ASKREPO_HYDE` | `0` | 将查询转换为假设代码片段并嵌入 |
| `ASKREPO_EXPAND` | `0` | 跟随调用图并检索调用的函数定义 |
| `ASKREPO_EXPAND_DEPTH` | `2` | `--expand` 跟随的调用层数 |
| `ASKREPO_EXPAND_CALLERS` | `1` | 使用 `--expand` 时,还添加几个结果的调用站点 |
| `ASKREPO_HOME` | `~/.askrepo` | 索引存储位置 |
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama端点 |
### 检索质量
- **丰富**(默认开启):嵌入文本以文件路径和符号为前缀,有助于自然语言问题与代码匹配。
- **上下文检索**(`askrepo index --contextual`):为每个分块生成一行LLM摘要并嵌入。概念问题质量更高,但索引速度慢 - 按需选择。
- **HyDE**(`askrepo ask --hyde`):将问题转换为假设代码片段并嵌入。每个问题额外进行一次LLM调用。
- **调用者展开**(`askrepo ask --expand`):检索后,跟随调用图 - 匹配分块调用的函数,然后调用的内容,展开到 `--expand-depth` 级别(默认为2)- 并将定义添加到上下文中,以便答案可以追溯到实际发生的位置并引用整个调用链。
每个都是独立的索引(或查询模式),因此使用 `askrepo eval` 查看在您的仓库中它是否实际有效,然后再开启。
索引按仓库路径 *和* 嵌入模型进行键控,因此切换后端或模型会构建单独的索引,而不是混合不兼容的向量。 `jina` 和 `nomic` 都是768维,但不可互换 - 切换后重新索引。
## 开发
```bash
pip install -e ".[dev]"
pytest
```
测试涵盖分块器(AST边界、装饰器、箭头函数、回退)、确定性引用映射和导入安全(符号链接和大小处理)。它们不需要Ollama或任何模型下载。
## 作用域
v1是只读问答。没有代码修改 - 检索器和答案层分开,因此可以在以后添加更改提案步骤而无需重写。
## 许可证
MIT - 参见 [LICENSE](LICENSE)。