01

Tree-sitter 代码解析引擎

从源码字节流到结构化 AST——CGB 如何"读懂"12种语言

什么是 Tree-sitter,为什么选它

Tree-sitter 不是一个语言解析器,而是一个解析器生成框架。每种语言对应一个独立的 grammar 包(如 `tree-sitter-python`),CGB 在运行时动态加载。 增量解析的核心价值在于:只有文件发生改动时才重新解析受影响的子树,对大型仓库的增量更新极为重要。

CGB 目前支持12种语言,每种语言对应一个 LanguageSpec 实例,在 ParserLoader 初始化时统一注册。

🐍
Python
JS / TS
🦀
Rust
🔵
Go
Java
⚙️
C / C++
🎯
C# / Scala
🐘
PHP / Lua

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个阶段,每个阶段都有明确的输入输出约定。

📦
ParserLoader
🌳
Parser.parse
🔍
S-expr Query
📋
FunctionMatch
🗄️
图数据库
点击"下一步"开始 Step 0 / 5

调用解析:call_query + FunctionRegistryTrie

Pass 1 把所有函数定义写入图之后,Pass 2 才执行调用解析。call_query 从每个函数体内提取所有 call_expression,得到被调函数的简名(如 parse)。 但简名无法直接定位到 FQN,需要在 FunctionRegistryTrie 中做模糊匹配。

Pass 2 调用解析 — 组件对话
0 / 4 messages
💡 关键洞察

调用解析的歧义是静态分析的核心难题。CGB 的策略是:优先用导入信息消歧,有唯一匹配则直接连边,有多匹配则按调用方与被调用方的模块距离打分取最近者,无法消歧时留存为 UnresolvedCall 节点(不丢数据)。这比全丢或随机猜都要好得多。

02

图数据库构建

三 Pass 策略、Schema 设计与增量更新机制

图 Schema:节点与关系类型

CGB 支持三种图后端:Kuzu(推荐,嵌入式)、Memgraph(生产级)、Memory(单测用)。 三者共享同一套图 Schema,通过抽象接口 GraphBackend 隔离。

🔴
Function / Method
主键 qualified_name,持有签名、参数列表、可见性、docstring、起止行号
🟣
Class / Interface / Enum
持有继承链、实现接口列表;Interface 区别于 Class,Enum 区别于 Type
🔵
Module / Package
模块对应单个源文件,Package 对应目录;通过 CONTAINS_MODULE / CONTAINS_PACKAGE 关联
🟡
File / Folder / Project
文件系统层,Project 是根节点,通过 CONTAINS_FILE / CONTAINS_FOLDER 形成树形结构

三 Pass 构建策略

图构建被拆成三个顺序执行的 Pass,原因很简单:调用解析(Pass 2)依赖 所有文件的函数定义都已写入图(Pass 1 完成),否则跨文件调用永远找不到目标节点。

1
Pass 1 — 定义提取(definition_processor)
扫描所有文件,写入 Project / Package / Folder / File / Module / Class / Function / Method 节点,及 CONTAINS_* / DEFINES / DEFINES_METHOD 边。同时把所有 FQN 注册进 FunctionRegistryTrie。此 Pass 完成后图中有完整的"结构骨架"。
2
Pass 2 — 调用与导入解析(call_processor / call_resolver / type_inference)
再次扫描所有文件,用 call_query 和 import_query 提取调用和导入,结合 Trie 消歧后写入 CALLS / IMPORTS / DEPENDS_ON_EXTERNAL 边。type_inference 负责为动态语言推断方法调用的接收者类型。
3
Pass 3 — Embedding 向量化(可选)
为每个函数节点生成 embedding 向量并存入向量索引(Chroma / Qdrant)。向量由函数签名 + docstring + 调用上下文拼接后送入 embedding 模型。此 Pass 可独立运行(cgb rebuild-embeddings)。

增量更新:基于 git diff 的智能重建

每次 cgb index 时,CGB 比较当前 HEAD commit hash 与上次索引时记录的 hash。如果相同则跳过;不同则通过 git diff --name-only 获取改动文件列表,仅重建这些文件。超过50个改动文件时触发全量重建。

📄
meta.json
🔀
git HEAD
📑
diff 列表
增量重建
🔄
全量重建
💾
更新 hash
点击"下一步"开始 Step 0 / 6

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 协作完成消歧。

方法调用消歧 — 内部协作
0 / 4 messages
03

API 文档生成

三层文档结构、调用树渲染与 LLM 增强模式

三层文档结构设计

