What Is an SEO Content Agent?
An SEO Content Agent is an autonomous AI system that combines large language models (LLMs) with specialized SEO tools to research, plan, and generate search-engine-optimized content. Built on LangChain, it acts as an intelligent assistant that can perform keyword research, analyze SERP competitors, generate content briefs, and produce fully optimized articles ā all while following your brand guidelines and SEO strategy.
Unlike a simple chatbot that generates text from a single prompt, an agent reasons through tasks step-by-step. It decides which tools to call, in what order, and iteratively refines its output based on intermediate results. Think of it as giving an experienced SEO specialist and content writer access to your entire tool stack, then asking them to collaborate on producing the perfect piece of content.
Why Building a Custom SEO Agent Matters
Generic AI content generators produce surface-level articles that often miss critical SEO elements like target keyword density, semantic LSI keywords, proper heading structure, and competitive differentiation. A purpose-built SEO agent addresses these gaps by:
- Tool-augmented reasoning: The agent actively queries keyword APIs, scrapes SERP data, and analyzes competitors before writing a single word
- Multi-step workflows: It breaks down content creation into discrete stages ā research, outline, draft, optimize, review ā mirroring a professional content team's process
- Data-driven decisions: Content structure, word count, and keyword placement are informed by actual SERP analysis rather than guesswork
- Consistency at scale: Once configured, the agent reliably applies your SEO playbook across hundreds of articles
- Cost efficiency: One agent replaces multiple tools and manual coordination between SEO analysts and writers
Prerequisites and Setup
Before building the agent, install the required dependencies. This tutorial uses LangChain with OpenAI's GPT-4o model, but you can swap in any LangChain-compatible LLM.
# Create a new project directory
mkdir seo-content-agent
cd seo-content-agent
# Create a virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install core dependencies
pip install langchain langchain-openai langchain-community
pip install requests beautifulsoup4 python-dotenv
pip install serpapi-client # For SERP data (or use your preferred provider)
Create a .env file to store your API keys securely:
OPENAI_API_KEY=your-openai-api-key-here
SERPAPI_API_KEY=your-serpapi-key-here
Building the Core Components
Step 1: Defining Custom Tools for SEO Research
LangChain agents rely on tools ā functions that the agent can call to interact with external systems. Each tool has a name, description, and a well-typed input schema. The description is crucial because the agent uses it to decide which tool to invoke for a given subtask.
Let's build three essential SEO tools: a keyword research tool, a SERP scraper, and a content outline generator.
import os
import json
import requests
from typing import List, Dict, Optional
from bs4 import BeautifulSoup
from dotenv import load_dotenv
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import SystemMessage, HumanMessage
load_dotenv()
# -------------------------------------------------------------------
# Tool 1: Keyword Research ā uses SerpAPI to get related keywords
# -------------------------------------------------------------------
@tool
def keyword_research(seed_keyword: str, max_results: int = 10) -> str:
"""
Perform keyword research for a given seed keyword.
Returns a JSON string containing related keywords, search volume estimates,
and competition levels. Use this BEFORE writing any content.
"""
from serpapi import GoogleSearch # requires serpapi-client
params = {
"q": seed_keyword,
"api_key": os.getenv("SERPAPI_API_KEY"),
"num": max_results,
"hl": "en",
"gl": "us",
}
try:
search = GoogleSearch(params)
results = search.get_dict()
keywords_data = []
# Extract organic results to infer keyword themes
if "organic_results" in results:
for result in results["organic_results"][:max_results]:
title = result.get("title", "")
snippet = result.get("snippet", "")
keywords_data.append({
"source_title": title,
"source_snippet": snippet[:200],
"implied_keywords": extract_keywords_from_text(title + " " + snippet)
})
# Also check related searches
related_searches = results.get("related_searches", [])
related_queries = []
for item in related_searches:
if isinstance(item, dict):
related_queries.append(item.get("query", str(item)))
output = {
"seed_keyword": seed_keyword,
"competitor_themes": keywords_data,
"related_searches": related_queries,
"total_organic_results": results.get("search_information", {}).get("total_results", 0)
}
return json.dumps(output, indent=2)
except Exception as e:
return json.dumps({"error": str(e), "message": "Tool failed, try a different query"})
def extract_keywords_from_text(text: str) -> List[str]:
"""Simple helper to extract meaningful phrases from text."""
# In production, replace with NLP-based keyword extraction
words = text.lower().split()
# Filter out common stop words and short words
stop_words = {"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
"of", "with", "by", "from", "is", "are", "was", "were", "be", "been",
"being", "have", "has", "had", "do", "does", "did", "will", "would",
"can", "could", "may", "might", "shall", "should", "this", "that"}
keywords = [w for w in words if len(w) > 3 and w not in stop_words]
return list(set(keywords))[:15]
# -------------------------------------------------------------------
# Tool 2: SERP Content Scraper ā analyzes competitor content
# -------------------------------------------------------------------
@tool
def scrape_competing_content(url: str) -> str:
"""
Scrape and extract the main content, headings, and meta information
from a competing URL. Use this to understand what top-ranking pages cover.
"""
try:
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
}
response = requests.get(url, headers=headers, timeout=15)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
# Extract meta title and description
meta_title = ""
title_tag = soup.find("title")
if title_tag:
meta_title = title_tag.get_text(strip=True)
meta_description = ""
meta_desc_tag = soup.find("meta", attrs={"name": "description"})
if meta_desc_tag and meta_desc_tag.get("content"):
meta_description = meta_desc_tag["content"]
# Extract headings hierarchy
headings = []
for tag in ["h1", "h2", "h3"]:
for heading in soup.find_all(tag):
text = heading.get_text(strip=True)
if text:
headings.append({"level": tag, "text": text})
# Extract main content (simplified ā production would use readability extractors)
article_body = soup.find("article") or soup.find("main") or soup.body
paragraphs = []
if article_body:
for p in article_body.find_all("p"):
text = p.get_text(strip=True)
if len(text) > 50: # Skip short paragraphs
paragraphs.append(text)
# Word count estimate
full_text = " ".join(paragraphs)
word_count = len(full_text.split())
result = {
"url": url,
"meta_title": meta_title,
"meta_description": meta_description,
"headings": headings,
"paragraph_count": len(paragraphs),
"estimated_word_count": word_count,
"content_sample": full_text[:1500] # First 1500 chars for analysis
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"error": str(e), "url": url})
# -------------------------------------------------------------------
# Tool 3: SEO Content Score Evaluator ā checks optimization level
# -------------------------------------------------------------------
@tool
def evaluate_seo_score(content: str, target_keyword: str) -> str:
"""
Evaluate a piece of content for SEO optimization.
Checks keyword density, heading structure, readability, and content length.
Returns a score from 0-100 with actionable recommendations.
"""
import re
content_lower = content.lower()
keyword_lower = target_keyword.lower()
# 1. Keyword density check
total_words = len(content.split())
keyword_count = content_lower.count(keyword_lower)
keyword_density = (keyword_count / total_words * 100) if total_words > 0 else 0
# 2. Check if keyword appears in first 100 words
first_100 = " ".join(content.split()[:100]).lower()
keyword_in_intro = keyword_lower in first_100
# 3. Heading structure check
h2_count = len(re.findall(r'^##\s', content, re.MULTILINE))
h3_count = len(re.findall(r'^###\s', content, re.MULTILINE))
has_proper_structure = h2_count >= 3 and h3_count >= 2
# 4. Content length check
length_score = min(100, total_words / 15) # Scales to 100 at 1500 words
# 5. Readability proxy (sentence length)
sentences = re.split(r'[.!?]+', content)
avg_sentence_length = sum(len(s.split()) for s in sentences if s.strip()) / max(1, len([s for s in sentences if s.strip()]))
readability_ok = 10 <= avg_sentence_length <= 25
# Composite score
score = 0
score += min(25, keyword_density * 25) # Up to 25 points for density
score += 15 if keyword_in_intro else 0
score += 15 if has_proper_structure else 5
score += min(25, length_score)
score += 10 if readability_ok else 3
score += 10 # Base score for having content
recommendations = []
if keyword_density < 0.5:
recommendations.append("Increase keyword density to 0.5-2%")
if keyword_density > 3:
recommendations.append("Reduce keyword density ā appears stuffed")
if not keyword_in_intro:
recommendations.append("Include target keyword in the first 100 words")
if not has_proper_structure:
recommendations.append("Add more H2 and H3 headings for structure")
if total_words < 800:
recommendations.append("Expand content to at least 800-1500 words")
if not readability_ok:
recommendations.append("Adjust sentence length for better readability")
result = {
"overall_score": round(score),
"total_words": total_words,
"keyword_density_percent": round(keyword_density, 2),
"keyword_in_intro": keyword_in_intro,
"h2_count": h2_count,
"h3_count": h3_count,
"recommendations": recommendations
}
return json.dumps(result, indent=2)
Step 2: Assembling the LangChain Agent
Now we create the agent using LangChain's OpenAI tools agent pattern. This agent receives a system prompt that defines its role, a list of tools it can call, and an execution loop that handles tool calling and response generation.
# -------------------------------------------------------------------
# Create the LLM and Agent
# -------------------------------------------------------------------
def create_seo_agent(verbose: bool = True):
"""
Build and return an SEO Content Agent executor.
"""
# Initialize the LLM ā GPT-4o is recommended for complex reasoning
llm = ChatOpenAI(
model="gpt-4o",
temperature=0.7,
max_tokens=4096,
)
# Define all tools the agent can use
tools = [
keyword_research,
scrape_competing_content,
evaluate_seo_score,
]
# System prompt that defines the agent's identity and workflow
system_prompt = """You are an expert SEO Content Agent. Your job is to create high-quality,
search-engine-optimized content following a structured methodology.
WORKFLOW YOU MUST FOLLOW:
1. RESEARCH PHASE: Use the keyword_research tool to understand the topic landscape.
Then use scrape_competing_content on 2-3 top-ranking URLs to analyze what performs well.
2. PLANNING PHASE: Based on research, create a detailed content outline with:
- Target keyword and 3-5 LSI/semantic keywords
- Article structure with H2 and H3 headings
- Estimated word count (typically 1000-2000 words for competitive topics)
- Key points to cover that differentiate from competitors
3. WRITING PHASE: Write the complete article in markdown format with:
- A compelling H1 title containing the target keyword
- An engaging introduction that hooks readers
- Well-structured H2 sections with supporting H3 subsections
- Natural keyword placement throughout (not forced or stuffed)
- Actionable insights, examples, and data points
4. OPTIMIZATION PHASE: Run evaluate_seo_score on your draft and refine based on recommendations.
Iterate until the score exceeds 80/100.
RULES:
- Always perform research BEFORE writing. Never skip the research phase.
- Write in clear, authoritative, helpful prose.
- Use markdown formatting for all content output.
- Include specific data points and examples from your research.
- Do not fabricate statistics ā only use data from your tool results.
- When a tool fails, try an alternative approach or proceed with available data.
"""
# Build the prompt template
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
MessagesPlaceholder(variable_name="chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
# Create the OpenAI tools agent
agent = create_openai_tools_agent(llm, tools, prompt)
# Wrap in an executor with error handling
executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=verbose,
handle_parsing_errors=True,
max_iterations=15,
max_execution_time=180, # 3 minutes max
return_intermediate_steps=False,
)
return executor
Step 3: Building a Content Pipeline Wrapper
While the agent handles the reasoning, wrapping it in a pipeline class gives you control over logging, caching research results, and post-processing the output. This makes the system production-ready.
import time
import hashlib
from datetime import datetime
from typing import Dict, Any
class SEOContentPipeline:
"""
Production wrapper around the SEO Content Agent.
Adds caching, structured output parsing, and logging.
"""
def __init__(self, agent_executor: AgentExecutor):
self.executor = agent_executor
self.research_cache = {} # Simple in-memory cache (use Redis in production)
def _cache_key(self, topic: str) -> str:
return hashlib.md5(topic.lower().strip().encode()).hexdigest()
def run_research_only(self, topic: str, competitor_urls: List[str] = None) -> Dict:
"""
Run only the research phase and return structured data.
Useful when you want to review research before generating content.
"""
cache_key = self._cache_key(topic)
if cache_key in self.research_cache:
print(f"[CACHE HIT] Using cached research for: {topic}")
return self.research_cache[cache_key]
prompt = f"""Please conduct thorough SEO research on the topic: "{topic}"
Phase 1: Use keyword_research to analyze this topic.
Phase 2: Scrape these competing URLs if provided: {competitor_urls or 'find top-ranking pages from your research'}
Phase 3: Provide a detailed research summary with:
- Primary keyword target
- 5-10 LSI/semantic keywords
- Average word count of top-ranking pages
- Common headings and content gaps
- Recommended content angle/differentiator
Do NOT write the full article yet ā only provide the research summary."""
result = self.executor.invoke({"input": prompt})
self.research_cache[cache_key] = result
return result
def generate_content(self, topic: str, research_data: Dict = None,
content_type: str = "comprehensive_guide",
target_word_count: int = 1500) -> str:
"""
Generate a complete SEO-optimized article based on research.
"""
research_context = ""
if research_data:
research_context = f"\n\nPREVIOUS RESEARCH:\n{json.dumps(research_data, indent=2)}"
prompt = f"""Create a complete SEO-optimized article about: "{topic}"
Content Type: {content_type}
Target Word Count: {target_word_count}{research_context}
Follow your full workflow: research (if not already provided), plan, write, optimize.
Use evaluate_seo_score to check your work and refine until the score exceeds 80.
Output the final article in clean markdown format."""
result = self.executor.invoke({"input": prompt})
return result.get("output", str(result))
def generate_content_brief(self, topic: str) -> str:
"""
Generate a structured content brief (outline + SEO specs) without the full article.
"""
prompt = f"""Create a detailed SEO content brief for the topic: "{topic}"
Include:
1. Target keyword and 5 semantic keywords
2. Recommended URL slug
3. Meta title (under 60 chars) and meta description (under 155 chars)
4. Full article outline with H2 and H3 headings
5. Key questions to answer in each section
6. Internal and external linking recommendations
7. Target word count and content type
Output as structured markdown."""
result = self.executor.invoke({"input": prompt})
return result.get("output", str(result))
Putting It All Together ā Complete Implementation
Here is the complete, runnable main.py that ties everything together. This script initializes the agent, runs the pipeline, and outputs the final article.
# main.py ā Complete SEO Content Agent Runner
import os
import json
from dotenv import load_dotenv
load_dotenv()
def main():
print("=" * 60)
print("SEO CONTENT AGENT ā Powered by LangChain")
print("=" * 60)
# Initialize the agent
agent = create_seo_agent(verbose=True)
pipeline = SEOContentPipeline(agent)
# Example: Generate a comprehensive guide
topic = "Serverless Architecture Best Practices for SaaS Applications"
print(f"\nšÆ Target Topic: {topic}\n")
# Step 1: Run research
print("[PHASE 1] Running SEO research...")
research = pipeline.run_research_only(
topic=topic,
competitor_urls=[
"https://aws.amazon.com/serverless/",
"https://cloud.google.com/serverless",
]
)
print("[PHASE 1] Research complete.\n")
# Step 2: Generate content brief (optional review step)
print("[PHASE 2] Generating content brief...")
brief = pipeline.generate_content_brief(topic)
print("[PHASE 2] Brief ready.\n")
# Step 3: Generate full article
print("[PHASE 3] Generating optimized article...")
article = pipeline.generate_content(
topic=topic,
research_data=research,
content_type="comprehensive_guide",
target_word_count=1800
)
print("\n" + "=" * 60)
print("FINAL ARTICLE OUTPUT")
print("=" * 60 + "\n")
print(article)
# Save to file
output_filename = f"article_{topic[:30].replace(' ', '_').lower()}.md"
with open(output_filename, "w", encoding="utf-8") as f:
f.write(article)
print(f"\nā
Article saved to: {output_filename}")
if __name__ == "__main__":
main()
How to Use the SEO Content Agent
Once your agent is built, there are several ways to integrate it into your workflow:
Option 1: Command-Line Content Generator
# Run directly from the terminal
python main.py
# Customize by passing arguments
python main.py --topic "Kubernetes Security Best Practices" --word-count 2000 --type tutorial
To support command-line arguments, extend main.py with argparse:
import argparse
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="SEO Content Agent")
parser.add_argument("--topic", type=str, required=True, help="Article topic")
parser.add_argument("--word-count", type=int, default=1500, help="Target word count")
parser.add_argument("--type", type=str, default="comprehensive_guide",
choices=["tutorial", "guide", "listicle", "comparison"])
args = parser.parse_args()
agent = create_seo_agent(verbose=False)
pipeline = SEOContentPipeline(agent)
article = pipeline.generate_content(
topic=args.topic,
content_type=args.type,
target_word_count=args.word_count
)
print(article)
Option 2: REST API Endpoint
Wrap the agent in a FastAPI server for integration with content management systems:
# api_server.py
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel
import uvicorn
app = FastAPI(title="SEO Content Agent API")
class ContentRequest(BaseModel):
topic: str
content_type: str = "comprehensive_guide"
word_count: int = 1500
webhook_url: str = None # For async delivery
# Initialize agent once at startup
agent = create_seo_agent(verbose=False)
pipeline = SEOContentPipeline(agent)
@app.post("/generate")
async def generate_content(request: ContentRequest):
"""Generate SEO-optimized content synchronously."""
article = pipeline.generate_content(
topic=request.topic,
content_type=request.content_type,
target_word_count=request.word_count
)
return {"topic": request.topic, "content": article}
@app.post("/generate-async")
async def generate_async(request: ContentRequest, background_tasks: BackgroundTasks):
"""Fire-and-forget generation with webhook callback."""
def generate_and_notify():
article = pipeline.generate_content(
topic=request.topic,
content_type=request.content_type,
target_word_count=request.word_count
)
if request.webhook_url:
import requests as req
req.post(request.webhook_url, json={"topic": request.topic, "content": article})
background_tasks.add_task(generate_and_notify)
return {"status": "processing", "topic": request.topic}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Option 3: Integration with Content Workflows
For teams using platforms like Contentful, WordPress, or Notion, build a connector that triggers the agent via webhook when a new content request is created, then automatically populates the draft with the generated article and SEO metadata.
Best Practices for Production SEO Agents
1. Build a Reliable Tool Layer
The quality of your agent's output depends entirely on the quality of data its tools return. Invest in robust error handling, rate limiting, and fallback mechanisms for each tool. If keyword_research fails, the agent should gracefully degrade ā perhaps using its internal knowledge ā rather than crashing the entire pipeline.
# Example: Tool with retry logic and fallback
@tool
def robust_keyword_research(seed_keyword: str) -> str:
"""Keyword research with retry and fallback."""
max_retries = 3
for attempt in range(max_retries):
try:
# Primary API call
result = call_keyword_api(seed_keyword)
return json.dumps(result)
except Exception as e:
if attempt == max_retries - 1:
# Fallback: return a structured prompt for the LLM to reason about
return json.dumps({
"fallback": True,
"seed_keyword": seed_keyword,
"message": "API unavailable. Use your knowledge of this topic to "
"infer common keywords and search intent."
})
time.sleep(2 ** attempt) # Exponential backoff
2. Implement Human-in-the-Loop Review
For high-stakes content, insert a review checkpoint between the research/planning phase and the writing phase. The agent can output a structured brief, wait for human approval (or edits), then proceed with writing only after confirmation. This prevents wasted API calls on incorrect direction.
# Pseudo-code for human-in-the-loop
def generate_with_review(topic: str):
# Phase 1: Research + Brief
brief = pipeline.generate_content_brief(topic)
# Present to human for review
print("š Content Brief:")
print(brief)
approval = input("Approve brief? (y/n/edit): ").strip().lower()
if approval == 'y':
return pipeline.generate_content(topic, content_type="guide")
elif approval.startswith('edit'):
# Incorporate human feedback into the prompt
feedback = input("Enter your feedback: ")
return pipeline.generate_content(topic + f"\nHuman feedback: {feedback}")
else:
print("Brief rejected. Restarting with different parameters.")
return None
3. Monitor and Log Agent Decisions
Agents make autonomous decisions about which tools to call and in what order. Logging these decisions is critical for debugging and optimization. LangChain's verbose=True mode provides console output, but for production, capture structured logs:
import logging
# Configure structured logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s | %(name)s | %(levelname)s | %(message)s',
handlers=[
logging.FileHandler("agent_decisions.log"),
logging.StreamHandler()
]
)
class LoggedAgentExecutor(AgentExecutor):
"""Extended executor that logs each tool call and decision."""
def _perform_tool_call(self, tool_name, tool_input):
logging.info(f"š§ Calling tool: {tool_name} with input: {tool_input[:200]}")
result = super()._perform_tool_call(tool_name, tool_input)
logging.info(f"ā
Tool {tool_name} returned {len(str(result))} chars")
return result
4. Guard Against Hallucinations with Grounding
LLMs can fabricate statistics and facts. Combat this by requiring the agent to cite sources from tool results. Modify your system prompt to enforce citation:
# Enhanced system prompt excerpt
ADDITIONAL RULES:
- When citing statistics or data points, reference which tool result provided them
- If a statistic is not available from your tools, explicitly state "based on available research"
rather than fabricating numbers
- Use inline citations like [Source: keyword_research tool] when presenting data
- Never invent competitor URLs ā only reference URLs you've actually scraped
5. Optimize Cost with Smart Model Selection
Not every step requires GPT-4o. Use a cheaper model for mechanical tasks like initial scraping synthesis, and reserve the powerful model for final writing and optimization:
from langchain_openai import ChatOpenAI
# Multi-model strategy
def create_cost_optimized_agent():
research_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3) # Cheaper, more deterministic
writing_llm = ChatOpenAI(model="gpt-4o", temperature=0.8) # Creative writing
# Use research_llm for tool result synthesis
# Use writing_llm for final article generation
# (Implementation requires custom agent routing logic)
6. Cache Aggressively
SEO research for a given keyword doesn't change minute-by-minute. Cache keyword research and SERP analysis results for at least 24 hours. This reduces API costs and speeds up repeat content operations:
import hashlib
import pickle
from pathlib import Path
class PersistentCache:
def __init__(self, cache_dir: str = "./seo_cache"):
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
def get(self, key: str) -> Optional[Dict]:
cache_file = self.cache_dir / f"{key}.pkl"
if cache_file.exists():
with open(cache_file, "rb") as f:
data = pickle.load(f)
# Check if cache is still fresh (24 hours)
if time.time() - data.get("timestamp", 0) < 86400:
return data.get("result")
return None
def set(self, key: str, result: Dict):
cache_file = self.cache_dir / f"{key}.pkl"
with open(cache_file, "wb") as f:
pickle.dump({"timestamp": time.time(), "result": result}, f)
7. Handle Edge Cases Gracefully
Your agent will encounter ambiguous topics, tool failures, and content that's difficult to optimize. Build defensive patterns:
- Ambiguous topics: Have the agent ask clarifying questions before proceeding
- Tool timeouts: Set per-tool timeouts and provide partial results rather than failing entirely
- Empty SERPs: If no competing content exists, the agent should acknowledge this is a new topic and rely on its knowledge while flagging the uncertainty
- Rate limits: Implement exponential backoff across all external API calls
8. Validate Output Before Delivery
Run automated checks on the final article before returning it to the user. Check for minimum word count, presence of target keyword in the first paragraph, proper heading hierarchy, and absence of hallucinated URLs:
def validate_article(content: str, expected_keyword: str) -> Dict[str, bool]:
"""Run validation checks on generated content."""
checks = {
"min_word_count": len(content.split()) >= 800,
"keyword_in_first_100": expected_keyword.lower() in " ".join(content.split()[:100]).lower(),
"has_h2_headings": "## " in content,
"has_conclusion": any(phrase in content.lower()[-500:] for phrase in
["conclusion", "summary", "wrapping up", "final thoughts"]),
"no_placeholder_text": "lorem ipsum" not in content.lower(),
}
return checks
Conclusion
Building an SEO Content Agent with LangChain transforms content creation from an art-and-guesswork process into a systematic, data-driven operation. By equipping an LLM with specialized tools for keyword research, competitor analysis, and SEO scoring, you create a system that reasons about content strategy before writing ā producing articles that are genuinely optimized for search performance rather than merely keyword-stuffed.
The architecture outlined here ā custom tools, a reasoning agent, a production pipeline wrapper, and robust error handling ā serves as a foundation you can extend with additional tools like internal linking analysis, readability scoring, image alt-text optimization, or integration with your CMS. Start with the core implementation, observe where the agent's output falls short of your standards, and iteratively add tools and prompt refinements to close those gaps. The result is a content engine that scales your SEO efforts while maintaining the quality and strategic thinking of your best human writers.