[2026 하계 모각코] - Team 하모 4회차

농담곰·2026년 7월 17일

2026.07.10 19:00 - 22:00
Discord 영상통화로 진행

Week 04 — RAG로 Nana의 기억을 검색하기 (출처별 검색 tool 분리)

들어가며

3주차까지 Nana는 일정을 SQLite에 저장하고 다시 불러올 수 있게 됐다. 4주차의 목표는 RAG(검색 증강 생성) 를 붙여서, 저장된 것들을 "의미로 검색"하는 것이다.

이번 주 과제의 핵심 메시지는 딱 한 문장이었다.

"RAG를 하나의 마법 함수로 보지 말고, 데이터 출처별로 검색 tool을 분리하라."

즉 "검색해줘" 하는 만능 함수 하나를 만드는 게 아니라, 데이터가 어디 저장돼 있느냐에 따라 검색 도구를 나누는 설계 감각을 배우는 주차였다.


잠깐, RAG가 뭐였더라

RAG(Retrieval-Augmented Generation)는 "검색해서 가져온 내용을 붙여서 생성"이라는 뜻이다.

사용자: "내가 오전 집중력에 대해 뭐라고 했었지?"
   ↓
1. ChromaDB에서 관련 문서를 검색 (Retrieval)
2. 찾은 문서를 prompt에 붙여서 LLM한테 전달 (Augmented)
3. LLM이 그 문서를 근거로 답변 생성 (Generation)

LLM은 원래 내가 개인적으로 저장한 내용을 모른다. 그래서 검색으로 근거를 찾아 붙여주는 것이다. 1주차 때 설치만 해뒀던 chromadb가 드디어 여기서 실제로 쓰였다.

여기서 검색을 담당하는 부품을 Retriever(리트리버) 라고 부른다. 이번 주에 만든 search_personal_references, search_saved_requests가 바로 retriever 역할이다. RAG = Retriever(검색) + Generator(LLM 생성)라고 보면 된다.


핵심: 세 개의 출처, 세 개의 검색

Nana가 기억하는 데이터는 저장된 곳이 다 다르다.

출처저장소검색 tool반환 키
내가 적어둔 메모/선호ChromaDB (벡터DB)search_personal_referenceshits
저장된 일정/할일/알림SQLitesearch_saved_requestsrows
일반 채팅 발화 (심화)ChromaDB (대화 sync)search_conversation_messages-

여기서 배운 게 "출처 분리" 다. 저장소가 다르면:

저장소가 다르면 (ChromaDB vs SQLite)
    ↓ 헬퍼 함수도 다르고
    ↓ tool도 다르고
    ↓ 반환 키까지 다르게 (hits vs rows)
    ↓ prompt에서도 "이 상황엔 이 도구"라고 구분

이 다섯 겹의 구분이 전부 "RAG를 하나로 뭉치지 말라"는 하나의 철학이었다.


구현한 것

얇은 tool + 헬퍼 위임 구조

tool 본문에 실제 로직을 직접 넣지 않고, 저장소(store) 호출과 결과 정리는 헬퍼 함수에 맡긴다.

def search_personal_reference_hits(reference_store, *, query, top_k=2):
    raw_hits = reference_store.search_personal_references(query=query, limit=top_k)
    return [
        {
            "id": hit.get("id"),
            "content": hit.get("content"),
            "distance": hit.get("distance"),
            "metadata": {
                "title": hit.get("title", ""),
                "tags": _to_tag_list(hit.get("tags")),
            },
        }
        for hit in raw_hits
    ]

@tool(args_schema=SearchPersonalReferencesInput)
def search_personal_references(query, top_k=2) -> str:
    safe_top_k = safe_limit(top_k, default=2, maximum=20)
    hits = search_personal_reference_hits(REFERENCE_STORE, query=query, top_k=safe_top_k)
    return json_payload(tool_result("search_personal_references", hits=hits))

헬퍼는 store가 준 원본 결과를 id/content/distance/metadata라는 표준 hit 구조로 재조립한다. 이렇게 통일해야 LLM이 매번 다른 형태를 해석하는 부담 없이 결과를 읽을 수 있다.

벡터 검색의 실제 동작

실제로 테스트하면서 벡터 검색이 "단어 일치"가 아니라 "의미 기반"이라는 걸 체감했다.

검색어: "집중"
결과:  "나는 오전에 집중이 잘 돼"  (distance: 1.29)

검색어: "메모"
결과:  "오전에 집중 잘 된다고 메모해둠"
      "팀 싱크는 60분 이하로..."   ← 전혀 다른 단어인데도 관련성 있다고 찾아옴

distance(유사도 거리)가 함께 나오는데, 값이 작을수록 더 관련성이 높다.

안전한 입력 처리

검색 개수(top_k)를 항상 안전한 범위로 강제했다.

def safe_limit(limit, default=5, maximum=50):
    try:
        value = int(limit)
    except (TypeError, ValueError):
        value = default
    return max(1, min(value, maximum))

음수, 과도하게 큰 값, 숫자가 아닌 값이 들어와도 앱이 죽지 않게 하는 방어 코드다. 3주차의 삭제 안전장치("조건 없으면 거부")와 같은 맥락이다.


막혔던 지점 — NameError

멘토 리뷰를 반영해 반환을 tool_result(...)로 통일했더니 앱 실행 시 이 에러가 났다.

NameError: name 'tool_result' is not defined

tool_result는 3주차 파일에 정의된 헬퍼인데, 4주차 파일에서는 week03_prompt_parts, week03_tools만 import하고 tool_result는 안 가져오고 있었다.

