Skip to content

feat: add TTL-based response cache for chatbot - #88

Merged
Yu-JeSeung merged 3 commits into
mainfrom
feat/chatbot_cache
Apr 21, 2026
Merged

feat: add TTL-based response cache for chatbot#88
Yu-JeSeung merged 3 commits into
mainfrom
feat/chatbot_cache

Conversation

@Yu-JeSeung

@Yu-JeSeung Yu-JeSeung commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

관련 이슈

Close #87

🎯 배경

  • 챗봇에 간단한 응답을 캐시처리를 통해 LLM호출 감소

🔍 주요 내용

  • TTL 기반 cache 구현
  • 규정집 24시간 캐싱
  • 다른 응답 1시간 캐싱
  • oss 호출 캐싱 제외
  • 캐시에 저장 후 반환 헬퍼 생성 _cache_and_return()
  • THIRD_PARTY_LICENSES.md 목록 갱신

변경 요약(1~3줄)

응답에 TTL 기반 인메모리 캐시를 추가해 LLM 호출을 줄였습니다. 규정집(rule_book) 응답은 24시간, 그 외 응답은 1시간 캐시하며 OSS 호출 등 특정 모드는 캐시에서 제외됩니다.

주요 변경점

  • mode별 TTL 캐시 도입: rule_book = 24시간, 기타 캐시 모드 = 1시간 (in-memory TTLCache)
  • 캐시 키 개선: "{mode}:{optional_relative_date_scope}:{sha256(정규화된 사용자 텍스트)}" 형태로 해시 기반 키 생성(공백 정규화 및 일정성 질의의 날짜 범위 포함)
  • 캐시 제외(_CACHE_SKIP) 모드 추가: oss, greet, whoami, relation, guard 등은 캐시 조회/저장 모두 우회
  • 스레드 안전성 확보를 위해 전역 threading.Lock 사용 및 캐시 조회 시 복사 반환
  • _cache_and_return() 헬퍼로 캐시 저장·반환 로직 중앙화
  • THIRD_PARTY_LICENSES.md에 aiofiles, cachetools, langgraph 항목 추가

주의/리스크

  • 인메모리 캐시로 인해 메모리 사용 증가 가능 — 용량 및 만료 동작 모니터링 필요
  • 일부 OSS 관련 분기(내부 fast 반환 등)가 캐시 우회 경로를 가질 수 있어 동작 일관성 확인 필요

다음 액션

  • 통합 테스트로 캐시 히트/미스, 우회 경로 및 동시성 동작 검증
  • 프로덕션에서 캐시 히트율·메모리 영향 관찰 및 필요 시 캐시 전략 조정

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ea264891-1daa-4f93-8bcd-81fedde683a9

📥 Commits

Reviewing files that changed from the base of the PR and between 627ca6c and e5c22b0.

📒 Files selected for processing (1)
  • LLM/OSS/Open_AI_OSS.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • LLM/OSS/Open_AI_OSS.py

📝 Walkthrough

Walkthrough

chat 함수에 입력 정규화(공백 정리)를 기반으로 키를 만들어 인메모리 TTL 캐싱을 도입했습니다. 캐시는 rule_book 응답용(24시간)과 기타 모드용(1시간) 두 인스턴스로 구분되며, 캐시 접근은 전역 threading.Lock으로 동기화됩니다. 일부 모드(oss, greet, whoami, relation, guard)는 캐시를 읽거나 쓰지 않습니다.

Changes

Cohort / File(s) Summary
Response Caching Implementation
LLM/OSS/Open_AI_OSS.py
cachetools.TTLCache 기반 인메모리 캐시 추가: rule_book용(24h)과 기타용(1h) 두 인스턴스. 캐시 키는 "{mode}:{optional_relative_date_scope}:{sha256(normalized_user_text)}" 형태로 생성(입력은 공백 정규화). _CACHE_SKIP 모드 집합(oss,greet,whoami,relation,guard)은 캐시를 우회. 전역 threading.Lock으로 get/set 동기화, 캐시 히트 시 복사본 조기 반환, _cache_and_return(resp) 헬퍼로 캐시 저장 중앙화.
Dependency Documentation
THIRD_PARTY_LICENSES.md
aiofiles (v23.2.1, Apache Software License), cachetools (v5.3.0, MIT License), langgraph (v0.2.0, MIT License) 항목 3건 추가.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Chat as ChatFunction
  participant Cache
  participant Mode as ModeHandler/LLM
  User->>Chat: 요청(모드, 텍스트)
  Chat->>Cache: 키 생성 및 조회 (with Lock)
  Cache-->>Chat: 캐시 히트? (있음/없음)
  alt 캐시 히트
    Chat-->>User: 캐시된 응답 복사 반환
  else 캐시 미스
    Chat->>Mode: 모드별 처리 / LLM 호출
    Mode-->>Chat: 응답
    Chat->>Cache: 응답 저장 (with Lock)  <!-- 저장 시 TTL 적용 -->
    Chat-->>User: 응답 반환
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 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 제목은 TTL 기반 응답 캐시 추가라는 주요 변경 사항을 명확하고 간결하게 요약합니다.
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 feat/chatbot_cache

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
LLM/OSS/Open_AI_OSS.py (1)

