본문으로 건너뛰기
wooncloud

Jev 대안이라던 laya를 써보고 Jev로 돌아온 이야기

오픈소스 laya 는 실제 데이터에서 정확도가 30~46%에 그쳐 하루 만에 지우고, 확신도 0.8 이상이 거의 다 맞는 Jev 로 전환해 이슈 분류 파이프라인을 구축한 경험입니다.

14분 분량

Claude Code 로 일하다 보면 비슷한 부탁을 자주 하게 됩니다. 이슈 수백 건을 버그인지 개선 요청인지 나눠 달라거나, 로그 줄을 심각도별로 묶어 달라거나요. 라벨은 이미 정해져 있고 항목만 많은 일입니다. LLM 에 통째로 맡기면 되긴 하는데 느리고, 돌릴 때마다 결과가 조금씩 달라서 늘 찜찜했습니다.

그러다 텍스트를 생성하지 않고 판정만 내리는 모델이 있다는 얘기를 들었습니다. TypeSafe 의 Jev 가 요즘 꽤 화제고, 그 대안으로 오픈소스 laya 를 쓴다는 글도 여럿 보였습니다. 로컬에서 돌고, 무료고, 한 번 판정하는 데 33ms 라고 했습니다. 그래서 laya 부터 붙여 봤습니다. 결론부터 말하면 laya 는 하루 만에 지웠고 지금은 Jev 를 씁니다. 그 과정을 정리해 둡니다.

판정 모델이 뭔가

보통 LLM 은 답을 한 글자씩 써 내려갑니다. 분류를 시키면 "bug" 라는 단어를 생성하고, 우리는 그걸 다시 파싱합니다. 판정 모델은 그 과정을 건너뜁니다. 상태(텍스트나 JSON)와 질문, 선택지를 주면 한 번의 계산으로 선택지별 확률을 돌려줍니다. 생성을 하지 않으니 없는 라벨을 지어낼 일이 없고, 확률이 같이 나오니 모델이 얼마나 확신하는지도 알 수 있습니다.

질문 형식은 Jev 와 laya 가 거의 같습니다. laya 가 Jev 의 형식을 따라 만들었기 때문입니다.

{
  "state": "결제가 두 번 됐어요. 환불해 주세요.",
  "questions": {
    "team": {
      "type": "choice",
      "instructions": "어느 팀이 처리해야 하나?",
      "criteria": { "billing": "결제와 환불", "technical": "버그와 장애" }
    }
  }
}

laya 를 로컬에 붙이기

M3 Pro 맥북에 설치했습니다. 처음엔 도커로 관리할까 했는데, 맥의 도커는 리눅스 가상머신 위에서 돌아서 Metal GPU 를 못 씁니다. CPU 로 떨어지면 몇 배는 느려지니 그냥 가상환경에 설치하고, 여러 세션이 같이 쓰도록 작은 로컬 데몬을 하나 띄웠습니다. 20분 동안 요청이 없으면 알아서 꺼지게 해 두었습니다.

설치하면서 재미있는 걸 하나 발견했습니다. 모델 하나 로드하는 데 30초가 걸렸는데, 프로파일을 떠 보니 그중 23초가 가중치를 무작위로 초기화하는 데 쓰이고 있었습니다. 뼈대를 만들 때 transformers 가 모든 가중치를 난수로 채우고, 바로 다음 줄에서 저장된 가중치로 전부 덮어쓰는 구조였습니다. 초기화를 건너뛰도록 감쌌더니 1초 안팎으로 줄었고, 출력은 완전히 같았습니다.

from transformers.initialization import no_init_weights
 
with no_init_weights():
    agent = laya.load("convaiinnovations/laya")

속도도 괜찮았습니다. 짧은 문장은 판정 한 번에 20~40ms, 파일 하나를 통째로 넣어도 0.1초 남짓이었습니다. 여기까지는 기대한 그대로였습니다.

실제 데이터에서는 30%

문제는 정확도였습니다. 벤치마크 숫자 말고 제 데이터로 재 보고 싶어서, 정답이 이미 있는 데이터를 골랐습니다. 옵시디언 노트는 들어 있는 폴더가 정답이고, 코드는 기능별 디렉터리가, 커밋은 feat, fix 같은 접두어가 정답입니다. 따로 라벨을 만들 필요가 없었습니다.

결과는 좋지 않았습니다. 한국어 업무 노트를 12개 도메인으로 나누는 과제에서 30%, 커밋 유형 맞히기에서 32%, 영어 코드 파일도 46% 였습니다. 무작위보다는 낫지만 믿고 맡길 수준은 아니었습니다.

마지막으로 laya 가 원래 잘한다는 티켓 분류로 한 번 더 확인했습니다. 회사 서비스의 GitHub 이슈 100건에 제가 먼저 정답을 매겨 두고(유형 5개, 기능 영역 11개), laya 에는 같은 라벨 정의만 줘서 분류시켰습니다. 두 축을 모두 맞힌 건 30건이었습니다. 유형만 놓고 보면 67% 인데, 전부 bug 라고 찍어도 56% 가 나오는 데이터라 사실상 거의 못 맞힌 셈입니다.

더 곤란했던 건 확신도였습니다. "ㅁㄴㅇ" 같은 의미 없는 입력을 67% 확신으로 bug 라고 했고, 여러 파일을 골라 다운로드하면 한 개만 받아진다는 이슈를 기능 영역 '프로젝트' 로 100% 확신했습니다. 본문에 '프로젝트 파일함' 이라는 단어가 들어 있었을 뿐입니다. 확신도가 높은 것만 골라 쓰면 되지 않을까 싶었는데, 틀린 답도 확신도가 높으니 걸러낼 방법이 없었습니다.

