06 Spring AI 컨텍스트 관리 요령
06 Spring AI 컨텍스트 관리 요령
1. Note
- LLM 컨텍스트 관리라는 것은 LLM을 위해 만들어진 전략이 X
- 메모리를 어떻게 관리하는가에 대한 전략 O
- 이 메모리전략으로 다른 시스템이나 모듈에서 패턴에 맞게 조정하면 사용이 가능함.
- 컴퓨터 사이언스의 기본 개념이라고 보면 될듯.
2. 기존 이력을 관리하며 응답하는 기본 패턴
1. 기존의 대화를 기억한다
- assistantMessage 패턴을 이용해서 구분해서 넣어주거나 히스토리처럼 이전의 대화를 같이 던져누는 방법
- 히스토리를 넣으면, 로직상에서 기존의 대화를 넣었는지 안넣었는지 구분이 잘안됨.
- 이럴때는 assistantMessage를 사용하는게 유리하기는 함.
- 메시지를 구성하는 작업이
- 복잡하게 잡혀있다면 assistantMessage로 분리해서 관리를 하는게 좋아보이고
- 단순 대화라면 History처럼 뭔가 DB에 저장했다가 한번에 때리는것도 좋아 보임!
- 기억을 주기만 하면 어쨋든 결과는 같음.
2. 소스패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 메모리에 저장, DB나 어딘가에 별도 저장 필요
private final Map<String, List<Message>> conversations = new ConcurrentHashMap<>();
// conversationId값은 AP와 AI내에서 소통하는 것
// 저 값은 사용자 - AP 간에는 별도 인증키등으로 하는게 필요
public ContextChatResponse chatWithHistory(String question, String conversationId) {
// ID가 없으면 새로 생성 / < ID값으로 대화 구분함. >
if (conversationId == null || conversationId.isBlank()) {
conversationId = UUID.randomUUID().toString();
}
// 기존 대화 이력을 가져오거나 새로 생성
// getOrDefault 첫번째 인자 넣었을때 없으면 default값 넣음(두번째 인자)
List<Message> history = conversations.getOrDefault(conversationId, new ArrayList<>());
// 사용자 질문 추가 후에 List에 내용을 추가함.
UserMessage userMessage = new UserMessage(question);
history.add(userMessage);
try {
// AI 호출: 지금까지의 history 전체를 메시지로 전달
ChatResponse response = chatClient.prompt()
.messages(history)
.call()
.chatResponse();
//실제 응답을 가져옴
String assistantResponse = response.getResult().getOutput().getText();
// AI 답변을 히스토리에 추가
AssistantMessage assistantMessage = new AssistantMessage(assistantResponse);
history.add(assistantMessage);
// 업데이트된 히스토리 저장
// 기존 히스토리 덮음
conversations.put(conversationId, history);
// 토큰 사용량 정보 추출 및 DTO 변환
var usage = response.getMetadata().getUsage();
ContextChatResponse.TokenUsage tokenUsage = ContextChatResponse.TokenUsage.builder()
.promptTokens(usage.getPromptTokens().intValue())
.completionTokens(usage.getCompletionTokens().intValue())
.totalTokens(usage.getTotalTokens().intValue())
.build();
return ContextChatResponse.builder()
.message(assistantResponse)
.conversationId(conversationId)
.timestamp(LocalDateTime.now())
.tokenUsage(tokenUsage)
.build();
} catch (Exception e) {
log.error("AI 호출 중 오류 발생: {}", e.getMessage());
throw new DomainException(DomainExceptionCode.AI_RESPONSE_ERROR);
}
}
3. 메모리관리 layer 개념
1. Layer 1: Volatile Memory (휘발성 메모리)
- 특징
- 요청 단위로만 유지 (세션 끝나면 사라짐)
- 가장 빠름
- LLM이 직접 참고하는 “현재 문맥”
- 저장소
- 애플리케이션 메모리 (Heap)
- 예: Java List, Map
- 또는 프롬프트 내부 (Chat History)
- 역할
- “지금 대화 흐름 유지”
- LLM에게 바로 입력되는 컨텍스트
- 예시
1 2 3
List<Message> chatHistory = new ArrayList<>(); chatHistory.add(new Message("user", "상품 추천해줘")); chatHistory.add(new Message("assistant", "어떤 카테고리요?"));
2. Layer 2: Non-volatile Memory (비휘발성 메모리)
- 특징
- 영구 저장 (DB)
- 세션이 끝나도 유지
- 속도는 상대적으로 느림
- 저장소
- RDB (MySQL, PostgreSQL)
- NoSQL (MongoDB, Redis 일부)
- 파일 저장소
- 역할
- “대화 기록 저장”
- “사용자 상태 유지”
- 예시
1
chatRepository.save(new ChatHistory(userId, "user", "상품 추천해줘"));
3. Layer 3: Semantic Memory (의미적 검색 메모리)
- 특징
- 벡터 기반 저장
- 의미로 검색 가능
- AI 기능의 핵심 레이어
- 저장소
- Vector DB : 의미(유사도) 기반으로 데이터를 찾는 데이터베이스
- Pinecone : 서버 관리 없이 바로 쓰는 클라우드형 벡터 DB
- Milvus : 대규모 데이터 처리에 강한 고성능 오픈소스 벡터 DB
- Weaviate : 의미 검색 + 필터 기능이 강한 유연한 벡터 DB
- 역할
- “비슷한 의미 찾기”
- RAG (Retrieval-Augmented Generation) 핵심
- 예시
- 저장
1 2 3
String text = "사과는 과일이다"; float[] embedding = embeddingModel.embed(text); vectorDB.save(embedding, text);
- 검색
1 2 3
String query = "과일 종류 알려줘"; float[] queryVector = embeddingModel.embed(query); List<String> results = vectorDB.search(queryVector);
- 저장
4. LLM 컨텍스트 관리 방법
1. 테이블 메모
1. memo
- 채팅창 리스트 테이블을 두고 FK로 채팅 내용 테이블을 둠.
- 1개 채팅창에 여러 채팅내용이 누적되어 조회할 수 있도록 함.
- 또 여러 채팅창을 보관할 수 있게됨.
- 컨텍스트 스위칭(Context Switching) 개념«
2. 테이블 형태
- chat_conversations (채팅창 구분)
1 2 3 4 5 6
CREATE TABLE chat_conversations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user ~ title ~ VARCHAR(255) NOT NULL, ~~~~~
- chat_messages (채팅 내용)
1 2 3 4 5 6 7
CREATE TABLE chat_messages id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, conversation_id UUID NOT NULL, (FK) role VARCHAR(20) NOT NULL, -- USER, ASSISTANT, SYSTEM, SUMMARY status VARCHAR(20) NOT NULL, -- ACTIVE, INACTIVE, DELETED message TEXT NOT NULL, //메시지를 저장 ~~~~
2. 컨텍스트 스위칭(Context Switching)
1. 개념
- 사용자가 여러 대화방이나 세션을 오갈 때, 각 대화의 맥락을 독립적으로 유지하고 필요 시 다시 불러오는 방식
- LLM은 상태를 자체적으로 기억하지 못하기 때문에, 매 요청마다 해당 대화의 이전 메시지를 함께 전달해야 함
- 이때 conversationId 같은 식별자를 기준으로 대화 이력을 조회하여 정확한 흐름을 복원함
- 결과적으로 사용자는 대화를 끊었다가 돌아와도 자연스럽게 이어짐
2. 동작 흐름
- 테이블 내용
- chat_conversations에서 특정의 유저 + 특정 제목을 가지고,
- chat_message에서 해당 채팅창의 대화했던 채팅 내용 조회하고,
- 메시지를 보낼때 이전 메시지를 통으로 조회해서 보내도록 함.
- 대화창 구분
1 2 3 4 5 6
CREATE TABLE chat_conversations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user ~ title ~ VARCHAR(255) NOT NULL, ~~~~~
- 대화 내용 구분
1 2 3 4 5 6 7
CREATE TABLE chat_messages id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, conversation_id UUID NOT NULL, (FK) role VARCHAR(20) NOT NULL, -- USER, ASSISTANT, SYSTEM, SUMMARY status VARCHAR(20) NOT NULL, -- ACTIVE, INACTIVE, DELETED message TEXT NOT NULL, //메시지를 저장 ~~~~
2. 프롬프트 최적화
1. 개념
- 대화가 길어질수록 모든 이력을 프롬프트에 넣으면 토큰 초과, 비용 증가, 속도 저하 문제가 발생함
- 이를 해결하기 위해 이전 대화를 요약한 summary를 생성하고, 이후에는 요약 + 최신 메시지만 사용함
- 이 방식은 핵심 맥락은 유지하면서 불필요한 토큰을 줄이는 데 목적이 있음
- 결과적으로 응답 속도와 비용 효율을 동시에 개선할 수 있음.
- 그런데 요약을 요청을 해야해서 토큰이 사용되는 부분은 있음.
2. 동작 흐름
- Service단에서 일정 시점에 요약 생성
- 00건 이상 또는 00토큰 이상 등
- 관리하기 편한 특정상황을 잡음.
이후에는 summary + 최근 대화만 사용
- 테이블
1 2 3 4 5 6 7
CREATE TABLE chat_messages id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, conversation_id UUID NOT NULL, (FK) role VARCHAR(20) NOT NULL, -- summary (☆) status VARCHAR(20) NOT NULL, message TEXT NOT NULL, //메시지를 저장 ~~~~
- 포인트
- 프롬프트에 메시지를 보낼때, SUMMARY는 시스템에 담아서 보내야함.
1 2 3 4 5
return chatClient.prompt() .system( /* SUMMARY */) // 여기! .user( /* message */) .call() .content();
- 프롬프트에 메시지를 보낼때, SUMMARY는 시스템에 담아서 보내야함.
2. 멀티테넌시(Multi-tenancy)
1. 개념
- 여러 사용자의 데이터를 하나의 시스템에서 처리하되, 서로의 데이터가 섞이지 않도록 분리하는 구조
- 보통 userId를 기준으로 데이터를 조회하여 논리적으로 격리함.
- 이 구조가 없으면 다른 사용자의 대화 내용이 잘못 노출될 위험이 있음
- 서비스 확장성과 보안을 위해 반드시 고려해야 하는 기본 설계 요소
2. 동작 흐름
- chat_conversations을 조회할때 특정 user기준으로 조회하도록 함.
- 그럼 ID값을 기준으로 연속적인 대화를 가질 수 있음.
1 2 3 4 5 6
CREATE TABLE chat_conversations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user ~ --여기에서 유저를 구분함. title ~ VARCHAR(255) NOT NULL, ~~~~~
1 2 3 4
SELECT * FROM chat_messages WHERE user_id = :userId -- 특정 userId가 조회 될 수 있도록! AND conversation_id = :conversationId
This post is licensed under CC BY 4.0 by the author.