OpenClaw 리포지토리를 무료로 분류(triage)하기 위해 로컬 모델을 활용했습니다!
요약
OpenClaw 리포지토리의 이슈와 PR을 효율적으로 분류하기 위해 Gemma 및 Qwen과 같은 로컬 모델을 활용한 에이전트 시스템 구축 사례를 소개합니다. 폐쇄형 모델의 비용과 가용성 문제를 해결하기 위해 이미 보유한 하드웨어에서 로컬 모델을 사용하여 실시간 분류 및 알림 시스템을 구현하는 방법을 다룹니다.
핵심 포인트
- 로컬 오픈 웨이트 모델을 활용한 비용 효율적인 이슈 분류 시스템 구축
- 에이전트 하네스를 통한 구조화된 출력(Structured Outputs) 활용
- 폐쇄형 모델 의존도를 낮추고 데이터 및 모델 소유권 확보
- 실시간 알림과 배치 처리 간의 트레이드오프 분석
*전기 비용을 제외하면 맥주처럼 무료이며, 이미 하드웨어를 보유하고 있다고 가정합니다
2026년 6월은 사람들이 폐쇄형 모델(closed models)이 언제든 사라질 수 있다는 것을 깨달은 순간으로 기록될 것입니다. Anthropic의 최신 플래그십 모델인 Claude Fable 5가 제거된 사건이 생생하게 기억되는 상황에서, 자신의 AI 스택을 소유하고 모델을 로컬(locally)에서 실행할 수 있는 능력을 갖추는 것이 그 어느 때보다 중요하다는 것을 알 수 있습니다. 특히 AI를 기반으로 비즈니스를 구축하고 있다면 더욱 그렇습니다.
이러한 관점에서, 우리는 Gemma 및 Qwen과 같은 로컬 모델을 에이전트 하네스(agent harness)에서 사용하여 분류(classification) 작업[^1]을 수행하는 방법을 공유하고자 합니다. 이 접근 방식은 분류를 위해 BERT와 같은 모델을 사용하는 것과는 다릅니다. Pi와 같은 에이전트 하네스 내의 로컬 모델은 구조화된 출력(structured outputs)과 함께 사용하여 레이블을 할당할 수 있습니다. 우리가 이 방식을 선택한 이유는 이미 로컬 모델과 하네스를 보유하고 있었으며, 로컬 모델의 성능이 향상됨에 따라 유사한 설정이 점점 더 인기를 끌 것이라고 확신하기 때문입니다.[^2]
우리의 시작점은 OpenClaw 리포지토리의 오픈 소스 기여(open source contributions)였습니다. OpenClaw는 매일 수백 개의 이슈(issues)와 PR(Pull Requests)을 받으며, 이를 유지 관리자(maintainers)에게 분류(triage), 우선순위 지정 및 전달해야 합니다. 저, Onur는 로컬 모델이 OpenClaw와 잘 작동하도록 만드는 작업을 하고 있습니다. 이 특정 버티컬(vertical)의 유지 관리자로서, 저는 모든 P0 이슈에 빠르게 대응해야 합니다.
GPT-5, Opus 또는 Sonnet과 같은 SOTA(State-of-the-Art) 폐쇄형 모델을 사용한다면 이는 꽤 간단한 작업입니다. 하지만 저는 마침 128GB의 통합 메모리, 즉 NVIDIA GB10을 보유하고 있습니다. 그래서 저는 다음과 같은 과제에 도전했습니다:
로컬 오픈 웨이트(open-weight) 모델을 사용하여, 내가 책임지는 이슈에 대해서만 필터링하고 알림을 보내주는 실시간 알림 시스템을 구축할 수 있을까?

