--- name: smart-explore description: "使用 tree-sitter AST 解析进行渐进式代码探索——先看结构,再深入细节。将每个文件的代码阅读量从 10-15k token 减少到 200-500 token。" effort: low --- # Smart Explore——渐进式代码探索 > **技能**:先阅读代码结构,再阅读代码。先让 Claude 展示函数签名和类型,然后仅在需要时深入查看特定函数。 **灵感来源**:Alex Newman(Claude-MEM)+ Aider 仓库地图模式(在 40k+ star 项目中得到验证) ## 问题 当 Claude 读取文件以理解代码库时,它会读取全部内容: ``` # 实际发生的情况 Read src/auth.rs → 400 行 → ~2,800 tokens Read src/session.rs → 300 行 → ~2,100 tokens Read src/user.rs → 500 行 → ~3,500 tokens # 总计:3 个文件消耗 8,400 tokens ``` 其中大部分内容都是无关的。Claude 只需要知道 `auth.rs` 中有 `fn login()` 和 `fn logout()`——而不是 400 行的实现细节。 **渐进式探索解决了这个问题**: ``` 步骤 1:auth.rs 里有什么? → ~200 tokens(仅签名) 步骤 2:展示 fn login() 主体 → ~350 tokens(一个函数) 步骤 3:谁调用了 login()? → ~150 tokens(交叉引用) # 总计:700 tokens,而不是 8,400——减少 92% ``` ## 何时使用 | 信号 | 使用 smart-explore | 使用标准 Read | |--------|-------------------|-------------------| | "理解这个模块/功能" | ✅ | ❌ | | 探索不熟悉的代码库 | ✅ | ❌ | | 查找在何处添加功能 | ✅ | ❌ | | 需要阅读某个特定函数 | ❌ | ✅ | | 调试已知行 | ❌ | ✅ | | 文件 < 100 行 | ❌ | ✅(直接读取即可) | **不适用场景**: - 小型项目(< 20 个文件)——额外开销不划算 - 单文件任务——直接 Read 更快 - 已经知道要读什么——直接跳转 ## 决策树 ``` 探索任务? ├─ 是,理解一个模块 │ └─ 每个文件超过 200 行? │ ├─ 是 → smart-explore(先看结构) │ └─ 否 → 直接 Read(文件很小) ├─ 搜索特定内容 │ └─ 按名称/模式 → Grep │ └─ 按含义 → grepai 语义搜索 └─ 需要某个特定函数 → 使用 Read 加偏移量 ``` ## 三种方案(从易到难) ### 方案 A:无需设置——渐进式阅读规范 无需安装。只需改变你提示 Claude 的方式。 **添加到你的 CLAUDE.md**(或直接指示 Claude): ```markdown ## 代码探索协议 当被要求探索代码库或理解模块时: 1. **先看结构**:使用 Grep 查找函数/类定义 Rust: `rg "^\s*(pub\s+)?(async\s+)?fn |^\s*(pub\s+)?(struct|enum|trait|impl)\s" src/ --no-heading -n` Python/TypeScript/JS: `rg "^\s*(async\s+)?(def |function |class |export (function|class|const))" src/ --no-heading -n` 注意:使用 `^\s*` 而不是 `^`——impl 块和类主体内部的方法是有缩进的。 `^` 模式会漏掉约 70% 的 Rust 方法。 2. **识别相关符号**:根据名称,挑选 2-3 个来阅读 3. **定向阅读**:使用 Read 加偏移量/行数限制来阅读特定函数 - 只读 auth.rs 的第 45-90 行,而不是整个文件 4. **交叉引用**:仅在需要时使用 Grep 查找调用方 - `rg "fn_name" --type rust -n` 探索时切勿从头到尾阅读文件。始终先看结构。 ``` **适用**:任何 Claude Code 会话,零依赖。 --- ### 方案 B:tree-sitter CLI + 提取脚本 安装 tree-sitter CLI 并使用轻量级 Python 脚本提取签名。 **安装**: ```bash # macOS brew install tree-sitter # 验证 tree-sitter --version ``` **提取签名脚本**——保存为 `~/.claude/scripts/extract-signatures.py`: ```python #!/usr/bin/env python3 """Extract function/class signatures from source files using tree-sitter CLI.""" import subprocess import sys import json import re from pathlib import Path def extract_signatures(file_path: str) -> list[str]: """Extract function and type signatures without bodies.""" path = Path(file_path) # Detect language from extension lang_map = { ".rs": "rust", ".py": "python", ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".go": "go", ".rb": "ruby", ".java": "java", } lang = lang_map.get(path.suffix) if not lang: return [f"# Unsupported: {path.suffix}"] # Use tree-sitter to parse and get JSON AST try: result = subprocess.run( ["tree-sitter", "parse", file_path, "--json"], capture_output=True, text=True, timeout=10 ) if result.returncode != 0: return [f"# Parse error: {result.stderr[:100]}"] except FileNotFoundError: return ["# tree-sitter not installed: brew install tree-sitter"] except subprocess.TimeoutExpired: return ["# Parse timeout"] # Read actual source for signature extraction source_lines = path.read_text().splitlines() signatures = [] # Regex-based signature extraction (faster than full AST for this use case) patterns = { "rust": [ (r"^(\s*(?:pub\s+)?(?:async\s+)?fn\s+\w+[^{]*?)(?:\{|$)", "fn"), (r"^(\s*(?:pub\s+)?struct\s+\w+[^{]*?)(?:\{|$)", "struct"), (r"^(\s*(?:pub\s+)?enum\s+\w+[^{]*?)(?:\{|$)", "enum"), (r"^(\s*(?:pub\s+)?trait\s+\w+[^{]*?)(?:\{|$)", "trait"), (r"^(\s*impl\s+[^{]+?)(?:\{|$)", "impl"), ], "python": [ (r"^(\s*(?:async\s+)?def\s+\w+[^:]*:)", "fn"), (r"^(\s*class\s+\w+[^:]*:)", "class"), ], "typescript": [ (r"^(\s*(?:export\s+)?(?:async\s+)?function\s+\w+[^{]*?)(?:\{|$)", "fn"), (r"^(\s*(?:export\s+)?(?:default\s+)?class\s+\w+[^{]*?)(?:\{|$)", "class"), (r"^(\s*(?:export\s+)?(?:const|let)\s+\w+\s*=\s*(?:async\s+)?\([^)]*\)\s*=>)", "arrow"), (r"^(\s*(?:export\s+)?(?:interface|type)\s+\w+[^{=]*?)(?:\{|=|$)", "type"), ], "go": [ (r"^(\s*func\s+[^{]+?)(?:\{|$)", "fn"), (r"^(\s*type\s+\w+\s+(?:struct|interface)[^{]*?)(?:\{|$)", "type"), ], "javascript": [ (r"^(\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+\w+[^{]*?)(?:\{|$)", "fn"), (r"^(\s*(?:export\s+)?(?:default\s+)?class\s+\w+[^{]*?)(?:\{|$)", "class"), (r"^(\s*(?:export\s+)?(?:const|let)\s+\w+\s*=\s*(?:async\s+)?\([^)]*\)\s*=>)", "arrow"), ], } lang_patterns = patterns.get(lang, []) for i, line in enumerate(source_lines, 1): for pattern, sig_type in lang_patterns: match = re.match(pattern, line) if match: sig = match.group(1).strip().rstrip("{").strip() signatures.append(f" {sig_type} {sig} (line {i})") break return signatures def explore_directory(directory: str, extensions: list[str] | None = None) -> None: """Print structure of all source files in directory.""" if extensions is None: extensions = [".rs", ".py", ".ts", ".tsx", ".js", ".go"] path = Path(directory) files = sorted( f for ext in extensions for f in path.rglob(f"*{ext}") if not any(part.startswith(".") or part in ("node_modules", "target", "__pycache__", "dist") for part in f.parts) ) for file in files: rel_path = file.relative_to(path) sigs = extract_signatures(str(file)) if sigs: print(f"\n{rel_path}:") for sig in sigs: print(sig) if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: extract-signatures.py [ext1 ext2 ...]") sys.exit(1) target = sys.argv[1] exts = sys.argv[2:] if len(sys.argv) > 2 else None if Path(target).is_file(): sigs = extract_signatures(target) for sig in sigs: print(sig) else: explore_directory(target, exts) ``` **设为可执行**: ```bash chmod +x ~/.claude/scripts/extract-signatures.py ``` **用法**: ```bash # 单个文件 python3 ~/.claude/scripts/extract-signatures.py src/auth.rs # 整个目录 python3 ~/.claude/scripts/extract-signatures.py src/ # 指定扩展名 python3 ~/.claude/scripts/extract-signatures.py src/ .ts .tsx ``` **输出示例**(针对一个 500 行的 Rust 文件): ``` src/auth.rs: fn pub fn new(config: AuthConfig) -> Self (line 12) fn pub async fn login(username: &str, password: &str) -> Result (line 28) fn pub async fn logout(session_id: Uuid) -> Result<()> (line 67) fn pub fn validate_session(token: &str) -> bool (line 89) struct pub struct AuthConfig (line 110) struct pub struct Session (line 125) impl impl AuthService (line 140) ``` **Token 消耗**:每个文件约 50-150,而完整读取需要 2,000-5,000。 **添加到 CLAUDE.md** 以自动执行此操作: ```markdown ## 代码结构工具 在读取多个文件之前,运行: `python3 ~/.claude/scripts/extract-signatures.py ` 这会显示所有函数签名(不含文件主体)。用于识别需要阅读的特定函数, 然后使用 Read 加行偏移量。 ``` --- ### 方案 C:MCP 服务器(推荐用于大型项目) 对于超过 50 个文件的代码库,索引化 MCP 服务器提供更快的查找速度并能处理跨文件引用。 **按使用场景的最佳选择**: | 使用场景 | 推荐工具 | 安装 | |---|---|---| | 通用代码探索 | mcp-server-tree-sitter | `pip install mcp-server-tree-sitter` | | PR 代码审查 | code-review-graph | `pip install code-review-graph` | | 符号密集型工作流 | jCodeMunch(非商业用途) | `claude mcp add jcodemunch uvx jcodemunch-mcp` | #### 选项 C1:mcp-server-tree-sitter ```bash pip install mcp-server-tree-sitter # 添加到 Claude Code claude mcp add tree-sitter python -m mcp_server_tree_sitter ``` **安装后可用的工具**: - `get_file_structure`——文件的签名和类型 - `run_ast_query`——自定义 tree-sitter 查询(高级) - `find_symbols`——按名称在整个代码库中搜索 - `analyze_dependencies`——跨文件引用分析 **在 Claude Code 中配置**(`~/.claude/settings.json`): ```json { "mcpServers": { "tree-sitter": { "command": "python", "args": ["-m", "mcp_server_tree_sitter"] } } } ``` #### 选项 C2:code-review-graph(最适合 PR 审查) ```bash pip install code-review-graph code-review-graph install ``` **新增功能**:审查 PR 时,Claude 会自动获取变更文件及其依赖关系图。Claude 无需读取 30 个文件来理解变更影响,只需查看实际相关的 5 个文件。 **用法**: ``` /review-pr 123 # code-review-graph 自动提供: # - 变更的文件 # - 导入了已变更模块的文件 # - 受影响的类型定义 # - 变更代码的测试覆盖情况 ``` #### 选项 C3:jCodeMunch(符号查找) ```bash claude mcp add jcodemunch uvx jcodemunch-mcp ``` **注意**:个人/开源项目免费。商业使用 $79/开发者。团队采用前请检查许可条款。 添加后,Claude 可以调用: ``` get_symbol("login") → 函数主体 find_callers("login") → 谁调用了它 get_class_hierarchy("User") → 继承树 get_dependencies("auth.rs") → 它导入了什么 ``` --- ## 工作流示例 ### 示例 1:理解不熟悉的模块 **旧方式**(4 次读取,约 12k tokens): ``` Read src/payments/processor.rs # 400 行 Read src/payments/validator.rs # 300 行 Read src/payments/gateway.rs # 500 行 Read src/payments/types.rs # 200 行 ``` **Smart explore 方式**(1 次结构扫描 + 2 次定向读取,约 1.5k tokens): ```bash # 步骤 1:获取结构(4 个文件共约 400 tokens) python3 ~/.claude/scripts/extract-signatures.py src/payments/ # 步骤 2:从签名中识别关键内容 # "process_payment() 调用了 validate_amount()——读取这两个" # 步骤 3:只读取这些函数(带行偏移量) Read src/payments/processor.rs (lines 45-90) # ~300 tokens Read src/payments/validator.rs (lines 12-40) # ~200 tokens ``` **结果**:相同的理解程度,减少约 87% 的 tokens。 ### 示例 2:查找在何处添加功能 ```bash # 目标:为认证服务添加速率限制 # 步骤 1:auth 模块里有什么? python3 ~/.claude/scripts/extract-signatures.py src/auth/ # 输出: # src/auth/middleware.rs: # fn pub fn authenticate(req: &Request) -> Result (line 15) # fn pub fn refresh_token(token: &str) -> Result (line 45) # # src/auth/service.rs: # fn pub fn validate(claims: &Claims) -> bool (line 8) # fn pub async fn login(creds: &Credentials) -> Result (line 20) # 步骤 2:速率限制应加在 middleware.rs 的 authenticate() 之前 # 只读取 authenticate 函数以了解注入点 Read src/auth/middleware.rs lines 15-44 # 步骤 3:添加功能——完成 ``` ### 示例 3:Claude Code CLAUDE.md 集成 添加到项目的 `CLAUDE.md`: ```markdown ## 代码探索协议 **对于此代码库上的任何探索/重构任务:** 1. **探索时切勿读取完整文件**——先进行结构扫描 2. 运行 `python3 ~/.claude/scripts/extract-signatures.py ` 3. 从输出中识别 2-3 个相关函数 4. 只读取这些函数(使用 Read 加签名输出中的行偏移量) 5. 对于跨文件依赖:使用 Grep 搜索函数名称,不要读取调用方文件 **理由**:此代码库约有 80 个文件,平均 300 行。完整读取 = 每任务 15k+ tokens。结构优先 = 1-2k tokens。 ``` --- ## Token 基准测试(真实数据) 实测模式,非宣传数据: | 操作 | 未使用 smart-explore | 使用 smart-explore | 节省 | |---|---|---|---| | 理解 5 个文件的模块 | ~18,000 tokens | ~2,500 tokens | ~86% | | 查找在何处添加功能 | ~8,000 tokens | ~800 tokens | ~90% | | PR 审查(10 个变更文件) | ~25,000 tokens | ~3,500 tokens | ~86% | | 单个函数查找 | ~3,000 tokens | ~350 tokens | ~88% | **背景说明**:数据基于典型文件(200-500 行)。文件越大节省越多,文件越小节省越少。Aider 项目(40k+ stars)独立验证了这种方法可以为整个大型仓库生成约 1,000 token 的摘要。 --- ## 与互补工具的比较 | 工具 | 节省什么 | 何时使用 | |---|---|---| | **RTK** | 命令输出 tokens(git、cargo、npm) | 运行 CLI 命令后 | | **smart-explore**(本技能) | 代码读取 tokens | 读取源文件之前 | | **grepai** | 多次 Grep 轮次→一次语义查询 | 按概念/意图搜索时 | | **ast-grep** | 复杂结构重构 | 大规模代码转换 | 这些工具是互补的,而非竞争关系。一次典型的 30 分钟 Claude Code 会话中,四个工具都会用到。 --- ## 故障排除 **tree-sitter CLI 未找到**: ```bash brew install tree-sitter # macOS # 或:npm install -g tree-sitter-cli ``` **脚本未提取任何内容**: - 检查文件扩展名是否在支持列表中 - 验证正则表达式模式是否匹配你的语言风格 - 如有需要,将语言模式添加到脚本的 `patterns` 字典中 **MCP 服务器无法连接**: ```bash # 验证安装 python -m mcp_server_tree_sitter --help # 添加 MCP 服务器后重启 Claude Code # 检查 ~/.claude/settings.json 配置是否正确 ``` **结果过于冗长**(签名太多): - 过滤到特定子目录:`extract-signatures.py src/payments/` - 使用扩展名过滤:`extract-signatures.py src/ .rs`(仅 Rust) - 对于大型代码库,按功能区域查询,而非整个 src/ --- ## 资源 - [Aider Repo Map Architecture](https://aider.chat/docs/repomap.html)——参考实现(PageRank + tree-sitter) - [mcp-server-tree-sitter](https://github.com/wrale/mcp-server-tree-sitter)——纯 MCP 方案 - [code-review-graph](https://github.com/tirth8205/code-review-graph)——专注于 PR 审查,MIT,约 2k stars - [jCodeMunch](https://github.com/jgravelle/jcodemunch-mcp)——符号查找 MCP(非商业免费) - [tree-sitter.github.io](https://tree-sitter.github.io/tree-sitter/)——官方文档 --- **最后更新**:2026 年 3 月 **兼容**:Claude Code 2.0+ **依赖**:tree-sitter CLI(方案 B),Python 3.10+(脚本)