发布时间:2026 年 1 月 30 日
在构建性能方面的 AI 助理时,主要的工程挑战是让 Gemini 能够顺利处理在开发者工具中记录的性能轨迹。
大语言模型 (LLM) 在“上下文窗口”内运行,这意味着它们一次性可处理的信息量存在严格限制。此容量以 token 为单位。对于 Gemini 模型,一个 token 大致相当于一组 4 个字符。
性能轨迹是巨大的 JSON 文件,通常包含数兆字节的数据。发送原始轨迹会立即耗尽模型的上下文窗口,从而无法提出问题。
为了实现 AI 辅助功能,我们必须设计一个系统,以尽可能少的令牌用量为 LLM 提供尽可能多的有用数据。在这篇博文中,您可以了解我们使用的技术,并将其应用于您自己的项目。
调整初始上下文
调试网站性能是一项复杂的任务。开发者可以查看完整轨迹以了解上下文,也可以专注于核心网页指标和轨迹的相关时间跨度,甚至可以深入了解细节,专注于点击或滚动等单个事件及其相关调用堆栈。
为了帮助进行调试,开发者工具的 AI 助理需要与这些开发者历程相匹配,并且仅使用相关数据来提供针对开发者关注点的建议。因此,我们没有始终发送完整轨迹,而是构建了 AI 辅助功能,可根据您的调试任务对数据进行切分:
| 调试任务 | 最初发送给 AI 助理的数据 |
|---|---|
| 就性能跟踪记录进行对话 | 轨迹摘要:一种基于文本的报告,其中包含轨迹和调试会话的概要信息。包括网页网址、节流条件、关键效果指标(LCP、INP、CLS)、可用数据分析列表,以及 CrUX 摘要(如有)。 |
| 就效果分析进行对话 | 轨迹摘要,以及所选性能洞见的名称。 |
| 通过轨迹聊天讨论任务 | 轨迹摘要,以及所选任务所在的序列化调用树。 |
| 就网络请求进行聊天 | 轨迹摘要,以及所选的请求键和时间戳 |
| 生成轨迹注释 | 所选任务所在的序列化调用树。序列化树用于标识所选任务。 |
轨迹摘要几乎总是会发送给 AI 辅助功能的底层模型 Gemini,以便为其提供初始背景信息。对于 AI 生成的注释,则会省略。
为 AI 提供工具
开发者工具中的 AI 辅助功能充当代理。这意味着,它能够根据开发者的初始提示和与其分享的初始上下文自主查询更多数据。为了查询更多数据,我们为 AI 助理提供了一组它可以调用的预定义函数。一种称为“函数调用”或“工具使用”的模式。
根据前面概述的调试历程,我们为代理定义了一组精细的功能。这些函数会深入分析根据初始上下文被认为重要的具体细节,类似于人类开发者进行性能调试的方式。函数集如下所示:
| 函数 | 说明 |
|---|---|
getInsightDetails(name) |
返回有关特定性能洞见的详细信息(例如,LCP 被标记的原因的详细信息)。 |
getEventByKey(key) |
返回单个特定事件的详细属性。 |
getMainThreadTrackSummary(start, end) |
返回指定边界的主线程活动的摘要,包括自上而下、自下而上和第三方摘要。 |
getNetworkTrackSummary(start, end) |
返回指定时间范围内的网络活动摘要。 |
getDetailedCallTree(event_key) |
返回性能轨迹中特定主线程事件的完整调用树 |
getFunctionCode(url, line, col) |
返回资源中在特定位置定义的函数的源代码,并使用性能轨迹中的运行时性能数据进行注释 |
getResourceContent(url) |
返回网页使用的文本资源(例如 HTML 或 CSS)的内容。 |
通过严格限制仅通过这些函数调用来检索数据,我们确保只有相关信息以明确定义的格式进入上下文窗口,从而优化令牌使用情况。
代理操作示例
我们来看一个实际示例,了解 AI 助理如何使用函数调用来检索更多信息。在最初提示“为什么此请求速度缓慢?”之后,AI 辅助功能可以逐步调用以下函数:
getEventByKey:获取用户选择的特定请求的详细时间分解(TTFB、下载时间)。getMainThreadTrackSummary:检查在请求本应开始时,主线程是否处于繁忙(阻塞)状态。getNetworkTrackSummary:分析是否有其他资源同时争用带宽。getInsightDetails:检查轨迹摘要是否已提及与此请求相关的瓶颈洞见。
通过合并这些调用的结果,AI 助理可以提供诊断结果并提出可行的步骤,例如建议使用 getFunctionCode 改进代码或根据 getResourceContent 优化资源加载。
不过,检索相关数据只是挑战的一半。即使函数提供的是精细数据,这些函数返回的数据也可能非常庞大。再举一个例子,getDetailedCallTree 可以返回一个包含数百个节点的树。在标准 JSON 中,仅用于嵌套的 { 和 } 就有很多!
因此,我们需要一种格式,既要足够密集以节省 token,又要足够结构化,以便 LLM 理解和参考。
序列化数据
让我们继续使用调用树示例,深入了解我们如何应对这一挑战,因为调用树构成了性能轨迹中的大部分数据。为供参考,以下示例展示了 JSON 中调用堆栈中的单个任务:
{
"id": 2,
"name": "animate",
"selected": true,
"duration": 150,
"selfTime": 20,
"children": [3, 5, 6, 7, 10, 11, 12]
}
一个性能轨迹可以包含数千个此类事件,如以下屏幕截图所示。每个彩色小框都使用此对象结构表示。

