多文件搜索的深度对比:六款工具的检索策略

wescode · 2026-11-14 · 搜索 / 检索 / 对比

利益声明:本文作者参与了 wescode 的开发。文中涉及 wescode 的技术描述基于内部测试数据;涉及其他工具的描述基于各家公开文档和社区反馈。

当你对 AI 说:"帮我找到所有调用 processPayment 的地方"——然后呢?

不同工具的处理方式天差地别:有的做文本匹配,有的做语义搜索,有的查调用图,有的直接 grep。这些差异直接决定了:你拿到的结果是精确的还是充满噪音的,是完整的还是遗漏了关键调用方的。


一、测试场景:一次跨 5 个文件的参数改动

假设你的项目里有一条支付调用链,跨越 5 个文件。你需要给 processPayment 加一个 currency 参数——问题是:到底有几个地方需要跟着改?

文件 1:接口定义

// src/gateways/PaymentGateway.ts
export interface PaymentGateway {
  processPayment(amount: number): Promise<PaymentResult>
  refundPayment(paymentId: string): Promise<void>
}

文件 2:实现类

// src/gateways/StripeGateway.ts
import type { PaymentGateway, PaymentResult } from './PaymentGateway'

export class StripeGateway implements PaymentGateway {
  constructor(private stripe: Stripe) {}

  async processPayment(amount: number): Promise<PaymentResult> {
    const charge = await this.stripe.charges.create({
      amount,
      currency: 'usd', // ← 当前硬编码,你想改成参数传入
    })
    return { id: charge.id, status: charge.status }
  }

  async refundPayment(paymentId: string): Promise<void> {
    await this.stripe.refunds.create({ charge: paymentId })
  }
}

文件 3:Service 调用 gateway

// src/services/OrderService.ts
import type { PaymentGateway } from '../gateways/PaymentGateway'

export class OrderService {
  constructor(private gateway: PaymentGateway, private repo: OrderRepo) {}

  async checkout(input: CheckoutInput): Promise<Order> {
    const payment = await this.gateway.processPayment(input.amount) // ← 调用点 1
    return this.repo.save({ ...input, paymentId: payment.id })
  }
}

文件 4:CronJob 通过接口间接调用

// src/cron/SettlementJob.ts
import type { PaymentGateway } from '../gateways/PaymentGateway'

export class SettlementJob {
  constructor(private gateway: PaymentGateway, private repo: OrderRepo) {}

  async execute() {
    const pending = await this.repo.findPendingOrders()
    for (const order of pending) {
      await this.gateway.processPayment(order.remainingAmount) // ← 调用点 2(接口调用)
    }
  }
}

完整调用链:

CheckoutController.handle()
  → OrderService.checkout()
    → PaymentGateway.processPayment()   ← 你要改的接口方法

SettlementJob.execute()
  → PaymentGateway.processPayment()     ← 通过同一接口间接调用

StripeGateway.processPayment()          ← 接口实现,参数签名必须一起改

你至少需要改 4 个地方:接口定义、StripeGateway 实现、OrderService 的调用、SettlementJob 的调用。漏一个 = 运行时类型错误。


二、补充场景:Python FastAPI 的跨文件依赖注入

TypeScript 的 implements 关系已经够难搜了。在 Python FastAPI 项目里,依赖注入让调用链更加隐蔽:

# app/services/payment_service.py
from app.repositories.payment_repo import PaymentRepo

class PaymentService:
    def __init__(self, repo: PaymentRepo):
        self.repo = repo

    async def process_payment(self, amount: float, user_id: str) -> dict:
        record = await self.repo.create_payment(amount, user_id)
        return {"payment_id": record.id, "status": "pending"}
# app/dependencies.py
from app.services.payment_service import PaymentService
from app.repositories.payment_repo import PaymentRepo

def get_payment_service() -> PaymentService:
    return PaymentService(repo=PaymentRepo())  # ← 依赖注入的工厂函数
# app/routers/orders.py
from fastapi import APIRouter, Depends
from app.dependencies import get_payment_service
from app.services.payment_service import PaymentService

router = APIRouter()

@router.post("/checkout")
async def checkout(
    body: CheckoutRequest,
    svc: PaymentService = Depends(get_payment_service)  # ← FastAPI DI
):
    result = await svc.process_payment(body.amount, body.user_id)  # ← 调用点
    return result

