Python으로 자체 호스팅 AI 코드 리뷰 도구 만들기

발행: (2026년 5월 23일 PM 06:21 GMT+9)
9 분 소요
원문: Dev.to

출처: Dev.to

모든 팀은 동일한 코드 리뷰 문제에 직면합니다. PR이 며칠씩 머물고, 리뷰어는 미묘한 논리 버그를 놓치며, 인증 레이어를 꼼꼼히 확인하지 않아 보안 문제가 빠져나갑니다. 린터는 문법 및 스타일 문제를 잡아내지만, 의도를 이해하지는 못합니다. 언어 모델은 이를 할 수 있으며, 소스 코드를 외부에 한 줄도 전송하지 않고 자체 인프라에서 완전히 실행할 수 있습니다.

이 가이드는 Python으로 자체 호스팅 AI 코드 리뷰 도구를 만드는 과정을 단계별로 안내합니다. Git diff를 읽고, 로컬에 호스팅된 언어 모델에 전달한 뒤, 구조화된 리뷰 코멘트를 반환하여 CI 워크플로에 바로 파이프할 수 있습니다.

소스 코드를 외부 API에 보내는 것은 중요한 신뢰 결정입니다. 독점 코드, 규제 산업, 혹은 보안에 민감한 경우 모델 추론이 자체 경계 안에서 이루어지길 원합니다. Ollama는 이를 깔끔하게 처리합니다. GGUF‑quantized 모델을 로컬에서 실행하고, OpenAI Python SDK와 완전히 호환되는 HTTP 엔드포인트를 제공합니다. 동일한 API 표면을 사용하면서 데이터 유출이 전혀 없습니다.

아키텍처는 의도적으로 단순합니다:

  • Python 스크립트가 git diff(또는 파일 경로)를 읽음
  • diff를 관리 가능한 청크로 분할
  • 각 청크를 구조화된 시스템 프롬프트와 함께 로컬 LLM에 전송
  • 모델이 JSON 형식의 리뷰 코멘트를 반환
  • 코멘트를 집계·표시하거나 CI 게이트에 전달

필요한 환경: Python 3.11+, openai SDK(호환 가능한 어떤 엔드포인트에도 작동), 로컬에서 실행 중인 Ollama와 코드 전용 모델. codellama:13b가 잘 동작하고, deepseek-coder:6.7b는 더 빠르면서 리뷰 작업에 거의 동일한 정확도를 제공합니다.

pip install openai gitpython
ollama pull deepseek-coder:6.7b

설정은 .env 파일에 저장합니다—스크립트가 환경 변수를 읽으므로 모델을 교체할 때 코드 수정이 필요 없습니다:

OLLAMA_BASE_URL=http://localhost:11434/v1
OLLAMA_API_KEY=ollama
OLLAMA_MODEL=deepseek-coder:6.7b

스크립트는 파일 인수 또는 stdin으로부터 diff를 읽어(이를 git hook에 연결하면 매우 간단합니다), 모델에 전달하고 구조화된 출력을 파싱합니다.

import os, json, sys
from openai import OpenAI

client = OpenAI(
    base_url=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1"),
    api_key=os.getenv("OLLAMA_API_KEY", "ollama"),
)
MODEL = os.getenv("OLLAMA_MODEL", "deepseek-coder:6.7b")

SYSTEM_PROMPT = (
    "You are a senior software engineer performing a code review.\n"
    "Analyze the provided code diff and return a JSON array of review comments.\n"
    "Each comment must have: severity (critical/warning/suggestion), "
    "line (int or null), message (str), fix (str or null).\n"
    "Return ONLY valid JSON. No prose outside the JSON array."
)

def review_diff(diff_text: str, max_chunk_chars: int = 6000) -> list[dict]:
    lines = diff_text.splitlines(keepends=True)
    chunks, current, current_len = [], [], 0
    for line in lines:
        if current_len + len(line) > max_chunk_chars and current:
            chunks.append("".join(current))
            current, current_len = [], 0
        current.append(line)
        current_len += len(line)
    if current:
        chunks.append("".join(current))

    all_comments = []
    for chunk in chunks:
        response = client.chat.completions.create(
            model=MODEL,
            messages=[
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": f"Review this diff:\n\n{chunk}"},
            ],
            temperature=0.1,
            max_tokens=1024,
        )
        raw = response.choices[0].message.content.strip()
        try:
            comments = json.loads(raw)
            if isinstance(comments, list):
                all_comments.extend(comments)
        except json.JSONDecodeError:
            pass
    return all_comments

