持续改进我们的智能体框架
我们构建 Cursor 智能体框架的方式,和构建任何有野心的软件产品并无不同。很大一部分工作由愿景驱动:我们先对理想的智能体体验应该是什么样形成判断。
然后,我们围绕如何更接近这一愿景提出假设,通过实验验证这些假设,并根据评估和真实用量中得到的定量与定性信号持续迭代。这个过程离不开合适的在线和离线观测机制,这样我们才能判断某项改动是否真的让框架变得更好。
当我们拿到新模型的 Early Access 时,这些方法就会汇聚到一起。我们会花上几周时间,围绕模型的优势和怪癖定制框架,直到同一个模型在我们专门调优过的框架中,明显变得更快、更聪明、更高效。
有时我们会发现带来跃迁式提升的改进。但更多时候,改进框架靠的是近乎执着地叠加一个个小优化,而这些优化合在一起,能让智能体更擅长构建软件。
上下文窗口的演进
与大语言模型交互的核心在于上下文窗口。当请求智能体构建某个内容时,上下文窗口会先包含系统提示和工具描述,接着是当前对话状态,最后是用户请求。
在 Cursor 的发展过程中,我们填充和管理这个窗口的方式已经发生了显著变化。
当我们在 2024 年末首次开发编程智能体时,模型自行选择上下文的能力还很弱,因此我们在上下文工程上投入了大量精力来设置护栏——例如,每次编辑后都向智能体提供 lint 和类型错误信息;如果它请求读取的行数过少,就改写它的文件读取请求;甚至限制它单轮最多可调用的工具数量。
我们还提供了大量静态上下文,这些内容在每个会话开始时都会预先提供给智能体。在不同阶段,这些内容包括代码库的文件夹布局、与查询语义匹配的代码片段,以及用户手动附加文件的压缩版本。
如今,这些做法大多已经淡出了。
我们仍会提供一些实用的静态上下文 (例如操作系统、git 状态、当前和最近查看的文件) 。但随着模型能力不断提升,我们也在调整策略,减少护栏,转而提供更多动态上下文,由智能体在工作过程中按需获取。在更早的一篇文章中,我们曾对动态上下文背后的一些技术做过深入解析,其中许多后来也被其他编程智能体采用。我们现在的大量工作,则聚焦于为智能体提供更多动态拉取上下文并与外部世界交互的方式。


评估框架变更的两种方式
框架和模型共同决定了智能体的表现,但“好”很难准确界定。为此,我们构建了多层衡量体系。
除了我们自己的评估套件外,我们还维护公开基准测试 CursorBench。它能让我们快速、标准化地了解质量水平,并支持跨时间对比。但即使是最好的基准测试,也只能近似反映真实使用情况,这意味着如果完全依赖它们,我们就会错过重要信号。
因此,我们也会进行在线实验,同时部署两个或更多框架变体,并在真实使用中对它们进行 A/B 测试。我们通过多种指标来衡量这些测试中的智能体质量。有些指标比较直接,例如延迟、token 效率、工具调用次数和缓存命中率。这些指标对判断趋势很有用,但仍然无法回答那些更模糊却更重要的问题:智能体是否真的把事情做好了。我们通过两种方式来衡量这些问题。
第一种是智能体生成代码的“保留率” (Keep Rate) 。对于智能体提出的一组代码变更,我们会跟踪其中有多少比例在固定时间间隔后仍然保留在用户的代码库中。这让我们能够了解用户何时需要手动调整智能体输出,或继续迭代并让智能体修复问题,而这都说明智能体的初始响应质量较低。
第二,我们会使用语言模型读取用户对智能体初始输出的回应,从语义层面判断用户是否满意。用户继续进入下一个功能,是智能体完成任务的强信号;而用户粘贴一段堆栈跟踪,则是它没有完成任务的可靠信号。
有时,这些在线测试会让我们搁置一些看起来很有前景的想法。在一项实验中,我们尝试了一个成本更高的模型来做上下文摘要,结果发现它对智能体质量的改善微乎其微,不值得付出更高成本。
跟踪并修复性能退化
随着我们加入更多模型和功能,框架 会像任何软件系统一样变得更复杂,潜在状态也更多。随之而来的是更多可能出现缺陷的环节,其中很多只有在大规模运行时才能被发现。
智能体使用的工具是最容易出现缺陷的界面之一,而工具调用错误可能会对 Cursor 中的一次会话造成极大影响。虽然智能体通常能够自行纠正,但错误仍会留在上下文中,浪费 token,并导致“上下文腐坏”——也就是累积的错误会降低模型后续决策的质量。
有时,一次失败的工具调用会让智能体卡住,或者完全失控。尽管工具调用量和错误率这类指标并不能直接衡量智能体是否完成得好,但它们可以作为信号,帮助我们发现更广泛的问题。
任何未知错误都意味着框架 中存在缺陷,我们也会按缺陷来处理。但很多错误是“预期内”的,例如模型偶尔提出错误的编辑,或尝试读取一个不存在的文件。我们会按成因对这些预期错误进行分类。InvalidArguments 和 UnexpectedEnvironment 用于归类模型出错以及上下文窗口中的矛盾,而 ProviderError 用于归类 GenerateImage 或 WebSearch 等工具提供方的服务中断。
我们还有其他几种分类,例如 UserAborted 和 Timeout,这些合起来覆盖了大多数预期错误。


