Phase1 范围 3 grammar/4 类 → 6 grammar/9 类。grammar_for/DEFINITION_KINDS/Cargo.toml 加表项(零架构改动,grammar crate 0.23 ABI 兼容 0.25 主 crate),补 3 处 py 相关测试断言(py 加支持后原无 grammar 样例失效),测试 306 passed。
14 KiB
AST 符号解析 — Code Intelligence 设计
日期:2026-06-24 | 状态:设计深化(2026-06-24:信息密度三态 + 主题压缩),Phase1 待实施 关联:memory devflow-info-density-concept / devflow-aichat-session-analysis-2026-06-22 / plan gentle-gliding-book
Context(为什么)
起因:实测会话 e46f5605「DevFlow上下文管理不足分」——LLM read_file 读 8 个源码文件(context.rs/anthropic_compat/intent.rs/prompt.rs/compress 等)各 500 行全文回灌,prompt 累积 360K token 超 glm-5.2 上下文上限 → LLM 失败 → 末尾 tool 后无 assistant(卡)。8dfe0b94 更滚到 5M token 爆炸。
已否决的治标方向:
- ❌ read_file 限制行数(500→150):"等于没读",LLM 被迫 offset 翻页,更多轮次 + 仍累积
- ❌ read_file content 字节截断(8KB):"不讲武德",LLM 要全文被砍
正路:AST 语义解析,精准提取调用链 + 函数代码,替代物理读全文。
双目标(用户明确):
- 精准获取调用链 + 函数代码,避免物理读文件的 LLM 上下文成本
- AST 树作后期 ai coding 编码支撑工具(符号检索/依赖图/生成上下文/向量编码)
根本目标(2026-06-24 深化):提高上下文信息密度——使进 prompt 的每一行与当前关注主题紧密结合,去噪、去多余。这是所有读取/压缩设计的判据。
核心原则:信息密度 ≠ 压缩(2026-06-24)
密度 = 只给与当前任务相关的内容;压缩 = 无差别减体积。两者不同。 压缩减了体积,密度不一定升——减什么是预设规则或盲猜,和"当前任务关注什么"无关。
分水岭:压缩器带不带当前任务上下文。
- 不带 → 通用摘要(治标,可能砍掉恰是任务关键的细节)
- 带(任务焦点 focus)→ 相关性提炼(治本,留相关去无关)
被否的治标方向:
- ❌ 物理行数截断上限(500→150→3):武断,任何值都不合理(3 行砍废、3 万行等于没设);且按行返本身错——该按语义结构层级返
- ❌ 算法裁剪(砍注释/doc/空行):格式去噪,留下的不一定和任务相关,密度提升有限
- ❌ 通用 LLM 摘要(不带 focus):压出"这个函数做什么"的通用摘要,可能丢任务关键行
正路(本设计采纳):
- 结构层级供给(拉模式/Progressive Disclosure):read_symbol 默认返骨架 → LLM 看骨架下钻局部 → 精准取。进 prompt 的都是 LLM 主动要的,天然相关。
- 主题驱动压缩(带 focus):大块必须给时,压缩 LLM 接收当前任务焦点,做相关性提炼(非通用摘要)。
→ 不按物理行返,按语义层级返;压缩必须带任务上下文。
一、技术定位:AST vs LSP(两个不同层面)
| AST(tree-sitter) | LSP(rust-analyzer) | |
|---|---|---|
| 本质 | 数据结构(语法树,解析产物) | 通信协议(语义服务,JSON-RPC) |
| 层面 | 语法层 | 语义层 |
| 提供什么 | 符号结构/调用语法 | 类型/精准引用/重构/诊断 |
| 形态 | 库(链接进进程) | 独立服务器(子进程) |
| 成本 | 轻(ms/MB) | 重(二进制 30-50MB/平台 + 索引 + 内存) |
抽象层级:源码 → [tree-sitter] → AST(语法,Phase1-3 停此) → [类型推断] → 语义 → [LSP 协议] → 客户端。LSP 依赖 AST(rust-analyzer 内部也用),但停在更深(语义层)。
二、目标论证(能力边界,不夸大)
目标1:调用链 + 函数代码
| 能力 | tree-sitter | Phase |
|---|---|---|
| 函数结构(骨架·默认) | ✅ function_definition → 签名+分支结构+调用图+行数(极小,高密度) |
1 |
| 函数代码(定义体) | ✅ full=true / drill 取完整或局部(语法边界 100% 准) |
1 |
| 同文件调用点 | ✅ call_expression 命中符号 → 调用位置 |
2 |
| 跨文件调用链 | ✅ 符号索引 + import 解析 | 3 |
动态调度(dyn Trait/虚函数) |
❌ 静态解析不能(需类型推断,LSP 级) | 局限 |
| 回调/闭包/函数指针/宏 | ❌ 需数据流/编译器 | 局限 |
覆盖度:静态直接调用占实际 ~70-80%(绝大多数业务代码);动态/间接/宏 ~20-30% 需 LSP。
避免上下文成本:read_symbol(compress) → 函数体几十行 + 静态调用点,非 read_file 全文 500 行 → prompt 降一个量级。对 e46f5605 类(读源码理解实现,静态为主)直接见效。
目标2:ai coding 编码支撑
AST 是语法层(parse tree)。语法层能:
- 符号级上下文(LLM 编码拿相关函数定义+调用关系,非全文 chunk)
- 结构化检索(符号/调用链/依赖,替代文本 grep)
- 变更影响分析(改函数 X → 影响哪些调用方,精准 review/测试范围)
- 代码生成约束(签名/调用语法)
语义层不能(需 LSP/编译器):类型检查/推断、安全跨文件重构、数据流/借用分析。
→ AST 够 aichat 的"精准读代码 + 调用链 + 上下文 + 影响分析"(95% 读代码场景);深层语义是 LSP 的事。
三、方案:tree-sitter + read_symbol
选型:tree-sitter
- GitHub 出品,增量 AST 解析,几十种语言 grammar
- Rust 生态
tree-sitter+ 各语言 grammar crate(编译期静态链接) - 增量解析:文件改只重解变更(<1ms)
grammar 策略(2026-06-24 定稿)
静态编译 + 集中 lookup(架构成本最低,详见 插件机制设计):
- 不做动态加载(grammar 编共享库运行期 dlopen)、不抽 trait(YAGNI,无第二实现不抽象)
- grammar 编译期静态链接进二进制(无 ABI 坑 / 无运行时状态 / 无分发负担)
- grammar 获取集中到一个普通函数
grammar_for(ext) -> Language(未来加动态只改这一点,不返工调用方)
Phase1 范围(覆盖 devflow 源码 + 主流用户项目):
| 文件类型 | grammar | 备注 |
|---|---|---|
.rs |
tree-sitter-rust | 后端主体 |
.ts/.tsx |
tree-sitter-typescript | 前端 script |
.js/.jsx |
tree-sitter-javascript | |
.vue |
借 tree-sitter-typescript | 切 <script> 段喂 ts;template/style 不做符号(Vue 逻辑在 script) |
.go |
tree-sitter-go | 2026-06-25 扩展 |
.java |
tree-sitter-java | 2026-06-25 扩展 |
.py |
tree-sitter-python | 2026-06-25 扩展 |
6 个 grammar 覆盖 9 类文件。Vue 借 TS 省 grammar + 避 SFC 嵌套复杂度。Go/Java/Python 2026-06-25 扩展(表驱动加表项:DEFINITION_KINDS 补 kind + grammar_for 加分支 + Cargo.toml 加 crate 0.23,ABI 兼容 0.25 主 crate,零架构改动)。
扩展:加标准语言 = 加 grammar crate + 一张节点映射表(表驱动,~30 行/语言)。小众(C++/Kotlin)按需;动态加载/集成插件化见插件机制设计文档。
三态读取(read_symbol,密度驱动)
read_symbol 不按物理行返、不截断,按语义结构层级返(消解"截断多少行"争论):
| 态 | 触发 | 返回 | 密度 |
|---|---|---|---|
| 骨架(默认) | read_symbol(path, symbol) |
签名 + 内部分支结构 + 调用列表 + 行数(不返函数体) | 极高(极小+全相关) |
| 下钻(LLM 拉) | read_symbol(..., drill=分支/调用名) |
该局部分支/调用的精准定义 | 高(LLM 主动要) |
| 全文(显式) | read_symbol(..., full=true) |
完整定义体(小函数常态直通) | 全 |
read_symbol(path, symbol, kind?, drill?, full?)
→ tree-sitter 解析 path,提取 symbol 定义节点
→ 默认:返骨架(签名 + 分支结构 + 调用图 + 行数)
→ drill=局部名:返该分支/调用的精准定义
→ full=true:返完整定义体
→ 返回 {symbol, kind, signature, skeleton|drill|def_body, line_range, file_hash, calls?}
主题压缩(compress_with_focus,大块兜底)
大块必须整体给时(复杂函数 / 大文件段),经带焦点压缩:
compress_with_focus(content, focus)
→ focus = 当前任务/问题(LLM 显式传)
→ LLM 相关性提炼:留与 focus 相关的路径/逻辑,去无关分支/防御/日志
→ 返提炼结果 + 原 hash(丢细节时可回取原始)
- focus 必传(分水岭:不带 focus 的通用摘要被否)
- hash 兜底:LLM 可凭 hash 回取原始(类似下钻)
- 与现有
compress_via_llm:现有压历史对话(P0-1 未真降 prompt),本工具压"工具返回入 prompt 当轮"——扩展触发点非新造
注册 / schema / 兜底
- read_symbol 注册
register_file_tools(与 read_file/grep 同域) - schema:
object_schema([("path",string,req),("symbol",string,req),("kind",string,opt),("drill",string,opt),("full",bool,opt)]) - handler:捕获
allowed_dirs,调resolve_workspace_path_with_allowed校验(复用 read_file 模式) - RiskLevel::Low(只读)
- 不报错底线(见七):无 grammar / 解析失败 / symbol 未找到 / 任何大小 → 走兜底或正常返,不 panic / 不阻断
四、Phase 路线
两条线:读取层(read_symbol 三态,tree-sitter)/ 压缩层(主题压缩,LLM,与 P0-1)。
读取层(tree-sitter):
| Phase | 能力 | 成本 | 缓存 |
|---|---|---|---|
| 1 单文件符号骨架+下钻+全文 | read_symbol 三态 | 低-中 | 实时解析(弃 AST) |
| 2 同文件调用点 | drill 跨分支调用图 | 中 | 实时 |
| 3 跨文件调用链 | 符号索引 + import | 高 | 符号表内存缓存 |
压缩层(LLM,与 P0-1):
| Phase | 能力 | 成本 | 缓存 |
|---|---|---|---|
| C1 compress_with_focus | 带 focus 相关性提炼 | 中(LLM 调用) | hash 缓存(同内容不重压) |
| C2 触发管线接入 | 工具返回→压缩→入 prompt | 中 | 阈值(小不压)+ 缓存 |
远期:Encoding(符号检索/依赖图/向量编码)。
五、缓存策略(分情况,对齐 tree-sitter 增量特性)
Phase1/2(单文件):情况1 实时解析
- tree-sitter 单文件 ms 级,解析完提取符号后丢弃 AST(不缓存)
- 每次 read_symbol 实时解析 → 最新最准(零缓存失效)
- 速度 ~1-10ms(几千行),增量 <1ms
Phase3(跨文件索引):情况2 缓存
- 全项目符号表(file→symbol→pos+签名),累积大
- 2a 内存缓存(devflow ~4000 符号 ~0.5-2MB,内存够)
- 大项目(几千文件/几十 MB)转 2b 文件缓存(
.devflow/symbols.cache,LRU 按需加载) - 失效:文件 mtime/sha256 变 → tree-sitter 增量重解析该文件(<1ms)→ 更新该文件项(不全扫)
增量解析是命脉:让"实时"(情况1)够快、"缓存失效"(情况2)够便宜。
六、估算(devflow 实测基数)
- 源码:280 文件 / 82,444 行(Rust 142/52.7K,TS 87/12K,Vue 51/17.6K)
- 符号:~4000(Rust ~2840 + TS ~1044 + Vue ~510)
- 符号表:Phase1 ~400KB / Phase2 ~1MB / Phase3(含调用链)~1.6-2MB
- → 内存缓存够(情况2a)
七、兜底(不报错底线,不破坏现有)
底线:任何情况不 panic / 不阻断 / 不因大小失败。
- 无 grammar(语言不支持)/ 解析失败(语法错误)/ symbol 未找到 → 兜底提示
grep+read_file(offset 精准读) - 任何大小函数 → 正常返(不截断天然不因大小失败;大函数走骨架+下钻/主题压缩,非报错)
- read_symbol 失败不阻断(返兜底提示让 LLM 回退 grep)
- compress_with_focus 失败 → 回原始内容(LLM 仍能用,降级非阻断)
八、LSP 后置结论(2026-06-24 评估)
LSP 成本高,重 tree-sitter 100x:
- 二进制 ~30-50MB/平台(跨平台 3 份,bundle 进安装包体积大)
- 首次索引 30s-2min(全项目类型推断),内存几百 MB-1GB,后台常驻
- JSON-RPC 通信复杂(初始化/能力协商/文件同步/多语言 server 生命周期)
- 分发:bundle 重 / 要求用户装(rustup/tsserver)门槛高
收益:语义层(动态调度/类型/重构/精准引用),tree-sitter 天花板外 ~20%。
结论:Phase1-3 tree-sitter 先(语法层,覆盖 ~80% 读代码/调用链/上下文,轻量零分发负担);LSP 后置/按需——检测用户装了 rust-analyzer 才启用深层语义,不强制 bundle,保分发轻。别一上来 LSP(成本/分发过重),tree-sitter 拿 80% 红利,LSP 留作"深层语义可选增强"。
九、与现有方向的关系(密度驱动,四层互补)
① 结构层级供给(read_symbol 骨架→下钻) ← 本设计,语义结构精准+密度
↓ 大块兜底
② 主题压缩(compress_with_focus) ← 带 focus 相关性提炼(非通用摘要)
↓ 兜底
③ grep+offset(read_file) ← 现有,文本精准
↓ 历史治理
④ 历史压缩(P0-1 compress_via_llm) ← prompt 层已读历史摘要
- LLM 默认 read_symbol 骨架 → 要细节下钻 → 大块经主题压缩 → 不支持回退 grep
- 已读历史由 P0-1 压缩
- 密度判据:①②③④ 每层都服务"只给任务相关内容",非"减体积"
十、风险 / 改动面
| 风险 | 缓解 |
|---|---|
| tree-sitter 依赖体积 | 按语言按需 enable grammar(非全装) |
| 跨语言覆盖(小众语言 grammar 弱) | 兜底 grep+read |
| 动态调度解析不准 | 文档标注局限,需时评估 LSP(后置) |
| Phase3 跨文件索引重 | 渐进,Phase1/2 先(实时,无索引) |
十一、验证(Phase1 落地后)
- 解析速度:tree-sitter 解析 devflow 大文件(context.rs 1552 行等),测 ms
- 符号表大小:全项目解析后序列化字节数(验估算 ~400KB)
- token 对比:read_symbol(compress) vs read_file(context.rs) 返回 token 量(验降一个量级)
- 单测:read_symbol 提取函数定义体 + 调用点(Phase2)
十二、落地步骤
- 本文档(设计定稿)
- Phase1 实施:tree-sitter-rust 集成 + read_symbol 工具 + 单文件符号提取 + 兜底 + 实测
- Phase2/3/4 + LSP 后置评估,按需
关键文件(Phase1 实施)
src-tauri/Cargo.toml:加 tree-sitter + tree-sitter-rustsrc-tauri/src/commands/ai/tool_registry.rs:register_file_tools内加 read_symbol(对齐 read_file handler)- 新模块
src-tauri/src/commands/ai/code_intel.rs:tree-sitter 解析 + 符号提取纯函数 - 单测:符号提取 + 兜底