if __name__ == "__main__":
    diff = open(sys.argv[1]).read() if len(sys.argv) > 1 else sys.stdin.read()
    comments = review_diff(diff)
    has_critical = False
    for c in sorted(comments, key=lambda x: ["critical","warning","suggestion"].index(x.get("severity","suggestion"))):
        print(f"[{c.get('severity','?').upper()}] line {c.get('line','?')}: {c.get('message','')}")
        if c.get("fix"):
            print(f"  → {c['fix']}\n")
        if c.get("severity") == "critical":
            has_critical = True
    sys.exit(1 if has_critical else 0)

스크립트는 critical 수준 이슈가 발견되면 종료 코드 1을 반환합니다. 따라서 차단형 pre‑push hook이나 CI 게이트로 사용하기가 매우 쉽습니다.

GitHub Actions 예시

name: AI Code Review
on: [pull_request]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install openai
      - name: Run AI review
        env:
          OLLAMA_BASE_URL: ${{ secrets.OLLAMA_BASE_URL }}
          OLLAMA_API_KEY: ${{ secrets.OLLAMA_API_KEY }}
          OLLAMA_MODEL: deepseek-coder:6.7b
        run: |
          git diff origin/${{ github.base_ref }}...HEAD > pr.diff
          python reviewer.py pr.diff

자체 호스팅 CI(Gitea Actions, GitLab CI, Jenkins 등)에서는 OLLAMA_BASE_URL을 내부 Ollama 인스턴스로 지정하면 됩니다. 러너가 해당 주소에 네트워크 접근할 수만 있으면 되며, 데이터는 절대로 경계를 벗어나지 않습니다. Ollama 노드가 프라이빗 서브넷에 있다면 프록시를 거치지 않고 해당 서브넷에 전용 러너를 배치하세요.

기본 프롬프트는 일반적인 코드 품질을 다룹니다. 보안에 초점을 맞춘 출력이 필요할 때(민감한 서비스의 사전 병합 게이트 등) 시스템 프롬프트를 다음과 같이 교체합니다:

SECURITY_PROMPT = (
    "You are a security-focused code reviewer.\n"
    "Flag only security vulnerabilities: injection flaws, auth bypasses, "
    "insecure deserialization, hardcoded credentials, missing input validation, "
    "race conditions, and OWASP Top 10 patterns.\n"
    "Return a JSON array: [{severity, cwe, line, message, fix}]. "
    "Return ONLY valid JSON."
)

SYSTEM_PROMPT 대신 위 변수를 사용하면 됩니다. cwe 필드는 취약점 트래커와 연동하거나 위험 점수 파이프라인에 투입할 때 유용합니다.

주의: 언어 모델은 비율이 무시할 수 없을 정도로 높은 false positive를 생성합니다. 이 레이어는 빠른 1차 선별용으로 활용하고, 수동 리뷰를 대체하지는 마세요. 실제 프로덕션에 배포하기 전 확인해야 할 항목에 대한 구조화된 가이드는 저희 보안 강화 체크리스트에서 확인할 수 있습니다.

파일 단위 청크 분할

문자 수 기준 청크는 동작하지만 파일 중간에 끊겨 모델이 혼란스러워질 수 있습니다. 파일 경계 기준으로 분할하면 더 좋은 결과를 얻을 수 있습니다:

import re

def split_diff_by_file(diff_text: str) -> list[str]:
    parts = re.split(r'(?=^diff --git )', diff_text, flags=re.MULTILINE)
    return [p for p in parts if p.strip()]

def review_all_files(diff_text: str) -> list[dict]:
    all_comments = []
    for file_diff in split_diff_by_file(diff_text):
        all_comments.extend(review_diff(file_diff))
    return all_comments

매우 큰 파일 처리

변경 라인이 300줄을 초과하는 경우 @@ hunk 마커를 기준으로 추가 분할이 필요합니다. 모델의 효과적인 컨텍스트는 약 4000 토큰을 넘으면 급격히 감소하므로, 작고 집중된 청크가 큰 덤프보다 일관되게 좋은 출력을 제공합니다.


셀프‑호스팅 AI 코드 리뷰는 파이프라인에서 빠르고 저렴한 1차 필터 역할을 합니다. 누락된 오류 처리, f‑string 으로 만든 SQL 쿼리, 하드코딩된 비밀키, 검증되지 않은 사용자 입력 등 흔한 패턴을 인간 리뷰어가 PR을 열기 전에 미리 잡아냅니다. 설정은 가볍습니다—Ollama, 하나의 Python 파일, CI 단계만 있으면 됩니다.

대체할 수

0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.