perf: 会话统计增量聚合——emitStats/buildContextInfo 不再每轮全量遍历
This commit is contained in:
@@ -467,13 +467,14 @@ export class AgentService {
|
|||||||
* 类目:消息 / 自定义工具 / 系统工具 / 系统提示词 / 技能 / 其他;
|
* 类目:消息 / 自定义工具 / 系统工具 / 系统提示词 / 技能 / 其他;
|
||||||
* 技能块注入在系统提示词内,从提示词估算中拆出(不重复计数)。 */
|
* 技能块注入在系统提示词内,从提示词估算中拆出(不重复计数)。 */
|
||||||
private buildContextInfo(session: AgentSession): ContextUsageInfo {
|
private buildContextInfo(session: AgentSession): ContextUsageInfo {
|
||||||
|
const agg = this.aggregateSession(session)
|
||||||
const breakdown: ContextBreakdownEntry[] = [
|
const breakdown: ContextBreakdownEntry[] = [
|
||||||
{ key: 'messages', label: '消息', tokens: 0 },
|
{ key: 'messages', label: '消息', tokens: agg.breakdown.get('messages') ?? 0 },
|
||||||
{ key: 'customTools', label: '自定义工具', tokens: 0 },
|
{ key: 'customTools', label: '自定义工具', tokens: agg.breakdown.get('customTools') ?? 0 },
|
||||||
{ key: 'builtinTools', label: '系统工具', tokens: 0 },
|
{ key: 'builtinTools', label: '系统工具', tokens: agg.breakdown.get('builtinTools') ?? 0 },
|
||||||
{ key: 'systemPrompt', label: '系统提示词', tokens: 0 },
|
{ key: 'systemPrompt', label: '系统提示词', tokens: 0 },
|
||||||
{ key: 'skills', 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 byKey = new Map(breakdown.map((b) => [b.key, b]))
|
||||||
const systemTokens = Math.ceil(session.systemPrompt.length / 4)
|
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('systemPrompt')!.tokens = Math.max(0, systemTokens - skillsTokens)
|
||||||
byKey.get('skills')!.tokens = 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()
|
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 {
|
return {
|
||||||
tokens: usage && typeof usage.tokens === 'number' ? usage.tokens : null,
|
tokens: usage && typeof usage.tokens === 'number' ? usage.tokens : null,
|
||||||
contextWindow: usage?.contextWindow ?? 0,
|
contextWindow: usage?.contextWindow ?? 0,
|
||||||
percent: usage && typeof usage.percent === 'number' ? usage.percent : null,
|
percent: usage && typeof usage.percent === 'number' ? usage.percent : null,
|
||||||
...(lastOutputTokens !== undefined ? { lastOutputTokens } : {}),
|
...(agg.lastOutputTokens !== undefined ? { lastOutputTokens: agg.lastOutputTokens } : {}),
|
||||||
breakdown
|
breakdown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,42 +507,114 @@ export class AgentService {
|
|||||||
return value
|
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> {
|
private async emitStats(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const runtime = await this.getRuntime()
|
const runtime = await this.getRuntime()
|
||||||
const session = runtime.session
|
const session = runtime.session
|
||||||
const stats = session.getSessionStats()
|
const stats = session.getSessionStats()
|
||||||
const usage = session.getContextUsage()
|
const usage = session.getContextUsage()
|
||||||
|
const agg = this.aggregateSession(session)
|
||||||
// 缓存命中率:读缓存 / 全部提示 token(input + cacheRead + cacheWrite),
|
// 缓存命中率:读缓存 / 全部提示 token(input + cacheRead + cacheWrite),
|
||||||
// 按本会话所有 assistant 响应聚合,0 表示会话内尚无任何响应
|
// 按本会话所有 assistant 响应聚合,0 表示会话内尚无任何响应
|
||||||
let cacheInput = 0
|
const denominator = agg.cacheInput + agg.cacheRead + agg.cacheWrite
|
||||||
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 info: SessionStatsInfo = {
|
const info: SessionStatsInfo = {
|
||||||
tokens: { ...stats.tokens },
|
tokens: { ...stats.tokens },
|
||||||
cost: stats.cost,
|
cost: stats.cost,
|
||||||
@@ -581,15 +622,15 @@ export class AgentService {
|
|||||||
context: this.buildContextInfo(session),
|
context: this.buildContextInfo(session),
|
||||||
...(denominator > 0
|
...(denominator > 0
|
||||||
? {
|
? {
|
||||||
cacheHitRate: Math.round((cacheRead / denominator) * 100),
|
cacheHitRate: Math.round((agg.cacheRead / denominator) * 100),
|
||||||
cacheTokens: { read: cacheRead, write: cacheWrite, input: cacheInput }
|
cacheTokens: { read: agg.cacheRead, write: agg.cacheWrite, input: agg.cacheInput }
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(toolCalls > 0
|
...(agg.toolCalls > 0
|
||||||
? {
|
? {
|
||||||
toolStats: {
|
toolStats: {
|
||||||
calls: toolCalls,
|
calls: agg.toolCalls,
|
||||||
successRate: Math.round(((toolCalls - toolErrors) / toolCalls) * 100)
|
successRate: Math.round(((agg.toolCalls - agg.toolErrors) / agg.toolCalls) * 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
|
|||||||
Reference in New Issue
Block a user