페이지 목차
이 페이지에서 검색
멀티 에이전트
이 페이지에 묻기
멀티 에이전트 · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.
생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.
MULTI · AGENT
멀티 에이전트
생성 수정
도구 실행·검색·기억·그래프 흐름·전문 에이전트의 협업을 구성하고 권한·평가·사람의 확인으로 통제합니다.
26개 문서 · 3일차 · 18 / 26
17차시 · 장기 기억 관리
지도 위치 : 3일차 · README 4장의 ④ 기억 층(장기) | 이전: 16차시 멀티턴 대화와 세션 관리 | 다음: 18차시 그래프 기반 워크플로우 키워드 : 요약 메모리, 장기 기억, 민감정보 관리
이 차시를 다 읽으면
- 단기 기억과 장기 기억을 저장 위치와 수명 으로 가르고, 장기 기억이 아닌 것(가중치 · 규정 문서 · 긴 컨텍스트)을 반례로 댈 수 있다.
- 대화 압축(요약 메모리)을 직접 구현하는 방식과 API의 자동 요약 기능 으로 나눠 장단을 말할 수 있다.
- 사용자 프로필을 스키마로 정의해 "무엇을 기억하고 무엇을 기억하지 않을지"를 코드로 정할 수 있다.
- 기억을 쓰는 시점과 읽는(주입) 시점 을 설계하고, 틀린 기억이 굳는 위험을 설명할 수 있다.
- 민감정보 관리의 다섯 가지 통제(저장 금지 · 마스킹 · 보관 기한 · 열람과 삭제 · 외부 전송)로 오피스봇의 기억을 점검할 수 있다.
0. 이 차시의 판별 축
| # | 축 | 왼쪽 (0) | 오른쪽 (1) | 이 축으로 갈리는 것 | 큰 축 (README 5장) |
|---|---|---|---|---|---|
| 1 | 단기 ↔ 장기 기억 ⭐ | 이번 세션의 대화 | 세션을 넘어 남는 사용자 정보 | 다음 대화에 무엇이 이어지는가 | ① 디스크 ↔ 메모리 (파생) |
| 2 | 디스크 ↔ 메모리 | 저장소(DB) — 꺼도 남음 | 컨텍스트 — 이번 호출에만 있음 | 기억이 지금 어디에 있는가 | ① 디스크 ↔ 메모리 |
| 3 | 로컬 ↔ 원격 ⭐ | 내 서버의 기억 저장소 | 외부 LLM API로 보내지는 프롬프트 | 기억이 밖으로 나가는가 | ④ 로컬 ↔ 원격 |
| 4 | 원문 ↔ 요약 | 대화를 그대로 보관 | 짧게 압축해 보관 | 비용과 정보 손실의 맞바꿈 | 이 차시의 축 |
| 5 | 부탁 ↔ 강제 | "민감정보는 기억하지 마"라고 프롬프트에 씀 | 스키마 · 필터 코드로 저장 자체를 막음 | 민감정보가 실제로 저장되는가 | ② 추상 ↔ 구상 (파생) |
이 차시가 답하는 질문은 하나입니다. "세션이 끝난 뒤에도 남겨야 할 것은 무엇이고, 남기면 안 되는 것은 무엇인가?" 앞에서 배운 세션 관리(16차시)는 "이번 대화를 어떻게 이어 가나"였습니다. LLM API는 지난 요청을 기억하지 못하므로(무상태) 앱이 대화 이력을 저장해 매번 다시 보냈습니다. 이번에는 그 대화가 끝난 뒤 를 다룹니다. 축 1과 2는 "무엇을 남기나", 축 3과 5는 "남긴 것이 안전한가"를 묻습니다.
1. 단기 기억과 장기 기억
1.1 정의
단기 기억 — [에이전트의 기억]인데, [ 한 세션 안에서만 유지되고 세션이 끝나면 버려진다 ]는 점에서 장기 기억과 구별된다. 16차시의 대화 이력이 여기에 해당합니다.
장기 기억 — [에이전트의 기억]인데, [ 세션이 끝나도 저장소에 남아 다음 세션 시작 시 다시 프롬프트에 주입된다 ]는 점에서 단기 기억과 구별된다.
| 단기 기억 | 장기 기억 | |
|---|---|---|
| 담는 것 | 이번 대화의 메시지 전체 | 요약 · 사용자 프로필 · 확정된 사실 |
| 수명 | 세션이 끝날 때까지 | 보관 기한까지 (정해야 함) |
| 저장 위치 | 세션 저장소 (메모리 dict ↔ Redis · DB) | 사용자별 DB 레코드 |
| 틀렸을 때 | 이번 대화만 꼬임 | 다음 대화마다 꼬임 |
⭐ 마지막 줄이 이 차시의 핵심 위험입니다. 장기 기억은 매번 다시 주입 되므로, 한 번 잘못 저장된 사실이 모든 대화에 따라다닙니다(4.3).
1.2 반례 — 이건 장기 기억이 아닙니다
| 대상 | 왜 아닌가 |
|---|---|
| 모델이 학습한 지식 (가중치) | 사용자마다 다르지 않고, 우리가 고칠 수 없음 |
| 규정 문서 벡터 DB (13차시) | 영구 저장이지만 회사 문서 에 관한 것. 특정 사용자에 관한 기억이 아님 (RAG = 문서를 찾아 프롬프트에 넣는 기법) |
| 컨텍스트 윈도우 100만 토큰 | 한 번에 많이 넣을 수 있을 뿐, 세션이 끝나면 사라짐. 크기 ≠ 수명 |
| 서버 로그 · 실행 추적 (11차시) | 남기는 하지만 다음 대화에 주입되지 않음 . 기록이지 기억이 아님 |
💡 비유 (기억 보조용) — 단기 기억은 통화 중에 적는 메모지 , 장기 기억은 고객 카드 입니다. 메모지는 통화가 끝나면 버리고, 고객 카드에는 다음 통화에 필요한 것만 옮겨 적습니다. 무엇을 옮겨 적을지 정하는 것이 이 차시의 일입니다.
2. 대화 압축 — 요약 메모리
2.1 정의
앞에서 배운 세션 관리(16차시)에서 대화가 길어질 때의 전략 세 가지를 봤습니다 — 최근 N턴만 유지, 오래된 부분 요약, 중요 사실 고정. 그중 "요약"을 장기 기억으로 넓힌 것이 요약 메모리입니다.
요약 메모리 — [대화 압축 방식]인데, [ 오래된 대화를 LLM이 쓴 짧은 요약으로 바꿔 원문 대신 넣는다 ]는 점에서 오래된 턴을 그냥 버리는 슬라이딩 윈도와 구별된다.
| 슬라이딩 윈도 (최근 N턴) | 요약 메모리 | |
|---|---|---|
| 오래된 대화 | 버림 | 요약해서 남김 |
| 추가 비용 | 없음 | 요약용 LLM 호출 한 번 |
| 정보 손실 | 오래된 것 전부 | 요약에서 빠진 것 |
| 틀릴 위험 | 없음 (버릴 뿐) | 요약이 틀릴 수 있음 |
2.2 직접 구현 — 요약 한 번 부르기
SUMMARY_PROMPT = """아래 대화를 다음 대화에 이어 쓸 수 있게 요약하라.
- 사용자가 확정한 사실과 아직 처리되지 않은 요청만 남겨라. 추측하지 마라.
- 주민등록번호 · 연봉 · 건강 정보 · 계좌번호는 요약에 넣지 마라."""
def summarize(old_messages: list[dict]) -> str:
response = client.messages.create(
model="claude-haiku-5-5", # 요약은 가벼운 일 → 작은 모델
max_tokens=1024,
system=SUMMARY_PROMPT,
messages=[{"role": "user", "content": render(old_messages)}],
)
return next(b.text for b in response.content if b.type == "text")
if count_tokens(history) > LIMIT: # 길어지면
summary = summarize(history[:-6]) # 최근 6개 메시지만 원문 유지
history = [{"role": "user", "content": f"[이전 대화 요약]\n{summary}"}] + history[-6:]
🔍 읽는 포인트
model="claude-haiku-5-5"— 요약은 판단이 가벼운 일이라 작고 싼 모델로 충분한 경우가 많습니다. 실제로 충분한지는 요약 결과를 읽어 보고 정합니다.count_tokens(...) > LIMIT— 토큰 수는client.messages.count_tokens(...)로 셉니다. 다른 회사 토크나이저로 세면 숫자가 다릅니다.history[-6:]— 최근 대화는 원문 그대로 둡니다. 방금 한 말까지 요약하면 대명사("그거")가 풀리지 않습니다.- ⚠️ 프롬프트의 "넣지 마라"는 부탁 입니다. 실제로 막는 것은 5절의 마스킹 코드입니다.
2.3 직접 구현 vs API의 자동 요약
Claude API에는 긴 대화를 서버 쪽에서 자동으로 압축해 주는 기능(compaction, 베타)이 있습니다. 켜는 방법과 조건은 버전에 따라 다르니 공식 문서를 확인 하세요.
| 직접 구현 (2.2) | API 자동 요약 | |
|---|---|---|
| 무엇을 남길지 | 내가 정함 (프롬프트 · 규칙) | 제공자가 정한 방식 |
| 민감정보 걸러내기 | 요약 전후에 내 코드로 필터 가능 | 요약 내용을 별도로 검사해야 함 |
| 세션을 넘어 저장 | 요약 결과를 내 DB에 저장 | 한 대화 안의 압축이 주목적 |
| 구현 부담 | 있음 | 적음 |
| 맞는 경우 | 장기 기억으로 남길 때, 기준을 통제해야 할 때 | 한 세션이 매우 길어질 때 |
3. 사용자 프로필 — 무엇을 기억할지 스키마로
3.1 정의
사용자 프로필 — [장기 기억]인데, [ 미리 정한 항목(스키마)에 맞춰 사실을 구조화해 저장한다 ]는 점에서 자유로운 글인 요약 메모리와 구별된다.
| 요약 메모리 | 사용자 프로필 | |
|---|---|---|
| 형태 | 자유로운 글 | 정해진 필드 |
| 무엇이 들어갈지 | 요약 프롬프트에 달림 (부탁) | 스키마에 없는 필드는 못 들어감 (강제) |
| 사용자에게 보여 주기 | 어려움 | 표로 보여 주기 쉬움 (5.1) |
3.2 스키마 — 앞에서 배운 구조화 출력 재사용
앞에서 배운 의도 분류기(4차시)에서는 모델의 답을 Pydantic 스키마로 받아 형식을 강제 했습니다. 같은 방법으로 "대화에서 프로필에 남길 사실"을 뽑습니다.
from pydantic import BaseModel
from typing import Literal
class ProfileUpdate(BaseModel):
department: str | None # 부서
preferred_language: Literal["ko", "en"] | None
answer_style: Literal["짧게", "자세히"] | None
frequent_topics: list[str] # 자주 묻는 주제 (예: "휴가", "VPN")
evidence: str # 어느 발화에서 나왔는지 그대로 인용
response = client.messages.parse(
model="claude-haiku-5-5",
max_tokens=1024,
system="대화에서 사용자가 스스로 밝힌 사실만 뽑아라. 없으면 null.",
messages=[{"role": "user", "content": render(session_messages)}],
output_format=ProfileUpdate,
)
update = response.parsed_output
🔍 읽는 포인트
- ⭐ 필드 목록이 곧 "기억할 것" 목록 입니다. 연봉 · 건강 · 주민번호 필드가 아예 없으므로 모델이 뽑고 싶어도 들어갈 자리가 없습니다. 부탁이 아니라 강제입니다.
evidence— 그 사실이 어느 발화에서 나왔는지 남깁니다. 틀린 기억을 추적할 때 씁니다(4.3).- ⚠️ 스키마는 형식 을 보장할 뿐 내용이 맞는지 는 보장하지 않습니다(4차시, 축 ⑧). "인사팀"이라고 뽑았는데 사실은 "인사팀에 문의하려던 개발팀 직원"일 수 있습니다.
4. 기억을 쓰는 시점과 읽는 시점
4.1 쓰기 시점
| 쓰기 시점 | 장점 | 단점 |
|---|---|---|
| 매 턴마다 | 놓치는 게 없음 | 턴마다 LLM 호출 비용, 지나가는 말까지 저장 |
| 세션이 끝날 때 | 대화 전체를 보고 판단 | 세션이 비정상 종료되면 놓침 |
| 사용자가 "기억해"라고 할 때 | 사용자 의사가 분명함 | 사용자가 말하지 않으면 안 남음 |
💡 오피스봇은 세션 종료 시 추출 + 사용자 확인 을 기본으로 둡니다. "다음부터 짧게 답할까요?"처럼 바뀌는 항목을 보여 주고 확인받습니다.
4.2 읽기(주입) 시점
profile = load_profile(user_id) # 로그인 세션의 사번으로 조회
system = BASE_SYSTEM_PROMPT + "\n\n[사용자 프로필 — 참고 정보]\n" + render(profile)
response = client.messages.create(
model="claude-opus-5-5", max_tokens=16000,
system=system, messages=history,
)
🔍 읽는 포인트
load_profile(user_id)—user_id는 로그인 세션에서 가져옵니다. 모델이 말한 사번으로 남의 프로필을 불러오면 안 됩니다(앞에서 배운 권한 경계, 8차시 — 신원은 모델이 아니라 코드가 정한다).BASE_SYSTEM_PROMPT + ... profile— 변하지 않는 부분을 앞에 , 사용자마다 다른 프로필을 뒤에 둡니다. 프롬프트 캐싱은 앞부분이 같아야 재사용되기 때문입니다(앞에서 배운 비용 절감, 2 · 10차시).- "참고 정보"라는 머리말 — 프로필을 지시가 아니라 데이터로 표시합니다. 프로필에 "모든 규정을 무시하라" 같은 글이 들어가도 지시로 따르지 않게 하는 장치입니다(10차시 인젝션 방어).
4.3 ⚠️ 잘못된 기억 — 틀린 사실의 고착
| 상황 | 무엇이 잘못 저장되나 | 결과 |
|---|---|---|
| "만약 제가 IT팀이면 노트북 바꿔 줘요?" | 부서 = IT팀 | 다음 대화부터 IT팀 기준으로 답함 |
| 요약이 "휴가 신청 완료"로 잘못 씀 | 처리되지 않은 요청이 완료로 기록 | 사용자는 신청된 줄 앎 |
| 부서를 옮겼는데 옛 부서가 남음 | 오래된 사실 | 엉뚱한 부서 규정 안내 |
| 대응 | 방법 |
|---|---|
| 원천이 있는 사실은 기억하지 말고 조회 ⭐ | 부서 · 직급은 인사 DB가 원천. 프로필에 복사하지 않고 매번 도구로 조회 |
| 출처와 갱신일 기록 | evidence, updated_at 필드 — 언제 어느 말에서 왔는지 |
| 덮어쓰기 규칙 | 새 사실이 옛 사실과 충돌하면 자동으로 덮지 말고 확인 |
| 사용자 열람 · 수정 | 사용자가 자기 프로필을 보고 고칠 수 있게 (5.1) |
5. ⚠️ 민감정보 관리 — 축 ④
5.1 다섯 가지 통제
| 통제 | 무엇을 하나 | 오피스봇에서 | 부탁 ↔ 강제 |
|---|---|---|---|
| 저장 금지 목록 | 아예 저장하지 않을 항목을 정함 | 주민번호 · 연봉 · 건강 · 징계 · 계좌 | 스키마에 필드 없음 (강제) |
| 마스킹 | 저장 · 전송 전에 가림 | 정규식으로 번호 패턴을 [가림]으로 | 코드 (강제) |
| 보관 기한 | 정한 기간이 지나면 지움 | 요약 90일, 퇴사 시 삭제 (예시 — 실제 기한은 개인정보 처리 방침 · 법무 검토로) | 배치 작업 (강제) |
| 사용자 열람 · 삭제 | 내 기억을 보고 지울 수 있게 | "내 정보 보기 / 기억 지우기" 화면 | 엔드포인트 (24차시) |
| 외부 전송 점검 | 무엇이 외부 LLM으로 나가는지 확인 | 주입되는 프로필 필드 목록을 문서로 | 사람이 검토 |
5.2 저장과 주입 — 두 번 점검
[대화] ─(1) 저장→ [내 DB: 장기 기억] ─(2) 주입→ [프롬프트] ─→ [외부 LLM API]
└── 로컬 (축 ④ 왼쪽) ──┘ └ 원격 (축 ④ 오른쪽)
⭐ 장기 기억은 두 번 위험합니다. (1) 내 DB에 쌓일 때, (2) 다음 대화마다 프롬프트에 실려 밖으로 나갈 때 . 저장은 한 번이지만 주입은 대화마다 반복됩니다. 그래서 주입할 필드도 "답에 필요한 최소한"으로 줄입니다. 실행 추적 도구(앞에서 배운 LangSmith, 11차시)를 켜 두었다면 추적 기록도 외부로 나가므로 같은 마스킹을 적용합니다.
5.3 마스킹 — 코드로 막기
import re
PATTERNS = {
"주민번호": r"\d{6}-?[1-4]\d{6}",
"전화번호": r"01[0-9]-?\d{3,4}-?\d{4}",
"계좌번호": r"\d{2,6}-\d{2,6}-\d{2,6}",
}
def mask(text: str) -> str:
for label, pattern in PATTERNS.items():
text = re.sub(pattern, f"[{label} 가림]", text)
return text
save_summary(user_id, mask(summary)) # 저장 전에
🔍 읽는 포인트
mask(summary)— 요약 프롬프트가 "넣지 마라"를 어겨도 저장 직전에 한 번 더 가립니다. 앞에서 배운 마스킹(8차시)을 기억 저장 경로에도 거는 것입니다.- ⚠️ 정규식은 형식이 정해진 정보 (번호)만 잡습니다. "저 요즘 우울증 치료 받아요" 같은 건강 정보는 패턴이 없습니다. 이런 것은 스키마에서 자리를 없애고(3.2), 요약 결과를 평가셋으로 검사합니다.
6. 오피스봇 적용 — 프로필 예
| 항목 | 예 | 출처 | 저장 | 주입 |
|---|---|---|---|---|
| 부서 | 개발팀 | 인사 DB | ❌ (원천 조회) | 도구로 조회 |
| 선호 언어 | ko | 사용자 발화 | ⭕ | ⭕ |
| 답변 스타일 | 짧게 | 사용자 확인 | ⭕ | ⭕ |
| 자주 묻는 주제 | 휴가, VPN | 대화 추출 | ⭕ | ⭕ |
| 진행 중 요청 요약 | "모니터 신청 승인 대기" | 세션 요약 | ⭕ (30일) | ⭕ |
| 연봉 · 건강 · 주민번호 | — | — | ❌ 저장 금지 | ❌ |
장면으로 보면 이렇습니다. 월요일에 "앞으로 답은 짧게 해 주세요"라고 말한 직원이 수요일에 새 세션을 엽니다. 오피스봇은 프로필의 answer_style=짧게를 시스템 프롬프트 뒤쪽에 주입해 처음부터 짧게 답합니다. 직원이 "제 기억 지워 주세요"라고 하면 삭제 엔드포인트가 프로필과 요약을 지우고, 다음 세션은 빈 프로필로 시작합니다.
앞으로 다시 만납니다 — 18차시에서 그래프의 State 를 배웁니다. State는 한 번의 실행 안에서 노드들이 공유하는 값으로, 이 차시의 단기 기억과 같은 쪽(메모리)에 있습니다. 23차시의 체크포인터 는 그 State를 저장소에 남겨 멈춘 흐름을 재개하게 합니다. 24차시에서는 열람 · 삭제를 API 엔드포인트로 엽니다.
7. 실습 — 맡기고 검수하기
지시
오피스봇에 장기 기억을 붙여줘.
목표: (1) 세션 종료 시 프로필 추출 · 저장 (2) 새 세션 시작 시 시스템 프롬프트 뒤쪽에 주입 (3) 긴 대화의 오래된 부분 요약.
참고 공식 문서: Claude API 구조화 출력(messages.parse), 토큰 세기(count_tokens), 긴 대화 압축(compaction) 문서.
제약:
- 프로필 스키마는 department(인사 DB 조회로 대체), preferred_language, answer_style, frequent_topics, evidence, updated_at 만.
- 연봉 · 건강 · 주민번호 · 계좌 필드는 만들지 말 것. 저장 직전 mask() 적용.
- user_id는 로그인 세션 값만 사용. 모델 출력의 사번으로 조회 금지.
- 프로필 열람(GET)과 삭제(DELETE) 함수를 만들 것. 실제 DB 삭제를 실행하기 전에는 나에게 승인받을 것.
테스트: ① "앞으로 짧게 답해줘" 후 새 세션 ② "만약 제가 IT팀이면?" 가정 발화 ③ 주민번호 · 전화번호를 말한 대화
④ "저 요즘 병원 다녀요" 건강 발화 ⑤ 다른 직원 사번으로 프로필 요청 ⑥ 삭제 후 새 세션
보고 형식: [시나리오 | 저장된 프로필 원문 | 주입된 시스템 프롬프트 끝부분 | 다음 세션의 답 | 판정] 표.
추측하지 말고 실행 출력 원문과 DB 레코드 원문을 붙여.
받을 증거
- 시나리오 6개의 표 와, 각 시나리오 뒤 DB에 실제로 저장된 레코드 원문
- ③의 저장 레코드에서 번호가
[가림]으로 바뀐 증거, ⑥ 삭제 전후의 레코드 조회 결과
내가 직접 확인할 것
| 볼 것 | 이걸로 아는 것 |
|---|---|
| ①에서 새 세션의 첫 답 이 짧은가 | 주입이 실제로 동작하는가 (4.2) |
| ②에서 부서가 IT팀으로 저장되지 않았는가 | 가정 발화가 사실로 굳지 않는가 (4.3) |
| ③ · ④의 DB 레코드에 번호 · 건강 정보가 남아 있지 않은가 | 저장 금지와 마스킹이 코드로 걸렸는가 (5.1, 5.3) |
| ⑤에서 거부 가 나왔는가 | 신원을 코드가 정하는가 |
| ⑥ 뒤 새 세션이 나를 모르는가 | 삭제가 진짜 삭제인가 |
| 프롬프트 원문에서 외부로 나가는 필드 목록 | 축 ④ — 무엇이 밖으로 나가는지 내가 알고 있는가 |
부록 A. 스스로 확인하기
| # | 질문 | 답이 있는 곳 |
|---|---|---|
| 1 | 단기 기억과 장기 기억을 유와 종차로 정의하면? | 1.1 |
| 2 | 요약 메모리와 슬라이딩 윈도는 무엇이 다른가? | 2.1 |
| 3 | 직접 요약 구현과 API 자동 요약은 각각 언제 맞는가? | 2.3 |
| 4 | 프로필 스키마가 "부탁"이 아니라 "강제"인 이유는? | 3.2 |
| 5 | 부서를 프로필에 저장하지 않고 조회하는 이유는? | 4.3 |
| 6 | 장기 기억이 "두 번 위험한" 이유는? | 5.2 |
| 7 | 정규식 마스킹이 못 잡는 정보는 어떻게 막는가? | 5.3 |
부록 B. 자주 하는 실수
| 실수 | 바로잡기 |
|---|---|
| "컨텍스트가 100만 토큰이니 장기 기억은 필요 없다" | ❌ 크기와 수명은 다른 축입니다. 세션이 끝나면 사라집니다 |
| 요약 프롬프트에 "민감정보 빼"라고만 씀 | ⚠️ 부탁입니다. 저장 직전 마스킹과 스키마로 막으세요 |
| 대화에서 나온 말을 전부 사실로 저장 | ❌ 가정 · 농담이 사실로 굳습니다. 출처를 남기고 확인받으세요 |
| 인사 DB에 있는 부서를 프로필에 복사 | ❌ 원천이 있으면 조회합니다. 복사본은 낡습니다 |
| 삭제 요청 시 화면에서만 숨김 | ❌ DB 레코드가 실제로 지워졌는지 확인하세요 |
부록 C. NotebookLM 복습 프롬프트
이 17차시 노트만 근거로 비전공자용 5문제를 내줘.
"장기 기억이 아닌 것 (반례)", "프로필 스키마가 강제인 이유", "틀린 기억의 고착", "저장과 주입의 두 번 점검"은 반드시 포함해줘.
내가 답한 뒤에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.
LangTailor