Chat Memory

Chat Memory (채팅 메모리)

ChatMessage 를 손으로 관리하는 건 번거로워요. LangChain4j 는 ChatMemory 추상화와 함께 여러 즉시 사용 가능한 구현을 제공해요. 저수준 컴포넌트로 단독 사용하거나, AI Services 같은 고수준 컴포넌트의 일부로 쓸 수 있어요.

출처: 공식문서

Memory vs History

"메모리"와 "히스토리"는 비슷하지만 다른 개념이에요.

  • History: 사용자와 AI 사이의 모든 메시지를 온전히 보존. UI 에서 사용자가 보는 것, 실제로 주고받은 대화 그 자체.
  • Memory: LLM 이 대화를 "기억하는" 것처럼 행동하도록 일부 정보만 제시. 메모리 알고리즘에 따라 히스토리를 퇴출하거나, 여러 메시지를 요약하거나, 불필요한 세부를 빼거나, RAG 나 구조화 출력용 지시를 주입하는 식으로 변형해요.

LangChain4j 는 현재 "memory"만 제공하고 "history"는 제공하지 않아요. 전체 히스토리를 보존해야 한다면 직접 관리해야 해요.

퇴출 정책 (Eviction)

퇴출 정책이 필요한 이유는 세 가지예요:

  1. 컨텍스트 윈도우 한도 — LLM 이 한 번에 처리할 수 있는 토큰 수에 상한이 있으므로, 대화가 한도를 넘기 전에 옛 메시지를 퇴출해야 해요.
  2. 비용 제어 — 토큰마다 비용이 붙으므로 불필요한 메시지를 퇴출하면 비용이 줄어요.
  3. 지연 제어 — 보내는 토큰이 많을수록 처리 시간이 길어져요.

즉시 사용 가능한 구현 두 가지:

  • MessageWindowChatMemory — 슬라이딩 윈도우로 최근 N 개 메시지를 유지하고 넘치는 옛 메시지를 퇴출. 메시지마다 토큰 수가 달라서 빠른 프로토타이핑에 주로 유용.
  • TokenWindowChatMemory — 역시 슬라이딩 윈도우지만 최근 N토큰을 유지. 메시지는 나눌 수 없어서 한 메시지가 안 맞으면 통째로 퇴출. TokenCountEstimator 가 필요해요.

영속화 (Persistence)

기본적으로 ChatMemory 구현은 ChatMessage 를 메모리에 저장해요. 영속화가 필요하면 커스텀 ChatMemoryStore 를 구현해 원하는 저장소에 메시지를 저장하면 돼요:

class PersistentChatMemoryStore implements ChatMemoryStore {

        @Override
        public List<ChatMessage> getMessages(Object memoryId) {
          // TODO: Implement getting all messages from the persistent store by memory ID.
          // ChatMessageDeserializer.messageFromJson(String) and 
          // ChatMessageDeserializer.messagesFromJson(String) helper methods can be used to
          // easily deserialize chat messages from JSON.
        }

        @Override
        public void updateMessages(Object memoryId, List<ChatMessage> messages) {
            // TODO: Implement updating all messages in the persistent store by memory ID.
            // ChatMessageSerializer.messageToJson(ChatMessage) and 
            // ChatMessageSerializer.messagesToJson(List<ChatMessage>) helper methods can be used to
            // easily serialize chat messages into JSON.
        }

        @Override
        public void deleteMessages(Object memoryId) {
          // TODO: Implement deleting all messages in the persistent store by memory ID.
        }
    }

ChatMemory chatMemory = MessageWindowChatMemory.builder()
        .id("12345")
        .maxMessages(10)
        .chatMemoryStore(new PersistentChatMemoryStore())
        .build();
  • updateMessages() 는 새 ChatMessage 가 추가될 때마다 호출돼요. LLM 과의 상호작용에서 보통 UserMessage 추가 시 한 번, AiMessage 추가 시 한 번 해서 두 번 호출돼요. 모든 메시지가 함께 업데이트돼야 해요.
  • 메시지를 퇴출하면 ChatMemoryStore 에서도 퇴출돼요. 퇴출 시 updateMessages() 가 퇴출된 메시지가 빠진 목록으로 호출돼요.
  • getMessages()ChatMemory 사용자가 메시지를 요청할 때 호출되고, id 인자로 여러 사용자·대화를 구분할 수 있어요.
  • deleteMessages()ChatMemory.clear() 가 호출될 때 호출돼요. 안 쓰면 빈 메서드로 둬도 돼요.

SystemMessage 특별 취급

SystemMessage 는 다른 메시지 유형과 다르게 취급돼요:

  • 한 번 추가되면 항상 유지돼요.
  • 한 번에 하나만 보유돼요.
  • 같은 내용의 새 SystemMessage 는 무시되고, 다른 내용이면 이전 것을 대체해요. 기본으로 목록 끝에 추가되는데, alwaysKeepSystemMessageFirst 속성으로 바꿀 수 있어요.

도구 메시지 특별 취급

ToolExecutionRequest 를 담은 AiMessage 가 퇴출되면, 관련된 고아 ToolExecutionResultMessage 들도 자동 퇴출돼요. OpenAI 같은 일부 프로바이더는 요청에 고아 ToolExecutionResultMessage 를 보내는 걸 금지하기 때문이에요.

:::note I/O 를 수행하는 ChatMemory/ChatMemoryStore 는 비동기 대응판(addAsync/messagesAsync/setAsync, getMessagesAsync/updateMessagesAsync/deleteMessagesAsync)을 구현해, AI Service 를 논블로킹 모드로 쓸 때 스레드를 블로킹하지 않게 할 수 있어요. (참고: Non-blocking and Reactive) :::

더 알아보기