用自然语言提问任意 GitHub 代码仓库。输入仓库地址,点击索引,然后直接问问题——系统会自动检索相关代码片段,交由 Claude 给出精确、带引用路径的回答。
Codebase Q&A 是一个本地运行的代码问答工具。它将 GitHub 仓库的源代码拆解成结构化代码块、嵌入向量数据库,然后用 AI Agent 回答开发者关于代码的任何问题:函数实现、架构设计、如何添加新功能、某个 bug 在哪里,等等。
架构总览
┌─────────────────────────────────────────────────────────────────┐
│ Streamlit UI (app.py) │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ 侧边栏:仓库索引 │ │ 主区域:对话界面 │ │
│ │ repo_url 输入框 │ │ 流式输出 Agent 回复 │ │
│ └────────┬─────────┘ └────────────┬─────────────┘ │
└────────────│────────────────────────────────────│───────────────┘
│ 索引流程 │ 问答流程
▼ ▼
┌────────────────────────┐ ┌─────────────────────────────┐
│ indexer 模块 │ │ agent 模块 │
│ │ │ │
│ cloner.py │ │ qa_agent.py │
│ ├── clone_repo() │ │ ├── Agent (Pydantic AI) │
│ └── remove_repo() │ │ ├── ask() │
│ │ │ └── ask_stream() │
│ chunker.py │ │ │
│ ├── _parse_python() │ │ tools.py │
│ │ └── AST 解析 │ │ ├── search_code │
│ ├── _sliding_window() │ │ ├── list_repo_files │
│ └── chunk_repo() │ │ └── read_file │
│ │ └──────────────┬──────────────┘
│ vector_store.py │ │ 调用
│ ├── index_chunks() │◄── 写入 ┌─────────────▼─────────────┐
│ ├── search() │──── 查询 ►│ ChromaDB (本地持久化) │
│ ├── list_files() │ │ all-MiniLM-L6-v2 嵌入 │
│ └── is_indexed() │ └───────────────────────────┘
└────────────────────────┘
│ │
▼ clone ▼ LLM 推理
┌─────────────────┐ ┌───────────────────────┐
│ repos/ 目录 │ │ LLM API │
│ (浅克隆,depth=1)│ │ (SiliconFlow/Anthropic)│
└─────────────────┘ └───────────────────────┘
| 组件 | 版本 / 说明 |
|---|---|
| Pydantic AI | pydantic-ai[anthropic]==1.91.0,Agent 框架,负责工具注册和流式调用 |
| LLM | SiliconFlow Qwen/Qwen3-32B(默认)或 Claude Sonnet(通过 Anthropic API) |
| ChromaDB | 本地持久化向量数据库,存储代码块嵌入 + 记忆库,支持语义检索 |
| sentence-transformers | all-MiniLM-L6-v2 模型,生成代码嵌入向量,完全本地运行 |
| Streamlit | Web UI,提供仓库索引侧边栏和流式对话主界面 |
| GitPython | 执行 depth=1 浅克隆,快速拉取仓库 |
| python-dotenv | 从 .env 文件加载 API Key |
| nest-asyncio | 解决 Streamlit 事件循环与 asyncio.run() 的嵌套冲突 |
完整的数据流分为两个阶段:索引阶段(一次性)和问答阶段(每次提问)。
GitHub URL
│
▼
clone_repo() # GitPython depth=1 浅克隆到 repos/owner__reponame/
│
▼
chunk_repo() # 遍历所有代码文件
├── .py 文件 → _parse_python()
│ ├── ast.parse() 提取 FunctionDef / AsyncFunctionDef / ClassDef
│ ├── 每个函数/类 = 一个 CodeChunk(含精确行号)
│ └── 无函数/类或 SyntaxError → 退回 _sliding_window()
└── 其他扩展名 → _sliding_window()
└── 按 MAX_CHUNK_TOKENS=512 (≈2048 chars) 切分
│
▼
index_chunks() # 写入 ChromaDB
├── 集合名 = "{owner}-{reponame}"(小写,最长 63 字符)
├── 每批 500 条批量写入(避免 SQLite 限制)
└── 重新索引时先删除旧集合再创建
用户输入问题
│
▼
ask_stream() # Pydantic AI Agent.run_stream()
│
├── Agent 决策:调用哪些工具
│ ├── search_code(query) → ChromaDB 语义检索,返回 top-5 代码片段
│ ├── list_repo_files(pattern) → 列出所有已索引文件路径
│ └── read_file(file_path) → 读取完整文件(超 8000 字符截断)
│
▼
LLM # 综合工具结果生成回答(SiliconFlow Qwen3-32B 或 Claude Sonnet)
│
▼
流式 token 输出 → Streamlit 实时渲染
# 1. 克隆本项目
git clone https://github.com/xyma2003/codebase-qa.git
cd codebase-qa
# 2. 创建虚拟环境(推荐)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 3. 安装依赖
pip install -r requirements.txt首次运行时,
sentence-transformers会自动从 Hugging Face 下载all-MiniLM-L6-v2模型(约 90MB),请确保网络畅通。
支持两种 LLM backend:SiliconFlow/OpenAI-compatible(国内默认)和 Anthropic(海外)。
Option A — SiliconFlow(国内推荐):
# .env
OPENAI_API_KEY=sk-...
OPENAI_API_BASE=https://api.siliconflow.cn/v1
OPENAI_MODEL=Qwen/Qwen3-32B获取 key:https://cloud.siliconflow.cn/(有免费额度)
Option B — Anthropic(海外):
# .env
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxx当
OPENAI_API_KEY设了,自动用 SiliconFlow;否则 fallback 到 Anthropic。
所有其他配置项在 config.py 中,可按需修改:
# LLM — .env 里配 OPENAI_API_KEY 则用 SiliconFlow,否则 fallback 到 Anthropic
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_API_BASE = os.getenv("OPENAI_API_BASE", "https://api.siliconflow.cn/v1")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "Qwen/Qwen3-32B")
CLAUDE_MODEL = "claude-sonnet-4-5" # Anthropic fallback 模型
CHROMA_DIR = "chroma_db" # 向量数据库存储目录
CLONE_DIR = "repos" # 仓库克隆目录
EMBED_MODEL = "all-MiniLM-L6-v2" # 嵌入模型(本地运行)
MAX_CHUNK_TOKENS = 512 # 滑动窗口单块最大 token 数streamlit run app.py浏览器会自动打开 http://localhost:8501。
- 在左侧侧边栏的"GitHub 仓库地址"输入框中填入仓库 URL,例如:
https://github.com/tiangolo/fastapi - 点击"克隆并索引"按钮。
- 进度条显示两个阶段:代码解析和向量写入。
- 完成后显示"已索引 ✓"和总代码块数。
已索引的仓库下次打开时会自动检测,无需重新索引。如需重新索引(例如仓库有更新),点击"重新索引"按钮。
索引完成后,在底部输入框直接用自然语言提问,按 Enter 发送。系统支持多轮对话,历史消息会保留在会话中。
这个项目的整体架构是什么?入口在哪里?
用户认证是如何实现的?
search_code 和 list_repo_files 两个函数的区别是什么?
如果我想添加一个新的 API 端点,应该修改哪些文件?
找出所有带 async 关键字的函数,它们分别做什么?
config.py 里有哪些可配置项?
Agent 拥有三个工具,会根据问题类型自主决定调用顺序和组合方式。
async def search_code(ctx: RunContext[RepoDeps], query: str) -> str对已索引的代码库执行语义检索,返回与 query 最相关的 5 个代码片段,每条结果包含文件路径、起止行号、块类型(function/class/other)和代码块名称。
- 适用场景:查找某个函数/类的实现、寻找处理特定逻辑的代码
- Agent 策略:几乎每次提问都会优先调用此工具
async def list_repo_files(ctx: RunContext[RepoDeps], pattern: str = "") -> str列出已索引仓库中的所有文件路径。支持通过 pattern 参数按子字符串过滤,例如传入 "test" 只返回测试相关文件,传入 ".py" 只返回 Python 文件。
- 适用场景:了解项目结构、定位特定类型的文件
- Agent 策略:当需要了解项目整体布局或找到某个文件的位置时调用
async def read_file(ctx: RunContext[RepoDeps], file_path: str) -> str读取指定文件的完整内容。file_path 应为 list_repo_files 返回的相对路径。超过 8000 字符的文件会被截断,并显示总字符数。
- 适用场景:需要了解某个文件的完整上下文,而不仅仅是片段
- Agent 策略:在
search_code找到相关文件后,有时会进一步调用此工具获取完整实现
除了每次问答的即时检索,系统还维护一个持久化记忆库——高质量问答对会自动存入 ChromaDB 的 memory 集合({owner}-{repo}-memory),后续相同/相似问题优先命中记忆,跳过完整的 Agent 推理流程。
用户提问
│
▼
memory.search(question) # 先查记忆库
│
├── 命中(相似度 ≥ 0.45)──► 直接返回历史答案,跳过 Agent
│
└── 未命中 ──► Agent 正常推理(search_code / read_file / ...)
│
▼
memory.quality.evaluate() # 纯 Python 规则评分(0.0-1.0)
│
└── 评分 ≥ 0.6 ──► memory.save() 存入记忆库
不调 LLM,零延迟。评分维度:
| 维度 | 加分项 |
|---|---|
| 技术深度 | 回答含函数名/类名/技术关键词(正则匹配) |
| 引用准确性 | 回答含文件路径(.py/.js/.ts 等) |
| 问答匹配 | 问题与回答有语义关联词 |
| 对话上下文 | 有多轮对话历史 |
扣分项:不确定性词汇("你确定吗"/"能详细说说")、回答过短、回答无技术词。
# config.py
MEMORY_SIMILARITY_THRESHOLD = 0.45 # memory 命中阈值
QUALITY_THRESHOLD = 0.6 # 问答质量评分阈值,>= 此值才保存对所有 .py 文件使用 Python 标准库 ast 模块进行语法树解析:
- 提取所有顶层及一级嵌套的
FunctionDef、AsyncFunctionDef、ClassDef - 每个定义块成为一个独立的
CodeChunk,包含精确的起止行号 - 若文件无任何函数/类(如纯配置文件),或存在
SyntaxError,则退回滑动窗口分块
对其他所有支持的扩展名使用滑动窗口切分:
- 估算方式:1 token ≈ 4 个字符
- 每块上限:
MAX_CHUNK_TOKENS * 4 = 2048字符 - 块类型标记为
"other",名称为文件名
.py .js .ts .tsx .jsx
.go .java .rs .cpp .c
.rb .swift .kt .scala
.sh .yaml .yml .toml .json .md
.git node_modules __pycache__ .venv venv dist build .next vendor
codebase-qa/
├── app.py # Streamlit 主入口,UI 逻辑
├── config.py # 全局配置(模型、路径、扩展名白名单、memory 阈值)
├── requirements.txt # Python 依赖
├── .env # API Key(需自行创建,不提交到 git)
│
├── indexer/ # 仓库索引模块
│ ├── __init__.py # 对外暴露 clone_repo/chunk_repo/index_chunks/is_indexed
│ ├── cloner.py # GitPython 浅克隆,repos/ 目录管理
│ ├── chunker.py # AST 解析 + 滑动窗口,CodeChunk dataclass
│ └── vector_store.py # ChromaDB 读写,嵌入函数,语义检索
│
├── agent/ # AI Agent 模块
│ ├── __init__.py # 对外暴露 ask/ask_stream/RepoDeps
│ ├── qa_agent.py # Pydantic AI Agent 定义,ask() 和 ask_stream()
│ └── tools.py # 三个工具函数 + RepoDeps dataclass
│
├── memory/ # 自学习记忆模块
│ ├── knowledge.py # 记忆库 CRUD(ChromaDB memory 集合)
│ └── quality.py # 问答质量评估(纯 Python 规则,零延迟)
│
├── chroma_db/ # ChromaDB 持久化目录(自动创建)
└── repos/ # 克隆的仓库目录(自动创建)
Q: 索引大型仓库(如 React、Django)时很慢怎么办?
大型仓库可能产生数千个代码块。克隆使用 depth=1 已尽量减少下载量,但解析和嵌入步骤仍需时间。嵌入计算在 CPU 上进行,首次索引耗时与仓库大小成正比,一般 1000 个文件约需 1-3 分钟。索引一次后会持久化,无需重复操作。
Q: 首次启动时卡在下载嵌入模型?
all-MiniLM-L6-v2 模型(约 90MB)首次使用时由 sentence-transformers 自动从 Hugging Face 下载,缓存在本地 ~/.cache/huggingface/。如网络受限,可手动下载后放置到缓存目录,或配置 HF 镜像:
export HF_ENDPOINT=https://hf-mirror.comQ: 如何对已索引的仓库重新索引(仓库有更新)?
在侧边栏输入同一仓库 URL,若检测到已索引,会显示"已索引 ✓"和"重新索引"按钮。点击"重新索引"会清空旧集合并重新执行完整索引流程。注意:本地克隆不会自动 pull,如需拉取最新代码,可手动删除 repos/owner__reponame/ 目录后重新索引。
Q: 私有仓库能否索引?
当前 clone_repo() 直接调用 Repo.clone_from(),依赖本地 git 凭证配置。只要本地 git 有权限 clone 该仓库(SSH key 或 HTTPS token),即可正常使用。
Q: 向量数据库存在哪里?多个仓库会冲突吗?
所有向量数据存储在 chroma_db/ 目录,每个仓库对应一个独立的 ChromaDB collection,命名规则为 {owner}-{reponame}(全小写,最长 63 字符)。多个仓库完全隔离,互不影响。
Q: 会话刷新后聊天记录丢失?
Streamlit 的 session_state 仅在当前浏览器会话内有效,刷新页面后聊天记录会清空。向量索引是持久化的(存在 chroma_db/),重新打开页面后只需再次输入仓库 URL,无需重新索引。
Q: read_file 有字符限制,读不到完整文件怎么办?
read_file 工具当前截断阈值为 8000 字符。对于超长文件,可以在提问时指明需要看的具体部分,Agent 通常会先用 search_code 定位到相关代码片段,再按需调用 read_file,从而避免读取整个文件。