当你想给 process_payment 加一个 currency 参数,grep process_payment 会返回什么?——除了真正的调用点,还有 get_payment_service 工厂函数里的类名引用、测试里的 mock、文档字符串,全部混在一起。而 Depends(get_payment_service) 这种 FastAPI 的依赖注入魔法,grep 完全看不出 svc 就是 PaymentService 的实例。

grep 半日困境


三、三种工具在这个任务上的搜索深度

Cursor:Embedding 语义搜索

Cursor 把整个代码库切片上传算向量,按语义相似度返回结果。它搜 processPayment 返回的前 5 个命中:

排名文件命中原因是你要改的吗
1StripeGateway.ts名字完全匹配✅ 接口实现
2PaymentGateway.ts → refundPayment同一个接口,名字像否(兄弟方法)
3PaymentHistory.ts名字里有 Payment否(无调用关系)
4BillingService.ts语义相关(都和钱有关)否(无调用关系)
5OrderService.tscheckout 里调了它✅ 调用方

找到 2 个,漏了 1 个关键调用方。 SettlementJob 没出现在前 5——因为"结算任务"和"处理支付"的语义距离比"退款"更远。而且日志行 logger.info("processPayment failed") 可能挤掉真正的调用方。

Claude Code:grep 暴力搜索

Claude Code 在终端跑 rg "processPayment" --type ts,返回 19 个结果:

类型数量是你要改的吗
接口声明 + 实现定义3⚠️ 部分是(实现要改,声明也要改)
直接调用 this.gateway.processPayment(...)2✅ 真正的调用方
测试 mock mockGateway.processPayment.mockResolvedValue(...)6测试代码
注释和文档 // 重构 processPayment 时...3注释
日志字符串 logger.info("processPayment failed")2字符串
类型引用 type ProcessPaymentArgs = Parameters<...>2⚠️ 可能需要改
旧版函数 processPaymentLegacy1不同函数

19 个结果里真正需要改的有 4-5 个。你需要手动排除 14 个干扰项——逐个点开文件、看上下文、判断是不是真调用。这就是"搜索用 5 秒,筛选用 20 分钟"的来源。

而且 grep 有一个根本性盲区:它能找到 this.gateway.processPayment() 是因为方法名相同。如果某天接口方法改名了,或者代码里写的是 this.pay.process()(别名调用),grep 就彻底搜不到了。

wescode CKG:调用图一键返回

CKG 基于 tree-sitter 语法解析建好的调用关系,直接返回:

调用方关系类型文件
OrderService.checkout()CALLSsrc/services/OrderService.ts
SettlementJob.execute()CALLS(经 IMPLEMENTS 解析)src/cron/SettlementJob.ts
StripeGateway.processPayment()IMPLEMENTSsrc/gateways/StripeGateway.ts

3 个结果,3 个真正需要改的地方。零干扰。

SettlementJob 能被找到,是因为 CKG 知道 this.gateway 的类型是 PaymentGateway,StripeGateway 实现了 PaymentGateway(IMPLEMENTS 边),所以 this.gateway.processPayment() 就是在调用 PaymentGateway.processPayment()。grep 做不到,Embedding 也做不到——只有语法级的类型解析才能推导出这种间接关系。

Before / After 对比:grep 搜索 vs CKG 搜索

维度grep / rg 搜索CKG 调用图查询
耗时搜索 5 秒 + 人工筛选 15-25 分钟查询 < 200ms(内部测试数据),零人工筛选
返回结果数19 个3 个
噪音率~74%(14/19 是干扰项)0%
遗漏率接口实现、覆写方法可能漏掉静态可分析的关系 0 遗漏
能否追踪间接调用否(只看字符串匹配)是(递归遍历调用链)

四、CKG 的多层遍历是怎么一层层追出去的

CKG 代码知识图谱全景

当你在 wescode 里选中 PaymentGateway.processPayment,CKG 的查询分三步完成:

第一步:从改动点出发,走 CALLS 边找直接调用方。

查询 call_edges 表里所有 target_symbol = 'PaymentGateway.processPayment' 的行。直接命中 OrderService.checkout() 和 SettlementJob.execute()——它们的代码里都写了 this.gateway.processPayment(...)。

第二步:走 IMPLEMENTS 边找接口实现方。

查询 implements_edges 表里所有实现了 PaymentGateway 的类。命中 StripeGateway——它的 processPayment 方法签名必须和接口一致,改接口就必须改它。如果还有 PayPalGateway、AlipayGateway,也会一并返回。

