AI & ML/LLM

LLM을 활용해 코드리뷰 자동화 챗봇 만들기 - Ollama 활용편

개발기록 & 일상노트 2025. 11. 18. 14:48
반응형

📕 서론

GitHub에는 diff를 바탕으로 다양한 정보를 얻을 수 있습니다.
대표적으로 커밋 내용이 있는데, 이 내용을 자동으로 분석하여

  1. 코드 리뷰 시간 절감
  2. 팀 협업에서 변경사항 빠르게 파악
  3. 문서화 자동화
  4. 대규모 레포지토리 변경 추적

을 위해 LLM을 활용하여 커밋 내용 자동 분석하는 기능을 만들어 보겠습니다.


🧩 1. Ollama 설치 및 LLM 모델 다운로드

Ollama는 인터넷이 없어도 로컬 환경에서 LLM 모델을 설치하고 사용할 수 있도록 해주는 도구입니다.

- Ollama 설치

# Linux/Mac
curl -fsSL https://ollama.com/install.sh | sh

# Windows
https://ollama.com/download


- Ollama 실행

ollama start &


- 모델 다운로드

# 2GB, 가볍고 빠름 (추천)
ollama pull llama3.2

# 4GB, 성능 좋음
ollama pull mistral

# 5GB, 한국어 최강
ollama pull qwen2.5:7b

# 5GB, 최신 모델
ollama pull deepseek-r1:7b


👉 이렇게 하면 LLM 모델을 사용할 준비가 완료되었습니다.


🧩 2. Git 커밋 정보 가져오기

Github 주소를 입력받아 최근 커밋 정보를 불러오기 위한 함수를 정의해줍니다.

# ========================================
# 1. Git 커밋 정보 가져오기
# ========================================
def get_recent_commits(repo_url, max_count=3):
    """GitHub 저장소에서 최근 커밋 정보 가져오기"""
    temp_dir = tempfile.mkdtemp()
    try:
        print(f"📦 저장소 클론 중: {repo_url}")
        repo = Repo.clone_from(repo_url, temp_dir, depth=max_count)
        branch = repo.head.ref.name
        commits_info = []

        for commit in list(repo.iter_commits(branch, max_count=max_count)):
            commit_data = {
                "hash": commit.hexsha[:8],
                "author": str(commit.author),
                "message": commit.message.strip(),
                "date": str(commit.committed_datetime),
                "diff": "",
                "files_changed": []
            }

            # diff 추출
            diffs = commit.diff(commit.parents[0] if commit.parents else None, create_patch=True)

            # 변경된 파일 목록
            for d in diffs:
                if d.a_path:
                    commit_data["files_changed"].append(d.a_path)

            # diff 텍스트
            diff_text = "\n".join(
                d.diff.decode("utf-8", errors="ignore") for d in diffs if d.diff
            )

            # diff가 너무 길면 자르기 (LLM 컨텍스트 제한)
            if len(diff_text) > 3000:
                diff_text = diff_text[:3000] + "\n... (변경 사항이 더 있음)"

            commit_data["diff"] = diff_text
            commits_info.append(commit_data)

        print(f"✅ {len(commits_info)}개 커밋 수집 완료\n")
        return commits_info
    except Exception as e:
        print(f"❌ 오류 발생: {e}")
        return []
    finally:
        shutil.rmtree(temp_dir, ignore_errors=True)

🧩 3. Ollama 로컬 LLM 연결

