Skip to content

refactor: organize chatbot rag orchestration - #121

Merged
Yu-JeSeung merged 3 commits into
mainfrom
refactor/chatbot-rag-orchestration
May 26, 2026
Merged

refactor: organize chatbot rag orchestration#121
Yu-JeSeung merged 3 commits into
mainfrom
refactor/chatbot-rag-orchestration

Conversation

@Yu-JeSeung

@Yu-JeSeung Yu-JeSeung commented May 24, 2026

Copy link
Copy Markdown
Contributor

🎯 배경

  • OSS 챗봇은 실제 Agent tool calling이 아니라 Ollama 기반 RAG 응답 구조이므로, 서버 주도 pseudo tool routing 형태로 흐름을 명확히 정리할 필요가 있었습니다.
  • 애매한 질의가 바로 자유형 Ollama 호출로 빠지지 않도록 RAG context 기반 방어선을 추가하고, 라우팅 결과를 추적 가능하게 만들었습니다.

🔍 주요 내용

  • ToolResultreason을 추가해 pseudo tool 라우팅 사유를 남기도록 개선
  • 챗봇 서비스에 chatbot_tool_route 로그 추가
  • oss 모드에서 fast path 이후 RAG context 기반 grounded fallback을 먼저 시도하도록 조정
  • fake module 기반 lightweight chatbot tool routing 회귀 테스트 추가
  • RAG light check workflow에 chatbot routing 체크 포함
  • 로컬 RAG 평가 리포트 폴더를 .gitignore에 추가

변경 요약(1~3줄)

OSS 챗봇의 서버 주도형(의사) 도구 라우팅을 명확히 하기 위해 ToolResult에 라우팅 근거(reason)를 추가하고 단계별 라우팅 로깅을 도입했습니다. OSS 모드에서 빠른 경로 후 RAG 컨텍스트 기반의 근거 있는 폴백을 우선 시도하도록 흐름을 재구성했습니다.

주요 변경점

  • ToolResult에 reason: str 필드 추가로 라우팅 근거 기록
  • _log_tool_route() 추가 및 chatbot_tool_route 로깅으로 쿼리 해시/모드/스테이지/도구/신뢰도/llm_required/사유 추적
  • OSS 흐름 재구성: mode_tools → oss_fast_path → grounded_fallback(run_final_fallback_tools) → LLM 호출 → empty_fallback
  • grounded_fallback의 텍스트 유무·llm_required 체크와 빈 응답 시 추가 분기(oss_empty_fallback, oss_grounded_empty_fallback) 처리
  • 회귀 테스트 추가: tests/regression/check_chatbot_tool_routing.py (가짜 모듈로 라우팅 경로 검증)
  • CI 통합: .github/workflows/rag-light-check.yml에 챗봇 라우팅 체크 추가
  • .gitignore에 로컬 RAG 평가 리포트 폴더(tests/regression/reports/) 추가

주의/리스크

  • 라우팅 흐름이 복잡해져 의도치 않은 경로 선택 가능성 — 추가 검증 필요
  • 쿼리별 상세 로깅으로 인한 성능/저장 오버헤드 모니터링 필요

다음 액션

  • 실제 트래픽에서 RAG 기반 폴백의 정확도와 LLM 호출 감소 효과 검증
  • 로깅 결과를 바탕으로 자주 잘못 라우팅되는 패턴 식별 후 규칙/우선순위 조정

Review Change Stack

@Yu-JeSeung Yu-JeSeung self-assigned this May 24, 2026
@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

워크스루

채봇 도구 라우팅 결과에 이유(reason) 필드를 추가하여 각 라우팅 결정을 추적하고, 서비스 계층에서 단계별 로깅을 구현하며, 회귀 테스트로 모든 라우팅 경로를 검증합니다.

변경사항

도구 라우팅 로깅 및 회귀 검증

