Project: Marketing Agency Swarm
Dec 30, 2025 β’ 30 min read
Goal
Build a self-orchestrating team of AI agents that takes a single topic ("AI in Healthcare") and autonomously researches trending angles, identifies SEO keywords, writes a 1000+ word article, adds internal links, and produces an editor-approved draft β with no human intervention after the initial prompt.
1. Architecture: Why Point-to-Point Fails
Simple agent chains (A calls B calls C) break with 4+ agents: Who handles conflicts? Who decides when a revision is complete? Who routes between agents based on context? You need a GroupChat Manager β an orchestrator LLM that reads the full conversation history and decides whose turn it is to speak next.
pip install pyautogen tavily-python
import autogen
from autogen import AssistantAgent, UserProxyAgent, GroupChat, GroupChatManager
# Configuration for two model tiers:
# - GPT-4 for complex reasoning agents (Manager, Editor)
# - GPT-4o-mini for simpler agents (reduces cost ~95%)
gpt4_config = {"config_list": [{"model": "gpt-4o", "api_key": "sk-..."}], "timeout": 120}
gpt4mini_config = {"config_list": [{"model": "gpt-4o-mini", "api_key": "sk-..."}], "timeout": 60}
# 1. Research Agent: Uses Tavily web search to find current information
researcher = AssistantAgent(
name="Researcher",
llm_config=gpt4mini_config,
system_message="""You are a research assistant specializing in finding current AI trends.
Your workflow:
1. Use the tavily_search tool to find 3-5 recent (last 3 months) articles on the given topic
2. Summarize key findings: Statistics, quotes, unique angles not covered in basic overviews
3. Output a structured research brief with: Main thesis, Supporting data, Interesting angles, Source URLs
4. When research is complete, say "Research complete. Handing to SEO Specialist."
Only search. Do not write the article.""",
)
# 2. SEO Specialist: Keyword research and content structure
seo_specialist = AssistantAgent(
name="SEO_Specialist",
llm_config=gpt4mini_config,
system_message="""You are an SEO expert. When the Research brief arrives:
1. Identify 1 primary keyword (high-volume, achievable difficulty) and 4-6 secondary keywords
2. Suggest an SEO-optimized title (60 chars max, primary keyword near front)
3. Create a content outline: H2 sections, word count targets per section
4. Specify internal linking opportunities (pages that should be referenced)
5. Output: JSON with {primary_kw, secondary_kws, title, outline, internal_links}
Then say: "SEO brief complete. Copywriter to write the article."
Output only the SEO brief JSON, no article content.""",
)
# 3. Copywriter: The writing agent
copywriter = AssistantAgent(
name="Copywriter",
llm_config=gpt4_config, # GPT-4 for best writing quality
system_message="""You are a senior tech content writer for a top AI publication.
Given the Research brief and SEO brief:
1. Write a complete article (minimum 1000 words, target 1200-1500)
2. Use the SEO outline as your structure
3. Include all primary and secondary keywords naturally (no keyword stuffing)
4. Add specific examples and statistics from the research brief
5. Format in Markdown with proper H2/H3 hierarchy
6. Include a compelling intro hook (no "In the world of AI..." clichΓ©s)
7. End with an actionable conclusion
After writing, say: "Draft complete. Editor please review."
Write the full article. Not an outline, not a summary β the complete article.""",
)
# 4. Editor: Quality gate with specific rejection criteria
editor = AssistantAgent(
name="Editor",
llm_config=gpt4_config,
system_message="""You are a harsh editor with high standards. Review the Copywriter's draft.
REJECT and request revision if ANY of:
- Article is under 900 words (count carefully)
- First paragraph contains "In the world of AI" or "In today's fast-paced"
- Statistics are not attributed to sources ("Studies show..." without a source)
- Article doesn't include at least 3 specific examples
- Title doesn't contain the primary keyword
If APPROVED (all criteria met):
- Write "CONTENT APPROVED" on its own line
- Provide a brief note on why it passed
- Say "TERMINATE" on the final line
If REJECTED:
- List EXACTLY which criteria failed
- Specify what the Copywriter must fix
- Do NOT say TERMINATE""",
)
# 5. Human proxy (runs headless in automation mode)
user_proxy = UserProxyAgent(
name="Admin",
human_input_mode="NEVER", # Fully autonomous
code_execution_config=False, # No code execution in this workflow
is_termination_msg=lambda msg: (
isinstance(msg.get("content"), str) and
"TERMINATE" in msg.get("content", "").upper()
),
max_consecutive_auto_reply=2,
)2. Tool-Equipped Agents (Web Search)
from tavily import TavilyClient
import json
tavily = TavilyClient(api_key="tvly-...")
# Register tool function for researcher agent
def tavily_search(query: str, max_results: int = 5) -> str:
"""Search for recent articles on a topic."""
results = tavily.search(
query=query,
search_depth="advanced", # Deep search (costs more but better results)
max_results=max_results,
include_answer=True, # Get synthesized answer + sources
include_raw_content=False, # Don't include full page content (too long)
days=90, # Only last 90 days
)
# Format results for LLM consumption
formatted = f"Search Query: {query}
"
formatted += f"Summary: {results.get('answer', 'N/A')}
"
formatted += "Sources:
"
for r in results.get('results', []):
formatted += f"- [{r['title']}]({r['url']})
{r.get('content', '')[:200]}...
"
return formatted
# Register tool with AutoGen
autogen.register_function(
tavily_search,
caller=researcher, # This agent can REQUEST the tool
executor=user_proxy, # This agent EXECUTES the tool (important for security)
name="tavily_search",
description="Search for recent articles, news, and data on AI topics. Use specific queries.",
)3. GroupChat Orchestration and Termination
# Define the speaking order logic
def custom_speaker_selection(last_speaker, groupchat):
"""
Custom speaker selection: enforces a logical workflow order.
Prevents agents from speaking out of turn.
"""
messages = groupchat.messages
if not messages:
return researcher # Always start with research
last_content = messages[-1].get("content", "")
last_name = messages[-1].get("name", "")
# State machine for speaker selection
if "Research complete" in last_content:
return seo_specialist
elif "SEO brief complete" in last_content:
return copywriter
elif "Draft complete" in last_content:
return editor
elif "REJECTED" in last_content.upper():
return copywriter # Send back for revision
else:
# Fallback: LLM-based selection (slower but handles edge cases)
return "auto"
groupchat = GroupChat(
agents=[user_proxy, researcher, seo_specialist, copywriter, editor],
messages=[],
max_round=20, # Maximum conversation turns (prevent infinite loops)
speaker_selection_method=custom_speaker_selection,
allow_repeat_speaker=True, # Allow Copywriter to revise multiple times
)
# Manager LLM routes conversation when custom selection returns "auto"
manager = GroupChatManager(
groupchat=groupchat,
llm_config=gpt4_config,
system_message="You are an editorial director. Ensure agents follow their roles and the workflow progresses toward a published article.",
)
# Launch the swarm
chat_result = user_proxy.initiate_chat(
manager,
message=(
"Write a comprehensive article on 'AI Agents in Healthcare 2025'. "
"Target audience: healthcare IT executives. "
"Primary keyword: 'AI healthcare automation'. "
"Tone: authoritative but accessible."
),
summary_method="reflection_with_llm", # Get a summary after completion
)
# Extract the final article from conversation history
final_article = None
for msg in reversed(chat_result.chat_history):
if msg.get("name") == "Copywriter" and len(msg.get("content", "")) > 900:
final_article = msg["content"]
break
print("=== Final Article ===")
print(final_article)
print(f"Total cost: " + str(chat_result.cost['total_cost']))Frequently Asked Questions
How do I prevent the infamous "politeness loop" where agents just thank each other?
Three layered defenses: (1) Hard termination strings: configure is_termination_msg to detect "TERMINATE" and hard-stop. Instruct only the editor to say TERMINATE, and only when satisfied. (2) max_round limit: set max_round=20 β if the loop hasn't resolved in 20 turns, something is wrong and you want to know. (3) Custom speaker selection: instead of "auto" (which uses an LLM and can get confused), implement a state machine based on keywords in the last message. Keyword detection is deterministic and doesn't require an expensive LLM call for routing decisions. The combination of all three makes infinite loops practically impossible.
What's the actual cost of running this swarm per article?
For a typical 3-revision cycle: Researcher (3 searches + summary): ~$0.01 (GPT-4o-mini + Tavily). SEO Specialist (keyword research): ~$0.005. Copywriter (initial draft + 2 revisions): ~$0.08 (GPT-4o at ~5,000 tokens/draft). Editor (2 reviews): ~$0.02. Manager routing: ~$0.01. Total: approximately $0.12-0.15 per article. Compared to human writer rates of $50-200 per 1000-word article, the ROI is significant for high-volume content operations. Track actual costs using chat_result.cost and set up budget alerts if deploying at scale.
Conclusion
Multi-agent swarms for content production demonstrate both the power and complexity of agentic AI systems. The key design decisions are: use a hierarchical manager (not peer-to-peer communication), implement hard termination conditions, equip agents with real tools (web search, not just LLM knowledge), and use cost-tiered models (GPT-4o for quality-critical roles, GPT-4o-mini for routine tasks). The custom speaker selection function is the most important engineering decision β it transforms an unpredictable LLM-routed conversation into a reliable state machine that produces consistent results. Add tool access for web search, CMS publishing, and image generation, and this system becomes a complete AI-powered content marketing pipeline.
Continue Reading
Vivek
AI EngineerFull-stack AI engineer with 4+ years building LLM-powered products, autonomous agents, and RAG pipelines. I've shipped AI features to production for startups and worked hands-on with GPT-4o, LangChain, LlamaIndex, and the Vercel AI SDK. I started OpnCrafter to share everything I wish I had when learning β no fluff, just working code and real-world context.