From c62e3e70b97a69a4f278afe580da5c0a5a4a080d Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 03:07:29 +0900 Subject: [PATCH] =?UTF-8?q?fix(insta-lab):=20ranked=EA=B0=80=20judge?= =?UTF-8?q?=EC=97=90=20=EB=B3=B4=EB=82=BC=20=ED=9B=84=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=83=81=EC=9C=84=2030=EA=B0=9C=EB=A1=9C=20cap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 미사용 키워드 대량 누적 시 judge 프롬프트/응답이 토큰 한도를 넘어 파싱 실패→claude 신호 전부 null로 degrade되던 문제(프로덕션 확인됨) 해결. base score 상위 JUDGE_CANDIDATE_CAP(30)개만 judge·선별에 적용해 claude 신호 일관 보장. Co-Authored-By: Claude Opus 4.8 (1M context) --- insta-lab/app/main.py | 9 ++++++++ insta-lab/tests/test_ranked_decision_api.py | 25 +++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/insta-lab/app/main.py b/insta-lab/app/main.py index 52aa1ca..a3177da 100644 --- a/insta-lab/app/main.py +++ b/insta-lab/app/main.py @@ -153,6 +153,11 @@ def list_keywords( return {"items": db.list_trending_keywords(category=category, used=used)} +# judge(Claude)에 보낼 최대 후보 수 — 미사용 키워드 대량 누적 시 응답 truncation으로 +# claude 점수가 전부 null로 degrade되는 것을 방지 (base score 상위 N개만 평가). +JUDGE_CANDIDATE_CAP = 30 + + @app.get("/api/insta/keywords/ranked") def ranked_keywords( limit: int = Query(20, ge=1, le=100), @@ -162,6 +167,10 @@ def ranked_keywords( candidates = db.list_trending_keywords(used=False) if not candidates: return {"items": []} + # base score 상위 JUDGE_CANDIDATE_CAP개로 제한 → judge·선별 동일 집합에 적용(claude 신호 일관) + candidates = sorted( + candidates, key=lambda c: float(c.get("score", 0.0)), reverse=True + )[:JUDGE_CANDIDATE_CAP] issued = db.list_recent_issued_topics(window_days=dedup_window_days) prefs = {p["category"]: p["weight"] for p in db.get_preferences()} claude_scores = selection_judge.judge_candidates(candidates) diff --git a/insta-lab/tests/test_ranked_decision_api.py b/insta-lab/tests/test_ranked_decision_api.py index dc34183..7634855 100644 --- a/insta-lab/tests/test_ranked_decision_api.py +++ b/insta-lab/tests/test_ranked_decision_api.py @@ -76,3 +76,28 @@ def test_ranked_respects_dedup_window(client): hwanul = next((i for i in items if i["keyword"] == "환율"), None) assert hwanul is not None, "'환율' 항목이 ranked 응답에 없음" assert hwanul["eligible"] is True, "발행 이력 없는 키워드는 eligible True여야 함" + + +def test_ranked_caps_candidates_to_judge(client, monkeypatch): + """후보가 많아도 judge(Claude)에는 base score 상위 N(JUDGE_CANDIDATE_CAP)개만 전달. + + 운영에서 미사용 키워드가 대량 누적되면 judge 프롬프트/응답이 토큰 한도를 넘어 + 파싱 실패 → claude 신호가 전부 null로 degrade되던 문제 방지. + """ + for i in range(40): + db.add_trending_keyword({"keyword": f"kw{i}", "category": "economy", "score": i * 0.01}) + captured = {} + + def fake_judge(cands): + captured["n"] = len(cands) + captured["min_score"] = min(c["score"] for c in cands) + return {} + + monkeypatch.setattr("app.selection_judge.judge_candidates", fake_judge) + r = client.get("/api/insta/keywords/ranked?threshold=0.0&limit=100") + assert r.status_code == 200 + + from app.main import JUDGE_CANDIDATE_CAP + assert captured["n"] == JUDGE_CANDIDATE_CAP, "judge에는 cap 개수만 전달돼야 함 (전체 X)" + # 상위 score만 전달됐는지 — 최저 score 후보(0.0)는 제외됐어야 함 + assert captured["min_score"] > 0.0