第三步:递归走 CALLS 边找上层调用方。

从第一步找到的 OrderService.checkout() 继续向上查——谁在调用 checkout()?命中 CheckoutController.handle()。这条链一直递归到入口层(Controller / CronJob / 事件处理器),直到没有更上层的调用方。

tree-sitter AST 解析流程

三步查询在本地 SQLite 上执行,总耗时毫秒级。一次查询,完整的影响链,零干扰项。


五、搜索策略对比表

上下文组装流程

维度CursorCopilotClaude Codewescode通义灵码Trae
搜索方法Embedding 语义搜索LSP + GitHub 搜索grep/rg 暴力搜索CKG 调用图 + FTS5IDE 搜索 + 模型推测Agent 搜索 + IDE
区分调用 vs 字符串✅ LSP 区分否(靠模型后处理)✅ tree-sitter 语法级
间接调用追踪不支持不支持不支持支持(递归查询)不支持不支持
模糊/自然语言查询✅ 强项⚠️ 有限⚠️ 靠模型构造⚠️ FTS5 文本搜索⚠️ 模型推测⚠️ 模型推测
大仓索引速度(10万行)分钟到小时级取决于 LSP即时(不索引)5-15 秒(内部测试数据)取决于 IDE取决于 IDE
数据存储Cursor 数据库本地 + GitHub不存储本地 SQLiteIDE 本地IDE 本地

六、CKG 的六种关系边:每种边的真实代码例子

CKG 通过 10-Pass 索引管线从 tree-sitter AST 中提取六种关系。每种关系对应一类你在重构中必须追踪的依赖:

1. CALLS — 直接调用

// OrderService.ts
async checkout(input: CheckoutInput) {
  const payment = await this.gateway.processPayment(input.amount)
  //                                 ^^^^^^^^^^^^^^^^ CALLS 边
}

这是最常见的关系。CKG 记录"谁调用了谁",改 processPayment 的签名时,所有 CALLS 边的源头都需要跟着改。

2. IMPLEMENTS — 接口实现

// StripeGateway.ts
export class StripeGateway implements PaymentGateway {
  //                          ^^^^^^^^^^^^^^^^^^ IMPLEMENTS 边
  async processPayment(amount: number) { /* ... */ }
}

grep 搜不到这种关系。 你搜 processPayment 能找到 StripeGateway 里的同名方法,但搜不出它是因为实现了 PaymentGateway 接口才必须有这个方法。CKG 从 implements 关键字直接提取。

3. OVERRIDES — 方法覆写

// PremiumPaymentProcessor.java
public class PremiumPaymentProcessor extends BasePaymentProcessor {
    @Override
    public PaymentResult processPayment(BigDecimal amount) {
        // ^^^^^^^^^^^^^^^^ OVERRIDES 边:覆写了父类方法
        applyDiscount(amount);
        return super.processPayment(amount.multiply(DISCOUNT_RATE));
    }
}

Java 项目里 @Override 满天飞。grep 搜 processPayment 能找到这个方法,但不知道它和父类的 processPayment 有覆写关系——你改了父类签名不改子类,编译器会报错,但 AI 如果不知道这层关系,就不会主动帮你一起改。

4. IMPORTS — 模块导入

// OrderService.ts
import type { PaymentGateway } from '../gateways/PaymentGateway'
//             ^^^^^^^^^^^^^^ IMPORTS 边

IMPORTS 边帮助 CKG 理解模块间的依赖方向。当你重命名一个导出符号时,所有 IMPORTS 边的源头都需要更新。

5. DEFINES — 符号定义

# app/services/payment_service.py
class PaymentService:
    async def process_payment(self, amount: float, user_id: str) -> dict:
        # ^^^^^^^^^^^^^^^ DEFINES 边:此文件定义了 process_payment
        ...

DEFINES 边是 CKG 的"定义跳转"基础——从任何一处调用点,沿 DEFINES 边直接跳到定义处。

6. EXTENDS — 类继承

// PremiumOrder.ts
export class PremiumOrder extends Order {
  //                      ^^^^^^^ EXTENDS 边
  calculateTotal(): number {
    return super.calculateTotal() * 0.9  // 继承父类方法并扩展
  }
}

EXTENDS 边和 OVERRIDES 边配合使用:EXTENDS 说"A 继承了 B",OVERRIDES 说"A 覆写了 B 的某个方法"。两者结合,CKG 能完整追踪继承树上的所有影响。

