Tree-sitter & Jedi

0 / 7
START

Code RAG 的两大支柱

在构建 Code RAG 的知识图谱时,你需要两个互补的工具

为什么需要它们?

构建 Code RAG 知识图谱时,你需要理解代码的结构语义

源代码文件
Tree-sitter
语法结构 · AST
+
Jedi
语义分析 · 类型推断
Code RAG 知识图谱

它们各自负责什么?

维度 Tree-sitter Jedi
本质 增量解析器 — 将源码变成 AST(抽象语法树) 静态分析引擎 — 理解 Python 代码语义
速度 极快(微秒级),适合实时解析 较慢(毫秒~秒级),需要类型推断
语言支持 几乎所有语言(Python, JS, Go...) 仅 Python
对 RAG 的价值 提取函数、类、导入等结构节点 解析引用、定义、类型等语义关系
学习路线:关卡 1-3 学习 Tree-sitter,关卡 4-5 学习 Jedi,关卡 6 将两者组合构建 RAG 图谱。每关通过测验后解锁下一关!
Tree-sitter

关卡 1:AST 是什么?

Tree-sitter 将源代码解析成抽象语法树,每个节点代表一个语法元素

解析一段 Python 代码

import tree_sitter_python as tspython from tree_sitter import Language, Parser parser = Parser(Language(tspython.language())) code = """ def greet(name: str) -> str: return f"Hello, {name}!" class Bot: def __init__(self, model): self.model = model """ tree = parser.parse(bytes(code, "utf-8")) root = tree.root_node # 根节点,类型是 module
核心概念:tree.root_node 是整棵 AST 的根。每个 Node 对象包含了该语法元素的类型、位置、文本和子节点。

AST 长什么样?

上面的代码被 Tree-sitter 解析后,会生成这样的树结构(简化版):

module # 根节点
function_definition # def greet(...)
name: identifier "greet"
parameters: parameters
typed_parameter
identifier "name"
type: type "str"
return_type: type "str"
body: block
return_statement
class_definition # class Bot:
name: identifier "Bot"
body: block
function_definition # def __init__(...)
注意两种颜色:绿色节点类型(node.type),橙色字段名(用于 child_by_field_name())。这两者的区别是闯关的关键!

Node 的核心属性

