<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>AI on Yarang's Tech Lair</title><link>https://blog.agentthread.dev/ko/categories/ai/</link><description>Recent content in AI 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.agentthread.dev/ko/categories/ai/index.xml" rel="self" type="application/rss+xml"/><item><title>NATS JetStream으로 멀티-LLM 분산 오케스트레이터 구축하기</title><link>https://blog.agentthread.dev/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.agentthread.dev/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.agentthread.dev/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.agentthread.dev/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>블로그 AI 자동 댓글 시스템 구축기 (1/3) — 아키텍처와 구현</title><link>https://blog.agentthread.dev/ko/post/ai-auto-comment-system-part1-architecture/</link><pubDate>Sun, 03 May 2026 01:00:00 +0900</pubDate><guid>https://blog.agentthread.dev/ko/post/ai-auto-comment-system-part1-architecture/</guid><description>&lt;h2 id="개요"&gt;개요
&lt;/h2&gt;&lt;p&gt;블로그 댓글에 AI가 자동으로 응답하는 시스템을 구축했습니다. 독자가 블로그 포스트에 댓글을 남기면, AI 어시스턴트가 포스트 컨텍스트를 분석하여 기술적으로 정확하면서도 친절한 답변을 자동으로 생성합니다.&lt;/p&gt;
&lt;p&gt;이 시리즈는 3부작으로 구성됩니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1부 (이 글)&lt;/strong&gt;: 전체 아키텍처 설계와 핵심 코드 구현&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2부&lt;/strong&gt;: 파일 기반 인증, 권한 관리, 보안 강화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3부&lt;/strong&gt;: systemd 배포, nginx 프록시, 트러블슈팅&lt;/li&gt;
&lt;/ul&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-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; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[giscus] → GitHub Discussions에 댓글 생성
&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;[GitHub Webhook] → HTTP POST 전송
&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;[nginx reverse proxy] → 헤더 포워딩
&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;[Flask Worker] → 시그니처 검증 → 댓글 분석
&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;[Claude Code CLI] → AI 응답 생성
&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;[GitHub GraphQL API] → Discussion에 답변 게시
&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;[giscus] → 블로그에 답변 표시
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이 아키텍처에서 핵심은 &lt;strong&gt;giscus가 GitHub Discussions를 댓글 저장소로 사용한다&lt;/strong&gt;는 점입니다. 따라서 GitHub Webhook으로 새 댓글 이벤트를 수신하고, 같은 GitHub API로 응답을 게시할 수 있습니다.&lt;/p&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;giscus&lt;/td&gt;
					&lt;td&gt;블로그 댓글 위젯&lt;/td&gt;
					&lt;td&gt;GitHub Discussions 기반&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;GitHub Webhook&lt;/td&gt;
					&lt;td&gt;이벤트 전달&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;discussion_comment&lt;/code&gt; 이벤트&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;nginx&lt;/td&gt;
					&lt;td&gt;리버스 프록시&lt;/td&gt;
					&lt;td&gt;헤더 포워딩, SSL 터미네이션&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Flask Worker&lt;/td&gt;
					&lt;td&gt;웹훅 수신 및 처리&lt;/td&gt;
					&lt;td&gt;Python, Flask, Flask-Limiter&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Claude Code&lt;/td&gt;
					&lt;td&gt;AI 응답 생성&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;--print&lt;/code&gt; 모드 CLI 호출&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;GitHub GraphQL&lt;/td&gt;
					&lt;td&gt;응답 게시&lt;/td&gt;
					&lt;td&gt;Mutation API&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="giscus-설정"&gt;giscus 설정
