본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 24. 00:28

150ms 미만의 FIM Autocomplete: Laptop에서 실행되는 Tree-sitter, nomic Embeddings, 그리고

요약

오픈 소스 AI 코딩 IDE인 Dhi의 FIM(Fill-In-the-Middle) 자동 완성 엔진 구축 과정을 설명합니다. Tree-sitter, nomic Embeddings, Ollama 등을 활용하여 로컬 환경에서 150ms 미만의 저지연 자동 완성을 구현하는 아키텍처를 다룹니다.

핵심 포인트

  • FIM(Fill-In-the-Middle) 방식을 통해 접두사와 접미사를 모두 고려한 정확한 코드 생성 구현
  • Tree-sitter 기반 semantic chunker와 Chroma 벡터 스토어를 활용한 컨텍스트 구성
  • Ollama와 StarCoder2-3B를 사용하여 로컬 노트북 환경에서 추론 서버 실행
  • VS Code 확장 프로그램을 통한 인라인 ghost-text 완성 기능 지원

참고: 이 포스트에는 아키텍처 다이어그램이 포함되어 있습니다. 완전히 렌더링된 버전을 보려면 원본 포스트를 방문하세요.

이전 포스트에서 저는 오픈 소스 AI 코딩 IDE의 전체 6계층 아키텍처를 설명했습니다. 오늘은 그것을 구축하기 시작합니다.

이 포스트는 Dhi (धी)의 첫 번째 실제 구성 요소를 제공합니다 — Dhi는 전적으로 오픈 소스 모델로 구축된 오픈 소스 AI 코딩 IDE입니다. _Dhi_는 산스크리트어로 Gayatri Mantra에서 유래한 '순수한 지성'을 의미합니다. 이 이름은 목적에 부합합니다: 목표는 API 키, 토큰 가격 책정, 그리고 폐쇄형 소스 추론 백엔드 없이 코드베이스에 대해 진정한 지능을 제공하는 IDE를 만드는 것입니다.

이 포스트를 마칠 때 여러분은 다음을 갖게 됩니다:

  • 노트북에서 실행되는 작동 가능한 FIM autocomplete 엔진
  • Python 및 TypeScript를 위한 Tree-sitter 기반의 semantic chunker
  • nomic-embed-text-v1.5 임베딩(embeddings)을 사용하는 Chroma 벡터 스토어
  • Ollama를 통한 StarCoder2-3B 추론(inference) 서버
  • ghost-text 인라인 완성을 지원하는 VS Code 확장 프로그램

모든 것은 docker compose up으로 실행됩니다. 전체 코드는 github.com/sochaty/dhi에 있으며, post-1 태그를 확인하세요.

FIM이란 실제로 무엇인가

대부분의 개발자들은 autocomplete를 _다음 토큰 예측 (next-token prediction)_으로 생각합니다: 모델이 커서 이전의 모든 것을 보고 다음에 올 것을 예측하는 방식입니다. 그것이 GPT-2가 작동하는 방식입니다. 하지만 그것은 현대적인 AI 코딩 어시스턴트가 작동하는 방식이 아닙니다.

2026년의 진정한 autocomplete는 **fill-in-the-middle (FIM)**입니다: 모델이 접두사(prefix, 커서 이전의 모든 것)와 접미사(suffix, 커서 이후의 모든 것)를 모두 보고, 그 둘을 연결하는 완성된 코드를 생성합니다. 이는 모델이 코드가 어디서 시작되었는지만이 아니라, 코드가 최종적으로 도달해야 할 지점을 알고 있기 때문에 훨씬 더 정확합니다.

FIM을 작동하게 만드는 세 가지 특수 토큰(special tokens):

토큰 (Token)포함 내용 (Contains)방향 (Direction)
<fim_prefix>커서 이전의 모든 내용→ 모델로 입력됨
...

StarCoder2, DeepSeek-Coder, 그리고 Qwen2.5-Coder는 모두 FIM을 네이티브로 지원합니다. 이는 프로덕션용 자동 완성 엔진(autocomplete engine)을 구축할 때 타협할 수 없는 필수 사항입니다. 네이티브 FIM 지원이 없는 모델은 인라인 완성(inline completions) 성능이 눈에 띄게 떨어집니다.

Dhi의 자동 완성 엔진을 위한 전체 요청 흐름(request flow):

VS Code Editor (커서 위치 이벤트)
    ↓ 150ms 디바운스 (debounce)
Context Assembler
...

여기서 핵심적인 설계 결정은 다음과 같습니다: 접두사(prefix)는 단순히 현재 파일만을 의미하지 않습니다. 여기에는 **저장소(repository)의 나머지 부분에서 검색된 청크(retrieved chunks)**가 포함됩니다. 이러한 컨텍스트가 없다면, 모델은 다른 파일에 정의된 헬퍼(helper)를 사용하는 함수 호출을 완성할 수 없습니다.

