콘텐츠로 이동

멀티턴 메모리 에이전트

Terminal window
uv run llamon agent my-agent --template agent-general --memory postgres --yes
cd my-agent
uv run llamon run .

메모리는 별도 템플릿이 아니라 agent-*, flow-* scaffold의 --memory 옵션입니다. 대화를 같은 세션으로 이어가려면 매 요청에 같은 message.contextId를 보내면 됩니다.


모드용도재시작 후 유지
off메모리 사용 안 함아니오
in-memory로컬 테스트아니오
postgres운영 기본 권장
sqlite레거시 호환

운영 기본값은 postgres입니다. in-memory는 프로세스가 죽으면 세션도 사라지므로 실험용으로만 쓰는 편이 안전합니다. sqlite는 기존 프로젝트와 명시적 CLI 호출의 레거시 호환용입니다. 대화형 CLI와 Studio의 신규 Scaffold 선택지에는 노출하지 않으며, 새 프로젝트는 postgres를 사용하세요.

표의 모드 이름은 CLI --memory 값입니다. 코드의 MemoryConfig(backend=...)에는 언더스코어 표기 in_memory를 넣습니다 — in-memory는 검증 오류가 납니다.


Terminal window
# 운영 기본
uv run llamon agent my-agent --template agent-general --memory postgres --yes
# 짧은 실험
uv run llamon agent my-agent --template agent-general --memory in-memory --yes

--memory postgres 프로젝트는:

  • PostgreSQL 백엔드 의존성이 SDK core에 기본 포함됩니다. 별도 extras는 필요 없습니다.
  • compose.ymlpostgres 서비스가 자동 추가됩니다.
  • 기본 DB 이름은 <project_name>_memory입니다.
  • uv run llamon run .으로 실행하면 compose의 POSTGRES_MEMORY_DSN이 컨테이너 주소로 맞춰집니다.

외부 PostgreSQL을 쓰면 .env의 DSN만 바꾸면 됩니다.

.env
AGENT_ID=my-agent
POSTGRES_MEMORY_DSN=postgresql://user:password@db-host:5432/my_agent_memory

코드에서 직접 설정할 때는 MemoryConfig를 넘깁니다.

app/config.py
MemoryConfig(
enabled=True,
backend="postgres",
postgres_dsn=settings.POSTGRES_MEMORY_DSN,
window_size=20,
)

같은 사용자의 대화를 이어가려면 매 요청에 같은 message.contextId를 보내야 합니다. 런타임 내부에서는 이 값을 thread_id로 씁니다.

{
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"message": {
"contextId": "user-alice-session",
"role": "user",
"parts": [{"kind": "text", "text": "이전에 뭐 얘기했지?"}]
}
}
}

contextId가 없거나 매번 바뀌면 요청마다 새 세션으로 처리됩니다. scaffold는 .envAGENT_ID=<project_name>을 기록하고, 공유 DB에서는 AGENT_ID + thread_id 조합으로 소유권을 구분합니다.


멀티턴 대화 가이드 (history_aware)

섹션 제목: “멀티턴 대화 가이드 (history_aware)”

같은 contextId의 후속 turn에서는 checkpointer가 이전 messages를 복원합니다. SDK는 2턴 이상부터 system prompt 끝에 이전 대화 참고 안내를 자동으로 덧붙입니다. 기본값은 history_aware=True입니다.

이 안내는 system_text가 있고, 이전 AIMessage가 있고, history_aware=True일 때만 붙습니다. 분류기·JSON-only·one-shot 검증처럼 이전 대화를 보면 안 되는 agent는 끄세요.

app/config.py
MemoryConfig(
enabled=True,
backend="postgres",
history_aware=False,
)

LLM 호출 직전에는 다음 INFO 로그가 남습니다.

multiturn: thread_msgs=4 window=20 sent_to_llm=4 has_prior=true
신호확인할 것
has_prior=false인데 멀티턴 기대contextId가 turn마다 같은지 확인
thread_msgs > sent_to_llmwindow_size 때문에 잘림
has_prior=true인데 이전 대화 미반영prompt 또는 agent 지시문 확인

