멀티턴 메모리 에이전트
uv run llamon agent my-agent --template agent-general --memory postgres --yescd my-agentuv 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는 검증 오류가 납니다.
PostgreSQL / in-memory 켜기
섹션 제목: “PostgreSQL / in-memory 켜기”# 운영 기본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.yml에postgres서비스가 자동 추가됩니다.- 기본 DB 이름은
<project_name>_memory입니다. uv run llamon run .으로 실행하면 compose의POSTGRES_MEMORY_DSN이 컨테이너 주소로 맞춰집니다.
외부 PostgreSQL을 쓰면 .env의 DSN만 바꾸면 됩니다.
AGENT_ID=my-agentPOSTGRES_MEMORY_DSN=postgresql://user:password@db-host:5432/my_agent_memory코드에서 직접 설정할 때는 MemoryConfig를 넘깁니다.
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는 .env에 AGENT_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는 끄세요.
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_llm | window_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입니다.
MemoryConfig( enabled=True, backend="postgres", window_size=20, summarize=True, summarize_threshold=40,)Registry 기반 구성이면 Memory 엔티티 config에 같은 키(summarize,
summarize_threshold)를 넣고, 실행 중에는 관리 API의
PATCH /api/v1/memory/config로 바꿀 수 있습니다.
| 옵션 | 기본값 | 동작 |
|---|---|---|
summarize | False | True면 윈도 밖 히스토리를 요약해 현재 prompt-context 모드로 전달. False면 밀려난 히스토리는 LLM 입력에서 그대로 소실 |
summarize_threshold | 40 | 전체 메시지 수가 이 값 이상이 되면 첫 요약 생성. 이후 threshold - window_size개가 새로 윈도 밖으로 밀릴 때마다 재요약. threshold <= window_size로 잘못 두면 window_size가 재요약 주기로 대신 쓰임 (매 호출 재요약 방지) |
비용·동작 특성:
- 요약 LLM 호출은 재요약 주기마다 1회입니다(기본 설정 기준 20턴마다). 매 턴 호출되지 않습니다. 재요약도 “이전 요약 + 새로 밀려난 구간”만 보내는 증분 방식이라 대화가 길어져도 요약 비용이 커지지 않습니다.
- 요약은
persistent_memory의summary키에 upsert로 캐시됩니다. 재요약해도 행이 누적되지 않고,postgres면 재시작 후에도 유지됩니다. - 켜면 모델 호출마다 요약 캐시 조회(단건 key-value 읽기)가 1회 발생합니다.
- 요약 생성이 실패하면 직전 캐시(없으면 요약 생략)로 계속 진행합니다. 요약 경로의 어떤 실패도 기존 window 동작을 깨지 않습니다.
MEMORY_PERSIST_TRIM_KEEP_GROUPS로 저장본을 잘라내는 배포에서도 요약 캐시는 남으므로, 잘려나간 turn의 맥락을 요약 블록이 보존합니다.
선택적 fact recall (MEMORY_FACTS_MODE)
섹션 제목: “선택적 fact recall (MEMORY_FACTS_MODE)”기본 세션 메모리는 최근 messages window만 LLM에 보냅니다. MEMORY_FACTS_MODE를 켜면 사용자 발화에서 오래 유지할 만한 사실을 별도 저장하고, 후속 질문과 맞는 fact를 현재 prompt-context 모드로 전달합니다. 기본 legacy_system은 system suffix, stable_tail은 비영속 runtime context를 사용합니다.
기본값은 off입니다.
MEMORY_FACTS_MODE=heuristicMEMORY_FACT_RECALL_MAX_ITEMS=3| 값 | 동작 |
|---|---|
off | fact 추출·recall 비활성 |
heuristic | 표준 라이브러리 규칙으로 사용자 사실 추출 |
llm | 설정된 LLM으로 fact 추출 시도, 실패 시 graceful fallback |
범위는 thread 단위입니다. 같은 message.contextId 안에서만 recall되고, 저장소는 기존 persistent_memory 백엔드를 재사용합니다. postgres면 재시작 후에도 유지되고, in-memory면 프로세스 재시작 시 사라집니다.
in-memory에서 PostgreSQL로 바꾸기
섹션 제목: “in-memory에서 PostgreSQL로 바꾸기”전환할 때는 설정만 바꾸면 됩니다. PostgreSQL 백엔드 패키지는 SDK core에 들어 있으므로 보통 pyproject.toml을 손댈 필요가 없습니다.
MemoryConfig( enabled=True, backend="postgres", postgres_dsn=settings.POSTGRES_MEMORY_DSN,)이미 떠 있던 in-memory 세션은 프로세스 메모리에만 있으므로 PostgreSQL로 자동 이관되지 않습니다. 보통 새 배포부터 PostgreSQL 저장을 시작하고, 기존 in-memory 세션은 종료합니다.
폐쇄망 배포 산출물을 다시 만들 때는 wheelhouse를 갱신하세요.
uv lock --refreshuv 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로 정리할 수 있습니다.
uv run llamon memory prune --dsn postgresql://... --older-than-days 30uv run llamon memory prune --dsn postgresql://... --thread-id user-session-abc --applyPgBouncer 호환성
섹션 제목: “PgBouncer 호환성”PgBouncer 뒤의 PostgreSQL을 쓸 때는 session 모드가 기본 권장입니다. 메모리 경로는 LangGraph 체크포인터와 SDK의 persistent_memory CRUD를 함께 쓰기 때문입니다.
활성 컨테이너 1개는 peak 기준 최대 5개 연결을 잡을 수 있습니다.
| 풀 | 용도 | 최대 연결 |
|---|---|---|
psycopg_pool | LangGraph 체크포인터 | 2 |
asyncpg | persistent_memory CRUD | 3 |
idle 에이전트는 min_size=0이라 슬롯을 점유하지 않습니다. PgBouncer pool size는 “전체 배포 수”가 아니라 동시에 트래픽을 받는 활성 컨테이너 수 기준으로 잡으세요.
transaction 모드가 필요하면 MEMORY_PG_STATEMENT_CACHE_SIZE=0으로 prepared statement cache를 끄고, 실제 워크로드로 검증한 뒤 운영에 반영하세요.
관련 문서
섹션 제목: “관련 문서”- 비즈니스 노드용 공유 풀: PostgreSQL 공유 풀 (asyncpg)
- 관리 API: 런타임 API — 관리 API
- 로컬 실행/점검: 로컬 실행/점검 (llamon run/doctor/prepare-offline)
- Registry 기반 구성: Registry 기반 에이전트 구성