1. 사전 준비
Hermes를 설정하기 전에 Ollama가 떠 있고 응답하는지 먼저 확인한다.
curl -s http://127.0.0.1:11434/v1/models
| python3 -m json.tool | head
nvidia-smi --query-gpu=index,name,memory.total,power.limit --format=csv
GPU 전력 제한은 드라이버 수준에서
걸리므로 컨테이너 안에서도 그대로 적용된다. 장시간 작업 전에 확인한다.
nvidia-smi
--query-gpu=index,power.limit,power.max_limit --format=csv
2. Hermes 설치
설치 스크립트는 실행 전에 직접 읽어본다. 이 스크립트는 ~/.hermes 밖에는 쓰지 않고, uv를 SHA256 검증과 함께 고정 버전으로 받으며, libatomic.so.1 이 이미 있으면 sudo 분기를 건너뛴다.
curl -fsSL
https://hermes-agent.nousresearch.com/install.sh -o /tmp/hermes-install.sh
less /tmp/hermes-install.sh # 실행 전에
읽는다
bash /tmp/hermes-install.sh
hermes --version
실행 파일은 ~/.local/bin/hermes 에, 소스는
~/.hermes/hermes-agent 에 설치된다.
외부 API 호출 없이 동작만 확인하는 테스트:
hermes -z "Reply with
exactly the word READY and nothing else." --safe-mode
3. 모델 설치와 선택
3.1 측정한 모델 비교
동일한 과제를 동일한 자동 채점 스크립트로 평가했다. 사람의 판단은 개입하지 않았다. 과제는 Flask와 Three.js로 태양계 시뮬레이터를 만드는 것이고, 요구사항은 여섯 가지였다. 파일 구성, 포트 지정, 살아 있는 CDN 주소, 매 프레임 애니메이션, 파이썬
API에서 받아오는 데이터, 서버 자동 실행 금지.
|
모델
|
크기
|
소요
|
점수
|
실패 내용
|
|
qwen3-coder:30b-a3b-q8_0
|
32 GB
|
39초
|
6/6
|
없음
|
|
gpt-oss:120b (온도 조정판)
|
65 GB
|
수 분
|
5/6
|
존재하지 않는 CDN 주소를 지어냄
|
|
devstral:24b
|
14 GB
|
54초
|
0/6
|
도구를 부르지 않고 글로
적음
|
·
파일 크기는 성능이 아니다. 두 모델 모두 전문가 혼합
구조이다. gpt-oss:120b 은 1168억 파라미터를
저장하지만 토큰당 128개 전문가 중 4개만 쓴다. 활성 파라미터는 약 51억이다. qwen3-coder:30b
은 128개 중 8개를 쓰고 활성 파라미터가
약 33억이다. 디스크는
3.6배인데 토큰당 연산은 약 1.5배다.
·
devstral 은 OpenHands 프레임워크 전용으로 학습되었다. 모델 시스템 프롬프트에
그렇게 적혀 있다. Hermes의 도구 호출 규약을 따르지 않으므로 이 조합에서는 쓸 수 없다.
3.2 설치
ollama pull
qwen3-coder:30b-a3b-q8_0 # 코딩 모델, 32
GB
ollama pull qwen3-vl:32b-instruct
# 비전 모델, 20 GB
ollama list
3.3 Hermes에 연결
hermes config set
model.provider ollama
hermes config set model.base_url http://127.0.0.1:11434/v1
hermes config set model.default
qwen3-coder:30b-a3b-q8_0
Hermes는 매 요청에 reasoning_effort 를 보낸다. 이를 지원하지 않는 모델은 400 오류로 거절하고, Hermes는 턴마다 API 호출 하나를 복구에 낭비한다. 오류 문구는 다음과 같다.
"<모델명>" does
not support thinking
모델별 예외를 넣어야 한다. 키에 콜론이 들어가므로 hermes
config set 으로는 쓸 수 없고 ~/.hermes/config.yaml 을 직접 편집한다.
agent:
reasoning_overrides:
"qwen3-coder:30b-a3b-q8_0":
"none"
먼저 지원 여부를 확인한다.
ollama show
qwen3-coder:30b-a3b-q8_0 | sed -n "/Capabilities/,/Parameters/p"
# completion, tools -> 예외 필요
# completion, tools, thinking -> 예외 불필요
3.4 코드 생성용 샘플링 조정
Hermes는 temperature, top_p, top_k 를 보내지 않는다. 따라서
모델 자체의 기본값이 그대로 쓰인다. 실제 요청을 가로채어 확인한 내용이다.
model qwen3-coder:30b-a3b-q8_0
options {"num_ctx":
131072}
reasoning_effort medium
tools 19
temperature 없음
모델을 탓하기 전에 출고 기본값부터 비교한다.
ollama show <모델명> | sed
-n "/Parameters/,/License/p"
qwen3-coder 는 temperature 0.7, top_p
0.8, top_k 20, repeat_penalty 1.05 로 출고된다. 이미 코드용으로 조여져 있다. 반면 gpt-oss 는 temperature
1 만 설정되어 있어 코드 생성에는 너무 높다. 다시 내려받지 않고 기존 가중치를 재사용하여 변형을 만들 수 있다.
cat > Modelfile.code
<<EOF
FROM gpt-oss:120b
PARAMETER temperature 0.4
PARAMETER top_p 0.9
PARAMETER top_k 40
PARAMETER repeat_penalty 1.05
EOF
ollama create gpt-oss:120b-code -f Modelfile.code # 블롭 재사용, 추가 용량 없음
4. 터미널 백엔드
두 가지 선택지가 있다. 이것이
보안 경계이므로 의식적으로 골라야 한다.
|
항목
|
도커 샌드박스
|
로컬
|
|
CPU
|
1코어 (기본값)
|
8코어 전부
|
|
메모리
|
5 GB (기본값)
|
62 GB
|
|
포트
|
공개되지 않음. 내부 웹앱에 호스트에서 접속 불가
|
제약 없음
|
|
conda 환경
|
사용 불가
|
사용 가능
|
|
파일 접근 범위
|
마운트한 디렉터리만
|
사용자가 읽을 수 있는
모든 것
|
4.1 방법 A: 도커 샌드박스
hermes config set
terminal.backend docker
hermes config set terminal.docker_image nousresearch/hermes-sandbox:desktop
hermes config set terminal.docker_mount_cwd_to_workspace true
hermes config set terminal.docker_run_as_host_user true
hermes config set terminal.container_persistent true
hermes config set terminal.cwd /home/tom3/project # 호스트 경로. 아래 함정 참고
컨테이너 안의 경로를 넣으면 마운트가 조용히 비활성화된다. tools/terminal_tool.py 의 해당 부분은 다음과 같다.
candidate = os.path.abspath(os.path.expanduser(
_tenv("TERMINAL_CWD") or
_safe_getcwd()))
if (_is_host_cwd(candidate)
or (os.path.isabs(candidate) and
os.path.isdir(candidate)
and not
candidate.startswith(("/workspace", "/root")))):
host_cwd = candidate
cwd = "/workspace"
값이 /workspace 로
시작하면 조건을 통과하지 못해 host_cwd 가 None 으로
남고 바인드 마운트가 생성되지 않는다. 호스트 경로를
/workspace 로 바꿔주는 일은 Hermes가 알아서 한다.
자원 상한을 올린다. 기본값이
매우 낮다.
hermes config set
terminal.container_cpu 6
hermes config set terminal.container_memory 32768
샌드박스에 GPU를 넘기려면
nvidia-container-toolkit 이 필요하다.
hermes config set
terminal.docker_extra_args '["--gpus","device=1"]'
컨테이너는 넘겨받은 GPU를 0번부터 다시 번호 매긴다. 호스트의
device=1 은 컨테이너 안에서 GPU 0 으로 보인다. 메모리 사용량을 대조하면 확인할 수 있다.
docker inspect <컨테이너>
--format '{{json .HostConfig.DeviceRequests}}'
마운트는 설정이 아니라
컨테이너 내부에서 확인한다.
docker inspect <컨테이너>
--format \
'{{range .Mounts}}{{.Source}} ->
{{.Destination}} rw={{.RW}}{{"\n"}}{{end}}'
hermes -z "Use the terminal tool to run: ls -1 /workspace | head -20"
4.2 방법 B: 로컬 + 전용 conda 환경
더 빠르고 웹앱 개발이 가능해진다. 대신
샌드박스를 잃는다.
conda create -y -n venv_hm
python=3.12
PYTHONNOUSERSITE=1 ~/miniconda3/envs/venv_hm/bin/pip install --ignore-installed
\
flask requests numpy pandas pillow
matplotlib beautifulsoup4 lxml \
httpx rich pyyaml python-dotenv tqdm
ENABLE_USER_SITE
가 True 이면 pip 는 ~/.local 의 패키지를 보고 이미 설치되었다고 판단하여 가상환경에는 아무것도 넣지 않는다. 그 상태로 import 를 확인하면 유출된 쪽을 읽어 통과해 버린다. 반드시 사용자 site 를 끈 상태로 검증한다.
PYTHONNOUSERSITE=1
~/miniconda3/envs/venv_hm/bin/python -c "
import sys, flask
print(sys.executable)
print([p for p in sys.path if p and 'venv_hm' not in p] or '유출 없음')
print(flask.__file__)"
스냅샷 셸에서 conda
activate 로 바꾼 PATH 는 살아남지 못한다. 반면 CUDA_VISIBLE_DEVICES 처럼 Hermes가 관리하지
않는 변수는 살아남는다. conda 셸 함수에 의존하지 말고 PATH
를 직접 지정한다.
~/.hermes/shell_init.sh 파일을 만든다.
HM_ENV="$HOME/miniconda3/envs/venv_hm"
if [ -d "$HM_ENV/bin" ]; then
PATH="$HM_ENV/bin:$(printf '%s'
"$PATH" | tr ':' '\n' \
| grep -vxF
"$HM_ENV/bin" | paste -sd:)"
export PATH
export
CONDA_PREFIX="$HM_ENV"
export CONDA_DEFAULT_ENV=venv_hm
fi
export PYTHONNOUSERSITE=1
export CUDA_VISIBLE_DEVICES=1 # GPU 0 은
Ollama 서버 전용
Hermes가 이 파일을 읽도록 지정한다. 사용자의 .bashrc 는 건드리지 않는다.
hermes config set
terminal.backend local
hermes config set terminal.cwd /home/tom3/project
hermes config set terminal.shell_init_files
'["~/.hermes/shell_init.sh"]'
설정이 아니라 에이전트를 통해 확인한다.
hermes -z "Use the
terminal tool to run: which python; \
python -c 'import flask;
print(flask.__file__)'; nproc; \
echo CUDA=$CUDA_VISIBLE_DEVICES;
pwd"
5. 텔레그램 연결
5.1 봇 생성 (휴대폰)
1. 텔레그램에서 @BotFather 를 검색한다. 파란 인증 표시가 있는 계정을 고른다.
2. START 를 누르고 /newbot 을 보낸다.
3. 표시 이름을 입력하고, 이어서 bot 으로 끝나는 사용자명을 입력한다.
4. 응답에 나오는 토큰을 복사한다. Use this token to access the HTTP API 아래 줄이다.
5. @userinfobot 을 검색해 START 를 누르면 숫자
Id 를 알려준다.
5.2 자격증명 기록
토큰을 대화 기록에 남기지 않는다. 입력을
가린 채 받고, 기록하기 전에 텔레그램 서버에 실제로 물어 검증한다.
hermes
gateway setup 을 쓰면 같은 일을 대화형으로 해준다. 단 TTY 가
필요하다.
5.3 게이트웨이 실행
cd /home/tom3/project # 도커 백엔드에서는 이 경로가 마운트에 쓰인다
hermes gateway run
로그에 Connected to Telegram (polling
mode) 가 나오면 성공이다. 폴링 방식이므로 공인 IP, 포트포워딩, 인바운드 방화벽 규칙이 모두 필요 없다.
안정적으로 동작하는 것을 확인한 뒤 서비스로 등록한다.
hermes gateway install
hermes gateway status
5.4 텔레그램 연결 안정성
이 회선에서 60분 동안 30초 간격으로 측정한 결과이다.
표본
120회, 정상 115회 (95.8%)
끊김 1회, 지속 시간 2분 20초
같은 시각 다른 사이트는 정상 (github 0.02초,
discord 0.04초)
api.telegram.org
만 선택적으로 막혔고, 모든 IP가 동시에 TCP 443 연결에 실패했다. 따라서 TELEGRAM_FALLBACK_IPS 로 IP를 바꿔도 해결되지
않는다. Hermes가 지수 백오프로 10회까지 자동 재접속하고, 텔레그램이 미수신 업데이트를 24시간 보관하므로 메시지는 유실되지
않는다. 응답이 수 분 늦어질 뿐이다. 복구는 로그에 남는다.
[Telegram] Telegram API
transport recovered via 149.154.166.110
[Telegram] Using sticky IPv4 Telegram API path (dual-stack hostname tried last)
DNS가 AAAA 레코드만 반환하는데 호스트에 IPv6 경로가 없으면 같은
증상이 난다. 다음으로 확인한다.
getent ahostsv4
api.telegram.org
getent ahostsv6 api.telegram.org
ip -6 route show default # 비어 있으면
IPv6 경로 없음
6. 비전 기능
hermes config set
auxiliary.vision.provider ollama
hermes config set auxiliary.vision.model
qwen3-vl:32b-instruct
hermes config set auxiliary.vision.base_url http://127.0.0.1:11434/v1
aux.vision 으로 쓰면 Hermes가 모르는 섹션이 만들어지고
경고 없이 무시된다. 이후 에이전트는 vision_analyze tool is not available in this environment 라고
답한다.
게이트가 열렸는지 확인하고, 반드시
실제 이미지로 다시 확인한다.
VENV=$(ls -d ~/.hermes/installs/*/environments/*/venv
| head -1)
cd ~/.hermes/hermes-agent && HERMES_HOME=~/.hermes
"$VENV/bin/python" -c "
import sys; sys.path.insert(0,'.')
from tools.vision_tools import check_vision_requirements
print(check_vision_requirements())"
# True 여야 한다
hermes -z "Use vision_analyze on /경로/스크린샷.png and quote the text you can
read."
설정이 맞아 보이는지가 아니라, 글자를
실제로 읽어내는지로 판정한다.
비전 모델은 필요할 때 적재되고 유휴 시간이 지나면 해제된다. qwen3-vl:32b-instruct 는 약 83 GB를 쓴다. GPU 0 에 55 GB짜리 코딩 모델이 있으면 Ollama가 GPU 1 에 올리므로, 이미지를 분석하는 동안에는 CUDA_VISIBLE_DEVICES=1 로
확보해 둔 몫이 거의 남지 않는다. 더 가벼운 선택지는
qwen3-vl:30b-a3b-instruct (19 GB) 이다.
7. 개발 작업 흐름
7.1 프롬프트 틀
중요한 것은 두 가지다. 외부
주소 검증과 자기 검증이다. 둘 다 실제로 관찰된 실패를 막기 위한 것이다. 모델은 그럴듯한 CDN 경로를 지어내고, 아무것도 실행하지 않은 채 성공했다고 보고한다.
<무엇>을
/home/tom3/project/<앱> 에 만들어라.
1. 파일은 app.py 와 templates/index.html 두 개만.
2. Flask 포트 <N>. 이미 쓰는 프로세스가 있으면 먼저 kill.
3. 라이브러리는 https://cdnjs.cloudflare.com 에서 버전 고정 UMD 빌드로
받는다.
script 주소를 쓰기 전에 curl -sI "<주소>" 로 HTTP 200 인지 확인해라.
404 면 다른 버전으로 바꾸고 다시 확인해라.
4. <동작 요구사항>
5. 데이터는 app.py 의 파이썬 리스트에 두고
GET /api/<x> 로 내보내고
JS 가 fetch 로
받는다. HTML 에 하드코딩 금지.
6. 작업이 끝나면 서버를 띄우고 curl 로 모든 경로가 200 인지 확인해라.
그 확인을 하기 전에는 완료라고 하지 마라.
7.2 주소 검증이 필요한 이유
가정이 아니라 관찰된 사실이다.
cdnjs 는 three.js 코어만 제공하며 애드온 하위 경로는 존재하지 않는다.
three.js/r128/three.min.js 200
three.js/r128/controls/OrbitControls.min.js 404
three.js/r128/postprocessing/EffectComposer.min.js 404
three.js/r128/objects/Lensflare.min.js 404
코어는 로드되므로 THREE 객체는
존재한다. 그러나 없는 애드온을 처음 쓰는 줄에서 Uncaught
TypeError 가 나고 페이지가 멈춘다. 로딩 문구가 지워지지 않는다. 서버는 내내 200 을 반환하므로 서버 쪽 점검으로는 아무 이상도
발견되지 않는다.
7.3 신규 작성과 기존 코드 수리의 차이
같은 모델로 두 가지를 측정했다.
|
작업 유형
|
결과
|
|
백지에서 신규 작성
|
요구사항 6개 전부 통과, 39초
|
|
깨진 기존 코드 수리 (1차)
|
의존성을 절반만 제거. 콘솔 오류 45건
|
|
깨진 기존 코드 수리 (2차, 같은 프롬프트)
|
한 줄도 고치지 않고
오류 없음이라고 보고
|
이 모델은 새로 쓰는 일은 잘하고, 남이
망가뜨린 코드를 외과적으로 고치는 일은 못한다. 수리를 두 번 시도해 실패하면 새 폴더에 처음부터 만드는
편이 빠르다. 기존 폴더는 지우지 말고 서버만 멈춰 두면 비교가 가능하다.
7.4 실행 중인 웹앱 캡처
google-chrome --headless=new
--disable-gpu --no-sandbox \
--use-gl=angle --use-angle=swiftshader
--enable-unsafe-swiftshader \
--window-size=1400,900
--virtual-time-budget=12000 \
--screenshot=/tmp/shot.png
"http://127.0.0.1:<포트>/"
스크린샷으로는 보이지 않는 콘솔 오류는 따로 받아야 한다.
google-chrome --headless=new
--disable-gpu --no-sandbox \
--enable-logging=stderr
--virtual-time-budget=10000 \
--dump-dom "http://127.0.0.1:<포트>/"
> /dev/null 2> console.log
grep -iE 'Uncaught|ERROR|Failed to load' console.log
페이지의 모든 외부 주소를 한 번에 검사한다.
for u in $(grep -oE
'https://[^"\' ]+\.js' templates/index.html | sort -u); do
printf "%-70s "
"$u"; curl -sI --max-time 12 "$u" | head -1
done
7.5 텔레그램 세션 관리
모델, 백엔드, 경로 중 하나라도 바꾸었으면 /new 또는 /reset 을 보낸다. 대화 기록은 새 모델에게도 그대로 전달되므로, 예전의 잘못된 결론과 더 이상 존재하지 않는 경로가 설정 변경 후에도 살아남아 사실처럼 반복된다.
8. 운영
hermes gateway status
hermes gateway restart
hermes config get <키>
curl -s http://127.0.0.1:11434/api/ps | python3 -m json.tool # 적재된 모델
nvidia-smi --query-gpu=index,memory.used,power.draw --format=csv
Ollama를 멈추지 않고 모델만 메모리에서
내린다.
curl -s
http://127.0.0.1:11434/api/generate -d '{"model":"<모델>","keep_alive":0}'