저장된 message 전체가 매번 모델로 들어가지는 않습니다. provider 호출 직전 SDK가 현재 system prompt를 다시 만들고 최근 message group만 llm_input_messages로 넘깁니다.

선별 규칙이유
최신 user turn 보존방금 들어온 요청이 window 밖으로 밀리지 않게 함
AIMessage(tool_calls)ToolMessage를 한 묶음으로 유지orphan tool result 방지
오래된 group부터 제외최근 대화를 우선하면서 turn 경계를 보존
system prompt 재조립현재 skill·guardrail과 prompt-context 모드 반영

따라서 실제 message 수는 window_size와 정확히 같지 않을 수 있습니다. MAX_HISTORY_TOKENS 계열 cap을 켜도 같은 group 경계를 유지하며 오래된 대화부터 줄입니다. 저장본 자체를 줄이는 MEMORY_PERSIST_TRIM_KEEP_GROUPS는 되돌릴 수 없는 삭제이므로 입력 window 제한과 구분하세요.

외부 workflow host가 새 contextId에 기존 대화를 넘겨야 할 때는 A2A metadata.history를 사용할 수 있습니다. SDK는 checkpointer가 비어 있는 첫 호출에만 기본 최대 50개 text message를 시드하고, 이후에는 저장 state를 우선합니다. 운영별 상한은 A2A_HISTORY_MAX_MESSAGES로 조절합니다. 요청 모양과 신뢰 경계는 런타임 API — 외부 히스토리 시드를 참고하세요.

LLAMON_PROMPT_CONTEXT_MODE=stable_tail을 켜도 위 조건과 저장 규칙은 같습니다. 이 모드에서 knowledge, recalled facts와 summary만 최신 실제 user message 뒤의 비영속 runtime context로 이동합니다. 첫 턴에서 둘째 턴으로 갈 때는 기존 history 안내가 생겨 system prefix가 한 번 바뀌고, 이후 같은 operator·skill·tools 조건에서는 안정됩니다. 자세한 cache 경계는 프롬프트 권한 분리와 provider cache를 참고하세요.


윈도 밖 히스토리 요약 (summarize)

섹션 제목: “윈도 밖 히스토리 요약 (summarize)”

기본 세션 메모리는 최근 window_size개 메시지만 LLM에 보내고, 그 밖으로 밀려난 이전 대화는 LLM 입력에서 빠집니다. summarize=True를 켜면 밀려난 히스토리를 에이전트의 LLM으로 요약합니다. 기본 legacy_system에서는 system prompt 끝에 [이전 대화 요약] 블록으로 주입하고, stable_tail에서는 비영속 runtime context로 전달합니다. 저장된 체크포인트(원본 대화)는 건드리지 않습니다.

기본값은 False입니다.

app/config.py
MemoryConfig(
enabled=True,
backend="postgres",
window_size=20,
summarize=True,
summarize_threshold=40,
)

Registry 기반 구성이면 Memory 엔티티 config에 같은 키(summarize, summarize_threshold)를 넣고, 실행 중에는 관리 APIPATCH /api/v1/memory/config로 바꿀 수 있습니다.

옵션기본값동작
summarizeFalseTrue면 윈도 밖 히스토리를 요약해 현재 prompt-context 모드로 전달. False면 밀려난 히스토리는 LLM 입력에서 그대로 소실
summarize_threshold40전체 메시지 수가 이 값 이상이 되면 첫 요약 생성. 이후 threshold - window_size개가 새로 윈도 밖으로 밀릴 때마다 재요약. threshold <= window_size로 잘못 두면 window_size가 재요약 주기로 대신 쓰임 (매 호출 재요약 방지)

비용·동작 특성:

  • 요약 LLM 호출은 재요약 주기마다 1회입니다(기본 설정 기준 20턴마다). 매 턴 호출되지 않습니다. 재요약도 “이전 요약 + 새로 밀려난 구간”만 보내는 증분 방식이라 대화가 길어져도 요약 비용이 커지지 않습니다.
  • 요약은 persistent_memorysummary 키에 upsert로 캐시됩니다. 재요약해도 행이 누적되지 않고, postgres면 재시작 후에도 유지됩니다.
  • 켜면 모델 호출마다 요약 캐시 조회(단건 key-value 읽기)가 1회 발생합니다.
  • 요약 생성이 실패하면 직전 캐시(없으면 요약 생략)로 계속 진행합니다. 요약 경로의 어떤 실패도 기존 window 동작을 깨지 않습니다.
  • MEMORY_PERSIST_TRIM_KEEP_GROUPS로 저장본을 잘라내는 배포에서도 요약 캐시는 남으므로, 잘려나간 turn의 맥락을 요약 블록이 보존합니다.

