scvcoder commited on
Commit
30fab83
·
verified ·
1 Parent(s): 09b80b8

context_builder: 라운드로빈 다양화 제거 → cross-source cross-encoder reranker. source 무관 적합도 순 top-N. pipeline 에서 query 전달

Browse files
Files changed (1) hide show
  1. src/kpaa/retrieval/context_builder.py +36 -53
src/kpaa/retrieval/context_builder.py CHANGED
@@ -92,74 +92,57 @@ def _format_metadata(e: Excerpt) -> str:
92
  return ""
93
 
94
 
95
- def _diversified_pick(excerpts: list[Excerpt], n: int) -> list[Excerpt]:
96
- """source_type 순수 라운드로빈으로 n개 자연스러운 다양성만 부여.
97
-
98
- 동작:
99
- 1) source_type 별 큐 구성 (입력 순서 유지). 큐 순서 = ranker 가 매긴
100
- 각 type 의 *첫 등장 순서*.
101
- 2) 라운드에 큐에서 1개씩 픽. n 채울 때까지 반복.
102
- 3) 큐가 소진되면 다른 큐에서 계속 채움.
103
-
104
- 설계 의도 *자유도 우선*:
105
- - 특정 type (비법조문 등) 을 강제 우선하지 *않음*. 검색 결과의 자연
106
- 분포 + ranker priority 그대로 하되, 단순 슬라이스 type
107
- 5건을 독점 편향만 막음.
108
- - LLM 이 5건 후보를 받아 자유롭게 가장 적합한 3건을 인용 → 검색이
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 n <= 0 or not excerpts:
120
- return []
121
- if len(excerpts) <= n:
122
  return list(excerpts)
 
 
 
 
123
 
124
- queues: dict[str, list[Excerpt]] = {}
125
- type_order: list[str] = []
126
- for e in excerpts:
127
- t = e.source_type
128
- if t not in queues:
129
- queues[t] = []
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
- 기본 5 — 답변 LLM prefill 토큰을 줄여 응답 속도 ↑.
161
- cap 적용 시 *source_type 다양화 픽* (`_diversified_pick`)
162
- 사용 종류만 5건이 차지하는 편향 회피.
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 = _diversified_pick(excerpts, max_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