콘텐츠로 이동

멀티턴 메모리 에이전트

메모리는 별도 템플릿이 아니라 agent-*, flow-* scaffold에서 --memory 옵션으로 켭니다. Registry 기반(agent-general 등)이 기본이며, 직접 모델 연결 템플릿(agent-local/agent-openai/agent-anthropic)에서도 동일하게 동작합니다.

이 페이지는 대화를 세션 단위로 이어가고 싶거나, postgresin-memory 중 무엇을 쓸지 결정할 때 보면 됩니다.


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

기본 추천은 postgres입니다.


  • A2A 세션 키는 message.contextId
  • 런타임 내부에서는 이를 thread_id로 사용
  • scaffold는 .envAGENT_ID=<project_name>를 자동 기록
  • 공유 DB에서는 AGENT_ID + thread_id 조합으로 소유권을 구분

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

이 경우:

  • PostgreSQL 백엔드 의존성(psycopg[binary], langgraph-checkpoint-postgres, asyncpg)은 SDK core에 기본 포함 — 별도 extras 불필요
  • docker-compose.local.ymlpostgres 서비스가 자동 추가됨
  • 기본 DB 이름은 <project_name>_memory
  • docker compose up 실행 시 POSTGRES_MEMORY_DSN@postgres:5432/...로 자동으로 덮어써지므로 .env 값을 건드리지 않아도 동작합니다 (외부 DB 사용 시에만 .env 교체 필요)
Terminal window
uv run llamon agent my-agent --template agent-general --memory in-memory --yes

짧은 실험에는 충분하지만 프로세스를 다시 띄우면 대화가 사라집니다.


.env에서 POSTGRES_MEMORY_DSN만 바꾸면 됩니다.

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

Kubernetes나 별도 DB를 쓰는 경우도 같은 방식입니다.


in-memory에서 PostgreSQL로 바꾸려면

섹션 제목: “in-memory에서 PostgreSQL로 바꾸려면”

운영으로 넘어가면서 in-memorypostgres로 바꾸는 경우가 많습니다.

핵심은 두 가지입니다.

  • 의존성은 그대로 두면 됩니다 — PostgreSQL 백엔드 패키지는 이미 SDK core에 포함돼 있습니다.
  • in-memory에 있던 기존 세션은 자동으로 PostgreSQL로 옮겨지지 않습니다.
pyproject.toml
dependencies = [
"llamon-agent",
"uvicorn",
]

lock / wheelhouse 기준으로 아래 패키지들이 core 의존성으로 기본 포함됩니다.

  • asyncpg
  • langgraph-checkpoint-postgres
  • psycopg
  • psycopg-binary
  • psycopg-pool

SQLite 레거시 백엔드를 유지해야 하는 경우에만 uv add "llamon-agent[memory]" extras가 필요합니다.

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

코드에서 직접 주입한다면:

MemoryConfig(
enabled=True,
backend="postgres",
postgres_dsn="postgresql://user:password@db-host:5432/my-agent_memory",
window_size=20,
history_aware=True, # 기본값 — 멀티턴 가이드 자동 prepend
)

폐쇄망 배포를 쓴다면 기존 wheelhouse를 그대로 쓰면 안 됩니다.

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

이후 wheelhouse에는 최소한 아래가 보여야 합니다.

  • asyncpg
  • langgraph-checkpoint-postgres
  • psycopg-binary

in-memory는 프로세스 메모리라서, 이미 떠 있던 세션 데이터는 앱 종료 시 사라집니다.

즉 이 전환은 보통:

  1. 새 배포부터 PostgreSQL 저장 시작
  2. 기존 in-memory 세션은 종료

이 흐름으로 봐야 합니다.

기존 세션 기록까지 유지해야 하면 애플리케이션 레벨 export/import를 별도로 만들어야 합니다.


같은 사용자의 대화를 이어가려면 매 요청에 같은 message.contextId를 보내야 합니다.

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

contextId가 없으면 요청마다 새 세션으로 처리됩니다.


멀티턴 대화 가이드 (history_aware)

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

같은 contextId 로 들어오는 후속 turn은 checkpointer가 이전 messages를 자동 복원합니다. 사용자 prompt가 이전 대화를 참고하지 않는 형태일 때 LLM이 대화 이력을 무시하지 않도록, SDK는 2턴 이상부터 system_prompt 끝에 “이전 대화 참고” 안내를 자동으로 덧붙입니다 (history_aware=True 가 기본값).

3중 가드(system_text 존재 + AIMessage 1개 이상 + history_aware=True) 모두 충족 시에만 활성. 1턴·stateless·옵트아웃은 기존 동작과 동일.

