Cursor编辑代码功能是如何实现的?
2025-05-11 10:00 阅读(127)

类似Cursor、Cline、Trae这样的AI IDE已经成为程序员日常开发的标配。

对待AI IDE,一种常见的误区是认为他们的核心流程很复杂。

实际上,考虑到执行效率,通常Agent的设计并不复杂,更多工作量主要来自:



如何保证流程稳定可控(LLM输出的不稳定造成)与性能优化



如何节约上下文空间



......

Cursor的编辑代码功能,就蕴含了节约上下文空间的巧思。

Cursor核心原理

Cursor的核心部分其实是个简单的Agent:


用户的需求给到Cursor后,他会思考要完成任务需要使用哪些内部工具?

使用具体工具后,结合工具调用结果继续思考下一步应该使用什么工具。

直到最终任务结束。

其内部通过Tool_Use(对于openAI旗下模型,就是Function_Call)字段定义了如下10个工具:



codebase_search:语义搜索工具



read_file:读取文件内容(或大纲)



run_terminal_cmd:代表用户在终端执行命令



list_dir:快速列出目录内容



grep_search:基于正则表达式的快速文本搜索



edit_file:编辑文件内容



file_search:基于模糊匹配的快速文件路径搜索



delete_file:删除指定路径的文件



reapply:在编辑结果不符合预期时,调用更智能的模型重新应用最后一次文件编辑



diff_history:查看近期文件修改历史



一个例子

假设我们的需求是用antd的Button组件替换页面中的主按钮

一种可能的执行过程是:


执行run_terminal_cmd,检查antd是否已安装,若未安装需后续处理,参数如下:

{
  "command": "npm list antd || echo 'antd not installed'",
  "require_user_approval": false,
  "is_background": false,
  "explanation": "检查antd是否已安装,若未安装需后续处理。"
}


执行grep_search,查找包含主按钮的代码位置,确定需要替换的文件,参数如下:

{
  "query": "主按钮|main-button|primary",
  "include_pattern": "*.jsx|*.tsx|*.js",
  "case_sensitive": false,
  "explanation": "查找包含主按钮的代码位置,确定需要替换的文件。"
}


根据上一步grep_search返回的目标文件,执行read_file,确认按钮的上下文代码(如样式、事件处理等),参数如下:

{
  "target_file": "src/components/ExamplePage.jsx",
  "start_line_one_indexed": 1,
  "end_line_one_indexed_inclusive": 50,
  "should_read_entire_file": false,
  "explanation": "读取包含主按钮的文件内容,分析按钮的属性和上下文。"
}


执行edit_file,将原生按钮或旧组件替换为antd Button,并保留原有逻辑(如onClick),参数如下:

{
  "target_file": "src/components/ExamplePage.jsx",
  "instructions": "将原生按钮替换为antd的Button组件,保留原有样式和事件。",
  "code_edit": "// ... existing code ...\nimport { Button } from 'antd';\n// ... existing code ...\n<Button type=\"primary\">主按钮</Button>\n// ... existing code ..."
}


由于LLM生成的随机性,完整的工具调用并不一定按上述步骤。

比如在“搜索主按钮的位置”时,也可能先使用list_dir工具,列出文件目录,再从文件名判断哪个文件可能与“主按钮”相关。

为了让LLM记得之前的执行步骤,以及接下来要做什么,这些必要的信息都会存在于模型上下文中:


由于模型上下文有限,Cursor会在多个层面做上下文长度优化,比如:



默认情况下,Agent只会执行20轮步骤



read_file一次最多只会读取200行代码,不够的话再继续读200行



本文要讲的edit_file工具就是上下文长度优化的表率。

edit_file的实现原理

如果说read_file已经够占用上下文了,那么未经优化的情况下,edit_file占用的上下文应该在read_file的两倍左右。

毕竟,要想修改文件,你得同时知道:



原始文件是什么样



要修改成什么样



所以,有别于其他工具的实现原理就是单次命令执行(比如list_dir对应ls),edit_file是一个独立的AI workflow。

他包含至少3个步骤,涉及至少2次模型调用:



读文件



生成编辑方案(使用先进的模型)



执行编辑方案(使用小参数模型)



(可选)如果编辑方案不理想,用先进模型再执行一次(使用reapply工具)



举个例子,假设用户需求是在 src/utils/math.ts 文件中添加一个计算斐波那契数列的函数

第一步,获取文件路径,读取内容。

假设内容如下:

     // 数学工具函数集合
     
     /**
      * 计算两个数的和
      */
     export function add(a: number, b: number): number {
       return a + b;
     }
     
     /**
      * 计算两个数的差
      */
     export function subtract(a: number, b: number): number {
       return a - b;
     }


第二步,首先准备编辑方案:


{
  "content": "上面读取到的原始代码",
  "query": "添加一个计算斐波那契数列的函数",
  "path": "src/utils/math.ts",
  "is_new": false
}


将上述方案发送给智能的模型(比如Claude 3.5及以上)。


模型返回如下内容:

// 数学工具函数集合
     
// ... existing code ...
     
/**
* 计算斐波那契数列的第n个数
*/
export function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}


注意,其中原始内容中未修改的部分被注释 // ... existing code ... 代替了。

这种方式的好处是:可以显著减少上下文空间占用。

但也有缺点:没法通过上述信息直接还原编辑后的代码,得通过模型的能力还原。

所以,第三步,将上述信息一齐给到模型,将注释替换为原始代码:

{
  "content": "读取到的原始代码",
  "query": "添加一个计算斐波那契数列的函数",
  "path": "src/utils/math.ts",
  "is_new": false,
  "code_edit": "上述代码编辑信息"
}


模型返回:


 // 数学工具函数集合

 /**
  * 计算两个数的和
  */
 export function add(a: number, b: number): number {
   return a + b;
 }

 /**
  * 计算两个数的差
  */
 export function subtract(a: number, b: number): number {
   return a - b;
 }

 /**
  * 计算斐波那契数列的第n个数
  */
 export function fibonacci(n: number): number {
   if (n <= 1) return n;
   return fibonacci(n - 1) + fibonacci(n - 2);
 }


由于这一步逻辑不复杂,且对模型生成速度要求较高,所以通常交给微调过的小参数模型执行。

如果模型的执行效果不好(比如在一个大文件中一次性修改多处),还能使用reapply工具使用更智能的模型再执行一遍。

总结

Cursor的核心逻辑是一个简单的Agent,包含10个可调用的内部工具。

其中,大部分工具的实现原理是简单的命令执行。

而edit_file工具是一条涉及3个步骤的AI Workflow,中间涉及到注释的替换。

之所以这么做,是为了节约模型上下文空间。


https://www.zuocode.com