blog-analyzer
5개 AI 엔진을 통합한 네이버 블로그 SEO 최적화 원고 자동 생성 플랫폼
Blog Analyzer
블로그 대행사에서 일하면서 매일 비슷한 구조의 글을 반복해서 쓰는 게 너무 비효율적이라고 느꼈다. 키워드만 넣으면 네이버 상위 노출에 최적화된 원고가 자동으로 나오면 좋겠다는 생각에서 시작했다.
처음에는 GPT API 하나만 연결해서 간단하게 만들었는데, 고객마다 원하는 톤이 다르고, 카테고리별로 글 스타일이 완전히 달라서 점점 복잡해졌다. 지금은 5개 AI 엔진을 상황에 맞게 선택할 수 있고, 46개 카테고리별 맞춤 프롬프트를 적용하는 시스템이 됐다.
주요 기능
- GPT, Claude, Gemini, Grok, SOLAR 5개 AI 엔진 통합
- 46개 카테고리별 전문 프롬프트 (위고비, 맛집, 웨딩홀 등)
- 참조 원고 스타일 학습 및 모방
- MongoDB 기반 분석 데이터 축적 및 재활용
- 10단계 단계별 원고 생성 (Step-by-Step)
- 청크 분할 생성으로 긴 원고도 안정적 처리
- 3단계 텍스트 후처리 파이프라인
기술 스택
| 분류 | 기술 |
|---|---|
| Backend | FastAPI, Uvicorn, Python 3.11 |
| AI/LLM | OpenAI, Anthropic, Google AI, Upstage, xAI |
| Database | MongoDB Atlas |
| 텍스트 처리 | 정규식, KoNLPy, KSS |
기술적 도전과제
1. AI 엔진마다 응답 형식이 다 다르다
처음에 GPT만 쓸 때는 문제없었는데, Claude랑 Gemini를 추가하니까 응답 형식이 제각각이었다. 어떤 건 JSON으로 오고, 어떤 건 마크다운 코드 블록 안에 JSON이 들어있고, 어떤 건 그냥 텍스트로 온다.
해결: 4단계 폴백 JSON 파싱 전략을 만들었다.
# llm/step_by/phase_functions.py
def parse_json_response(response_text: str) -> dict:
# 1. 전체가 JSON인 경우
try:
return json.loads(response_text)
except:
pass
# 2. 코드 블록(```json) 안의 JSON 추출
json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response_text)
if json_match:
return json.loads(json_match.group(1))
# 3. 중괄호로 시작하는 가장 큰 블록 찾기
brace_match = re.search(r'\{[\s\S]*\}', response_text)
if brace_match:
return json.loads(brace_match.group())
raise ValueError("JSON 파싱 실패")
이렇게 하니까 어떤 AI 엔진을 써도 JSON 응답을 안정적으로 받을 수 있게 됐다.
2. "AI가 쓴 글 같다"는 피드백
가장 많이 받은 피드백이 이거였다. 글이 너무 깔끔하고 정돈되어 있어서 오히려 AI 티가 난다는 것.
문제 분석:
- "첫째, 둘째, 셋째" 같은 나열식 표현
- "~해야 합니다", "~하는 것이 좋습니다" 같은 교과서 어투
- 문장 길이가 일정함
- 감정 표현이 없음
해결: _prompts/rules/human_writing_style.py에 인간화 규칙을 정의했다.
# 핵심 규칙 발췌
human_writing_rules = """
## 금지 패턴
- "첫째/둘째/셋째" 대신 자연스러운 흐름으로
- "~해야 합니다" 대신 "~해보세요", "~하면 좋아요"
- 요약이나 정리 문단 금지
- 번역투 한자어 회피
## 필수 패턴
- 개인 경험 삽입 ("저는 처음에... ㅜㅜ")
- 대화 요소 ("여러분도 그렇지 않나요?")
- 감탄사 사용 ("와... 이건 진짜 대박!")
- 문장 길이 불규칙하게 (짧은 것, 긴 것 섞기)
"""
프롬프트에 이 규칙을 넣으니까 확실히 자연스러워졌다. 근데 아직도 가끔 AI 티가 나서 계속 개선 중이다.
3. 2000자 이상 원고 생성 시 불안정
긴 원고를 한 번에 생성하려고 하면 API가 타임아웃 나거나, 중간에 내용이 끊기는 경우가 있었다.
해결: 10단계 단계별 생성 방식을 도입했다.
PHASE 1: 화자 설정
PHASE 2: 소제목 5개 생성
PHASE 3: 연관 키워드 40개 추출
PHASE 4: 제목 생성
PHASE 5: 도입부 (200자)
PHASE 6: 본문 (350자 x 5개 소제목)
PHASE 7: 마무리
PHASE 8: 키워드 반복율 검증
각 단계별로 API를 호출하고, 이전 단계 결과를 다음 단계에 전달하는 방식이다. 한 번에 생성하는 것보다 훨씬 안정적이고, 중간에 문제가 생겨도 어느 단계에서 실패했는지 바로 알 수 있다.
단점은 API 호출 횟수가 많아져서 비용이 늘어난다는 점. 근데 안정성이 더 중요해서 이 방식을 유지하고 있다.
4. 5개 AI 엔진 동시 호출 시 레이트 리미팅
여러 요청이 동시에 들어오면 API 레이트 리미트에 걸려서 에러가 터졌다.
해결: Asyncio Semaphore로 동시 요청 수를 제한했다.
# api.py
LLM_CONCURRENCY = int(os.getenv("LLM_CONCURRENCY", 3))
llm_semaphore = asyncio.Semaphore(LLM_CONCURRENCY)
# 라우터에서 사용
async with llm_semaphore:
result = await run_in_threadpool(ai_generate, ...)
동시에 3개까지만 AI API를 호출하도록 제한했다. 이것만으로도 레이트 리미트 에러가 거의 사라졌다.
5. 줄바꿈 규칙이 생각보다 복잡했다
네이버 블로그는 모바일에서 많이 읽히는데, 한 줄이 너무 길면 읽기 힘들다. 그래서 적절한 위치에서 줄바꿈을 넣어야 하는데, 이게 생각보다 까다로웠다.
문제:
- 소제목 중간에서 줄바꿈하면 안 됨
- "3,000원" 같은 숫자+단위는 분리하면 안 됨
- 마침표가 있어도 소수점이면 줄바꿈하면 안 됨
해결: ai_lib/line_break_service.py에서 AI 기반 줄바꿈 처리를 구현했다.
line_break_prompt = """
한 줄 30자 내외로 자연스럽게 줄바꿈해.
단, 다음은 절대 건드리지 마:
- 소제목 (## 으로 시작하는 줄)
- 숫자+단위 (3,000원, 10km 등)
- 소수점이 포함된 숫자
마침표, 쉼표 뒤에서 끊는 게 자연스러워.
"""
정규식으로 처리하려다가 예외 케이스가 너무 많아서 결국 AI한테 맡겼다. 비용은 좀 들지만 정확도가 훨씬 높다.
트러블슈팅 사례
제목이 반복되는 버그
Git 커밋: 0cf6550 fix: 제목반복제거
같은 키워드로 여러 번 생성하면 제목이 거의 똑같이 나오는 문제가 있었다. AI가 학습 데이터에서 본 패턴을 그대로 반복하는 것 같았다.
원인: temperature 값이 너무 낮았음 (0.3)
해결: 제목 생성 시에만 temperature를 0.8로 올렸다. 본문은 일관성이 중요해서 0.5 유지.
외국어가 섞여 나오는 문제
Git 커밋: e8588b1 fix(prompts): 외국어 표현 금지 규칙 강력 적용
맛집 카테고리에서 일본어나 영어가 섞여 나오는 경우가 있었다. "美味しい", "delicious" 같은 표현이 갑자기 튀어나옴.
해결: 프롬프트에 외국어 금지 규칙을 강하게 추가했다.
# _prompts/category/맛집.py
foreign_language_rule = """
## 절대 금지
- 일본어, 중국어, 영어 등 외국어 사용 금지
- "맛있다"를 "美味しい"나 "delicious"로 쓰지 마
- 외래어도 최소화 (OK: 메뉴, NO: 퀄리티)
"""
MongoDB 연결이 안 끊어지는 문제
초반에 MongoDB 연결을 제대로 종료하지 않아서 연결 풀이 고갈되는 문제가 있었다.
해결: finally 블록에서 반드시 연결을 종료하도록 패턴을 정립했다.
db_service = MongoDBService()
try:
result = db_service.find_documents(...)
return result
finally:
db_service.close_connection() # 무조건 실행
성능 개선
MongoDB 쿼리 최적화
처음에는 모든 문서를 가져와서 Python에서 필터링했는데, 데이터가 쌓이니까 느려졌다.
Before:
# 모든 문서 가져와서 Python에서 처리
all_docs = collection.find({})
unique_words = set(doc["word"] for doc in all_docs)
After:
# MongoDB에서 바로 중복 제거
unique_words = collection.distinct("word", {"category": category})
쿼리 시간이 3초 -> 0.3초로 줄었다.
집계 파이프라인 활용
카테고리별 표현 패턴을 추출할 때 aggregation을 사용했다.
pipeline = [
{"$match": {"category": category}},
{"$group": {
"_id": "$category",
"expressions": {"$addToSet": "$expression"}
}}
]
result = collection.aggregate(pipeline)
서버 사이드에서 처리하니까 네트워크 트래픽도 줄고 속도도 빨라졌다.
배운 점
-
프롬프트 엔지니어링이 생각보다 어렵다
- 한 글자 바꿔도 결과가 완전히 달라짐
- 카테고리별로 미세 조정이 필요함
- 규칙을 너무 많이 넣으면 오히려 이상해짐
-
멀티 AI 엔진 운영은 비용 관리가 핵심
- 각 엔진별 토큰 단가가 다름
- 간단한 작업은 저렴한 엔진, 복잡한 작업은 고성능 엔진
- 로깅해서 비용 추적하는 게 중요함
-
"완벽한 자동화"는 없다
- 결국 사람이 최종 검수해야 함
- 자동화는 시간을 줄여주는 거지, 품질을 보장하지는 않음
향후 계획
- 생성된 원고 품질 점수 자동 평가 시스템
- A/B 테스트로 어떤 스타일이 상위 노출에 효과적인지 분석
- 이미지 자동 삽입 위치 추천
- 실시간 네이버 검색 트렌드 반영