Cursor:如何构建 AI Coding 最佳实践?

0 评论 336 浏览 1 收藏 93 分钟

在AI编程的新时代,Cursor正以其创新的IDE平台,重新定义代码与开发者的互动方式。随着LLM技术的进步,Cursor不仅在底层模型上进行了革命性的提升,更在UI/UX设计上不断突破,为程序员提供了一个更为智能、高效的编程环境。本文深入探讨了Cursor如何通过AI的力量,提升编程体验,以及它对未来编程模式的深刻洞见。

AI coding 是模型推理能力增加之后的下一个竞争高地,除了模型厂商、AI Labs 之外,这个领域的参与者也有着 Cursor 这样的初创团队。作为一个 LLM-first IDE,Cursor 在今年迅速出圈,一方面在于底层模型 Claude Sonnet 3.5 模型 coding 能力提升带来的体验升级,另一方面也在于团队在 AI Coding UI/UX 上的持续投入。

本篇内容是 Lex 对 Cursor 团队访谈的详细整理,团队创始成员 Aman Sanger (CEO)、Arvid Lunnemark(CTO)、Sualeh Asif(COO)和 Michael Truell(设计主管)详细分享了 Cursor 产品体验、infra、模型训练、数据安全等细节以及他们对于 AI coding、AI Agent 的思考,通过这些分享也能了解 Cursor UI/UX 背后的理念。

• o1 不会干掉 Cursor,AI Coding 领域才刚刚开始;

• 围绕代码预测、补齐等各类任务 Cursor 还训练了一系列专门的小模型;

• Cursor 正在试验一个叫做 Shadow Space 的产品概念,通过在后台运行一个隐藏窗口让 AI 在不影响到开发者的操作的情况下进行 coding 任务;

• 团队在 code base  indexing 上投入了大量精力,这个 indexing 系统会成为接下来其他代码任务可以展开的基础;

• 未来编程会是自然语言和代码将共存,根据具体任务选择最有效的交互;

• AI 正在重塑编程体验,提高效率的同时保持程序员的创造力和控制力;

• Cursor 认为 Claude 3.5 Sonnet 的综合实力更强,Sonnet 最强的地方在于能够很好地理解开发者表述并不清晰的目标,预测程序员接下来的操作、给出适当建议;

• 即便是 SOTA 模型也不擅长找 bug,这会是 Cursor 的机会;

• 当前的代码任务基准测试并不能准确反映模型的真实能力,因为现实中的代码任务更加复杂多样,并且充满了模糊性和上下文依赖。Cursor 团队更倾向于通过真实用户的使用反馈来评估模型的性能;

• 目前还没有人能很好地解决 models routing 问题,底座模型和自有模型之间可以初步实现模型切换,但如果是 GPT-4o、Claude sonnet 和 o1 之间的切换可能要更加复杂。

         💡 目录 💡        

   01 Cursor 的诞生

   02 Cursor 的产品

   03 Cursor 是如何提高产品响应速度的

   04 Priompt :以网页为灵感的提示词工程  

   05 Shadow Workspace:

        Coding Agent  的工作流设计

   06 SOTA 模型也不擅长找 bug

   07 Code Indexing 和 Context

   08 Scaling law 和复现 o1 模型

一、Cursor 的诞生

Github Copilot 是第一个 LLM-driven 的消费级应用

Lex:代码编辑器这个领域整体是什么样的?

Michael:代码编辑器(code editor)主要用来开发软件。对外行来说,可以把代码编辑器想象成是一个专门给程序员设计的高级版的文字处理器。之所以说它是高级版,是因为代码有不同结构,而代码编辑器能做的事情比普通的文字处理器要多得多。

比如,代码编辑器能让我们直观地区分代码中不同的标记、快速浏览代码、在代码库里快速浏览内容,就像在网上点击超链接一样,快速跳转到需要的定义;甚至还能检查错误,帮助发现一些简单的 bug。代码编辑器主要的功能一般就是这些。但我认为,在接下来的 10 年里,由于软件开发方式发生变化,代码编辑器的概念也会发生很大的改变。

Lex:Cursor 是新一代代码编辑器,flok 了VS Code 的一个分支。你们自己使用代码编辑器的经历是什么样的?

Aman:一开始我们都是用 Vim 做代码编辑。当时还没有 Neovim,只有 Vim 和一个终端。2021 年 Copilot 发布的时候,我很想体验一下 Copilot,但由于 Copilot 只能在 VS Code 上使用,所以我就开始用 VS Code 了。Copilot 和 VS Code 这个组合的使用体验特别好,所以即便我很喜欢 Vim,我还是转向了 VS Code。在我们开发 Cursor 之前,VS Code 都是我的默认编辑器。

Arvid:我觉得 GitHub Copilot 有一个被低估的方面是,即使它的代码补齐出现了错误,但最多只是让人觉得有点麻烦,我们再多打一个字符,它可能就懂我们的需求了,所以也不会太差。

Sualeh:其实就是通过“犯错-纠正”的过程来迭代。Copilot 还有一个被低估的地方,它是第一个真正的 AI 产品,也是第一个 LLM-driven 的消费级产品。

Lex: Cursor 是怎么诞生的?

Michael:大概 2020 年的时候,OpenAI 提出了 Scaling Law,从那时候开始 LLM 领域的进步就变得清晰可预测,只要算力和数据够多,模型表现就能更好,也是在那时候,开始有很多人讨论“这项技术的进步能给我们带来什么”,“它要怎么去改善各种不同的知识工作领域”等等。

有几个时刻让我感受到 scaling law 的预测变得越来越具体。第一个是 Copilot  beta 版本推出,第二时刻是 GPT-4 发布的时候。

GPT-4 的提升让我们看到 scaling laws 对于“算力-模型能力”的预测已经可以落到具体的地方了,有可能很快就能开发出来更多有价值的东西。而且,如果我们继续往下做,不仅能点对点地解决问题,甚至整个编程领域都可以用到这些模型,只不过需要我们搭载一个不一样的编程环境,提供一种不同的编程方式。这就是为什么我们开始围绕这个更大的 vision 去进行开发。

Sualeh:2022 年 6 月左右,Shengtong 和 Aman 打过一个赌,预测到 2024 年 6 月或 7 月,人们能不能靠模型能力拿到 IMO 金牌。虽然很确定模型能取得进步,但我当时的想法是模型赢得奥数金牌完全不可能,但 Aman 认为可以。

Aman:不过我还要补充一点,能取得多大进步,也取决于我们在哪个领域。数学就是一个很好的领域,尤其是形式化定理证明(formal theorem proving),因为我们能通过信号来验证对不对,所以 RL 就能很好地起作用。不过,即便有的模型可以在数学能力上超越人类,但是我们还是没有达到 AGI。

Cursor 是什么?

Lex:VS Code 可以说在某种程度上统一了开发者社区,既然已经可以在  VS Code 里安装很多 Copilot 这样的 AI extension,你们为什么还要重新做一个  VS Code?

Michael:开发一个编辑器的想法其实是自然而然形成的。因为在开发 Cursor 的时候,我们想的是,模型肯定还会变得更好,能力还会提高,不但能显著提高生产力,也会从根本上改变现在我们开发软件的方式。如果只是在现有的编辑器里加一个插件,对编辑器的控制提升是很有限,我们不像被这些限制束缚,希望能够构建最有用的东西。

Lex:VS Code 和 Copilot 的组合可以说是 Cursor 最直接的竞争对手,Cursor 要如何在竞争中获胜?

Aman:如果我们回看之前的几次科技浪潮,会发现它们的普遍特点是,有某一件大事发生,然后围绕这个大事件一波新的公司开始出现。但现在不同的是,每年只要有一个模型的能力取得了飞跃,都会出来一波新的功能,有一波新的可能性。尤其是在代码领域,甚至都不需要一年,只要领先几个月,就可以让我们的产品更有用。一年后我们再回过头来看今天的 Cursor,会发现它也已经过时了。Microsoft 确实做了很多非常出色的事情,但是从编程的角度上来说,我觉得他们不像别的初创公司一样在一直创新。

Sualeh:我其实不会从功能的角度来考虑,我会从程序员的能力角度来看这个问题。o1 模型刚出来的时候,我就觉得肯定还会有很多不同的模型,比如更长的上下文、或者算力更快的模型。所有这些听起来很疯狂的想法都值得尝试,最后大概能有 1/10 的想法可以真正做成比较有用的东西,我们希望它们推出的速度能更快。

