@echomem/mcp 1.4.6 → 1.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/hud/session-viewer.html +1699 -0
- package/dist/city/10-problems-report.html +649 -0
- package/dist/city/echo-ai-city-only.html +39 -647
- package/dist/city/echo-ai-city-only.template.html +4 -4
- package/dist/forensics-10-problems.js +632 -0
- package/dist/hud/cli.js +0 -0
- package/dist/hud/electron-main.js +48 -4
- package/dist/hud/preload.cjs +9 -0
- package/dist/hud/server.js +93 -0
- package/dist/hud/web.js +82 -7
- package/dist/index.js +247 -14
- package/dist/setup.js +2 -2
- package/dist/v1-contract.js +134 -3
- package/package.json +1 -1
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>脏上下文 10 大退化模式 — 本地扫描报告</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0a0a0f;
|
|
10
|
+
--surface: #12121a;
|
|
11
|
+
--surface2: #1a1a26;
|
|
12
|
+
--border: #2a2a3a;
|
|
13
|
+
--text: #e8e8f0;
|
|
14
|
+
--text2: #8888a0;
|
|
15
|
+
--accent: #ff6b35;
|
|
16
|
+
--accent2: #ff9f1c;
|
|
17
|
+
--red: #ef4444;
|
|
18
|
+
--green: #22c55e;
|
|
19
|
+
--blue: #3b82f6;
|
|
20
|
+
--purple: #a855f7;
|
|
21
|
+
--yellow: #eab308;
|
|
22
|
+
--cyan: #06b6d4;
|
|
23
|
+
}
|
|
24
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
25
|
+
body {
|
|
26
|
+
font-family: -apple-system, "SF Pro Text", "Helvetica Neue", "PingFang SC", "Noto Sans CJK SC", sans-serif;
|
|
27
|
+
background: var(--bg);
|
|
28
|
+
color: var(--text);
|
|
29
|
+
line-height: 1.6;
|
|
30
|
+
min-height: 100vh;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* ---- Header ---- */
|
|
34
|
+
.header {
|
|
35
|
+
padding: 48px 40px 32px;
|
|
36
|
+
max-width: 1200px;
|
|
37
|
+
margin: 0 auto;
|
|
38
|
+
}
|
|
39
|
+
.header h1 {
|
|
40
|
+
font-size: 28px;
|
|
41
|
+
font-weight: 700;
|
|
42
|
+
letter-spacing: -0.5px;
|
|
43
|
+
margin-bottom: 6px;
|
|
44
|
+
}
|
|
45
|
+
.header .subtitle {
|
|
46
|
+
color: var(--text2);
|
|
47
|
+
font-size: 14px;
|
|
48
|
+
}
|
|
49
|
+
.header .subtitle span { color: var(--accent); font-weight: 600; }
|
|
50
|
+
|
|
51
|
+
/* ---- KPI Strip ---- */
|
|
52
|
+
.kpi-strip {
|
|
53
|
+
display: flex;
|
|
54
|
+
gap: 16px;
|
|
55
|
+
max-width: 1200px;
|
|
56
|
+
margin: 0 auto;
|
|
57
|
+
padding: 0 40px 32px;
|
|
58
|
+
flex-wrap: wrap;
|
|
59
|
+
}
|
|
60
|
+
.kpi {
|
|
61
|
+
flex: 1;
|
|
62
|
+
min-width: 160px;
|
|
63
|
+
background: var(--surface);
|
|
64
|
+
border: 1px solid var(--border);
|
|
65
|
+
border-radius: 12px;
|
|
66
|
+
padding: 20px 24px;
|
|
67
|
+
}
|
|
68
|
+
.kpi .label { color: var(--text2); font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
|
69
|
+
.kpi .value { font-size: 28px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
70
|
+
.kpi .unit { font-size: 14px; color: var(--text2); font-weight: 400; margin-left: 4px; }
|
|
71
|
+
.kpi .detail { font-size: 12px; color: var(--text2); margin-top: 4px; }
|
|
72
|
+
.kpi .value.waste { color: var(--red); }
|
|
73
|
+
.kpi .value.rate { color: var(--accent); }
|
|
74
|
+
|
|
75
|
+
/* ---- Main content ---- */
|
|
76
|
+
.content { max-width: 1200px; margin: 0 auto; padding: 0 40px 60px; }
|
|
77
|
+
|
|
78
|
+
/* ---- Bar chart ---- */
|
|
79
|
+
.chart-section {
|
|
80
|
+
background: var(--surface);
|
|
81
|
+
border: 1px solid var(--border);
|
|
82
|
+
border-radius: 16px;
|
|
83
|
+
padding: 32px;
|
|
84
|
+
margin-bottom: 32px;
|
|
85
|
+
}
|
|
86
|
+
.chart-section h2 { font-size: 18px; font-weight: 600; margin-bottom: 24px; }
|
|
87
|
+
.bar-row {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
margin-bottom: 12px;
|
|
91
|
+
gap: 12px;
|
|
92
|
+
}
|
|
93
|
+
.bar-label {
|
|
94
|
+
width: 180px;
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
font-size: 13px;
|
|
97
|
+
text-align: right;
|
|
98
|
+
color: var(--text2);
|
|
99
|
+
}
|
|
100
|
+
.bar-label .pid { color: var(--accent); font-weight: 600; margin-right: 4px; }
|
|
101
|
+
.bar-track {
|
|
102
|
+
flex: 1;
|
|
103
|
+
height: 28px;
|
|
104
|
+
background: var(--surface2);
|
|
105
|
+
border-radius: 6px;
|
|
106
|
+
overflow: hidden;
|
|
107
|
+
position: relative;
|
|
108
|
+
}
|
|
109
|
+
.bar-fill {
|
|
110
|
+
height: 100%;
|
|
111
|
+
border-radius: 6px;
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
padding-left: 10px;
|
|
115
|
+
font-size: 12px;
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
color: #fff;
|
|
118
|
+
min-width: fit-content;
|
|
119
|
+
transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1);
|
|
120
|
+
}
|
|
121
|
+
.bar-tokens {
|
|
122
|
+
width: 100px;
|
|
123
|
+
flex-shrink: 0;
|
|
124
|
+
font-size: 13px;
|
|
125
|
+
font-variant-numeric: tabular-nums;
|
|
126
|
+
color: var(--text2);
|
|
127
|
+
text-align: right;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* ---- Tier legend ---- */
|
|
131
|
+
.tier-legend {
|
|
132
|
+
display: flex;
|
|
133
|
+
gap: 24px;
|
|
134
|
+
margin-bottom: 24px;
|
|
135
|
+
flex-wrap: wrap;
|
|
136
|
+
}
|
|
137
|
+
.tier-badge {
|
|
138
|
+
display: inline-flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
gap: 6px;
|
|
141
|
+
font-size: 12px;
|
|
142
|
+
color: var(--text2);
|
|
143
|
+
}
|
|
144
|
+
.tier-dot {
|
|
145
|
+
width: 10px;
|
|
146
|
+
height: 10px;
|
|
147
|
+
border-radius: 50%;
|
|
148
|
+
}
|
|
149
|
+
.tier-dot.t1 { background: var(--green); }
|
|
150
|
+
.tier-dot.t2 { background: var(--yellow); }
|
|
151
|
+
.tier-dot.t3 { background: var(--purple); }
|
|
152
|
+
|
|
153
|
+
/* ---- Problem cards ---- */
|
|
154
|
+
.problems-grid {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-columns: 1fr 1fr;
|
|
157
|
+
gap: 20px;
|
|
158
|
+
}
|
|
159
|
+
@media (max-width: 900px) { .problems-grid { grid-template-columns: 1fr; } }
|
|
160
|
+
|
|
161
|
+
.problem-card {
|
|
162
|
+
background: var(--surface);
|
|
163
|
+
border: 1px solid var(--border);
|
|
164
|
+
border-radius: 14px;
|
|
165
|
+
padding: 24px;
|
|
166
|
+
position: relative;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
}
|
|
169
|
+
.problem-card::before {
|
|
170
|
+
content: '';
|
|
171
|
+
position: absolute;
|
|
172
|
+
top: 0; left: 0; right: 0;
|
|
173
|
+
height: 3px;
|
|
174
|
+
}
|
|
175
|
+
.problem-card.tier1::before { background: var(--green); }
|
|
176
|
+
.problem-card.tier2::before { background: var(--yellow); }
|
|
177
|
+
.problem-card.tier3::before { background: var(--purple); }
|
|
178
|
+
|
|
179
|
+
.card-head {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
gap: 10px;
|
|
183
|
+
margin-bottom: 12px;
|
|
184
|
+
}
|
|
185
|
+
.card-pid {
|
|
186
|
+
font-size: 13px;
|
|
187
|
+
font-weight: 700;
|
|
188
|
+
color: var(--accent);
|
|
189
|
+
background: rgba(255, 107, 53, 0.12);
|
|
190
|
+
padding: 2px 8px;
|
|
191
|
+
border-radius: 6px;
|
|
192
|
+
}
|
|
193
|
+
.card-name {
|
|
194
|
+
font-size: 15px;
|
|
195
|
+
font-weight: 600;
|
|
196
|
+
}
|
|
197
|
+
.card-tier {
|
|
198
|
+
margin-left: auto;
|
|
199
|
+
font-size: 11px;
|
|
200
|
+
padding: 2px 8px;
|
|
201
|
+
border-radius: 4px;
|
|
202
|
+
font-weight: 600;
|
|
203
|
+
}
|
|
204
|
+
.card-tier.t1 { background: rgba(34,197,94,0.15); color: var(--green); }
|
|
205
|
+
.card-tier.t2 { background: rgba(234,179,8,0.15); color: var(--yellow); }
|
|
206
|
+
.card-tier.t3 { background: rgba(168,85,247,0.15); color: var(--purple); }
|
|
207
|
+
|
|
208
|
+
.card-desc {
|
|
209
|
+
font-size: 13px;
|
|
210
|
+
color: var(--text2);
|
|
211
|
+
margin-bottom: 16px;
|
|
212
|
+
line-height: 1.7;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.card-stats {
|
|
216
|
+
display: flex;
|
|
217
|
+
gap: 20px;
|
|
218
|
+
margin-bottom: 16px;
|
|
219
|
+
}
|
|
220
|
+
.card-stat .s-label { font-size: 11px; color: var(--text2); text-transform: uppercase; letter-spacing: 0.3px; }
|
|
221
|
+
.card-stat .s-val { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
222
|
+
|
|
223
|
+
.card-example {
|
|
224
|
+
background: var(--bg);
|
|
225
|
+
border: 1px solid var(--border);
|
|
226
|
+
border-radius: 10px;
|
|
227
|
+
padding: 14px 16px;
|
|
228
|
+
}
|
|
229
|
+
.card-example .ex-label {
|
|
230
|
+
font-size: 11px;
|
|
231
|
+
text-transform: uppercase;
|
|
232
|
+
letter-spacing: 0.4px;
|
|
233
|
+
color: var(--accent2);
|
|
234
|
+
margin-bottom: 6px;
|
|
235
|
+
font-weight: 600;
|
|
236
|
+
}
|
|
237
|
+
.card-example .ex-body {
|
|
238
|
+
font-size: 12px;
|
|
239
|
+
color: var(--text2);
|
|
240
|
+
line-height: 1.6;
|
|
241
|
+
word-break: break-all;
|
|
242
|
+
}
|
|
243
|
+
.card-example .ex-body .file { color: var(--cyan); }
|
|
244
|
+
.card-example .ex-body .tokens { color: var(--red); font-weight: 600; }
|
|
245
|
+
.card-example .ex-body .time { color: var(--yellow); }
|
|
246
|
+
|
|
247
|
+
/* ---- Waste breakdown donut ---- */
|
|
248
|
+
.donut-section {
|
|
249
|
+
display: flex;
|
|
250
|
+
gap: 32px;
|
|
251
|
+
align-items: center;
|
|
252
|
+
flex-wrap: wrap;
|
|
253
|
+
}
|
|
254
|
+
.donut-wrap { flex-shrink: 0; }
|
|
255
|
+
.donut-legend {
|
|
256
|
+
display: flex;
|
|
257
|
+
flex-direction: column;
|
|
258
|
+
gap: 8px;
|
|
259
|
+
}
|
|
260
|
+
.legend-row {
|
|
261
|
+
display: flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
gap: 8px;
|
|
264
|
+
font-size: 13px;
|
|
265
|
+
}
|
|
266
|
+
.legend-swatch {
|
|
267
|
+
width: 12px;
|
|
268
|
+
height: 12px;
|
|
269
|
+
border-radius: 3px;
|
|
270
|
+
flex-shrink: 0;
|
|
271
|
+
}
|
|
272
|
+
.legend-pct { color: var(--text2); font-variant-numeric: tabular-nums; width: 40px; text-align: right; }
|
|
273
|
+
|
|
274
|
+
/* ---- Footer ---- */
|
|
275
|
+
.footer {
|
|
276
|
+
max-width: 1200px;
|
|
277
|
+
margin: 0 auto;
|
|
278
|
+
padding: 0 40px 48px;
|
|
279
|
+
font-size: 12px;
|
|
280
|
+
color: var(--text2);
|
|
281
|
+
display: flex;
|
|
282
|
+
gap: 24px;
|
|
283
|
+
flex-wrap: wrap;
|
|
284
|
+
}
|
|
285
|
+
.footer .badge {
|
|
286
|
+
display: inline-flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
gap: 4px;
|
|
289
|
+
background: var(--surface);
|
|
290
|
+
border: 1px solid var(--border);
|
|
291
|
+
padding: 4px 10px;
|
|
292
|
+
border-radius: 6px;
|
|
293
|
+
font-size: 11px;
|
|
294
|
+
}
|
|
295
|
+
.footer .badge .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); }
|
|
296
|
+
</style>
|
|
297
|
+
</head>
|
|
298
|
+
<body>
|
|
299
|
+
|
|
300
|
+
<div class="header">
|
|
301
|
+
<h1>脏上下文 · 10 大退化模式</h1>
|
|
302
|
+
<div class="subtitle">
|
|
303
|
+
基于本地转录的增量扫描 · <span>257 个会话</span> · <span>3,877 turns</span> · 扫描窗口 2026-04-25 → 2026-07-01
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="kpi-strip">
|
|
308
|
+
<div class="kpi">
|
|
309
|
+
<div class="label">新增内容 Token</div>
|
|
310
|
+
<div class="value">196.5<span class="unit">M</span></div>
|
|
311
|
+
<div class="detail">fresh 输入 + output;不含每 turn 重发的缓存</div>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="kpi">
|
|
314
|
+
<div class="label">浪费 Token</div>
|
|
315
|
+
<div class="value waste">109.9<span class="unit">M</span></div>
|
|
316
|
+
<div class="detail">10 个问题的归因总和</div>
|
|
317
|
+
</div>
|
|
318
|
+
<div class="kpi">
|
|
319
|
+
<div class="label">浪费率</div>
|
|
320
|
+
<div class="value rate">55.9<span class="unit">%</span></div>
|
|
321
|
+
<div class="detail">浪费 / 新增内容(旧口径 13.85% 分母误用重发缓存)</div>
|
|
322
|
+
</div>
|
|
323
|
+
<div class="kpi">
|
|
324
|
+
<div class="label">重发缓存占账单</div>
|
|
325
|
+
<div class="value">79.6<span class="unit">%</span></div>
|
|
326
|
+
<div class="detail">总账单 962M 中被每 turn 重发的部分</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div class="content">
|
|
331
|
+
|
|
332
|
+
<!-- ===== Bar Chart ===== -->
|
|
333
|
+
<div class="chart-section">
|
|
334
|
+
<h2>各问题浪费 Token 分布</h2>
|
|
335
|
+
<div class="tier-legend">
|
|
336
|
+
<div class="tier-badge"><div class="tier-dot t1"></div>Tier 1 · 确定性检测 · 高置信</div>
|
|
337
|
+
<div class="tier-badge"><div class="tier-dot t2"></div>Tier 2 · 结构启发式 · 中置信</div>
|
|
338
|
+
<div class="tier-badge"><div class="tier-dot t3"></div>Tier 3 · 关键词分类 · 低置信</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<div id="barChart"></div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<!-- ===== Donut ===== -->
|
|
345
|
+
<div class="chart-section">
|
|
346
|
+
<h2>浪费构成</h2>
|
|
347
|
+
<div class="donut-section">
|
|
348
|
+
<div class="donut-wrap">
|
|
349
|
+
<svg id="donut" width="220" height="220" viewBox="0 0 220 220"></svg>
|
|
350
|
+
</div>
|
|
351
|
+
<div class="donut-legend" id="donutLegend"></div>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<!-- ===== Problem Cards ===== -->
|
|
356
|
+
<h2 style="font-size: 20px; font-weight: 600; margin-bottom: 20px;">逐项分析与真实案例</h2>
|
|
357
|
+
<div class="problems-grid" id="problemsGrid"></div>
|
|
358
|
+
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<div class="footer">
|
|
362
|
+
<div class="badge"><div class="dot"></div>全部本地扫描 · 无 LLM 调用</div>
|
|
363
|
+
<div class="badge">转录数据未上传</div>
|
|
364
|
+
<div class="badge">forensics-10-problems.ts v2 · 浪费率按新增内容重算</div>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
<script>
|
|
368
|
+
const DATA = {
|
|
369
|
+
problems: [
|
|
370
|
+
{
|
|
371
|
+
id: "P01", name: "图片洪水", nameEn: "Image Flood",
|
|
372
|
+
tier: 1, confidence: "高",
|
|
373
|
+
turnsAffected: 239, turnsPct: 6,
|
|
374
|
+
wastedTokens: 41728949, wastedPct: 38,
|
|
375
|
+
color: "#ef4444",
|
|
376
|
+
desc: "Agent 在 tool 调用中加载了图片/二进制文件(screenshot、png、svg 等)。这些内容进入上下文后,compaction 清不掉——它们以 base64 或大块 chars 驻留,每个后续 turn 都要为它们付出 input token 代价。",
|
|
377
|
+
example: {
|
|
378
|
+
session: "rollout-2026-06-*",
|
|
379
|
+
turn: 9,
|
|
380
|
+
tokens: "2,933,789",
|
|
381
|
+
detail: "单个 turn 加载了 <span class='tokens'>293 万 token</span> 的图片内容。这相当于约 73 万汉字的噪声一次性灌入 context,后续每个 turn 都要为此买单。"
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
id: "P02", name: "纠错级联", nameEn: "Correction Cascade",
|
|
386
|
+
tier: 3, confidence: "低",
|
|
387
|
+
turnsAffected: 21, turnsPct: 1,
|
|
388
|
+
wastedTokens: 891547, wastedPct: 1,
|
|
389
|
+
color: "#a855f7",
|
|
390
|
+
desc: "连续 ≥2 个 turn 都被用户判定为 rework(「不对」、「还是错了」、「重新来」)。第一次失败后的每一次重试都在用新的 output 污染已经很脏的 context,形成「越改越错」的恶性循环。",
|
|
391
|
+
example: {
|
|
392
|
+
session: "60a044aa-*",
|
|
393
|
+
turn: "1→3",
|
|
394
|
+
tokens: "322,157",
|
|
395
|
+
detail: "3 轮纠错级联。用户连续说了「不对」→「还是不对」→「重做」,agent 在第 2、3 轮总共产生 <span class='tokens'>32.2 万 token</span> 的无效输出,全部残留在 context 中。"
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: "P03", name: "文件影子版本", nameEn: "Shadow File Versions",
|
|
400
|
+
tier: 2, confidence: "中",
|
|
401
|
+
turnsAffected: 973, turnsPct: 25,
|
|
402
|
+
wastedTokens: 62376380, wastedPct: 57,
|
|
403
|
+
color: "#eab308",
|
|
404
|
+
desc: "Agent 先 Read 了一个文件,然后在同一 compaction 周期内 Edit 了它——但旧版本的 Read 输出仍然驻留在 context 中。此时 context 里同时存在「过时的旧版」和「agent 脑中的新版」,模型需要在两个矛盾的版本间做决策,且旧版的 token 完全是浪费。这是最大的浪费源。",
|
|
405
|
+
example: {
|
|
406
|
+
session: "rollout-2026-06-*",
|
|
407
|
+
turn: 5,
|
|
408
|
+
tokens: "4,696,967",
|
|
409
|
+
detail: "单个 turn 编辑了 53 个文件(批量更新 outreach 文档)。这些文件之前都被 Read 过,编辑后旧版 Read 输出仍在 context 中:<span class='tokens'>469.7 万 token</span> 的影子版本。影响了 <span class='tokens'>25%</span> 的所有 turn。"
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
id: "P04", name: "压缩后重新发现", nameEn: "Post-Compaction Re-discovery",
|
|
414
|
+
tier: 1, confidence: "高",
|
|
415
|
+
turnsAffected: 241, turnsPct: 6,
|
|
416
|
+
wastedTokens: 1948942, wastedPct: 2,
|
|
417
|
+
color: "#22c55e",
|
|
418
|
+
desc: "Context compaction 清理了历史内容,但 agent 随即重新 Read 了 compaction 前已经读过且未修改的文件。说明 agent 对「自己已经知道什么」没有记忆——compaction 本应节省 token,结果被立即浪费。",
|
|
419
|
+
example: {
|
|
420
|
+
session: "rollout-2026-04-*",
|
|
421
|
+
turn: 25,
|
|
422
|
+
tokens: "55,315",
|
|
423
|
+
detail: "Compaction 后 agent 重新读取了 <span class='file'>route.ts</span>、<span class='file'>useTimelineLabData.ts</span>、<span class='file'>SKILL.md</span> 等 4 个文件——它们自上次读取后没有任何修改。<span class='tokens'>5.5 万 token</span> 白白重新加载。"
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
id: "P05", name: "假完成", nameEn: "False Completion",
|
|
428
|
+
tier: 3, confidence: "低",
|
|
429
|
+
turnsAffected: 12, turnsPct: 0,
|
|
430
|
+
wastedTokens: 99285, wastedPct: 0,
|
|
431
|
+
color: "#a855f7",
|
|
432
|
+
desc: "Agent 执行了 git commit(认为任务完成),但用户在下一个 turn 立即给出 rework 信号。说明 agent 提交了一个不符合用户意图的结果——commit 本身和那个 turn 的全部输出都是浪费。",
|
|
433
|
+
example: {
|
|
434
|
+
session: "31ef6182-*",
|
|
435
|
+
turn: 19,
|
|
436
|
+
tokens: "43,781",
|
|
437
|
+
detail: "Agent 在 turn 19 执行了 <span class='file'>git commit</span>,但 turn 20 用户说「不对,重做」。提交的 turn 产出了 <span class='tokens'>4.4 万 token</span>,全部变成需要回退的无效工作。"
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
id: "P06", name: "长间隔状态脱节", nameEn: "Gap State Disconnect",
|
|
442
|
+
tier: 2, confidence: "中",
|
|
443
|
+
turnsAffected: 105, turnsPct: 3,
|
|
444
|
+
wastedTokens: 909981, wastedPct: 1,
|
|
445
|
+
color: "#eab308",
|
|
446
|
+
desc: "两个 turn 之间超过 30 分钟间隔后,agent 在恢复时重新读取了之前已经读过的文件。这表明 agent 不确定「休息」期间文件是否被人类修改,选择了保守的全量重新加载策略。",
|
|
447
|
+
example: {
|
|
448
|
+
session: "rollout-2026-04-*",
|
|
449
|
+
turn: 5,
|
|
450
|
+
tokens: "45,319",
|
|
451
|
+
detail: "间隔 <span class='time'>62 分钟</span>后恢复,agent 重新读取了 <span class='file'>memory-log-v3.html</span>、<span class='file'>MemoryCard.tsx</span> 等 7 个文件。<span class='tokens'>4.5 万 token</span> 用于重建早已存在的工作现场。"
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
id: "P07", name: "中止留下坏补丁", nameEn: "Abort Leaves Bad Patch",
|
|
456
|
+
tier: 2, confidence: "中",
|
|
457
|
+
turnsAffected: 13, turnsPct: 0,
|
|
458
|
+
wastedTokens: 275735, wastedPct: 0,
|
|
459
|
+
color: "#eab308",
|
|
460
|
+
desc: "Agent 做了文件编辑,但用户在下一个 turn 说了「revert」「撤回」「还原」。Agent 的编辑不仅被丢弃,其输出 token 和残留在磁盘上的 diff 都构成了 context 污染。",
|
|
461
|
+
example: {
|
|
462
|
+
session: "31ef6182-*",
|
|
463
|
+
turn: 0,
|
|
464
|
+
tokens: "86,300",
|
|
465
|
+
detail: "Agent 在 turn 0 编辑了 <span class='file'>dirty-context-article.md</span>,turn 1 用户说「撤回」。整个 turn 的 <span class='tokens'>8.6 万 token</span> 产出变成废料,且文件的旧 diff 残留在 context 中。"
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
id: "P08", name: "Git 当记忆替身", nameEn: "Git as Memory Substitute",
|
|
470
|
+
tier: 1, confidence: "高",
|
|
471
|
+
turnsAffected: 300, turnsPct: 8,
|
|
472
|
+
wastedTokens: 448393, wastedPct: 0,
|
|
473
|
+
color: "#22c55e",
|
|
474
|
+
desc: "Agent 重复执行完全相同的 git log / diff / blame / show 命令。这说明 agent 把 git 当作「外置记忆」——每次需要信息时重新查询,而不是记住之前的结果。不包括 git status(状态检查)。",
|
|
475
|
+
example: {
|
|
476
|
+
session: "rollout-2026-05-*",
|
|
477
|
+
turn: 18,
|
|
478
|
+
tokens: "14,655",
|
|
479
|
+
detail: "在同一 session 中重复执行了相同的 <span class='file'>git log</span> 命令。<span class='tokens'>1.5 万 token</span> 的输出是之前某个 turn 的完全副本。影响了 <span class='tokens'>8%</span> 的所有 turn——这是影响面最广的问题。"
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
id: "P09", name: "用户拉回偏离轨迹", nameEn: "User Pulls Back Trajectory",
|
|
484
|
+
tier: 3, confidence: "低",
|
|
485
|
+
turnsAffected: 47, turnsPct: 1,
|
|
486
|
+
wastedTokens: 1009484, wastedPct: 1,
|
|
487
|
+
color: "#a855f7",
|
|
488
|
+
desc: "用户在 session 中使用了 ≥2 次明确的方向纠正语句(「你跑偏了」「回到刚才的」「我说的是…」「focus on…」)。每次纠正前的那个 turn,agent 的输出大概率是偏离目标的——被浪费了。",
|
|
489
|
+
example: {
|
|
490
|
+
session: "214b6e2c-*",
|
|
491
|
+
turn: 10,
|
|
492
|
+
tokens: "136,337",
|
|
493
|
+
detail: "用户在 turn 11 说了方向纠正。Agent 在 turn 10 花了 <span class='tokens'>13.6 万 token</span> 走偏了方向。这个 session 中出现了多次 redirect,说明 agent 的任务理解存在持续漂移。"
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
id: "P10", name: "搜索重放", nameEn: "Search Replay",
|
|
498
|
+
tier: 1, confidence: "高",
|
|
499
|
+
turnsAffected: 124, turnsPct: 3,
|
|
500
|
+
wastedTokens: 247113, wastedPct: 0,
|
|
501
|
+
color: "#22c55e",
|
|
502
|
+
desc: "Agent 在 session 中重复执行了完全相同的 grep / rg / find / fd 等搜索命令(空格归一化后精确匹配)。搜索结果在 context 中已经存在,重复查询只增加噪声。",
|
|
503
|
+
example: {
|
|
504
|
+
session: "rollout-2026-05-*",
|
|
505
|
+
turn: 136,
|
|
506
|
+
tokens: "24,404",
|
|
507
|
+
detail: "单个 turn 中包含 4 条重复搜索命令。<span class='tokens'>2.4 万 token</span> 的搜索结果是之前 turn 的副本。"
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
]
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// ---- Render bar chart ----
|
|
514
|
+
const maxTokens = Math.max(...DATA.problems.map(p => p.wastedTokens));
|
|
515
|
+
const barChart = document.getElementById('barChart');
|
|
516
|
+
const sorted = [...DATA.problems].sort((a, b) => b.wastedTokens - a.wastedTokens);
|
|
517
|
+
|
|
518
|
+
function fmtTokens(n) {
|
|
519
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
520
|
+
if (n >= 1e3) return (n / 1e3).toFixed(0) + 'K';
|
|
521
|
+
return n;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
sorted.forEach(p => {
|
|
525
|
+
const pct = (p.wastedTokens / maxTokens) * 100;
|
|
526
|
+
const tierColor = p.tier === 1 ? '#22c55e' : p.tier === 2 ? '#eab308' : '#a855f7';
|
|
527
|
+
barChart.innerHTML += `
|
|
528
|
+
<div class="bar-row">
|
|
529
|
+
<div class="bar-label"><span class="pid">${p.id}</span> ${p.name}</div>
|
|
530
|
+
<div class="bar-track">
|
|
531
|
+
<div class="bar-fill" style="width: ${Math.max(pct, 2)}%; background: ${tierColor};">
|
|
532
|
+
${p.wastedPct > 2 ? p.wastedPct + '%' : ''}
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
<div class="bar-tokens">${fmtTokens(p.wastedTokens)}</div>
|
|
536
|
+
</div>
|
|
537
|
+
`;
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// ---- Render donut ----
|
|
541
|
+
const donutColors = ['#ef4444', '#eab308', '#22c55e', '#a855f7', '#3b82f6', '#06b6d4', '#f97316', '#ec4899', '#6366f1', '#84cc16'];
|
|
542
|
+
const svg = document.getElementById('donut');
|
|
543
|
+
const cx = 110, cy = 110, r = 80, sw = 28;
|
|
544
|
+
let cumAngle = -90;
|
|
545
|
+
const totalWaste = DATA.problems.reduce((s, p) => s + p.wastedTokens, 0);
|
|
546
|
+
const legend = document.getElementById('donutLegend');
|
|
547
|
+
|
|
548
|
+
// Filter to show top problems, group rest
|
|
549
|
+
const threshold = 0.01;
|
|
550
|
+
const shown = [];
|
|
551
|
+
let otherTokens = 0;
|
|
552
|
+
sorted.forEach(p => {
|
|
553
|
+
if (p.wastedTokens / totalWaste >= threshold) shown.push(p);
|
|
554
|
+
else otherTokens += p.wastedTokens;
|
|
555
|
+
});
|
|
556
|
+
if (otherTokens > 0) shown.push({ id: '其他', name: '其余问题', wastedTokens: otherTokens, wastedPct: Math.round(otherTokens / totalWaste * 100) });
|
|
557
|
+
|
|
558
|
+
shown.forEach((p, i) => {
|
|
559
|
+
const frac = p.wastedTokens / totalWaste;
|
|
560
|
+
const angle = frac * 360;
|
|
561
|
+
const startAngle = cumAngle;
|
|
562
|
+
const endAngle = cumAngle + angle;
|
|
563
|
+
cumAngle = endAngle;
|
|
564
|
+
|
|
565
|
+
const startRad = (startAngle * Math.PI) / 180;
|
|
566
|
+
const endRad = (endAngle * Math.PI) / 180;
|
|
567
|
+
const x1 = cx + r * Math.cos(startRad);
|
|
568
|
+
const y1 = cy + r * Math.sin(startRad);
|
|
569
|
+
const x2 = cx + r * Math.cos(endRad);
|
|
570
|
+
const y2 = cy + r * Math.sin(endRad);
|
|
571
|
+
const largeArc = angle > 180 ? 1 : 0;
|
|
572
|
+
const color = donutColors[i % donutColors.length];
|
|
573
|
+
|
|
574
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
575
|
+
path.setAttribute('d', `M ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2}`);
|
|
576
|
+
path.setAttribute('fill', 'none');
|
|
577
|
+
path.setAttribute('stroke', color);
|
|
578
|
+
path.setAttribute('stroke-width', sw);
|
|
579
|
+
path.setAttribute('stroke-linecap', 'butt');
|
|
580
|
+
svg.appendChild(path);
|
|
581
|
+
|
|
582
|
+
const pct = Math.round(frac * 100);
|
|
583
|
+
legend.innerHTML += `
|
|
584
|
+
<div class="legend-row">
|
|
585
|
+
<div class="legend-swatch" style="background: ${color};"></div>
|
|
586
|
+
<span>${p.id} ${p.name}</span>
|
|
587
|
+
<span class="legend-pct">${pct}%</span>
|
|
588
|
+
</div>
|
|
589
|
+
`;
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Center text
|
|
593
|
+
const centerText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
594
|
+
centerText.setAttribute('x', cx);
|
|
595
|
+
centerText.setAttribute('y', cy - 6);
|
|
596
|
+
centerText.setAttribute('text-anchor', 'middle');
|
|
597
|
+
centerText.setAttribute('fill', '#e8e8f0');
|
|
598
|
+
centerText.setAttribute('font-size', '24');
|
|
599
|
+
centerText.setAttribute('font-weight', '700');
|
|
600
|
+
centerText.textContent = fmtTokens(totalWaste);
|
|
601
|
+
svg.appendChild(centerText);
|
|
602
|
+
const subText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
603
|
+
subText.setAttribute('x', cx);
|
|
604
|
+
subText.setAttribute('y', cy + 16);
|
|
605
|
+
subText.setAttribute('text-anchor', 'middle');
|
|
606
|
+
subText.setAttribute('fill', '#8888a0');
|
|
607
|
+
subText.setAttribute('font-size', '12');
|
|
608
|
+
subText.textContent = '总浪费 token';
|
|
609
|
+
svg.appendChild(subText);
|
|
610
|
+
|
|
611
|
+
// ---- Render problem cards ----
|
|
612
|
+
const grid = document.getElementById('problemsGrid');
|
|
613
|
+
DATA.problems.forEach(p => {
|
|
614
|
+
const tierClass = `tier${p.tier}`;
|
|
615
|
+
const tierLabel = p.tier === 1 ? 'Tier 1 · 确定性' : p.tier === 2 ? 'Tier 2 · 启发式' : 'Tier 3 · 关键词';
|
|
616
|
+
const tierBadge = `t${p.tier}`;
|
|
617
|
+
|
|
618
|
+
grid.innerHTML += `
|
|
619
|
+
<div class="problem-card ${tierClass}">
|
|
620
|
+
<div class="card-head">
|
|
621
|
+
<div class="card-pid">${p.id}</div>
|
|
622
|
+
<div class="card-name">${p.name}</div>
|
|
623
|
+
<div class="card-tier ${tierBadge}">${tierLabel}</div>
|
|
624
|
+
</div>
|
|
625
|
+
<div class="card-desc">${p.desc}</div>
|
|
626
|
+
<div class="card-stats">
|
|
627
|
+
<div class="card-stat">
|
|
628
|
+
<div class="s-label">浪费 Token</div>
|
|
629
|
+
<div class="s-val" style="color: var(--red);">${fmtTokens(p.wastedTokens)}</div>
|
|
630
|
+
</div>
|
|
631
|
+
<div class="card-stat">
|
|
632
|
+
<div class="s-label">影响 Turn</div>
|
|
633
|
+
<div class="s-val">${p.turnsAffected}<span style="font-size:14px;color:var(--text2);font-weight:400;"> (${p.turnsPct}%)</span></div>
|
|
634
|
+
</div>
|
|
635
|
+
<div class="card-stat">
|
|
636
|
+
<div class="s-label">占浪费总量</div>
|
|
637
|
+
<div class="s-val">${p.wastedPct}%</div>
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
<div class="card-example">
|
|
641
|
+
<div class="ex-label">真实案例 · ${p.example.session} · Turn ${p.example.turn}</div>
|
|
642
|
+
<div class="ex-body">${p.example.detail}</div>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
`;
|
|
646
|
+
});
|
|
647
|
+
</script>
|
|
648
|
+
</body>
|
|
649
|
+
</html>
|