智能光标工具CursorClaw:基于AST的代码语义导航与编辑器集成实战
1. 项目概述:一个为开发者“减负”的智能光标工具
如果你和我一样,每天有超过8小时的时间是在代码编辑器中度过的,那么你一定对“光标移动”这个看似微不足道,实则频繁到令人烦躁的操作深有体会。无论是从函数定义跳到调用处,还是在同一行代码的不同位置间反复横跳,或者是在一个超长的对象字面量里寻找某个属性,每一次移动都意味着一次键盘敲击或鼠标点击。日积月累,这不仅降低了编码效率,更在无形中消耗着我们的专注力。今天要聊的这个项目——keunsy/cursorclaw,就是一位开发者为了解决这个痛点而创造的“智能光标”工具。它不是一个庞大的IDE插件,而是一个轻量级、可高度定制的命令行工具,旨在通过理解你的代码上下文,让光标“自动”移动到你想去的地方。
简单来说,cursorclaw的核心思想是“预测并直达”。它通过分析你当前光标所在位置的代码语义(比如,你正指向一个变量名、一个函数调用、一个导入语句),结合你预设的规则或简单的自然语言指令,自动将光标移动到相关联的目标位置。想象一下,你不再需要手动滚动查找import语句来确认某个模块是否已引入,也不再需要在一堆嵌套的if-else中费力地定位某个分支的闭合花括号。cursorclaw试图成为你手指和思维之间的“快捷键”,将意图直接转化为光标动作。
这个项目特别适合那些追求极致效率、厌倦了重复性机械操作的中高级开发者。无论你主要使用 Vim、Emacs、VSCode、Sublime Text 还是其他编辑器,只要它支持通过命令行或脚本与外部工具交互,cursorclaw就有可能集成进去,为你带来一种全新的、更“智能”的代码导航体验。它不是要取代你现有的编辑器快捷键,而是作为一个强大的补充,处理那些快捷键难以覆盖的、需要“理解”代码的复杂跳转场景。
2. 核心设计理念与架构拆解
2.1 从“手动寻路”到“语义导航”的范式转变
传统的代码导航依赖于相对位置(上下左右移动)、基于文本的搜索(Ctrl+F),或是编辑器提供的有限语义功能(如“转到定义”、“查找所有引用”)。这些方法在大多数时候是有效的,但它们本质上还是“盲操作”。你需要先知道目标大概在哪里,或者目标的确切文本是什么,然后通过一系列操作去“接近”它。
cursorclaw的设计哲学是反其道而行之:它希望你先“想”要去哪里,然后由工具来“找”并“跳”。这个“想”的过程,可以通过多种方式触发:
- 模式匹配:你告诉工具“当光标在
function关键字上时,跳转到对应的函数名”。 - 自然语言指令:你输入一个简单的命令,如“跳转到这个变量的声明处”或“找到这个
import语句对应的模块文件”。 - 上下文推断:工具自动分析当前代码块的类型(如循环、条件判断、函数体),并提供最可能的目标位置(如循环的结束标记、条件语句的另一个分支)。
为了实现这种范式转变,cursorclaw的架构必须包含几个核心组件:
- 代码解析器:这是工具的大脑。它需要能够理解不同编程语言的语法结构,将纯文本代码转化为抽象的语法树(AST)。只有这样,它才能知道“
foo”是一个变量名,“bar()”是一个函数调用,“import”后面跟着的是模块路径。 - 规则引擎:这是工具的行为准则。它定义了一系列“条件-动作”对。例如,条件可以是“当前光标所在的AST节点类型是
CallExpression(函数调用)”,动作则是“向上查找最近的、与之同名的FunctionDeclaration(函数声明)节点,并将光标移动到其名称位置”。 - 编辑器桥接层:这是工具的手脚。它负责与外部编辑器通信。在获取到目标位置(行号、列号)后,它需要通过编辑器提供的API(如VSCode的扩展API、Neovim的RPC接口)或模拟键盘事件,将光标实际移动过去。
- 用户界面/交互层:这是工具的沟通方式。它可能是一个命令行界面(CLI),接收自然语言指令;也可能是一个后台服务,监听特定的快捷键组合,然后根据当前上下文自动执行最可能的跳转。
cursorclaw的巧妙之处在于,它没有试图重新发明轮子去实现一个完整的语言服务器(那太重量级了),而是聚焦于“光标移动”这一单一场景,利用现有的、成熟的语法分析库(如用于JavaScript/TypeScript的@babel/parser,用于Python的ast模块)来快速获取上下文信息,然后应用相对轻量的规则进行决策。
2.2 技术栈选型与权衡
项目的技术选型直接反映了其“轻量、高效、可嵌入”的定位。
核心语言:Node.js / Python。从项目名和常见实践推断,
cursorclaw很可能选择Node.js或Python作为实现语言。这两者都拥有极其丰富的生态系统,特别是在文本处理、AST操作和进程间通信方面。- Node.js 优势:非阻塞I/O适合处理大量小规模的、并发的编辑器请求;NPM上有海量的语言解析器(Babel for JS/TS,
@vue/compiler-domfor Vue, 等等);易于打包成可执行文件(pkg)或VSCode扩展。 - Python 优势:标准库中的
ast模块对Python自身的解析是原生且高效的;通过subprocess或socket与编辑器通信同样方便;在数据科学和机器学习领域的生态,为未来引入更“智能”的预测模型(如基于历史跳转记录学习个人习惯)提供了可能。 选择哪一种,取决于作者最熟悉的语言和目标编辑器的集成难度。如果主要面向VSCode(基于Electron/Node.js),那么Node.js是更自然的选择。
- Node.js 优势:非阻塞I/O适合处理大量小规模的、并发的编辑器请求;NPM上有海量的语言解析器(Babel for JS/TS,
语法解析:专用库而非通用LSP。
cursorclaw没有选择依赖Language Server Protocol(LSP),虽然LSP能提供最全面的语义信息。原因在于LSP通常需要启动一个语言服务器进程,内存和CPU开销较大,且响应速度对于“光标移动”这种要求瞬时反馈的操作来说可能不够理想。相反,选用轻量级的语法解析库,虽然获取的信息可能不如LSP深入(例如,无法进行跨文件的类型推断),但对于单文件内的、基于语法的跳转已经足够,且速度极快。规则定义:YAML/JSON配置与DSL。为了让工具足够灵活,规则必须可由用户自定义。一种常见的做法是使用YAML或JSON文件来定义规则列表。每条规则包含
pattern(用于匹配AST节点或文本的正则/描述)和action(跳转逻辑)。更高级的版本可能会引入一个领域特定语言(DSL),让用户能以更接近自然语言的方式编写规则,例如jump from “call” to “definition” within same file。编辑器集成:标准输入输出与扩展API。为了最大化兼容性,最基础的集成方式是通过标准输入(stdin)和标准输出(stdout)。编辑器将当前文件内容、光标位置、语言类型通过stdin发送给
cursorclaw,cursorclaw计算后通过stdout返回目标行号和列号,再由编辑器脚本执行移动。对于深度集成,如作为VSCode扩展,则可以直接调用VSCode的API来获取编辑器和文档信息,并控制光标。
注意:这种架构的一个潜在挑战是性能。对于非常大的文件,实时生成AST可能会有可感知的延迟。因此,在实际实现中,缓存机制(如对解析后的AST进行缓存,直到文件内容改变)和增量解析(只重新解析文件中被修改的部分)是必须考虑的高级优化点。
3. 核心功能解析与实操配置
3.1 基础跳转规则详解
cursorclaw的强大源于其规则集。我们来看几个最实用、最可能被内置的规则,并理解其背后的实现逻辑。
规则一:函数调用与定义的互跳
- 场景:光标位于一个函数调用(如
processData(userInput))上,你想立刻查看它的实现。 - 规则逻辑:
- 解析器定位光标所在位置,识别出这是一个
CallExpression节点,并提取被调用函数的名字(processData)。 - 规则引擎在当前文件的AST中,向上(或全局)搜索名为
processData的FunctionDeclaration或FunctionExpression节点。 - 如果找到,计算该节点在源代码中的起始位置(行、列)。
- 返回该位置坐标。
- 解析器定位光标所在位置,识别出这是一个
- 配置示例(YAML格式):
- name: "jump_to_function_definition" trigger: node_type: "CallExpression" # 触发的AST节点类型 action: type: "find_definition" target_name: "self.callee.name" # 从当前节点中提取目标名称的路径 scope: "file" # 搜索范围:当前文件 - 实操心得:这个规则看似简单,但在有函数重载或同名函数(如不同类的成员方法)时会有歧义。一个增强版规则可以结合简单的作用域分析,比如优先在当前作用域链内查找。
规则二:配对符号的快速跳转
- 场景:在复杂的嵌套括号
()、花括号{}或方括号[]中,快速从开头跳到结尾,或反之。 - 规则逻辑:这比基于语义的跳转更“语法化”。解析器需要计算括号的嵌套深度。当光标在某一个括号上时,工具需要找到与之配对的另一个括号。
- 获取光标位置的字符,判断是否为
(, ), {, }, [, ]。 - 从该位置开始,向前或向后扫描文本,维护一个计数器。遇到开括号则加一,遇到闭括号则减一。
- 当计数器归零时,当前位置即为配对括号的位置。
- 获取光标位置的字符,判断是否为
- 配置示例:这类规则可能不需要复杂配置,而是作为内置的“语法感知移动”命令。
- 注意事项:纯文本扫描在字符串或注释中包含的括号时会误判。因此,更健壮的实现需要基于AST,或者至少在进行文本扫描时,忽略掉被标记为字符串或注释的内容。
规则三:导入(Import)语句的模块定位
- 场景:光标在
import { useState } from 'react'的'react'字符串上,想快速打开该模块的文件(或至少查看其路径)。 - 规则逻辑:
- 识别出光标位于一个
ImportDeclaration节点的source属性(即模块说明符字符串)。 - 根据模块解析算法(类似于Node.js或打包工具的规则),将相对路径
./utils或模块名react解析为绝对文件路径。 - 动作可以是:a) 在编辑器中打开该文件;b) 将光标移动到该导入语句行(如果只是想快速定位到文件顶部import区域)。
- 识别出光标位于一个
- 配置示例:
- name: "open_imported_module" trigger: node_type: "StringLiteral" parent_node_type: "ImportDeclaration" # 增加父节点约束,确保是import的模块名 action: type: "resolve_and_open" resolver: "node" # 使用Node.js的模块解析逻辑 - 实操心得:模块解析是一个复杂问题,涉及
node_modules、路径别名(Webpack/Vite的alias)、以及多种文件扩展名。一个实用的cursorclaw实现可能会允许用户配置自定义的解析映射表,或者直接集成项目使用的打包工具的配置文件。
3.2 高级特性:自然语言指令与上下文学习
基础规则解决了80%的常见跳转,但cursorclaw的野心可能不止于此。高级特性旨在处理那些不规则、难以用固定模式描述的跳转意图。
自然语言指令接口: 用户可以输入诸如“跳转到这个变量的定义”、“去这个函数的调用者那里”、“找到这个错误类被抛出的地方”等指令。实现这一功能需要:
- 意图识别:一个轻量级的NLP模型或规则集,将自然语言映射到内部的操作指令(
JUMP_TO_DEFINITION,FIND_REFERENCES,FIND_THROW_SITE)。 - 指代消解:理解“这个变量”、“这个函数”具体指代的是光标当前所在的哪个代码实体。这需要结合光标位置的AST信息。
- 执行对应动作:调用与基础规则类似的查找逻辑。
上下文学习与个性化: 工具可以默默记录用户的跳转行为。例如,当用户多次从logger.info(...)跳转到同一个日志配置函数时,工具可以学习到这种关联,即使它们之间没有直接的语法联系(比如不是同一个函数名)。未来在类似上下文中,工具可以优先推荐或自动执行这个跳转。这需要引入一个简单的向量存储或键值对数据库来记录(上下文特征, 目标位置)的映射。
提示:对于个人项目或小团队,自然语言指令的初期实现可以非常“取巧”。例如,只支持几个固定的关键词短语(
“def”代表定义,“ref”代表引用),通过字符串匹配来实现,这比引入完整的NLP管道要简单可靠得多。
4. 与不同编辑器的集成实战
cursorclaw的价值只有在嵌入编辑器后才能体现。下面以VSCode和Neovim为例,讲解集成思路。
4.1 在VSCode中作为扩展运行
这是最无缝的集成方式。我们需要创建一个VSCode扩展。
项目初始化:
npm install -g yo generator-code yo code # 选择“New Extension (TypeScript)”这会产生一个基本的扩展骨架。
扩展激活与命令注册:在
extension.ts的activate函数中,注册一个命令。import * as vscode from 'vscode'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); export function activate(context: vscode.ExtensionContext) { let disposable = vscode.commands.registerCommand('cursorclaw.jump', async () => { const editor = vscode.window.activeTextEditor; if (!editor) { return; } const document = editor.document; const position = editor.selection.active; const languageId = document.languageId; const fileContent = document.getText(); // 构建调用 cursorclaw 的输入数据 const inputData = JSON.stringify({ content: fileContent, line: position.line + 1, // 编辑器行号通常从0开始,CLI可能从1开始 column: position.character + 1, language: languageId, filePath: document.fileName }); try { // 假设 cursorclaw 二进制文件已在 PATH 中 const { stdout } = await execAsync(`cursorclaw --stdin`, { input: inputData }); const result = JSON.parse(stdout); if (result.success && result.location) { const newPosition = new vscode.Position( result.location.line - 1, result.location.column - 1 ); editor.selection = new vscode.Selection(newPosition, newPosition); // 可选:滚动到该位置 editor.revealRange(new vscode.Range(newPosition, newPosition)); } else { vscode.window.showInformationMessage(`CursorClaw: ${result.message || 'No jump target found.'}`); } } catch (error) { vscode.window.showErrorMessage(`CursorClaw error: ${error}`); } }); context.subscriptions.push(disposable); }同时,在
package.json中配置命令和快捷键绑定。"contributes": { "commands": [{ "command": "cursorclaw.jump", "title": "CursorClaw: Smart Jump" }], "keybindings": [{ "command": "cursorclaw.jump", "key": "ctrl+alt+j", "when": "editorTextFocus" }] }打包与分发:使用
vsce工具打包成.vsix文件,即可安装或发布到市场。
VSCode集成注意事项:
- 性能:频繁调用外部进程(
exec)可能有开销。对于更高效的集成,可以考虑将cursorclaw的核心逻辑用TypeScript重写,直接作为扩展的一部分运行,或者使用Node.js的worker_threads在后台运行。 - 状态管理:需要处理编辑器文档内容变更后,缓存AST的失效和更新问题。
- UI反馈:在计算跳转位置时,最好在状态栏显示一个加载指示器,避免用户以为无响应。
4.2 在Neovim中通过Lua插件集成
Neovim的Lua插件体系非常强大,集成外部工具也很方便。
创建插件结构:假设插件名为
cursorclaw.nvim。cursorclaw.nvim/ ├── lua/ │ └── cursorclaw/ │ ├── init.lua │ └── core.lua └── README.md核心跳转函数:在
core.lua中实现主要逻辑。local M = {} -- 假设 cursorclaw 命令行工具已安装 local function run_cursorclaw(content, line, col, lang, filepath) local json = require("cjson") -- 需要安装 lua-cjson local input_data = json.encode({ content = content, line = line, column = col, language = lang, filePath = filepath }) local cmd = string.format('echo %s | cursorclaw --stdin', vim.fn.shellescape(input_data)) local handle = io.popen(cmd, 'r') if not handle then return nil end local output = handle:read('*a') handle:close() local ok, result = pcall(json.decode, output) if ok and result.success and result.location then return { line = result.location.line, col = result.location.column } end return nil end function M.smart_jump() local buf = vim.api.nvim_get_current_buf() local filepath = vim.api.nvim_buf_get_name(buf) local content = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), '\n') local pos = vim.api.nvim_win_get_cursor(0) -- 获取光标位置,行/列都是0-based local line, col = pos[1], pos[2] + 1 -- col需要+1,因为lua是0-based,但工具可能期望1-based -- 获取文件类型,映射到 cursorclaw 支持的语言标识 local ft = vim.bo[buf].filetype local lang_map = { javascript = "js", typescript = "ts", python = "py", lua = "lua" } local lang = lang_map[ft] or ft local target = run_cursorclaw(content, line, col, lang, filepath) if target then -- 跳转到目标位置,注意转换回Neovim的0-based索引 vim.api.nvim_win_set_cursor(0, {target.line, target.col - 1}) else vim.notify("CursorClaw: No jump target found.", vim.log.levels.INFO) end end return M插件初始化与映射:在
init.lua中设置命令和快捷键。local cursorclaw = require('cursorclaw') vim.api.nvim_create_user_command('CursorClawJump', cursorclaw.smart_jump, {}) -- 设置一个快捷键,例如 `<Leader>cj` vim.keymap.set('n', '<Leader>cj', cursorclaw.smart_jump, { noremap = true, silent = true, desc = "CursorClaw Smart Jump" })
Neovim集成心得:
- 异步处理:上面的例子使用了同步的
io.popen,在解析大文件时可能会阻塞Neovim。生产环境应该使用Neovim的异步Job API (vim.fn.jobstart) 来非阻塞地调用外部命令。 - 缓存:同样,为了避免每次跳转都解析整个文件,可以在内存中缓存每个buffer的AST,并在
BufWritePost事件中清除对应缓存。 - 配置化:通过
setup函数让用户能配置cursorclaw的二进制路径、默认快捷键等。
5. 性能优化与疑难排查
5.1 确保响应速度:解析策略与缓存
光标移动操作对延迟极其敏感,理想情况应在100毫秒内完成。以下是关键优化点:
增量解析与缓存:
- 策略:为每个打开的文件维护一个AST缓存。当文件被修改时,不要重新解析整个文件。对于文本编辑,可以使用类似Tree-sitter的增量解析能力,或者对于较小的修改,手动计算受影响的文本范围,并尝试局部更新AST。
- 实现示例(伪代码):
const astCache = new Map(); // filePath -> { ast, contentHash } function getCachedAst(filePath, currentContent) { const cached = astCache.get(filePath); const currentHash = hash(currentContent); if (cached && cached.contentHash === currentHash) { return cached.ast; // 缓存命中 } // 缓存未命中或失效,重新解析 const newAst = parser.parse(currentContent); astCache.set(filePath, { ast: newAst, contentHash: currentHash }); return newAst; } - 缓存失效:监听编辑器的文件保存或内容变更事件,更新或清除缓存。
懒加载与按需解析:
- 不是每次跳转都需要完整的AST。对于“跳转到匹配括号”这类操作,一个轻量级的、仅扫描括号的文本分析器可能比构建完整AST更快。
cursorclaw可以根据触发的规则类型,选择不同的解析粒度。
- 不是每次跳转都需要完整的AST。对于“跳转到匹配括号”这类操作,一个轻量级的、仅扫描括号的文本分析器可能比构建完整AST更快。
Worker线程:
- 将耗时的AST解析和规则匹配计算放在单独的Worker线程中,避免阻塞编辑器的主线程或事件循环。这在VSCode扩展或Electron应用中尤为重要。
5.2 常见问题与解决方案速查表
在实际使用和开发cursorclaw过程中,你可能会遇到以下典型问题:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 执行跳转命令后毫无反应 | 1. 命令未正确绑定。 2. cursorclaw进程启动失败。3. 输入数据格式错误,进程静默崩溃。 | 1. 检查编辑器快捷键绑定或命令面板输入是否正确。 2. 在终端手动运行 cursorclaw --help,确认安装和PATH配置正确。3. 在集成代码中添加详细的日志,打印出发送给 cursorclaw的输入数据和接收到的输出。 |
| 跳转位置不准确(偏移几行几列) | 行号/列号的索引基准不统一。编辑器、AST解析库、cursorclaw内部可能分别使用0-based或1-based索引。 | 这是最常见的问题。仔细检查数据流: 1. 从编辑器获取的位置(通常是0-based行,0-based列)。 2. 传递给 cursorclaw时是否做了转换(例如+1变为1-based)。3. cursorclaw内部计算使用的是哪种基准。4. 返回给编辑器前是否做了反向转换。统一在整个链条中使用1-based索引(内部计算)并在边界处进行转换,通常更清晰。 |
| 对某些语言或语法支持不好 | 1. 使用的语法解析器不支持该语言的新特性。 2. 规则定义未覆盖该语言特有的语法结构。 | 1. 升级解析器库(如Babel、pygls)到最新版本。2. 为特定语言编写自定义规则。检查 cursorclaw的日志,看它识别出的AST节点类型是什么,然后据此调整或新增规则。 |
| 在大文件上操作明显卡顿 | 1. 每次跳转都进行全文件解析。 2. 规则匹配算法效率低下(如全AST遍历)。 3. 未使用Worker线程,阻塞UI。 | 1.实施缓存策略(见上文)。 2. 优化规则引擎。为常用规则(如找定义)建立索引,例如在解析AST时,顺便构建一个 {标识符名: 位置}的映射表。3.将计算移入后台线程。 |
| 规则冲突或意外跳转 | 多条规则可能匹配同一上下文,执行了非预期的动作。 | 1. 为规则设置优先级(priority字段),高优先级先执行。2. 增加规则的匹配条件,使其更精确(例如,不仅匹配节点类型,还匹配父节点类型或代码上下文)。 3. 提供“撤销上次跳转”的功能,并记录日志方便用户查看是哪条规则被触发了。 |
5.3 调试与日志记录
一个健壮的工具离不开良好的可观测性。在cursorclaw核心代码中,应加入不同级别的日志。
// 在核心跳转函数中 function executeJump(context, rules) { logger.debug(`开始跳转计算。文件:${context.filePath}, 位置:(${context.line}, ${context.column})`); const ast = getAst(context.content); const currentNode = locateNodeInAst(ast, context.line, context.column); if (!currentNode) { logger.warn(`无法在AST中定位到光标对应的节点。`); return null; } logger.debug(`当前AST节点类型:${currentNode.type}, 内容:${context.content.slice(currentNode.start, currentNode.end)}`); for (const rule of sortedRules) { if (rule.matcher(currentNode, context)) { logger.info(`规则匹配成功:${rule.name}`); const target = rule.action(currentNode, ast, context); if (target) { logger.debug(`跳转目标位置:(${target.line}, ${target.column})`); return target; } } } logger.info(`未找到匹配的跳转规则。`); return null; }通过设置环境变量(如CURSORCLAW_LOG_LEVEL=debug)来控制日志输出,可以在出现问题时快速定位是解析出错、规则未匹配,还是动作执行失败。
6. 扩展思路与个性化定制
cursorclaw的基础框架搭建好后,它的潜力远不止于简单的跳转。你可以根据自己的工作流,对它进行深度定制和扩展。
1. 项目特定规则(.cursorclawrc): 在你的项目根目录放置一个配置文件,定义本项目特有的跳转规则。例如,在一个使用Redux的React项目中,你可以定义一条规则:当光标在connect(mapStateToProps)(MyComponent)这样的调用上时,跳转到对应的mapStateToProps函数定义。这使跳转逻辑与项目架构深度结合。
2. 与代码审查/静态分析工具结合: 跳转动作不仅可以指向“定义”,还可以指向“问题”。例如,当光标在一个变量上时,可以触发一个规则,查找所有对该变量进行“可能为null”的赋值或使用的地点,并快速在这些位置间循环跳转,辅助进行代码审查或缺陷排查。
3. 基于历史的智能排序: 当多条规则匹配或一个标识符有多个定义时(如函数重载),跳转目标列表可以不再随机排列,而是根据用户的历史选择进行排序。最常被选择的目标排在前面。这需要持久化存储用户的选择记录。
4. 创建“跳转宏”或“导航路径”: 将一系列跳转组合成一个“宏”。例如,一个常见的操作是:查看函数定义 -> 查看其调用的某个子函数 -> 返回原函数。你可以录制这个序列,并绑定到一个快捷键上,实现复杂的导航自动化。
我个人在构思和实现这类工具时的体会是,真正的效率提升来自于对“高频、琐碎、可预测”操作的自动化。cursorclaw的价值不在于处理那些复杂的、一次性的导航,而在于每天为你节省数百次微不足道的击键和鼠标移动。它的实现过程本身,也是对编程语言语法、编辑器生态和工具设计的一次深刻学习。从一个简单的“找定义”规则开始,逐步添加对更多语言、更复杂场景的支持,看着它一点点理解你的代码意图,并准确地将你带到想去的地方,这种成就感是开发普通业务代码难以比拟的。最后一个小技巧是,在开发初期,尽量让规则匹配“宽松”一些,即使偶尔跳转不精确,也先保证它能动起来、有反馈,快速收集使用数据,然后再基于真实的使用模式去收紧规则、优化精度,这样迭代起来会更顺畅。
