몇 주째 Hermes를 컨테이너로 띄우고 채널마다 봇을 붙여 써왔는데, 정작 "이게 결국 뭐냐"를 한 번도 개념부터 세운 적이 없었다. 공식 문서를 처음부터 읽으며 되물었다. 기능 목록이 아니라 실체, 자가개선을 관측하는 법, /goal 루프의 동작 원리, Claude Code와의 경계, Mattermost에서의 실제 요청 흐름을 확정한 기록이다.

Hermes는 모델이 아니라 "모델을 굴리는 프로그램"이다

가장 큰 오해부터 풀어야 했다. Hermes는 LLM이 아니다. 공식 아키텍처에서 껍데기를 걷으면 중심에 AIAgent라는 클래스 하나가 있고, 하는 일은 단순한 루프다.

  1. 시스템 프롬프트와 도구 목록을 조립한다
  2. LLM API를 호출한다 (OpenRouter, OpenAI, Anthropic, 로컬 모델 등 아무거나)
  3. 모델이 "도구 써"라고 하면 도구를 실행한다 (파일 편집, 웹검색, 코드 실행)
  4. 결과를 대화에 붙이고 2번으로 돌아간다
  5. 끝나면 메모리를 디스크에 저장한다

model=...에 남의 모델을 꽂아 넣고 대신 굴려주는 런타임이다. 모델이 자기가 아니라, 모델을 쥐고 도구를 먹이는 하네스다. 이 한 줄을 잡으니 나머지가 전부 제자리를 찾았다.

진입점만 여럿, 몸통은 AIAgent 하나

CLI, 메신저 게이트웨이, OpenAI 호환 API 서버, 파이썬 라이브러리. 문서에는 진입점이 여러 개 나열되지만 전부 같은 AIAgent를 감싸는 껍데기다.

진입점 정체
CLI 터미널에서 대화
Gateway Telegram·Slack·Mattermost 등의 메시지를 AIAgent로 전달
API Server OpenAI 호환 API로 외부 노출
Python Library from run_agent import AIAgent

컨테이너로 채널마다 봇을 붙이는 구성이 바로 이 그림이다. 컨테이너 하나가 Gateway 진입점을 프로필별로 여러 개 돌린다. 채널의 "봇"은 Mattermost가 만든 것이 아니라 AIAgent가 어댑터를 통해 그 채널에 붙어 있는 것이다.

세 번째 정체성은 끝나도 잊지 않는다는 점이다. 대화는 메모리로, 잘 풀린 절차는 스킬로 스스로 저장한다. "오래 돌릴수록 유능해진다"의 실체는 모델이 아니라 모델 옆에 쌓이는 파일들이다.

"자가개선"은 마법이 아니라 파일시스템 이벤트다

여기가 이 에이전트의 세일즈 포인트인데, 추상적으로 들려서 실제로 어디서 확인되는지를 짚었다. 결론은 네 곳에서 눈으로 보인다.

  1. 스킬 디렉터리 — 에이전트가 만든 스킬은 ~/.hermes/.../skills/에 파일로 떨어진다. ls -lt로 최근 생성분, .archive/로 치워진 것을 본다.
  2. 메모리 파일memories/MEMORY.mdUSER.md. 어제와 오늘의 diff가 곧 "무엇을 새로 기억했나"다.
  3. hermes curator run --dry-run — 변경 없이 정리·통합 계획만 리포트한다.
  4. config.yamlcurator: 섹션 — 통제 여부.

여기서 짚어둘 함정 하나. 내장 메모리는 기업 지식 저장소가 아니다. MEMORY.md는 2,200자 (약 800토큰), USER.md는 1,375자 한도다. 초과하면 조용히 버리는 게 아니라 에러를 내고 에이전트가 직접 정리한다. 즉 의도적으로 작은 "개인화"용이다. 규정·매뉴얼 같은 조직 지식은 여기 넣는 게 아니라 별도 RAG로 외부화해야 한다. 앞서 사내 규정 문서 RAG — MCP 서버부터 크로스인코더 리랭커까지에서 RAG를 별도 서버로 뺀 판단을 이 수치가 뒷받침한다.

Curator의 역할

