
스키마 및 메타데이터가 충분한 질의에 대한 답변

스키마 및 메타데이터가 충분한 질의에 대한 엉터리 답변 ❗
public String ask(String question, boolean dbConfirmed) {
ChatClient chatClient = chatClientBuilder.build();
// ① 질문 유형 분류
String type = classifyQuestion(question, chatClient);
log.info("질문 유형: {}", type);
// ② DB 접근 허용 요청 (사실 허용 요청보다 DB 접속 정보 재확인)
if ((type.contains("DB") || type.contains("BOTH")) && !dbConfirmed) {
return "__DB_CONFIRM__:" + extractDbInfo(datasourceUrl);
}
String dbContext = "";
String docContext = "";
// ③-1. DB 질문이면 SQL 생성
if (type.contains("DB") || type.contains("BOTH")) {
String tableList = getTableList();
String sql = generateSqlDirect(question, tableList, chatClient);
log.info("생성된 SQL: {}", sql);
if (sql != null && !sql.isBlank() && !sql.equalsIgnoreCase("NONE")) {
String result = executeQuery(sql);
if (result.startsWith("SQL 실행 오류")) {
log.info("Self-correction 시도");
String schema = fetchLiveSchema(extractTableFromSql(sql));
String correctedSql = refineSql(question, schema, sql, result, chatClient);
result = executeQuery(correctedSql);
}
dbContext = result;
}
}
// ③-2. 문서 질문이면 벡터 검색
if (type.contains("DOC") || type.contains("BOTH")) {
docContext = fetchVectorContext(question);
}
log.info("DB: {}자, DOC: {}자", dbContext.length(), docContext.length());
return generateAnswer(question, dbContext, docContext, chatClient);
}
① [질의 라우팅] 질문 유형 분류
private String classifyQuestion(String question, ChatClient chatClient) {
String result = chatClient.prompt()
.system("""
질문을 분석해서 답변 생성에 필요한 데이터 소스를 판단하세요.
DB: 현재 데이터 조회 (실시간, 현재, 건수, 현황, 수량, 날짜별, 작업, 재고, 에러, 알람, 위치)
DOC: 사용법, 절차, 설명, 매뉴얼, 설계서 내용, 운영 방안
BOTH: DB + 문서 둘 다 필요
LLM: 일반 지식으로 답변 가능 (기술 설명, 개념 등)
DB, DOC, BOTH, LLM 중 하나만 출력하세요.
""")
.user(question)
.call()
.content()
.trim()
.toUpperCase();
if (result.contains("BOTH")) return "BOTH";
if (result.contains("DB")) return "DB";
if (result.contains("DOC")) return "DOC";
return "LLM";
}
ClassifyQuestion 함수 내 프롬프트 라우팅으로 RAG는 검색 전략 유형을 수립하도록 한다.
위 버전은 초기 RAG 질의 라우팅에 사용한 코드이다.
*폐쇄망을 고려하기에 일반적인 웹 검색은 고려하지 않도록 한다.
② [DB 조회] getTableList
가령, 스키마에는 있지만 데이터베이스에는 없는 테이블, 혹은 그 반대의 경우가 존재함을 방지하고자 함이다.
private String getTableList() {
try {
List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
SELECT
TABLE_NAME AS NAME,
'TABLE' AS OBJ_TYPE,
c.COMMENTS
FROM ALL_TABLES
WHERE OWNER='HMX_KCTC'
UNION ALL
SELECT
VIEW_NAME AS NAME,
'VIEW' AS OBJ_TYPE,
c.COMMENTS
FROM ALL_VIEWS
WHERE OWNER='HMX_KCTC'
ORDER BY OBJ_TYPE, NAME
""");
return rows.stream()
.map(r -> {
String line = r.get("OBJ_TYPE") + ": " + r.get("NAME");
if (r.get("COMMENTS") != null) {
line += " -- " + r.get("COMMENTS");
}
return line;
})
.collect(Collectors.joining("\n"));
} catch (Exception e) {
log.error("getTableList ERROR: {}", e.getMessage());
return "";
}
}
③ [DB 조회] generateSqlDirect
private String generateSqlDirect(String question, String tableList, ChatClient chatClient) {
// Chroma에서 관련 스키마 검색
String schemaContext = fetchVectorContext(question + " 컬럼 DDL 스키마");
return chatClient.prompt()
.system("""
Oracle DBA 엔지니어이자 자동화창고 도메인 전문가이자 시스템 WCS*WMS 전문가입니다.
[테이블/뷰 목록]과 [스키마 정보]를 참고하여
질문에 맞는 SQL을 생성하세요.
규칙:
- SELECT 문만 생성
- SQL 코드만 출력 (마크다운 없이)
- 반드시 HMX_KCTC.테이블명 형식
- FETCH FIRST 20 ROWS ONLY
- 세미콜론 절대 포함 금지
- [테이블/뷰 목록]에 없는 컬럼명 절대 사용 금지
- [테이블/뷰 목록]에 없는 테이블,뷰 절대 사용 금지 (엄수해야한다.)
- [스키마 정보]에 있더라도 [테이블/뷰 목록]에 없으면 사용 금지
- 뷰와 테이블 모두 키워드를 포함한다면, 뷰 우선 사용 (e,g. V_T_WORK_MONIT와 T_WORK 둘 다 있으면 V_T_WORK_MONIT 조회 결과 우선 사용)
- DB 조회 불필요하면 NONE
""")
.user("""
[테이블/뷰 목록]
%s
[스키마 정보]
%s
[질문]
%s
SQL:
""".formatted(tableList, schemaContext, question))
.call()
.content()
.replaceAll("```sql", "")
.replaceAll("```", "")
.trim();
}
이때, fetchVectorContext 함수는 RAG 벡터 검색을 수행한다.
private String fetchVectorContext(String question) {
try {
// 벡터 내 유사도 검색 (코사인 유사도 거리가 높은 5순위까지)
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.builder()
.query(question)
.topK(5)
.build()
);
// 26.05.28 Reranking 로직 추가 가능 (예: LLM으로 간단히 재점수화)
log.info("벡터 검색 결과: {}건", docs.size());
docs.forEach(doc -> log.info("문서 미리보기: {}",
doc.getText().substring(0, Math.min(200, doc.getText().length()))));
if (docs.isEmpty()) return "";
return docs.stream()
.map(Document::getText)
.collect(Collectors.joining("\n---\n"));
} catch (Exception e) {
log.error("벡터 검색 오류: {}", e.getMessage());
return "";
}
}