검색(Retrieval)이 없다면 여전히 RAG인가?
요약
글쓴이는 자신의 방대한 경력과 지식을 활용하여, 임베딩이나 벡터 스토어 없이도 질의응답이 가능한 개인 포트폴리오 챗봇을 개발했습니다. 이 시스템은 오직 큐레이션된 Markdown 파일에 명시된 사실만을 기반으로 답변하며, 허위 정보 생성(Hallucination)을 엄격히 방지하는 것이 핵심입니다.
핵심 포인트
- RAG의 필수 요소인 임베딩/벡터 스토어 없이도 지식 검색이 가능함을 입증함.
- 데이터 유출 위험 최소화를 위해 NDA 및 기밀 정보를 철저히 익명화하고 제한함.
- 챗봇은 오직 원본 데이터에 명시된 사실만을 답변하며, 추측이나 판단을 하지 않도록 설계됨.
- 개인 경력 관리를 위한 새로운 접근 방식으로, '지식 기반' 자체를 제품화하는 시도임.
저는 28년 동안 소프트웨어를 출시해 왔으며, 그중 21년은 100개가 넘는 다양한 프로젝트에서 컨설턴트로 일했습니다. 어느 시점부터는 이력서가 기록이 아니라 부담스러운 편집 문제가 됩니다. 두 페이지로는 제가 그들과 대화할 기회조차 갖기 전에 누군가가 읽을 수 있도록 손으로 고른 반 다발의 경력만 겨우 담을 수 있습니다. 나머지 모든 것은 누군가, 어딘가에서 알고 싶어 할 실제 경험입니다.
특정 채용 관리자가 원하는 단 하나의 프로젝트가 빠졌다는 사실이 저를 괴롭혔습니다. 소매 기술팀에게 중요했을 판매 시점(point-of-sale) 통합 스토리가 직무 설명에 더 잘 맞는 무언가로 밀려났을 수도 있습니다.
그래서 저는 (휴, 또 다른) 챗봇을 만들었습니다. 제가 구직 시장에 나서는 동안, 마지막 제품을 출시한 이후부터 머릿속에 있던 아이디어가 다시 떠올랐습니다. 저의 경력 이력, 기술, 취미, 관심사를 누구나 질의할 수 있는 단일 인터페이스 뒤에 두고, 데모가 아닌 실제 제품으로 출시하는 것입니다. 두 마리 토끼를 한 번에 잡는 거죠.
결과적으로 저는 하나의 큐레이션된 Markdown 파일이 전체 지식 기반인 작은 Go 및 HTMX 앱을 갖게 되었습니다: 임베딩(embeddings)도, 벡터 스토어(vector store)도, 에이전트 루프(agent loop)도 전혀 없습니다. 이것은 라이브이며, 링크를 원하시면 제 LinkedIn 프로필이나 이력서 사본을 방문하셔야 합니다. 이게 과연 RAG로 간주되는지 여부는 공정한 질문이며, 저는 이 글에서 답하고자 합니다. 이어지는 내용은 제가 어떻게 이를 제한했고, 무엇을 만들었으며, 가장 재미있었던 부분입니다.
디자인 브리프
구현과 기술 선택에 도움을 주기 위해 코드를 작성하기 전에 제약 조건을 설정했습니다.
내가 말할 수 없는 것들. 21년간의 컨설팅 경력은 비공개 계약(NDA), 기밀 유지 협약, 그리고 모든 고객 이름이 기록에 남아있다는 것을 의미합니다. 원본 데이터는 지식이 되기 전에 유출 위험으로 취급되어야 했습니다. 고객 이름은 실제 이름 대신 "국내 대형 식료품 체인점"과 같은 익명화된 설명어로 대체되었고, 연락처 세부 정보와 상업적 조건은 제거했으며, NDA가 적용되는 모든 내용은 아예 제외되었습니다. 이후 여러 차례의 검토 과정(review passes)을 거쳤습니다: 에이전트 기반 검토(agentic review), 제가 기억하는 모든 고객 이름에 대한 기계적인 키워드 검색(mechanical keyword sweep), 전체 파일에 대한 수동 읽기, 그리고 재구성이 이루어졌습니다. 이 모든 지식의 경계는 하나의 큐레이션된 Markdown 파일로 구성됩니다. 파일에 없는 내용은 봇이 말할 수 없습니다.
나 자신을 제외한 나. 주소, 전화번호, 이메일은 없습니다. 번들 안에 포함된 개인 정보는 제가 의도적으로 넣은 것들뿐입니다: 거주지, 근무 가능 여부, 그리고 문화 적합성 질문에 사용될 취미 및 관심사입니다. LinkedIn이 유일한 연락 경로이며, 그 이상은 없습니다. 저의 개인 정보는 이미 충분히 많은 수상한 데이터 브로커들의 손에 넘어갔습니다.
절대 지어내지 않기. 지식의 경계가 봇이 답변할 수 있는 내용을 결정하며, 이 규칙은 답변하는 방식을 규정합니다: 오직 원본 데이터에 명시적으로 나타난 사실과 측정 지표만을 사용해야 합니다. 참여 횟수를 임의로 올리거나, 인접한 기술에서 능력을 추론하거나, 도움이 되는 추측을 해서는 안 됩니다. 질문이 번들 범위를 벗어날 경우, 봇은 답변을 정중하게 거절합니다. 저의 경험에 대해 허위 정보를 만들어내는 챗봇은 제가 처음 가지고 있던 이력서 문제보다 더 나쁩니다.
주관적인 의견 금지. 봇은 단순한 사실 검색기 이상의 역할을 합니다. "Jason이 이 역할에 적합한가요?"라고 질문받으면, 번들이 제공하는 사실만을 답변하고 면접에서 언급하면 좋을 만한 내용을 제안할 뿐입니다. 저의 강점에 대해 편집하거나 판결을 내리지 않습니다. 적합성을 판단하는 것은 채용팀의 임무이며, 봇의 임무는 그들이 완전한 정보를 바탕으로 판단하도록 보장하는 것입니다.
이는 에이전트가 아닙니다
핵심 설계 결정은 다음으로 내려졌습니다. 이 봇은 도구도, ReAct 루프도, 어떤 추론 주기(reasoning cycle)도 없습니다. 그저 하나의 질의 도구일 뿐입니다. 질문을 하면, 번들에서 답을 찾아서 간단하게 제공합니다.
여기서는 '에이전트(agent)'라는 개념 자체가 의미가 없었기 때문에, 모든 것이 사전 처리(pre-processing)와 번들 빌드 시간으로 이동했습니다. 정보를 찾아보라고요? 전체 지식 기반이 프롬프트 안에 들어 있습니다. 수년간의 .NET 경험을 계산하라고요? 모든 집계값은 미리 계산되었습니다. 행동을 취하라고요? 할 것이 아무것도 없습니다. 이 제품은 대화 그 자체입니다.
에이전트 루프를 제거함으로써 과정이 간소화되었습니다. 작성해야 할 도구 호출 코드도 없고, 유지 관리할 스키마도 없으며, 모델이 연속으로 세 번이나 잘못된 것을 호출하기로 결정했을 때 디버깅할 루프도 없습니다. 요청은 미니 세션(mini-sessions)이고 응답은 빠르며, 실패는 지루합니다. 또한 보안 표면적(security surface)을 줄여줍니다. 도구를 호출하고 행동을 취할 수 있는 에이전트는 공격자에게 목표물을 제공하지만, 이 봇은 말만 할 수 있습니다.
'RAG'를 사용할 것인가, 사용하지 않을 것인가
검색 증강 생성(Retrieval-augmented generation, RAG)은 모델이 사용자 데이터로 답변해야 할 때의 표준적인 해답입니다. 문서를 청크(chunk)로 나누고, 임베딩(embed)하고, 벡터 스토어에 넣은 다음, 각 질문에 대해 상위 일치 항목을 가져오는 방식이죠. 제 경력 전체 지식 기반이 현대적인 컨텍스트 창(context window)에 여유 공간을 두고 모두 들어갈 수 있었기 때문에, 런타임 시에는 검색할 것이 아무것도 없었습니다. 모든 청크는 항상 '검색된' 상태입니다.
약어 자체를 포기하는 대신, 저는 그 세 단계를 재배치했습니다.
**검색(Retrieval)**은 사용자가 나타나기 훨씬 전, 제가 이 봇을 시작하기도 전에 오프라인으로 이루어졌습니다. 이는 마이닝 및 정제 과정이었습니다. 고객 파일과 노트에서 기록을 추출하고, 큐레이션 게이트(curation gate)를 적용하며, 그 결과를 단일 Markdown 파일로 준비하는 것이었죠. 봇이 알아야 할 검색 질문은 한 번에 답변되었고, 수동으로 검토되고 편집되었습니다.
**증강(Augmentation)**은 모든 채팅 세션 시작 시, 대화의 일부로서 발생합니다. 전체 번들이 컨텍스트로 들어갑니다. 질문당 가져오는 것이 없기 때문에, 이미 모든 것이 그곳에 있기 때문입니다.
생성(Generation) 단계가 '검색(Retrieval)'이라는 단어에 가장 충실한 부분입니다. 모델은 질문을 읽고, 어떤 참여 경험(engagements), 기술(skills), 지표(metrics)이 해당 질의에 답하는지 추론하며 전체 코퍼스에서 답변을 구성합니다. 이 추론 과정 자체가 핵심이며, 이는 제 이력서에 대한 키워드 검색으로는 할 수 없는 부분입니다.**
이러한 트레이드오프는 고품질 결과를 만들어냅니다. 청킹(chunking)이 없다는 것은 검색 누락이 없다는 의미입니다. 모든 것이 존재하기 때문에 관련성 있는 내용을 놓칠 일이 없습니다. 유지해야 할 임베딩 파이프라인도 없고, 인덱싱된 내용과 작성된 내용 사이에 드리프트(drift)가 발생하지 않습니다. 단점은 매 세션마다 컨텍스트 창(context window)을 사용한다는 것인데, 이는 나중에 프롬프트 캐싱(prompt caching)으로 통제할 수 있습니다.
**코퍼스가 창 크기를 초과하는 순간 이 방식은 더 이상 실현 가능하지 않습니다. 그때 청킹, 임베딩, 벡터 검색이 제 역할을 하게 되며, 저는 진정한 RAG를 수행하게 될 것입니다.
환각(Confabulation) 감소하기
설계 명세서에서 '절대 발명 금지(never-invent)' 규칙을 지키려면 기계적인 도움이 필요했습니다. 왜냐하면 경력 사실들로 가득 찬 자료를 받은 모델이라도 질문을 받으면 여전히 유용하게 숫자를 꾸며내기 때문입니다. 이 문제를 해결하는 데 두 가지 결정이 포함됩니다.
모델 선택이 중요했습니다. 저는 화려한 추론(reasoning) 모델 대신, 최신 명령어 수행 모델인 Gemini 3.6 Flash를 선택했습니다. 이 작업의 목표는 제공된 컨텍스트에서 충실하게 정보를 추출하는 것이며, 명령어 수행 모델이 스크립트를 지키는 경향이 있기 때문입니다. 비용상의 이유로 약간 더 오래된 3.6 모델을 사용했고, 추적 상태를 유지하기 위해 미니 평가 하네스(mini-evaluation harness)를 추가했습니다.
수학 계산은 모델에서 완전히 제거되었습니다. 기술별 연차, 산업별 참여 건수, 채용 담당자에게 유용한 종합 테이블 등 모든 것은 구조화된 날짜로부터 사전에 계산되었고 (LLM이 아닌 기계적으로), 봇이 그대로 인용하는 부록 형태로 번들에 작성되었습니다. 중복되는 컨설팅 참여 경험은 단순한 날짜 산술을 그럴듯하게 잘못되게 만들 수 있으며, 자신감 있게 틀린
모든 경력 데이터가 세션마다 함께 전송되면서, 대화가 길어질수록 입력 토큰이 제품의 비용이 되었습니다. 프롬프트 캐싱(Prompt caching)이 이를 해결합니다. 제공업체들은 사용자가 이전에 본 토큰에 대해서는 일반 입력 가격의 일부만 지불할 수 있도록 허용하는데, 단 전송되는 프롬프트가 매번 정확히 동일한 바이트로 시작해야 합니다.
시스템 프롬프트와 전체 경력 번들(career bundle) 같은 안정적인 부분들이 먼저 전송되고, 그 뒤에 대화 내용이 이어집니다. 요청마다 들어오는 어떤 것도—타임스탬프나 세션 ID 같은 것, 심지어 도움이 되는 "오늘은"과 같은 문구라도—캐시를 무효화하고 모든 채팅을 다시 전체 가격의 채팅으로 만듭니다. 저는 접두사(prefix)가 바이트 단위로 동일하게 유지되도록 하고 세션을 관리하여 각 대화가 일관된 캐시 키에 매핑되도록 노력했습니다.
이후 테스트 하네스(test harness)와 OpenRouter 로그를 직접 확인하며, 여러 세션에서 캐싱이 일관되고 지원되는지 조정했습니다. 번들이 변경되면 캐시는 무효화됩니다(cache busts). 괜찮습니다. 번들은 자주 바뀌지 않으니까요.
빌드: 다시 잔인할 정도로 효율적으로
저의 지난 사이드 프로젝트인 DumbQuestion.ai는 제가 유지해 온 빌드 원칙, 즉 '잔인할 정도의 효율성(brutally efficient)'을 가르쳐 주었습니다. 그래서 이 봇은 UI에 HTMX를 사용한 또 다른 Go 바이너리입니다. JavaScript 프레임워크도 없고, 클라이언트 측 상태 관리도 없으며, Go 자체의 빌드 파이프라인 외에는 없습니다. 작은 컨테이너 하나, 프로세스 하나로 GitHub 플래이버 마크다운(GitHub-flavored Markdown)을 채팅창에 바로 렌더링합니다(번들에서 가져온 집계 테이블은 표로 렌더링되어야 합니다).
제가 무(無)에서 시작한 것은 아닙니다. Turnstile 남용 게이트, 프롬프트 가드 라이브러리(prompt-guard library), 임베디드 에셋 파이프라인 등 이 모든 것이 DumbQuestion 코드베이스에서 가져온 것입니다. 번들 자체는 컨테이너 빌드에 직접 포함됩니다. 원래 설계에서는 부팅 시 R2 버킷에서 데이터를 가져오고 재확인 루프를 거쳤지만, 데이터가 자주 변하지 않기 때문에 그 버킷은 더 이상 필요 없는 움직이는 부분이 되었습니다. 이제 경력 데이터를 업데이트하는 것은 단순히 재빌드하고 재배포하는 것만으로 충분하며, 전체 시스템이 하나의 아티팩트(artifact)로 배포됩니다.
OpenRouter는 모델 트래픽을 처리하며, 이는 이전 섹션의 캐싱 작업에 대한 단일 API 인터페이스를 제공합니다. 이를 통해 Google model 트래픽을 제 GCP 계정으로 라우팅하여 Google AI Pro 구독의 월별 크레딧 혜택을 누릴 수 있었습니다. 배포는 간단합니다: GitHub Actions가 컨테이너를 빌드하고 Cloud Run에 푸시하여 규모 축소(scale-to-zero)와 매우 빠른 콜드 스타트 시간을 확보합니다.
남용 방지 (Abuse protection)
사용량 측정 방식의 LLM API를 백엔드로 하는 공개 챗봇은 낯선 사람들의 호기심에 대해 비용을 청구하며, 만약 낯선 사람이 나에 대해 조금이라도 알고 싶어 한다면 괜찮습니다. 하지만 남용 방지는 선택 사항이 아니었습니다. 모든 것이 세션마다 걸려 있기 때문에, 열성적인 스크래퍼는 '저렴한' 것을 하룻밤 사이에 예상치 못한 청구서로 바꿀 수 있었습니다. 저는 적어도 모델 사용량에 대한 월별 예산을 설정하는 것을 잊지 않았습니다.
외부 계층은 Cloudflare입니다: 채팅 앞에 속도 제한(rate limits), 봇 보호, 그리고 Turnstile을 배치했습니다. 저렴한 거절은 에지에서 발생하며, 그 비용은 제게 아무것도 들지 않습니다. 또한, 제가 경력에 대해 질문할 수 있도록 허용하기 전에 사용자가 인간임을 증명하도록 하는 것은 거의 모든 캐주얼하게 자동화된 시도를 걸러냅니다.
내부 계층은 prompt-guard입니다. 이는 DumbQuestion 코드베이스에서 가져온 주입 탐지(injection detection) 기능입니다. 이 기능은 지침을 무효화하거나, 시스템 프롬프트를 노출시키거나, 또는 봇을 우회하려는 시도를 분류합니다. 봇이 이를 감지했을 때 어떻게 반응하는지가 재미있는 부분이며, 다음 내용에서 다룹니다.
개성: 재미있는 부분 (Personality: the fun part)
설계 명세서는 의견을 말하지 않도록 했습니다. 캐릭터에 대해서는 아무것도 언급하지 않았고, 챗봇이 탈옥 시도(jailbreak attempt)에 응답하는 방식은 제품의 표면입니다. 따라서 prompt-guard가 이를 플래그 지정하면, 봇은 그 캐릭터에 맞춰 답변합니다. 저는 원래 DumbQuestion의 독특한 개성을 가져왔지만, 지나치게 공격적인 풍자는 대화형 경력 챗봇에는 적합하지 않았습니다.
프롬프트 주입(prompt inject)을 시도해 보면 발표 내용에 대한 도박성 발언이 나옵니다. 이는 보안 우선의 사고방식이며, 본문 초반부에서 언급된 큐레이션 게이트와 에이전트 배제 결정의 근거와 같습니다. 봇에게 지침 무시를 설득하려 하면, 그것이 멋진 인터뷰 질문이 될 것이라고 제안합니다. 제 나이를 물어보면, 제가 소프트웨어를 출시한 기간에 대해 기쁘게 대답합니다.
진정한 방어는 데이터 격리(data isolation)입니다. 민감한 정보가 없는 단일 큐레이션 파일과 그 박스에서 벗어나지 못하게 하는 도구의 부재가 핵심입니다. 이 '개성' 자체가 거절에 대한 사용자 경험(UX)인 셈이죠.
어떤 것도 기록되거나 추적되지 않으므로, 외부에서의 전쟁 이야기는 없습니다. 위 응답들은 설계된 행동이며, 데모 그 자체가 봇입니다.
제가 처음에 언급했던 이력서 문제는 이력서 모양의 해결책을 가지고 있지 않았습니다. 28년 경력과 100건 이상의 프로젝트 경험이 두 페이지로 압축될 수는 없으며, 그렇게 할 필요도 없습니다. 어떤 채용 관리자도 12페이지짜리 CV를 원하지 않습니다. 봇은 이 모든 것을 담고 있으며, 자신이 아는 것에 대해서는 정직하게 답하고, 모르는 것은 거절하며, 운영 비용은 거의 들지 않습니다. 그 과정에서 저는 또 다른 AI 제품을 출시했는데, 이것이 바로 핵심 포인트의 나머지 절반이었습니다.
만약 귀하의 역사가 형식에 맞지 않는다면, 해결책은 간단합니다. 데이터를 먼저 큐레이션하고, 전체 내용을 컨텍스트(context)에 넣고, 실제로 필요할 때까지 벡터 스토어(vector store)는 건너뛰세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기