perf: 会话统计增量聚合——emitStats/buildContextInfo 不再每轮全量遍历
This commit is contained in:
@@ -467,13 +467,14 @@ export class AgentService {
|
||||
* 类目:消息 / 自定义工具 / 系统工具 / 系统提示词 / 技能 / 其他;
|
||||
* 技能块注入在系统提示词内,从提示词估算中拆出(不重复计数)。 */
|
||||
private buildContextInfo(session: AgentSession): ContextUsageInfo {
|
||||
const agg = this.aggregateSession(session)
|
||||
const breakdown: ContextBreakdownEntry[] = [
|
||||
{ key: 'messages', label: '消息', tokens: 0 },
|
||||
{ key: 'customTools', label: '自定义工具', tokens: 0 },
|
||||
{ key: 'builtinTools', label: '系统工具', tokens: 0 },
|
||||
{ key: 'messages', label: '消息', tokens: agg.breakdown.get('messages') ?? 0 },
|
||||
{ key: 'customTools', label: '自定义工具', tokens: agg.breakdown.get('customTools') ?? 0 },
|
||||
{ key: 'builtinTools', label: '系统工具', tokens: agg.breakdown.get('builtinTools') ?? 0 },
|
||||
{ key: 'systemPrompt', label: '系统提示词', tokens: 0 },
|
||||
{ key: 'skills', label: '技能', tokens: 0 },
|
||||
{ key: 'other', label: '其他', tokens: 0 }
|
||||
{ key: 'other', label: '其他', tokens: agg.breakdown.get('other') ?? 0 }
|
||||
]
|
||||
const byKey = new Map(breakdown.map((b) => [b.key, b]))
|
||||
const systemTokens = Math.ceil(session.systemPrompt.length / 4)
|
||||
@@ -481,44 +482,12 @@ export class AgentService {
|
||||
byKey.get('systemPrompt')!.tokens = Math.max(0, systemTokens - skillsTokens)
|
||||
byKey.get('skills')!.tokens = skillsTokens
|
||||
|
||||
for (const message of session.messages) {
|
||||
const m = message as { role?: string; toolName?: unknown; content?: unknown }
|
||||
switch (m.role) {
|
||||
case 'user':
|
||||
case 'assistant':
|
||||
byKey.get('messages')!.tokens += estimateMessageTokens(message)
|
||||
break
|
||||
case 'toolResult': {
|
||||
const key =
|
||||
typeof m.toolName === 'string' && CUSTOM_TOOL_NAMES.has(m.toolName) ? 'customTools' : 'builtinTools'
|
||||
byKey.get(key)!.tokens += estimateMessageTokens(message)
|
||||
break
|
||||
}
|
||||
// bash 执行、扩展注入、压缩/分支摘要等注入上下文的消息
|
||||
case 'bashExecution':
|
||||
case 'custom':
|
||||
case 'compactionSummary':
|
||||
case 'branchSummary':
|
||||
byKey.get('other')!.tokens += estimateMessageTokens(message)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
const usage = session.getContextUsage()
|
||||
let lastOutputTokens: number | undefined
|
||||
for (let i = session.messages.length - 1; i >= 0; i--) {
|
||||
const m = session.messages[i] as { role?: string; usage?: { output?: unknown } }
|
||||
if (m.role === 'assistant' && m.usage && typeof m.usage.output === 'number') {
|
||||
lastOutputTokens = m.usage.output
|
||||
break
|
||||
}
|
||||
}
|
||||
return {
|
||||
tokens: usage && typeof usage.tokens === 'number' ? usage.tokens : null,
|
||||
contextWindow: usage?.contextWindow ?? 0,
|
||||
percent: usage && typeof usage.percent === 'number' ? usage.percent : null,
|
||||
...(lastOutputTokens !== undefined ? { lastOutputTokens } : {}),
|
||||
...(agg.lastOutputTokens !== undefined ? { lastOutputTokens: agg.lastOutputTokens } : {}),
|
||||
breakdown
|
||||
}
|
||||
}
|
||||
@@ -538,42 +507,114 @@ export class AgentService {
|
||||
return value
|
||||
}
|
||||
|
||||
// ---- 会话统计增量聚合 ----
|
||||
// emitStats 每条 assistant 消息落地都会触发;长会话下每次全量遍历估算
|
||||
// token 是 O(全部消息)。消息列表正常只追加:以「已聚合前缀长度 + 前缀
|
||||
// 末条消息引用」为锚点,每次只聚合新增尾部;压缩/回溯/会话切换/运行时
|
||||
// 重建后消息对象引用必变(或长度回退),锚点失配即自动整体重算。
|
||||
|
||||
private sessionAgg: {
|
||||
count: number
|
||||
anchor: unknown
|
||||
cacheInput: number
|
||||
cacheRead: number
|
||||
cacheWrite: number
|
||||
toolCalls: number
|
||||
toolErrors: number
|
||||
/** 消息侧 breakdown(systemPrompt/skills 在外层另算) */
|
||||
breakdown: Map<string, number>
|
||||
lastOutputTokens: number | undefined
|
||||
} | null = null
|
||||
|
||||
private aggregateSession(session: AgentSession) {
|
||||
const messages = session.messages
|
||||
let agg = this.sessionAgg
|
||||
if (
|
||||
agg === null ||
|
||||
messages.length < agg.count ||
|
||||
(agg.count > 0 && messages[agg.count - 1] !== agg.anchor)
|
||||
) {
|
||||
agg = {
|
||||
count: 0,
|
||||
anchor: null,
|
||||
cacheInput: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
toolCalls: 0,
|
||||
toolErrors: 0,
|
||||
breakdown: new Map([
|
||||
['messages', 0],
|
||||
['customTools', 0],
|
||||
['builtinTools', 0],
|
||||
['other', 0]
|
||||
]),
|
||||
lastOutputTokens: undefined
|
||||
}
|
||||
}
|
||||
for (let i = agg.count; i < messages.length; i++) {
|
||||
const m = messages[i] as {
|
||||
role?: string
|
||||
toolName?: unknown
|
||||
content?: unknown
|
||||
isError?: unknown
|
||||
usage?: { input?: unknown; output?: unknown; cacheRead?: unknown; cacheWrite?: unknown }
|
||||
}
|
||||
const bump = (key: string): void => {
|
||||
agg!.breakdown.set(key, (agg!.breakdown.get(key) ?? 0) + estimateMessageTokens(messages[i]))
|
||||
}
|
||||
switch (m.role) {
|
||||
case 'assistant': {
|
||||
bump('messages')
|
||||
if (m.usage) {
|
||||
agg.cacheInput += Number(m.usage.input ?? 0) || 0
|
||||
agg.cacheRead += Number(m.usage.cacheRead ?? 0) || 0
|
||||
agg.cacheWrite += Number(m.usage.cacheWrite ?? 0) || 0
|
||||
if (typeof m.usage.output === 'number') agg.lastOutputTokens = m.usage.output
|
||||
}
|
||||
if (Array.isArray(m.content)) {
|
||||
for (const block of m.content as Array<{ type?: unknown }>) {
|
||||
if (block && typeof block === 'object' && block.type === 'toolCall') agg.toolCalls += 1
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'user':
|
||||
bump('messages')
|
||||
break
|
||||
case 'toolResult': {
|
||||
if (m.isError === true) agg.toolErrors += 1
|
||||
bump(
|
||||
typeof m.toolName === 'string' && CUSTOM_TOOL_NAMES.has(m.toolName) ? 'customTools' : 'builtinTools'
|
||||
)
|
||||
break
|
||||
}
|
||||
// bash 执行、扩展注入、压缩/分支摘要等注入上下文的消息
|
||||
case 'bashExecution':
|
||||
case 'custom':
|
||||
case 'compactionSummary':
|
||||
case 'branchSummary':
|
||||
bump('other')
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
agg.count = messages.length
|
||||
agg.anchor = messages.length > 0 ? messages[messages.length - 1] : null
|
||||
this.sessionAgg = agg
|
||||
return agg
|
||||
}
|
||||
|
||||
private async emitStats(): Promise<void> {
|
||||
try {
|
||||
const runtime = await this.getRuntime()
|
||||
const session = runtime.session
|
||||
const stats = session.getSessionStats()
|
||||
const usage = session.getContextUsage()
|
||||
const agg = this.aggregateSession(session)
|
||||
// 缓存命中率:读缓存 / 全部提示 token(input + cacheRead + cacheWrite),
|
||||
// 按本会话所有 assistant 响应聚合,0 表示会话内尚无任何响应
|
||||
let cacheInput = 0
|
||||
let cacheRead = 0
|
||||
let cacheWrite = 0
|
||||
let toolCalls = 0
|
||||
let toolErrors = 0
|
||||
for (const message of session.messages) {
|
||||
const m = message as {
|
||||
role?: string
|
||||
usage?: { input?: unknown; cacheRead?: unknown; cacheWrite?: unknown }
|
||||
content?: unknown
|
||||
isError?: unknown
|
||||
}
|
||||
if (m.role === 'assistant') {
|
||||
if (m.usage) {
|
||||
cacheInput += Number(m.usage.input ?? 0) || 0
|
||||
cacheRead += Number(m.usage.cacheRead ?? 0) || 0
|
||||
cacheWrite += Number(m.usage.cacheWrite ?? 0) || 0
|
||||
}
|
||||
if (Array.isArray(m.content)) {
|
||||
for (const block of m.content as Array<{ type?: unknown }>) {
|
||||
if (block && typeof block === 'object' && block.type === 'toolCall') toolCalls += 1
|
||||
}
|
||||
}
|
||||
} else if (m.role === 'toolResult' && m.isError === true) {
|
||||
toolErrors += 1
|
||||
}
|
||||
}
|
||||
const denominator = cacheInput + cacheRead + cacheWrite
|
||||
const denominator = agg.cacheInput + agg.cacheRead + agg.cacheWrite
|
||||
const info: SessionStatsInfo = {
|
||||
tokens: { ...stats.tokens },
|
||||
cost: stats.cost,
|
||||
@@ -581,15 +622,15 @@ export class AgentService {
|
||||
context: this.buildContextInfo(session),
|
||||
...(denominator > 0
|
||||
? {
|
||||
cacheHitRate: Math.round((cacheRead / denominator) * 100),
|
||||
cacheTokens: { read: cacheRead, write: cacheWrite, input: cacheInput }
|
||||
cacheHitRate: Math.round((agg.cacheRead / denominator) * 100),
|
||||
cacheTokens: { read: agg.cacheRead, write: agg.cacheWrite, input: agg.cacheInput }
|
||||
}
|
||||
: {}),
|
||||
...(toolCalls > 0
|
||||
...(agg.toolCalls > 0
|
||||
? {
|
||||
toolStats: {
|
||||
calls: toolCalls,
|
||||
successRate: Math.round(((toolCalls - toolErrors) / toolCalls) * 100)
|
||||
calls: agg.toolCalls,
|
||||
successRate: Math.round(((agg.toolCalls - agg.toolErrors) / agg.toolCalls) * 100)
|
||||
}
|
||||
}
|
||||
: {})
|
||||
|
||||
Reference in New Issue
Block a user