에이전트 메모리추출프롬프트 설계진단

한국어 대화에서 영어 기억이 추출되던 이유

추출 프롬프트에 영어 few-shot 예시가 14개 있었다. 지시문을 어디에 넣어도 안 바뀌었고, 예시를 3개로 줄이고 하나를 한국어 쌍으로 바꾸자 해결됐다.

2026년 8월 15일8분

에이전트에 장기 기억을 붙이는 일을 맡았다. 사용자가 대화 중에 흘린 사실을 추출해 저장해 두고, 다음 대화에서 꺼내 쓰는 구조다.

어느 날 저장된 기억을 열어 봤더니 이렇게 적혀 있었다.

User asked why pgvector 0.8 is necessary for service deployment.

대화는 처음부터 끝까지 한국어였다.

이 일이 어디서 일어나는가

먼저 구조부터. 기억 서비스는 대화 원문을 소유하지 않는다. 원문은 대화로그 저장소 한 곳에만 있고, 이쪽은 요약과 커서와 기억만 가진다.

원문 기록신호다시 읽기추출·저장회상사용자에이전트대화로그 저장소source of truth신호 큐stream적재 워커기억 저장소vector + ledger

노드에 커서를 올리면 연결과 설명이 나타납니다.

원문을 복제하지 않기로 한 것은 초기 결정 중 가장 잘한 것이었다. 보존 기간, 삭제 요청, 개인정보 범위가 전부 저장소 한 곳의 문제로 남는다. 대신 워커가 매번 원문을 다시 읽어야 한다.

매 턴마다 추출하면 안 되는 이유

처음 설계는 단순했다. 턴이 하나 생기면 그 턴을 추출한다.

토큰을 재보고 접었다.

추출 1회 입력의 구성 · 9,564 토큰
고정 추출 프롬프트7,606 토큰79.5%
직전 대화 5턴965 토큰10.1%
기존 기억 20건800 토큰8.4%
실제 처리할 대화 1턴193 토큰2.0%

막대나 항목에 커서를 올리면 설명이 나옵니다.

2%를 처리하려고 98%를 매번 다시 보내고 있었으니, 여러 턴을 묶어서 고정비를 나눠 물기로 했다.

묶음 크기에 따른 1,000턴 처리 비용
한 번에 묶는 턴 수
5턴
200
추출 호출 횟수
2.07M
총 입력 토큰
4.6배 절감
매 턴 대비

5턴으로 묶으면 4.6배가 줄어든다. 슬라이더를 더 밀면 더 줄기는 하는데, 고정비를 나눠 무는 효과는 초반에 거의 다 나오고 그 뒤로는 기억이 저장되기까지의 지연만 길어진다. 실서비스에서 기억이 필요해지는 시점도 세션 단위라 5턴 언저리가 실제와 가장 가까웠다.

에이전트신호 큐적재 워커대화로그기억 저장소턴 생겼음깨움N턴 or 무활동 대기커서 이후 조회원문 반환마스킹 후 추출기억 저장
1 / 7

01매 턴 신호를 보내지만 대화 내용은 싣지 않는다. 큐는 "무언가 생겼다"만 알린다.

지시문을 어디에 넣어도 안 됐다

고정비가 80%라면 묶는 것 말고 고정비 자체를 줄이는 길도 있다. 그래서 그 프롬프트를 열어 보다가 언어 문제와 마주쳤다.

처음엔 그냥 시켜 봤다. "입력과 같은 언어로 써라." 네 가지 조건으로 실험했다.

조건지시문 위치결과
A유저 프롬프트 끝에 8줄영어
B시스템 프롬프트 맨 뒤에 강하게영어
C시스템 프롬프트 맨 앞에 강하게영어
E예시 없는 최소 프롬프트 + 한 줄 지시한국어

A·B·C는 지시문을 어디에 두든 전부 영어로 나왔고, 한국어가 나온 건 예시를 전부 뺀 E 하나였다.

원인은 예시 14개였다