还有一个被低估的事实是,我们其实是在给自己制作这个工具。我们之所以有 Cursor 这个想法也是因为我们能明确感受到模型能力的提升,但 Copilot 给我们带来的体验并没有变得更好,也很疑惑为什么。当然,Copilot 卖得很好,对于 Microsoft 来说也是一个很成功的业务,但相当长时间 Copilot 都没有新东西出来,我个人来说会很想试试新的东西。

Arvid:我们是一整个团队在开发 Cursor,不管是开发用户体验,还是探索模型的交互方式,我们都在同步去开发怎么才能让模型给出更好的答案,比如怎么构建 prompt,怎么找到上下文,怎么用 Cursor Tab 训模型等等。

二、Cursor 的产品

Cursor Tab

Lex:Cursor 的交互中, Tab 键是一个很独特的存在,它可以看作是增强版的自动补全,几乎什么都会。可以展开讲讲 Tab 键的逻辑的吗?

Sualeh:我们很想要实现的一件事就是让模型帮我们做代码编辑。为此我们尝试了很多种办法,当我们有了能力很强的模型之后,为了让使用体验更加流畅,我们花了很大功夫提高了推理速度,优化用户体验。

Tab 键存在的目标是,如果用户接受了某一处编辑建议,模型应该很自然地就知道下一个可能需要编辑的位置在哪里。比如说在我改动了一个地方之后,模型就应该知道下一处要改动的地方需要跳转到 18 行下面,而不是像 Vim 那样需要用户自己来按 18JJ 这样的快捷键。

所以我们解决这个需求的方式就是 Tab 键。用户只需要按下 Tab 键,模型就可以自动跳转到第 18 行可能需要编辑的位置,显示下一条编辑建议,再按一下 Tab 键,又会显示下一条,可以这么一直按下去。

所以 Tab 背后的核心思想是:如何让编辑过程的熵为 0。一旦用户表达了他们的想法,模型应该能够推断出接下来的编辑内容,也就是说没有新的信息需要用户输入。但现在用户还是需要输入一些字符,才能让计算机理解他们的想法。理想情况下,模型应该能够“读懂”用户的想法,所有不需要新信息的编辑部分都应该通过按 Tab 键来跳过。

Aman:如果我们观察不同类型语言模型的 loss,会发现在代码领域,比特每字节(bits per byte)的 loss 比语言领域要低。也就是说,在代码领域,很多 token 和字符完全是可以预测的。所以除了自动补全以外,在预测用户编辑代码后的下一步操作时,这种可预测的现象会更明显。

Cursor Tab 就是要消除编辑器中所有低熵的操作。一旦确定了用户的操作步骤,就应该直接跳过这些低熵的操作,让用户直接跳到下一处编辑点。

Lex:Cursor 是怎么做预测的?背后的技术细节是什么样的?

Aman:首先,Cursor 的延迟很低。我们训练了专门的小模型来解决这类任务。这些模型很依赖 pre-fill tokens,也就是说,这些模型面对的是非常长的 prompt,需要处理很多代码行,但是实际生成的 token 并不多。这种情况下使用稀疏模型(Sparse Model)就很合适,这是一种 MoE 模型。这是我们做的一个突破,这个突破显著提高了模型处理长上下文时的性能。

另一个关键点是我们基于推测解码(Speculative Decoding)构建了推测编辑(Speculative Edits)。

这两个因素在我看来是 Cursor 生成质量高、速度快的关键。

缓存在这里的作用很大,由于输入的 token 非常多,如果每在一个给定行输入,都要在 token 上重新运行模型,那么延迟会很高,GPU 也会因为过载而崩掉,所以就需要设计模型的 prompt,这样它们就可以进行缓存。我们也会跨请求重用 KV Cache,这样就减少了工作量和需要的算力。

Lex:除了代码生产、代码补齐、跨行编辑,以及在同一个文件内的不同位置之间跳转外,Cursor 的 Tab 键还能做哪些事情?

Sualeh:我们接下来想通过 Tab 键来实现在不同文件之间的跳转。有可能用户先编辑了某个文件,但是想要在另一个文件中继续编辑,这样的话 Tab 也应该要能跳转到另一个文件。

Arvid:这个过程可以概括成下一步动作预测(next action prediction)。比如有时候在终端运行命令,需要 Cursor 根据已经写好的代码来推荐命令,它虽然能给出一些建议,但如果没有更多的信息,用户很难判断到底对不对。所以,Tab 就可以引导用户先跳转到定义,了解了定义知识以后再跳转回来,这样用户就知道了这个概念,愿意接受下一处补全。

Michael:Coding 是一个很独特的领域。很多时候,在编程中其实是可以根据用户前面的工作来预测接下来 5 分钟他们想要做什么的。

所以我们可能可以发展到这样一个状态:在人类开发者已经做了一些工作之后,接下来要么让 Cursor 帮忙接管接下来 5 分钟的任务,对于人类员工来说,只需要稍微看一下 Cursor  的步骤,发现它完成得很不错,再在屏幕上点击 Cursor 做出的改动就可以了。

Code diff

Lex:Cursor diff 会通过红绿对比来告诉我们怎么修改代码,在聊天窗里也可以查看并且接受 diff。diff 的设计逻辑是什么样的?

Sualeh:我们总共有四到五种不同的 diff 视图,比如我们就为自动补全优化了专门的 diff 视图,这个 diff 界面和审查大块代码时的 diff 界面是不一样的。针对处理多个不同文件的情况,我们也在优化响应的 diff 视图。

从 high-level 角度,自动补全会要求更快的阅读速度,程序员的视线需要集中在一个特定区域,不能关注到太多地方,避免注意力分散,所以我们做这个任务的 diff 的时候会优先考虑这个问题。

现在自动补齐的 diff 界面中,除了代码写作框外,如果我们想要删掉某个位置的代码,添加另一个代码,它会在旁边独立显示一个框。

我们围绕代码补齐做过三、四种不同的 diff 界面。最初是类似于 Google Doc 的删除线的形态,还尝试过红色高亮块等等形态,最终选定了这个方案。下一版迭代会更有趣。首先会有一个蓝色高亮来突出 AI 给出建议的位置,但具体的建议内容不会直接显示出来,用户可以通过按住 Option 键的方式来查看 AI 的建议到底是什么,如果松开 Option 就又会回到原始代码界面。

Arvid:我个人很期待接下来可以围绕 diff 做出很多改进和优化。diff 涉及的场景通常是代码审查里面的验证难题,diffs 很适合小范围的代码改动,但如果代码改动的范围比较大,或者涉及到多个文件,要来回审查这些 diff 其实就有点难了。

关于这一点,我们其实也有几种解决思路:

其中一个想法是,在比较大规模的代码改动中,有些 diff 的信息量很大,很重要,而有些 diff 只是一样的信息来回重复,也就是说它们的熵很低。所以我们就把重要的那些 diff 高亮,不重要的标成灰色。

另一种思路是,我们可以有一个模型来对这些 diff 进行审查对比,如果找到某个地方有 bug 的话,就用红色波浪线划出来,让用户重新审查这里的 diff。

这些想法都让我觉得在产品体验上我们还有很多事情值得做。

Lex:这些产品思路最终想达到的目标是让人类工程师在代码编辑和审查中只关注他们最需要阅读的内容,从而达到最佳效果?

Arvid:我们也考虑过设计一个模型来做这件事,现在 diff 环节的算法只是普通的算法,没有特别智能。虽然我们在设计这套算法的时候将人类的智能融入了进去,但在实际执行中,算法并不会关心处理的具体任务是什么,而这正是我们希望模型能做到的事情。

Sualeh:问题就在于,这些模型会越来越聪明,如果模型越来越聪明,它们能提出的代码改动的内容和范围也会越来越大。如果改动越来越大,程序员就要做越来越多的代码审查工作,而 Cursor 就可以帮他们做这个,这样他们就不需要花大把的时间来做代码审查。

Lex:涉及到多个文件的代码审查时,diff 又是怎么设计和实现的?

Aman:就像 Arvid 刚才说的,我们可以把人从代码审查中解放出来。现在,我们要花很长时间去理解陌生的代码,而且它找出的 bug 往往非常多。但是使用 LLM  就可以很大程度上提高审查的体验,比方说,Arvid 刚刚说的那些想法就可以引导用户关注真正重要的地方。

代码审查的用户体验设计通常要考虑到 reviewer 和写代码的人两类角色,但如果写代码的是 LLM 的话,我们就可以不用考虑代码编写的具体体验了,从而可以更多去围绕 reviewer 来设计整个产品体验,从而让 reviewer 可以轻松高效地完成任务。