Layer / File(s) Summary
도구 결과 데이터 계약과 이유 필드
LLM/OSS/tools.py
ToolResult 데이터클래스에 reason: str 필드를 추가하고, EMPTY_TOOL_RESULT 및 모든 도구 함수(_direct_answer_tool, _schedule_tool, _clarification_tool, _postprocess_tool, _confident_search_tool)에서 매칭 조건을 설명하는 이유 문자열을 설정합니다. 또한 run_empty_oss_fallback_tools와 run_final_fallback_tools의 폴백 경로에서도 이유를 명시적으로 채웁니다.
도구 라우팅 로깅 및 서비스 흐름 재구성
LLM/OSS/service.py
ToolResult를 import하고 _log_tool_route 함수를 추가하여 쿼리 해시, 모드, 단계, 도구명, 엔진, 신뢰도, LLM 필요 여부, 텍스트 존재 여부, 이유를 logger.info로 기록합니다. chat_with_oss의 OSS 모드 처리를 grounded_fallback 중심으로 재구성하여 mode_tools, oss_fast_path, oss_grounded_fallback, oss_grounded_empty_fallback, oss_empty_fallback, final_fallback 단계별로 로깅하고, LLM 호출과 조건부 후처리를 적용합니다.
도구 라우팅 회귀 테스트
tests/regression/check_chatbot_tool_routing.py
가짜 LLM.sub_model.query_indexLLM.sub_model.schedule_index 모듈을 sys.modules에 주입하여 run_mode_tools, run_oss_fast_path_tools, run_final_fallback_tools의 라우팅 결과를 검증합니다. 각 검증은 resolved, reason, llm_required, fallback 텍스트 등을 확인하고, 오류를 JSON으로 출력하며 실패 시 종료 코드 1을 반환합니다.
CI 워크플로 및 저장소 설정
.github/workflows/rag-light-check.yml, .gitignore
rag-light-check 워크플로의 py_compile 대상에 tests/regression/check_chatbot_tool_routing.py, LLM/OSS/tools.py, LLM/OSS/service.py를 추가하고, "Run lightweight query-index compatibility checks" 스텝에서 해당 회귀 스크립트를 실행하도록 변경했습니다. 또한 tests/regression/reports/ 디렉터리를 .gitignore에 추가했습니다.

예상되는 코드 리뷰 난이도

🎯 3 (중간) | ⏱️ ~25 분

관련된 PR

  • dongsooop/AI#119: 두 PR 모두 LLM/OSS/service.py의 챗봇 도구 라우팅 흐름과 LLM/OSS/tools.py의 라우팅 결과를 직접 수정하므로 밀접한 연관이 있습니다.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 main change인 chatbot RAG 오케스트레이션 재구성과 관련되어 있으나, 구체적이지 않고 다소 일반적입니다.
Description check ✅ Passed PR 설명이 필수 섹션 중 '관련 이슈' 부분이 없으나, 배경(🎯 배경)과 주요 내용(🔍 주요 내용)은 충분히 작성되어 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/chatbot-rag-orchestration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
LLM/OSS/service.py (1)

404-441: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

OSS 빈 응답 경로에서 모델 호출이 중복될 수 있습니다.

mode=="oss"에서 grounded_fallback.text가 비어 있는 경우, Line 404에서 1회 호출 후 미해결 시 Line 430에서 빈 컨텍스트로 다시 호출될 수 있습니다. 동일 요청당 외부 호출이 최대 2회 발생해 지연/비용이 커집니다.

🔧 제안 수정
@@
         if output:
             latency = int((time.monotonic() - start) * 1000)
             _log_executor.submit(_log_chatbot, user_text, "oss", output, None, False, latency)
             return {"engine": "oss", "text": output}
