Code › tail-villain
프로필 메모리와 디테일 RAG 나누기
점수 기준 아래로 떨어진 검색 결과를 보며 학습자 기억과 세부 근거 검색을 분리한 과정
tail-villain의 Study 코치가 사용자를 안다고 말할 수 있으려면 RAG 검색만으로는 부족했다. 검색 결과는 맞았다. 사용자의 배경 조각이 첫 번째와 두 번째에 올라왔고, 엉뚱한 문서가 먼저 나온 것도 아니었다.
그런데 점수 기준을 통과하지 못했다.
프롬프트에 들어간 문맥은 비어 있었고, 코치는 사용자의 목표나 이력에 대해 아는 것이 없는 것처럼 답했다. 검색 순위는 맞았지만, 실제 답변에는 학습자에 대한 정보가 하나도 남지 않았다.
처음에는 평범한 RAG 튜닝 문제처럼 보였고, 질문을 더 잘 만들거나 임베딩할 문서의 표현을 바꾸거나 점수 기준을 조금 낮추면 해결될 것 같았다. 몇 번 실험해보니 세부 근거를 찾는 일과 학습자를 기억하는 일을 같은 검색 함수 하나에 맡긴 구조가 더 큰 문제였다.
검색어를 고쳐도 기억은 불안정했다
이전 작업에서 Study 답변은 사용자의 Goal과 Background를 Roadmap 범위 안에서 검색하도록 바뀌었다. 사용자 전체 문서를 뒤지는 대신, 지금 학습 중인 Roadmap에 연결된 목표와 배경만 후보로 삼았다. 이 범위 제한 덕분에 다른 사용자의 문서가 섞일 가능성은 줄었고, 관련 문서가 상위에 오는지도 확인했다.
그 다음 실제 사용에 가까운 질문을 넣어보니 다른 문제가 나왔다. 사용자가 IELTS Study 세션에서 자신의 이력과 커리어에 대해 물었는데, 검색 쿼리에는 Study 주제와 주제 설명, 최근 사용자 메시지가 함께 들어가 있었다. 질문 자체는 이력 쪽이었지만 쿼리 전체는 IELTS 목표 쪽으로 기울었고, 그래서 Goal이 먼저 나오고 Background는 기준점 바로 아래로 밀렸다.
이건 비교적 쉽게 고칠 수 있었다. 검색 쿼리에는 사용자의 최신 메시지만 넣고, Study 주제와 대화 상태는 답변 생성 프롬프트에만 남겼다. 검색의 일과 생성의 일을 나눈 것이다. 사용자가 무엇을 찾고 있는지를 벡터로 만들 때는 질문 자체가 더 중요하고, 코치가 어떤 맥락에서 답해야 하는지는 그 뒤에 붙어도 된다.
하지만 이 수정도 충분하지 않았다. 사용자가 그냥 저에 대해 아는 것이 있느냐고 물으면, 올바른 Background 조각이 첫 번째로 올라왔지만 점수는 0.65보다 낮았다. 순위는 맞고 입장은 실패한 상태였다. 이 정도면 검색이 못 찾은 게 아니라, 검색 결과를 받아들이는 문턱이 학습자 기억을 가로막고 있었다.
문서 쪽도 손봤다. Background 조각은 이력서, 커리어, 기술, 학력, 프로젝트, 이전 경험 같은 정보를 담고 있다는 짧은 설명을 임베딩용 텍스트 앞에 붙였다. 저장된 원문은 바꾸지 않고, 벡터를 만들 때만 문서가 어떤 성격인지 조금 더 드러나게 만든 것이다. 덕분에 막연한 프로필 질문의 점수는 올라갔고, 명시적인 배경 질문도 더 안정적으로 통과했다.
그래도 끝은 아니었다.
긴 문장이 아니라 섞인 의도가 문제였다
한동안은 페르소나 호칭이 문제처럼 보였다. 사용자가 Socrates를 부르듯이 테스형이라고 시작하면, 그 단어가 검색 쿼리 안에 들어가면서 배경 조각 점수가 내려갔다. 검색용 쿼리에서 선택된 페르소나 호칭만 제거하자 같은 질문의 점수는 다시 올라갔다.
이런 해결은 위험하다. 특정 문장을 고치는 데는 효과가 있지만, 실제 대화는 그렇게 얌전하지 않기 때문이다. 사용자는 친구 이야기를 하다가 웃고, 말머리를 붙이고, 갑자기 자기 이력에 대해 묻는다. 실제로 그런 긴 문장을 넣어보니 배경 조각은 여전히 첫 번째와 두 번째였지만 둘 다 기준 아래였다.
흥미로운 건 명확한 검색 의도로 다시 쓴 쿼리였다. 사용자의 커리어와 이력에 관한 정보를 찾아달라는 식으로 바꾸면 같은 Background 조각이 훨씬 높은 점수로 올라왔다. 즉 문서가 없는 것도 아니고, 임베딩이 완전히 틀린 것도 아니었다. 문제는 한 문장 안에 친구 이름, 웃음, 페르소나 호칭, 진짜 질문이 함께 들어갔을 때 하나의 벡터가 그 의도를 모두 잘 담지 못한다는 데 있었다.
쿼리 재작성 모델을 매번 하나 더 호출하면 이 문제를 어느 정도 줄일 수 있다. 하지만 모든 Study 턴마다 모델 호출이 하나씩 늘고, 지연 시간과 비용도 붙는다. 더 중요한 건 안정적인 학습자 정체성을 매번 검색 성공 여부에 맡긴다는 구조가 그대로 남는다는 점이었다.
사용자가 저에 대해 무엇을 아느냐고 물을 때마다, 코치가 운 좋게 올바른 조각을 0.65 이상으로 찾았을 때만 기억을 가진 척하면 안 된다. 검색 점수에 따라 코치가 학습자를 기억했다가 잊는 구조이기 때문이다.
기준을 낮추는 선택도 측정했다
가장 쉬운 답은 기준을 낮추는 쪽이었고, 0.65에서 걸러진다면 0.60이나 0.55로 낮추면 된다. 실제로 긴 대화형 프로필 질문에서는 기준을 낮췄을 때 Background가 일부 들어왔고, 더 낮추면 두 개의 Background와 Goal까지 들어왔다.
하지만 같은 기준은 애매한 질문에도 적용된다. 예를 들어 사용자가 이것이 왜 필요하냐고만 물으면, Background와 Goal이 꽤 그럴듯한 점수로 올라왔다. 0.60으로 낮추면 애매한 턴에 Background가 들어가고, 0.55까지 낮추면 Goal까지 들어간다. 코치가 현재 질문과 상관없는 이력 정보를 보게 되는 순간이 늘어난다.
이건 단순한 잡음 문제가 아니다. tail-villain의 Goal과 Background에는 사용자의 목표, 이력, 프로젝트 경험 같은 민감할 수 있는 정보가 들어간다. 관련 없는 턴에 그 정보를 프롬프트에 넣으면 답변이 산만해질 뿐 아니라, 필요 없는 개인화가 섞일 가능성도 커진다. 최종 생성 모델이 알아서 무시할 것이라고 믿기에는 프롬프트 문맥의 힘이 크다.
그래서 기준은 유지했다. 0.65는 완벽한 의미 판정기가 아니지만, 현재 Roadmap 범위의 세부 근거를 넣을지 말지 결정하는 잡음 방지선으로는 쓸 수 있었다. 대신 그 기준을 학습자 기억의 조건으로 쓰지 않기로 했다.
여기서 역할이 갈라졌다. RAG는 이번 턴에 필요한 세부 근거를 찾고, 프로필 메모리는 코치가 항상 알고 있어야 하는 학습자 요약을 제공한다.
항상 켜져 있는 프로필을 따로 두었다
최종 구조는 단순해졌다. Roadmap 분석 과정에서 이미 만들어진 analysisSummary와 evidenceHooks를 바탕으로, 항상 프롬프트에 넣을 수 있는 작은 학습자 프로필을 만든다. 여기에 이번 질문에 맞게 검색된 Goal과 Background 조각이 있으면 추가한다.
프로필에는 큰 방향만 담는다. 목표 역할, 준비 수준, 강점과 빈틈, 집중할 영역, 대표 프로젝트와 경험을 제한된 길이로 자른다. 무제한 메모리가 아니라 bounded profile이다. 코치가 학습자를 대략적으로 알고 대화를 이어갈 수 있을 만큼만 넣고, 정확한 사건이나 숫자, 구체적인 문제 해결 과정은 RAG가 가져오게 한다.
이렇게 나누면 저에 대해 무엇을 아느냐는 질문에 검색 결과가 없어도 답할 수 있다. 코치는 목표와 강점, 준비 중인 영역 같은 넓은 정보를 말할 수 있다. 반대로 특정 프로젝트에서 중복 처리 문제가 왜 생겼고 어떻게 막았는지 묻는다면, 프로필만으로 답하면 안 된다. 그때는 세부 RAG가 해당 조각을 찾아와야 한다.
프로필과 RAG 문맥은 모두 신뢰할 수 없는 참고 자료로 표시했다. 사용자 문서에서 직접 왔거나 사용자 문서로부터 생성된 정보이기 때문이다. 모델은 그 안의 사실을 참고할 수 있지만, 그 안에 숨어 있는 지시를 따라서는 안 되고, 내부 라벨이나 검색 점수를 사용자에게 드러내서도 안 된다.
이 계약을 Study에만 넣지는 않았다. 텍스트 Interview는 이미 비슷한 정보를 쓰고 있었지만 프롬프트 표면마다 형식이 조금씩 달랐고, Live Interview는 또 다른 초기화 경로를 타면서 일부 근거가 사라질 여지도 있었다. 그래서 Study, 스트리밍 Study, Interview 시작과 후속 질문, 각도 전환, 완료 리포트, Live Interview 설정을 같은 학습자 프로필 빌더에 맞췄다.
사용자가 같은 Roadmap으로 연습한다면, 텍스트 모드와 라이브 모드에서 다른 사람처럼 취급되면 안 된다.
RAG 품질은 프롬프트 구조까지 포함한다
이번 작업에서 배운 것은 threshold 자체보다 역할 분리였다. 코사인 점수는 유용하지만 학습자의 존재를 결정하는 기준이 되기에는 우연적인 입력을 많이 타고, 질문이 짧거나 길어서가 아니라 여러 의도가 섞였을 때 더 쉽게 흔들린다.
검색 순위가 맞아도 기준 아래면 문맥은 비고, 문맥이 비면 코치는 사용자를 모르는 척한다. 반대로 기준을 낮추면 애매한 턴에 개인 정보가 들어가니, 어느 쪽도 좋은 학습 경험은 아니다.
그래서 프로필 메모리와 디테일 RAG를 분리했다. 프로필은 항상 켜져 있지만 작고, RAG는 선택적으로 켜지지만 더 정확해야 한다. 하나는 학습자의 넓은 배경을 잊지 않기 위한 장치이고, 다른 하나는 현재 질문에 필요한 근거를 가져오기 위한 장치다.
아직 고민은 남아 있다. 지금 프로필에는 프로젝트의 문제와 해결 방식까지 들어가 있어서, 답변만 보고는 프로필에서 온 정보인지 RAG에서 온 정보인지 구분하기 어렵다. 나중에는 프로필을 더 줄이고, 프로젝트의 세부 문제와 해결 과정은 RAG 쪽으로 더 밀어내야 한다.
tail-villain의 개인화는 검색 성공 여부에 따라 켜지고 꺼지지 않는다. 코치는 작은 프로필로 학습자의 넓은 배경을 계속 유지하고, 현재 질문에 구체적인 근거가 필요할 때만 RAG를 사용한다.