Engineering · Agent Engineering

에이전트 기억 계층 설계: 저장만 되던 교훈이 다음 실행을 바꾸기까지

하네스의 기억을 작업 기억, 실행 상태, 장기 기억으로 수명에 따라 나눠 엔진 훅과 4스코프 메모리로 구현했습니다. 경고 없이 0행이던 상태 기록과, 실패한 답만 담긴 채 정렬 없이 공급되던 교훈을 추적해 고쳤고, 다음 실행의 첫 초안이 재시도 없이 통과하는 것을 확인했습니다.

기간
2026.06 – 2026.07
소속
플래티어 · XGEN
역할
메모리·상태 계층 설계

Key Results

61 / 69
다음 실행으로 이월된 정제 기억 / 교훈로컬 스택, 앞선 실행이 남긴 기록을 다음 실행이 resume한 로그
약 3/387
수정 전 최신 교훈이 선택될 확률정렬 없는 공급에서 엔진 상한 교훈 3건을 가져갈 때의 산정치
재시도 0회
교훈 수정 후 다음 실행로컬 스택 E2E, 미달 사유 교훈만 남긴 상태에서 첫 초안 검증 1.00

XGEN의 에이전트 실행 엔진(이하 하네스)에는 6월 초까지 기억이라고 부를 만한 것이 대화 이력 하나뿐이었습니다. 한 실행 안에서는 판정(judge)이 미달한 답을 다시 쓰게 하는 재시도 루프가 있었지만, 판정이 남긴 피드백은 실행이 끝나면 사라졌습니다. 같은 사용자가 다른 워크플로우에서 대화를 시작하면 선호를 처음부터 다시 설명해야 했고, 긴 작업에서 이미 확인한 근거는 대화 기록에 섞여 있다가 압축될 때 함께 소실됐습니다.

6월 중순부터 7월 중순까지 이 공백을 세 층의 기억으로 나눠 구현했습니다. 구현보다 시간이 많이 든 것은 "저장됐다"와 "효과가 있다" 사이의 거리였습니다. 상태 기록 테이블은 경고 하나 없이 0행이었고, 실패 교훈은 다음 실행으로 넘어가는데도 재시도가 줄지 않았습니다.

이 글은 기억을 수명 기준으로 나눈 이유, 엔진과 플랫폼 사이에 책임을 나눈 방식, 그리고 두 가지 조용한 실패를 로그와 코드로 좁혀 간 과정을 다룹니다.

대화 이력 하나에 수명이 다른 정보가 섞여 있었습니다

하네스는 한 번의 실행을 10개 단계로 처리합니다. 기억과 관련된 단계는 네 곳입니다. S02는 대화 이력과 기억을 불러오고, S03은 그것을 시스템 프롬프트에 병합하고, S08은 답을 채점해 계속·재시도·완료를 결정하고, S09는 최종 출력을 정리해 저장합니다.

6월 12일 사례조사 문서에서 당시 구조를 정리했을 때, 엔진에는 실행 단위의 인메모리 상태, 여러 턴을 잇는 세션 저장소, 세션 간 작업 인계용 진행 기록이 있었습니다. 플랫폼 쪽에는 턴·대화 스레드·워크플로우 단위의 실행 기록이 있었습니다. user_id는 전 계층에 전파되고 있었지만, user_id를 키로 하는 기억 저장소는 없었습니다.

더 큰 공백은 실행과 실행 사이였습니다. S08의 judge_then_loop 전략은 점수(validation_score)와 피드백(validation_feedback)을 상태에 기록하고 임계값에 못 미치면 재시도를 결정합니다. 그런데 S09는 점수만 메트릭으로 남기고 피드백 텍스트는 버렸습니다. 같은 유형의 실패가 다음 실행에서 반복돼도 참조할 길이 없었습니다. 한 실행 안의 루프는 있는데 실행 사이의 루프는 끊겨 있었습니다.

정리하면 세 종류의 정보가 같은 대화 기록에 기대고 있었습니다.

작업 기억   한 실행 안에서만 필요   찾은 근거, 확인한 사실           → 실행이 끝나면 폐기
실행 상태   실행과 실행을 이음     회차 기록, 정제 결과, 실패 교훈    → 다음 실행이 읽음
장기 기억   대화 경계를 넘음       사용자 선호, 워크플로우 규칙       → 소유자와 삭제 정책 필요

