본문으로 건너뛰기

Memory Management

Memory Management는 JARVIS 가 지난 대화와 사용자에 대한 사실을 기억해 매 턴의 프롬프트에 넣는 기능이다. 사용자를 오래 보좌하는 Personal Agent일수록 무엇을 기억하고 언제 꺼내는지가 응답의 질을 정한다. 그래서 모델에는 최근 3턴만 넣어 프롬프트를 작게 유지하고, 그보다 앞의 대화와 사실은 장기 기억에서 필요한 만큼만 회상해 되돌린다. Inner Thought, Open Loop, 멀티 에이전트 위임처럼 맥락이 필요한 다른 기능도 모두 이 기억을 읽고 쓴다.

Memory Management최근 3턴만 모델에 넣고, 그보다 앞의 대화와 사용자 사실은 장기 기억에서 떠올린다최근 3턴 창턴 단위로 자름사용자 프로필새 사실로 대체턴마다 일화발화와 답 기록지칭 풀기직전 대화 붙임큰 사건 자리중요도 0.8 이상작업 패턴에이전트별 방식층별 읽기 권한남의 대화 막음대화 한 턴의 기억사용자 발화마지막 200자로 찾는다질의만으로 충분한가?예아니오"그거", 짧은 말엔 직전대화를 붙인다장기 기억 회상핵심 사실 3건, 회상 3건읽기 권한이 있는가?예아니오권한 밖 일화는 뺀다LLM 호출기억 블록과 최근 3턴턴 일화 쓰기발화와 답을 1,000자씩답에 쓰인 기억인가?예아니오되뇌기로 세지 않는다되뇌기 +1중요도가 덜 식는다

Memory Management가 주목한 점​

몇 주 전에 거주지가 서울이라고 알려 준 사용자가 "서울에서?" 하고 되묻는 장면을 떠올려 보자. 대화가 길어지면 지난 이력을 전부 모델에 넣을 수 없기 때문에, 기존의 Chatbot은 오래된 대화를 잘라 내거나 요약해 압축하는 방식으로 대처한다. 하지만 이렇게 하면 에이전트는 그 사실을 이미 잃었고, "그거 다시 해줘" 처럼 앞을 가리키는 말은 찾을 낱말이 없어 단순한 검색으로는 회상에도 걸리지 않는다. 에이전트가 여럿이라면 KAREN 과 나눈 대화가 JARVIS 에게 새지 않아야 한다는 문제도 생긴다. Jarvis-Agent는 기억을 종류별로 나누고, 종류마다 언제 쓰고, 누가 읽고, 언제 꺼내는지를 따로 정해 사람이 기억을 다루듯 적재적소에 사용하도록 설계했다.

어떤 기능을 가지고 있나요?​

평가 실행기를 기능보다 먼저 만들었다​

  • 메모리 모듈은 처음에 범위와 종류를 정의하고 기능별 평가 실행기부터 만들었다. 그다음 단계마다(범위 키 데이터 모델, 작업 기억, 장기 기억 정책, 서브 에이전트 범위 상속) 스위트를 늘리며 통과를 확인했다.
  • 대안으로는 기능을 먼저 만들고 동작을 보며 "잘 되는 것 같다" 로 판단하는 방식이 있었다.
  • 기억은 그렇게 판단하기 쉬운 기능이라, 무엇이 새고 무엇이 남아야 하는지를 사례로 먼저 못 박았다.

기억의 종류마다 다른 규칙​

  • 종류별로 정책을 나눴다. 의미 기억(사실)은 같은 주제의 옛 사실을 superseded_by 로 대체하고, 일화는 사건 시각으로 최근 것이 먼저 떠오르고, 절차는 스킬 레지스트리를 읽기 전용으로 본다. 검색은 최근성, 중요도, 관련도, 단서 겹침을 합친 점수다. 최근성과 중요도는 반감기로 줄고, 핵심 사실만은 감쇠 없이 원래 값으로 고른다.
  • 대안으로는 장기 기억을 한 저장소, 한 점수로 다루는 방식이 있었다.
  • 한 저장소, 한 점수로 다루면 사실이 쌓여 서로 모순된다.

상대 지칭은 회상 질의에서만 푼다​

  • 메인 모델은 최근 창 안에서 지칭을 스스로 푼다. 단서가 없어 막히는 곳은 장기 기억 회상뿐이라, 거기에만 직전 주고받음을 붙인다.
  • 대안으로는 "그거 해줘" 를 LLM 이 먼저 풀어 쓴 지시문으로 바꾸고 최근 메시지는 빼는 방식이 있었다.
  • 그 방식을 하지 않은 이유는 셋이다. 지칭을 풀려면 어차피 최근 메시지를 읽어야 하고, 잘못 풀면 원문이 없는 메인 모델이 고칠 길이 없고, 매 턴 호출이 하나 늘어 음성 응답이 느려진다. LLM 으로 고쳐 쓰는 단계는 이 방식으로 회상이 실패하는 사례가 평가에서 나오면 넣는다.

기억의 값은 원문 검색과 맞대어 잰다​

  • 기억을 만드는 일(턴 일화, 사실 추출, 점수)이 얼마나 도움이 되는지는 원문을 그대로 조각 내 찾는 naive RAG 와 같은 질문으로 비교해 잰다. 질문마다 Jarvis 는 맞고 RAG 는 틀리면 benefit, 반대면 tax 로 세고, 둘의 차이(net)를 기억의 순이득으로 본다. Memory Tax: Dissecting the Net Value of Agent Memory(He et al., 2026)의 분해를 그대로 따랐다.
  • 대안으로는 기억 시스템끼리만 비교하거나, 정답률 하나만 보는 방식이 있었다.
  • 그 논문은 여러 기억 시스템이 naive RAG 보다 못한 경우가 많고, 정답률이 비슷해도 맞히는 질문이 서로 다르다는 것을 보였다. 정답률 하나로는 무엇을 얻고 무엇을 잃었는지 보이지 않아서, 잃은 질문(tax)을 따로 세어 그 원인을 고친다. 비교군마다 모델에 보이는 글자 수를 같게 맞춘다(아래 실험 절).

추출한 사실에서 원래 턴으로 돌아갈 길을 남긴다​

  • 통합이 뽑은 사실("알레르기: 땅콩")마다 내용이 가장 많이 겹치는 원래 턴 일화를 출처로 남긴다. 회상 때는 후보에 든 사실의 출처 턴을 후보에 더하고, 사실과 턴을 질의 관련도 하나로 다시 매긴다. 출처 턴이 지워졌으면 사실만 남는다.
  • 대안으로는 사실만 넣는 기존 방식과, 사실 아래 출처 턴의 발췌(최대 300자)를 붙이는 방식이 있었다.
  • 추출은 원문에서 벗어난다. 줄이고, 바꿔 말하고, 영어 대화를 한국어 사실로 옮긴다. Memory Tax 는 이 일탈이 benefit 과 tax 를 함께 만든다고 보고, 원래 기록으로 돌아갈 가벼운 길을 남겨 기억 검색과 원문 검색을 함께 재순위하라고 권한다(src-aug). 처음에는 발췌를 붙였는데, 같은 노출 예산에서 사실 하나가 300자를 더 먹어 다른 사실과 턴을 밀어냈다. LongMemEval-s 정답률이 0.43 에서 0.32 로 떨어졌다(아래 실험 절). 논문처럼 공동 재순위로 바꾸자 0.52 가 됐고, 실서비스 블록 조건에서는 0.28 이 0.50 이 됐다. 그래서 기본으로 켠다.