프롬프트는 33,661자였고 그 안에 영어 few-shot 예시가 14개 있었다. language라는 단어는 전체 어디에도 없었고, 한글은 한 글자도 없었다. 프롬프트를 줄이려고 처음부터 읽기 전까지는 아무도 몰랐던 사실이다.

지시문 한 줄보다 "한국어를 받아 영어로 쓰는" 시연 열네 개가 더 세게 먹힌 것 같다.

같은 입력, 다른 프롬프트

예시를 3개로 줄이고 그중 하나를 한국어 입출력 쌍으로 두었다.

프롬프트만 바꾸고 입력은 그대로
입력 — 한국어 대화
user우리 서비스 배포할 때 pgvector 0.8 이 꼭 필요한 이유가 뭐였지?
assistantHNSW 가 post-filter 라서 스코프 필터가 강하면 결과가 모자랍니다. 0.8 의 iterative index scan 이 그걸 해결합니다.
↓
추출된 기억
User asked why pgvector 0.8 is necessary for service deployment.
실패한국어 대화인데 영어로 저장됐다. 게다가 assistant 가 말한 기술적 사실은 통째로 빠졌다.

실측

합성 데이터로 먼저 돌려 보고, 마지막은 실제 대화 6개 스레드로 확인했다. 순서 편향을 없애려고 A/B 순서를 뒤집어서도 돌렸다.

프롬프트 길이
기존
33,661자
축소
3,549자

1/9.5로 줄었다. 출력 스키마는 그대로 두고 가이드라인과 예시만 압축했다.

추출 1회 입력 토큰 (실제 대화 6스레드 평균)
기존
12,810
축소
6,112

회당 6,698 토큰 절감. 비율로는 52.3%인데, 3차 데이터는 대화 본문 자체가 길어서 프롬프트 비중이 낮게 나왔다.

50% → 100%
한국어 유지율 (3/6 → 6/6)
9 : 3
품질 판정, 축소판 승
97 → 69
추출 건수. 과분할이 줄어든 것으로 본다

그전까지 알고 있던 영어 추출률은 13.4%였다. 합성 데이터에서 나온 값이었는데, 실제 대화에서는 절반이 영어로 나왔으니 네 배 가까이 나빴던 것이다.

덤으로 따라온 것

예시를 다시 쓰면서 그동안 방치돼 있던 오염 두 가지도 같이 규칙에 넣었다.

하나는 회상 재적재다. 사용자가 "내가 뭘 좋아한다고 했지?"라고 묻고 에이전트가 저장된 기억을 나열하면, 그 나열이 새 사실로 다시 추출됐다. 같은 사실의 사본이 대화할 때마다 불어났다.

다른 하나는 부재 진술이다. "그런 기록은 없네요"라는 답변이 "기록이 없다"는 기억으로 저장됐다.

오염기존축소
회상 재적재2/70/7
부재 진술5/70/7
귀속 정확도회귀 없음회귀 없음

사본이 실제로 얼마나 불어나는지는 세션을 쌓아 봐야 보인다. 열다섯 세션을 연속으로 돌리며 누적 중복 사본을 셌다.

세션이 쌓일수록 누적되는 중복 사본
017.535세션누적115
억제 없음331차 억제17작은 모델 참조군7최종3

억제가 없으면 사본이 선형으로 늘어난다. 최종 버전은 일곱 번째 세션부터 3에서 더 늘지 않았다. 같은 사실을 다시 말해도 새 사본이 생기지 않았다는 얘기다. 그동안 정당한 사실은 24건 중 22건이 보존됐다.

프롬프트로 안 되는 경우

에이전트가 "남색이 무난합니다"라고 제안하고 사용자가 아무 대답도 하지 않았는데, "사용자는 남색을 선호한다"가 저장되는 경우다. 지시문으로도, 예시를 더해도, 무응답 예시를 따로 넣어도 돌린 것마다 전부 실패했다.

없는 결정을 지어내는 쪽은 규칙으로 전부 막혔다. 하지만 누가 한 말을 누구 의견으로 볼지는 프롬프트로는 안 됐고, 적재 이후의 필터 계층에서 다뤄야 할 것 같다.

끝