세 가지는 수명과 소유자가 다릅니다. 하나의 저장소에 넣으면 무엇을 언제 지울지 정할 수 없었습니다. 그래서 층마다 저장 위치와 정리 규칙을 따로 두기로 했습니다.

설계 제약은 두 가지였습니다. 첫째, 엔진은 특정 플랫폼을 모르는 패키지로 유지해야 했습니다. 엔진에는 인터페이스만 두고 실제 저장·검색·권한은 플랫폼 이식 계층이 구현합니다. 둘째, 기존 워크플로우의 동작이 바뀌면 안 됐습니다. 모든 기능은 설정하지 않으면 기존 경로로만 실행되는 가산적 변경이어야 했습니다.

세 층을 서로 다른 방식으로 엔진에 연결했습니다

작업 기억: 새 저장소 대신 기존 PD 저장소를 확장

작업 기억의 목적은 긴 검색 작업에서 모델이 매 턴 "무엇을 봤는지, 무엇이 유효한지"를 대화 기록에 싣고 다니는 비효율을 없애는 것이었습니다. 선택지는 세 가지였습니다.

선택지 장점 단점 판단
별도 작업 기억 저장소 신설 구조를 자유롭게 설계 조회 도구와 압축 경로를 새로 만들어야 함 기각
기존 PD 저장소에 recall 종류 추가 본문 조회 도구를 수정 없이 재사용 저장소 형식에 맞춰야 함 채택
매 턴 S03에서 자동 렌더 모델이 항상 볼 수 있음 프롬프트 구성 코어를 수정하는 위험 후속으로 분리

하네스에는 이미 Progressive Disclosure(PD, 점진적 공개) 저장소가 있었습니다. 큰 도구 결과는 미리보기만 대화에 남기고 본문은 저장소에 보존했다가 조회 도구로 꺼내는 구조입니다. 작업 기억을 이 저장소의 새 종류로 얹자 기존 조회 도구가 수정 없이 본문을 꺼낼 수 있었습니다. 이질적인 모듈을 이식하기보다 엔진 자신의 패턴을 확장하는 쪽이 회귀 위험이 작았습니다.

역할도 나눴습니다. 모델은 Keep, Check, Recall 도구로 무엇을 남길지 의미만 결정하고, 엔진은 우선순위 정렬, 중복 제거, 상한(기본 50), 확인 기록, 압축 렌더를 관리합니다. Check는 자동 사실 검증이 아닙니다. 범용 엔진에는 검색기가 없으므로 모델의 판정을 기록해 재확인을 막는 캐시가 맞는 형태라고 판단했습니다. 같은 도구를 같은 인자로 반복 호출하는 패턴은 기존 Guard 체계에 도구 다양성 가드를 추가해 도구 실행 직전에 막았습니다. 이 기능은 도구 목록에 명시할 때만 등록되는 opt-in이라 대상이 아닌 하네스에는 영향이 없습니다.

실행 상태: 엔진은 훅만, 저장은 플랫폼이

실행 상태는 엔진에 StateProvider(읽기)와 StateRecorder(쓰기) 훅을 두고 기본값을 no-op으로 했습니다. 주입하지 않으면 동작이 그대로이므로 회귀가 없습니다. 플랫폼은 자기 상태 테이블을 백엔드로 하는 구현체를 주입합니다.

설계의 중심은 메모리 누적을 막는 것이었습니다. Provider는 개수 상한과 문자 예산 안에서 매번 무상태로 다시 계산하고, Recorder는 정수 시퀀스만 들고 기록한 뒤 참조를 해제합니다. 재시도 회차는 Reflexion 방식으로 교훈이 되어 FIFO 상한 버퍼에 쌓이고, 다음 회차의 상태 뷰에 노출됩니다. 사람이 정한 설정을 그대로 쓰고 기록만 하는 manual과, 정제 기억과 교훈을 다시 읽어 반영하는 auto를 게이트로 나눴습니다.

장기 기억: 4개 스코프와 두 축의 우선순위

장기 기억은 엔진에 MemoryStore 프로토콜, S02 다중 컬렉션 회상, S09 추출 훅만 두고, 스키마·추출 판정·권한은 플랫폼이 소유하게 했습니다. 스코프는 session, workflow, user, platform 네 가지입니다. session은 대화가 끝나면 소멸하거나 승격되고, workflow는 워크플로우가 삭제되면 함께 삭제되며, user는 사용자가 지울 수 있고, platform은 관리자만 씁니다.

session의 부모는 user와 workflow 둘이므로 스코프 구조는 트리가 아니라 DAG입니다. 회상은 session에서 조상 방향으로 올라가며 경로상의 기억을 모읍니다.

충돌 해소에서 처음 떠오른 규칙은 "구체적인 스코프가 이긴다"였습니다. 이 규칙은 선호에는 맞지만 보안 정책 같은 제약에는 틀립니다. 그래서 타입 축과 스코프 축을 분리했습니다. constraint·policy 타입은 platform부터 넓은 스코프가 우선하고, preference·lesson·decision 타입은 session부터 좁은 스코프가 우선합니다. 같은 키가 여러 스코프에 있으면 이 순서로 하나를 채택하고 나머지는 참고로 내립니다.

회상 방식은 도구 검색과 같은 구조로 설계했습니다. 도구는 목차를 검색한 뒤 필요한 스키마만 펼치듯, 기억도 목차를 먼저 보고 필요한 본문만 꺼내는 방향입니다.

상태 테이블이 경고 없이 0행이었습니다

첫 번째 조용한 실패는 실행 상태에서 나왔습니다. 6월 24일 상태 저장 서비스는 있는데 호출자가 없다는 지적을 받고 런타임 배선을 넣었고, 6월 29일 엔진 훅과 단위 테스트까지 마쳤습니다. 그런데 로컬 스택의 캔버스에서 하네스 노드를 실행해도 상태 테이블은 계속 0행이었습니다. 실행 자체는 문제없이 끝났고 경고도 없었습니다.

호출 경로를 따라가 보니 기록기를 만드는 함수가 실행 ID를 받지 못하고 있었습니다. 캔버스 인라인 실행 경로가 interaction_id를 넘기지 않았고 workflow_id는 빈 문자열이었습니다. 기록기 팩토리는 실행 ID가 없으면 None을 반환하고, None이면 기록기가 주입되지 않아 회차 기록이 no-op이 됩니다. SDK는 이 경우를 오류 없이 넘기도록 되어 있었습니다.

no-op 기본값은 회귀를 막으려고 의도한 설계였습니다. 그 설계가 "배선되지 않음"과 "기록할 것 없음"을 구분하지 못하게 만든 것입니다. 수정은 실행기가 노드에 주입하는 interaction_id와 workflow_id를 노드에서 인라인 실행 함수, 내부 비동기 실행, 어댑터 실행까지 전파하는 것이었습니다. 이후 캔버스 실행마다 회차 기록 행과 완료 시 정제 기억 행이 run_id는 interaction_id, thread_id는 workflow_id로 쌓이는 것을 확인했습니다. 쓰기는 manual과 auto 공통이고, 다시 읽어 반영하는 읽기는 auto에서만 동작합니다.

장기 기억 1단계(user 스코프)를 라이브로 검증할 때도 같은 종류의 결함이 두 건 나왔습니다. S09 추출 훅이 persist 전략 안에 묶여 있어 기본 마무리 전략에서는 실행되지 않았고, 회상한 기억을 시스템 프롬프트 뒤에 덧붙이던 방식은 S03이 프롬프트를 다시 구성하면서 유실됐습니다. 추출 훅은 턴 종료 시 독립 실행으로 분리하고, 회상 기억은 S03의 정식 섹션(규칙 직후의 "알려진 사실")으로 렌더하게 바꿨습니다. 세 건 모두 단위 테스트는 통과했고 실제 실행 경로에서만 드러났습니다.

교훈이 이월되는데 재시도가 줄지 않았습니다

두 번째 실패가 이 글의 중심입니다. 7월 4일 앞선 실행이 기록한 교훈을 다음 실행이 읽도록 연결했고, 로컬 스택에서 다음 실행 로그에 "정제기억 61·교훈 69 이월"이 찍히는 것을 확인했습니다. 7월 10일 데모를 만들며 연속 실행해 보니, 교훈을 이월해도 같은 기준에서 재시도가 그대로 발생했습니다.