Dhi 부트스트래핑 (Bootstrapping Dhi)

저장소를 클론하고 CPU 스택을 시작하세요:

git clone https://github.com/sochaty/dhi
cd dhi
git checkout post-1
...

이 태그의 docker-compose.yml은 세 가지 서비스를 실행합니다:

services:
  server:
    build: ./server
...

처음 시작할 때, Ollama가 StarCoder2-3B(~1.7GB)를 가져옵니다(pull). 이후 시작 시에는 즉시 실행됩니다. post-1 태그에서의 전체 저장소 디렉토리 구조는 다음과 같습니다:

dhi/
├── docker-compose.yml
├── extension/
...

레이어 1: Tree-sitter 의미론적 청킹 (Semantic Chunking)

벡터 저장소(vector store)를 위해 코드를 청킹하는 단순한(naive) 방식은 글자 수 기준으로 나누는 것입니다. 즉, 매 500자마다 하나의 청크를 만드는 방식입니다. 이는 두 가지 이유로 잘못된 방식입니다: 함수 본문 중간을 잘라버려 (의미론적 의미를 파괴하고) 관련 없는 코드들을 하나로 묶어버리기 때문입니다 (검색 결과의 오염 유발).

Dhi는 Tree-sitter를 사용하여 의미론적 경계(semantic boundaries)를 기준으로 분할합니다. 함수 정의는 하나의 청크가 됩니다. 임포트 블록(import block)은 하나의 청크가 됩니다. 클래스 본문(class body)은 하나의 청크가 됩니다. 각 청크는 독립적이며 문맥적으로 일관성을 유지합니다.

소스 파일 (.py / .ts)
    ↓
Tree-sitter 파서 (Parser)  →  구문 트리 (Syntax Tree)
...

다음은 Python과 TypeScript에 대한 전체 구현이 담긴 server/rag/chunker.py입니다:

from dataclasses import dataclass
from pathlib import Path
from typing import Generator
...

주의 깊게 살펴볼 두 가지 사항이 있습니다:

탐욕적 최상위 청킹 (Greedy top-level chunking). walk 함수는 노드를 생성(yield)하고 해당 노드 내부로의 재귀를 중단합니다. 클래스 본문(class body)은 하나의 청크(chunk)가 되며, 개별 메서드들을 별도의 청크로 생성하지 않습니다. 이는 검색 단위(retrieval units)가 일관성을 유지할 수 있을 만큼 충분히 크게 유지되도록 합니다.

언어별 문법 패키지 (Language-specific grammar packages). tree_sitter_pythontree_sitter_typescript는 사전 컴파일된 문법(grammars)을 제공하는 PyPI 패키지입니다. C 컴파일 단계가 필요하지 않으며, 이는 Docker 이미지 구축 시 중요한 요소입니다.

레이어 2: 임베딩 및 벡터 저장 (Embedding and Vector Storage)

각 청크는 nomic-embed-text-v1.5를 통해 임베딩됩니다. 이 모델은 Ollama를 통해 로컬에서 실행되는 768차원 모델입니다. 이 모델은 코드 검색 벤치마크에서 OpenAI의 ada-002보다 뛰어난 성능을 보이면서도 쿼리당 비용이 전혀 들지 않습니다.

다음은 server/rag/store.py입니다:

import hashlib
import os
from typing import Sequence
...

upsert 메서드는 멱등성(idempotent)을 가집니다. 즉, 파일을 다시 인덱싱하면 청크가 중복되는 대신 기존 청크가 교체됩니다. 청크 ID는 file_path:start_line:end_line의 결정론적 해시(deterministic hash)이므로, 동일한 청크는 항상 동일한 Chroma 문서로 매핑됩니다.

레이어 3: FIM 프롬프트 조립 (Assembling the FIM Prompt)

FIM 프롬프트는 정밀한 구조를 가집니다. 접두사(prefix) 슬롯은 두 개의 하위 부분으로 구성됩니다. 먼저 저장소(repo)의 나머지 부분에서 검색된 컨텍스트(retrieved context)가 오고, 그 뒤를 이어 커서(cursor)까지의 현재 파일 내용이 옵니다. 접미사(suffix)는 커서 이후의 모든 내용입니다.

<fim_prefix>
  # Repo context        ← 상위 3개 검색된 청크 (~1500 토큰)
  # Current file        ← 0번 라인 → 커서 (~800 토큰)
...

다음은 server/inference/fim.py입니다:

import os
from dataclasses import dataclass

...

설명이 필요한 두 가지 구현 선택 사항입니다:

낮은 온도 (Low temperature, 0.1). 자동 완성(Autocomplete)은 창의적인 작업이 아닙니다. 다양한 샘플이 아니라 가장 확률이 높은 연속된 내용을 원하기 때문입니다. 높은 온도(High temperature)는 환각(hallucination)된 변수 이름이나 잘못된 함수 시그니처(function signatures)를 생성합니다.

