본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 06. 22:18

내 Linux 머신에서 완전히 실행되는 1920년대 집사 AI를 만들었습니다. 그러다 포기했고, 그 후 Copilot이 수정을 도와주었습니다.

요약

사용자의 Linux 환경에서 로컬로 실행되는 1920년대 집사 컨셉의 AI 어시스턴트 'Bantz' 개발기입니다. Ollama, MarianMT, ChromaDB 등을 활용하여 개인정보 보호와 언어 최적화를 구현했으며, Copilot의 도움으로 프로젝트를 완성했습니다.

핵심 포인트

  • Ollama를 활용한 로컬 LLM 기반의 오프라인 AI 구현
  • 터키어 번역 레이어 및 음성 I/O(Whisper, Piper) 통합
  • ChromaDB와 SQLite를 이용한 지속적 메모리 시스템 구축
  • 데스크톱 자동화 및 셸 명령 실행을 위한 멀티스텝 플래너 설계

이 게시물은 GitHub Finish-Up-A-Thon Challenge를 위한 제출물입니다.

내가 만든 것

Bantz는 사용자의 Linux 머신에서 완전히 실행되는 로컬 우선(local-first), 오프라인 지원 AI 어시스턴트입니다. 이 어시스턴트는 1920년대 영국 집사로 자신을 소개합니다. 항상 예의 바르고, 미묘하게 비꼬는 말투를 사용하며, 자신이 당신과 함께 방 안에 서 있는 실제 사람이라고 확신하고 있습니다.

저는 Linux 데스크톱을 사용하는 터키어 사용자입니다. 제가 시도했던 모든 클라우드 어시스턴트는 외국어로 말을 걸었고, 다른 누군가의 서버로 데이터를 전송했으며, 세션이 종료되는 순간 모든 것을 잊어버렸습니다. 저는 다른 것을 원했습니다. 터키어를 모국어로 구사하고, 내 하드웨어에서 실행되며, 문맥(context)을 기억하고, 실제로 내 데스크톱을 제어할 수 있는 어시스턴트 말입니다. 그래서 Bantz를 만들기 시작했습니다.

개념은 야심 차게 설계되었습니다. 핵심 요소는 다음과 같습니다: Helsinki-NLP의 MarianMT를 활용한 터키어 ↔ 영어 번역 레이어, 웹 검색, Gmail, Calendar, 셸 명령(shell commands), 파일 시스템 접근, 그리고 AT-SPI 데스크톱 자동화를 체인 형태로 연결할 수 있는 다단계 도구 플래너(multi-step tool planner) — 이 모든 것은 Ollama를 통해 로컬에서 실행되는 LLM에 의해 조정됩니다. 그 외에도 faster-whisper와 Piper TTS를 통한 음성 I/O, ChromaDB와 SQLite 지식 그래프(MemPalace)를 기반으로 한 지속적 메모리(persistent memory), CPU 부하와 시간대에 따라 어조가 변하는 6단계 집사 페르소나, 그리고 실시간 상태 표시줄이 있는 Textual TUI가 포함됩니다.

아키텍처는 진정으로 흥미로웠습니다. 하지만 2026년 5월 현재, 실행 결과는 엉망진창이었습니다.

데모

GitHub repo: github.com/miclaldogan/bantzv2

Broadcast Channel — Bantz와 채팅하기, 웹 검색 + 데스크톱 제어 작동 중:

전체 과정 (Full walkthrough) — Operations Center의 모든 페이지:

스크린샷 (Screenshots)

...

재기 성공 스토리 (The Comeback Story)

이전 — 2026년 5월

17개의 미해결 이슈 (backlog)가 쌓여 있었고, 피해 상황을 기록하기 위해 작성한 BROKEN_STATE.md 파일이 있었습니다. 그 내용은 다음과 같았습니다:

기능 감사 (feature audit) 결과는 처참했습니다:

기능상태
음성 입력 (Whisper)🔴 고장남 — 패키지 3개 누락, Picovoice 키 미설정
...

가장 창피한 부분은 이것이었습니다. 설정에 따라 Claude, OpenAI, Gemini 또는 Ollama로 분기할 수 있는 정교한 멀티 프로바이더 LLM 라우터 (router.py)를 만들어 놓고는, finalizer.py, summarizer.py 및 스트리밍 경로의 모든 호출 지점(callsite)에서 그냥... from bantz.llm.ollama import ollama를 직접 하드코딩해 버린 것입니다. 라우터가 완전히 무시되었습니다. BANTZ_LLM_PROVIDER=claude로 설정한 사용자는 오류도, 경고도 없이 아무런 징후 없이 Ollama의 응답을 받게 될 것이었습니다.

첫 실행 경험은 특히 고통스러웠습니다. bantz --once "merhaba"를 실행하면 MarianMT가 로드되고, Ollama가 추론하고, Piper가 합성하는 과정이 모두 순차적으로, 그리고 아무런 출력 없이 진행되느라 최대 30초 동안 완전한 정적 속에서 멈춰 있었습니다. 신규 사용자들은 프로세스를 강제 종료하고 다시는 돌아오지 않았습니다.

이후 — 2026년 6월

단 한 번의 집중적인 세션 동안 7개의 이슈를 해결하여 모두 main 브랜치로 스쿼시 머지 (squash-merged) 했습니다:

