RAG 검색 라우팅, 프롬프트를 고쳐도 안 돼서 온톨로지한테 넘겼더니 풀렸다
검색을 어디로 보낼지, LLM한테 맡기고 있었더니
RAG 파이프라인에는 "검색 라우팅"이라는 단계가 있습니다.
사용자가 질문을 입력하면, 그 질문을 벡터 검색으로 보낼지, 그래프 탐색으로 보낼지, 아니면 둘 다 보낼지를 결정하는 거거든요.
저희 시스템에는 검색 경로가 7~8가지 있었습니다.
문서 벡터 검색, 시스템 정보 검색, 그래프 이웃 탐색 등등.
이 중에서 질문에 맞는 경로를 골라야 하는데, 이걸 LLM(Gemini)한테 맡기고 있었습니다.
질문을 던지면 LLM이 의도를 분석하고, "이 질문은 벡터 검색이 적절하겠다" 또는 "그래프 탐색이 필요하겠다"를 판단해서 검색 대상을 정해주는 구조였거든요.
그래프로 가야 하는 질문을, 벡터 검색으로 보내버린다
문제는 LLM이 그래프 검색이 필요한 질문을 자꾸 벡터 검색으로 보내는 거였습니다.
예를 들어 "물류서버-A에 연결된 시스템은?" 같은 질문이 있으면, Neo4j에서 물류서버-A 노드의 관계를 탐색하는 게 맞거든요.
그런데 LLM은 이걸 벡터 검색으로 보냈습니다.
"물류서버-A"가 포함된 문서를 찾아서 거기서 답을 만들려고 하는 거예요.
문서에 물류서버-A가 언급된 적 있으면 뭔가 답이 나오기는 하는데, 실제 연결 관계와는 다른 내용이 나옵니다.
그래프에는 물류서버-A에 연결된 시스템이 5개로 나오는데, 벡터 검색 결과에서는 문서에 같이 언급된 시스템 2개만 나오는 식이거든요.
"결제서비스의 의존성을 알려줘" 같은 질문도 마찬가지였습니다.
의존성 관계는 그래프에 노드 간 연결로 들어있는 건데, LLM은 이것도 벡터 검색으로 보냈어요.
프롬프트를 고쳐봤지만, 근본적으로 안 된 이유
당연히 프롬프트를 고쳐봤습니다.
첫 번째 시도는 프롬프트에 예시를 추가한 거였어요.
"물류서버-A에 연결된 시스템은? → 그래프 탐색"처럼 명시적으로 넣었거든요.
그런데 LLM이 예시와 거의 같은 구조의 질문을 받아도 벡터 검색으로 보내는 경우가 있었습니다.
프롬프트가 길어질수록 예시를 무시하는 빈도가 늘어나더라고요.
두 번째 시도는 키워드 강제 규칙이었습니다.
"연결", "영향", "관계" 같은 단어가 들어가면 무조건 그래프 검색으로 보내는 규칙이요.
이건 반대 부작용이 생겼습니다.
"장애 대응 회의 내용 알려줘"처럼 "관계"와 상관없는 질문에도 "장애"라는 단어 때문에 그래프로 보내는 거예요.
세 번째로 검색 경로 설명 자체를 더 상세하게 바꿔봤는데, 일부 케이스만 개선되고 다른 케이스가 깨지는 패턴이 반복됐습니다.
시도 | 결과 |
|---|---|
프롬프트에 예시 추가 | 유사 질문에서도 무시하고 벡터 검색으로 보냄 |
키워드 강제 규칙 | "장애 회의 내용" 같은 질문까지 그래프로 보내는 부작용 |
검색 경로 설명 수정 | 일부 개선, 다른 케이스 깨짐 |
세 가지를 다 시도하고 나서 느낀 건, 프롬프트를 고치는 걸로는 한계가 있다는 거였습니다.
LLM은 "물류서버-A"가 그래프에 실제로 존재하는 노드인지 아닌지를 모르거든요.
질문의 텍스트만 보고 판단하니까, 확률적으로 틀릴 수밖에 없었어요.

