
Bot마다 추론 엔진을 분리하고 싶다! Ollama × OpenRouter 혼용 운용
요약
Ollama와 OpenRouter를 혼용하여 로컬 LLM과 클라우드 LLM을 효율적으로 운용하는 방법을 다룹니다. Hermes Agent의 profile 기능을 활용해 봇마다 추론 엔진, 메모리, 정체성을 독립적으로 분리하는 설계 방식을 설명합니다.
핵심 포인트
- profile 단위를 통해 LLM 프로바이더와 봇의 정체성을 통합 관리
- 가벼운 작업은 Ollama(로컬), 무거운 작업은 OpenRouter(클라우드)로 라우팅
- 세션, 메모리, SOUL.md는 프로필별로 독립적으로 유지
- 병렬 추론 시 KV cache 경합 방지를 위한 Ollama 환경 변수 설정 팁
Mac Studio 1대로 여러 개의 AI bot을 구동하다 보면, 곧바로 다음과 같은 요구사항에 부딪힌다.
- 가벼운 작업은 로컬 LLM (Ollama)으로 돌려서 비용을 제로로 만들고 싶다
- 무거운 작업이나 실전 운용만 클라우드 (OpenRouter)로 던지고 싶다
- 하지만 대화 이력, 기억, 인격, 접근 가능한 파일은 bot마다 완전히 분리하고 싶다
Hermes Agent는 이를 profile이라는 단위로 깔끔하게 해결할 수 있다. 본 기사에서는 bot A를 로컬 LLM으로, bot B를 OpenRouter로 라우팅(routing)하는 구성을 실제 현장 설정 절차로서 정리한다.
결론부터 말하자면, profile을 「LLM provider + bot identity + memory + skill + 제약 사항」의 한 덩어리로 취급하는 설계가 여러 bot을 운용하는 정답이었다. bot을 늘리는 것과 profile을 늘리는 것을 동의어로 만드는 사고방식이다.
Hermes Agent에서는 profile이라는 단위로 bot을 독립적으로 가동할 수 있으며, 각각 별도의 LLM 프로바이더(provider)로 라우팅할 수 있다. ~/.hermes/<profile>/config.yaml 내의 model.provider를 바꿔 쓰는 것만으로 전환된다.
최소 구성:
- bot A (default profile) → Ollama
gemma4:26b-mxfp8(local) - bot B (qi_bidding profile) → OpenRouter
openai/gpt-5.4-mini
참고: 모델명 (gemma4:26b-mxfp8 / openai/gpt-5.4-mini)은 환경에 맞춰 바꿔주길 바란다. ollama list나 OpenRouter의 모델 목록에 존재하는 실제 태그로 교체할 것.
profile의 독립성을 이해하는 가장 빠른 방법은 디렉토리 구조를 살펴보는 것이다.
~/.hermes/
├── config.yaml ← bot A (default profile)의 메인 config
├── .env ← bot A의 secrets
...
포인트는, sessions/ memories/ SOUL.md는 profile마다 독립되어 있는 반면, auth.json과 kanban.db는 모든 profile이 공유하고 있다는 점이다. 이 「무엇이 섞이고 무엇이 분리되는가」의 경계 설정이 운용 설계의 핵심이 된다 (상세 내용은 §8 참조).
# bot B용 profile 생성
# (내부적으로 <profile>/ 디렉토리 + 기본 config를 템플릿으로 전개)
hermes profile create qi_bidding
...
이것만으로 ~/.hermes/profiles/qi_bidding/ 아래에 config 템플릿이 전개된다.
~/.hermes/config.yaml의 서두:
model:
default: gemma4:26b-mxfp8 # ollama list에서 나오는 tag 그대로
provider: ollama
...
사전 준비:
brew install --cask ollama
open -a Ollama # daemon 기동
ollama pull gemma4:26b-mxfp8 # 모델 취득 (~26GB)
launchctl setenv OLLAMA_NUM_PARALLEL 1
launchctl setenv OLLAMA_MAX_LOADED_MODELS 1
launchctl setenv OLLAMA_MAX_QUEUE 10
...
병렬 추론 시 KV cache 경합으로 인해 30B급 모델이 막히는 동작에 대한 대처법이다. OLLAMA_NUM_PARALLEL=1로 추론을 순차화한다. launchd plist로 영구화해 두는 것이 안전하다 (~/Library/LaunchAgents/ai.ollama.envsetter.plist).
이 「로컬 LLM의 의사 병렬 처리를 직렬화하는」 패턴은 별도의 Hermes Agent + Ollama 병렬 처리 아키텍처 기사에서 심도 있게 다루고 있으므로, 그쪽도 참고해 주길 바란다. 본 기사에서는 「병렬 처리의 함정이 있으니 미리 막아둔다」 정도로만 언급한다.
~/.hermes/profiles/qi_bidding/config.yaml
의 서두:
model:
default: openai/gpt-5.4-mini # OpenRouter의 모델 ID 형식
provider: openrouter
...
~/.hermes/profiles/qi_bidding/.env
에:
OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
default의 .env에도 동일한 이름의 키를 두면 공유할 수 있지만, 프로파일 (profile) 별로 별도 계약 및 별도 키 (key)를 사용하는 것이 더 안전하다. 비용 관리도 분리하기 쉬워진다.
Discord는 1 bot token = 1 WebSocket session이므로, 봇(bot) 2개 = 봇 토큰(bot token) 2개가 필요하다. Discord Developer Portal에서 2개의 Application을 만들고, 각각의 토큰을 할당한다.
# ~/.hermes/.env (bot A 용)
DISCORD_BOT_TOKEN=MT...bot-A-token...
DISCORD_HOME_CHANNEL=<CHANNELID_1>
...
주의사항:
Privileged Gateway Intents에서 Message Content Intent를 ON으로 설정하지 않으면, 봇 기동 시 PrivilegedIntentsRequired 에러로 중단된다.
DISCORD_ALLOWED_USERS=<username>와 같이 사용자 이름으로 작성하면 Hermes가 이름 확인(name resolution)을 위해 Server Members Intent를 요구하게 되어, 관리자(admin) 승인 대기 상태에서 막히게 된다. 숫자 User ID(Discord 개발자 모드에서 복사)를 입력할 것.
~/Library/LaunchAgents/ai.hermes.gateway.plist
(bot A 용, 템플릿):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
...
bot B 용은 --profile qi_bidding을 ProgramArguments에 끼워 넣기만 하면 된다:
<array>
<string>/Users/llm/.hermes/hermes-agent/venv/bin/python</string>
<string>-m</string><string>hermes_cli.main</string>
...
</array>
Label은 다른 이름(ai.hermes.gateway-qi_bidding)으로 설정할 것.
파일 디스크립터(FD) 상한은 두 plist 모두 8192로 올리는 것이 철칙이다. macOS LaunchAgent의 기본값인 256으로 두면, 장시간 가동 시 파일 디스크립터(file descriptor)가 고갈되어 봇이 조용히 연결이 끊기게 된다.
기동:
launchctl bootstrap gui/501 ~/Library/LaunchAgents/ai.hermes.gateway.plist
launchctl bootstrap gui/501 ~/Library/LaunchAgents/ai.hermes.gateway-qi_bidding.plist
# 두 gateway가 독립 프로세스로 동작하고 있는지 확인
ps -ax | grep "hermes_cli.main" | grep -v grep
# 출력 이미지:
...
각각의 모델 (model) 설정 확인:
hermes status | grep -iE "Model|Provider"
hermes -p qi_bidding status | grep -iE "Model|Provider"
# 출력 예시:
...
스모크 테스트 (smoke test):
hermes -z "Say pong" --yolo # bot A 의 Ollama 경유
hermes -p qi_bidding -z "Say pong" --yolo # bot B 의 OpenRouter 경유
두 bot이 독립적으로 서로 다른 엔진을 통해 응답하면 완성입니다.
프로파일 (profile) 설계에서 가장 사고가 나기 쉬운 부분은 "무엇이 공유되고 무엇이 분리되는가"를 혼동하는 것입니다.
| 리소스 | 범위 | 코멘트 |
|---|---|---|
~/.hermes/auth.json (OAuth pool) | 모든 프로파일 공유 | API key와 OAuth 모두 하나의 pool입니다. 프로파일별 (profile-specific)로 관리하고 싶다면 별도 계정으로 key를 발급하세요 |
~/.hermes/kanban.db + boards/* | 모든 프로파일 공유 | bot 간에 태스크 전달을 자연스럽게 할 수 있는 설계입니다. 완전히 분리하고 싶다면 HERMES_KANBAN_DB 환경 변수 (env)를 통해 별도 경로로 지정하세요 |
sessions/ / memories/ / SOUL.md | 프로파일별 독립 | 대화 이력, 기억, 인격이 섞이지 않습니다 |
| Discord/Teams credentials | 프로파일별 독립 | 별도의 bot token이 필요합니다 |
| LLM provider | 프로파일별 독립 | 이 글의 주제입니다 |
| ... |
프로파일의 독립성이 가장 큰 효과를 발휘하는 것은 bot별로 "접근 가능한 영역"을 제약하는 것입니다. ~/.hermes/profiles/qi_bidding/SOUL.md에 bot별 시스템 프롬프트 (system prompt)와 경계를 작성합니다.
You are Hermes_For_QI_Bidding, the QI Bidding agent.
You assist bid/proposal preparation.
## Workspace boundaries (STRICT)
...
이렇게 하면 bot B는 bidding vault만 접근합니다. bot A는 기본 (default) SOUL.md를 사용하여 다른 경계(또는 제한 없음)를 갖게 합니다. 업무용 bot으로부터 개인용 vault를 물리적으로 보호할 수 있다는 점은 프로파일을 분리함으로써 얻는 큰 부수적 효과입니다.
| 증상 | 원인 | 대처 |
|---|---|---|
| Bot 1개만 Discord에서 반응하지 않음 | DISCORD_BOT_TOKEN을 실수로 동일한 값으로 설정하여 두 번째 bot이 kick됨 | bot 단위로 별도 token 사용 |
PrivilegedIntentsRequired로 인해 실행 실패 | Developer Portal에서 Message Content Intent가 OFF 상태임 | Portal에서 ON으로 설정 |
Server Members Intent privileged… | DISCORD_ALLOWED_USERS에 사용자 이름(비숫자)을 입력함 | 숫자 User ID로 교체 |
| 로컬 LLM이 11분 동안 응답하지 않음 | OLLAMA_NUM_PARALLEL > 1 설정으로 인해 KV cache 경합 발생 | NUM_PARALLEL=1로 순차 처리 |
| 30시간 가동 후 bot이 조용히 연결 끊김 | macOS LaunchAgent의 FD(File Descriptor) 상한 256 소진 | plist의 SoftResourceLimits=8192 설정 |
| OpenRouter에서 504 타임아웃 발생 | provider_routing.only로 제한하여 모든 요청이 실패함 | provider_routing: {}로 설정하여 OpenRouter 기본값 사용 |
| 한쪽 bot만 webhook이 실행되지 않음 | 두 프로파일이 :8644 포트를 서로 점유하려고 함 | qi_bidding 측에서 platforms.webhook.enabled: false 설정 |
| 업데이트 후 DB 스키마 (schema) 불일치 | kanban DB에 새로운 컬럼이 추가되지 않음 | 수동으로 ALTER TABLE tasks ADD COLUMN session_id TEXT 실행 |
Discord 내에서 /model <name> 슬래시 커맨드 (slash command)를 입력하면 세션 (session) 단위로 모델을 전환할 수 있습니다 (gateway 재시작 불필요).
/model openrouter/anthropic/claude-sonnet-4
/model ollama/qwen3.6:35b-a3b-coding-mxfp8
세션 (session) 스코프이므로 다른 스레드 (thread)나 다른 봇 (bot)에는 영향을 주지 않습니다. "무거운 작업은 강력한 모델로 전환", "가벼운 잡담은 Ollama로"와 같은 운용이 가능합니다.
~/.hermes/config.yaml에 추가:
fallback_model:
provider: openrouter
model: google/gemma-4-31b-it
프라이머리 (primary) 모델이 레이트 리밋 (rate limit, 429) / 과부하 (overload, 529) / 5xx 에러 / 연결 실패로 다운되었을 때, 자동으로 폴백 (fallback) 모델로 전환됩니다. 클라우드 측의 동적 할당 초과 상황에서도 유효합니다.
프로파일 (profile)을 「LLM 프로바이더 (provider) + 봇 아이덴티티 (bot identity) + 메모리 (memory) + 스킬 (skill) + 제약 (constraint)」의 묶음 단위로 취급하는 설계가 여러 봇을 운용하는 정답입니다. 봇을 늘리는 것과 프로파일을 늘리는 것을 동의어로 만듭니다.
"봇마다 다른 엔진 (engine)"이라는 기능을 구현하기 위해, Hermes는 설정을 프로파일 스코프 (profile scope)로 분리합니다. 그 결과 대화 이력도, 인격도, 제약도 독립적이 됩니다. 이것이 부수적인 효과로서 다음과 같은 운용에 도움이 됩니다.
- 개인 볼트 (vault)를 업무용 봇으로부터 보호
- 한쪽 프로바이더 (provider)가 불안정할 때도 다른 쪽은 무사함
- 한쪽은 Ollama 검증용, 다른 한쪽은 실운영용으로 사용
본 기사에서는 프로파일 단위로 LLM 프로바이더를 나누는 설계를 다루었으나, 봇을 Discord가 아닌 Microsoft Teams에서 구동할 경우, "프라이빗 채널에서 봇이 반응하지 않는" Teams 고유의 함정이 있습니다. 이는 Hermes의 문제가 아니라 Teams의 설계 사양이며, 프로파일 설계와는 별개의 레이어 문제입니다.
자세한 내용은 별도 기사인 "Microsoft Teams의 프라이빗 채널에서 봇이 반응하지 않는 문제 — 설계 사양과 회피책"에 정리해 두었습니다. Teams 어댑터 (adapter)의 chat_id 형식 제약 (세미콜론 사용 불가)에 대해서도 해당 기사에서 다루고 있습니다.
HermesAgent
Ollama
OpenRouter
Discord
LLM
AI 에이전트 (AI Agent)
macOS
LaunchAgent
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기