此格式非常适合在开发者工具中以编程方式使用,但对于 LLM 而言却很浪费,原因如下:
- 冗余键:
"duration"、"selfTime"和"children"等字符串在调用树中的每个节点上都会重复出现。因此,如果将包含 500 个节点的树发送给模型,则每次使用这些键都会消耗令牌,总共消耗 500 次。 - 详细列表:通过
children单独列出每个子 ID 会消耗大量令牌,尤其是对于会触发许多下游事件的任务。
为所有与 AI 辅助效果功能搭配使用的数据实现节省令牌的格式是一个循序渐进的过程。
首次迭代
在开始开发性能方面的 AI 辅助功能时,我们优先考虑的是发布速度。我们采用的令牌优化方法非常简单,即从原始 JSON 中去除大括号和逗号,从而生成如下格式:
allUrls = [...]
Node: 1 - update
Selected: false
Duration: 200
Self Time: 50
Children:
2 - animate
Node: 2 - animate
Selected: true
Duration: 150
Self Time: 20
URL: 0
Children:
3 - calculatePosition
5 - applyStyles
6 - applyStyles
7 - calculateLayout
10 - applyStyles
11 - applyStyles
12 - applyStyles
Node: 3 - calculatePosition
Selected: false
Duration: 15
Self Time: 2
URL: 0
Children:
4 - getBoundingClientRect
...
但第一个版本仅比原始 JSON 略有改进。它仍然明确列出了具有 ID 和名称的节点子级,并在每行前面添加了描述性重复键(Node:、Selected:、Duration:…)。
优化子节点列表
为了进一步优化,我们移除了节点子级的名称(上例中的 calculatePosition、applyStyles 等)。由于 AI 助理可以通过函数调用访问所有节点,并且此信息已位于节点头 (Node: 3 - calculatePosition) 中,因此无需重复此信息。这样一来,我们就可以将 Children 简化为简单的整数列表:
Node: 2 - animate
Selected: true
Duration: 150
Self Time: 20
URL: 0
Children: 3, 5, 6, 7, 10, 11, 12
..
虽然这比之前有了显著改进,但仍有进一步优化的空间。查看上一个示例时,您可能会注意到 Children 几乎是按顺序排列的,只缺少 4、8 和 9。
原因是,在第一次尝试中,我们使用深度优先搜索 (DFS) 算法来序列化性能轨迹中的树数据。这导致同级节点的 ID 不连续,我们需要单独列出每个 ID。
我们发现,如果使用广度优先搜索 (BFS) 重新为树编制索引,我们将获得连续的 ID,从而可以进行另一项优化。现在,我们无需列出各个 ID,只需使用单个紧凑范围(例如原始示例中的 3-9)即可表示数百个子项。
优化后的最终节点表示法(包含 Children 列表)如下所示:
allUrls = [...]
Node: 2 - animate
Selected: true
Duration: 150
Self Time: 20
URL: 0
Children: 3-9
减少键的数量
优化了节点列表后,我们开始处理冗余键。我们首先剥离了之前格式的所有键,得到了以下结果:
allUrls = [...]
2;animate;150;20;0;3-10
虽然这种方式确实节省了 token,但我们仍然需要向 Gemini 提供有关如何理解这些数据的指令。因此,我们首次向 Gemini 发送调用树时,添加了以下提示:
...
Each call frame is presented in the following format:
'id;name;duration;selfTime;urlIndex;childRange;[S]'
Key definitions:
* id: A unique numerical identifier for the call frame.
* name: A concise string describing the call frame (e.g., 'Evaluate Script', 'render', 'fetchData').
* duration: The total execution time of the call frame, including its children.
* selfTime: The time spent directly within the call frame, excluding its children's execution.
* urlIndex: Index referencing the "All URLs" list. Empty if no specific script URL is associated.
* childRange: Specifies the direct children of this node using their IDs. If empty ('' or 'S' at the end), the node has no children. If a single number (e.g., '4'), the node has one child with that ID. If in the format 'firstId-lastId' (e.g., '4-5'), it indicates a consecutive range of child IDs from 'firstId' to 'lastId', inclusive.
* S: **Optional marker.** The letter 'S' appears at the end of the line **only** for the single call frame selected by the user.
....
虽然这种格式说明会产生 token 费用,但这是一项静态费用,在整个对话中只需支付一次。通过之前的优化节省的费用超过了该费用。
总结
使用 AI 构建内容时,优化令牌用量是一项至关重要的考虑因素。通过从原始 JSON 转换为专门的自定义格式、使用广度优先搜索重新为树编制索引,以及使用工具调用按需提取数据,我们显著减少了 Chrome 开发者工具中的 AI 辅助功能所消耗的令牌数量。
这些优化是为性能轨迹启用 AI 辅助功能的先决条件。由于其上下文窗口有限,否则无法处理如此庞大的数据量。但优化后的格式可让性能代理能够保留更长的对话历史记录,并提供更准确、更具情境意识的回答,而不会被噪声所干扰。
我们希望这些技巧能启发您在设计 AI 时重新审视自己的数据结构。如需开始在 Web 应用中使用 AI,请探索 Learn AI on web.dev。