API 文档被组织为三层,从粗到细:项目级汇总 → 模块级列表 → 函数级详情。 这个层次对应两种使用场景:人类浏览和 LLM embedding 的输入粒度。 所有文档存储在 artifact_dir/api_docs/ 目录下。

📋
L1 — index.md
项目级汇总表。列出所有模块路径、每个模块的函数/类数量。是进入文档系统的入口,也是 MCP list_api_docs 工具的数据来源。
📁
L2 — modules/{qn}.md
单模块文档。列出该模块所有公开函数和类,含一句话摘要和签名。文件名用模块 FQN 的点替换成下划线,避免路径冲突。
📄
L3 — funcs/{qn}.md
单函数详情页。为 embedding 优化的密集格式:签名、描述、ASCII 调用树、调用方列表(带文件+行号)、源码片段。是语义搜索的基础单元。

文档生成流水线

文档生成从图数据库出发,通过 Cypher 查询提取数据,渲染为 Markdown 后写入文件系统。

🗄️
Cypher 查询
📁
L2 模块文档
🌿
调用树渲染
📄
L3 函数文档
📋
L1 index.md
点击"下一步"开始 Step 0 / 5

_build_call_tree:防环递归调用树

调用树渲染面临两个挑战:递归/循环调用导致的死循环、以及树深度爆炸。 _build_call_tree() 使用 visited set + 深度限制双重防护。

LLM 增强模式:为无 docstring 函数生成描述

mode='enhance' 时,文档生成器会检测没有 docstring 的函数,将其签名 + 源码片段 + 调用上下文发送给 LLM,生成一句话描述,填入 <!-- FILL --> 位置。

enhance 模式 — LLM 增强流程
0 / 4 messages

_infer_ownership:C++ 指针所有权分析

_infer_ownership() 是专门为 C++ 代码设计的静态分析功能。它通过分析参数类型标注和函数体内的内存操作,推断每个指针参数是owned 还是 borrowed,写入 L3 文档的参数表格。

💡 关键洞察

三层文档结构的本质是粒度分层的语义索引。L1/L2 面向人类导航;L3 面向向量化——每个函数一个文件,embedding 时天然按语义单元切分,避免了文档块切割的粒度问题。LLM enhance 模式进一步把"静态代码结构"升维为"可读语义描述",让语义搜索从"字符串相似"进化为"意图匹配"。

04

Embedding 构建

向量化引擎、双编码器设计与内存向量存储

为什么代码需要 Embedding

代码图谱解决了"结构查询":给定一个函数名,找到它的调用链。但还有另一类需求—— 语义搜索:用自然语言描述意图,找到对应的代码,哪怕你不记得函数名。 Embedding 是这一能力的核心:把代码和自然语言都转换为 高维浮点数向量, 然后通过向量距离衡量语义相似度。

🧠
语义搜索
自然语言描述 → 找到相关函数,跨越字面差异,支持中英文混合查询
📐
2560 维向量
Qwen3-Embedding-4B 输出维度。维度越高,语义表达能力越强,代价是存储和计算量增加
API 驱动
无需本地 GPU,通过 DashScope API 调用。批处理最大 25 个文本/请求,指数退避重试
🔄
双编码器
查询和文档使用不同任务指令,这是 bi-encoder 检索模型的关键设计,提升跨语言匹配精度
💡 核心直觉

把 2560 维向量想象成每个函数在语义空间中的"坐标"。embed_query(query) 把用户问题也投影到同一个坐标系,然后用余弦相似度找最近邻。这就是为什么"解析命令行参数"能找到 parse_args()——两者在这个坐标系里很近。

Qwen3Embedder:双编码器实现

双编码器设计 是 Qwen3Embedder 的核心。embed_documents()embed_query() 使用不同的编码路径—— 前者直接嵌入代码文本,后者附加任务指令,引导模型从"检索意图"角度理解查询。

向量化建索引流程

cgb index 执行时,L3 函数文档被批量向量化并存入 MemoryVectorStore, 最终序列化为 vectors.pkl。下次启动直接加载,无需重新向量化。

📄
L3 Docs
✂️
文本组装
🌐
DashScope
🗄️
VectorStore
💾
vectors.pkl
点击"下一步"开始 Step 0 / 5
1
文本组装策略
每个 L3 函数文档提取:函数签名(精确语义锚点)+ 一句话描述(docstring 或 LLM 生成)+ 调用树摘要(结构上下文)+ 源码前 30 行(实现语义)。这4个维度共同构成函数的语义指纹。
2
VectorRecord 创建
每个函数创建一个 VectorRecord(node_id, qualified_name, embedding, metadata),metadata 存储 name/type/file_path 等字段,用于搜索后回填详情,无需再查图数据库。
3
Pickle 持久化
向量化完成后序列化为 vectors.pkl(Pickle 格式)。下次 cgb mcp 启动时直接 load() 恢复内存状态,跳过全量向量化,启动时间从分钟级降为秒级。