긴 답은 잘라 버리지 않고 조각으로 남긴다​

  • 턴 일화는 발화와 답을 1,000자씩만 남긴다. 답이 그보다 길면 나머지를 문단 경계에서 1,000자 이하 조각 일화(최대 2개)로 더 남긴다. 자연 키에 조각 번호가 들어가 다시 돌려도 같은 항목이다.
  • 대안으로는 그대로 자르는 기존 방식과, 답 전체를 한 일화에 넣는 방식이 있었다.
  • Memory Tax 는 구성 단계에서 사라진 증거는 검색으로 되찾을 수 없다고 지적한다. LongMemEval-s 의 정답 턴으로 재 보니 답(assistant) 쪽 정답 턴의 8% 가 1,000자 자르기에서 정답을 잃었다(아래 실험 절). 답 전체를 한 일화에 넣으면 임베딩 한 개가 긴 글을 뭉개 찾기 어려워지므로 조각으로 나눴다. 조각은 통합과 절차 정제에 쓰지 않는다.

세부 동작​

한 눈에 보기​

(a) 매 턴 읽기세션 히스토리턴 창최근 3턴만회상 거르기창 안의 턴, 사고 로그 빼기프롬프트[핵심 사실] 3건[장기 기억] 회상 3건[절차 기억] 스킬[작업 패턴] 2건까지[작업 공유 메모리] 위임 중최근 3턴LLM장기 기억프로필 공유, 일화와 절차는 정책이 연 것만회상: 키워드 + 벡터, 점수스킬 레지스트리읽기만, 스킬 파일이 정본(b) 턴마다 그 턴을 일화로 쓰고, 나머지는 정해진 순간에만 쓴다/remember프로필 사실턴 끝그 턴을 일화로통합사실 뽑기, 절차 초안사고, 저널, 위임일화remember_task_pattern에이전트별 절차쓰기주 경로흐름저장소강조
Figure 1. (a) 매 턴 히스토리의 최근 3턴만 두고, 장기 기억에서 회상한 블록을 앞에 붙여 LLM 에 넣는다. 창 안의 턴과 사고 로그는 회상 블록에 넣지 않는다. [절차 기억] 은 장기 기억이 아니라 스킬 레지스트리에서 읽는다. (b) 턴이 끝날 때마다 그 턴을 일화(EDM)로 쓰고, 그 밖에는 정해진 경로로만 쓴다. /remember 와 주거 프로필 도구는 사용자 프로필 사실을, remember_task_pattern 은 에이전트별 절차(PDM)를 쓴다.

memory/ 의 MemoryService 가 작업 기억과 장기 기억 두 층을 가진다. 하네스가 턴마다 observe 로 기억을 프롬프트에 넣고, 답이 나오면 reinforce 로 답에 쓰인 기억을 강하게 한다. 3턴 밖의 대화는 회상으로 돌아온다.

기억을 읽고 쓰는 모든 호출은 범위(MemoryScope: 사용자, 에이전트, 세션)를 인자로 받는다. 숨은 전역 값에서 사용자를 꺼내지 않는다. 화면은 슬래시 명령에 접속한 사용자의 범위를 넘기고, 세션은 도구와 사고 엔진에 자기 범위를 넘기며, 위임이나 에이전트 사이 대화로 불린 세션은 부른 쪽 범위(caller_scope)에서 사용자를 받는다.

계층​

계층무엇어디
작업 기억세션 히스토리 위의 메타데이터. 되뇌기 횟수, 메모(최대 50)메모리 또는 Postgres jarvis_working_memory
장기 기억MemoryItem. 종류는 의미(semantic), 일화(episodic), 절차(procedural)메모리 또는 Postgres jarvis_ltm_entries(pgvector 선택)
절차 기억스킬은 저장하지 않고 스킬 레지스트리를 그대로 읽는다
작업 패턴사용자가 알려 준 작업 방식. 장기 기억의 절차(procedural) 항목이다. 저장한 에이전트와, memory.read.pdm 에 그 에이전트를 적은 에이전트가 본다장기 기억과 같은 곳

세션 히스토리 자체의 정본은 세션 저장소다. 모델에 넣는 창은 그 이력의 최근 3턴이다.

