본문으로 건너뛰기
페이지 목차
이 페이지에서 검색

멀티 에이전트

이 페이지에 묻기

멀티 에이전트 · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.

생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.

MULTI · AGENT

멀티 에이전트

생성 수정

도구 실행·검색·기억·그래프 흐름·전문 에이전트의 협업을 구성하고 권한·평가·사람의 확인으로 통제합니다.

26개 문서 · 2일차 · 17 / 26

16차시 · 멀티턴 대화와 세션 관리

지도 위치 : 2일차 · README 4장의 ④ 기억 층 — 기억 층의 첫 칸, 한 대화 안의 앞말을 이어받는 자리 | 이전: 15차시 검색 품질 개선 | 다음: 17차시 장기 기억 관리 키워드 : 대화 이력, 세션, 컨텍스트 윈도우

이 차시를 다 읽으면

  1. ⭐ "API는 무상태이고, 기억은 앱이 이력을 들고 매번 다시 보내는 것"을 축 ⑦로 설명할 수 있다.
  2. 세션을 유와 종차로 정의하고, 루프의 messages(9차시) · 장기 기억(17차시)과 구별할 수 있다.
  3. 세션 저장소를 메모리 ↔ Redis/DB로 비교하고, 서버가 여러 대일 때 무엇이 달라지는지 말할 수 있다.
  4. 컨텍스트 윈도우를 정의하고, 이력이 길어질 때의 문제와 세 가지 전략(슬라이딩 윈도 · 요약 · 핵심 사실 고정)을 비교할 수 있다.
  5. ⚠️ 이력을 자를 때 tool_use / tool_result 짝을 깨지 않는 법과, 세션 분리 실패가 왜 개인정보 사고인지 설명할 수 있다.

0. 이 차시의 판별 축

#축왼쪽 (0)오른쪽 (1)이 축으로 갈리는 것큰 축 (README 5장)
1⭐ 무상태 ↔ 상태LLM API — 지난 요청을 모름앱 — 세션별로 이력을 들고 다시 보냄대화가 "이어지는" 이유⑦ 무상태 ↔ 상태
2디스크 ↔ 메모리Redis · DB에 저장된 세션앱 프로세스 안의 dict재시작 · 여러 서버에서 이력이 남는가① 디스크 ↔ 메모리
3단기 ↔ 장기 기억이번 세션의 대화세션을 넘어 남는 사용자 정보이 차시 ↔ 17차시파생 축 (부모 ①)
4이력 전부 ↔ 줄인 이력처음부터 다 보냄잘라 내거나 요약해 보냄비용 · 지연 ↔ 잊어버림이 차시의 축

이 차시의 질문 — 직원이 "경조휴가 며칠이야?"라고 묻고, 이어서 "그거 반차로 나눠 써도 돼?"라고 묻습니다. 사람에게는 당연한 "그거"가, 무상태 API에게는 아무것도 가리키지 않습니다. 9차시에서는 루프 한 번 안에서 messages가 쌓였습니다. 이 차시는 그 쌓임을 요청과 요청 사이로 넓힙니다. README가 축 ⑦의 정점으로 꼽은 자리입니다.


1. ⭐ 축 ⑦의 정점 — API는 기억하지 않는다

1.1 멀티턴은 "다시 보내기"다

앞에서 배운 대로(2차시) LLM API는 무상태입니다. 두 번째 질문을 보낼 때 첫 번째 질문과 답을 함께 보내지 않으면 , 모델은 첫 번째를 모릅니다.

1턴 보냄: [user "경조휴가 며칠이야?"]
    받음: assistant "본인 결혼 5일, ... [취업규칙 제24조, p.13]"
2턴 보냄: [user "경조휴가 며칠이야?", assistant "본인 결혼 5일 ...", user "그거 반차로 나눠 써도 돼?"]
    받음: assistant "경조휴가는 ... "          ← "그거" = 경조휴가임을 앎
history = []                                   # ⚠️ 이 줄이 이 절의 함정 (2절)

def chat(user_text: str) -> str:
    history.append({"role": "user", "content": user_text})
    response = client.messages.create(model=MODEL, max_tokens=16000,
                                      system=SYSTEM, messages=history)
    history.append({"role": "assistant", "content": response.content})
    return "".join(b.text for b in response.content if b.type == "text")

