📚 Agentic AI - 기업용 자율 에이전트 개발 7장 · 기억하는 에이전트 Agentic AI란

thread_id — 사용자별로 대화를 나눠 두기

한 줄 요약

thread_id 는 "어느 서랍의 대화인가"를 가리키는 이름표입니다. 이것 하나로 손님 수백 명의 대화가 섞이지 않습니다. 2장 6절에서 미뤄 둔 한계 ①의 답입니다.


1. 2장에서 미뤄 둔 문제

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

history = []          # 리스트가 하나뿐이다

손님 A와 손님 B가 동시에 문의하면 한 리스트에 뒤섞입니다.

history = [
  "제 이름은 오지우입니다",        ← 손님 A
  "네 오지우 고객님",
  "제 이름은 박서준입니다",        ← 손님 B
  "네 박서준 고객님",
  "제 이름이 뭐라고 했죠?",        ← 손님 A가 물었는데
  "박서준 고객님이십니다"           ← 마지막 것으로 답함
]

직접 해결하려면 이렇게 해야 합니다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

histories = {}                        # 손님ID → 대화 목록
histories.setdefault("A", []).append(...)
histories.setdefault("B", []).append(...)

그리고 요청이 올 때마다 맞는 리스트를 찾아 오고, 끝나면 다시 넣는 코드를 짜야 하죠.

thread_id 가 이 일을 대신합니다.


2. 쓰는 법

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

config = {"configurable": {"thread_id": "손님-오지우"}}
chatbot.invoke({"messages": [{"role": "user", "content": text}]}, config)

두 번째 인자로 넘깁니다. {"configurable": {...}} 라는 형태로 감싸는 게 낯설죠.

configurable(컨피규러블 — "설정할 수 있는")은 "실행할 때마다 바꿔 넣을 수 있는 값" 이라는 뜻입니다. 그래프를 compile() 할 때 고정되는 것(체크포인터, 노드, 엣지)과 달리, invoke 할 때마다 달라질 수 있는 값을 여기 넣습니다. thread_id 가 대표적이고, recursion_limit 처럼 configurable 바깥에 놓이는 실행 설정도 함께 이 딕셔너리에 들어갑니다.

  {"configurable": {"thread_id": "손님-오지우"},   ← 실행마다 바뀔 수 있는 값
   "recursion_limit": 10}                        ← 실행 설정

이 과정에서 configurable 에 넣는 것은 thread_id 하나뿐입니다.

thread_id 값은 문자열이면 아무거나 됩니다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

"손님-오지우"
"C0107"
"session-a3f8b21e"
"user_42__2026-08-20"

3. 실행으로 확인

==============================================================
[1] 같은 thread_id — 앞말을 기억한다

  손님: 제 이름은 오지우입니다. 스마트워치를 샀어요.
  봇  : 오지우 고객님, 스마트워치 구매 건으로 연락 주셨군요.
  손님: 제가 뭘 샀다고 했죠?
  봇  : 고객님께서는 스마트워치를 구매하셨다고 말씀해주셨습니다.

==============================================================
[2] thread_id 를 바꾸면 다른 손님 — 대화가 섞이지 않는다

  손님: 제가 뭘 샀다고 했죠?
  봇  : 아직 어떤 상품을 구매하셨는지 말씀해주시지 않았습니다.

같은 질문, 다른 답. thread_id 만 바꿨을 뿐입니다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

say("제가 뭘 샀다고 했죠?", thread_id='손님-오지우')   → "스마트워치를 구매하셨다고..."
say("제가 뭘 샀다고 했죠?", thread_id='손님-박서준')   → "말씀해주시지 않았습니다"

박서준의 서랍은 비어 있습니다. 그래서 모른다고 합니다.


4. 서랍 비유

체크포인터 = 서랍장thread_id = 서랍 이름표서랍장 (InMemorySaver)손님-오지우[오지우입니다, 네 고객님, …]손님-박서준(비어 있음)재고담당-1[P0003 재고는?, 3개입니다, …]서랍이 다르면 서로의 대화를 볼 수 없습니다. 사용자마다 다른 thread_id를 줘야 하는 이유입니다.
서랍이 다르면 서로의 대화를 볼 수 없다

우리는 "어느 서랍인지"만 말하면 됩니다. 여닫고 정리하는 건 체크포인터가 합니다.


5. 무엇을 thread_id 로 쓸까

서비스 구조에 따라 다릅니다.

무엇을 쓰나 의미 언제
고객 ID (C0107) 고객마다 대화 하나 상담 이력이 계속 이어져야 할 때
세션 ID (session-a3f8) 접속마다 새 대화 브라우저 탭마다 독립
고객ID + 날짜 (C0107_2026-08-20) 하루 단위로 분리 오래된 맥락을 안 끌고 오려면
문의 번호 (TICKET-1042) 문의 건마다 문의별로 완결

