
1. 从一次安全声明里掉出来的索引细节你可能已经在用 Cursor 写代码但未必想过它为什么能读懂整个仓库。前段时间 Cursor 在安全与合规页面里公开了代码库索引的技术流程本意是说明数据怎么处理结果把索引的工程实现也一并交代了扫描文件算哈希、构建 Merkle 树同步、定期上传变更、服务端分块嵌入、按哈希索引去重、推理时做最近邻搜索。这套流程本身不神秘但把它拆开看你会发现每一环都能在自己的项目里复现。代码库索引要解决的问题很具体一个几万文件的仓库怎么让模型在提问时快速找到相关代码片段。全量塞进上下文不现实每次重新扫描又太慢所以需要增量同步加向量检索。Merkle 树负责哪些文件变了哈希值负责这段内容是否已索引过最近邻搜索负责哪几块代码和问题最相关。三者串起来就是一套可落地的索引骨架。这篇面向想理解 AI 编程工具索引原理的开发者我会用 Python 写一套最小可跑的索引流程包含哈希分块、Merkle 树构建、本地最近邻检索三个部分。你不需要 GPU也不需要外部向量数据库用标准库加一个轻量依赖就能验证整条链路。跑完之后你对 Cursor 那套流程的理解会从听说过变成我实现过。2. 前置准备TaoToken 与本地环境2.1 为什么这里会用到 TaoToken上面的索引流程里分块和嵌入是服务端做的事。如果你想在本地复现嵌入 最近邻这一段就需要一个能调 embedding 和对话模型的接口。TaoToken 提供统一的 API 入口兼容常见的 OpenAI 风格调用方式适合用来做这类验证。它的官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 路径不带 UTM 参数。你需要先拿到一个 API Key在控制台里创建即可https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。创建后复制保存后面配置环境变量会用到。如果你只是想先看看模型对话效果可以走模型对话入口https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。2.2 本地依赖我用的环境是 Python 3.10依赖只有两个requests 用于调 APInumpy 用于向量计算。Merkle 树和哈希分块全部用标准库 hashlib 实现不引入额外包。pip install requests numpy环境变量配置如下把 Key 换成你自己的export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api注意API 地址写 https://taotoken.net/api 即可不要在后面拼多余的路径具体端点由代码里的相对路径决定。3. 可复制配置哈希分块与 Merkle 树3.1 文件扫描与哈希计算第一步和 Cursor 流程一致扫描目录对每个文件算哈希。这里用 SHA-256把文件内容读成字节后直接哈希。为了让结果稳定我按相对路径排序后再处理。import hashlib import os def hash_file(path: str) - str: h hashlib.sha256() with open(path, rb) as f: for chunk in iter(lambda: f.read(8192), b): h.update(chunk) return h.hexdigest() def scan_repo(root: str) - dict: result {} for dirpath, _, filenames in os.walk(root): for name in filenames: full os.path.join(dirpath, name) rel os.path.relpath(full, root) result[rel] hash_file(full) return dict(sorted(result.items()))跑一下scan_repo(.)你会得到一个{相对路径: 哈希值}的字典。这个字典就是后续 Merkle 树的叶子输入。3.2 构建 Merkle 树Merkle 树的作用是让哪些文件变了这个问题可以用一次根哈希比较来回答。构建方式是叶子节点是文件哈希两两配对做哈希得到父节点逐层向上直到只剩一个根。奇数个节点时最后一个直接复制一份参与配对。def build_merkle(leaves: list) - dict: if not leaves: return {root: None, levels: []} levels [leaves[:]] current leaves[:] while len(current) 1: if len(current) % 2 1: current current [current[-1]] nxt [] for i in range(0, len(current), 2): combined (current[i] current[i 1]).encode() nxt.append(hashlib.sha256(combined).hexdigest()) levels.append(nxt) current nxt return {root: current[0], levels: levels}把上一步的哈希值列表传进去root就是整棵树的根哈希。下次扫描时只要根哈希没变就说明整个仓库没动过不需要重新索引。这就是 Cursor 说的按哈希值索引以加快重复索引速度。3.3 分块策略服务端分块和嵌入是 Cursor 内部做的事但分块逻辑我们可以自己定。我按固定行数切分每块 40 行重叠 10 行避免函数被从中间切断。def chunk_text(text: str, size: int 40, overlap: int 10) - list: lines text.splitlines() chunks [] step size - overlap for i in range(0, len(lines), step): block \n.join(lines[i:i size]) if block.strip(): chunks.append(block) return chunks每个块再算一次哈希作为去重键。如果同一个块在多个文件里出现嵌入只算一次检索时按哈希回查。这一步对应 Cursor 流程里的存储嵌入并按哈希值索引。4. 验证请求嵌入与最近邻搜索4.1 调用嵌入接口把分块结果批量发给嵌入接口拿到向量后存到本地。这里用 TaoToken 的 API端点走/v1/embeddings。import os import requests BASE os.environ[TAOTOKEN_BASE_URL] KEY os.environ[TAOTOKEN_API_KEY] def embed(texts: list) - list: resp requests.post( f{BASE}/v1/embeddings, headers{Authorization: fBearer {KEY}}, json{model: text-embedding-3-small, input: texts}, timeout60, ) resp.raise_for_status() data resp.json()[data] return [item[embedding] for item in data]把上一步所有块拼成一个列表传进去返回的向量列表顺序和输入一致。存成 numpy 数组方便后面算余弦相似度。4.2 最近邻检索推理时的最近邻搜索本质是把用户问题也嵌入成向量然后和所有块向量算相似度取 top-k。用 numpy 做矩阵乘法即可不需要专门的向量库。import numpy as np def cosine_topk(query_vec, matrix, k5): q np.array(query_vec) m np.array(matrix) q q / np.linalg.norm(q) m m / np.linalg.norm(m, axis1, keepdimsTrue) scores m q idx np.argsort(scores)[::-1][:k] return [(int(i), float(scores[i])) for i in idx]把问题嵌入后调用这个函数返回的就是最相关的块索引和相似度分数。你可以把索引映射回原始代码片段拼进提示词里再问模型。4.3 端到端验证写一个 main 把流程串起来扫描仓库、构建 Merkle 树、分块、嵌入、检索。if __name__ __main__: repo scan_repo(.) leaves list(repo.values()) tree build_merkle(leaves) print(Merkle root:, tree[root]) all_chunks [] for rel, _ in repo.items(): with open(rel, r, errorsignore) as f: all_chunks.extend(chunk_text(f.read())) vectors embed(all_chunks) query embed([Merkle 树是怎么构建的])[0] for i, score in cosine_topk(query, vectors): print(round(score, 4), all_chunks[i][:80].replace(\n, ))跑通后你会看到根哈希和 top-5 相关块。如果根哈希不变第二次运行可以跳过嵌入直接复用向量这就是增量索引的核心。5. 本篇常见错排查5.1 根哈希每次都变最常见的原因是文件顺序不稳定。os.walk返回顺序依赖文件系统必须显式排序。我在scan_repo里用了dict(sorted(...))如果你自己改代码记得保留排序。另一个原因是把.git目录也扫进去了里面的文件频繁变动建议加过滤。5.2 嵌入接口返回 401 或 404401 一般是 Key 没读到检查环境变量名是否和代码一致。404 多半是路径拼错确认BASE是https://taotoken.net/api代码里拼的是/v1/embeddings。如果还是不通去接入文档核对端点https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。5.3 相似度分数普遍偏低检查向量是否做了归一化。余弦相似度必须在单位向量上算否则点积结果没有可比性。另外分块太大也会稀释语义40 行左右比较合适函数级别的块效果最好。5.4 检索结果不相关先确认问题语言和代码语言是否一致。中文问题检索英文代码时嵌入模型可能对齐不好可以先把问题翻译成英文再嵌入。另外 top-k 不要设太小5 到 10 之间比较稳。6. 继续往下走如果你打算把这套索引流程用到长期编码或 Agent 场景里单次调用不够需要稳定的额度和并发支持可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。它更适合持续跑索引和检索任务的场景。Claude Code 相关的接入配置可以参考https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-codeutm_campaignrewrite 里面有针对编码工具的端点说明。我自己跑这套流程时踩过的一个坑是一开始没做块级哈希去重同一个工具函数在十几个文件里重复嵌入向量库膨胀了三倍检索还变慢了。加上哈希去重后索引体积和查询延迟都降下来了。你可以先在自己的小项目上跑通再逐步加过滤规则和增量更新逻辑。