턴마다 일어나는 일​

  • 창: 최근 stm_turns(3) 턴만 넣는다. 턴 단위로 자르므로 도구 호출과 결과 쌍이 갈라지지 않는다. 컴팩션(LLM 요약)은 하지 않는다
  • 턴 기록: 답이 나오면 그 턴의 사용자 발화와 답을 각각 1,000자까지 turn 일화로 쓴다. 도구 결과는 남기지 않는다. metadata 에 작업 종류(task_type: 그 턴에서 처음 부른 도구, 없으면 chat), 성패(outcome: 도구 오류나 모델 오류가 있으면 failure), 부른 도구 목록을 남긴다. (사용자, 에이전트, 세션, 턴 번호)가 자연 키라 같은 턴을 다시 돌려도 항목은 하나다. 턴마다 임베딩이 한 번 돈다
  • 긴 답의 조각: 답이 1,000자를 넘으면 나머지를 문단 경계에서 1,000자 이하로 나눠 turn_part 일화로 쓴다(최대 2개, 답 약 3,000자까지). 잘린 문단은 처음부터 다시 담는다. 조각 본문은 발화 앞 200자와 답(2): 처럼 몇 번째 조각인지로 시작한다. 자연 키는 턴 키에 조각 번호를 더한 것이고, 같은 턴을 더 짧은 답으로 다시 쓰면 남는 조각을 지운다. 조각마다 임베딩이 한 번 더 돈다. 조각은 통합, 절차 정제, 실패 반성의 대상이 아니다
  • 세션 삭제: 세션을 지우면 그 세션의 턴 일화, 사고 로그, 작업 기억도 지운다(MemoryService.forget_session, 세션 id 가 정확히 같은 행만). 사용자 프로필 사실과 절차는 남는다
  • 창 안의 턴: 지금 창에 있는 같은 세션의 턴 일화(조각 포함)는 회상 블록에 넣지 않는다. 같은 내용이 두 번 들어가지 않게 하기 위해서다. 뺄 몫만큼 후보를 더 받으므로 회상 자리는 줄지 않는다
  • 사고 로그 제외: 회상 블록은 사고 로그(thought)를 후보에서 뺀다. 사고 로그는 턴마다 사용자 말을 다시 적어서, 같은 낱말로 턴 일화보다 많이, 더 최근에 걸린다. 실서버에서 5턴 전의 사실을 물었을 때 턴 일화는 6위로 밀렸고 답은 사고 로그 덕에 맞았다. 사고 로그는 지우지 않고 저장소에 남는다
  • 골라 떠올리기: 에이전트는 recall_episodes(query, task_type, outcome, since, until) 도구로 자기 일화를 조건으로 골라 읽는다("지난주에 실패한 대출 계산"). 날짜는 이날 포함이고, 읽기 정책 안에서만 찾는다. 모든 에이전트에게 붙는다
  • 회상 질의: 마지막 사용자 발화 200자로 검색한다. 발화에 "그거", "아까", "그 방식" 같은 상대 지칭이 있거나 8자보다 짧으면 직전 주고받음을 질의 앞에 붙인다(memory/working/deixis.py). 메인 모델에 넣는 최근 메시지는 바꾸지 않는다
  • 회상 후보: 키워드(부분 문자열, Postgres 는 ILIKE) 또는 벡터(코사인 ≥ 0.3)에 걸린 항목을 최신순, 중요도순, 벡터 거리순으로 각각 모아 합친다. 질의 낱말 앞뒤의 문장부호와 끝의 조사는 떼고 찾는다("서울에서?" 로 "거주지: 서울" 이 걸린다). 조사는 목록 휴리스틱이라 "~도" 로 끝나는 명사를 자르기도 하지만(중요도 → 중요), 실제 대화와 기억 4천여 건에서 명사를 자른 비율은 0.02% 였다
  • 점수: 최신성, 중요도, 관련성, 단서 겹침의 가중합이고, 종류마다 가중치가 다르다(의미 기억은 관련성 0.6, 일화는 최신성 0.4). 되뇌기 횟수가 많을수록 중요도가 덜 식는다
  • 섬광 기억 자리: 저장된 중요도가 0.8 이상이고 질의 낱말의 절반 이상이 내용에 있는 항목 하나는 나이와 무관하게 결과에 남긴다. 일화는 최신성 비중이 커서, 같은 낱말의 어제 일화 몇 개가 몇 달 전의 큰 사건을 늘 밀어내기 때문이다
  • 출처 회상: 회상 블록의 후보에 든 추출 사실마다 출처 턴(metadata.from_episode)을 저장소에서 꺼내 후보에 더한다. 지워졌거나 회상 필터(사고 로그 제외)에 걸리는 출처는 건너뛴다. 그다음 종류를 가리지 않고 질의 관련도(벡터가 있으면 코사인, 없으면 질의 낱말 비율) 하나로 줄 세워 상위 3건을 넣는다(MemoryService.recall_with_sources, retrieval/scoring.py 의 joint_rank). 사실 하나당 저장소 조회가 한 번 늘고 LLM 호출은 없다. 핵심 사실 블록은 그대로다. MemoryService(source_recall=False) 면 예전처럼 합성 점수로 고른다
  • 되뇌기: 질의에 걸린 항목만 last_accessed 가 갱신된다. 되뇌기 횟수는 그중 답에 쓰인 항목만 오른다(항목 낱말의 절반 이상이 답에 나오면 쓰였다고 본다). 핵심 사실 블록에 들어갔다는 이유만으로는 세지 않는다

장기 기억에 쓰이는 순간​

출처층언제
explicit사용자 프로필(의미)/remember <text>. 중요도 0.8 로 저장한다. 내용에 상대 지칭("그거 기억해")이 있으면 직전 주고받음을 metadata.referent 로 같이 남긴다
extracted사용자 프로필(의미)통합(consolidation)이 일화에서 뽑은 사실
housing_profile사용자 프로필(의미)KAREN 의 housing_profile(action=set). 사용자마다 고정 id 하나로 덮어쓴다
turnEDM(일화)턴이 끝날 때마다. 발화와 답
turn_partEDM(일화)답이 1,000자를 넘는 턴. 나머지 답의 조각(최대 2개)
manual_compactEDM(일화)/compact
thought, journalEDM(일화)사고 로그, 저널
delegationEDM(일화)멀티 에이전트 위임 기록과 오케스트레이션 결과. 위임한 쪽 범위에 남는다
promotedEDM(일화)블랙보드 --promote 결과. 승격한 부모 에이전트 범위에 남는다
task_patternPDM(절차)에이전트가 remember_task_pattern 도구로 사용자가 알려 준 방식을 저장할 때

표에 없는 출처는 EDM 으로 쓴다. 사용자 프로필에는 사용자에 대한 사실만 들어가고, 에이전트가 한 일은 섞이지 않는다.

읽기 권한​

읽기 권한은 층마다 정한다. 항목에 따로 공개 여부를 적지 않는다.

층누가 읽나
사용자 프로필(의미)같은 사용자의 모든 에이전트
EDM(일화)쓴 에이전트, 그리고 정책에 그 에이전트를 적은 에이전트
PDM(절차)쓴 에이전트, 그리고 정책에 그 에이전트를 적은 에이전트

정책은 에이전트 정의 파일(team/<id>.yaml)의 memory.read 에 둔다. 적지 않으면 자기 것만 읽는다. 다른 에이전트의 대화는 기본으로 막고, 절차는 적은 에이전트에게만 연다.

# team/karen.yaml
memory:
read:
edm: [self] # 기본값. self 는 이 에이전트 자신
pdm: [self, main] # JARVIS 의 작업 패턴도 읽는다
  • 권한은 한쪽 방향이다. KAREN 이 main 을 적어도 JARVIS 는 KAREN 의 기억을 읽지 못한다
  • 모르는 키(memory.write 같은 오타)가 있으면 그 정의 파일을 거부한다
  • JARVIS(main)와 서브 에이전트는 정의 파일이 없어 기본 정책, 즉 자기 것만 읽는다
  • 저장소가 강제한다. Postgres 는 사용자 경계를 먼저 보고, 층마다 agent_id = ANY(...) 로 거른다(memory/access.py, stores/postgres.py)

의미 기억이 "주제: 값" 꼴이면 주제 태그가 붙는다. 같은 주제의 이전 사실은 superseded_by 로 가려질 뿐 지워지지 않는다. 주제가 다른데 값이 비슷하면 간섭 후보로 표시만 한다.

작업 패턴​

