tunaLlama
Claude Code 의 메인 세션(아키텍트)이 무거운 코드 생성을 로컬 LLM(Ollama / LM Studio)에 위임하고, 분해와 검증만 유료 모델에 남겨두기 위한 백엔드 + 플러그인.
버전: v0.1.0 출시 · v0.2.0 대기 (Phase 2 통합 완료, Phase 3 진행 중)
라이선스: MIT.
English: README.en.md.
1. 무엇이고 왜 만들었나
Claude Code 로 코딩하다 보면 출력이 긴 단계 - 코드 생성, 파일 리뷰, 리팩터 - 가 토큰을 가장 많이 먹는다. 그런데 이 단계는 보통 결정적이고 모델 품질의 차이가 작다. 반대로 분해(요구사항 → 작업 목록)와 검증(돌려받은 결과가 요구사항을 만족하는지)은 짧은 입출력이지만 모델 품질 차이가 크다.
tunaLlama 는 이 비대칭을 그대로 코드 흐름으로 굳혀 둔다. 도메인 패턴은 OllamaClaude (Jadael/OllamaClaude) 와 같지만, Python 으로 처음부터 다시 짰고 한국어 검색·문서 기반 워크플로우·약점 카탈로그가 추가됐다. 코드 복사는 없다.
| 역할 | 모델 | 책임 |
|---|
| Architect | Claude Code (유료) | 분해 / 사양 작성 / 검증 / 통합 |
| Developer | 로컬 LLM (Ollama / Cloud / LM Studio) | 코드 생성 / 자체 리뷰 / 자체 수정 |
| Reviewer | Claude Code (유료, 같은 세션) | 최종 판정 |
토큰 헤비 단계만 로컬로 빠지고, 짧은 분해·검증 단계는 그대로 Claude 에 남는다.
2. 동작 원리
전형적인 호출 흐름:
- 사용자가 한국어/영어로 task 를 말함.
- Claude(아키텍트)가 task 를 분해. 짧으면
tuna_dev_review(requirements, language) 한 번 호출, 길면 markdown spec 문서를 docs/specs/<name>.md 에 작성한 뒤 tuna_dev_review_from_spec(path) 호출.
- backend 가 generate → review → (이슈 있으면) fix → 다시 review 를
max_iterations 까지 자동 반복. 모든 호출은 SQLite 에 기록되고 한국어 형태소로 색인된다.
- backend 가 최종 코드 + iteration 로그를 반환.
- Claude 가 그 결과를 읽고 자체 검증. 의심스러우면 사용자에게 조각 단위로 보여주거나
tuna_log_limitation() 으로 약점을 카탈로그에 추가한다 (다음 호출의 prompt 앞에 자동 prepend).
핵심은 backend 가 도구 호출을 메모리 + recall 까지 한 호출에서 모두 처리 한다는 점이다. Claude 가 file 내용·중간 코드·review 텍스트를 자기 컨텍스트로 끌어들이지 않아도 된다.
3. 아키텍처
tunallama_core/ # 백엔드 - 재사용 가능, MCP-agnostic
config/ # TOML 로드 + 검증 + frozen dataclass
llm/ # Provider 추상화 (ollama / lmstudio / factory)
memory/ # SQLite + FTS5 + Kiwi (BM25) + BGE-M3 (벡터) + RRF + graph
delegation/ # 10 도구 + 공통 runner + 시스템 프롬프트
workflow/ # dev_review_loop / spec / limitations
routing.py # auto_recall 정책
errors.py # 도메인 예외
cli/ # tunallama init / doctor
plugin/ # Claude Code 플러그인 - backend 소비
.claude-plugin/plugin.json
.mcp.json
mcp_server.py # FastMCP 서버, 14 tuna_* 도구 노출
_state.py # lazy 싱글톤 + .env 자동 로드
_format.py # recall 결과 직렬화
hooks/pre_tool_use.py # 큰 파일 Read 시 권유 (off by default)
skills/delegate-to-ollama/SKILL.md
agents/tuna-developer.md
tests/
core/ # 단위 + 통합 (실 Ollama Cloud / LM Studio)
plugin/ # MCP 도구 + 매니페스트 + state + hook
불변 규칙: tunallama_core 는 plugin 을 절대 import 하지 않는다. Phase 4 에서 Codex 프론트엔드를 추가할 때 backend 를 그대로 가져다 쓸 수 있게 하려는 것이다.
4. 메모리와 검색
모든 delegation 호출은 SQLite 에 한 줄씩 적재된다.
CREATE TABLE calls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
tool_name TEXT NOT NULL,
inputs_json TEXT NOT NULL,
output TEXT NOT NULL,
model TEXT NOT NULL,
duration_ms INTEGER NOT NULL,
tokens_estimated INTEGER,
project_root TEXT,
session_id TEXT,
tags TEXT NOT NULL DEFAULT '[]'
);
CREATE VIRTUAL TABLE calls_fts USING fts5(
inputs_text, output_text,
tokenize='unicode61 remove_diacritics 2'
);
FTS5 의 unicode61 토크나이저는 한국어를 음절/자모로만 자르기 때문에 한국어 검색 리콜이 나쁘다. 그래서 write 시점에 Python 에서 Kiwi 로 형태소 분리 한 결과를 원문과 함께 색인한다. "이메일검증" 처럼 띄어쓰기 없는 입력에 대해 "이메일" 로 검색해도 매칭된다.
# tunallama_core/memory/tokenize.py
_KEEP_TAGS = {"NNG", "NNP", "NNB", "VV", "VA", "MAG", "MAJ", "SL"}
def kiwi_morphemes(text: str) -> str:
tokens = _get_kiwi().tokenize(text)
morph = " ".join(t.form for t in tokens if t.tag in _KEEP_TAGS)
return f"{morph} {text}".strip()
NNB(의존명사)는 seCall 프로젝트의 토크나이저 패턴을 참고해 추가했다. 트리거를 두지 않고 application 레이어에서 calls 와 calls_fts 에 명시적으로 INSERT 한다 - 한국어 사전 토큰화가 트리거 안에 들어가지 않기 때문에, 이중 INSERT 가 더 단순하고 디버깅 가능하다.
리콜은 tuna_recall(query, limit) 으로 호출. 응답은 항상 요약 + 발췌 형식이라 컨텍스트를 폭발시키지 않는다.
4.1. 의미 기반 검색 - 벡터 임베딩 (Phase 2)
record_call 시점에 BAAI/bge-m3 (1024-dim) 임베딩을 자동 계산해 calls.embedding BLOB 에 저장한다. 모델 로드는 lazy + thread-locked. 임베딩 파이프라인이 실패해도(모델 미설치, OOM 등) record 자체는 BM25 만으로 정상 저장 - 옵션 기능이므로 BM25 path 는 영향 없음.
# tunallama_core/memory/vector.py
EMBEDDING_MODEL = "BAAI/bge-m3"
EMBEDDING_DIM = 1024
def embed(text: str) -> np.ndarray:
model = _get_model()
return model.encode(text, convert_to_numpy=True, normalize_embeddings=True).astype(np.float32)
MemoryStore.search_vectors(query, limit, project_root) 는 cosine 유사도 brute-force (numpy dot - 1만 record 까지는 충분). NULL/corrupt blob 자동 skip.
4.2. 하이브리드 검색 - RRF (Phase 2)
recall_hybrid(store, query, limit, k=60) 가 BM25 + 벡터 결과를 Reciprocal Rank Fusion 으로 병합. 각 결과 list 의 1-based rank 로 score = 1/(k+rank) 부여하고 같은 record id 가 양쪽에 잡히면 score 합산. 벡터 결과 비어도 BM25 만으로 정상 동작 - 옛 db / 모델 미가용 환경 호환.
4.3. 검색 품질 (실측, 2026-05-10)
두 시드로 정량 측정: