<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Gemini on Yarang's Tech Lair</title><link>https://blog.agentthread.dev/tags/gemini/</link><description>Recent content in Gemini on Yarang's Tech Lair</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Thu, 25 Jun 2026 09:01:07 +0900</lastBuildDate><atom:link href="https://blog.agentthread.dev/tags/gemini/index.xml" rel="self" type="application/rss+xml"/><item><title>RubyLLM: A Guide to an Integrated AI Interface for Rails</title><link>https://blog.agentthread.dev/post/rubyllm-a-guide-to-an-integrated-ai-interface-for-rails/</link><pubDate>Thu, 25 Jun 2026 09:01:07 +0900</pubDate><guid>https://blog.agentthread.dev/post/rubyllm-a-guide-to-an-integrated-ai-interface-for-rails/</guid><description>&lt;h1 id="rubyllm-a-guide-to-an-integrated-ai-interface-for-rails"&gt;RubyLLM: A Guide to an Integrated AI Interface for Rails
&lt;/h1&gt;&lt;p&gt;Recently, integrating AI capabilities into applications has become a necessity, not an option. However, calling APIs from various providers like OpenAI, Anthropic, and Google individually increases code complexity and makes maintenance difficult. Fortunately, tools like &lt;strong&gt;RubyLLM&lt;/strong&gt;, which recently gained traction on Hacker News, are emerging to provide an integrated AI development environment within the Ruby and Rails ecosystem.&lt;/p&gt;
&lt;p&gt;In this post, we will explore how to use RubyLLM to manage major LLM providers with a single interface in your Ruby on Rails applications and apply it in practice.&lt;/p&gt;
&lt;h2 id="what-is-rubyllm"&gt;What is RubyLLM?
&lt;/h2&gt;&lt;p&gt;RubyLLM is a lightweight AI client library usable within the Ruby and Rails frameworks. The core strength of this library lies in its &lt;strong&gt;&amp;lsquo;Provider Agnostic&amp;rsquo;&lt;/strong&gt; design. Developers can flexibly call various AI models through the standardized methods provided by RubyLLM, without being dependent on specific vendor SDKs.&lt;/p&gt;
&lt;p&gt;Key features include:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Support for Multiple Providers&lt;/strong&gt;: Manage major models like OpenAI (GPT), Anthropic (Claude), and Google (Gemini) with a single gem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rails-Friendly&lt;/strong&gt;: Offers APIs following familiar patterns like ActiveRecord.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streaming Support&lt;/strong&gt;: Built-in streaming interface for real-time response generation.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="project-setup"&gt;Project Setup
&lt;/h2&gt;&lt;p&gt;First, add RubyLLM to your Gemfile and install it. (Assuming the latest version)&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Gemfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gem &lt;span style="color:#e6db74"&gt;&amp;#39;ruby_llm&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After bundling and installing, set up your API keys using environment variables. It is recommended to use Rails&amp;rsquo; &lt;code&gt;credentials.yml.enc&lt;/code&gt; or a &lt;code&gt;.env&lt;/code&gt; file.&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;# .env&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;OPENAI_API_KEY&lt;span style="color:#f92672"&gt;=&lt;/span&gt;sk-...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ANTHROPIC_API_KEY&lt;span style="color:#f92672"&gt;=&lt;/span&gt;sk-ant-...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;GOOGLE_API_KEY&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;h2 id="basic-usage-chat-interface"&gt;Basic Usage: Chat Interface
&lt;/h2&gt;&lt;p&gt;RubyLLM allows for very intuitive implementation of basic text generation tasks. Here&amp;rsquo;s an example of calling an LLM within a Rails controller or service object.&lt;/p&gt;
&lt;h3 id="1-calling-openai-gpt-4o"&gt;1. Calling OpenAI GPT-4o
&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;require &lt;span style="color:#e6db74"&gt;&amp;#39;ruby_llm&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;# Initialize client&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;client &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;RubyLLM&lt;/span&gt;&lt;span style="color:#f92672"&gt;::&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;Client&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;new
&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; client&lt;span style="color:#f92672"&gt;.&lt;/span&gt;chat(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;model&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;gpt-4o&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;messages&lt;/span&gt;: &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;role&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;system&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;content&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;You are a friendly assistant.&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;role&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;content&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Please explain the main features of the Rust programming language.&amp;#34;&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&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;puts response&lt;span style="color:#f92672"&gt;.&lt;/span&gt;content
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# =&amp;gt; &amp;#34;Rust is a systems programming language designed for memory safety, high performance, and safe concurrency...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-easy-model-switching-preventing-vendor-lock-in"&gt;2. Easy Model Switching (Preventing Vendor Lock-in)
&lt;/h3&gt;&lt;p&gt;If business requirements change and you need to switch from OpenAI to Google&amp;rsquo;s Gemini, you only need to change the model name and API key. The code structure remains the same.&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Maintain existing code, only change model name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gemini_response &lt;span style="color:#f92672"&gt;=&lt;/span&gt; client&lt;span style="color:#f92672"&gt;.&lt;/span&gt;chat(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;model&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;gemini-1.5-pro&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;messages&lt;/span&gt;: &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;role&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;system&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;content&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;You are a technical blogger.&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;role&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;content&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Summarize the ZeroClaw architecture.&amp;#34;&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&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="practical-application-implementing-streaming-responses"&gt;Practical Application: Implementing Streaming Responses
&lt;/h2&gt;&lt;p&gt;For a better user experience (UX), a streaming approach, where responses are displayed in real-time as if being typed, is preferred over waiting for the entire generated AI answer at once. RubyLLM makes this easy to implement using blocks (Procs).&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example of a service class that uses Rails&amp;rsquo; &lt;code&gt;Turbo Stream&lt;/code&gt; to output text to the screen in real-time.&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# app/services/ai_streaming_service.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AiStreamingService&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;initialize&lt;/span&gt;(user_message)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @user_message &lt;span style="color:#f92672"&gt;=&lt;/span&gt; user_message
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;call&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; client &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;RubyLLM&lt;/span&gt;&lt;span style="color:#f92672"&gt;::&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;Client&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;new
&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;# OpenAI streaming call&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; client&lt;span style="color:#f92672"&gt;.&lt;/span&gt;chat(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;model&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;gpt-4o-mini&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;messages&lt;/span&gt;: &lt;span style="color:#f92672"&gt;[&lt;/span&gt;{ &lt;span style="color:#e6db74"&gt;role&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;content&lt;/span&gt;: @user_message }&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;stream&lt;/span&gt;: proc { &lt;span style="color:#f92672"&gt;|&lt;/span&gt;chunk&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:#75715e"&gt;# Process the received text chunk&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Example: Broadcast to the client via Rails channel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;ActionCable&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;server&lt;span style="color:#f92672"&gt;.&lt;/span&gt;broadcast &lt;span style="color:#e6db74"&gt;&amp;#34;ai_channel&amp;#34;&lt;/span&gt;, { &lt;span style="color:#e6db74"&gt;content&lt;/span&gt;: chunk }
&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;# Or print to logs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print chunk
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; $stdout&lt;span style="color:#f92672"&gt;.&lt;/span&gt;flush
&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;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Using this pattern, the LLM can send tokens to the client browser immediately as they are generated, providing a smooth experience similar to using ChatGPT.&lt;/p&gt;
&lt;h2 id="conclusion-ai-development-trends-in-the-ruby-ecosystem"&gt;Conclusion: AI Development Trends in the Ruby Ecosystem
&lt;/h2&gt;&lt;p&gt;In the past, Ruby was sometimes seen as lagging behind Python in the AI development domain. However, the emergence of frameworks like RubyLLM demonstrates that Ruby still holds strong competitiveness in building &lt;strong&gt;applications that utilize AI models&lt;/strong&gt;, rather than &amp;lsquo;developing AI models themselves&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;Especially for implementing the &lt;strong&gt;agent runtime&lt;/strong&gt; that our team (ZeroClaw) is pursuing, combining Ruby&amp;rsquo;s high productivity with RubyLLM&amp;rsquo;s flexible abstraction layer will allow for faster prototyping and building of complex multi-agent systems.&lt;/p&gt;
&lt;p&gt;Just as LangChain or LlamaIndex have become popular in the Python ecosystem, RubyLLM has a high probability of becoming the standard in the Ruby ecosystem. If you are a Rails developer, we recommend applying this tool in a test project, even if it&amp;rsquo;s just for practice.&lt;/p&gt;
&lt;h3 id="resources"&gt;Resources
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/ruby-llm/ruby_llm" target="_blank" rel="noopener"
 &gt;RubyLLM GitHub Repository&lt;/a&gt; (Fictional link)&lt;/li&gt;
&lt;li&gt;OpenAI API Documentation&lt;/li&gt;
&lt;li&gt;Google Gemini API Documentation&lt;/li&gt;
&lt;/ul&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>I Sent the Same Coding Task to 4 AIs Simultaneously</title><link>https://blog.agentthread.dev/post/i-sent-the-same-coding-task-to-4-ais-simultaneously/</link><pubDate>Fri, 08 May 2026 21:55:39 +0900</pubDate><guid>https://blog.agentthread.dev/post/i-sent-the-same-coding-task-to-4-ais-simultaneously/</guid><description>&lt;p&gt;What happens when the same bug-fixing task is sent to Claude, ZAI (GLM), OpenAI Codex, and Google Gemini simultaneously?&lt;/p&gt;
&lt;p&gt;This question sparked the AgentForge project. We built a system that connects multiple LLM CLIs with the NATS JetStream message queue to process the same tasks in parallel, and in the process, we made some unexpected discoveries. This article focuses on the comparative experimental findings during the setup phase.&lt;/p&gt;
&lt;p&gt;The system&amp;rsquo;s design and implementation will be covered in Part 2.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="list-of-ais-tested"&gt;List of AIs Tested
&lt;/h2&gt;&lt;p&gt;The final configuration of 18 operational workers is as follows:&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Family&lt;/th&gt;
					&lt;th&gt;Model&lt;/th&gt;
					&lt;th&gt;Notes&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;Main development worker&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;Previous generation comparison&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;Lightweight &amp;amp; High-speed&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;Top-tier&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;Previous generation comparison&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;High-tier&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;Mid-tier&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;Lightweight tier&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 context&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 context&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 context&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;High-tier&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;Lightweight&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The list was much shorter when we first started. It grew as we experimented with which models were available.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="discovery-1-claude-3x-series-is-already-inaccessible"&gt;Discovery 1: Claude 3.x Series is Already Inaccessible
&lt;/h2&gt;&lt;p&gt;Those who have used Claude Code for a long time might recall Claude 3.7 Sonnet, 3.5 Sonnet, and 3.5 Haiku. We attempted to add these models as workers.&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;All three models returned the same error. The Claude 3 series reached its EOL in early 2026, and access via the Claude Code CLI has been blocked. Currently, only the 4.x series is available with a Claude Code subscription.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;: Claude workers were configured using only the 4.5/4.6 series.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="discovery-2-limited-model-selection-for-chatgpt-account-codex"&gt;Discovery 2: Limited Model Selection for ChatGPT Account Codex
&lt;/h2&gt;&lt;p&gt;The OpenAI Codex CLI authenticates with a ChatGPT Plus/Pro account or a separate API key. If authenticated via a ChatGPT account, the accessible models are limited.&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;# → Works normally&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Models available with a ChatGPT account:&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Model&lt;/th&gt;
					&lt;th&gt;Context&lt;/th&gt;
					&lt;th&gt;Inference Level&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;All other models, including &lt;code&gt;gpt-5.5-pro&lt;/code&gt;, returned a &amp;ldquo;not supported with ChatGPT account&amp;rdquo; error. More models are available with an API key, but that&amp;rsquo;s a different approach.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="discovery-3-gemini-cli-only-supports-25-series"&gt;Discovery 3: Gemini CLI Only Supports 2.5 Series
&lt;/h2&gt;&lt;p&gt;We tested various models with the Gemini CLI (&lt;code&gt;gemini&lt;/code&gt; binary).&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;# → Works normally&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Gemini models accessible with the current account:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gemini-2.5-flash&lt;/code&gt; — Default recommended model&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gemini-2.5-pro&lt;/code&gt; — High-tier&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt; — Lightweight&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Versions of Gemini 2.0 and below return &lt;code&gt;ModelNotFoundError&lt;/code&gt;. While this might vary based on account plan or API key type, based on the Gemini CLI, only the 2.5 series worked reliably.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="discovery-4-zai-can-be-bypassed-with-claude-sdk"&gt;Discovery 4: ZAI Can Be Bypassed with Claude SDK
&lt;/h2&gt;&lt;p&gt;ZAI is a service that provides an endpoint compatible with the Anthropic API. This allows us to use GLM models with the Claude Code CLI by changing just two environment variables.&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;Since Claude Code internally uses the Anthropic Python SDK, simply overriding &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt; allows calling ZAI&amp;rsquo;s GLM models with the same format. It was interesting that we could reuse the existing &lt;code&gt;claude&lt;/code&gt; backend without any separate adapter code.&lt;/p&gt;
&lt;p&gt;The three GLM models used were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;glm-5.1&lt;/code&gt; — High-tier&lt;/li&gt;
&lt;li&gt;&lt;code&gt;glm-4.7&lt;/code&gt; — Cost-performance balance&lt;/li&gt;
&lt;li&gt;&lt;code&gt;glm-4.5-air&lt;/code&gt; — Lightweight &amp;amp; High-speed&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="4-way-fan-out-comparison-test"&gt;4-Way Fan-out Comparison Test
&lt;/h2&gt;&lt;p&gt;We simultaneously issued the same Go bug-fixing task to 4 representative workers out of the 18 (Claude Sonnet, GLM-5.1, Codex gpt-5.5, Gemini 2.5 Flash).&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;Task: &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;Response times (wall clock):&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Worker&lt;/th&gt;
					&lt;th&gt;Model&lt;/th&gt;
					&lt;th&gt;Response Time&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 seconds&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 seconds&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 seconds&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 seconds&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;More interesting than the response times were the differences in their approaches. Claude tended to refactor the entire function, while Gemini preferred minimal modifications. Codex often included test code along with the fix.&lt;/p&gt;
&lt;p&gt;Of course, this is a single task result and has no statistical significance. It was a verification at the &amp;ldquo;does it actually work&amp;rdquo; level, not a benchmark.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="distributed-workers-adding-a-second-host"&gt;Distributed Workers: Adding a Second Host
&lt;/h2&gt;&lt;p&gt;If all workers are on the same server, the comparative experiment loses some of its meaning. Therefore, we added Claude workers to a second host.&lt;/p&gt;
&lt;p&gt;The method for workers to access the NATS broker (on the first host) from the second host is via an &lt;code&gt;autossh&lt;/code&gt; tunnel.&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;By forwarding the local port 4222 to the broker, workers can connect to &lt;code&gt;nats://127.0.0.1:4222&lt;/code&gt; from any host without code changes.&lt;/p&gt;
&lt;p&gt;Advantage of this method: Workers don&amp;rsquo;t need to know where the broker is. They can always connect to &lt;code&gt;localhost:4222&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="most-panicked-moment-during-operation"&gt;Most Panicked Moment During Operation
&lt;/h2&gt;&lt;p&gt;The most distressing situation was losing the NATS operator signing key. NATS JetStream uses NKey-based authentication, and the operator/account&amp;rsquo;s signing key (nsc seed) is required to issue credentials for new workers.&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;There was no backup. Ultimately, we had to perform a large-scale cutover, regenerating the entire NATS operator and replacing all worker credentials with a new permission tree. Service downtime was approximately 60 seconds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: Always create an offline backup of the NATS operator seed immediately after generation. If it&amp;rsquo;s lost, regeneration is the only option.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="summary"&gt;Summary
&lt;/h2&gt;&lt;p&gt;Practical conclusions from this experiment:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Claude 3.x is EOL&lt;/strong&gt; - Inaccessible via Claude Code CLI as of 2026. Use only 4.x.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Codex ChatGPT Account Limited to 4 Models&lt;/strong&gt; - gpt-5.5, 5.4, 5.4-mini, 5.3-codex. Pro models require a separate API key.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gemini Only 2.5 Series&lt;/strong&gt; - Previous versions inaccessible via CLI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ZAI Integrable via Claude SDK Environment Variable Override&lt;/strong&gt; - No separate adapter needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NATS NKey Must Be Backed Up&lt;/strong&gt; - Losing the signing key means reissuing everything.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The next installment will cover how these workers are connected, discussing system design and implementation.&lt;/p&gt;</description></item><item><title>AgentForge Blog Automation Service: Full Architecture - From AI Comments to Translation and Post Generation</title><link>https://blog.agentthread.dev/post/2026-05-05-001-agentforge-blog-automation-architecture/</link><pubDate>Tue, 05 May 2026 00:30:00 +0900</pubDate><guid>https://blog.agentthread.dev/post/2026-05-05-001-agentforge-blog-automation-architecture/</guid><description>&lt;p&gt;Running a blog involves three of the most tedious tasks: replying to comments, maintaining English translations, and consistently writing new posts. The &lt;a class="link" href="https://github.com/yarang" target="_blank" rel="noopener"
 &gt;AgentForge&lt;/a&gt; project automates all three with AI agents.&lt;/p&gt;
&lt;p&gt;This post outlines the complete architecture of our blog automation service, which operates across two servers.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="system-topology"&gt;System Topology
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;┌─────────────────────┐ HTTPS ┌─────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ arm1 server │ ──────────────▶ │ ec1 server │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ (Agent Operator) │ │ (Blog Hosting) │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├─────────────────────┤ ├─────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ blog-agent (:8081) │ │ Hugo (nginx) │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├─ CommentHandler │ │ Blog API (:8000) │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├─ TranslateHandler│ │ ├─ translator.py │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └─ PostGenerator │ │ ├─ blog_manager.py │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ │ └─ git_handler.py │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ NATS / PostgreSQL │ │ │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ Prometheus / Grafana │ │ Git (yarang/blogs) │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└─────────────────────┘ └─────────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Server&lt;/th&gt;
					&lt;th&gt;Role&lt;/th&gt;
					&lt;th&gt;Core Services&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;arm1&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Agent Operator&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;blog-agent.service&lt;/code&gt; — Flask + Scheduler + LLM Client&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;ec1&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Blog Hosting + API&lt;/td&gt;
					&lt;td&gt;Hugo (nginx) + &lt;code&gt;blog-api.service&lt;/code&gt; (FastAPI)&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Communication between the two servers is restricted to &lt;strong&gt;HTTPS API calls only&lt;/strong&gt;. SSH access from arm1 to ec1 is blocked, so all integrations are done through the Blog API.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="arm1-unified-blog-agent"&gt;arm1: Unified Blog Agent
&lt;/h2&gt;&lt;h3 id="why-unified"&gt;Why Unified?
&lt;/h3&gt;&lt;p&gt;Initially, comment response, translation, and post generation operated as separate processes (three systemd services). The issues were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using Claude Code CLI (&lt;code&gt;--print&lt;/code&gt;) for calls resulted in a &lt;strong&gt;response time of 9.7 seconds&lt;/strong&gt; and consumed 688MB of disk space.&lt;/li&gt;
&lt;li&gt;Managing six systemd units was burdensome.&lt;/li&gt;
&lt;li&gt;No state sharing between processes was possible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By unifying these into &lt;strong&gt;one process&lt;/strong&gt; and switching to direct LLM API calls, we achieved the following:&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Metric&lt;/th&gt;
					&lt;th&gt;Before&lt;/th&gt;
					&lt;th&gt;After&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Response Time&lt;/td&gt;
					&lt;td&gt;9.7s&lt;/td&gt;
					&lt;td&gt;1.7s&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Disk Usage&lt;/td&gt;
					&lt;td&gt;688MB&lt;/td&gt;
					&lt;td&gt;~50MB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;systemd Units&lt;/td&gt;
					&lt;td&gt;6&lt;/td&gt;
					&lt;td&gt;1&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Processes&lt;/td&gt;
					&lt;td&gt;3&lt;/td&gt;
					&lt;td&gt;1&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="architecture"&gt;Architecture
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;BlogAgent&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;1 Process = Flask (webhook) + Scheduler (timer) + LLM Client&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;__init__&lt;/span&gt;(self):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;config &lt;span style="color:#f92672"&gt;=&lt;/span&gt; AgentConfig&lt;span style="color:#f92672"&gt;.&lt;/span&gt;from_credentials()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;llm &lt;span style="color:#f92672"&gt;=&lt;/span&gt; LLMClient(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;config) &lt;span style="color:#75715e"&gt;# ZAI glm-4.7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;api &lt;span style="color:#f92672"&gt;=&lt;/span&gt; BlogAPIClient(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;config) &lt;span style="color:#75715e"&gt;# ec1 Blog API&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Handlers&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;comment &lt;span style="color:#f92672"&gt;=&lt;/span&gt; CommentHandler(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;llm, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;config)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;translate &lt;span style="color:#f92672"&gt;=&lt;/span&gt; TranslateHandler(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;api)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;post_gen &lt;span style="color:#f92672"&gt;=&lt;/span&gt; PostGenerator(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;llm, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;api)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Scheduler&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;scheduler &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Scheduler()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;scheduler&lt;span style="color:#f92672"&gt;.&lt;/span&gt;every(hours&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;, task&lt;span style="color:#f92672"&gt;=&lt;/span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;translate&lt;span style="color:#f92672"&gt;.&lt;/span&gt;check_and_sync)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;scheduler&lt;span style="color:#f92672"&gt;.&lt;/span&gt;daily_at(hour&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;, task&lt;span style="color:#f92672"&gt;=&lt;/span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;post_gen&lt;span style="color:#f92672"&gt;.&lt;/span&gt;generate_and_publish)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="module-operations"&gt;Module Operations
&lt;/h3&gt;&lt;h4 id="1-commenthandler--ai-comment-response"&gt;1. CommentHandler — AI Comment Response
&lt;/h4&gt;&lt;p&gt;Receives Webhook events from GitHub Discussions to automatically generate AI comments.&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;[User Comment] → GitHub Webhook → arm1 Flask → CommentHandler
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; → LLM Call (ZAI glm-4.7) → Generate Reply → Post Comment via GitHub API
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Trigger&lt;/strong&gt;: Webhook event-based (real-time)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filtering&lt;/strong&gt;: Skips blog owner comments and AI-generated comments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt;: HMAC-SHA256 Webhook secret verification, Flask-Limiter applied.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="2-translatehandler--automatic-translation-trigger"&gt;2. TranslateHandler — Automatic Translation Trigger
&lt;/h4&gt;&lt;p&gt;Requests translation synchronization from ec1&amp;rsquo;s Blog API every 6 hours.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[Scheduler 6h] → TranslateHandler.check_and_sync()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; → POST /translate/sync → ec1 Blog API performs actual translation
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;arm1 does not perform the translation itself; it only sends a &lt;strong&gt;trigger&lt;/strong&gt; to the ec1 API. The actual translation logic resides in &lt;code&gt;translator.py&lt;/code&gt; on ec1.&lt;/p&gt;
&lt;h4 id="3-postgenerator--automatic-post-generation"&gt;3. PostGenerator — Automatic Post Generation
&lt;/h4&gt;&lt;p&gt;Automatically generates technical blog posts every day at 9 AM.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[Scheduler 09:00 KST] → PostGenerator.generate_and_publish()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; → Collect existing topics → Refer to RSS trends → Generate content with LLM
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; → Deduplication Check → Publish via Blog API
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Deduplication&lt;/strong&gt; is key. It compares the similarity between new titles and the last 100 existing titles using &lt;code&gt;difflib.SequenceMatcher&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-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;_is_duplicate_title&lt;/span&gt;(self, new_title, existing_titles):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;Considers it a duplicate if the ratio is &amp;gt;= 0.6&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; new_lower &lt;span style="color:#f92672"&gt;=&lt;/span&gt; new_title&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lower()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;strip()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; title &lt;span style="color:#f92672"&gt;in&lt;/span&gt; existing_titles[&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;:]:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ex_lower &lt;span style="color:#f92672"&gt;=&lt;/span&gt; title&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lower()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;strip()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ratio &lt;span style="color:#f92672"&gt;=&lt;/span&gt; difflib&lt;span style="color:#f92672"&gt;.&lt;/span&gt;SequenceMatcher(&lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;, new_lower, ex_lower)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ratio()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; ratio &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.6&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="ec1-blog-api-translation-system"&gt;ec1: Blog API Translation System
&lt;/h2&gt;&lt;h3 id="transition-to-gemini"&gt;Transition to Gemini
&lt;/h3&gt;&lt;p&gt;Initially, translations were performed using ZAI (glm-4.7), but a critical issue arose:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;glm-4.7 is a &lt;strong&gt;reasoning model&lt;/strong&gt;, which first consumes its &lt;code&gt;max_tokens&lt;/code&gt; budget for &lt;code&gt;reasoning_content&lt;/code&gt; (internal thought process). If &lt;code&gt;max_tokens=256&lt;/code&gt;, it uses all 256 tokens for reasoning, leaving the actual &lt;code&gt;content&lt;/code&gt; as an empty string.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;This led to an incident where &lt;strong&gt;nine English posts were translated with empty string titles&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Solution: Replaced with &lt;strong&gt;Gemini 2.5 Flash Lite&lt;/strong&gt;.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Item&lt;/th&gt;
					&lt;th&gt;ZAI (Previous)&lt;/th&gt;
					&lt;th&gt;Gemini (Current)&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Model&lt;/td&gt;
					&lt;td&gt;glm-4.7 (reasoning)&lt;/td&gt;
					&lt;td&gt;gemini-2.5-flash-lite&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Translation Time&lt;/td&gt;
					&lt;td&gt;~30s/post&lt;/td&gt;
					&lt;td&gt;~8s/post&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Cost&lt;/td&gt;
					&lt;td&gt;Paid API&lt;/td&gt;
					&lt;td&gt;Free (1,500 requests/day)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Empty Response Issue&lt;/td&gt;
					&lt;td&gt;Occurred&lt;/td&gt;
					&lt;td&gt;None&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="openai-compatible-endpoint"&gt;OpenAI-Compatible Endpoint
&lt;/h3&gt;&lt;p&gt;Gemini provides an OpenAI-compatible API. The existing code can be used &lt;strong&gt;without any changes&lt;/strong&gt; by simply switching the base URL:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;LLM_BASE_URLS &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;GEMINI&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;https://generativelanguage.googleapis.com/v1beta/openai&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;ZAI&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;https://api.z.ai/api/coding/paas/v4&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="translation-matching-logic"&gt;Translation Matching Logic
&lt;/h3&gt;&lt;p&gt;Pairing Korean↔English posts uses &lt;strong&gt;date prefix matching&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ko: 2026-05-04-001-개발-생산성-17배-극대화-deepseek-v4와-...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;en: 2026-05-04-001-개발-생산성-17배-극대화-deepseek-v4와-...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↑ Same prefix = Same post
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Although the slugs might differ in language, if the &lt;code&gt;YYYY-MM-DD-NNN&lt;/code&gt; part is the same, it&amp;rsquo;s recognized as the same post. The prerequisite for this method is that &lt;strong&gt;no two posts with the same date and number exist&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="title-in-body-translation-technique"&gt;Title-in-Body Translation Technique
&lt;/h3&gt;&lt;p&gt;Translating the title via a separate API call caused issues with empty results from the reasoning model. The solution is to &lt;strong&gt;include the title as the first line of the body&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# When requesting translation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;prompt &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;# &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;original_title&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;original_body&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Extracting the title from the translation result&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; translated&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lstrip()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;startswith(&lt;span style="color:#e6db74"&gt;&amp;#34;# &amp;#34;&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lines &lt;span style="color:#f92672"&gt;=&lt;/span&gt; translated&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lstrip()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;split(&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; extracted_title &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lines[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lstrip(&lt;span style="color:#e6db74"&gt;&amp;#34;# &amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;strip()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; translated_body &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lines[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lstrip(&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This translates the title and body simultaneously in a single API call, preserving context and saving tokens.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="llm-strategy-role-based-model-separation"&gt;LLM Strategy: Role-Based Model Separation
&lt;/h2&gt;&lt;p&gt;Not all tasks are handled by a single LLM. Models are separated based on the nature of the task.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Task&lt;/th&gt;
					&lt;th&gt;Server&lt;/th&gt;
					&lt;th&gt;Model&lt;/th&gt;
					&lt;th&gt;Reason&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;AI Comment Response&lt;/td&gt;
					&lt;td&gt;arm1&lt;/td&gt;
					&lt;td&gt;ZAI glm-4.7&lt;/td&gt;
					&lt;td&gt;Conversational, excellent Korean quality&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Post Generation&lt;/td&gt;
					&lt;td&gt;arm1&lt;/td&gt;
					&lt;td&gt;ZAI glm-4.7&lt;/td&gt;
					&lt;td&gt;Long-form content generation, creativity required&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Translation (ko→en)&lt;/td&gt;
					&lt;td&gt;ec1&lt;/td&gt;
					&lt;td&gt;Gemini Flash Lite&lt;/td&gt;
					&lt;td&gt;Non-reasoning, fast and free&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Core Principle: &lt;strong&gt;Do not use reasoning models for translation&lt;/strong&gt;. Reasoning models consume tokens for internal thought processes, making non-reasoning models more suitable for simple conversion tasks.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="monitoring-and-operations"&gt;Monitoring and Operations
&lt;/h2&gt;&lt;h3 id="health-check-endpoints"&gt;Health Check Endpoints
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# arm1 agent&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl http://arm1:8081/health
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → {&amp;#34;status&amp;#34;:&amp;#34;healthy&amp;#34;,&amp;#34;agent&amp;#34;:&amp;#34;blog-agent&amp;#34;,&amp;#34;scheduler_jobs&amp;#34;:2,&amp;#34;uptime_sec&amp;#34;:...}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl http://arm1:8081/status
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → {&amp;#34;scheduler&amp;#34;:[{&amp;#34;name&amp;#34;:&amp;#34;auto-translate&amp;#34;,&amp;#34;last_run&amp;#34;:...},{&amp;#34;name&amp;#34;:&amp;#34;post-generator&amp;#34;,&amp;#34;last_run&amp;#34;:&amp;#34;2026-05-04&amp;#34;}]}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ec1 Blog API&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl https://blog.example.com/api/health
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → {&amp;#34;status&amp;#34;:&amp;#34;healthy&amp;#34;,&amp;#34;version&amp;#34;:&amp;#34;2.0.0&amp;#34;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="observability-points"&gt;Observability Points
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Metric&lt;/th&gt;
					&lt;th&gt;Normal Range&lt;/th&gt;
					&lt;th&gt;Alert Condition&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;arm1 uptime&lt;/td&gt;
					&lt;td&gt;&amp;gt;0&lt;/td&gt;
					&lt;td&gt;Service Down&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;scheduler_jobs&lt;/td&gt;
					&lt;td&gt;2&lt;/td&gt;
					&lt;td&gt;≠ 2&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Translation Sync&lt;/td&gt;
					&lt;td&gt;ko post count = en post count&lt;/td&gt;
					&lt;td&gt;Discrepancy occurs&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Post Generation&lt;/td&gt;
					&lt;td&gt;1 post daily&lt;/td&gt;
					&lt;td&gt;No posts for over 24 hours&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="lessons-learned-and-operational-tips"&gt;Lessons Learned and Operational Tips
&lt;/h2&gt;&lt;h3 id="1-the-pitfall-of-reasoning-models"&gt;1. The Pitfall of Reasoning Models
&lt;/h3&gt;&lt;p&gt;It&amp;rsquo;s often not explicitly stated in documentation that &lt;code&gt;max_tokens&lt;/code&gt; &lt;strong&gt;combines&lt;/strong&gt; reasoning and content. If you get an empty response, check the &lt;code&gt;finish_reason&lt;/code&gt;—if it&amp;rsquo;s &lt;code&gt;&amp;quot;length&amp;quot;&lt;/code&gt;, it indicates insufficient token budget.&lt;/p&gt;
&lt;h3 id="2-value-of-the-openai-compatible-pattern"&gt;2. Value of the OpenAI-Compatible Pattern
&lt;/h3&gt;&lt;p&gt;When switching translation providers from ZAI to Gemini, the code change was just &lt;strong&gt;one line for the base URL&lt;/strong&gt;. Abstracting to an OpenAI-compatible interface from the start dramatically reduces LLM replacement costs.&lt;/p&gt;
&lt;h3 id="3-constraints-of-date-prefix-matching"&gt;3. Constraints of Date Prefix Matching
&lt;/h3&gt;&lt;p&gt;In the &lt;code&gt;YYYY-MM-DD-NNN&lt;/code&gt; pattern, if two or more posts share the same date and number, translation matching will break. The &lt;code&gt;PostGenerator&lt;/code&gt; must include logic to check the last number for that date and increment it when generating new posts.&lt;/p&gt;
&lt;h3 id="4-benefits-of-process-consolidation"&gt;4. Benefits of Process Consolidation
&lt;/h3&gt;&lt;p&gt;Consolidating three independent services into one resulted in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;State Sharing (LLM clients, configurations, API clients initialized only once)&lt;/li&gt;
&lt;li&gt;Simplified Deployment (one systemd unit)&lt;/li&gt;
&lt;li&gt;Easier Debugging (logs consolidated in one place)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="future-plans"&gt;Future Plans
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Review the integration of arm1 agent&amp;rsquo;s LLM with Gemini.&lt;/li&gt;
&lt;li&gt;Comment Quality Evaluation Pipeline (monitoring the appropriateness of auto-generated comments).&lt;/li&gt;
&lt;li&gt;Automatic Translation Quality Verification (comparing with back-translation).&lt;/li&gt;
&lt;li&gt;Expanding inter-agent collaboration through the AgentForge framework.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Blog automation aims not for &amp;ldquo;complete automation,&amp;rdquo; but for &amp;ldquo;minimal human intervention.&amp;rdquo; A structure where AI generates content, humans review it, and the system alerts operators to anomalies is the key to stable operation.&lt;/p&gt;</description></item><item><title>Multi-Model AI Agent Team Design: Composed Architecture and 5-Team Hierarchy</title><link>https://blog.agentthread.dev/post/multi-model-ai-agent-team-design-composed-architecture-and-5-team-hierarchy/</link><pubDate>Mon, 30 Mar 2026 00:33:34 +0900</pubDate><guid>https://blog.agentthread.dev/post/multi-model-ai-agent-team-design-composed-architecture-and-5-team-hierarchy/</guid><description>&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;For building a blog system, I designed a multi-model agent team consisting of &lt;strong&gt;14 AI specialists, 5 teams, and 4 LLM models&lt;/strong&gt;. The core innovations are two:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Composed Agent&lt;/strong&gt;: Separating role definitions from execution profiles for maximum reusability&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hierarchical Bridge Leadership&lt;/strong&gt;: Dual membership of tech leads between upper and lower teams to resolve communication bottlenecks&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This post covers the final structure, model distribution strategy, and the composed architecture design process.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="background-why-multi-model"&gt;Background: Why Multi-Model?
&lt;/h2&gt;&lt;p&gt;Using a single LLM for all tasks creates two problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: Running 14 specialists on a Claude Opus-level model makes costs uncontrollable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fit&lt;/strong&gt;: Design needs fast reasoning, security analysis needs deep logic, implementation needs stable coding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So we distributed models based on task characteristics.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="final-team-structure"&gt;Final Team Structure
&lt;/h2&gt;&lt;p&gt;5 teams, 14 specialists, 4 models.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 subgraph UPPER["Upper — Steering Team · consensus"]
 ORC["Orchestrator&lt;br/&gt;relay:steering-orchestrator"]
 DES["Architect&lt;br/&gt;gemini:gemini-2.5-flash"]
 SEC["Security Reviewer&lt;br/&gt;codex:gpt-4o"]
 STL["Backend Tech Lead&lt;br/&gt;relay:developer-zai"]
 FTL["Frontend Tech Lead&lt;br/&gt;relay:developer-zai"]
 DTL["Desktop Tech Lead&lt;br/&gt;relay:developer-zai"]
 INF["Infra Network&lt;br/&gt;gemini:gemini-2.5-flash"]
 SAD["Server Admin&lt;br/&gt;relay:developer-zai"]
 end

 subgraph LOWER_BE["Backend Team · leader_decides"]
 BTL["Backend Tech Lead"]
 BDEV["Backend Developer"]
 end

 subgraph LOWER_FE["Frontend Team · leader_decides"]
 FTL2["Frontend Tech Lead"]
 FDEV["Frontend Developer"]
 FUX["UX Designer"]
 end

 subgraph LOWER_DT["Desktop Team · leader_decides"]
 DTL2["Desktop Tech Lead"]
 DDEV["Desktop Developer"]
 DUX["UX Designer"]
 end

 subgraph LOWER_INFRA["Infra Team · leader_decides"]
 SAD2["Server Admin (Leader)"]
 INET["Cloud Network"]
 DBA["DB Architect"]
 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="team-details"&gt;Team Details
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Team&lt;/th&gt;
					&lt;th&gt;Type&lt;/th&gt;
					&lt;th&gt;Decision-Making&lt;/th&gt;
					&lt;th&gt;Leader&lt;/th&gt;
					&lt;th&gt;Members&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Steering Team&lt;/td&gt;
					&lt;td&gt;upper&lt;/td&gt;
					&lt;td&gt;consensus&lt;/td&gt;
					&lt;td&gt;Orchestrator&lt;/td&gt;
					&lt;td&gt;8 (including bridges)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Backend Team&lt;/td&gt;
					&lt;td&gt;lower&lt;/td&gt;
					&lt;td&gt;leader_decides&lt;/td&gt;
					&lt;td&gt;Backend Tech Lead&lt;/td&gt;
					&lt;td&gt;2&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Frontend Team&lt;/td&gt;
					&lt;td&gt;lower&lt;/td&gt;
					&lt;td&gt;leader_decides&lt;/td&gt;
					&lt;td&gt;Frontend Tech Lead&lt;/td&gt;
					&lt;td&gt;3&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Desktop Team&lt;/td&gt;
					&lt;td&gt;lower&lt;/td&gt;
					&lt;td&gt;leader_decides&lt;/td&gt;
					&lt;td&gt;Desktop Tech Lead&lt;/td&gt;
					&lt;td&gt;3&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Infra Team&lt;/td&gt;
					&lt;td&gt;lower&lt;/td&gt;
					&lt;td&gt;leader_decides&lt;/td&gt;
					&lt;td&gt;Server Admin&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="the-infra-team-separation-decision"&gt;The Infra Team Separation Decision
&lt;/h2&gt;&lt;p&gt;In the initial design, DB Architect and Server Admin were part of the backend team. But we separated them based on &lt;strong&gt;workspace&lt;/strong&gt;.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 subgraph BackendTeam["Backend Team"]
 B["API Code Writing&lt;br/&gt;FastAPI, Python&lt;br/&gt;workspace: VS Code / SSH"]
 end

 subgraph InfraTeam["Infra Team"]
 S["Server Management&lt;br/&gt;Docker, Ubuntu, Nginx&lt;br/&gt;workspace: SSH Terminal"]
 N["Cloud Network&lt;br/&gt;Cloudflare Dashboard&lt;br/&gt;workspace: Web Console"]
 D["DB Management&lt;br/&gt;PostgreSQL, Migrations&lt;br/&gt;workspace: psql / SSH"]
 end

 B -.-&gt;|API deployment| S
 B -.-&gt;|Query optimization| D&lt;/pre&gt;&lt;p&gt;When workspaces differ, separation is more natural than keeping them together.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="model-distribution-strategy"&gt;Model Distribution Strategy
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;pie title Specialists by Model
 "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;Model&lt;/th&gt;
					&lt;th&gt;Specialists&lt;/th&gt;
					&lt;th&gt;Purpose&lt;/th&gt;
					&lt;th&gt;Why Chosen&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;Implementation, ops, leads&lt;/td&gt;
					&lt;td&gt;Cost-effective, stable coding&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;Design, infra network&lt;/td&gt;
					&lt;td&gt;Fast response, easy external API calls&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;Security review&lt;/td&gt;
					&lt;td&gt;High reasoning, OWASP knowledge&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;Context compression&lt;/td&gt;
					&lt;td&gt;Free tier, text summarization specialized&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;By assigning 10 implementation specialists to GLM, we achieved &lt;strong&gt;60-70% total cost reduction&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="composed-agent-architecture"&gt;Composed Agent Architecture
&lt;/h2&gt;&lt;p&gt;The core innovation is &lt;strong&gt;separating role definitions (Expert) from execution profiles (Definition)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;With the traditional approach, role and execution logic are coupled — any change requires a full rewrite, and reuse is impossible.&lt;/p&gt;
&lt;h3 id="composed-approach"&gt;Composed Approach
&lt;/h3&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 DEF["Definition&lt;br/&gt;Backend Developer"]
 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; |"compose"| RUN["Runtime Agent"]
 CAP --&gt; |"compose"| RUN
 PLAT --&gt; |"compose"| RUN
 POL --&gt; |"compose"| RUN&lt;/pre&gt;&lt;h3 id="module-structure"&gt;Module Structure
&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 agent definitions
&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 base modules
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── capabilities/ ← 15 capability modules
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── platforms/ ← 5 platform modules
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── policies/ ← 1 policy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── runs/ ← execution history
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="advantages"&gt;Advantages
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reusability&lt;/strong&gt;: &lt;code&gt;rest-api&lt;/code&gt; capability module shared by backend developer and backend tech lead&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Platform swap&lt;/strong&gt;: Change &lt;code&gt;platform: fastapi&lt;/code&gt; to &lt;code&gt;platform: django&lt;/code&gt; for instant switching&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capability extension&lt;/strong&gt;: Add a new capability module and connect it to the Definition&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy unification&lt;/strong&gt;: All agents follow the same &lt;code&gt;blog-default&lt;/code&gt; policy&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="expert-definition-mapping"&gt;Expert-Definition Mapping
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Expert&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;Backend Developer&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;Backend Tech Lead&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;Frontend Developer&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;Server Admin&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;Infra Network&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;Security Reviewer&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;Context Compressor&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-certificate-strategy-cloudflare-origin-ca"&gt;TLS Certificate Strategy: Cloudflare Origin CA
&lt;/h2&gt;&lt;p&gt;For production TLS certificates, we chose &lt;strong&gt;Cloudflare Origin CA&lt;/strong&gt; over Let&amp;rsquo;s Encrypt.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;sequenceDiagram
 participant Client as Visitor
 participant CF as Cloudflare (Proxy)
 participant Nginx as Nginx (Origin)
 participant API as FastAPI

 Client-&gt;&gt;CF: HTTPS request
 CF-&gt;&gt;CF: Terminate with Cloudflare-managed cert
 CF-&gt;&gt;Nginx: Encrypt with Origin CA cert
 Nginx-&gt;&gt;API: HTTP (local)
 API--&gt;&gt;Nginx: Response
 Nginx--&gt;&gt;CF: Encrypted with Origin CA
 CF--&gt;&gt;Client: Response&lt;/pre&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Item&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;Validity&lt;/td&gt;
					&lt;td&gt;90 days (renewal required)&lt;/td&gt;
					&lt;td&gt;15 years (no renewal)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Issuance&lt;/td&gt;
					&lt;td&gt;ACME automation required&lt;/td&gt;
					&lt;td&gt;Manual from Dashboard&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Complexity&lt;/td&gt;
					&lt;td&gt;certbot setup&lt;/td&gt;
					&lt;td&gt;Copy cert files only&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Production architecture:&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 (installed directly on host)
&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-compatible storage)
&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-plugin-agent-invocation-mechanism"&gt;Relay Plugin: Agent Invocation Mechanism
&lt;/h2&gt;&lt;p&gt;The team structure runs in Claude Code through the &lt;strong&gt;Relay plugin&lt;/strong&gt;.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;sequenceDiagram
 participant User as User
 participant Claude as Claude Code
 participant Plugin as Relay Plugin
 participant MCP as MCP Server
 participant LLM as External LLM

 User-&gt;&gt;Claude: /relay:invoke-agent
 Claude-&gt;&gt;Plugin: Load definition by expert slug
 Plugin-&gt;&gt;Plugin: Compose Definition (base + capabilities + platform + policy)
 Plugin-&gt;&gt;Plugin: Check backed_by

 alt relay:developer-zai
 Plugin-&gt;&gt;Claude: Run internal agent
 else gemini:*
 Plugin-&gt;&gt;MCP: Call gemini_mcp server
 MCP-&gt;&gt;LLM: Gemini API
 LLM--&gt;&gt;MCP: Response
 MCP--&gt;&gt;Plugin: Result
 else codex:*
 Plugin-&gt;&gt;MCP: Call codex_mcp server
 MCP-&gt;&gt;LLM: OpenAI API
 LLM--&gt;&gt;MCP: Response
 MCP--&gt;&gt;Plugin: Result
 end

 Plugin--&gt;&gt;Claude: Final result
 Claude--&gt;&gt;User: Response&lt;/pre&gt;&lt;h3 id="backed_by-namespaces"&gt;backed_by Namespaces
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Namespace&lt;/th&gt;
					&lt;th&gt;MCP Server&lt;/th&gt;
					&lt;th&gt;Purpose&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;internal agent&lt;/td&gt;
					&lt;td&gt;Implementation, ops (low cost)&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;internal agent&lt;/td&gt;
					&lt;td&gt;Coordination, final decisions&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;Design, external APIs&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;Security analysis&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;Context compression&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="design-decision-log"&gt;Design Decision Log
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Decision&lt;/th&gt;
					&lt;th&gt;Alternative&lt;/th&gt;
					&lt;th&gt;Rationale&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Separate infra team&lt;/td&gt;
					&lt;td&gt;Include in backend&lt;/td&gt;
					&lt;td&gt;Different workspaces (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-year validity, no renewal&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;PostgreSQL on host&lt;/td&gt;
					&lt;td&gt;Docker container&lt;/td&gt;
					&lt;td&gt;Memory efficiency on single server&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Composed agent&lt;/td&gt;
					&lt;td&gt;Single-definition agent&lt;/td&gt;
					&lt;td&gt;Module reusability, easy platform swap&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Assign many GLM&lt;/td&gt;
					&lt;td&gt;Assign many Claude&lt;/td&gt;
					&lt;td&gt;60-70% cost reduction&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="retrospective-what-i-learned"&gt;Retrospective: What I Learned
&lt;/h2&gt;&lt;h3 id="1-executable-structure-over-perfect-structure"&gt;1. &amp;ldquo;Executable Structure&amp;rdquo; over &amp;ldquo;Perfect Structure&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;Trying to design everything perfectly can prevent you from ever starting. It&amp;rsquo;s better to compromise and improve as you execute.&lt;/p&gt;
&lt;h3 id="2-workspace-defines-team-boundaries"&gt;2. Workspace Defines Team Boundaries
&lt;/h3&gt;&lt;p&gt;People who write code and people who manage servers have different physical work environments — that&amp;rsquo;s a natural team boundary.&lt;/p&gt;
&lt;h3 id="3-the-value-of-composed-architecture"&gt;3. The Value of Composed Architecture
&lt;/h3&gt;&lt;p&gt;In an environment with 14 specialists, 5 teams, and 4 models interacting, module separation is essential. It minimizes the scope of changes and maximizes reusability.&lt;/p&gt;
&lt;h3 id="4-cost-is-determined-at-design-time"&gt;4. Cost is Determined at Design Time
&lt;/h3&gt;&lt;p&gt;Asking &amp;ldquo;does this task really need a high-cost model?&amp;rdquo; every time naturally optimizes costs.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="next-steps"&gt;Next Steps
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Start Phase 1 implementation: DB, Auth, Post/Category CRUD, Docker&lt;/li&gt;
&lt;li&gt;Share team operation experience: Problems encountered during actual execution&lt;/li&gt;
&lt;li&gt;Performance monitoring: Response time per model, cost vs quality analysis&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;

 &lt;blockquote&gt;
 &lt;p&gt;This post summarizes my experience building an AI agent team using Claude Code + Relay plugin.
It reflects learnings from applying this to a real project — different approaches may be more suitable depending on your situation.&lt;/p&gt;

 &lt;/blockquote&gt;</description></item></channel></rss>