&lt;/h2&gt;&lt;p&gt;Hugo 블로그에 giscus를 통합하려면 &lt;code&gt;hugo.toml&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-toml" data-lang="toml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;comments&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&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:#a6e22e"&gt;provider&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;giscus&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;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;comments&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;giscus&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;repo&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;yarang/blogs&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;repoId&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;YOUR_REPO_ID&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;category&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;General&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;categoryId&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;YOUR_CATEGORY_ID&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;mapping&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;pathname&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;strict&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;0&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;reactionsEnabled&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;1&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;emitMetadata&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;0&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;inputPosition&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;bottom&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;lang&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;ko&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;theme&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;noborder_gray&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;mapping = &amp;quot;pathname&amp;quot;&lt;/code&gt;은 포스트 URL 경로를 기준으로 Discussion을 매핑합니다. 이렇게 하면 각 블로그 포스트마다 독립적인 Discussion이 생성됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="github-webhook-구성"&gt;GitHub Webhook 구성
&lt;/h2&gt;&lt;p&gt;GitHub 리포지토리 Settings &amp;gt; Webhooks에서:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Payload URL&lt;/strong&gt;: &lt;code&gt;https://your-domain/webhook&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content type&lt;/strong&gt;: &lt;code&gt;application/json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secret&lt;/strong&gt;: HMAC-SHA256 서명용 시크릿 (보안 편에서 상세히 다룸)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Events&lt;/strong&gt;: &lt;code&gt;Discussion comments&lt;/code&gt; 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Webhook은 댓글이 생성될 때마다 &lt;code&gt;discussion_comment&lt;/code&gt; 이벤트를 Flask 워커로 전송합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="flask-워커-구현"&gt;Flask 워커 구현
&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-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;auto-comment-worker/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── scripts/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── auto-comment-worker.py # 메인 워커
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── deploy/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── auto-comment-worker.service # systemd 서비스
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── venv/ # Python 가상환경
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── logs/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── audit.log # 감사 로그
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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:#f92672"&gt;from&lt;/span&gt; flask &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Flask, request, jsonify
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; flask_limiter &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Limiter
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; flask_limiter.util &lt;span style="color:#f92672"&gt;import&lt;/span&gt; get_remote_address
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; marshmallow &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Schema, fields, validate, ValidationError
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flask&lt;/strong&gt;: 경량 웹 프레임워크로 Webhook 엔드포인트 제공&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flask-Limiter&lt;/strong&gt;: 속도 제한으로 남용 방지 (분당 10회)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;marshmallow&lt;/strong&gt;: 요청 스키마 검증으로 안전한 데이터 파싱&lt;/li&gt;
&lt;/ul&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:#a6e22e"&gt;@app.route&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/webhook&amp;#39;&lt;/span&gt;, methods&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#39;POST&amp;#39;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@limiter.limit&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;10 per minute&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;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;github_webhook&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;GitHub Webhook 수신&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 style="color:#75715e"&gt;# 1. 시그니처 검증&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; signature &lt;span style="color:#f92672"&gt;=&lt;/span&gt; request&lt;span style="color:#f92672"&gt;.&lt;/span&gt;headers&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;X-Hub-Signature-256&amp;#39;&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; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; verify_webhook_signature(request&lt;span style="color:#f92672"&gt;.&lt;/span&gt;data, signature):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log_audit(&lt;span style="color:#e6db74"&gt;&amp;#39;SIGNATURE_INVALID&amp;#39;&lt;/span&gt;, {&lt;span style="color:#e6db74"&gt;&amp;#39;ip&amp;#39;&lt;/span&gt;: request&lt;span style="color:#f92672"&gt;.&lt;/span&gt;remote_addr})
&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; jsonify({&lt;span style="color:#e6db74"&gt;&amp;#39;status&amp;#39;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;unauthorized&amp;#39;&lt;/span&gt;}), &lt;span style="color:#ae81ff"&gt;401&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;# 2. 요청 스키마 검증&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; schema &lt;span style="color:#f92672"&gt;=&lt;/span&gt; WebhookSchema()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; payload &lt;span style="color:#f92672"&gt;=&lt;/span&gt; schema&lt;span style="color:#f92672"&gt;.&lt;/span&gt;load(request&lt;span style="color:#f92672"&gt;.&lt;/span&gt;json)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;except&lt;/span&gt; ValidationError &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; err:
&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; jsonify({&lt;span style="color:#e6db74"&gt;&amp;#39;status&amp;#39;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;invalid&amp;#39;&lt;/span&gt;}), &lt;span style="color:#ae81ff"&gt;400&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;# 3. 댓글 정보 추출 및 필터링&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; comment &lt;span style="color:#f92672"&gt;=&lt;/span&gt; payload&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;comment&amp;#39;&lt;/span&gt;, {})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; discussion &lt;span style="color:#f92672"&gt;=&lt;/span&gt; payload&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;discussion&amp;#39;&lt;/span&gt;, {})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; original_author &lt;span style="color:#f92672"&gt;=&lt;/span&gt; comment&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;user&amp;#39;&lt;/span&gt;, {})&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;login&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;사용자&amp;#39;&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; _is_blog_owner(original_author):
&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; jsonify({&lt;span style="color:#e6db74"&gt;&amp;#39;status&amp;#39;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;owner_comment_ignored&amp;#39;&lt;/span&gt;}), &lt;span style="color:#ae81ff"&gt;200&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;# AI가 생성한 댓글이면 무시 (무한 루프 방지)&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; _is_ai_generated_comment(comment&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;body&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&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; jsonify({&lt;span style="color:#e6db74"&gt;&amp;#39;status&amp;#39;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;ai_comment_ignored&amp;#39;&lt;/span&gt;}), &lt;span style="color:#ae81ff"&gt;200&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;# 4. AI 응답 생성 및 게시&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; discussion_graphql_id &lt;span style="color:#f92672"&gt;=&lt;/span&gt; get_discussion_graphql_id(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; repo_owner, repo_name, discussion_number
&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; context &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;discussion_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;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;discussion_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; reply &lt;span style="color:#f92672"&gt;=&lt;/span&gt; analyze_comment(context, comment_body)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; post_reply_graphql(discussion_graphql_id, comment_body, original_author, reply)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;핵심 흐름은 4단계입니다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;시그니처 검증&lt;/strong&gt;: HMAC-SHA256으로 요청이 GitHub에서 왔는지 확인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스키마 검증&lt;/strong&gt;: marshmallow로 페이로드 구조 검증&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;: Claude Code로 AI 응답 생성 후 GraphQL API로 게시&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="무한-루프-방지"&gt;무한 루프 방지
&lt;/h3&gt;&lt;p&gt;AI가 생성한 댓글에 다시 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-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_ai_generated_comment&lt;/span&gt;(body: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; bool:
&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;AI가 생성한 댓글인지 식별&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ai_markers &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;#39;🤖 AI 어시스턴트&amp;#39;&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;#39;AI 어시스턴트&amp;#39;&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;#39;AgentForge&amp;#39;&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;#39;Claude Code로 자동 생성&amp;#39;&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;#39;자동 생성되었습니다&amp;#39;&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; body_lower &lt;span style="color:#f92672"&gt;=&lt;/span&gt; body&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lower()
&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; any(marker&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lower() &lt;span style="color:#f92672"&gt;in&lt;/span&gt; body_lower &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; marker &lt;span style="color:#f92672"&gt;in&lt;/span&gt; ai_markers)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;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-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;body &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;&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;**🤖 AI 어시스턴트**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&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;{&lt;/span&gt;safe_reply&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&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;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;*이 댓글은 AgentForge + Claude Code로 자동 생성되었습니다.*
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&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;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="claude-code-cli-호출"&gt;Claude Code CLI 호출
&lt;/h3&gt;&lt;p&gt;Claude Code의 &lt;code&gt;--print&lt;/code&gt; 모드를 사용하여 비대화형으로 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-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;analyze_comment&lt;/span&gt;(context: str, comment: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; str:
&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;AgentForge 설정으로 Claude Code 실행&amp;#34;&amp;#34;&amp;#34;&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;&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;{&lt;/span&gt;context[:&lt;span style="color:#ae81ff"&gt;2000&lt;/span&gt;]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&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;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&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;{&lt;/span&gt;comment&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&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;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&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;- 기술 블로그 어시스턴트로서 전문적이면서 친절하게
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;- 200자 이내로 간결하게
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&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;&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; cmd &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CLAUDE_CODE_PATH,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;--settings&amp;#39;&lt;/span&gt;, AGENTFORGE_CONFIG,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;--print&amp;#39;&lt;/span&gt;, prompt
&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; result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; run(cmd, capture_output&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;, text&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;, timeout&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;60&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;if&lt;/span&gt; result&lt;span style="color:#f92672"&gt;.&lt;/span&gt;returncode &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#f92672"&gt;and&lt;/span&gt; result&lt;span style="color:#f92672"&gt;.&lt;/span&gt;stdout&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;return&lt;/span&gt; result&lt;span style="color:#f92672"&gt;.&lt;/span&gt;stdout&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&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:#e6db74"&gt;&amp;#34;의견 감사합니다! 기술적인 부분에 대해 더 논의해보면 좋을 것 같습니다. 🙏&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;--settings&lt;/code&gt; 플래그로 AgentForge 전용 설정 파일을 지정합니다. 이 설정 파일에서 모델, 토큰 제한 등을 관리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--print&lt;/code&gt; 플래그는 Claude Code를 비대화형 모드로 실행하여 결과를 stdout으로 출력합니다. 대화형 모드와 달리 프롬프트-응답 한 번으로 종료됩니다.&lt;/p&gt;
&lt;h3 id="github-graphql-api-통합"&gt;GitHub GraphQL API 통합
&lt;/h3&gt;&lt;p&gt;giscus는 GitHub Discussions를 사용하므로, 응답도 GitHub GraphQL API로 게시해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discussion GraphQL ID 조회:&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:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;get_discussion_graphql_id&lt;/span&gt;(repo_owner, repo_name, discussion_number):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; query &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&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 style="color:#e6db74"&gt; query($owner: String!, $name: String!, $number: Int!) {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; repository(owner: $owner, name: $name) {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; discussion(number: $number) {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; id
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&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; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&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;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; variables &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;owner&amp;#34;&lt;/span&gt;: repo_owner,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: repo_name,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;number&amp;#34;&lt;/span&gt;: discussion_number
&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; response &lt;span style="color:#f92672"&gt;=&lt;/span&gt; requests&lt;span style="color:#f92672"&gt;.&lt;/span&gt;post(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; GITHUB_API_URL,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; json&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;query&amp;#34;&lt;/span&gt;: query, &lt;span style="color:#e6db74"&gt;&amp;#34;variables&amp;#34;&lt;/span&gt;: variables},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; headers&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;Authorization&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Bearer &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;GITHUB_TOKEN&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 style="color:#e6db74"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;application/json&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; timeout&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;10&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;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; response&lt;span style="color:#f92672"&gt;.&lt;/span&gt;status_code &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data &lt;span style="color:#f92672"&gt;=&lt;/span&gt; response&lt;span style="color:#f92672"&gt;.&lt;/span&gt;json()
&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; data[&lt;span style="color:#e6db74"&gt;&amp;#34;data&amp;#34;&lt;/span&gt;][&lt;span style="color:#e6db74"&gt;&amp;#34;repository&amp;#34;&lt;/span&gt;][&lt;span style="color:#e6db74"&gt;&amp;#34;discussion&amp;#34;&lt;/span&gt;][&lt;span style="color:#e6db74"&gt;&amp;#34;id&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;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Webhook 페이로드에는 Discussion의 REST API ID만 포함됩니다. 하지만 댓글을 게시하려면 GraphQL Node ID가 필요합니다. 따라서 먼저 GraphQL 쿼리로 Discussion의 Node ID를 조회한 뒤, 이를 사용하여 댓글을 게시합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;응답 게시 (Mutation):&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:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;post_reply_graphql&lt;/span&gt;(discussion_graphql_id, original_comment, original_author, reply):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; query &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&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 style="color:#e6db74"&gt; mutation($discussionId: ID!, $body: String!) {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; addDiscussionComment(input: {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; discussionId: $discussionId, body: $body
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&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; comment { id, databaseId }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&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; }
&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;&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;&lt;code&gt;addDiscussionComment&lt;/code&gt; mutation은 Discussion 전체에 새 댓글을 추가합니다. 특정 댓글에 대한 답글(reply)이 아니라 Discussion 레벨의 댓글입니다.&lt;/p&gt;
&lt;h3 id="입력-sanitization"&gt;입력 Sanitization
&lt;/h3&gt;&lt;p&gt;사용자 댓글은 외부 입력이므로 반드시 sanitize합니다:&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;sanitize_comment&lt;/span&gt;(body: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; str:
&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;사용자 입력 sanitization&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 style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; body:
&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; body
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; body &lt;span style="color:#f92672"&gt;=&lt;/span&gt; re&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sub(&lt;span style="color:#e6db74"&gt;r&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;[^&amp;gt;]+&amp;gt;&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;, body) &lt;span style="color:#75715e"&gt;# HTML 태그 제거&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; body &lt;span style="color:#f92672"&gt;=&lt;/span&gt; html&lt;span style="color:#f92672"&gt;.&lt;/span&gt;escape(body) &lt;span style="color:#75715e"&gt;# HTML 엔티티 이스케이프&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; body &lt;span style="color:#f92672"&gt;=&lt;/span&gt; body[:&lt;span style="color:#ae81ff"&gt;1000&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;return&lt;/span&gt; body
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;XSS 공격을 방지하기 위해 HTML 태그를 제거하고, 남은 특수 문자를 이스케이프하며, 길이를 1000자로 제한합니다.&lt;/p&gt;
&lt;h3 id="감사-로그"&gt;감사 로그
&lt;/h3&gt;&lt;p&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:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;log_audit&lt;/span&gt;(event_type: str, details: dict):
&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;보안 이벤트 감사 로그 기록&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 style="color:#66d9ef"&gt;with&lt;/span&gt; open(AUDIT_LOG, &lt;span style="color:#e6db74"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; f:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; f&lt;span style="color:#f92672"&gt;.&lt;/span&gt;write(json&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dumps({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;timestamp&amp;#39;&lt;/span&gt;: datetime&lt;span style="color:#f92672"&gt;.&lt;/span&gt;utcnow()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;isoformat(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;event&amp;#39;&lt;/span&gt;: event_type,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;details&amp;#39;&lt;/span&gt;: details
&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:#e6db74"&gt;&amp;#39;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;기록하는 이벤트 유형:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SIGNATURE_INVALID&lt;/code&gt;: 웹훅 시그니처 검증 실패&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INVALID_PAYLOAD&lt;/code&gt;: 잘못된 요청 페이로드&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WEBHOOK_RECEIVED&lt;/code&gt;: 웹훅 수신 성공&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AI_RESPONSE_SENT&lt;/code&gt;: AI 응답 게시 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="마무리"&gt;마무리
&lt;/h2&gt;&lt;p&gt;이번 1부에서는 giscus, GitHub Webhook, Flask 워커, Claude Code CLI, GitHub GraphQL API를 연결하는 전체 아키텍처와 핵심 구현 코드를 다뤘습니다.&lt;/p&gt;
&lt;p&gt;다음 2부에서는 이 시스템의 &lt;strong&gt;보안 강화&lt;/strong&gt; — 파일 기반 인증 관리, 파일 권한 검증, HMAC-SHA256 시그니처 검증 등을 상세히 다룹니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;이 글은 AgentForge 블로그 자동 댓글 시스템 시리즈의 1부입니다.&lt;/em&gt;&lt;/p&gt;</description></item><item><title>멀티모델 AI 에이전트 팀 설계: 조합형 아키텍처와 5팀 계층 구조</title><link>https://blog.agentthread.dev/ko/post/%EB%A9%80%ED%8B%B0%EB%AA%A8%EB%8D%B8-ai-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%ED%8C%80-%EC%84%A4%EA%B3%84-%EC%A1%B0%ED%95%A9%ED%98%95-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%99%80-5%ED%8C%80-%EA%B3%84%EC%B8%B5-%EA%B5%AC%EC%A1%B0/</link><pubDate>Mon, 30 Mar 2026 00:31:36 +0900</pubDate><guid>https://blog.agentthread.dev/ko/post/%EB%A9%80%ED%8B%B0%EB%AA%A8%EB%8D%B8-ai-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%ED%8C%80-%EC%84%A4%EA%B3%84-%EC%A1%B0%ED%95%A9%ED%98%95-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%99%80-5%ED%8C%80-%EA%B3%84%EC%B8%B5-%EA%B5%AC%EC%A1%B0/</guid><description>&lt;h2 id="개요"&gt;개요
&lt;/h2&gt;&lt;p&gt;블로그 시스템 구축을 위해 &lt;strong&gt;14명의 AI 전문가, 5개 팀, 4개 LLM 모델&lt;/strong&gt;로 구성된 멀티모델 에이전트 팀을 설계했습니다. 핵심은 두 가지입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;조합형 에이전트(Composed Agent)&lt;/strong&gt;: 역할 정의와 실행 프로필을 분리해 재사용성 극대화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;계층형 브릿지 리더십&lt;/strong&gt;: 상위팀-하위팀 간 기술 리드의 이중 소속으로 소통 병목 해결&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 글에서는 최종 구조, 모델 배분 전략, 조합형 아키텍처 설계 과정을 공유합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="배경-왜-멀티모델인가"&gt;배경: 왜 멀티모델인가
&lt;/h2&gt;&lt;p&gt;하나의 LLM으로 모든 작업을 처리하면 두 가지 문제가 발생합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;비용&lt;/strong&gt;: Claude Opus 수준의 모델로 14명 전문가를 실행하면 비용이 통제 불가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;적합성&lt;/strong&gt;: 설계에는 빠른 추론이, 보안 분석에는 깊은 논리가, 구현에는 안정적인 코딩이 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 작업 성격에 맞춰 모델을 분배했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="최종-팀-구조"&gt;최종 팀 구조
&lt;/h2&gt;&lt;p&gt;5개 팀, 14명 전문가, 4개 모델로 구성됩니다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 subgraph UPPER["상위팀 — 조정 팀 (steering-team) · consensus"]
 ORC["오케스트레이터&lt;br/&gt;relay:steering-orchestrator"]
 DES["설계자&lt;br/&gt;gemini:gemini-2.5-flash"]
 SEC["보안 검토자&lt;br/&gt;codex:gpt-4o"]
 STL["백엔드 기술 리드&lt;br/&gt;relay:developer-zai"]
 FTL["프론트엔드 기술 리드&lt;br/&gt;relay:developer-zai"]
 DTL["데스크탑 기술 리드&lt;br/&gt;relay:developer-zai"]
 INF["인프라 네트워크&lt;br/&gt;gemini:gemini-2.5-flash"]
 SAD["서버 관리자&lt;br/&gt;relay:developer-zai"]
 end

 subgraph LOWER_BE["백엔드 팀 · leader_decides"]
 BTL["백엔드 기술 리드"]
 BDEV["백엔드 개발자"]
 end

 subgraph LOWER_FE["프론트엔드 팀 · leader_decides"]
 FTL2["프론트엔드 기술 리드"]
 FDEV["프론트엔드 개발자"]
 FUX["UX 디자이너"]
 end

 subgraph LOWER_DT["데스크탑 팀 · leader_decides"]
 DTL2["데스크탑 기술 리드"]
 DDEV["데스크탑 개발자"]
 DUX["UX 디자이너"]
 end

 subgraph LOWER_INFRA["인프라 팀 · leader_decides"]
 SAD2["서버 관리자 (리더)"]
 INET["클라우드 네트워크"]
 DBA["DB 아키텍트"]
 end

 UPPER -.-&gt;|bridge| LOWER_BE
 UPPER -.-&gt;|bridge| LOWER_FE
 UPPER -.-&gt;|bridge| LOWER_DT
 UPPER -.-&gt;|bridge| LOWER_INFRA

 BTL --&gt; BDEV
 FTL2 --&gt; FDEV
 FTL2 --&gt; FUX
 DTL2 --&gt; DDEV
 DTL2 --&gt; DUX
 SAD2 --&gt; INET
 SAD2 --&gt; DBA&lt;/pre&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;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;/td&gt;
					&lt;td&gt;upper&lt;/td&gt;
					&lt;td&gt;consensus&lt;/td&gt;
					&lt;td&gt;오케스트레이터&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;lower&lt;/td&gt;
					&lt;td&gt;leader_decides&lt;/td&gt;
					&lt;td&gt;백엔드 기술 리드&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;lower&lt;/td&gt;
					&lt;td&gt;leader_decides&lt;/td&gt;
					&lt;td&gt;프론트엔드 기술 리드&lt;/td&gt;
					&lt;td&gt;3명&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;데스크탑 팀&lt;/td&gt;
					&lt;td&gt;lower&lt;/td&gt;
					&lt;td&gt;leader_decides&lt;/td&gt;
					&lt;td&gt;데스크탑 기술 리드&lt;/td&gt;
					&lt;td&gt;3명&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;인프라 팀&lt;/td&gt;
					&lt;td&gt;lower&lt;/td&gt;
					&lt;td&gt;leader_decides&lt;/td&gt;
					&lt;td&gt;서버 관리자&lt;/td&gt;
					&lt;td&gt;3명&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="인프라-팀-분리-결정"&gt;인프라 팀 분리 결정
&lt;/h2&gt;&lt;p&gt;초기 설계에서는 DB 아키텍트와 서버 관리자가 백엔드 팀에 포함되어 있었습니다. 하지만 &lt;strong&gt;작업 공간(Workspace) 기준&lt;/strong&gt;으로 분리했습니다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 subgraph 백엔드팀
 B["API 코드 작성&lt;br/&gt;FastAPI, Python&lt;br/&gt;workspace: VS Code / SSH"]
 end

 subgraph 인프라팀
 S["서버 관리&lt;br/&gt;Docker, Ubuntu, Nginx&lt;br/&gt;workspace: SSH 터미널"]
 N["클라우드 네트워크&lt;br/&gt;Cloudflare Dashboard&lt;br/&gt;workspace: 웹 콘솔"]
 D["DB 관리&lt;br/&gt;PostgreSQL, 마이그레이션&lt;br/&gt;workspace: psql / SSH"]
 end

 B -.-&gt;|API 배포| S
 B -.-&gt;|쿼리 최적화| D&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;분리 이유:&lt;/strong&gt; 작업 공간이 다르면 같은 팀에 두는 것보다 분리하는 것이 자연스럽습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="모델-배분-전략"&gt;모델 배분 전략
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;pie title 모델별 전문가 수
 "relay:developer-zai (GLM)" : 10
 "gemini:gemini-2.5-flash" : 2
 "codex:gpt-4o" : 1
 "zai:glm-4" : 1&lt;/pre&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;relay:developer-zai&lt;/td&gt;
					&lt;td&gt;10명&lt;/td&gt;
					&lt;td&gt;구현, 운영, 리드&lt;/td&gt;
					&lt;td&gt;비용 효율적, 안정적 코딩&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;gemini:gemini-2.5-flash&lt;/td&gt;
					&lt;td&gt;2명&lt;/td&gt;
					&lt;td&gt;설계, 인프라 네트워크&lt;/td&gt;
					&lt;td&gt;빠른 응답, 외부 API 호출 용이&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;codex:gpt-4o&lt;/td&gt;
					&lt;td&gt;1명&lt;/td&gt;
					&lt;td&gt;보안 검토&lt;/td&gt;
					&lt;td&gt;높은 추론 능력, OWASP 지식&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;zai:glm-4&lt;/td&gt;
					&lt;td&gt;1명&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;p&gt;10명의 구현 전문가를 GLM(저비용 모델)에 배정하여 전체 비용의 &lt;strong&gt;60-70%를 절감&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="조합형-에이전트-아키텍처-composed-agent-pattern"&gt;조합형 에이전트 아키텍처 (Composed Agent Pattern)
&lt;/h2&gt;&lt;p&gt;이번 설계의 핵심 혁신은 &lt;strong&gt;역할 정의(Expert)와 실행 프로필(Definition)의 분리&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3 id="기존-방식의-문제"&gt;기존 방식의 문제
&lt;/h3&gt;&lt;p&gt;기존에는 역할과 실행 로직이 결합되어 변경 시 전체 재작성이 필요하고 재사용이 불가능했습니다.&lt;/p&gt;
&lt;h3 id="조합형-방식"&gt;조합형 방식
&lt;/h3&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 DEF["Definition&lt;br/&gt;백엔드 개발자"]
 DEF --&gt; BASE["Base: backend-core"]
 DEF --&gt; CAP["Capabilities:&lt;br/&gt;rest-api, crud, auth-jwt"]
 DEF --&gt; PLAT["Platform: fastapi"]
 DEF --&gt; POL["Policy: blog-default"]

 BASE --&gt; |"조합"| RUN["런타임 에이전트"]
 CAP --&gt; |"조합"| RUN
 PLAT --&gt; |"조합"| RUN
 POL --&gt; |"조합"| RUN&lt;/pre&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;agent-library/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── definitions/ ← 14개 에이전트 정의
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── modules/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── base/ ← 6개 기본 모듈
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── capabilities/ ← 15개 역량 모듈
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── platforms/ ← 5개 플랫폼 모듈
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── policies/ ← 1개 정책
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── runs/ ← 실행 이력
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="장점"&gt;장점
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;재사용성&lt;/strong&gt;: &lt;code&gt;rest-api&lt;/code&gt; 역량 모듈은 백엔드 개발자와 기술 리드가 공유&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;플랫폼 교체&lt;/strong&gt;: &lt;code&gt;platform: fastapi&lt;/code&gt;를 &lt;code&gt;platform: django&lt;/code&gt;로 변경하면 즉시 전환&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;역량 확장&lt;/strong&gt;: 새 역량 모듈을 추가하고 Definition에 연결만 하면 됨&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정책 통일&lt;/strong&gt;: 모든 에이전트가 동일한 &lt;code&gt;blog-default&lt;/code&gt; 정책을 따름&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="전문가-definition-매핑"&gt;전문가-Definition 매핑
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;전문가&lt;/th&gt;
					&lt;th&gt;Definition&lt;/th&gt;
					&lt;th&gt;Base&lt;/th&gt;
					&lt;th&gt;Capabilities&lt;/th&gt;
					&lt;th&gt;Platform&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;백엔드 개발자&lt;/td&gt;
					&lt;td&gt;backend-developer&lt;/td&gt;
					&lt;td&gt;backend-core&lt;/td&gt;
					&lt;td&gt;rest-api, crud, auth-jwt&lt;/td&gt;
					&lt;td&gt;fastapi&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;백엔드 기술 리드&lt;/td&gt;
					&lt;td&gt;backend-tech-lead&lt;/td&gt;
					&lt;td&gt;backend-core&lt;/td&gt;
					&lt;td&gt;rest-api, crud, code-review&lt;/td&gt;
					&lt;td&gt;fastapi&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;프론트엔드 개발자&lt;/td&gt;
					&lt;td&gt;frontend-developer&lt;/td&gt;
					&lt;td&gt;frontend-core&lt;/td&gt;
					&lt;td&gt;markdown-renderer, list-filter-sort&lt;/td&gt;
					&lt;td&gt;nextjs&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;서버 관리자&lt;/td&gt;
					&lt;td&gt;server-administrator&lt;/td&gt;
					&lt;td&gt;server-core&lt;/td&gt;
					&lt;td&gt;docker-management, nginx-config, postgres-admin&lt;/td&gt;
					&lt;td&gt;ubuntu&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;인프라 네트워크&lt;/td&gt;
					&lt;td&gt;infra-network-admin&lt;/td&gt;
					&lt;td&gt;infra-core&lt;/td&gt;
					&lt;td&gt;dns-management, ssl-certificates, rate-limiting&lt;/td&gt;
					&lt;td&gt;cloudflare&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;보안 검토자&lt;/td&gt;
					&lt;td&gt;security-auditor&lt;/td&gt;
					&lt;td&gt;specialist-core&lt;/td&gt;
					&lt;td&gt;security-audit&lt;/td&gt;
					&lt;td&gt;fastapi&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;컨텍스트 압축&lt;/td&gt;
					&lt;td&gt;context-compressor&lt;/td&gt;
					&lt;td&gt;specialist-core&lt;/td&gt;
					&lt;td&gt;context-compression&lt;/td&gt;
					&lt;td&gt;markdown&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="tls-인증서-전략-cloudflare-origin-ca"&gt;TLS 인증서 전략: Cloudflare Origin CA
&lt;/h2&gt;&lt;p&gt;프로덕션 환경의 TLS 인증서로 Let&amp;rsquo;s Encrypt 대신 &lt;strong&gt;Cloudflare Origin CA&lt;/strong&gt;를 선택했습니다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;sequenceDiagram
 participant Client as 방문자
 participant CF as Cloudflare (Proxy)
 participant Nginx as Nginx (Origin)
 participant API as FastAPI

 Client-&gt;&gt;CF: HTTPS 요청
 CF-&gt;&gt;CF: Cloudflare 관리 인증서로 종료
 CF-&gt;&gt;Nginx: Origin CA 인증서로 암호화
 Nginx-&gt;&gt;API: HTTP (로컬)
 API--&gt;&gt;Nginx: 응답
 Nginx--&gt;&gt;CF: Origin CA로 암호화
 CF--&gt;&gt;Client: 응답&lt;/pre&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;항목&lt;/th&gt;
					&lt;th&gt;Let&amp;rsquo;s Encrypt&lt;/th&gt;
					&lt;th&gt;Cloudflare Origin CA&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;유효 기간&lt;/td&gt;
					&lt;td&gt;90일 (갱신 필요)&lt;/td&gt;
					&lt;td&gt;15년 (갱신 불필요)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;발급 방식&lt;/td&gt;
					&lt;td&gt;ACME 자동화 필요&lt;/td&gt;
					&lt;td&gt;Dashboard에서 수동 발급&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;복잡도&lt;/td&gt;
					&lt;td&gt;certbot 설정&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;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;Oracle Cloud ARM (4 OCPU, 24GB)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── PostgreSQL (호스트 직접 설치)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── Docker Compose
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── blog-api (FastAPI)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── blog-frontend (Next.js standalone)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── MinIO (S3 호환 스토리지)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── Nginx (Cloudflare Origin CA)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── Cloudflare Proxy (Full Strict SSL)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="relay-플러그인-에이전트-호출-메커니즘"&gt;Relay 플러그인: 에이전트 호출 메커니즘
&lt;/h2&gt;&lt;p&gt;팀 구조는 &lt;strong&gt;Relay 플러그인&lt;/strong&gt;을 통해 Claude Code에서 실행됩니다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;sequenceDiagram
 participant User as 사용자
 participant Claude as Claude Code
 participant Plugin as Relay Plugin
 participant MCP as MCP 서버
 participant LLM as 외부 LLM

 User-&gt;&gt;Claude: /relay:invoke-agent
 Claude-&gt;&gt;Plugin: 전문가 slug로 정의 로드
 Plugin-&gt;&gt;Plugin: Definition 조합 (base + capabilities + platform + policy)
 Plugin-&gt;&gt;Plugin: backed_by 확인

 alt relay:developer-zai
 Plugin-&gt;&gt;Claude: 내부 에이전트 실행
 else gemini:*
 Plugin-&gt;&gt;MCP: gemini_mcp 서버 호출
 MCP-&gt;&gt;LLM: Gemini API
 LLM--&gt;&gt;MCP: 응답
 MCP--&gt;&gt;Plugin: 결과
 else codex:*
 Plugin-&gt;&gt;MCP: codex_mcp 서버 호출
 MCP-&gt;&gt;LLM: OpenAI API
 LLM--&gt;&gt;MCP: 응답
 MCP--&gt;&gt;Plugin: 결과
 end

 Plugin--&gt;&gt;Claude: 최종 결과
 Claude--&gt;&gt;User: 응답&lt;/pre&gt;&lt;h3 id="backed_by-네임스페이스"&gt;backed_by 네임스페이스
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;네임스페이스&lt;/th&gt;
					&lt;th&gt;MCP 서버&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;code&gt;relay:developer-zai&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;내부 에이전트&lt;/td&gt;
					&lt;td&gt;구현, 운영 (저비용)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;relay:steering-orchestrator&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;내부 에이전트&lt;/td&gt;
					&lt;td&gt;조율, 최종 결정&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;gemini:gemini-2.5-flash&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;gemini_mcp&lt;/td&gt;
					&lt;td&gt;설계, 외부 API&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;codex:gpt-4o&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;codex_mcp&lt;/td&gt;
					&lt;td&gt;보안 분석&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;zai:glm-4&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;zai_mcp&lt;/td&gt;
					&lt;td&gt;컨텍스트 압축&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="설계-결정-이력"&gt;설계 결정 이력
&lt;/h2&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;/td&gt;
					&lt;td&gt;백엔드 팀에 포함&lt;/td&gt;
					&lt;td&gt;작업 공간이 다름 (SSH vs IDE)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Cloudflare Origin CA&lt;/td&gt;
					&lt;td&gt;Let&amp;rsquo;s Encrypt&lt;/td&gt;
					&lt;td&gt;15년 유효, 갱신 불필요&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;PostgreSQL 호스트 설치&lt;/td&gt;
					&lt;td&gt;Docker 컨테이너&lt;/td&gt;
					&lt;td&gt;단일 서버에서 메모리 효율 우선&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;tr&gt;
					&lt;td&gt;GLM 다수 배정&lt;/td&gt;
					&lt;td&gt;Claude 다수 배정&lt;/td&gt;
					&lt;td&gt;60-70% 비용 절감&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-완벽한-구조보다-실행-가능한-구조"&gt;1. &amp;ldquo;완벽한 구조&amp;quot;보다 &amp;ldquo;실행 가능한 구조&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;팀 구조, 모델 배정, 인프라 설정을 완벽하게 설계하려다 보면 시작조차 못 합니다.&lt;/p&gt;
&lt;h3 id="2-작업-공간이-곧-팀-경계"&gt;2. 작업 공간이 곧 팀 경계
&lt;/h3&gt;&lt;p&gt;코드를 작성하는 사람과 서버를 관리하는 사람은 물리적 작업 환경이 다르고, 그것이 자연스러운 팀 경계가 됩니다.&lt;/p&gt;
&lt;h3 id="3-조합형-아키텍처의-가치"&gt;3. 조합형 아키텍처의 가치
&lt;/h3&gt;&lt;p&gt;14명의 전문가, 5개 팀, 4개 모델이 얽히는 환경에서는 모듈 분리가 필수적입니다.&lt;/p&gt;
&lt;h3 id="4-비용은-설계-단계에서-결정된다"&gt;4. 비용은 설계 단계에서 결정된다
&lt;/h3&gt;&lt;p&gt;&amp;ldquo;이 작업에 꼭 고비용 모델이 필요한가?&amp;ldquo;를 매번 물어보면 자연스럽게 비용이 최적화됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="다음-단계"&gt;다음 단계
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Phase 1 구현 착수: DB, Auth, Post/Category CRUD, Docker&lt;/li&gt;
&lt;li&gt;팀 운영 경험 공유: 실제 실행 중 겪은 문제와 해결 과정&lt;/li&gt;
&lt;li&gt;성능 모니터링: 모델별 응답 시간, 비용 대비 품질 분석&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;

 &lt;blockquote&gt;
 &lt;p&gt;이 글은 Claude Code + Relay 플러그인을 활용한 AI 에이전트 팀 구성 경험을 정리한 것입니다.&lt;/p&gt;

 &lt;/blockquote&gt;</description></item><item><title>[LLM] 프롬프트 잘 작성하는 방법</title><link>https://blog.agentthread.dev/ko/post/2026-02-21-002-llm-prompt-guide/</link><pubDate>Sat, 21 Feb 2026 20:40:00 +0900</pubDate><guid>https://blog.agentthread.dev/ko/post/2026-02-21-002-llm-prompt-guide/</guid><description>&lt;h2 id="들어가며"&gt;들어가며
&lt;/h2&gt;&lt;p&gt;LLM(Large Language Model)을 효과적으로 활용하기 위해서는 좋은 프롬프트를 작성하는 것이 필수적입니다. 이 글에서는 프롬프트 엔지니어링의 핵심 원칙과 실전 패턴을 정리합니다.&lt;/p&gt;
&lt;h2 id="좋은-프롬프트의-핵심-원칙"&gt;좋은 프롬프트의 핵심 원칙
&lt;/h2&gt;&lt;h3 id="1-명확성-clarity"&gt;1. 명확성 (Clarity)
&lt;/h3&gt;&lt;p&gt;모호한 표현을 피하고 구체적으로 작성하세요.&lt;/p&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;좋은 코드를 작성해줘
&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;/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;Python으로 이진 탐색 트리를 구현해줘.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;insert, search, delete 메서드를 포함하고,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;시간 복잡도는 O(log n)이어야 해.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-컨텍스트-제공-context"&gt;2. 컨텍스트 제공 (Context)
&lt;/h3&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-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;나는 React 초보자야.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;useState와 useEffect의 차이점을 예제 코드와 함께 설명해줘.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-역할-부여-role-playing"&gt;3. 역할 부여 (Role Playing)
&lt;/h3&gt;&lt;p&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;당신은 10년 경력의 시니어 백엔드 개발자입니다.
&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="4-출력-형식-지정-output-format"&gt;4. 출력 형식 지정 (Output Format)
&lt;/h3&gt;&lt;p&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;다음 내용을 마크다운 표로 정리해줘:
&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;- 사용 사례
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="프롬프트-패턴"&gt;프롬프트 패턴
&lt;/h2&gt;&lt;h3 id="chain-of-thought-사고-과정"&gt;Chain of Thought (사고 과정)
&lt;/h3&gt;&lt;p&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;이 문제를 단계별로 생각해보자:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;1. 먼저 문제를 분석하고
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2. 해결 방법을 고민한 후
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;3. 최종 답변을 작성해
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="few-shot-learning"&gt;Few-Shot Learning
&lt;/h3&gt;&lt;p&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;다음 형식으로 요약해줘:
&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;입력: &amp;#34;오늘 날씨가 좋다&amp;#34;
&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;입력: &amp;#34;회의가 너무 길었다&amp;#34;
&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;입력: &amp;#34;새로운 프로젝트를 시작했다&amp;#34;
&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;/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;사용자 인증 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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- JWT 토큰 사용
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- Refresh token 로테이션
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- Rate limiting 적용
&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;1. API 엔드포인트 명세
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2. 시퀀스 다이어그램
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;3. 보안 고려사항
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="자주-하는-실수"&gt;자주 하는 실수
&lt;/h2&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;/td&gt;
					&lt;td&gt;핵심이 흐려짐&lt;/td&gt;
					&lt;td&gt;핵심만 간결하게&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;tr&gt;
					&lt;td&gt;컨텍스트 누락&lt;/td&gt;
					&lt;td&gt;부정확한 답변&lt;/td&gt;
					&lt;td&gt;배경 정보 추가&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;h2 id="실전-체크리스트"&gt;실전 체크리스트
&lt;/h2&gt;&lt;p&gt;프롬프트 작성 전 확인하세요:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 목표가 명확한가?&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 필요한 컨텍스트를 포함했는가?&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 출력 형식을 지정했는가?&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 제약사항을 명시했는가?&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 예시가 도움이 될 것인가?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="마무리"&gt;마무리
&lt;/h2&gt;&lt;p&gt;좋은 프롬프트는 명확하고, 구체적이며, 필요한 컨텍스트를 제공합니다. 연습을 통해 프롬프트 작성 실력을 향상시키세요.&lt;/p&gt;
&lt;h2 id="참고-자료"&gt;참고 자료
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://platform.openai.com/docs/guides/prompt-engineering" target="_blank" rel="noopener"
 &gt;OpenAI Prompt Engineering Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.anthropic.com/claude/docs/prompt-engineering" target="_blank" rel="noopener"
 &gt;Anthropic Claude Prompt Engineering&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;영어 버전:&lt;/strong&gt; &lt;a class="link" href="https://blog.agentthread.dev/post/2026-02-21-002-how-to-write-effective-llm-prompts/" &gt;English Version&lt;/a&gt;&lt;/p&gt;</description></item></channel></rss>