# ========================================
# 2. Ollama 로컬 LLM
# ========================================
class OllamaLLM:
    def __init__(self, model="llama3.2", base_url="http://localhost:11434"):
        """
        Args:
            model: Ollama 모델 이름
                - llama3.2 (추천, 2GB)
                - mistral (4GB)
                - qwen2.5:7b (5GB, 한국어 강력)
                - deepseek-r1:7b (5GB, 최신)
            base_url: Ollama 서버 주소
        """
        self.model = model
        self.base_url = base_url
        self.generate_url = f"{base_url}/api/generate"
        self.chat_url = f"{base_url}/api/chat"

        # Ollama 연결 확인
        self._check_connection()

    def _check_connection(self):
        """Ollama 서버 연결 확인"""
        try:
            response = requests.get(f"{self.base_url}/api/tags", timeout=3)
            if response.status_code == 200:
                models = response.json().get("models", [])
                model_names = [m["name"] for m in models]
                print(f"✅ Ollama 연결 성공!")
                print(f"📦 설치된 모델: {', '.join(model_names) if model_names else '없음'}")

                if self.model not in model_names and f"{self.model}:latest" not in model_names:
                    print(f"\n⚠️  모델 '{self.model}'이(가) 설치되지 않았습니다.")
                    print(f"💡 다음 명령어로 설치하세요: ollama pull {self.model}")
            else:
                print("⚠️  Ollama 서버가 응답하지 않습니다.")
        except Exception as e:
            print("❌ Ollama 연결 실패!")
            print("💡 해결 방법:")
            print("   1. Ollama 설치: https://ollama.ai")
            print("   2. 모델 다운로드: ollama pull llama3.2")
            print("   3. 서버 실행: ollama serve (보통 자동 실행됨)")
            raise Exception(f"Ollama 미설치 또는 실행 중이 아닙니다: {str(e)}")

    def summarize(self, prompt, stream=False):
        """텍스트 요약 생성"""
        payload = {
            "model": self.model,
            "prompt": prompt,
            "stream": stream,
            "options": {
                "temperature": 0.7,
                "num_predict": 300  # 최대 토큰 수
            }
        }

        try:
            response = requests.post(
                self.generate_url,
                json=payload,
                timeout=120
            )

            if response.status_code == 200:
                result = response.json()
                return result.get("response", "요약 생성 실패").strip()
            else:
                return f"❌ API 오류 (코드: {response.status_code})"
        except Exception as e:
            return f"❌ 오류 발생: {str(e)}"

    def chat(self, messages, stream=False):
        """대화형 요약 (더 나은 품질)"""
        payload = {
            "model": self.model,
            "messages": messages,
            "stream": stream,
            "options": {
                "temperature": 0.7,
                "num_predict": 300
            }
        }

        try:
            response = requests.post(
                self.chat_url,
                json=payload,
                timeout=120
            )

            if response.status_code == 200:
                result = response.json()
                return result.get("message", {}).get("content", "요약 생성 실패").strip()
            else:
                return f"❌ API 오류 (코드: {response.status_code})"
        except Exception as e:
            return f"❌ 오류 발생: {str(e)}"


💡 핵심 요약
1. 서버 연결 점검: Ollama 서버와 모델이 정상적으로 준비되었는지 확인
2. summarize(): 일반 프롬프트 기반 요약/텍스트 생성
3. chat(): 채팅형 대화 모델 호출 (더 품질 높은 응답 생성)


🧩 4. 프롬프트 생성

