AI 메모리 쓰기 트리거를 변경하자 지원 봇이 모든 것을 잊어버리는 문제가 해결되었습니다
요약
지원 봇의 메모리 문제를 해결하기 위해, 대화 전체 기록 대신 특정 이벤트 유형(확인된 사실, 선호도 변경 등)에서만 정보를 저장하는 방법을 제안합니다. 이 접근 방식은 검색 데이터를 깨끗하게 유지하고 프롬프트 과부하를 막아 챗봇 성능을 향상시킵니다.
핵심 포인트
- 메모리 기록 시점을 최적화해야 합니다.
- 대화 전체 기록(transcript) 저장은 노이즈를 만듭니다.
- 오직 '확인된 사실' 등 핵심 이벤트에서만 메모리를 쓰세요.
- 검색 데이터가 작고 깨끗할수록 성능이 좋습니다.
데모에서는 멋져 보였지만 실제 운영 환경에서는 건망증에 걸린 것처럼 행동하는 지원 봇을 가지고 있었습니다.
실패 모드는 놀라울 정도로 간단했습니다:
- 고객이 라우터를 이미 세 번 재부팅했다고 말함
- 고객이 펌웨어 버전을 이미 확인했다고 말함
- 고객이 SMS 대신 이메일 후속 조치를 요청했다고 말함
- 봇이 그 모든 것을 다시 하라고 요구함
사용된 스택은 특별하지 않았습니다:
- 오케스트레이션을 위한 n8n
- 장기 메모리를 위한 pgvector
- 모델 호출을 위한 OpenAI와 호환되는 API
- 대부분의 실행에 사용된 GPT-5.4
- 더 어려운 답변에 사용된 Claude Opus 4.6
모델은 대화 기록(transcript)을 가지고 있었습니다. 모델은 큰 컨텍스트 창(context window)을 가지고 있었습니다. 워크플로우에는 검색(retrieval) 기능이 있었습니다.
그럼에도 불구하고, 마치 이 케이스를 전에 본 적이 없는 것처럼 행동했습니다.
해결책은 더 나은 모델이 아니었습니다.
해결책은 메모리가 기록되는 시점을 변경하는 것이었습니다.
제 지원 에이전트를 고친 규칙: 메모리는 오직 5가지 이벤트 유형에서만 쓰기(write)해야 합니다 — 확인된 사실, 선호도 변경, 상태를 변경하는 문제 해결 단계, 결정, 그리고 결과.
이렇게 하자 검색 데이터가 더 작고, 더 깨끗하며, 실제로 유용해졌습니다.
실제 버그: 케이스 기록 대신 채팅 기록을 저장했습니다
제 첫 번째 버전은 거의 모든 턴(turn)을 pgvector에 기록했습니다.
여기에는 다음과 같은 쓰레기 데이터가 포함되었습니다:
- "예"
- "여전히 고장남"
- "이미 시도해 봤어요"
- "알겠습니다"
- "다시 설명해 주실 수 있나요"
처음에는 이것이 안전하다고 느껴집니다. 데이터가 많으면 회상력이 더 좋아져야 하잖아요?
하지만 저는 그것이 지원 워크플로우의 경우 정반대라고 생각합니다.
지원 봇에게 전체 대화 기록 메모리는 보통 이벤트 메모리보다 나쁜데, 그 이유는 다음과 같기 때문입니다:
- 검색 데이터를 오염시킵니다 (pollutes retrieval)
- 반복적인 실수를 증가시킵니다
- 프롬프트를 비대하게 만듭니다 (bloats prompts)
- 가치가 낮은 컨텍스트를 다시 로드하면서 토큰을 소모합니다 (burns tokens reloading low-value context)
만약 워크플로우가 n8n, Make, Zapier, OpenClaw 또는 사용자 지정 에이전트 루프에서 지속적으로 실행된다면, 시간이 지남에 따라 이 문제는 더 심해집니다. 티켓 하나하나가 더 많은 잡동사니를 추가합니다. 검색 단계마다 노이즈가 쌓입니다. 프롬프트는 더 많은 부담을 안게 됩니다.
제가 처음에 시도했던 잘못된 해결책
메모리 기록 방식을 모든 대화 청크를 저장하기 전에 요약하도록 변경했습니다.
그것이 더 똑똑하게 들렸지만, 실제로는 그렇지 않았습니다.
이제 저는 원본 노이즈 대신 압축된 노이즈를 갖게 되었습니다.
GPT-5.4와 Grok 4.20 모두 일반적으로 메모리 시스템에 과부하가 걸릴 때 하는 것과 같은 일을 했습니다. 즉, 그럴듯하지만 가치가 낮은 세부 사항에 집착한 다음 이미 해결된 단계를 반복했습니다.
이것은 지원 자동화를 구축할 때 많은 사람들이 놓치는 부분입니다:
메모리가 크다고 해서 더 좋은 메모리는 아닙니다.
만약 메모리 계층(memory layer)이 부실하면, 모델이 더 정보가 풍부해지는 것이 아니라, 산만해지기만 합니다.
실제로 효과를 본 5가지 메모리 기록 트리거
저는 모든 턴마다 기록하는 것을 중단하고, 5가지 이벤트 유형에서만 메모리를 기록했습니다.
1. 확정된 사실 (Confirmed facts)
명시적으로 확인된 경우에만 사실을 저장합니다.
좋은 예:
- 고객이 Netgear Nighthawk R7000 사용 중임
- 계정이 Pro 플랜임
- ISP가 Comcast임
- 장치가 펌웨어 1.0.11로 실행됨
나쁜 예:
- 고객은 구형 라우터를 가지고 있을 가능성이 있음
- 이것은 DNS 문제인 것 같음
- 사용자는 무료 등급일 수 있음
모델이 추론한 것이라면, 장기 메모리에 속하지 않습니다.
2. 선호도 변경 (Preference changes)
이것들은 무시하기 쉽지만 끊임없이 중요합니다.
예:
- 고객은 전화보다 이메일을 선호함
- 업무 시간 중에는 장치를 재부팅하지 말 것
- 현지 시간 오후 5시 이후에만 연락할 것
- 단계별 설명 대신 간결한 답변을 원함
지원 봇이 기술적으로 정확하더라도, 선호도를 잊으면 여전히 고장 난 것처럼 느껴질 수 있습니다.
3. 상태 변경 문제 해결 단계 (State-changing troubleshooting steps)
가장 중요했던 부분입니다.
시스템 상태를 변경한 경우에만 단계를 저장합니다.
예:
- 공장 초기화 완료
- DNS가 ISP 기본값에서 Cloudflare로 변경됨
- 펌웨어가 1.0.11에서 1.0.9로 다운그레이드됨
- 라우터 모드가 브리지 모드에서 라우터 모드로 이동함
제안은 저장하지 않습니다.
나쁜 메모리:
- 공장 초기화를 제안함
- DNS 확인을 권장함
- 고객에게 재부팅을 요청함
그것은 상태가 아닙니다. 그것은 대화입니다.
4. 결정 (Decisions)
예시:
- Tier 2로 에스컬레이션
- 자동화를 일시 중지하고 스크린샷을 기다림
- 현장 기술자 방문 일정 잡기
- 고객 응답 대기하며 루프 닫기
결정(Decisions)은 다음 실행이 무엇을 해야 할지 결정합니다. 그것들은 메모리에 저장되어야 합니다.
5. 결과 (Outcomes)
예시:
- 펌웨어 롤백 후 문제 해결
- 비활성으로 인해 미해결 상태로 티켓 종료
- 모뎀 교체 후에도 패킷 손실 지속
- 네트워크 팀에 의해 에스컬레이션 수락
결과(Outcomes)는 에이전트가 이미 해결된 경로를 다시 여는 것을 방지합니다.
테이블 형태의 규칙 (The rule in table form)
| 이벤트 유형 (Event Type) | 저장할까요? (Store It?) |
|---|---|
| 확인된 하드웨어 모델 (Confirmed hardware model) | 예 (Yes) |
| ... |
메모리 페이로드의 모습 변화 (What the memory payload started to look like)
트랜스크립트 청크(transcript chunks)를 pgvector에 그냥 채워 넣는 대신, 저는 구조화된 이벤트(structured events)를 저장하기 시작했습니다.
이런 식입니다:
{
"ticket_id": "T-18422",
"event_type": "state_change",
...
또는 이것입니다:
{
"ticket_id": "T-18422",
"event_type": "preference_change",
...
이것은 검색(retrieval) 기능에 순위를 매길 유용한 정보를 제공했습니다.
실용적인 쓰기 필터 (A practical write filter)
이것은 제가 처음부터 가졌으면 좋았을 논리입니다.
export function shouldWriteMemory(event: {
type: string;
confirmed?: boolean;
...
그리고 검색 측면에서는 컨텍스트를 작게 유지하는 데 있어 공격적(aggressive)으로 남아 있었습니다.
export function buildMemoryContext(memories: MemoryRecord[]): string {
return memories
.sort((a, b) => b.score - a.score)
...
이 slice(0, 5)가 제가 예상했던 것보다 훨씬 중요했습니다.
50개의 기록도 아니고. 전체 티켓 기록도 아닙니다. 가장 신호가 강한 이벤트들만입니다.
이것이 프롬프트를 어떻게 바꾸는가 (How this changes the prompt)
이전:
Here is the previous conversation history:
[huge blob of transcript chunks and summaries]
이후:
Relevant case memory:
- [confirmed_fact] Customer is on Netgear Nighthawk R7000
- [state_change] Factory reset completed
...
두 번째 프롬프트는 더 짧고 모델이 오해하기가 훨씬 어렵습니다.
프로덕션에서 바뀐 점 (What changed in production)
행동 변화는 즉각적이었습니다.
봇은 멈췄습니다:
- 해결된 단계를 고객에게 반복 요청하는 것
- 이미 실패한 수정 사항을 재제안하는 것
- 실행 간에 핸드오프(handoff) 컨텍스트를 잃는 것
- 거대한 메모리 페이로드에 토큰을 낭비하는 것
개선된 부분:
- 열려 있는 티켓을 올바르게 계속 처리하는 것
- 비동기 후속 조치를 처리하는 것
- 더 깔끔한 요약으로 에스컬레이션(escalating)하는 것
- 고객 선호도를 존중하는 것
이것은 또한 장시간 실행되는 워크플로우의 신뢰성을 향상시켰습니다. 만약 에이전트가 n8n이나 Make에서 하루 종일 실행된다면, 나쁜 메모리 규칙들은 누적됩니다. 좋은 메모리 규칙들도 마찬가지로 누적됩니다.
토큰당 비용을 지불하는 경우 더욱 중요한 이유
이것은 단순한 품질 문제가 아닙니다. 또한 비용 문제이기도 합니다.
나쁜 검색(retrieval) 설계는 조용히 값비싼 습관이 됩니다:
- 더 큰 프롬프트(prompts)
- 더 많은 검색 오버헤드(overhead)
- 잘못된 답변 후 더 많은 재시도(retries)
- 워크플로우가 이미 해결된 문제에 계속 루프를 돌기 때문에 발생하는 더 많은 모델 호출
토큰당 비용을 지불한다면, 노이즈가 많은 메모리는 당신에게 두 번 벌칙을 줍니다. 한 번은 품질로, 그리고 다시 청구서 상으로 말입니다.
제가 n8n이나 사용자 지정 에이전트 루프에서 검색(retrieval), 프롬프트, 다단계 자동화를 테스트할 때도 워크플로우를 조정하면서 모든 토큰에 집착하지 않아도 되는 OpenAI와 호환되는 레이어를 사용하는 것을 좋아하는 이유가 바로 이것입니다.
Standard Compute를 사용하면 동일한 OpenAI SDK 패턴을 유지하고, GPT-5.4, Claude Opus 4.6, Grok 4.20과 같은 모델들 사이로 라우팅(route)할 수 있으며, 토큰당 미터기를 하루 종일 지켜보지 않고도 에이전트 중심의 자동화를 실행할 수 있습니다.
그것이 좋은 메모리 설계의 필요성을 없애는 것은 아닙니다. 아무것도 그것을 없앨 수는 없습니다. 하지만 검색(retrieval), 프롬프트, 다단계 자동화를 n8n이나 사용자 지정 에이전트 루프에서 테스트할 때 반복 작업을 훨씬 덜 짜증나게 만듭니다.
지원 봇을 구축하는 경우, 제 조언은 간단합니다
메모리를 대화의 백업본이 아니라 상태 변화의 타임라인처럼 취급하세요.
지원 업무는 창작 글쓰기 과제와 가깝지 않습니다. 그것은 인간 언어가 감싸고 있는 상태 기계(state machine)에 더 가깝습니다.
저장할 것:
- 무엇이 진실인지
- 무엇이 변경되었는지
- 무엇이 결정되었는지
- 다음에 무슨 일이 일어났는지
가능하다고 해서 모든 문장을 저장하지 마세요.
단 하나의 변화가 제가 사용하던 지원 봇의
에이전트가 명백한 사실들을 계속해서 잊는다면, 모델을 탓하기 전에 메모리 쓰기 트리거를 확인해 보는 것이 좋겠습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기