llm 분석기능

This commit is contained in:
Macbook
2026-06-17 13:40:52 +09:00
parent 3ed09cd966
commit 7a2509a045
28 changed files with 2783 additions and 3 deletions
@@ -0,0 +1,188 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.goldenchart.dto.LlmTestResponse;
import com.goldenchart.dto.StrategyEvaluationAiVerifyResponse;
import com.goldenchart.service.LlmSettingsResolver.ResolvedLlmSettings;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import java.time.Duration;
/**
* 전략 평가 AI 검증 — 챗봇 MLX LLM(OpenAI 호환) 프록시.
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class StrategyEvaluationLlmService {
private static final String SYSTEM_PROMPT = """
당신은 GoldenChart 전략 평가(Ta4j Rule) 검증 전문가입니다.
사용자 JSON에는 선택 봉 시점의 조건 평가 결과, 차트 매매 시그널, 전략 DSL 요약이 포함됩니다.
ta4jDiagnostics 섹션은 서버가 동일 경로로 Ta4j Rule 을 재빌드·재평가한 결과(Rule descriptor, trace 로그, frontend vs backend 비교)입니다.
다음을 한국어로 분석하세요:
1) 선택 봉에서 매수/매도 전체 Rule(overallEntryMet/overallExitMet)과 개별 조건(satisfied)이 논리적으로 일치하는지
2) ta4jDiagnostics.reEvaluation·entryRule/exitRule.satisfiedAtEvalIndex 와 프론트 evaluation 이 일치하는지
3) entryRule/exitRule.evaluationTraceLog 로 Rule 평가 경로를 추적해 불일치 원인을 설명
4) 차트에 표시된 매매 시그널(selectedBarSignals, barSignalHighlight)이 평가 결과와 맞는지
5) 시그널이 없을 때: 실제로 조건 미충족이라 시그널이 없는 것인지, 로직·데이터 불일치 가능성이 있는지
6) 의심 지점이 있으면 구체적 조건 ID·지표·수치·Rule 타입을 인용
출력 형식(마크다운):
## 종합 판정
(정상 / 주의 / 오류 의심 — 한 줄 요약)
## 조건 평가 검증
(매수·매도 각각 bullet)
## 차트 시그널 검증
(선택 봉 시그널 vs Rule 결과)
## 결론 및 권장 조치
(1~3문장)
로직 문제(종합 판정이 주의·오류 의심)인 경우, 위 분석에서 불일치 원인·조건 ID·지표를 구체적으로 명시하세요.
""";
private static final String TEST_USER_PROMPT = "ping — 연결 테스트입니다. 한 문장으로 응답해 주세요.";
private final WebClient.Builder webClientBuilder;
private final ObjectMapper objectMapper;
private final LlmSettingsResolver llmSettingsResolver;
private final StrategyEvaluationTa4jDiagnosticsService ta4jDiagnosticsService;
public StrategyEvaluationAiVerifyResponse verify(JsonNode context, Long userId, String deviceId) throws Exception {
ResolvedLlmSettings cfg = llmSettingsResolver.resolve(userId, deviceId);
ensureEnabled(cfg);
if (context == null || context.isNull()) {
throw new IllegalArgumentException("context 가 필요합니다.");
}
ObjectNode enrichedContext = ta4jDiagnosticsService.enrichContext(context, userId, deviceId);
JsonNode ta4jDiagnostics = enrichedContext.path("ta4jDiagnostics");
long started = System.currentTimeMillis();
ObjectNode body = buildChatBody(cfg, SYSTEM_PROMPT,
"전략 평가 검증 데이터(JSON):\n"
+ objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(enrichedContext));
String responseJson = postChat(cfg, body);
JsonNode root = objectMapper.readTree(responseJson);
String content = extractAssistantContent(root);
if (content == null || content.isBlank()) {
throw new IllegalStateException("LLM 응답에서 분석 텍스트를 찾을 수 없습니다.");
}
return StrategyEvaluationAiVerifyResponse.builder()
.analysis(content.trim())
.model(root.path("model").asText(cfg.model()))
.latencyMs(System.currentTimeMillis() - started)
.ta4jDiagnostics(ta4jDiagnostics.isMissingNode() || ta4jDiagnostics.isNull()
? null : ta4jDiagnostics)
.build();
}
public LlmTestResponse testConnection(Long userId, String deviceId) {
ResolvedLlmSettings cfg = llmSettingsResolver.resolve(userId, deviceId);
ensureEnabled(cfg);
long started = System.currentTimeMillis();
try {
ObjectNode body = buildChatBody(cfg, "You are a helpful assistant.", TEST_USER_PROMPT);
body.put("max_tokens", 32);
String responseJson = postChat(cfg, body);
JsonNode root = objectMapper.readTree(responseJson);
String content = extractAssistantContent(root);
if (content == null || content.isBlank()) {
return LlmTestResponse.builder()
.ok(false)
.message("LLM 응답에서 텍스트를 찾을 수 없습니다.")
.model(root.path("model").asText(cfg.model()))
.latencyMs(System.currentTimeMillis() - started)
.build();
}
return LlmTestResponse.builder()
.ok(true)
.message("연결 성공: " + content.trim())
.model(root.path("model").asText(cfg.model()))
.latencyMs(System.currentTimeMillis() - started)
.build();
} catch (IllegalStateException e) {
return LlmTestResponse.builder()
.ok(false)
.message(e.getMessage())
.model(cfg.model())
.latencyMs(System.currentTimeMillis() - started)
.build();
} catch (Exception e) {
log.warn("[StrategyEvaluationLlm] 연결 테스트 실패: {}", e.getMessage());
return LlmTestResponse.builder()
.ok(false)
.message("연결 테스트 실패: " + e.getMessage())
.model(cfg.model())
.latencyMs(System.currentTimeMillis() - started)
.build();
}
}
private void ensureEnabled(ResolvedLlmSettings cfg) {
if (!cfg.enabled()) {
throw new IllegalStateException("LLM AI 검증이 비활성화되어 있습니다. 설정 > LLM에서 사용을 켜 주세요.");
}
}
private ObjectNode buildChatBody(ResolvedLlmSettings cfg, String systemPrompt, String userPrompt) {
ObjectNode body = objectMapper.createObjectNode();
body.put("model", cfg.model());
body.put("max_tokens", cfg.maxTokens());
body.put("temperature", cfg.temperature());
ArrayNode messages = body.putArray("messages");
messages.addObject().put("role", "system").put("content", systemPrompt);
messages.addObject().put("role", "user").put("content", userPrompt);
return body;
}
private String postChat(ResolvedLlmSettings cfg, ObjectNode body) {
WebClient client = webClientBuilder.baseUrl(cfg.baseUrl()).build();
try {
String responseJson = client.post()
.uri(cfg.chatPath())
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(body)
.retrieve()
.bodyToMono(String.class)
.block(Duration.ofMillis(cfg.timeoutMs()));
if (responseJson == null || responseJson.isBlank()) {
throw new IllegalStateException("LLM 응답이 비어 있습니다.");
}
return responseJson;
} catch (WebClientResponseException e) {
log.warn("[StrategyEvaluationLlm] LLM HTTP {} — {}", e.getStatusCode(), e.getResponseBodyAsString());
throw new IllegalStateException("LLM 서버 응답 오류: " + e.getStatusCode().value());
} catch (IllegalStateException e) {
throw e;
} catch (Exception e) {
log.warn("[StrategyEvaluationLlm] LLM 연결 실패: {}", e.getMessage());
throw new IllegalStateException("LLM 서버에 연결할 수 없습니다. (" + cfg.baseUrl() + cfg.chatPath() + ")");
}
}
private static String extractAssistantContent(JsonNode root) {
JsonNode choices = root.path("choices");
if (!choices.isArray() || choices.isEmpty()) return null;
JsonNode message = choices.get(0).path("message");
if (message.hasNonNull("content")) {
return message.path("content").asText(null);
}
return null;
}
}