# ========================================
# 3. 프롬프트 생성
# ========================================
def make_prompt(commit, style="detailed"):
    """
    커밋 정보를 요약 프롬프트로 변환

    Args:
        commit: 커밋 정보 딕셔너리
        style: "simple" (간단), "detailed" (상세), "technical" (기술적), "review" (코드리뷰)

    작동 원리:
        1. 변경된 파일 목록을 정리 (최대 5개까지 표시)
        2. 선택한 스타일에 따라 다른 형식의 프롬프트 생성
        3. LLM에게 명확한 지시사항 + 구조화된 데이터 제공
        4. 원하는 형식의 답변을 유도
    """
    files_changed = ", ".join(commit["files_changed"][:5])
    if len(commit["files_changed"]) > 5:
        files_changed += f" 외 {len(commit['files_changed']) - 5}개"

    if style == "simple":
        # 스타일 1: 간단 요약 (1-2문장)
        # - 최소한의 정보만 제공
        # - LLM이 핵심만 추출하도록 유도
        prompt = f"""다음 Git 커밋을 1-2문장으로 간단히 요약해주세요.

                    커밋 메시지: {commit['message']}
                    변경된 파일: {files_changed}

                    요약:
                """

    elif style == "detailed":
        # 스타일 2: 상세 분석
        # - 커밋 정보 + diff 코드 제공
        # - 구조화된 형식으로 답변 요청
        # - 목적, 변경사항, 영향 범위로 나눠서 분석
        prompt = f"""다음 Git 커밋의 변경사항을 분석하고 요약해주세요.

                    [커밋 정보]
                    - 메시지: {commit['message']}
                    - 작성자: {commit['author']}
                    - 변경된 파일: {files_changed}

                    [주요 변경 내용]
                    {commit['diff'][:2000]}

                    다음 형식으로 요약해주세요:
                    1. 변경 목적: (한 문장)
                    2. 주요 변경사항: (2-3개 항목)
                    3. 영향 범위: (간단히)
                """

    elif style == "technical":
        # 스타일 3: 기술적 분석
        # - 더 많은 diff 제공 (2500자)
        # - 문제 해결, 패턴, 코드 품질 관점에서 분석
        prompt = f"""다음 Git 커밋을 기술적으로 분석해주세요.

                    커밋 메시지: {commit['message']}
                    Diff:
                    {commit['diff'][:2500]}

                    분석 내용:
                    - 어떤 문제를 해결했나요?
                    - 어떤 기술/패턴을 사용했나요?
                    - 코드 품질에 미치는 영향은?
                """

    elif style == "review":
        # 스타일 4: 코드 리뷰 (NEW!)
        # - 코드 리뷰어 관점에서 분석
        # - 장점, 개선점, 보안, 성능 등 다각도 검토
        prompt = f"""다음 Git 커밋에 대해 코드 리뷰를 수행해주세요.

                    [커밋 정보]
                    - 메시지: {commit['message']}
                    - 작성자: {commit['author']}
                    - 변경 파일: {files_changed}

                    [변경된 코드]
                    {commit['diff'][:3000]}

                    다음 항목을 검토해주세요:

                    ✅ 좋은 점 (Good):
                    - 잘 작성된 부분
                    - 개선된 점

                    ⚠️ 개선 사항 (Improvements):
                    - 코드 품질
                    - 가독성
                    - 성능

                    🔒 보안 체크 (Security):
                    - 잠재적 보안 이슈
                    - 데이터 검증

                    🐛 잠재적 버그 (Bugs):
                    - 버그 가능성
                    - 엣지 케이스

                    💡 제안 사항 (Suggestions):
                    - 대안적 접근법
                    - 추가 고려사항
                """

    else:
        prompt = commit['message']

    return prompt


💡 핵심 요약
1. simple: 1~2 문장으로 간단 요약
2. detailed: 목적, 변경사항, 영향 범위로 나눠서 분석
3. technical: 문제 해결, 패턴, 코드 품질 관점에서 분석
4. review: 장점, 개선점, 보안, 성능 등에 대해 검토


🧩 5. 메인 실행 함수

# ========================================
# 4. 메인 실행 함수
# ========================================
def analyze_repo(repo_url, max_commits=3, model="llama3.2", style="detailed", use_chat=True):
    """
    Git 저장소 커밋 분석

    Args:
        repo_url: Git 저장소 URL
        max_commits: 분석할 커밋 개수
        model: Ollama 모델 ("llama3.2", "mistral", "qwen2.5:7b")
        style: 요약 스타일 ("simple", "detailed", "technical", "review")
        use_chat: True면 chat API 사용 (더 나은 품질)
    """
    print(f"\n{'='*70}")
    print(f"🤖 Git Commit 요약기 (Ollama 로컬 LLM)")
    print(f"{'='*70}\n")

    # 1. LLM 초기화
    try:
        llm = OllamaLLM(model=model)
    except Exception as e:
        print(f"\n{e}")
        return

    # 2. 커밋 정보 수집
    commits = get_recent_commits(repo_url, max_count=max_commits)

    if not commits:
        print("❌ 커밋 정보를 가져올 수 없습니다.")
        return

    # 3. 각 커밋 요약
    print(f"\n{'='*70}")
    print(f"📝 커밋 분석 중... (모델: {model}, 스타일: {style})")
    print(f"{'='*70}\n")

    for i, commit in enumerate(commits, 1):
        print(f"\n{'─'*70}")
        print(f"🔹 커밋 {i}/{len(commits)}")
        print(f"{'─'*70}")
        print(f"📌 Hash    : {commit['hash']}")
        print(f"👤 작성자  : {commit['author']}")
        print(f"📅 날짜    : {commit['date']}")
        print(f"💬 메시지  : {commit['message']}")
        print(f"📁 변경 파일: {len(commit['files_changed'])}개")

        # 요약 생성
        if style == "review":
            print(f"\n🔍 코드 리뷰 수행 중...", end=" ", flush=True)
        else:
            print(f"\n🔄 AI 요약 생성 중...", end=" ", flush=True)

        if use_chat:
            review_mode = (style == "review")
            messages = make_chat_prompt(commit, style, review_mode=review_mode)
            summary = llm.chat(messages)
        else:
            prompt = make_prompt(commit, style=style)
            summary = llm.summarize(prompt)

        print("완료!")

        if style == "review":
            print(f"\n📋 코드 리뷰 결과:")
        else:
            print(f"\n💡 AI 분석:")
        print(f"{'─'*10}")
        for line in summary.split('\n'):
            if line.strip():
                print(f"   {line}")
        print(f"{'─'*10}")

    print(f"\n{'='*10}")
    print("✅ 분석 완료!")
    print(f"{'='*10}\n")