사용자가 "앞으로 비슷한 요청이 오면 이렇게 해, 기억해" 처럼 일하는 방식을 알려 주면, 에이전트는 그 방식을 작업 패턴으로 저장하고, 다음에 같은 종류의 요청이 오면 그대로 따른다. 사용자에 대한 사실(/remember, 사용자 프로필)과 달리 작업 패턴은 에이전트마다 따로 기억한다. 같은 "예산 추천" 이라도 KAREN 과 JARVIS 가 하는 일이 다르기 때문이다.

  • 저장: 에이전트가 remember_task_pattern(name, when, steps, keywords) 도구(core/tools/task_pattern.py)로 저장한다. 모든 에이전트가 이 도구를 쓰고, 정의의 tools 에 적지 않아도 붙는다. 패턴은 도구를 부른 세션의 에이전트 범위로 저장된다
  • 저장 안내: 사용자 말에 "기억해", "앞으로", "다음부터", "이 방식" 같은 말이 있으면 메모리가 [작업 패턴 저장] 안내 블록을 붙인다. 저장할지는 모델이 정한다
  • 자연 키: (사용자, 에이전트, 이름)이 키다. 같은 이름으로 다시 저장하면 그 항목을 고치므로, 몇 번을 저장해도 하나만 남는다
  • 회상: 요청 글에 패턴의 키워드가 들어 있으면 가장 많이 걸린 패턴 2개까지 [작업 패턴] 블록으로 넣는다. 부분 문자열로 대조하므로 조사가 붙어도 걸린다. 일반 회상 블록([장기 기억])에는 넣지 않는다
  • 격리: 작업 패턴은 PDM 이고 agent_id 가 저장한 에이전트다. 다른 에이전트의 회상이나 /patterns 에는 그 에이전트의 memory.read.pdm 에 저장한 에이전트가 있을 때만 나온다
  • 보기와 지우기: /patterns 는 지금 에이전트의 패턴 목록을 보여주고, /patterns forget <이름> 은 그 패턴을 지운다
  • 실패에서 온 초안: 실패 반성(절차 학습)이 작업 습관(agent_behavior)의 문제로 분류한 교훈은 작업 패턴 초안(~/.jarvis/procedures/pattern-<이름>.md)으로 남는다. /procedures approve pattern-<이름> 으로 승인하면 그 에이전트의 작업 패턴으로 저장된다. 승인 전에는 회상에 들어가지 않는다
  • 구현: memory/long_term/task_pattern.py(순수 함수), MemoryService.remember_pattern, task_patterns, forget_pattern
노트

저장 안내 블록은 실서버에서 관찰한 모델 한계 때문에 넣었다. 모델이 "기억하겠습니다" 라고 말만 하고 아무것도 남기지 않는 일이 있었다.

통합 (consolidation)​

통합은 일화에서 오래 가는 사실을 뽑아 의미 기억으로 올리는 일이다. 여러 번 돌려도 결과가 같다(멱등하다).

  1. turn, manual_compact 일화(그리고 재설계 전에 쓴 session_end, auto_summary 요약) 중 아직 통합되지 않은 것을 최근 50건까지 본다. 턴 일화는 세션별로 턴 순서대로 묶어 LLM 을 한 번만 부른다. 턴 일화는 사용자 발화만 넘긴다. 답까지 넘기면 JARVIS 가 추천한 메뉴("저녁 메뉴: 볶음밥")가 사용자 사실로 뽑혔다
  2. LLM 이 {key, value, importance} 목록을 낸다. 중요도 0.5 미만은 버린다
  3. extracted 사실로 저장하고, 원래 일화에 통합됨 표시를 남긴다. 사실마다 묶음 안에서 사실 낱말이 발화에 가장 많이 나온 턴을 출처(metadata.from_episode)로 남긴다. 겹치는 낱말이 없으면(영어 대화에서 한국어 사실을 뽑은 경우) 임베딩이 가장 가까운 턴을 고른다. 이때만 사실 임베딩이 한 번 더 돈다
  4. 주제가 다른데 내용이 비슷한(0.6 이상) 의미 사실 쌍을 간섭 후보로 표시한다
  5. 같은 작업 종류의 성공 턴이 3회 쌓였으면 절차 초안으로 정제한다(Tool & Approve System → 절차 학습). 승인하면 PDM 절차가 된다
  6. 작업 기억의 되뇌기 횟수를 항목으로 옮기고 비운다

통합은 세션 종료(Web 셧다운) 때 항상 돈다. 유휴 때는 JARVIS_MEMORY__CONSOLIDATE_ON_IDLE(기본 켜짐)이고 사용자가 JARVIS_PROACTIVE__DAEMON_IDLE_SEC(180초)보다 오래 조용하면, 성찰 daemon 틱마다 성찰 뒤에 돈다. 트리거 종류(interval, idle, both)와는 상관없고, 답하는 중인 세션은 건너뛴다. daemon 이 꺼져 있으면 유휴 통합도 돌지 않는다.

임베딩​

JARVIS_MEMORY__EMBEDDING_PROVIDER 를 openai(text-embedding-3-small, 1536), ollama(nomic-embed-text, 768), vllm(모델과 차원 필수) 중 하나로 두면 의미 검색이 켜진다. Postgres 는 pgvector 확장을 만들고 HNSW 인덱스를 시도한다. 확장이 없으면 키워드 검색으로 내려간다. 벡터가 없는 항목은 /remember reembed 로 채운다.

한국어 대화라면 ollama 에서 bge-m3(1024)를 쓴다. 기본값 nomic-embed-text 는 한국어 문장끼리 코사인이 0.68~0.97 로 몰려서 다른 내용도 구분하지 못했다. Open loop의 약속 중복 판정도 같은 임베딩 설정을 쓴다.

사용자 모델과 저널​

  • 사용자 모델 (agent_layer/usermodel/): LLM 이 최근 기억 30건을 요약과 성향(최대 8개)으로 증류한다. /usermodel build 로 만들고 ~/.jarvis/user_model.json 에 덮어쓴다. 모든 사고 프롬프트의 {{user_model}} 에 들어간다. 자동으로는 만들지 않는다
  • 저널 (agent_layer/journal/): 최근 사고 기록 40건을 보고 1 에서 3 문장의 메타 성찰을 쓴다. /journal write 로 실행한다. 일화 기억으로 저장되어 회상에 섞여 들어온다

