따라하기 — CSV 분석 에이전트에 기억 붙이기
한 줄 요약
5장에서 만든 에이전트에 checkpointer=InMemorySaver() 한 줄을 더하면 멀티턴이 됩니다. create_agent 도 결국 StateGraph 이기 때문입니다.
1. 한 줄 차이
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다. (5장 파일과 7장 파일을 나란히 놓은 것입니다.)
# 5장 — ch05_csv_agent.py
llm = get_chat(temperature=0)
agent = create_agent(llm, tools=[run_pandas], system_prompt=SYSTEM)
# 7장 — ch07_memory.py
analyst_with_memory = create_agent(
llm, tools=[run_pandas], system_prompt=CSV_SYSTEM,
checkpointer=InMemorySaver(), # ← 이 한 줄이 전부다
)
앞 절에서 compile(checkpointer=...) 에 넣었던 그것을 create_agent 에 넘긴 것입니다.
왜 되는가 — create_agent 는 내부에서 그래프를 만들고 compile 합니다. 그때 우리가 준 체크포인터를 그대로 넘겨 줍니다. 5장 4절에서 "create_agent 는 사실 그래프다" 라고 한 것이 여기서 도움이 됩니다.
2. 도구는 5장 것을 그대로 가져옵니다
아래는 code/ch07_memory.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
from ch05_csv_agent import run_pandas # 5장에서 만든 pandas 도구를 그대로 재사용
파일을 다시 안 씁니다. 5장에서 만든 도구를 그냥 import 합니다.
ch05_csv_agent.py 의 맨 아래를 보면 이렇게 되어 있습니다.
아래는 5장에 만든 code/ch05_csv_agent.py 의 일부입니다 — 이미 있는 코드이니 고치지 마세요.
if __name__ == "__main__":
print("=" * 62)
...
if __name__ == "__main__": 아래는 직접 실행할 때만 돌아갑니다. import 할 때는 안 돌아가죠. 그래서 run_pandas 만 조용히 가져올 수 있습니다.
이 과정의 파일들이 전부 이 구조입니다. 그래서 8장에서
ch04·ch05·ch06을 전부 가져다 쓸 수 있습니다. 재사용을 염두에 두고 짠 것입니다.
3. 시스템 지시에 한 줄 추가
아래는 code/ch07_memory.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
CSV_SYSTEM = (
"너는 승승장구몰의 데이터 분석가다. "
"숫자는 반드시 run_pandas 도구로 계산한다. "
"손님이 '그거'·'그 상품'처럼 앞에서 말한 것을 가리키면 대화 기록에서 찾아 이어서 답한다. "
"한국어로 간결하게 답한다."
)
세 번째 줄이 5장에 없던 것입니다.
기억이 생겼다고 모델이 알아서 잘 쓰는 건 아닙니다. "앞말을 가리키면 기록에서 찾아라" 를 명시해 주면 훨씬 안정적입니다.
2장 2절의 원칙 — 시스템 지시는 지시를 따를 가능성을 높이는 장치 — 가 여기서도 적용됩니다. 기억이라는 재료는 체크포인터가 주고, 그걸 쓰라고 하는 것은 시스템 지시입니다.
4. 부르는 함수
아래는 code/ch07_memory.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
def ask_analyst(text: str, thread_id: str) -> str:
config = {"configurable": {"thread_id": thread_id}}
result = analyst_with_memory.invoke(
{"messages": [{"role": "user", "content": text}]}, config)
return result["messages"][-1].text.strip()
앞 절의 say() 와 똑같은 모양입니다.
5장의 ask() 와 비교하면 딱 하나 다릅니다.
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
# 5장
out = agent.invoke({"messages": [...]}) # 설정 없음
# 7장
result = analyst_with_memory.invoke({"messages": [...]}, config) # 설정 추가
두 번째 인자로 thread_id 를 넘기는 것뿐입니다.
체크포인터를 붙였는데
thread_id를 안 넘기면 오류가 납니다. "어느 서랍인지 알려 달라"는 것이죠. 둘은 짝입니다.
5. 실행 — [3] 부분
터미널에서 실행 — 프로젝트 폴더
agentic_ai에서,(agentic)표시를 확인한 뒤.
python code/ch07_memory.py
==============================================================
[3] 기억을 붙인 CSV 분석 에이전트 — '그거'가 통한다
손님: P0003 스마트워치 재고가 얼마나 남았나요?
봇 : P0003 스마트워치 재고는 3개 남았습니다.
손님: 그거 재주문 기준은요?
봇 : P0003 스마트워치의 재주문 기준은 30개입니다.
손님: 그럼 지금 발주해야 하나요?
봇 : 네, 발주해야 합니다.
세 번째 질문을 눈여겨보세요.
"그럼 지금 발주해야 하나요?"
이 문장에는 상품 이름도, 숫자도 없습니다. 그런데 답이 나옵니다.
앞의 두 답변(재고 3개, 기준 30개)이 대화 기록에 남아 있어서, 모델이 그 둘을 비교해 "3 < 30이니 발주해야 한다" 고 판단한 것입니다.
6. 안에서 무슨 일이
세 번째 질문 시점에 모델이 받은 것을 펼쳐 보면 이렇습니다.
system : 너는 승승장구몰의 데이터 분석가다. ... 손님이 '그거'처럼 ...
human : P0003 스마트워치 재고가 얼마나 남았나요?
ai : (run_pandas 호출 요청)
tool : [{'stock': 3}]
ai : P0003 스마트워치 재고는 3개 남았습니다.
human : 그거 재주문 기준은요?
ai : (run_pandas 호출 요청)
tool : [{'reorder_level': 30}]
ai : P0003 스마트워치의 재주문 기준은 30개입니다.
human : 그럼 지금 발주해야 하나요? ← 이번 질문
도구 호출 요청과 실행 결과까지 전부 쌓여 있습니다. 4장에서 history 에 기록을 쌓던 작업이 자동으로 이루어지고 있는 것입니다.
직접 확인해 보세요.
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
config = {"configurable": {"thread_id": "재고담당-1"}}
for m in analyst_with_memory.get_state(config).values["messages"]:
print(f" {type(m).__name__:14s} {str(m.content)[:50]}")
7. 이 방식의 위험 — 잘못된 기억
기억이 좋기만 한 건 아닙니다.
손님: P0003 재고 알려주세요
봇 : 3개입니다.
손님: 아니 P0004요.
봇 : P0004는 12개입니다.
손님: 그거 재주문 기준은요?
봇 : ??? ← '그거'가 P0003인가 P0004인가
대화가 꼬이면 기억도 꼬입니다. 그리고 꼬인 기억은 계속 따라다닙니다.
앞 절에서 본 대책들이 여기서도 필요합니다.
| 상황 | 대책 |
|---|---|
| 주제가 바뀜 | 새 thread_id 로 시작 |
| 대화가 꼬임 | 기록을 지우고 다시 (thread_id 교체) |
| 너무 길어짐 | 최근 N개만 보내기 |
실제 서비스에서는 "새 대화 시작" 버튼이 있습니다. 그게 하는 일이 정확히
thread_id를 새로 발급하는 것입니다.
8. 5장 에이전트는 그대로입니다
확인해 둘 것 — ch05_csv_agent.py 는 한 글자도 안 고쳤습니다.
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
from ch05_csv_agent import run_pandas # 도구만 가져옴
ch05_csv_agent.py 의 agent 는 여전히 기억이 없습니다. 7장에서 새 에이전트를 따로 만든 것입니다.
왜 이렇게 하나 — 5장 실습을 다시 돌려도 그대로 동작해야 하니까요. 앞 장을 망가뜨리지 않으면서 기능을 더하는 방식입니다.
실무에서도 같습니다 — 도구는 재사용하고, 에이전트는 용도에 맞게 새로 조립합니다. 8장에서 이 원칙이 본격적으로 쓰입니다.
스스로 해 보기
- 위의
get_state()코드로 쌓인 메시지를 전부 찍어 보세요.ToolMessage가 몇 개인가요? - 질문을 이어 붙여 대화를 더 길게 해 보세요.
python for q in ["P0003 스마트워치 재고가 얼마나 남았나요?", "그거 재주문 기준은요?", "그럼 지금 발주해야 하나요?", "몇 개를 발주해야 하죠?", # ← 추가 "그 상품 리뷰 평점은 어떤가요?"]: # ← 추가
"몇 개를 발주해야 하죠?" 에 27개라고 답하나요? CSV_SYSTEM에서 "손님이 '그거'처럼..." 줄을 지우고 실행해 보세요. 달라지나요?thread_id를 질문할 때마다 다르게 넘겨 보세요. 어떻게 되나요?
2번의 "몇 개를 발주해야 하죠?"는 두 숫자를 빼야 나오는 답입니다. 모델이 도구를 쓰는지, 머릿속으로 계산하는지
get_state()로 확인해 보세요.
핵심 정리
checkpointer=InMemorySaver()한 줄로 5장 에이전트가 멀티턴이 됩니다.- 되는 이유는
create_agent도 결국 StateGraph 이기 때문입니다. - 도구는 5장 것을
import로 재사용합니다.if __name__ == "__main__":덕분에 안전합니다. - 시스템 지시에 "앞말을 가리키면 기록에서 찾아라" 를 추가하면 안정적입니다.
- 부를 때
thread_id를 넘기는 것만 5장과 다릅니다. 체크포인터와thread_id는 짝입니다. - "그럼 지금 발주해야 하나요?" — 상품명도 숫자도 없는 질문에 답합니다. 앞 답변이 기록에 남아 있기 때문입니다.
- 잘못된 기억은 계속 따라다닙니다. 대책은
thread_id를 새로 발급하는 것 — "새 대화 시작" 버튼이 그 일을 합니다. - 5장 파일은 안 고쳤습니다. 도구는 재사용하고 에이전트는 새로 조립합니다.