先明确要解决的那个具体问题

如果你的仓库不到几千行,agent 直接读文件就够了,这篇用不上。但到了几万行以上,「把相关文件找出来」会变成每次对话里最贵的一步:找不准,它就开始编;找得太多,上下文被填满,真正的推理反而没空间。

这篇搭的东西很小:一张结构表 + 一个查询接口。它不判断语义相似度,也不做向量检索——它只回答「这个函数在哪、谁调用了它、这个文件里有哪些定义」。听起来朴素,但这恰好覆盖了 agent 最常需要的那类确定性查询:定位、跳转、看调用关系。

第一步:装 tree-sitter,先只支持一种语言

tree-sitter 是一个解析器生成工具,把源码解析成语法树,各语言有现成的语法包。第一版只支持你最常用的那一种语言——不要一上来就铺十种,因为不同语言的「什么是函数」定义并不一样,先跑通一种你才知道表结构该怎么设计。

安装用各语言的官方绑定即可,关键是确认能拿到语法树里的节点类型名(比如函数定义、类定义、调用表达式在语法包里分别叫什么)。这些名字是后面提取的基础,建议先拿一个小文件把节点类型打印出来看一眼。

第二步:定一张够用的表,别追求解析全

表设计遵循一条:只存你打算查询的东西。最小版本三张表就够——files(路径、语言、行数、内容哈希)、symbols(所属文件、名字、种类、起始行、结束行、签名摘要)、calls(调用方符号、被调用名字、所在行)。

不要在这一步存完整语法树或全文。理由很实际:语法树更新频繁、体积大,而你要回答的问题几乎都能用「符号 + 行号」表达;全文检索交给文件的字符串搜索就够了,不必进库。

第三步:把仓库灌进库,并让更新是增量的

第一遍把整个仓库扫一遍写进 SQLite,之后每次只处理变过的文件——用内容哈希判断,哈希没变就跳过。这一步是整套东西能不能日用下去的分水岭:全量扫描在中等仓库里要几分钟,增量通常是一秒内的事。

解析失败的文件不要中止整个流程(语法包版本、生成代码、模板文件都可能报错),记录下来跳过即可。你的目标是「大部分文件有结构」,不是「百分之百」。

第四步:写三个查询,覆盖八成需求

接口先只做三个:按名字找符号(支持前缀匹配,返回文件、行号、签名);给一个符号找出它调用了谁、以及谁调用了它;给一个文件列出它的全部定义。这三个查询对应 agent 最常见的三类动作:定位、追调用链、快速建立文件印象。

每个查询返回的内容都要带「文件路径 + 起止行号」,并且只返回必要片段(比如符号前后各若干行),不要返回整个文件。片段带行号是它比「全文搜索」强的地方——agent 可以据此精确读取,而不是猜。

第五步:暴露给 agent,别让它自由发挥

暴露方式有两类:一类是包成 MCP 服务让支持的客户端调用;另一类更简单——写成命令行(比如 codeindex find <名字>),在系统提示词里告诉 agent「找代码时先跑这条命令」。第二类上手最快,也便于你先观察它到底怎么用。

不管用哪种方式,都要把返回格式固定下来(JSON,字段名列清楚),并在提示词里写明:先查询、再读文件、最后才改。否则模型会把你的检索层当成又一个可选项,继续按老习惯漫游仓库。

第六步:验证它真的省事,而不是感觉上省事

用同一批任务做对照:找一处实现、改一个函数、追一条调用链,分别记录「agent 读了多少文件、花了多少 token、答对没有」。你会看到两种典型结果——定位类任务明显变快变准,而需要跨语言或依赖隐式约定的任务提升有限。

这个对照很重要,因为它决定了你后面该不该继续投入:如果提升集中在定位与跳转,那说明方向对了,可以再补语言和查询;如果提升不明显,多半不是检索层的问题,而是任务本身需要「理解意图」,那不是结构表能解决的。

第七步:写清它的边界,否则会被误用

最后把这份工具的边界写在文档最上面:它只知道「语法上有什么」,不知道「运行时会怎样」;它按名字和结构匹配,不做语义近似;动态语言里的很多调用它看不到。把这些写清楚,能防止团队里有人把它当成「全知索引」,在查不到结论时得出「这个功能不存在」的错误判断。

维护上只有一条纪律:语言版本或语法包升级后,抽查几个典型符号的行号是否还准。行号错了比查不到更危险——查不到你会去读文件,行号错了你会直接相信它。