기술

Text-to-Cypher를 도구로 만들면서 검증 레이어를 같이 깔았습니다

2026.05.1613분 읽기

도구 20개로 답할 수 없는 질문이 있었습니다

RAG 시스템에 검색 도구와 그래프 도구를 9개 만들어 두었습니다. 이메일 검색, 문서 검색, 시스템 검색, 그래프 이웃 조회, 영향도 분석, 최단 경로 같은 도구들이에요. 일반적인 질문은 이 9개 조합으로 답이 나왔습니다.

그런데 어떤 질문은 이 도구들 어디에도 깔끔하게 매칭되지 않았어요.

"가장 많은 시스템이 의존하는 서버 Top 5"

"DEPENDS_ON 관계가 3개 이상인 시스템 목록"

"이메일을 가장 많이 보낸 사람 Top 3"

이런 질문들은 본질적으로 집계, 정렬, 조건 필터 가 결합된 쿼리입니다. 미리 만들어둔 도구 하나로 답하기 어렵고, 그렇다고 모든 가능한 집계 패턴을 도구로 만들어둘 수도 없었어요.

답은 명확했습니다. 자연어를 Cypher 쿼리로 변환하는 도구를 만드는 것입니다. Neo4j의 쿼리 언어인 Cypher로 변환하면 어떤 집계·필터·정렬도 표현할 수 있으니까요.

비슷한 시점에 node2vec 기반 노드 임베딩 도구도 같이 만들었습니다. 그래프 구조에서 "비슷한 위치에 있는 노드"를 찾는 도구인데, "특정 시스템이 죽으면 대체할 만한 시스템은?" 같은 질문에 쓰입니다. 64차원 벡터로 노드를 표현하고 Qdrant에 적재해서, 유사도 검색으로 대체 후보를 찾습니다.

대체 후보 검증 결과:
  특정 시스템 → 비슷한 서비스 시스템 (유사도 0.94)
  특정 시스템 → 디렉토리 서버 (유사도 0.88)

node2vec은 비교적 안전한 도구였습니다. 입력은 노드 이름이고, 출력은 미리 계산된 벡터 유사도였어요. LLM이 끼어들 여지가 없었습니다.

문제는 Text-to-Cypher였습니다.


LLM이 만든 쿼리를 DB에 그대로 실행하는 게 무서웠습니다

Text-to-Cypher의 동작 흐름은 단순해 보입니다.

사용자: "가장 많은 시스템이 의존하는 서버 Top 5"
  → LLM에 스키마 + 질문 전달
  → LLM이 Cypher 생성
  → Neo4j 실행
  → 결과 반환

그런데 가운데 단계가 위험합니다. LLM이 만든 쿼리를 DB에 그대로 실행한다는 게 핵심이에요.

LLM이 만들 수 있는 위험한 쿼리들이 있습니다.

쓰기 쿼리: CREATE, DELETE, SET — 데이터 변경 또는 삭제

무제한 경로 탐색: [*] 또는 [*..10] — 전체 그래프를 휘젓는 쿼리

전체 스캔: MATCH (n) RETURN n — 조건 없이 모든 노드 조회

무한 루프 가능 쿼리: 카르테시안 곱이나 사이클이 있는 깊은 탐색

LLM이 의도적으로 위험한 쿼리를 만들 가능성은 낮지만, 실수로 만들 가능성은 있습니다. 자연어가 모호하면 LLM이 잘못 해석할 수 있고, 그 결과 DB가 다운되거나 잘못된 데이터 변경이 일어날 수 있어요.

LLM의 출력을 100% 신뢰할 수 없다면, 출력을 받아서 그대로 실행하기 전에 검증 레이어 가 필요합니다.


가드레일 4가지를 깔았습니다

검증 레이어를 4단계로 만들었어요.

가드레일

동작

차단되는 케이스

쓰기 쿼리 차단

코드에서 자동 거부

CREATE, DELETE, SET, MERGE 포함 시

경로 깊이 제한

4홉 이상 자동 거부

[*..4] 이상의 경로 변수

전체 스캔 금지

프롬프트에서 명시

WHERE 없는 광범위 MATCH

실행 타임아웃

10초 초과 시 중단

의도치 않은 무거운 쿼리

가드레일의 위치가 중요했어요. 코드로 막을 수 있는 건 코드로, 프롬프트로 막아야 할 건 프롬프트로 갈렸습니다.

코드 가드레일 (1, 2, 4번): 쿼리 텍스트를 받은 직후 정규식 검증, 그리고 Neo4j 실행 시 타임아웃 설정. 이건 LLM이 어떤 출력을 내든 코드 레벨에서 끝납니다.

프롬프트 가드레일 (3번): "조건 없는 광범위한 매칭을 하지 마라"는 지시. 이건 정규식으로 잡기 어려운 영역이라 프롬프트에 명시했어요. LLM이 따르도록 유도하지만 100% 보장은 안 됩니다. 그래서 타임아웃이 마지막 안전망 역할을 합니다.

# 코드 가드레일 예시
def validate_cypher(query: str) -> tuple[bool, str]:
    write_keywords = ["CREATE", "DELETE", "SET", "MERGE", "DROP"]
    if any(kw in query.upper() for kw in write_keywords):
        return False, "쓰기 쿼리 금지"

    if re.search(r"\[\*\.\.[4-9]\]|\[\*\.\.\d{2,}\]|\[\*\]", query):
        return False, "경로 깊이 4홉 이상 금지"

    return True, ""

이 가드레일을 통과한 쿼리만 Neo4j에서 실행됩니다. 차단된 쿼리는 사용자에게 "이 질문은 더 구체적으로 묻거나 다른 방식으로 풀어 달라"는 응답이 가요.

