feat(blog-lab): 평가자 단계 — 6기준 60점 체계 + link_natural 추가
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -168,18 +168,19 @@ def _seed_templates(conn: sqlite3.Connection) -> None:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "quality_review",
|
"name": "quality_review",
|
||||||
"description": "블로그 글 품질 리뷰 (5기준 × 10점)",
|
"description": "블로그 글 품질 리뷰 (6기준 × 10점)",
|
||||||
"template": (
|
"template": (
|
||||||
"당신은 블로그 콘텐츠 품질 평가 전문가입니다.\n"
|
"당신은 블로그 콘텐츠 품질 평가 전문가입니다.\n"
|
||||||
"아래 블로그 글을 5가지 기준으로 평가해주세요.\n\n"
|
"아래 블로그 글을 6가지 기준으로 평가해주세요.\n\n"
|
||||||
"제목: {title}\n"
|
"제목: {title}\n"
|
||||||
"본문: {body}\n\n"
|
"본문: {body}\n\n"
|
||||||
"평가 기준 (각 1-10점):\n"
|
"평가 기준 (각 1-10점):\n"
|
||||||
"1. 독자 공감도: 1인칭 체험기가 자연스럽고 공감되는가?\n"
|
"1. 독자 공감도 (empathy): 1인칭 체험기가 자연스럽고 공감되는가?\n"
|
||||||
"2. 제목 클릭 유도력: 검색 결과에서 클릭하고 싶은 제목인가?\n"
|
"2. 제목 클릭 유도력 (click_appeal): 검색 결과에서 클릭하고 싶은 제목인가?\n"
|
||||||
"3. 구매 전환력: 읽고 나서 제품을 사고 싶어지는가?\n"
|
"3. 구매 전환력 (conversion): 읽고 나서 제품을 사고 싶어지는가?\n"
|
||||||
"4. SEO 최적화: 키워드 배치, 소제목, 길이가 적절한가?\n"
|
"4. SEO 최적화 (seo): 키워드 배치, 소제목, 길이가 적절한가?\n"
|
||||||
"5. 형식 완성도: 비교표, 이미지 설명, 단락 구성이 잘 되어있는가?\n\n"
|
"5. 형식 완성도 (format): 비교표, 이미지 설명, 단락 구성이 잘 되어있는가?\n"
|
||||||
|
"6. 링크 자연스러움 (link_natural): 제휴 링크가 광고처럼 느껴지지 않고 자연스럽게 녹아있는가? (링크가 없으면 5점 기본)\n\n"
|
||||||
"JSON 형식으로 응답:\n"
|
"JSON 형식으로 응답:\n"
|
||||||
"{{\n"
|
"{{\n"
|
||||||
" \"scores\": {{\n"
|
" \"scores\": {{\n"
|
||||||
@@ -187,7 +188,8 @@ def _seed_templates(conn: sqlite3.Connection) -> None:
|
|||||||
" \"click_appeal\": N,\n"
|
" \"click_appeal\": N,\n"
|
||||||
" \"conversion\": N,\n"
|
" \"conversion\": N,\n"
|
||||||
" \"seo\": N,\n"
|
" \"seo\": N,\n"
|
||||||
" \"format\": N\n"
|
" \"format\": N,\n"
|
||||||
|
" \"link_natural\": N\n"
|
||||||
" }},\n"
|
" }},\n"
|
||||||
" \"total\": N,\n"
|
" \"total\": N,\n"
|
||||||
" \"pass\": true/false,\n"
|
" \"pass\": true/false,\n"
|
||||||
@@ -258,6 +260,38 @@ def _migrate_templates(conn: sqlite3.Connection) -> None:
|
|||||||
(new_blog_write,),
|
(new_blog_write,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
new_quality_review = (
|
||||||
|
"당신은 블로그 콘텐츠 품질 평가 전문가입니다.\n"
|
||||||
|
"아래 블로그 글을 6가지 기준으로 평가해주세요.\n\n"
|
||||||
|
"제목: {title}\n"
|
||||||
|
"본문: {body}\n\n"
|
||||||
|
"평가 기준 (각 1-10점):\n"
|
||||||
|
"1. 독자 공감도 (empathy): 1인칭 체험기가 자연스럽고 공감되는가?\n"
|
||||||
|
"2. 제목 클릭 유도력 (click_appeal): 검색 결과에서 클릭하고 싶은 제목인가?\n"
|
||||||
|
"3. 구매 전환력 (conversion): 읽고 나서 제품을 사고 싶어지는가?\n"
|
||||||
|
"4. SEO 최적화 (seo): 키워드 배치, 소제목, 길이가 적절한가?\n"
|
||||||
|
"5. 형식 완성도 (format): 비교표, 이미지 설명, 단락 구성이 잘 되어있는가?\n"
|
||||||
|
"6. 링크 자연스러움 (link_natural): 제휴 링크가 광고처럼 느껴지지 않고 자연스럽게 녹아있는가? (링크가 없으면 5점 기본)\n\n"
|
||||||
|
"JSON 형식으로 응답:\n"
|
||||||
|
"{{\n"
|
||||||
|
" \"scores\": {{\n"
|
||||||
|
" \"empathy\": N,\n"
|
||||||
|
" \"click_appeal\": N,\n"
|
||||||
|
" \"conversion\": N,\n"
|
||||||
|
" \"seo\": N,\n"
|
||||||
|
" \"format\": N,\n"
|
||||||
|
" \"link_natural\": N\n"
|
||||||
|
" }},\n"
|
||||||
|
" \"total\": N,\n"
|
||||||
|
" \"pass\": true/false,\n"
|
||||||
|
" \"feedback\": \"개선 사항 설명\"\n"
|
||||||
|
"}}"
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE prompt_templates SET template = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE name = 'quality_review'",
|
||||||
|
(new_quality_review,),
|
||||||
|
)
|
||||||
|
|
||||||
# marketer_enhance가 없으면 추가
|
# marketer_enhance가 없으면 추가
|
||||||
existing = conn.execute("SELECT id FROM prompt_templates WHERE name = 'marketer_enhance'").fetchone()
|
existing = conn.execute("SELECT id FROM prompt_templates WHERE name = 'marketer_enhance'").fetchone()
|
||||||
if not existing:
|
if not existing:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Claude API 기반 블로그 글 품질 리뷰 — 5기준 × 10점, 35/50 통과."""
|
"""Claude API 기반 블로그 글 품질 리뷰 — 6기준 × 10점, 42/60 통과."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -11,7 +11,7 @@ from .db import get_template
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
PASS_THRESHOLD = 35 # 50점 만점 중 35점 이상이면 통과
|
PASS_THRESHOLD = 42 # 60점 만점 중 42점 이상이면 통과 (70%)
|
||||||
|
|
||||||
_client: Optional[anthropic.Anthropic] = None
|
_client: Optional[anthropic.Anthropic] = None
|
||||||
|
|
||||||
@@ -28,7 +28,10 @@ def review_post(title: str, body: str) -> Dict[str, Any]:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
{
|
{
|
||||||
"scores": {"empathy": N, "click_appeal": N, "conversion": N, "seo": N, "format": N},
|
"scores": {
|
||||||
|
"empathy": N, "click_appeal": N, "conversion": N,
|
||||||
|
"seo": N, "format": N, "link_natural": N
|
||||||
|
},
|
||||||
"total": N,
|
"total": N,
|
||||||
"pass": bool,
|
"pass": bool,
|
||||||
"feedback": str
|
"feedback": str
|
||||||
@@ -69,7 +72,10 @@ def review_post(title: str, body: str) -> Dict[str, Any]:
|
|||||||
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
||||||
logger.warning("Quality review JSON parse failed: %s", e)
|
logger.warning("Quality review JSON parse failed: %s", e)
|
||||||
return {
|
return {
|
||||||
"scores": {"empathy": 0, "click_appeal": 0, "conversion": 0, "seo": 0, "format": 0},
|
"scores": {
|
||||||
|
"empathy": 0, "click_appeal": 0, "conversion": 0,
|
||||||
|
"seo": 0, "format": 0, "link_natural": 0,
|
||||||
|
},
|
||||||
"total": 0,
|
"total": 0,
|
||||||
"pass": False,
|
"pass": False,
|
||||||
"feedback": f"리뷰 파싱 실패. 원본 응답:\n{raw[:500]}",
|
"feedback": f"리뷰 파싱 실패. 원본 응답:\n{raw[:500]}",
|
||||||
|
|||||||
74
blog-lab/tests/test_evaluator.py
Normal file
74
blog-lab/tests/test_evaluator.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"""평가자 단계 테스트 — 6기준 60점."""
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_post_has_6_criteria():
|
||||||
|
"""6개 기준으로 채점하는지 확인."""
|
||||||
|
from app.quality_reviewer import review_post
|
||||||
|
|
||||||
|
mock_response = json.dumps({
|
||||||
|
"scores": {
|
||||||
|
"empathy": 8, "click_appeal": 7, "conversion": 9,
|
||||||
|
"seo": 8, "format": 7, "link_natural": 9,
|
||||||
|
},
|
||||||
|
"total": 48,
|
||||||
|
"pass": True,
|
||||||
|
"feedback": "전체적으로 우수합니다",
|
||||||
|
})
|
||||||
|
|
||||||
|
with patch("app.quality_reviewer._get_client") as mock_client_fn, \
|
||||||
|
patch("app.quality_reviewer.get_template", return_value="제목: {title}\n본문: {body}"):
|
||||||
|
mock_client = mock_client_fn.return_value
|
||||||
|
mock_client.messages.create.return_value.content = [type("C", (), {"text": mock_response})()]
|
||||||
|
result = review_post("테스트 제목", "<p>본문</p>")
|
||||||
|
|
||||||
|
assert "link_natural" in result["scores"]
|
||||||
|
assert len(result["scores"]) == 6
|
||||||
|
assert result["total"] == 48
|
||||||
|
assert result["pass"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_pass_threshold_is_42():
|
||||||
|
"""통과 기준이 42점인지 확인."""
|
||||||
|
from app.quality_reviewer import PASS_THRESHOLD
|
||||||
|
assert PASS_THRESHOLD == 42
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_fails_below_42():
|
||||||
|
"""42점 미만이면 불통과."""
|
||||||
|
from app.quality_reviewer import review_post
|
||||||
|
|
||||||
|
mock_response = json.dumps({
|
||||||
|
"scores": {
|
||||||
|
"empathy": 5, "click_appeal": 5, "conversion": 5,
|
||||||
|
"seo": 5, "format": 5, "link_natural": 5,
|
||||||
|
},
|
||||||
|
"total": 30,
|
||||||
|
"pass": False,
|
||||||
|
"feedback": "개선 필요",
|
||||||
|
})
|
||||||
|
|
||||||
|
with patch("app.quality_reviewer._get_client") as mock_client_fn, \
|
||||||
|
patch("app.quality_reviewer.get_template", return_value="제목: {title}\n본문: {body}"):
|
||||||
|
mock_client = mock_client_fn.return_value
|
||||||
|
mock_client.messages.create.return_value.content = [type("C", (), {"text": mock_response})()]
|
||||||
|
result = review_post("제목", "<p>본문</p>")
|
||||||
|
|
||||||
|
assert result["pass"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_handles_parse_failure():
|
||||||
|
"""JSON 파싱 실패 시 기본값 반환 (6개 기준)."""
|
||||||
|
from app.quality_reviewer import review_post
|
||||||
|
|
||||||
|
with patch("app.quality_reviewer._get_client") as mock_client_fn, \
|
||||||
|
patch("app.quality_reviewer.get_template", return_value="제목: {title}\n본문: {body}"):
|
||||||
|
mock_client = mock_client_fn.return_value
|
||||||
|
mock_client.messages.create.return_value.content = [type("C", (), {"text": "잘못된 응답"})()]
|
||||||
|
result = review_post("제목", "<p>본문</p>")
|
||||||
|
|
||||||
|
assert result["pass"] is False
|
||||||
|
assert "link_natural" in result["scores"]
|
||||||
|
assert result["total"] == 0
|
||||||
Reference in New Issue
Block a user