만약 매월 200달러인 ChatGPT Pro 플랜을 사용하여 실행되는 OpenClaw 메인 에이전트를 설정해 모든 새로운 이슈나 PR에 대해 작업을 트리거하게 한다면, 제 할당량(quota)을 모두 소진하게 될 것입니다. 대신 2시간 또는 6시간마다 실행되도록 설정할 수도 있습니다. 이렇게 하면 더 긴 기간 동안 이슈를 배치(batch) 처리하게 되므로, 실시간 알림을 지연된 처리와 맞바꾸게 되는 셈입니다.
만약 제가 이미 가동 중인 하드웨어에서 로컬 모델 (local model)로 이를 실행한다면, 거의 즉각적인 알림을 받을 수 있을 뿐만 아니라 무료로(정확히는 전기 요금만큼의 비용으로) 수행할 수도 있을 것입니다.
우리는 분류(triage)해야 하는 이슈의 카테고리를 나타내는 유한한 라벨 세트를 고안했고, 그 후 로컬 모델을 사용하여 각 이슈를 local_models, self_hosted_inference, acp, agent_runtime, codex, ui_tui 등과 같은 카테고리 중 하나로 분류합니다.[^3]
하지만 풀 리퀘스트 (pull requests)는 어떻게 분류할까요? 도구 JSON 스키마 (tool JSON schema)와 주제를 열거형 (enum)으로 포함하여 Chat Completions 엔드포인트에 단일 요청을 보내는 단순한 방식일까요?
어느 정도는 그렇습니다. 하지만 지금은 2023년이 아니라 2026년이며, 우리에게는 에이전트 (AGENTS)가 있습니다. 더 잘할 수 있습니다!
로컬 모델 선택을 위해 우리는 gemma-4-26b-a4b와 qwen3.6-35b-a3b를 테스트했습니다. 성능 최적화를 통해 두 모델 모두 로컬에서 초당 수백 개의 토큰을 생성할 수 있습니다.
우리는 분류 실행을 구동하기 위해 에이전트 하네스 (agent harness)를 사용합니다. 이를 위해 우리는 로컬 모델 엔드포인트를 호출할 수 있는 하네스로 pi를 묶었습니다.
에이전트는 기본적으로 첫 번째 프롬프트에서 PR 제목, 본문, 그리고 PR 디프 (diff)의 잘린 발췌본을 전달받습니다. 그런 다음, 에이전트는 (코드베이스를 살펴봐야 할 경우를 대비해) OpenClaw 리포지토리에 대해 읽기 전용 작업을 수행하기 위해 bash 도구를 사용하거나, 최종 분류 결과를 제출하기 위해 final_json 도구를 사용할 수 있습니다.
이러한 고처리량 (high-throughput) 환경에서 실행되는 로컬 모델에 전체 bash 권한을 부여하고 싶지는 않을 것입니다. 프롬프트 주입 (prompt-injected)된 이슈나 PR이 모델을 분류와 무관한 동작을 하도록 유도할 수 있기 때문입니다.
그러한 이유로 우리는 bash 대신 reposhell을 사용합니다. 이는 OpenClaw 리포지토리에 대해 읽기 전용 작업(ls, find, cat, grep 등)만 허용하는 제한된 bash 스타일의 셸입니다. 모델은 자신이 bash를 사용하고 있다고 생각하지만, 허용되지 않는 모든 작업은 거부됩니다:
reposhell bound cwd=/repo/openclaw repos=openclaw
type help for allowed commands; exit or quit to leave
reposhell /repo/openclaw> help
...
이것이 중요하게 작용했던 구체적인 사례가 있습니다. 저장된 세션 예시 중 하나에서, qwen3.6-35b-a3b 모델은 Fix Kimi tool-call rewriting stop reason handling이라는 제목의 openclaw/openclaw#84621을 분류하고 있었습니다.
사고 과정(thinking block)을 보면, 모델은 변경된 경로인 extensions/kimi-coding 때문에 그것이 타당해 보여서 처음에 coding_agent_integrations를 고려했음을 보여줍니다. 모델은 reposhell을 사용하여 ls extensions, ls extensions/kimi-coding, cat extensions/kimi-coding/package.json과 같은 단순한 읽기 전용(read-only) 명령어로 로컬 리포지토리(local repo)를 조사했습니다. 해당 패키지 메타데이터를 통해 해당 확장이 실제로는 OpenClaw Kimi 프로바이더(provider) 플러그인인 @openclaw/kimi-provider임을 확인했습니다. 이에 따라 모델은 최종 라벨을 inference_api 및 tool_calling으로 수정하였고, coding_agent_integrations는 명시적으로 제외했습니다.
앞서 언급했듯이, 저희는 읽기 전용 작업만 수행하고 분류 결과(classification output)를 반환할 수 있는 특정 pi 설정을 번들로 제공합니다. 저희는 이를 본 프로젝트의 메인 프로젝트인 localpager의 이름을 따서 localpager-agent라고 부릅니다. 각 PR(Pull Request)과 이슈(issue)는 프롬프트(prompt)를 생성하며, 이는 아래와 같이 다른 인자(args)와 함께 CLI로 전달됩니다:
localpager-agent \
--model "<model-id>" \
--base-url "<openai-compatible-base-url>" \
...
그렇다면 들어오는 PR/이슈와 Discord의 최종 알림 사이에서 이 모든 것을 오케스트레이션(orchestrate)하는 것은 무엇일까요?