🔍 읽는 포인트 : messages=history — 매번 이력 전체 를 보냅니다. 기억은 모델 안이 아니라 history라는 앱의 변수 안 에 있습니다.

1.2 반례 — 이건 API가 기억하는 것이 아닙니다

흔한 말실제로 일어난 일
"Claude 채팅 앱은 앞 대화를 기억하던데?"채팅 앱 이 이력을 저장하고 매번 다시 보냄. API 자체는 무상태
"프롬프트 캐싱을 켜면 모델이 기억한다"캐싱은 같은 앞부분의 처리 비용을 줄일 뿐 (10차시). 보내지 않은 내용은 모름
"대화 ID를 API에 넘기면 이어진다"이력을 보내는 것은 여전히 앱의 일

2. 세션 — 누구의 이력인가

2.1 정의와 경계

1.1의 코드에는 큰 함정이 있습니다. history가 하나 뿐이라, 직원 A와 직원 B가 동시에 쓰면 같은 이력에 섞입니다.

세션 (Session) — [앱의 상태 관리 단위]인데, [ 한 사용자의 한 대화를 세션 ID로 묶어, 그 대화의 이력을 요청 사이에 보관한다 ]는 점에서 요청 하나가 끝나면 사라지는 루프의 messages와, 세션을 넘어 남는 장기 기억과 구별된다.

루프의 messages (9차시)세션 이력 (이 차시)장기 기억 (17차시)
범위질문 하나 처리하는 동안대화 하나 (여러 턴)대화를 넘어
언제 사라지나요청이 끝나면세션 만료 · 종료 시정책에 따른 보관 기한
오피스봇 예잔여 조회 → 규정 확인"그거" = 경조휴가"이 직원은 개발팀, 한국어 선호"

2.2 세션 ID와 주인 확인

sessions: dict[str, dict] = {}                     # 세션 ID → {"owner": 사번, "history": [...]}

def chat(session_id: str, user, text: str) -> str:
    s = sessions.get(session_id)
    if s is None or s["owner"] != user.employee_id:   # ⭐ 이 세션의 주인인가
        raise PermissionError("세션 접근 권한 없음")
    s["history"].append({"role": "user", "content": text})
    ...

🔍 읽는 포인트 : 세션 ID만 맞으면 통과시키지 않고, 로그인한 사용자 = 세션 주인 인지 확인합니다. 앞에서 배운 권한 경계(8차시: 신원은 로그인 세션에서, 요청자 = 대상인지 코드가 검사)가 이력에도 그대로 적용됩니다. 세션 ID는 서버가 추측할 수 없는 무작위 값 으로 발급합니다.


3. 세션 저장소 — 메모리 ↔ Redis/DB (축 ①)

앱 메모리 (dict)RedisDB (테이블)
서버 재시작❌ 모든 대화가 사라짐설정에 따라 유지⭕ 유지
서버 여러 대❌ 다른 서버로 간 요청은 이력을 모름⭕ 모든 서버가 공유⭕ 공유
만료 (30분 무응답 시 삭제 등)직접 구현⭕ 만료 시간 지정이 쉬움정리 작업 필요
감사 · 분석❌제한적⭕ 조회 · 보관 쉬움
맞는 곳실습 · 시제품운영 서비스의 진행 중 대화대화 기록을 남겨야 할 때

Redis — [데이터 저장소]인데, [ 별도 서버의 메모리에 키-값을 두고 여러 앱 서버가 함께 읽고 쓰며, 항목마다 만료 시간을 줄 수 있다 ]는 점에서 앱 프로세스 안에만 있는 dict와 구별된다.

⚠️ 서버가 두 대가 되는 순간 dict 방식은 조용히 망가집니다. 첫 질문은 1번 서버로, 둘째 질문은 2번 서버로 가면 "그거"를 모릅니다. 에러는 나지 않고 답만 엉뚱해집니다.

⚠️ 저장소에 남는 이력에는 직원의 질문 · 조회 결과가 담깁니다. 보관 기한 · 마스킹 (8차시)을 저장 단계에도 적용하고, 무엇을 얼마나 남길지는 17차시에서 장기 기억과 함께 정합니다.


4. 컨텍스트 윈도우 — 이력이 길어지면

4.1 정의

