
大家好,我是 Rafael——每周我都会从一个构建 AI 系统的工程师视角,分享我最近遇到的有趣挑战和进展。
过去两年里,涌现了大量 AI 驱动的应用和 AI 功能。像 Slack 这样的老牌玩家也迅速在其产品阵容中加入了 AI 功能。而对于 Cursor、Notion、ChatGPT Desktop 和 Claude Desktop 这类 AI 原生产品来说,AI 从一开始就是它们存在的核心理由。
这些应用发布的 AI 功能越多,我就越想去探究它们的内部运作机制。希望能借此揭开一些引擎盖下的秘密;至少,我也能学到一两点桌面应用开发的知识。
巧合的是,我注意到自己每个月的 Copilot 额度消耗得越来越早。这最终促使我去挑选一个主要的实验对象。我决定深入钻研 VS Code 和 Copilot。
一个共同点:Electron
上述所有应用的共同点在于,它们都是使用 Electron 构建的。Electron 是一个 JavaScript 框架,帮助开发者构建和分发桌面应用程序。通俗地说,它的工作原理是将 Node.js 运行时与 HTML、CSS 和 JavaScript 产物打包在一起,然后通过 Chromium 进行渲染。

这消除了为不同平台维护多套原生语言代码库的需求(例如,Windows 用 C#,macOS 用 Swift),使开发者能够更轻松地从单一代码库构建跨平台运行的桌面应用程序。(原生模块和某些打包步骤通常仍需要针对每个平台单独处理,但大部分应用逻辑是共享的。)
因为它们共享 Electron,所以它们也共享大致相似的架构,这意味着我通过探查其中一个所学到的东西,应该也能应用到其他应用上。
Lighthouse Newsletter 是免费的 :) 如果把它分享给可能感兴趣的朋友和同事,就是对我最大的支持 👇
网络数据包,然后是源码
我的第一反应是直接浏览 VS Code 的源码,看看它能否回答我的问题。问题在于,我还没有一套完整的问题清单——要在数百万行代码中寻找它们,要么花费太多时间,要么消耗太多模型 token。
源码能告诉你一个应用能做什么;而发现它在运行时实际做了什么则更具挑战性。尤其是当你还不知道自己要找什么的时候。
还有第二个问题。VS Code 是我最初开始研究的那些应用中的一个例外:它的源码(至少是绝大部分)是开放的。但 Claude、ChatGPT、Codex、Notion 和 Slack 并非如此。
这促使我转向逆向工程的路线:先被动观察网络流量,让请求和响应告诉我哪些问题值得提出,然后再去源码中确认(或反驳)我所看到的内容。
这意味着我需要深入 Electron 的架构和网络栈——这些技能在之后也不会白费。
Electron 的网络架构
到目前为止,我们知道 Electron 应用内置了 Chromium。浏览器为应用基于 Web 的 UI 提供渲染引擎,同时也提供了一个网络栈,渲染进程可以用它来进行 HTTP 和 WebSocket 连接。
这是让应用与远程后端通信的一种常见(且推荐)方式,但并非唯一方式。应用还可以使用 Node 的 http/https/fetch 来发起 HTTP 请求。当你试图拦截请求时,请求走哪条路径就变得很重要了。
在某些情况下,比如 VS Code,应用会采用解耦架构,即有一组独立的进程充当扩展宿主。这有助于在职责之间保持清晰的边界;以 VS Code 为例,就是在 UI、代码 IDE 功能和插件/扩展之间保持清晰的边界。

