数据截至 (上游 commit e839e559ac61)
代码知识图谱(CKG):tree-sitter 建图 + SQLite 查询 + 快照缓存
30 秒导读: 大模型改代码时老要问"某某函数长啥样、定义在哪"。CKG(Code Knowledge Graph,代码知识图谱)预先把整个代码库的函数和类用 tree-sitter 解析成 AST、抽成一张 SQLite 表;agent 给一个符号名(
search_function foo),就能秒回它的文件、行号、函数体——不用 grep 全库,也不用把整份代码塞进上下文。
本章讲 Trae Agent 里工程含量最高的一支工具。它是 02-tools.md 里"内置工具"的一员(注册名 ckg),但复杂度远超其它工具,值得单开一章。
1. 这是什么(零基础也能懂)
一句话定义: CKG 是一个"按函数名/类名查代码"的本地索引——把代码库里所有函数和类抽出来,存进 SQLite,给 agent 当"代码目录"用。
它解决谁的什么问题? 想象 agent 要修一个几万行的项目。它读到某处调用了 parse_config,想看这个函数怎么写的。没有 CKG,它只能:
- 用
grep全库找parse_config—— 命中一堆无关的字符串、注释、调用点,还得自己数行号; - 或者把相关文件整份读进上下文 —— 又贵又占窗口。
有了 CKG,agent 只需一条命令,直接拿到"这个符号在哪、第几行到第几行、函数体是什么"。
它能做什么(三条命令):
| 命令 | 查什么 |
|---|---|
search_function | 顶层函数(不属于任何类) |
search_class | 类(附带字段、方法签名列表) |
search_class_method | 类里的方法 |
用起来什么样。 工具是给 LLM 调的,一次调用长这样(参数见 get_parameters,ckg_tool.py:51):
command = "search_function"
path = "/repo"
identifier = "parse_config"
print_body = true # 默认 true,连函数体一起返回
返回(见 _search_function,ckg_tool.py:135):
Found 1 functions named parse_config:
1. /repo/src/config.py:12-30
def parse_config(path):
...
一句话直觉/类比: 把 CKG 当成一本自动生成的"代码电话簿"——你报一个名字(符号),它翻到那一页(文件:行号)并把内容念给你听。而"电话簿"本身是用 tree-sitter 读懂每份源码的语法结构后编出来的。
2. 顶层全景(它大概怎么转)
CKG 分三个阶段:建图(解析源码 → 落库)、查图(符号 → 记录)、缓存(用快照哈希决定复用还是重建)。
怎么读下图: 从上到下是一次工具调用的生命周期;左侧是"第一次遇到这个代码库"要走的建图路径,右侧是"命中缓存"的快路径。
agent 调用 ckg 工具 (path, identifier, command)
│
▼
CKGTool.execute ← ckg_tool.py:80
│
按 path 找已建好的 CKGDatabase?
┌──────┴───────┐
有 │ │ 没有 → new CKGDatabase(path)
│ ▼
│ get_folder_snapshot_hash(path) ← ckg_database.py:97
│ │ (git 状态 / 文件元数据 → 一个哈希串)
│ ┌─────┴─────┐
│ 哈希已有 .db 哈希是新的
│ 复用 SQLite _construct_ckg() 重建 ← ckg_database.py:534
│ │ (tree-sitter 遍历所有源码 → 落库)
└────────┴─────┬─────┘
▼
query_function / query_class ← ckg_database.py:648 / 695
(SELECT ... WHERE name = ?)
▼
拼输出 + MAX_RESPONSE_LEN 截断 ← ckg_tool.py:155
部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
CKGTool | 对外三命令、参数校验、缓存 DB 实例、拼输出+截断 | tools/ckg_tool.py:14 |
CKGDatabase | 建图与查图的核心;持有一个 SQLite 连接 | tools/ckg/ckg_database.py:148 |
FunctionEntry/ClassEntry | 承载一条函数/类记录的 dataclass | tools/ckg/base.py:9 :24 |
extension_to_language | 文件后缀 → tree-sitter 语言名 | tools/ckg/base.py:40 |
get_folder_snapshot_hash | 给代码库算一个快照哈希,判断要不要重建 | tools/ckg/ckg_database.py:97 |
clear_older_ckg | 清理一周前的旧 .db,agent 启动时调一次 | tools/ckg/ckg_database.py:107 |
主线走一遍(高层): agent 传入 (path, identifier, command) → CKGTool 若没为该 path 建过库,就 new CKGDatabase → 后者用快照哈希查缓存,命中就复用 .db,否则 tree-sitter 遍历所有源码建表 → 三命令各自走 query_function/query_class 做一次 SELECT ... WHERE name = ? → 结果拼成文本,超过 16000 字符就截断。
3. 核心原理(逐个机制,由浅入深)
3.1 对外的三命令与截断(CKGTool)
它要解决的小问题: 把"查函数/查类/查方法"这三种意图,变成三条简单命令,并保证再多的命中也不会撑爆上下文。
入口 execute。 先做参数校验(command/path/identifier 缺一不可,path 必须是存在的目录,ckg_tool.py:82-112),然后按 path 缓存 CKGDatabase 实例——同一个代码库只建一次库,后续调用直接复用:
ckg_database = self._ckg_databases.get(codebase_path)
if ckg_database is None:
ckg_database = CKGDatabase(codebase_path) # 第一次才建
self._ckg_databases[codebase_path] = ckg_database
(ckg_tool.py:114-117,_ckg_databases 是个 dict[Path, CKGDatabase])
三命令 → 两个查询函数的映射(ckg_tool.py:119-133):
| 命令 | 底层调用 | 关键区别 |
|---|---|---|
search_function | query_function(id, entry_type="function") | 只要 parent_class is None 的 |
search_class_method | query_function(id, entry_type="class_method") | 只要 parent_class is not None 的 |
search_class | query_class(id) | 查 classes 表 |
注意前两个共用同一张 functions 表和同一个查询函数,靠 parent_class 字段区分"顶层函数"还是"类方法"(见 §3.4)。
输出与 print_body。 每条命中打印 序号. 文件:起行-止行,print_body=True(默认)时附上完整函数体;search_class 还会额外打印 Fields: 和 Methods: 两块(ckg_tool.py:179-184)。search_class_method 的行里多一句 within class <parent_class>(ckg_tool.py:211)。
MAX_RESPONSE_LEN 截断(= 16000,来自 tools/run.py:18)。 三个 _search_* 都在循环里累加输出,一旦超长就砍掉尾巴并附提示,还诚实告诉 agent 剩几条没显示:
if len(output) > MAX_RESPONSE_LEN:
output = (
output[:MAX_RESPONSE_LEN]
+ f"\n<response clipped> {len(entries) - index + 1} more entries not shown"
)
break
(ckg_tool.py:155-160,三处结构相同)
3.2 建图:tree-sitter 遍历 AST 抽符号(_construct_ckg)
它要解决的小问题: 怎么从一份源码文本里,可靠地抽出"这里有个函数、名字叫 X、从第 12 行到第 30 行、body 是这段"——而且要跨 Python/Java/C/C++/TS/JS 六种语言。