README 에 이미 적혀 있었다

돌아보면 답은 README 에 있었습니다. 맨 위에는 laya 가 Jev 보다 정확하다는 비교표가 있는데, 그 숫자는 해당 벤치마크의 학습 데이터로 파인튜닝한 모델의 결과입니다. 같은 과제에서 튜닝 전 기본 모델은 0.36 으로, 전부 한 가지로 찍는 기준선보다도 낮습니다. 한계를 설명하는 섹션에는 튜닝해서 쓰는 베이스이지 제로샷 판정 엔진이 아니라고 분명하게 적혀 있었습니다.

모델 크기도 다릅니다. laya 는 3억에서 4억 파라미터짜리 BERT 계열 인코더입니다. 한국어 입력은 그보다 작은 다국어 모델로 가고, 사내 용어는 당연히 모릅니다. 라벨을 모아 파인튜닝하면 달라지겠지만, 매번 질문이 바뀌는 제 용도와는 맞지 않았습니다.

Jev 로 다시 재기

Jev 는 TypeSafe 에 가입하려 했더니 인원이 꽉 찼다며 막혀 있었습니다. 대신 OpenRouter 에서 쓸 수 있습니다. 처음에는 OpenRouter 모델 목록에 Jev 가 없어서 없는 줄 알았는데, 목록에서만 빠져 있고 모델 상세 페이지와 API 는 멀쩡히 살아 있었습니다. 목록 하나만 보고 판단하면 안 된다는 걸 여기서 배웠습니다.

한 가지 주의할 점은 채팅 API 가 아니라는 것입니다. OpenAI 호환 엔드포인트로는 호출할 수 없고, 판정 전용 엔드포인트를 씁니다. 요청 형식은 위에서 본 것과 같습니다.

curl https://openrouter.ai/api/alpha/decisions \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "typesafe/jev-1.13", "state": "결제가 두 번 됐어요", "questions": {"team": {"type": "choice", "instructions": "어느 팀이 처리해야 하나?", "criteria": {"billing": "결제와 환불", "technical": "버그와 장애"}}}}'

같은 이슈 100건, 같은 라벨 정의로 돌렸습니다. 비교를 위해 Gemini 3.8 Flash 에 100건을 한 번에 넣어 분류시키는 방식도 같이 쟀습니다.

방식두 축 모두 맞음100건 처리 시간비용
laya (로컬)30%11초무료
Gemini 3.8 Flash, 한 번에 배치85~98%21~75초구독
Jev 1.13, 동시 8개88~89%6초약 0.0046달러

Jev 가 좋았던 점

첫째는 속도입니다. 한 건에 0.3초 정도라 동시에 여러 개를 보내면 100건이 6초 만에 끝납니다. LLM 은 한 번 호출하는 데 20초가 넘게 걸려서 여러 건을 한꺼번에 묶어야 효율이 나오는데, Jev 는 작업하다가 한 건씩 물어봐도 부담이 없습니다.

둘째는 비용입니다. 입력 100만 토큰에 0.042달러이고 출력에는 돈을 받지 않습니다. 이슈 100건을 두 가지 질문으로 분류하는 데 0.5센트가 안 들었습니다.

셋째가 제일 중요한데, 확신도를 믿을 수 있습니다. 확신도가 0.8 이상인 답만 모아 보면 거의 다 맞았습니다. 첫 실행에서는 유형 78건, 영역 62건이 전부 정답이었고, 두 번 돌린 결과를 합쳐도 96건 중 95건이 맞았습니다. laya 에서 가장 아쉬웠던 부분이 정확히 여기였습니다.

정확도 자체는 LLM 과 비슷하거나 조금 낮습니다. 틀린 건 대부분 기능 영역의 경계가 애매한 이슈였습니다. 프로젝트 화면에서 미리보기가 안 된다는 이슈를 '미리보기' 로 볼지 '프로젝트' 로 볼지 같은 것들인데, 정답을 매긴 저도 헷갈렸던 부분입니다.

그래서 이렇게 쓴다

확신도를 믿을 수 있으니 역할을 나눌 수 있습니다. Jev 가 먼저 전부 분류하고, 확신도가 0.8 이상인 건 그대로 씁니다. 나머지만 모아서 LLM 에 한 번에 넘깁니다.

실제로 이슈 328건 전체를 이렇게 처리했습니다. Jev 가 14초 만에 전부 분류했고 그중 174건을 그대로 채택했습니다. 남은 154건은 Gemini 가 40초 동안 처리했습니다. 정답을 알고 있는 100건으로 확인해 보니 92건이 맞았습니다. 결과는 GitHub 라벨로 붙여 두었고, 틀린 건 보이는 대로 사람이 고치면 됩니다.

정리

laya 에 들인 하루가 아깝지는 않습니다. 몇 가지를 배웠습니다.

벤치마크 헤드라인보다 한계 섹션을 먼저 읽어야 합니다. 그리고 정답이 있는 내 데이터로 30분만 재 보면 대부분 판가름이 납니다. 폴더 구조나 커밋 접두어처럼 이미 정답이 붙어 있는 데이터는 생각보다 가까이에 있습니다.

판정 모델에서 정확도만큼 중요한 건 확신도가 맞느냐입니다. 확신도를 믿을 수 있으면 쉬운 건 자동으로 처리하고 어려운 것만 사람이나 큰 모델에 넘기는 구조를 만들 수 있습니다. 확신도가 틀리면 결과 전체를 다시 봐야 해서 쓰는 의미가 없어집니다.

마지막으로, 무료이고 로컬에서 돈다는 장점은 정확도가 받쳐 줄 때만 의미가 있습니다. 이슈 100건에 0.5센트라면 굳이 로컬을 고집할 이유가 없었습니다.