AI GitHub 어시스턴트를 만드는 데 주말이면 충분할 줄 알았다. 내 착각이었다.
요약
GitHub 저장소의 구조를 이해하고 질문에 답할 수 있는 AI 어시스턴트 'Octogen' 개발 과정과 시행착오를 다룹니다. 단순한 임베딩을 넘어 컨텍스트 품질 확보와 API 비용 및 제한 관리의 중요성을 강조합니다.
핵심 포인트
- 단순 임베딩보다 지능적인 문서 분할과 요약이 검색 품질에 결정적임
- 더 큰 모델을 사용하는 것보다 더 나은 컨텍스트를 제공하는 것이 더 중요함
- 대규모 저장소 처리 시 토큰 제한(TPM) 및 API 비용 관리가 필수적임
- AI 기능 구현보다 주변 엔지니어링 환경 구축이 더 큰 도전 과제임
하나의 문제를 해결할 때마다 또 다른 문제가 나타났다. 돌이켜보면, 바로 그 점이 이 프로젝트를 만들 가치가 있게 만들었다.
나는 Octogen을 만들었습니다: github.com/SplinterSword/octogen
Octogen을 만들기 시작했을 때, 나는 매우 단순한 아이디어를 가지고 있었습니다.
"파일들을 검색하며 몇 시간을 보내는 대신, 어떤 GitHub 저장소(repository)에 대해서든 질문을 던질 수 있다면 어떨까?"
그것은 간단해 보였습니다.
저장소를 클론(Clone)한다.
임베딩 (embeddings)을 생성한다.
어딘가에 저장한다.
검색된 컨텍스트 (context)를 LLM에 전달한다.
끝.
...적어도 나는 그렇게 생각했습니다.
며칠 지나지 않아 나는 AI 부분이 실제로는 프로젝트에서 가장 쉬운 부분이라는 것을 깨달았습니다.
그 주변의 모든 것들이 진짜 엔지니어링이 일어나는 곳이었습니다.
왜 이것을 만들기 시작했는가
모든 인턴십이나 새로운 프로젝트는 똑같은 방식으로 시작됩니다.
저장소를 클론합니다.
파일 50개를 엽니다.
"auth"를 검색합니다.
"login"을 검색합니다.
"middleware"를 검색합니다.
결국 하루의 절반을 허비한 후에야 아키텍처 (architecture)를 이해하게 됩니다.
나는 다른 무언가를 원했습니다.
파일명들을 검색하는 대신, 다음과 같이 질문하고 싶었습니다:
"인증 (authentication)은 어디에 구현되어 있나요?"
또는
"어떤 파일이 Stripe 웹훅 (webhooks)을 처리하나요?"
그것이 목표가 되었습니다.
문서화를 대체하려는 것이 아닙니다.
GitHub를 대체하려는 것도 아닙니다.
그저 다른 사람의 코드를 이해하는 데 걸리는 시간을 줄이고 싶었을 뿐입니다.
첫 번째 버전은 작동했다...
...실제 저장소에서 테스트하기 전까지는 말이죠.
작은 저장소들은 훌륭했습니다.
대규모 저장소들은 모든 것을 완전히 망가뜨렸습니다.
테스트 케이스가 더 현실적으로 변할수록, 내가 했던 모든 가정들이 얼마나 잘못되었는지 더 빨리 발견하게 되었습니다.
문제 1: 임베딩 (Embeddings)은 쉬운 부분이었다
대부분의 튜토리얼은 다음과 같이 말하고 끝납니다:
임베딩을 생성하고 유사도 검색 (similarity search)을 수행하세요.
그 부분은 실제로 작동했습니다.
어려운 질문은 이것이었습니다:
정확히 무엇을 임베딩해야 하는가?
파일 전체?
함수 (Functions)?
문단 (Paragraphs)?
무작위 청크 (chunks)?
파일 전체를 임베딩하면 검색 결과에 노이즈가 생겼습니다.
모든 것을 아주 작은 청크로 나누면, 모델이 컨텍스트 (context)를 놓쳤습니다.
결국 저는 문서를 지능적으로 분할하고, 임베딩 (embeddings)을 저장하기 전에 요약본을 생성하는 방식으로 결론을 내렸습니다.
검색 (retrieval) 품질이 극적으로 향상되면서, 요약본은 코드 자체만큼이나 가치 있는 존재가 되었습니다.
제가 얻은 가장 큰 교훈은 간단했습니다:
더 나은 컨텍스트 (context)가 더 큰 모델보다 낫다.
문제 2: AI 비용은 생각보다 빠르게 증가한다
저는 AI가 생성하는 커밋 요약 (commit summaries) 기능도 원했습니다.
첫 번째 구현 방식은 부끄러울 정도로 단순했습니다.
모든 커밋 디프 (commit diff)를 동일한 모델로 직접 보냈습니다.
작동은 했습니다...
수백 개의 커밋이 있는 저장소 (repositories)를 테스트하기 전까지는 말이죠.
그때부터 모든 것이 실패하기 시작했습니다.
버그 때문이 아니었습니다.
Groq의 TPM (Tokens Per Minute, 분당 토큰 수) 제한에 부딪혔기 때문입니다.
저는 실수로 제 자신의 API 키를 대상으로 서비스 거부 공격 (denial-of-service attack)을 수행하는 시스템을 구축한 셈이었습니다.
해결책
모든 요청을 동일하게 처리하는 대신, 라우팅 (routing) 시스템을 구축했습니다.
작은 커밋인가요?
→ 가벼운 모델을 사용합니다.
중간 크기의 커밋인가요?
→ 더 큰 Groq 모델을 사용합니다.
거대한 커밋인가요?
→ 거대한 컨텍스트 윈도우 (context window)를 가진 Gemini로 보냅니다.
그 외에도 다음과 같은 기능을 추가했습니다:
- 락 파일 (lock files) 및 생성된 코드를 제거하는 필터링 (filtering).
- 수백 개의 요청을 동시에 날리는 대신 배치 (batching) 처리.
- 제공자 (providers) 간의 자동 폴백 (fallback).
결과는 단순히 실패 횟수가 줄어든 것만이 아니었습니다.
비용이 극적으로 저렴해졌습니다.
때로는 인프라 (infrastructure) 문제를 해결하는 것이 프롬프트 (prompts)를 최적화하는 것보다 더 많은 돈을 아껴줍니다.
문제 3: 저장소가 노이즈로 가득했다
또 다른 문제가 저를 놀라게 했습니다.
AI가 환각 (hallucinating)을 일으키는 것이 아니었습니다.
엉망인 컨텍스트를 바탕으로 답변을 하고 있었던 것입니다.
대규모 저장소에는 다음과 같은 것들이 포함되어 있습니다:
package-lock.jsonbun.lockdist/.next/- 생성된 파일들 (Generated files)
이러한 파일들은 수천 개의 쓸모없는 토큰 (tokens)을 발생시킵니다.
모델은 어쨌든 그것들을 기쁘게 소비해 버립니다.
저는 제가 AI 문제를 만들고 있는 것이 아니라는 사실을 깨달았습니다.
저는 필터링 (filtering) 문제를 만들고 있었던 것입니다.
인덱싱 (indexing) 전에 생성된 파일들을 공격적으로 제거한 후, 검색 (retrieval) 품질이 즉시 향상되었습니다.
때로는 최고의 프롬프트 엔지니어링 (prompt engineering)은 데이터를 삭제하는 것입니다.
문제 4: 운영 환경은 개발 환경과 전혀 다르게 동작한다
로컬에서는 모든 것이 완벽해 보였습니다.
그러자 운영 (production) 빌드가 실패하기 시작했습니다.
TypeScript 에러.
ESLint 에러.
다르게 동작하는 미들웨어 (Middleware).
특히나 좌절감을 주었던 한 가지 문제는 거의 웃음이 나올 정도였습니다.
인증 (Authentication)이 그냥 작동을 거부했습니다.
몇 시간 뒤에 저는 파일 이름이 다음과 같다는 것을 발견했습니다:
proxy.ts
middleware.ts 대신에 말이죠.
파일 이름 하나 때문에.
몇 시간이 날아갔습니다.
프레임워크 컨벤션 (Framework conventions)은 보기보다 훨씬 더 엄격합니다.
때로는 가장 큰 버그가 가장 작은 수정으로 해결된다는 것을 뼈아프게 상기시켜 준 경험이었습니다.
문제 5: PostgreSQL은 진정한 벡터 데이터베이스가 아니다
저는 Pinecone이나 다른 관리형 벡터 데이터베이스 (managed vector database)를 사용하는 것을 포기했습니다.
하나의 데이터베이스만 원했습니다.
PostgreSQL입니다.
pgvector를 추가하는 것은 간단해 보였습니다.
하지만 그렇지 않았습니다.
Prisma는 벡터 컬럼 (vector columns)을 기본적으로 이해하지 못합니다.
그것은 생 SQL (raw SQL)을 의미했습니다.
지원되지 않는 필드 타입.
커스텀 유사도 쿼리 (Custom similarity queries).
문서에는 그 조각들이 다뤄져 있었지만, 모든 것을 하나로 합치는 데는 예상보다 훨씬 더 오랜 시간이 걸렸습니다.
결국, 관계형 데이터 (relational data)와 임베딩 (embeddings)을 함께 유지하는 것이 아키텍처를 훨씬 더 단순하게 만들었습니다.
때로는 다른 서비스를 피하는 것이 약간의 추가 작업을 감수할 가치가 있습니다.
가장 큰 교훈
사람들이 AI 프로젝트를 볼 때 보통 모델 (model)에 집중합니다.
이 프로젝트에서 모델은 아마 20% 정도였을 것입니다.
나머지 **80%**는 다음과 같았습니다:
- API 실패 처리
- 속도 제한 (rate limits) 관리
- 토큰 (token) 사용 최적화
- 검색 (retrieval) 설계
- 비용 절감
- 프레임워크의 특이사항 (quirks) 처리
- 모든 것을 신뢰할 수 있게 만들기
그것이 사람들이 보통 스크린샷을 올리지 않는 부분입니다.
하지만 바로 그곳에서 대부분의 엔지니어링이 일어납니다.
AI 제품을 만드는 것은 프롬프트 엔지니어링 (prompt engineering)보다는 시스템 엔지니어링 (systems engineering)에 훨씬 더 가깝습니다.
만약 제가 다시 만든다면...
이제 과정을 한 번 겪고 나니, 다르게 접근하고 싶은 몇 가지 사항들이 있습니다.
리포지토리를 처음부터 다시 구축하는 대신 증분 인덱싱 (incremental indexing)을 구현할 것입니다.
리포지토리가 자동으로 동기화될 수 있도록 GitHub 웹훅 (webhooks)을 추가했을 것입니다.
오래 걸리는 작업 (long-running operations)을 동기적 (synchronously)으로 처리하기보다는, 훨씬 더 이른 단계에서 백그라운드 작업 큐 (background job queues)를 도입했을 것입니다.
가장 중요한 것은, 더 많은 AI 기능을 추가하기 전에 관측 가능성 (observability)에 투자했을 것입니다.
시스템이 어디에서 고장 나는지 명확하게 볼 수 있을 때 시스템을 개선하기가 훨씬 더 쉽기 때문입니다.
마치며
Octogen을 구축하면서 예상치 못한 것을 배웠습니다.
AI 제품의 어려운 점은 LLM을 호출하는 것이 아닙니다.
그건 누구나 할 수 있습니다.
진짜 어려운 점은 수천 개의 파일, 수백 개의 커밋, API 제한 (API limits), 프로덕션 트래픽 (production traffic), 그리고 첫날에는 생각지도 못했던 모든 에지 케이스 (edge case) 상황 이후에도 계속해서 작동하는 시스템을 구축하는 것입니다.
AI는 퍼즐의 한 조각일 뿐이었습니다.
진정한 도전은 그 주변의 인프라 (infrastructure)를 구축하는 것이었습니다.
그리고 솔직히 말하자면...
그 점이 바로 구축하는 것을 즐겁게 만들었습니다.
읽어주셔서 감사합니다!
최근에 AI 애플리케이션을 구축해 보셨다면, 다음 질문에 대한 답변을 듣고 싶습니다:
어떤 엔지니어링 문제가 당신을 가장 놀라게 했나요?
프로젝트 링크: github.com/SplinterSword/octogen
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기