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

따라하기 — 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.pyagent 는 여전히 기억이 없습니다. 7장에서 새 에이전트를 따로 만든 것입니다.

왜 이렇게 하나 — 5장 실습을 다시 돌려도 그대로 동작해야 하니까요. 앞 장을 망가뜨리지 않으면서 기능을 더하는 방식입니다.

실무에서도 같습니다 — 도구는 재사용하고, 에이전트는 용도에 맞게 새로 조립합니다. 8장에서 이 원칙이 본격적으로 쓰입니다.


스스로 해 보기

  1. 위의 get_state() 코드로 쌓인 메시지를 전부 찍어 보세요. ToolMessage 가 몇 개인가요?
  2. 질문을 이어 붙여 대화를 더 길게 해 보세요.
    python for q in ["P0003 스마트워치 재고가 얼마나 남았나요?", "그거 재주문 기준은요?", "그럼 지금 발주해야 하나요?", "몇 개를 발주해야 하죠?", # ← 추가 "그 상품 리뷰 평점은 어떤가요?"]: # ← 추가
    "몇 개를 발주해야 하죠?" 에 27개라고 답하나요?
  3. CSV_SYSTEM 에서 "손님이 '그거'처럼..." 줄을 지우고 실행해 보세요. 달라지나요?
  4. thread_id질문할 때마다 다르게 넘겨 보세요. 어떻게 되나요?

2번의 "몇 개를 발주해야 하죠?"는 두 숫자를 빼야 나오는 답입니다. 모델이 도구를 쓰는지, 머릿속으로 계산하는지 get_state() 로 확인해 보세요.


핵심 정리

  • checkpointer=InMemorySaver() 한 줄로 5장 에이전트가 멀티턴이 됩니다.
  • 되는 이유는 create_agent 도 결국 StateGraph 이기 때문입니다.
  • 도구는 5장 것을 import 로 재사용합니다. if __name__ == "__main__": 덕분에 안전합니다.
  • 시스템 지시에 "앞말을 가리키면 기록에서 찾아라" 를 추가하면 안정적입니다.
  • 부를 때 thread_id 를 넘기는 것만 5장과 다릅니다. 체크포인터와 thread_id 는 짝입니다.
  • "그럼 지금 발주해야 하나요?" — 상품명도 숫자도 없는 질문에 답합니다. 앞 답변이 기록에 남아 있기 때문입니다.
  • 잘못된 기억은 계속 따라다닙니다. 대책은 thread_id 를 새로 발급하는 것 — "새 대화 시작" 버튼이 그 일을 합니다.
  • 5장 파일은 안 고쳤습니다. 도구는 재사용하고 에이전트는 새로 조립합니다.
← 이전 절04. 체크포인터 한 줄이면 기억이 생긴다다음 절 →06. 따라하기 그 상품 지난달은 이어서 묻기