属性/方法返回类型说明
node.type str 节点类型,如 "function_definition""identifier"
node.text bytes 该节点对应的源代码文本(需 .decode()
node.start_point (row, col) 起始位置(行号, 列号),从 0 开始
node.end_point (row, col) 结束位置
node.children list[Node] 所有子节点(含标点符号如 :(
node.named_children list[Node] 仅有意义的子节点(排除标点符号)
node.child_by_field_name() Node | None 按字段名取子节点,如 .child_by_field_name("name")
node.parent Node | None 父节点
node.is_named bool 是否为命名节点(区别于匿名的标点符号节点)

通关测验

回答以下问题,全部答对即可解锁下一关。

Q1:node.childrennode.named_children 的区别是什么?

A children 只返回直接子节点,named_children 返回所有后代
B named_children 排除了标点符号等匿名节点,children 包含全部
C 两者完全相同,只是别名
D children 按名称排序,named_children 按位置排序

Q2:要获取一个 function_definition 节点的函数名,应该用?

A node.text
B node.children[0].type
C node.child_by_field_name("name").text
D node.name

Q3:node.text 返回的数据类型是?

A bytes —— 需要 .decode("utf-8") 转为字符串
B str —— 直接就是字符串
C list[str] —— 每行一个元素
D Node —— 还是一个节点对象
Tree-sitter

关卡 2:RAG 关键节点类型

对于 Code RAG,你不需要关心所有节点类型 —— 只需要掌握构建图谱所需的核心类型

Python 中的核心节点类型

以下是构建 Code RAG 图谱时最常用的节点类型,以及它们包含的字段(field)

节点类型 (type)字段 (fields)RAG 用途
function_definition name, parameters, return_type, body 图谱中的函数节点
class_definition name, superclasses, body 图谱中的类节点
import_statement name 依赖关系边
import_from_statement module_name, name 依赖关系边(更精确)
decorated_definition 子节点包含 decorator + 被装饰的定义 识别 route、property 等
call function, arguments 函数调用关系边
assignment left, right, type 全局变量/常量

交互探索:点击节点查看字段

源代码
from flask import Flask class APIServer(Flask): def health_check(self) -> dict: return {"status": "ok"}

点击节点查看其属性 ↓

import_from_statement class_definition function_definition type (return) argument_list (超类)
节点详情

← 点击左侧节点查看详情

遍历 AST:提取 RAG 节点的代码模板

def extract_definitions(node): """递归遍历 AST,提取所有函数和类定义""" results = [] for child in node.children: if child.type == "function_definition": name = child.child_by_field_name("name").text.decode() params = child.child_by_field_name("parameters") ret = child.child_by_field_name("return_type") results.append({ "type": "function", "name": name, "params": params.text.decode() if params else "", "return_type": ret.text.decode() if ret else None, "start_line": child.start_point[0], "end_line": child.end_point[0], "body_text": child.text.decode(), # 完整函数代码 }) elif child.type == "class_definition": name = child.child_by_field_name("name").text.decode() supers = child.child_by_field_name("superclasses") body = child.child_by_field_name("body") # 递归提取类内部的方法 methods = extract_definitions(body) if body else [] results.append({ "type": "class", "name": name, "bases": supers.text.decode() if supers else "", "methods": methods, }) # 递归进入子节点(处理嵌套定义) results.extend(extract_definitions(child)) return results

通关测验

将左侧的字段名与右侧的描述配对。点击左侧选一个,再点右侧匹配。

Tree-sitter

关卡 3:Tree-sitter Query 语法

除了遍历,Tree-sitter 还提供了声明式的 Query 语法来高效匹配节点

Query 是什么?

Query 是 Tree-sitter 的模式匹配语言,类似于正则表达式,但匹配的是 AST 节点。
对于 RAG 来说,Query 比手动遍历更高效、更简洁

# Query 基本语法:S-表达式 # 匹配所有函数定义,并捕获函数名 query_str = """ (function_definition name: (identifier) @func.name parameters: (parameters) @func.params return_type: (type)? @func.return_type body: (block) @func.body ) @func.def """ # 使用 Query query = Language(tspython.language()).query(query_str) captures = query.captures(tree.root_node) # captures 是 dict: {"func.name": [node, ...], "func.def": [node, ...], ...} for node in captures.get("func.name", []): print(node.text.decode()) # 输出每个函数名

Query 语法速查

语法含义示例
(node_type) 匹配指定类型的节点 (function_definition)
@name 捕获节点,绑定到名称 (identifier) @func.name
field: (type) 匹配特定字段的子节点 name: (identifier)
(type)? 可选匹配 return_type: (type)? @ret
(_) 匹配任意命名节点(通配符) value: (_) @val
"text" 匹配匿名节点(文本字面量) "async" 匹配 async 关键字

RAG 常用 Query 模板

# 捕获所有函数定义 (function_definition name: (identifier) @name parameters: (parameters) @params ) @definition.function
# 捕获所有类定义及其父类 (class_definition name: (identifier) @name superclasses: (argument_list)? @bases body: (block) @body ) @definition.class
# 捕获 from X import Y (import_from_statement module_name: (dotted_name) @source name: (dotted_name) @imported ) @import
# 捕获函数调用 (call function: [ (identifier) @call.name (attribute object: (_) @call.object attribute: (identifier) @call.method) ] arguments: (argument_list) @call.args ) @call

captures() 返回值详解

query.captures(node) 返回一个 dict[str, list[Node]]
- Key 是你在 Query 中定义的 @name(不含 @)
- Value 是匹配到的所有 Node 组成的列表

注意:还有 query.matches(node),返回的是 list[tuple[pattern_idx, dict]],每个 match 是一组捕获。对 RAG 来说 captures 更常用,因为你通常只需要所有匹配节点的集合。

通关测验

根据题目选择正确的 Query 语法。

Q1:以下哪个 Query 能正确捕获所有类的名称?

A (class_definition @name)
B (class_definition name: (identifier) @name)
C (class name: @name)
D class_definition.name @name

Q2:(type)? 中的 ? 表示什么?

A 匹配一个或多个
B 匹配零个或多个
C 可选匹配 — 匹配零个或一个
D 通配符 — 匹配任意类型

Q3:query.captures(root) 的返回类型是?

A dict[str, list[Node]] — 按捕获名称分组
B list[Node] — 所有匹配节点的列表
C list[tuple[str, Node]] — 名称和节点的配对
D str — 匹配的文本
Jedi

关卡 4:Jedi 入门

Jedi 是 Python 的静态分析库,能理解类型、引用和定义 —— Tree-sitter 做不到的事

Tree-sitter vs Jedi:为什么需要 Jedi?

Tree-sitter 的局限:它只做语法解析,理解语义。例如:
- 看到 x = foo(),Tree-sitter 知道这是赋值和函数调用,但不知道 foo 在哪里定义
- 看到 self.model,不知道 model 是什么类型
- 无法追踪跨文件的 import 关系到具体定义
Jedi 补充的能力:跨文件定义追踪、类型推断、引用查找、作用域分析

Jedi 的核心 API:Script 对象

import jedi # 创建 Script —— Jedi 的核心入口 script = jedi.Script( source="import os\nos.path.join('a', 'b')", path="example.py", # 可选:文件路径(帮助解析相对导入) ) # 或者直接从文件创建 script = jedi.Script(path="my_module.py")

Script 的关键方法

方法参数说明RAG 用途
.goto(line, col) 行号, 列号 (从1开始) 跳转到定义处 构建"定义于"
.infer(line, col) 行号, 列号 推断该位置的类型 给节点添加类型标注
.references(line, col) 行号, 列号 查找所有引用该名称的位置 构建"引用"
.get_names() all_scopes, definitions, references 获取文件中所有名称 快速获取文件的符号列表
.complete(line, col) 行号, 列号 获取补全建议 RAG 中较少使用
.rename(line, col, new_name) 行号, 列号, 新名称 重命名重构 RAG 中不使用
重要:Jedi 的行号从 1 开始,而 Tree-sitter 的 start_point0 开始。混用时务必 +1

实际使用示例

import jedi code = """ from pathlib import Path def read_config(filepath: str) -> dict: p = Path(filepath) return json.loads(p.read_text()) result = read_config("config.json") """ script = jedi.Script(source=code, path="app.py") # goto: "read_config" 在第 7 行被调用,跳转到定义处 defs = script.goto(7, 10) # → [Name: read_config, line=3] # infer: "result" 的类型是什么? types = script.infer(7, 1) # → [Name: dict] # references: "Path" 在哪些地方被使用? refs = script.references(2, 21) # → [line 2 (import), line 5 (usage)] # get_names: 获取文件所有顶层名称 names = script.get_names() for n in names: print(n.name, n.type, n.line) # Path module 2 # read_config function 3 # result statement 7

通关测验

选择正确的 Jedi 方法。

Q1:要查找某个变量是在哪里定义的,应该用哪个方法?

A script.references(line, col)
B script.infer(line, col)
C script.goto(line, col)
D script.complete(line, col)

Q2:Tree-sitter 的 start_point(3, 0),对应 Jedi 的行号应该传?

A line=3 — 两者相同
B line=4 — Jedi 从 1 开始,需要 +1
C line=2 — Jedi 从 1 开始,需要 -1
D row=3 — Jedi 用 row 不用 line

Q3:要快速获取一个 Python 文件中所有的顶层符号(函数名、类名、变量名),应该用?

A script.goto(1, 0)
B script.references(1, 0)
C script.infer(1, 0)
D script.get_names()
Jedi

关卡 5:Name 对象全解析

Jedi 的几乎所有方法都返回 Name 对象列表 —— 这是你和 Jedi 交互的核心数据结构

Name 对象的属性一览

goto()infer()references()get_names() 都返回 list[Name]。每个 Name 包含:

属性类型说明RAG 图谱中的用途
.name str 名称字符串,如 "read_config" 节点标签
.type str "module" / "class" / "function" / "param" / "statement" 节点类型分类
.module_name str 所属模块的完全限定名,如 "pathlib" 跨模块关系边
.module_path Path | None 定义所在的文件路径 关联到文件节点
.line int 行号(从 1 开始) 精确定位
.column int 列号(从 0 开始) 精确定位
.full_name str | None 完全限定名,如 "pathlib.Path" 唯一标识节点
.description str 可读描述,如 "def read_config(filepath: str) -> dict" 图谱节点的摘要文本
.is_definition() bool 是否是定义位置(区别于引用位置) 区分定义节点和引用边
.defined_names() list[Name] 该作用域内定义的名称(如类的方法列表) 构建"包含"关系边
.parent() Name | None 父作用域 构建层级关系
.goto() list[Name] 跳转到该名称的定义处(Name 也有此方法!) 追踪引用链

Name.type 的所有可能值

.type 是一个字符串,这是 Code RAG 分类节点的关键依据:

module class function param statement instance property keyword

实战:用 Jedi 构建函数信息

import jedi def build_function_info(filepath): """用 Jedi 提取文件中所有函数的语义信息""" script = jedi.Script(path=filepath) names = script.get_names(all_scopes=False) # 仅顶层 functions = [] for name in names: if name.type != "function": continue func_info = { "name": name.name, # "read_config" "full_name": name.full_name, # "app.read_config" "description": name.description, # "def read_config(filepath: str) -> dict" "module": name.module_name, # "app" "line": name.line, # 3 "is_def": name.is_definition(),# True } # 获取函数的参数(子名称) params = [n.name for n in name.defined_names() if n.type == "param"] func_info["params"] = params # 用 goto 追踪函数内部的调用 # (通常配合 Tree-sitter 找到调用位置后再用 Jedi) functions.append(func_info) return functions

通关测验

测试你对 Name 对象的理解。

Q1:要获取一个类内定义的所有方法,应该对类的 Name 对象调用?

A name.defined_names() 然后过滤 type == "function"
B name.goto()
C name.parent()
D name.description

Q2:name.full_name 返回 "pathlib.Path.read_text",这说明什么?

A 这是一个 module 类型的名称
B 这个名称定义在当前文件中
C 这是 pathlib 模块中 Path 类的 read_text 方法
D read_text 是 pathlib 的一个子模块

Q3:以下哪个属性最适合作为 RAG 图谱中节点的全局唯一标识

A name.name — 短名称
B name.full_name — 完全限定名
C name.description — 描述文本
D name.line — 行号
FINAL

关卡 6:组合构建 Code RAG 图谱

将 Tree-sitter 和 Jedi 结合,构建完整的代码知识图谱

分工策略

Tree-sitter 负责:
1. 快速解析所有文件的 AST
2. 提取函数、类、导入的结构信息
3. 定位代码中的调用表达式
4. 提供精确的行号/列号给 Jedi
Jedi 负责:
1. 解析调用目标的定义位置
2. 推断变量/返回值的类型
3. 追踪跨文件的引用关系
4. 提供完全限定名作为节点 ID

完整流程:构建图谱

import jedi import tree_sitter_python as tspython from tree_sitter import Language, Parser from pathlib import Path PY_LANGUAGE = Language(tspython.language()) parser = Parser(PY_LANGUAGE) # ── Step 1: Tree-sitter 提取结构 ── def extract_structure(filepath): """用 Tree-sitter 快速提取文件的代码结构""" code = Path(filepath).read_bytes() tree = parser.parse(code) # Query: 一次性提取函数、类、导入、调用 q = PY_LANGUAGE.query(""" (function_definition name: (identifier) @func_name) @func (class_definition name: (identifier) @class_name) @class (import_from_statement module_name: (dotted_name) @import_source name: (dotted_name) @import_name) @import (call function: (identifier) @call_name) @call_expr (call function: (attribute attribute: (identifier) @method_name)) @method_call """) captures = q.captures(tree.root_node) return captures # ── Step 2: Jedi 补充语义 ── def enrich_with_semantics(filepath, ts_captures): """用 Jedi 为 Tree-sitter 提取的节点添加语义信息""" script = jedi.Script(path=filepath) graph_nodes = [] graph_edges = [] # 处理函数定义 for node in ts_captures.get("func_name", []): # Tree-sitter 行号从 0 开始,Jedi 从 1 开始 line = node.start_point[0] + 1 col = node.start_point[1] # 用 Jedi 获取完全限定名和类型 jedi_names = script.goto(line, col) if jedi_names: n = jedi_names[0] graph_nodes.append({ "id": n.full_name or n.name, "type": "function", "name": n.name, "desc": n.description, "file": str(filepath), "line": n.line, }) # 处理调用 —— 构建"调用"边 for node in ts_captures.get("call_name", []): line = node.start_point[0] + 1 col = node.start_point[1] # Jedi 追踪到定义处 targets = script.goto(line, col) if targets: t = targets[0] # 找到调用者(包含此调用的函数) caller = find_enclosing_function(node) # 用 Tree-sitter parent 向上找 if caller: graph_edges.append({ "from": caller, "to": t.full_name or t.name, "relation": "calls", }) return graph_nodes, graph_edges def find_enclosing_function(node): """沿 Tree-sitter AST 向上查找包含此节点的函数""" current = node.parent while current: if current.type == "function_definition": name_node = current.child_by_field_name("name") return name_node.text.decode() if name_node else None current = current.parent return None

图谱结构总结

图谱元素来源说明
节点:函数 TS: function_definition + Jedi: full_name, description 结构来自 TS,唯一ID和描述来自 Jedi
节点:类 TS: class_definition + Jedi: defined_names() TS 取结构,Jedi 取方法列表
节点:模块 Jedi: module_name, module_path 文件级别的节点
边:调用 TS: call 节点 + Jedi: goto() TS 找到调用位置,Jedi 解析目标
边:导入 TS: import_from_statement 纯 TS 即可,Jedi 可补充路径
边:继承 TS: superclasses + Jedi: goto() TS 取父类名,Jedi 追踪到定义
边:包含 TS: 树的父子关系 / Jedi: parent() 模块包含类,类包含方法

最终测验

综合运用所学知识!全部答对即可通关。

Q1:想知道 self.db.execute(query)execute 的定义在哪个文件,应该怎么做?

A 只用 Tree-sitter 的 child_by_field_name("function")
B 只用 Jedi 的 get_names()
C Tree-sitter 定位调用位置 → Jedi goto() 追踪到定义 → .module_path 获取文件
Dgrep 搜索

Q2:Tree-sitter 的行号是 start_point = (10, 5),传给 Jedi 的 goto() 应该是?

A goto(10, 5)
B goto(11, 5)
C goto(10, 6)
D goto(11, 6)

Q3:以下哪个组合最适合作为 RAG 图谱中"函数"节点的属性?

A TS: node.text 作为唯一 ID
B Jedi: name.line 作为唯一 ID
C TS: node.type + Jedi: name.name
D Jedi: full_name 作 ID + description 作摘要 + TS: node.text 作代码内容

Q4:为什么不能只用 Jedi 而完全不用 Tree-sitter?

A Jedi 只支持 Python,且速度慢;Tree-sitter 多语言且极快,适合批量解析
B Jedi 无法解析 Python 代码
C Tree-sitter 能推断类型,Jedi 不能
D Jedi 不支持 Python 3
🏆

全部通关!

你已经掌握了用 Tree-sitter + Jedi 构建 Code RAG 知识图谱的核心知识。

速查手册

Tree-sitter 常用 Node 属性:
node.type · node.text.decode() · node.start_point · node.end_point
node.children · node.named_children · node.child_by_field_name() · node.parent
Jedi 常用 Script 方法:
script.goto(line, col) · script.infer(line, col) · script.references(line, col) · script.get_names()
Jedi Name 对象关键属性:
.name · .type · .full_name · .description · .module_name · .module_path
.line · .column · .is_definition() · .defined_names() · .parent() · .goto()

下一步建议

1. 用 Tree-sitter 解析你的项目文件,提取所有函数/类节点
2. 用 Jedi 的 goto() 解析调用关系,构建边
3. 用 full_name 作为节点唯一 ID
4. 将 descriptionnode.text 作为 embedding 的输入文本
5. 将图谱存入 Neo4j / NetworkX,配合向量数据库实现 RAG 检索