컨텍스트 윈도우 (Context Window) — [모델의 입력 한도]인데, [ 한 번의 호출에서 다룰 수 있는 토큰(시스템 프롬프트 · 이력 · 도구 · 답)의 상한 ]이라는 점에서 대화를 저장해 두는 세션 저장소의 크기와 구별된다. 최신 Claude 모델은 100만 토큰입니다.

4.2 한도 안이어도 문제다

문제왜
비용턴마다 이력 전체를 다시 보냄. 10턴째는 1~9턴을 또 보냄 → 대화 전체 비용은 턴 수에 비례하는 게 아니라 더 빠르게 늘어남 (10차시 3.3)
지연입력이 길수록 첫 글자가 나오기까지 오래 걸림
묻힘긴 이력 속에서 앞부분의 중요한 정보가 덜 반영되기도 함
한도 초과도구 결과 · 검색 청크가 이력에 쌓이면 생각보다 빨리 커짐

💡 현재 이력의 크기는 client.messages.count_tokens(model=..., system=..., messages=history)로 미리 셉니다. 다른 회사의 토크나이저로 세면 맞지 않습니다.

4.3 세 가지 전략

전략어떻게장점약점오피스봇
슬라이딩 윈도최근 N턴만 보냄단순, 비용 상한이 분명오래된 내용은 완전히 잊음기본값 (예: 최근 10턴)
요약오래된 턴을 LLM으로 요약해 앞에 둠오래된 맥락을 짧게 유지요약 호출 비용, 요약이 틀리거나 빠뜨릴 수 있음긴 상담 대화
핵심 사실 고정대화 중 확정된 사실(다루는 휴가 종류, 진행 중 신청)을 별도 칸에 두고 매번 넣음중요한 것은 절대 안 잊음무엇이 핵심인지 정하는 규칙 필요신청 절차 진행 중

⭐ 실무에서는 섞어 씁니다 — 최근 N턴 + 오래된 부분 요약 + 핵심 사실 고정. 그리고 어느 전략이든 이력을 바꾸면 앞부분이 달라져 프롬프트 캐시가 깨진다 (10차시 6절)는 점을 비용 계산에 넣습니다. 서버 측에서 긴 대화를 자동 요약해 주는 기능(compaction, 베타)도 있습니다. 직접 요약과의 비교는 17차시에서 다룹니다.


5. ⚠️ 도구 호출 기록까지 담은 이력 자르기

오피스봇의 이력에는 사용자 · 답 텍스트만 있는 게 아닙니다. 9차시 루프의 tool_use · tool_result도 들어 있습니다.

[0] user       "다음 주 휴가 쓸 수 있어?"              ← 턴 1 시작
[1] assistant  tool_use(get_leave_balance)  id=t1
[2] user       tool_result(t1) "5일"
[3] assistant  "네, 5일 남아 ..."
[4] user       "경조휴가는 며칠이야?"                   ← 턴 2 시작
[5] assistant  tool_use(search_policy)      id=t2
[6] user       tool_result(t2) "제24조 ..."
[7] assistant  "본인 결혼 5일 ..."
❌ 잘못 자름결과
[2]부터 보냄짝 없는 tool_result(t1) — 400 에러
[6]부터 보냄짝 없는 tool_result(t2) — 400 에러
[5]까지만 보냄결과 없는 tool_use(t2) — 400 에러
⭕ [4]부터 보냄턴 2 전체 — 정상
def trim(history: list, max_turns: int) -> list:
    starts = [i for i, m in enumerate(history)
              if m["role"] == "user" and isinstance(m["content"], str)]   # 사람이 쓴 메시지 = 턴 시작
    return history if len(starts) <= max_turns else history[starts[-max_turns]:]

🔍 읽는 포인트 : ⭐ 자르는 자리는 턴의 시작 (사람이 직접 쓴 user 메시지)뿐입니다. tool_result도 role은 user이지만 내용이 블록 목록이라 턴 시작으로 치지 않습니다. 이렇게 하면 짝이 항상 함께 남거나 함께 잘립니다.

이력에 도구 기록을장점약점
남긴다모델이 "이미 조회한 값"을 알아 같은 도구를 다시 안 부름이력이 빨리 커짐, 자르기 규칙이 까다로움
턴이 끝나면 최종 답 텍스트만 남긴다이력이 작고 단순다음 턴에 같은 조회를 반복할 수 있음

