(4/4) 컨텍스트가 꽉 찰 때 — 압축·정리·요약과 장기 기억
🧠 AWS Bedrock 챗봇 — 대화 맥락 유지 설계 4부작 — Bedrock 으로 챗봇을 만들면 가장 먼저 부딪히는 벽이 “모델이 대화를 하나도 기억하지 못한다” 예요. API 가 스테이트리스라서 맥락은 전부 우리가 들고 다녀야 하죠. 이 시리즈는 그 맥락을 어디에 쌓고(저장), 어떻게 싸게 다시 보내고(캐싱), 넘칠 때 어떻게 줄이는지(압축) 를 순서대로 설계합니다. 전체 그림과 Bedrock 특유의 제약을 먼저 잡고(1편), DynamoDB 세션 스토어와 메시지 스키마를 짜고(2편), 프롬프트 캐싱으로 재전송 비용을 걷어내고(3편), 컨텍스트 한계에서 압축·요약·장기기억으로 버티는 것(4편)까지 이어집니다. 전체 4편.
- 대화 맥락은 어떻게 유지하나 — 전체 설계도
- 대화 저장소 — DynamoDB 세션 스토어와 메시지 스키마
- 프롬프트 캐싱 — 같은 히스토리를 매번 보내면서 돈 아끼기
- 컨텍스트가 꽉 찰 때 — 압축·정리·요약과 장기 기억 ← 지금 글
Summary
3편에서 프롬프트 캐싱으로 비용 곡선을 눌렀어요. 그런데 캐싱은 “다시 계산하는 값”을 싸게 만들어줄 뿐, 프롬프트를 작게 만들어주지는 않아요. 히스토리는 여전히 계속 자랍니다.
그러면 언제 문제가 되나요? 많은 분들이 “컨텍스트 창이 찰 때”라고 생각하시는데, 실무에서는 그보다 훨씬 먼저 아파와요.
창이 차기 전에 먼저 오는 것들
→ 첫 토큰까지의 지연이 늘어남 (프롬프트가 클수록 프리필이 오래 걸려요)
→ 캐시 미적중 한 번의 타격이 커짐
→ 모델이 오래된 무관한 내용에 끌려다니기 시작함
마지막 항목이 특히 얄궂어요. 히스토리가 길어지면 품질이 떨어지는 방향으로 움직입니다. 그래서 “넘치면 어떻게 하나”가 아니라 “적당한 크기로 계속 유지하려면 어떻게 하나” 가 진짜 질문이에요.
이번 마지막 편은 그 도구 네 개를 비교하고, 어느 상황에 무엇을 쓸지 정합니다.
💡 이 글에서 다루는 것
- 줄이는 네 가지 방법 비교 — 자르기 · 정리 · 압축 · 요약
- 압축(compaction) 붙이는 법과 상태를 잃어버리는 함정
- 컨텍스트 편집으로 낡은 도구 결과 비우기
- 압축·정리가 프롬프트 캐싱과 충돌하는 지점
- 직접 요약 윈도잉 — 통제가 필요할 때
- 세션을 넘어가는 장기 기억 설계
stop_reason처리와 토큰 사전 계측- 운영 체크리스트와 시리즈 정리
1. 줄이는 네 가지 방법
먼저 전체 지도예요. 이름이 비슷해서 헷갈리기 쉬운데, 하는 일이 서로 다릅니다.
| 방법 | 하는 일 | 잃는 것 |
|---|---|---|
| 자르기 | 오래된 턴을 버림 |
버린 내용 전부 |
| 정리 | 낡은 도구 결과· 사고를 비움 |
도구 출력 세부 |
| 압축 | 서버가 요약해 접어둠 |
요약 과정의 디테일 |
| 요약 | 우리가 요약해 다시 넣음 |
요약 품질에 달림 |
성격을 한 줄로 구분하면 이래요.
- 자르기(windowing) 는 그냥 버려요. 구현이 제일 쉽고, 잊는 것도 제일 확실해요.
- 정리(context editing) 는 대화 구조는 남기고 부피가 큰 부속물만 비워요. 도구를 많이 쓰는 챗봇에서 효과가 큽니다.
- 압축(compaction) 은 서버가 앞부분을 요약해서 압축 블록으로 바꿔줘요. 대화의 흐름은 남습니다.
- 요약(직접) 은 우리가 요약을 만들어 관리해요. 손이 많이 가지만 스키마와 시점을 우리가 통제할 수 있어요.
고르는 기준은 이렇게 잡으면 돼요.
| 상황 | 권하는 방법 |
|---|---|
| 짧은 Q&A 봇, 이전 맥락 거의 불필요 |
자르기 |
| 도구를 많이 쓰는 에이전트형 |
정리 |
| 긴 상담·코칭처럼 흐름이 중요 |
압축 |
| 요약 스키마를 우리가 정해야 함 |
직접 요약 |
| 세션이 끝나도 남아야 하는 사실 |
장기 기억 |
⚠️ 이 넷은 배타적이지 않아요. 실무에서는 정리 + 압축을 같이 쓰거나, 압축 + 장기 기억을 조합하는 게 보통이에요. 다만 처음부터 다 붙이지는 마세요. 계측(3편)을 켜두고 실제로 어디가 부풀고 있는지 본 다음에 하나씩 넣는 게 맞습니다.
2. 자르기 — 가장 단순한 시작점
2편에서 이미 절반은 만들었어요. trim_to_valid_window 로 최근 N턴만 남기면서 도구 짝을 지키는 함수였죠. 토큰 기준으로 자르는 버전은 이렇게 됩니다.
def trim_by_tokens(messages: list, system: list, budget: int = 60_000) -> list:
"""토큰 예산에 맞을 때까지 앞에서부터 잘라냅니다."""
while messages:
n = client.messages.count_tokens(
model="anthropic.claude-opus-5",
system=system,
messages=messages,
).input_tokens
if n <= budget:
return messages
# 안전한 지점까지 앞에서 덜어냄 (도구 짝 유지)
messages = trim_to_valid_window(messages, len(messages) - 2)
return messages
토큰 카운팅은 Bedrock 에서도 쓸 수 있어요. 추정하지 말고 이 API 로 재세요. 다른 토크나이저(예: OpenAI 계열용)로 어림하면 한국어에서 특히 크게 틀립니다.
자르기의 장단점은 명확해요.
- 장점 — 구현이 단순하고, 동작이 예측 가능하고, 비용 상한이 확실해요.
- 단점 — 자른 건 진짜로 없어져요. “아까 말한 그 조건으로 다시” 같은 요청에서 봇이 멍청해집니다.
그래서 자르기는 짧은 Q&A 나 각 질문이 독립적인 봇에는 정답에 가깝고, 상담형 대화에는 부족해요.
3. 정리 — 낡은 도구 결과 비우기
도구를 쓰는 챗봇의 히스토리를 열어보면 재미있는 사실을 알게 돼요. 부피의 대부분이 도구 결과예요. 사용자와 봇이 주고받은 말은 얼마 안 되고, DB 조회 결과나 API 응답 JSON 이 자리를 다 차지하고 있습니다.
그리고 그 결과들은 대개 한 번 쓰고 나면 다시 안 봐요. 20턴 전에 조회한 재고 수량을 지금 다시 볼 일은 없죠.
컨텍스트 편집이 이걸 정리해줘요. 요약하는 게 아니라 비웁니다.
response = client.beta.messages.create(
model="anthropic.claude-opus-5",
max_tokens=16000,
betas=["context-management-2025-06-27"],
context_management={
"edits": [
{"type": "clear_tool_uses_20250919"}, # 낡은 도구 결과 비우기
{"type": "clear_thinking_20251015"}, # 낡은 사고 블록 비우기
]
},
system=SYSTEM_BLOCKS,
tools=TOOLS,
messages=messages,
)
clear_tool_uses_20250919— 오래된 도구 결과를 비웁니다. 옵션으로clear_tool_inputs: true를 주면 도구 호출 인자까지 비워요.clear_thinking_20251015— 오래된 사고 블록을 비웁니다.
⚠️ 여기서 이름이 비슷해서 자주 헷갈리는 게 있어요.
context_management라는 같은 파라미터를 쓰지만, 컨텍스트 편집과 압축은 서로 다른 기능이고 베타 헤더도 다릅니다. 편집은context-management-2025-06-27+clear_*타입이고, 압축은compact-2026-01-12+compact_20260112타입이에요. 헤더와 타입을 섞으면 요청이 거절돼요.
정리는 에이전트형 챗봇에서 가성비가 가장 좋아요. 대화 구조를 그대로 두면서 부피만 빠지니까, 모델이 흐름을 잃지 않습니다. 도구를 안 쓰는 순수 대화형 봇에는 별 효과가 없어요.
4. 압축 — 서버가 요약해서 접어주기
대화 자체가 길어지는 경우에는 압축이 답이에요. 히스토리가 임계값에 가까워지면 서버가 앞부분을 요약해서 압축 블록 하나로 바꿔 돌려줍니다.
response = client.beta.messages.create(
model="anthropic.claude-opus-5",
max_tokens=16000,
betas=["compact-2026-01-12"],
context_management={"edits": [{"type": "compact_20260112"}]},
system=SYSTEM_BLOCKS,
messages=messages,
)
# ★★★ 여기가 핵심 — content 를 통째로 저장해야 합니다
append_message(session_id, "assistant", response.content)
4-1. 압축 블록을 잃어버리는 함정
이게 압축에서 가장 자주, 가장 조용히 터지는 문제예요. 2편에서 “텍스트만 뽑아 저장하지 마세요” 라고 했던 이유가 여기서 제일 크게 드러납니다.
# ❌ 이러면 압축이 매 턴 처음부터 다시 일어나요
text = "".join(b.text for b in response.content if b.type == "text")
append_message(session_id, "assistant", text)
압축 결과는 응답 content 안의 압축 블록에 담겨 있어요. 다음 요청에 그 블록을 다시 넣어주면, API 가 “아, 이 앞은 이미 요약됐구나” 하고 원본 히스토리를 요구하지 않습니다. 그런데 텍스트만 뽑아 저장하면 그 블록이 사라져요.
증상이 특이해서 원인 짐작이 어려워요.
증상: 압축을 켰는데 프롬프트가 안 줄어든다
+ 매 턴 압축이 다시 일어나는 것처럼 느려진다
+ 에러는 안 난다
원인: 압축 블록을 저장하지 않아서 상태가 매번 초기화됨
해법은 하나예요. response.content 를 그대로 저장하고, 그대로 다시 넣으세요. 우리가 해석할 필요가 없어요.
4-2. 압축은 캐시를 깹니다
이건 트레이드오프라서 미리 알고 있어야 해요.
압축이 일어나면 히스토리 앞부분이 다른 내용으로 교체돼요. 3편의 규칙을 떠올려보면 — prefix 가 바뀌었으니 그 뒤 캐시가 전부 무효입니다.
압축 전: [system|경계] 턴1 턴2 ... 턴40 [경계] ← 캐시 잘 먹고 있음
압축 후: [system|경계] [압축블록] 턴38 턴39 턴40 ← 캐시 다시 써야 함
그래서 압축이 일어나는 턴은 비용이 한 번 튀어요. 대신 그 다음 턴부터는 훨씬 작은 프롬프트로 돌아가니까, 몇 턴만 지나면 회수됩니다.
여기서 나오는 운영 원칙이 있어요.
✅ 압축은 자주 일어나면 안 돼요. 임계값을 너무 낮게 잡으면 캐시를 계속 깨면서 요약 비용까지 내는 최악의 조합이 됩니다. 기본 임계값(약 15만 토큰 근처)을 크게 낮추기 전에, 정리(3절)나 자르기로 먼저 부피를 줄일 수 있는지 보세요.
5. 직접 요약 윈도잉 — 통제가 필요할 때
압축이 편한데도 직접 요약을 짜야 하는 경우가 있어요.
- 베타 기능을 운영에 넣을 수 없는 조직 정책일 때
- 요약에 꼭 남아야 하는 항목이 정해져 있을 때 (예: 상담이면 고객 요구사항·약속한 조건·미해결 이슈)
- 요약 내용을 화면에도 보여주거나 감사 로그로 남겨야 할 때
- 요약을 더 싼 모델로 돌려서 비용을 아끼고 싶을 때
구조는 이렇게 잡습니다. “요약 노트 + 최근 원문 N턴” 이에요.
[ system (고정) ] ← 경계 ①
[ 사용자턴: <대화요약> ... </대화요약> ] ← 요약 노트 (갱신될 때만 변함)
[ 최근 N턴 원문 ] ← 경계 ②
요약 노트를 사용자 턴 안에 넣는 이유는 3편과 같아요. 시스템 프롬프트를 고치면 캐시가 통째로 날아가지만, 대화 앞쪽 사용자 턴에 두면 그 뒤만 영향을 받습니다.
SUMMARY_PROMPT = """아래 대화를 다음 항목으로 정리하세요. 없으면 '없음'.
## 사용자에 대해 확인된 사실
## 사용자가 요청한 것
## 합의되거나 결정된 것
## 아직 해결되지 않은 것
## 다음에 이어서 할 것
추측은 쓰지 마세요. 대화에 나온 내용만 쓰세요."""
def make_summary(old_messages: list, prev_summary: str | None) -> str:
"""오래된 턴들을 요약 노트로 접습니다."""
parts = []
if prev_summary:
parts.append({"type": "text",
"text": f"[기존 요약]\n{prev_summary}"})
parts.append({"type": "text",
"text": "[추가 대화]\n" + render_plain(old_messages)})
res = client.messages.create(
model="anthropic.claude-haiku-4-5", # 요약은 저렴한 모델로 충분
max_tokens=2000,
system=[{"type": "text", "text": SUMMARY_PROMPT}],
messages=[{"role": "user", "content": parts}],
)
return "".join(b.text for b in res.content if b.type == "text")
def build_with_summary(session_id: str, user_blocks: list,
keep: int = 8) -> list:
history = load_messages(session_id)
summary = load_summary(session_id)
if len(history) > keep * 2:
fold, history = history[:-keep], history[-keep:]
# 도구 짝이 깨지지 않는 지점으로 보정
history = trim_to_valid_window(history, len(history))
summary = make_summary(fold, summary)
save_summary(session_id, summary)
messages = []
if summary:
messages.append({
"role": "user",
"content": [{"type": "text",
"text": f"<대화요약>\n{summary}\n</대화요약>"}],
})
messages.append({
"role": "assistant",
"content": [{"type": "text", "text": "요약을 확인했습니다."}],
})
messages += history
messages.append({"role": "user", "content": user_blocks})
return messages
몇 가지 설계 선택을 짚어둘게요.
- 요약 프롬프트에 항목을 못 박아두세요. “요약해줘” 라고만 하면 매번 다른 모양이 나와서, 다음 요약에서 정보가 조금씩 유실돼요. 항목을 고정하면 누적 요약이 안정됩니다.
- 기존 요약을 입력에 넣어요. 매번 처음부터 요약하면 비싸고, 이미 접힌 내용을 다시 볼 수도 없어요. “기존 요약 + 새로 접을 대화” 를 합쳐 갱신하는 방식이 맞습니다.
- 요약 뒤에 짧은 어시스턴트 턴을 넣어요. 사용자 턴이 연달아 오는 게 문제가 되지는 않지만, 요약이 “사용자가 한 말”로 읽히지 않게 구분해주는 효과가 있어요.
- 요약은 싼 모델로 돌려요. 요약은 창작이 아니라 정리라서, 상위 모델을 쓸 필요가 거의 없어요.
- 요약 갱신 시점을 드물게. 매 턴 갱신하면 요약 노트가 계속 바뀌면서 캐시를 계속 깨요.
keep * 2처럼 여유를 두고 뭉텅이로 접으세요.
6. 장기 기억 — 세션을 넘어서
지금까지는 한 세션 안의 이야기였어요. 그런데 챗봇에 조금 쓰다 보면 이런 요구가 옵니다.
"어제 말한 그 프로젝트 있잖아, 거기에 이어서"
"매번 존댓말 쓰지 말라고 했는데"
"내 팀은 3명이라고 아까 말했는데 또 물어보네"
세션 히스토리로는 못 풀어요. 어제 세션은 이미 끝났으니까요. 그렇다고 사용자의 모든 과거 대화를 프롬프트에 넣을 수도 없죠.
여기서 원칙은 하나예요.
히스토리를 들고 다니지 말고, 사실만 밖에 저장해두고 필요할 때 꺼내 쓰세요.
6-1. 저장 — 요약 카드
세션이 끝날 때(또는 유휴 상태가 되면) 다음 세션에 쓸 만한 것만 추출해서 저장합니다.
MEMORY_PROMPT = """이 대화에서 '다음 대화에도 유용할 사실'만 뽑으세요.
포함할 것: 사용자의 지속적 선호, 역할·소속, 진행 중인 일,
반복해서 다룰 대상의 고유 정보
제외할 것: 이번 대화에서만 의미 있는 내용, 추측, 일시적 상태
비밀 취급: 자격증명·토큰·주민번호 등은 절대 기록하지 마세요.
각 항목은 한 줄로, 근거가 되는 사용자 발언과 함께 쓰세요.
없으면 빈 목록을 반환하세요."""
저장 위치는 세션 스토어와 분리해요. 수명이 다르니까요.
| 항목 | 값 |
|---|---|
| PK | USER#<uid> |
| SK | MEM#<카테고리>#<id> |
| 속성 | fact · source_session· confidence · updated_at |
TTL 은 세션보다 훨씬 길게 두거나 안 두고, 대신 사용자가 지우거나 수정할 수 있는 경로를 반드시 만들어두세요. 잘못 기록된 사실이 영구히 따라다니는 게 장기 기억의 최대 리스크예요.
6-2. 주입 — 필요할 때만, 뒤쪽에
세션을 시작할 때 관련 기억을 골라 프롬프트에 넣습니다. 어디에 넣느냐가 중요해요.
def build_first_turn(user_id: str, user_text: str) -> list:
facts = search_memory(user_id, query=user_text, limit=8)
blocks = []
if facts:
lines = "\n".join(f"- {f['fact']}" for f in facts)
blocks.append({
"type": "text",
"text": ("<사용자기억>\n"
"이전 대화에서 확인된 사실입니다. 틀렸다고 하면 "
"사용자 말을 따르세요.\n"
f"{lines}\n</사용자기억>"),
})
blocks.append({"type": "text", "text": user_text})
return blocks
- 시스템 프롬프트에 넣지 마세요. 사용자마다 다른 내용이 시스템에 들어가면, 3편에서 봤듯이 사용자별로 캐시가 갈라져서 공용 prefix 재사용이 사라져요. 시스템은 모든 사용자에게 동일하게 유지하는 게 캐싱 관점에서 훨씬 유리합니다.
- “틀렸으면 사용자 말을 따르라”를 명시하세요. 장기 기억은 오래된 사실을 담고 있을 수 있어요. 사용자가 “이제 아니야” 라고 하면 기억을 우선하지 않도록 지시를 걸어두어야 합니다.
- 전부 넣지 말고 골라 넣으세요. 기억이 100개가 되면 그중 이번 대화와 관련된 것만 넣어야 해요. 키워드 검색으로 시작하고, 부족하면 임베딩 검색을 붙이면 됩니다.
⚠️ 장기 기억에는 절대 자격증명·토큰·개인식별정보를 저장하지 마세요. 기억은 이후 모든 세션에 다시 주입되기 때문에, 한 번 잘못 들어간 비밀은 계속 재생됩니다. 추출 프롬프트에 금지 지시를 넣는 것만으로는 부족하고, 저장 직전에 패턴 검사로 한 번 더 걸러야 해요.
7. stop_reason 과 사전 계측
마지막으로 실전에서 반드시 처리해야 하는 두 가지예요.
7-1. 응답이 왜 멈췄는지 확인하기
응답의 stop_reason 을 안 보고 content[0].text 로 바로 가는 코드가 정말 많아요. 그러면 잘린 답을 온전한 답처럼 사용자에게 보여주게 됩니다.
stop_reason |
의미 | 대응 |
|---|---|---|
end_turn |
정상 종료 | 그대로 사용 |
max_tokens |
출력 한도에 걸려 잘림 |
한도 올리거나 이어받기 |
model_context_window_exceeded |
입력이 창을 넘음 |
줄이고 재시도 |
tool_use |
도구 호출 대기 |
실행 후 결과 돌려주기 |
refusal |
안전 정책상 거절 |
재시도 말고 안내 |
def read_response(response):
if response.stop_reason == "refusal":
return "이 요청은 처리할 수 없어요. 다른 방식으로 물어봐 주세요."
if response.stop_reason == "model_context_window_exceeded":
raise ContextTooLarge() # 상위에서 압축/자르기 후 재시도
text = "".join(b.text for b in response.content if b.type == "text")
if response.stop_reason == "max_tokens":
text += "\n\n(답변이 길어서 중간에 끊겼어요)"
return text
refusal 은 특히 HTTP 200 으로 정상 응답처럼 옵니다. content 가 비어 있을 수도 있어서, content[0] 을 바로 참조하면 그 자리에서 예외가 나요. stop_reason 을 먼저 보는 습관이 필요합니다.
그리고 max_tokens 관련해서 최근 모델에서 하나 조심할 게 있어요. max_tokens 는 사고 토큰과 답변 텍스트를 합친 한도예요. 최신 모델은 기본적으로 사고가 켜져 있으니, 예전 모델 기준으로 좁게 잡아둔 max_tokens 를 그대로 쓰면 사고에 예산을 다 쓰고 답변이 잘릴 수 있어요. 넉넉하게 두세요.
7-2. 보내기 전에 재보기
컨텍스트 초과는 맞고 나서 대응하는 것보다 미리 재는 게 훨씬 싸요.
def send_safely(session_id: str, user_blocks: list, budget: int = 120_000):
messages = build_with_summary(session_id, user_blocks)
n = client.messages.count_tokens(
model="anthropic.claude-opus-5",
system=SYSTEM_BLOCKS,
messages=messages,
).input_tokens
if n > budget:
messages = trim_by_tokens(messages, SYSTEM_BLOCKS, budget)
assert_pairs_ok(messages) # 2편의 도구 짝 검증
return client.messages.create(
model="anthropic.claude-opus-5",
max_tokens=16000,
system=SYSTEM_BLOCKS,
messages=build_messages_with_cache_marks(messages),
)
예산(budget)을 창 크기가 아니라 그보다 훨씬 낮게 잡은 것에 주의해주세요. 창을 꽉 채워 쓰는 건 지연·비용·품질 모든 면에서 좋지 않아요. 창 크기는 상한이지 목표가 아니에요.
8. 운영 체크리스트
이 층에서 확인할 항목이에요.
response.content를 통째로 저장하나요? (압축 블록 보존)- 컨텍스트 편집과 압축의 베타 헤더·타입을 섞지 않았나요?
- 압축 임계값이 너무 낮지 않나요? (캐시를 계속 깨게 돼요)
- 요약 노트를 시스템이 아니라 사용자 턴에 넣나요?
- 요약 갱신이 매 턴 일어나지 않나요?
- 토큰을 추정하지 않고 카운팅 API 로 재나요?
- 예산을 창 크기보다 낮게 잡았나요?
stop_reason을 content 접근 전에 확인하나요?- 장기 기억에 비밀·개인정보 필터가 있나요?
- 사용자가 자기 기억을 보고 지울 수 있나요?
9. 시리즈 정리
4편에 걸쳐 온 이야기를 한 장으로 접으면 이렇게 돼요.
| 층 | 핵심 결론 |
|---|---|
| 1. 세션 스토어 | 응답 content 를 원문그대로 쌓아라 |
| 2. 프롬프트 캐싱 | 안 바뀌는 건 앞에, 바뀌는 건 뒤에 |
| 3. 컨텍스트 관리 | 창이 차기 전에 미리 줄여라 |
| 4. 장기 기억 | 히스토리 대신 사실만 남겨라 |
그리고 이 시리즈에서 반복해서 나온 문장 하나가 있어요. “모델이 준 content 는 해석하지 말고 그대로 보관하라.” 도구 호출 짝이 깨지는 것도, 사고 블록 서명이 어긋나는 것도, 압축 상태가 사라지는 것도 다 이 하나를 안 지켜서 생기는 문제였어요. Bedrock 챗봇 맥락 유지에서 가장 값싸게 얻을 수 있는 안정성이 여기 있습니다.
Bedrock 특유의 제약도 다시 짚어둘게요. 자동 프롬프트 캐싱이 없어서 경계를 직접 붙여야 하고, 대화 중간 시스템 메시지가 없어서 사용자 턴 안에 넣는 우회가 필요하고, 압축·컨텍스트 편집은 베타예요. 이 세 개를 모르고 Anthropic 직접 API 기준 코드를 그대로 옮기면 “왜 캐시가 안 먹지” 부터 시작해서 한참 헤매게 됩니다.
일단 오늘은 여기까지…..
맥락 유지는 화려한 기능이 아니라, 이렇게 작은 규칙들을 안 어기는 일이 거의 전부예요. 챗봇이 갑자기 기억을 잃거나 청구서가 이상하게 커진다면, 이 4편의 체크리스트를 위에서부터 훑어보시면 대개 그 안에 답이 있을 거예요.
이전 글 ←: (3/4) 프롬프트 캐싱 — 같은 히스토리를 매번 보내면서 돈 아끼기
시리즈 처음으로 →: (1/4) AWS Bedrock 챗봇, 대화 맥락은 어떻게 유지하나 — 전체 설계도