我觉得只是关注代码审查环节本身的话并不能真正解决问题,我们在产品设计中可以更有创意,去探索可能性的边界。

Arvid:我觉得需要指出来的一点是,顺序也很关键(ordering matters)。一般来说,在审查时我们会先看到一串文件列表,然后习惯性地从上往下看。但实际上,有些内容因为逻辑上是基础的,应该最先理解这部分内容,之后才是其他部分信息。这种梳理顺序的工作就可以交给模型,让模型来引导我们去找到信息逻辑顺序,再来完成审查。

Lex:未来代码的创作过程是不是会越来越多地使用自然语言?变得像写书一样。

Arvid:有时候会用到自然语言,但不是所有的编程任务都会用到自然语言。比如,如果我和 Sualeh 一起编程,Sualeh 可能坐在电脑前敲键盘,而我在开车,那我会告诉 Sualeh 要怎么去做。但也有些有时候,要给 Sualeh 解释清楚要做什么很麻烦,那我就会直接接过键盘,给他演示一下要怎么做,我只需要写一部分代码,他就懂了,这种沟通方式很简单直接。对 AI 来说也是这样的。有时候最简单的沟通方式就是给 AI 一个例子,这样它就会继续往下做完这个任务。

或者,如果我们要设计网站,最简单的办法并不是告诉 AI 我想要的是什么,而是直接拖动、绘制页面,给它展示应该怎么做。可能到最后我们也会通过脑机接口让 AI 理解我们的想法,到那时自然语言一定会有一席之地,但是我觉得大多数时候,自然语言不会是大多数人写代码的方式。

底层模型

Lex: Cursor 背后的模型是什么?

Aman:除了调用最新一代模型能力之外,Cursor 自己也训练了一些自定义模型(custom models),这些模型会配合那些在复杂推理任务上表现更好的头部模型,共同完成任务。

Cursor Tab 就是一个很好的例子,在 Cursor Tab 的需求下,我们训练了一个针对这个特定任务的模型,最新一代特定模型在特定任务上的表现甚至超过前沿模型,这一点从评估结果就能看出来。

另一个需要特定任务模型的是 Apply,在 Apply 任务中,自定义模型不仅很有必要,而且效果也很好。虽然头部模型很擅长做代码规划、生成大致的修改框架,但是它们不能很好地显示 diff。

无论是 Sonnet 还是 o1,这些模型都会在一些简单的细节上出错,比如数代码行,尤其是在处理超大文件时,这类问题会更普遍。为了解决这个问题,我们就让模型先勾勒出一个大致的代码块,用来表示需要修改的内容,然后再训一个模型把更改 Apply 到文件中。

我们也还在探索怎么把 o1 模型的能力嵌入到产品体验里,我觉得如何目前就 o1 的最佳实践还没有定论,但比较明确的方向是,它可以让我们更容易地在后台处理一些任务、agent 的属性更强。

Aman:o1 也有一些明显的限制,先抛开能力本身,它不支持流式输出这一点就很影响体验。o1 目前的状态更像是一个 V0 版本,还有很多需要改进的方面。

Lex:有消息说 GitHub Copilot 可能会以某种方式整合 o1 模型的能力,也因此会有人说“Cursor 完了”,你们怎么看这件事?

Michael:我认为这个领域与过去 2010 年左右的软件领域有些不同,因为这里的上限真的非常高。我认为再等 3-4 年,到那个时候最好的 AI 编程产品可能比现在的要实用得多。就当下说只要能打造出更好的产品,就有机会超越那些拥有大量用户的竞争者。因此,我认为接下来的几年关键在于打造最好的产品和系统,不仅包括模型引擎的改进,还包括优化代码编辑体验。

Aman:Cursor 相比其他产品对于用户的附加值不仅仅在于能快速整合 o1 这样的新模型,它的价值还来自于我们针对各种类型任务开发的专有模型,这些模型可能在用户不知情的情况下默默发挥作用,并且每个功能都是围绕用户体验精心设计的。

Lex:在你们的实践经验中,GPT 和 Claude 对比,谁的代码能力更强?不过由于 Cursor 各个环节涉及到的模型不同,答案可能不是绝对的?

Aman:没有哪个模型能在所有方面的表现都比其他模型更好,包括速度、代码编辑能力、处理大量代码的能力、上下文长度和代码能力等等。不过,就目前来看,我觉得整体上表现最好的模型是 Sonnet,这也是大家的共识。

o1 确实很有意思,它很擅长推理,如果给它一些非常难的编程面试题问题或者 LeetCode 题目,它可以处理得很好,但是它不像 Sonnet 那样能很好地理解到用户的模糊需求。

至于其他的头部模型,我并不是想说它们是针对基准测试训练出来的,但确实存在一个顾虑是,和中腰部水平模型相比,它们在基准测试中表现很出色,但一旦涉及到超出这些范围的任务,这些模型的表现就会下降。

但当我们的任务范围超出基准测试时,Sonnet 是最能保持其原有能力的模型,它在基准测试中展现的能力和在实际代码任务中的表现是保持一致的。

Lex:前面提到的 Apply 这个功能可以查看代码,提出很好的建议,告诉我们接下来怎么做。对我们来说,把这两个模型结合起来好像也不难,为什么你们觉得并不简单?

Sualeh:和大家的猜测相反,Apply 的实现并没有采用确定性算法(deterministic algorithm)。

Aman:人们在其他代码产品那里看到的 Apply 其实都是对 Cursor Apply 的简单模仿,可能很多人觉得 Apply 是可以通过确定性匹配的方式来实现,但这种方式下有 40% 的概率都会出错,产品体验也会很差。

我们在设计 Apply 的时候还能最智能的模型在生成代码时,使用较少的 token,从而降低延迟和成本。我们可以给出一个非常粗略的代码,让模型去跑,因为跑通这个框架的代码对模型来说很简单。

这样的形式还可以继续下去。可以用越来越智能的模型来做规划,然后让不那么智能的模型来跑。比如我们可以用 o1 或者更智能的模型来做更 high-level 的规划,再先后让 Sauna 和 Apply 来处理。围绕“如何让 Cursor 反应更快”也有很多细节在里面。

三、Cursor 是如何提高产品响应速度的

Lex:你们是怎么提高 Cursor 响应速度的?

Aman:其中一个重要组成就是推测编辑(speculative edits),这是从推测解码(speculative decoding)中衍生出来的。

可以先解释一下推测解码是什么。大部分情况下,尤其是在 LLM 生成受内存限制的情况下,并行处理 tokens 比逐个生成 tokens 更快。如果查看每秒生成的 tokens 数量,会发现处理 prompt tokens 的速度比逐个生成 tokens 要快得多。推测解码可以通过并行处理提高模型速度。

传统的推测解码是用一个很小的模型来预测 draft tokens,然后用更大的模型来做验证。但因为预测的 code 实际就是已有的 code 本身,而我们已经很熟悉这些 code,所以可以直接把原始的 code 片段输入到模型中,让模型去验证代码相不相符。绝大多数情况下,模型会认为这些 code 片段是正确的,并且直接原样输出。

这样,我们可以并行处理足够多的代码行,直到模型预测的文本与原始 code 出现不一致的地方。这时,模型会生成新的 token,我们则根据 code 的匹配程度判断什么时候重新开始检测代码块。

最终呈现出来的效果就像是加速版的代码编辑,看起来就像是模型在以更快的速度重写所有 code。所以我们就可以用和 diff 一样的界面,只不过数据传输的效率更高。

Sualeh:这样设计的好处是,我们可以在数据传输的时候就开始做代码审查而不是等到全部加载完才开始整个过程。这个设计很有意思,但其实 speculation 这个路径现在已经很常见了,不仅仅是 LLM,CPU 和数据库实践中都有用到 speculation。

Lex:具体到 chat、diff 这种功能上,是怎么让模型响应这么快的?

Aman:我们用了很多策略,其中一个比较有意思的就是缓存预热(cache warming)。当用户在输入时,我们能预判他们可能用到的上下文信息。我们之前提到过,复用 KV catch 可以降低延迟和成本,还能跨请求共享。只要用户开始输入内容,我们就可以立即使用当前的文件内容预热缓存,这样用户按下回车后,需要预处理的 token 就少了很多,可以更快开始生成响应,大大降低了首次响应时间(TTFT)。

Lex:还有哪些更 high-level 的缓存机制可以帮助提升响应速度?