6. ⚠️ 세션 분리 실패 = 개인정보 사고

이력에는 직원의 잔여 연차 · 질문 내용 · 조회 결과가 들어 있습니다. 세션이 섞이면 다른 직원에게 그 내용이 답으로 나갑니다. 모델의 실수가 아니라 앱의 사고 입니다.

실패어떻게 생기나막는 법
전역 이력 공유1.1처럼 history 하나세션 ID별 저장 (2.2)
세션 ID 추측1, 2, 3 순번으로 발급무작위 · 충분히 긴 값
남의 세션 ID로 요청주인 확인 없음로그인 사용자 = 세션 주인 검사 (2.2)
로그아웃 후에도 이력이 남음만료 없음만료 시간 · 로그아웃 시 삭제
공용 PC에서 이전 사용자의 대화브라우저에 세션이 남음로그아웃 시 세션 무효화

⭐ 세션 분리는 부탁이 아니라 강제 의 영역입니다. "이전 사용자의 대화는 언급하지 마"라는 프롬프트는 아무것도 막지 못합니다 — 이력이 섞인 순간 이미 모델 입력에 들어가 있기 때문입니다.


7. 오피스봇 적용 — 세 턴의 대화

턴직원이력이 하는 일연결
1"경조휴가 며칠이야?"—검색 · 근거 답 (14차시)
2"그거 반차로 나눠 써도 돼?""그거" = 경조휴가. 이력으로 쿼리 재작성 → "경조휴가 분할 사용"15차시 5절의 대명사 풀기 회수
3"그럼 다음 주 월요일로 신청해 줘"휴가 종류 · 날짜를 이력에서 모음 → 신청 도구 요청쓰기 → 승인 대기 (10차시, 23차시)

턴 3에서 "신청 대상: 경조휴가, 날짜: 다음 주 월요일"은 핵심 사실 고정 (4.3) 칸에 올려 둡니다. 이력이 잘려도 진행 중인 신청 내용은 잊지 않게 합니다.


8. 2일차 정리와 3일차 예고

차시만든 것핵심 축
9스스로 반복하는 에이전트 루프⭐ 누가 흐름을 정하는가, ⑦
10종료 조건 · 비용 상한 · 인젝션 방어부탁 ↔ 강제, 한 건 ↔ 전체 비용
11프레임워크 이전 · 트레이싱 · 회귀 테스트② 추상 ↔ 구상, 증거 ↔ 주장
12문서 로딩 · 조항 단위 청킹 · 메타데이터밖에서 찾는가 ↔ 안을 고치는가, ⑤
13임베딩 · 벡터 DB · 권한 필터⑧, ①
14근거 · 출처가 붙은 답, "못 찾음"⭐ 증거 ↔ 주장
15측정 · 하이브리드 · 리랭킹 · 쿼리 재작성⑧, ⑤
16세션별 대화 이력⭐ ⑦ 무상태 ↔ 상태

2일차가 끝나면 오피스봇은 스스로 반복하고(행동), 근거를 찾고(지식), 한 대화를 이어 갑니다(기억). 하지만 세션이 끝나면 이력은 사라집니다. 내일 다시 오면 오피스봇은 이 직원이 개발팀이라는 것도, 어제 경조휴가를 물었다는 것도 모릅니다.

앞으로 다시 만납니다 — 17차시에서 세션을 넘어 남는 장기 기억 (요약 메모리 · 사용자 프로필)과 그 민감정보 관리를, 18차시에서 이 차시의 이력과 같은 생각을 그래프의 State · 체크포인트 (세션별 상태 저장 · 재개)로, 23차시에서 턴 3의 승인 대기를 다룹니다.


9. 실습 — 맡기고 검수하기

지시

