한국어 대화에서 영어 기억이 추출되던 이유
추출 프롬프트에 영어 few-shot 예시가 14개 있었다. 지시문을 어디에 넣어도 안 바뀌었고, 예시를 3개로 줄이고 하나를 한국어 쌍으로 바꾸자 해결됐다.
에이전트에 장기 기억을 붙이는 일을 맡았다. 사용자가 대화 중에 흘린 사실을 추출해 저장해 두고, 다음 대화에서 꺼내 쓰는 구조다.
어느 날 저장된 기억을 열어 봤더니 이렇게 적혀 있었다.
User asked why pgvector 0.8 is necessary for service deployment.
대화는 처음부터 끝까지 한국어였다.
이 일이 어디서 일어나는가
먼저 구조부터. 기억 서비스는 대화 원문을 소유하지 않는다. 원문은 대화로그 저장소 한 곳에만 있고, 이쪽은 요약과 커서와 기억만 가진다.
노드에 커서를 올리면 연결과 설명이 나타납니다.
원문을 복제하지 않기로 한 것은 초기 결정 중 가장 잘한 것이었다. 보존 기간, 삭제 요청, 개인정보 범위가 전부 저장소 한 곳의 문제로 남는다. 대신 워커가 매번 원문을 다시 읽어야 한다.
매 턴마다 추출하면 안 되는 이유
처음 설계는 단순했다. 턴이 하나 생기면 그 턴을 추출한다.
토큰을 재보고 접었다.
막대나 항목에 커서를 올리면 설명이 나옵니다.
2%를 처리하려고 98%를 매번 다시 보내고 있었으니, 여러 턴을 묶어서 고정비를 나눠 물기로 했다.
5턴으로 묶으면 4.6배가 줄어든다. 슬라이더를 더 밀면 더 줄기는 하는데, 고정비를 나눠 무는 효과는 초반에 거의 다 나오고 그 뒤로는 기억이 저장되기까지의 지연만 길어진다. 실서비스에서 기억이 필요해지는 시점도 세션 단위라 5턴 언저리가 실제와 가장 가까웠다.
01매 턴 신호를 보내지만 대화 내용은 싣지 않는다. 큐는 "무언가 생겼다"만 알린다.
지시문을 어디에 넣어도 안 됐다
고정비가 80%라면 묶는 것 말고 고정비 자체를 줄이는 길도 있다. 그래서 그 프롬프트를 열어 보다가 언어 문제와 마주쳤다.
처음엔 그냥 시켜 봤다. "입력과 같은 언어로 써라." 네 가지 조건으로 실험했다.
| 조건 | 지시문 위치 | 결과 |
|---|---|---|
| A | 유저 프롬프트 끝에 8줄 | 영어 |
| B | 시스템 프롬프트 맨 뒤에 강하게 | 영어 |
| C | 시스템 프롬프트 맨 앞에 강하게 | 영어 |
| E | 예시 없는 최소 프롬프트 + 한 줄 지시 | 한국어 |
A·B·C는 지시문을 어디에 두든 전부 영어로 나왔고, 한국어가 나온 건 예시를 전부 뺀 E 하나였다.
원인은 예시 14개였다
프롬프트는 33,661자였고 그 안에 영어 few-shot 예시가 14개 있었다. language라는 단어는 전체 어디에도 없었고, 한글은 한 글자도 없었다. 프롬프트를 줄이려고 처음부터 읽기 전까지는 아무도 몰랐던 사실이다.
지시문 한 줄보다 "한국어를 받아 영어로 쓰는" 시연 열네 개가 더 세게 먹힌 것 같다.
같은 입력, 다른 프롬프트
예시를 3개로 줄이고 그중 하나를 한국어 입출력 쌍으로 두었다.
실측
합성 데이터로 먼저 돌려 보고, 마지막은 실제 대화 6개 스레드로 확인했다. 순서 편향을 없애려고 A/B 순서를 뒤집어서도 돌렸다.
1/9.5로 줄었다. 출력 스키마는 그대로 두고 가이드라인과 예시만 압축했다.
회당 6,698 토큰 절감. 비율로는 52.3%인데, 3차 데이터는 대화 본문 자체가 길어서 프롬프트 비중이 낮게 나왔다.
그전까지 알고 있던 영어 추출률은 13.4%였다. 합성 데이터에서 나온 값이었는데, 실제 대화에서는 절반이 영어로 나왔으니 네 배 가까이 나빴던 것이다.
덤으로 따라온 것
예시를 다시 쓰면서 그동안 방치돼 있던 오염 두 가지도 같이 규칙에 넣었다.
하나는 회상 재적재다. 사용자가 "내가 뭘 좋아한다고 했지?"라고 묻고 에이전트가 저장된 기억을 나열하면, 그 나열이 새 사실로 다시 추출됐다. 같은 사실의 사본이 대화할 때마다 불어났다.
다른 하나는 부재 진술이다. "그런 기록은 없네요"라는 답변이 "기록이 없다"는 기억으로 저장됐다.
| 오염 | 기존 | 축소 |
|---|---|---|
| 회상 재적재 | 2/7 | 0/7 |
| 부재 진술 | 5/7 | 0/7 |
| 귀속 정확도 | 회귀 없음 | 회귀 없음 |
사본이 실제로 얼마나 불어나는지는 세션을 쌓아 봐야 보인다. 열다섯 세션을 연속으로 돌리며 누적 중복 사본을 셌다.
억제가 없으면 사본이 선형으로 늘어난다. 최종 버전은 일곱 번째 세션부터 3에서 더 늘지 않았다. 같은 사실을 다시 말해도 새 사본이 생기지 않았다는 얘기다. 그동안 정당한 사실은 24건 중 22건이 보존됐다.
프롬프트로 안 되는 경우
에이전트가 "남색이 무난합니다"라고 제안하고 사용자가 아무 대답도 하지 않았는데, "사용자는 남색을 선호한다"가 저장되는 경우다. 지시문으로도, 예시를 더해도, 무응답 예시를 따로 넣어도 돌린 것마다 전부 실패했다.
없는 결정을 지어내는 쪽은 규칙으로 전부 막혔다. 하지만 누가 한 말을 누구 의견으로 볼지는 프롬프트로는 안 됐고, 적재 이후의 필터 계층에서 다뤄야 할 것 같다.
끝