[AgentForge] 블로그 자동화 서비스 전체 아키텍처 — AI 댓글, 번역, 포스트 생성까지

블로그를 운영하면서 가장 번거로운 작업 세 가지가 있습니다. 댓글에 답하기, 영문 번역 유지하기, 그리고 꾸준히 글 쓰기. AgentForge 프로젝트에서는 이 세 가지를 모두 AI 에이전트로 자동화했습니다.

이 글에서는 2개 서버에 걸쳐 동작하는 블로그 자동화 서비스의 전체 아키텍처를 정리합니다.


시스템 토폴로지

┌─────────────────────┐      HTTPS       ┌─────────────────────┐
│      arm1 서버       │ ──────────────▶  │      ec1 서버       │
│  (에이전트 오퍼레이터)  │                  │  (블로그 호스팅)      │
├─────────────────────┤                  ├─────────────────────┤
│ blog-agent (:8081)  │                  │ Hugo (nginx)        │
│  ├─ CommentHandler  │                  │ Blog API (:8000)    │
│  ├─ TranslateHandler│                  │  ├─ translator.py   │
│  └─ PostGenerator   │                  │  ├─ blog_manager.py │
│                     │                  │  └─ git_handler.py  │
│ NATS / PostgreSQL   │                  │                     │
│ Prometheus / Grafana │                  │ Git (yarang/blogs)  │
└─────────────────────┘                  └─────────────────────┘
서버역할핵심 서비스
arm1에이전트 오퍼레이터blog-agent.service — Flask + Scheduler + LLM Client
ec1블로그 호스팅 + APIHugo (nginx) + blog-api.service (FastAPI)

두 서버 간 통신은 HTTPS API 호출만 가능합니다. arm1에서 ec1로의 SSH 접속은 차단되어 있어, 모든 연동은 Blog API를 통해 이루어집니다.


arm1: 통합 블로그 에이전트

왜 통합했는가

초기에는 댓글 응답, 번역, 포스트 생성이 각각 독립 프로세스(3개 systemd 서비스)로 운영되었습니다. 문제는:

  • Claude Code CLI(--print) 호출 방식으로 응답 시간 9.7초, 디스크 688MB 소모
  • systemd 유닛 6개 관리 부담
  • 프로세스 간 상태 공유 불가

이를 1개 프로세스로 통합하면서 직접 LLM API 호출로 전환했습니다. 결과:

지표BeforeAfter
응답 시간9.7초1.7초
디스크 사용688MB~50MB
systemd 유닛6개1개
프로세스3개1개

아키텍처

class BlogAgent:
    """1 프로세스 = Flask (webhook) + Scheduler (timer) + LLM Client"""
    
    def __init__(self):
        self.config = AgentConfig.from_credentials()
        self.llm = LLMClient(self.config)       # ZAI glm-4.7
        self.api = BlogAPIClient(self.config)     # ec1 Blog API
        
        # 핸들러
        self.comment = CommentHandler(self.llm, self.config)
        self.translate = TranslateHandler(self.api)
        self.post_gen = PostGenerator(self.llm, self.api)
        
        # 스케줄러
        self.scheduler = Scheduler()
        self.scheduler.every(hours=6, task=self.translate.check_and_sync)
        self.scheduler.daily_at(hour=9, task=self.post_gen.generate_and_publish)

모듈별 동작

1. CommentHandler — AI 댓글 응답

GitHub Discussions의 Webhook 이벤트를 수신하여 자동으로 AI 댓글을 생성합니다.

[사용자 댓글] → GitHub Webhook → arm1 Flask → CommentHandler
    → LLM 호출 (ZAI glm-4.7) → 답변 생성 → GitHub API로 댓글 게시
  • 트리거: Webhook 이벤트 기반 (실시간)
  • 필터링: 블로그 소유자 댓글, AI 생성 댓글은 건너뜀
  • 보안: HMAC-SHA256 Webhook 시크릿 검증, Flask-Limiter 적용

2. TranslateHandler — 자동 번역 트리거

6시간마다 ec1의 Blog API에 번역 동기화를 요청합니다.

[Scheduler 6h] → TranslateHandler.check_and_sync()
    → POST /translate/sync → ec1 Blog API가 실제 번역 수행

arm1은 번역을 직접 수행하지 않고, ec1 API에 트리거만 보냅니다. 실제 번역 로직은 ec1의 translator.py에 있습니다.

3. PostGenerator — 자동 포스트 생성

매일 오전 9시에 기술 블로그 포스트를 자동 생성합니다.

[Scheduler 09:00 KST] → PostGenerator.generate_and_publish()
    → 기존 주제 수집 → RSS 트렌드 참조 → LLM으로 콘텐츠 생성
    → 중복 검사 → Blog API로 게시

중복 방지가 핵심입니다. difflib.SequenceMatcher로 새 제목과 최근 100개 기존 제목의 유사도를 비교합니다:

def _is_duplicate_title(self, new_title, existing_titles):
    """threshold 0.6 이상이면 중복으로 판정"""
    new_lower = new_title.lower().strip()
    for title in existing_titles[-100:]:
        ex_lower = title.lower().strip()
        ratio = difflib.SequenceMatcher(None, new_lower, ex_lower).ratio()
        if ratio >= 0.6:
            return True
    return False

ec1: Blog API 번역 시스템

Gemini로의 전환

초기에는 ZAI(glm-4.7)로 번역을 수행했으나, 치명적인 문제가 발생했습니다:

glm-4.7은 reasoning 모델로, max_tokens 예산을 reasoning_content(내부 사고 과정)에 먼저 소진합니다. max_tokens=256이면 reasoning에 256토큰을 모두 쓰고, 실제 content는 빈 문자열이 됩니다.

