context_builder: 라운드로빈 다양화 제거 → cross-source cross-encoder reranker. source 무관 적합도 순 top-N. pipeline 에서 query 전달
Browse files
src/kpaa/retrieval/context_builder.py
CHANGED
|
@@ -92,74 +92,57 @@ def _format_metadata(e: Excerpt) -> str:
|
|
| 92 |
return ""
|
| 93 |
|
| 94 |
|
| 95 |
-
def
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
효과 시나리오:
|
| 112 |
-
- [case1, law1, law2, law3, law4]
|
| 113 |
-
→ [case1, law1, law2, law3, law4] (case 1건뿐 — 자연 한계)
|
| 114 |
-
- [case1, law1, law2, law3, guide1, pipc1]
|
| 115 |
-
→ [case1, law1, guide1, pipc1, law2] (4종 다 노출)
|
| 116 |
-
- [law1..law6]
|
| 117 |
-
→ [law1, law2, law3, law4, law5] (검색 자체가 한 종류면 그대로)
|
| 118 |
"""
|
| 119 |
-
if
|
| 120 |
-
return []
|
| 121 |
-
if len(excerpts) <=
|
| 122 |
return list(excerpts)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
type_order.append(t)
|
| 131 |
-
queues[t].append(e)
|
| 132 |
-
|
| 133 |
-
picked: list[Excerpt] = []
|
| 134 |
-
while len(picked) < n:
|
| 135 |
-
progressed = False
|
| 136 |
-
for t in type_order:
|
| 137 |
-
if not queues[t]:
|
| 138 |
-
continue
|
| 139 |
-
picked.append(queues[t].pop(0))
|
| 140 |
-
progressed = True
|
| 141 |
-
if len(picked) >= n:
|
| 142 |
-
break
|
| 143 |
-
if not progressed:
|
| 144 |
-
break
|
| 145 |
-
return picked
|
| 146 |
|
| 147 |
|
| 148 |
def build(
|
| 149 |
excerpts: list[Excerpt],
|
| 150 |
*,
|
|
|
|
| 151 |
max_chars: int = DEFAULT_MAX_CHARS,
|
| 152 |
max_excerpts: int | None = DEFAULT_MAX_EXCERPTS,
|
| 153 |
) -> str:
|
| 154 |
"""`[근거1] ... [근거2] ...` 형태의 단일 문자열 반환.
|
| 155 |
|
| 156 |
Args:
|
| 157 |
-
excerpts: ranker 정렬 후의 우선순위
|
|
|
|
| 158 |
max_chars: 최종 블록 글자 수 cap. 초과 시 마지막 항목 절단.
|
| 159 |
max_excerpts: LLM 에 전달할 상위 N건 cap. None 이면 무제한 (전체 사용).
|
| 160 |
-
기본
|
| 161 |
-
cap 적용 시
|
| 162 |
-
|
| 163 |
retrieval result 의 `excerpts` 자체는 안 건드리므로 UI
|
| 164 |
references 패널엔 전체가 그대로 노출됨.
|
| 165 |
"""
|
|
@@ -167,7 +150,7 @@ def build(
|
|
| 167 |
return "(검색된 근거가 없습니다.)"
|
| 168 |
|
| 169 |
if max_excerpts is not None:
|
| 170 |
-
excerpts =
|
| 171 |
|
| 172 |
blocks: list[str] = []
|
| 173 |
used = 0
|
|
|
|
| 92 |
return ""
|
| 93 |
|
| 94 |
|
| 95 |
+
def _rerank_text(e: Excerpt) -> str:
|
| 96 |
+
title = e.title or e.citation or ""
|
| 97 |
+
body = (e.content or "")[:1500]
|
| 98 |
+
return f"{title}\n{body}" if title else body
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _cross_source_rerank(query: str, excerpts: list[Excerpt], *, k: int) -> list[Excerpt]:
|
| 102 |
+
"""Cross-encoder reranker 로 source 무관 적합도 순 top-k 선정.
|
| 103 |
+
|
| 104 |
+
이전 버전(2026-05-05까지)에는 `_diversified_pick` 라운드로빈으로 source_type
|
| 105 |
+
다양성을 강제했으나, cross-encoder 도입 후엔 source 무관 적합도가 가장 신뢰할
|
| 106 |
+
만한 신호 — 정답이 한 source 에 집중돼 있어도 reranker 가 옳게 골라내고,
|
| 107 |
+
실제로 다양한 source 가 적합하면 자연스럽게 섞임.
|
| 108 |
+
|
| 109 |
+
Reranker 미설치/disabled 또는 query 없으면 입력 순서 그대로 슬라이스 (이때는
|
| 110 |
+
ranker.rank 의 sort_priority 순).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
"""
|
| 112 |
+
if not excerpts or k <= 0:
|
| 113 |
+
return excerpts[:k]
|
| 114 |
+
if len(excerpts) <= k:
|
| 115 |
return list(excerpts)
|
| 116 |
+
if not query:
|
| 117 |
+
return excerpts[:k]
|
| 118 |
+
try:
|
| 119 |
+
from kpaa.retrieval.reranker import Reranker
|
| 120 |
|
| 121 |
+
rr = Reranker.default()
|
| 122 |
+
except Exception: # noqa: BLE001
|
| 123 |
+
rr = None
|
| 124 |
+
if rr is None:
|
| 125 |
+
return excerpts[:k]
|
| 126 |
+
return rr.rerank(query, excerpts, text_fn=_rerank_text, top_k=k)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
|
| 129 |
def build(
|
| 130 |
excerpts: list[Excerpt],
|
| 131 |
*,
|
| 132 |
+
query: str | None = None,
|
| 133 |
max_chars: int = DEFAULT_MAX_CHARS,
|
| 134 |
max_excerpts: int | None = DEFAULT_MAX_EXCERPTS,
|
| 135 |
) -> str:
|
| 136 |
"""`[근거1] ... [근거2] ...` 형태의 단일 문자열 반환.
|
| 137 |
|
| 138 |
Args:
|
| 139 |
+
excerpts: ranker 정렬 후의 후보 리스트 (source 간 dedup·우선순위 적용됨).
|
| 140 |
+
query: 사용자 원본 질문. cross-source reranker 입력. 없으면 입력 순서 슬라이스.
|
| 141 |
max_chars: 최종 블록 글자 수 cap. 초과 시 마지막 항목 절단.
|
| 142 |
max_excerpts: LLM 에 전달할 상위 N건 cap. None 이면 무제한 (전체 사용).
|
| 143 |
+
기본 7 — 답변 LLM prefill 토큰을 줄여 응답 속도 ↑.
|
| 144 |
+
cap 적용 시 cross-encoder 가 source 무관 적합도 순으로 top-N
|
| 145 |
+
을 골라냄. 라운드로빈 다양화 X (reranker 신호 우선).
|
| 146 |
retrieval result 의 `excerpts` 자체는 안 건드리므로 UI
|
| 147 |
references 패널엔 전체가 그대로 노출됨.
|
| 148 |
"""
|
|
|
|
| 150 |
return "(검색된 근거가 없습니다.)"
|
| 151 |
|
| 152 |
if max_excerpts is not None:
|
| 153 |
+
excerpts = _cross_source_rerank(query or "", excerpts, k=max_excerpts)
|
| 154 |
|
| 155 |
blocks: list[str] = []
|
| 156 |
used = 0
|