며칠 전 오래된 Korg X2/X3 신시사이저의 매뉴얼을 뒤적이다가 한 가지 생각을 했다. Basic Guide와 Reference Guide를 AI에게 모두 읽혀 놓고 한국어로 질문하면 매뉴얼을 근거로 답해 주는 개인용 챗봇을 만들 수 있지 않을까? 요즘 흔히 말하는 RAG(Retrieval-Augmented Generation)를 로컬 PC에서 구현하면 될 것 같았다. 매뉴얼에서 질문과 관련된 내용을 검색하고, 그것을 로컬에서 실행되는 LLM에 넘겨 답을 만들게 하는 것이다. 별도의 API 사용료 없이 내 노트북에서 모든 것을 처리한다는 것도 매력적으로 느껴졌다.
처음 기대한 것은 제법 그럴듯했다. “드럼 키트는 어디에서 편집하지?”, “패턴을 트랙에 넣으려면?”, “MIDI로 Program을 선택하는 방법은?”과 같이 한국어로 물어보면 AI가 매뉴얼에서 근거를 찾아 답해 주는 시스템을 생각했다. 그러나 며칠 동안 작은 모델과 임베딩, OCR, 여러 단계의 답변 생성 방법을 시험해 본 끝에 결과물은 처음 구상과 상당히 달라졌다. 내 노트북의 제한된 자원으로 무엇을 할 수 있고, 무엇을 굳이 하지 않는 편이 나은지를 알아 가면서 목표 자체가 바뀌었기 때문이다. 내가 쓰는 노트북은 2022년 여름 용산에서 구입한 ThinkPad E14 Gen3이다(구입 당시 쓴 글).
Python 작업 환경부터 만들기
Windows에서 프로젝트 폴더를 하나 만들고 Python 가상환경(venv)을 만들었다. Python은 3.11을 사용했다. PowerShell에서 프로젝트 디렉터리로 이동한 다음 다음과 같이 가상환경을 만들고 활성화하였다.
python -m venv .venv
.\.venv\Scripts\Activate.ps1
PowerShell의 실행 정책 때문에 활성화가 막힌다면 현재 세션에 한해서 다음과 같이 허용할 수 있다.
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\.venv\Scripts\Activate.ps1
필요한 Python 패키지도 설치하였다. 최종적으로 남은 프로그램만 생각한다면 다음 정도면 된다.
python -m pip install --upgrade pip
pip install streamlit pymupdf requests pillow pytesseract
PDF가 스캔 이미지이므로 OCR을 위해 Tesseract도 별도로 설치했다.
여기에서 처음 접하는 사람이라면 조금 헷갈릴 수 있는데,
pip install pytesseract로 설치되는 것은 OCR 프로그램 자체가 아니다.
pytesseract는 Python에서 Tesseract를 호출하기 위한 인터페이스이고,
실제 Tesseract OCR 프로그램은 Windows에 따로 설치해야 한다.
처음에는 임베딩을 이용한 검색도 시도했기 때문에 다음 패키지도 설치하였다.
pip install sentence-transformers numpy
그러나 이것들은 최종 프로그램에서는 사용하지 않게 되었다. 시행착오를 그대로 재현하려는 것이 아니라면 설치할 필요가 없다.
스캔한 매뉴얼을 검색할 수 있게 만들기
내가 가지고 있는 Korg X2/X3 매뉴얼은 텍스트가 포함된 PDF가 아니라 스캔한 문서다.
따라서 PDF 파일을 열었다고 해서 Drum Kit나 Pattern 같은 단어를 곧바로 검색할 수 있는 것은 아니었다.
PyMuPDF로 PDF의 각 페이지를 읽어 이미지로 만들고 Tesseract로 OCR을 수행한 뒤,
페이지별로 추출된 텍스트를 manual_index.json이라는 파일에 저장했다.
구조를 단순하게 표현하면 다음과 같다.
Basic Guide
PDF page 1 → OCR text
PDF page 2 → OCR text
...
Reference Guide
PDF page 1 → OCR text
PDF page 2 → OCR text
...
OCR 결과는 물론 완벽하지 않았다.
오래된 영문 매뉴얼을 읽히다 보니 170을 17O로 읽거나
171이 전혀 엉뚱한 문자로 변하는 일도 있었다.
그래도 내가 원하는 것은 완벽한 전자책을 만드는 것이 아니라 관련 페이지를 찾는 것이므로,
Drum Kit, Pattern, MIDI Data Dump,
Program Change 같은 기술 용어가 대체로 제대로 인식되는 정도면 충분했다.
RAG라면 임베딩부터 해야 하는 줄 알았다
RAG라는 말을 처음 접하면 문서를 잘게 나누고 임베딩(IBM의 짧은 설명)을 만들어 벡터 검색을 해야 할 것 같은 생각부터 든다.
나도 그렇게 시작했다.
paraphrase-multilingual-MiniLM-L12-v2를 사용하여
288페이지의 매뉴얼을 384차원의 벡터로 만들고
manual_embeddings.npy라는 파일로 저장하였다.
한국어로 질문해도 의미가 비슷한 영문 매뉴얼 페이지를 찾아줄 것이라고 기대했다.
여기서 ‘문서를 잘게 나눈다’는 것은 긴 문서 전체를 한꺼번에 검색 대상으로 삼지 않고, 몇 문단이나 한 페이지 정도의 작은 단위로 쪼개는 것을 뜻한다. 그리고 ‘임베딩(embedding)’은 각각의 작은 문서 조각을 그 내용의 특징을 나타내는 여러 개의 숫자로 바꾸는 과정이다. 이렇게 만들어진 숫자의 묶음을 벡터(vector)라고 한다. 질문도 같은 방법으로 벡터로 바꾼 뒤, 질문 벡터와 가까운 문서 벡터를 찾으면 단어가 정확히 일치하지 않아도 의미가 비슷한 내용을 찾을 수 있다. 이것이 여기서 말하는 벡터 검색이다.
그런데 실제로 시험해 보니 이 매뉴얼에서는 생각만큼 만족스럽지 않았다.
내가 찾고 싶은 것은 문장의 추상적인 의미가 비슷한 페이지라기보다
Drum Kit Setup, Put to Track,
Program Change, MIDI Data Dump처럼
매뉴얼에 실제로 쓰인 기술 용어가 등장하는 곳이었다.
그렇다면 굳이 모든 페이지를 벡터로 바꾸어 의미상의 거리를 계산하기보다,
한국어 질문을 매뉴얼에서 사용될 법한 영어 검색어로 잘 바꾼 다음 전통적인 문자열 검색을 하는 편이 나을 수도 있었다.
이것은 이번 작업에서 처음으로 목표를 크게 줄인 지점이었다. 임베딩 검색이 나쁜 기술이라는 뜻은 아니다. 내가 가진 자료의 양과 성격, 그리고 찾으려는 정보의 종류에는 더 단순한 방법이 잘 맞았다는 뜻이다.
작은 로컬 LLM에게 너무 많은 일을 시켰다
로컬 LLM은 Ollama를 통해 실행했다. 처음에는 Qwen 모델에게 질문을 영어 검색어로 바꾸는 일뿐 아니라 검색된 매뉴얼 내용을 읽고, 적절한 근거인지 판단하고, 마지막에는 한국어 답변까지 작성하도록 했다. 관련 없는 질문을 걸러 내는 relevance gate도 붙여 보고, 한 번의 호출로 잘 안 되면 두 번, 세 번에 나누어 일을 시키는 방법도 시험했다.
Qwen은 중국의 Alibaba Cloud가 개발하는 AI 언어 모델 계열이고, Ollama는 미국의 Ollama가 개발하는 로컬 AI 모델 실행 소프트웨어다. Qwen 자체는 일반 응용 프로그램이라기보다 문장을 이해하고 생성하도록 학습된 AI 모델이며, Ollama는 Qwen을 비롯한 여러 언어 모델을 내 컴퓨터에 내려받아 쉽게 실행하고 다른 프로그램에서 호출할 수 있게 해 준다. 이번 작업에서는 Ollama를 설치하고, 이를 통해 비교적 작은 Qwen 3 계열의 1.7B 모델(
qwen3:1.7b)을 내 노트북에서 실행하였다.
문제는 내 노트북에 별도의 GPU가 없다는 것이다.
Qwen 1.7B 정도의 작은 모델은 CPU만으로도 비교적 부담 없이 실행되었지만,
매뉴얼을 정확하게 해석하여 답을 만드는 일에서는 신뢰하기 어려운 경우가 있었다.
원문에는 분명히 Global Mode라고 적혀 있는데
답을 만드는 과정에서 다른 모드로 바꾸어 말하는 식의 오류가 생겼다.
더 큰 4B급 모델도 시험했지만 이번에는 기다리는 시간이 현실적인 문제가 되었다.
좋은 답 하나를 얻기 위해 매번 오래 기다려야 한다면 매뉴얼을 직접 펼쳐 보는 것보다 나을 것이 없다.
그렇다고 작은 모델이 쓸모없는 것은 아니었다.
오히려 일을 하나만 시키니 제법 잘했다.
“답하지 말고, 한국어 질문을 이 영문 매뉴얼에서 검색하기 좋은 영어 단어로 바꾸어라”라고 역할을 제한한 것이다.
예를 들어 “드럼 키트를 편집하려면?”이라는 질문을 넣으면
drum kit edit setup global mode와 같은 검색어를 만들어 주고,
“패턴을 트랙에 넣으려면?”이라고 하면
pattern put to track assign pattern track과 같은 결과를 내놓았다.
이 정도의 일은 1.7B 모델도 빠르고 제법 안정적으로 해냈다.
결국 검색 알고리즘은 아주 단순해졌다
LLM이 영어 검색어를 만들어 주고 나면 그다음부터는 AI라고 부를 만한 것도 별로 없다. OCR한 각 페이지에서 검색어가 얼마나 많이 등장하는지를 세고 점수를 주었다. 검색어 전체가 그대로 등장하면 추가 점수를 주는 정도의 단순한 방식이다.
핵심 아이디어를 단순화하면 다음과 같다.
def score_page(text, query):
text = text.lower()
query = query.lower()
score = 0
if query in text:
score += 100
for term in query.split():
score += text.count(term) * 5
return score
이렇게 해도 의외로 결과가 괜찮았다.
예를 들어 GM Song을 재생하는 방법을 묻는 질문에서 LLM이
gm song play라는 검색어를 만들면 Basic Guide의
Playing GM Songs가 있는 페이지가 상위에 나온다.
드럼 키트 편집을 물으면 Reference Guide의 Global Mode에 있는
Drum Kit Setup 페이지가 잘 검색된다.
기술 매뉴얼에서는 용어 자체가 상당한 정보를 가지고 있기 때문에 가능한 일일 것이다.
AI에게 답을 쓰게 하지 말고 원본 페이지를 보여 주자
여기에서 다시 한 번 방향을 바꾸었다. 검색된 OCR 텍스트를 작은 LLM에게 넘겨 답을 작성하게 하는 대신, 검색된 원본 PDF 페이지를 그대로 브라우저에 보여 주기로 했다. OCR은 어디까지나 검색용으로만 사용하고, 사람이 실제로 읽는 것은 스캔된 원래 매뉴얼이다.
이렇게 하니 여러 문제가 한꺼번에 사라졌다. OCR이 일부 문자를 잘못 인식하더라도 검색만 성공하면 원본 페이지에서 정확한 내용을 확인할 수 있다. 작은 LLM이 근거 문장을 잘못 해석하거나 그럴듯한 설명을 덧붙이는 문제도 없다. 무엇보다 내가 원하는 것은 Korg X2에 대해 유창하게 설명하는 AI가 아니라, 200쪽이 넘는 Reference Guide에서 필요한 페이지를 빨리 찾는 도구라는 사실이 분명해졌다.
결국 현재 프로그램의 처리 과정은 다음과 같이 단순해졌다.
한국어 질문
↓
Qwen 1.7B
↓
영문 매뉴얼용 검색어
↓
OCR 텍스트 검색
↓
관련 페이지 선정
↓
원본 PDF 페이지 표시
전형적인 RAG라면 마지막 단계에서 검색된 문서를 LLM에 다시 넣고 답변을 생성할 것이다. 내 프로그램은 그 단계를 아예 없애 버렸다. 따라서 이제 이것을 RAG 챗봇이라고 부르는 것은 적절하지 않다. “자연어로 매뉴얼 검색” 정도가 지금 하는 일을 가장 정확하게 표현한다.
Streamlit으로 검색 화면 만들기
사용자 인터페이스는 Streamlit으로 만들었다. 프로그램을 실행하는 방법은 간단하다.
streamlit run app.py
브라우저에는 Search, Basic Guide, Reference Guide의 세 탭이 나타난다. Search 탭에서 한국어나 영어로 궁금한 내용을 입력하면 관련도가 높은 Basic Guide와 Reference Guide 페이지를 각각 찾아 준다. 검색 결과 가운데 하나를 선택하면 바로 아래에 원본 PDF 페이지가 표시되고, 필요하면 앞뒤 페이지도 함께 볼 수 있게 하였다.
Basic Guide와 Reference Guide 탭은 전자 목차처럼 만들었다.
원래 매뉴얼의 Table of Contents를 OCR한 뒤 오류가 난 페이지 번호를 확인하여
별도의 toc.json으로 정리했다.
Chapter와 Section을 선택하면 해당 원본 페이지로 바로 이동한다.
오래된 종이 매뉴얼을 스캔한 PDF가 이제는 검색과 목차 이동이 가능한 작은 웹 애플리케이션이 된 셈이다.
시행착오가 사라지고 남은 파일들
개발 중에는 여러 방법을 시험하느라
ask_manual_2call.py, ask_manual_3call_gate.py,
ask_manual_grounded.py, search_embedding.py,
build_embeddings.py 같은 파일이 계속 생겨났다.
이름만 보아도 작은 LLM에게 어떻게든 정확한 답을 만들어 내게 하려고 애쓴 흔적이 남아 있었다.
최종 방향을 정한 뒤에는 이런 실험용 파일과 임베딩 데이터도 모두 지웠다.
현재 남아 있는 것은 다음 정도다.
x2-assistant/
│
├─ .venv/
├─ manuals/
│ ├─ basic.pdf
│ └─ reference.pdf
│
├─ app.py
├─ build_index.py
├─ manual_index.json
└─ toc.json
app.py가 Streamlit 애플리케이션이고,
manual_index.json에는 검색용 OCR 텍스트가 들어 있다.
toc.json은 두 매뉴얼의 목차 이동에 사용하며,
build_index.py는 나중에 PDF에서 OCR 인덱스를 다시 만들어야 할 경우를 대비해 남겨 두었다.
처음 생각했던 시스템에 비하면 놀랄 만큼 단출하다.
API, 로컬 LLM, RAG를 직접 만져 보면서
이번 일을 시작하기 전에는 API를 이용하는 AI와 내 컴퓨터에서 직접 돌리는 LLM의 차이, RAG에서 retrieval과 generation이 각각 무슨 역할을 하는지, 임베딩 검색이 왜 필요한지 같은 개념을 막연하게만 알고 있었다. 직접 만들어 보니 이런 용어들이 조금씩 구체적으로 느껴졌다. 특히 LLM을 사용한다고 해서 모든 단계를 LLM에게 맡길 필요는 없다는 점이 인상적이었다.
성능 좋은 외부 AI의 API를 사용했다면 검색된 몇 페이지를 넘겨서 꽤 훌륭한 답을 만들 수도 있었을 것이다. 그러나 이번에는 별도의 API 비용 없이 내 노트북 안에서 해결하는 것이 조건이었다. 그렇다면 CPU에서 작은 모델이 잘할 수 있는 일과 기존 프로그램이 훨씬 잘할 수 있는 일을 나누는 것이 합리적이다. 한국어 질문을 영어 기술 용어로 바꾸는 것은 작은 LLM에게 맡기고, 단어를 세어 페이지를 찾는 일은 평범한 Python 코드에 맡기며, 최종 판단은 원본 문서를 읽는 사람이 하도록 했다.
거창하게 시작했지만 이 정도가 오히려 쓸 만하다
처음에는 “내 노트북에서 돌아가는 Korg X2 전용 RAG 챗봇”을 생각했다. 임베딩을 만들고, 관련 문장을 추출하고, relevance gate를 붙이고, LLM을 여러 번 호출하면서 어떻게든 그럴듯한 답을 만들어 보려고 했다. 그러나 CPU만 사용하는 노트북에서 큰 모델을 돌리는 데에는 한계가 있었고, 작은 모델에게 복잡한 판단까지 맡기면 정확도가 마음에 들지 않았다. 결국 많은 기능을 하나씩 걷어 내고 작은 LLM, 단순한 문자열 검색, OCR 인덱스와 PDF 뷰어만 남겼다.
결과적으로 목표는 상당히 작아졌지만 실제 용도에는 오히려 더 잘 맞는다. 내가 필요한 것은 30년 된 신시사이저에 대해 AI와 대화를 나누는 것이 아니라, “Put to Track이 어디에 있었지?”라고 물었을 때 Reference Guide의 해당 페이지를 바로 펼쳐 주는 도구이기 때문이다. 제한된 컴퓨터 자원 때문에 타협했다고 생각할 수도 있지만, 며칠 동안 이것저것 만들어 보고 지우면서 느낀 것은 조금 다르다. AI를 얼마나 많이 사용하는가보다, AI에게 무슨 일을 맡기고 무슨 일은 맡기지 않을지를 정하는 것이 더 중요했다.
| '자연어로 매뉴얼 찾기'는 약간 과장이다. |
이번 작업은 거창한 AI 개발 프로젝트라기보다 AI를 직접 만져 본 나의 첫 작은 실험에 가까웠다. 그동안 AI를 이용하거나 관련 이야기를 접할 기회는 많았지만, 로컬 LLM을 직접 설치하고 임베딩을 만들어 검색해 보고, 작은 모델에게 답변을 시키면서 그 한계를 확인해 본 것은 이번이 처음이었다. 그런데 그 첫 경험이 업무에서가 아니라, 30년 된 신시사이저의 매뉴얼을 좀 더 편하게 찾아보겠다는 취미 프로젝트에서 이루어졌다는 점도 재미있다. 모든 일을 AI에게 맡기기보다 AI가 잘하는 작은 역할만 맡기고 나머지는 기존의 단순한 프로그램으로 처리하는 편이 더 나을 때가 있다는 것도 직접 확인했다. RAG나 임베딩, 로컬 LLM 같은 용어 역시 글로 읽을 때보다 직접 만들고 실패해 보니 훨씬 구체적으로 이해할 수 있었다.
이제 X2를 Ardule에서 어떻게 써먹을까?
이렇게 매뉴얼을 다시 들여다보고 SysEx와 MIDI 구조까지 조금씩 이해하게 된 이유는 결국 하나다. 30년이나 된 Korg X2를 지금 내가 만들고 있는 Ardule 생태계에서 어떻게 활용할 수 있을까? X2의 시퀀서나 패턴 기능을 오늘날의 소프트웨어로 그대로 흉내 낼 필요는 없을 것이다. 그런 일은 Raspberry Pi와 소프트웨어가 훨씬 자유롭게 할 수 있다. 대신 X2에는 지금도 쓸 만한 고유한 음색과 드럼 키트, Program과 Combination이 있고, MIDI와 SysEx를 이용하면 외부에서 이들을 꽤 세밀하게 제어할 수 있다.
따라서 Ardule에서는 FluidSynth와 SoundFont를 주된 음원으로 사용하면서 X2를 선택적으로 불러 쓰는 외부 하드웨어 음원으로 두는 것이 재미있을 것 같다. Ardule이 곡이나 연주 상황에 따라 MIDI 채널을 배분하고 Program이나 Combination을 선택하며, 필요한 경우 미리 준비한 SysEx를 보내 X2의 상태를 설정하는 식이다. 오래된 X2의 패턴을 분석하여 유용한 리듬은 Ardule 쪽의 패턴 라이브러리로 가져올 수도 있다. 그렇게 하면 X2의 낡은 시퀀서를 억지로 중심에 놓지 않으면서도 그 안에 들어 있는 소리와 데이터는 계속 활용할 수 있다.
어쩌면 이번 매뉴얼 검색기 만들기도 그 과정의 일부였던 셈이다. 1990년대의 신시사이저와 Raspberry Pi, Arduino, SoundFont, 그리고 이제는 로컬 LLM까지 한데 섞이기 시작했다. 서로 만들어진 시대도 다르고 원래 함께 쓰라고 만들어진 물건들도 아니지만, 필요한 부분만 골라 연결하는 것이 내가 생각하는 Ardule의 재미다. X2가 언제까지 정상적으로 작동할지는 모르겠지만, 적어도 당분간은 창고로 들어갈 이유가 하나 더 줄었다.
다음은 X2 하판을 고정하는 screw boss를 보강한 어제의 작업 결과를 사진으로 남긴 것이다.
분해 조립을 자주 하면서 플라스틱으로 만들어진 나사산도 많이 뭉개지고, 몇 개의 보스는 갈라져 버렸다. 여기에 알리익스프레스에서 구입한 내경 8mm 스틸 무나사 스페이서를 끼워서 더 이상 갈라지지 않게 보강한 것이 주요 내용이다. 내경 9mm짜리를 구입했더라면 보스의 바깥을 갈아내느라 고생을 하지 않아도 되었을 것이다. 들어가다 만 것을 빼느라 플라이어로 이 금속 부품을 꽉 잡고 좌우로 돌려서 비틀었더니 보스 하나가 부러지는 사고를 치는 바람에 응급 수술도 하였다.
아직 뭉개진 나사산에 대한 보수 작업은 하지 않은 상태이다. 망가진 X2의 플로피 드라이브를 대신할 에뮬레이터를 구입할지도 아직 결정하지 못하였다. 이것이 있다면 USB 메모리를 이용하여 설정 상태를 백업하고 복원하는 일이 매우 편해진다. 모든 것을 SysEx 전송으로 해결할 수도 있지만 MIDI 자체의 전송 속도가 느리기 때문이다. 그나마 ChatGPT의 도움을 받아 KORG X2/X3의 SysEx 데이터와 SNG 파일 구조를 분석하는 리버스 엔지니어링 작업을 훨씬 수월하게 할 수 있다는 것이 다행이다.
