sonsdf commited on
Commit
6d00f20
·
verified ·
1 Parent(s): 076115a

fix: 감리사례 패널 인용 앵커 유실 수정 + 사이드카 지연시간 개선(per_coll 65→20)

Browse files
Files changed (2) hide show
  1. rag/app.py +57 -27
  2. rag/graph.py +5 -1
rag/app.py CHANGED
@@ -158,6 +158,29 @@ def render_audit_section(cases):
158
  st.divider()
159
 
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  # ---------------------------------------------------------------- 실시간 평가 (B트랙)
162
  def render_eval(eval_cfg, question, ans, retrieved):
163
  """답변 후 판사 LLM으로 Faithfulness/Answer Relevancy 채점 + 표시.
@@ -290,15 +313,28 @@ def main():
290
 
291
  api_key, local, eval_cfg = sidebar()
292
 
293
- # 지난 대화 렌더
294
- for msg in st.session_state.messages:
 
 
 
 
 
 
 
 
 
295
  with st.chat_message(msg["role"]):
296
  st.markdown(msg["content"])
297
 
298
- q = st.chat_input("질문을 입력하세요")
299
  if not q:
300
- # 감리사례 버튼/닫기 등으로 인한 리런(q 없음): 직전 답변의 사이드카 패널을 유지 렌더
301
- render_audit_section(st.session_state.get("last_audit_cases"))
 
 
 
 
 
302
  return
303
  with st.chat_message("user"):
304
  st.markdown(q)
@@ -360,28 +396,22 @@ def main():
360
  valid = {h["ref_key"] or h["doc_no"] for h in final.get("retrieved", [])}
361
  used = set(ans.get("used_refs", []))
362
  anchors = {r: anchor_id(r) for r in used} # 인용 배지 → 근거 카드 스크롤
363
- # 3층-2: 근거 카드 — 인용된 것 ★+앵커로 '교체' 렌더 (중복 없이 이전 것을 대체)
364
- if final.get("retrieved") and used:
365
- with ev_box.container():
366
- render_evidence(final["retrieved"], used_refs=used, anchors=anchors)
367
- st.markdown("##### 💬 답변")
368
- # 3층-1: 답변 (인용 배지 클릭 → 카드로 스크롤)
369
- ans_box.markdown(highlight_citations(ans.get("answer", ""), valid, anchors))
370
- # 3층-2.5: 감리지적사례 사이드카 — 답변 근거/인용과 분리된 참고용 (버튼 → 우측 패널).
371
- # 새 답변은 접힌 상태(버튼)로 시작. 리런 시엔 위 `if not q` 분기가 유지 렌더.
372
- st.session_state.last_audit_cases = final.get("audit_cases", []) or []
373
- st.session_state.audit_panel_open = False
374
- render_audit_section(st.session_state.last_audit_cases)
375
- # 3층-3: 해설 (원문과 시각 구분)
376
- with st.container():
377
- st.divider()
378
- if ans.get("has_grounds"):
379
- st.caption(f"🔗 사용 근거: {', '.join(ans.get('used_refs', [])) or '—'} · "
380
- f"이 답변은 위 근거 원문에서 생성됐습니다.")
381
- else:
382
- st.caption("⚠️ 근거를 찾지 못해 답���하지 않았습니다 (환각 방지).")
383
-
384
- # 3층-4(선택): 실시간 품질 평가 — 토글 on일 때만 판사 호출
385
  render_eval(eval_cfg, q, ans, final.get("retrieved", []))
386
 
387
  st.session_state.messages.append(
 
158
  st.divider()
159
 
160
 
161
+ def render_turn_result(turn):
162
+ """한 턴의 근거카드+답변(클릭 가능한 인용)+감리사례+해설을 렌더(3층-1~3, 2.5).
163
+
164
+ 신선한 답변 직후와, q 없는 리런(감리사례 버튼/닫기 클릭) 양쪽에서 동일하게 호출된다.
165
+ st.session_state.last_turn에 저장된 값을 그대로 재생하므로 리런 후에도 highlight_citations
166
+ 의 앵커 링크(인용 배지 클릭 → 근거카드 스크롤)가 계속 동작한다. eval(3층-4)은 판사 LLM
167
+ 재호출·비용 문제로 여기 포함하지 않고 신선한 경로에서만 별도 호출한다.
168
+ """
169
+ retrieved, used, anchors = turn.get("retrieved", []), turn.get("used"), turn.get("anchors")
170
+ if retrieved and used:
171
+ render_evidence(retrieved, used_refs=used, anchors=anchors)
172
+ st.markdown("##### 💬 답변")
173
+ st.markdown(turn.get("answer_html", ""))
174
+ render_audit_section(turn.get("audit_cases", []))
175
+ with st.container():
176
+ st.divider()
177
+ if turn.get("has_grounds"):
178
+ st.caption(f"🔗 사용 근거: {', '.join(turn.get('used_refs', [])) or '—'} · "
179
+ f"이 답변은 위 근거 원문에서 생성됐습니다.")
180
+ else:
181
+ st.caption("⚠️ 근거를 찾지 못해 답변하지 않았습니다 (환각 방지).")
182
+
183
+
184
  # ---------------------------------------------------------------- 실시간 평가 (B트랙)
185
  def render_eval(eval_cfg, question, ans, retrieved):
186
  """답변 후 판사 LLM으로 Faithfulness/Answer Relevancy 채점 + 표시.
 
313
 
314
  api_key, local, eval_cfg = sidebar()
315
 
316
+ q = st.chat_input("질문을 입력하세요")
317
+
318
+ # 지난 대화 렌더. q 없는 리런(감리사례 버튼/닫기 클릭 등)이면 마지막 assistant 메시지는
319
+ # 아래에서 last_turn으로 풍부하게(근거카드+클릭 가능한 인용 배지) 재렌더하므로 평문 중복 방지.
320
+ messages = st.session_state.messages
321
+ skip_last_idx = (len(messages) - 1) if (not q and messages
322
+ and messages[-1]["role"] == "assistant"
323
+ and st.session_state.get("last_turn")) else -1
324
+ for i, msg in enumerate(messages):
325
+ if i == skip_last_idx:
326
+ continue
327
  with st.chat_message(msg["role"]):
328
  st.markdown(msg["content"])
329
 
 
330
  if not q:
331
+ # 감리사례 버튼/닫기 등으로 인한 리런(q 없음): 직전 턴 전체(근거카드·인용·해설·사이드카)를
332
+ # 세션 상태에서 재렌더 — highlight_citations의 앵커 링크(조항 클릭→근거카드 스크롤)가
333
+ # 리런 후에도 계속 동작해야 하므로 평문(messages)이 아니라 last_turn을 그대로 재생.
334
+ last_turn = st.session_state.get("last_turn")
335
+ if last_turn:
336
+ with st.chat_message("assistant"):
337
+ render_turn_result(last_turn)
338
  return
339
  with st.chat_message("user"):
340
  st.markdown(q)
 
396
  valid = {h["ref_key"] or h["doc_no"] for h in final.get("retrieved", [])}
397
  used = set(ans.get("used_refs", []))
398
  anchors = {r: anchor_id(r) for r in used} # 인용 배지 → 근거 카드 스크롤
399
+ # ev_box/ans_box(교체형 st.empty)를 비워, 아래 render_turn_result가 그리는 최종본과
400
+ # 중복 렌더되지 않게 한다(스트리밍 중 임시 렌더 제거).
401
+ ev_box.empty()
402
+ ans_box.empty()
403
+ st.session_state.last_turn = {
404
+ "retrieved": final.get("retrieved", []), "used": used, "anchors": anchors,
405
+ "answer_html": highlight_citations(ans.get("answer", ""), valid, anchors),
406
+ "has_grounds": ans.get("has_grounds"), "used_refs": ans.get("used_refs", []),
407
+ "audit_cases": final.get("audit_cases", []) or [],
408
+ }
409
+ st.session_state.audit_panel_open = False # 새 답변은 접힌 상태(버튼)로 시작
410
+ # 3층-1~3, 2.5: 근거카드(★+앵커)·클릭 가능한 인용·감리사례·해설 (q 없는 리런에서도
411
+ # render_turn_result(last_turn)으로 동일하게 재생돼 인용 앵커 링크가 유지된다)
412
+ render_turn_result(st.session_state.last_turn)
413
+
414
+ # 3층-4(선택): 실시간 품질 평가 — 토글 on일 때만, 신선한 답변 직후 1회만 호출(재실행 방지)
 
 
 
 
 
 
415
  render_eval(eval_cfg, q, ans, final.get("retrieved", []))
416
 
417
  st.session_state.messages.append(
rag/graph.py CHANGED
@@ -214,8 +214,12 @@ class Pipeline:
214
  cases = []
215
  if "audit_cases" in self.index.colls:
216
  try:
 
 
 
 
217
  hits = self.index.retrieve_routed(state["rewritten"], ["audit_cases"],
218
- k=3, min_standards=0, per_coll=65)
219
  cases = [_audit_card(h) for h in _audit_filter(hits)]
220
  except Exception: # noqa: BLE001 — 사이드카 실패는 답변에 영향 없이 조용히 무시
221
  cases = []
 
214
  cases = []
215
  if "audit_cases" in self.index.colls:
216
  try:
217
+ # per_coll=20(코드베이스 관행값): 65건 전체(구버전 per_coll=65)를 매 질문마다
218
+ # 리랭킹하면 12초대 지연 유발(실측). dense 사전필터 20위 안에 진짜 관련
219
+ # 사례가 안정적으로 들어옴(스모크테스트 자기회수 점수 0.86~1.0로 확인) →
220
+ # 후보를 줄여도 recall 손실 거의 없이 리랭킹 비용만 3배 이상 절감.
221
  hits = self.index.retrieve_routed(state["rewritten"], ["audit_cases"],
222
+ k=3, min_standards=0, per_coll=20)
223
  cases = [_audit_card(h) for h in _audit_filter(hits)]
224
  except Exception: # noqa: BLE001 — 사이드카 실패는 답변에 영향 없이 조용히 무시
225
  cases = []