Jermes: 홀드아웃에서 이득이 재현된 절차만 스킬로 승격하는 엔진과, 실측이 찾은 게이트의 구멍들
에이전트 실행 기록에서 재사용 절차를 뽑되, 작성에 쓰지 않은 홀드아웃 케이스에서 이득이 재현될 때만 스킬로 승격하는 자가학습 엔진입니다. dev에서만 오른 스킬의 승격, 1건짜리 홀드아웃, 부호가 뒤집히는 평균 같은 게이트의 구멍을 실측으로 막았고, 실제 XGEN 에이전트 노드의 정답이 0/6에서 6/6이 됐습니다.
Key Results
- 44.2% → 88.9%
- 누적 정답률(학습 없음 대비 학습)실제 에이전트 플랫폼 사용자 질문 190건을 콜드 스타트로 순차 투입, 같은 순서·같은 지점 비교
- 5/20 → 20/20
- 계산형 절차 정답할인·부가세·반올림 절차, 원시 LLM 대비 단조된 도구
- 0/6 → 6/6
- 실제 에이전트 노드 정답같은 과제·모델·노드에서 검증된 스킬 회상만 OFF/ON, 소요 66초 → 56초
에이전트가 실행 경험에서 재사용할 절차를 뽑아 스킬로 저장하는 기능이 주목받던 시점에, 대표적인 오픈소스 구현인 Hermes Agent를 소스 수준으로 읽었습니다. 스킬을 만드는 경로는 정교했지만, 만들어진 스킬이 실제로 도움이 되는지 확인하는 단계는 없었습니다. 스킬은 사용·조회·수정 횟수와 마지막 사용 시각, 그리고 LLM 리뷰로 유지되거나 정리됐습니다. 자주 조회됐다는 것은 효과가 있었다는 증거가 아니고, 검증 없이 저장되는 스킬은 프롬프트 인젝션이 영속화되는 통로가 되기도 합니다.
Jermes는 이 빈자리를 겨냥한 개인 프로젝트입니다. 슬로건은 "Hermes learns. Jermes proves."이고, 원칙은 하나입니다. 스킬이든 도구든 기억이든 그럴듯해 보여서가 아니라 본 적 없는 케이스에서 측정됐을 때만 신뢰한다는 것입니다. 7월 말부터 약 한 달간 런타임 의존성 없이 표준 라이브러리만으로 엔진을 만들었고, 실제 XGEN 에이전트 노드와 실제 사용자 질문으로 효과를 측정했습니다.
이 글은 승격 판정을 설계한 방식, 실측이 그 게이트에서 찾아낸 구멍들, 결정론적 절차를 실행 가능한 도구로 단조한 이유, 검증된 스킬이 정작 모델에 닿지 않던 전달 경로 결함, 그리고 실제 노드에서 0/6이 6/6이 된 정면 비교를 다룹니다.
검증 없는 승격이라는 빈자리
조사 대상은 셋이었습니다. Hermes Agent는 스킬 자동 생성 경로가 둘(대화 중 유도, 도구 호출이 10회를 넘으면 백그라운드 리뷰)이지만 생성된 스킬을 검증하지 않았습니다. 다른 오픈소스 메모리 엔진은 신뢰 신호와 쿼리 해시 기반 25% 홀드아웃 게이트로 기억을 정교하게 다뤘지만 스킬 자동 생성이 없었습니다. XGEN에는 기억을 스킬로 승격하는 경로 자체가 없었습니다. 한편 Agent Skills가 공개 표준으로 넓게 채택되고 있었는데, 표준의 검증기는 프론트매터 문법과 이름만 확인하고 효능은 정의하지 않았습니다. 승격 전 검증은 누구도 차지하지 않은 자리였습니다.
설계의 출발점은 "이 스킬이 도움이 된다"를 어떻게 측정하는가였습니다.
| 선택지 | 장점 | 단점 | 판단 |
|---|---|---|---|
| 사용 빈도로 유지 | 구현이 간단하고 측정 비용이 없음 | 조회 빈도는 효과의 증거가 아님 | 기각 |
| LLM judge로 스킬 품질 채점 | 정답이 없는 과제에도 적용 가능 | 판정이 채점 모델에 종속 | 기각 |
| 스킬 유무를 재생 비교, 기계 채점, 홀드아웃 확인 | 판정이 모델과 독립, 일반화 여부를 직접 확인 | 케이스 수 × 2회의 재생 비용 | 채택 |
파이프라인과 하나뿐인 판정 함수
RunTrace (끝난 실행)
├─ remember() 교훈과 정제된 기억만 (원시 도구 출력은 기억으로 굳히지 않음)
├─ signals() 복잡한 성공 / 실패 후 복구 / 사용자 교정 탐지
├─ draft() LLM 초안 (3층 복구 + 앙상블)
├─ curate() 중복 제거 · 안전 필터 · 신규보다 패치 우선
├─ gate() 규약 → 홀드아웃 재생 → promoted | staged | rejected
├─ ledger append-only 원장 (출처 · 버전 · 롤백)
└─ recall() 검증된 스킬과 관련 기억을 검증 라벨과 함께 컨텍스트로게이트는 케이스를 dev와 holdout으로 나누고(기본 비율 25%), 스킬이 없을 때와 있을 때를 각각 재생해 점수를 비교합니다. 채점은 기대 문자열 포함, 정규식, 금지 문자열 같은 기계 검사로만 하고 LLM judge는 쓰지 않습니다. 세 가지 판정은 한 함수에서만 나오게 했습니다. 스킬은 확률적 개선으로, 도구는 전건 통과로 재지만 세 낱말의 뜻은 같아야 하고, 두 곳에서 따로 계산하면 언젠가 어긋나며 어긋난 쪽이 조용히 이기기 때문입니다.
def decide(measured: bool, dev_ok: bool, holdout: str) -> str:
if not measured:
return "staged" # 못 쟀다 (실패했다는 뜻이 아님)
if not dev_ok:
return "rejected" # dev에서도 도움이 안 됨
if holdout == "regressed":
return "rejected" # 감춘 케이스에서 나빠짐: 외운 것
if holdout == "confirmed":
return "promoted" # 감춘 케이스에서도 좋아짐
return "staged" # dev에서만 좋아짐: 사람이 판단실측이 찾아낸 게이트의 구멍들
게이트는 처음부터 이 모양이 아니었습니다. 대부분의 규칙은 실측에서 구멍이 드러난 뒤에 추가됐습니다.
첫 번째 구멍은 실제 XGEN 실행 기록으로 학습하던 중에 나왔습니다. 처음에는 RAG에 근거한 워크플로우를 LLM 단독으로 재생하니 baseline이 0이라 어떤 스킬도 점수를 올릴 수 없었습니다(변별력 보정 후 0/8). 검색은 읽기 전용이라 부작용이 없다는 점에 착안해 검색만 다시 실행하는 도구 동반 재생으로 바꾸자 baseline이 dev 0.417, holdout 0.500으로 살아났고, 실행 기록에서 나온 스킬 두 개가 처음으로 측정됐습니다. 그런데 두 스킬은 dev에서만 각각 +0.067, +0.033이 올랐고 holdout은 +0.000이었는데도 승격됐습니다. 기존 게이트가 holdout에 "악화 금지"만 요구했기 때문입니다. 스킬이 작성된 케이스에 맞춘 것을 검증됨으로 부르는 과적합 구멍이었고, holdout 이득이 0보다 클 때만 승격하고 dev에서만 오른 것은 staged로 사람에게 넘기도록 바꿨습니다.
두 번째는 케이스를 나누는 방식이었습니다. 처음에는 케이스마다 해시로 동전을 던져 holdout을 정했는데, 케이스 8개일 때 10% 확률로 holdout이 0개가 됐고 그러면 게이트는 무엇을 넣어도 승격할 수 없었습니다. 해시로 정렬한 뒤 비율만큼 잘라 결정적이면서 비율을 지키고, 양쪽에 최소 1개를 보장하도록 바꿨습니다. 그다음에는 케이스가 4개면 holdout이 1건이라, 운 좋게 그 한 건만 맞은 스킬이 승격되는 일이 생겼습니다. 같은 스킬이 케이스 8건에서는 rejected였습니다. 승격에 필요한 holdout 최소 개수를 2로 두고, 미달이면 staged로 보내며 몇 건이 더 필요한지 알려 주게 했습니다.
세 번째는 평균의 한계였습니다. 같은 스킬이 케이스 12건에서 +0.056, 28건에서 -0.016으로 부호가 뒤집혔습니다. 숫자가 적혀 있으니 잰 것처럼 보이지만, 실은 어떤 케이스가 뽑혔는지만 말하고 있었습니다. 한 건은 크게 좋아지고 다른 한 건은 조금 나빠지는 상황을 일부러 만들어 보니 holdout +0.100으로 승격이 나왔는데, 갈린 방향으로 보면 1건 개선, 1건 악화(부호검정 p=0.750)였습니다. 그래서 짝지은 자료에 부호검정을 더하고, holdout에서 좋아진 건수가 나빠진 건수를 넘어야 승격되게 했습니다. 분포 가정이 없고, 케이스가 적어도 표준 라이브러리로 정확히 계산된다는 점에서 이 검정을 골랐습니다. p<0.05 같은 엄격한 문턱은 걸지 않았습니다. 케이스가 적은 환경에서는 진짜로 작은 개선까지 죽이는데, 실제로 그날 유일한 자동 승격이 dev +0.037이었기 때문입니다.
네 번째는 못 잰 것을 "도움이 안 된다"로 판정한 경우입니다. 예산이 떨어지면 재생이 전부 빈 답이 되고, 스킬을 넣으나 빼나 같은 점수라 +0.000이 나옵니다. 큰 세션 하나에서 LLM 호출 5회로 후보 5건을 그렇게 거절한 것을 발견하고, 측정 불가는 staged로 보내고 이유를 적게 했습니다.
이렇게 다듬은 게이트를 실험으로 확인했습니다. 진짜 효과가 있는 스킬(dev와 holdout 모두 0.000 → 1.000)은 승격됐고(감도), 무용한 스킬과 과적합 스킬은 거절하고 진짜 스킬과 완전한 표는 승격해 4/4로 구분했습니다(특이도). 이득 격차가 크면 rejected("외운 증거"), 작으면 staged("못 증명함")로 등급이 나뉘었습니다. 라이브 승격이 0건인 것은 기계 고장이 아니라 재료 문제라는 것도 이 실험으로 확정됐습니다.
판단은 스킬로, 결정론적 절차는 도구로
절차의 성격에 따라 형태를 나눴습니다. 판단이 필요한 절차는 문서형 스킬로 남기고, 반복되는 결정론적 절차는 Python 스크립트로 단조해 케이스에 직접 실행시켜 판정합니다. 입력을 넣고 출력을 비교하므로 채점에 LLM이 전혀 개입하지 않습니다. 실패하면 오류를 모델에 전달해 다시 쓰게 하지만 dev 실패만 보여 주어 holdout을 정직하게 유지합니다. 도구는 검증 케이스를 내장해 이후 LLM 호출 없이 언제든 회귀 검사를 할 수 있고, 수리본이 원본보다 나쁘면 폐기합니다.
같은 20문항으로 세 방식을 비교했습니다. 문자열 구성 같은 쉬운 절차는 세 방식 모두 20/20이었지만, 도구는 1.4초에 토큰 0개, 원시 LLM은 6.6초에 720토큰이었습니다. 할인, 부가세, 반올림이 결합된 어려운 절차에서는 원시 LLM이 5/20, 절차서를 프롬프트에 넣은 문서형 스킬이 6/20, 단조된 도구가 20/20이었습니다. 절차를 문서로 적어 줘도 모델이 산술을 틀리면 소용이 없고, 실행하면 해결됩니다. 단조 비용은 1회 4.2초였고 손익분기는 약 8회 사용이었습니다. 이 이득은 결정론적 절차에 한정되며 판단은 여전히 스킬의 몫입니다.
초안 작성은 일부러 약한 모델에도 맡겨 봤습니다. 질문은 "스킬 품질이 모델 지능에서 나오는가, 선택압에서 나오는가"였습니다. 여러 샘플을 생성하되 샘플마다 관점을 바꾸고, 근중복을 합치고, 파싱 실패를 3층으로 복구한 뒤 게이트를 통과시키자, 같은 실행 기록과 같은 재현 벤치에서 소형 상용 모델부터 27B급 모델, 상위 Claude Sonnet까지 모두 동일하게 verified 스킬 정확히 1개만 승격했습니다(약한 모델은 3~8초). 품질은 결정론적 선택압에서 나온다고 판단한 근거입니다. 다만 약한 모델이 few-shot 예시를 그대로 복제하는 현상이 라이브에서 나와 예시 유출 가드를 추가했습니다.
검증된 스킬이 모델에 닿지 않았다
게이트가 엄밀해도 결과가 실행하는 모델에 전달되지 않으면 의미가 없습니다. 전달 경로를 실험하자 결함이 나왔습니다. 원장은 스킬의 이름, 설명, 검증 여부를 모두 주는데 스킬 목록 함수가 이름만 남기고 나머지를 버렸고, 프롬프트에는 스킬 카탈로그를 미리 보여 주는 섹션도 없었습니다. 이름만 줄 때 12문항 중 3문항이 맞고 평균 7.8턴이 걸렸는데, 이름과 설명과 검증 라벨을 카탈로그 섹션으로 노출하자 12/12에 3.8턴이 됐습니다.
실제 실행 기록에서 게이트까지 가는 경로도 끊겨 있었습니다. 같은 워크플로우의 실행을 실행 ID의 이름 관례로 찾고 있었는데, 전체 1,158건 중 관례를 따르는 것은 17%뿐이라 나머지는 케이스가 늘 4개 미만이었습니다. 실행 입출력 기록의 워크플로우 ID라는 관계로 찾도록 바꾸자 동종 실행 판별이 97%, 케이스 구성이 93%가 됐습니다. 입력 추출도 실제 행의 모양을 가정과 다르게 읽어 표본 300행에서 0건이었습니다. 둘 다 가정한 모양에 코드를 맞춘 같은 종류의 실패였습니다.
그다음 실제 XGEN 에이전트 노드로 정면 비교를 했습니다. 실제 벤치, 실제 게이트 승격, 실제 원장, 실제 스킬 레지스트리 회상, 노드가 스킬을 로드하는 전 구간을 실물로 연결하고, 같은 과제, 모델, 노드에서 회상만 끄고 켰습니다. 첫 측정에서는 회상을 끈 쪽도 4/6이 나왔는데, 스킬 레지스트리가 프로세스 전역이라 앞선 시도의 회상 설치가 남아 있었기 때문입니다. 컨테이너를 재기동해 다시 측정하자 OFF는 0/6이었습니다. ON에서 남은 2건의 실패는 시스템 결함이 아니라 문항 표현이 모호해 부서 코드를 회사명으로 읽은 것이었고, 문항을 고치자 6/6이 됐습니다. 소요 시간도 66초에서 56초로 줄었습니다. 회상이 없으면 에이전트가 자료를 찾아 헤매다 거부하고, 있으면 규칙을 꺼내 바로 답했습니다.
마지막으로 자가 개선 루프가 실제로 닫히는지를 봤습니다. 실제 에이전트 플랫폼의 사용자 질문 190건(정답은 실제로 그 질문을 처리한 워크플로우)을 콜드 스타트에서 한 건씩 흘려보내며 매번 "고른다, 채점한다, 그다음 기록한다" 순서를 지켰습니다. 처음에는 라우터가 맞았을 때만 기록하게 만들었다가, 초반에 거의 다 틀리니 아무것도 쌓이지 않아 두 조건의 곡선이 똑같이 움직이는 것을 보고 실제로 처리한 쪽을 기록하도록 고쳤습니다. 이 실험은 라우팅이 좋아지는지만 재며, 무엇이 성공했는지를 안다고 가정한다는 점에서 낙관적인 조건입니다.
결과
| 지표 | 이전 | 이후 | 측정 조건 |
|---|---|---|---|
| 실제 에이전트 노드 정답 | 0/6 (회상 OFF) | 6/6 (회상 ON) | 같은 과제·모델·노드, 컨테이너 재기동 후 A/B |
| 같은 비교의 소요 시간 | 66초 | 56초 | 같은 조건 |
| 스킬 전달 | 3/12, 평균 7.8턴 | 12/12, 평균 3.8턴 | 이름만 노출 vs 이름·설명·검증 라벨 노출 |
| 계산형 절차 정답 | 5/20 (원시 LLM), 6/20 (문서형 스킬) | 20/20 (단조 도구) | 할인·부가세·반올림 20문항 |
| 누적 라우팅 정답률 | 44.2% (학습 없음) | 88.9% (학습) | 실제 사용자 질문 190건, 같은 순서 콜드 스타트 |
| 동종 실행 판별 | 17% | 97% | 전체 실행 기록 1,158건 |
누적 정답률은 두 조건이 같은 질문을 같은 순서로 받았으므로 같은 지점의 차이로 비교했습니다. 대조군도 뒤로 갈수록 오르는데 이는 학습이 아니라 데이터 순서 때문이라, 기울기가 아니라 같은 지점의 차이를 지표로 삼았습니다.
회고와 남은 과제
게이트는 판정할 재료가 있어야 작동합니다. 실제 로컬 세션에서 학습했을 때 초안 스킬 4개가 모두 holdout 하락으로 거절됐습니다(예: dev 0.333 → 0.250, holdout 0.500 → 0.333). 다른 시스템이었다면 유지됐을 스킬들이고, 이것이 의도한 동작입니다. 반대로 라이브 XGEN 실행 기록에서 유기적으로 승격된 스킬은 측정 시점까지 0건이었습니다. RAG 컬렉션의 임베딩 차원 불일치로 검색이 실패했고, 초안 모델이 기존 실행에서 재사용할 절차를 찾지 못하는 재료 빈곤이 겹쳤습니다.
또 하나의 교훈은 "0"에는 반드시 이유가 함께 기록돼야 한다는 것입니다. 저장된 노드의 실행 ID 접두 필터가 프로비저닝 당시 값으로 굳어, 스케줄이 세 번 연속 "성공"을 보고하면서 아무것도 배우지 않은 적이 있습니다. 학습 명령이 판정을 출력하는데 실제로는 채점 함수가 항상 0을 반환하고 케이스가 비어 있던 적도 있습니다. 이후 0건의 이유(스캔 수, 접두 통과 수, 이미 학습한 수, 모델이 빈 목록을 반환한 경우)를 숫자로 내게 하고, 실제 사용자 흐름이 한 번도 도달하지 않는 함수를 찾는 감사 도구를 따로 두었습니다. 남은 과제는 도구형 스킬을 MCP로 컴파일해 플랫폼에 연동하는 것, 실행 종료 시 결과를 원장에 되먹이는 훅, 그리고 라우팅 실험이 가정한 "무엇이 성공했는가"를 호스트에서 얻는 경로입니다.