에이전트 생성 스킬이 무한정 쌓이지 않도록 백그라운드에서 정리하는 청소부다. 스킬별 조회·사용·패치 횟수를 추적하고 상태를 active → stale(30일) → archived(90일)로 굴린다. 절대 삭제하지 않는다. 최악이 .archive/로 이동이고 복구 가능하다. 트리거는 cron이 아니라 유휴 감지이고(기본 7일 간격 + 2시간 유휴), 조건이 맞으면 AIAgent를 백그라운드로 fork해서 돈다. 현재 대화는 건드리지 않는다. 기본은 결정론적 정리만 하고, LLM으로 겹치는 스킬을 병합하는 패스는 curator.consolidate: true 옵트인이다.

자가생성 스킬은 정말 생기는가 — 한 프로필에서 실측

관측 방법을 안 김에 실제로 세어봤다. 한 dev 프로필의 스킬 디렉터리에 SKILL.md가 73개. 수정시각을 보니 72개가 정확히 같은 시각에 찍혀 있었다. 설치 시 한꺼번에 seeding된 번들이다. 그리고 딱 하나만 다음 날 시각으로 튀었다.

autonomous-ai-agents/hermes-docker-ops   (author: "Hermes session")

번들 카탈로그에 없는 이름이고, frontmatter의 authorHermes session이다. 에이전트가 세션 도중 스스로 만든 스킬이다. 내용을 열어보니 삽질하며 배운 운영지식의 박제였다.

섹션 박제된 교훈
1 컨테이너 안에서 터미널 백엔드가 docker면 nested docker라 도구가 죽는다. local로 바꿔라
2 localhost 금지, host.docker.internal을 써야 호스트쪽 서비스에 붙는다
3 config.yaml 직접 편집은 거부된다. hermes config set을 써라
4 .env와 config는 startup에만 읽는다. 게이트웨이를 재기동해야 반영된다
5 로컬 LLM은 컨텍스트 64K 미만이면 거부된다. thinking 모델은 max_tokens를 키워라

이 목록이 흥미로웠던 건, 내가 이 시스템을 굴리며 독립적으로 겪은 문제와 거의 겹쳤기 때문이다. 에이전트가 같은 함정에 빠지고 같은 결론에 도달해 스킬로 남긴 것이다. "오래 돌릴수록 유능"이 이 파일 하나로 관측됐다.

흠도 있었다. SKILL.md 끝이 존재하지 않는 참조 파일을 가리키고 있었고, 설명문이 하우스 스타일 길이 제한을 크게 넘겼다. 자동 생성물의 전형적 흠이다. 90일 미사용 시 Curator가 아카이브할 수 있으므로, 아까운 스킬은 hermes curator pin <이름>으로 고정해두는 게 안전하다. pin은 자동 아카이브와 에이전트의 삭제 도구로부터 스킬을 보호하되, 개선(patch)은 계속 허용한다.

/goal(Persistent Goals)은 cron이 아니다 — judge가 매 턴 채점하는 루프

"목표를 걸어두면 완료할 때까지 스스로 이어간다"는 기능이 있어서, 이게 cron과 뭐가 다른지 파고들었다. 핵심은 judge 모델이 매 턴 채점한다는 것이다.

/goal <목표> → [턴 실행] → [judge가 "목표 충족?" 채점]
                              ├ 예   → 종료
                              └ 아니오 → 같은 세션에 continuation prompt 자동 주입 → 다시 실행
                                        (↻ Continuing toward goal (1/20): <judge의 이유>)

경량 judge 모델이 매 턴 판정한다. 멈추는 조건은 세 가지다. 목표 달성, 사람이 pause·clear, 또는 턴 예산 소진(기본 20턴). 진짜 사용자 메시지가 들어오면 루프를 선점한다.

최대 약점은 목표가 애매하면 판정도 애매해진다는 것이다. 그래서 완료 계약을 붙일 수 있다. outcome(끝났을 때 참이어야 할 상태), verification(그걸 증명하는 명령·테스트·산출물), constraints, boundaries, stop_when. 계약이 걸리면 judge는 "됐어 보임"이 아니라 verification의 실제 증거가 있어야만 done을 준다. 조기 종료와 무한 루프를 동시에 억제하는 장치다.