检查 Electron 应用的网络流量
拦截应用网络流量的经典方法之一是搭建一个代理服务器,并配置该应用使用这个代理。
该代理充当中间人(MITM):它拦截来自客户端的 HTTP 请求,将其转发到服务器,并将服务器的响应回传给客户端。
有趣的事实:类似的方法在企业网络环境中相当常见,主要用于流量检查,尤其是在高度监管的行业中。恰如其分的是,用于此目的的主要开源工具之一名为 mitmproxy,我们将在接下来的步骤中使用它。
一个重要的细节是,如今大多数网络流量都通过安全的 HTTP(HTTPS)进行。这意味着流量使用 TLS 进行加密。
通过信任 mitmproxy 本地生成的证书颁发机构(CA),客户端可以接受 mitmproxy 为每个目标动态生成的证书。这样,你得到的不是一个端到端的加密连接,而是两个:一个在应用程序和 mitmproxy 之间,另一个在 mitmproxy 和目标服务器之间。
因此,mitmproxy 可以解密请求、检查它、在上游建立单独的 TLS 连接,并将响应转发回应用程序。
开始使用
如果你不想跟着代码操作,只想看结果,可以跳过本节。
安装 mitmproxy
在 macOS 上,最简单的方法是使用 brew:
brew install mitmproxy VS Code 配置
我们需要更改 VS Code 中的一些设置,以将其流量路由到 mitmproxy。你可以使用快捷键组合 Cmd+Shift+P 并搜索“用户设置”来更改这些设置。然后,你需要确保以下设置具有以下值:
Http 代理:http://localhost:8080(mitmproxy 将在此端口监听连接)
Http 代理严格 SSL:取消勾选(我们希望跳过对 mitmproxy 证书与 CA 列表的验证)
Http:代理支持:覆盖(强制扩展使用代理支持)

进行这些更改后,请务必重启 VS Code。
mitm Web UI
开始之前的最后一步是启动 mitmproxy 的 Web UI:
mitmweb 给它几秒钟,你应该会开始看到来自 VS Code 的一些网络流量流过它。
你会注意到顶部有一些文本字段。目前可以先忽略其中大部分;最有用的是第一个字段“搜索”。该字段提供强大的搜索功能,如关键词搜索、正则表达式等。例如,如果我们特别关注 VS Code 向其扩展市场 API 发出的请求,只需使用 `marketplace` 作为过滤字符串即可。
这将匹配所有发送至 https://marketplace.visualstudio.com 及其所有子路径的请求。

过期的扩展宿主进程
即使经过上述所有操作,你的代理可能仍然无法捕获扩展流量。如果 VS Code 的扩展宿主进程组变得过期,就可能发生这种情况。要确认这一点,请在终端中运行:
ps -eo pid,ppid,lstart,command | grep -i -E "copilot|extensionHost|Code Helper"
# Should display something like this:
27896 27243 Fri Jul 24 15:40:11 2026. \
/Applications/Visual Studio Code.app/Contents/Frameworks/ # (...) 确认显示的日期。如果该日期和时间与你重启 VS Code 的时间不一致,则扩展宿主进程很可能已过期。解决方法很简单:
在 VSCode 中,打开命令面板(Cmd+Shift+P)
运行“开发人员:重启扩展宿主”
之后重新运行你的 `ps grep` 命令——你现在应该会看到带有今天时间戳的 Code Helper 的新 PID。
在你输入任何内容之前,Copilot 在做什么
快速浏览 mitmweb 中来自 VS Code 的网络请求,你会注意到其中大多数与 GitHub 或 Github Copilot 相关。在我们于 VS Code 或 Copilot 扩展中按下任何一个键之前,就已经发出了一些 HTTP 请求。
高层分析
VS Code 和 Copilot 在引导阶段发出的请求可分为以下几类:认证与会话、配置与策略、MCP 注册表、仓库与会话上下文、模型发现以及最近仓库。
在接下来的段落中,我们将讨论我对每一类请求的发现:请求和响应的头部及负载中包含哪些内容。

认证与会话引导
这是 Copilot 在启动时做的第一件事。它获取一个 OAuth token,将其兑换为短期有效的 token,并验证用户的权限资格。该流程是一个相当标准的 OAuth 流程;如下图所示。

模型与能力发现
在发出任何大语言模型请求之前,Copilot 会检查你的账户/套餐可用的模型和智能体能力有哪些。
这里有两种不同类型的请求。首先,会向 /models 发出一个请求。这个初始请求会返回 Copilot 中可用模型的一般列表。
然后,会向 /agents/swe/models 发出第二个请求。这是一个特定请求,用于查明哪些模型可用于与软件工程(SWE)相关的智能体能力。

