ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 로컬 챗봇 시리즈 #6 — LLM을 우회하는 슬래시 커맨드: 작은 모델의 메타 판단 한계를 사용자가 보완하는 채널
    IT 2026. 5. 8. 23:30
    로컬 챗봇 시리즈 #6 — LLM을 우회하는 슬래시 커맨드: 작은 모델의 메타 판단 한계를 사용자가 보완하는 채널

    들어가며 — "기억해줘"가 작은 모델에서 흔들릴 때

    "내 가족 구성원의 생일은 5월 12일이야, 기억해줘"라고 챗봇에 말하면 ChatGPT는 알아서 long_term memory에 저장한다. 잘 동작할 때는 마법 같지만, 작은 모델은 "이건 메모리에 저장할 만한 사실이다"라는 메타 판단이 약해서 며칠 후 다시 물으면 모른다. 자동 메모리 도구(save_memory)는 호출 자체가 안 일어났을 가능성이 크다.

    "모델을 더 똑똑하게 학습"하는 건 시간이 걸리는 길이고, "사용자가 명시적으로 신호를 보낼 채널"을 추가하는 건 즉시 가능한 길이다. 슬래시 커맨드 — /remember <텍스트>. 이번 글은 이 한 채널이 LLM 호출을 어떻게 우회하고, 왜 그 우회가 사용자 신뢰의 핵심인지 다룬다.


    1. 자동 추출의 두 가지 실패 모드

    자동 메모리는 사실 두 단계 추론이다. (a) 모델이 "이 사용자 발화에 저장할 만한 사실이 있다"고 판단해야 하고, (b) save_memory 도구를 호출 인자(name, body 등)와 함께 정확히 호출해야 한다. 둘 다 작은 모델에서 약하다. 그림으로 정리하면:

    diagram

    이 그림이 보여주는 핵심은 두 단계 모두 통과해야 메모리가 실제로 저장된다는 점이다. 단계 1에서 "이건 저장할 사실이다"라는 메타 판단을 모델이 못 하면 도구 호출 자체가 안 일어난다. 사용자의 "기억해줘"라는 명령조 신호도 큰 모델은 잘 해석하지만 작은 모델은 "네, 기억하겠습니다"라고 답하면서도 정작 save_memory 도구를 안 부른다 — 정중한 거짓말이다. 단계 2까지 갔다 해도 함정이 또 있다. 도구의 인자 시그니처(name, body, entry_type 등 5개)를 정확히 채워야 하는데, 작은 모델은 형식을 약간 어긋나게 하거나 인자 이름을 환각해서 넣는다. 도구가 거부하거나 잘못된 위치에 저장된다.

    이 함정은 모델이 작아질수록 빈도가 높다. 그리고 가장 나쁜 건 사용자가 "저장 실패"를 즉각 알 수 없다는 것이다. 며칠 후 회상이 안 될 때야 발견. "모델이 거짓말한 것 같다"는 인상이 굳어지면 자동 메모리 자체에 대한 사용자 신뢰가 회복 불가능해진다. 한 번 신뢰가 깨지면 사용자는 "이 챗봇은 기억 못 해"라고 판단하고 메모리 기능을 안 쓴다.


    2. 답 — 사용자가 명시적으로 보낼 수 있는 LLM 우회 채널

    해법은 단순하다. 사용자가 /remember로 시작하는 메시지를 보내면 챗봇은 LLM을 호출하지 않고 직접 메모리에 저장하고 즉시 응답한다. 채팅 흐름이 두 갈래로 분기되는 디자인을 그림으로 보면:

    diagram

    이 그림이 보여주는 디자인의 우아함은 "단 한 줄짜리 분기"에 있다. 챗봇의 메인 진입점인 handle_chat에서 사용자 메시지의 첫 단어가 슬래시 커맨드인지 한 번 체크하고, 그 결과로 두 갈래로 나뉜다. 왼쪽 갈래(슬래시)는 LLM을 호출하지 않고 직접 메모리에 저장한 뒤 SSE로 즉시 응답을 흘려보낸다. 오른쪽 갈래(일반)는 평상시 그대로 vLLM 호출 → 모델 추론 → 스트리밍 응답. 두 갈래는 코드상으로 완전히 격리되어 있어서 슬래시 처리가 평상 채팅 코드를 한 줄도 건드리지 않는다.

    코드는 단순하다.

    # slash_commands.py
    KNOWN_COMMANDS = {"/remember", "/forget", "/memories"}
    
    def is_slash_command(msg: str) -> bool:
        if not msg or not msg.startswith("/"): return False
        head = msg.split(maxsplit=1)[0].strip().lower()
        return head in KNOWN_COMMANDS
    
    def handle_slash(msg, long_term) -> str | None:
        if not msg or not msg.startswith("/"): return None
        parts = msg.split(maxsplit=1)
        head, arg = parts[0].lower(), (parts[1] if len(parts) > 1 else "")
        if head == "/remember":
            long_term.add_entry(name=arg[:30], description=arg[:80],
                                entry_type="note", body=arg.strip(),
                                source="slash_command")
            return f"✅ 장기 메모리에 저장: `{arg[:80]}`"
        # ... /forget, /memories
    

    이 코드의 두 가지 결정이 핵심. 첫째, KNOWN_COMMANDSset으로 정의 — 알려진 커맨드만 인터셉트하고, "/foo bar" 같은 미지의 슬래시는 그대로 LLM으로 보낸다(LLM이 그것을 자연어로 해석하게). 사용자가 실수로 슬래시를 잘못 쳐도 에러가 나지 않고 그냥 일반 채팅처럼 자연스럽게 처리된다. 둘째, handle_slash의 반환 타입을 str | None으로 둔다. None이면 호출자가 "이건 슬래시 아니구나, 평상 흐름으로 보내자"로 판단하고, str이면 호출자는 그것을 바로 사용자 응답으로 흘려보낸다. 호출자는 분기 로직을 None 체크 한 줄로 끝낸다.

    proxy 측은 진입 시점에 한 번만 검사하면 된다.

    # proxy.handle_chat 진입부
    import slash_commands as _sc
    if _sc.is_slash_command(last_user_msg):
        slash_reply = _sc.handle_slash(last_user_msg, _long_term)
        if slash_reply is not None:
            await _send_sse_chunk(response, slash_reply)
            session.add("user", last_user_msg)
            session.add("assistant", slash_reply)
            _long_term_last_reload = 0   # 다음 요청에서 fresh 로드
            return response
    # ... 일반 LLM 호출 흐름
    

    여기서 두 가지 디테일을 짚어두자. 첫째, session.add로 user/assistant 메시지를 둘 다 저장한다. 슬래시 응답도 일반 채팅 응답과 같이 세션 히스토리에 남는다 — 사용자가 나중에 "내가 그때 뭘 저장했지?"를 세션 검색으로 찾을 수 있다. 둘째, _long_term_last_reload = 0은 메모리 캐시 무효화. long_term은 60초마다 디스크에서 reload되는 캐시 구조라, 슬래시로 새 메모리를 저장한 후 그 메모리가 다음 요청에서 시스템 프롬프트에 즉시 반영되도록 강제 reload 트리거를 던진다. 이 한 줄이 빠지면 "방금 저장했는데 왜 회상 안 되지?" 사용자 혼란을 만든다.

    "명시 채널"의 위력은 단순하다. 모델이 잘 못 하는 일을 사용자가 한 줄로 정확히 표현할 수 있게 해주는 것. 사용자는 자기가 무엇을 저장하고 싶은지 100% 안다. 모델이 그것을 추측할 필요가 없다.


    3. 같은 패턴이 다른 LLM 약점에도 적용 가능

    "LLM 우회 채널"의 발상은 메모리 저장에만 머물지 않는다. 작은 모델이 신뢰할 만하지 않은 다른 영역도 같은 방식으로 우회 가능하다.

    diagram

    이 그림이 보여주는 핵심은 슬래시 커맨드의 발상이 메모리 외에도 일반화 가능하다는 점이다. 위쪽 두 박스는 "LLM 도구 호출이 약한 영역"의 대안 채널이다. 사용자가 강제로 RAG·웹 검색을 트리거하거나, 모델이 잘 안 부르는 메타 액션(대화 정리, export)을 직접 호출할 수 있게 해준다. 아래쪽 두 박스는 "UI 클릭을 키보드로 대체"하는 워크플로우 보조다. 키보드 위주 사용자가 마우스를 안 잡고도 봇 전환·검색 강제를 할 수 있다. 공통 원칙은 "사용자가 정확한 의도를 가지고 있을 때 LLM의 추측을 거치지 않는 직통 채널을 제공한다"는 것이다. 매번 LLM이 결정하는 것이 잘 동작하는 영역(자유 대화, 일반 도구 호출)은 그대로 두고, LLM이 자주 흔들리는 영역에만 우회 채널을 추가한다.


    4. 트레이드오프 — 명시 채널의 비용

    4-1. 사용자가 명령어를 외워야 한다 — 자동완성 popup으로 보완

    슬래시 커맨드는 강력하지만 학습 비용이 있다. 처음 며칠은 사용자가 "/remember?", "/recall이었던가?" 같은 혼동을 겪는다. 한 번 익히면 영원히 쓰는 도구지만, 그 며칠을 돕지 않으면 사용자가 슬래시 자체를 안 쓰게 된다.

    완화책은 입력창 자동완성 popup이다. 사용자가 /만 한 글자 치면 popup이 떠서 알려진 커맨드 3개(/remember, /forget, /memories)와 각각의 짧은 설명이 나온다. @ 멘션과 같은 popup으로 통합 — 시리즈 #11에서 다룰 "트리거 문자에 따라 후보 필터링" 디자인. 사용자는 두 모드를 자유롭게 쓸 수 있다 — 익숙한 사용자는 직접 타이핑, 익숙하지 않은 사용자는 popup 보고 골라 클릭.

    학습 비용을 0으로 만들 수는 없지만, popup이 그 비용을 "한 글자 치면 보이는 도움말" 수준으로 떨어뜨린다. 사용자가 굳이 외우지 않아도 자연스럽게 익힌다.

    4-2. 자동 메모리(save_memory 도구)는 여전히 살아있다 — 두 채널 공존이 핵심

    슬래시는 추가 채널이지 대체가 아니다. 모델이 평상 대화에서 save_memory 도구를 호출하면 그것도 정상 저장된다. 같은 long_term 메모리에 들어가니 데이터 정합성 문제는 없다.

    왜 자동 메모리를 안 없애는가? 사용자 인지 부담 때문. 모든 저장 신호를 사용자가 명시해야 한다면 그것 또한 사용자에게 부담이다. 자연스러운 대화에서 "내 가족 알레르기 정보를 알려줘야겠다" 같은 식으로 정보를 흘리면 모델이 알아서 저장하는 게 가장 편하다 — 작동만 하면. 그래서 두 채널을 공존시킨다 — 자동이 우선, 명시는 백업. 자동이 잘 동작하면 사용자가 슬래시를 안 써도 되고, 자동이 흔들리면 사용자가 슬래시로 강제할 수 있다.

    이 디자인의 함의 — 모델이 더 똑똑해지면 자동 채널이 점점 잘 동작하고, 슬래시 사용 빈도가 자연스럽게 줄어든다. 슬래시는 "지금 모델 능력의 한계 보완"인 셈. 모델 업그레이드 시 슬래시 코드를 제거할 필요 없이 점차 사용자가 안 쓰게 되는 자연스러운 진화 경로.

    4-3. /forget이 substring 매칭이라 정확한 메모리 1개를 짚기 어렵다

    "/forget 가족"이라고 하면 "가족"이 들어간 모든 메모리가 매칭 후보. 현재 구현은 첫 번째 매칭만 삭제한다. 그게 사용자가 의도한 그 메모리가 맞는지 확신할 수 없다 — 같은 키워드가 들어간 다른 메모리가 우연히 먼저 매칭될 수 있다.

    이게 위험한 이유. "/forget"은 되돌릴 수 없는 동작이다. 잘못 지운 메모리는 영구 손실. 사용자가 "5월 12일 생일"을 지우려고 했는데 우연히 "5월 가족여행" 메모리가 먼저 매칭되어 그게 지워졌다면 다시 채워야 한다. 더 나쁜 건 사용자가 잘못 지워졌다는 것을 모를 수 있다는 점 — 며칠 후 회상이 안 될 때야 발견.

    권장 흐름은 "/memories로 먼저 목록 확인 → 정확한 키워드로 /forget". /memories는 모든 메모리의 name과 body 스니펫을 보여주니 사용자가 어떤 키워드가 어디 들어가 있는지 시각적으로 확인할 수 있다. 그러나 이 흐름을 사용자가 지킬지는 모른다. 더 안전한 디자인은 (a) /forget이 매칭 후보가 여러 개일 때 "이것 중 무엇을 지울까요?" 확인 다이얼로그를 띄우거나, (b) /forget이 ID 기반으로 동작(/forget <name> 또는 /forget #3)하는 것. 현재는 단순함을 우선해 substring 매칭 + 첫 번째 매칭만 삭제로 시작했지만, 사용자가 잘못 지움 사고가 한 번이라도 일어나면 ID 기반으로 옮겨야 할 것이다.


    5. 마무리

    "명시 vs 암묵"은 LLM UX 디자인에서 자주 만나는 갈림길이다. 자동만 믿으면 작은 모델로는 신뢰가 흔들리고, 명시만 강요하면 사용자에게 인지 부담이 간다. 두 채널을 같이 두면 사용자가 자유롭게 고른다는 게 이번 묶음의 한 줄 교훈이다. 그리고 "LLM 우회 채널"은 슬래시 커맨드 외에도 모델 약점 패턴마다 활용 가능한 일반 도구다.

    다른 부수 작업 — handle_chat 진입의 단일 분기 지점, SSE로 즉시 응답, 자동완성 popup 통합 — 은 표준 도구로 마무리한다.

    다음 편은 에이전트 도구 풀이다. 11개 도구가 있을 때 작은 모델이 어떻게 헷갈리는지, 그리고 그 풀을 좁히는 안전판(_safe_tool, allowed_tools 화이트리스트)을 어떻게 만들었는지 다룬다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.