분류기·JSON-only·one-shot 검증처럼 이전 대화를 보지 않는 게 정답인 agent 는 옵트아웃:

app/config.py
MemoryConfig(
enabled=True,
backend="postgres",
history_aware=False, # 이전 대화 미참고 — 옵트아웃
)

매 LLM 호출 직전 한 줄을 INFO 로그로 emit:

multiturn: thread_msgs=4 window=20 sent_to_llm=4 has_prior=true
신호해석
has_prior=false 인데 멀티턴 기대contextId가 매 turn 다름 또는 thread_id 미전달
thread_msgs > sent_to_llmwindow가 작아 잘림 → window_size 상향 또는 summarize=True
has_prior=true 인데 응답에 history 미반영prompt 자체 문제 — history_aware 가이드가 보조함

  • checkpoints, checkpoint_blobs, checkpoint_writes는 LangGraph가 관리
  • persistent_memory는 SDK의 장기 기억 저장용 테이블
  • llamon memory prune으로 오래된 thread를 정리 가능

예:

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

운영 환경에서 PostgreSQL을 PgBouncer 뒤에 두는 경우 SDK는 session 모드 전제로 동작합니다. 이는 AsyncPostgresSaver(LangGraph 체크포인터)와 asyncpg(영구 메모리 CRUD) 모두 prepared statement / 장기 트랜잭션을 사용하기 때문입니다.

SDK 내부 풀 옵션 (수정 불가, 참고용)

섹션 제목: “SDK 내부 풀 옵션 (수정 불가, 참고용)”

PostgresBackend는 다음 값을 모듈 상수로 하드코딩합니다 — 모든 에이전트가 동일 SDK로 만들어지는 전제이므로 운영팀이 인스턴스별로 튜닝하지 않습니다.

minmaxlifetimeidle 회수
psycopg_pool (체크포인터)021800s300s
asyncpg (영구 메모리)03300s
  • min_size=0: idle 에이전트가 PgBouncer 슬롯을 점유하지 않음 → 다수 에이전트 운영 핵심
  • max_lifetime=1800: PgBouncer SERVER_LIFETIME=3600의 절반 → SDK가 먼저 conn 회전, 죽은 conn 잡지 않음
  • max_idle=300: PgBouncer SERVER_IDLE_TIMEOUT=600의 절반 → SDK가 먼저 idle conn 회수
  • 컨테이너당 peak conn = 5개 (psycopg 2 + asyncpg 3)
pgbouncer:
environment:
POOL_MODE: session
DEFAULT_POOL_SIZE: 250 # 50개 활성 에이전트 = 5 conn × 50
MIN_POOL_SIZE: 0 # idle 슬롯 점유 X
RESERVE_POOL_SIZE: 30 # 트래픽 스파이크 대응
SERVER_LIFETIME: 3600
SERVER_IDLE_TIMEOUT: 600
MAX_CLIENT_CONN: 2000
postgres:
command:
- postgres
- -c
- max_connections=300 # 250 + 관리용 50 여유
- -c
- shared_buffers=1GB
동시 활성 에이전트필요 PgBouncer poolPostgres max_connections
~50개250300
50-70개280300 (한도)
70개 초과400+500+ (증설 필요)

idle 에이전트는 PgBouncer 슬롯을 점유하지 않으므로 위 표의 “활성”은 동시에 트래픽 받는 컨테이너 수입니다. 전체 에이전트 수와는 무관합니다.

PgBouncer를 transaction 모드로 운영하려면 SDK 수정이 필요합니다. src/llamon_agent/outbound/memory/backends.py_ASYNCPG_STATEMENT_CACHE_SIZE0으로 변경해야 합니다 (prepared statement 비활성화). psycopg 측은 이미 prepare_threshold=0이라 그대로 동작합니다.

일반 CRUD(체크포인트 read/write, persistent 저장)는 풀 기본값 _ASYNCPG_COMMAND_TIMEOUT=10s를 사용합니다. 관리 API(DELETE /memory/threads?all=true, DELETE /memory/threads/{id}, POST /memory/migrate-agent-id, GET /memory/threads)는 대량 데이터 처리 가능성이 있어 호출별로 _ASYNCPG_ADMIN_COMMAND_TIMEOUT=120s를 적용합니다.

천 개 이상 thread를 일괄 삭제하면 120초도 부족할 수 있어 배치 처리(thread_id 100개씩 분할 호출)를 권장합니다.

배포 후 다음 쿼리로 SDK 풀 사용을 추적합니다.

SELECT state, count(*)
FROM pg_stat_activity
WHERE application_name = 'llamon-agent-sdk'
GROUP BY state;

active 수가 PgBouncer pool의 70%를 넘으면 DEFAULT_POOL_SIZE 증설 시점입니다.