MemoryVectorStore:余弦相似度搜索

MemoryVectorStore 用 Python dict 存储向量,用暴力余弦相似度 搜索。对代码库(通常几千个函数)这完全够用——暴力搜索比维护 HNSW 索引简单得多,也不需要近似误差的trade-off。

组件对话 — 查询 "解析命令行参数" 的执行路径
0 / 4 messages
💡 关键词提权(Keyword Boost)

纯向量搜索有时对词语变形敏感——"拉短"和"缩短"可能检索分数差距较大。CGB 在向量搜索结果上叠加了关键词匹配:query 分词后,每有一个 token 出现在函数名/签名/模块名中,相似度分数 +0.05。这个小奖励不会颠覆向量搜索的排序,但能把本该排第2的正确答案推到第1。

VectorRecord 数据结构与配置

每个向量记录的结构设计决定了搜索后能回填哪些信息。metadata 字段的选择是工程权衡: 存太少则每次搜索后还要再查图数据库;存太多则 pkl 文件膨胀、加载变慢。

字段 类型 说明
node_id int 图数据库节点 ID,用于搜索后回查图获取完整信息
qualified_name str 函数 FQN(如 entrypoints.cli.cli.parse_args),关键词提权和去重依赖此字段
embedding list[float] 2560 维向量,约 20KB/函数(float64)。1000 个函数约 20MB 内存
metadata dict 存储 name、type、path、start_line、end_line、signature,搜索结果可直接展示无需回查
💡 为什么不用 Qdrant / Faiss

CGB 同时实现了 MemoryVectorStoreQdrantVectorStore 两个后端。对代码库(几百到几千个函数),暴力搜索延迟 <10ms,完全没有引入 Qdrant 的必要——那会多一个外部依赖、一个网络跳跃、以及近似搜索的精度损失。规模达到十万函数以上再考虑切换。

05

RAG 系统

双通道检索、Cypher 生成与 LLM 上下文组装

RAG = 检索 + 生成,CGB 的双通道设计

RAG 解决的核心问题是:LLM 不认识你的私有代码库。CGB 的 RAG 是 双通道检索——不只用向量相似度,还能把自然语言转成 Cypher 图查询, 两路结果融合后送给 LLM。这让回答既有语义广度,又有结构精度。

🔍
SemanticSearchService
向量通道。把 query 转向量,在 MemoryVectorStore 中找最相似函数。支持关键词提权再排序。
domains/core/search/semantic_search.py
🔮
CypherGenerator
图查询通道。把自然语言问题通过 LLM 翻译成 Cypher,执行后得到精确的结构化结果(调用链、继承关系等)。
domains/upper/rag/cypher_generator.py
🤖
RAGEngine
总协调器。驱动两路检索、组装 prompt 上下文、调用 Kimi LLM 生成答案、构建 RAGResult 返回。
domains/upper/rag/rag_engine.py
💡 双通道的互补性

向量搜索擅长"模糊意图匹配"——即使描述不精确也能找到相关函数。Cypher 查询擅长"精确结构查询"——"找所有继承了 BaseEmbedder 的类"这种问题向量搜索无能为力,Cypher 一条 MATCH 语句搞定。两路结果去重后融合,LLM 拿到的上下文既有广度又有深度。

CypherGenerator:NL → 图查询

CypherGenerator 的实现出人意料地简洁:给 LLM 一个描述图 Schema 的 system prompt, 把用户问题直接发过去,LLM 输出 Cypher。没有手写规则,没有 AST 解析——全靠 LLM 理解 Schema 后的 zero-shot 推理

Q
自然语言:找所有解析命令行参数的函数
MATCH (f:Function) WHERE f.name CONTAINS 'parse' RETURN f.qualified_name, f.signature LIMIT 50
Q
自然语言:找所有继承了 BaseEmbedder 的类
MATCH (c:Class)-[:INHERITS]->(b:Class) WHERE b.name = 'BaseEmbedder' RETURN c.qualified_name, c.path LIMIT 50

SemanticSearchService:向量 + 图上下文融合

语义搜索不只返回向量相似度结果——它还会从图数据库回填函数的完整元数据(源码、文件路径、行号), 以及可选的调用方/被调用方列表,形成 SemanticSearchResult 传给 RAGEngine 组装 prompt。