이로 인해 9개 영문 게시글의 제목이 빈 문자열로 번역되는 사고가 발생했습니다.

해결책: Gemini 2.5 Flash Lite로 교체.

항목ZAI (이전)Gemini (현재)
모델glm-4.7 (reasoning)gemini-2.5-flash-lite
번역 시간~30초/포스트~8초/포스트
비용API 유료무료 (1,500건/일)
빈 응답 문제발생없음

OpenAI-Compatible 엔드포인트

Gemini는 OpenAI 호환 API를 제공합니다. 기존 코드를 한 줄도 바꾸지 않고 base URL만 교체하면 됩니다:

LLM_BASE_URLS = {
    "GEMINI": "https://generativelanguage.googleapis.com/v1beta/openai",
    "ZAI":    "https://api.z.ai/api/coding/paas/v4",
}

번역 매칭 로직

한국어↔영어 게시글 페어링은 날짜 접두사 매칭을 사용합니다:

ko: 2026-05-04-001-개발-생산성-17배-극대화-deepseek-v4와-...
en: 2026-05-04-001-개발-생산성-17배-극대화-deepseek-v4와-...
                    ↑ 같은 접두사 = 같은 게시글

slug의 언어가 다를 수 있지만, YYYY-MM-DD-NNN 부분이 같으면 같은 게시글로 인식합니다. 이 방식의 전제 조건은 같은 날짜에 같은 번호가 2개 이상 존재하면 안 된다는 것입니다.

Title-in-Body 번역 기법

제목을 별도 API 호출로 번역하면 reasoning 모델에서 빈 결과가 나오는 문제가 있었습니다. 해결책은 제목을 본문 첫 줄에 포함시키는 것:

# 번역 요청 시
prompt = f"# {original_title}\n\n{original_body}"

# 번역 결과에서 제목 추출
if translated.lstrip().startswith("# "):
    lines = translated.lstrip().split("\n", 1)
    extracted_title = lines[0].lstrip("# ").strip()
    translated_body = lines[1].lstrip("\n")

하나의 API 호출로 제목과 본문을 동시에 번역하므로, 맥락이 보존되고 토큰도 절약됩니다.


LLM 전략: 역할별 모델 분리

하나의 LLM으로 모든 작업을 처리하지 않습니다. 작업 성격에 맞춰 모델을 분리했습니다.

작업서버모델이유
댓글 AI 응답arm1ZAI glm-4.7대화형, 한국어 품질 우수
포스트 생성arm1ZAI glm-4.7긴 글 생성, 창의성 필요
번역 (ko→en)ec1Gemini Flash Lite비추론형, 빠르고 무료

핵심 원칙: reasoning 모델은 번역에 쓰지 않는다. reasoning 모델은 내부 사고에 토큰을 소비하므로, 단순 변환 작업에는 비추론형 모델이 적합합니다.


모니터링과 운영

헬스체크 엔드포인트

# arm1 에이전트
curl http://arm1:8081/health
# → {"status":"healthy","agent":"blog-agent","scheduler_jobs":2,"uptime_sec":...}

curl http://arm1:8081/status
# → {"scheduler":[{"name":"auto-translate","last_run":...},{"name":"post-generator","last_run":"2026-05-04"}]}

# ec1 Blog API
curl https://blog.example.com/api/health
# → {"status":"healthy","version":"2.0.0"}

관찰 포인트

지표정상 범위알림 조건
arm1 uptime>0서비스 다운
scheduler_jobs2≠ 2
번역 동기화ko=en 개수 일치차이 발생
포스트 생성매일 1건24시간 이상 미생성

교훈과 운영 팁

1. Reasoning 모델의 함정

max_tokens가 reasoning과 content를 합산한다는 것을 문서에서 명시하지 않는 경우가 많습니다. 빈 응답이 나오면 finish_reason을 확인하세요 — "length"라면 토큰 예산 부족입니다.

2. OpenAI-Compatible 패턴의 가치

번역 제공자를 ZAI에서 Gemini로 바꿀 때 코드 변경이 base URL 1줄이었습니다. 처음부터 OpenAI-compatible 인터페이스로 추상화하면 LLM 교체 비용이 극적으로 줄어듭니다.

3. 날짜 접두사 매칭의 제약

YYYY-MM-DD-NNN 패턴에서 같은 날짜에 같은 번호가 2개 이상 존재하면 번역 매칭이 깨집니다. PostGenerator에서 새 게시글 생성 시 해당 날짜의 마지막 번호 + 1을 확인하는 로직이 필수입니다.

4. 통합 프로세스의 이점

3개 독립 서비스를 1개로 통합하면서 얻은 것:

  • 상태 공유 (LLM 클라이언트, 설정, API 클라이언트를 한 번만 초기화)
  • 배포 단순화 (systemd 유닛 1개)
  • 디버깅 용이 (로그가 한 곳에 모임)

향후 계획

  • arm1 에이전트의 LLM도 Gemini로 통합 검토
  • 댓글 품질 평가 파이프라인 (자동 생성 댓글의 적절성 모니터링)
  • 번역 품질 자동 검증 (역번역 비교)
  • AgentForge 프레임워크를 통한 에이전트 간 협업 확대

블로그 자동화는 “완전 자동"이 아니라 “최소 개입"을 목표로 합니다. AI가 생성한 콘텐츠를 사람이 검토하고, 시스템이 이상 징후를 감지하면 운영자에게 알리는 구조가 안정적인 운영의 핵심입니다.

Hugo로 만듦
JimmyStack 테마 사용 중