부제: 캐시를 살리려고 시스템 프롬프트를 통째로 고정한 이야기

이 프로젝트는 Gemini를 기준으로 만들었다. Gemini는 다른 모델과 다른 구석이 몇 있는데, 그중 캐싱과 시스템 프롬프트 다루는 방식이 설계를 꽤 바꿔놨다. 그 얘기다.
Gemini 캐시는 접두사로 동작한다
Gemini의 implicit caching은 요청의 앞에서부터(prefix) 같은 부분을 찾아 재사용한다. 순서는 대략 system_instruction → tools → 대화 내용 이다.
여기서 중요한 성질이 하나 있다. 앞쪽이 1바이트라도 바뀌면 그 뒤 전체가 캐시 미스다. 접두사 매칭이니 처음 달라지는 지점부터는 전부 새로 계산한다.
그러니 캐시를 살리고 싶으면 맨 앞의 system_instruction을 절대 건드리면 안 된다.
처음엔 시스템 프롬프트에 다 넣었다
초기엔 시스템 프롬프트 안에 현재 시각, 사용자 정보, 메모리, 요약을 다 넣었다. 구조상 깔끔해 보였다.
문제는 현재 시각이었다. 시각은 분 단위로 바뀐다. 이게 system_instruction 안에 들어 있으니 매 턴 앞부분이 달라졌고, 캐시는 매번 깨졌다. 적중률이 사실상 0이었다.
정리한 방식 — 앞은 고정, 동적인 건 뒤로
system_instruction을 100% 정적으로 고정했다. 모든 유저, 모든 턴에서 글자 하나까지 똑같은 텍스트다. 런타임 값 치환도 없앴다.
그럼 동적인 정보(시각·메모리·건강·요약)는 어디에 넣나. 사용자 메시지(HumanMessage) 안에 <context> 블록으로 넣었다.
<context>
<datetime>- current_time: 2026-04-27 11:35 (월)</datetime>
<memories>- [2026-03-28] 고혈압 약 복용 중</memories>
...
</context>
<user_message>
(사용자 실제 입력)
</user_message>
이러면 동적 정보가 바뀌어도 그 변화는 뒤쪽(대화 내용)에서 일어난다. 앞쪽 system_instruction은 그대로 남고, 접두사가 안전하다.
Gemini만의 특이한 지점 — 대화 중간에 system을 못 넣는다
그럼 동적 정보를 그때그때 system 메시지로 중간에 끼우면 안 되나? 다른 모델(OpenAI, Claude)에선 대화 도중에 system 역할 메시지를 넣을 수 있다. 그런데 Gemini는 그게 안 된다.
Gemini API는 대화 내용(contents)에 user와 model 두 역할만 둔다. system은 거기 끼는 역할이 아니다. 요청 맨 위의 별도 단일 필드(system_instruction)로 딱 하나만 존재한다. 그러니 대화 중간에 system을 끼워 넣을 자리가 아예 없다.
실제로 LangChain에서 Gemini에 system 메시지를 대화 중간에 넣으면 이런 에러를 만난다.
Message of 'system' type not supported by Gemini.
Please only provide it with Human or AI (user/assistant) messages.
이게 "동적 정보를 system이 아니라 HumanMessage에 넣는다"는 결정의 또 다른 이유였다. 캐시 때문만이 아니라 Gemini에선 애초에 중간 system이 들어갈 자리가 없다. 하나뿐인 system_instruction은 고정해 두는 게 여러모로 맞았다.
시각은 프롬프트가 아니라 도구로
현재 시각은 <context>에 매 턴 박을 수도 있었다. 그것마저 빼서 도구로 돌렸다. get_current_time이라는 도구를 두고 모델이 시간이 필요할 때(인사, 식사·수면·운동 조언, "지금/오늘" 같은 표현이 나올 때) 부르게 했다.
이유는 같다. 시각을 매 턴 텍스트로 박으면 그만큼 앞부분이 자주 바뀐다. 도구 호출은 필요할 때만 대화 흐름 안에서 일어나서 고정된 앞부분을 안 건드린다. 도구 한 번 부르는 비용은 작다. 그 대신 캐시를 지킨다.
결과
첫 턴이야 캐시가 없으니 0%다. 두세 턴만 지나면 적중률이 70~80%대로 올라와 그대로 유지됐다. 요약이나 메모리가 갱신되는 턴엔 앞부분이 바뀌어 잠깐 떨어지지만 다음 턴이면 다시 회복된다.
같은 정보를 어디에 두느냐(앞 고정 영역 vs 뒤 가변 영역)만 바꿨을 뿐인데 비용과 응답 속도가 같이 좋아졌다.
정리
- Gemini 캐시는 접두사 기반이라 앞쪽 system_instruction이 바뀌면 그 뒤 전체가 미스다
- 그래서 system_instruction은 100% 고정하고, 동적 정보는 HumanMessage의 <context>로 뺀다
- Gemini는 대화 중간에 system 역할을 못 넣는다 — system은 요청당 하나뿐인 별도 필드다
- 현재 시각처럼 자주 바뀌는 값은 프롬프트에 박지 말고 도구로 빼면 캐시가 보존된다
캐싱은 "무엇을 넣느냐"보다 "어디에 두느냐"의 문제에 가까웠다.
'개발 > AI' 카테고리의 다른 글
| [에이전트] ReAct 도구 순서 가드 read 후 write 강제 (0) | 2026.06.25 |
|---|---|
| [프롬프트] persona drift 대응 생성 지점 context 재주입 (0) | 2026.06.25 |
| [메모리] 추출 중복 차단 context 주입 단일 출처 (0) | 2026.06.25 |
| [메모리] 문맥 전환 감지 임베딩 유사도 한계 매턴 검색 회귀 (0) | 2026.06.25 |
| [Gemini] thought signature 보존 환각 도구 호출 방지 (0) | 2026.06.25 |