한계​

  • 끝의 조사는 목록 휴리스틱으로 뗀다. "~도" 로 끝나는 명사를 자르기도 한다(실제 대화와 기억 4천여 건에서 0.02%)
  • 일화의 중요도는 아직 저장할 때 매기지 않아(기본 0.5), 섬광 기억 자리는 지금 /remember(0.8)와 통합이 뽑은 중요한 사실에만 실제로 작동한다
  • 상대 지칭은 직전 주고받음을 붙여 회상할 뿐 LLM 으로 풀어 쓰지 않는다. 이 방식으로 회상이 실패하는 사례가 평가에서 나오면 고쳐 쓰는 단계를 넣는다
  • pgvector 확장이 없으면 의미 검색 없이 키워드 검색으로 내려간다
  • 사용자 모델은 /usermodel build 로만 만들고 자동으로는 만들지 않는다
  • 기능 스위트는 직접 만든 작은 사례와 가짜 임베더다. 공개 벤치마크는 Memory Tax 의 셋(LongMemEval-s, PersonaMem-v2, MINT-Eval)을 층화 표본으로만 쟀다(아래 실험 절)
  • 출처 턴은 낱말 겹침으로 고르고, 겹침이 없을 때만 임베딩으로 고른다
  • 출처 회상을 켜면 회상 블록은 질의 관련도 하나로 고른다. 최근성, 중요도, 섬광 기억 자리는 이 블록에서 쓰지 않는다. 회상 품질 시나리오(아래 실험 절)는 켜고 꺼도 같았지만, 최근 일화를 묻는 회상이 평가에서 밀리면 관련도가 같은 구간에만 합성 점수를 쓰도록 바꾼다
  • 긴 답은 조각 2개(약 3,000자)까지만 남긴다. 그보다 뒤의 증거는 여전히 잃는다

구현 위치​

왼쪽이 사용자와 화면, 가운데가 코드 모듈, 오른쪽이 저장소다. 화살표는 호출이나 데이터의 방향이다.

  • 구현: memory/memory_service.py. 하네스가 턴 앞에서 observe, 답 뒤에서 reinforce 를 부른다
  • 핵심 모듈: memory/
  • 저장: 메모리 또는 Postgres

Performance Experiments​

기억 기능 전체의 정확도, 요약의 사실과 언어 보존, 오래된 기억의 회상을 측정한 결과를 한눈에 보기 쉽도록 정리했다.

기능 스위트​

  • 질문: 범위 격리, 대체, 회상, 읽기 정책 같은 기억 규칙이 구조를 바꾼 뒤에도 지켜지는가
  • 설정: 직접 만든 10개 스위트 68사례. 임베더는 가짜(mock)로 바꿨다. 재설계 전 구조(50 메시지 창과 컴팩션, 가시성 두 값, 일화 metadata 없음)와 같은 사례로 비교했다. bge-m3 로는 회상 품질 개선 전의 49사례까지만 쟀다
  • 결과: 68사례가 가짜 임베더에서 모두 통과한다(재현율 1.00, 다른 범위로 새는 비율 0.00). 두 번 돌려도 출력이 같다
스위트사례재는 것
isolation8다른 사용자, 다른 에이전트, 다른 세션의 기억이 새지 않는가
semantic15같은 주제의 새 사실이 옛 사실을 대체하는가, 조사가 붙은 질의에도 걸리는가
episodic7최근 일화가 먼저 떠오르는가, 오래된 큰 사건이 새 일화에 묻히지 않는가
procedural5절차가 이름과 별칭으로만 떠오르는가
paraphrase6바꿔 말한 질문에도 떠오르는가
faithfulness3/compact 요약 뒤에 심어 둔 사실이 남는가
retention5사실을 말하고 2, 5, 20, 60 턴 뒤에 물어도 모델 컨텍스트에 있는가
episode_filter5작업 종류, 성패, 기간 조건에 맞는 일화만 고르는가(recall_episodes)
access8층별 읽기 정책대로 허락한 기억만 읽는가
consolidation6같은 종류 성공 3회만 절차 정제 후보가 되는가
  • 해석: 재설계 전 구조는 같은 사례에서 access 5/8, episode_filter 0/5, consolidation 4/6, retention 4/5 였다. retention 질문 턴 컨텍스트 합계는 117,786자에서 14,129자가 됐다. 최근 3턴 창과 턴 일화, 층별 읽기 정책으로 바꾼 근거다
  • 한계: 모두 만점이라 모델 비교에는 쓸 수 없다. 사례가 작고 직접 만든 것이라, 구조를 바꿀 때 깨지지 않았는지 보는 회귀 검사로 쓴다. 공개 벤치마크 결과는 아래 "원문 검색보다 나은가" 절에 있다
  • 재현: python -m jarvis.memory.eval --suite all

요약이 사실과 대화 언어를 지키는가​

  • 질문: /compact 요약이 심어 둔 사실을 남기고, 대화와 같은 언어로 쓰이는가
  • 설정: /compact 와 예전 컴팩션이 쓰던 요약기를 쟀다(지금 턴마다 도는 컴팩션은 없다). 한국어 대화 셋에 심어 둔 사실 12개(이름, 요일, 금액, 결정 사항)가 요약에 남는지, 요약이 대화 언어로 쓰이는지를 쟀다. 요약 모델만 바꾸고 판정 모델은 gpt-4o-mini 하나로 고정했다. 모델마다 3회다
  • 결과:
한국어 대화 3개 × 3회 요약, 심어 둔 사실 12개gemma4:e2bgemma4:e4bgpt-4o-mini한국어 요약 (이전 지시문)0.560.220.00한국어 요약 (지금)1.001.001.00낱말 그대로 (이전 지시문)0.720.560.33낱말 그대로 (지금)1.001.001.000.00.51.0비율 (0~1)사실 보존율(판정 모델 gpt-4o-mini 고정)은 지시문을 바꾸기 전과 후 모두 세 모델 1.00
Figure 2. 예전 요약 지시문은 한국어 대화도 대부분 영어로 요약했다(27번 중 20번, gpt-4o-mini 는 9번 모두, e2b 는 한국어 5번). 요약 언어를 코드가 정해 지시문에 못 박자 세 모델 모두 9번 다 한국어로 썼고, 이름과 숫자가 원문 표기 그대로 남았다. 사실 보존은 전후 모두 1.00 이다.
  • 문제: 요약 지시문이 영어("You are a summarization assistant")였다. 요약은 장기 기억으로도 저장되는데, 임베딩 없이 낱말 겹침으로 찾을 때 한국어 질문이 영어 요약("Bori", "12 million KRW")에 닿지 않을 수 있다(재지는 않았다)
  • 첫 시도의 회귀: 지시문을 한국어로 바꾸고 "대화에 주로 쓰인 언어로 쓴다" 고만 적자, 이번에는 gpt-4o-mini 와 e4b 가 영어 대화를 한국어로 요약했다(영어 대화 1개를 각 2번 요약해 확인했고, 그때 출력은 저장하지 않았다). 한국어 지시문에 끌린 것으로 보인다(추정)
  • 해석: 언어는 이제 코드가 대화 본문의 글자(한글과 라틴)를 세어 정하고(conversation_language), 지시문 끝에 "반드시 한국어로" 또는 "in English only" 를 붙인다. 어시스턴트 메시지의 JSON 키는 세지 않고, 한글 한 글자를 라틴 글자 열 개만큼으로 쳐서 코드가 섞인 한국어 대화도 한국어로 본다. 한국어 대화 2개, 영어 대화 2개, 코드가 섞인 한국어 대화 1개를 세 모델이 두 번씩 요약했을 때 모두 대화 언어로 썼다(10/10, scripts/experiments/summary_language_check.py)
  • 세 모델 모두 사실 보존이 1.00 이라 이 실험으로는 모델을 가를 수 없다. 사실 12개짜리 대화 셋은 요약이 잃을 만큼 길지 않다
  • 측정을 고친 내역: 처음에는 요약한 모델이 자기 요약을 판정했고, 판정 지시문이 "사실을 담고 있는가" 를 물었다. 사실 칸이 단서 낱말("화요일")이라 gpt-4o-mini 는 요약에 그대로 있어도 no 라고 했다. 그때의 보존율(요약 모델별 3회 평균 0.44~0.75)은 대부분 판정 오류였다. 지금 판정기는 단서가 가리키는 정보를 언어와 표기를 넘어 묻고, 지시문의 예시는 시험 단서와 겹치지 않는 낱말(서울, 오백만원, 3시)이다. 정답 예시 16개에서 gpt-4o-mini 와 gemma4:e4b 모두 16/16 이다(scripts/experiments/judge_check.py)
  • 한계: 대화 셋, 사실 12개, 언어 확인 대화 5개로 작다. 이미 저장된 영어 요약은 다시 만들지 않았다
  • 재현: uv run python -m scripts.experiments.compaction_faithfulness --model gemma4:e2b-it-qat