我们基于这些指标定义告警,以捕捉进入生产环境的重大回归。由于未知错误始终都是缺陷,所以只要任何工具的未知错误率超过固定阈值,我们就会触发告警。但要判断预期错误究竟代表框架 中的缺陷,还是属于预期行为,往往并不容易。
比如,一次 grep 搜索超时,可能是工具存在性能问题,也可能只是 代码库 太大,而模型构造了一个低效的查询。为了解决这个问题,我们设置了异常检测告警:当预期错误显著高于基线时就会触发。我们会按每个工具、每个模型分别计算基线,因为不同模型在工具调用上的出错率可能不同。
我们还会每周运行一个配备专门技能的 自动化,教模型如何搜索我们的日志,找出新出现或近期激增的问题,并在待办列表中创建或更新附带调查信息的工单。我们大量依赖云端智能体,同时推动许多问题的修复,甚至还能直接从 Linear 触发它们。
这个流程也是我们为智能体框架 打造自动化“软件工厂”的一部分。在今年早些时候一次集中的冲刺中,我们将意外工具调用错误降低了一个数量级。
为不同模型定制框架
我们的所有框架抽象都不依赖具体模型,并且可以针对我们支持的每个模型进行深度定制。比如,OpenAI 的模型经过训练,会使用基于 patch 的格式来编辑文件,而 Anthropic 的模型则习惯于字符串替换。两类模型其实都能使用这两种工具,但如果给它们不熟悉的那一种,就会额外消耗 reasoning token,并产生更多错误。因此,在我们的框架中,我们会为每个模型配置它在训练时所使用的工具格式。
这种定制非常深入,包括针对不同提供商,甚至不同模型版本的自定义提示。OpenAI 的模型在遵循指令时通常更偏字面理解,也更精确;而 Claude 则更偏直觉,对不够精确的指令容忍度也更高。
当我们在正式发布前提前拿到某个新模型的 Early Access 时,我们会从与它最接近的现有模型框架入手,然后开始反复迭代。我们会运行离线评估,找出模型容易出错或困惑的地方,让团队成员实际使用它并反馈问题,再据此调整框架。我们会这样不断迭代,直到得到一个我们有信心发布的模型-框架组合。
这个调优过程很大一部分是在根据新模型的优势来定制框架,但有时我们也会遇到一些模型本身的怪癖,而这些问题可以通过框架来缓解。比如,我们观察到某个模型出现了我们后来称为“上下文焦虑”的问题:随着它的上下文窗口逐渐被填满,它会开始拒绝执行任务,并犹豫地表示这个任务看起来太大了。我们通过调整提示,成功减轻了这种行为。
支持在聊天中途切换模型
要设计好框架以支持用户在对话中途切换模型尤其棘手,因为不同模型的行为、提示和工具接口形式各不相同。
当用户切换模型时,Cursor 会自动切换到相应的框架,并使用该模型定制的一组提示和工具。不过,模型仍然需要把这些工具应用到一段由其他模型生成的对话历史上,而这类历史内容并不符合它训练时接触的数据分布。
为了解决这个问题,我们会添加自定义指令,告诉模型它是在聊天中途从另一个模型手中接管对话。这些指令也会引导它避免调用那些虽然出现在对话历史中、却不属于它自身工具集的工具。


第二个挑战是,缓存是提供商和模型特定的,因此切换会导致缓存未命中,使第一轮响应更慢、成本更高。我们通过在切换时总结对话来缓解这个问题,这会为模型提供一份清晰的摘要,从而减轻缓存失效带来的影响。但如果用户已经深入处理一个复杂任务,摘要可能会丢失重要细节,这也是为什么我们通常建议,除非您有明确的切换理由,否则尽量在整个对话过程中保持使用同一个模型。
另一种绕开对话中途切换模型所带来挑战的方法,是改用子智能体,因为它会从一个全新的上下文窗口开始。我们最近还为框架添加了一项能力,让用户可以直接请求用特定模型运行一个子智能体。
框架与软件开发的未来
AI 辅助的软件工程将迈向多智能体模式。系统不再把每个子任务都交给单一智能体处理,而是学会在专业化的智能体和子智能体之间进行委派:一个负责规划,另一个负责快速编辑,第三个负责调试,各自专注于自己最擅长的工作。
要把这件事做好,归根结底是框架的问题。系统需要知道该调度哪个智能体,如何根据该智能体的优势来描述任务,以及如何将结果整合成连贯的工作流。这种协同编排能力将体现在框架中,而非任何单个智能体身上。这也意味着,尽管框架工程一直都是智能体成功的关键,未来它只会变得更加重要。