问题:代码搜索的两难困境

“这个项目的用户认证流程是怎么实现的?”

这个问题看似简单,但现有工具都不太好回答:

  • 向量搜索:把代码切成 chunks,做 embedding,然后相似度匹配。问题是"相似"不等于"相关"——搜"认证"可能返回一堆包含 auth 字符串的注释和配置文件,而不是真正的认证逻辑。

  • ast-grep:能精确匹配代码结构,但你得先知道要搜什么模式。ast-grep -p 'authenticate($USER)'?还是 login($$$)?还是 verify_token($TOKEN)?不知道代码怎么写的,就不知道怎么搜。

  • grep/ripgrep:快,但看到的是文本行,不是代码结构。注释里的、字符串里的、真正的调用,全混在一起。

这就是困境:向量搜索不理解结构,结构搜索需要先知道模式

最近在研究 PageIndexast-grep 时,我发现这两个工具的结合可能是一个有趣的方向。

两个工具的核心能力

ast-grep:代码的结构化视角

ast-grep 用 Tree-sitter 解析代码,生成语法树,然后让你用代码片段作为搜索模式。

1function greet(name) {
2  console.log("Hello, " + name);
3}

在 ast-grep 眼中,这不是三行文本,而是:

program
└── function_declaration
    ├── name: identifier "greet"
    ├── parameters: formal_parameters
    │   └── identifier "name"
    └── body: statement_block
        └── call_expression
            ├── function: member_expression (console.log)
            └── arguments: binary_expression

它知道什么是函数、什么是调用、什么是参数。这是精确提取的基础。

PageIndex:文档的推理导航

PageIndex 的核心思路是:不用向量相似度,而是把文档转换成层级树结构(类似"目录"),然后让 LLM 在树上推理导航。

Financial Report 2024
├── Executive Summary (p.1-5)
├── Financial Statements (p.6-50)
│   ├── Balance Sheet (p.6-15)
│   ├── Income Statement (p.16-30)
│   └── Cash Flow (p.31-50)
└── Risk Factors (p.51-80)

当你问"公司的现金流状况如何",LLM 不是做向量搜索,而是像人类专家一样:先看目录 -> 定位到 Financial Statements -> 找到 Cash Flow -> 读取 p.31-50 的内容。

这是推理定位的基础。

方向一:ast-grep 增强 PageIndex(构建代码的语义树索引)

第一个结合方向:用 ast-grep 的结构提取能力,为代码库构建 PageIndex 风格的层级索引。

代码库的"目录"应该长什么样?

传统的代码索引是文件列表:

src/
├── auth/
│   ├── login.ts
│   ├── logout.ts
│   └── token.ts
├── api/
│   ├── users.ts
│   └── orders.ts
└── utils/
    └── helpers.ts

这是物理结构,不是语义结构

用 ast-grep 提取后,可以构建这样的语义树:

Authentication System
├── User Login Flow
│   ├── validateCredentials(email, password) -> boolean
│   │   └── Summary: 验证用户凭证,检查密码哈希
│   ├── createSession(userId) -> Session
│   │   └── Summary: 创建用户会话,设置过期时间
│   └── handleLoginError(error) -> Response
│       └── Summary: 处理登录失败,记录日志,返回错误信息
├── Token Management
│   ├── generateToken(payload) -> JWT
│   ├── verifyToken(token) -> Payload | null
│   └── refreshToken(oldToken) -> JWT
└── Session Cleanup
    └── cleanExpiredSessions() -> void

每个节点不是文件名,而是语义单元:函数、类、模块、关键逻辑块。每个节点带有:

  • 函数签名
  • 摘要(从注释或代码逻辑生成)
  • 源码位置(文件:行号)

如何用 ast-grep 提取这些结构?

ast-grep 可以精确提取代码中的结构元素:

1# 提取所有函数定义
2id: extract-functions
3language: typescript
4rule:
5  any:
6    - kind: function_declaration
7    - kind: method_definition
8    - kind: arrow_function
1# 提取类定义及其方法
2id: extract-classes
3language: typescript
4rule:
5  kind: class_declaration
6  has:
7    kind: method_definition
1# 提取模块导出
2id: extract-exports
3language: typescript
4rule:
5  kind: export_statement

把这些结构信息组织成树,就得到了代码库的"语义目录"。

LLM 如何在代码树上推理?

有了语义树,LLM 可以像 PageIndex 处理文档一样处理代码:

用户:这个项目的用户认证流程是怎么实现的?

LLM 推理过程:
1. 查看代码树顶层结构
2. 定位到 "Authentication System" 节点
3. 展开子节点,看到 "User Login Flow"
4. 进一步展开,找到 validateCredentials、createSession、handleLoginError
5. 返回这些函数的摘要和位置

这比向量搜索靠谱——不会被 // TODO: add authentication 这样的注释干扰。

方向二:PageIndex 增强 ast-grep(推理导航 + 精确提取)

第二个结合方向:先用 PageIndex 风格的推理定位到相关区域,再用 ast-grep 做精确的结构化查询。

解决"不知道搜什么模式"的问题

