Files
dsh_shenxian/dsh-server-docs/07-scripts/analyze-turn.mjs
T
admin 5ec99ce227
build / build-and-scan (push) Canceled after 0s
chore: 文档库全量重新入库(2026-10-20)
2026-10-20 15:05:55 +08:00

72 lines
3.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 会话每轮指标:TTFT(turn/start → 首个流式 chunk)、轮时长、工具耗时
* 只输出时间与类型,不输出任何正文。
* 用法:node analyze-turn.mjs <session.jsonl.zstd>
*/
import { readFileSync } from 'node:fs'
import { zstdDecompressSync } from 'node:zlib'
const raw = readFileSync(process.argv[2])
const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
const offs = []
for (let i = 0; i + 4 <= raw.length; i++) if (raw.compare(MAGIC, 0, 4, i, i + 4) === 0) offs.push(i)
let text = ''
const frames = offs.length > 0 ? offs : [0]
for (let f = 0; f < frames.length; f++) {
const s = frames[f], e = f + 1 < frames.length ? frames[f + 1] : raw.length
try { text += zstdDecompressSync(raw.subarray(s, e)).toString('utf8') } catch {}
}
if (text === '') { try { text = zstdDecompressSync(raw).toString('utf8') } catch (e) { console.log('fail', e.message); process.exit(1) } }
const pick = (o, keys, d = 0) => {
if (o === null || typeof o !== 'object' || d > 3) return undefined
for (const k of keys) if (typeof o[k] !== 'undefined') return o[k]
for (const v of Object.values(o)) { const h = pick(v, keys, d + 1); if (h !== undefined) return h }
return undefined
}
const ev = []
for (const line of text.split('\n')) {
if (line.trim() === '') continue
let o; try { o = JSON.parse(line) } catch { continue }
const kind = pick(o, ['kind', 'type', 'event'])
const ts = pick(o, ['ts', 'time', 'timestamp', 'at', 'createdAt'])
const t = typeof ts === 'number' ? (ts < 1e12 ? ts * 1000 : ts) : typeof ts === 'string' ? Date.parse(ts) : NaN
if (typeof kind === 'string') ev.push({ k: kind, t: Number.isNaN(t) ? undefined : t })
}
const fmt = (ms) => new Date(ms).toISOString().slice(11, 19) + 'Z'
let turns = 0
for (let i = 0; i < ev.length; i++) {
if (ev[i].k !== 'turn/start') continue
turns++
const t0 = ev[i].t
let end, firstChunk, firstText
for (let j = i + 1; j < ev.length; j++) {
if (ev[j].k === 'turn/end' && end === undefined) { end = ev[j].t; break }
if (firstChunk === undefined && (ev[j].k === 'assistant/chunk' || ev[j].k === 'reasoning-chunks') && ev[j].t !== undefined) firstChunk = ev[j].t
if (firstText === undefined && ev[j].k === 'text-chunks' && ev[j].t !== undefined) firstText = ev[j].t
}
const ttft = t0 !== undefined && firstChunk !== undefined ? ((firstChunk - t0) / 1000).toFixed(1) : '?'
const ttftText = t0 !== undefined && firstText !== undefined ? ((firstText - t0) / 1000).toFixed(1) : '?'
const dur = t0 !== undefined && end !== undefined ? ((end - t0) / 1000).toFixed(1) : '?'
console.log(`轮 ${String(turns).padStart(2)} ${t0 !== undefined ? fmt(t0) : '?'} TTFT=${ttft}s 首文本=${ttftText}s 轮时长=${dur}s`)
}
// 工具耗时
const tools = ev.map((e, i) => ({ e, i })).filter((x) => x.e.k === 'tool/call')
const dt = []
for (const { i } of tools) {
for (let j = i + 1; j < ev.length; j++) {
if (ev[j].k === 'tool/result') {
if (ev[i].t !== undefined && ev[j].t !== undefined) dt.push((ev[j].t - ev[i].t) / 1000)
break
}
}
}
if (dt.length > 0) {
dt.sort((a, b) => b - a)
const avg = dt.reduce((a, b) => a + b, 0) / dt.length
console.log(`\n工具调用 ${dt.length} 次;平均 ${avg.toFixed(1)}s;最慢 5 个: ${dt.slice(0, 5).map((x) => x.toFixed(1) + 's').join(', ')}`)
}
console.log(`\n合计轮数: ${turns}`)