Tree-sitter 代码解析引擎
从源码字节流到结构化 AST——CGB 如何"读懂"12种语言
什么是 Tree-sitter,为什么选它
Tree-sitter 不是一个语言解析器,而是一个解析器生成框架。每种语言对应一个独立的 grammar 包(如 `tree-sitter-python`),CGB 在运行时动态加载。 增量解析的核心价值在于:只有文件发生改动时才重新解析受影响的子树,对大型仓库的增量更新极为重要。
CGB 目前支持12种语言,每种语言对应一个 LanguageSpec 实例,在 ParserLoader 初始化时统一注册。
S 表达式查询:精确捕获 AST 节点
Tree-sitter 使用 S 表达式查询语法在整棵 AST 上做模式匹配。每个 LanguageSpec 持有5类查询:
function_query(函数定义)、class_query(类/接口)、call_query(调用表达式)、import_query(导入语句)、func_ptr_assign_query(函数指针赋值,主要用于 C/C++)。
FunctionMatch 与 FQN 构建
每次查询命中后,解析器将原始 Node 对象包装成 FunctionMatch。
其中最关键的字段是 qualified_name(FQN,完全限定名),它是图数据库中函数节点的主键。
解析流水线:从字节到图节点
完整的解析过程分为5个阶段,每个阶段都有明确的输入输出约定。
调用解析:call_query + FunctionRegistryTrie
Pass 1 把所有函数定义写入图之后,Pass 2 才执行调用解析。call_query 从每个函数体内提取所有 call_expression,得到被调函数的简名(如 parse)。
但简名无法直接定位到 FQN,需要在 FunctionRegistryTrie 中做模糊匹配。
调用解析的歧义是静态分析的核心难题。CGB 的策略是:优先用导入信息消歧,有唯一匹配则直接连边,有多匹配则按调用方与被调用方的模块距离打分取最近者,无法消歧时留存为 UnresolvedCall 节点(不丢数据)。这比全丢或随机猜都要好得多。
图数据库构建
三 Pass 策略、Schema 设计与增量更新机制
图 Schema:节点与关系类型
CGB 支持三种图后端:Kuzu(推荐,嵌入式)、Memgraph(生产级)、Memory(单测用)。
三者共享同一套图 Schema,通过抽象接口 GraphBackend 隔离。
qualified_name,持有签名、参数列表、可见性、docstring、起止行号三 Pass 构建策略
图构建被拆成三个顺序执行的 Pass,原因很简单:调用解析(Pass 2)依赖 所有文件的函数定义都已写入图(Pass 1 完成),否则跨文件调用永远找不到目标节点。
FunctionRegistryTrie。此 Pass 完成后图中有完整的"结构骨架"。cgb rebuild-embeddings)。增量更新:基于 git diff 的智能重建
每次 cgb index 时,CGB 比较当前 HEAD commit hash 与上次索引时记录的 hash。如果相同则跳过;不同则通过 git diff --name-only 获取改动文件列表,仅重建这些文件。超过50个改动文件时触发全量重建。
Kuzu 锁竞争与并发写入处理
Kuzu 是嵌入式数据库,同一时刻只允许一个进程持有写锁。当 MCP 服务器正在读图、同时用户触发 cgb index 写图时,就会发生锁竞争。CGB 的处理策略是指数退避重试。
图 Schema 中"14种节点 + 11种关系"的设计颗粒度,直接决定了查询能力的上限。颗粒度不够(如把 Function 和 Method 合并)会丢失"方法归属哪个类"的信息;颗粒度过细(如区分 AsyncFunction 和 Function)则会让 Cypher 查询复杂度爆炸。CGB 的当前 Schema 是在可查询性和写入复杂度之间权衡的结果。
Pass 2 内部协作:call_processor 与 type_inference
对于动态语言(Python/JavaScript),方法调用 obj.parse() 需要先推断 obj 的类型才能确定调用目标。type_inference 模块承担这个职责,与 call_resolver 协作完成消歧。
API 文档生成
三层文档结构、调用树渲染与 LLM 增强模式
三层文档结构设计
API 文档被组织为三层,从粗到细:项目级汇总 → 模块级列表 → 函数级详情。
这个层次对应两种使用场景:人类浏览和 LLM embedding 的输入粒度。
所有文档存储在 artifact_dir/api_docs/ 目录下。
list_api_docs 工具的数据来源。文档生成流水线
文档生成从图数据库出发,通过 Cypher 查询提取数据,渲染为 Markdown 后写入文件系统。
_build_call_tree:防环递归调用树
调用树渲染面临两个挑战:递归/循环调用导致的死循环、以及树深度爆炸。
_build_call_tree() 使用 visited set + 深度限制双重防护。
LLM 增强模式:为无 docstring 函数生成描述
当 mode='enhance' 时,文档生成器会检测没有 docstring 的函数,将其签名 + 源码片段 + 调用上下文发送给 LLM,生成一句话描述,填入 <!-- FILL --> 位置。
_infer_ownership:C++ 指针所有权分析
_infer_ownership() 是专门为 C++ 代码设计的静态分析功能。它通过分析参数类型标注和函数体内的内存操作,推断每个指针参数是owned 还是 borrowed,写入 L3 文档的参数表格。
三层文档结构的本质是粒度分层的语义索引。L1/L2 面向人类导航;L3 面向向量化——每个函数一个文件,embedding 时天然按语义单元切分,避免了文档块切割的粒度问题。LLM enhance 模式进一步把"静态代码结构"升维为"可读语义描述",让语义搜索从"字符串相似"进化为"意图匹配"。
Embedding 构建
向量化引擎、双编码器设计与内存向量存储
为什么代码需要 Embedding
代码图谱解决了"结构查询":给定一个函数名,找到它的调用链。但还有另一类需求—— 语义搜索:用自然语言描述意图,找到对应的代码,哪怕你不记得函数名。 Embedding 是这一能力的核心:把代码和自然语言都转换为 高维浮点数向量, 然后通过向量距离衡量语义相似度。
把 2560 维向量想象成每个函数在语义空间中的"坐标"。embed_query(query) 把用户问题也投影到同一个坐标系,然后用余弦相似度找最近邻。这就是为什么"解析命令行参数"能找到 parse_args()——两者在这个坐标系里很近。
Qwen3Embedder:双编码器实现
双编码器设计
是 Qwen3Embedder 的核心。embed_documents() 和 embed_query() 使用不同的编码路径——
前者直接嵌入代码文本,后者附加任务指令,引导模型从"检索意图"角度理解查询。
向量化建索引流程
cgb index 执行时,L3 函数文档被批量向量化并存入
MemoryVectorStore,
最终序列化为 vectors.pkl。下次启动直接加载,无需重新向量化。
VectorRecord(node_id, qualified_name, embedding, metadata),metadata 存储 name/type/file_path 等字段,用于搜索后回填详情,无需再查图数据库。vectors.pkl(Pickle 格式)。下次 cgb mcp 启动时直接 load() 恢复内存状态,跳过全量向量化,启动时间从分钟级降为秒级。MemoryVectorStore:余弦相似度搜索
MemoryVectorStore 用 Python dict 存储向量,用暴力余弦相似度
搜索。对代码库(通常几千个函数)这完全够用——暴力搜索比维护 HNSW 索引简单得多,也不需要近似误差的trade-off。
纯向量搜索有时对词语变形敏感——"拉短"和"缩短"可能检索分数差距较大。CGB 在向量搜索结果上叠加了关键词匹配:query 分词后,每有一个 token 出现在函数名/签名/模块名中,相似度分数 +0.05。这个小奖励不会颠覆向量搜索的排序,但能把本该排第2的正确答案推到第1。
VectorRecord 数据结构与配置
每个向量记录的结构设计决定了搜索后能回填哪些信息。metadata 字段的选择是工程权衡: 存太少则每次搜索后还要再查图数据库;存太多则 pkl 文件膨胀、加载变慢。
CGB 同时实现了 MemoryVectorStore 和 QdrantVectorStore 两个后端。对代码库(几百到几千个函数),暴力搜索延迟 <10ms,完全没有引入 Qdrant 的必要——那会多一个外部依赖、一个网络跳跃、以及近似搜索的精度损失。规模达到十万函数以上再考虑切换。
RAG 系统
双通道检索、Cypher 生成与 LLM 上下文组装
RAG = 检索 + 生成,CGB 的双通道设计
RAG 解决的核心问题是:LLM 不认识你的私有代码库。CGB 的 RAG 是 双通道检索——不只用向量相似度,还能把自然语言转成 Cypher 图查询, 两路结果融合后送给 LLM。这让回答既有语义广度,又有结构精度。
向量搜索擅长"模糊意图匹配"——即使描述不精确也能找到相关函数。Cypher 查询擅长"精确结构查询"——"找所有继承了 BaseEmbedder 的类"这种问题向量搜索无能为力,Cypher 一条 MATCH 语句搞定。两路结果去重后融合,LLM 拿到的上下文既有广度又有深度。
CypherGenerator:NL → 图查询
CypherGenerator 的实现出人意料地简洁:给 LLM 一个描述图 Schema 的 system prompt,
把用户问题直接发过去,LLM 输出 Cypher。没有手写规则,没有 AST 解析——全靠 LLM 理解 Schema 后的
zero-shot 推理。
SemanticSearchService:向量 + 图上下文融合
语义搜索不只返回向量相似度结果——它还会从图数据库回填函数的完整元数据(源码、文件路径、行号), 以及可选的调用方/被调用方列表,形成 SemanticSearchResult 传给 RAGEngine 组装 prompt。
RAGEngine:从 query 到最终答案
RAGEngine.query() 是整个 RAG 链路的入口。它驱动语义检索、
CodeContext
组装和 LLM 调用,最终返回带源码引用的 RAGResult。
LLM 后端使用 Kimi(Moonshot AI),temperature=0.7,max_tokens=4096。
配置与输出:RAGConfig 和 RAGResult
RAGConfig 由三个子配置组成,全部支持从环境变量读取(RAGConfig.from_env())。
RAGResult 是 RAG 系统的输出契约,包含答案文本、来源引用列表和执行元数据,
可直接通过 to_markdown() 渲染为 Markdown 文档。
没有 RAG,AI 助手只能基于训练数据推测你的代码行为。有了 RAG,每次回答都基于真实检索到的源码——LLM 不是在"编",而是在"读你的代码再解释"。双通道(向量+图)确保无论是模糊的"语义查询"还是精确的"结构查询",都能找到相关代码,最终回答质量远高于直接问 LLM。
MCP 工具使用
13 个工具、stdio 协议、增量同步与工作空间布局
MCP:让 Claude 直接调用 CGB 的 13 个工具
MCP(Model Context Protocol) 是 Anthropic 定义的标准,让 LLM 能调用外部工具。CGB 的 MCP server 通过 stdio 管道 通信——没有 HTTP 服务器,没有端口,Claude 直接 spawn 子进程,通过 stdin/stdout 交换 JSON-RPC 消息。
stdio 通信的好处:零网络配置、进程级别权限隔离、没有端口冲突、无需认证 token(调用方本身就是本机进程),非常适合开发者工具场景。CGB 的 MCP server 入口是 cgb-mcp,底层是 entrypoints/mcp/__main__.py,用 Python 的 sys.stdin/stdout 实现完整的 JSON-RPC 2.0 消息循环。
工具调用全过程:Claude → JSON-RPC → 处理 → 响应
以 find_api 为例,看一次完整的工具调用链路。Claude 把自然语言问题转成工具调用请求,
MCP server 处理后返回结构化的 JSON 响应,Claude 再基于这个上下文组织回答。
整个过程对用户透明——Claude 自动决策何时调用哪个工具。
Claude 自动决定何时、调用哪个工具、传入什么参数——用户只需提问。整个 JSON-RPC 往返通常在 100-500ms 内完成。如果 Claude Desktop 配置了 CGB MCP,每次对话中 Claude 都能访问最新的代码图谱,回答基于真实代码,而不是训练记忆。
13 个工具分类详解
每个工具都有精心设计的参数接口,Claude 可以通过 tools/list 方法获取完整的 JSON Schema 描述,自动理解如何调用。
工作空间布局与增量同步机制
所有索引产物都存在 ~/.code-graph-builder/ 目录下。每个仓库有独立的 artifact_dir, 每次工具调用前会自动检查是否需要增量同步。
增量同步流程
当代码未改动时,检查开销极低(读一次文件 + 一次 git 命令)<1ms,对工具调用几乎无感知。 当有代码改动时,只重建涉及变更文件的节点,避免对整个代码库全量重建(可能需要数分钟)。 这套机制让 CGB MCP server 可以在日常开发中"持续在线"——代码改动后不需要手动触发重建, 下次工具调用时自动同步。
CLI 使用
完整工作流、核心命令、调试技巧与典型使用场景
三个等价入口 + 首次上手完整流程
CGB 提供三个命令入口,pip install 后全部自动注册到 PATH。
日常使用推荐 cgb(最短);MCP 模式用
cgb-mcp。
CGB 优先读取环境变量,再读 ~/.code-graph-builder/config.yaml。生产/CI 环境推荐用环境变量(不落盘),本地开发推荐写 config.yaml(避免每次 export)。支持的变量名:LLM_API_KEY、LLM_BASE_URL、LLM_MODEL、DASHSCOPE_API_KEY、EMBED_* 系列、CGB_WORKSPACE、CGB_DEBUG。
索引、库管理与搜索命令详解
cgb index 是最核心的命令,触发完整或增量的索引流程。
cgb index
每次执行都可以用 flag 控制粒度:只重建图、只重建文档、或只重建 Embedding。
搜索与查询命令
CLI 完整工作流:从代码库到可查询知识图谱
cgb index 背后触发了一个四阶段流水线。每个阶段产出的
artifact
会被下游阶段消费,形成完整的知识图谱。
底层命令:cgb scan(调试专用)
典型场景演示
两个实际场景展示 CLI 的完整使用节奏:初次索引大型项目,以及日常增量更新。
shutil.get_terminal_size(),动态调整进度条长度
当你想验证图数据库里到底存了什么、某个关系是否正确建立,直接用 cgb query 跑
Cypher
查询是最直接的方式。比如
MATCH (f:Function)-[:CALLS]->(g) WHERE f.qualified_name='auth.login' RETURN g.qualified_name
马上验证调用关系是否正确。加 --format json 可以把结果管道给 jq 做进一步处理。