Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,25 @@ public class CreateChatbotResponseDto {
private String graphId;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm")
private LocalDateTime createdAt;
private List<String> contextChunks; //LLM에 넘긴 context 문장들 (이름은 `augmentedSentences` 등으로 변경 권장)
private List<String> retrievedTriples; //관계 중심의 3요소 표현 ("물 -상태변화→ 응고")
private List<String> sourceNodes; //질의에 사용된 핵심 노드들 ("물", "응고" 등)
private List<String> 증강할때쓴자료; //LLM에 넘긴 context 문장들 (이름은 `augmentedSentences` 등으로 변경 권장)

private Map<String, String> ragMeta; //(ex: 사용한 쿼리문 등)

public static CreateChatbotResponseDto of(
String chatContent,
String graphId,
LocalDateTime createdAt,
List<String> retrievedChunks,
List<String> contextChunks,
List<String> retrievedTriples,
List<String> sourceNodes
) {
return CreateChatbotResponseDto.builder()
.chatContent(chatContent)
.graphId(graphId)
.createdAt(createdAt)
.retrievedTriples(retrievedChunks)
.contextChunks(contextChunks)
.retrievedTriples(retrievedTriples)
.sourceNodes(sourceNodes)
.ragMeta(Map.of("chunkCount", String.valueOf(retrievedChunks.size())))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@
@Getter
@AllArgsConstructor
public class GraphQueryResult {
private String sentence;
private String nodeLabel;
}
private String sentence; // 예: "물은 응고되어 얼음이 된다."
private String sourceLabel; // 예: "물"
private String relationLabel; // 예: "응고"
private String targetLabel; // 예: "얼음"
private String nodeLabel; // 예: "물" (질의어에 가까운 노드)

public String toTripleString() {
if (sourceLabel == null || relationLabel == null || targetLabel == null) return null;

// 혹시 내부 문자열이 "null"로 들어오는 것도 막기
if ("null".equals(sourceLabel) || "null".equals(relationLabel) || "null".equals(targetLabel)) return null;

return String.format("(%s)-[:RELATED {label: '%s'}]->(%s)", sourceLabel, relationLabel, targetLabel);
}

}
Original file line number Diff line number Diff line change
@@ -1,38 +1,49 @@
package com.going.server.domain.rag.service;

import com.going.server.domain.openai.service.OpenAIService;
import com.theokanning.openai.OpenAiService;
import com.theokanning.openai.completion.chat.ChatMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.List;

// 1. 질문 → Cypher 쿼리 생성 (LLM)
@Component
@RequiredArgsConstructor
public class CypherQueryGenerator {
private final OpenAIService openAIService;

public String generate(String userQuestion) {
String prompt = """
당신은 Neo4j용 Cypher 쿼리를 생성하는 AI입니다.
주어진 질문에 대해 Cypher 쿼리만 반환하세요. 코드블록, 설명 없이 오직 쿼리만 출력해야 합니다.
당신은 Neo4j 그래프 데이터베이스에서 정보를 추출하기 위한 Cypher 쿼리를 생성하는 AI입니다.

예:
질문: "고래와 관련된 개념들을 알려줘"
→ MATCH (n:GraphNode)-[r]->(m:GraphNode)\s
WHERE n.label = '고래'\s
RETURN m.label AS nodeLabel, m.includeSentence AS sentence\s
LIMIT 10

질문: "${userQuestion}"
- 주어진 질문에서 핵심 개념과 연관된 개념들을 찾아야 합니다.
- 질문에 포함된 키워드와 의미적으로 밀접한 노드 쌍 간의 관계(triple)를 추출해야 합니다.
- 반드시 관계 중심 구조 (시작 노드, 관계 라벨, 도착 노드)를 반환하는 Cypher 쿼리를 작성하세요.
- 관계나 노드에 포함된 설명 문장 중 하나를 함께 반환하세요. (r.sentence → 없으면 a.includeSentence → 없으면 b.includeSentence 순으로)
- 반환 항목은 다음과 같아야 합니다:
sourceLabel, relationLabel, targetLabel, sentence, nodeLabel
- 코드는 반드시 Cypher 쿼리 한 줄만 출력하며, 코드블록이나 설명은 포함하지 마세요.

예시:
질문: "고래와 관련된 개념들을 알려줘"
MATCH (a:GraphNode)-[r:RELATED]-(b:GraphNode)
WHERE toLower(a.label) CONTAINS toLower('고래') OR toLower(b.label) CONTAINS toLower('고래')
RETURN
a.label AS sourceLabel,
r.label AS relationLabel,
b.label AS targetLabel,
COALESCE(r.sentence, a.includeSentence, b.includeSentence, "") AS sentence,
a.label AS nodeLabel
LIMIT 15

질문: "%s"
""".formatted(userQuestion);
""".formatted(userQuestion);

return openAIService.getCompletionResponse(
List.of(new ChatMessage("user", prompt)),
"gpt-4-0125-preview", 0.2, 1000
"gpt-4o", 0.2, 500
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,45 @@
@RequiredArgsConstructor
public class GraphQueryExecutor {

private final Driver neo4jDriver; // Neo4j Java Driver
private final Driver neo4jDriver;

public List<GraphQueryResult> runQuery(Long graphId, String cypherQuery) {
List<GraphQueryResult> results = new ArrayList<>();

try (Session session = neo4jDriver.session()) {
Result result = session.run(cypherQuery);

while (result.hasNext()) {
Record record = result.next();

// 필드 이름은 Cypher 쿼리 결과와 일치해야 함
String sentence = record.get("sentence").asString("");
String nodeLabel = record.get("nodeLabel").asString("");
String sentence = getSafeString(record, "sentence");
String nodeLabel = getSafeString(record, "nodeLabel");

String sourceLabel = getSafeString(record, "sourceLabel");
String relationLabel = getSafeString(record, "relationLabel");
String targetLabel = getSafeString(record, "targetLabel");

results.add(new GraphQueryResult(sentence, nodeLabel));
results.add(new GraphQueryResult(
sentence,
nodeLabel,
sourceLabel,
relationLabel,
targetLabel
));
}

} catch (Exception e) {
System.err.println("[GraphRAG] Cypher 쿼리 실행 중 오류 발생:");
e.printStackTrace();
}

return results;
}

// 안전한 String 추출 (null-safe)
private String getSafeString(Record record, String key) {
return record.containsKey(key) && !record.get(key).isNull()
? record.get(key).asString()
: null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Service
@RequiredArgsConstructor
Expand All @@ -30,13 +32,7 @@ public class GraphRAGService {

/**
* 사용자 질문에 대해 Cypher 쿼리 → 그래프 정보 검색 → 프롬프트 생성 → LLM 응답 생성
* 본 메서드는 LangChain 없이 구현한 Spring 기반 GraphRAG의 핵심 흐름입니다.
*
* private LocalDateTime createdAt;
* private List<String> retrievedTriples; //관계 중심의 3요소 표현 ("물 -상태변화→ 응고")
* private List<String> sourceNodes; //질의에 사용된 핵심 노드들 ("물", "응고" 등)
* private List<String> 증강할때쓴자료; //LLM에 넘긴 context 문장들 (이름은 `augmentedSentences` 등으로 변경 권장)
* -> 이렇게 결과 나오도록 정리
* LangChain 없이 구현한 Spring 기반 GraphRAG의 핵심 흐름
*/
public CreateChatbotResponseDto createAnswerWithGraphRAG(
Long dbId,
Expand All @@ -47,52 +43,60 @@ public CreateChatbotResponseDto createAnswerWithGraphRAG(
log.info("[GraphRAG] dbId: {}, question: {}", dbId, userQuestion);

// 1. 질문 → Cypher 쿼리 생성
String cypherQuery = cypherQueryGenerator.generate(userQuestion).trim()
.replaceAll("(?s)```cypher.*?```", "") // 마크다운 제거
.replaceAll("```", "") // 남은 ``` 제거
.trim();
log.info("[GraphRAG] Generated Cypher Query:\n{}", cypherQuery);
String rawQuery = cypherQueryGenerator.generate(userQuestion);
// ```cypher ~ ``` 블록 제거
Matcher m = Pattern.compile("(?s)```cypher\\s*(.*?)\\s*```").matcher(rawQuery);
String cleaned = m.find() ? m.group(1) : rawQuery;
// 남은 ``` 제거
cleaned = cleaned.replaceAll("```", "").trim();
log.info("[GraphRAG] Cypher Query 생성됨:\n----\n{}\n----", cleaned);

// 2. 쿼리 실행 → 문맥(context) 및 노드 라벨 추출
List<GraphQueryResult> queryResults = graphQueryExecutor.runQuery(dbId, cypherQuery);
List<GraphQueryResult> queryResults = graphQueryExecutor.runQuery(dbId, cleaned);
// 문장
List<String> contextChunks = queryResults.stream()
.map(GraphQueryResult::getSentence)
.filter(s -> s != null && !s.isBlank())
.distinct()
.toList();

// 관계 트리플
List<String> retrievedTriples = queryResults.stream()
.map(GraphQueryResult::toTripleString)
.distinct()
.toList();
// 노드
List<String> sourceNodes = queryResults.stream()
.map(GraphQueryResult::getNodeLabel)
.filter(n -> n != null && !n.isBlank())
.distinct()
.toList();

log.info("[GraphRAG] Retrieved {} context chunks", contextChunks.size());
retrievedTriples.forEach(triple ->
log.info("[GraphRAG] Triple: {}", triple)
);
log.info("[GraphRAG] Retrieved {} triples", retrievedTriples.size());

// 3. 프롬프트 구성
String finalPrompt = promptBuilder.buildPrompt(contextChunks, userQuestion);
String finalPrompt = promptBuilder.buildPrompt(contextChunks, retrievedTriples, userQuestion);
log.info("[GraphRAG] Final Prompt constructed");

// 4. RAG 응답 생성
String response = contextChunks.isEmpty()
? ragAnswerCreateService.chat(chatHistory, userQuestion)
: ragAnswerCreateService.chatWithContext(chatHistory, finalPrompt);
boolean hasContext = !contextChunks.isEmpty() || !retrievedTriples.isEmpty();
String response = hasContext
? ragAnswerCreateService.chatWithContext(chatHistory, finalPrompt)
: ragAnswerCreateService.chat(chatHistory, userQuestion);
log.info("[GraphRAG] Response generated by LLM");

// 5. 응답 저장
Chatting answer = Chatting.ofGPT(graph, response);
chattingRepository.save(answer);
log.info("[GraphRAG] Response saved to DB");

// 임시 retrievedTriples 설정
List<String> retrievedTriples = List.of(
"(물)-[:RELATED {label: '상태변화'}]->(기화)",
"(기화)-[:RELATED {label: '조건'}]->(높은 온도)",
"(수증기)-[:RELATED {label: '응결'}]->(물방울)",
"(물)-[:RELATED {label: '응고'}]->(얼음)",
"(응고)-[:RELATED {label: '예시'}]->(겨울철 얼어붙은 길)"
);

return CreateChatbotResponseDto.of(
response,
dbId.toString(),
answer.getCreatedAt(),
contextChunks,
retrievedTriples,
sourceNodes
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ public class RagAnswerCreateService {
private final OpenAIService openAIService;

private static final String SYSTEM_PROMPT = """
당신은 초등학생의 이해를 돕는 친절하고 정확한 지식 튜터입니다.
- 아래 제공된 데이터를 기반으로 질문에 대해 매우 길고 정확하게 설명해주세요.
- 만약 참고 데이터가 없다면, 관련정보 없다고 하세요.
- 반드시 한글로만 응답하고, 인사말이나 불필요한 문장은 생략한 대답만 반환하세요.
""";
당신은 초등학생의 이해를 돕는 친절하고 정확한 지식 튜터입니다.

- 아래에 제공된 '관계 정보'와 '설명 문장'은 질문과 관련된 지식그래프에서 추출된 정보입니다.
- 반드시 이 정보를 바탕으로 질문에 대해 정확하고 구체적으로 설명해주세요.
- 관계 간의 연결 흐름이나 개념 간 연관성을 쉽게 풀어 설명해 주세요.
- 필요 이상으로 친절하거나 장황하게 말하지 말고, 정확하고 알기 쉽게 대답만 하세요.
- 대답은 반드시 한글로만 작성하고, 인사말이나 부가 설명 없이 본문만 반환하세요.
""";

private static final String MODEL_NAME = "gpt-4o";
private static final double TEMPERATURE = 0.3;
private static final int MAX_TOKENS = 1500;
private static final int MAX_TOKENS = 1200;

public String chat(List<Chatting> chatHistory, String question) {
List<ChatMessage> messages = new ArrayList<>();
Expand Down
18 changes: 13 additions & 5 deletions src/main/java/com/going/server/domain/rag/util/PromptBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,25 @@
@Component
public class PromptBuilder {

public String buildPrompt(List<String> chunks, String question) {
public String buildPrompt(List<String> contextChunks, List<String> triples, String userQuestion) {
StringBuilder sb = new StringBuilder();

sb.append("다음 정보를 참고하여 질문에 답해주세요.\n\n");
sb.append("[관련 정보]\n");

for (String chunk : chunks) {
sb.append("- ").append(chunk.trim()).append("\n");
if (!triples.isEmpty()) {
sb.append("[관계 정보]\n");
triples.forEach(triple -> sb.append("- ").append(triple).append("\n"));
sb.append("\n");
}
if (!contextChunks.isEmpty()) {
sb.append("[설명 문장]\n");
for (int i = 0; i < contextChunks.size(); i++) {
sb.append(i + 1).append(". ").append(contextChunks.get(i)).append("\n");
}
sb.append("\n");
}

sb.append("\n[질문]\n").append(question.trim()).append("\n\n");
sb.append("질문: ").append(userQuestion);
sb.append("[답변]\n");

return sb.toString();
Expand Down