쓸 자리는 분명하다. 검증 가능한 끝 상태가 있는, 여러 턴짜리 단발 임무를 사람이 옆에서 "계속"을 눌러주지 않고 끝까지 밀 때다. 예를 들어 "테스트가 통과할 때까지 실패한 테스트를 고쳐라". 반복 작업은 cron이고, 단발 질문은 그냥 대화다. /goal은 그 사이 영역이다. 남용하면 토큰만 태운다.

Claude Code와의 경계 — "무인 daemon"의 정확한 뜻

두 도구는 근본이 같다. 둘 다 하네스이고, 둘 다 SKILL.md 스킬·MCP·훅·서브에이전트·슬래시 커맨드· 메모리 파일·권한 게이트를 갖는다. 실제로 Hermes 번들에는 Claude Code를 도구로 호출하는 스킬까지 있어서, 경쟁이자 오케스트레이션 관계다.

그럼 무엇이 다른가. "무인으로 돈다"는 표현을 정확히 갈라야 했다. 둘 다 트리거 없이 혼자 생각을 이어가지는 않고, 둘 다 스케줄 실행이 된다. 단순 예약 실행만 보면 차이가 크지 않다.

진짜 차이는 프로세스가 사는 방식에 있다. 예약형 실행은 정해진 시각에 에이전트를 잠깐 띄우고 죽인다. 반면 게이트웨이는 WebSocket을 물고 상주하는 서비스다. 컨테이너 uptime이 며칠씩 이어지고, 프로세스가 죽지 않고 산다. 그리고 트리거가 사람 메시지만이 아니다. webhook(예: 저장소에 PR이 열림)과 cron을 하나의 상주 프로세스가 동시에 수신한다. 즉 "무인"의 뜻은 "혼자 생각한다"가 아니라 "항상 켜져서 여러 입력원과 여러 사용자를 상시 수신하는 상주 서비스"다. 이 상주·다원·다자 모델이 예약형 실행과 갈리는 지점이다.

Mattermost에서 /goal 요청하기 — 선행 슬래시 함정

개념을 잡았으니 실제로 채널에서 어떻게 부르는지가 남았다. 여기 함정이 하나 있다.

Mattermost는 메시지 첫 글자가 /면 자기 서버 명령으로 가로챈다. 그냥 /goal ...이라고 치면 "Command not found"가 뜨고 봇에게 도달하지 않는다. 해결책은 멘션을 앞에 두는 것이다.

@agents-bot /goal draft 실패한 테스트를 전부 통과시켜라
verify: 테스트 스위트 통과
boundaries: 해당 저장소만
stop when: DB 스키마 변경이 필요하면 멈추고 물어봐

첫 글자가 @라 Mattermost가 가로채지 않고, 채널의 멘션 요구(require_mention)도 동시에 충족한다. draft는 한 줄 목표를 완료 계약으로 확장해 세팅하고 즉시 첫 턴을 시작한다. 이후 봇이 각 턴 결과를 올리며 ↻ Continuing toward goal (n/20)으로 자동으로 이어간다. MATTERMOST_REPLY_MODE=thread로 두면 continuation이 스레드로 접혀 채널이 깨끗하다. 중간 개입도 전부 멘션과 함께 한다. /subgoal로 합격 기준을 추가하고, /goal status|pause|resume|clear로 제어한다.

여기서 require_mention=true가 오히려 유리하게 작동한다. 채널 잡담은 멘션이 아니라 에이전트 앞에서 드롭되므로 루프를 끊지 않는다. 봇을 멘션한 메시지만 선점한다. 반대로 DM은 모든 메시지가 선점하므로, 조용한 루프를 돌리려면 채널과 스레드가 유리하다.

권한도 확인할 값이다. 게이트웨이는 admin과 user를 나누는 2단 권한을 지원한다. allow_admin_from이 설정되지 않았으면 하위호환 모드라 허용된 사용자 전원이 모든 명령을 쓸 수 있다. 팀원이 /goal을 쓰게 하려면 사용자 허용 명령 목록을 확인한다.

관련 글