-
로컬 챗봇 시리즈 #8 — 봇은 도구 풀을 좁히는 장치다: '지식금고 검색가' 한 도구 봇이 가장 효과적인 이유IT 2026. 5. 9. 21:30
들어가며 — 봇은 GPTs의 라이트 버전이 아니라 도구 풀 좁히기 장치다
"사용자 정의 봇"이라는 기능을 보면 ChatGPT의 GPTs를 흉내낸 라이트 버전 같다. 시스템 프롬프트 + 도구 묶음 + 선택적으로 외부 MCP 서버를 묶어둔 것이다. 사실은 맞다. 그런데 이걸 단순히 "GPTs 클론"으로 보면 가장 큰 가치를 놓친다.
시리즈 #7에서 본 "도구 paradox" — 작은 모델은 도구 풀이 커지면 호출 정확도가 떨어진다. 봇은 그 paradox에 대한 답이다. "이 작업에는 이 3-5개 도구만 써라"라는 의도를 사용자가 작업 단위로 미리 정의해두는 메커니즘이다. 이번 글은 봇의 진짜 가치 — 도구 풀 좁히기의 효과를 한 사례("지식금고 검색가" 봇)로 풀어쓴다.
1. "지식금고 검색가" 봇 — 도구 1개만 노출했더니 호출 빈도가 5배 됐다
실제 사용 사례 하나를 보자. 지식금고에서 과거 노트를 자주 찾는다. 시리즈 #7의 도구 11개 풀에서는 모델이 망설이다 그냥 답해버리는 일이 많아 RAG 호출 빈도가 낮았다. 그래서 봇 하나를 만들었다.
이 그림은 한 봇의 정의를 통째로 보여준다 — 위에서부터 이름(노란색), 시스템 프롬프트(파란색), 허용 도구(녹색), MCP 서버(보라색)의 네 필드로 이뤄진다. 핵심은 녹색 박스의 도구 목록이 단 하나라는 점이다. 11개 도구 풀에서 단 1개만 노출하니, 모델이 도구 호출을 결정할 때 "search_knowledge_vault를 부를까, 안 부를까" 두 갈래뿐이다 — RAG 호출이냐 자연어 직답이냐. 시스템 프롬프트(파란색)가 그 두 갈래 중 무엇을 선호할지를 모델에게 강하게 가이드한다 — "무조건 RAG 먼저, 자체 지식으로 답하지 마라". 이 두 가지(좁힌 도구 풀 + 명확한 가이드)가 결합하니 모델이 거의 매번 올바른 선택을 한다.
효과는 즉각이었다. 같은 사용자 질문에 대해 — 봇 비활성 시 RAG 호출률 ~30% (모델이 도구 11개 중 망설임), 봇 활성 시 ~95%로 올라갔다. 한 질문 한 호출이 거의 보장된다. 사용자 입장에서는 "내 지식금고 노트가 갑자기 실제로 챗봇에 작용하기 시작했다"는 인상을 받는다.
2. 봇은 시스템 프롬프트 조립의 맨 끝에 합류한다
시리즈 #2에서 시스템 프롬프트 조립 순서를 다뤘다(① BASE → ② 글로벌 CI → ③ 프로젝트 지침 → ④ 첨부 → ⑤ 메모리). 봇은 이 시퀀스에서 ③ 위에 추가로 끼어든다.
이 그림이 보여주는 봇 페르소나 블록(빨간색)의 위치가 중요하다. ③ 프로젝트 지침 바로 다음, ④ 첨부 직전이다. 즉 모든 일반적 지시(① BASE, ② 글로벌 CI, ③ 프로젝트) 뒤에 배치되어 모델이 가장 강하게 의식하는 위치를 차지한다. 시리즈 #2에서 다룬 "뒤에 오는 지시 우선" 원칙의 활용이다. 봇이 시스템 프롬프트의 마지막 페르소나 신호로 작동하니, 봇 활성 시 모델 행동이 봇 의도대로 강하게 정렬된다.
구현은 헤더 한 줄과 분기 몇 줄이다.
# proxy.handle_agent_chat bot_id = request.headers.get("X-Bot-ID") if bot_id: bot = _bot_mod.get_bot(bot_id) if bot: if bot.get("system_prompt"): system_prompt += f"\n\n[봇 페르소나: {bot['name']}]\n{bot['system_prompt']}" if bot.get("allowed_tools"): bot_allowed_tools = list(bot["allowed_tools"]) # → 도구 풀 좁힘 if bot.get("mcp_servers"): bot_mcp_servers = list(bot["mcp_servers"]) # → MCP 동적 합류이 코드의 디자인 비결은 "두 가지 변경을 같은 헤더로 동시에 트리거"한다는 점이다.
X-Bot-ID한 헤더가 (a) 시스템 프롬프트에 페르소나 추가, (b) 도구 풀 화이트리스트 적용, (c) MCP 서버 활성화 — 세 변경을 한꺼번에 발동시킨다. 클라이언트 입장에서는 봇 셀렉터에서 한 번 클릭하면 헤더가 바뀌고, 다음 채팅부터는 다른 페르소나의 챗봇처럼 동작한다. 사용자 인지 비용이 낮다.봇 정의는 단순한 JSON 파일(
data/bots/<id>.json)이다. 가져오기·내보내기·공유가 쉽고, 백엔드 데이터 모델 복잡도는 거의 0이다.
3. "봇 풀"의 디자인 원칙 — 작업 단위로 좁게
봇을 만들기 시작하면 처음에는 "범용 봇"을 만들고 싶어진다. 도구 9개를 묶고 시스템 프롬프트도 길게 쓰고 싶어진다. 그러면 효과가 없다 — 일반 챗봇과 같아진다. 봇의 가치는 "좁힘"에서 온다. 자주 쓰는 봇 3개 정도가 황금비다.
이 그림이 보여주는 세 봇의 공통점은 모두 도구 1-3개로 좁아져 있다는 점이다. 일반 챗봇 모드(도구 11개)는 "어떤 봇도 안 어울리는 자유 대화"용으로 남겨두고, 명확한 작업 단위가 있으면 즉시 봇을 활성화한다. 코딩 작업이면 Coder 봇, 가족 일정 관리면 가족일정 도우미 봇, 지식금고 노트 검색이면 지식금고 검색가 봇을 쓴다. 각 봇이 자기 영역에서는 도구 호출 정확도가 거의 100%에 가깝고, 자기 영역을 벗어나는 질문은 봇 시스템 프롬프트가 부드럽게 거절(또는 사용자가 다시 봇 비활성화)한다. 상단 셀렉터에서 한 번 클릭으로 페르소나·도구가 다 바뀐다.
4. 트레이드오프 — 봇의 비용과 함정
4-1. 화이트리스트가 작아질수록 회복 수단이 사라진다
"지식금고 검색가" 봇처럼 도구 1개만 노출하면 그 도구가 실패할 때 회복이 어렵다. 구체적으로 — RAG 서버(vault-search)가 다운되어 있으면 모델이 search_knowledge_vault를 호출하고 "
[도구 오류] search_kv: ConnectionError"만 받는다. 다음 시도할 도구가 없다. 모델은 자연어로 답하긴 하지만 사용자 의도(노트 검색)를 만족하지 못한다. 사용자는 "왜 봇이 멍청해졌지?"라고 느낀다.이 함정은 "좁힘"의 양면성을 보여준다. 도구를 좁히면 호출 정확도가 올라가지만 동시에 단일 장애점이 생긴다. 11개 도구 중 1개가 실패해도 나머지 10개로 우회할 수 있는 일반 챗봇과 달리, 1도구 봇은 그 1개의 가용성에 100% 의존한다. 운영 측면에서는 vault-search 같은 핵심 의존성의 모니터링이 더 중요해진다.
완화책은 봇에 fallback 도구 1-2개를 더 두는 것이다(예: web_search). 그러면 RAG가 실패해도 web으로 우회한다. 하지만 그러면 "한 도구 봇"의 단순함과 정확도 이점이 줄어든다. 도구 1개로 정확도 95%였던 게 도구 2개로 늘리면 정확도 80%로 떨어질 수 있다. 가용성과 정확도 사이의 트레이드오프 — 사용자 패턴에 따라 결정한다. 핵심 도구의 가용성이 충분히 높다면 1도구 봇이 합리적이고, 자주 흔들린다면 fallback 추가가 합리적이다.
4-2. 봇 시스템 프롬프트가 글로벌 CI 위에 누적된다
봇 페르소나는 글로벌 Custom Instructions의 다음 자리에 추가되니, 두 개 합산이 토큰을 더 잡아먹는다. 봇 프롬프트 길이가 200자라면 글로벌 CI 4000자 + 봇 200자 = 4200자가 시스템 프롬프트에 들어간다. 매 요청 토큰 비용이 든다. 시리즈 #2에서 다룬 vLLM prefix caching이 이 비용을 거의 무료로 만들지만, 봇을 자주 전환하는 시나리오에서는 캐시 hit률이 떨어진다 — 매 봇 전환마다 시스템 프롬프트가 달라지니 새 prefix를 캐시에 빌드해야 한다.
또 한 가지 함정 — 시리즈 #2의 "뒤에 오는 지시 우선" 원칙이 봇에도 적용된다. 봇 페르소나가 글로벌 CI보다 강하게 작동하니, 글로벌 CI에 모순되는 봇 페르소나를 만들면 봇이 이긴다. 의도된 동작이지만 사용자 인식이 필요하다. 예를 들어 글로벌 CI에 "코드 예시 짧게"라고 적어뒀는데 Coder 봇 시스템 프롬프트에 "긴 코드 예시 자세히"라고 쓰면, Coder 봇 활성 시 긴 코드가 나온다 — 의도일 수도 있고, 사용자 본인이 잊고 있던 모순일 수도 있다. 봇 시스템 프롬프트는 짧게(200자 이내), 글로벌 CI와 명백한 충돌을 피하는 형태로 작성하는 게 안전하다.
4-3. MCP 도구 합류는 화이트리스트를 우회한다
봇이 활성화한 MCP 서버의 도구는
allowed_tools화이트리스트 적용 후에 추가된다. 즉 봇이allowed_tools: ["search_knowledge_vault"]로 1개 도구만 허용한다고 적어도, 같은 봇이mcp_servers: ["github"]로 GitHub MCP를 활성화하면 GitHub의 도구 5-10개가 추가로 합류한다. 결과적으로 봇 도구 풀이 1개 + 5-10개 = 6-11개로 늘어나 "도구 paradox" 영역으로 다시 진입할 수 있다.이 디자인 결정의 함의는 봇 디자이너가 "MCP 서버를 활성화한다는 것은 그 서버의 모든 도구를 신뢰한다"는 인식을 가져야 한다는 점이다. MCP 서버가 제공하는 모든 도구가 봇의 작업과 잘 맞는지 미리 확인해야 한다. 그렇지 않으면 봇 화이트리스트로 좁힌 효과가 MCP 합류로 깨진다. 더 정밀한 제어가 필요하면 — MCP 서버가 5개 도구를 노출하는데 그중 3개만 쓰고 싶다면 — 별도 화이트리스트 메커니즘이 필요한데 현재는 미구현이다. 단순함을 우선한 결정이지만 MCP 사용이 늘면 재검토해야 할 것이다.
5. 마무리
봇의 진짜 가치는 "GPTs의 라이트 버전"이 아니라 "도구 풀을 작업 단위로 미리 좁혀두는 사용자 도구"라는 점이다. 시리즈 #7에서 발견한 "도구 paradox"의 답이 봇 화이트리스트 한 줄 — 작은 모델일수록 그 효과가 크다.
다른 부수 — 봇 정의는
data/bots/<id>.json, 활성화는X-Bot-ID헤더, UI는 상단 셀렉터 + 봇 관리/편집 모달, localStorage로 마지막 활성 봇 기억 — 는 표준 작업이다.다음 편은 MCP 통합이다. 봇의 mcp_servers 필드가 외부 표준 도구를 어떻게 동적으로 합류시키는지 다룬다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
Whisper Small에서 Turbo로 — 아이 발음을 위한 STT 모델 선택 (0) 2026.05.10 로컬 챗봇 시리즈 #12 (완) — 정책을 데이터로 표현하기: jobs.conf 한 줄이 모든 GPU 동거 정책을 결정한다 (0) 2026.05.09 로컬 챗봇 시리즈 #11 — Esc 한 키가 깨끗해야 한다: UI 임시 상태의 우선순위 스택 디자인 (0) 2026.05.09 로컬 챗봇 시리즈 #10 — [hidden] 속성이 안 먹는 한 시간: HTML5의 작은 속성과 컴포넌트 CSS의 충돌 (0) 2026.05.09 로컬 챗봇 시리즈 #9 — MCP 외부 서버 장애를 graceful하게 흡수하는 디자인: '채팅이 안 막히는 게 우선' (0) 2026.05.09 로컬 챗봇 시리즈 #7 — 도구 11개가 모이면 모델이 헷갈리기 시작한다: 풀 격리와 _safe_tool 안전판 (0) 2026.05.09 로컬 챗봇 시리즈 #6 — LLM을 우회하는 슬래시 커맨드: 작은 모델의 메타 판단 한계를 사용자가 보완하는 채널 (0) 2026.05.08 로컬 챗봇 시리즈 #5 — 다른 프로젝트의 venv를 subprocess로 빌려쓰기: 의존성 추가를 거부하는 영리함 (0) 2026.05.08 로컬 챗봇 시리즈 #4 — Vision 32B에서 7B로, 그리고 포기까지 — 두 vLLM 동거 시행착오 (0) 2026.05.08 로컬 챗봇 시리즈 #3 — 도구에 '지금 누구의 첨부인지' 어떻게 알려주나: ContextVar 패턴 (0) 2026.05.08