💬
Query
🎯
向量检索
🗄️
图查询
🔗
上下文增强
📋
SearchResult
点击"下一步"开始 Step 0 / 5
字段 类型 来源
score float (0-1) 余弦相似度 + 关键词奖励,4位小数
qualified_name str VectorRecord.qualified_name 直接透传
source_code str | None 图数据库补填,或从文件提取 start_line~end_line
type str Function / Class / Method 等,图数据库补填

RAGEngine:从 query 到最终答案

RAGEngine.query() 是整个 RAG 链路的入口。它驱动语义检索、 CodeContext 组装和 LLM 调用,最终返回带源码引用的 RAGResult。 LLM 后端使用 Kimi(Moonshot AI),temperature=0.7,max_tokens=4096。

组件对话 — query("embed_query 和 embed_documents 有什么区别") 执行过程
0 / 5 messages

配置与输出:RAGConfig 和 RAGResult

RAGConfig 由三个子配置组成,全部支持从环境变量读取(RAGConfig.from_env())。 RAGResult 是 RAG 系统的输出契约,包含答案文本、来源引用列表和执行元数据, 可直接通过 to_markdown() 渲染为 Markdown 文档。

配置项 默认值 环境变量
moonshot.model kimi-k2.5 MOONSHOT_MODEL
moonshot.temperature 0.7 ——
retrieval.semantic_top_k 10 RAG_SEMANTIC_TOP_K
retrieval.max_context_tokens 8000 ——
retrieval.include_callers True ——
retrieval.graph_max_depth 2 ——
💡 RAG 的价值在哪里

没有 RAG,AI 助手只能基于训练数据推测你的代码行为。有了 RAG,每次回答都基于真实检索到的源码——LLM 不是在"编",而是在"读你的代码再解释"。双通道(向量+图)确保无论是模糊的"语义查询"还是精确的"结构查询",都能找到相关代码,最终回答质量远高于直接问 LLM。

06

MCP 工具使用

13 个工具、stdio 协议、增量同步与工作空间布局

MCP:让 Claude 直接调用 CGB 的 13 个工具

MCP(Model Context Protocol) 是 Anthropic 定义的标准,让 LLM 能调用外部工具。CGB 的 MCP server 通过 stdio 管道 通信——没有 HTTP 服务器,没有端口,Claude 直接 spawn 子进程,通过 stdin/stdout 交换 JSON-RPC 消息。

仓库管理
5 个工具
初始化、查状态、列表、切换活跃库、链接已有索引
代码搜索和文档
4 个工具
混合搜索、浏览 L1/L2 文档、获取 L3 函数文档、生成文档
调用图分析
2 个工具
查找所有调用者、BFS 追踪完整调用链
配置和维护
2 个工具
查看服务器配置、重建向量索引(不重建图)
为什么选 stdio 而不是 HTTP?

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 自动决策何时调用哪个工具。

工具调用过程 — find_api 示例
0 / 6 messages
工具调用对用户完全透明

Claude 自动决定何时、调用哪个工具、传入什么参数——用户只需提问。整个 JSON-RPC 往返通常在 100-500ms 内完成。如果 Claude Desktop 配置了 CGB MCP,每次对话中 Claude 都能访问最新的代码图谱,回答基于真实代码,而不是训练记忆。

13 个工具分类详解

每个工具都有精心设计的参数接口,Claude 可以通过 tools/list 方法获取完整的 JSON Schema 描述,自动理解如何调用。

仓库管理(5个)
initialize_repository
完整索引流程:graph → api-docs → embeddings。支持进度回调,可传入 repo_path(默认当前目录)。
get_repository_info
活跃库快照:节点数 / 关系数 / 文件数 / 上次索引时间,无参数,O(1)。
list_repositories
列出 ~/.code-graph-builder/ 下所有 artifact_dir,含 indexed_at 时间戳。
switch_repository
切换活跃库:写 active.txt。可传 repo_name 或 artifact_dir 路径。
link_repository
链接已有索引到新路径,不重建。适合团队共享同一份索引产物。
代码搜索和文档(4个)
find_api
混合搜索(语义向量 + 关键词)。参数:query, top_k。返回匹配函数列表 + score + 源码预览。
list_api_docs
浏览 L1/L2 文档层级。可选 module_name;不传则返回所有模块概览。
get_api_doc
获取 L3 函数文档。参数:qualified_name。返回完整文档(含源码、调用树、参数说明)。
generate_api_docs
重生成 API 文档。mode:full / resume / enhance。resume 跳过已有文档,增量补全。
调用图分析(2个)
find_callers
查找调用某函数的所有位置。输入:qualified_name。返回 caller 列表 + 文件位置。
trace_call_chain
BFS 追踪完整调用链。参数:start_function(可选 target)。返回 wiki 格式调查报告。
配置和维护(2个)
get_config
显示服务器配置:LLM 设置、Embedding 设置、工作空间路径。调试时首先调用。
rebuild_embeddings
重建向量索引,不重建图。适合更换 embedding 模型后使用,比全量重建快数倍。