LLM을 고치지 말고, 데이터가 알려주게 하자
발상을 바꿨습니다.
LLM이 "물류서버-A"가 그래프에 있는 노드인지 모른다면, Neo4j한테 직접 물어보면 되는 거거든요.
질문에서 단어를 뽑고, 그 단어가 Neo4j에 실제로 존재하는 엔티티인지 확인하는 겁니다.
존재하면 그래프 검색을 자동으로 추가하고, 없으면 LLM 판단 그대로 두고요.
비유하자면 이런 거예요.
도서관에서 "물류서버 관련 자료 찾아주세요"라고 하면, 사서(LLM)가 키워드 검색 코너로 보내줄 수 있거든요.
그런데 도서관에 "물류서버" 전용 서랍이 따로 있다면, 그 서랍이 있다는 사실 자체가 "여기도 같이 찾아봐"라는 신호가 되는 거예요.
사서의 판단을 뒤집는 게 아니라, 서랍이 존재한다는 정보를 사서한테 알려주는 방식입니다.
이게 핵심 발상 전환이었습니다.
프롬프트를 더 정교하게 만드는 게 아니라, 데이터 구조가 라우팅을 보정하게 한 거예요.
LLM 라우팅은 그대로 두고, 뒤에서 온톨로지가 빠진 경로를 채워주는 구조입니다.
온톨로지 보강, 실제로 이렇게 동작한다
구체적인 동작 흐름을 보면 이렇습니다.
1단계 — LLM 라우팅 (기존 그대로)
사용자가 "물류서버-A에 연결된 시스템은?"이라고 질문하면, LLM이 의도를 분석합니다.
여기서 LLM이 "벡터 검색 + 시스템 검색"을 선택했다고 가정할게요.
그래프 탐색은 빠진 상태입니다.
2단계 — 온톨로지 보강 (새로 추가한 단계)
LLM이 그래프 탐색을 선택하지 않았으면, 보강 로직이 작동합니다.
질문에서 단어를 추출하고, 각 단어를 Neo4j에서 검색해요.
"물류서버-A"를 검색했더니 System 타입 노드로 존재하고, 관계가 20건 이상 연결되어 있으면 — 검색 대상에 그래프 탐색을 자동으로 추가합니다.
3단계 — 검색 실행
이제 벡터 검색 + 시스템 검색 + 그래프 탐색이 동시에 실행됩니다.
그래프 탐색이 자동으로 끼어든 거예요.
4단계 — 결과 합산
세 가지 검색 결과를 합쳐서 리랭크하고, LLM이 최종 답변을 생성합니다.
보강 조건은 단순합니다.
1. LLM이 그래프 검색을 이미 선택했으면 → 보강 안 함 (중복 방지)
2. 질문에서 2글자 이상 단어 추출
3. 각 단어를 Neo4j에서 검색 (System → Service → Person 순)
4. 매칭 노드가 있고 관계가 1건 이상이면 → 그래프 탐색 자동 추가Neo4j 조회 1건이 추가되는 건데, 단순 매칭 쿼리라 0.1초도 안 걸리거든요. LLM 라우팅 코드를 건드리지 않아도 되고, 새로운 엔티티가 Neo4j에 추가되면 보강 범위도 자동으로 늘어납니다.

이 구조가 좋았던 이유
이 방식의 가장 큰 장점은 LLM 라우팅을 건드리지 않는다는 거였습니다.
프롬프트를 고치면 한쪽이 좋아질 때 다른 쪽이 깨지는 문제가 있었거든요.온톨로지 보강은 LLM 판단 결과를 받은 뒤에 동작하니까, 기존 라우팅 로직에 영향을 주지 않습니다.
두 번째는 자동 적응입니다.
Neo4j에 새로운 서버나 서비스 노드가 추가되면, 별도 코드 수정 없이 해당 엔티티에 대한 질문도 보강 대상이 됩니다. 프롬프트에 예시를 추가하는 방식은 엔티티가 늘어날 때마다 수동으로 업데이트해야 했는데, 이건 그럴 필요가 없어요.
특성 | 프롬프트 방식 | 온톨로지 보강 |
|---|---|---|
엔티티 추가 시 | 프롬프트에 예시 수동 추가 | 자동 반영 |
부작용 | 한쪽 고치면 다른 쪽 깨짐 | LLM 라우팅과 독립적 |
비용 | 프롬프트 길어질수록 토큰 증가 | Neo4j 조회 1건 (~0.1초) |
유지보수 | 케이스마다 프롬프트 관리 | 코드 변경 불필요 |

마무리
LLM 라우팅이 자꾸 틀릴 때, 프롬프트를 정교하게 다듬는 게 첫 번째 반응이었습니다.
예시를 추가하고, 키워드 규칙을 넣고, 설명을 바꿔봤지만, 한쪽을 고치면 다른 쪽이 깨지는 패턴이 반복됐거든요.
결국 풀린 건 방향을 바꾸고 나서였습니다.
LLM한테 더 잘 가르치려 하지 않고, Neo4j에 이미 있는 온톨로지가 "이 질문은 그래프도 봐야 해"를 직접 알려주게 한 거예요. LLM 라우팅 코드는 한 줄도 안 고쳤고, Neo4j 조회 한 건이 추가된 것뿐입니다.
프롬프트를 고쳐야 할지 고민되는 상황이라면, 데이터 구조 쪽에서 보정할 수 있는지를 먼저 확인해보는 걸 추천합니다.