PR #467 — 1줄 수정, 완전한 침묵의 원인 규명. WsBroadcastServer 내부의 _WSLogHandlerself._q라는 실제 큐 속성 대신 self._log_q를 참조하고 있었습니다. 글자 하나가 틀린 오타였습니다. WebSocket 서버가 작성된 이후의 모든 로그 레코드는 AttributeError를 발생시켰지만 조용히 삼켜졌고, 그 결과 Tauri 데스크톱 UI에는 로그 출력이 전혀 전달되지 않았습니다. 수정 완료되었습니다.

PR #468 & #469 — 라우팅되지 않았던 라우터. 세 곳의 모든 finalizer.py 호출 지점과 summarizer.py의 Gemini/Ollama 폴백 (fallback) 체인을 from bantz.llm.router import get_llm으로 교체했습니다. router.py에 편의를 위한 별칭(alias)으로 get_llm = get_provider를 추가했습니다. 이제 Claude, OpenAI

_Translator에 256개의 항목을 가진 FIFO LRU 캐시(cache)를 추가했습니다. 이제 자주 사용되는 문구는 첫 호출 이후 약 0ms 만에 번역됩니다. 그다음, finalize_stream()._stream()을 문장 경계((?<=[.!?])\s+)까지 LLM 토큰을 버퍼링하고 문장마다 즉시 bridge.to_turkish()를 호출하도록 재작성했습니다. 이를 통해 LLM이 다음 문장을 계속 생성하는 동안 번역된 출력을 제공할 수 있게 되었습니다. 이제 번역은 추론(inference)이 끝난 후 실행되는 대신 추론과 겹쳐서(overlap) 진행됩니다. 또한 ws_server.py의 스트리밍 경로에서 중복되는 await _to_tr("".join(parts)) 재번역 과정을 제거했습니다. 브릿지(bridge)가 활성화되어 있으면 finalize_stream이 이미 사전 번역된 토큰을 방출하기 때문입니다.

Issue #463 — 이미 수정됨 (PR 불필요). Copilot은 Live(screen=False)가 이미 설정되어 있고 REFRESH_FPS = 4가 이미 존재함을 확인했습니다. 설명 댓글과 함께 이슈를 종료했습니다. 때로는 올바른 해결책이 고칠 것이 아무것도 없음을 인식하는 것일 수도 있습니다.

GitHub Copilot 사용 경험

저는 세션 전체 동안 에이전트 모드(Claude Sonnet 4.6)로 Copilot을 사용했습니다. 가장 놀라웠던 점은 제가 엉망으로 방치했던 코드베이스에 Copilot이 가져다준 규율(discipline)이었습니다.

모든 이슈에 대해 Copilot은 별도의 지시 없이도 동일한 워크플로우를 따랐습니다:

  1. 무엇인가를 건드리기 전에 영향을 받는 파일을 읽습니다. 요약이 아니라 실제 파일을 처음부터 끝까지 읽습니다.
  2. 문제를 일으키는 정확한 심볼(symbol)을 검색합니다. 이슈 #462의 경우, ws_server.py에서 _q|_log_q를 검색하여 불일치를 즉시 찾아냈습니다. #465의 경우, 모든 `

가장 가치 있었던 순간은 이슈 #422(번역 지연 시간) 상황이었습니다. 번역이 느리다는 것은 알고 있었지만, 저는 그것이 단순히 하드웨어의 한계라고 가정했습니다. CPU에서 실행되는 MarianMT는 그만큼의 시간이 걸리는 법이니까요. Copilot은 finalize_stream()부터 ws_server.py까지의 전체 데이터 흐름을 추적하여 병목 현상이 단순히 모델 때문만이 아니라는 점을 식별해냈습니다. 그것은 바로 아키텍처의 문제였습니다. 완료 후의 순차적 실행(sequential execution), 그리고 매 호출마다 동일한 입력값이 재번역되는 구조가 문제였습니다. Copilot이 제안한 문장 경계 스트리밍(sentence-boundary streaming) 방식은 제가 전혀 생각하지 못했던 것이었으며, 단 한 번의 시도만에 성공했습니다.

또 다른 감사했던 순간은 이슈 #463이었습니다. Copilot은 자신의 존재 가치를 증명하기 위해 억지로 변경 사항을 만들어내는 대신, live_ui.py에서 screen=을 검색하여 해당 값이 이미 False임을 확인하고, REFRESH_FPS를 점검한 뒤 이슈가 이미 해결되었음을 저에게 알려주었습니다. 버그 리포트를 "이미 수정되었습니다"라며 닫는 것이야말로 올바른 결과입니다. 출력을 생성하도록 최적화된 도구로부터 이러한 절제력을 얻기란 쉽지 않은 일입니다.

Bantz는 아직 완성되지 않았습니다. 음성 입력에는 여전히 세 개의 패키지가 필요하고, --doctor 출력은 아직 다듬어야 하며, 제대로 된 온보딩(onboarding) 흐름을 추가하고 싶습니다. 하지만 이제 핵심 파이프라인은 지원되는 모든 LLM 제공업체에 대해 올바르게 작동하며, TUI는 깔끔하게 렌더링되고, 터키어 응답은 10초 이내에 도착하며, 집사의 로그가 마침내 데스크톱 UI에 도달합니다. 이는 "당혹스러울 정도로 망가져 있던" 프로젝트에서 "실제로 출시 가능한" 프로젝트로 변모한 것이며, Copilot은 단 한 번의 오후 만에 이를 가능하게 만든 페어 프로그래밍 파트너(pairing partner)였습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0