1.jpeg


Cypher 로그로 피드백 루프를 준비했습니다

가드레일을 깔았다고 끝이 아니었습니다. LLM이 만든 쿼리가 시간이 지나면서 어떻게 진화하는지를 봐야 했어요.

어떤 질문에서 자주 실패하는가?

어떤 쿼리가 가드레일에 막히는가?

성공한 쿼리들에 공통 패턴이 있는가?

이걸 알려면 모든 실행 이력이 적재되어야 합니다. 그래서 별도 cypher_log 테이블을 만들었어요.

cypher_log (
  id, conversation_id, original_query,
  generated_cypher,       -- LLM이 생성한 쿼리
  status,                 -- success / error / blocked
  result_count, error_message,
  latency_ms, created_at
)

generate_cypher() 함수의 finally 블록에서 자동 저장합니다. 성공·에러·차단을 모두 기록해요.

운영 화면에서 통계를 볼 수 있게 했습니다.

총 실행 / 성공 / 에러 / 성공률 / 평균 응답 시간

필터: 전체 / 성공 / 에러 / 차단

상세: 원본 질문 + 생성된 Cypher + 에러 메시지

처음에는 적재만 시작했어요. 분석은 데이터가 충분히 쌓인 후에 합니다. 이 구조가 만들어둔 피드백 루프는 4단계입니다.

1단계: 적재          (지금)
2단계: 축적          (시간 지나며)
3단계: 분석          (에러 패턴 확인)
4단계: 개선
  경로 A — 에러 패턴 → 프롬프트에 규칙 추가
  경로 B — 성공 패턴 → Cypher 템플릿 캐싱

운영 시스템에서 자주 보는 패턴이에요. 로그를 미리 적재해두지 않으면 나중에 개선할 데이터가 없습니다. 분석은 나중에 해도 되지만 적재는 처음부터 깔아둬야 했습니다.

2.jpeg


CRAG와 연결해서 실패도 답변이 되게 했습니다

Cypher 쿼리가 실패하거나 차단되면 어떻게 처리할지도 결정해야 했어요.

처음에는 단순하게 "에러를 사용자에게 보여준다"고 생각했습니다. 그런데 사용자는 Cypher 쿼리가 뭔지 모릅니다. "쿼리 문법 에러"라는 응답은 도움이 안 돼요.

대신 CRAG와 연동시켰습니다. Cypher가 실패하면 자동으로 다른 텍스트 검색 도구로 확장해서 답을 시도합니다.

사용자 질문 → Cypher 생성 → 실행 실패
            → CRAG 발동 → 텍스트 검색 도구로 확장
            → 답변 생성

비슷한 패턴은 다른 곳에서도 봤어요. LLM 라우팅이 100% 정확하지 않다는 걸 인정하고 보조 시그널을 같이 까는 구조와 같은 발상이었습니다. 단일 메커니즘에 책임을 다 떠넘기지 않는 것이 운영 안정성의 기본기였어요.


LLM에게 코드 생성을 맡길 때의 일반 패턴

이번 작업을 정리하면서 한 가지 일반 패턴이 보였습니다. LLM에게 코드나 쿼리 생성을 맡기는 모든 케이스에 적용되는 원칙이에요.

단계

역할

위치

1. LLM 생성

자연어 → 코드

LLM

2. 정적 검증

위험 패턴 차단

코드 (정규식·AST)

3. 의미 검증

의도 일치 확인

프롬프트

4. 실행 안전망

타임아웃·리소스 제한

실행 환경

5. 로그 적재

모든 시도 기록

DB

6. 폴백

실패 시 대안

다른 도구 / CRAG

이 6단계 중 어느 하나도 빠지면 안전성이 떨어집니다. 특히 2번(정적 검증)과 4번(실행 안전망)이 코드 레벨, 3번(의미 검증)이 프롬프트 레벨, 5번(로그)이 운영 레벨에서 따로 책임지는 구조여야 했어요.

Text-to-SQL, Text-to-Bash, Text-to-Python 같은 다른 영역도 본질적으로 같은 패턴입니다. LLM이 만든 코드를 시스템에 실행시킨다는 점에서 위험성이 동일하니까요.


마무리

기존 검색·그래프 도구로 답할 수 없는 집계·정렬 질문을 처리하기 위해 Text-to-Cypher 도구를 만들었습니다. 자연어를 LLM이 Cypher 쿼리로 변환하고 Neo4j에서 실행하는 도구인데, LLM의 출력을 DB에 그대로 실행하는 위험을 줄이기 위해 4단계 가드레일(쓰기 차단, 경로 깊이 제한, 전체 스캔 금지, 타임아웃)을 같이 깔았습니다.

Cypher 로그 테이블을 별도로 만들어 모든 실행 이력을 적재했습니다. 지금은 적재만 하고 있지만, 데이터가 쌓이면 에러 패턴 분석 → 프롬프트 개선 → 성공 패턴 캐싱으로 이어지는 피드백 루프의 기반이 됩니다. 실패한 쿼리는 CRAG와 연결해서 다른 도구로 자동 확장되도록 했어요.

LLM에게 코드나 쿼리 생성을 맡기는 작업이 있다면, 정적 검증과 실행 안전망과 로그 적재 세 가지를 같이 까는 것을 추천합니다. LLM의 출력을 100% 신뢰할 수 없다면, 그 출력이 시스템에 영향을 주기 전에 여러 층의 검증이 있어야 했어요. 그리고 모든 시도를 적재해야 나중에 개선할 데이터가 생깁니다.

#Text-to-Cypher#LLM가드레일#Neo4j#node2vec#Cypher#피드백루프#AI전환기