오피스봇에 세션별 멀티턴 대화를 붙여줘.
- 참고: Claude API 공식 문서(messages, token counting). 확인한 주소를 보고에 적을 것
- 세션: 서버가 무작위 세션 ID 발급, {owner: 로그인 사번, history} 저장. 요청마다 owner == 로그인 사번 검사
- 저장소: 개발은 dict, 설정 하나로 Redis로 바꿀 수 있게. 30분 무응답 시 만료
- 이력 자르기: 최근 10턴 슬라이딩 윈도. 반드시 "사람이 쓴 user 메시지" 위치에서만 자를 것
- 핵심 사실 칸: 진행 중인 신청(휴가 종류·날짜)을 따로 저장해 매 요청 앞에 넣을 것
- 매 요청 전에 count_tokens로 입력 토큰을 재서 로그에 남길 것
- 저장 전 마스킹 적용 (주민번호·전화번호)
테스트: ① 3턴 대화(경조휴가 → 그거 반차로? → 다음 주 월요일 신청)
 ② 15턴 대화로 자르기 동작, 도구 호출이 섞인 턴에서 잘릴 때 400 에러가 없는지
 ③ 직원 A 세션 ID로 직원 B 로그인 상태에서 요청
 ④ 서버 재시작 후 같은 세션 이어 묻기 (dict / Redis 각각)
추측하지 말고 실행 출력 원문을 붙여.
보고 형식: [테스트 | 턴 | 보낸 메시지 수 | 입력 토큰 | 결과 | 에러 여부] 표

받을 증거

  • 테스트 ①~④의 표와 요청별 로그 원문
  • ②에서 자르기 직전 · 직후의 이력 구조(역할과 블록 종류만)
  • ③의 거부 응답 원문

내가 직접 확인할 것

볼 것이걸로 아는 것
⭐ 브라우저 두 개(또는 일반 창 + 시크릿 창)에 다른 직원으로 로그인해 번갈아 질문 — 한쪽 답에 다른 쪽 내용이 섞이는가세션 분리 (2절, 6절)
한쪽 창의 세션 ID를 다른 창에 붙여 넣어 요청주인 확인 (2.2)
①의 턴 2 답이 "그거"를 경조휴가로 이해했는가이력이 다시 보내지는가 (1.1)
15턴 대화의 입력 토큰이 10턴 이후 더 늘지 않는가슬라이딩 윈도 동작 (4.3)
오래된 턴이 잘린 뒤에도 진행 중 신청 내용을 기억하는가핵심 사실 고정 (4.3, 7절)
④에서 dict는 잊고 Redis는 기억하는가축 ① (3절)

부록 A. 스스로 확인하기

#질문답이 있는 곳
1API가 무상태인데 대화가 이어지는 이유는?1.1
2"채팅 앱이 기억한다"와 "API가 기억한다"의 차이는?1.2
3세션 · 루프의 messages · 장기 기억은 무엇으로 갈리나?2.1
4세션 ID만 맞으면 통과시키면 안 되는 이유는?2.2
5서버가 두 대가 되면 dict 저장소에 무슨 일이?3절
6컨텍스트 윈도우 안이어도 긴 이력이 문제인 이유는?4.2
7슬라이딩 윈도 · 요약 · 핵심 사실 고정의 약점은?4.3
8이력을 자를 때 400 에러가 나는 경우와 안전한 자리는?5절
9세션 분리 실패가 프롬프트로 막히지 않는 이유는?6절

부록 B. 자주 하는 실수

실수바로잡기
전역 변수 하나에 이력을 쌓음❌ 모든 사용자가 섞임. 세션 ID별로
세션 ID를 순번으로 발급❌ 추측 가능. 무작위 값
세션 ID만 확인하고 주인 확인 생략❌ 로그인 사용자 = 세션 주인 검사
메시지 개수로 앞에서부터 잘라 냄⚠️ tool_use / tool_result 짝이 깨져 400. 턴 시작에서만
"100만 토큰이니 이력은 다 보내도 된다"⚠️ 비용 · 지연 · 묻힘. 줄이는 전략 필요
운영에서도 dict 저장소⚠️ 재시작 · 서버 여러 대에서 조용히 망가짐
요약에 맡기고 진행 중 신청 내용을 잃음⭕ 핵심 사실은 따로 고정
이력을 마스킹 없이 오래 보관⚠️ 보관 기한 · 마스킹 (17차시에서 정책)

부록 C. NotebookLM 복습 프롬프트

이 16차시 노트만 근거로 6문제를 내줘. 비전공자 기준으로.
"API는 무상태, 앱이 이력을 다시 보낸다(축 ⑦)", "세션과 주인 확인", "컨텍스트 윈도우와 세 가지 줄이기 전략", "tool_use/tool_result 짝을 깨지 않고 자르기"는 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.