Skip to content

Commit 025b82a

Browse files
committed
chore: 컨텍스트 캐싱 효과 측정 — cachedContentTokenCount 기록
- generation_log에 cached_tokens 추가(V12) + 생성기 3종이 usageMetadata의 cachedContentTokenCount를 캡처(댓글·답글·카드 output에 cachedTokens) - 카드 생성도 generation_log에 기록(GenerationType.CARD 추가) — 대화당 전체 측정 가능 - 대시보드 품질 지표에 캐시 토큰·적중률 노출 - 총 토큰(totalTokenCount)은 캐싱으로 안 줄고, 실효 비용은 cached_tokens로 계산
1 parent c0644c9 commit 025b82a

18 files changed

Lines changed: 74 additions & 19 deletions

File tree

admin-web/src/pages/dashboard/quality-section.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ interface QualityStats {
2222
avgLatencyMs: number
2323
p95LatencyMs: number
2424
totalTokens: number
25+
cachedTokens: number
26+
cacheHitRate: number | null
2527
stuckPending: number
2628
dailyGeneration: DailyGeneration[]
2729
}
@@ -101,7 +103,12 @@ export function QualitySection() {
101103
/>
102104
<MetricCard label="평균 지연" value={latency(stats.avgLatencyMs)} icon={<Gauge className="size-4" />} />
103105
<MetricCard label="p95 지연" value={latency(stats.p95LatencyMs)} hint="상위 5% 대기시간" />
104-
<MetricCard label="토큰 사용량" value={stats.totalTokens.toLocaleString()} hint="최근 기간 누적" icon={<Coins className="size-4" />} />
106+
<MetricCard
107+
label="토큰 사용량"
108+
value={stats.totalTokens.toLocaleString()}
109+
hint={stats.cacheHitRate === null ? '최근 기간 누적' : `캐시 적중 ${stats.cacheHitRate}% (${stats.cachedTokens.toLocaleString()})`}
110+
icon={<Coins className="size-4" />}
111+
/>
105112
<MetricCard label="총 생성 요청" value={stats.totalGenerations.toLocaleString()} hint="최근 기간" />
106113
</div>
107114

src/main/kotlin/com/nexters/gamss/admin/controller/dto/QualityStatsResponse.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ data class QualityStatsResponse(
2020
val avgLatencyMs: Long,
2121
@field:Schema(description = "p95 생성 지연(ms)", example = "5300")
2222
val p95LatencyMs: Long,
23-
@field:Schema(description = "누적 토큰 사용량(최근 기간)", example = "184320")
23+
@field:Schema(description = "누적 토큰 사용량(총합, 캐싱과 무관하게 처리된 전체 토큰)", example = "184320")
2424
val totalTokens: Long,
25+
@field:Schema(description = "그중 컨텍스트 캐시로 처리돼 할인 과금된 토큰", example = "120500")
26+
val cachedTokens: Long,
27+
@field:Schema(description = "캐시 적중률(%). 총 토큰 대비 캐시 토큰 비율. 총 토큰이 0이면 null", example = "65.4", nullable = true)
28+
val cacheHitRate: Double?,
2529
@field:Schema(description = "막힌 PENDING(고아 생성) 수 — 즉시 대응 필요 신호", example = "0")
2630
val stuckPending: Long,
2731
@field:Schema(description = "일별 생성 성공/실패 추이(오래된 날 → 오늘)")

src/main/kotlin/com/nexters/gamss/admin/service/QualityStatsService.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class QualityStatsService(
3535
val retried = logs.count { it.attemptCount > 1 }.toLong()
3636
val latencies = logs.map { it.latencyMs }.sorted()
3737
val stuckBefore = Instant.now().minus(conversationProperties.commentPendingTimeout)
38+
val totalTokens = logs.sumOf { (it.usedTokens ?: 0).toLong() }
39+
val cachedTokens = logs.sumOf { (it.cachedTokens ?: 0).toLong() }
3840

3941
return QualityStatsResponse(
4042
totalGenerations = total,
@@ -45,7 +47,9 @@ class QualityStatsService(
4547
retryRate = percentageOrNull(retried, total),
4648
avgLatencyMs = if (latencies.isEmpty()) 0 else latencies.average().roundToLong(),
4749
p95LatencyMs = percentile(latencies, P95),
48-
totalTokens = logs.sumOf { (it.usedTokens ?: 0).toLong() },
50+
totalTokens = totalTokens,
51+
cachedTokens = cachedTokens,
52+
cacheHitRate = percentageOrNull(cachedTokens, totalTokens),
4953
stuckPending = messageRepository.countByCommentStatusOlderThan(CommentStatus.PENDING, stuckBefore),
5054
dailyGeneration = buildDailyGeneration(today, days, logs),
5155
)

src/main/kotlin/com/nexters/gamss/card/service/CardService.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import com.nexters.gamss.global.exception.BusinessException
1010
import com.nexters.gamss.global.exception.ErrorCode
1111
import com.nexters.gamss.llm.error.CardGenerationFailedException
1212
import com.nexters.gamss.llm.generation.CardMessageGenerator
13+
import com.nexters.gamss.monitoring.domain.GenerationType
14+
import com.nexters.gamss.monitoring.service.GenerationLogRecorder
1315
import org.springframework.dao.DataIntegrityViolationException
1416
import org.springframework.stereotype.Service
1517
import org.springframework.transaction.annotation.Transactional
@@ -26,6 +28,7 @@ class CardService(
2628
private val cardRepository: CardRepository,
2729
private val conversationRepository: ConversationRepository,
2830
private val cardMessageGenerator: CardMessageGenerator,
31+
private val generationLogRecorder: GenerationLogRecorder,
2932
) {
3033
/**
3134
* 종료된 대화에 대해 대표 감정 캐릭터의 한 줄 대사를 생성해 카드를 저장한다.
@@ -46,12 +49,28 @@ class CardService(
4649
if (cardRepository.existsByConversationId(conversationId)) {
4750
throw BusinessException(ErrorCode.CARD_ALREADY_EXISTS)
4851
}
52+
val startedAt = System.currentTimeMillis()
4953
val output =
5054
try {
5155
cardMessageGenerator.generate(emotion, summary)
5256
} catch (e: CardGenerationFailedException) {
57+
generationLogRecorder.record(
58+
type = GenerationType.CARD,
59+
success = false,
60+
attemptCount = 1,
61+
latencyMs = System.currentTimeMillis() - startedAt,
62+
failureReason = (e.cause ?: e).javaClass.simpleName,
63+
)
5364
throw BusinessException(ErrorCode.CARD_GENERATION_FAILED, e.message)
5465
}
66+
generationLogRecorder.record(
67+
type = GenerationType.CARD,
68+
success = true,
69+
attemptCount = 1,
70+
latencyMs = System.currentTimeMillis() - startedAt,
71+
usedTokens = output.usedTokens,
72+
cachedTokens = output.cachedTokens,
73+
)
5574
val card =
5675
Card(
5776
memberId = memberId,

src/main/kotlin/com/nexters/gamss/conversation/service/CommentGenerationService.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class CommentGenerationService(
188188
attemptCount = attempt + 1,
189189
latencyMs = System.currentTimeMillis() - startedAt,
190190
usedTokens = output.usedTokens,
191+
cachedTokens = output.cachedTokens,
191192
)
192193
return output
193194
} catch (e: CommentGenerationFailedException) {
@@ -244,6 +245,7 @@ class CommentGenerationService(
244245
attemptCount = attempt + 1,
245246
latencyMs = System.currentTimeMillis() - startedAt,
246247
usedTokens = output.usedTokens,
248+
cachedTokens = output.cachedTokens,
247249
)
248250
return output
249251
} catch (e: CommentGenerationFailedException) {

src/main/kotlin/com/nexters/gamss/llm/generation/CardMessageOutput.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ package com.nexters.gamss.llm.generation
44
data class CardMessageOutput(
55
val message: String,
66
val usedTokens: Int,
7+
val cachedTokens: Int,
78
)

src/main/kotlin/com/nexters/gamss/llm/generation/CommentGenerationOutput.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ import com.nexters.gamss.llm.parsing.CommentFeed
66
data class CommentGenerationOutput(
77
val feed: CommentFeed,
88
val usedTokens: Int,
9+
val cachedTokens: Int,
910
)

src/main/kotlin/com/nexters/gamss/llm/generation/GeminiCardMessageGenerator.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ class GeminiCardMessageGenerator(
5353

5454
val message = parseLine(text)
5555
val usedTokens = response.usageMetadata().flatMap { it.totalTokenCount() }.orElse(0)
56-
return CardMessageOutput(message, usedTokens)
56+
val cachedTokens = response.usageMetadata().flatMap { it.cachedContentTokenCount() }.orElse(0)
57+
return CardMessageOutput(message, usedTokens, cachedTokens)
5758
}
5859

5960
private fun parseLine(text: String): String {

src/main/kotlin/com/nexters/gamss/llm/generation/GeminiCommentGenerator.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ class GeminiCommentGenerator(
6464

6565
val feed = commentFeedJsonParser.parse(text)
6666
val usedTokens = response.usageMetadata().flatMap { it.totalTokenCount() }.orElse(0)
67-
return CommentGenerationOutput(feed, usedTokens)
67+
val cachedTokens = response.usageMetadata().flatMap { it.cachedContentTokenCount() }.orElse(0)
68+
return CommentGenerationOutput(feed, usedTokens, cachedTokens)
6869
}
6970

7071
override fun generateReply(
@@ -91,7 +92,8 @@ class GeminiCommentGenerator(
9192

9293
val replyText = replyJsonParser.parse(text)
9394
val usedTokens = response.usageMetadata().flatMap { it.totalTokenCount() }.orElse(0)
94-
return ReplyGenerationOutput(replyText, usedTokens)
95+
val cachedTokens = response.usageMetadata().flatMap { it.cachedContentTokenCount() }.orElse(0)
96+
return ReplyGenerationOutput(replyText, usedTokens, cachedTokens)
9597
}
9698

9799
private fun buildConfig(

src/main/kotlin/com/nexters/gamss/llm/generation/ReplyGenerationOutput.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ package com.nexters.gamss.llm.generation
44
data class ReplyGenerationOutput(
55
val text: String,
66
val usedTokens: Int,
7+
val cachedTokens: Int,
78
)

0 commit comments

Comments
 (0)