-
Notifications
You must be signed in to change notification settings - Fork 0
Feature: chat result hanlder #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
데코레이터 패턴 (리플렉션) 방법을 사용한 비용 산출 방법 각 주요 프로세스마다 시간, 비용을 측정하여 기록하고 저장
set을 통한 접근제어
Walkthrough채팅 파이프라인이 컨텍스트 기반(ChatProcessContext)으로 재구성되었고, TTS 사용 플래그(UseTTS)가 요청/명령 모델에 추가되었습니다. 비용/메트릭 수집 인프라(IChatMetricsService, 데코레이터)가 도입되어 LLM/입력해석/TTS 단계에 적용되었습니다. 여러 이전 컨텍스트/결과 모델이 제거되고, 포맷/프로세서/서비스가 이에 맞춰 서명과 흐름을 변경했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client as Client
participant ChatSvc as ChatService
participant Metrics as IChatMetricsService
participant UIA as ICostTrackingDecorator<UserInputAnalysisProcessor>
participant LLM as ICostTrackingDecorator<ChatLLMProcessor>
participant TTS as ICostTrackingDecorator<ChatTTSProcessor>
participant Result as ChatResultProcessor
participant WS as IWebSocketManager
participant Fail as ChatFailureHandler
Client->>ChatSvc: EnqueueChatRequestAsync(ChatRequest)
ChatSvc->>Metrics: StartChatMetrics(sessionId,userId,characterId)
ChatSvc->>ChatSvc: Build ChatProcessContext
ChatSvc->>UIA: ProcessAsync(userInput, history)
UIA->>Metrics: StartProcessMetrics("User_Input_Analysis")
UIA-->>UIA: 내부 UserInputAnalysis 실행
UIA->>Metrics: EndProcessMetrics(cost)
ChatSvc->>LLM: ProcessAsync(context)
LLM->>Metrics: StartProcessMetrics("LLM_Processing")
LLM-->>LLM: 내부 ChatLLMProcessor 실행 (세그먼트/비용 설정)
LLM->>Metrics: EndProcessMetrics(cost)
alt context.UseTTS == true
ChatSvc->>TTS: ProcessAsync(context)
TTS->>Metrics: StartProcessMetrics("TTS_Processing")
TTS-->>TTS: 내부 ChatTTSProcessor 실행 (오디오 적용/비용 누적)
TTS->>Metrics: EndProcessMetrics(cost)
end
ChatSvc->>Result: PersistResultsAsync(context)
ChatSvc->>Result: SendResultsAsync(context)
Result-->>WS: WebSocketMessage들 전송
ChatSvc->>Metrics: EndChatMetrics()
ChatSvc->>Metrics: LogChatMetrics()
Note over ChatSvc,Metrics: 성공 시
rect rgba(255,230,230,0.4)
opt 예외 발생
ChatSvc->>Fail: HandleFailureAsync(context, ex)
ChatSvc->>Metrics: EndChatMetrics()
ChatSvc->>Metrics: LogChatMetrics()
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs (2)
39-42
: Null 안전한 사용자 메시지 프리뷰 처리 필요
request.UserMessage[..Math.Min(50, request.UserMessage.Length)]
는UserMessage
가 null일 때 NRE가 발생합니다. 안전하게 프리뷰를 계산해 주세요.- _logger.LogDebug("LLM 요청 시작: {Model}, 사용자 메시지: {UserMessage}", - request.Model, - request.UserMessage[..Math.Min(50, request.UserMessage.Length)]); + var preview = request.UserMessage ?? string.Empty; + preview = preview.Length > 50 ? preview[..50] : preview; + _logger.LogDebug("LLM 요청 시작: {Model}, 사용자 메시지: {UserMessage}", request.Model, preview);
78-88
: 모의 응답(Mock)에서 Success=true와 토큰 설정은 비용/메트릭을 오염시킬 수 있음연결 오류 시 모의 응답을
Success = true
로 반환하고, 토큰/시간을 채워 넣으면 실제와 다른 비용이 집계될 수 있습니다. 최소한 프로덕션에서는 비활성화하거나, 모의 응답 시 토큰을 0으로 설정하거나, 별도 플래그를 두는 것이 안전합니다.옵션 A: 환경/설정으로 모의 응답 사용 여부 가드
- return new LLMResponse + if (!_enableMockOnError) + { + return new LLMResponse { Success = false, ErrorMessage = "LLM 서비스 연결 오류(네트워크)" }; + } + return new LLMResponse { Success = true, Id = "mock-chatcmpl-" + Guid.NewGuid().ToString("N")[..8], Response = "안녕하세요! 저는 현재 Mock 모드로 동작하고 있습니다. 실제 LLM 서비스가 연결되지 않았습니다.", - TokensUsed = 50, - InputTokens = 30, - OutputTokens = 20, + TokensUsed = 0, + InputTokens = 0, + OutputTokens = 0, ResponseTime = 100 };생성자에 설정 주입:
- public LLMClient(HttpClient httpClient, ILogger<LLMClient> logger, IConfiguration configuration) + public LLMClient(HttpClient httpClient, ILogger<LLMClient> logger, IConfiguration configuration) { ... + _enableMockOnError = bool.TryParse(configuration["LLM:EnableMockOnError"], out var v) && v; } + private readonly bool _enableMockOnError;추가로,
ResponseTime
단위(ms/s)도 주석이나 XML 문서로 명시해 주세요.
🧹 Nitpick comments (33)
ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs (1)
10-27
: 의존성 주입 구조가 적절함필요한 서비스들이 올바르게 주입되고 있습니다. 다만
_conversationService
와_memoryClient
가 현재 코드에서 사용되지 않고 있습니다.ProjectVG.Api/Models/Chat/Request/ChatRequest.cs (1)
29-31
: UseTTS 속성 기본값 및 네이밍 검토 요청
기본값 변경 제안
현재UseTTS { get; set; } = true;
로 설정되어 있어, 클라이언트가use_tts
값을 보내지 않아도 TTS가 자동 활성화됩니다.
• 의도한 동작이라면 유지 가능하나, 별도 명시하지 않으면 TTS가 켜져 비용·지연이 증가할 수 있으므로
• 안전하게 기본값을false
로 변경하는 방안을 검토해 주세요.네이밍 컨벤션 정합성
.NET 명명 규칙상 약어가 세 글자 이상일 때는 PascalCase(첫 글자만 대문자)를 권장합니다.
UseTTS
→UseTts
로 리네이밍할 경우, 해당 속성을 참조하는 모든 위치를 함께 변경해야 합니다.영향 범위(스크립트 검사 결과 기준)
- ProjectVG.Api/Models/Chat/Request/ChatRequest.cs
• 정의: 라인 13, 21, 29–30- ProcessChatCommand 초기화부
• 할당문: 라인 35, 49- ChatContext 처리 로직
• 조건문: 라인 22- 로그 출력
•_logger.LogDebug(..., context.UseTTS, ...)
: 라인 23–24ProjectVG.Application/ApplicationServiceCollectionExtensions.cs (1)
41-44
: 비용 추적 데코레이터 레이블 일관성 제안현재 레이블이
"LLM_Processing"
,"TTS_Processing"
,"User_Input_Analysis"
로 혼재(스네이크/파스칼/스페이스 없음)되어 있습니다. 대시 또는 파스칼케이스로 일관화하면 대시보드/로그 집계가 깔끔해집니다. 예:"LLM"
,"TTS"
,"UserInputAnalysis"
.- services.AddCostTrackingDecorator<ChatLLMProcessor>("LLM_Processing"); - services.AddCostTrackingDecorator<ChatTTSProcessor>("TTS_Processing"); - services.AddCostTrackingDecorator<UserInputAnalysisProcessor>("User_Input_Analysis"); + services.AddCostTrackingDecorator<ChatLLMProcessor>("LLM"); + services.AddCostTrackingDecorator<ChatTTSProcessor>("TTS"); + services.AddCostTrackingDecorator<UserInputAnalysisProcessor>("UserInputAnalysis");ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (1)
207-210
: 코스트 계산 중앙화 반영: LGTM + 단위 주석 추가 제안
LLMModelInfo.CalculateCost(Model, promptTokens, completionTokens)
위임으로 일원화된 점 좋습니다. 비용 단위(예: 마이크로달러/센트 등)를 명확히 하기 위해 메서드 또는 클래스 수준 XML 주석으로 단위를 명시하면 이후 메트릭 합산/표시(달러 환산 등)에서 혼선을 줄일 수 있습니다.ProjectVG.Application/Models/Chat/UserInputAnalysis.cs (1)
12-12
: Cost 속성: 단위 명시 주석 추가 제안
double Cost
의 화폐/단위가 명확하지 않습니다. XML 주석에 단위를 명시(예: “마이크로달러 단위(1e-5 USD)”)하면 비용 합산/로그 포맷에서 일관성이 보장됩니다.ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs (3)
32-33
: Accept 헤더 추가 시 중복 추가 가능성이미 구성된
HttpClient
에 Accept 헤더가 존재하면Add
호출이 예외를 던질 수 있습니다. 존재 여부 확인 후 추가하도록 변경을 권장합니다.- _httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + if (!_httpClient.DefaultRequestHeaders.Accept.Any(h => h.MediaType == "application/json")) + { + _httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json"); + }필요한 using:
using System.Net.Http.Headers;
63-66
: 토큰 로그 포맷을 새 필드에 맞춰 갱신새로 도입된
InputTokens
/OutputTokens
가 있는데, 로그는 여전히TokensUsed
만 기록합니다. 비용 집계의 가독성을 위해 입력/출력 토큰을 함께 로그하세요.- _logger.LogInformation("LLM 요청 성공: 토큰 {TokensUsed}, 응답 길이 {ResponseLength}", - llmResponse.TokensUsed, - llmResponse.Response?.Length ?? 0); + _logger.LogInformation( + "LLM 요청 성공: 입력 토큰 {InputTokens}, 출력 토큰 {OutputTokens}, 총 토큰 {TokensUsed}, 응답 길이 {ResponseLength}", + llmResponse.InputTokens, + llmResponse.OutputTokens, + llmResponse.TokensUsed, + llmResponse.Response?.Length ?? 0);
44-45
: 취소 토큰 미전달로 요청 취소 제어 한계
PostAsync
에CancellationToken
이 전달되지 않아 상위 레이어에서 세밀한 타임아웃/취소 제어가 어렵습니다. 메서드 시그니처에CancellationToken
을 추가하고 전파를 권장합니다.- public async Task<LLMResponse> SendRequestAsync(LLMRequest request) + public async Task<LLMResponse> SendRequestAsync(LLMRequest request, CancellationToken ct = default) { ... - using var response = await _httpClient.PostAsync("api/v1/chat", jsonContent); + using var response = await _httpClient.PostAsync("api/v1/chat", jsonContent, ct); ... } - return await SendRequestAsync(request); + return await SendRequestAsync(request, CancellationToken.None);원하시면 상위
CreateTextResponseAsync
에도 선택적CancellationToken
을 노출하는 패치까지 만들어 드리겠습니다.Also applies to: 110-133
ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs (1)
9-11
: 메서드 시그니처에 CancellationToken 지원 제안긴 작업(LLM 호출, TTS 등)을 데코레이터로 감쌀 때 취소 전파가 필요합니다. 선택적
CancellationToken
추가를 고려해 주세요. 구현인CostTrackingDecorator<T>
에도 전달되어야 합니다.- Task ProcessAsync(ChatProcessContext context); - Task<UserInputAnalysis> ProcessAsync(string userInput, IEnumerable<ConversationHistory> conversationHistory); + Task ProcessAsync(ChatProcessContext context, CancellationToken ct = default); + Task<UserInputAnalysis> ProcessAsync(string userInput, IEnumerable<ConversationHistory> conversationHistory, CancellationToken ct = default);원하시면 연쇄 변경(데코레이터/프로세서/서비스 등록부)까지 한 번에 패치해 드립니다.
ProjectVG.Common/Constants/TTSCostInfo.cs (2)
5-9
: 단위(밀리센트) 명시 및 문서화 권장본 클래스는 비용 단위를 "밀리센트(1달러 = 100,000)"로 가정합니다. 유지보수성을 위해 XML 주석으로 단위를 명시하고, 상수명/주석에 예시를 추가하면 혼선을 줄일 수 있습니다.
예: "TTS_COST_PER_SECOND: 초당 비용(밀리센트)". 또한 외부 로깅/메트릭(
ChatMetricsService
)과 동일한 단위를 사용함을 주석으로 남겨 주세요.
15-19
: 부동소수 오차 회피 및 음수 입력 방지현재 계산은 부동소수 연산을 사용합니다. 0.1초 단위 과금이라면 정수 연산으로 일관되고 재현 가능한 결과를 보장할 수 있습니다. 또한 음수 입력을 0으로 클램프하세요.
옵션 A(리턴 타입 유지, 내부 정수화):
- public static double CalculateTTSCost(double durationInSeconds) + public static double CalculateTTSCost(double durationInSeconds) { - var roundedDuration = Math.Ceiling(durationInSeconds * 10) / 10.0; - return Math.Ceiling(roundedDuration * TTS_COST_PER_SECOND); + var tenths = Math.Max(0, (long)Math.Ceiling(durationInSeconds * 10)); + // TTS_COST_PER_SECOND: 초당 비용(밀리센트) + // 0.1초 단가 = TTS_COST_PER_SECOND / 10 + var costPerTenth = TTS_COST_PER_SECOND / 10.0; + return Math.Ceiling(tenths * costPerTenth); }옵션 B(권장, 정수 반환 — 시스템 전반 타입 영향 검토 필요):
- public static double CalculateTTSCost(double durationInSeconds) + public static long CalculateTTSCost(double durationInSeconds) { - var roundedDuration = Math.Ceiling(durationInSeconds * 10) / 10.0; - return Math.Ceiling(roundedDuration * TTS_COST_PER_SECOND); + var tenths = Math.Max(0, (long)Math.Ceiling(durationInSeconds * 10)); + // 1초 = 10 * 0.1초 + // 비용(밀리센트) = tenths * (초당 비용(밀리센트) / 10) + return (long)Math.Ceiling(tenths * (TTS_COST_PER_SECOND / 10.0)); }추가로, 경계값 테스트(예: 0.04s→0.1s, 0.05s→0.1s, 0.149s→0.2s, 음수 입력→0)를 포함한 단위 테스트 작성을 권장합니다. 필요하시면 템플릿 제공 가능합니다.
ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecoratorFactory.cs (2)
15-17
: 원본 서비스 중복 등록 가능성(T 중복 Add)호출부에서 이미
services.AddScoped<ChatLLMProcessor>();
등으로 등록한 뒤AddCostTrackingDecorator<T>
를 호출하고 있습니다. 여기서 다시T
를 등록하면 동일 서비스가 중복 등록됩니다. MS.DI에서는 마지막 등록이 우선되지만, 혼란/중복 dispose 등의 리스크가 있어 지양합니다.- // 원본 서비스 등록 - services.Add(new ServiceDescriptor(typeof(T), typeof(T), lifetime)); + // 원본 서비스는 호출부에서 등록한다고 가정 (중복 등록 방지) + // 필요 시 TryAdd*로 idempotent 처리 가능또는
Microsoft.Extensions.DependencyInjection.Extensions
의TryAddScoped/TryAdd
를 사용해 중복을 방지하세요.+ using Microsoft.Extensions.DependencyInjection.Extensions; ... - services.Add(new ServiceDescriptor(typeof(T), typeof(T), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(T), typeof(T), lifetime));
19-27
: 데코레이터 생성자 의존성 확인과 예외 메시지 보강 제안
GetRequiredService
는 누락 시 예외를 던지지만, 어떤 프로세스에 대한 데코레이터 생성 실패인지 메시지를 보강하면 트러블슈팅이 수월합니다. 또한processName
유효성 체크(Null/Whitespace)를 권장합니다.- provider => + provider => { + if (string.IsNullOrWhiteSpace(processName)) + throw new ArgumentException("processName must be non-empty", nameof(processName)); var service = provider.GetRequiredService<T>(); var metricsService = provider.GetRequiredService<IChatMetricsService>(); return new CostTrackingDecorator<T>(service, metricsService, processName); },ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs (3)
28-40
: 메트릭 시작 전 초기화 누락 시 무소음(silent) 동작현재
_currentMetrics.Value == null
이면 조용히 return 합니다. 실제로는 파이프라인 어디선가 StartChatMetrics 호출 누락, 또는 스코프 경계 문제일 수 있으므로 경고 로그를 남기는 편이 디버깅에 유리합니다. 또한processName
유효성도 확인해 주세요.- if (_currentMetrics.Value == null) return; + if (_currentMetrics.Value == null) { + _logger.LogWarning("StartProcessMetrics 호출됨: 현재 ChatMetrics 컨텍스트가 없습니다. process={ProcessName}", processName); + return; + } + if (string.IsNullOrWhiteSpace(processName)) { + _logger.LogWarning("유효하지 않은 프로세스 이름입니다."); + return; + }
46-57
: 종료 대상 프로세스 미발견 시 처리/로깅 없음동일
processName
으로 여러 번 시작된 경우 첫 미종료 항목을 종료하는 설계는 합리적입니다. 다만 대상이 없을 때(시작 누락/이중 종료 등) 아무 로그가 없어 원인분석이 어렵습니다. 경고 로그를 추가하세요. 필요 시processName + 인덱스/고유ID
전략으로 중복명을 회피하는 개선도 고려해 볼 수 있습니다.- if (processMetrics != null) + if (processMetrics != null) { ... } + else + { + _logger.LogWarning("EndProcessMetrics: 종료할 프로세스를 찾지 못했습니다. process={ProcessName}", processName); + }
73-92
: 매직 넘버 제거 및 라이프사이클 정리
- 비용 스케일 100_000.0은 의미가 드러나지 않습니다. 상수로 추출하고 주석으로 단위를 명확히 해주세요.
- Log 후
_currentMetrics.Value
를 정리하지 않아(AsyncLocal 보관) 테스트/장기 실행 시 예상치 못한 재사용이 일어날 수 있습니다. Log 마지막에서 null로 클리어하는 것을 권장합니다(현재 ChatService가 EndChatMetrics→LogChatMetrics 순서를 보장).- var totalCostInDollars = (double)metrics.TotalCost / 100_000.0; + const double CostScale = 100_000.0; // 내부 비용단위를 USD로 변환하기 위한 스케일 + var totalCostInDollars = metrics.TotalCost / CostScale; ... foreach (var process in metrics.ProcessMetrics) { - var processCostInDollars = (double)process.Cost / 100_000.0; + var processCostInDollars = process.Cost / CostScale; _logger.LogInformation( " - {ProcessName}: {Duration}ms, 비용: ${Cost:F6}", process.ProcessName, process.Duration.TotalMilliseconds, processCostInDollars); } + // 메트릭 라이프사이클 종료 + _currentMetrics.Value = null;ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs (4)
29-36
: 메모리 저장 정책 확인 필요(전체 응답 저장 vs 요약 저장)
_memoryClient.AddMemoryAsync(context.MemoryStore, context.Response)
로 전체 응답을 저장합니다. 메모리 크기/검색 품질/PII 관점에서 요약본이나 핵심 키워드만 저장하는 정책이 더 적합할 수 있습니다. 저장 전략을 재검토해 주세요.
38-55
: 텍스트 전용 세그먼트에도 AudioFormat이 "wav"로 세팅됨텍스트만 있는 세그먼트에도
AudioFormat = "wav"
가 채워져 클라이언트가 오해할 수 있습니다. 오디오가 있을 때만 오디오 관련 필드를 설정해 주세요. 또한Text
가 null인 오디오 전용 세그먼트는 빈 문자열로 정규화하면 소비 측 NRE를 피할 수 있습니다.- foreach (var segment in context.Segments.OrderBy(s => s.Order)) { + foreach (var segment in context.Segments.OrderBy(s => s.Order)) { if (segment.IsEmpty) continue; - var integratedMessage = new IntegratedChatMessage { - SessionId = context.SessionId, - Text = segment.Text, - AudioFormat = segment.AudioContentType ?? "wav", - AudioLength = segment.AudioLength, - Timestamp = DateTime.UtcNow - }; - - integratedMessage.SetAudioData(segment.AudioData); + var integratedMessage = new IntegratedChatMessage { + SessionId = context.SessionId, + Text = segment.Text ?? string.Empty, + Timestamp = DateTime.UtcNow + }; + if (segment.HasAudio) { + integratedMessage.AudioFormat = segment.AudioContentType ?? "wav"; + integratedMessage.AudioLength = segment.AudioLength; + integratedMessage.SetAudioData(segment.AudioData); + } else { + // 오디오 없음 명시적 표기(직렬화 시 null) + integratedMessage.SetAudioData(null); + }
38-55
: 전송 순서 보장과 성능 트레이드오프세그먼트를 순차 await로 전송하면 순서는 보장되지만 지연이 커질 수 있습니다. 필요 시 소량 버퍼링(예: N개 묶음) 또는 서버측 큐잉으로 throughput을 개선할 수 있습니다. 단, 순서 보장이 핵심이면 현 방식 유지 권장.
57-59
: 세그먼트 카운트 로깅 기준 통일
!s.IsEmpty
기준으로 세고 있습니다. 상위 컴포넌트 로그/메트릭에서도 동일 기준(텍스트/오디오 중 하나 이상 존재)을 사용하도록 문서화하면 디버깅 시 혼선이 줄어듭니다.ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs (2)
24-33
: LLM 실패/이상 응답 가드 추가실서비스 호출 시 네트워크/서버 오류로
Success=false
또는Response
가 비었을 수 있습니다. 현재는 무조건 파싱/코스트 계산을 진행합니다. 실패 시 예외를 던져 상위 실패 핸들러로 위임하거나, 안전한 폴백을 적용해 주세요.var llmResponse = await _llmClient.CreateTextResponseAsync( ... ); + if (llmResponse == null || llmResponse.Success != true || string.IsNullOrWhiteSpace(llmResponse.Response)) + { + _logger.LogWarning("LLM 응답 실패 또는 비어 있음: 세션 {SessionId}, Id={Id}, Success={Success}", + context.SessionId, llmResponse?.Id, llmResponse?.Success); + throw new InvalidOperationException("LLM 응답이 유효하지 않습니다."); + }Also applies to: 35-43
38-41
: 중복 로그(콘솔+ILogger) 레벨 정리 제안Console.WriteLine과 LogDebug가 동일 내용을 중복 기록합니다. 운영 환경에서는 ILogger만 남기고 Console은 개발용 플래그 조건부 실행을 권장합니다.
ProjectVG.Application/Models/Chat/ChatMetrics.cs (2)
5-7
: 식별자 타입(Guid ↔ string) 혼용
ChatProcessContext
는Guid
(UserId/CharacterId), 메트릭 모델은 string입니다. 사용처에서ToString()
변환이 필요합니다. 메트릭 저장소/시각화 요구사항에 따라 Guid 보존이 필요하다면Guid
로 유지하거나 두 필드를 병행 보관하는 것도 방법입니다.
23-23
: AdditionalData 직렬화 호환성
Dictionary<string, object>?
는 로거/스토리지 직렬화 시 문제를 일으킬 수 있습니다. JSON 저장을 고려한다면Dictionary<string, string>
또는 명시적 DTO를 권장합니다.ProjectVG.Application/Models/Chat/ChatMessageSegment.cs (2)
12-15
: 공백만 있는 텍스트 처리 강화
HasText
가IsNullOrEmpty
기준이라 공백(" ")만 있는 경우에도 텍스트가 있다고 판단됩니다.IsNullOrWhiteSpace
를 사용하면 불필요한 빈 세그먼트 전송을 줄일 수 있습니다.IsEmpty
는HasText/HasAudio
기반이므로 자동으로 정합성 유지됩니다.- public bool HasText => !string.IsNullOrEmpty(Text); + public bool HasText => !string.IsNullOrWhiteSpace(Text);
26-31
: 오디오 데이터 해제 시 필드 정리 보완현재는 전달받은 파라미터 그대로 설정합니다.
audioData
가 null/빈 배열일 때AudioContentType/AudioLength
도 함께 null로 정리하면 상태 일관성이 좋아집니다.- public void SetAudioData(byte[]? audioData, string? audioContentType, float? audioLength) - { - AudioData = audioData; - AudioContentType = audioContentType; - AudioLength = audioLength; - } + public void SetAudioData(byte[]? audioData, string? audioContentType, float? audioLength) + { + if (audioData == null || audioData.Length == 0) + { + AudioData = null; + AudioContentType = null; + AudioLength = null; + return; + } + AudioData = audioData; + AudioContentType = audioContentType; + AudioLength = audioLength; + }ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs (2)
66-68
: 디버그 로그의 과도한 정보 출력프로덕션 환경에서도
Console.WriteLine
을 통해 비용 정보와 타입 정보를 출력하고 있습니다.조건부 컴파일이나 로거를 사용하도록 수정:
-Console.WriteLine($"[COST_TRACKING] {_processName} - 추출된 비용: {cost:F0} Cost"); -Console.WriteLine($"[COST_TRACKING] {_processName} - 컨텍스트 타입: {context?.GetType().Name}, Cost 속성 값: {context?.GetType().GetProperty("Cost")?.GetValue(context)}"); +#if DEBUG +Console.WriteLine($"[COST_TRACKING] {_processName} - 추출된 비용: {cost:F0} Cost"); +Console.WriteLine($"[COST_TRACKING] {_processName} - 컨텍스트 타입: {context?.GetType().Name}, Cost 속성 값: {context?.GetType().GetProperty("Cost")?.GetValue(context)}"); +#endif
98-98
: 불필요한 null-forgiving 연산자 사용Line 98에서
taskResult
는 이미 null이 아님이 보장되는데!
연산자를 사용하고 있습니다.-var result = await taskResult!; +var result = await taskResult;ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs (3)
22-26
: 조건문 검사 순서 최적화 가능
context.Segments?.Count == 0
검사는 null 체크와 비어있음 체크를 동시에 수행하지만,Any()
메서드를 사용하면 더 명확합니다.-if (!context.UseTTS || string.IsNullOrWhiteSpace(context.Character?.VoiceId) || context.Segments?.Count == 0) { +if (!context.UseTTS || string.IsNullOrWhiteSpace(context.Character?.VoiceId) || context.Segments?.Any() != true) {
41-41
: Task.Run 사용의 필요성 검토이미 비동기 메서드 내에서
Task.Run
을 사용하여 추가 스레드를 생성하는 것은 불필요할 수 있습니다.I/O 바운드 작업이므로
Task.Run
없이 직접 비동기 호출:-ttsTasks.Add(Task.Run(async () => (idx, await GenerateTTSAsync(profile, segment.Text!, emotion)))); +ttsTasks.Add(GenerateTTSWithIndexAsync(idx, profile, segment.Text!, emotion));헬퍼 메서드 추가:
private async Task<(int idx, TextToSpeechResponse)> GenerateTTSWithIndexAsync( int idx, VoiceProfile profile, string text, string emotion) { var response = await GenerateTTSAsync(profile, text, emotion); return (idx, response); }
81-84
: 매직 넘버 사용텍스트 길이 제한 300자가 하드코딩되어 있습니다.
상수로 정의하여 유지보수성 향상:
+private const int MaxTextLength = 300; + private async Task<TextToSpeechResponse> GenerateTTSAsync(VoiceProfile profile, string text, string emotion) { // ... if (string.IsNullOrWhiteSpace(text)) throw new ValidationException(ErrorCode.MESSAGE_EMPTY, "텍스트가 비어있습니다."); - if (text.Length > 300) - throw new ValidationException(ErrorCode.MESSAGE_TOO_LONG, "텍스트는 300자를 초과할 수 없습니다."); + if (text.Length > MaxTextLength) + throw new ValidationException(ErrorCode.MESSAGE_TOO_LONG, $"텍스트는 {MaxTextLength}자를 초과할 수 없습니다.");ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (2)
125-140
: 중첩된 조건문으로 인한 복잡도
ProcessMatches
메서드 내의 중첩된 if 문이 복잡도를 높입니다.early return 패턴으로 가독성 개선:
private void ProcessMatches(MatchCollection matches, Dictionary<string, string>? emotionMap, List<ChatMessageSegment> segments, HashSet<string> seenTexts) { for (int i = 0; i < matches.Count; i++) { var match = matches[i]; - if (match.Groups.Count >= 3) - { - var originalEmotion = match.Groups[1].Value.Trim(); - var mappedEmotion = emotionMap != null && emotionMap.ContainsKey(originalEmotion) - ? emotionMap[originalEmotion] - : originalEmotion; - var text = match.Groups[2].Value.Trim(); - - if (!seenTexts.Contains(text)) - { - seenTexts.Add(text); - var segment = ChatMessageSegment.CreateTextOnly(text, segments.Count); - segment.Emotion = mappedEmotion; - segments.Add(segment); - } - } + if (match.Groups.Count < 3) continue; + + var originalEmotion = match.Groups[1].Value.Trim(); + var mappedEmotion = emotionMap != null && emotionMap.ContainsKey(originalEmotion) + ? emotionMap[originalEmotion] + : originalEmotion; + var text = match.Groups[2].Value.Trim(); + + if (seenTexts.Contains(text)) continue; + + seenTexts.Add(text); + var segment = ChatMessageSegment.CreateTextOnly(text, segments.Count); + segment.Emotion = mappedEmotion; + segments.Add(segment); } }
128-130
: Dictionary 접근 패턴 개선 가능
ContainsKey
후 인덱서 접근 대신TryGetValue
사용이 더 효율적입니다.-var mappedEmotion = emotionMap != null && emotionMap.ContainsKey(originalEmotion) - ? emotionMap[originalEmotion] - : originalEmotion; +var mappedEmotion = emotionMap?.TryGetValue(originalEmotion, out var mapped) == true + ? mapped + : originalEmotion;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (29)
ProjectVG.Api/Models/Chat/Request/ChatRequest.cs
(2 hunks)ProjectVG.Application/ApplicationServiceCollectionExtensions.cs
(2 hunks)ProjectVG.Application/Models/Chat/ChatMessageSegment.cs
(1 hunks)ProjectVG.Application/Models/Chat/ChatMetrics.cs
(1 hunks)ProjectVG.Application/Models/Chat/ChatOutputFormatResult.cs
(0 hunks)ProjectVG.Application/Models/Chat/ChatPreprocessContext.cs
(0 hunks)ProjectVG.Application/Models/Chat/ChatPreprocessResult.cs
(0 hunks)ProjectVG.Application/Models/Chat/ChatProcessContext.cs
(1 hunks)ProjectVG.Application/Models/Chat/ChatProcessResult.cs
(0 hunks)ProjectVG.Application/Models/Chat/ProcessChatCommand.cs
(1 hunks)ProjectVG.Application/Models/Chat/UserInputAnalysis.cs
(3 hunks)ProjectVG.Application/Services/Chat/ChatService.cs
(5 hunks)ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs
(1 hunks)ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs
(1 hunks)ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecoratorFactory.cs
(1 hunks)ProjectVG.Application/Services/Chat/CostTracking/IChatMetricsService.cs
(1 hunks)ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs
(1 hunks)ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs
(4 hunks)ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs
(1 hunks)ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs
(1 hunks)ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs
(1 hunks)ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs
(1 hunks)ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs
(1 hunks)ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs
(2 hunks)ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs
(1 hunks)ProjectVG.Common/Constants/LLMModelInfo.cs
(4 hunks)ProjectVG.Common/Constants/TTSCostInfo.cs
(1 hunks)ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs
(1 hunks)ProjectVG.Infrastructure/Integrations/LLMClient/Models/LLMResponse.cs
(2 hunks)
💤 Files with no reviewable changes (4)
- ProjectVG.Application/Models/Chat/ChatPreprocessResult.cs
- ProjectVG.Application/Models/Chat/ChatPreprocessContext.cs
- ProjectVG.Application/Models/Chat/ChatOutputFormatResult.cs
- ProjectVG.Application/Models/Chat/ChatProcessResult.cs
🧰 Additional context used
🧬 Code graph analysis (20)
ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecoratorFactory.cs (2)
ProjectVG.Application/ApplicationServiceCollectionExtensions.cs (1)
IServiceCollection
(21-47)ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs (2)
CostTrackingDecorator
(7-113)CostTrackingDecorator
(13-18)
ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs (2)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (4)
ChatProcessContext
(6-76)ChatProcessContext
(28-36)ChatProcessContext
(38-54)IEnumerable
(68-75)ProjectVG.Domain/Entities/ConversationHistory/ConversationHistory.cs (1)
ConversationHistory
(6-24)
ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs (3)
ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (1)
CalculateCost
(67-70)ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (1)
CalculateCost
(207-210)ProjectVG.Common/Constants/LLMModelInfo.cs (1)
CalculateCost
(315-324)
ProjectVG.Application/Models/Chat/ChatMetrics.cs (2)
ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs (1)
ChatMetrics
(68-71)ProjectVG.Application/Services/Chat/CostTracking/IChatMetricsService.cs (1)
ChatMetrics
(11-11)
ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (3)
ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (1)
CalculateCost
(67-70)ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs (1)
CalculateCost
(11-11)ProjectVG.Common/Constants/LLMModelInfo.cs (2)
CalculateCost
(315-324)LLMModelInfo
(3-325)
ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs (4)
ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs (2)
Task
(20-63)Task
(76-111)ProjectVG.Application/Services/Chat/ChatService.cs (3)
Task
(63-76)Task
(82-93)Task
(98-117)ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs (1)
Task
(20-43)ProjectVG.Application/Models/Chat/ChatProcessContext.cs (3)
ChatProcessContext
(6-76)ChatProcessContext
(28-36)ChatProcessContext
(38-54)
ProjectVG.Application/Models/Chat/UserInputAnalysis.cs (1)
ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (1)
DateTime
(188-199)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (5)
ProjectVG.Domain/Entities/ConversationHistory/ConversationHistory.cs (1)
ConversationHistory
(6-24)ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (2)
List
(62-65)List
(85-109)ProjectVG.Application/Models/Chat/ChatMessageSegment.cs (2)
ChatMessageSegment
(3-34)ChatMessageSegment
(17-24)ProjectVG.Api/Models/Chat/Request/ChatRequest.cs (1)
ProcessChatCommand
(32-45)ProjectVG.Application/Models/Chat/ProcessChatCommand.cs (1)
ProcessChatCommand
(5-31)
ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs (2)
ProjectVG.Application/Services/Chat/ChatService.cs (3)
Task
(63-76)Task
(82-93)Task
(98-117)ProjectVG.Application/Models/Chat/ChatProcessContext.cs (3)
ChatProcessContext
(6-76)ChatProcessContext
(28-36)ChatProcessContext
(38-54)
ProjectVG.Application/ApplicationServiceCollectionExtensions.cs (4)
ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs (2)
ChatFailureHandler
(10-47)ChatFailureHandler
(17-27)ProjectVG.Application/Services/Chat/ChatService.cs (2)
ChatService
(13-118)ChatService
(32-61)ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs (2)
ChatMetricsService
(6-93)ChatMetricsService
(11-14)ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs (2)
UserInputAnalysisProcessor
(9-61)UserInputAnalysisProcessor
(14-20)
ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs (4)
ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs (2)
Task
(43-75)Task
(79-112)ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs (2)
Task
(35-108)Task
(110-133)ProjectVG.Application/Models/Chat/ChatProcessContext.cs (4)
ChatProcessContext
(6-76)ChatProcessContext
(28-36)ChatProcessContext
(38-54)SetResponse
(56-61)ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (1)
CalculateCost
(67-70)
ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs (7)
ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs (2)
Task
(20-63)Task
(76-111)ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs (2)
Task
(9-9)Task
(10-10)ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs (1)
Task
(20-43)ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs (1)
Task
(22-60)ProjectVG.Application/Models/Chat/ChatProcessContext.cs (4)
ChatProcessContext
(6-76)ChatProcessContext
(28-36)ChatProcessContext
(38-54)IEnumerable
(68-75)ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs (2)
StartProcessMetrics
(28-40)EndProcessMetrics
(42-57)ProjectVG.Application/Services/Chat/CostTracking/IChatMetricsService.cs (2)
StartProcessMetrics
(8-8)EndProcessMetrics
(9-9)
ProjectVG.Application/Services/Chat/CostTracking/IChatMetricsService.cs (1)
ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs (5)
StartChatMetrics
(16-26)StartProcessMetrics
(28-40)EndProcessMetrics
(42-57)EndChatMetrics
(59-66)LogChatMetrics
(73-92)
ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs (2)
ProjectVG.Application/Models/Chat/ChatMetrics.cs (2)
ChatMetrics
(3-13)ProcessMetrics
(15-24)ProjectVG.Application/Services/Chat/CostTracking/IChatMetricsService.cs (6)
ChatMetrics
(11-11)StartChatMetrics
(7-7)StartProcessMetrics
(8-8)EndProcessMetrics
(9-9)EndChatMetrics
(10-10)LogChatMetrics
(12-12)
ProjectVG.Common/Constants/LLMModelInfo.cs (3)
ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (1)
CalculateCost
(67-70)ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs (1)
CalculateCost
(11-11)ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (1)
CalculateCost
(207-210)
ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (5)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (3)
ChatProcessContext
(6-76)ChatProcessContext
(28-36)ChatProcessContext
(38-54)ProjectVG.Application/Models/Chat/ChatMessageSegment.cs (2)
ChatMessageSegment
(3-34)ChatMessageSegment
(17-24)ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs (3)
GetSystemMessage
(5-5)GetInstructions
(6-6)CalculateCost
(11-11)ProjectVG.Common/Constants/LLMModelInfo.cs (2)
CalculateCost
(315-324)LLMModelInfo
(3-325)ProjectVG.Common/Constants/VoiceCatalog.cs (1)
VoiceCatalog
(20-123)
ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs (7)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (4)
ChatProcessContext
(6-76)ChatProcessContext
(28-36)ChatProcessContext
(38-54)AddCost
(63-66)ProjectVG.Common/Constants/VoiceCatalog.cs (1)
VoiceCatalog
(20-123)ProjectVG.Infrastructure/Integrations/TextToSpeechClient/Models/TextToSpeechResponse.cs (1)
TextToSpeechResponse
(5-42)ProjectVG.Application/Models/Chat/ChatMessageSegment.cs (1)
SetAudioData
(26-31)ProjectVG.Common/Constants/TTSCostInfo.cs (2)
TTSCostInfo
(3-20)CalculateTTSCost
(15-19)ProjectVG.Common/Exceptions/ValidationException.cs (4)
ValidationException
(5-26)ValidationException
(9-13)ValidationException
(15-19)ValidationException
(21-25)ProjectVG.Infrastructure/Integrations/TextToSpeechClient/Models/TextToSpeechRequest.cs (1)
TextToSpeechRequest
(5-40)
ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs (4)
ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (1)
CalculateCost
(67-70)ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs (1)
CalculateCost
(11-11)ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (1)
CalculateCost
(207-210)ProjectVG.Common/Constants/LLMModelInfo.cs (1)
CalculateCost
(315-324)
ProjectVG.Application/Models/Chat/ChatMessageSegment.cs (1)
ProjectVG.Application/Models/Chat/IntegratedChatMessage.cs (1)
SetAudioData
(34-44)
ProjectVG.Application/Services/Chat/ChatService.cs (10)
ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs (3)
UserInputAnalysisProcessor
(9-61)UserInputAnalysisProcessor
(14-20)Task
(22-60)ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs (3)
ChatLLMProcessor
(7-44)ChatLLMProcessor
(12-18)Task
(20-43)ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs (4)
ChatTTSProcessor
(7-112)ChatTTSProcessor
(12-18)Task
(20-63)Task
(76-111)ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs (4)
ChatResultProcessor
(10-60)ChatResultProcessor
(17-27)Task
(29-36)Task
(38-59)ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs (4)
ChatFailureHandler
(10-47)ChatFailureHandler
(17-27)Task
(29-33)Task
(35-46)ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs (2)
Task
(43-75)Task
(79-112)ProjectVG.Application/Models/Chat/ProcessChatCommand.cs (1)
ProcessChatCommand
(5-31)ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs (3)
StartChatMetrics
(16-26)EndChatMetrics
(59-66)LogChatMetrics
(73-92)ProjectVG.Application/Services/Chat/CostTracking/IChatMetricsService.cs (3)
StartChatMetrics
(7-7)EndChatMetrics
(10-10)LogChatMetrics
(12-12)ProjectVG.Application/Models/Chat/ChatProcessContext.cs (3)
ChatProcessContext
(6-76)ChatProcessContext
(28-36)ChatProcessContext
(38-54)
🔇 Additional comments (20)
ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs (1)
11-11
: 비용 계산 메서드 시그니처 변경이 올바르게 적용됨단일 토큰 수 기반에서 입력/출력 토큰을 분리한 비용 계산으로 개선되었습니다. 이는 실제 LLM 비용 모델과 일치합니다.
ProjectVG.Infrastructure/Integrations/LLMClient/Models/LLMResponse.cs (3)
7-11
: OpenAI API 응답 ID 필드 추가API 응답 추적을 위한 ID 필드가 추가되었습니다. 필드명과 JSON 매핑이 적절합니다.
25-29
: 기존 TokensUsed 필드 설명 개선총합 토큰 수임을 명확히 표시하도록 주석이 개선되었습니다.
31-41
: 입력/출력 토큰 수 필드 추가세분화된 비용 계산을 위해 입력 토큰과 출력 토큰을 별도로 추적할 수 있게 되었습니다. JSON 매핑도 적절합니다.
ProjectVG.Common/Constants/LLMModelInfo.cs (3)
5-7
: 비용 계산을 위한 상수 정의백만 토큰당 비용을 실제 토큰당 비용으로 변환하는 계산 팩터가 적절히 정의되었습니다. 상수명과 값이 명확합니다.
252-252
: switch 문 기본 케이스의 주석 제거불필요한 주석이 제거되어 코드가 더 깔끔해졌습니다.
Also applies to: 280-280
284-292
: 토큰당 비용 계산 메서드 추가입력/출력 토큰별 비용을 계산하는 유틸리티 메서드가 추가되었습니다. 구현이 간단하고 명확합니다.
ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs (2)
29-33
: 예외 처리 및 로깅이 적절함세션 ID를 포함한 구체적인 로깅과 함께 사용자 친화적인 오류 메시지 전송이 구현되어 있습니다.
35-46
: WebSocket 오류 메시지 전송 로직이중 예외 처리로 WebSocket 전송 실패까지 안전하게 처리하고 있습니다. 구현이 견고합니다.
ProjectVG.Application/Models/Chat/ProcessChatCommand.cs (1)
21-21
: TTS 사용 플래그 추가기본값 true로 설정된 UseTTS 속성이 적절히 추가되었습니다. 이는 새로운 TTS 제어 기능의 기반이 됩니다.
ProjectVG.Api/Models/Chat/Request/ChatRequest.cs (1)
42-44
: ProcessChatCommand 매핑에 UseTTS 전달 추가: LGTM
ChatRequest.UseTTS
가ProcessChatCommand.UseTTS
로 정상 전파됩니다. 상위 파이프라인에서 TTS 활성화 플래그를 일관되게 활용할 수 있습니다.ProjectVG.Application/ApplicationServiceCollectionExtensions.cs (1)
32-32
: 실패 처리 핸들러 스코프 등록: LGTM
ChatFailureHandler
를 Scoped로 등록한 선택 적절합니다. 요청 단위 컨텍스트/로거를 활용하기 좋습니다.ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs (2)
44-47
: 토큰 기반 비용 계산 및 결과 반영: LGTM
format.CalculateCost(...)
로 산출한 값을analysis.Cost
에 저장하고 있어 이후 비용 추적 파이프라인과의 연계가 명확합니다.
48-50
: Console.WriteLine 제거 및 구조적 로깅 통합– ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs
• 48–50행의Console.WriteLine(…)
호출 제거
• 단일_logger.LogDebug(…)
호출로 통합제안되는 변경 예시:
- Console.WriteLine($"[USER_INPUT_ANALYSIS_DEBUG] ID: {llmResponse.Id}, 입력 토큰: {llmResponse.InputTokens}, 출력 토큰: {llmResponse.OutputTokens}, 총 토큰: {llmResponse.TokensUsed}, 계산된 비용: {cost:F0} Cost"); - _logger.LogDebug("사용자 입력 분석 완료: '{Input}' -> 맥락: {Context}, 의도: {Intent}, 액션: {Action}, 비용: {Cost}", - userInput, analysis.ConversationContext, analysis.UserIntent, analysis.Action, cost); + _logger.LogDebug( + "사용자 입력 분석 완료: ID={Id}, 입력토큰={InputTokens}, 출력토큰={OutputTokens}, 총토큰={TokensUsed}, 비용={Cost:F0}, 입력='{Input}', 맥락={Context}, 의도={Intent}, 액션={Action}", + llmResponse.Id, + llmResponse.InputTokens, + llmResponse.OutputTokens, + llmResponse.TokensUsed, + cost, + userInput, + analysis.ConversationContext, + analysis.UserIntent, + analysis.Action);– 실행 시 나오는 비용 단위(
format.CalculateCost
→LLMModelInfo.CalculateCost
)가
ChatMetricsService
의/ 100_000.0
환산 로직과 일치하는지 확인해주세요.
(현재 메트릭 서비스는metrics.TotalCost
를100_000.0
으로 나눠 달러 단위로 표시)ProjectVG.Application/Models/Chat/UserInputAnalysis.cs (1)
24-26
: CreateValid 시그니처 확장 및 할당: LGTM선택 매개변수로
contextTime
,cost
를 추가하고 반환 객체에 반영한 구조 합리적입니다. 기존 호출부와도 호환됩니다(디폴트 인자).Also applies to: 34-36
ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs (1)
1-3
: 네임스페이스 일치 확인:ConversationHistorys
사용 유지
ProjectVG.Domain/Entities/ConversationHistory/ConversationHistory.cs
파일을 확인한 결과, 내부에 아래와 같이 선언되어 있습니다:namespace ProjectVG.Domain.Entities.ConversationHistorys { public class ConversationHistory : BaseEntity … }따라서
- using ProjectVG.Domain.Entities.ConversationHistorys; + using ProjectVG.Domain.Entities.ConversationHistory;로 변경 시 오히려 존재하지 않는 네임스페이스를 참조하게 되어 컴파일 오류가 발생합니다. 제안된 단수형(
ConversationHistory
) 네임스페이스는 코드베이스에 정의되어 있지 않으므로, 원래의ConversationHistorys
임포트를 그대로 유지하시면 됩니다.Likely an incorrect or invalid review comment.
ProjectVG.Application/Services/Chat/ChatService.cs (1)
105-108
: DI 스코프 내에서 resultProcessor를 다시 resolve하는 이유 불명확생성자에서 이미 주입받은
_resultProcessor
가 있는데, 새로운 스코프에서ChatResultProcessor
를 다시 resolve하고 있습니다.이 패턴이 의도적인지 확인이 필요합니다. 만약 scoped 서비스의 생명주기 문제라면 주석으로 설명을 추가하는 것이 좋겠습니다:
+// ChatResultProcessor가 scoped 서비스를 사용하므로 백그라운드 작업에서 새 스코프 생성 using var scope = _scopeFactory.CreateScope(); var resultProcessor = scope.ServiceProvider.GetRequiredService<ChatResultProcessor>();
아니면 생성자 주입된
_resultProcessor
를 사용:-using var scope = _scopeFactory.CreateScope(); -var resultProcessor = scope.ServiceProvider.GetRequiredService<ChatResultProcessor>(); -await resultProcessor.SendResultsAsync(context); -await resultProcessor.PersistResultsAsync(context); +await _resultProcessor.SendResultsAsync(context); +await _resultProcessor.PersistResultsAsync(context);ProjectVG.Application/Models/Chat/ChatProcessContext.cs (3)
8-14
: 불변성 보장을 위한 private setter 사용 검토모든 속성이
private set
으로 되어 있어 외부에서 수정할 수 없도록 보호되어 있습니다. 좋은 설계입니다!
23-23
: FullText 속성에서 null 체크 누락
s.Text
가 null일 수 있는데string.Join
에 전달될 수 있습니다.null 체크 추가:
-public string FullText => string.Join(" ", Segments.Where(s => s.HasText).Select(s => s.Text)); +public string FullText => string.Join(" ", Segments.Where(s => s.HasText).Select(s => s.Text ?? string.Empty));Likely an incorrect or invalid review comment.
63-66
: 아래 코드를 확인한 결과, 현재AddCost
메서드는 다음과 같은 흐름에서만 호출되고 있어 동시에(병렬로) 실행될 우려가 없습니다.
- CostTrackingDecorator
- 데코레이터는 내부 프로세서(
ChatLLMProcessor
등)의ProcessAsync
호출이 완료된 이후에 한 번만context.AddCost()
를 호출합니다.- ChatTTSProcessor
- 내부적으로
Task.Run
으로 TTS 작업을 병렬 실행하되,await Task.WhenAll
이후에 순차(foreach
)로 비용을 합산합니다.즉,
Cost += additionalCost
동시 접근은 발생하지 않으므로, 현재로서는 락(lock)·Interlocked 같은 추가 동시성 제어가 불필요**합니다.
필요 시 향후 아래 지점에 동시성 제어를 도입할 수 있습니다:
ChatProcessContext.AddCost
내부- 데코레이터에서 여러 프로세서를 병렬 실행하게 변경할 경우
services.AddScoped<IChatMetricsService, ChatMetricsService>(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
ChatMetricsService 수명(Scope)으로 인한 백그라운드 작업 파손 위험 — Singleton 권장
현재 IChatMetricsService
가 Scoped입니다. 하지만 ChatService
에서 Task.Run
으로 백그라운드 파이프라인을 구동하고, 해당 메서드 내에서 필드로 보관된 IChatMetricsService
인스턴스를 사용합니다. HTTP 요청이 빠르게 반환되면 원 스코프가 먼저 Dispose되어, 백그라운드에서 Scoped 서비스를 참조하는 순간 예기치 않은 동작/예외가 발생할 수 있습니다. 메트릭 서비스는 AsyncLocal
기반 컨텍스트를 사용하므로 Singleton로 두는 편이 안전하고 일반적입니다.
또한 관련 구현(참조 스니펫의 ChatMetricsService)에는 필드명이 혼재되어 컴파일 실패 가능성도 있습니다: _currentMetrics
vs _current_metrics
. 이 부분도 함께 정리해야 합니다.
권장 수정:
IChatMetricsService
를 Singleton으로 전환- (별도 PR 또는 동시)
ChatService
의 백그라운드 처리 전반을 새IServiceScope
안에서 해결하고, 백그라운드에서 사용할 서비스는 스코프 내부에서 획득하도록 리팩터링 ChatMetricsService
내 오탈자 필드명 정리
아래는 등록 수명 변경 diff입니다.
- services.AddScoped<IChatMetricsService, ChatMetricsService>();
+ services.AddSingleton<IChatMetricsService, ChatMetricsService>();
다음 스크립트로 파손 가능 지점을 빠르게 점검해 주세요.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Task.Run 사용 지점 확인 (백그라운드 작업) =="
rg -nP --glob '!**/bin/**' --glob '!**/obj/**' '\bTask\.Run\s*\(' -C3
echo
echo "== IChatService 등록 수명 확인 =="
rg -nP --glob '!**/bin/**' --glob '!**/obj/**' 'AddScoped\s*<\s*IChatService\s*,\s*ChatService\s*>\s*\(' -C2
echo
echo "== ChatMetricsService 내 필드명 혼용(_current_metrics) 확인 =="
rg -nP --glob '!**/bin/**' --glob '!**/obj/**' '_current_metrics|_currentMetrics' ProjectVG.Application/Services/Chat/CostTracking -n -C2
Length of output: 7823
ChatService에서 IChatMetricsService
주입 및 Task.Run
내 사용 위치를 확인하기 위해 아래 스크립트를 실행해 주세요.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== ChatService 클래스 정의 및 생성자 위치 확인 =="
rg -nP 'class ChatService' -R ProjectVG.Application/Services/Chat/ChatService.cs -C5
rg -nP 'public ChatService' -R ProjectVG.Application/Services/Chat/ChatService.cs -C5
echo
echo "== ChatService 내부에서 IChatMetricsService 주입 검색 =="
rg -nP 'IChatMetricsService' -R ProjectVG.Application/Services/Chat/ChatService.cs -C5
echo
echo "== ChatService 내 Task.Run 사용 지점 확인 =="
rg -nP 'Task\.Run' -R ProjectVG.Application/Services/Chat/ChatService.cs -C5
Length of output: 594
다음과 같이 rg
명령의 파일 경로 지정을 수정하여 다시 실행해 주세요.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== ChatService 클래스 정의 및 생성자 위치 확인 =="
rg -nP 'class\s+ChatService' ProjectVG.Application/Services/Chat/ChatService.cs -C5
rg -nP 'public\s+ChatService' ProjectVG.Application/Services/Chat/ChatService.cs -C5
echo
echo "== ChatService 내부에서 IChatMetricsService 주입 검색 =="
rg -nP 'IChatMetricsService' ProjectVG.Application/Services/Chat/ChatService.cs -C5
echo
echo "== ChatService 내 Task.Run 사용 지점 확인 =="
rg -nP 'Task\.Run' ProjectVG.Application/Services/Chat/ChatService.cs -C5
Length of output: 3185
ChatMetricsService를 Singleton으로 전환하여 백그라운드 작업 안전성 확보
Scoped 수명의 IChatMetricsService
를 Task.Run
으로 실행되는 백그라운드 작업에서 클로저로 사용하면, DI 컨테이너의 스코프가 종료된 이후에도 해당 인스턴스가 참조되어 의도치 않은 경고나 예외가 발생할 수 있습니다.
또한, ChatMetricsService
내부 필드명 _currentMetrics
의 혼용(_current_metrics
)은 코드 상에서 발견되지 않았으므로 별도 수정이 필요 없습니다.
– 수정 대상
ProjectVG.Application/ApplicationServiceCollectionExtensions.cs
(라인 39): 수명 변경ProjectVG.Application/Services/Chat/ChatService.cs
(라인 71 인근): 백그라운드 작업 시IServiceScopeFactory.CreateScope()
로 새 스코프를 열고, 그 안에서 필요한 서비스들을 Resolve하도록 리팩터링
– 등록 수명 변경 예시
- services.AddScoped<IChatMetricsService, ChatMetricsService>();
+ services.AddSingleton<IChatMetricsService, ChatMetricsService>();
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
services.AddScoped<IChatMetricsService, ChatMetricsService>(); | |
services.AddSingleton<IChatMetricsService, ChatMetricsService>(); |
public decimal TotalCost { get; set; } | ||
public TimeSpan TotalDuration { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
코스트 타입을 double로 통일하여 파이프라인과 정합성 확보
파이프라인 전반의 비용 산출이 double
(LLM/TTS/Context)인 반면 본 모델은 decimal
입니다. 변환 지점이 늘어나고, 인터페이스/서비스 시그니처와 불일치가 생깁니다. 아래처럼 double
로 통일하는 것을 권장합니다.
- public decimal TotalCost { get; set; }
+ public double TotalCost { get; set; }
...
- public decimal Cost { get; set; }
+ public double Cost { get; set; }
참고: IChatMetricsService.EndProcessMetrics의 파라미터도 double
로 일치시켜 주세요.
Also applies to: 21-21
🤖 Prompt for AI Agents
In ProjectVG.Application/Models/Chat/ChatMetrics.cs around lines 11-12 (and
similarly line 21), the TotalCost property is declared as decimal while the rest
of the pipeline (LLM/TTS/Context) and related interfaces use double; change the
TotalCost property type from decimal to double to ensure type consistency across
the pipeline, and also update the IChatMetricsService.EndProcessMetrics
parameter signature to use double so service/interface signatures match.
_metricsService.StartChatMetrics(command.SessionId, command.UserId.ToString(), command.CharacterId.ToString()); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
메트릭 시작 시점이 검증 이전에 위치함
StartChatMetrics
가 요청 검증(ValidateAsync
) 이전에 호출되고 있습니다. 검증 실패 시에도 메트릭이 시작되지만 종료되지 않아 메트릭 데이터가 불완전할 수 있습니다.
다음과 같이 검증 이후로 메트릭 시작을 이동하는 것을 제안합니다:
public async Task<ChatRequestResponse> EnqueueChatRequestAsync(ProcessChatCommand command)
{
- _metricsService.StartChatMetrics(command.SessionId, command.UserId.ToString(), command.CharacterId.ToString());
-
await _validator.ValidateAsync(command);
+
+ _metricsService.StartChatMetrics(command.SessionId, command.UserId.ToString(), command.CharacterId.ToString());
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
_metricsService.StartChatMetrics(command.SessionId, command.UserId.ToString(), command.CharacterId.ToString()); | |
public async Task<ChatRequestResponse> EnqueueChatRequestAsync(ProcessChatCommand command) | |
{ | |
// First, validate the incoming command | |
await _validator.ValidateAsync(command); | |
// Only after successful validation do we start the chat metrics | |
_metricsService.StartChatMetrics( | |
command.SessionId, | |
command.UserId.ToString(), | |
command.CharacterId.ToString() | |
); | |
// ... rest of the method implementation | |
} |
🤖 Prompt for AI Agents
ProjectVG.Application/Services/Chat/ChatService.cs around lines 65-66: Move the
_metricsService.StartChatMetrics(...) call so it runs after the request
validation (after ValidateAsync completes successfully); remove the current
pre-validation call, place it immediately after the successful validation
return, and ensure any existing exception paths still call the corresponding
metrics stop/finalize to avoid orphaned metrics (add a try/finally around the
chat handling if needed).
_ = Task.Run(async () => { | ||
using var processScope = _scopeFactory.CreateScope(); | ||
await ProcessChatRequestInternalAsync(preprocessContext); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fire-and-forget 패턴에서 예외 처리 누락
Task.Run
으로 백그라운드 작업을 시작하지만 반환된 Task를 처리하지 않아, 처리되지 않은 예외가 발생할 수 있습니다.
예외를 명시적으로 처리하도록 수정:
-_ = Task.Run(async () => {
- await ProcessChatRequestInternalAsync(preprocessContext);
-});
+_ = Task.Run(async () => {
+ try {
+ await ProcessChatRequestInternalAsync(preprocessContext);
+ }
+ catch (Exception ex) {
+ _logger.LogError(ex, "백그라운드 채팅 처리 중 예외 발생: 세션 {SessionId}", preprocessContext.SessionId);
+ }
+});
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
_ = Task.Run(async () => { | |
using var processScope = _scopeFactory.CreateScope(); | |
await ProcessChatRequestInternalAsync(preprocessContext); | |
}); | |
_ = Task.Run(async () => { | |
try { | |
await ProcessChatRequestInternalAsync(preprocessContext); | |
} | |
catch (Exception ex) { | |
_logger.LogError(ex, "백그라운드 채팅 처리 중 예외 발생: 세션 {SessionId}", preprocessContext.SessionId); | |
} | |
}); |
🤖 Prompt for AI Agents
In ProjectVG.Application/Services/Chat/ChatService.cs around lines 71 to 73, the
fire-and-forget Task.Run call starts ProcessChatRequestInternalAsync without
observing exceptions, which can lead to unhandled exceptions; change this to
ensure exceptions are observed by either awaiting the Task (if caller can be
async) or explicitly handling errors inside the background task: wrap the
awaited call in a try/catch inside Task.Run and log/handle exceptions, or
capture the returned Task and attach a continuation (e.g., ContinueWith) that
logs exceptions, so no exceptions go unobserved.
public void EndProcessMetrics(string processName, decimal cost = 0, string? errorMessage = null, Dictionary<string, object>? additionalData = null) | ||
{ | ||
if (_currentMetrics.Value == null) return; | ||
|
||
var processMetrics = _currentMetrics.Value.ProcessMetrics | ||
.FirstOrDefault(p => p.ProcessName == processName && p.EndTime == default); | ||
|
||
if (processMetrics != null) | ||
{ | ||
processMetrics.EndTime = DateTime.UtcNow; | ||
processMetrics.Duration = processMetrics.EndTime - processMetrics.StartTime; | ||
processMetrics.Cost = cost; | ||
processMetrics.ErrorMessage = errorMessage; | ||
processMetrics.AdditionalData = additionalData; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
비용 타입(double ↔ decimal) 불일치로 인한 잠재적 컴파일/정밀도 문제
- IChatMetricsService.EndProcessMetrics의
cost
파라미터와 이 클래스 내부 누적 필드가decimal
인데, 파이프라인 전반(예: ChatProcessContext.Cost, LLM/TTS 비용 계산)은double
을 사용합니다. 데코레이터(CostTrackingDecorator)에서ExtractCost(context)
가double
을 반환한다면 현재 시그니처로는 암시적 변환이 없어 컴파일 실패 또는 부정확한 형 변환이 발생할 수 있습니다. - 또한 LogChatMetrics에서
decimal
을 다시double
로 변환해 100_000.0으로 나누는 등 반복적 캐스팅과 “매직 넘버”가 혼재합니다.
아래와 같이 서비스/모델 전반을 double
로 통일하거나, 최소한 이 서비스의 파라미터와 내부 합산 타입을 double
로 맞추는 것을 권장합니다. 동시에 변환 상수를 상수 필드로 추출하세요.
적용 예(이 파일 내 변경점):
- public void EndProcessMetrics(string processName, decimal cost = 0, string? errorMessage = null, Dictionary<string, object>? additionalData = null)
+ public void EndProcessMetrics(string processName, double cost = 0, string? errorMessage = null, Dictionary<string, object>? additionalData = null)
- var totalCostInDollars = (double)metrics.TotalCost / 100_000.0;
+ const double CostScale = 100_000.0;
+ var totalCostInDollars = metrics.TotalCost / CostScale;
동일 맥락으로 ProjectVG.Application/Models/Chat/ChatMetrics.cs의 TotalCost
와 ProcessMetrics.Cost
도 double
로 통일하는 것을 제안합니다(해당 파일 코멘트 참조).
검증 스크립트:
Also applies to: 80-91
🏁 Script executed:
#!/bin/bash
# 비용 타입 사용 지점 점검
rg -nP --type cs '\bclass\s+ChatProcessContext\b.*?\bdouble\s+Cost\b' -C3
rg -nP --type cs '\bclass\s+ChatMetrics\b.*?\b(decimal|double)\s+TotalCost\b' -C3
rg -nP --type cs '\bclass\s+ProcessMetrics\b.*?\b(decimal|double)\s+Cost\b' -C3
rg -nP --type cs 'EndProcessMetrics\s*\(' -C2
Length of output: 4185
🏁 Script executed:
#!/bin/bash
# Search for the ExtractCost method in the CostTrackingDecorator to determine its return type
rg -nP --type cs 'ExtractCost' -C3 ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs
Length of output: 1294
🏁 Script executed:
#!/bin/bash
# Locate ChatProcessContext and inspect Cost property type
ctx_file=$(rg -lP --type cs 'class\s+ChatProcessContext' -g '*.cs')
echo "ChatProcessContext file: $ctx_file"
rg -nC3 'class\s+ChatProcessContext' "$ctx_file"
rg -nC3 '\bCost\b' "$ctx_file"
# Locate ChatMetrics model to inspect TotalCost
metrics_file=$(rg -lP --type cs 'class\s+ChatMetrics' -g '*.cs')
echo "ChatMetrics file: $metrics_file"
rg -nC3 'class\s+ChatMetrics' "$metrics_file"
rg -nC3 '\bTotalCost\b' "$metrics_file"
# Locate ProcessMetrics model to inspect Cost property
proc_file=$(rg -lP --type cs 'class\s+ProcessMetrics' -g '*.cs')
echo "ProcessMetrics file: $proc_file"
rg -nC3 'class\s+ProcessMetrics' "$proc_file"
rg -nC3 '\bCost\b' "$proc_file"
Length of output: 3558
🏁 Script executed:
#!/bin/bash
# 1. ExtractCost 구현 상세 확인
rg -nP --type cs 'private decimal ExtractCost' -C20 ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs
# 2. ChatMetricsService 전체 흐름 및 TotalCost 계산 부분 확인
sed -n '1,200p' ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs
# 3. ChatMetrics 모델에 TotalCost 속성 존재 여부 및 타입 확인
sed -n '1,200p' ProjectVG.Application/Models/Chat/ChatMetrics.cs
Length of output: 6197
Chat 파이프라인과 Metrics 서비스 간 비용 타입 불일치( double ↔ decimal ) 해결 필요
현재
ChatProcessContext.Cost
와AddCost
는double
IChatMetricsService.EndProcessMetrics
파라미터와ChatMetrics.TotalCost
·ProcessMetrics.Cost
는decimal
- 비용 추출(
ExtractCost
) 시Convert.ToDecimal
을 사용해double
→decimal
으로 변환 - 로그 출력 시 다시
(double)
캐스팅 후100_000.0
으로 나누는 매직 넘버 사용
이처럼 타입이 혼재되어 있어
- 명시적 타입 통일 부재로 인한 정밀도·가독성 저하
- 런타임 변환 코드 증가 및 유지보수 비용 상승
- 매직 넘버 하드코딩
문제가 있으므로, 아래와 같이 리팩터링을 반드시 적용하세요.
• ChatProcessContext 타입 통일
ProjectVG.Application/Models/Chat/ChatProcessContext.cs
- public double Cost { get; private set; } + public decimal Cost { get; private set; } - public ChatProcessContext(..., double cost) + public ChatProcessContext(..., decimal cost) { - Cost = cost; + Cost = cost; } - public void AddCost(double additionalCost) + public void AddCost(decimal additionalCost) { - Cost += additionalCost; + Cost += additionalCost; }
• LogChatMetrics 매직 넘버 상수화 및 캐스팅 통일
ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs
public void LogChatMetrics() { var metrics = _currentMetrics.Value; if (metrics == null) return; Console.WriteLine($"[METRICS] 채팅 메트릭 로그 시작: {metrics.SessionId}"); - var totalCostInDollars = (double)metrics.TotalCost / 100_000.0; + const double CostScale = 100_000.0; + var totalCostInDollars = (double)metrics.TotalCost / CostScale; _logger.LogInformation( "채팅 메트릭 - SessionId: {SessionId}, 총 비용: ${TotalCost:F6}, 총 시간: {TotalDuration}", metrics.SessionId, totalCostInDollars, metrics.TotalDuration);
• 서비스·모델 전반이 동일한 타입(decimal
)을 사용하도록 통일
IChatMetricsService.EndProcessMetrics
시그니처(decimal cost
) 유지ChatMetrics.TotalCost
,ProcessMetrics.Cost
는 이미decimal
위 변경으로 파이프라인부터 최종 로깅까지 비용 처리가 일관된 타입으로 수행되어 정밀도 및 유지보수성이 개선됩니다.
Committable suggestion skipped: line range outside the PR's diff.
public async Task ProcessAsync(ChatProcessContext context) | ||
{ | ||
_metricsService.StartProcessMetrics(_processName); | ||
|
||
try | ||
{ | ||
// 리플렉션으로 ProcessAsync 메서드 호출 | ||
var method = typeof(T).GetMethod("ProcessAsync", new[] { typeof(ChatProcessContext) }); | ||
|
||
if (method == null) | ||
throw new InvalidOperationException($"ProcessAsync 메서드를 찾을 수 없습니다: {typeof(T).Name}"); | ||
|
||
var invokeResult = method.Invoke(_service, new object[] { context }); | ||
if (invokeResult == null) | ||
throw new InvalidOperationException($"ProcessAsync 메서드 호출 결과가 null입니다: {typeof(T).Name}"); | ||
|
||
if (invokeResult is not Task taskResult) | ||
throw new InvalidOperationException($"ProcessAsync 메서드 반환 타입이 올바르지 않습니다: {typeof(T).Name}"); | ||
|
||
await taskResult; | ||
|
||
// Cost 값만 직접 추출 | ||
var cost = ExtractCost(context); | ||
Console.WriteLine($"[COST_TRACKING] {_processName} - 추출된 비용: {cost:F0} Cost"); | ||
Console.WriteLine($"[COST_TRACKING] {_processName} - 컨텍스트 타입: {context?.GetType().Name}, Cost 속성 값: {context?.GetType().GetProperty("Cost")?.GetValue(context)}"); | ||
_metricsService.EndProcessMetrics(_processName, cost); | ||
} | ||
catch (Exception ex) | ||
{ | ||
_metricsService.EndProcessMetrics(_processName, 0, ex.Message); | ||
throw; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
리플렉션 사용으로 인한 성능 및 타입 안전성 문제
ProcessAsync(ChatProcessContext)
메서드를 리플렉션으로 호출하는 것은 성능 저하와 컴파일 타임 타입 체크를 우회하는 문제가 있습니다.
인터페이스를 사용하여 타입 안전성을 보장하는 방법을 제안합니다:
-// 리플렉션으로 ProcessAsync 메서드 호출
-var method = typeof(T).GetMethod("ProcessAsync", new[] { typeof(ChatProcessContext) });
-
-if (method == null)
- throw new InvalidOperationException($"ProcessAsync 메서드를 찾을 수 없습니다: {typeof(T).Name}");
-
-var invokeResult = method.Invoke(_service, new object[] { context });
-if (invokeResult == null)
- throw new InvalidOperationException($"ProcessAsync 메서드 호출 결과가 null입니다: {typeof(T).Name}");
-
-if (invokeResult is not Task taskResult)
- throw new InvalidOperationException($"ProcessAsync 메서드 반환 타입이 올바르지 않습니다: {typeof(T).Name}");
-
-await taskResult;
+// T가 IChatProcessor 인터페이스를 구현하도록 제약 추가
+if (_service is IChatProcessor processor)
+{
+ await processor.ProcessAsync(context);
+}
+else
+{
+ throw new InvalidOperationException($"{typeof(T).Name}은 IChatProcessor를 구현해야 합니다");
+}
또는 제네릭 제약을 클래스 선언에 추가:
public class CostTrackingDecorator<T> : ICostTrackingDecorator<T>
where T : class, IChatProcessor
🤖 Prompt for AI Agents
ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs lines
43-75: the method currently uses reflection to call ProcessAsync which hurts
performance and bypasses compile-time type checks; change the class generic
declaration to constrain T to an interface (e.g. where T : class,
IChatProcessor) that declares Task ProcessAsync(ChatProcessContext) and replace
the reflection invocation with a direct call await
_service.ProcessAsync(context); remove the reflection code and related null/type
checks, ensure IChatProcessor is defined with the correct signature, and update
any references/imports accordingly.
public interface IChatMetricsService | ||
{ | ||
void StartChatMetrics(string sessionId, string userId, string characterId); | ||
void StartProcessMetrics(string processName); | ||
void EndProcessMetrics(string processName, decimal cost = 0, string? errorMessage = null, Dictionary<string, object>? additionalData = null); | ||
void EndChatMetrics(); | ||
ChatMetrics? GetCurrentChatMetrics(); | ||
void LogChatMetrics(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
비용 단위와 식별자 타입 명확화
- 비용 단위가 시스템 전반에서 "밀리센트(1$ = 100,000)"로 사용됩니다. 인터페이스에 XML 주석으로 단위를 명시하면 혼선을 줄일 수 있습니다.
StartChatMetrics(string userId, string characterId)
는 실제 모델에서Guid
를 사용합니다(예:ChatProcessContext.UserId/CharacterId
). 인터페이스도Guid
를 허용하거나 오버로드 추가를 고려해 보세요. 현재는 문자열 변환 일관성이 호출부에 위임됩니다.
정합성 확인 스크립트:
🏁 Script executed:
#!/bin/bash
# ChatProcessContext의 UserId/CharacterId 타입 확인
rg -nP -C2 'class\s+ChatProcessContext\b.*' ProjectVG.Application/Models/Chat -g '**/*.cs'
rg -nP 'Guid\s+UserId|Guid\s+CharacterId' ProjectVG.Application/Models/Chat -C2
Length of output: 2589
IChatMetricsService 인터페이스에 비용 단위 명시 및 UserId/CharacterId 타입 일관성 보강
다음 점을 반영하면 호출부 혼선을 줄이고 모델과의 일관성을 확보할 수 있습니다.
• 수정 대상
- ProjectVG.Application/Services/Chat/CostTracking/IChatMetricsService.cs (라인 6–13)
• 권장 변경 사항
- XML 주석에 비용 단위(밀리센트, 1 $ = 100 000 단위)를 명시
StartChatMetrics
메서드의userId
/characterId
파라미터를Guid
타입으로 오버로드 추가 또는 변경- 기존 문자열 기반 버전은
[Obsolete]
처리 후 추후 제거
- 기존 문자열 기반 버전은
• 예시 Diff
public interface IChatMetricsService
{
- void StartChatMetrics(string sessionId, string userId, string characterId);
+ /// <summary>
+ /// 채팅 세션 메트릭 수집 시작
+ /// </summary>
+ /// <param name="sessionId">세션 식별자</param>
+ /// <param name="userId">사용자 ID (Guid)</param>
+ /// <param name="characterId">캐릭터 ID (Guid)</param>
+ /// <remarks>비용 단위: 밀리센트 (1$ = 100,000)</remarks>
+ void StartChatMetrics(string sessionId, Guid userId, Guid characterId);
+
+ /// <summary>
+ /// 기존 문자열 기반 파라미터 버전 (Guid 버전으로 대체 예정)
+ /// </summary>
+ [Obsolete("문자열 기반 userId/characterId 파라미터는 Guid 버전으로 변경하세요.")]
+ void StartChatMetrics(string sessionId, string userId, string characterId);
위와 같이 XML 주석을 통해 단위를 명확히 하고, Guid 타입 오버로드를 추가하여 모델의 ChatProcessContext.UserId/CharacterId
(Guid) 사용과 일치시켜주세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public interface IChatMetricsService | |
{ | |
void StartChatMetrics(string sessionId, string userId, string characterId); | |
void StartProcessMetrics(string processName); | |
void EndProcessMetrics(string processName, decimal cost = 0, string? errorMessage = null, Dictionary<string, object>? additionalData = null); | |
void EndChatMetrics(); | |
ChatMetrics? GetCurrentChatMetrics(); | |
void LogChatMetrics(); | |
} | |
namespace ProjectVG.Application.Services.Chat.CostTracking | |
{ | |
public interface IChatMetricsService | |
{ | |
/// <summary> | |
/// 채팅 세션 메트릭 수집 시작 | |
/// </summary> | |
/// <param name="sessionId">세션 식별자</param> | |
/// <param name="userId">사용자 ID (Guid)</param> | |
/// <param name="characterId">캐릭터 ID (Guid)</param> | |
/// <remarks>비용 단위: 밀리센트 (1$ = 100,000)</remarks> | |
void StartChatMetrics(string sessionId, Guid userId, Guid characterId); | |
/// <summary> | |
/// 기존 문자열 기반 파라미터 버전 (Guid 버전으로 대체 예정) | |
/// </summary> | |
[Obsolete("문자열 기반 userId/characterId 파라미터는 Guid 버전으로 변경하세요.")] | |
void StartChatMetrics(string sessionId, string userId, string characterId); | |
void StartProcessMetrics(string processName); | |
void EndProcessMetrics(string processName, decimal cost = 0, string? errorMessage = null, Dictionary<string, object>? additionalData = null); | |
void EndChatMetrics(); | |
ChatMetrics? GetCurrentChatMetrics(); | |
void LogChatMetrics(); | |
} | |
} |
public static double CalculateCost(string model, int promptTokens, int completionTokens) | ||
{ | ||
var inputCostPerToken = GetInputCostPerToken(model); | ||
var outputCostPerToken = GetOutputCostPerToken(model); | ||
|
||
var inputCostTotal = Math.Ceiling(promptTokens * inputCostPerToken); | ||
var outputCostTotal = Math.Ceiling(completionTokens * outputCostPerToken); | ||
|
||
return inputCostTotal + outputCostTotal; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
통합 비용 계산 메서드 구현 검토
Math.Ceiling을 사용한 비용 계산에 대해 검증이 필요합니다. 일반적으로 토큰 비용은 소수점 단위로 계산되므로 올림 처리가 적절한지 확인해야 합니다.
다음 스크립트로 비용 계산 로직을 검증해보겠습니다:
🏁 Script executed:
#!/bin/bash
# 비용 계산 로직 검증 - Math.Ceiling 사용이 적절한지 확인
echo "LLM 모델별 비용 정보 확인..."
rg -nP -A5 -B5 "CalculateCost.*promptTokens.*completionTokens" --type=cs
echo "Math.Ceiling 사용 사례 확인..."
rg -nP "Math\.Ceiling.*Token" --type=cs
Length of output: 4963
비용 계산 반올림 로직 개선 필요
현재 CalculateCost
메서드는 Math.Ceiling(promptTokens * inputCostPerToken)
와 Math.Ceiling(completionTokens * outputCostPerToken)
을 사용해 소수점 비용을 무조건 정수 단위로 올림 처리하고 있습니다 (LLMModelInfo.cs:320–321). 이로 인해 1,000,000토큰당 $1.50로 계산될 때 단일 토큰당 약 $0.0000015임에도 불구하고, 모든 비용이 최솟값 $1로 과도하게 산출될 수 있습니다.
OpenAI는 토큰 비용을 소수점(예: gpt-3.5-turbo-0613의 경우 입력 $1.50/1M, 출력 $2.00/1M → $0.0000015/token, $0.0000020/token)으로 책정하며, 비용 청구 시 일반적으로 전체 비용을 소수점 둘째 자리(센트)까지 반올림하거나 센트 단위로 올림 처리합니다 (openai.com).
따라서, 아래 중 하나의 방식으로 반올림 로직을 조정할 것을 강력히 권장합니다:
- 센트 단위 무조건 올림:
Math.Ceiling(rawCost * 100) / 100
- 소수점 둘째 자리 일반 반올림:
Math.Round(rawCost, 2, MidpointRounding.AwayFromZero)
수정 위치:
- ProjectVG.Common/Constants/LLMModelInfo.cs:
CalculateCost
메서드 (315–324줄)
예시 변경:
public static double CalculateCost(string model, int promptTokens, int completionTokens)
{
- var inputCostTotal = Math.Ceiling(promptTokens * inputCostPerToken);
- var outputCostTotal = Math.Ceiling(completionTokens * outputCostPerToken);
- return inputCostTotal + outputCostTotal;
+ var rawCost = (promptTokens * inputCostPerToken)
+ + (completionTokens * outputCostPerToken);
+
+ // 센트 단위 무조건 올림
+ return Math.Ceiling(rawCost * 100) / 100;
+
+ // 또는 소수점 둘째 자리 일반 반올림
+ // return Math.Round(rawCost, 2, MidpointRounding.AwayFromZero);
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public static double CalculateCost(string model, int promptTokens, int completionTokens) | |
{ | |
var inputCostPerToken = GetInputCostPerToken(model); | |
var outputCostPerToken = GetOutputCostPerToken(model); | |
var inputCostTotal = Math.Ceiling(promptTokens * inputCostPerToken); | |
var outputCostTotal = Math.Ceiling(completionTokens * outputCostPerToken); | |
return inputCostTotal + outputCostTotal; | |
} | |
public static double CalculateCost(string model, int promptTokens, int completionTokens) | |
{ | |
var inputCostPerToken = GetInputCostPerToken(model); | |
var outputCostPerToken = GetOutputCostPerToken(model); | |
- var inputCostTotal = Math.Ceiling(promptTokens * inputCostPerToken); | |
- var outputCostTotal = Math.Ceiling(completionTokens * outputCostPerToken); | |
- | |
var rawCost = (promptTokens * inputCostPerToken) | |
+ (completionTokens * outputCostPerToken); | |
// 센트 단위 무조건 올림 | |
return Math.Ceiling(rawCost * 100) / 100; | |
// 또는 소수점 둘째 자리 일반 반올림 | |
// return Math.Round(rawCost, 2, MidpointRounding.AwayFromZero); | |
} |
🤖 Prompt for AI Agents
ProjectVG.Common/Constants/LLMModelInfo.cs around lines 315–324: the current
CalculateCost uses Math.Ceiling on per-token totals which inflates costs; change
to compute raw inputCost = promptTokens * inputCostPerToken and raw outputCost =
completionTokens * outputCostPerToken, then apply cent-level rounding to the
summed totals per the reviewer choice — either round up to cents using
Math.Ceiling((rawInput+rawOutput) * 100) / 100 or round to two decimals using
Math.Round(rawInput+rawOutput, 2, MidpointRounding.AwayFromZero) — and return
that rounded decimal value instead of the current integer-ceiled totals.
Summary by CodeRabbit