가설 1: 교훈이 읽히지 않는다

가장 먼저 의심한 것은 이월 자체였습니다. 하지만 resume 로그에 이월 건수가 찍혔고, 상태 뷰에 교훈이 들어가는 경로도 확인됐습니다. 읽기는 동작하고 있었으므로 이 가설은 배제했습니다. 다만 교훈 로그의 요약 칸이 항상 "(요약없음)"으로 찍히고 있었고, 이것도 로그 키 오류였습니다. 관측 수단이 교훈의 내용을 보여 주지 못하고 있었던 것입니다.

가설 2: 교훈의 내용이 교정 신호가 아니다

그래서 교훈이 무엇으로 만들어지는지 코드를 읽었습니다. 재시도 교훈은 사용자 질문과 실패한 답을 정제해 만들고 있었습니다. S08이 만든 미달 사유(validation_feedback)는 입력에 없었습니다. 다음 실행은 "왜 반려됐는가" 대신 "무엇을 답했는가"를 받고 있었고, 결과적으로 오답을 다시 주입하는 셈이었습니다.

수정 전: 교훈 = 정제(사용자 질문, 실패한 답)
수정 후: 교훈 = 정제(사용자 질문, 판정 피드백 우선 · 피드백이 없을 때만 실패한 답)

재시도가 발생하는 지점이 S08 판정 하나뿐이라는 것도 확인했습니다. 그래서 교훈을 만들 때 쓰는 피드백이 이전 회차의 낡은 값일 가능성은 없었습니다.

가설 3: 어떤 교훈이 들어가는지 정해져 있지 않다

내용을 고쳐도 남는 문제가 있었습니다. 플랫폼의 상태 조회가 교훈을 정렬 없이 공급했고, 엔진은 상한(교훈 3건, 정제 기억 5건)만큼 앞에서부터 가져갔습니다. 이 조건에서 방금 저장한 교훈이 선택될 확률은 약 3/387로 산정됐습니다. 저장소가 반환하는 순서에 기대는 한, 교훈이 쌓일수록 최신 교훈이 들어갈 확률은 더 낮아집니다. 공급 직전에 생성 시각과 id 역순으로 정렬하도록 고쳤습니다.

효과를 분리해서 확인한 방법

수정 후 검증은 교훈 하나의 효과만 남도록 설계했습니다. 먼저 해당 기준에 관한 기억을 모두 비활성화한 상태에서 실행해 0.70으로 미달시키고, 교훈에 미달 사유 원문이 기록되는 것을 확인했습니다. 다음으로 정제 기억과 사용자 기억을 다시 비활성화해 그 사유 교훈만 남겼습니다. 이 상태에서 다음 실행의 첫 초안이 검증 1.00으로 통과했고 재시도는 0회였습니다. 단위 테스트에서는 가짜 상태로 교훈의 결과 필드가 검증 사유와 같고 실패한 답이 들어가지 않는 것을 확인했습니다.

4스코프 구현에서 엔진과 플랫폼의 경계를 지킨 지점

장기 기억을 4스코프로 확장할 때 엔진 쪽 변경은 하나였습니다. S02는 스코프 템플릿의 자리표시자를 허용 목록 두 개(user_id, interaction_id)로만 치환하고 있었습니다. 이를 상태 속성 이름으로 일반 치환하게 바꿨습니다. 원문 기준의 동작을 의사 코드로 옮기면 다음과 같습니다.

입력: 스코프 템플릿 목록   예) "session:{interaction_id}", "workflow:{workflow_id}", "user:{user_id}", "platform"
1. 각 템플릿에서 정규식 \{([a-zA-Z_]\w*)\} 에 걸리는 이름을 상태 객체의 같은 이름 속성 값으로 치환
2. 치환 후에도 자리표시자가 남은 템플릿은 검색 대상에서 제외

이 변경으로 엔진은 플랫폼의 스코프 이름을 몰라도 되고, 플랫폼은 노드 설정에서 session·workflow·user·platform을 이름으로 고르면 엔진 템플릿으로 매핑합니다. 새 식별자(예: thread_id)가 생겨도 엔진을 고칠 필요가 없습니다.

