사용자가 app/main.py를 통해 임의의 쿼리를 입력하여 답변을 받기까지의 처리 흐름을 단계별, 경우의 수별로 정리한 문서입니다. 메인 로직은 src/graph/workflow.py의 RAGChatbotV17.answer 메서드에 위치합니다.
- 경로:
app/main.py➔run_query(chatbot, query) - 동작 방식:
- Streamlit UI 텍스트 인풋에서
query문자열을 받습니다. - 캐싱된
chatbot인스턴스(즉,RAGChatbotV17)의answer메서드를 호출하여 답변을 요청합니다. (top_k=10전달)
- Streamlit UI 텍스트 인풋에서
- 경로:
src/graph/workflow.py➔RAGChatbotV17.answer - 여기에서 질문(query)의 의도에 따라 다음과 같은 분기를 거치게 됩니다.
- 경로:
src/graph/nodes.py➔QueryIntentParser.parse(query) - 동작 방식 및 분기:
- LLM(
intent_llm)이나 규칙 기반으로 쿼리의 의도(Intent)와 관련된 조직(org_name) 등을 추출합니다. - 랭킹 질의 (
query_type == "ranking"):- "사업비가 가장 많은 3곳은?" 같은 의도의 경우 로컬 캐시/CSV를 기반으로 즉시
_handle_ranking_query(intent)를 호출해 결과를 리턴. (검색 비용 최소화) - 예외조건: 명시적인 대상 기관 이름이 특정된 경우 정밀도를 위해 일반 검색(Search)으로 우회.
- "사업비가 가장 많은 3곳은?" 같은 의도의 경우 로컬 캐시/CSV를 기반으로 즉시
- 카테고리 질의 (
query_type == "category"):- 문서 종류/구분 등에 대한 경우
_handle_category_query(intent)호출 후 즉시 리턴.
- 문서 종류/구분 등에 대한 경우
- LLM(
- 경로:
src/graph/workflow.py내QuestionPlanner,ConversationContext활용 - 동작 방식:
self.conversation.get_follow_up_context(query)를 통해 사용자가 앞서 묻던 기관/문서인지 파악.question_plan = self.question_planner.build(query, target_org)를 호출하여, 질의가 "수치 확인", "일정(Deadline)", "다문서 비교(Comparison)" 중 어떤 답변 형태에 적합한지 플랜을 만듭니다.
벡터 검색(Retrieval)이 필요치 않거나 메타데이터 만으로 충분히 처리할 수 있는 간단한 질의에 대한 최적화 우회 로직.
_try_csv_short_circuit: CSV 인덱스에 매칭되는 기본 사업비/기간 등의 메타데이터 질의._try_org_overview_short_circuit: "특정 기관의 전체 개요" 질의 시 메타데이터 요약으로 리턴._try_chunk_budget_short_circuit: 청크/예산 관련 질의._try_org_document_scan_short_circuit: 서류/평가 기준 등에 대한 스캔.- 조건: 위 함수 중 하나라도 성공적인 답변 페이로드를 만들면 그 즉시 LLM 및 VectorStore 검색 단계를 건너뛰고 마무리 단계로 이동합니다.
주요 분기와 단축 처리를 뚫고 내려온 본격적인 문서 질의는 여기서 문서를 탐색합니다.
- 경로:
src/graph/workflow.py➔_retrieve_results - 동작 방식 및 분기:
- 비교(Comparison) / 다문서(Multi-doc) 대상: 기관 명을 한 곳에 특정하지 않거나, 대상을 병합(coverage_targets 결합)하여 더 넓은 범위를 검색.
top_k를 최대 30~48까지 상향 조정. - 단일 기관 (Single Org):
- 특정
org_name으로 스코프를 제한하여_retrieve_results를 호출. - 예외조건: 검색 결과가 부족하면 전역(global) 검색으로 확대한 후
_filter_results_by_org로 다시 한 번 보완 탐색 시도.
- 특정
- 추가적으로, 문서가 너무 청크에만 의존할 경우 원본 문서(
pdf,hwp)에 가산점을 부여하거나(prefer_original), 원본 전용 재조회 모델을 실행하여 결과를 취합.
- 비교(Comparison) / 다문서(Multi-doc) 대상: 기관 명을 한 곳에 특정하지 않거나, 대상을 병합(coverage_targets 결합)하여 더 넓은 범위를 검색.
검색된 청크/문서 정보를 컨텍스트로 변환해 응답을 만듭니다.
- 경로:
src/graph/workflow.py➔_answer_with_results - 분기 조건 (답변 형식에 따라):
- 비교 질의 (
is_multi_target == True):_build_comparison_answer_from_results호출.- 두 개 외 이상의 문서를 대조하여 표 형식이거나 구조화된 비교문을 생성합니다. 한 곳의 근거가 없으면 경고(warning)와 함께 하나의 정보만 표출.
- 사실 기반 단문형(
Extractive First):- 비용, 마감기한 등 수치 데이터의 경우 LLM을 태우기 전
_build_non_llm_answer로 정규식 기반 추출 시도. (추출된 답변extractive_draft) 추출이 명확할 경우 비용 절감을 위해 이 단계에서 반환 준비.
- 비용, 마감기한 등 수치 데이터의 경우 LLM을 태우기 전
- 일반/서술형 질의 (
Generative LLM):self.answer_generator.generate(query, context, history, extractive_draft)호출. (실제src/graph/nodes.py의RFPAnswerGenerator실행).- 검색된 Context와 LLM을 조합하여 완성된 형태의 답변 생성.
- 비교 질의 (
결론, 근거, 불필요한 라벨(요약:, 출처: 등)을 다듬고 톤 앤 매너(Tone & Manner)를 조정합니다.
- 경로:
RAGChatbotV17.answer내 중첩 함수_finalize_payload - 동작 방식:
- LLM 모델(
self.llm)이 존재하고 답변이 Generative 모드인 경우, 요약 질의(_is_summary_focus_query)가 아니라면_polish_answer_with_llm를 통해 가독성 높은 한국어로 어투 조정. (Concise / Guide 스타일 분기 적용). - LLM을 쓰지 않거나 부적합할 경우 정규식 기반
_compact_answer_sections을 사용하여 라벨 제거. - 수치나 단답형(
_is_single_value_query)인 경우, 문맥(Context)을 앞뒤로 소량 덧붙여 너무 기계적이지 않도록 문장 완성. - 응답 Latency 등의 통계 값을 페이로드(
payload)에 담아app/main.py리턴.
- LLM 모델(
- 의도 분석 / 기획: 무엇을 물어보는지 확인 (일정? 예산? 비교? 요약?)
- 사전 차단 (우회): 단순 메타데이터만으로 대답할 수 있으면 LLM/DB 검색 생략 (
Short-Circuit). - 분석 및 검색: 단일 문서인지 비교 문서인지에 맞추어 벡터DB 질의. 확장 조회 필요시 자동 결합.
- 추출 및 생성: 추출만으로 가능하면 정규식 기반 가공(
_build_non_llm_answer); 서술형이면 LLM에 Context를 주어 답변 생성. - 어투 정리: 최종 응답 전, 안내형/간결형 톤으로 응답 스타일 정리 후 UI (Streamlit)로 전달.
- 질의 문자열을
strip()했을 때 비어 있으면 즉시"질문을 입력해 주세요."페이로드를 반환합니다. - 질의마다
self.vector_store.last_search_results를 초기화하고,perf_stats(LLM 호출 수/검색 시간/예산 소진 여부 등)를 새로 만듭니다.
QueryIntentParser.parse()는 아래 순서로 실행됩니다.- 캐시 확인: exact cache / signature cache hit 시 즉시 반환
- Regex 파싱
- LLM 호출 필요성 판단 (
_should_call_llm) - 필요 시 LLM 파싱 수행 후,
llm_conf >= max(0.7, regex_conf + 0.03)일 때만 LLM 결과 채택
- 즉, "LLM이 항상 최종 결정"이 아니라 confidence margin 비교로 regex 결과가 유지될 수 있습니다.
ConversationContext.get_follow_up_context()로 후속질문 여부를 판별하고, 명시 기관이 없으면 직전 기관(last_org)을 질의에 주입할 수 있습니다.QuestionPlanner.build()는comparison / owner / deadline / fact_numeric / multi_doc / single_doc를 구분합니다.comparison_like_query계산 시 예외가 중요합니다.- 단일 기관 예산 질문, 단일 기관 시각질문은 비교 플래그를 강제로 끕니다.
- 다기관 질의인 경우
coverage_targets(비교 대상 기관 목록)를 복원하고, 필요 시org_name스코프를 해제해 전역 비교 검색으로 전환합니다.
- 요약 질의라도
"본문/원문/조항/근거/페이지"같은 근거 직접 검증 요청이 있으면 short-circuit를 우회하고 본문 RAG 경로로 보냅니다. - 단, CSV 매칭 신뢰도가 충분하면
_can_override_short_circuit_bypass()가 우회를 다시 해제하여 short-circuit를 허용합니다. - short-circuit는 아래 순서로 시도합니다.
_try_csv_short_circuit_try_org_overview_short_circuit_try_chunk_budget_short_circuit_try_org_document_scan_short_circuit
_retrieve_results()는 단일 1회 검색이 아니라 동적 전략 기반 다단계입니다.- 질의 확장 (
_expand_query_terms) + 확장 개수 cap (_resolve_expansion_cap) - 다중 pass 검색 (
RETRIEVAL_SEARCH_PASSES)과 pass별k증대 - 조건부 CSV boost pass
- 조건부 fallback pass(pdf/hwp/csv 재조합)
- 조건부 asset-sidecar pass(시각/정밀 질의 보강)
- 조건부 source-local probe(같은 source 내부 청크 재스캔)
- rerank + anchor promotion + comparison diversity 보정
- 질의 확장 (
- 조기 종료(
_should_stop_retrieval_early)도 엄격 조건을 만족할 때만 동작합니다.- 정밀 사실 질의나 책임 주체 질의에서 anchor evidence가 부족하면 조기 종료를 막습니다.
- 비교 질의이면서 양측 커버리지가 부족하면 단정 비교를 금지하고 warning 응답으로 종료합니다.
- 사실형/기한형/책임형은
extractive-first를 우선 시도합니다.extractive_draft가 확보되고 요약 집중 질의가 아니면 LLM 생성을 건너뛰고 추출답으로 종료할 수 있습니다.
- LLM이 없으면 규칙 기반(
_build_non_llm_answer) -> 실패 시 최소 요약(_create_multi_org_summary) 순서. - LLM이 있으면
RFPAnswerGenerator.generate()를 타며, 내부는- 근거 정제
- 최종 답변 생성
- 실패 시 단일 생성 프롬프트 fallback 로 구성됩니다.
- 생성 결과가 불확실/오류면 추출 초안으로 되돌리는 하이브리드 폴백이 다시 적용됩니다.
- 조건을 만족하면
_polish_answer_with_llm로 문장 다듬기, 아니면_compact_answer_sections로 정리합니다. - 단일값 질의는
_extract_single_value_from_fact_answer+_render_single_value_answer로 문장형 단답을 강제합니다. - 생성 답변(
answer_mode == generative)은_restrict_answer_to_evidence로 evidence 정합성을 재검증합니다. - 모든 경로에서
latencies와retrieved_docs를 보정해 UI/평가 파이프라인이 공통 포맷을 받도록 맞춥니다.
- 기관 스코프 검색 실패 시 전역 재시도 + 기관 필터링까지 수행한 뒤에도 없으면
_build_org_not_found_payload를 반환합니다. - 전역 검색까지 모두 실패하면
"관련 정보를 찾을 수 없습니다."로 종료합니다.
이번 변경으로 workflow.py는 오케스트레이션 중심으로 유지하고, 공통 로직을 src/* 모듈로 분리했습니다.
src/utils/text_ops.pynormalize_text_for_match,clip_text_safely,looks_incomplete_clauseclean_extracted_line,is_noise_line
src/parsers/csv_runtime_utils.py- CSV 값/시간 정규화, VAT 추출, 메타 필드 추출, 공고번호 추출, 답변용 시간 포맷
src/prompts/answer_postprocess.pyformat_answer_for_readability,compact_answer_sectionsenforce_honorific_tone,normalize_answer_for_compare
src/evaluation/runtime_diagnostics.pyestimate_slot_fill_rate,estimate_confidencecollect_answer_content_lines,looks_uncertain_answershould_fallback_to_extractive_draft
- 기존 내부 메서드는 대다수를 래퍼 형태로 유지하여 외부 호출 계약(메서드명/시그니처)을 깨지 않도록 했습니다.
- 응답 생성/후처리/평가 보조 로직은 각 모듈 함수를 import해 호출하도록 전환했습니다.
- 결과적으로 향후
retrievers/prompts/parsers/evaluation/utils단위 실험 및 회귀 확인이 쉬워졌습니다.
python3 -m compileall src통과from src.graph.workflow import RAGChatbotV17import 스모크 통과- 평가 산출물:
eval_results_full20_chunk_synced_after_module_split_phase2_20260301.json