揭秘OpenAI新神器:Cursor源码深度解析与应用探索
揭秘OpenAI新神器:Cursor源码深度解析与应用探索
背景
Cursor
(
https://
github.com/getcursor/cu
rsor
)是一款专为编程与人工智能而设计的编辑器。
虽然现在还处于早期阶段,但目前Cursor可以完成以下几个任务:
-
写作:使用比
Copilot
更智能的AI生成10-100行代码; - 差异:请求AI编辑一段代码块,并只查看建议的更改;
- 聊天:采用类ChatGPT界面,了解当前的文件;
- 以及更多:请求修复lint错误,在悬停时生成测试/注释等。
Cursor背后的公司于 2023 年在旧金山成立,主要开发利用 LLM 从基层建立的 IDE。
创始团队目前2位,已获得
OpenAI
的投资:
-
Aman Sanger
,2022 年毕业于麻省理工学院数学与计算机科学专业,Abelian AI 联合创始人 -
Michael Truell
,2022 年毕业于麻省理工学院数学与计算机科学专业。
Cursor源码架构
Cursor
目前开源的部分是基于
Electron
+
CodeMirror
搭建的,源码代码质量不高,我甚至都怀疑代码都是用AI写的…拼凑感很强,另外没有一条测试用例。不过从市场角度也能理解,毕竟要快速抓住市场为主,两位MIT的高材生也没有太多前端工程经验。
整体的架构可以画一张图来表示:
在
Electron
架构之上主要构建了6个模块:
-
LSP
,语言服务,内置了对TS、Python、C++等常见的语言支持 -
Settings
,一系列设置,比如OpenAI
的key,开启的语言服务等等 -
Comment
,注释,生成注释用 -
CodeMirror
,一些基于CodeMirror的补丁 -
Chat
,核心模块,generation也在这里面,是与AI的交互部分,也是我们本文分析的重点模块 -
extensions
,扩展,比如编辑器相关的扩展,自动补全,等等,目前还没有开放插件能力
特性分析
Cursor
至今的官网和代码仓库都十分的简陋,没有详细的文档介绍。
在它的Github主页声称比
Copilot
更智能,但目前只能从作者的Twitter上的一个视频(
https://
twitter.com/amanrsanger
/status/1615539968772050946?cxt=HHwWhMDU8djCxussAAAA
)来评测它的功能,这个视频一共执行了五条指令:
- Build the `SearchResult component showing file icons, names, and paths
- Connect this component to redux
- How do I add a keyboard shortcut in electron?
- Where in the code are shortcuts and redux reducers to open file search
-
Make cmd+p with label
File Search
open file search
目前
Cursor
支持的交互方式有两种,一个是
cmd+k
唤起指令模式,这个指令会调用AI进行generate直接生成代码,另外一种是
cmd+l
唤起的聊天模式,会返回
markdown
的文本显示在一个浮层上。
在上面的指令中,1、2、5应该是指令模式,3、4是聊天模式。
接下来,我们仿照这个视频的环境(它用的是
cursor
源码并且打开了一个fileSearch文件),深入探索下这些交互背后发生了什么。
第一条指令,生成代码
基于命令生成代码应该是AI的基本操作,这里的逻辑位于源码文件的
features/chat/chatThunks.ts
中:
const thunkFactory = (
actionCreator: ActionCreatorWithoutPayload,
name: string
createAsyncThunk(
`chat/${name}`,
async (payload: null, { getState, dispatch }) => {
dispatch(actionCreator())
// If message type is chat_edit, then we want to change the message type to chat
(getState() as FullState).chatState.userMessages.at(-1)
?.msgType == 'chat_edit'
dispatch(diffResponse('chat'))
} else {
dispatch(streamResponse({}))
当我们输入回车的时候,就会触发submit的action,而这里面的actionCreator都是经过thunkFactory包裹的,在这里面会看到,最终执行了
streamResponse
。
由于
streamResponse
函数十分冗长,我们截取一下请求部分:
const server = `${API_ROOT}/conversation`
const response = await fetch(server, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getBearerTokenHeader(getState),
// Cookie: `repo_path=${state.global.rootPath}`,
//credentials: 'include',
body: JSON.stringify(data),
核心就是向
cursor
的服务器发送了一个POST请求,目前
cursor
的后台代码并未开源,所以对这块是黑盒,不过从命名可以看出这个是一个对话接口,后台的实现中肯定也包含了对AI的调用。 值得注意的是这并不是一个普通的POST请求,它返回的是一个
text/event-stream
的MIME,可能大部分同学并不熟悉,实际上它用于实现服务器向客户端推送实时数据流。它是基于HTTP长轮询和服务器发送事件(
SSE
)的一种技术,能够实现服务器与客户端之间的双向通信。
实际上
cursor
的server是将信息通过token的方式流式返回的,这样也保证了在界面中类似
chatGPT
那样的体验。
我们来看一下这个请求的入参:
编辑器传递给
server
有价值的信息包括:
- 当前的文件信息,包括文件的所有代码,文件名和路径
-
交互的类型和指令
message
- 上下文的Code(源码中可以看到,是按照20行切分的字符串,猜测是提供给模型上下文的,同时避免超过最大token的限制)
基于这些信息,生成合适的
prompt
,AI就可以生成代码了。
第二条指令,AI续写内容
第二条指令与第一条指令不同,是选中了某段文本,然后要求改写,让我们来看看这次发起的请求:
会发现和上次请求的不同之处在于
selection
和
msgType
,分别传入了选中的文本和edit,这样server应该就能更准确地生成相应的prompt。
在edit这个模式下,很容易会触发一个
continue
的请求,这是因为选中的文本,再加上AI返回的内容,很容易就会超过模型最大token的限制,所以
Cursor
这里还加了一个
continue
接口,用来接上之前不能一次性返回的内容:
可以看到
continue
接口多了一个
botMessages
字段,用来将上一次AI返回的信息再次传递过去,而接口应该也是根据这个上下文信息要求AI能够续写上之前的内容。
continue
的逻辑在源码中是有一个
interrupted
作为标识的,如果判断是
edit
模式并且因为token问题被中断了,就会触发
continue
的逻辑:
lastBotMessage = getLastBotMessage(
(getState() as FullState).chatState
lastBotMessage.type == 'edit' &&