이와 관련된 오케스트레이션은 매우 간단합니다. 오직 분류(classification) 단계에만 LLM(대규모 언어 모델)이 관여합니다.
- 저희는 openclaw/gitcrawl을 사용하여 해당 리포지토리의 로컬 미러(local mirror) 역할을 수행하게 합니다. 새로운 PR(Pull Request)이나 이슈(issue)가 발생할 때마다, 각 항목은 동일한 형태로 정규화(normalized)되어 localpager 자체의 SQLite 데이터베이스에 기록됩니다. 만약 해당 항목이 새로운 것이라면, localpager는 이를 위한 분류(classification) 작업을 생성합니다.
- 그 후 워커(worker)가 해당 큐(queue)에서 작업을 가져옵니다. 워커는 이슈 또는 PR의 제목, 본문, 라벨(labels), 작성자, 상태(state)를 포함하고, 선택적으로 댓글(comments), 변경된 파일(changed files), 선택된 diff 발췌본(diff excerpts)을 포함하는 GitHub 컨텍스트 객체(context object)를 구축합니다. 이는 로컬 모델이 대부분의 경우 GitHub을 직접 탐색하거나 URL을 직접 열 필요가 없음을 의미합니다. 모델에게는 모든 관련 컨텍스트가 전달됩니다.
- 이 컨텍스트 객체는 프롬프트(prompt)로 렌더링되어 이전 섹션에서 설명한 바와 같이
localpager-agent로 전달됩니다. 에이전트는 사고(think)하고 reposhell을 사용할 수 있지만, 최종적으로는 정의된 스키마(schema)에 따라 분류 결과(classification result)를 출력해야 합니다. 출력 결과는 다시 localpager SQLite 데이터베이스에 저장되며, 사용자가 설정한 알림 정책(예: 특정 주제에 대해서는 알림을 받되, 다른 주제는 제외)에 따라 Discord로 전달됩니다.
아래는 localpager의 전체 아키텍처(architecture)를 보여주는 그림입니다:

이 아키텍처는 세미 에이전틱(semi-agentic)합니다. 라벨링(labeling)은 에이전틱하게 수행되는 반면, 알림을 보내는 것은 결정론적 규칙(deterministic rules)에 의해 처리됩니다. 이는 작업의 가장 단순한 부분에서 추론(inference)의 필요성을 제거함으로써 알림 파이프라인을 더 빠르게 만들기 위함입니다. 로컬 추론(local inference)은 비용이 들지 않지만, 각 작업에는 자원 경합 비용(resource contention cost)이 따릅니다. 즉, GPU 대역폭은 추론이 반드시 필요한 작업에 예약되어야 합니다. 이는 또한 알림으로 인한 오류 가능성을 줄여줍니다.
솔직히 말씀드리면, 이 시스템의 초기 로컬 버전들은 노이즈가 많았습니다. 처음 테스트된 모델인 gemma-4-e4b-it는
엔드 투 엔드 (end-to-end) 로컬 파이프라인을 작동시키는 데는 유용했지만, PR(Pull Request)이나 이슈(issue)에 관련 없는 레이블을 너무 많이 붙이는 경향이 있었습니다. 잘못된 레이블(False positive labels)은 Discord 피드를 소란스럽게 만들고, 제가 올바른 이슈에 집중하는 것을 방해합니다. 이로 인해 우리는 아래의 330행 평가 데이터셋(evaluation set)에서 gemma-4-26b-a4b 및 qwen3.6-35b-a3b를 포함한 더 큰 로컬 모델들을 테스트하게 되었습니다.
초기 프롬프트 작업 단계에서는, 이전 데이터셋 레이블을 생성하기 위해 antirez의 DS4 구현[^4]을 통해 DeepSeek-V4-Flash를 사용하기도 했습니다. 해당 설정은 CUDA 기반의 DS4 서버를 사용했습니다. 하지만 DS4는 실행할 때마다 레이블링이 일관되지 않았기 때문에, 결국 레이블러(labeler)로서의 DS4 사용은 포기했습니다. 또한 DS4는 우리 하드웨어에서 충분한 처리량(throughput)을 확보하기에는 너무 컸기 때문에, 주요 localpager-agent 모델로 고려하지도 않았습니다. DS4 서버는 최대 동시성(concurrency)이 1인 상태에서 초당 약 14개의 토큰을 생성했습니다.
모델 성능을 테스트하기 위해, 우리는 330개의 GitHub 이슈와 PR을 선정하여 레이블을 생성했습니다. 각 항목은 5번씩 레이블링되었으며(GPT-5.5 3회, Opus 4.8 2회), 모델들이 서로 일치해야만 레이블로 채택되었습니다. 이 과정에는 수동 판정(hand adjudicating), 레이블 정의 개선, 그리고 모델을 위한 내부 제품 설계 선택 사항 강조 작업이 포함되었습니다. 이를 통해 우리는 더 작은 모델들과 비교할 수 있는 안정적이고 재현 가능한 레이블 세트를 확보할 수 있었습니다.
이 평가 데이터셋에서 유용한 결과를 얻기 전까지 gemma-4-26b-a4b나 qwen3.6-35b-a3b에 대한 프롬프트 최적화(prompt optimization)를 수행할 필요는 없었습니다. 동일한 라우팅 프롬프트(routing prompt)를 사용했을 때, Gemma는 더 높은 재현율(recall)과 행당 더 짧은 실제 소요 시간(wall-clock time)을 보였으며, Qwen은 더 높은 정밀도(precision), 더 높은 완전 일치(exact match)율, 그리고 더 적은 오탐(false positives)을 기록했습니다. 우리는 또한 DeepSeek-V4-Flash를 실행했습니다.
동일한 데이터 세트를 기준으로 삼았습니다. 이 모델은 오탐(false positives)이 가장 적었지만, 모델 크기와 처리량(throughput) 문제로 인해 NVIDIA GB10에서 이러한 작업을 실시간으로 실행하기에는 비현실적입니다. 각 행(row)은 여러 개의 레이블(label)을 가질 수 있으므로, 오탐(false positives)과 미탐(false negatives)은 모든 행에 걸친 총 레이블 수로 계산됩니다. 아래의 Qwen 결과는 모델이 final_json을 호출하기 전에 출력 토큰(output tokens)이 소진되어 발생한 구조화된 출력(structured-output) 실패를 재시도한 후의 결과입니다.
Gemma 및 Qwen의 경우, 반복 실행 지표(repeated-run metrics)는 3회 실행에 대한 평균 ± 표본 표준 편차(sample standard deviation)를 보고합니다. DeepSeek-V4-Flash는 기준으로 삼기 위해 한 번 실행되었습니다.
| 지표 (Metric) | gemma-4-26b-a4b |
|---|---|---|qwen3.6-35b-a3b |
|---|---|---|DeepSeek-V4-Flash |
|---|---|---||
| 정밀도 (Precision) | 0.716 ± 0.010 | 0.831 ± 0.007 | 0.938 |
| 재현율 (Recall) | 0.905 ± 0.004 | 0.818 ± 0.006 | 0.714 |
| ... |
여기서 제시된 처리량(throughput)과 실제 소요 시간(wall-clock) 수치는 이 하드웨어에서 해당 모델들이 낼 수 있는 확정적인 최대 성능 수치가 아닙니다. 이는 우리가 당시 사용 가능한 최적화 기술을 적용하여 사용했던 설정값입니다. 예를 들어, 별도의 테스트에서 gemma-4-26b-a4b는 동시성(concurrency) 32를 지원하며 초당 700개 이상의 총 출력 토큰(aggregate output tokens)에 도달하기도 했습니다.
Gemma 벤치마크를 위해, 우리는 이 설정에서 사용 가능한 최적화 기술을 사용하여 vLLM으로 gemma-4-26b-a4b를 서빙했습니다. 그 중 큰 비중을 차지하는 것은 NVFP4 양자화(quantization)입니다. GB10급 Blackwell 하드웨어에서 이는 단순히 모델 파일 크기를 줄이는 것뿐만 아니라, Q4_K_M과 같은 휴대용 GGUF 양자화보다 NVIDIA/vLLM 실행 경로를 더 직접적으로 사용할 수 있는 하드웨어 친화적인 포맷임을 의미합니다. 실제로 이는 메모리 트래픽을 줄이고 배치(batching)를 위한 더 많은 여유 공간을 확보함을 의미합니다. 우리는 또한 접두사 캐싱(prefix caching), FP8 KV 캐시(KV cache), CUTLASS MoE 백엔드, 그리고 언어 모델 전용 모드(language-model-only mode)를 활성화했습니다. 전체 330행 실행은 동시성 16에서 약 7.5분 만에 완료되었습니다.
앞서 언급했듯이, 새로운 이슈(issue)나 PR(Pull Request)이 발생할 때마다 로컬 모델로 작업을 실행하는 대신, OpenClaw에서 실행되는 GPT-5.5와 같은 SOTA(State-of-the-Art, 최첨단) 클라우드 모델을 사용하여 n시간마다(예: 2시간마다) 배치 작업(batch job)을 실행함으로써 동일한 결과를 얻을 수 있습니다.[^5]
이 경우 ChatGPT Pro 플랜이 필요합니다. 모델이 SOTA이기 때문에, 2시간 분량의 이슈/PR을 함께 배치 처리하더라도 여전히 상당히 우수한 성능을 보여줄 것으로 기대할 수 있습니다.
로컬 분류기(local classifier)가 GPT-5.5에 비해 얼마나 잘 작동하는지 확인하기 위해, 우리는 두 모델을 동시에 실행하며 2시간마다 GPT-5.5가 오탐(false positives)과 미탐(false negatives)을 판정하도록 했습니다.
안전을 위해, 우리는 결과를 보고할 공개 리포지토리(public repo)에만 접근 권한을 가진 샌드박스(sandbox) 환경에서 OpenClaw 작업을 실행합니다. 우리의 경우, OpenClaw 작업이 기계 판독 가능 파일(machine-readable file)을 업데이트하도록 하고, 간단한 스크립트가 Codex가 할당한 레이블(labels)을 읽어 오탐/미탐 상태를 계산하도록 했습니다. 출력 예시:
미탐 (False negatives)
-
Issue #88499 openai-responses provider: 404 on previous_response_id when store=false (default)
-
inventory area: OpenAI-compatible/proxy; notifier topics: agent_runtime, api_surface, sessions; notification: none
오탐 (False positives)
-
PR #88275 fix(models-config): allow self-hosted providers without apiKey in models.json (#88267)
-
notifier interest: i0; topics: self_hosted_inference, local_model_providers, config; notification: sent
-
PR #88266 refactor: extract model catalog core package
-
notifier interest: i1; topics: config, api_surface, local_model_providers; notification: sent
-
PR #88247 feat: add hosted model providers
AI 자동 생성 콘텐츠
본 콘텐츠는 HuggingFace Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기