이력서 RAG 채팅을 만들며 정리한 구조와 고민
이번 작업의 목표는 단순히 "이력서 페이지에 AI 채팅을 붙인다"가 아니었습니다.내 이력과 프로젝트 데이터를 한곳에 모아두고, 방문자가 자연어로 질문하면 그 데이터 안에서 근거를 찾아 답변하는 페이지를 만드는 것이 목표였습니다. 처음에는 당연히 벡터 DB를 중심에 두고 생각했습니다. RAG라고 하면 보통 문서를 chunking하고, embedding을 만들고, vector similarity로 검색한 뒤 LLM에 넘기는 그림을 먼저 떠올리기 때문입니다.그런데 실제 작업을 진행하면서 중요한 질문이 생겼습니다.지금 데이터는 정말 벡터 검색이 먼저 필요한 데이터인가?이번 글은 그 질문에서 출발해 구조를 다시 잡고, 기능을 운영 환경까지 붙이면서 했던 고민을 정리한 기록입니다.
1. 처음 목표: 이력 데이터를 런타임의 진실로 만들기
가장 먼저 정리해야 했던 것은 데이터의 위치였습니다.기존 이력 데이터는 MDX, Markdown, JSON에 가까운 형태로 흩어져 있었습니다. 사람이 읽기에는 좋지만, 채팅 API가 요청을 받을 때마다 파일을 직접 읽는 구조는 목표와 맞지 않았습니다. 사용자가 원한 구조는 명확했습니다.- 이력 정보는 DB에 모은다.
- 질문 응답은 DB에 들어간 데이터에서 근거를 찾는다.
- AI 모델은 자연어 답변 생성 역할만 맡긴다.
- 모델은 아직 확정하지 않았으므로 교체 가능해야 한다.
이력 원본 파일
-> import pipeline
-> resume_source_items
-> chat API runtime retrieval
-> answer generation
-> web chat UI2. 벡터 DB를 바로 쓰지 않은 이유
처음 설계에는resume_vector_chunks와 embedding 설정도 포함했습니다. 하지만 운영 채팅을 붙이는 단계에서는 벡터 검색을 바로 중심에 두지 않았습니다.이유는 데이터 성격 때문입니다.이력서는 보통 다음과 같은 텍스트입니다.- 회사명
- 프로젝트명
- 기술 스택
- 도메인 키워드
- 성과와 역할
- 업무 맥락
Oprimed에서 어떤 일을 했어?
프론트엔드 강점은 뭐야?
CI/CD 경험 있어?이런 질문은 embedding이 없어도 회사명, 기술명, 경험/성과/강점 같은 키워드만으로 꽤 높은 품질의 후보를 뽑을 수 있습니다. 반대로 처음부터 벡터 DB를 필수로 두면 다음 부담이 생깁니다.
- embedding provider와 모델을 지금 결정해야 한다.
- 운영 환경에 API key와 dimensions 같은 설정이 추가된다.
- vector index 재생성 절차가 필요하다.
- 검색 품질이 나빠도 원인이 chunking인지 embedding인지 retriever인지 분리하기 어렵다.
현재 운영 채팅:
DB text / keyword retrieval
미래 확장:
optional vector index pipeline3. 자연어 처리는 모델이 아니라 경계로 먼저 정의했다
처음에는 "OpenAI embedding을 어떻게 붙였냐"는 질문이 나왔지만, 실제 목표는 특정 모델을 붙이는 것이 아니었습니다. 모델은 바뀔 수 있고, 지금 당장 확정하지 않아도 되는 영역이었습니다.그래서 구조를 모델 중심이 아니라 provider boundary 중심으로 잡았습니다.ResumeRagService
-> ResumeRagRetrieverService
-> ChatProvider
-> CodexAppServerProvider
-> future OpenAI-compatible provider- 검색 결과가 틀렸는가?
- 검색 결과는 맞는데 답변 생성이 과장됐는가?
- 모델 설정이 없는가?
- Codex app-server가 죽었는가?
4. 공개 채팅에서 로그인은 강제하지 않는다
이 기능은 "나를 궁금해하는 사람이 들어와서 질문하는 페이지"입니다. 그래서 로그인 강제는 사용자 흐름과 맞지 않았습니다.처음에는 인증을 붙이는 선택지도 있었지만, 곧 방향을 바꿨습니다.로그인 강제: 하지 않음
운영 웹 origin 제한: 함
CSP connect-src 정리: 함
실패 UI: 명확히 보여줌5. AI를 부르기 전에 질문 범위를 먼저 본다
운영에 붙인 뒤 또 하나의 고민이 생겼습니다.이 채팅은 이력서 질문을 위한 기능입니다. 그런데 사용자는 얼마든지 날씨, 코인 가격, 일반 지식 같은 질문을 보낼 수 있습니다. 이걸 모두 AI까지 보내는 것은 비용과 품질 양쪽에서 좋지 않았습니다.그래서 키워드 기반 scope gate를 먼저 두었습니다.question
-> keyword scope gate
-> in scope: retrieval + answer generation
-> out of scope: fixed message이 질문은 제 이력 범위를 벗어난 것 같아요. 프로젝트, 기술 경험, 업무 성과, 강점처럼 이력과 관련된 내용으로 다시 물어봐 주세요.작은 문구 변경이지만, 방향은 중요했습니다.
- 내부 구현: 키워드가 없다
- 사용자 관점: 이력 범위를 벗어났다
6. 실패 UI는 나중에 붙이는 장식이 아니었다
AI 기능은 실패 가능성이 많습니다.- 운영 origin이 아니어서 막힘
- Codex app-server 설정이 없음
- rate limit
- API 응답 contract 불일치
- 네트워크 실패
- 검색 근거 부족
- 질문 범위 벗어남
7. 최종 구조
현재 구조를 단순화하면 다음과 같습니다.Web
/resume/question
- public chat UI
- suggested questions
- failure states
- source citations
API
POST /resume-rag/chat
- public origin guard
- keyword scope gate
- DB text retriever
- Codex app-server chat provider
- grounded answer response
Data
resume_source_items
- runtime retrieval source
- imported from raw resume/career docs
Optional Future
resume_vector_chunks
- embedding/vector search path
- not required for current production chat- Web은 DB를 모른다.
- API는 파일을 직접 읽지 않는다.
- Retriever는 답변을 생성하지 않는다.
- Chat provider는 검색하지 않는다.
- Codex는 근거 밖의 답변을 만들지 않아야 한다.
- Vector index는 현재 운영 필수가 아니라 확장 옵션이다.
8. 배포하면서 확인한 것들
운영에 반영하면서는 로컬 테스트만으로 끝내지 않았습니다.- API unit test
- web typecheck / lint / build
- smoke E2E
- GitHub self-hosted runner 기반 API 배포
- 운영 API 직접 호출
- 운영 웹에서 실제 질문 전송
- 실패 UI 캡처 확인
9. 남은 고민
아직 끝난 구조는 아닙니다. 오히려 이제 운영 가능한 첫 형태에 가깝습니다.남은 고민은 몇 가지가 있습니다.out-of-scope와 no-evidence 분리
현재는grounded=false라는 값이 여러 의미를 가질 수 있습니다. 검색 근거가 부족한 경우와 질문 범위가 벗어난 경우는 UI에서 다르게 보여주는 것이 맞습니다.다음에는 API 응답에 reason 같은 필드를 추가해 상태를 더 명확히 나눌 수 있습니다.벡터 검색 재도입 시점
지금은 텍스트/키워드 검색으로 충분하지만, 질문이 더 추상화되면 벡터 검색이 필요해질 수 있습니다.예를 들면 이런 질문입니다.복잡한 도메인을 제품화한 경험이 있어?이 질문은 특정 회사명이나 기술명보다 문맥 유사도가 중요합니다. 이런 유형의 질문이 많아지면
resume_vector_chunks를 다시 활성화할 이유가 생깁니다.모델 교체 가능성
현재는 Codex app-server를 사용하지만, 구조상 다른 OpenAI-compatible provider로 바꿀 여지를 남겨두었습니다. 중요한 것은 어떤 모델을 쓰느냐보다 모델 교체가 전체 구조를 흔들지 않게 하는 것입니다.마치며
이번 작업에서 가장 크게 느낀 것은 RAG라는 이름에 끌려 구조를 과하게 만들 필요는 없다는 점입니다.벡터 DB, embedding, LLM은 모두 강력한 도구입니다. 하지만 먼저 정해야 하는 것은 도구가 아니라 경계입니다.- 어떤 데이터가 진실인가?
- 검색은 어디서 책임지는가?
- 자연어 생성은 어디까지 허용하는가?
- 사용자는 실패를 어떻게 이해하는가?
- 운영 환경에서는 무엇이 실제로 검증되어야 하는가?