Aman:在 Cursor Tab 的设计里,系统可以预测用户可能会接受哪个建议,提前处理可能会发生的下一个请求操作。这种方法结合了缓存和预测。通过预测用户的选择,系统可以提前缓存下一个建议,这样一来,当用户按下 Tab 键的时候,系统就会立即给用户呈现下一个建议。这是一种启发式的策略,虽然模型本身没有变化,但通过更 high level 的缓存机制,会让用户感觉响应速度很快。

Sualeh:如果我们能让 KV Cache 变得更小,就可以做更多预测。比如,系统可以预测并缓存接下来 10 个可能的建议,用户在这 10 个选项中选中一个的概率会比单个预测要高得多。用户可能输入新字符后会选择缓存中的其他选项。这个策略在 RL 上也很有用,单个预测样本可能不够准确,但如果有多个预测选项,其中至少有一个正确的概率就会大大提升。

模型内部会对于哪个建议是正确的、用户想要的是什么建议,都存在一定的不确定性。我们在用 RL 训 Cursor Tab 的模型时的其中一个事情就是预测模型生成的 100 个建议中,哪一个对用户来说更合适,更符合他们的偏好。

模型可能在某些方面能够做出很准确的预测,而在其他方面预测得就不是太准确,基于用户反馈,我们可以对模型进行奖励或惩罚,如果用户喜欢这个建议,就给它奖励,如果模型喜欢而用户不喜欢,就给它惩罚。通过这种方式,我们可以训练模型输出更多用户会喜欢的建议。

Aman:虽然这样不能直接提高响应速度,但是二者之间其实是有联系的。因为即使是小模型,只要能用 RL 训模型,就有可能达到与大模型相当的性能水平。除了前面的 KV catch 的角度,还有一些技术能帮忙提高速度。

比如,两年前比较主流的是多头注意力机制(multi-head attention),现在已经逐渐转向使用更加高效的注意力机制,比如 group query 或者 multi-query attention,这些机制能在更大的 batch size 下更快地生成 token。

但这些方法并不会影响首个 token 的预填充速度,它们主要是提升的是后续 token 的生成速度。这是因为用户生成 token 时面临的瓶颈不再是在所有 token 上执行高度可并行的矩阵乘法,而是在处理 long context 和更大规模的 batch size 时,如何快速读取那些缓存的 keys 和 values。这就又涉及到内存带宽的问题,我们可以通过压缩 keys 和 values 来提升速度。group query 或者 multi-query attention 的目的都是减少 KV Cache 的大小。

DeepSeek 开发的 MLA(Multi-Latent Attention,多隐向量注意力机制)也是一种,但MLA 的思路更新:用一个大的共享向量来存储所有keys 和 values,同时为每个 token 配备较小的向量,这样系统就只需存储较小的向量,它的原理是把所有注意力头的键值都转换成一个隐向量,这个向量随后会在时间维度上扩展。

Lex:这些在处理内存限制上的细节最终是如何影响用户体验的?

Aman:主要有两方面的影响。首先,我们可以大幅增加缓存容量,因为 KV Cache 占用空间变小了。这也意味着我们可以缓存更多内容,提高缓存命中率(cache hits),从而减少首个 token 的生成时间。其次,即使用户的请求量增加、批次变大,模型推理时的速度也不会明显下降,因为模型能以匹配的速度生成 token。

Sualeh:这种方法还能支持更长的 prompt,从而给到模型的提示也更明确。

Aman:KV Cache 是“所有 prompts 的总大小”和“并行处理的 prompt 数量”的乘积,所以这两个维度中任何一个的增加也都不会降低 token 生成的延迟。

四、Priompt :以网页设计为灵感的提示词工程

Lex:我们刚刚提到,基准测试中的 prompt 往往结构清晰、表述规范。Arvid 也写过一篇 Prompt Design 的文章,在你看来一个好的 prompt 能起到什么作用?

💡

Arvid 在 Prompt Design 一文中参考现代网页设计实践提出了“提示词设计”的概念,认为二者的共性在于都要做到清晰有效的沟通、对动态内容作出响应、涉及到不同“屏幕尺寸”的适应等。

Arvid:每个模型都不太一样,对不同的 prompt 的反应也不一样,比如之前的一系列模型都对 prompt 的要求非常严格,上下文窗口也很小。

在代码开发场景中,我们会遇到大量和 prompt 相关的信息,包括文档、添加的文件和对话历史等。这就带来一个问题:在 context window 有限的情况下,该如何筛选和组织这些信息?拿现在的模型来说,即使上下文窗口更长了,但要是塞满整个窗口,模型的响应速度就会变慢,有些时候还会被 prompt 混淆,而且有的模型比其他模型更容易受到混淆。

为了解决这个问题,我们开发了一个叫 Priompt 的内部系统,它在一定程度上帮我们解决了这个问题。

Priompt 借鉴了现代网页开发的最佳实践。和固定版式的杂志排版不同,网站开发中会涉及到的一个情况是,不同设备的中信息的展示多少、格式等是动态变化的,而用户到底在哪里查看网站开发者事前并不知道,但无论终端怎么变,都要保证网站信息在不同设备上正常显示。AI 提示词工程也是类似,我们要做到无论 input 内容多大、怎么变,output 的格式都能正确展示。

我们比较喜欢用 React 库的声明式编程方法(declarative approach),也就是说,我们可以通过 JSX 直接描述期望的 UI 呈现方式,例如通过设置样式属性或CSS类名来声明元素的展示层级(如z-index)或其他视觉特性。

就像浏览器有自己的渲染引擎一样,Cursor 也有一个 Priompt  渲染器,它的作用是把内容合理地排布在页面上,我们只需要告诉它你想要什么,它就会帮你实现。

我们发现这种方法非常有用,而且它的作用也在不断演变,一开始设计它是为了应对有限的上下文窗口,现在它帮我们把数据处理和实际展示分成了两个部分,这样做的好处是 debug 就会变得更容易:在不实际修改代码的情况下,我们可以先通过输入 prompt、Priompt 帮助预渲染的方式来看某个代码改动是否真的有效。

Arvid:Priompt 和 React 类似,它还有一些组件,比如文件组件会获取当前光标的位置,因为理论上用户正在查看的那行代码往往是最重要的。我们会给不同的代码行设置优先级,光标所在行的优先级最高,每远离一行优先级就降低一级。最终在渲染的时候,系统会计算一共能显示多少行代码,并以光标所在行为整个代码的中心。

Aman:Priompt 和还有一些更复杂的功能,如果整个代码库中有大量代码块,我们还可以使用 retrieval 、embedding 和 re-ranking scores 等方式让组件自动设定优先级。

Lex:为了让模型更好理解用户提问,是不是也可以用到类似方法?

Arvid:我们的目标是,“让用户做自己的事情就好”,我们的工作或者说整个 Cursor 要做的就是如何去 retrieve 相关信息。

Lex:我和 Perplexity 的 CEO Aravind Srinivas 也聊过这个问题,他认为要让用户想怎么问就怎么问。对于程序员来说,我们是不是还可以提出更高的要求?在完全放任用户随意提问和引导用户给出更多 prompt 之外是不是也存在一种情况,即系统在鼓励甚至要求用户把想法表达得更清楚,这里说的不是语法或者句子要多规范,而是 prompt 中体现出的思考深度。

Aman:我认为即便模型能力已经接近完美,当用户向模型提出需求的时候,往往还是没法完全表达自己的想法。要解决这个问题有几个方法:

• 第一,让模型直接告诉用户,基于用户的问题,它还是不太确定这部分内容,让用户再明确一下;

• 第二,如果模型可以一次生成 5-6 种可能的结果,基于这些结果也可以告诉用户,由于用户的问题有一些模糊的地方,模型会把所有可能的结果都展示出来,让用户来选择最合适的。

Lex:让模型主动提问有多难?我是不是需要通过提问来获得更多信息,从而减少一些可能发生歧义的情况?

Sualeh:最近我们在 Cursor 里新增了一个文件推荐功能。用户在编辑问题的时候,模型就能猜到问题是什么,比如,用户在编写 API 代码的时候,模型会根据这个文件之前的提交记录,推测出客户端和服务器端的相关代码可能也很重要。这背后存在一个技术上的难题:怎么在历史记录中找到之前的信息,并根据用户当前给的 prompt 判断出哪些文件最重要?目前我们的版本还比较简单,这个功能还在试验阶段,相信未来会越来越精确。有个这个功能,用户就可以很方便地添加文件,让模型来编辑。