# 기존
from student_parts.week03_build_nanas_logbook import week03_prompt_parts, week03_tools

# 수정
from student_parts.week03_build_nanas_logbook import (
    week03_prompt_parts,
    week03_tools,
    tool_result,   # ← 추가
)

함수를 다른 파일에서 쓰려면 정의만 있는 게 아니라 import까지 돼 있어야 한다는 걸 다시 확인했다.


멘토 리뷰로 다듬은 것들

이번 주는 코드를 만든 뒤 멘토 리뷰로 여러 부분을 개선했다.

1. tags를 항상 리스트로 정규화 — store가 tags를 "team,meeting" 같은 콤마 문자열로 반환하는데, "목록 성격의 값은 없을 때도 빈 리스트로 두자"는 원칙에 따라 문자열을 리스트로 변환했다. (3주차에서 membersNone 대신 []로 처리한 것과 같은 맥락)

2. 리스트 컴프리헨션 — 단순 변환만 하는 경우 hits = [] 만들고 append하는 대신 컴프리헨션으로 바로 반환하도록 간결화했다.

3. tool_result로 반환 통일 — 4주차 tool 3개를 전부 tool_result로 감싸서, 3주차 tool들과 같은 포맷(ok, tool_name이 항상 들어가는)으로 맞췄다.

4. 프롬프트 개선 — 세 가지를 반영했다.

  • 주차 명시: prompt가 주차별로 누적되니 [Week 4] 태그를 붙이고 "이전 주차보다 우선"이라고 명시
  • 자동 저장 강제: "메모해둬" 할 때 말로만 답하지 말고 반드시 add_personal_reference를 호출하도록
  • 검색 결과 인용: 검색 결과의 실제 내용을 답변에 인용하고, 기억이나 추측으로 답하지 않도록

보너스로 알게 된 것 — CLAUDE.md

이번 주에 강사님이 Claude Code의 /doctor(환경 진단)와 CLAUDE.md를 써보라고 했다.

CLAUDE.md는 Claude Code한테 주는 프로젝트 설명서다. 프로젝트 폴더에 이 파일을 만들어두면, Claude Code가 실행될 때마다 자동으로 읽어서 프로젝트 규칙을 이해한다. 그래서 매번 설명하지 않아도 된다.

# CLAUDE.md 예시
## 작업 규칙
- fixed/ 폴더는 수정 금지
- student_parts/ 안의 본인 주차 파일만 구현
- 반환은 tool_result 헬퍼로 통일
- 목록 성격의 값은 없을 때 빈 리스트([])로 처리

지금까지 이 프로젝트에서 계속 반복했던 규칙들(fixed 수정 금지, tool_result 통일, 빈 리스트 처리 등)을 CLAUDE.md에 적어두면, Claude Code가 처음부터 알고 작업해서 나중에 지적받을 일이 줄어든다. /init 명령으로 초안을 자동 생성할 수도 있다.


회고

이번 주 가장 크게 배운 건 "tool을 나누는 것만으로는 부족하다" 는 점이다.

저장소별로 검색 tool을 잘 나눠놨는데도, "회의 있어?"라고 물었을 때 LLM이 4주차 검색 tool(search_saved_requests)이 아니라 3주차 목록 조회 tool(personal_list_saved_schedules)을 골랐다. 둘 다 SQLite를 보지만 목적이 다른데, LLM이 헷갈린 것이다.

즉 도구를 나누는 건 절반이고, 나머지 절반은 LLM이 그 나눠진 도구 중 올바른 걸 고르게 유도하는 prompt였다. 3주차에서 "tool 이름이 비슷하면 헷갈린다"를 배웠다면, 4주차는 "목적이 비슷한 tool 사이의 선택"까지 prompt로 안내해야 한다는 걸 배운 셈이다.

그리고 AI한테 코드만 받아 붙여넣지 않고 "이번 주차가 뭐가 다른지", "RAG가 뭔지", "이 헬퍼 두 개는 무슨 차이인지"를 계속 물어보며 개념부터 이해한 게 이번 주 작업을 훨씬 수월하게 만들었다.


부록 — 이번 주에 추가로 찾아본 용어

실습과 직접 관련은 없지만, 공부하다 궁금해서 정리해둔 용어들이다.

MoE (Mixture of Experts)

"전문가 혼합"이라는 AI 모델 구조다. 거대한 하나의 모델이 모든 걸 처리하는 대신, 여러 개의 작은 "전문가(expert)" 모델을 두고 입력마다 관련된 전문가 몇 개만 골라서 활성화한다. 전체 모델은 크지만 매번 일부만 쓰니까 효율적이라, 최근 대형 LLM에서 많이 쓰는 구조다.

"코딩 질문이네" → 코딩 전문가만 활성화
"번역 질문이네" → 번역 전문가만 활성화

Retriever (리트리버)

"검색기". RAG의 핵심 부품으로, 사용자 질문에 맞는 문서를 찾아오는 역할이다. 이번 주에 만든 search_personal_references, search_saved_requests가 바로 retriever다. RAG = Retriever(검색) + Generator(LLM 생성)로 이해하면 된다.

PRD (Product Requirements Document)

"제품 요구사항 문서". AI 용어가 아니라 기획/개발 문서다. 어떤 제품이나 기능을 만들기 전에 "무엇을, 왜, 어떻게 만들지"(목적, 대상 사용자, 요구사항, 성공 기준)를 정리해서 팀이 합의하는 용도로 쓴다. 실무나 프로젝트 후반에 자주 나온다.

profile
둥글게살자

0개의 댓글