기본 세션 메모리는 최근 messages window만 LLM에 보냅니다. MEMORY_FACTS_MODE를 켜면 사용자 발화에서 오래 유지할 만한 사실을 별도 저장하고, 후속 질문과 맞는 fact를 현재 prompt-context 모드로 전달합니다. 기본 legacy_system은 system suffix, stable_tail은 비영속 runtime context를 사용합니다.

기본값은 off입니다.

.env
MEMORY_FACTS_MODE=heuristic
MEMORY_FACT_RECALL_MAX_ITEMS=3
동작
offfact 추출·recall 비활성
heuristic표준 라이브러리 규칙으로 사용자 사실 추출
llm설정된 LLM으로 fact 추출 시도, 실패 시 graceful fallback

범위는 thread 단위입니다. 같은 message.contextId 안에서만 recall되고, 저장소는 기존 persistent_memory 백엔드를 재사용합니다. postgres면 재시작 후에도 유지되고, in-memory면 프로세스 재시작 시 사라집니다.


전환할 때는 설정만 바꾸면 됩니다. PostgreSQL 백엔드 패키지는 SDK core에 들어 있으므로 보통 pyproject.toml을 손댈 필요가 없습니다.

app/config.py
MemoryConfig(
enabled=True,
backend="postgres",
postgres_dsn=settings.POSTGRES_MEMORY_DSN,
)

이미 떠 있던 in-memory 세션은 프로세스 메모리에만 있으므로 PostgreSQL로 자동 이관되지 않습니다. 보통 새 배포부터 PostgreSQL 저장을 시작하고, 기존 in-memory 세션은 종료합니다.

폐쇄망 배포 산출물을 다시 만들 때는 wheelhouse를 갱신하세요.

Terminal window
uv lock --refresh
uv run llamon prepare-offline . --clean

  • checkpoints, checkpoint_blobs, checkpoint_writes는 LangGraph가 관리합니다.
  • persistent_memory는 SDK 장기 기억 저장용 테이블이며, fact recall도 같은 백엔드를 씁니다.
  • MEMORY_WINDOW_SIZE는 LLM에 보낼 최근 메시지 수를 제한합니다.
  • MEMORY_PERSIST_TRIM_KEEP_GROUPS는 저장본 자체를 줄입니다. 오래된 turn을 영구 삭제하므로 신중히 켜세요.
  • 비즈니스 노드용 일반 asyncpg 풀은 메모리 백엔드와 별개입니다. 해당 풀은 PostgreSQL 공유 풀을 보세요.

오래된 thread는 CLI로 정리할 수 있습니다.

Terminal window
uv run llamon memory prune --dsn postgresql://... --older-than-days 30
uv run llamon memory prune --dsn postgresql://... --thread-id user-session-abc --apply

PgBouncer 뒤의 PostgreSQL을 쓸 때는 session 모드가 기본 권장입니다. 메모리 경로는 LangGraph 체크포인터와 SDK의 persistent_memory CRUD를 함께 쓰기 때문입니다.

활성 컨테이너 1개는 peak 기준 최대 5개 연결을 잡을 수 있습니다.

용도최대 연결
psycopg_poolLangGraph 체크포인터2
asyncpgpersistent_memory CRUD3

idle 에이전트는 min_size=0이라 슬롯을 점유하지 않습니다. PgBouncer pool size는 “전체 배포 수”가 아니라 동시에 트래픽을 받는 활성 컨테이너 수 기준으로 잡으세요.

transaction 모드가 필요하면 MEMORY_PG_STATEMENT_CACHE_SIZE=0으로 prepared statement cache를 끄고, 실제 워크로드로 검증한 뒤 운영에 반영하세요.