"새 대화 시작" 버튼이 하는 일이 thread_id 를 발급하는 것입니다. 사용자에게는 "기억을 지우는" 것처럼 보이지만, 실제로는 새 서랍을 여는 것이죠. 이전 서랍은 그대로 남아 있습니다.


6. 주의할 점 셋

① 겹치면 안 됩니다

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

thread_id = "user"        # ✗ 모든 손님이 같은 서랍을 쓴다
thread_id = user_id       # ✓

당연해 보이지만, 개발 중에 테스트용으로 "test" 를 박아 두고 잊는 일이 흔합니다.

② 추측 가능하면 위험합니다

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

thread_id = "user-1", "user-2", "user-3", ...

만약 사용자가 thread_id 를 지정할 수 있는 구조라면, 남의 대화를 훔쳐볼 수 있습니다.

대화 내용은 개인정보입니다. thread_id서버가 발급하고, 사용자 입력을 그대로 쓰지 않아야 합니다.

③ 체크포인터가 없으면 무시됩니다

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

chatbot = graph.compile()                          # 체크포인터 없음
chatbot.invoke(..., {"configurable": {"thread_id": "A"}})   # thread_id 를 줘도

저장할 곳이 없으니 아무 일도 안 일어납니다. 매번 새 대화가 됩니다.

반대로 체크포인터는 있는데 thread_id 를 안 주면 오류가 납니다. 둘은 짝입니다.


7. 대화 목록 보기

thread_id 의 이력을 통째로 꺼낼 수 있습니다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

config = {"configurable": {"thread_id": "손님-오지우"}}
snapshot = chatbot.get_state(config)
for m in snapshot.values["messages"]:
    print(f"  {type(m).__name__:14s} {str(m.content)[:50]}")

과거의 중간 상태들도 볼 수 있습니다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

for snapshot in chatbot.get_state_history(config):
    print(len(snapshot.values.get("messages", [])), "개 시점")

앞 절에서 말한 "노드가 끝날 때마다 저장" 덕분입니다. 되짚어 볼 수 있는 것이죠.

이 과정에서는 get_state() 만 씁니다. get_state_history() 는 "이런 것도 된다" 정도로 알아 두세요.


8. 8장으로 이어집니다

8장의 멀티에이전트 그래프도 똑같이 thread_id 를 씁니다.

아래는 8장에서 만들 code/ch08_multi_agent.py 의 일부를 미리 보는 것입니다 — 지금 붙여 넣지 않아도 됩니다.

team = graph.compile(checkpointer=InMemorySaver())

def ask(question: str, thread_id: str = "대응팀-1") -> str:
    config = {"configurable": {"thread_id": thread_id}}
    result = team.invoke({"messages": [{"role": "user", "content": question}]}, config)
    return result["messages"][-1].text.strip()

노드가 셋으로 늘어나도 쓰는 법은 같습니다.

그래서 8장에서 이런 게 됩니다.

질문 1: "승승 스마트워치 Fit 5, 지금 어떻게 대응해야 할까요?"
        → 담당 셋이 조사 → 종합 답변

질문 2: "그 상품을 산 주문 O001203은 지금 어떤 상태인가요?
         그리고 재주문 기준을 채우려면 몇 개를 발주해야 하나요?"
        → "그 상품"이 통한다 → 주문담당 + 데이터분석가 → 배송완료 / 27개

멀티에이전트 + 멀티턴입니다. 이번 장에서 배운 것이 그대로 얹힙니다.


스스로 해 보기

  1. [2]thread_id'손님-오지우' 로 바꿔 실행해 보세요. 답이 어떻게 달라지나요?
  2. thread_id 를 세 개 만들어 각각 다른 대화를 나눠 보세요. 섞이지 않는지 확인하세요.
  3. get_state() 로 각 서랍의 내용을 확인하세요.
  4. chatbot.get_state_history(config) 을 돌려 보세요. 몇 개의 시점이 나오나요?

핵심 정리

  • thread_id 는 "어느 서랍의 대화인가"를 가리키는 이름표입니다.
  • {"configurable": {"thread_id": "..."}}invoke 의 두 번째 인자로 넘깁니다. configurable실행할 때마다 바꿔 넣을 수 있는 값이라는 뜻입니다.
  • 같은 질문이라도 thread_id 가 다르면 다른 답이 나옵니다. 서랍이 다르니까요.
  • 값은 문자열이면 아무거나 됩니다 — 고객 ID · 세션 ID · 문의 번호 등 서비스 구조에 맞게 고릅니다.
  • "새 대화 시작" 버튼이 하는 일이 thread_id 발급입니다.
  • 주의 셋 — 겹치면 안 되고, 추측 가능하면 위험하고, 체크포인터와 짝입니다.
  • 대화 내용은 개인정보입니다. thread_id 는 서버가 발급해야 합니다.
  • 8장의 멀티에이전트 그래프도 똑같이 thread_id 를 씁니다.
← 이전 절06. 따라하기 그 상품 지난달은 이어서 묻기다음 절 →08. 따라하기 세션이 섞이지 않는지 검증하기