比如说,当用户在编写 API 时,可能还需要修改使用这个 API 的客户端代码和处理 API 请求的服务器端代码。如果 Cursor 能在用户输入 prompt 并按下回车键之前,就帮助理解这些模糊的地方,就能提供很大帮助。

五、Shadow Workspace:Coding Agent 的工作流设计

Lex:你们怎么看 Agent?

Arvid:虽然目前 agent 在很多场景下还不是特别实用,不过我觉得它们离真正派上用场已经不远了。有些任务非常适合使用 agent,比如我们遇到了一个 产品 bug,这个 bug 可能是“聊天框里的内容不能复制粘贴”,我只需要用两句话来说明这个任务:“这里出问题了,请修复一下”,agent 就会立刻去处理。第二天我回来看看结果就行了。

在这个任务里,Agent 首先能定位到正确的文件,尝试重现并修复 bug,然后验证修复后的代码对不对。这个过程可能需要很长时间,所以我很希望能有这样的 agent 来帮忙。

很多人觉得代码 Agent 最终会完全取代程序员,但我不这么想,因为编程的价值在于迭代,很多人在看到一个初始版本之前并不明确地知道自己想要什么。在大多数的编程任务里,用户需要的是一个能立即给出初始版本的系统,让他们可以立即开始迭代优化、补充更多信息。

Lex:在 Replica Agent 的设计中,Agent 不仅能设置开发环境、解决软件包问题、配置数据库,还能部署应用,这种设计是不是符合你们对于代码 Agent 的预期?Cursor 会开发类似的功能吗?

Arvid:是的,Replica Agent 的确是在这个方向上。对于某些类型的编程来说,这个工具非常好用。

目前 Cursor 还没有去积极开发类似的产品,不过我们的目标是让程序员的工作更轻松有趣,那些繁琐的重复性任务完全可以交给 agent 处理。我们甚至可以让 agent 在后台运行,理解程序员的操作意图。

假设有一个同时涉及后端和前端的 pull request,程序员在前端工作时,可以让一个 agent 在后台运行,理解程序员的做法。当程序员从前端转到后端部分时,agent 就可以生成一些初始代码,供程序员在这一基础上进行迭代。

Lex:Arvid 写过一篇文章叫 Shadow Workspace: Iterating on Code in the Background。可以具体讲一下 Shadow Workspace 吗?

Arvid:我们希望很多任务可以在后台处理完成,我们也在这个方向上做尝试。

除了缓存预热(cache warming)或者确定进入命令提示词的正确上下文之外,我们还没有太多后台操作。整体想法是,如果我们能在后台进行计算,那么就可以在更长的时间维度上帮助用户完成任务,不仅仅是预测接下来几行你要写什么,而是预测在接下来的 10 分钟里,用户打算做什么,通过在后台处理,可以投入更多的计算资源。

基于这个想法我们实验了 Shadow Workspace 。想要充分利用后台处理的优势,就需要给模型提供反馈信号。如果没有反馈,单纯延长模型的思考时间也能提升性能,比如 o1 就是个很好的例子。

另一种提升性能的方式是让模型迭代和获取反馈。对于程序员来说,一个非常重要的反馈来源是 language server ,它们可以在开发者 coding 的时候就给出错误提示,还支持跳转到代码的定义位置,理解代码的结构。我们的做法和 VS code 一样,基于 LSP 给开发者作出对应提示,但我们还想在不影响用户的前提下,在后台把同样的信息提供给底层的 LLM 模型。

💡

Language Server Protocol (语言服务器协议,简称 LSP)是微软在 2016 年提出的一套统一的通讯协议方案。,定义了一套代码编辑器或 IDE 与语言服务器之间使用的协议,语言服务器提供自动完成、转到定义、查找所有引用等语言功能。每种编程语言都有其独立的 language server,这些 language server 都会通过 LSP(language server protocol)与VS Code进行交互,由此 VS Code 就不需要自己内置一些东西来支持各类提示需求。

所以 Shadow Workspace 背后的思路是,也许我们可以在后台运行一个隐藏窗口,通过特定的标志,把这个隐藏的窗口设置成不可见状态,开发者用户视角下是无法看到这个窗口的。在这个窗口中,AI agent 可以随意修改代码,只要它们不保存就行,因为隐藏窗口和主窗口共用一个文件夹,只要不保存修改,就不会影响到实际文件。然后 agent 可以从 linting 中获得反馈,实现 go to definition 功能,对代码进行迭代。

最终的预期是 Agent 可以在后台执行一切。

我们的很多 blog 也都在在探讨怎么实现这件事,因为确实有点复杂。我们希望这个系统能在用户的电脑上运行,这样它就能精确地镜像用户的环境。

在 Linux 系统上,我们可以镜像文件系统。通过这种方式对文件进行修改,会让 AI 以为自己是在操作实际文件,但这些修改都是存储在内存中的。要实现这个功能,需要开发一个类似内核的扩展。在 Mac 和 Windows 系统上,实现起来可能会稍微困难一些,但这个技术挑战也很有意思。

Aman:为了避免人类开发者和后台 agent 同时工作时可能出现的文件写入混乱,我们还设计了一个写入锁定的限制。具体来说,在个过程中,我们 LLM 会对文件写入权限加锁,Agent 只在 Shadow Workspace 中操作,所有改动都只存在于内存中,而不直接修改磁盘上的实际文件。在这个过程中, LLM/Agent 仍然可以触发代码检查和获取语法提示。当 Agent  需要运行代码时,系统会发出提示,如果用户同意这个操作,就会从 language server 或 Shadow Workspace 那里解除锁定。

六、SOTA 模型也不擅长找 bug

Lex:让模型修改文档这件事听起来很有趣,以后我们就可以让 agent 去执行一系列任务,第二天再来看这个 Agent 同事都完成了哪些工作。

Aman:我们可能会设计不同的运行版本。有些任务比较简单,比如用户在编程时,有些操作模型几分钟就可以帮用户完成,这种时候可以让代码在用户的本地机器上运行。但有些任务更复杂,需要更长时间来完成更大的更改,可能更适合在远程沙盒环境中执行。这就带来了一个问题:如何精确地复制或最大程度复制用户的本地环境,确保远程沙盒与用户本地环境中运行代码的效果一致。

Lex:我觉得 Agent 应用范围不应该局限于 coding。比如说我们现在正在录的这期播客就涉及视频剪辑、上传视频、翻译和配音等,我可能会希望 Agent 可以帮助我们把很多和视频编辑不直接相关的任务自动化。在 coding 上,我比较期待 agent 能帮忙找 bug,特别是逻辑错误,以及帮助确定大方向等等。

Aman:有一个很有意思的现象是,如果用户给出的 prompt 是找 bug,那么模型的准确率其实很低,在这类任务上即便是最聪明的模型表现也很差,甚至是 o1 这样的模型。

Lex:怎么解释这个现象?

Aman:模型是 pre-train 数据分布的一个强映射,随着 loss 越来越低,模型的泛化能力也会不断提升,但目前 loss 还没有低到能让模型在代码领域完全实现泛化。我们目前使用到的最新一代的模型很擅长代码生成和问答,这是因为在 pre-train 阶段这类数据特别多,GitHub 上有几万亿个 token,Stack Overflow 上也有海量的问答内容。

但是如果我们想让模型做一些网上很少见的任务,比如 Cursor Tab 要做的根据已完成的编辑预测下一处编辑,这些模型在这类任务和场景上的弱点就会暴露出来。漏洞检测(bug  detection)也是一个很好的例子能说明模型的这个短板。互联网上真实的 bug 检测和修复的完整案例其实很少,并不像代码问答那样数据丰富,所以模型在这类任务上就表现得很吃力。

不过我认为这是模型能力迁移的问题。就像我们已经看到的,我们可以把模型在 pre-tarin 阶段获得的通用的代码理解能力迁移到 Cursor Tab 的任务上。如果一个通用模型在代码方面很强,也应该能通过类似的方式把它的能力迁移到 bug 检测任务上,只是需要一些适当的引导和调整。

Sualeh:需要说清楚的一点是,这些头部模型其实对代码的理解是很到位的。在  pre-train 阶段,在构建内部 representation 时 ,我们几乎可以确定,在处理流程的某个环节,模型能够感知到代码中可能存在一些问题,但是仅仅是感知到“不对劲”是不够的,最关键的问题在于,人类对于哪些bug真正重要有着准确的判断力,比如有的 bug 无关紧要,有的却会导致服务器宕机。