플랫폼 쪽 추출은 보조 judge가 기억 후보를 user·workflow·session으로 분류하고 실제 스코프 키로 라우팅합니다. platform 스코프 자동 저장은 금지했습니다. 저장 진입점에는 기존 지시 무시, 비밀번호·토큰 접근, 사용자 몰래 행동, 시스템 프롬프트 언급 같은 패턴을 거부하는 안전 필터를 두었습니다. 영속 기억은 세션을 넘어 프롬프트 인젝션이 지속되는 경로가 될 수 있다는 점을 사례조사에서 확인했기 때문입니다. 회상 결과는 앞의 두 축 우선순위로 중복을 정리한 뒤 프롬프트에 넣습니다. 관리용으로 목록 조회, 스코프 단위 삭제(기본은 soft delete), 만료 항목 비활성화(sweep) 함수를 만들었습니다.

저장에서 다음 실행의 변화까지 확인한 결과

지표 이전 이후 측정 조건
상태 테이블 기록 0행, 경고 없음 실행마다 회차·완료 행 누적 로컬 스택 캔버스 실행
상태 루프 내부 누적 해당 없음 101회 반복 후 0 엔진 단위 테스트
재시도 교훈 버퍼 해당 없음 재시도 10회에 버퍼 3건 유지 엔진 단위 테스트, FIFO 상한
대화 경계를 넘는 회상 불가 새 대화에서 저장한 이름을 회상 Qwen3.6-27B, user 스코프
스코프별 분리 저장 user 고정 선호는 user, 출처 표기 규칙은 workflow Qwen3.6-27B, 4스코프
실행 간 교훈 이월 저장만 됨 정제 기억 61건·교훈 69건 이월 로컬 스택, 연속 실행 로그
다음 실행 재시도 이월 후에도 재시도 첫 초안 1.00, 재시도 0회 로컬 스택, 사유 교훈만 남긴 상태

4스코프 검증에서는 "매운 음식을 좋아한다"와 "이 워크플로우는 출처를 붙인다"를 한 입력으로 넣었을 때 각각 user 선호와 workflow 제약으로 나뉘어 저장됐습니다. 새 세션의 답변은 선호를 회상하면서 출처 표기 규칙도 함께 적용했습니다. 회상 로그에는 session, workflow, user, platform 네 스코프 키가 실제 값으로 치환되어 있었습니다. 관리 기능은 실제 PostgreSQL에서 목록 2건 조회, 안전 필터 거부, 스코프 삭제 후 활성 0건(soft delete 2건 보존), sweep 무오류까지 확인했습니다.

회고와 남은 과제

두 실패에서 공통으로 배운 것은 no-op과 무정렬이 둘 다 "조용한 기본값"이라는 점입니다. 회귀를 막으려고 둔 no-op은 배선 누락을 숨겼고, 저장소가 주는 순서를 그대로 쓰는 공급은 교훈이 쌓일수록 효과를 희석했습니다. 둘 다 저장 API의 성공 여부로는 보이지 않았고, 다음 실행의 결과까지 따라가야 드러났습니다.

남은 과제도 분명합니다. 교훈 선별은 아직 관련성이 아니라 최신순입니다. 질문 유사도 기반 랭킹은 축적이 커지면 넣을 후보로 남겼습니다. 키워드 회상은 한국어 조사 변형에 약해, 개인 기억이 소량이라는 전제로 최근순 폴백에 기대고 있습니다. 임베딩 회상은 후속입니다. 설계 문서에 적었던 인사이트 유효 카운터(적용 후 점수가 오르면 증가, 내리면 감소)도 구현하지 않았습니다.

생명주기는 7월 14일 별도로 점검했습니다. 장기 기억을 꺼도 기본 user 스코프가 회상·추출 설정에 남는 문제, soft delete한 기억이 자동 추출로 다시 활성화되는 문제, 복구해도 만료 시각이 남아 회상되지 않는 문제, workflow_id와 interaction_id가 사용자별로 겹칠 수 있는데 스코프에 소유권이 충분히 반영되지 않은 문제를 확인했습니다. 회귀 테스트 10건까지는 통과시켰지만 세션이 완료 없이 끝났고, 실제 PostgreSQL과 브라우저에서의 삭제·복구 흐름은 검증하지 못했습니다. 이 부분은 완료된 구현이 아니라 미완료 체크포인트로 남아 있습니다.