关于提示词、上下文和测试框架的一些细节
引导阶段之后才是真正有趣的部分。
Copilot 的模型路由器
我在本次实验的所有 Copilot 测试中都选择了自动模式。在我发送每条消息后,我都能在任何模型回答之前捕获到一个发往 /models/session/intent 端点的请求。
这里发生的事情是:你的提示词会根据可能的意图进行评分,例如代码生成、调试、推理和工具使用。意图分类的结果帮助 Copilot 决定哪些可用模型将完成该任务。

这其实不是什么秘密;这种行为在 Copilot 的文档中已有描述。不过,亲眼看到其背后的实际请求和响应还是很有趣的。
(秘密的)环境变量
我开始摆弄行内补全和幽灵文本,观察通过 HTTP 发送的内容。我早就知道行内补全会把当前文件作为上下文注入到提示词中;这本来就是它的工作方式。所以到目前为止并不意外。
但我仍然想知道还有什么其他内容被发送出去,所以我做了一个小测试。我在一个 .env 文件中放了一个假秘密——就是那个我们所有人都被告知不要提交、但有些人还是会提交的臭名昭著的文件。
TEST_ENV_VAR_SECRET=”a realistic looking fake token” 编辑这个文件没有触发任何 HTTP 请求,我觉得这很好。然后我打开了一个完全不相关的 pyproject.toml 文件,并开始在其中输入内容。
结果你猜怎么着,就在我操作的时候,下面这个补全请求发出去了:
{
"prompt":"TEST_ENV_VAR_SECRET=\"mysecretenvvar\"\n\nT",
"suffix":"",
"max_tokens":500,
"temperature":0,
"top_p":1,
"n":1,
"stop":["\n\n\n","\n```"],
"stream":true,
"extra":{
"language":"dotenv",
"next_indent":0,
"trim_by_indentation":true,
"prompt_tokens":175,
"suffix_tokens":0,
"context":[
"Path: .env",
"These are recently edited files. Do not suggest code that has been deleted.\nFile: pyproject.toml\n--- a/file:///Users/rafaelpierre/copilot-mitm/pyproject.toml\n+++ b/file:///Users/rafaelpierre/copilot-mitm/pyproject.toml\n@@ -18,4 +18,4 @@\n \"polars>=1.41.0\",\n ]\n \n+# testing\n- --- IGNORE ---\nFile: config.ini\n--- a/file:///Users/rafaelpierre/copilot-mitm/config.ini\n+++ b/file:///Users/rafaelpierre/copilot-mitm/config.ini\n@@ -1,2 +1,3 @@\n TEST_CONFIG=\"test-config\"\n \n+# test .env\nEnd of recent edits"
]
},
"code_annotations":false
} 我第一反应是:行吧,那我干脆把 Copilot 对 .env 文件的补全关掉算了。结果发现它其实早就被关掉了;是我自己忘了这回事。
不过关不关都无所谓;这个请求是从 pyproject.toml 文件里的按键操作触发的,而那个文件的内联补全功能一直是开着的。
心里记一笔:把 .env 本身或任何其他“敏感”扩展名的内联补全关掉根本没用,因为请求并不是由它触发的。但其他请求确实可能被触发。
让 Copilot 帮我回忆一下
在我拦截到的许多补全请求的系统提示词里,我都看到过一个 session_store_sql 工具的定义。下面是从其中一个请求里拿到的工具描述:
Query the local session store containing history from past coding sessions.
Uses SQLite syntax (NOT DuckDB or Postgres).
SQL queries are read-only — only SELECT and WITH are allowed.
Use `datetime('now', '-1 day')` for date math (NOT `now() - INTERVAL '1 day'`), FTS5 `MATCH` for text search.
Tables: `sessions`, `turns`, `session_files`, `session_refs`, `checkpoints`, `search_index`.
For column details and query patterns, use the **chronicle** skill.
Actions: 'query' (execute SQL ‚Äî supports JOINs, FTS5 MATCH, aggregations), 'reindex' (rebuild index from debug logs). 不过,在那之后我没看到有任何工具调用结果被传回来。这个工具大概并没有被真正调用。为了确认一下,我继续在聊天里问了一个简单的问题来试着强制触发一次工具调用:“我这周都干了些什么?”
接下来就是模型和一个本地 SQLite 数据库之间的一来一回,这个数据库叫 session-store.db,我之前根本不知道它存在:

通过查看这些对话,我了解到 session_store_sql 是 Copilot 的 Chronicle 工具的一部分,它让 Copilot 能对 session-store.db 执行 SQL 查询。这个数据库存有会话摘要、你处理过的代码仓库和分支。
它还存着你所有的提示词,以及对应的 LLM 响应。Copilot 正在保存一份关于你问过它的一切的可查询历史记录,并在需要的时候去翻查这份历史。
有一点很显眼:模型事先并不知道数据库的表结构。它一开始尝试了下面这个查询,结果失败了。
# Tool definition gets sent
{
"type":"function_call",
"name":"session_store_sql",
"arguments":"{
\"action\":\"query\",
\"description\":\"Fetch recent session activity for the past week\",
\"query\":\"SELECT s.id, s.start_time, s.title, t.turn_index, t.role, t.content FROM sessions s JOIN turns t ON t.session_id = s.id WHERE s.start_time >= datetime('now', '-7 days') ORDER BY s.start_time, t.turn_index;\"
}",
"call_id":"call_Ay35CDeV0EFXtvFI8l3VgbWI"
}
# Tool gets executed locally, results are sent back to the agent/LLM:
{
"type":"function_call_output",
"call_id":"call_Ay35CDeV0EFXtvFI8l3VgbWI",
"output":"Error: no such column: s.start_time"
} 然后它去查看了表结构的元数据,找到了表定义。在那之后,它终于能从我的本地 SQLite 数据库里取到一些记录了。
# Session Store SQLite DB introspection tool call
{
"type":"function_call",
"name":"session_store_sql",
"arguments":"{
\"action\":\"query\",
\"description\":\"Inspect session store schema\",
\"query\":\"
SELECT name, sql
FROM sqlite_schema
WHERE type IN ('table','view');
\"
}",
"call_id":"call_wY9dGEI4DSYbOXzpzPg3JTGN"
}
# Introspection tool call results get sent back to agent/LLM:
{
"type":"function_call_output",
"call_id":"call_wY9dGEI4DSYbOXzpzPg3JTGN",
"output":"Results: 13 rows (source: local)
| name | sql |
| --- | --- |
| schema_version | CREATE TABLE schema_version (\n\t\t\t\tversion INTEGER NOT NULL (...)\
",
} 后来我逐渐好奇起来,想查一查我的会话数据,看看里面还存了些什么。于是我先从元数据入手。
$ sqlite3 ~/Library/Application Support/Code/User/globalStorage/github.copilot-chat/session-store.db
# Output
CREATE TABLE turns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id),
turn_index INTEGER NOT NULL,
user_message TEXT,
assistant_response TEXT,
timestamp TEXT DEFAULT (...),
UNIQUE(session_id, turn_index)
); 如你所见,user_message 和 assistant_response 都是以明文存储的。我们手动查一下其中一些数据。
$ sqlite3 session-store.db "SELECT substr(user_message,1,60) FROM turns LIMIT 5;"
What is ML?
hello
testing 这些是我之前发给 Copilot 用来测试 mitmproxy 抓包的消息,所以同样没什么意外。但那些可能包含更……敏感内容的消息呢?
为了弄清楚这一点,我给 Copilot 发了一条包含伪造机密数据的聊天消息:一个伪造的 GitHub token、一个伪造的 AWS key,以及一个带密码的连接字符串。
然后,我回到数据库里查看写入的内容。
$ sqlite3 session-store.db "SELECT user_message FROM turns \
WHERE user_message LIKE '%ghp_%' OR user_message LIKE '%postgres://%';"
...
GITHUB_TOKEN=ghp_«fake token, stored exactly as typed»
DATABASE_URL=postgres://admin:«password»@db.example.com:5432/prod
... 我承认,我一度很想用“全部明文存储”作为标题。
但尽管事实确实如此,我的结论其实没那么令人担忧——而且我认为反而更有意思:AI 编程工具正在变成有状态系统。
AI 编程工具正在变成有状态系统。它们越来越多地把用户工作区 + 近期编辑 + 对话 + 工具 + 历史记录 + 模型路由整合在一起。
每一个新的上下文来源都能提升实用性,同时也会增加系统可访问的开发状态量。但这也带来了额外的挑战:上下文不断膨胀、数据保密性和隐私问题。
虽然我很享受这次逆向工程的练习,但我也很好奇我的假设是否真的有实际代码支撑。为了确认这一点,我需要去看代码。
将这些发现与源代码进行核对
未加密的会话存储
会话存储代码属于 Chronicle 扩展的一部分,位于 sessionStore.ts 中。表定义和我磁盘上看到的一模一样:user_message 和 assistant_response 都是明文,没有任何列级脱敏之类的处理。
但 schema 本身并不能告诉你写入路径上是否有东西对数据做了清洗。写入路径才能说明问题。下面是记录每一轮对话的 insert 语句:
INSERT INTO turns (session_id, turn_index, user_message, assistant_response, timestamp)
VALUES (?, ?, ?, ?, ?) ……以及绑定到它的值:
turn.session_id,
turn.turn_index,
turn.user_message ?? null,
turn.assistant_response ?? null,
turn.timestamp ?? new Date().toISOString(), `turn.user_message` 按原样进入。我在代码中搜索了写入路径中是否有任何编辑、清理、密钥过滤或掩码处理。什么都没有,没有清洗步骤。明文存储不是 bug 或遗漏的边界情况;它只是代码的实际行为。
这回答了第一个问题:这是故意的,因为从未构建任何东西来阻止它。
泄露还是不泄露
我在 mitm 抓包中看到的“最近编辑的文件”字符串来自 recentEdits.tsx。默认的滑动窗口行为是硬编码的:最多 20 个文件、8 条编辑摘要,以及每次更改周围的 3 行上下文,这就是我从未碰过的一行(包含假密钥的那行)如何成为发送到 Copilot API 的 HTTP 请求的一部分。
任何地方都没有默认的 .env 规则。在个人方案中,没有任何东西将 .env 视为特殊文件,也没有与当前空间的 .gitignore 集成。
有一个排除门控,但它与“仓库策略”绑定,这是 Business/Enterprise 版 GitHub 功能,由管理员控制。
结语
能力越大,责任越大
这最终成为一次很好的练习,让我理解了 AI 编程工具如何实现其框架。我相信很多这些细节和实践可以被构建自己 AI 系统的不同团队所吸收。
在构建此类系统时我经常问自己的一些问题仍然存在。应该注入什么上下文?应该向模型发送什么?什么应该留在本地?模型应该能够调用哪些工具?什么存储在短期记忆中?什么被提升到长期记忆?
上下文正在成为产品
我越来越认为,上下文正在成为产品。
模型和 SOTA 基准结果当然很重要。但 AI 编程工具之间——以及更广泛的 AI 工具之间——真正的差异化似乎正在转向它们如何组装正确的上下文:你的代码、最近的更改、操作、对话、工具、历史,以及任何其他可能有助于解决当前任务的内容。这带来了两个挑战。
第一个是工程问题:上下文更多并不必然意味着上下文更好。难点在于让上下文保持精简、相关且利于缓存,同时避免模型被提示词膨胀所淹没。
第二个问题关乎隐私与保密。编排层收集和持久化上下文数据越多,就越需要谨慎地界定哪些内容可以跨越边界——无论是文件之间、会话之间、机器之间,还是最终到达模型 API 的边界。
Copilot 显然正在朝这个方向迈进,我发现的其中一些做法颇为巧妙,也有一些让我感到不适。而到目前为止,没有任何一点能说服我重新成为付费用户。
但它确实让我明白了另一件事:如果你在构建 AI 应用,逆向工程并研究模型外围的编排层,可能比研究模型本身能让你学到更多。
希望你喜欢这篇文章。如果你有任何疑问、产生了共鸣,或者有建议,欢迎回复这封邮件或留下评论——每一条我都会认真阅读。