ast-grep 的强大在于精确匹配,但前提是你知道要搜什么模式。

结合 PageIndex 的推理能力,可以这样工作:

用户:找出所有 API 请求失败后的重试逻辑

步骤 1:PageIndex 风格的推理导航
-> 分析代码树,定位到 src/services/http/ 目录
-> 找到 retry.ts 和 interceptor.ts
-> 识别出关键函数 withRetry()、retryOnError()

步骤 2:ast-grep 精确提取
-> 生成搜索模式:withRetry($FUNC, $OPTIONS)
-> 在整个代码库中搜索所有调用
-> 提取重试配置的具体参数

实际工作流示例

用户:这个项目里,数据库事务是怎么处理的?

系统:[PageIndex 推理]
分析代码树...
-> 定位到 src/database/ 模块
-> 找到 transaction.ts
-> 识别关键模式:withTransaction、beginTransaction、commit、rollback

系统:[ast-grep 精确提取]
搜索模式:withTransaction($CALLBACK)
找到 23 处调用

搜索模式:try { $$$BODY } catch ($ERR) { rollback($$$) }
找到 5 处手动事务处理

结果:
1. 主要使用 withTransaction 包装器(23 处)
2. 5 处手动事务处理,位于:
   - src/services/order.ts:145
   - src/services/payment.ts:89
   - ...

代码审计场景

这种结合在代码审计中特别有用:

用户:找出所有没有错误处理的外部 API 调用

系统:[PageIndex 推理]
-> 定位到所有 HTTP 客户端调用
-> 识别模式:fetch、axios、httpClient

系统:[ast-grep 精确提取]
搜索:fetch($URL) 且不在 try-catch 块内
搜索:axios.get($URL) 且不在 try-catch 块内

结果:
发现 12 处未处理的 API 调用:
- src/api/users.ts:34 - fetch(userEndpoint)
- src/api/orders.ts:78 - axios.get(orderUrl)
- ...

技术实现思路

索引构建流程

1. 代码解析
   └── ast-grep 遍历所有文件,提取结构元素

2. 语义分组
   └── 按模块/功能聚合相关函数和类

3. 摘要生成
   └── LLM 为每个节点生成简短摘要

4. 树构建
   └── 组织成 PageIndex 风格的层级结构

5. 索引存储
   └── 树结构 + 节点内容分表存储

在线检索流程

1. 问题理解
   └── 识别用户意图:查找、审计、理解、迁移

2. 树导航(PageIndex 风格)
   └── LLM 在代码树上推理,定位相关节点

3. 模式生成
   └── 根据定位结果,生成 ast-grep 搜索模式

4. 精确提取(ast-grep)
   └── 在代码库中执行结构化搜索

5. 结果组装
   └── 返回代码片段 + 位置 + 上下文

挑战与思考

1. 如何自动生成有意义的代码层级?

文档有目录,代码没有。需要启发式规则:

  • 按目录结构初步分组
  • 按 import/export 关系聚合
  • 按命名约定识别功能模块
  • LLM 辅助生成语义分组

2. 跨文件依赖如何表达?

代码的调用关系是图,不是树。可能的方案:

  • 树结构表达主要层级
  • 额外的"引用"字段表达跨节点依赖
  • 或者接受"多棵树"的表达方式

3. LLM 推理成本如何控制?

每次查询都让 LLM 推理,成本太高。优化方向:

  • 缓存常见查询的导航路径
  • 用小模型做初步筛选,大模型做精确定位
  • 预计算热门节点的摘要和关联

4. 增量更新如何处理?

代码库频繁变更,全量重建不现实:

  • 监听文件变更,增量更新受影响的节点
  • 定期全量校验,修复漂移

与现有方案的对比

方案优势劣势
向量搜索语义理解,不需要精确模式相似不等于相关,结构信息丢失
ast-grep精确匹配,理解代码结构需要先知道搜什么模式
LSP/ctags精确的定义/引用跳转只能点对点,不能语义查询
ast-grep + PageIndex推理导航 + 精确提取实现复杂,需要 LLM 成本

可能的应用场景

  1. 代码库问答:“这个项目的缓存策略是什么?”
  2. 代码审计:“找出所有 SQL 拼接(潜在注入风险)”
  3. 代码迁移:“找出所有需要从 v1 API 升级到 v2 的调用”
  4. 新人 onboarding:“这个模块的核心流程是什么?”
  5. 代码评审辅助:“这个 PR 影响了哪些关键路径?”

总结

ast-grep 和 PageIndex 代表了两种不同的智能:

  • ast-grep:精确的结构理解,“我知道代码长什么样”
  • PageIndex:推理的导航能力,“我知道去哪里找”

把它们结合起来:

  • ast-grep 增强 PageIndex:用结构提取构建语义索引
  • PageIndex 增强 ast-grep:用推理导航指导精确搜索

这可能是代码搜索的下一个形态:结构化理解 + 推理导航 + 精确提取

当然,这还只是想法。真正实现需要解决很多工程问题。但方向是清晰的:让 AI 像人类专家一样理解代码——先看结构,再定位,最后精确提取。


参考资料