22-25: 캐시 TTL/maxsize를 환경변수로 외부화하는 것을 권장합니다.

현재 maxsizettl이 하드코딩되어 있어 운영 중 트래픽/메모리 특성에 맞춰 튜닝하려면 재배포가 필요합니다. 규정집 갱신 이벤트가 있을 때 TTL을 짧게 바꾸거나, 메모리 압박 시 maxsize를 줄이는 시나리오에서 환경변수 기반이면 운영 편의성이 크게 좋아집니다.

♻️ 제안 예시
-# rule_book: 24시간 / 그 외 모드: 1시간
-_CACHE_RULE_BOOK: TTLCache = TTLCache(maxsize=200, ttl=86400)
-_CACHE_GENERAL:   TTLCache = TTLCache(maxsize=500, ttl=3600)
-_cache_lock = threading.Lock()
+# rule_book: 24시간 / 그 외 모드: 1시간 (환경변수로 오버라이드 가능)
+_RB_MAXSIZE = int(os.getenv("CHAT_CACHE_RULE_BOOK_MAXSIZE", "200"))
+_RB_TTL     = int(os.getenv("CHAT_CACHE_RULE_BOOK_TTL", "86400"))
+_GEN_MAXSIZE = int(os.getenv("CHAT_CACHE_GENERAL_MAXSIZE", "500"))
+_GEN_TTL     = int(os.getenv("CHAT_CACHE_GENERAL_TTL", "3600"))
+_CACHE_RULE_BOOK: TTLCache = TTLCache(maxsize=_RB_MAXSIZE, ttl=_RB_TTL)
+_CACHE_GENERAL:   TTLCache = TTLCache(maxsize=_GEN_MAXSIZE, ttl=_GEN_TTL)
+_cache_lock = threading.Lock()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@LLM/OSS/Open_AI_OSS.py` around lines 22 - 25, The hardcoded TTLCache
parameters (_CACHE_RULE_BOOK and _CACHE_GENERAL) should be made configurable via
environment variables: read and parse integer env vars (e.g.,
RULE_BOOK_CACHE_TTL, RULE_BOOK_CACHE_MAXSIZE, GENERAL_CACHE_TTL,
GENERAL_CACHE_MAXSIZE) with sensible defaults (86400/200 and 3600/500) before
creating TTLCache instances; validate/fallback on parse errors and then
instantiate TTLCache(maxsize=..., ttl=...) for _CACHE_RULE_BOOK and
_CACHE_GENERAL respectively (leave _cache_lock as-is), and update any inline
comment to note the env var names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@LLM/OSS/Open_AI_OSS.py`:
- Around line 833-849: The cached dict is returned by reference which risks
cache pollution; change the logic around
_CACHE_SKIP/_cache_key/_cached/_cache_and_return so you (1) promote _CACHE_SKIP
to a module-level constant, (2) normalize user_text before building _cache_key
(e.g., strip and collapse whitespace) to improve hit rate, and (3) return a
shallow copy of cached dicts instead of the stored object and also store a
shallow copy when setting the cache (use the same copy semantics for both
_CACHE_RULE_BOOK and _CACHE_GENERAL under _cache_lock) so callers cannot mutate
the cached entry.

---

Nitpick comments:
In `@LLM/OSS/Open_AI_OSS.py`:
- Around line 22-25: The hardcoded TTLCache parameters (_CACHE_RULE_BOOK and
_CACHE_GENERAL) should be made configurable via environment variables: read and
parse integer env vars (e.g., RULE_BOOK_CACHE_TTL, RULE_BOOK_CACHE_MAXSIZE,
GENERAL_CACHE_TTL, GENERAL_CACHE_MAXSIZE) with sensible defaults (86400/200 and
3600/500) before creating TTLCache instances; validate/fallback on parse errors
and then instantiate TTLCache(maxsize=..., ttl=...) for _CACHE_RULE_BOOK and
_CACHE_GENERAL respectively (leave _cache_lock as-is), and update any inline
comment to note the env var names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d9b5331f-f885-4f82-9136-f675435d64d6

📥 Commits

Reviewing files that changed from the base of the PR and between 28d28ef and 5f5407a.

📒 Files selected for processing (2)
  • LLM/OSS/Open_AI_OSS.py
  • THIRD_PARTY_LICENSES.md

Comment thread LLM/OSS/Open_AI_OSS.py Outdated
Comment on lines +833 to +849
# oss 모드는 대화 히스토리 의존 → 캐시 제외
# greet/whoami/relation 은 고정 문자열 → 캐시 불필요
_CACHE_SKIP = {"oss", "greet", "whoami", "relation", "guard"}
if mode not in _CACHE_SKIP:
_cache = _CACHE_RULE_BOOK if mode == "rule_book" else _CACHE_GENERAL
_cache_key = f"{mode}:{user_text}"
with _cache_lock:
_cached = _cache.get(_cache_key)
if _cached is not None:
return _cached

def _cache_and_return(resp: dict) -> dict:
if mode not in _CACHE_SKIP:
_c = _CACHE_RULE_BOOK if mode == "rule_book" else _CACHE_GENERAL
with _cache_lock:
_c[_cache_key] = resp
return resp

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

캐시 히트 시 dict 참조를 그대로 반환하면 캐시 오염 위험이 있습니다.

Line 842 의 return _cached 는 캐시에 저장된 동일 객체를 그대로 돌려줍니다. 호출 체인 어딘가(미들웨어, 추후 리팩터에서 resp["url"] = ... 같은 후처리가 추가되는 경우 등)에서 응답 dict 를 변형하면, 이후 같은 키로 히트한 모든 요청이 변형된 값을 받게 되어 재현하기 어려운 버그의 원인이 됩니다. 방어적으로 얕은 복사본을 반환하는 편이 안전합니다.

부가로, _CACHE_SKIP 은 요청마다 재생성할 필요가 없으므로 모듈 상수로 올리는 것을 권장합니다. 또한 _cache_key = f"{mode}:{user_text}" 는 공백·트레일링 기호 차이로 키가 달라져 히트율이 떨어지므로, 간단한 정규화(예: user_text.strip() + 다중 공백 축약)를 추가하면 효과가 큽니다.

