<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Agentforge on Yarang's Tech Lair</title><link>https://blog.fcoinfup.com/ko/tags/agentforge/</link><description>Recent content in Agentforge on Yarang's Tech Lair</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Fri, 08 May 2026 21:57:11 +0900</lastBuildDate><atom:link href="https://blog.fcoinfup.com/ko/tags/agentforge/index.xml" rel="self" type="application/rss+xml"/><item><title>NATS JetStream으로 멀티-LLM 분산 오케스트레이터 구축하기</title><link>https://blog.fcoinfup.com/ko/post/nats-jetstream%EC%9C%BC%EB%A1%9C-%EB%A9%80%ED%8B%B0-llm-%EB%B6%84%EC%82%B0-%EC%98%A4%EC%BC%80%EC%8A%A4%ED%8A%B8%EB%A0%88%EC%9D%B4%ED%84%B0-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0/</link><pubDate>Fri, 08 May 2026 21:57:11 +0900</pubDate><guid>https://blog.fcoinfup.com/ko/post/nats-jetstream%EC%9C%BC%EB%A1%9C-%EB%A9%80%ED%8B%B0-llm-%EB%B6%84%EC%82%B0-%EC%98%A4%EC%BC%80%EC%8A%A4%ED%8A%B8%EB%A0%88%EC%9D%B4%ED%84%B0-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0/</guid><description>&lt;p&gt;1편에서는 Claude, ZAI, Codex, Gemini 네 가지 AI를 같은 태스크에 동시에 돌리면서 발견한 모델별 제한 사항을 다뤘다. 이번 편은 &amp;ldquo;어떻게 그게 가능하도록 만들었나&amp;rdquo;—시스템 설계와 구현 이야기다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="시스템-개요"&gt;시스템 개요
&lt;/h2&gt;&lt;p&gt;AgentForge는 세 가지 요소로 이루어진다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[태스크 발행자]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ NATS JetStream publish
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[NATS 브로커] ─── af.worker.{id}.inbox
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ JetStream consume (워커별 독립 스트림)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[워커 폴러] × N (poller.py × 18개)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ LLM CLI 실행 (claude / codex / gemini)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[결과 반환] af.task.{task_id}.completed
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;발행자가 NATS에 태스크를 올리면, 각 워커가 독립적으로 구독하고 있다가 자신의 inbox로 들어온 메시지를 받아 LLM CLI를 실행한다. 결과는 완료 주제로 다시 publish된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="왜-nats-jetstream인가"&gt;왜 NATS JetStream인가
&lt;/h2&gt;&lt;p&gt;메시지 브로커 선택지는 여러 개였다: Redis Streams, Kafka, RabbitMQ, NATS JetStream.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NATS JetStream을 선택한 이유:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;단일 바이너리&lt;/strong&gt; — 별도의 런타임 없이 &lt;code&gt;nats-server&lt;/code&gt; 하나로 동작한다. Kafka의 ZooKeeper나 RabbitMQ의 Erlang/OTP 의존성이 없다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;내장 영속성&lt;/strong&gt; — JetStream은 NATS 위에 올라가는 스트리밍 레이어로, 메시지를 파일시스템에 저장한다. 워커가 재시작되어도 처리 안 된 태스크가 유실되지 않는다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;NKey 기반 인증&lt;/strong&gt; — 워커별로 독립된 Ed25519 keypair를 발급할 수 있다. 한 워커가 침해되어도 다른 워커의 자격증명은 유효하다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;경량&lt;/strong&gt; — 단일 서버에서 메모리 사용량 ~30MB. 18개 워커를 연결해도 브로커 부하가 거의 없다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id="핵심-pollerpy의-백엔드-어댑터"&gt;핵심: poller.py의 백엔드 어댑터
&lt;/h2&gt;&lt;p&gt;워커의 핵심은 &lt;code&gt;poller.py&lt;/code&gt;다. 이 파일 하나가 NATS 구독, LLM CLI 실행, 결과 반환을 모두 담당한다.&lt;/p&gt;
&lt;p&gt;LLM별 실행 방식이 다르기 때문에, 백엔드 어댑터 딕셔너리로 분리했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;_BACKENDS: dict[str, dict] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;claude&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;bin&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;CLAUDE_BIN&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/usr/local/bin/claude&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;tools&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;ALLOWED_TOOLS&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;Read,Edit,Write,Glob,Grep&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;CLAUDE_MODEL&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;codex&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;bin&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;CODEX_BIN&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/usr/bin/codex&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;CODEX_MODEL&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;sandbox&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;CODEX_SANDBOX&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;read-only&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;gemini_cli&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;bin&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;GEMINI_BIN&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/usr/bin/gemini&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;GEMINI_MODEL&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;MODEL_BACKEND&lt;/code&gt; 환경변수로 어떤 LLM을 쓸지 결정한다. 덕분에 동일한 &lt;code&gt;poller.py&lt;/code&gt; 코드로 18개 워커가 각자 다른 LLM을 실행한다.&lt;/p&gt;
&lt;h3 id="claude-백엔드"&gt;Claude 백엔드
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;run_claude&lt;/span&gt;(instructions: str, task_id: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; tuple[int, str]:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cfg &lt;span style="color:#f92672"&gt;=&lt;/span&gt; _BACKENDS[&lt;span style="color:#e6db74"&gt;&amp;#34;claude&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cmd &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [cfg[&lt;span style="color:#e6db74"&gt;&amp;#34;bin&amp;#34;&lt;/span&gt;], &lt;span style="color:#e6db74"&gt;&amp;#34;--print&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;--allowedTools&amp;#34;&lt;/span&gt;, cfg[&lt;span style="color:#e6db74"&gt;&amp;#34;tools&amp;#34;&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; cfg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cmd &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;--model&amp;#34;&lt;/span&gt;, cfg[&lt;span style="color:#e6db74"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proc &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; asyncio&lt;span style="color:#f92672"&gt;.&lt;/span&gt;create_subprocess_exec(&lt;span style="color:#f92672"&gt;*&lt;/span&gt;cmd, stdin&lt;span style="color:#f92672"&gt;=&lt;/span&gt;PIPE, stdout&lt;span style="color:#f92672"&gt;=&lt;/span&gt;PIPE, stderr&lt;span style="color:#f92672"&gt;=&lt;/span&gt;PIPE)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;--print&lt;/code&gt; 플래그가 핵심이다. Claude Code가 대화 모드가 아닌 비대화형 모드로 실행되어 stdout으로 결과를 반환하게 만든다.&lt;/p&gt;
&lt;h3 id="zai-백엔드"&gt;ZAI 백엔드
&lt;/h3&gt;&lt;p&gt;ZAI는 Anthropic API 호환 엔드포인트를 제공하기 때문에 별도 백엔드가 없다. 환경변수 두 개로 라우팅을 바꾼다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /etc/agentforge/cc-zai-high-dev-01.env&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ANTHROPIC_BASE_URL&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;lt;ZAI endpoint&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ANTHROPIC_AUTH_TOKEN&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;lt;ZAI API key&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;systemd &lt;code&gt;EnvironmentFile=&lt;/code&gt; 지시어로 이 파일을 주입하면, claude 바이너리가 ZAI 엔드포인트로 요청을 보낸다. 코드 변경 없이 환경변수만으로 다른 LLM 공급자를 연결하는 셈이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="선언적-관리-fleetyaml--serversyaml"&gt;선언적 관리: fleet.yaml × servers.yaml
&lt;/h2&gt;&lt;p&gt;18개 워커를 수동으로 관리하는 건 비현실적이다. 두 개의 YAML 파일로 전체 인프라를 선언적으로 정의했다.&lt;/p&gt;
&lt;h3 id="serversyaml--서버-인벤토리"&gt;servers.yaml — 서버 인벤토리
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;servers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-node-1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;role&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;services&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;agentforge-worker, tunnel-arm1]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;broker-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;role&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;broker-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;services&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;nats-jetstream, postgres]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-node-2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;role&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;services&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;agentforge-worker, tunnel-arm1]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="fleetyaml--워커-배치"&gt;fleet.yaml — 워커 배치
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;workers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;worker_id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;cc-go-dev-01&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;llm&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;claude-code&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;model&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;claude-sonnet-4-6&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;lang&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;go&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;role&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;developer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;host&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-node-1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;create_pr&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;worker_id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;codex-py-dev-01&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;llm&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;codex&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;model&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gpt-5.5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;lang&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;python&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;role&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;developer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;host&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-node-1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;create_pr&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;host&lt;/code&gt; 필드 하나를 바꾸면 워커가 다른 서버로 이동한다. &lt;code&gt;enabled: false&lt;/code&gt;로 설정하면 배포 스크립트가 해당 워커를 중지한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="워커-템플릿-시스템-provision_workerpy"&gt;워커 템플릿 시스템: provision_worker.py
&lt;/h2&gt;&lt;p&gt;워커를 새로 추가할 때마다 systemd 유닛 파일을 직접 작성하는 건 오류가 생기기 쉽다. Jinja2 템플릿 + 프로비저닝 스크립트로 자동화했다.&lt;/p&gt;
&lt;h3 id="템플릿-구조"&gt;템플릿 구조
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;templates/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; systemd/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; claude.service.j2 # claude-code, ZAI 공용
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; codex.service.j2 # OpenAI Codex
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gemini.service.j2 # Google Gemini CLI
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;claude.service.j2&lt;/code&gt;의 핵심 부분:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;MODEL_BACKEND&lt;span style="color:#f92672"&gt;=&lt;/span&gt;claude
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CLAUDE_BIN&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ claude_bin }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; claude_model &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CLAUDE_MODEL&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ claude_model }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; endif &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; env_file &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EnvironmentFile&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ env_file }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; endif &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;WORK_BASE&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ work_base }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;WORK_DIR&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ work_base }}&lt;span style="color:#f92672"&gt;/&lt;/span&gt;repo
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;{{ &amp;#39;ALLOWED_TOOLS=&amp;#39; + allowed_tools }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CREATE_PR&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ &lt;span style="color:#e6db74"&gt;&amp;#39;true&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; create_pr &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;false&amp;#39;&lt;/span&gt; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; create_pr &lt;span style="color:#f92672"&gt;and&lt;/span&gt; github_remote &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;GITHUB_REMOTE&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ github_remote }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; endif &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ZAI 워커는 &lt;code&gt;env_file&lt;/code&gt; 블록이 활성화되어 EnvironmentFile이 추가된다. PR 생성 워커는 &lt;code&gt;github_remote&lt;/code&gt;가 주입된다. 나머지는 기본값을 쓴다.&lt;/p&gt;
&lt;h3 id="provision_workerpy-사용법"&gt;provision_worker.py 사용법
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 미리보기 (실제 배포 없음)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 scripts/provision_worker.py --worker new-worker-id --dry-run
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 실제 배포 (NATS creds 발급 포함)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 scripts/provision_worker.py --worker new-worker-id --issue-creds
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# fleet.yaml 전체 일괄 배포&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 scripts/provision_worker.py --all
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;내부적으로 수행하는 작업:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;fleet.yaml&lt;/code&gt;에서 워커 항목 읽기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;servers.yaml&lt;/code&gt;에서 대상 호스트 읽기&lt;/li&gt;
&lt;li&gt;Jinja2 템플릿 렌더링&lt;/li&gt;
&lt;li&gt;SSH로 &lt;code&gt;/etc/systemd/system/{worker_id}-poller.service&lt;/code&gt; 배포&lt;/li&gt;
&lt;li&gt;워크 디렉터리 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;systemctl daemon-reload &amp;amp;&amp;amp; enable --now&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;(선택) &lt;code&gt;nsc add user&lt;/code&gt;로 NATS NKey 발급 → creds 배포 → &lt;code&gt;auth.conf&lt;/code&gt; 재생성&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id="분산-호스트-두-번째-서버에-워커-추가"&gt;분산 호스트: 두 번째 서버에 워커 추가
&lt;/h2&gt;&lt;p&gt;모든 워커를 한 서버에서 돌리면 단일 장애점이 된다. 두 번째 호스트에 Claude 워커를 추가했다.&lt;/p&gt;
&lt;p&gt;두 번째 호스트에서 NATS 브로커에 연결하는 방법은 autossh 터널이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Description&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;NATS 브로커 터널&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;After&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;network-online.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ExecStart&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/usr/bin/autossh -N \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -L 4222:127.0.0.1:4222 \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -i /home/ubuntu/.ssh/id_ed25519 \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; broker-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Restart&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;always&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;RestartSec&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이 설정이 활성화된 상태에서 워커는 항상 &lt;code&gt;nats://127.0.0.1:4222&lt;/code&gt;로 연결한다. 브로커 호스트 주소를 몰라도 된다. 터널만 살아있으면 어느 호스트에서든 동일하게 동작한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="nats-자격증명-운영-경험"&gt;NATS 자격증명 운영 경험
&lt;/h2&gt;&lt;p&gt;구현 중 가장 복잡했던 부분은 NATS NKey 관리다.&lt;/p&gt;
&lt;p&gt;NATS JetStream의 인증 구조는 계층적이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Operator (최상위 서명 기관)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── Account: SYS (시스템 계정)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── Account: Services (워커 계정)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├── User: cc-dev-01
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├── User: cc-go-dev-01
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├── User: codex-py-dev-01
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── ...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;각 워커는 독립된 User NKey를 가지고, Services 계정의 권한 범위(&lt;code&gt;af.&amp;gt;&lt;/code&gt;, &lt;code&gt;_INBOX.&amp;gt;&lt;/code&gt;, &lt;code&gt;$JS.&amp;gt;&lt;/code&gt;) 내에서만 publish/subscribe할 수 있다.&lt;/p&gt;
&lt;p&gt;신규 워커를 추가할 때 Operator의 signing key가 필요하다. 초기에 이 키의 백업을 만들지 않았다가 분실하는 사고가 있었다. 결과적으로 Operator를 전부 재생성하고 모든 워커의 creds를 일괄 교체했다. 서비스 다운타임은 약 60초.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 재생성 절차&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nsc add operator AgentForge
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nsc add account SYS
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nsc add account Services
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; worker in cc-dev-01 cc-go-dev-01 ...; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nsc add user --account Services --name $worker &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --allow-pub &lt;span style="color:#e6db74"&gt;&amp;#34;af.&amp;gt;,_INBOX.&amp;gt;,&lt;/span&gt;$JS&lt;span style="color:#e6db74"&gt;.&amp;gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --allow-sub &lt;span style="color:#e6db74"&gt;&amp;#34;af.&amp;gt;,_INBOX.&amp;gt;,&lt;/span&gt;$JS&lt;span style="color:#e6db74"&gt;.&amp;gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nsc generate config --mem-resolver --sys-account SYS &amp;gt; auth.new.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="새-워커-추가-전체-절차"&gt;새 워커 추가: 전체 절차
&lt;/h2&gt;&lt;p&gt;이 시스템이 완성된 이후 새 워커를 추가하는 절차는 단순하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1단계&lt;/strong&gt;: &lt;code&gt;fleet.yaml&lt;/code&gt;에 항목 추가&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;worker_id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;my-new-worker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;llm&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;claude-code&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;model&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;claude-haiku-4-5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;lang&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;multi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;role&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;developer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;host&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-node-1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;create_pr&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;2단계&lt;/strong&gt;: 미리보기&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 scripts/provision_worker.py --worker my-new-worker --dry-run
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;3단계&lt;/strong&gt;: 실제 배포&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 scripts/provision_worker.py --worker my-new-worker --issue-creds
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;끝이다. 템플릿 렌더링, SSH 배포, NATS 자격증명 발급, 서비스 등록까지 한 명령으로 처리된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="다음-단계"&gt;다음 단계
&lt;/h2&gt;&lt;p&gt;현재 시스템은 워커가 태스크를 독립적으로 처리하는 구조다. 앞으로 만들고 싶은 것:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;라우팅 정책&lt;/strong&gt;: 태스크 특성에 따라 적합한 워커를 자동 선택 (Go 코드 → claude-go-dev, 비용 최우선 → ZAI 경량 티어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;결과 비교 대시보드&lt;/strong&gt;: fan-out 결과를 나란히 보여주는 UI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비용 추적&lt;/strong&gt;: 워커별 API 호출 비용 집계&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;코드는 GitHub에 공개되어 있다.&lt;/p&gt;</description></item><item><title>4개의 AI에게 같은 코딩 태스크를 동시에 보내봤다</title><link>https://blog.fcoinfup.com/ko/post/4%EA%B0%9C%EC%9D%98-ai%EC%97%90%EA%B2%8C-%EA%B0%99%EC%9D%80-%EC%BD%94%EB%94%A9-%ED%83%9C%EC%8A%A4%ED%81%AC%EB%A5%BC-%EB%8F%99%EC%8B%9C%EC%97%90-%EB%B3%B4%EB%82%B4%EB%B4%A4%EB%8B%A4/</link><pubDate>Fri, 08 May 2026 21:55:39 +0900</pubDate><guid>https://blog.fcoinfup.com/ko/post/4%EA%B0%9C%EC%9D%98-ai%EC%97%90%EA%B2%8C-%EA%B0%99%EC%9D%80-%EC%BD%94%EB%94%A9-%ED%83%9C%EC%8A%A4%ED%81%AC%EB%A5%BC-%EB%8F%99%EC%8B%9C%EC%97%90-%EB%B3%B4%EB%82%B4%EB%B4%A4%EB%8B%A4/</guid><description>&lt;p&gt;같은 버그 수정 태스크를 Claude, ZAI(GLM), OpenAI Codex, Google Gemini에게 동시에 던지면 어떤 일이 벌어질까?&lt;/p&gt;
&lt;p&gt;이 질문에서 AgentForge 프로젝트가 시작됐다. 여러 LLM CLI를 NATS JetStream 메시지 큐로 묶어서 같은 태스크를 병렬로 처리하는 시스템을 만들었고, 그 과정에서 예상치 못한 발견들이 있었다. 이번 글은 &amp;ldquo;설정하면서 뭘 발견했나&amp;quot;에 집중한 비교 실험 기록이다.&lt;/p&gt;
&lt;p&gt;시스템의 설계·구현 이야기는 2편에서 다룬다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="테스트한-ai-목록"&gt;테스트한 AI 목록
&lt;/h2&gt;&lt;p&gt;최종적으로 운영 중인 워커 18개의 구성은 다음과 같다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;계열&lt;/th&gt;
 &lt;th&gt;모델&lt;/th&gt;
 &lt;th&gt;비고&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;claude-sonnet-4-6&lt;/td&gt;
 &lt;td&gt;메인 개발 워커&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Claude Code&lt;/td&gt;
 &lt;td&gt;claude-sonnet-4-5&lt;/td&gt;
 &lt;td&gt;이전 세대 비교용&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Claude Code&lt;/td&gt;
 &lt;td&gt;claude-haiku-4-5&lt;/td&gt;
 &lt;td&gt;경량·고속&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Claude Code&lt;/td&gt;
 &lt;td&gt;claude-opus-4-6&lt;/td&gt;
 &lt;td&gt;최고 사양&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Claude Code&lt;/td&gt;
 &lt;td&gt;claude-opus-4-5&lt;/td&gt;
 &lt;td&gt;이전 세대 비교용&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;ZAI (GLM)&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;glm-5.1&lt;/td&gt;
 &lt;td&gt;고사양 티어&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;ZAI (GLM)&lt;/td&gt;
 &lt;td&gt;glm-4.7&lt;/td&gt;
 &lt;td&gt;중간 티어&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;ZAI (GLM)&lt;/td&gt;
 &lt;td&gt;glm-4.5-air&lt;/td&gt;
 &lt;td&gt;경량 티어&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;OpenAI Codex&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;gpt-5.5&lt;/td&gt;
 &lt;td&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Codex&lt;/td&gt;
 &lt;td&gt;gpt-5.4&lt;/td&gt;
 &lt;td&gt;1M 컨텍스트&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Codex&lt;/td&gt;
 &lt;td&gt;gpt-5.4-mini&lt;/td&gt;
 &lt;td&gt;400K 컨텍스트&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Codex&lt;/td&gt;
 &lt;td&gt;gpt-5.3-codex&lt;/td&gt;
 &lt;td&gt;272K 컨텍스트&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Google Gemini&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;gemini-2.5-flash&lt;/td&gt;
 &lt;td&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Gemini&lt;/td&gt;
 &lt;td&gt;gemini-2.5-pro&lt;/td&gt;
 &lt;td&gt;고사양&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Gemini&lt;/td&gt;
 &lt;td&gt;gemini-2.5-flash-lite&lt;/td&gt;
 &lt;td&gt;경량&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;처음 시작할 때 목록은 훨씬 짧았다. 어떤 모델을 쓸 수 있는지 직접 실험해보면서 늘어났다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="발견-1-claude-3x-시리즈는-이미-접근-불가"&gt;발견 1: Claude 3.x 시리즈는 이미 접근 불가
&lt;/h2&gt;&lt;p&gt;Claude Code를 오래 써온 사람이라면 Claude 3.7 Sonnet, 3.5 Sonnet, 3.5 Haiku를 떠올릴 수 있다. 그래서 이 모델들도 워커로 추가하려 했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;claude --model claude-3-7-sonnet-20250219 --print &lt;span style="color:#e6db74"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → &amp;#34;may not exist or no access&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;세 모델 모두 동일한 오류. Claude 3 시리즈는 2026년 초에 EOL을 맞이했고, Claude Code CLI를 통한 접근이 차단됐다. 현재 Claude Code 구독으로 쓸 수 있는 것은 4.x 계열뿐이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결론&lt;/strong&gt;: Claude 워커는 4.5/4.6 계열로만 구성했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="발견-2-chatgpt-계정-codex는-모델-선택이-제한적이다"&gt;발견 2: ChatGPT 계정 Codex는 모델 선택이 제한적이다
&lt;/h2&gt;&lt;p&gt;OpenAI Codex CLI는 ChatGPT Plus/Pro 계정이나 별도 API 키로 인증한다. ChatGPT 계정 기반일 경우 접근 가능한 모델이 제한된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;codex --model gpt-5.5-pro &lt;span style="color:#e6db74"&gt;&amp;#34;fix the bug&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → &amp;#34;Model gpt-5.5-pro is not supported with ChatGPT account&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;codex --model gpt-5.5 &lt;span style="color:#e6db74"&gt;&amp;#34;fix the bug&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → 정상 작동&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ChatGPT 계정으로 사용할 수 있는 모델:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;모델&lt;/th&gt;
 &lt;th&gt;컨텍스트&lt;/th&gt;
 &lt;th&gt;추론 수준&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;gpt-5.5&lt;/td&gt;
 &lt;td&gt;1M / 1M&lt;/td&gt;
 &lt;td&gt;High&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;gpt-5.4&lt;/td&gt;
 &lt;td&gt;1M / 1M&lt;/td&gt;
 &lt;td&gt;Medium&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;gpt-5.4-mini&lt;/td&gt;
 &lt;td&gt;400K / 400K&lt;/td&gt;
 &lt;td&gt;Medium&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;gpt-5.3-codex&lt;/td&gt;
 &lt;td&gt;272K / 400K&lt;/td&gt;
 &lt;td&gt;Medium&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;gpt-5.5-pro&lt;/code&gt;를 포함한 다른 모델은 모두 &amp;ldquo;not supported with ChatGPT account&amp;rdquo; 오류를 반환한다. API 키 방식이라면 더 많은 모델을 쓸 수 있지만, 그건 다른 접근 방식이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="발견-3-gemini-cli는-25-시리즈만-된다"&gt;발견 3: Gemini CLI는 2.5 시리즈만 된다
&lt;/h2&gt;&lt;p&gt;Gemini CLI(&lt;code&gt;gemini&lt;/code&gt; 바이너리)로 여러 모델을 테스트했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gemini -p &lt;span style="color:#e6db74"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt; -m gemini-2.0-flash
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → ModelNotFoundError: models/gemini-2.0-flash is not found&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gemini -p &lt;span style="color:#e6db74"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt; -m gemini-1.5-pro
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → ModelNotFoundError&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gemini -p &lt;span style="color:#e6db74"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt; -m gemini-2.5-flash
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → 정상 작동&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;현재 계정으로 접근 가능한 Gemini 모델:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gemini-2.5-flash&lt;/code&gt; — 기본 추천 모델&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gemini-2.5-pro&lt;/code&gt; — 고사양&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt; — 경량&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Gemini 2.0 이하 버전은 ModelNotFoundError를 반환한다. 계정 플랜이나 API 키 종류에 따라 다를 수 있지만, Gemini CLI 기준으로는 2.5 시리즈만 안정적으로 동작했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="발견-4-zai는-claude-sdk로-우회할-수-있다"&gt;발견 4: ZAI는 Claude SDK로 우회할 수 있다
&lt;/h2&gt;&lt;p&gt;ZAI는 Anthropic API와 호환되는 엔드포인트를 제공하는 서비스다. 덕분에 Claude Code CLI에서 환경변수 두 개만 바꿔서 GLM 모델을 쓸 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ANTHROPIC_BASE_URL&lt;span style="color:#f92672"&gt;=&lt;/span&gt;https://&amp;lt;ZAI endpoint&amp;gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ANTHROPIC_AUTH_TOKEN&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&amp;lt;ZAI_KEY&amp;gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;claude --model glm-5.1 --print &lt;span style="color:#e6db74"&gt;&amp;#34;fix the bug&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Claude Code가 내부적으로 Anthropic Python SDK를 쓰기 때문에, &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt;만 오버라이드하면 동일한 포맷으로 ZAI의 GLM 모델을 호출한다. 별도의 어댑터 코드 없이 기존 &lt;code&gt;claude&lt;/code&gt; 백엔드를 그대로 재사용할 수 있다는 점이 흥미로웠다.&lt;/p&gt;
&lt;p&gt;사용한 GLM 모델 3종:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;glm-5.1&lt;/code&gt; — 고사양 티어&lt;/li&gt;
&lt;li&gt;&lt;code&gt;glm-4.7&lt;/code&gt; — 비용·성능 균형점&lt;/li&gt;
&lt;li&gt;&lt;code&gt;glm-4.5-air&lt;/code&gt; — 경량·고속&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="4-way-fan-out-비교-테스트"&gt;4-way Fan-out 비교 테스트
&lt;/h2&gt;&lt;p&gt;18개 워커 중 대표 4개(Claude Sonnet, GLM-5.1, Codex gpt-5.5, Gemini 2.5 Flash)에 동일한 Go 버그 수정 태스크를 동시에 발행했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;태스크: &amp;#34;fix the off-by-one error in the binary search function&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;응답 시간 (wall clock):&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;워커&lt;/th&gt;
 &lt;th&gt;모델&lt;/th&gt;
 &lt;th&gt;응답 시간&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;cc-go-dev-01&lt;/td&gt;
 &lt;td&gt;claude-sonnet-4-6&lt;/td&gt;
 &lt;td&gt;~8초&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;cc-zai-high-dev-01&lt;/td&gt;
 &lt;td&gt;glm-5.1&lt;/td&gt;
 &lt;td&gt;~12초&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;codex-py-dev-01&lt;/td&gt;
 &lt;td&gt;gpt-5.5&lt;/td&gt;
 &lt;td&gt;~15초&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;gemini-py-dev-01&lt;/td&gt;
 &lt;td&gt;gemini-2.5-flash&lt;/td&gt;
 &lt;td&gt;~10초&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;응답 시간보다 흥미로운 건 접근 방식의 차이다. Claude는 함수 전체를 리팩토링하는 경향이 있었고, Gemini는 최소한의 수정을 선호했다. Codex는 테스트 코드까지 함께 추가하는 경우가 많았다.&lt;/p&gt;
&lt;p&gt;물론 이건 단일 태스크 결과라 통계적 의미는 없다. 벤치마크가 아니라 &amp;ldquo;실제로 동작하는지 확인&amp;quot;하는 수준의 검증이었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="분산-워커-두-번째-호스트-추가"&gt;분산 워커: 두 번째 호스트 추가
&lt;/h2&gt;&lt;p&gt;워커들이 모두 한 서버에 있으면 비교 실험의 의미가 약해진다. 그래서 두 번째 호스트에 Claude 워커를 추가했다.&lt;/p&gt;
&lt;p&gt;두 번째 호스트에서 NATS 브로커(첫 번째 호스트)에 접근하는 방법은 autossh 터널이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ExecStart&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;autossh -N -L 4222:127.0.0.1:4222 broker-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;로컬의 4222 포트를 브로커로 포워딩하면 워커 코드 변경 없이 어느 호스트에서나 &lt;code&gt;nats://127.0.0.1:4222&lt;/code&gt;로 접속할 수 있다.&lt;/p&gt;
&lt;p&gt;이 방식의 장점: 워커는 브로커가 어디 있는지 알 필요가 없다. 항상 &lt;code&gt;localhost:4222&lt;/code&gt;로 연결하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="운영하면서-가장-당황했던-순간"&gt;운영하면서 가장 당황했던 순간
&lt;/h2&gt;&lt;p&gt;가장 곤혹스러운 상황은 NATS operator signing key를 분실한 것이었다. NATS JetStream은 NKey 기반 인증을 쓰는데, 신규 워커의 credentials를 발급하려면 operator/account의 signing key(nsc seed)가 필요하다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nsc add user --account Services --name new-worker
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → &amp;#34;signing key not found&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;백업이 없었다. 결국 NATS operator를 통째로 재생성하고, 모든 워커의 credentials를 새 권한 트리로 교체하는 대규모 컷오버를 진행했다. 서비스 다운타임은 약 60초였다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;교훈&lt;/strong&gt;: NATS operator seed는 생성 즉시 오프라인 백업을 만들어라. 분실하면 재생성 외에 방법이 없다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="정리"&gt;정리
&lt;/h2&gt;&lt;p&gt;이번 실험에서 얻은 실용적인 결론:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Claude 3.x는 EOL&lt;/strong&gt; - 2026년 기준 Claude Code CLI에서 접근 불가. 4.x만 쓸 것.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Codex ChatGPT 계정은 모델 4종만&lt;/strong&gt; - gpt-5.5, 5.4, 5.4-mini, 5.3-codex. Pro 모델은 별도 API 키 필요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gemini는 2.5 시리즈만&lt;/strong&gt; - CLI 기준 이전 버전 접근 불가.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ZAI는 Claude SDK 환경변수 오버라이드로 통합 가능&lt;/strong&gt; - 별도 어댑터 불필요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NATS NKey는 반드시 백업&lt;/strong&gt; - signing key 분실 = 전체 재발급.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;다음 편에서는 이 워커들이 어떻게 연결되는지, 시스템 설계와 구현을 다룬다.&lt;/p&gt;</description></item><item><title>[AgentForge] 블로그 자동화 서비스 전체 아키텍처 — AI 댓글, 번역, 포스트 생성까지</title><link>https://blog.fcoinfup.com/ko/post/2026-05-05-001-agentforge-blog-automation-architecture/</link><pubDate>Tue, 05 May 2026 00:30:00 +0900</pubDate><guid>https://blog.fcoinfup.com/ko/post/2026-05-05-001-agentforge-blog-automation-architecture/</guid><description>&lt;p&gt;블로그를 운영하면서 가장 번거로운 작업 세 가지가 있습니다. 댓글에 답하기, 영문 번역 유지하기, 그리고 꾸준히 글 쓰기. &lt;a class="link" href="https://github.com/yarang" target="_blank" rel="noopener"
 &gt;AgentForge&lt;/a&gt; 프로젝트에서는 이 세 가지를 모두 AI 에이전트로 자동화했습니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 2개 서버에 걸쳐 동작하는 블로그 자동화 서비스의 전체 아키텍처를 정리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="시스템-토폴로지"&gt;시스템 토폴로지
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;┌─────────────────────┐ HTTPS ┌─────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ arm1 서버 │ ──────────────▶ │ ec1 서버 │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ (에이전트 오퍼레이터) │ │ (블로그 호스팅) │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├─────────────────────┤ ├─────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ blog-agent (:8081) │ │ Hugo (nginx) │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├─ CommentHandler │ │ Blog API (:8000) │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├─ TranslateHandler│ │ ├─ translator.py │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └─ PostGenerator │ │ ├─ blog_manager.py │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ │ └─ git_handler.py │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ NATS / PostgreSQL │ │ │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ Prometheus / Grafana │ │ Git (yarang/blogs) │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└─────────────────────┘ └─────────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;서버&lt;/th&gt;
 &lt;th&gt;역할&lt;/th&gt;
 &lt;th&gt;핵심 서비스&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;arm1&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;에이전트 오퍼레이터&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;blog-agent.service&lt;/code&gt; — Flask + Scheduler + LLM Client&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;ec1&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;블로그 호스팅 + API&lt;/td&gt;
 &lt;td&gt;Hugo (nginx) + &lt;code&gt;blog-api.service&lt;/code&gt; (FastAPI)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;두 서버 간 통신은 &lt;strong&gt;HTTPS API 호출만&lt;/strong&gt; 가능합니다. arm1에서 ec1로의 SSH 접속은 차단되어 있어, 모든 연동은 Blog API를 통해 이루어집니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="arm1-통합-블로그-에이전트"&gt;arm1: 통합 블로그 에이전트
&lt;/h2&gt;&lt;h3 id="왜-통합했는가"&gt;왜 통합했는가
&lt;/h3&gt;&lt;p&gt;초기에는 댓글 응답, 번역, 포스트 생성이 각각 독립 프로세스(3개 systemd 서비스)로 운영되었습니다. 문제는:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude Code CLI(&lt;code&gt;--print&lt;/code&gt;) 호출 방식으로 &lt;strong&gt;응답 시간 9.7초&lt;/strong&gt;, 디스크 688MB 소모&lt;/li&gt;
&lt;li&gt;systemd 유닛 6개 관리 부담&lt;/li&gt;
&lt;li&gt;프로세스 간 상태 공유 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이를 &lt;strong&gt;1개 프로세스&lt;/strong&gt;로 통합하면서 직접 LLM API 호출로 전환했습니다. 결과:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;지표&lt;/th&gt;
 &lt;th&gt;Before&lt;/th&gt;
 &lt;th&gt;After&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;응답 시간&lt;/td&gt;
 &lt;td&gt;9.7초&lt;/td&gt;
 &lt;td&gt;1.7초&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;디스크 사용&lt;/td&gt;
 &lt;td&gt;688MB&lt;/td&gt;
 &lt;td&gt;~50MB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;systemd 유닛&lt;/td&gt;
 &lt;td&gt;6개&lt;/td&gt;
 &lt;td&gt;1개&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;프로세스&lt;/td&gt;
 &lt;td&gt;3개&lt;/td&gt;
 &lt;td&gt;1개&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="아키텍처"&gt;아키텍처
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;BlogAgent&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;1 프로세스 = Flask (webhook) + Scheduler (timer) + LLM Client&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;__init__&lt;/span&gt;(self):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;config &lt;span style="color:#f92672"&gt;=&lt;/span&gt; AgentConfig&lt;span style="color:#f92672"&gt;.&lt;/span&gt;from_credentials()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;llm &lt;span style="color:#f92672"&gt;=&lt;/span&gt; LLMClient(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;config) &lt;span style="color:#75715e"&gt;# ZAI glm-4.7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;api &lt;span style="color:#f92672"&gt;=&lt;/span&gt; BlogAPIClient(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;config) &lt;span style="color:#75715e"&gt;# ec1 Blog API&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 핸들러&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;comment &lt;span style="color:#f92672"&gt;=&lt;/span&gt; CommentHandler(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;llm, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;config)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;translate &lt;span style="color:#f92672"&gt;=&lt;/span&gt; TranslateHandler(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;api)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;post_gen &lt;span style="color:#f92672"&gt;=&lt;/span&gt; PostGenerator(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;llm, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;api)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 스케줄러&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;scheduler &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Scheduler()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;scheduler&lt;span style="color:#f92672"&gt;.&lt;/span&gt;every(hours&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;, task&lt;span style="color:#f92672"&gt;=&lt;/span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;translate&lt;span style="color:#f92672"&gt;.&lt;/span&gt;check_and_sync)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;scheduler&lt;span style="color:#f92672"&gt;.&lt;/span&gt;daily_at(hour&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;, task&lt;span style="color:#f92672"&gt;=&lt;/span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;post_gen&lt;span style="color:#f92672"&gt;.&lt;/span&gt;generate_and_publish)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="모듈별-동작"&gt;모듈별 동작
&lt;/h3&gt;&lt;h4 id="1-commenthandler--ai-댓글-응답"&gt;1. CommentHandler — AI 댓글 응답
&lt;/h4&gt;&lt;p&gt;GitHub Discussions의 Webhook 이벤트를 수신하여 자동으로 AI 댓글을 생성합니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[사용자 댓글] → GitHub Webhook → arm1 Flask → CommentHandler
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; → LLM 호출 (ZAI glm-4.7) → 답변 생성 → GitHub API로 댓글 게시
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;트리거&lt;/strong&gt;: Webhook 이벤트 기반 (실시간)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;필터링&lt;/strong&gt;: 블로그 소유자 댓글, AI 생성 댓글은 건너뜀&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;보안&lt;/strong&gt;: HMAC-SHA256 Webhook 시크릿 검증, Flask-Limiter 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="2-translatehandler--자동-번역-트리거"&gt;2. TranslateHandler — 자동 번역 트리거
&lt;/h4&gt;&lt;p&gt;6시간마다 ec1의 Blog API에 번역 동기화를 요청합니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[Scheduler 6h] → TranslateHandler.check_and_sync()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; → POST /translate/sync → ec1 Blog API가 실제 번역 수행
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;arm1은 번역을 직접 수행하지 않고, ec1 API에 &lt;strong&gt;트리거만&lt;/strong&gt; 보냅니다. 실제 번역 로직은 ec1의 &lt;code&gt;translator.py&lt;/code&gt;에 있습니다.&lt;/p&gt;
&lt;h4 id="3-postgenerator--자동-포스트-생성"&gt;3. PostGenerator — 자동 포스트 생성
&lt;/h4&gt;&lt;p&gt;매일 오전 9시에 기술 블로그 포스트를 자동 생성합니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[Scheduler 09:00 KST] → PostGenerator.generate_and_publish()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; → 기존 주제 수집 → RSS 트렌드 참조 → LLM으로 콘텐츠 생성
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; → 중복 검사 → Blog API로 게시
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;중복 방지&lt;/strong&gt;가 핵심입니다. &lt;code&gt;difflib.SequenceMatcher&lt;/code&gt;로 새 제목과 최근 100개 기존 제목의 유사도를 비교합니다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;_is_duplicate_title&lt;/span&gt;(self, new_title, existing_titles):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;threshold 0.6 이상이면 중복으로 판정&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; new_lower &lt;span style="color:#f92672"&gt;=&lt;/span&gt; new_title&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lower()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;strip()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; title &lt;span style="color:#f92672"&gt;in&lt;/span&gt; existing_titles[&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;:]:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ex_lower &lt;span style="color:#f92672"&gt;=&lt;/span&gt; title&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lower()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;strip()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ratio &lt;span style="color:#f92672"&gt;=&lt;/span&gt; difflib&lt;span style="color:#f92672"&gt;.&lt;/span&gt;SequenceMatcher(&lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;, new_lower, ex_lower)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ratio()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; ratio &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.6&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="ec1-blog-api-번역-시스템"&gt;ec1: Blog API 번역 시스템
&lt;/h2&gt;&lt;h3 id="gemini로의-전환"&gt;Gemini로의 전환
&lt;/h3&gt;&lt;p&gt;초기에는 ZAI(glm-4.7)로 번역을 수행했으나, 치명적인 문제가 발생했습니다:&lt;/p&gt;

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

 &lt;/blockquote&gt;
&lt;p&gt;이로 인해 &lt;strong&gt;9개 영문 게시글의 제목이 빈 문자열&lt;/strong&gt;로 번역되는 사고가 발생했습니다.&lt;/p&gt;
&lt;p&gt;해결책: &lt;strong&gt;Gemini 2.5 Flash Lite&lt;/strong&gt;로 교체.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;항목&lt;/th&gt;
 &lt;th&gt;ZAI (이전)&lt;/th&gt;
 &lt;th&gt;Gemini (현재)&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;모델&lt;/td&gt;
 &lt;td&gt;glm-4.7 (reasoning)&lt;/td&gt;
 &lt;td&gt;gemini-2.5-flash-lite&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;번역 시간&lt;/td&gt;
 &lt;td&gt;~30초/포스트&lt;/td&gt;
 &lt;td&gt;~8초/포스트&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;비용&lt;/td&gt;
 &lt;td&gt;API 유료&lt;/td&gt;
 &lt;td&gt;무료 (1,500건/일)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;빈 응답 문제&lt;/td&gt;
 &lt;td&gt;발생&lt;/td&gt;
 &lt;td&gt;없음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="openai-compatible-엔드포인트"&gt;OpenAI-Compatible 엔드포인트
&lt;/h3&gt;&lt;p&gt;Gemini는 OpenAI 호환 API를 제공합니다. 기존 코드를 &lt;strong&gt;한 줄도 바꾸지 않고&lt;/strong&gt; base URL만 교체하면 됩니다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;LLM_BASE_URLS &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;GEMINI&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;https://generativelanguage.googleapis.com/v1beta/openai&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;ZAI&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;https://api.z.ai/api/coding/paas/v4&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="번역-매칭-로직"&gt;번역 매칭 로직
&lt;/h3&gt;&lt;p&gt;한국어↔영어 게시글 페어링은 &lt;strong&gt;날짜 접두사 매칭&lt;/strong&gt;을 사용합니다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ko: 2026-05-04-001-개발-생산성-17배-극대화-deepseek-v4와-...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;en: 2026-05-04-001-개발-생산성-17배-극대화-deepseek-v4와-...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↑ 같은 접두사 = 같은 게시글
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;slug의 언어가 다를 수 있지만, &lt;code&gt;YYYY-MM-DD-NNN&lt;/code&gt; 부분이 같으면 같은 게시글로 인식합니다. 이 방식의 전제 조건은 &lt;strong&gt;같은 날짜에 같은 번호가 2개 이상 존재하면 안 된다&lt;/strong&gt;는 것입니다.&lt;/p&gt;
&lt;h3 id="title-in-body-번역-기법"&gt;Title-in-Body 번역 기법
&lt;/h3&gt;&lt;p&gt;제목을 별도 API 호출로 번역하면 reasoning 모델에서 빈 결과가 나오는 문제가 있었습니다. 해결책은 &lt;strong&gt;제목을 본문 첫 줄에 포함&lt;/strong&gt;시키는 것:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 번역 요청 시&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;prompt &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;# &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;original_title&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;original_body&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 번역 결과에서 제목 추출&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; translated&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lstrip()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;startswith(&lt;span style="color:#e6db74"&gt;&amp;#34;# &amp;#34;&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lines &lt;span style="color:#f92672"&gt;=&lt;/span&gt; translated&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lstrip()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;split(&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; extracted_title &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lines[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lstrip(&lt;span style="color:#e6db74"&gt;&amp;#34;# &amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;strip()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; translated_body &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lines[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lstrip(&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;하나의 API 호출로 제목과 본문을 동시에 번역하므로, 맥락이 보존되고 토큰도 절약됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="llm-전략-역할별-모델-분리"&gt;LLM 전략: 역할별 모델 분리
&lt;/h2&gt;&lt;p&gt;하나의 LLM으로 모든 작업을 처리하지 않습니다. 작업 성격에 맞춰 모델을 분리했습니다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;작업&lt;/th&gt;
 &lt;th&gt;서버&lt;/th&gt;
 &lt;th&gt;모델&lt;/th&gt;
 &lt;th&gt;이유&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;댓글 AI 응답&lt;/td&gt;
 &lt;td&gt;arm1&lt;/td&gt;
 &lt;td&gt;ZAI glm-4.7&lt;/td&gt;
 &lt;td&gt;대화형, 한국어 품질 우수&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;포스트 생성&lt;/td&gt;
 &lt;td&gt;arm1&lt;/td&gt;
 &lt;td&gt;ZAI glm-4.7&lt;/td&gt;
 &lt;td&gt;긴 글 생성, 창의성 필요&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;번역 (ko→en)&lt;/td&gt;
 &lt;td&gt;ec1&lt;/td&gt;
 &lt;td&gt;Gemini Flash Lite&lt;/td&gt;
 &lt;td&gt;비추론형, 빠르고 무료&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;핵심 원칙: &lt;strong&gt;reasoning 모델은 번역에 쓰지 않는다&lt;/strong&gt;. reasoning 모델은 내부 사고에 토큰을 소비하므로, 단순 변환 작업에는 비추론형 모델이 적합합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="모니터링과-운영"&gt;모니터링과 운영
&lt;/h2&gt;&lt;h3 id="헬스체크-엔드포인트"&gt;헬스체크 엔드포인트
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# arm1 에이전트&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl http://arm1:8081/health
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → {&amp;#34;status&amp;#34;:&amp;#34;healthy&amp;#34;,&amp;#34;agent&amp;#34;:&amp;#34;blog-agent&amp;#34;,&amp;#34;scheduler_jobs&amp;#34;:2,&amp;#34;uptime_sec&amp;#34;:...}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl http://arm1:8081/status
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → {&amp;#34;scheduler&amp;#34;:[{&amp;#34;name&amp;#34;:&amp;#34;auto-translate&amp;#34;,&amp;#34;last_run&amp;#34;:...},{&amp;#34;name&amp;#34;:&amp;#34;post-generator&amp;#34;,&amp;#34;last_run&amp;#34;:&amp;#34;2026-05-04&amp;#34;}]}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ec1 Blog API&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl https://blog.example.com/api/health
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → {&amp;#34;status&amp;#34;:&amp;#34;healthy&amp;#34;,&amp;#34;version&amp;#34;:&amp;#34;2.0.0&amp;#34;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="관찰-포인트"&gt;관찰 포인트
&lt;/h3&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;지표&lt;/th&gt;
 &lt;th&gt;정상 범위&lt;/th&gt;
 &lt;th&gt;알림 조건&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;arm1 uptime&lt;/td&gt;
 &lt;td&gt;&amp;gt;0&lt;/td&gt;
 &lt;td&gt;서비스 다운&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;scheduler_jobs&lt;/td&gt;
 &lt;td&gt;2&lt;/td&gt;
 &lt;td&gt;≠ 2&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;번역 동기화&lt;/td&gt;
 &lt;td&gt;ko=en 개수 일치&lt;/td&gt;
 &lt;td&gt;차이 발생&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;포스트 생성&lt;/td&gt;
 &lt;td&gt;매일 1건&lt;/td&gt;
 &lt;td&gt;24시간 이상 미생성&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="교훈과-운영-팁"&gt;교훈과 운영 팁
&lt;/h2&gt;&lt;h3 id="1-reasoning-모델의-함정"&gt;1. Reasoning 모델의 함정
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;max_tokens&lt;/code&gt;가 reasoning과 content를 &lt;strong&gt;합산&lt;/strong&gt;한다는 것을 문서에서 명시하지 않는 경우가 많습니다. 빈 응답이 나오면 &lt;code&gt;finish_reason&lt;/code&gt;을 확인하세요 — &lt;code&gt;&amp;quot;length&amp;quot;&lt;/code&gt;라면 토큰 예산 부족입니다.&lt;/p&gt;
&lt;h3 id="2-openai-compatible-패턴의-가치"&gt;2. OpenAI-Compatible 패턴의 가치
&lt;/h3&gt;&lt;p&gt;번역 제공자를 ZAI에서 Gemini로 바꿀 때 코드 변경이 &lt;strong&gt;base URL 1줄&lt;/strong&gt;이었습니다. 처음부터 OpenAI-compatible 인터페이스로 추상화하면 LLM 교체 비용이 극적으로 줄어듭니다.&lt;/p&gt;
&lt;h3 id="3-날짜-접두사-매칭의-제약"&gt;3. 날짜 접두사 매칭의 제약
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;YYYY-MM-DD-NNN&lt;/code&gt; 패턴에서 같은 날짜에 같은 번호가 2개 이상 존재하면 번역 매칭이 깨집니다. PostGenerator에서 새 게시글 생성 시 해당 날짜의 마지막 번호 + 1을 확인하는 로직이 필수입니다.&lt;/p&gt;
&lt;h3 id="4-통합-프로세스의-이점"&gt;4. 통합 프로세스의 이점
&lt;/h3&gt;&lt;p&gt;3개 독립 서비스를 1개로 통합하면서 얻은 것:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상태 공유 (LLM 클라이언트, 설정, API 클라이언트를 한 번만 초기화)&lt;/li&gt;
&lt;li&gt;배포 단순화 (systemd 유닛 1개)&lt;/li&gt;
&lt;li&gt;디버깅 용이 (로그가 한 곳에 모임)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="향후-계획"&gt;향후 계획
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;arm1 에이전트의 LLM도 Gemini로 통합 검토&lt;/li&gt;
&lt;li&gt;댓글 품질 평가 파이프라인 (자동 생성 댓글의 적절성 모니터링)&lt;/li&gt;
&lt;li&gt;번역 품질 자동 검증 (역번역 비교)&lt;/li&gt;
&lt;li&gt;AgentForge 프레임워크를 통한 에이전트 간 협업 확대&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;블로그 자동화는 &amp;ldquo;완전 자동&amp;quot;이 아니라 &amp;ldquo;최소 개입&amp;quot;을 목표로 합니다. AI가 생성한 콘텐츠를 사람이 검토하고, 시스템이 이상 징후를 감지하면 운영자에게 알리는 구조가 안정적인 운영의 핵심입니다.&lt;/p&gt;</description></item></channel></rss>