Claude Code 摩擦點偵測實作:自動分析 Session 低效行為與改善建議
Lab 2:摩擦點偵測器(Day 17-19)
要找出高摩擦 session,分類摩擦原因,輸出改善建議
2-1 摩擦偵測模組
新增 lib/friction.mjs:
import { extractToolCalls } from './analytics.mjs';
const FRICTION_TYPES = {
F1: { name: '工具執行失敗', severity: 'high' },
F2: { name: '重複修改同一檔案', severity: 'medium' },
F3: { name: 'Session 過長', severity: 'medium' },
F4: { name: 'Token 燃耗過高', severity: 'medium' },
F7: { name: '/compact 觸發', severity: 'low' },
};
/**
* 偵測單一 session 的摩擦點
* @returns { type, severity, detail, recommendation }[]
*/
export function detectFrictions(messages) {
const frictions = [];
const turns = messages.filter(m => m.type === 'assistant').length;
const toolCalls = extractToolCalls(messages);
// F1:工具執行失敗
let toolErrors = 0;
const errorMessages = [];
for (const msg of messages) {
if (msg.type !== 'user') continue;
for (const c of msg.message?.content ?? []) {
if (c.type === 'tool_result' && c.is_error) {
toolErrors++;
errorMessages.push(String(c.content ?? '').slice(0, 80));
}
}
}
if (toolErrors > 0) {
frictions.push({
type: 'F1',
severity: toolErrors >= 5 ? 'high' : 'medium',
detail: `${toolErrors} 次工具錯誤`,
examples: errorMessages.slice(0, 3),
recommendation: '在 CLAUDE.md 中補充路徑規範和常見錯誤處理方式',
});
}
// F2:重複修改同一檔案(3 次以上)
const editCounts = {};
for (const call of toolCalls) {
if (['Edit', 'Write', 'MultiEdit'].includes(call.name)) {
const fp = call.input?.file_path ?? 'unknown';
editCounts[fp] = (editCounts[fp] ?? 0) + 1;
}
}
const repeatedFiles = Object.entries(editCounts)
.filter(([, n]) => n >= 3)
.sort(([, a], [, b]) => b - a);
if (repeatedFiles.length > 0) {
frictions.push({
type: 'F2',
severity: 'medium',
detail: `${repeatedFiles.length} 個檔案被重複修改 3+ 次`,
examples: repeatedFiles.slice(0, 3).map(([f, n]) => `${f} (${n}次)`),
recommendation: '提問時一次說清楚所有需求,或使用 MultiEdit 批量修改',
});
}
// F3:Session 過長
if (turns > 50) {
frictions.push({
type: 'F3',
severity: 'medium',
detail: `Session 長達 ${turns} 輪(建議上限 30 輪)`,
examples: [],
recommendation: '將大任務拆成多個獨立 session,每個 session 只處理一個子任務',
});
}
// F4:Token 燃耗過高
let totalTokens = 0;
for (const msg of messages) {
if (msg.type !== 'assistant') continue;
const u = msg.message?.usage;
if (u) totalTokens += (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
}
const burnRate = turns > 0 ? totalTokens / turns : 0;
if (burnRate > 20000) {
frictions.push({
type: 'F4',
severity: 'medium',
detail: `每輪平均 ${burnRate.toFixed(0)} tokens(過高)`,
examples: [],
recommendation: '使用 /compact 壓縮 context,或拆分 session',
});
}
// F7:/compact 觸發
const summaries = messages.filter(m => m.type === 'summary').length;
if (summaries > 0) {
frictions.push({
type: 'F7',
severity: 'low',
detail: `觸發 ${summaries} 次 /compact`,
examples: [],
recommendation: '考慮在任務中途主動拆分 session,而非等到 context 撐滿',
});
}
return frictions;
}
/**
* 分析多個 sessions,統計摩擦類型頻率
*/
export function frictionSummary(sessionFrictions) {
const typeCounts = {};
for (const { frictions } of sessionFrictions) {
for (const f of frictions) {
typeCounts[f.type] = (typeCounts[f.type] ?? 0) + 1;
}
}
return Object.entries(typeCounts)
.map(([type, count]) => ({ type, ...FRICTION_TYPES[type], count }))
.sort((a, b) => b.count - a.count);
}
2-2 摩擦偵測報告
新增 friction-report.mjs:
import { scanProjects, scanProjectSessions } from './lib/scanner.mjs';
import { parseJsonl } from './lib/parser.mjs';
import { detectFrictions, frictionSummary } from './lib/friction.mjs';
import { calcEfficiencyMetrics } from './lib/efficiency.mjs';
const DAYS = parseInt(process.argv[2] ?? '30', 10);
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - DAYS);
const sessionFrictions = [];
for (const proj of scanProjects()) {
for (const session of scanProjectSessions(proj)) {
if (new Date(session.first_message_time) < cutoff) continue;
const messages = parseJsonl(session.file_path);
const frictions = detectFrictions(messages);
const metrics = calcEfficiencyMetrics(messages);
if (frictions.length > 0) {
sessionFrictions.push({ session, messages, frictions, metrics });
}
}
}
console.log(`\n 摩擦點分析報告(過去 ${DAYS} 天)\n`);
console.log(` 有摩擦的 session:${sessionFrictions.length} 個\n`);
// 摩擦類型統計
const summary = frictionSummary(sessionFrictions);
console.log('摩擦類型排行:\n');
for (const item of summary) {
const bar = '█'.repeat(Math.min(item.count, 20));
console.log(` [${item.type}] ${item.name.padEnd(16)} ${bar} ${item.count} 次`);
}
// 最高摩擦 sessions
const ranked = sessionFrictions
.sort((a, b) => b.frictions.length - a.frictions.length)
.slice(0, 5);
console.log('\n\n 摩擦最多的 Sessions:\n');
for (const { session, frictions, metrics } of ranked) {
const date = new Date(session.first_message_time).toLocaleDateString();
const summary = (session.summary ?? '(無摘要)').slice(0, 50);
console.log(` [${date}] 效率分 ${metrics.efficiency_score.toFixed(0)} | ${frictions.length} 個摩擦點`);
console.log(` ${summary}`);
for (const f of frictions) {
console.log(` → [${f.type}] ${f.detail}`);
if (f.examples.length > 0) {
console.log(` 例:${f.examples[0]}`);
}
}
console.log();
}
// 改善建議
const recommendations = new Set();
for (const { frictions } of sessionFrictions) {
for (const f of frictions) {
recommendations.add(`[${f.type}] ${f.recommendation}`);
}
}
console.log(' 改善建議:\n');
for (const rec of recommendations) {
console.log(` • ${rec}`);
}
開始執行
node friction-report.mjs # 過去 30 天
D:\Git\mathTalk\class\Claude-Code-Analytics\src>node friction-report.mjs
摩擦點分析報告(過去 30 天)
有摩擦的 session:99 個
摩擦類型排行:
[F2] 重複修改同一檔案 ████████████████████ 89 次
[F1] 工具執行失敗 ████████████████████ 80 次
[F3] Session 過長 ████████████████████ 75 次
摩擦最多的 Sessions:
[2026/6/10] 效率分 0 | 3 個摩擦點
(無摘要)
→ [F1] 1 次工具錯誤
例:File does not exist. Note: your current working directory is D:\OlgCase\RS\HSLR_
→ [F2] 1 個檔案被重複修改 3+ 次
例:unknown (10次)
→ [F3] Session 長達 127 輪(建議上限 30 輪)
[2026/6/9] 效率分 0 | 3 個摩擦點
(無摘要)
→ [F1] 4 次工具錯誤
例:<tool_use_error>File has been modified since read, either by the user or by a li
→ [F2] 1 個檔案被重複修改 3+ 次
例:unknown (56次)
→ [F3] Session 長達 385 輪(建議上限 30 輪)
[2026/6/8] 效率分 0 | 3 個摩擦點
(無摘要)
→ [F1] 8 次工具錯誤
例:Exit code 2
total 312
drwxr-xr-x 1 joker0820 1049089 0 Jun 8 17:31 .
drwxr-
→ [F2] 1 個檔案被重複修改 3+ 次
例:unknown (8次)
→ [F3] Session 長達 172 輪(建議上限 30 輪)
[2026/6/8] 效率分 0 | 3 個摩擦點
(無摘要)
→ [F1] 9 次工具錯誤
例:Exit code 1
Traceback (most recent call last):
File "<stdin>", line 3, in <mo
→ [F2] 1 個檔案被重複修改 3+ 次
例:unknown (32次)
→ [F3] Session 長達 501 輪(建議上限 30 輪)
[2026/6/10] 效率分 0 | 3 個摩擦點
(無摘要)
→ [F1] 11 次工具錯誤
例:Ripgrep search timed out after 20 seconds. The search may have matched files but
→ [F2] 1 個檔案被重複修改 3+ 次
例:unknown (146次)
→ [F3] Session 長達 879 輪(建議上限 30 輪)
改善建議:
• [F1] 在 CLAUDE.md 中補充路徑規範和常見錯誤處理方式
• [F2] 提問時一次說清楚所有需求,或使用 MultiEdit 批量修改
• [F3] 將大任務拆成多個獨立 session,每個 session 只處理一個子任務