CI에 AI 리뷰를 추가하는 저렴한 방법: 소형 로컬 모델과 프롬프트 캐싱 (Prompt Caching) 활용
요약
CI 환경에서 AI 코드 리뷰 비용을 절감하기 위해 소형 로컬 모델과 프롬프트 캐싱을 결합한 2단계 구조를 제안합니다. 소형 모델로 위험도를 먼저 분류하고, 고위험 변경 사항에 대해서만 대형 모델과 캐싱 기술을 적용하여 효율성을 극대화합니다.
핵심 포인트
- 소형 모델을 활용한 1단계 분류(Triage)로 불필요한 API 호출 방지
- 고위험 변경 사항에 대해서만 대형 모델로 에스컬레이션 수행
- 프롬프트 캐싱을 통해 시스템 프롬프트 및 저장소 컨텍스트 비용 절감
- 안정적인 접두사를 앞에 배치하는 캐싱 최적화 전략 활용
저는 CI(지속적 통합)에 AI 코드 리뷰어를 연결하고 스스로 똑똑하다고 느꼈지만, PR(Pull Request)이 몰린 바쁜 한 주가 지나고 청구서를 보고 깜짝 놀랐습니다. 모든 푸시(push)와 모든 커밋(commit)마다 전체 디프(diff)를 거대 프론티어 모델(frontier model)로 보내고 있었기 때문입니다. 비용은 예상보다 빠르게 쌓였고, 리뷰 대상의 대부분은 천재적인 분석이 필요 없는 포맷팅 변경이나 README 수정 사항이었습니다. 그래서 저는 이를 2단계(two tiers) 구조로 재설계했고, 중요한 탐지 기능은 유지하면서 비용을 대폭 절감했습니다.
아이디어는 간단합니다. 모든 것을 비싼 모델로 보내지 않는 것입니다. 저렴한 모델을 '가드(bouncer)'로 사용하여 에스컬레이션(escalation)할 가치가 있는지 결정하게 하고, 진짜 문제가 될 수 있는 파일에 대해서만 (캐싱(caching)이 활성화된) 대형 모델을 사용하도록 예약하는 것입니다.
1단계: 저렴한 분류(triage) 단계
첫 번째 단계는 디프(diff)를 살펴보고 한 가지 질문에 답합니다: "여기에 실제 리뷰를 정당화할 만큼 위험한 내용이 있는가?" 이것은 분류(classification) 작업이며, 분류에는 프론티어 모델이 필요하지 않습니다.
저는 이 단계를 소형 로컬 모델(small local model)로 실행합니다. 제 개인 컴퓨터에서는 Ollama를 사용한 qwen2.5-coder를 사용하지만, CI에서는 자체 호스팅되는 소형 모델이나 클라우드 제공업체가 제공하는 가장 저렴한 티어(tier)를 사용할 수 있습니다. 이 작업의 목적은 판단이 아니라 분류(triage)입니다.
분류 프롬프트(triage prompt)는 의도적으로 좁게 설정했습니다. 리뷰를 해달라고 요청하지 않고, 경로를 지정(route)해달라고 요청합니다:
당신은 코드 리뷰를 위한 분류 필터입니다. 아래의 디프(diff)에 대해 JSON으로만 출력하세요:
{ "risk": "low" | "high", "reason": "<짧은 문구 하나>" }
...
소형 모델은 "이것이 인증(auth)이나 결제(money) 관련 부분을 건드리는가"를 판단하는 데 매우 능숙합니다. 모델이 'low'라고 답하면, CI는 "고위험 변경 사항이 감지되지 않았습니다"라는 한 줄의 메시지를 게시하고 중단합니다. 비싼 API 호출은 발생하지 않습니다. 실제로 대부분의 PR은 문서, 테스트, 그리고 단순한 구조 작업(plumbing)이기 때문에, 이 방식은 대다수의 PR 과정을 단축해 줍니다.
2단계: 위험한 파일에 대해서만 캐싱을 적용한 대형 모델 사용
1단계에서 'high'라고 판단하면, 위험한 파일들은 강력한 모델로 에스컬레이션됩니다. 여기서 실제 리뷰가 이루어지며, 바로 이 지점에서 프롬프트 캐싱(prompt caching)이 제 역할을 다하게 됩니다.
핵심 비결은 대형 모델(big model)에 보내는 내용의 대부분이 리뷰 사이에 변하지 않는다는 점입니다. 시스템 프롬프트(system prompt, 즉 리뷰 루브릭(rubric), 심각도 정의, 사내 규칙 등)는 매번 동일합니다. 또한 저장소 컨텍스트(repo context, 주변 파일, 컨벤션, 변경된 코드가 의존하는 인터페이스 등)는 동일한 영역을 건드리는 수많은 작은 PR(Pull Request)들에 걸쳐 안정적으로 유지됩니다. 이 안정적인 접두사(prefix)를 캐싱(caching)하면, 처음 한 번만 전체 비용을 지불하고 이후 이를 재사용하는 모든 리뷰에서는 아주 적은 비용만 지불하면 됩니다.
따라서 저는 요청을 다음과 같이 구성합니다: 캐싱된 시스템 프롬프트, 그다음 캐싱된 저장소 컨텍스트, 마지막으로 최신 diff(차이점) 순서입니다. Anthropic 스타일의 캐싱은 안정적인 접두사를 기준으로 키를 생성하므로 순서가 중요합니다. 안정적인 것을 먼저, 변동성이 큰 것을 마지막에 배치합니다.
# 의사코드(pseudocode), 제공자 독립적인 형태
response = client.messages.create(
model="big-model",
...
루브릭과 저장소 컨텍스트는 캐시를 탑니다. 매 실행 시 오직 diff만 새로 들어옵니다. 수많은 작은 PR이 동일한 모듈을 건드리는 저장소의 경우, 캐시 히트율(cache hit rate)이 높아지며, 리뷰당 대형 모델 비용은 대부분 새로운 토큰(fresh tokens) 비용으로 떨어집니다.
GitHub Actions 스케치
이를 하나로 엮는 것은 생소한 작업이 아닙니다. 하나의 작업(job), 두 단계(step), 그리고 조건부 에스컬레이션(conditional escalation)으로 구성됩니다.
name: ai-review
on: pull_request
...
두 번째 단계의 if: 구문이 비용 관리의 핵심입니다. 저위험 PR은 비싼 모델에 도달하지 않습니다. 고위험 PR만 전체 프로세스를 거치게 됩니다.
diff 전용 리뷰가 놓치는 함정
사람들이 흔히 실수하는 부분이 바로 여기이며, 이것이 단순한(naive) AI 리뷰어가 잘못된 확신을 주는 이유입니다. 만약 diff만 보낸다면, 모델은 파일 간의 버그(cross-file bugs)를 볼 수 없습니다.
예를 들어, 한 PR이 함수의 반환 타입(return type)이나 매개변수(parameter) 중 하나의 의미를 변경한다고 가정해 봅시다. diff만 보면 로컬적으로는 문제가 없어 보입니다. 리뷰어는 해당 라인들만 보고 승인(approve)을 내립니다. 하지만 다른 세 개의 파일이 해당 함수를 호출하고 있으며, 이제 이전의 가정을 바탕으로 값을 전달하고 있습니다. 그런데 그 파일들은 diff에 전혀 포함되어 있지 않습니다. 버그는 실제로 존재하며 심각하지만, diff 전용 리뷰는 구조적으로 이를 포착할 수 없습니다.
저렴한 해결책은 변경된 함수의 호출자(callers)를 전송하는 컨텍스트(context)에 포함하는 것입니다. 저장소 전체를 보낼 필요는 없습니다. 변경된 심볼(symbols)과 그 심볼을 사용하는 모든 코드를 보내면 됩니다.
# 이 PR에서 변경된 함수를 호출하는 대상을 찾습니다
changed_funcs=$(git diff origin/main...HEAD \
| grep -oP '^\+.*function \K\w+' | sort -u)
...
이러한 호출자 파일들을 2단계 컨텍스트(tier-two context, 호출자는 변경 빈도가 낮으므로 캐싱되는 부분)에 입력합니다. 이제 변경 사항이 인터페이스(interface)를 수정하면, 모델은 이전 동작에 의존하던 코드를 확인할 수 있고 불일치(mismatch)를 찾아낼 수 있습니다. 이 단 한 번의 추가만으로도 순수 diff 리뷰로는 절대 포착할 수 없는 부류의 버그를 잡아낼 수 있으며, 호출자들은 안정적이기 때문에 첫 번째 캐싱 실행 이후에는 비용이 거의 들지 않습니다.
이것이 실제로 가져다주는 이점
경제성은 어려운 작업을 수행하는 더 저렴한 모델에서 오는 것이 아니라, 라우팅(routing)에서 옵니다. 소형 모델은 필터(filter) 역할을 하며, 필터링은 저렴하고 쉽습니다. 대형 모델은 실제 작업을 수행하지만, 그럴 가치가 있는 아주 작은 부분에 대해서만 수행합니다. 또한 루브릭(rubric)과 컨텍스트는 캐시를 이용하는 동안, 변경되는 diff에 대해서만 전체 토큰 비용을 지불합니다.
저는 spectr-ai가 비용을 추론하는 방식에도 이 방식의 일부를 사용합니다. 즉, 주의 깊고 비용이 많이 드는 검토를 할 가치가 있는지 결정하기 위해 빠른 로컬 패스(local pass)를 거친 다음, 중요한 부분에 대해서만 비용이 많이 드는 검토를 수행합니다. 분류(triage) 단계에서 qwen2.5-coder를 로컬에서 실행하면, 이미 보유하고 있는 하드웨어 덕분에 해당 단계는 사실상 무료가 됩니다.
피해야 할 실패 사례는 저렴한 단계(cheap tier)를 리뷰어(reviewer)로 취급하는 것입니다. 그것은 리뷰어가 아닙니다. 그것은 바운서(bouncer, 입구 관리자)입니다. 만약 분류 모델이 리뷰 코멘트를 작성하기 시작한다면, 당신은 잘못된 도구에 어려운 일을 맡긴 것입니다. 1단계(tier one)는 좁게 유지하고, 2단계(tier two)는 호출자와 캐싱된 컨텍스트로 충분히 채워주세요. 그러면 버그 포착 능력은 유지하면서 청구되는 비용은 작게 유지할 수 있습니다.
CI에서 AI 리뷰를 실행하고 계신가요? 그렇다면 단순히 bare diff만 보내고 계신가요, 아니면 변경된 사항의 호출자까지 함께 가져오고 계신가요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기