🧩 6. 대화형 챗봇 모드 추가

단순 실행보다 대화형 챗봇 모드도 있으면 좋을 것 같아서 추가해 보았습니다.

# ========================================
# 5. 대화형 챗봇 모드
# ========================================
def make_chat_prompt(commit, style, review_mode=False):
    """
    대화형 프롬프트 (더 나은 품질)

    Args:
        commit: 커밋 정보
        review_mode: True면 코드리뷰 모드
    """
    files_changed = ", ".join(commit["files_changed"][:5])

    if review_mode:
        # 코드 리뷰 모드
        messages = [
            {
                "role": "system",
                "content": "당신은 시니어 개발자이자 코드 리뷰어입니다. 코드의 장단점을 명확히 지적하고, 건설적인 피드백을 제공합니다."
            },
            {
                "role": "user",
                "content": f"""다음 Git 커밋을 코드 리뷰해주세요.

                커밋 메시지: {commit['message']}
                변경 파일: {files_changed}

                변경 코드:
                {commit['diff'][:3000]}

                다음을 분석해주세요:
                1. ✅ 좋은 점
                2. ⚠️ 개선할 점
                3. 🔒 보안 이슈 (있다면)
                4. 🐛 잠재적 버그 (있다면)
                5. 💡 제안사항
                """
            }
        ]
    else:
        if style == "simple":
            # 스타일 1: 간단 요약 (1-2문장)
            # - 최소한의 정보만 제공
            # - LLM이 핵심만 추출하도록 유도
            messages = [
                {
                    "role": "system",
                    "content": "당신은 Git 커밋을 분석하고 요약하는 전문가입니다."
                },
                {
                    "role": "user",
                    "content": f"""다음 Git 커밋을 1-2문장으로 간단히 요약해주세요.

                        커밋 메시지: {commit['message']}
                        변경된 파일: {files_changed}

                        요약:
                    """
                }
            ]

        elif style == "detailed":
            # 스타일 2: 상세 분석
            # - 커밋 정보 + diff 코드 제공
            # - 구조화된 형식으로 답변 요청
            # - 목적, 변경사항, 영향 범위로 나눠서 분석
            messages = [
                {
                    "role": "system",
                    "content": "당신은 Git 커밋을 분석하고 요약하는 전문가입니다."
                },
                {
                    "role": "user",
                    "content": f"""다음 Git 커밋의 변경사항을 분석하고 요약해주세요.

                        [커밋 정보]
                        - 메시지: {commit['message']}
                        - 작성자: {commit['author']}
                        - 변경된 파일: {files_changed}

                        [주요 변경 내용]
                        {commit['diff'][:2000]}

                        다음 형식으로 요약해주세요:
                        1. 변경 목적: (한 문장)
                        2. 주요 변경사항: (2-3개 항목)
                        3. 영향 범위: (간단히)
                    """
                }
            ]

        elif style == "technical":
            # 스타일 3: 기술적 분석
            # - 더 많은 diff 제공 (2500자)
            # - 문제 해결, 패턴, 코드 품질 관점에서 분석
            messages = [
                {
                    "role": "system",
                    "content": "당신은 Git 커밋을 분석하고 요약하는 전문가입니다."
                },
                {
                    "role": "user",
                    "content": f"""다음 Git 커밋을 기술적으로 분석해주세요.

                        커밋 메시지: {commit['message']}
                        Diff:
                        {commit['diff'][:2500]}

                        분석 내용:
                        - 어떤 문제를 해결했나요?
                        - 어떤 기술/패턴을 사용했나요?
                        - 코드 품질에 미치는 영향은?
                    """

                }
            ]

    return messages