오래된 큰 기억과 상대 지칭을 회상하는가​

  • 질문: 같은 낱말의 새 일화가 쌓여도 오래된 큰 사건과 핵심 사실이 떠오르는가, 조사 붙은 질의와 상대 지칭에도 회상이 되는가, 그 비용은 얼마인가
  • 설정: 회상 후보 선정, 섬광 기억 자리, 조사 처리, 상대 지칭 처리를 넣기 전(이전 코드)과 후(지금)를 같은 시나리오로 쟀다. 저장소는 메모리, 임베더는 없음이고, 지연만 pgvector 컨테이너에서 쟀다. 시나리오는 결정적이라 반복하지 않았다
  • 결과:
이전 코드지금점에 마우스를 올리면 값이 보인다(a) 200일 전 큰 사건을 떠올리는 비율가로: 그 뒤 쌓인 같은 낱말의 사소한 일화 수0%50%100%0369122040이전 코드, 0개 뒤: 100%이전 코드, 3개 뒤: 0%이전 코드, 6개 뒤: 0%이전 코드, 9개 뒤: 0%이전 코드, 12개 뒤: 0%이전 코드, 20개 뒤: 0%이전 코드, 40개 뒤: 0%이전 코드 0%지금, 0개 뒤: 100%지금, 3개 뒤: 100%지금, 6개 뒤: 100%지금, 9개 뒤: 100%지금, 12개 뒤: 100%지금, 20개 뒤: 100%지금, 40개 뒤: 100%지금 100%(b) 100일 전 핵심 사실이 고정 블록에 남는 비율가로: 그 뒤 쌓인 사소한 의미 사실 수0%50%100%048122040이전 후보 풀 12개이전 코드, 0개 뒤: 100%이전 코드, 4개 뒤: 100%이전 코드, 8개 뒤: 100%이전 코드, 12개 뒤: 0%이전 코드, 20개 뒤: 0%이전 코드, 40개 뒤: 0%이전 코드 0%지금, 0개 뒤: 100%지금, 4개 뒤: 100%지금, 8개 뒤: 100%지금, 12개 뒤: 100%지금, 20개 뒤: 100%지금, 40개 뒤: 100%지금 100%(c) 시나리오별 적중률0%50%100%조사 붙은 질의 (10개)조사 붙은 질의 (10개), 이전 코드: 0%조사 붙은 질의 (10개), 지금: 100%100%0%"그거 다시 해줘" 뒤 회상 (8개)"그거 다시 해줘" 뒤 회상 (8개), 이전 코드: 0%"그거 다시 해줘" 뒤 회상 (8개), 지금: 100%100%0%"그거 기억해" 로 남긴 기억 (8개)"그거 기억해" 로 남긴 기억 (8개), 이전 코드: 12%"그거 기억해" 로 남긴 기억 (8개), 지금: 100%100%12%엉뚱한 질의에 끼어듦 (8개, 낮을수록 좋음)엉뚱한 질의에 끼어듦 (8개, 낮을수록 좋음): 전후 모두 0%0%, 전후 같음(d) 무관한 10턴의 되뇌기 횟수10이전 코드0지금고정 사실 하나당. 질의에 걸린 것만 센다(e) Postgres 지연 (ms, 낮을수록 좋음)0 ms5 ms10 msrecall p50recall p50, 이전 코드: 2.3recall p50, 지금: 7.37.32.3recall p95recall p95, 이전 코드: 3.0recall p95, 지금: 8.88.83.0fixed_facts p50fixed_facts p50, 이전 코드: 0.7fixed_facts p50, 지금: 2.12.10.7일화 2,000개, pgvector, 질의 200회. LLM 호출 한 번(수백 ms 이상)에 비하면 작다
Figure 3. (a) 이전 코드는 같은 낱말의 새 일화가 3개만 쌓여도 200일 전의 큰 사건을 떠올리지 못했다. 지금은 40개가 쌓여도 떠올린다. (b) 핵심 사실은 새 의미 사실이 12개 쌓이면 고정 블록에서 빠졌다. (c) 조사 붙은 질의, "그거 다시 해줘" 뒤의 회상, "그거 기억해" 로 남긴 기억이 0.00 또는 0.12 에서 1.00 이 됐다. 느슨하게 닮은 질의에 큰 사건이 끼어드는 비율은 전후 모두 0 이다. (d) 무관한 턴에도 쌓이던 되뇌기가 없어졌다. (e) 대신 회상 지연이 2.3 ms 에서 7.3 ms 로 늘었다.
그래프의 수치
지표이전 코드지금
(a) 큰 사건 회상, 뒤에 쌓인 일화 0 / 3 / 6 / 9 / 12 / 20 / 40개100 / 0 / 0 / 0 / 0 / 0 / 0%모두 100%
(b) 핵심 사실 유지, 뒤에 쌓인 사실 0 / 4 / 8 / 12 / 20 / 40개100 / 100 / 100 / 0 / 0 / 0%모두 100%
(c) 조사 붙은 질의0%100%
(c) "그거 다시 해줘" 뒤 회상0%100%
(c) "그거 기억해" 로 남긴 기억12%100%
(c) 엉뚱한 질의에 끼어듦0%0%
(d) 무관한 10턴 동안 고정 사실 하나의 되뇌기10회0회
(e) recall p50 / p952.3 / 3.0 ms7.3 / 8.8 ms
(e) fixed_facts p500.7 ms2.1 ms
  • 원인 (a): 두 가지가 겹쳐 있었다. 저장소가 후보를 최신순 12개로 먼저 잘랐고, 후보에 들더라도 일화 가중치(최신성 0.4, 중요도 0.2)에서는 어제 일화가 0.707, 200일 전 큰 사건이 0.368 이었다. 후보를 최신, 중요도, 거리 세 갈래로 모으고 섬광 기억 자리를 둔 것이 각각을 고친다
  • 원인 (b): 핵심 사실 후보도 최신순 12개였다. 중요도순 후보를 더했다
  • 원인 (c): 키워드 검색이 공백으로만 낱말을 나눠 "서울에서" 가 "서울" 에 닿지 않았다. 지칭만 있는 발화("그거 다시 해줘")에는 찾을 낱말이 없었고, "그거 기억해" 로 남긴 기억에는 주제 낱말이 없었다. 이전 코드의 0.12 는 "그 계획 기억해" 의 "계획" 이 질의와 우연히 겹친 한 건이다
  • 측정이 찾은 빈틈: 처음 구현은 "그거", "이거" 같은 대명사만 지칭으로 봐서 "그 방식 기억해" 를 놓쳤다(지칭으로 남긴 기억 0.88). 띄어 쓴 관형사 "그", "이", "저" 를 더해 1.00 이 됐다
  • 해석 (e): 하위 쿼리 셋이 각각 키워드 일치 행을 훑는다. 두 번씩 더 잰 결과도 2.63.1 ms 대 5.78.0 ms 로 같은 경향이었다. LLM 호출 한 번(수백 ms 이상)에 비하면 작아서 받아들였다
  • 한계: 시나리오는 직접 만든 51개(주제 8개를 쌓인 수마다 반복한 (a) 와 (b), 조사 10, 지칭 8, 지칭으로 남긴 기억 8, 과잉일반화 8, 되뇌기 1)이고 임베더 없이 쟀다. 실제 임베딩 모델과 공개 벤치마크(LongMemEval, LoCoMo)로는 재지 않았다. 일화의 중요도는 아직 저장할 때 매기지 않아(기본 0.5), 섬광 기억 자리는 지금 /remember(0.8)와 통합이 뽑은 중요한 사실에만 실제로 작동한다
  • 재현: uv run python -m scripts.experiments.memory_recall --label after --postgres(이전 코드는 그 커밋을 git worktree 로 받아 PYTHONPATH=<worktree>/src 로 같은 명령을 --label before 로 돌린다)

