Cursor編輯代碼功能是如何實(shí)現(xiàn)的?
大家好,我卡頌。
類似Cursor
、Cline
、Trae
這樣的AI IDE已經(jīng)成為程序員日常開(kāi)發(fā)的標(biāo)配。
對(duì)待AI IDE,一種常見(jiàn)的誤區(qū)是認(rèn)為他們的核心流程很復(fù)雜。
實(shí)際上,考慮到執(zhí)行效率,通常Agent
的設(shè)計(jì)并不復(fù)雜,更多工作量主要來(lái)自:
- 如何保證流程穩(wěn)定可控(
LLM
輸出的不穩(wěn)定造成)與性能優(yōu)化 - 如何節(jié)約上下文空間
......
Cursor
的「編輯代碼」功能,就蘊(yùn)含了「節(jié)約上下文空間」的巧思。
Cursor核心原理
Cursor
的核心部分其實(shí)是個(gè)簡(jiǎn)單的Agent
:
用戶的需求給到Cursor
后,他會(huì)思考「要完成任務(wù)需要使用哪些內(nèi)部工具」?
使用具體工具后,結(jié)合「工具調(diào)用結(jié)果」繼續(xù)思考下一步應(yīng)該使用什么工具。
直到最終任務(wù)結(jié)束。
其內(nèi)部通過(guò)Tool_Use
(對(duì)于openAI
旗下模型,就是Function_Call
)字段定義了如下10個(gè)工具:
- codebase_search:語(yǔ)義搜索工具
- read_file:讀取文件內(nèi)容(或大綱)
- run_terminal_cmd:代表用戶在終端執(zhí)行命令
- list_dir:快速列出目錄內(nèi)容
- grep_search:基于正則表達(dá)式的快速文本搜索
- edit_file:編輯文件內(nèi)容
- file_search:基于模糊匹配的快速文件路徑搜索
- delete_file:刪除指定路徑的文件
- reapply:在編輯結(jié)果不符合預(yù)期時(shí),調(diào)用更智能的模型重新應(yīng)用最后一次文件編輯
- diff_history:查看近期文件修改歷史
一個(gè)例子
假設(shè)我們的需求是「用antd的Button組件替換頁(yè)面中的主按鈕」
一種可能的執(zhí)行過(guò)程是:
執(zhí)行run_terminal_cmd
,檢查antd
是否已安裝,若未安裝需后續(xù)處理,參數(shù)如下:
{
"command": "npm list antd || echo 'antd not installed'",
"require_user_approval": false,
"is_background": false,
"explanation": "檢查antd是否已安裝,若未安裝需后續(xù)處理。"
}
執(zhí)行grep_search
,查找包含主按鈕的代碼位置,確定需要替換的文件,參數(shù)如下:
{
"query": "主按鈕|main-button|primary",
"include_pattern": "*.jsx|*.tsx|*.js",
"case_sensitive": false,
"explanation": "查找包含主按鈕的代碼位置,確定需要替換的文件。"
}
根據(jù)上一步grep_search
返回的目標(biāo)文件,執(zhí)行read_file
,確認(rèn)按鈕的上下文代碼(如樣式、事件處理等),參數(shù)如下:
{
"target_file": "src/components/ExamplePage.jsx",
"start_line_one_indexed": 1,
"end_line_one_indexed_inclusive": 50,
"should_read_entire_file": false,
"explanation": "讀取包含主按鈕的文件內(nèi)容,分析按鈕的屬性和上下文。"
}
執(zhí)行edit_file
,將原生按鈕或舊組件替換為antd Button
,并保留原有邏輯(如onClick
),參數(shù)如下:
{
"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
生成的隨機(jī)性,完整的工具調(diào)用并不一定按上述步驟。
比如在“搜索主按鈕的位置”時(shí),也可能先使用list_dir
工具,列出文件目錄,再?gòu)奈募袛唷改膫€(gè)文件可能與“主按鈕”相關(guān)」。
為了讓LLM
記得「之前的執(zhí)行步驟」,以及「接下來(lái)要做什么」,這些必要的信息都會(huì)存在于模型上下文中:
由于模型上下文有限,Cursor
會(huì)在多個(gè)層面做「上下文長(zhǎng)度優(yōu)化」,比如:
- 默認(rèn)情況下,
Agent
只會(huì)執(zhí)行20輪步驟 read_file
一次最多只會(huì)讀取200行代碼,不夠的話再繼續(xù)讀200行
本文要講的edit_file
工具就是「上下文長(zhǎng)度優(yōu)化」的表率。
edit_file的實(shí)現(xiàn)原理
如果說(shuō)read_file
已經(jīng)夠占用上下文了,那么未經(jīng)優(yōu)化的情況下,edit_file
占用的上下文應(yīng)該在read_file
的兩倍左右。
畢竟,要想修改文件,你得同時(shí)知道:
- 原始文件是什么樣
- 要修改成什么樣
所以,有別于其他工具的實(shí)現(xiàn)原理就是「單次命令執(zhí)行」(比如list_dir
對(duì)應(yīng)ls
),edit_file
是一個(gè)獨(dú)立的AI workflow
。
他包含至少3個(gè)步驟,涉及至少2次模型調(diào)用:
- 讀文件
- 生成編輯方案(使用先進(jìn)的模型)
- 執(zhí)行編輯方案(使用小參數(shù)模型)
- (可選)如果編輯方案不理想,用先進(jìn)模型再執(zhí)行一次(使用
reapply
工具)
舉個(gè)例子,假設(shè)用戶需求是「在 src/utils/math.ts 文件中添加一個(gè)計(jì)算斐波那契數(shù)列的函數(shù)」
第一步,獲取文件路徑,讀取內(nèi)容。
假設(shè)內(nèi)容如下:
// 數(shù)學(xué)工具函數(shù)集合
/**
* 計(jì)算兩個(gè)數(shù)的和
*/
export function add(a: number, b: number): number {
return a + b;
}
/**
* 計(jì)算兩個(gè)數(shù)的差
*/
export function subtract(a: number, b: number): number {
return a - b;
}
第二步,首先準(zhǔn)備編輯方案:
{
"content": "上面讀取到的原始代碼",
"query": "添加一個(gè)計(jì)算斐波那契數(shù)列的函數(shù)",
"path": "src/utils/math.ts",
"is_new": false
}
將上述方案發(fā)送給智能的模型(比如Claude 3.5
及以上)。
模型返回如下內(nèi)容:
// 數(shù)學(xué)工具函數(shù)集合
// ... existing code ...
/**
* 計(jì)算斐波那契數(shù)列的第n個(gè)數(shù)
*/
export function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
注意,其中原始內(nèi)容中「未修改的部分」被注釋 // ... existing code ...
代替了。
這種方式的好處是:可以顯著減少上下文空間占用。
但也有缺點(diǎn):沒(méi)法通過(guò)上述信息直接還原編輯后的代碼,得通過(guò)模型的能力還原。
所以,第三步,將上述信息一齊給到模型,將注釋替換為原始代碼:
{
"content": "讀取到的原始代碼",
"query": "添加一個(gè)計(jì)算斐波那契數(shù)列的函數(shù)",
"path": "src/utils/math.ts",
"is_new": false,
"code_edit": "上述代碼編輯信息"
}
模型返回:
// 數(shù)學(xué)工具函數(shù)集合
/**
* 計(jì)算兩個(gè)數(shù)的和
*/
export function add(a: number, b: number): number {
return a + b;
}
/**
* 計(jì)算兩個(gè)數(shù)的差
*/
export function subtract(a: number, b: number): number {
return a - b;
}
/**
* 計(jì)算斐波那契數(shù)列的第n個(gè)數(shù)
*/
export function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
由于這一步邏輯不復(fù)雜,且對(duì)模型生成速度要求較高,所以通常交給微調(diào)過(guò)的小參數(shù)模型執(zhí)行。
如果模型的執(zhí)行效果不好(比如在一個(gè)大文件中一次性修改多處),還能使用reapply
工具使用更智能的模型再執(zhí)行一遍。
總結(jié)
Cursor
的核心邏輯是一個(gè)簡(jiǎn)單的Agent
,包含10個(gè)可調(diào)用的內(nèi)部工具。
其中,大部分工具的實(shí)現(xiàn)原理是「簡(jiǎn)單的命令執(zhí)行」。
而edit_file
工具是一條涉及3個(gè)步驟的AI Workflow
,中間涉及到注釋的替換。
之所以這么做,是為了節(jié)約模型上下文空間。