중지 토큰 (Stop tokens)에는 \n\n이 포함됩니다. 단일 빈 줄은 완성 (completion)의 자연스러운 종료 지점입니다. 이 중지 토큰이 없으면 모델은 max_new_tokens에 도달할 때까지 계속해서 생성하며, 이는 지연 시간 (latency)을 낭비하고 과도한 생성 (over-completion)을 초래합니다.

모델 선택 (Model Selection)

모든 개발자가 동일한 하드웨어를 보유하고 있지는 않습니다. 계층별 권장 모델은 다음과 같습니다:

모델 (Model)크기 (Size)VRAMFIM 지원권장 대상
StarCoder2-3B3B4GB✅ 네이티브 (Native)8GB GPU 또는 Apple M-series
...

.env 파일에서 모델을 변경하세요:

FIM_MODEL=qwen2.5-coder:7b

Ollama는 다음 컨테이너 시작 시 모델을 가져옵니다. 코드 변경은 필요하지 않습니다. 모델 제품군(model families)마다 FIM 특수 토큰 (special tokens)이 다르지만, Ollama가 토큰화 (tokenization)를 자동으로 처리합니다.

레이어 4: VS Code 확장 프로그램 (The VS Code Extension)

확장 프로그램은 InlineCompletionItemProvider를 등록합니다. VS Code는 사용자가 타이핑을 멈출 때마다 이를 호출합니다. 디바운스 (debounce)는 모든 키 입력마다 네트워크 왕복 (network round-trip)이 발생하는 것을 방지합니다.

키 입력 (Keystroke) → 디바운스 (Debounce) 150ms → 컨텍스트 추출 (Extract context)
    → POST /complete {file_path, prefix, suffix, language}
    → Dhi 서버 (FIM 프롬프트 → Ollama → 완성 (completion))
...

extension/src/completion/provider.ts 파일의 내용은 다음과 같습니다:

import * as vscode from 'vscode';

const SERVER_URL = vscode.workspace
...

extension.ts에 등록하세요:

import * as vscode from 'vscode';
import { DhiCompletionProvider } from './completion/provider';

...

그리고 server/main.py에 있는 FastAPI 엔드포인트 (endpoint)입니다:

from fastapi import FastAPI
from pydantic import BaseModel

...

/index 엔드포인트는 확장 프로그램의 파일 감시자 (file-watcher)에 의해 파일이 저장될 때마다 호출됩니다. 이를 통해 전체 재색인 (re-index) 없이도 벡터 저장소 (vector store)를 편집 내용과 동기화 상태로 유지할 수 있습니다.

실제 지연 시간 (Latency in Practice)

Ollama를 통한 StarCoder2-3B를 사용하는 Apple M3 Pro (외부 GPU 없음) 환경에서의 결과입니다:

시나리오P50P95
Cold (Ollama 캐시 없음)380ms520ms
...

95ms의 Warm P50은 < 150ms 목표치 안에 충분히 들어옵니다. 컨텍스트 검색 (Context retrieval)은 약 15ms를 추가하지만, 저장소 인식 (repo-aware) 완성 기능이 제공하는 품질 향상에 비하면 작은 비용입니다.

지연 시간 (Latency)에 무엇보다 큰 영향을 미치는 세 가지 요소:

1. Max new tokens (최대 신규 토큰 수). 기본값은 64입니다. 단일 행 완성 (single-line completions)의 경우 32면 충분하며 생성 시간을 거의 절반으로 줄일 수 있습니다. 더 빠른 단일 행 제안을 원한다면 .env 파일에 FIM_MODEL_MAX_TOKENS=32를 설정하세요.

2. Prefix length (접두사 길이). 전송하기 전에 접두사를 약 800 토큰에서 자르세요 (Truncate). 접두사가 길어질수록 트랜스포머 (Transformer) 모델에서 프롬프트 처리 시간은 이차 함수적으로 증가합니다.

3. Cold start (콜드 스타트). docker compose up 이후 첫 번째 요청은 항상 느린데, 이는 Ollama가 모델을 메모리에 로드하기 때문입니다. M3 Pro에서는 약 3초가 소요됩니다. 이후의 요청은 Warm 모델 캐시를 사용합니다.

Debounce wait      ~80ms
Query embedding    ~8ms
Chroma query       ~4ms
...

저장소 인덱싱 (Indexing Your Repo)

확장 프로그램은 파일이 저장될 때마다 /index를 호출합니다. 첫 실행 시 기존 프로젝트를 인덱싱하려면 다음 명령어를 추가하세요:

// extension.ts
vscode.commands.registerCommand('dhi.indexWorkspace', async () => {
    const files = await vscode.workspace.findFiles(
...

Ctrl+Shift+P → Dhi: Index Workspace를 통해 한 번 실행하세요. 중간 규모의 TypeScript 프로젝트(약 200개 파일)의 경우 약 40초가 소요되며 약 1,

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0