def interactive_mode(model="llama3.2"):
    """대화형 Git 분석 모드"""
    print(f"\n{'='*70}")
    print(f"💬 대화형 Git 분석 모드")
    print(f"{'='*70}\n")

    llm = OllamaLLM(model=model)

    while True:
        print("\n옵션을 선택하세요:")
        print("1. 저장소 요약 분석")
        print("2. 저장소 코드 리뷰")
        print("3. 직접 질문하기")
        print("4. 종료")

        choice = input("\n선택 (1/2/3/4): ").strip()

        if choice == "1":
            repo_url = input("\n📦 Git 저장소 URL: ").strip()
            max_commits = input("📊 분석할 커밋 개수 (기본 3): ").strip()
            max_commits = int(max_commits) if max_commits.isdigit() else 3

            style = input("📝 스타일 (simple/detailed/technical/review, 기본 detailed): ").strip()
            style = style if style in ["simple", "detailed", "technical"] else "detailed"

            analyze_repo(repo_url, max_commits, model, style)

        elif choice == "2":
            repo_url = input("\n📦 Git 저장소 URL: ").strip()
            max_commits = input("📊 리뷰할 커밋 개수 (기본 3): ").strip()
            max_commits = int(max_commits) if max_commits.isdigit() else 3

            print("\n🔍 코드 리뷰 모드로 실행합니다...")
            analyze_repo(repo_url, max_commits, model, style="review")

        elif choice == "3":
            question = input("\n💬 질문: ").strip()
            if question:
                response = llm.summarize(question)
                print(f"\n🤖 답변:\n{response}")

        elif choice == "4":
            print("\n👋 종료합니다.")
            break

🧩 7. 실행 예시

주석 처리한 부분은 각각 simple, technical, review, detailed 등 옵션을 실행할 수 있는 기본적인 예제입니다.
4번째 예제인 interactive_mode (대화형 모드)에서도 위의 옵션을 전부 실행해 볼 수 있습니다.

# ========================================
# 6. 실행 예시
# ========================================
if __name__ == "__main__":
#     예시 1: 일반 요약
#     analyze_repo(
#         repo_url="https://github.com/ZZmarkus/OCR-Model-Test-Using-RestfulAPI.git",
#         max_commits=3,
#         model="llama3.2",
#         style="technical",
#         use_chat=False
#     )

#     예시 2: 코드 리뷰 모드
#     analyze_repo(
#         repo_url="https://github.com/ZZmarkus/OCR-Model-Test-Using-RestfulAPI.git",
#         max_commits=5,
#         model="llama3.2",  
#         style="review",
#         use_chat=False
#     )

#     예시 3: 간단 요약
#     analyze_repo(
#         repo_url="https://github.com/ZZmarkus/OCR-Model-Test-Using-RestfulAPI.git",
#         max_commits=10,
#         model="llama3.2",
#         style="simple",
#         use_chat=False
#     )

    # 예시 4: 대화형 모드
    interactive_mode(model="qwen2.5:7b")

1) 옵션 선택하기

옵션 선택하기


2) Git 저장소 URL 입력하기

Git 저장소 URL 입력하기


3) 분석할 커밋 개수 입력하기

분석할 깃 커밋의 개수


4) 분석 방법 입력하기

Simple 분석 결과


5) 코드리뷰 모드

코드리뷰모드 분석 결과

💡 실행 예시

🔹 커밋 1/3
───────
📌 Hash    : fe959083
👤 작성자  : DESKTOP-IGD4989\11703
📅 날짜    : 2024-06-27 15:04:49+09:00
💬 메시지  : 간단한 OCR모델 테스트 API
📁 변경 파일: 25개

🔍 코드 리뷰 수행 중... 완료!

📋 코드 리뷰 결과:
───────
   Git 커밋을 코드 리뷰해본 결과以下은 좋은 점, 개선할 점, 보안 이슈, 잠재적 버그, 제안 사항입니다.
   1. ✅ 좋은 점:
      - 커밋 메시지가 간결하고 명확합니다.
      - Gradle의 configuration과 dependencies를 잘 정의했습니다.
      - Gradle wrapper에 대한 설정은 적절합니다.
   2. ⚠️ 개선할 점:
      - Gradle wrapper의 version을 7.6.1으로 setting해두었으나, Gradle의 latest 버전이 8.4.1이기 때문에 이중 사용을 피하기 위해 version을 updates 하시는 것을 권장합니다.
      - Gradle의 version과 dependencies를 잘 확인하여 update를 하세요.
   3. 🔒 보안 이슈 (있다면):
      - None
      -Gradle wrapper에 대한 configuration은 보안 관련이 없습니다.
   4. 🐛 잠재적 버그 (있다면):
      - Gradle wrapper의 version을 update 후, Gradle version이 변경되는 경우 Gradle wrapper가 updated되지 않는 문제가 발생할 수 있습니다. 
      - Gradle version을 확인하고, Gradle wrapper를 update하세요.
   5. 💡 제안사항:
      - Gradle configuration에서 `spring-boot-starter-data-jpa`를 두 번 반복적으로 설정하는 것을 피하세요
───────

───────
🔹 커밋 2/3
───────
📌 Hash    : ebb5af14
👤 작성자  : Cho Hyeon Min
📅 날짜    : 2024-06-27 15:00:57+09:00
💬 메시지  : Update README.md
📁 변경 파일: 1개

🔍 코드 리뷰 수행 중... 완료!

📋 코드 리뷰 결과:
────────
   👍 codes를 분석해 드리겠습니다.
   **✅ 좋은 점**
   - 커밋 메시지가 충분히 짧고 clear합니다. README.md에 대한 업데이트가.clear하게 나타나며, 쉽게 이해할 수 있습니다.
   - 커뮤니티에서 도움이 되거나, 관련된 프로젝트에 영향을 줄 수 있는 변경 사항은 커밋 메시지의 첫 번째 단어로 시작하여 명확히합니다.
   **⚠️ 개선할 점**
   - 커밋 메시지가 너무 짧습니다. 짧은 커밋 메시지는 업데이트가 어떤 내용이인지를 알기 어렵습니다. 따라서 커뮤니티에서 도움이 되거나, 관련된 프로젝트에 영향을 줄 수 있는 변경 사항은 커밋 메시지의 첫 번째 단어로 시작하여 명확히하고, 한-Line 이외에는 더 많은 정보가 필요합니다.
   - 커밋 메시지의 내용이 README.md에 대한 업데이트를 나타내는 것만으로 confines되어 있습니다. 커뮤니티가 어떤 변경 사항을 알 수 있는지 확인해 보십시오.
   **🔒 보안 이슈 (있다면)**
   - 현재는 보안 이슈가 없습니다. 하지만, Future의 보안에 대한 고려가 필요합니다.
   **🐛 잠재적 버그 (있다면)**

📗 마치며

이번 포스팅에서는 온프레미스 환경에서 LLM 모델을 다운로드하여

Github의 Diff 정보를 바탕으로 코드리뷰 및 커밋내용 요약하는 기능을 만들어 보았습니다.

 

여러 LLM 오픈소스 모델을 Finetuning 하여 이러한 코드리뷰 자동화 프로세스를 구축할 수 있다면

개발 생산성을 크게 향상해 줄 수 있을 것 같습니다.

 

🎯 이 아이디어를 확장하면

  • PR 기반 자동 리뷰 시스템 구축
  • 커밋 history를 Notion/Confluence 문서로 자동 정리
  • CI 과정 내에서 자동 정책 위반 검사
  • Jira 자동 업데이트

등 다양한 방향으로 기능을 업데이트할 수 있을 것 같습니다.

반응형