대화가 길어질수록 에이전트가 멍청해지는 이유
에이전트를 처음 돌려 보면 첫 몇 턴은 반응이 훌륭합니다. 그런데 대화가 길어지거나 도구 호출이 열 번쯤 쌓이면 갑자기 엉뚱한 답을 하거나, 이미 확인한 정보를 다시 물어보는 걸 본 적이 있을 겁니다. 대부분 모델이 "멍청해진" 게 아니라 컨텍스트 관리를 안 해서 생기는 문제입니다.
메시지 배열은 계속 자란다
2장에서 말했듯 API는 상태를 기억하지 않습니다. 매 호출마다 messages 배열 전체를 다시 보냅니다. 에이전트 루프에서는 매 턴마다 assistant의 도구 호출 메시지와 tool_result 메시지가 하나씩 추가되니, 배열은 단조증가합니다.
# 턴 1: user, assistant(tool_use)
# 턴 2: + tool_result, assistant(tool_use)
# 턴 3: + tool_result, assistant(tool_use)
# ...
len(messages) # 턴이 늘수록 계속 커진다검색 도구가 문서 본문을 통째로 반환하는 상황이라면 이 배열은 몇 턴 만에 수만 토큰이 됩니다. 문제는 비용만이 아닙니다. 컨텍스트가 길어질수록 모델이 배열 중간에 있는 지시사항(시스템 프롬프트에서 준 규칙 같은 것)의 비중을 상대적으로 덜 따르는 경향이 실제로 관찰됩니다 — 이른바 "lost in the middle" 현상입니다.
도구 결과를 넣기 전에 압축한다
가장 실용적인 대응은 도구가 반환하는 값 자체를 다이어트시키는 겁니다.
def search_docs(query: str) -> str:
results = vector_search(query, top_k=20)
# 본문 전체를 넣지 않는다 — 제목과 한 줄 요약만
summaries = [f"- {r.title}: {r.snippet[:100]}" for r in results[:5]]
return "\n".join(summaries)모델이 요약만 보고 특정 문서의 본문이 더 필요하다고 판단하면, 그때 get_document(doc_id) 같은 별도 도구를 한 번 더 부르게 설계합니다. "혹시 필요할까 봐" 모든 정보를 미리 다 넣어주는 건 친절이 아니라 컨텍스트 낭비입니다.
오래된 턴을 요약해서 접는다
턴 수가 특정 임계치를 넘으면, 오래된 메시지들을 요약 하나로 접어치우는 전략도 씁니다.
def compact_if_needed(messages: list, threshold: int = 20) -> list:
if len(messages) <= threshold:
return messages
old, recent = messages[:-6], messages[-6:]
summary = summarize_messages(old) # 별도 LLM 호출로 요약
return [{"role": "user", "content": f"[이전 대화 요약]\n{summary}"}] + recent이 방식은 공짜가 아닙니다. 요약 자체가 LLM 호출 한 번을 더 쓰고, 요약 과정에서 디테일이 사라집니다. 사용자가 다섯 턴 전에 언급한 특정 숫자를 모델이 요약 이후로 못 찾는 경우가 생길 수 있습니다. 그래서 저는 이걸 "정말 필요할 때만" 켭니다 — 짧은 작업이 확실한 에이전트(질문 하나에 답 하나)에는 아예 넣지 않고, 여러 턴에 걸친 리서치나 코딩 에이전트처럼 세션이 길어지는 게 뻔한 경우에만 넣습니다.
대화 메모리와 작업 메모리를 구분한다
여기서 "메모리"라는 말이 두 가지 다른 걸 가리킨다는 점도 짚어야 합니다. 하나는 이번 세션 안에서 쌓이는 메시지 배열(작업 메모리)이고, 다른 하나는 세션이 끝난 뒤에도 남아야 하는 정보(사용자가 전에 말한 선호, 과거 티켓 이력 같은 것 — 장기 메모리)입니다. 장기 메모리는 메시지 배열에 무한정 쌓아 둘 수 없으니, 보통 별도 저장소(벡터 DB든 그냥 테이블이든)에 저장해 두고 필요할 때 도구 호출로 꺼내 오는 형태로 설계합니다. 즉 장기 메모리도 결국 3장에서 다룬 "도구"의 한 형태로 구현되는 경우가 많습니다 — 별도의 특별한 메커니즘이 있는 게 아닙니다.
컨텍스트를 아무리 잘 관리해도 모델이 같은 도구를 무한히 반복 호출하는 상황 자체를 막지는 못합니다. 다음 장에서는 이런 실패가 실제로 어떤 모습으로 나타나는지, 그리고 배포 후에야 드러나는 패턴들을 봅니다.