开发者逆向分析了 Cursor 的 LLM 交互层,揭示内部通信协议和优化策略。对理解 AI IDE 实现和性能优化有启发。
Cursor 底层究竟发生了什么?
我们在 Cursor 和 LLM 之间接入 TensorZero,以观察每个 token 的动向……并掌控这些 API 调用。
TensorZero 是一个开源框架,帮助工程师用下游反馈信号(例如生产指标、人工反馈、用户行为)来优化 LLM 应用。我们想到,能否在我们自己用得最频繁的 LLM 应用——Cursor——上使用 TensorZero,会不会很有意思?
借助我们在 Cursor 和 LLM 提供商之间的网关,我们可以观察正在发出的 LLM 调用,对单个推理运行评估,使用推理时优化,甚至可以对 Cursor 使用的提示词和模型进行实验和优化。
Cursor 针对其整体用户群体进行了优化——除了 .cursorrules,能否通过深入挖掘并根据个人用户进行定制来进一步改进?
能够根据你的个人使用模式,以经验驱动的方式对 Cursor 进行实验和优化,会是什么样的体验?
除了优化,Cursor 仍然是一个黑盒。能看到实际发送给模型的内容,不是很有意思吗?
几乎所有 LLM 优化、评估和实验技术都需要推理数据及其实际后果。在这篇文章中,我们将专注于前者,深入探讨我们如何将 TensorZero 设置为 Cursor 和它所调用的 LLM 之间的自托管代理。如果你想自己尝试,可以查看我们仓库中的示例。我们会在后续博文中讨论如何收集反馈并闭合优化循环。
首先我们注意到,Cursor 允许你覆盖 OpenAI 的基础 URL 和模型名称。完美。TensorZero 暴露了一个 OpenAI 兼容的推理端点,所以我们可以轻松配置 Cursor 调用 TensorZero 而不是 OpenAI。
在 TensorZero 配置中,我们定义了一个 TensorZero 函数 cursorzero,这样我们可以自动用不同的模型和提示词进行实验,同时在数据库中存储与提供商无关的推理和反馈数据,用于可观测性和优化。
Cursor 最初无法连接到本地运行的 TensorZero。事实证明,Cursor 首先向自己的服务器发送请求,在那里进行额外处理后才进行 LLM 调用,所以它无法连接到我们在 localhost 上的网关。(这也意味着你的凭证必须转发到 Cursor 的服务器,允许它们收集关于你的推理和代码库的数据。)
作为测试我们的计划是否可行的一种方式,我们将 Cursor 指向 OpenRouter,并意识到我们可以在 Cursor 的 Ask、Agent 和 Cmd+K 交互中使用它的模型。我们仍然能够使用普通的 Cursor Tab 补全,它依赖 Cursor 为内联补全提供的专有模型(它非常出色,所以我们很高兴能够保持它)。
解决方案是设置一个反向代理来暴露一个公共端点,它会将请求转发回我们的机器。我们使用 Ngrok 来保持简单。由于我们向公网暴露了一个带有 LLM 凭证的网关,我们在网关前面加了一层 Nginx 来认证请求。我们将 Cursor 的基础 URL 设置为我们的 Ngrok 端点,添加了我们在 Nginx 中设置的凭证,关闭了内置模型,最后在模型名称 tensorzero::function_name::cursorzero 下添加了我们的新 TensorZero 函数。
最终,工作流看起来像这样:Cursor → Ngrok → Nginx (自托管) → TensorZero (自托管) → LLM 提供商
认证过程失败了。
Nginx 日志显示有一个 OPTIONS 请求击中了我们的端点,所以我们配置了 Nginx 在 OPTIONS 请求时返回头,并逐步添加了我们在 OpenAI API 响应中看到的头。这是来自本地 Cursor IDE 的初始验证请求。CORS 要求可能来自 Electron。
初始验证后,所有请求都来自 Cursor 的服务器。
# --- CORS helper macro ---
set $CORS_ALLOW_ORIGIN $http_origin; # reflect the caller's origin
set $CORS_ALLOW_HEADERS "Authorization,Content-Type";
location / {
# --- pre-flight ---
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $CORS_ALLOW_ORIGIN always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Methods "GET,POST,OPTIONS" always;
add_header Access-Control-Allow-Headers $CORS_ALLOW_HEADERS always;
add_header Access-Control-Max-Age 86400 always;
return 204; # 204 (No Content) is conventional for pre-flight
}
}
你可以在 GitHub 上找到整个 "CursorZero" 代码库。
我们终于可以看到 Cursor 的所有进出流量——包括它的提示词。
例如,在问 Cursor "how do I get nginx to get the key from an environment variable in my .env?" 后,我们看到了这样的内容:
You are a an AI coding assistant, powered by tensorzero::function_name::cursorzero. You operate in Cursor
You are pair programming with a USER to solve their coding task. Each time the USER sends a message, we may automatically attach some information about their current state, such as what files they have open, where your cursor is, recently viewed files, edit history in their session so far, linter errors, and more. This information may or may not be relevant to the coding task, it is up for you to decide.
Your main goal is to follow the USER's instructions at each message, denoted by the <user_query> tag.
<communication>
When using markdown in assistant messages, use backticks to format file,
directory, function, and class names. Use ( and ) for inline math, [ and ]
for block math.
</communication>
<search_and_reading>
If you are unsure about the answer to the USER's request or how to satiate their request, you should gather more information. This can be done by asking the USER for more information.
Bias towards not asking the user for help if you can find the answer yourself.
</search_and_reading>
<making_code_changes>
The user is likely just asking questions and not looking for edits. Only suggest edits if you are certain that the user is looking for edits.
When the user is asking for edits to their code, please output a simplified version of the code block that highlights the changes necessary and adds comments to indicate where unchanged code has been skipped. For example:
```language:path/to/file
// ... existing code ...
{{ edit_1 }}
// ... existing code ...
{{ edit_2 }}
// ... existing code ...
The user can see the entire file, so they prefer to only read the updates to the code. Often this will mean that the start/end of the file will be skipped, but that's okay! Rewrite the entire file only if specifically requested. Always provide a brief explanation of the updates, unless the user specifically requests only the code.
These edit codeblocks are also read by a less intelligent language model, colloquially called the apply model, to update the file. To help specify the edit to the apply model, you will be very careful when generating the codeblock to not introduce ambiguity. You will specify all unchanged regions (code and comments) of the file with "// ... existing code ..." comment markers. This will ensure the apply model will not delete existing unchanged code or comments when editing the file. You will not mention the apply model. </making_code_changes>
<user_info> The user's OS version is darwin 24.3.0. The absolute path of the user's workspace is /Users/viraj/tensorzero/tensorzero/examples/cursorzero. The user's shell is /bin/zsh. </user_info>
You MUST use the following format when citing code regions or blocks:
// ... existing code ...
This is the ONLY acceptable format for code citations. The format is startLine:endLine:filepath where startLine and endLine are line numbers.
<additional_data> Below are some potentially helpful/relevant pieces of information for figuring out to respond
<current_file>
Path: nginx/nginx.conf
Line: 1
Line Content: events {}
</current_file>
<attached_files>
<file_contents>
events {}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
access_log /var/log/nginx/access.log main;
map $http_authorization $is_authorized {
default 0;
"~*^Bearer your-token-here$" 1;
}
server {
listen 80;
# --- CORS helper macro ---
set $CORS_ALLOW_ORIGIN "$http_origin"; # reflect the caller's origin
set $CORS_ALLOW_HEADERS "Authorization,Content-Type";
location / {
# --- pre-flight ---
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "$CORS_ALLOW_ORIGIN" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Methods "GET,POST,OPTIONS" always;
add_header Access-Control-Allow-Headers "$CORS_ALLOW_HEADERS" always;
add_header Access-Control-Max-Age 86400 always;
return 204; # 204 No Content is conventional for pre‑flight
}
# --- authenticated requests ---
if ($is_authorized = 0) {
return 401;
}
proxy_pass http://gateway:3000;
proxy_set_header Host $host;
# expose CORS headers on the real response too
add_header Access-Control-Allow-Origin "$CORS_ALLOW_ORIGIN" always;
add_header Access-Control-Allow-Credentials "true" always;
}
}
}
</file_contents>
<manually_added_selection>
events {}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
access_log /var/log/nginx/access.log main;
map $http_authorization $is_authorized {
default 0;
"~*^Bearer your-token-here$" 1;
}
server {
listen 80;
</manually_added_selection>
</attached_files> </additional_data>
<user_query> how do I get nginx to get the key from an environment variable in my .env? </user_query>
I see you're trying to use an environment variable for your authentication token in the Nginx configuration instead of hardcoding "your-token-here" in the nginx.conf file.
Unfortunately, Nginx doesn't directly read environment variables from a .env file. However, there are a few approaches you can take:
nginx.conf.template) with a placeholder:map $http_authorization $is_authorized {
default 0;
"~*^Bearer ${API_TOKEN}$" 1;
}
#!/bin/bash
# Load environment variables from .env
source .env
# Replace placeholder with actual token
envsubst '${API_TOKEN}$' < /path/to/nginx.conf.template > /path/to/nginx.conf
# Start Nginx
nginx -g 'daemon off;'
If you're running Nginx in Docker, you can pass environment variables to the container and use envsubst:
FROM nginx:alpine
COPY nginx.conf.template /etc/nginx/templates/default.conf.template
CMD ["/bin/sh", "-c", "envsubst '${API_TOKEN}$' < /etc/nginx/templates/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"]
Then run with:
docker run -e API_TOKEN=your-secret-token -p 80:80 your-nginx-image
Instead of hardcoding the token in Nginx, you could proxy authentication requests to a separate service that has access to environment variables:
location /auth {
proxy_pass http://auth-service:3001;
# This service can read environment variables
}
For your specific case, I'd recommend Option 1 or 2 depending on your deployment setup. This way, you can keep your token in your .env file and have it securely injected into the Nginx configuration at startup time.
### 提示词工程 Cursor
这个例子描绘了对最先进的 AI 编码助手进行提示词工程的样子。
这个提示词和其他类似的提示词(有不同的提示词用于提出问题、编辑源代码和在 shell 中工作)就是 Cursor 用来回答问题和生成代码的全部内容。我们觉得值得注意的是这里的内容并不如预期丰富,整个软件工程已经被当前一代 LLM 充分内化,以至于你可以用一个 642 token 的系统提示词就能得到 Cursor 背后的精髓。这一定归功于大型实验室的广泛后训练努力。
### 显式的 AI 层级?
我们发现以下代码片段特别有意思:
These edit codeblocks are also read by a less intelligent language model, colloquially called the apply model, to update the file. To help specify the edit to the apply model, you will [...]. You will not mention the apply model.
混合使用不同模型来优化成本和延迟是很常见的,但 Cursor 向模型自己解释这个层级?有意思。
有了 TensorZero,我们拥有了完整的 Cursor 体验,同时对可观测性和 LLM 请求实验的控制权。我们已经运行了多天繁重的软件工程工作的 CursorZero:它很稳定,没有明显的额外延迟。
我们目前运行着 Claude 4.0 Sonnet、GPT-4.1、o4 Mini 和 Gemini 2.5 Pro 的随机均等分割——感觉和 Cursor 一样好。
对分析你自己对 AI 编码助手的使用感兴趣?你可以在 GitHub 上找到说明来重现这项工作,使用 Cursor 和 OpenAI Codex。
TensorZero 帮助你理解——并优化——你的 LLM agent,即使你没有自己构建它们!
### 但等等……还有更多!
这篇博文展示了我们如何通过将 TensorZero 设置为自托管代理服务来成功逆向工程 Cursor 的 LLM 客户端,使我们能够观察、分析和实验不同的 LLM 模型,同时保持完整的 Cursor 体验。
在我们系列的下一篇文章中,我们将解释我们如何评估 AI 编码助手的实际使用,以及我们在模型之间进行的内部 A/B 测试的结果。抢先预览:git hooks、tree-sitter 等等。
然后,我们将探索这个反馈信号是否能够通过优化你个人使用模式的模型和推理来改进 Cursor。
敬请关注,订阅下方。