工作空间布局与增量同步机制

所有索引产物都存在 ~/.code-graph-builder/ 目录下。每个仓库有独立的 artifact_dir, 每次工具调用前会自动检查是否需要增量同步。

增量同步流程

🔔
工具调用
📄
meta.json
🌿
git HEAD
跳过
or
📋
git diff
🔧
增量重建
点击"下一步"开始 Step 0 / 6
增量同步的开销分析

当代码未改动时,检查开销极低(读一次文件 + 一次 git 命令)<1ms,对工具调用几乎无感知。 当有代码改动时,只重建涉及变更文件的节点,避免对整个代码库全量重建(可能需要数分钟)。 这套机制让 CGB MCP server 可以在日常开发中"持续在线"——代码改动后不需要手动触发重建, 下次工具调用时自动同步。

07

CLI 使用

完整工作流、核心命令、调试技巧与典型使用场景

三个等价入口 + 首次上手完整流程

CGB 提供三个命令入口,pip install 后全部自动注册到 PATH。 日常使用推荐 cgb(最短);MCP 模式用 cgb-mcp

code-graph-builder
完整名称,安装后可用
cgb
推荐缩写
cgb-mcp
MCP server 模式
环境变量 vs config.yaml

CGB 优先读取环境变量,再读 ~/.code-graph-builder/config.yaml。生产/CI 环境推荐用环境变量(不落盘),本地开发推荐写 config.yaml(避免每次 export)。支持的变量名:LLM_API_KEYLLM_BASE_URLLLM_MODELDASHSCOPE_API_KEYEMBED_* 系列、CGB_WORKSPACECGB_DEBUG

索引、库管理与搜索命令详解

cgb index 是最核心的命令,触发完整或增量的索引流程。 cgb index 每次执行都可以用 flag 控制粒度:只重建图、只重建文档、或只重建 Embedding。

搜索与查询命令

CLI 完整工作流:从代码库到可查询知识图谱

cgb index 背后触发了一个四阶段流水线。每个阶段产出的 artifact 会被下游阶段消费,形成完整的知识图谱。

🌲
Scan
graph.db
📝
API Docs
api_docs/
🎯
Embed
vectors.pkl
📚
Wiki
可选
Ready
可查询
点击"下一步"开始 Step 0 / 5

底层命令:cgb scan(调试专用)

典型场景演示

两个实际场景展示 CLI 的完整使用节奏:初次索引大型项目,以及日常增量更新。

场景 A:初次索引大型 C++ 项目
# 不含 Wiki,加速初次索引
$ cgb index /path/to/cpp-project --no-wiki
→ [=========] 100% Done. ~3-5 min
$ cgb status
→ Functions: 12,450 | Classes: 890
→ Files: 1,203 | Indexed: 3m ago
$ cgb search "GPU initialization"
→ 1. cuda.init_device (0.87)
→ 2. vulkan.create_context (0.82)
场景 B:日常增量更新
# 改了几个文件,提交前快速同步
$ git add -p && git commit -m "fix: ..."
$ cgb index -i
→ Detected 3 changed files
→ Rebuilding affected nodes...
→ Done: 28s (vs full 5min)
# MCP 下次调用时自动同步,
# 无需手动触发
CLI 操作流 — 增量更新后精确查询
0 / 6 messages
进度条格式
[========> ] 45% Step 2/4: Generating docs...
单行滚动:ANSI escape code 覆写同一行,不污染终端滚动历史
自适应宽度:检测 shutil.get_terminal_size(),动态调整进度条长度
Windows 兼容:检测 VT100 支持,不支持时退化为纯文本输出
cgb query:最强的调试工具

当你想验证图数据库里到底存了什么、某个关系是否正确建立,直接用 cgb queryCypher 查询是最直接的方式。比如 MATCH (f:Function)-[:CALLS]->(g) WHERE f.qualified_name='auth.login' RETURN g.qualified_name 马上验证调用关系是否正确。加 --format json 可以把结果管道给 jq 做进一步处理。