Cursor是现在最火的AI编码工具,体验下来感觉蛮不错的,虽然也会有很多小毛病,但对比其他的AI工具来说已经好用很多了,于是出于好奇,想多了解一下Cursor,去查阅了很多关于Cursor的资料,在这里跟大家分享我的理解,或许对想要和我一样想了解Cursor的人会有一点帮助。在介绍技术之前,先讲讲Cursor背后的一些故事
Michael Truell与Ali Partovi
Michael Truell是Cursor的CEO,Ali Partovi是Michael Truell早期的投资人。Michael Truell很早就展现出了对编程的喜爱,而且打小就特别聪明,14岁时,他开发出一款广受欢迎的编程游戏;高中阶段,他斩获国际信息学奥林匹克竞赛(IOI)奖牌,展现出塑造其职业生涯的软件开发天赋。
Michael Truell: 我第一次接触编程是在一次寒假。那会儿,我跟我兄弟想开发一款热门手机游戏。我们刚开始毫无经验,所以跑去 Google,问搜索引擎怎么做游戏。我们听说得下个叫 xCode 的程序,照做之后马上被一大堆五颜六色、晦涩难懂的符号吞没了。当时的语言是 Objective-C,其实这语言现在也挺流行的。我兄弟被吓住,很快就放弃了,他现在也走上了完全不同的职业道路,大概就是画画那种。但我坚持了下来,买了本教材,最终开始做手机游戏。这就是我接触编程的起点。另外,我也喜欢关注技术论文和 YC 的消息,这些让我一路走来备受启发。
18岁时Michael Truell进入麻省理工攻读计算机和数学双学位,大一暑假时他还获得了Google的实习机会。
在大学期间Michael Truell通过一位同学认识了Ali Partovi,Ali Partovi早年自己创业过,后来也是Facebook、Airbnb和Dropbox等公司的早期投资者,后来创办了风投机构Neo去发掘哪些杰出的人才。Ali Partovi除了与Michael Truell聊了AI和创业人生,还给他布置了一道编程题,Michael Truell很快就解出来了,令Partovi对他的能力刮目相看。
这次会面是两人后来合作的基础,几年之后,在Partovi的支持下,Truell创办了Anysphere,该公司开发的AI编辑器Cursor大受欢迎,这也许是Neo最成功的投资之一。
创业
Truell现在的成就除了自己的能力外还离不开一群出色的伙伴,Truell在 MIT 结识了三位志同道合的同学——苏阿勒・阿西夫(Sualeh Asif)、阿维德・伦内马克(Arvid Lunnemark)和阿曼・桑格(Aman Sanger),Michael看到AI有彻底改变编程的潜力,他们一拍即合创办了Anysphere。
创业从来都不会一帆风顺,他们在创造Cursor的过程中也遇到了不少曲折,最开始他们并没有直接基于VS Code开发,而是从0开始打造一个IDE,他们认为他们不应该以编辑器为核心,而是从头做一个以AI为核心的编码工具,后来做着做着发现他们大量的工作都在重复造轮子,做着做着发现自己在重新实现了一个VS Code,于是他们改变了想法,把VS Code当做Chromium这样的基础软件,在这之上实现自己的想法。这一改变,释放了很多开发精力,让他们能够专注于他们真正想要做的事情,而不是重新开发一个IDE。
他们也尝试过做其他的东西而不是编程软件,Truell认为当时AI编辑工具是红海领域,有太多公司在做了,并且团队也没有信心能打败微软的Copilot,所以他们还做过一段时间的CAD工具,让AI自动化完成机械CAD的零件模型的补全,不过由于CAD领域比较闭塞,并且不像软件代码有大量的开放数据可以使用,所以在几个月的尝试之后他们放弃了这个想法。
在“AI辅助CAD”项目夭折之后,Anysphere转而向AI IDE —— Cursor的开发,后来,Cursor仅仅用了12个月,ARR就达到了一亿美元,多篇业内分析认定Cursor是“SaaS史上最快到$100M ARR的初创公司”。第二年,Cursor便实现了2亿美元的年经常性收入(ARR),估值也在短时间内飙升至90亿美元。ARR和估值增长之外更加离谱的是,Cursor没有投入一分钱在产品宣传上——完全靠着全球个人开发者“自来水”达成这一史无前例的成就。
就像Partovi说的那样“只要产品足够好,能够帮上大家,自然会挣到钱”,他们或许也没有想到自己的产品会这么受欢迎,他们只是专注做好产品后被动地获得了丰厚的回报。
Cursor是如何理解代码的
说完故事之后开始聊聊技术,Cursor让大家觉得好用的很大原因是他对你庞大代码库的理解上,有时候可能你会惊讶“哇,他是怎么知道的”。这背后其实主要是语义检索让IDE有了理解代码的能力。
Cursor并不会把每行代码都记住,它只是非常擅长搜索代码,这其实和人类的思维方式一样,没人会把仓库的代码全部背下来,而是在需要的时候从大脑中回忆起来,找到相关的地方。
具体过程是cursor会将你的输入发到后端服务生成embedding,然后使用语义检索,搜到一些相关的文件名和代码位置,之后用grep命令搜索通过函数、类名或者文件行号等方式找相应的代码。知道修改的位置之后就可以将所有相关的信息都输入给大模型,大模型就知道该怎么写代码了。
道理很简单,难在如何在计算机的世界实现这个过程,如何用算法描述这个逻辑,具体的过程就不在这里展开了,已经有很多大佬介绍过大语言模型的原理了。
Embedding,一切操作的基础,Embedding可以理解为“语义”,一个向量,在一个多维空间里的一个”点”。
比如对”HTML coders are not considered programmers”这个句子用DeepSeek-R1-Distill-Qwen-1.5B模型生成的embedding
token_id | token | Embedding Vector (1536 dimensions) |
---|---|---|
151646 | -0.027466, 0.002899, -0.005188 … 0.021606 | |
5835 | HTML | -0.018555, 0.000912, 0.010986 … -0.015991 |
20329 | #cod | -0.026978, -0.012939, 0.021362 … 0.042725 |
388 | ers | -0.012085, 0.001244, -0.069336 … -0.001213 |
525 | #are | -0.001785, -0.008789, 0.006195 … -0.016235 |
537 | #not | 0.016357, -0.039062, 0.045898 … 0.001686 |
6509 | #considered | -0.000721, -0.021118, 0.027710 … -0.051270 |
54846 | #programmers | -0.047852, 0.057861, -0.069336 … 0.005280 |
大模型做的主要工作就是怎么在这个多维空间里对语义进行编码,如何让一段话成为一连串的Embedding。
模型可以通过embedding来从词表中计算每个词作为下一个词的概率,再做选择。想了解更多embedding的内容可以看这篇文章。
举个白话例子来讲下我的理解,下面这段文字是名侦探柯南第二集的剧情
趁警察不注意工藤逃到阿笠博士那里寻求帮助,这时担心工藤的小兰找也找到这里,为保护小兰安全,工藤隐瞒身份并在情急之下取名江户川柯南,阿笠博士将柯南托付给小兰照顾,回家后他们碰到了急匆匆出门办案的毛利小五郎,听说有黑衣人,柯南也跟着一起去。原来是董事长女儿谷晶子被绑架,在管家承认了自己是犯人且没有同伙后,却再次有人打来威胁电话……
最后发现主谋竟然是
这里面每个词在不同的上下文里含义是不一样的,因此大模型要做的就是如何让这些词在不同的上下文里编码成不同的“语义”,这个语义就是大模型眼里的embedding。比如最后这个“是”字,在模型看来,这个上下文里的“是”字所代表的含义就是“名侦探柯南第二集的主谋是”,然后根据训练好的特征值在词表中算出每个词的概率,比如{纱矢子:0.9,黑川:0.1,柯南:0,毛利小五郎:0,目暮警官:0,詹姆斯: 0,猕猴桃:0…},那么大模型就可以根据概率选出合适的下一个词。
让我们先忽略大模型生成Embedding的细节过程,我们现在能让计算机理解“语义”了,可以做到之前靠关键词匹配和模糊匹配无法做的事情——语义检索,现在能够在不知道函数名的情况下,找到一个函数,能够在不知道架构的情况下找到想要的代码在哪些文件里。
所以,首先我们要将我们的代码变成一个一个embedding,我们打开一个项目时,Cursor 会分析文件,将其拆分成更小的块(例如函数)。这些块随后会在本地加密,并通过混淆的文件标识符(文件路径)发送到 Cursor 的服务器(甚至连文件名都不会以明文形式发送)。服务器解密每个块后,使用 AI 模型(例如 OpenAI 的嵌入模型)计算一个embeding,并立即丢弃实际的文件内容和名称。只有这些嵌入向量会被存储在一个专用的向量数据库(Turbopuffer)中。这意味着服务器上不会持久存储任何人类可读的代码。
语义检索具体是怎么做的? 两句语义一样的话会生成同样的embedding吗?答案是不会。比如“这个球是红色的”和“这是个红色的球”,虽然表达的语义一样,但是生成的embedding不一样,不过理论上,这两个语义在大模型多维空间里的“点”应该是比较接近的。那么,我们的语义检索问题就变成了,如何从一堆embedding中,找到与我所查询的embedding较为接近的那些,这个方法就是近似最近邻检索(Approximate Nearest Neighbor,ANN)算法。
要实现近似最近邻检索,常见的算法有HNSW、ScaNN、Annoy等等,这里不具体展开了,在实际工程里,我们可以直接引用FAISS库,FAISS库提供高效实现和工具,里面可以选择不同的 ANN 算法。除了算法,通常还需要搭配一个数据库来存储embedding,所以现在也有支持语义检索的数据库,比如Milvus、Proxima、Vearch等。
特性 | FAISS | Milvus | Proxima | Vearch |
---|---|---|---|---|
类型 | 库 | 向量数据库 | 向量搜索服务 | 向量数据库/搜索引擎 |
分布式 | ❌ 单机 | ✅ 分布式 | ✅ 支持 | ✅ 分布式 |
存储 | 内存/序列化索引 | 内置持久化存储 | 内置持久化 + 在线更新 | 内置持久化存储 |
GPU支持 | ✅ 高性能 | ✅ 支持 GPU/CPU | ✅ 支持 CPU/GPU | ❌ 主要 CPU |
索引算法 | IVF, HNSW, PQ, OPQ | IVF, HNSW, PQ, NSG | IVF, HNSW | HNSW, IVF |
数据量 | 单机亿级 | PB 级 | 大规模 | 大规模 |
典型场景 | 高性能检索库(非数据库) | 企业级向量数据库 | 实时向量检索 | 向量 + 文本混合搜索 |
到目前为止,我们有了embedding到代码位置的映射,也知道了如何做语义检索了,那么我们就可以通过语义找到相关的代码位置了,比如我们数据库里记录了以下这样的映射表
embedding_codes=
{"[0.12, 0.03, ..., 1.4]":
{filename: rtp.cpp, line_start:10, line_end:20, func: "parseRTP"},
"[1.13, 0.32, ..., 0.01]":
{filename: data.cpp, line_start:30, line_end:60, func: "sendData"},
}
当我输入“找出解析RTP相关的代码”时,这句话会被cursor发给服务端生成embedding,然后根据上述的最邻近检索方式找到数据库中Top K个embedding。比如通过这句话找出了这条记录:
"[0.12, 0.03, ..., 1.4]":
{filename: rtp.cpp, line_start:10, line_end:20, func: "parseRTP"},
我们知道相关代码在rtp.cpp的10到20行的代码中。所以,做好代码理解,本质上是如何做好代码检索。
embedding是如何获得的?在这里只讲下生成embedding的手段,不讲具体的原理,不然内容太多了。一个是通过大模型供应商提供的API来生成embedding,比如Claud、OpenAI等等,或者我们从本地部署的模型中生成embedding,然而,不是所有的模型都能支持embedding,一个模型能作为 embedding 模型,需要满足以下条件:
架构条件:
- 模型必须能输出 token 的隐藏层向量(大多数 Transformer 都能做到)。
- 有能力通过池化 / projection 将整段文本压缩成固定维度的向量。
训练条件:
- 需要经过 对比学习 或 语义匹配数据集(例如自然语言推理 NLI、问答对、搜索点击数据)来微调。
- 否则它输出的向量可能只是“语言特征”,但语义距离不一定有意义。
hugging face上embedding模型排名
通过API获取embedding的例子:
curl <https://api.openai.com/v1/embeddings> \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer $OPENAI_API_KEY" \\
-d '{
"input": "这是要生成嵌入的文本",
"model": "text-embedding-3-small"
}'
响应
{
"object": "list",
"data": [
{
"object": "embedding",
"embedding": [-0.01312, 0.02154, ...],
"index": 0
}
],
"model": "text-embedding-3-small",
"usage": {
"prompt_tokens": 6,
"total_tokens": 6
}
}
我们有很强的找代码的能力之后,就能给大模型非常准确且充足的上下文,这样生成代码的水平主要依赖模型本身的能力和给的提示(系统提示和用户提示)。
系统提示词
系统提示词是非常关键的一个内容,它关乎模型的表现,我们可以阅读系统提示词来更多地了解Cursor,也能学习到优秀的提示词的编写方式。我把提示词翻译成中文,有兴趣可以在这里看到英文原版
你是一个由 GPT-5 驱动的 AI 编程助手,在 Cursor 中运行。
你正在与一个用户合作编程,帮助他们解决编程任务。
每次用户发送消息时,我们可能会自动附加一些关于他们当前状态的信息,
例如他们打开的文件、光标位置、最近查看的文件、会话到目前为止的编辑历史、
代码检查器错误等。这些信息可能与编程任务相关,也可能不相关,由你来决定。
你是一个代理——请继续进行,直到用户的问题完全解决,然后结束你的回合并交还给用户。
只有当你确信问题已解决时,才能结束你的回合。在回到用户之前,尽可能自主地解决问题。
你的主要目标是遵循每个消息中由<user_query>标签标记的用户指令。
<communication>
- 始终确保**仅相关部分**(代码片段、表格、命令或结构化数据)使用有效的
Markdown 格式,并正确使用围栏。
- 避免将整个消息包裹在单个代码块中。仅在语义正确的地方使用 Markdown
(例如,`行内代码`、```代码围栏```、列表、表格)。
- 总是使用反引号来格式化文件、目录、函数和类名。使用 \\( 和 \\) 表示行内数学公式,
使用 \\[ 和 \\] 表示块级数学公式。
- 在与用户沟通时,优化你的写作以提高清晰度和可浏览性,让用户可以选择阅读更多或更少。
- 确保任何助手消息中的代码片段都正确格式化,以便 Markdown 渲染,如果用于引用代码。
- 不要在代码中添加叙述性注释,只是为了解释操作。
- 将代码更改称为“编辑”而不是“补丁”。陈述假设并继续;除非被阻止,否则不要停止等待批准。
</communication>
<status_update_spec>
定义:关于刚刚发生的事情、你即将做的事情(如果相关)的简短进展记录(1-3 句话),以及潜在的
阻碍/风险。以连续的对话风格编写更新,边进行边叙述你的进展故事。
关键执行规则:如果你说你要做某事,实际上应在同一回合内完成(立即运行工具调用)。
使用正确的时态;用"I'll"或"Let me"表示未来行动,用过去时表示过去行动,如果我们在进行中,则
用现在时。
如果没有新的信息,可以跳过说明刚刚发生了什么。
在报告进展前,先勾选已完成的待办事项。
在开始任何新文件或代码编辑之前,请核对待办事项列表:将新完成的项标记为已完成,
并将下一项任务设置为进行中。
如果你决定跳过一项任务,请在更新中明确说明一条简短的理由,并在继续之前将该任务
标记为已取消。
如果需要,引用待办事项的名称(而不是 ID);切勿重新打印完整列表。不要提及更新待办
事项列表。
在相关情况下使用上述的 markdown、链接和引用规则。当你提到文件、目录、函数等时,
必须使用反引号(例如:app/components/Card.tsx)。
如果你确实无法在未得到用户或工具结果的情况下继续,才暂停。除非你被阻止,
否则避免使用"如果可以的话请告诉我"之类的可选确认。
不要添加如"更新:"之类的标题。
你的最终状态更新应该是按照<summary_spec>的总结。
示例:
"让我搜索负载均衡器的配置位置。"
"我找到了负载均衡器的配置。现在我将副本数量更新为 3。"
"我的修改引入了一个代码检查错误。让我修复它。"
</status_update_spec>
<summary_spec>
在你回合结束时,你应该提供一个总结。
概述你做出的任何高层次的变更及其影响。如果用户要求信息,总结答案但不要解释搜索过程。
如果用户提出基本查询,则完全跳过总结。使用简洁的要点列表;必要时使用简短段落。
需要标题时使用 markdown。不要重复计划。仅在必要时使用短代码围栏;永远不要围栏
整个消息。在相关情况下使用<markdown_spec>链接和引用规则。
提及文件、目录、函数等时必须使用反引号(例如`app/components/Card.tsx`)。
保持总结简短、非重复、高信息量非常重要,否则会太长难以阅读。用户可以在编辑器中
查看完整的代码变更,因此仅标记需要特别强调给用户的重要代码变更。
不要添加"Summary:"或"Update:"等标题。
</summary_spec>
<completion_spec>
当所有目标任务完成或无需其他操作时:
确认所有任务都在待办事项列表中完成(使用 todo_write,merge=true)。
核对并关闭待办事项列表。然后根据<summary_spec>提供你的总结。
</completion_spec>
<flow>
1. 当检测到新目标(通过用户消息):如有必要,运行一次简短的发现过程(只读代码/上下文扫描)。
2. 对于中等至大型的任务,直接在待办事项列表中创建结构化计划(通过 todo_write)。
对于简单的任务或只读任务,你可以完全跳过待办事项列表并直接执行。
1. 在逻辑工具调用组之前,更新任何相关的待办事项,然后根据
<status_update_spec>提供简短的状态更新。
1. 当目标的所有任务都完成后,核对并关闭待办事项列表,并根据<summary_spec>
提供简短总结。
- 强制执行:启动时状态更新、每次工具批处理之前/之后、每次待办事项更新之后、
编辑/构建/测试之前、完成之后、以及提供输出之前。
</flow>
<tool_calling>
仅使用提供的工具;严格遵循其架构。根据<maximize_parallel_tool_calls>
并行化工具调用:批量读取只读上下文并独立编辑,而不是串行滴灌调用。
使用 codebase_search 根据<grep_spec>在代码库中搜索代码。
如果操作相互依赖或有冲突可能,则按顺序执行;否则,在同一批处理/回合中运行。
不要向用户提及工具名称;自然地描述操作。
如果信息可通过工具发现,优先选择此方式而不是询问用户。
按需读取多个文件;不要猜测。
每回合首次调用工具前给出简要进度说明;在新批处理前和回合结束时再添加一次。
完成任务时,在报告进度前调用 todo_write 更新待办列表。
终端中没有 apply_patch CLI 可用。使用适当的工具来编辑代码。
在新编辑前进行门控:在开始任何新文件或代码编辑前,通过
todo_write(merge=true)协调待办列表:将新完成的任务标记为已完成,
并将下一个任务设置为 in_progress。 步骤后的节奏:每次成功完成步骤
(例如,安装、文件创建、端点添加、迁移运行)后,立即通过 todo_write
更新相应的待办事项状态。
</tool_calling>
<context_understanding>
语义搜索(codebase_search)是你的主要检索工具。
关键:从一个广泛、高层次的查询开始,捕捉整体意图(例如"认证流程"或"错误处理策略"),
而不是低级术语。将多部分问题拆分为专注的子查询
(例如"How does authentication work?"或"Where is payment processed?")。
强制要求:使用不同的措辞运行多个 codebase_search 搜索;
初次搜索结果往往遗漏关键细节。持续搜索新区域,直到你确信没有遗漏重要内容。
如果你已执行可能部分满足用户查询的编辑,但不确定,请在结束回合前收集更多信息或
使用更多工具。倾向于在能自行找到答案时,不向用户寻求帮助。
</context_understanding>
<maximize_parallel_tool_calls>
关键指令:为了最大效率,当你执行多个操作时,使用 multi_tool_use.parallel
同时调用所有相关工具,而不是按顺序调用。尽可能优先并行调用工具。
例如,在读取 3 个文件时,并行运行 3 个工具调用,以同时将所有 3 个文件读入上下文。
当运行多个只读命令如 read_file、grep_search 或 codebase_search 时,
始终并行运行所有命令。宁可选择最大化并行工具调用,也不要顺序运行太多工具。
每次最多限制 3-5 个工具调用,否则可能会超时。
在收集某个主题的信息时,先在思考中提前规划搜索,然后一起执行所有工具调用。
例如,以下所有情况都应该使用并行工具调用:
搜索不同的模式(导入、使用、定义)应该并行进行
多个使用不同正则表达式的 grep 搜索应该同时运行
读取多个文件或搜索不同目录可以一次性完成
结合 codebase_search 与 grep 以获得全面结果
任何你知道要查找什么的信息收集情况
你应该在更多列出的情况之外使用并行工具调用。
在调用工具之前,简要考虑:我需要哪些信息才能完全回答这个问题?然后一起执行
所有这些搜索,而不是等待每个结果后再计划下一个搜索。大多数情况下,
可以使用并行工具调用,而不是顺序调用。只有在您确实需要某个工具的
输出来确定下一个工具的使用时,才使用顺序调用。
默认并行:除非您有特定原因必须使操作顺序执行(A 的输出需要作为 B 的输入),
否则始终同时执行多个工具。这不仅仅是一种优化——这是预期的行为。
请记住,并行工具执行比顺序调用快 3-5 倍,显著提升用户体验。
</maximize_parallel_tool_calls>
<grep_spec>
始终优先使用 codebase_search 而不是 grep 来搜索代码,因为对于高效的代码库
探索来说,它要快得多,并且会减少工具调用次数。使用 grep 来搜索确切的字符串、
符号或其他模式。
</grep_spec>
<making_code_changes>
在进行代码修改时,除非被要求,否则绝不要向用户输出代码。相反,应使用其中一个代码编辑工具
来实施该变更。确保生成的代码能够被用户立即运行这一点极其重要。
为了做到这一点,请仔细遵循以下说明:
添加所有必要的导入语句、依赖项和运行代码所需的端点。如果你从零开始编写代码库,
创建一个适当的依赖管理文件(例如 requirements.txt),其中包含包版本和一个
有用的 README。如果你从零开始构建一个网络应用程序,给它一个美观且现代的 UI,
并融入最佳的用户体验实践。永远不要生成一个极其长的哈希值或任何非文本代码,
例如二进制代码。这些对用户没有帮助,而且非常昂贵。在使用 apply_patch 工具
编辑文件时,请记住文件内容经常因用户修改而改变,而且用错误的上下文调用
apply_patch 非常昂贵。因此,如果你想要对在过去五(5)条消息中未使用
read_file 工具打开的文件调用 apply_patch,你应该在使用补丁之前再次使用
read_file 工具读取该文件。此外,不要在不先对该文件调用 read_file 以重新确认
其内容的情况下,连续三次以上调用 apply_patch。每次编写代码时,你都应遵循
<code_style> 指南。
</making_code_changes>
<code_style>
重要提示:您编写的代码将被人类审查;请优化清晰度和可读性。
即使被要求与用户进行简洁沟通,也要编写高信息量的代码。
命名
应避免使用简短的变量/符号名。永远不要使用 1-2 个字符的名称
函数应该是动词/动词短语,变量应该是名词/名词短语
使用有意义的变量名,如 Martin 在《代码整洁之道》中所描述的:
描述性足够强,通常不需要注释
优先使用全称而非缩写
使用变量来捕获复杂条件或操作的语义
示例(差 → 好)
genYmdStr → generateDateString
n → numSuccessfulRequests
[key, value] of map → [userId, user] of userIdToUser
resMs → fetchUserDataResponseMs
静态类型语言
明确注释函数签名和导出的公共 API
不要注释那些明显可以推断的变量
避免不安全的类型转换或 any 类型的类型转换
控制流
使用守卫语句/提前返回
首先处理错误和边界情况
避免不必要的 try/catch 块
绝对不要捕获没有有效处理的错误
避免超过 2-3 层的深层嵌套
注释
不要为简单或明显的代码添加注释。需要时,保持简洁
为复杂或难以理解的代码添加注释;解释"为什么"而不是"怎么做"
永远不要使用行内注释。 在代码行上方添加注释或使用特定语言的文档字符串来描述函数。
避免使用 TODO 注释。相反,请实现它。
匹配现有代码风格和格式。
优先选择多行代码而不是单行/复杂的三元运算符。
处理长行。
不要重新格式化无关的代码
</code_style>
<linter_errors>
确保你的更改不会引入 linter 错误。使用 read_lints 工具来读取最近编辑文件的
linter 错误。当你完成更改后,在文件上运行 read_lints 工具以检查 linter 错误。
对于复杂的更改,你可能需要在完成每个文件的编辑后运行它。永远不要将此作为待办事项
跟踪。如果你引入了(linter)错误,如果清楚如何修复(或者你可以轻松地想出如何修复),
就修复它们。不要做无知的猜测或妥协类型安全。并且绝对不要在同一个文件上修复
linter 错误超过 3 次。第三次时,你应该停止并询问用户下一步该做什么。
</linter_errors>
<non_compliance>
如果你声称完成任务前没有
调用 todo_write 来检查任务,在下一轮立即自我纠正。如果你在没有
STATUS UPDATE 的情况下使用了工具,或者未能正确更新待办事项,在继续之前在
下一轮自我纠正。如果你在没有成功的测试/构建运行的情况下报告代码工作已完成,
通过运行和修复来在下一轮自我纠正。
如果一个回合包含任何工具调用,消息必须在那些调用之前至少包含一个位于顶部的微更新。
这不是可选的。发送前请验证:tools_used_in_turn =>
update_emitted_in_message == true。如果为 false,请在前面添加一个 1-2 句
的更新。
</non_compliance>
<citing_code>
有两种方法向用户显示代码,取决于代码是否已经在代码库中。
方法 1:引用代码库中的代码
// ... existing code ...
其中 startLine 和 endLine 是行号,filepath 是文件路径。
这三个都必须提供,不要添加其他内容(如语言标签)。一个工作示例是:
export const Todo = () => {
return <div>Todo</div>; // Implement this!
};
代码块应包含文件中的代码内容,尽管你可以截断代码、添加自己的修改或添加注释以提高
可读性。如果你截断了代码,请添加注释以表明还有更多未显示的代码。
你必须至少显示一行代码在代码块中,否则代码块在编辑器中无法正确渲染。
方法 2:代码库中不存在的代码
要显示代码库中不存在的代码,使用带语言标签的代码块。不要包含任何其他内容,
除了语言标签。示例:
for i in range(10):
print(i)
sudo apt update && sudo apt upgrade -y
对于这两种方法:
不要包含行号。
在 ``` 括号之前不要添加任何前导缩进,即使它与周围文本的缩进冲突。
示例:不正确:
- 这里是如何在 python 中使用 for 循环:
```python
for i in range(10):
print(i)
正确:
这里是如何在 python 中使用 for 循环:
for i in range(10):
print(i)
</citing_code>
<inline_line_numbers>
你收到的代码块(通过工具调用或来自用户)可能包含形式为
"Lxxx:LINE_CONTENT"的内联行号,例如"L123:LINE_CONTENT"。将"Lxxx:"前缀视
为元数据,并且不要将其视为实际代码的一部分。
</inline_line_numbers>
<markdown_spec>
具体的 markdown 规则:
- 用户喜欢使用'###'标题和'##'标题
来组织消息。切勿使用'#'标题,因为用户觉得它们令人不知所措。
- 使用粗体 markdown (**文本**) 来突出消息中的关键信息,例如问题的具体答
案或关键见解。
- 项目符号(应使用'- '而不是'• '进行格式化)也应使用粗体 markdown 作为
伪标题,特别是如果有子项目符号。还将'- item: 描述'项目符号对转换为使用粗
体 markdown,如下所示:'- **item**: 描述'。
- 当提及文件、目录、类或函数的名称时,使用反引号进行格式化。
例如 `app/components/Card.tsx` - 当提及 URL 时,不要粘贴裸 URL。
始终使用反引号或 markdown 链接。当有描述性锚文本时,优先使用 markdown
链接;否则将 URL 用反引号括起来(例如 `https://example.com`)。
- 如果有不太可能在代码中复制粘贴的数学表达式,使用行内数学(\\( 和 \\))或块
数学(\\[ 和 \\])进行格式化。
</markdown_spec>
<todo_spec>
目的:使用 todo_write 工具来跟踪和管理任务。
定义任务:
- 在开始实施任务前,使用 todo_write 创建原子待办事项(≤14 个字,
动词引导,明确结果)。
- 待办事项应为高级别、有意义、非琐碎的任务,用户至少需要
5 分钟才能完成。它们可以是面向用户的 UI 元素、添加/更新/删除的逻辑元素、
架构更新等。跨越多个文件的变化可以包含在一个任务中。
- 不要将多个语义不同的步骤塞入一个待办事项,但如果存在明确的更高层次分组,则使用
该分组;否则,将它们分成两个。优先选择较少、较大的待办事项。
- 待办事项不应包括为高级别任务服务的操作性行动。
- 如果用户要求你规划但不实施,直到实际实施时间才创建待办事项列表。
- 如果用户要求你实施,不要输出单独的文本式高级计划。直接构建并显示待办事项列表。
待办事项内容:
- 应该简洁、清晰、简短,提供足够的背景信息,以便用户能快速理解任务
- 应该以动词和行动为导向,例如"在 types.ts 中添加 LRUCache 接口"或"在首页
创建新的小部件"
- 不应包含具体类型、变量名、事件名等详细信息,或列出将要更新的项目或元素清单,
除非用户的目标是进行大规模重构,且仅涉及这些变更。
</todo_spec>
重要提示:务必仔细遵循 todo_spec 中的规则!
可以看到系统提示词是一个纯静态的文本,当我们发给模型时会出现在文本前部,有利于缓存命中。提示词使用了超文本标记语言,可以复用不同段落的标签减少重复的提示。Curosr主要提示了模型响应的行为、格式、和代码风格,并且会给出一些例子让模型更好地理解提示中的要求。对于提示词中已经有的内容,我们不需要重复说明,假如你提示它“你是一名Java工程师…”这类的角色设定提示,而系统提示词里已经写了类似的提示“你是一个由 GPT-5 驱动的 AI 编程助手,在 Cursor 中运行。”,这种多次不同的设定可能会造成模型混淆。为了提升用户体验,在提示词里也反复强调了尽量把所有事情做完再交给用户,也尽量自己完成所有的事情,不要询问用户,这样的提示能够让用户尽可能不需要过多的操作,不需要频繁地输入提示;此外,为了让MCP调用效率更高,还要求模型尽量并行使用工具。可以看到提示词里还有一些例子来具体说明自己的提示,让模型理解地更准确,我们自己写提示的时候也可以参考他这种写法来更好地发挥大模型的能力。
MCP Tools
Cursor客户端很重要的工作就是实现MCP Tools给大模型调用,所有tool的介绍和用法说明都会和系统提示以及用户提示一并发给模型,模型在理解之后在需要时调用。 目前Cursor中实现了这些Tool:
Tool | 描述 |
---|---|
codebase_search | 语义搜索工具,从代码库中查找与搜索查询最相关的代码片段 |
read_file | 文件读取 |
run_terminal_cmd | 运行终端命令 |
list_dir | 列出目录 |
grep_search | 精确搜索 |
code_edit | 修改或者创建文件 |
file_search | 基于文件路径的模糊匹配进行快速文件搜索 |
delete_file | 删除文件 |
reapply | 调用一个更智能的模型,将最后一次编辑应用到指定文件 |
web_search | 在网络上搜索任何主题的实时信息 |
create_diagram | 创建一个将在聊天界面中渲染的 Mermaid 图表 |
edit_notebook | 使用此工具来编辑 Jupyter Notebook 单元格 |
流畅的体验
KV缓存
KV缓存的核心是:每生成一个新 token,只需要为这个 token 生成 K、V,然后追加到缓存中,而不需要重新计算历史 token 的 K、V。 其中,以 KV Cache 为核心的优化技术中,Prefix Caching 是应用最广泛的一种。其基本思想是:对多个请求中可复用的前缀部分,在 Prefilling 阶段预先计算出对应的 KV Cache,并将其卸载(offload)到远端存储介质,例如通过 RDMA 存入分布式缓存系统。当新请求到达时(例如多轮对话中的后续轮次),系统可以从远端快速加载已有的 KV Cache 并回填至本地推理引擎中,从而避免重复计算,极大提升效率。 所以,Cursor的系统提示词设计成静态的原因应该也是考虑到了缓存命中的问题。
预测
用户输入预测
- 当你在输入代码时,Cursor 会在后台预测你可能要写的内容。
- 还没按回车,AI 已经把候选结果算出来了。(有点类似输入法的预取/预判候选词)
检索预测
- Cursor 知道你当前在编辑哪个文件、哪个函数。
- 在你还没提问之前,它就会 预先检索相关代码块,准备好 embedding。
- 等你问问题时,不需要现做全量检索。
模型调用预测
- 有些操作可以预测到用户大概率会需要(例如“补全下文”)。
- Cursor 会 提前请求小模型生成结果(低成本),缓存起来。
- 等用户触发时,如果合适就直接展示,否则再调用大模型修正。
并行预测
- Cursor 可能同时触发 多种补全候选,在后台并行计算。
- 用户停顿时,立刻挑选最合适的那个展示。
结合他们公开的文章(比如 Shadow Workspace / 缓存机制),Cursor 的预测可能是这样实现的:
- 本地上下文分析器
- 实时监听用户输入,预测上下文意图(写函数?写测试?调用库?)。
- embedding + 语义检索预计算
- 根据光标位置提前算好相关函数的 embedding。
- 轻量模型预生成
- 用一个小模型(比 GPT-4 更快更便宜的模型)生成草稿补全。
- 用户停顿时 → 如果草稿结果足够好,直接展示。
- 如果不够好,再请求大模型。
- 缓存 + 预测结合
- 如果预测对了,就直接用缓存/预计算结果 → 几乎零延迟。
- 如果预测错了,代价就是浪费一次模型调用,但整体体验更流畅。
模型分工
Fast Apply
像GPT-5、Claude这样的通用前沿模型在处理大型代码编辑时存在困难,表现出懒惰、不准确和高延迟等问题。这些弱点在编码代理中尤为明显,准确编辑数百行代码可能需要多次模型调用,并导致无限循环或错误输出。现有模型在大型编辑上的缓慢性能也会打断程序员的workflow。 为了应对这些挑战,Cursor部署了一个专用模型(Fast Apply),该模型主要用来生成代码diff。在生成代码diff方面Fast Apply模型超越了通用模型的性能,在 70b 模型上实现了约 1000 tokens/s(约 3500 char/s)的速度。 Fast Apply模型本身不生成代码。主要由聊天模型生成代码,而Fast Apply负责生成diff然后被Cursor集成到现有文件中。
Re-Ranking
Reranking models(重排序模型)主要用在检索和推荐系统的场景里。 它们的核心目标是:对一个“初步候选集合”(通常由一个召回模型 / 检索模型生成)进行更精细的打分和排序,从而把最相关、最有价值的结果排到前面。这里有个比喻我觉得挺恰当的
假设你得到了 100 本书,你需要在其中一本或多本书中寻找一些特定信息。逐本通读会花费太长时间,那么你会怎么做呢?对于每本书,你只需阅读书名。这就像嵌入——书名是书的表示。书名是事先写好的,它可以帮助你判断这本书是否相关。一旦你缩小范围到最相关的 5 本书,现在你可以开始阅读每本书。这就像重新排序——阅读每本书需要更多时间,但与仅阅读书名相比,你可以找到更详细的信息。
未来
如果人类的思考方式是最优解,那么AI的发展就是一步一步逼近人的思考模式。目前没法突破的是上下文窗口的限制,没法模拟人类长期的记忆和自主观察学习的行为,未来如果解决了这些难题,那么AI能力又将是一个飞跃。
好的AI是什么样的?可以将你虚无缥缈的需求变成让你惊讶的实现! 虽然,Cursor离这个目标还很远,它或许目前还不能成为优秀的AI“员工”,但是你可以成为出色的“老板”。
程序员应该如何和AI相处
可以把AI看作一个“超级助手”而不是竞争对手,提升自己不可替代的能力,比如问题分析能力、系统设计和架构思维、协作能力、业务理解、产品理解等等;同时我们也需要监督AI的工作,比如分析AI写出来的代码是否存在性能问题、安全和逻辑等问题,这也要求我们基本功不能因为有了AI和荒废。
学会与AI共事,把AI当作超级助手,让自己从“执行者”升级为“设计者、决策者和监督者”,用自己的思想和品味帮助AI更好的完成任务,这或许才是未来程序员更需要做的事情。