에이전트 메모리프롬프트 설계LLM한국어

예시가 지시문을 이긴다

한국어 대화에서 뽑은 기억이 영어로 저장되고 있었다. 지시문을 어디에 넣어도 소용없었고, 원인은 다른 곳에 있었다.

2026년 8월 15일9

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

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

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만 통과했다는 것이 답이었다. 위치의 문제가 아니었다. E에서 뺀 것은 위치가 아니라 예시였다.

원인은 예시 14개였다

프롬프트는 33,661자였고, 그 안에 영어 few-shot 예시가 14개 있었다. 그리고 그 프롬프트 전체에서 language라는 단어는 0번 나왔다. 한글도 한 글자 없었다.

모델은 지시문을 안 읽은 게 아니다. 읽었지만, 열네 번 반복된 "한국어를 받아 영어로 쓰는" 시연이 한 줄짜리 지시를 이겼다.

같은 입력, 다른 프롬프트

예시를 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
추출 건수. 과분할이 줄어든 것으로 본다

가장 뼈아픈 숫자는 50%다. 그전까지 알고 있던 영어 추출률은 13.4%였다. 합성 데이터로 잰 값이었고, 실제 대화에서는 네 배 가까이 나빴다.

덤으로 따라온 것

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

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

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

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

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

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

억제가 없으면 사본이 선형으로 늘어난다. 중요한 것은 최종선이 낮다는 것보다 일찍 평평해진다는 점이다. 같은 사실을 다시 말해도 새 사본이 생기지 않는다는 뜻이다. 이 사이 정당한 사실의 보존은 24건 중 22건으로 유지됐다.

못 고친 것

프롬프트로 안 되는 것도 확실해졌다.

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

없는 결정을 지어내는 쪽은 규칙으로 전부 막혔다. 하지만 말해진 것을 누구 의견으로 볼지는 프롬프트가 판정할 문제가 아니었다. 이건 적재 이후의 필터 계층에서 다뤄야 한다.

남은 것

세 가지를 배웠다.

모델에게 예시는 지시보다 강한 신호다. 프롬프트에 규칙과 시연이 함께 있고 둘이 어긋나면, 이기는 쪽은 시연이다. 그러니 지시문을 강화하는 방향으로 고치려 들면 계속 실패한다. 어긋난 시연을 찾아 고쳐야 한다.

합성 벤치는 스케일의 자리만 정해 준다. 13.4%와 50%의 차이가 그 값이다. 합성 데이터로 임계값의 자릿수를 잡는 것까지는 유효하고, 그 값을 실환경 근거로 쓰는 순간 틀린다.

긴 프롬프트는 비용이자 버그 표면이다. 33,661자 안에 language가 0번 나온다는 사실을 아무도 몰랐다. 줄이는 작업이 곧 읽는 작업이었고, 문제는 읽는 중에 발견됐다.