grep 能覆盖几种?

关系grep 能找到吗说明
CALLS✅ 名字出现时能找到但无法区分调用 vs 字符串
IMPLEMENTS搜不到这是 grep 最大的盲区
OVERRIDES搜不到grep 不知道覆写关系
IMPORTS✅import 语句本身是文本
DEFINES✅定义处包含方法名
EXTENDS✅extends 关键字是文本

grep 在 6 种关系里有 4 种能搜到,但 IMPLEMENTS 和 OVERRIDES 完全搜不到——而这两种恰恰是 TypeScript / Java 项目里密度最高的关系。接口抽象越多、依赖注入越重的项目,grep 的盲区越大。

CKG 追不到的场景

场景为什么追不到替代方案
动态派发 obj[methodName]()运行时才确定调用目标grep 兜底
配置文件引用 YAML/JSON 里的字符串tree-sitter 不解析非代码文件grep 兜底
消息队列 生产者 → Redis/Kafka → 消费者没有直接调用关系grep + 文档
事件总线 eventBus.emit('payment') → eventBus.on('payment')字符串匹配不是调用grep 兜底
元编程 / 代码生成CKG 只分析源码,不分析生成物看生成后的代码

覆盖率在大多数业务项目上在 85-95%(内部测试数据)——因为大多数代码的调用关系是静态可分析的。剩下的 5-15% 靠 FTS5 全文搜索和 grep 兜底。不完整但精确的结果,比完整但充满干扰的结果有用得多。


七、索引性能

CKG 所有索引在本地完成,存储在本地 SQLite。不上传任何代码到服务器。

项目规模首次索引增量更新(文件保存后)索引大小
1 万行< 2 秒< 50ms~1MB
10 万行5-15 秒(内部测试数据)100-200ms~10MB
50 万行30-60 秒(内部测试数据)200-500ms~50MB

对比 Cursor 的 Embedding 索引:Cursor 需要把代码分块上传到服务端算向量,受 Embedding API 吞吐量限制。CKG 是纯本地 CPU 计算,不受网络和 API 限速约束。


八、实战选型建议

你的主要需求推荐工具原因
"谁调用了 X" 精确查询wescode(CKG)调用图遍历,语法级精度
"找到和 X 相关的代码" 模糊查询Cursor(Embedding)语义理解强,自然语言查询好
不需要预索引,即时搜索Claude Code(grep)无索引开销,即开即搜
强类型语言的精确引用Copilot(LSP)TypeScript/Java 的类型级精度
修改函数后的影响分析wescode(CKG 递归查询)唯一能追踪间接调用链的方案

总结:如果你的工作流以重构和影响分析为主——CKG 调用图是独一无二的能力。Embedding 在"找类似代码"场景下更自然,grep 在配置文件搜索上不可替代。三种策略各有用处,但当你问的是**"改了这里还要改哪里"**——这是每次重构前最关键的问题——只有调用图能给你一个准确且完整的答案。


FAQ

Q:CKG 对动态语言(Python / JavaScript)准确吗? A:静态分析对 Python 的 duck typing、JavaScript 的 obj[methodName]() 确实有盲区。但大多数业务代码的调用关系是静态可分析的——Service 调 Repository、Controller 调 Service,这些都是确定性的。覆盖率 85-95%,比向量检索"名字像就塞进来"准确得多。 Q:能不能同时用 Embedding + 调用图? A:技术上可以。但两种结果混在一起展示时,用户更难分辨哪些是"真有调用关系"、哪些是"名字像"。wescode 选择了只做调用图——宁可不给"类似代码"推荐,也不让干扰项混进影响分析结果。 Q:wescode 还是 VS Code 吗? A:wescode 是 VS Code 的开源 Fork。你的扩展、快捷键、主题、设置全部保留,零迁移成本。CKG 是在 VS Code 基座上加的能力层,不是另一个编辑器。

Q:CKG 支持哪些语言? A:CKG 基于 tree-sitter 做语法解析,支持 TypeScript、JavaScript、Python、Java、Rust 等主流语言。六种关系边在不同语言中的提取精度略有差异——强类型语言(TypeScript、Java)的 IMPLEMENTS / OVERRIDES 边最精确,动态语言(Python、JavaScript)的 CALLS 边覆盖率约 85-90%。