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. 서랍 비유
우리는 "어느 서랍인지"만 말하면 됩니다. 여닫고 정리하는 건 체크포인터가 합니다.
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개
멀티에이전트 + 멀티턴입니다. 이번 장에서 배운 것이 그대로 얹힙니다.
스스로 해 보기
[2]의thread_id를'손님-오지우'로 바꿔 실행해 보세요. 답이 어떻게 달라지나요?thread_id를 세 개 만들어 각각 다른 대화를 나눠 보세요. 섞이지 않는지 확인하세요.get_state()로 각 서랍의 내용을 확인하세요.chatbot.get_state_history(config)을 돌려 보세요. 몇 개의 시점이 나오나요?
핵심 정리
thread_id는 "어느 서랍의 대화인가"를 가리키는 이름표입니다.{"configurable": {"thread_id": "..."}}를invoke의 두 번째 인자로 넘깁니다.configurable은 실행할 때마다 바꿔 넣을 수 있는 값이라는 뜻입니다.- 같은 질문이라도
thread_id가 다르면 다른 답이 나옵니다. 서랍이 다르니까요. - 값은 문자열이면 아무거나 됩니다 — 고객 ID · 세션 ID · 문의 번호 등 서비스 구조에 맞게 고릅니다.
- "새 대화 시작" 버튼이 하는 일이 새
thread_id발급입니다. - 주의 셋 — 겹치면 안 되고, 추측 가능하면 위험하고, 체크포인터와 짝입니다.
- 대화 내용은 개인정보입니다.
thread_id는 서버가 발급해야 합니다. - 8장의 멀티에이전트 그래프도 똑같이
thread_id를 씁니다.