원문 검색(RAG)보다 나은가: Memory Tax 벤치마크 셋​

  • 질문: Jarvis 가 기억을 만드는 방식(턴 일화, 사실 추출, 합성 점수)은 원문을 그대로 조각 내 찾는 naive RAG 보다 나은가. 무엇을 얻고 무엇을 잃는가. 출처 회상(MT-2)과 긴 답 조각(MT-3)은 잃는 것을 줄이는가
  • 설정:
    • Memory Tax(He et al., 2026)가 쓴 세 벤치마크를 옮겼다. LongMemEval-s는 질문 유형으로 층화한 60문항이다(질문마다 약 50세션). PersonaMem-v2는 text 의 128k 이력에서 사용자 10명을 고르고 선호 유형으로 층화한 60문항이며, 4지선다라 보기 기호로 채점한다. MINT-Eval은 네 영역마다 맥락 글자 수 상한 아래 묶음 2개에서 10문항씩, 모두 40문항이다. seed 는 0 이다
    • 비교군은 넷이다. rag 는 논문 부록의 조각 규칙(세션 안에서 짧은 턴을 약 1,024자로 묶고 마지막 턴을 겹침)으로 원문을 조각 내 임베딩 코사인 top-k 만 쓴다. raw 는 턴마다 일화 하나를 Jarvis 합성 점수로 회상한다. turns 는 실서비스 경로다(턴 일화, 긴 답 조각, 세션마다 gpt-4o-mini 로 사실 추출). turns+src 는 같은 메모리에서 출처 회상을 켠다. LongMemEval-s 에서만 처음 시도한 발췌 방식(turns+excerpt)과 조각 없는 이전 코드도 쟀다
    • 같은 답 프롬프트, 같은 판정기(gpt-4o-mini, temperature 0), 같은 임베딩(bge-m3)을 쓴다. 논문은 노출 토큰이 비교군마다 크게 다르지 않게 top-k 를 맞췄다(Retrieval Scope). 여기서는 더 엄격하게, 비교군마다 모델에 보이는 기억 글자 수를 RAG 조각 5개분(5,120자)으로 같게 했다. 순위대로 담다가 예산을 넘는 항목은 남은 만큼 자른다. 그래서 짧은 사실이 많은 turns 는 평균 127항목, rag 는 7항목이다
    • 실서비스와 같은 조건(핵심 사실 3건과 회상 3건, 예산 없음)으로도 turns 와 turns+src 를 쟀다
    • 추출 사실의 증거 재현율은 그 사실의 출처 턴으로 센다. MINT-Eval 은 정답 맥락 라벨이 없어 재현율을 내지 않는다
  • 결과:
(a) 비교군별 정답률. 노출 예산 5,120자, 판정 gpt-4o-minirag (기준선)rawturnsturns+srcLongMemEval-s0.570.530.430.52PersonaMem-v20.420.380.350.37MINT-Eval0.200.070.070.070.00.51.0정답률 (0~1)(b) RAG 대비 질문 단위 benefit 과 tax(질문 수 대비 비율)LongMemEval-s, rawnet -3%LongMemEval-s, turnsnet -13%LongMemEval-s, turns+srcnet -5%PersonaMem-v2, rawnet -3%PersonaMem-v2, turnsnet -7%PersonaMem-v2, turns+srcnet -5%MINT-Eval, rawnet -12%MINT-Eval, turnsnet -12%MINT-Eval, turns+srcnet -12%30%15%0%15%30%tax (RAG 는 맞고 비교군은 틀림)benefit (RAG 는 틀리고 비교군은 맞음)(c) 턴 일화에 정답이 남는 비율: 1,000자 자르기(이전 코드)와 조각 일화(지금)이전 코드지금0%50%100%답(assistant) 정답 턴답(assistant) 정답 턴, 이전 코드: 92%답(assistant) 정답 턴, 지금: 100%100%92%발화(user) 정답 턴발화(user) 정답 턴: 전후 모두 100%100%, 전후 같음전체 정답 턴전체 정답 턴, 이전 코드: 99%전체 정답 턴, 지금: 100%100%99%turns 정답률 (표본 60)turns 정답률 (표본 60), 이전 코드: 42%turns 정답률 (표본 60), 지금: 43%43%42%LongMemEval-s 500문항, 정답 낱말이 원문에 있는 정답 턴 368개(답 50개)(d) 실서비스 블록(핵심 사실 3 + 회상 3): 출처 회상 없음과 있음이전 코드지금0%50%100%정답률정답률, 이전 코드: 28%정답률, 지금: 50%50%28%증거 세션 재현율증거 세션 재현율, 이전 코드: 70%증거 세션 재현율, 지금: 92%92%70%증거 턴 재현율증거 턴 재현율, 이전 코드: 52%증거 턴 재현율, 지금: 81%81%52%LongMemEval-s 60문항. 출처 회상이 새로 맞힌 문항 13, 잃은 문항 0
Figure 4. (a) 세 벤치마크 모두 RAG 가 가장 높다. (b) 모든 비교군의 순이득이 음수다. LongMemEval-s 에서 turns 는 11문항을 잃고 3문항을 얻었고, 출처 회상을 켜면 잃는 문항이 6개로 준다. (c) 1,000자 자르기는 답 쪽 정답 턴의 8% 에서 정답을 지웠고, 조각 일화가 이를 100% 로 되돌렸다. 정답률 차이는 표본 60에서 1문항이다. (d) 실서비스 블록 조건에서 출처 회상은 정답률을 28% 에서 50% 로 올렸고 잃은 문항은 없었다.
그래프의 수치
벤치마크비교군정답률증거 세션 재현율증거 턴 재현율평균 항목RAG 대비 benefit / tax / net
LongMemEval-s (60)rag0.570.980.967.0기준
raw0.530.970.968.91 / 3 / -2
turns0.431.000.81127.33 / 11 / -8
turns+src0.520.950.837.73 / 6 / -3
turns+excerpt0.320.800.6725.23 / 18 / -15
turns, 조각 없음(이전 코드)0.421.000.83131.54 / 13 / -9
PersonaMem-v2 (60)rag0.420.480.356.1기준
raw0.380.650.4013.63 / 5 / -2
turns0.350.500.407.12 / 6 / -4
turns+src0.370.500.407.73 / 6 / -3
MINT-Eval (40)rag0.20--4.5기준
raw, turns, turns+src0.075--16.6 / 55.9 / 16.41 / 6 / -5
LongMemEval-s 실서비스 블록turns0.280.700.526.03 / 20 / -17
turns+src0.500.920.816.04 / 8 / -4

턴 일화 구성 손실(LongMemEval-s 500문항, 정답 낱말이 원문에 있는 정답 턴): 답 쪽 46/50(92%) 에서 50/50, 발화 쪽 318/318, 전체 364/368(98.9%) 에서 368/368.

  • 해석:
    • 논문과 같은 결론이다. 이 벤치마크들은 원문 검색만으로 잘 풀리는 설정이라 기억을 만드는 비용(tax)이 benefit 보다 크게 보인다. Jarvis 의 순이득은 세 벤치마크 모두 음수다
    • turns 의 tax 는 주로 순위에서 온다. 추출 사실은 짧고 관련도 가중치가 커서 예산을 사실 120여 개가 채우고, 증거가 담긴 턴 일화는 끝으로 밀린다(턴 일화는 질문당 평균 1.9개만 보였다). 추출기가 한국어 지시문을 받아 영어 대화에서 한국어 사실을 뽑는 것도 일탈이다("사용자 관계: 친구 Emma에게 Negroni 만드는 법 배움")
    • 처음 시도한 발췌 방식이 turns 보다 나빴던 것은 버그가 아니라 설계 탓이다. 문항을 열어 보니 사실마다 300자 발췌가 붙어 같은 예산에 들어가는 항목이 127개에서 25개로 줄었고, 그 25개가 모두 사실이라 턴 일화가 하나도 남지 않은 문항이 많았다(턴 일화 평균 0.28개). 출처를 사실에 붙이는 대신 논문처럼 출처 턴을 후보로 올려 같은 잣대로 다시 매기자 tax 가 11 에서 6 으로 줄었다
    • 실서비스 블록은 회상이 3건뿐이라, 합성 점수에서는 짧은 사실 3개가 자리를 다 차지한다. 출처 회상은 그 자리에 증거 턴을 올려 13문항을 새로 맞혔다. 대신 프롬프트에 들어가는 기억이 평균 181자에서 2,211자로 늘었다
    • 긴 답 조각은 구성 손실을 없앴지만 정답률 차이는 1문항이라 표본으로는 가를 수 없다. 손실이 답 쪽 정답 턴(전체의 6%)에만 있었기 때문이다
    • MINT-Eval 에서 Jarvis 비교군은 모두 같은 점수다. 상태 추적과 순서를 묻는 질문이 많은데, 합성 점수와 관련도 순위는 시간 순서를 지키지 않는다. RAG 는 조각 안에 연속한 맥락이 남는다
  • 논문과 다른 점: 논문은 BM25 와 임베딩 검색을 합치고 재순위 모델을 썼다. 여기서는 임베딩 코사인만 쓴다. 노출은 Retrieval Scope 비율이 아니라 같은 글자 예산으로 맞췄다. 판정 모델은 Qwen3.6-27B 가 아니라 gpt-4o-mini 다. PersonaMem-v2 는 이력에 세션이 없어 40메시지씩 묶어 가짜 세션으로 두었고(턴 비교군의 통합 단위), 페르소나 전체를 담은 system 턴은 정답이 새므로 뺐다
  • 한계: 표본이 작아(60, 60, 40) 1~2문항 차이는 잡음과 구분되지 않는다. 판정과 답은 같은 모델이다. 실서비스 블록 조건은 노출량이 비교군마다 다르다. 회상 품질 시나리오(위 절)는 출처 회상을 켜도 같은 값이었지만, 최근 일화를 우선하는 회상은 이 벤치마크로 재지 않았다
  • 재현: uv run python -m scripts.experiments.memory_net_value. 데이터는 ~/.jarvis/benchmarks/ 에 둔다(PersonaMem-v2 의 이력은 스크립트가 고른 10명분만 받는다. MINT-Eval 은 parquet 를 jsonl 로 바꿔 둔다). 임베딩, 사실 추출, 판정은 ~/.jarvis/benchmarks/cache/ 에 캐시되어 다시 돌리면 같은 수치가 나온다

참고 문헌​

  • Xuan He, Sining Zhoubian, Ruozhen Yang, et al. 2026. Memory Tax: Dissecting the Net Value of Agent Memory. arXiv:2610.34073
  • Di Wu, Hongwei Wang, Wenhao Yu, Yuwei Zhang, Kai-Wei Chang, Dong Yu. 2025. LongMemEval: Benchmarking Chat Assistants on Long-Term Interactive Memory. arXiv:2410.10813
  • Bowen Jiang, Yuan Yuan, Maohao Shen, et al. 2025. PersonaMem-v2: Towards Personalized Intelligence via Learning Implicit User Personas and Agentic Memory. arXiv:2512.06688
  • Hyunji Lee, Justin Chih-Yao Chen, Joykirat Singh, Zaid Khan, Elias Stengel-Eskin, Mohit Bansal. 2026. MINTEval: Evaluating Memory under Multi-Target Interference in Long-Horizon Agent Systems. arXiv:2605.18565