예시가 지시문을 이긴다
한국어 대화에서 뽑은 기억이 영어로 저장되고 있었다. 지시문을 어디에 넣어도 소용없었고, 원인은 다른 곳에 있었다.
에이전트에 장기 기억을 붙이는 일을 맡았다. 사용자가 대화 중에 흘린 사실을 추출해 저장해 두고, 다음 대화에서 꺼내 쓰는 구조다.
어느 날 저장된 기억을 열어 봤더니 이렇게 적혀 있었다.
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만 통과했다는 것이 답이었다. 위치의 문제가 아니었다. E에서 뺀 것은 위치가 아니라 예시였다.
원인은 예시 14개였다
프롬프트는 33,661자였고, 그 안에 영어 few-shot 예시가 14개 있었다. 그리고 그 프롬프트 전체에서 language라는 단어는 0번 나왔다. 한글도 한 글자 없었다.
모델은 지시문을 안 읽은 게 아니다. 읽었지만, 열네 번 반복된 "한국어를 받아 영어로 쓰는" 시연이 한 줄짜리 지시를 이겼다.
같은 입력, 다른 프롬프트
예시를 3개로 줄이고 그중 하나를 한국어 입출력 쌍으로 두었다. 언어 규칙은 문장이 아니라 예시로 뒷받침했다.
실측
합성 데이터로 두 번 재고, 세 번째는 실제 대화 6개 스레드로 쟀다. 순서 편향을 없애려고 A/B를 뒤집어 두 번씩 돌렸다.
1/9.5로 줄었다. 출력 스키마는 그대로 두고 가이드라인과 예시만 압축했다.
회당 6,698 토큰 절감. 비율로는 52.3%인데, 3차 데이터는 대화 본문 자체가 길어서 프롬프트 비중이 낮게 나왔다.
가장 뼈아픈 숫자는 50%다. 그전까지 알고 있던 영어 추출률은 13.4%였다. 합성 데이터로 잰 값이었고, 실제 대화에서는 네 배 가까이 나빴다.
덤으로 따라온 것
예시를 다시 쓰면서 그동안 방치돼 있던 오염 두 가지도 같이 규칙에 넣었다.
하나는 회상 재적재다. 사용자가 "내가 뭘 좋아한다고 했지?"라고 묻고 에이전트가 저장된 기억을 나열하면, 그 나열이 새 사실로 다시 추출됐다. 같은 사실의 사본이 대화할 때마다 불어났다.
다른 하나는 부재 진술이다. "그런 기록은 없네요"라는 답변이 "기록이 없다"는 기억으로 저장됐다. 조회 결과는 기억이 아니다.
| 오염 | 기존 | 축소 |
|---|---|---|
| 회상 재적재 | 2/7 | 0/7 |
| 부재 진술 | 5/7 | 0/7 |
| 귀속 정확도 | 회귀 없음 | 회귀 없음 |
사본이 실제로 얼마나 불어나는지는 세션을 쌓아 봐야 보인다. 열다섯 세션을 연속으로 돌리며 누적 중복 사본을 셌다.
억제가 없으면 사본이 선형으로 늘어난다. 중요한 것은 최종선이 낮다는 것보다 일찍 평평해진다는 점이다. 같은 사실을 다시 말해도 새 사본이 생기지 않는다는 뜻이다. 이 사이 정당한 사실의 보존은 24건 중 22건으로 유지됐다.
못 고친 것
프롬프트로 안 되는 것도 확실해졌다.
에이전트가 "남색이 무난합니다"라고 제안하고 사용자가 아무 대답도 하지 않았는데, "사용자는 남색을 선호한다"가 저장되는 경우다. 지시문으로도, 예시를 더해도, 무응답 예시를 따로 넣어도 3번 중 3번 다 실패했다.
없는 결정을 지어내는 쪽은 규칙으로 전부 막혔다. 하지만 말해진 것을 누구 의견으로 볼지는 프롬프트가 판정할 문제가 아니었다. 이건 적재 이후의 필터 계층에서 다뤄야 한다.
남은 것
세 가지를 배웠다.
모델에게 예시는 지시보다 강한 신호다. 프롬프트에 규칙과 시연이 함께 있고 둘이 어긋나면, 이기는 쪽은 시연이다. 그러니 지시문을 강화하는 방향으로 고치려 들면 계속 실패한다. 어긋난 시연을 찾아 고쳐야 한다.
합성 벤치는 스케일의 자리만 정해 준다. 13.4%와 50%의 차이가 그 값이다. 합성 데이터로 임계값의 자릿수를 잡는 것까지는 유효하고, 그 값을 실환경 근거로 쓰는 순간 틀린다.
긴 프롬프트는 비용이자 버그 표면이다. 33,661자 안에 language가 0번 나온다는 사실을 아무도 몰랐다. 줄이는 작업이 곧 읽는 작업이었고, 문제는 읽는 중에 발견됐다.