Cursor編輯代碼功能是如何實現的?
大家好,我卡頌。
類似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": "http:// ... 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
,中間涉及到注釋的替換。
之所以這么做,是為了節約模型上下文空間。