+
+        if grounded_fallback is not None and not grounded_fallback.text.strip():
+            return cache_and_return({
+                "engine": "oss",
+                "text": "관련 근거를 충분히 확인하지 못했어요. 질문을 조금 더 구체적으로 다시 입력해 주세요.",
+            })
 
     fallback = grounded_fallback or run_final_fallback_tools(mode, user_text)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@LLM/OSS/service.py` around lines 404 - 441, The OSS path can call
call_oss_async twice for the same request when the first output is empty and
grounded_fallback.text is empty, causing duplicate external calls; fix by
tracking the initial OSS attempt (e.g., a flag like oss_attempted_output or
reusing the variable output) and skip the second fused call if the first OSS
call already ran for this user_text and produced an empty/unsuccessful result,
or only build the fused prompt when grounded_fallback.text (or fallback.text) is
non-empty; update logic around call_oss_async, grounded_fallback, fallback, and
the fused call to avoid making the second call for the same empty-context case.
🧹 Nitpick comments (1)
tests/regression/check_chatbot_tool_routing.py (1)

80-81: ⚡ Quick win

fallback reason 검증이 문구 결합에 과도하게 의존합니다.

현재는 "rag context" 문자열 포함 여부를 고정 검사해서, 의미는 같아도 문구만 바뀌면 테스트가 실패합니다. 이 케이스는 non-empty 또는 구조적 신호(llm_required, name) 중심으로 확인하는 편이 더 안정적입니다.

🔧 제안 수정
-    if "rag context" not in fallback.reason:
-        errors.append(f"fallback_reason_unexpected:{fallback.reason}")
+    if not fallback.reason:
+        errors.append("fallback_reason_missing")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/regression/check_chatbot_tool_routing.py` around lines 80 - 81, The
current test relies on a brittle substring match of fallback.reason ("rag
context"); instead, update the check to validate structural/non-empty signals:
verify that fallback.llm_required is truthy or fallback.name is a non-empty
string, or at minimum that fallback.reason is non-empty, and only append to
errors via errors.append(...) if none of those structural conditions hold;
target the fallback object attributes (fallback.reason, fallback.llm_required,
fallback.name) and replace the substring check with these resilience checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@LLM/OSS/service.py`:
- Around line 404-441: The OSS path can call call_oss_async twice for the same
request when the first output is empty and grounded_fallback.text is empty,
causing duplicate external calls; fix by tracking the initial OSS attempt (e.g.,
a flag like oss_attempted_output or reusing the variable output) and skip the
second fused call if the first OSS call already ran for this user_text and
produced an empty/unsuccessful result, or only build the fused prompt when
grounded_fallback.text (or fallback.text) is non-empty; update logic around
call_oss_async, grounded_fallback, fallback, and the fused call to avoid making
the second call for the same empty-context case.

---

Nitpick comments:
In `@tests/regression/check_chatbot_tool_routing.py`:
- Around line 80-81: The current test relies on a brittle substring match of
fallback.reason ("rag context"); instead, update the check to validate
structural/non-empty signals: verify that fallback.llm_required is truthy or
fallback.name is a non-empty string, or at minimum that fallback.reason is
non-empty, and only append to errors via errors.append(...) if none of those
structural conditions hold; target the fallback object attributes
(fallback.reason, fallback.llm_required, fallback.name) and replace the
substring check with these resilience checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a636c700-c28f-4f97-98c6-dd839032754f

📥 Commits

Reviewing files that changed from the base of the PR and between 7351767 and e249469.

📒 Files selected for processing (5)
  • .github/workflows/rag-light-check.yml
  • .gitignore
  • LLM/OSS/service.py
  • LLM/OSS/tools.py
  • tests/regression/check_chatbot_tool_routing.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
LLM/OSS/service.py (1)

404-429: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

빈 RAG 컨텍스트에서도 자유형 OSS 호출이 다시 열립니다.

Line 404에서 grounded_fallback.text가 비어 있어도 call_oss_async(messages_for_oss)를 먼저 호출합니다. LLM/OSS/tools.pyrun_final_fallback_tools()llm_required=Truerag_context_required를 빈 text로도 반환할 수 있어서, 현재 분기에서는 근거가 없는데도 OSS가 임의 답변을 생성할 수 있습니다. Line 425의 안내 응답은 OSS 출력도 비었을 때만 실행돼 방어가 되지 않습니다.

🔧 제안 수정
-        output = await call_oss_async(messages_for_oss)
+        if grounded_fallback is not None and not grounded_fallback.text.strip():
+            return cache_and_return({
+                "engine": "oss",
+                "text": "관련 근거를 충분히 확인하지 못했어요. 질문을 조금 더 구체적으로 다시 입력해 주세요.",
+            })
+
+        output = await call_oss_async(messages_for_oss)
         if not any(keyword in user_text for keyword in ("연락처", "전화", "번호", "문의")) and not any(keyword in user_text for keyword in COUNCIL_KWS):
             output = scrub_non_contact(output)
@@
-        if grounded_fallback is not None and not grounded_fallback.text.strip():
-            return cache_and_return({
-                "engine": "oss",
-                "text": "관련 근거를 충분히 확인하지 못했어요. 질문을 조금 더 구체적으로 다시 입력해주세요."
-            })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@LLM/OSS/service.py` around lines 404 - 429, The code calls
call_oss_async(messages_for_oss) before checking grounded_fallback, allowing OSS
to answer even when RAG context is empty; move the grounded_fallback check
earlier: run run_final_fallback_tools() (or otherwise obtain grounded_fallback)
before calling call_oss_async, and if grounded_fallback is not None and
grounded_fallback.text.strip() is false, immediately return the guidance
response via cache_and_return instead of invoking call_oss_async; update the
flow around messages_for_oss / call_oss_async and the subsequent empty-output
guard so OSS is only invoked when grounded_fallback permits generating free-form
responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@LLM/OSS/service.py`:
- Around line 404-429: The code calls call_oss_async(messages_for_oss) before
checking grounded_fallback, allowing OSS to answer even when RAG context is
empty; move the grounded_fallback check earlier: run run_final_fallback_tools()
(or otherwise obtain grounded_fallback) before calling call_oss_async, and if
grounded_fallback is not None and grounded_fallback.text.strip() is false,
immediately return the guidance response via cache_and_return instead of
invoking call_oss_async; update the flow around messages_for_oss /
call_oss_async and the subsequent empty-output guard so OSS is only invoked when
grounded_fallback permits generating free-form responses.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 85a8e6cf-d47a-47dd-ab42-b1913a407d0d

📥 Commits

Reviewing files that changed from the base of the PR and between e249469 and 199e633.

📒 Files selected for processing (1)
  • LLM/OSS/service.py

@Yu-JeSeung
Yu-JeSeung merged commit bea3a86 into main May 26, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant