왜 중복 호출이 문제인가
LLM 에이전트는 하나의 작업을 여러 스텝으로 쪼개 실행한다. 이 과정에서 같은 도구를 같은 인자로 반복 호출하는 일이 흔하다. 예를 들어 코드베이스를 분석하는 에이전트는 read_file("config.yaml")를 스텝마다 다시 호출하고, 리서치 에이전트는 동일한 검색어로 웹 검색을 반복한다. 모델이 이전 스텝의 결과를 잊거나, 계획을 재수립하면서 같은 정보를 다시 요구하기 때문이다.
이 중복은 세 가지 비용을 만든다. 첫째, 외부 API 호출 비용과 레이트리밋 소모. 둘째, 도구 실행 지연(특히 네트워크·DB 호출)이 전체 응답 시간을 늘린다. 셋째, 도구 결과가 다시 프롬프트에 실려 입력 토큰을 재차 소비한다. 특히 결정론적 도구(같은 입력→같은 출력)에서 중복 호출은 순수한 낭비다.
캐싱의 기본 구조
핵심은 (도구 이름 + 정규화된 인자)를 키로 하고 결과를 값으로 저장하는 것이다. 인자는 반드시 정규화해야 한다. JSON 키 순서, 공백, 기본값 생략 여부가 다르면 같은 의미라도 다른 키가 되어 캐시 적중률이 떨어진다.
import hashlib, json
def cache_key(tool_name: str, args: dict) -> str:
# 키 정렬로 인자 순서 차이를 흡수
normalized = json.dumps(args, sort_keys=True, separators=(",", ":"))
digest = hashlib.sha256(normalized.encode()).hexdigest()[:16]
return f"tool:{tool_name}:{digest}"
실행 래퍼로 감싸기
도구 호출 지점을 데코레이터나 래퍼로 감싸면 에이전트 로직을 건드리지 않고 캐싱을 넣을 수 있다. TTL을 두어 오래된 결과를 만료시키고, 결정론 여부에 따라 캐시 대상을 구분한다.
import time
class ToolCache:
def __init__(self, store): # store: redis 등
self.store = store
def call(self, tool, args, ttl=300, cacheable=True):
if not cacheable:
return tool.run(args)
key = cache_key(tool.name, args)
hit = self.store.get(key)
if hit is not None:
return json.loads(hit)
result = tool.run(args)
self.store.setex(key, ttl, json.dumps(result))
return result
부작용이 있는 도구(파일 쓰기, 메일 발송, 결제)는 cacheable=False로 반드시 제외한다. 읽기 전용·조회성 도구만 캐싱 대상으로 삼는 것이 원칙이다.
어떤 도구를 캐싱할 것인가
| 도구 유형 | 캐싱 여부 | 권장 TTL |
|---|---|---|
| 파일 읽기(로컬) | 가능 | 세션 단위 |
| 웹 검색·문서 조회 | 가능 | 수 분~수 시간 |
| DB 조회(읽기) | 조건부 | 짧게 |
| 쓰기·전송·결제 | 불가 | - |
DB 조회처럼 데이터가 바뀔 수 있는 경우는 TTL을 짧게 잡거나, 쓰기 발생 시 관련 키를 무효화하는 방식을 함께 쓴다.
범위(scope)와 무효화
캐시 범위를 정하는 것이 중요하다. 단일 실행 안에서만 유효한 세션 캐시는 안전하지만 재사용성이 낮다. 사용자·시간을 넘어 공유하는 전역 캐시는 적중률이 높은 대신, 데이터가 바뀌면 낡은 값을 줄 위험이 있다.
# 쓰기 도구 실행 시 관련 조회 키 무효화
def on_write(store, entity_id):
pattern = f"tool:get_entity:*{entity_id}*"
for key in store.scan_iter(pattern):
store.delete(key)
키에 사용자 ID나 권한 컨텍스트를 포함시키지 않으면, A 사용자의 결과가 B에게 노출되는 보안 사고가 날 수 있다. 권한이 개입하는 도구는 키에 주체를 반드시 넣는다.
주의점
- 비결정적 도구: 시각·랜덤·실시간 값을 반환하는 도구는 캐싱하면 오답을 만든다.
- 에러 캐싱 금지: 일시적 실패(타임아웃)를 캐싱하면 정상 복구 후에도 계속 실패를 반환한다. 성공 결과만 저장한다.
- 직렬화 크기: 대용량 결과를 그대로 캐싱하면 메모리와 네트워크가 오히려 병목이 된다. 크기 상한을 둔다.
- 관측: 적중률·미스율을 로깅해 TTL과 대상 도구를 조정한다.
정리
도구 결과 캐싱은 에이전트 코드를 크게 고치지 않고도 비용·지연·토큰을 동시에 줄이는 실용적 최적화다. 관건은 '무엇을 캐싱하지 않을지'를 먼저 정하는 것이다. 읽기 전용·결정론 도구만 대상으로 삼고, 인자 정규화·범위·무효화·에러 제외 네 가지를 지키면 안정적으로 중복 호출을 제거할 수 있다.