🛡️ 제안 diff
-# rule_book: 24시간 / 그 외 모드: 1시간
-_CACHE_RULE_BOOK: TTLCache = TTLCache(maxsize=200, ttl=86400)
-_CACHE_GENERAL:   TTLCache = TTLCache(maxsize=500, ttl=3600)
-_cache_lock = threading.Lock()
+# rule_book: 24시간 / 그 외 모드: 1시간
+_CACHE_RULE_BOOK: TTLCache = TTLCache(maxsize=200, ttl=86400)
+_CACHE_GENERAL:   TTLCache = TTLCache(maxsize=500, ttl=3600)
+_cache_lock = threading.Lock()
+_CACHE_SKIP = frozenset({"oss", "greet", "whoami", "relation", "guard"})
-    # oss 모드는 대화 히스토리 의존 → 캐시 제외
-    # greet/whoami/relation 은 고정 문자열 → 캐시 불필요
-    _CACHE_SKIP = {"oss", "greet", "whoami", "relation", "guard"}
-    if mode not in _CACHE_SKIP:
-        _cache = _CACHE_RULE_BOOK if mode == "rule_book" else _CACHE_GENERAL
-        _cache_key = f"{mode}:{user_text}"
-        with _cache_lock:
-            _cached = _cache.get(_cache_key)
-        if _cached is not None:
-            return _cached
-
-    def _cache_and_return(resp: dict) -> dict:
-        if mode not in _CACHE_SKIP:
-            _c = _CACHE_RULE_BOOK if mode == "rule_book" else _CACHE_GENERAL
-            with _cache_lock:
-                _c[_cache_key] = resp
-        return resp
+    # oss 모드는 대화 히스토리 의존 → 캐시 제외
+    # greet/whoami/relation 은 고정 문자열 → 캐시 불필요
+    _cache_key = f"{mode}:{re.sub(r'\s+', ' ', (user_text or '').strip())}"
+    if mode not in _CACHE_SKIP:
+        _cache = _CACHE_RULE_BOOK if mode == "rule_book" else _CACHE_GENERAL
+        with _cache_lock:
+            _cached = _cache.get(_cache_key)
+        if _cached is not None:
+            return dict(_cached)  # 참조 공유로 인한 캐시 오염 방지
+
+    def _cache_and_return(resp: dict) -> dict:
+        if mode not in _CACHE_SKIP:
+            _c = _CACHE_RULE_BOOK if mode == "rule_book" else _CACHE_GENERAL
+            with _cache_lock:
+                _c[_cache_key] = dict(resp)
+        return resp
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@LLM/OSS/Open_AI_OSS.py` around lines 833 - 849, The cached dict is returned
by reference which risks cache pollution; change the logic around
_CACHE_SKIP/_cache_key/_cached/_cache_and_return so you (1) promote _CACHE_SKIP
to a module-level constant, (2) normalize user_text before building _cache_key
(e.g., strip and collapse whitespace) to improve hit rate, and (3) return a
shallow copy of cached dicts instead of the stored object and also store a
shallow copy when setting the cache (use the same copy semantics for both
_CACHE_RULE_BOOK and _CACHE_GENERAL under _cache_lock) so callers cannot mutate
the cached entry.

@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.

Actionable comments posted: 2

♻️ Duplicate comments (1)
LLM/OSS/Open_AI_OSS.py (1)

845-850: ⚠️ Potential issue | 🟡 Minor

캐시에 저장할 때도 응답 dict를 복사해 주세요.

Line 849에서 저장한 resp와 반환한 resp가 같은 객체라서, 반환 후 객체가 변형되면 캐시 엔트리도 오염될 수 있습니다.

🛡️ 제안 diff
             _c = _CACHE_RULE_BOOK if mode == "rule_book" else _CACHE_GENERAL
             with _cache_lock:
-                _c[_cache_key] = resp
+                _c[_cache_key] = dict(resp)
         return resp
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@LLM/OSS/Open_AI_OSS.py` around lines 845 - 850, The function
_cache_and_return currently stores the same resp object into the cache so later
mutations to the returned dict will corrupt the cached entry; fix it by storing
a deep copy of resp under _c[_cache_key] (e.g., use copy.deepcopy(resp)) while
still returning the original resp, and ensure copy is imported (add import copy
at top) and the assignment happens inside the existing with _cache_lock block
that uses _CACHE_RULE_BOOK/_CACHE_GENERAL and _cache_key (skip caching when mode
in _CACHE_SKIP).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@LLM/OSS/Open_AI_OSS.py`:
- Around line 836-837: The cache key currently stores the entire normalized
user_text in _cache_key which can leak large/sensitive inputs and cause stale
results for relative-date queries; change this to compute a fixed-size hash
(e.g., SHA256) of _normalized and combine it with a date-scope token (e.g.,
current ISO date, ISO week, or month depending on required granularity) and the
mode so the key uses symbols _normalized, _cache_key, user_text, and mode;
ensure the hash is used instead of raw text and the date-scope is generated at
runtime so relative-time queries expire at the desired boundary.
- Line 875: Replace the single-line if statements that assign url into the resp
dict with a proper block form; specifically find occurrences of the pattern if
url: resp["url"] = url (variables resp and url) and change them to an indented
block form (if url: newline with an indented resp["url"] = url) at each
occurrence (the occurrences reported around the if url usage in this file).

---

Duplicate comments:
In `@LLM/OSS/Open_AI_OSS.py`:
- Around line 845-850: The function _cache_and_return currently stores the same
resp object into the cache so later mutations to the returned dict will corrupt
the cached entry; fix it by storing a deep copy of resp under _c[_cache_key]
(e.g., use copy.deepcopy(resp)) while still returning the original resp, and
ensure copy is imported (add import copy at top) and the assignment happens
inside the existing with _cache_lock block that uses
_CACHE_RULE_BOOK/_CACHE_GENERAL and _cache_key (skip caching when mode in
_CACHE_SKIP).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5f7a0737-a8d1-4862-8d09-64b2f6384310

📥 Commits

Reviewing files that changed from the base of the PR and between 5f5407a and 627ca6c.

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

Comment thread LLM/OSS/Open_AI_OSS.py Outdated
Comment thread LLM/OSS/Open_AI_OSS.py Outdated
@Yu-JeSeung
Yu-JeSeung merged commit 0594df7 into main Apr 21, 2026
1 check passed
@Yu-JeSeung
Yu-JeSeung deleted the feat/chatbot_cache branch April 21, 2026 03:03
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.

[FEAT] 챗봇 Cache 처리

1 participant