人类工程师之所以能做到这一点一部分也是行业经验和文化的积累。为什么资深工程师很厉害,是因为他们知道 3 年前有人写了一段有问题的代码导致了服务器宕机,相比之下,如果只是在做实验性质的代码,有一些 bug 也没关系,因为主要目的是尝试和体验。所以如果在写实验代码时,模型过分较真,不停地给出提示,反而会很烦人。

但如果是在写生产环境的代码,比如写一个数据库,在 Postgres 或 Linux 这样的系统写代码,或者假如我们是 Linus Torvalds 这样的人(注:Linux 的创始人),那就必须做到万无一失,任何 edge case 的 bug 都不能有,这就要求模型能准确理解开发者对代码质量的要求程度。

Aman:但即使作为开发者我们把对模型代码任务的质量要求调到最高,模型还是不能完全理解到这些微妙的差异。

Lex:但其实人类工程师要真正做到准确判断不同 bug 的重要性也很难。比如你们也有一个原则是,如果某一段代码可能会带来不可控的影响,应该给它特别注释去说明“This line of code is dangerous.”,甚至还会用到大写标记来突出。

Arvid:是的,我觉得这个原则对今天的 AI 模型同样适用。如果每一行代码上都有专门的标注,那么模型就会更关注这一部分,也更有可能发现这一部分的bug。但这个做法本身也也存在争议。一些人会觉得这种做法不太美观,比如 Sualeh 就不喜欢。

除非我们可以做到所有代码都能通过严格的数学方法来证明其正确性,到那个时候就可以随意写代码了,因为只要验证通过,我们就能确定这里没有引入到任何 bug。

Sualeh:虽然从代码审美角度来看我并不喜欢这种做法,但我认为它确实对模型和人类都有用,因为人类经常会忘记很多事情,而且很容易犯一些小错误,从而导致服务器崩溃。就算我们进行了大量的测试,但在这方面我们还是要格外谨慎。

Aman:是的,就拿普通的文档注释来说,人们在修改代码时往往只是匆匆扫一眼,想着“哦,我知道怎么做”,但实际上我们还需要更加明确地提醒他们注意某些细节,否则很容易就疏忽了。

Arvid:我认为未来人们不会再手写测试代码了。当 coding 的同时,AI 模型会自动为这段代码生成一个规范说明(spec),开发者只需要审核这个规范是否符合预期。同时,推理模型会通过计算来验证代码实现是否完全符合这个规范。我觉得这种方式将适用于大多数代码功能的测试。

Michael:这件事是不是又会涉及到我们在前面提到的,人们在开发软件时可能很难很清楚地描述自己的目标和意图,而有时候之所以难以证明代码实现符合预期,也可能正是因为我们很难准确地描述出我们想要什么。

即便模型给出了一个 spec ,我们还是要面对一个问题:我们真的能做到  formal verification 吗?这在技术上是否可行?我觉得这里面还有很多需要深入探讨的地方。又比如 spec 是用自然语言编写的还是用公式来表达?即便是 spec 也还有很多细节很难被清晰界定。所以只是考虑  formal verification  还不够。

Lex:你在前面提到模型在处理 bug findings 这类任务时通常表现不是很好,长远来看你的预期是什么样的?

Sualeh:我觉得模型首先应该能帮助我们解决那些简单的小错误,它应该能快速检查并及时发现“差一错误(off by one error)”这种简单的 bug,因为这类问题非常常见。在这个时候模型就应该给出对应提示,但长远来看,它肯定还需要能够捕捉到更难发现的bug。

Michael:我觉得很重要的一点是,找 bug 能力是让 AI 完成更多 coding 任务的必要条件。当我们想要让 AI 帮我们构建越来越多的系统时,就不仅需要它帮助做代码生成,还需要能验证代码的正确性,如果没有这种验证能力,我们前面聊到的这些模型在代码任务上的问题就很难解决,让 AI 拥有找 bug 的能力不仅仅是为了找出人类写代码时产生的 bug,更关键的是能够验证和检查 AI 生成的代码。

Arvid:关于如何实现让 AI 更好地找到 bug 我们也有过很多次讨论,比如如何训练一个 bug 检测模型,还有一个更受欢迎的想法是:“制造 bug 比找 bug 更简单”,所以对应的思路是,我们可以先训练一个模型,让它学会在现有的正确代码中注入各种类型的 bug,基于这种方式我们会得到大量带有已知 bug 的训练数据,然后,就可以用这些合成数据来训练另一个模型,专门用于发现和检测代码中的 bug。这只是众多可能方法中的一个例子,还有很多其他的训练思路可以尝试。

Michael:还可以有其他思路来完成。我们可以先想象有一个 access 范围足够宽的模型,这个模型可以接触到包括代码在内的各种信息,因为只盯着一个文件来找 bug 其实是很难的,对人类来说也很难,所以通常情况下我们的做法是运行代码、查看traces、通过调试器逐步检查。这是另一个完全不同的方向。

所以这里有两种不同的产品形态。一种是有一个非常专业且运行速度很快的模型在后台运行,一直去寻找 bug,另一种情况就是开发者已经很明确有一个 bug 需要解决,在这种情况下,人们可能会投入大量的计算资源来做这件事。

Lex:你们考虑过代码生成、编辑和 debugging 环节里引入付费的可能性吗?如果能帮助发现 bug,或者生成让用户非常满意的代码,用户愿意为之付费或者打赏,而除了获得收入之外,付费也是比“接受代码”更强的信号来帮助优化产品和模型。

Arvid:这个思路很有意思但也很有争议,并且还涉及到我们对于用户心理的把握,也涉及到算力成本和定价之间的平衡。但整体上我们会觉得引入这个机制可能会让产品变得不那么有趣,可能通过订阅的方式,每月支付一定费用后享受所有这些功能更符合开发者的需求,而不是将 debugging 等功能点单独付费,即便是打赏机制也会有影响。

Lex:在具体的代码任务和代码终端结果之间有更好的信息交互?比如是否存在一个类似于 code -> terminal -> output -> analysis -> code suggestions 这样的循环和反馈机制?

Aman:用户在使用 command+K 功能的时候会利用到终端上下文信息,但我们目前还没有实现循环反馈的部分,但我们觉得这样的功能应该很有价值,问题在于这个功能是在前台执行,还是像我们之前讨论的那样在后台执行。

Lex:前面提到基准测试并不能真实反应模型代码任务的能力?这二者之间有什么区别?如果要评估模型,我们看到的基础测试有什么不足?

Sualeh:基准测试和现实中的代码任务中间有一个非常重要且关键的细节差异,那就是现实中的代码任务并不是“编程面试题”。在现实的工作中,人们有时会用不标准的英语来表达任务需求,或者会给出“照我之前那样做”、“添加这个,然后帮我做那个,再做一个 UI 元素” 之类的指令。很多任务都是依赖于具体场景的。

所以对于模型和产品来说,真正需要的是理解用户意图并完成他们想要的事情,真实的人类需求往往是模糊的、不那么明确的,而不是“面试题”那样具有非常明确、详细规范说明的任务。

Michael:除了 Sualeh 和  Aman  提到的问题外,基准测试还存在一个问题是,实际可以建模的内容与真实的编程之间存在一定的偏差,这种偏差很难概括,因为真实的编程是很混杂的,有些时候很难去明确解释什么是对的,什么是不对的。

而且,由于测试集是公开数据集的原因,这件事会变得更难。有两方面的原因,首先,公开的基准测试有时会被进行针对性地“爬坡优化”(hill climbing),另一方面,要从模型中剔除掉这些公开基准测试的数据也非常难。

举个例子,SWE-Bench 是目前最受欢迎的 Agent 基准测试之一,但它的测试数据也是 Foundation Model 的训练数据的一部分。所以如果我们给到 Foundation Model 一个 SWE-Bench 问题,即使没有提供代码库的上下文,模型还是可以“编造”出一个正确的文件路径和函数名。

💡

SWE-Bench 是一个用于评估 LLM 解决从 GitHub 获取的现实世界软件问题的能力的基准测试。该基准测试包括向 Agent 提供代码存储库和问题描述,并要求它们生成可解决问题描述的补丁。SWE-bench 中的每个测试样本都来自 GitHub 上 12 个开源 Python 存储库之一中已解决的 GitHub issue。每个样本都有一个关联的拉取请求 (PR),其中包括解决方案代码和用于验证代码正确性的单元测试。

为了更准确地评估 AI 模型的软件工程能力,OpenAI  在对 SWE-bench 的改进基础上推出了 SWE-bench Verified。

Aman:在这种情况下,模型可以直接在 issues 和 pull requests 上训练,也许各个实验室会开始做得更好,或者他们已经在数据清理方面做得不错了,但他们不太可能完全剔除掉本身的训练数据。这些都是一些最受欢迎的 Python 库,比如 SimPy。我不认为他们会为了在基准测试中获得真实的评估分数,而牺牲模型在 SimPy 这些主流 Python 库上的性能。

Michael:由于基准测试的这些 bug,很多大模型团队开始用其它测评方式来获得模型反馈,比如让真人实打实地去使用这些系统并提供定性反馈。我还知道有一些 foundation model 公司中,有专门的员工就是专门做这件事。在我们内部,除了用我们自己的私有测试集外,我们也非常依赖这种定性评估。

七、Code Indexing 和 Context

Code base Indexing

Lex:如何防止Cursor对数据库进行未经授权的或错误的修改?

Sualeh:目前确实有一些还不错的解决方案,我们也在基于 PlantScale 开发一个 API ,这个 API 的核心功能是为数据库提供分支(branches)。如果开发人员在开发某个功能并想要对整个数据库进行测试,但又不想直接对整个数据库进行测试时,就可以通过给数据库添加 branches 来做这件事。他们实现这一功能的方式是在 write-ahead log 中添加一个分支,而要做到完全正确涉及很多技术复杂性。今天数据库公司也在不断迭代来应对新的需求。

我们使用的一个数据库 Turbopuffer 未来可能会实现 write-ahead log 的分支功能,AI agents可以利用这些分支进行测试,这可能会成为数据库必须支持的一个功能。

并且我觉得到未来所有东西都需要 branches,随着 branches 越来越多就要设计出更好的算法来做内存、CPU 以及其它资源的优化。

Lex:随着用户规模的扩大,Cursor 遇到哪些挑战吗?

Michael:用户增长的过程其实也是每秒请求数不断增加一个数量级的过程。一开始我们是用通用组件进行缓存和数据库操作,随着系统规模越来越大,就会遇到各种问题,今天我们的规模就会出现表溢出(table overflows)这样的问题。所以到后来,我们也构建了一些自定义系统,比如我们的 indexing 系统,用于计算代码库的语义索引并回答关于代码库的问题。我觉得系统一直最难 scale 的事情之一。

Sualeh:我有一些朋友是很资深的工程师,他们经常提到,在系统 scale 的过程中,很难准确预判判断系统会在哪个环节出问题,即使事先做出了预测,但还是会有遗漏,比如每次增加新功能时总会发生一些意外。

不过对于刚刚提到的代码 indexing 系统,我们目前在做的事情是,在当上传代码时,我们将所有代码分块让然后发送出去。我们对待客户问题非常谨慎,将代码 embedding 存储在数据库中实际上储存的并不是代码本身的内容,这样做的好处是可以确保我们不会引入客户端的bug。此外,我们还在服务器上存储了很多关于代码块的详细信息,并且所有内容都已加密处理。

我们的方法是先把所有代码分块,然后将这些代码做 embedding,在这之后,我们把这些 emebdings 存储到数据库中,而不是去存储任何源代码。这么做首先客户确保不会引入用户代码库中的 bug,另外这些存到服务器上的所有内容也都是加密的。

在这个过程中我们遇到的一个技术难点就是,如何确保本地 index 和代码库的状态与服务器上的保持一致?

我们最终采取的技术方案是,给每一个文件和文件夹都设置一个哈希值,其中文件夹的哈希值是其所有子文件和子文件夹哈希值的组合。这个过程可以递归进行,直到根目录。这其实是相对复杂的一种方式,还有另一种简单的方法是,为每一个文件保存一个哈希值,然后再以每分钟为单位尝试从服务器上下载这些哈希值,找出哪些文件在服务器上不存在,从而保证客户端和服务器之间的状态一致。

但这种操作会给客户端带来严重的网络负担,没有人希望使用 Cursor 时,我们一直占用它们的网络带宽,而且这种操作也会给数据库带来庞大负担,差不多每秒钟都要读取数 10TB,甚至接近 20TB 的数据库,所以肯定不能这样做。

所以我们采取的方法是只尝试对项目根目录的单个哈希值进行比对,如果发现有不匹配,才进一步去找出具体哪里不一致,再通过子节点一层一层向下找到具体哪里发生了变化。这种逐层检查和递归的方式,能够在确保同步准确性的同时最大程度地减少网络和数据库成本,因为对于大多数用户、大部分任务来说,哈希值都是一致的。这个结构叫做 Merkle Tree。

Sualeh:代码检索系统的 scale 之所以很难是因为,不仅用户量增加,其中一些客户的代码库相当庞大的,我们一开始设计的时候参考的是 Electron 这样大型的代码库,但它的规模显然和那些存在了20 年、拥有海量文件的公司的代码库还差很多,与此同时还需要考虑到怎么支持一大群程序员来使用。

这里涉及很多细节,构建一个简单的方案很容易,但要考虑到扩展到支持大量用户、大量公司客户显然是个很困难的问题。如果去拓展代码检索系统是 Cursor 规模扩展过程中遇到的挑战之一。过去几周几个月里一直在处理这些扩展性问题。

Aman:我们在设计这个 index 系统的时候也加入了很多巧妙的细节。比如,成本的瓶颈并不在于向量数据库或数据库中存储的数据量激增,而在于 embedding ,假如一个公司中有好几个人使用了相同的一段代码,就没必要为每个人的每一次操作单独做 embedding 的计算,即便这些代码在不同 branches 上,或者只是做了一些本地修改。

在这个时候,就可以使用到一个技巧:对给定代码块的哈希值所计算出的实际向量进行缓存。这意味着,当公司里第 n 个人需要嵌入他们的代码库时,系统就可以快速响应。而且,这个过程中我们其实不需要在服务器上存储源代码,因为只调用到了向量数据库和向量缓存中存储向量。

Lex:对代码库 indexing 的投入带来的好处是什么?

Arvid:我认为最明显的好处是,当用户在庞大的代码库中想要查找某个功能的具体实现位置时,可能只记得个大概,比如“我想找到我们实现 X 功能的地方”,但在普通的文本搜索中,用高糊并不清楚应该搜索什么关键词,如果有了 code base 的 indexing,就可以向一个 code base chatbot 提问,很多时候,code base chatbot  都能准确找到对应的代码位置。

Aman:我认为在未来,这个功能只会变得越来越强,我们也在努力提高检索的质量。我认为这代码搜索这部分的想象力比人们想象中的要大。

Lex:在这个过程中为什么没有考虑过端侧方案?

Arvid:在本地做 embedding 听起来很厉害,但实际操作起来很难。有一点我想指出的是,用户们所使用的设备性能参差不齐,比如有些用户用的是最新的MacBook Pro,但实际中超过80%的用户用的是 Windows系统的电脑,而且其中很多电脑的性能都不是很好。因此,本地模型实际上实际上只适用于最新的高性能电脑而且会带来很大的负担,即使我们想实现本地嵌入,目前也没办法集中精力去实现这一点。

Sualeh:除了计算机性能的限制之外,还有个现实问题是这种方案会牺牲轻松和高效的体验。大公司的代码库规模极大,即便一个公司给自己的程序员都配上了最高性能的电脑,处理这么大的代码库也会非常吃力,即便是头部公司的顶尖程序员,如果一切都在本地运行,体验也会非常糟糕。

Aman:是的,在处理近似最近邻算法(nearest neighbors)和大型代码库时,本地做 embedding 会大量占用内存和CPU 资源。现在端侧模型的发展的一个趋势是向 MoE方向发展,好处是它更多地依赖于内存带宽,而不是 GPU,但缺点是这些模型通常都很大,甚至可能需要多个节点来支持,即使是性能非常好的 MacBook 也无法支持这些模型。特别是在 coding 场景,这不仅是一个模型够不够好的问题,而是模型能否满足开发者日益增长的期望。也许对于一些问题,本地模型已经模型够用,但人们总想用最好、最智能、最强大的模型。但这些模型对于大多数人来说,在本地运行都非常困难。

Lex:相对于中心化的头部 AI Labs,还是有不少人在提升端侧模型能力的,尤其是在开源社区。你们怎么看这一趋势?

Arvid:除了端侧模型,还有一个我很喜欢的替代案,但目前还处于研究阶段,就是用同态加密(homomorphic encryption)来做 LLM 的推理,也就是说,我们在本地设备上的输入会被加密后发送到云端来做计算推理,这部分可以调用头部模型能力来完成,但服务器看不到原始数据,当服务器完成推理计算后返回答案,我们可以在本地进行解密,整个过程中只有用户自己才能看到最终的结果。

这个技术还很早期,并且这里面的关键在于如何降低计算成本。同态加密可以很好解决目前的用户数据隐私会暴露给模型的问题,所以如果这件事能实现就会影响很大。因为随着模型能力越来越强、可以带来的经济价值是越来越高的。

💡

同态加密(homomorphic encryption):密码学中的一种特殊加密技术,它允许在不解密数据的情况下对密态数据执行特定的计算操作,并且计算结果仍然是密文状态,对密态结果解密后可以得到与直接用明文数据计算相同的结果。这一技术被用于隐私计算与安全多方协同计算领域。

如何解决 Context 问题

Lex:Cursor 是怎么解决 Context 问题的?模型自动找出上下文这件事有多难?

Michael:Context 是一个很复杂的问题,我认为未来我们的确可以在自动找上下文这件事上做得更好,但也需要指出来的是,自动识别 context 这件事是有成本的。

首先,当我们给模型提供的上下文越多,它们的响应速度就越慢、请求的成本就越高,这就会牺牲我们对模型能力的调用以及可以在后台处理的任务就会减少,此外,对于许多模型来说,如果 prompt 中包含大量信息,它们也会感到困惑。因此,我们在给模型提供上下文时需要确保信息的准确性和相关性都很高。

我们其实已经在产品中实现了一些自动上下文的功能,这是我们希望做得更好的地方,比如更好的 indexing 系统、embedding 和 retrieval 等。

这里面还有很多很有意思的学术问题,既有我们内部在尝试的,也有一些共识性的讨论。

比如,如何让语言模型真正理解一个新的信息语料库?很多人都会提到的一个方向是,能不能让上下文窗口无限大?如果可以实现,那么能不能让模型在具体的任务中考虑到这个无限大上下文?如果这个问题成立,那么接下来就又会考虑能不能对无限大的上下文进行缓存?

与此同时我们也在尝试其他思路,比如类似于模型微调的思路,将这些信息融入到 weights 中进行学习。如果模型更多地是在 weights 级别上学习到这些信息,而不是在上下文的话,那么结果又会不一样。

但最终要怎么实现目前还没一个定论。

Aman:让模型直接在 weights 层面学习这个路径可以先用 VS Code 来做概念验证。因为 VS Code 本质是开源的,也就是说,模型在 pre-train 阶段就学习过这些代码,甚至也学习过相关的代码问答内容,然后经过微调和 RLHF 后,这些模型就被训练到能够回答一些代码相关的通用问题。当我们问这个模型和 VS Code 相关的问题时,虽然有时候它会产生幻觉,但有时候它确实能很好给出答案,我认为这只是它碰巧表现得还不错,但如果我们专门地对模型进行训练或者 post-training,从而让它更好地理解整个代码库,可能结果又会不一样?

还有一个值得探索的问题是,我们到底是希望模型能够端到端地完成所有工作,比如自己做完 retrieval、回答问题、生成代码,还是说我们更希望将 retrieval 和底层模型独立开?如果是后者,可能在未来几个月内就会出现一批比今天开源 SOTA 模型还要强的模型,在这种情况下,我们可能可以把一个高质量的开源模型单独训练成 retrieval 模型,由这个独立的 retrieval 模型给更强的头部底层模型提供 context。

八、Scaling law 、复现 o1 模型

Scaling laws

Lex:你们怎么看接下来 scaling law 的发展?

Aman:我觉得 Chinchilla 之后,大家某些程度上在 scaling law 上有些走偏,现在大家更关心的是,怎么在给定的inference budget下,让模型的表现尽可能好。

Scaling law 实际上考虑的维度比我们之前提到的算力规模、参数数量和数据量要多得多。例如,inference computer 就是一个显而易见的维度,我认为 context 长度也是一个。如果有人跟关注这两者的优化,那么可能就会训练出一个处理超长上下文时成本更低、速度更快的模型,即便训这个模型需要投入多 10 倍的算力,但这个过程中优先级目标是 long context window 和 inference compute。所以怎么理解人们对不同纬度之间的关系,以及围绕这些纬度做权衡和探索其实很有意思。

Lex:也有人认为 Scaling law 即将遇到上限。

Aman:单纯就智能能力讲的话,一定是模型越大、能力越好。

我很看好看好蒸馏。蒸馏也许是解决 data wall 的另一种方法。当数据不够用的时候,我们可以先在已有这些 tokens 上训练一个非常大的模型,然后将其蒸馏成一个较小的模型。这样一来,与直接训练这个小模型相比,我们可能能够从这个更小的模型中获得每个token的更多信息。

如果投入大量资金进行训练,能够得到最具性价比的模型,那么要调整多少个参数来实现。应该重视模型推理时间的计算。然而,有些人可能已经采取了一种过于简单的方法来关注推理时间的计算,即他们只是用远超必要数量的数据来过度训练那些70亿参数的模型,就像Llama。

如果你真的很在意这个问题,也许可以采用 Google Gamma-27B 的做法:不要仅仅训练 token,而是直接训练模型来最小化与Gemma 27B分布之间的KL散度(KL divergence)。这就是知识蒸馏的思路。我们实际上是在用这个 270 亿参数的模型处理所有这些token,最终目的是得到一个更小的模型。

如何复现 OpenAI o1

Lex:你们怎么看 OpenAI o1?Test time compute system 未来会将在代码任务中扮演什么角色?

Aman:Test time compute 提供了 scaling up 之外的一个不同的思路,即,我们也可以通过增加 inference 使用的 flops 来提升模型性能,通过更长时间的 inference 来实现和更大规模模型的同等质量的输出。

有一个很有意思的现实是,有些任务可能需要一个拥有 100 万亿参数、基于100 万亿 tokens 训练的超大模型才能解决,但这样的问题可能只占到 queries 的 1% 甚至 0.1%。那么,这种情况下愿意投入如此巨大资源、时间和算力去训练一个成本高昂但使用频率极低的模型是否还有意义呢?这样做感觉很浪费。

相比之下,一个更合理的策略是,训练一个规模较小的模型,这个模型能够高效地处理 99.9% 的查询,然后对于那些对智能水平极高的 query,可以选择通过更长的 inference time 来实现解决。

Lex:如何判断某个问题需要哪种程度的智能?能不能根据实际情况动态决定,比如什么时候使用GPT-4,什么时候使用小模型、什么时候又需要用到 o1?

Aman:这个问题还没有答案,我觉得目前还没有人能很好地解决这种 model routing 问题。我们在 Cursor Tab 上做过类似的初步尝试,但如果是 GPT-4o、Claude sonnet 和 o1 之间的切换可能要更加复杂。

这里还会涉及到个一个问题是,我们需要什么水平的模型来帮助判断某个任务是否超出了 GPT-4 水平模型的能力范围?可能需要一个 o1 水平的模型来作判断。这个问题目前也没有答案。

Lex:OpenAI 的做法是不直接给用户展示思维链,而是让模型对思维链进行总结。同时他们还在后台监控这些思维链,以确保模型不会尝试操控用户,你怎么看这种做法?

Michael:对于OpenAI来说的一个顾虑可能是他们不想让人们从他们的模型中提炼出这种能力。如果能够访问到那些被隐藏的思维链数据,复刻 o1 可能会变得更加容易,因为我们可以直接看到模型得到最终结果所采取的步骤,这些数据是非常重要的。

Lex:CoT 数据可以被用来直接训模型吗?

Michael:有一些类似情况可以参考,不过也只是推测。之前有些模型的 API 会提供所有生成 token 的对数概率(log probabilities)的访问权限,包括对 prompt tokens 的概率,不过后来一些模型 API 移除了这些功能。其中一个猜测就是:如果外界可以拿到对数概率数据,就像今天我们可以拿到被 OpenAI 隐藏的 CoT 数据一样,这些数据都能帮助提供更多信息,让人们能够把 SOTA 模型的能力提取出来,再蒸馏到自有模型中。

编译:海外独角兽
本文由人人都是产品经理作者【海外独角兽】,微信公众号:【海外独角兽】,原创/授权 发布于人人都是产品经理,未经许可,禁止转载。

题图来自Unsplash,基于 CC0 协议。

更多精彩内容,请关注人人都是产品经理微信公众号或下载App
评论
评论请登录
  1. 目前还没评论,等你发挥!