[๊ฐ•์˜๋…ธํŠธ] LangChain Academy : Introduction to LangGraph (Module 2)

Posted by Euisuk's Dev Log on October 16, 2024

[๊ฐ•์˜๋…ธํŠธ] LangChain Academy : Introduction to LangGraph (Module 2)

์›๋ณธ ๊ฒŒ์‹œ๊ธ€: https://velog.io/@euisuk-chung/LangChain-Academy-Introduction-to-LangGraph-Module-2

๋žญ์ฒด์ธ(LangChain)๊ณผ ๋žญ๊ทธ๋ž˜ํ”„(LangGraph)๋Š” ๋Œ€๊ทœ๋ชจ ์–ธ์–ด ๋ชจ๋ธ(LLM)์„ ํ™œ์šฉํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์„ ์œ„ํ•œ ๋„๊ตฌ๋“ค์ž…๋‹ˆ๋‹ค. ์œ„ ๊ฐ•์˜๋Š” LangChain์—์„œ ์šด์˜ํ•˜๋Š” LangChain Academy์—์„œ ์ œ์ž‘ํ•œ โ€œIntroduction to LangGraphโ€ ๊ฐ•์˜์˜ ๋‚ด์šฉ์„ ์ •๋ฆฌ ๋ฐ ์ถ”๊ฐ€ ์„ค๋ช…ํ•œ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.

์ด๋ฒˆ ํฌ์ŠคํŠธ๋Š” โ€œModule2โ€๋‚ด์šฉ์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค:

๋ชฉ์ฐจ

  • Lesson 1: State Schema
  • Lesson 2: State Reducers
  • Lesson 3: Multiple Schemas
  • Lesson 4: Trim and Filter Messages
  • Lesson 5: Chatbot w/ Summarizing Messages and Memory
  • Lesson 6: Chatbot w/ Summarizing Messages and External Memory

Lesson 1: State Schema

LangGraph์˜ ์ฃผ์š” ๊ฐœ๋…๊ณผ ๊ตฌ์„ฑ ์š”์†Œ

  1. ๊ทธ๋ž˜ํ”„ ๊ตฌ์กฐ:

    • LangGraph๋Š” ์—์ด์ „ํŠธ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ทธ๋ž˜ํ”„๋กœ ๋ชจ๋ธ๋งํ•ฉ๋‹ˆ๋‹ค.
    • ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ: State(์ƒํƒœ), Nodes(๋…ธ๋“œ), Edges(์—ฃ์ง€)
  2. StateGraph์™€ MessageGraph:

    • StateGraph: ์‚ฌ์šฉ์ž ์ •์˜ State ๊ฐ์ฒด๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ ์ฃผ์š” ๊ทธ๋ž˜ํ”„ ํด๋ž˜์Šค
    • MessageGraph: State๊ฐ€ ๋ฉ”์‹œ์ง€ ๋ฆฌ์ŠคํŠธ๋กœ๋งŒ ๊ตฌ์„ฑ๋œ ํŠน์ˆ˜ํ•œ ๊ทธ๋ž˜ํ”„ ์œ ํ˜•
  3. ์ƒํƒœ ๊ด€๋ฆฌ:

    • TypedDict๋‚˜ Pydantic ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํƒœ ์Šคํ‚ค๋งˆ ์ •์˜
    • Reducer ํ•จ์ˆ˜๋ฅผ ํ†ตํ•œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋ฐฉ์‹ ์ •์˜
    • ๋‹ค์ค‘ ์Šคํ‚ค๋งˆ ์ง€์›: ๋‚ด๋ถ€ ๋…ธ๋“œ ํ†ต์‹ , ์ž…์ถœ๋ ฅ ์Šคํ‚ค๋งˆ ๋ถ„๋ฆฌ ๋“ฑ
  4. ๋…ธ๋“œ์™€ ์—ฃ์ง€:

    • ๋…ธ๋“œ: ๊ทธ๋ž˜ํ”„์˜ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋Š” Python ํ•จ์ˆ˜
    • ์—ฃ์ง€: ๋…ธ๋“œ ๊ฐ„ ์—ฐ๊ฒฐ๊ณผ ์‹คํ–‰ ํ๋ฆ„์„ ์ •์˜
    • ์กฐ๊ฑด๋ถ€ ์—ฃ์ง€๋ฅผ ํ†ตํ•œ ๋™์  ๋ผ์šฐํŒ…
  5. ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ:

    • add_messages ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ ํšจ์œจ์ ์ธ ๋ฉ”์‹œ์ง€ ๊ด€๋ฆฌ
    • MessagesState๋ฅผ ํ†ตํ•œ ๊ฐ„ํŽธํ•œ ๋ฉ”์‹œ์ง€ ์ƒํƒœ ๊ด€๋ฆฌ
  6. ๊ทธ๋ž˜ํ”„ ์‹คํ–‰:

    • ์ปดํŒŒ์ผ ๊ณผ์ •์„ ํ†ตํ•œ ๊ทธ๋ž˜ํ”„ ๊ตฌ์กฐ ๊ฒ€์ฆ
    • โ€œsuper-stepsโ€๋ฅผ ํ†ตํ•œ ์ด์‚ฐ์  ์‹คํ–‰ ๋ฐฉ์‹
  7. ์œ ์—ฐ์„ฑ๊ณผ ํ™•์žฅ์„ฑ:

    • ๋‹ค์–‘ํ•œ ์ƒํƒœ ์Šคํ‚ค๋งˆ ๋ฐ ์—…๋ฐ์ดํŠธ ๋ฐฉ์‹ ์ง€์›
    • ๋ณต์žกํ•œ ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌํ˜„ ๊ฐ€๋Šฅ

State Schema๋Š” Langraph์—์„œ ์—์ด์ „ํŠธ๊ฐ€ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐ์ดํ„ฐ์˜ ๊ตฌ์กฐ์™€ ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ด ์Šคํ‚ค๋งˆ๋Š” ๋ฐ์ดํ„ฐ์˜ ํ˜•์‹์„ ์ง€์ •ํ•˜์—ฌ, ๊ทธ๋ž˜ํ”„ ๋‚ด์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌ๋˜๊ณ  ์—…๋ฐ์ดํŠธ๋˜๋Š”์ง€๋ฅผ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค.

  • Langraph์—์„œ๋Š” Typedict, Python Data Classes, Pydantic ๋“ฑ ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ๊ฐ์˜ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

    1. Typedict: ์ฃผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๋ฐฉ์‹์œผ๋กœ, Python์˜ ๋”•์…”๋„ˆ๋ฆฌ ํ˜•์‹์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜์—ฌ ํ‚ค(key)์™€ ํ•ด๋‹น ๊ฐ’์˜ ํƒ€์ž…์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
      • ํ•˜์ง€๋งŒ Typedict๋Š” ๋Ÿฐํƒ€์ž„์—์„œ ํƒ€์ž… ๊ฒ€์‚ฌ๋ฅผ ํ•˜์ง€ ์•Š์•„, ์ž˜๋ชป๋œ ๊ฐ’์ด ํ• ๋‹น๋˜์–ด๋„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

    • ์•ž์— Module 1์—์„œ ๋‚˜์™”๋˜ ์˜ˆ์‹œ๋กœ ์•„๋ž˜์™€ ๊ฐ™์€ ์˜ˆ์‹œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      
      # Module 1 - TypeDict์˜ˆ์‹œ
      import random
      from IPython.display import Image, display
      from langgraph.graph import StateGraph, START, END
          
      def node_1(state):
          print("---Node 1---")
          return {"name": state['name'] + " is ... "}
          
      def node_2(state):
          print("---Node 2---")
          return {"mood": "happy"}
          
      def node_3(state):
          print("---Node 3---")
          return {"mood": "sad"}
          
      def decide_mood(state) -> Literal["node_2", "node_3"]:
          
          # Here, let's just do a 50 / 50 split between nodes 2, 3
          if random.random() < 0.5:
          
              # 50% of the time, we return Node 2
              return "node_2"
          
          # 50% of the time, we return Node 3
          return "node_3"
          
      # Build graph
      builder = StateGraph(TypedDictState)
      builder.add_node("node_1", node_1)
      builder.add_node("node_2", node_2)
      builder.add_node("node_3", node_3)
          
      # Logic
      builder.add_edge(START, "node_1")
      builder.add_conditional_edges("node_1", decide_mood)
      builder.add_edge("node_2", END)
      builder.add_edge("node_3", END)
          
      # Add
      graph = builder.compile()
          
      # View
      display(Image(graph.get_graph().draw_mermaid_png()))
      

      (์ฐธ๊ณ ) ์ผ๋ฐ˜ ํด๋ž˜์Šค

      • ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ๊ฐ์ฒด ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์˜ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.
      • ์†์„ฑ๊ณผ ๋ฉ”์„œ๋“œ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
      • init() ๋“ฑ์˜ ๋ฉ”์„œ๋“œ๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      • ์บก์Аํ™”, ์ƒ์†, ๋‹คํ˜•์„ฑ ๋“ฑ์˜ OOP ๊ฐœ๋…์„ ์™„์ „ํžˆ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    (์ฐธ๊ณ ) TypedDict

    • Python 3.8๋ถ€ํ„ฐ ์ •์‹์œผ๋กœ ๋„์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค

      (์ด์ „ ๋ฒ„์ „์—์„œ๋Š” typing_extensions ๋ชจ๋“ˆ์„ ํ†ตํ•ด ์‚ฌ์šฉ ๊ฐ€๋Šฅ).

    • ๋”•์…”๋„ˆ๋ฆฌ์˜ ํ‚ค์™€ ๊ฐ’์— ๋Œ€ํ•œ ํƒ€์ž…์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.
    • ๋Ÿฐํƒ€์ž„์—๋Š” ์ผ๋ฐ˜ ๋”•์…”๋„ˆ๋ฆฌ์ฒ˜๋Ÿผ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
    • ํ‚ค์— ๋Œ€ํ•œ ์ ‘๊ทผ์€ ๋”•์…”๋„ˆ๋ฆฌ ๋ฌธ๋ฒ•์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค (์˜ˆ: obj["key"]).
    • ์ฃผ๋กœ ์ •์  ํƒ€์ž… ๊ฒ€์‚ฌ๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

    (์ฐธ๊ณ ) Dataclass

    • Python 3.7๋ถ€ํ„ฐ ๋„์ž…๋œ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.
    • ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ ํด๋ž˜์Šค๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.
    • ์ž๋™์œผ๋กœ init(), repr(), eq() ๋“ฑ์˜ ๋ฉ”์„œ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    • ํƒ€์ž… ํžŒํŒ…์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
    • ์†์„ฑ์— ์ง์ ‘ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค (์˜ˆ: obj.attribute).
    • ๋ถˆ๋ณ€์„ฑ(immutability)์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    (์ฐธ๊ณ ) TypedDict vs Dataclass

    • ์ฃผ์š” ์ฐจ์ด์ ์œผ๋กœ๋Š” ์†์„ฑ ์ ‘๊ทผ ๋ฐฉ์‹์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

      • TypedDict: ๋”•์…”๋„ˆ๋ฆฌ ์Šคํƒ€์ผ๋กœ ์ ‘๊ทผ (state[โ€œnameโ€])
      • dataclass: ๊ฐ์ฒด ์†์„ฑ ์Šคํƒ€์ผ๋กœ ์ ‘๊ทผ (state.name)
    1. Python Data Classes: ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค๋Š” ๋” ๊ฐ„๊ฒฐํ•œ ๊ตฌ๋ฌธ์„ ์ œ๊ณตํ•˜๋ฉฐ, ์†์„ฑ์— ์ ‘๊ทผํ•  ๋•Œ dict ๋Œ€์‹  dot(.) ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
      • ํ•˜์ง€๋งŒ ์—ญ์‹œ ํƒ€์ž… ํžŒํŠธ๊ฐ€ ๋Ÿฐํƒ€์ž„์— ๊ฐ•์ œ๋˜์ง€ ์•Š์•„ ์ž˜๋ชป๋œ ๊ฐ’์„ ํ• ๋‹นํ•ด๋„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
        def node_1(state):
          print("---Node 1---")
          return {"name": state.name + " is ... "}
        
        # Build graph
        builder = StateGraph(DataclassState)
        builder.add_node("node_1", node_1)
        builder.add_node("node_2", node_2)
        builder.add_node("node_3", node_3)
        
        # Logic
        builder.add_edge(START, "node_1")
        builder.add_conditional_edges("node_1", decide_mood)
        builder.add_edge("node_2", END)
        builder.add_edge("node_3", END)
        
        # Add
        graph = builder.compile()
        
        # View
        display(Image(graph.get_graph().draw_mermaid_png()))
    
    1. Pydantic: ๋Ÿฐํƒ€์ž„์—์„œ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
      • Pydantic์€ Python์˜ ํƒ€์ž… ํžŒํŒ…์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ๊ณผ ์„ค์ • ๊ด€๋ฆฌ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. LangGraph์—์„œ ์ƒํƒœ ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•  ๋•Œ ํŠนํžˆ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
      • Pydantic ์ฃผ์š” ํŠน์ง•
    • ๋Ÿฐํƒ€์ž„ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ
    • ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์ง€์›
    • ์ž๋™ ๋ฌธ์„œํ™”
    • ์„ค์ • ๊ด€๋ฆฌ
    • JSON ์Šคํ‚ค๋งˆ ์ƒ์„ฑ
    • ์•„๋ž˜๋Š” ์‹ค์ œ Pydantic ํด๋ž˜์Šค๋ฅผ ์„ ์–ธํ•˜๊ณ  validation์„ ์ˆ˜ํ–‰ํ•˜๋Š” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      from pydantic import BaseModel, field_validator, ValidationError
      # BaseModel ์ƒ์†
      class PydanticState(BaseModel):
          name: str
          mood: str # "happy" or "sad" 
          
          @field_validator('mood')
          @classmethod
          def validate_mood(cls, value):
              # Ensure the mood is either "happy" or "sad"
              if value not in ["happy", "sad"]:
                  raise ValueError("Each mood must be either 'happy' or 'sad'")
              return value
          
      try:
          state = PydanticState(name="John Doe", mood="mad")
      except ValidationError as e:
          print("Validation Error:", e)
      

      a. BaseModel ์ƒ์†:

      • PydanticState(BaseModel): PydanticState ํด๋ž˜์Šค๋Š” Pydantic์˜ BaseModel์„ ์ƒ์†๋ฐ›์•„ ์ •์˜๋ฉ๋‹ˆ๋‹ค.
      • ์ด๋ฅผ ํ†ตํ•ด ์ž๋™ ๊ฒ€์ฆ, ์ง๋ ฌํ™”, ์—ญ์ง๋ ฌํ™” ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

      b. ํ•„๋“œ ์ •์˜:

      • name๊ณผ mood ํ•„๋“œ๋ฅผ str ํƒ€์ž…์œผ๋กœ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

      c. @field_validator ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ:

      • @field_validator ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ : Pydantic ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. Pydantic์€ ํŒŒ์ด์ฌ์—์„œ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ์ •์˜ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
      • @field_validator๋Š” ํŠน์ • ํ•„๋“œ์˜ ๊ฐ’์ด ๋งž๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋กœ์ง์„ ์ง์ ‘ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.
      • ์ฆ‰, ์–ด๋–ค ํ•„๋“œ์— ๊ฐ’์ด ๋“ค์–ด์™”์„ ๋•Œ ๊ทธ ๊ฐ’์ด ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ์กฐ๊ฑด์— ๋งž๋Š”์ง€ ์ž๋™์œผ๋กœ ๊ฒ€์‚ฌํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์ด์—์š”.
      • ์˜ˆ๋ฅผ ๋“ค์–ด, @field_validator('mood')๋ผ๋ฉด, โ€œmoodโ€œ๋ผ๋Š” ํ•„๋“œ์— ๊ฐ’์ด ๋“ค์–ด์˜ฌ ๋•Œ, ๊ทธ ๊ฐ’์ด โ€œhappyโ€๋‚˜ โ€œsadโ€ ์ค‘ ํ•˜๋‚˜์ธ์ง€ ํ™•์ธํ•˜๊ณ ์ž ํ•˜๋Š” ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉ๋˜์—ˆ๋‹ค๊ณ  ๋ณด๋ฉด ๋ฉ๋‹ˆ๋‹ค.

      d. @classmethod:

      • ํด๋ž˜์Šค ์ž์ฒด์— ๋Œ€ํ•ด ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.
      • ์ผ๋ฐ˜์ ์œผ๋กœ ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ค๋ฉด ์ธ์Šคํ„ด์Šค(๊ฐ์ฒด)๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ˜ธ์ถœํ•ด์•ผ ํ•˜๋Š”๋ฐ, ํด๋ž˜์Šค ๋ฉ”์„œ๋“œ๋Š” ์ธ์Šคํ„ด์Šค๊ฐ€ ์•„๋‹Œ ํด๋ž˜์Šค ์ž์ฒด์— ์—ฐ๊ฒฐ๋˜์–ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
      • Pydantic์—์„œ ํ•„๋“œ ๊ฒ€์ฆ์„ ํ•  ๋•Œ, ๊ฒ€์ฆ ๋ฉ”์„œ๋“œ๋ฅผ ํด๋ž˜์Šค ๋ ˆ๋ฒจ์—์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— @classmethod๊ฐ€ ํ•„์š”๋กœํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
      • ์ฆ‰, ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค๊ธฐ ์ „์— ํ•„๋“œ ๊ฐ’์„ ํ™•์ธํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํด๋ž˜์Šค ์ „์ฒด์—์„œ ๊ทธ ๊ฒ€์ฆ ๋กœ์ง์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก @classmethod๋กœ ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•˜๋Š” ๊ฒƒ์ด์ฃ .

      e. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ:

      • ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ๋กœ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๋ ค๊ณ  ํ•˜๋ฉด ValidationError๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
      • ์•„๋ž˜ ์˜ˆ์‹œ์—์„œ๋Š” โ€œmadโ€๋ผ๋Š” ์œ ํšจํ•˜์ง€ ์•Š์€ mood ๊ฐ’์„ ์‚ฌ์šฉํ•˜์—ฌ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Build graph
builder = StateGraph(PydanticState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)

# Logic
builder.add_edge(START, "node_1")
builder.add_conditional_edges("node_1", decide_mood)
builder.add_edge("node_2", END)
builder.add_edge("node_3", END)

# Add
graph = builder.compile()

# View
display(Image(graph.get_graph().draw_mermaid_png()))


Lesson 2: State Reducers

์•ž์— Module 1์—์„œ ์„ค๋ช…ํ–ˆ๋˜ State Reducer์— ๋Œ€ํ•ด์„œ ๋”์šฑ ์ž์„ธํ•˜๊ฒŒ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ ์˜ˆ์‹œ๋Š” TypeDict๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์ถฉ๋Œ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class State(TypedDict):
    foo: int

def node_1(state):
    print("---Node 1---")
    return {"foo": state['foo'] + 1}

def node_2(state):
    print("---Node 2---")
    return {"foo": state['foo'] + 1}

def node_3(state):
    print("---Node 3---")
    return {"foo": state['foo'] + 1}

# Build graph
builder = StateGraph(State)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)

# Logic
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_1", "node_3")
builder.add_edge("node_2", END)
builder.add_edge("node_3", END)

# Add
graph = builder.compile()

# View
display(Image(graph.get_graph().draw_mermaid_png()))

์œ„ ๊ทธ๋ฆผ๋ฅผ ํ•ด์„ํ•ด๋ณด๋ฉด node_1์€ node_2์™€ node_3์œผ๋กœ ๋ถ„๊ธฐ๋ฉ๋‹ˆ๋‹ค. node_2์™€ node_3๋Š” ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

1
2
3
4
5
from langgraph.errors import InvalidUpdateError
try:
    graph.invoke({"foo" : 1})
except InvalidUpdateError as e:
    print(f"InvalidUpdateError occurred: {e}")

์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋ฌธ์ œ:

  • ๊ฐ ๋…ธ๋“œ๋Š” โ€˜fooโ€™ ๊ฐ’์„ 1์”ฉ ์ฆ๊ฐ€์‹œํ‚ค๋ ค ํ•ฉ๋‹ˆ๋‹ค.
  • node_2์™€ node_3๊ฐ€ ๋™์‹œ์— ์‹คํ–‰๋˜๋ฉด์„œ ๋‘˜ ๋‹ค โ€˜fooโ€™ ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ•˜๋ ค ํ•ฉ๋‹ˆ๋‹ค.

์ถฉ๋Œ ๋ฐœ์ƒ:

  • ๋ณ‘๋ ฌ ์‹คํ–‰ ์ค‘ ๋‘ ๋…ธ๋“œ๊ฐ€ ๊ฐ™์€ ํ‚ค(โ€˜fooโ€™)๋ฅผ ๋™์‹œ์— ์—…๋ฐ์ดํŠธํ•˜๋ ค ํ•ฉ๋‹ˆ๋‹ค.
  • LangGraph๋Š” ์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ ์–ด๋–ค ์—…๋ฐ์ดํŠธ๋ฅผ ์ ์šฉํ•ด์•ผ ํ• ์ง€ ๊ฒฐ์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
  • ์ด๋Ÿฌํ•œ ์ถฉ๋Œ ์ƒํ™ฉ์—์„œ LangGraph๋Š” InvalidUpdateError๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

    1
    2
    3
    4
    5
    6
    
      
    We see a problem! 
    Node 1 branches to nodes 2 and 3.
    Nodes 2 and 3 run in parallel, which means they run in the same step of the graph.
    They both attempt to overwrite the state *within the same step*. 
    This is ambiguous for the graph! Which state should it keep? 
    
  • ์ด๋Ÿฌํ•œ ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋ฐฉ์‹์„ ๋ช…ํ™•ํžˆ ์ •์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋•Œ ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ์ด ๋ฐ”๋กœ ๊ธฐ๋ณธ Reducer ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

  • Reducer๋Š” LangGraph์—์„œ ์ƒํƒœ(State) ์—…๋ฐ์ดํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

    • ์—…๋ฐ์ดํŠธ ๋ช…์‹œ์ ์œผ๋กœ ์ •์˜: ๋…ธ๋“œ์—์„œ ๋ฐ˜ํ™˜๋œ ์—…๋ฐ์ดํŠธ๋ฅผ ํ˜„์žฌ ์ƒํƒœ์— ์–ด๋–ป๊ฒŒ ์ ์šฉํ• ์ง€ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
    • ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ถฉ๋Œ์„ ํ•ด๊ฒฐ: ์—ฌ๋Ÿฌ ๋…ธ๋“œ์—์„œ ๋™์‹œ์— ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๋•Œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
  • Python์˜ typing ๋ชจ๋“ˆ์—์„œ ์ œ๊ณตํ•˜๋Š” Annotated๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ reducer ํ•จ์ˆ˜๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

    • Annotated๋Š” Python์˜ typing ๋ชจ๋“ˆ์—์„œ ์ œ๊ณตํ•˜๋Š” ํŠน๋ณ„ํ•œ ํƒ€์ž… ํžŒํŠธ์ž…๋‹ˆ๋‹ค.

      Annotated[Type, metadata1, metadata2, ...]

  • Python์˜ ๋‚ด์žฅ operator ๋ชจ๋“ˆ์˜ add ํ•จ์ˆ˜๋ฅผ reducer๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

    • LangGraph์—์„œ operator.add๋ฅผ reducer๋กœ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์ฃผ๋กœ ๋ฆฌ์ŠคํŠธ ์—ฐ๊ฒฐ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์ƒˆ๋กœ์šด ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ๊ธฐ์กด ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

      โž• operator.add

      • Python์˜ ๋‚ด์žฅ operator ๋ชจ๋“ˆ์—์„œ ์ œ๊ณตํ•˜๋Š” ์ผ๋ฐ˜์ ์ธ ๋ง์…ˆ ์—ฐ์‚ฐ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ํ•จ์ˆ˜๋Š” ๋‹จ์ˆœํžˆ ์ˆซ์ž๋ฅผ ๋”ํ•˜๋Š” ๊ฒƒ ์ด์ƒ์˜ ๊ธฐ๋Šฅ์„ ํ•ฉ๋‹ˆ๋‹ค:
        1. ์ˆซ์ž ๋ง์…ˆ:
          • ๋‘ ์ˆซ์ž๋ฅผ ๋”ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ: operator.add(1, 2) ๋Š” 3์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
        2. ๋ฌธ์ž์—ด ์—ฐ๊ฒฐ:
          • ๋‘ ๋ฌธ์ž์—ด์„ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ: operator.add("Hello", "World") ๋Š” "HelloWorld"๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
        3. ๋ฆฌ์ŠคํŠธ ์—ฐ๊ฒฐ:
          • ๋‘ ๋ฆฌ์ŠคํŠธ๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ: operator.add([1, 2], [3, 4]) ๋Š” [1, 2, 3, 4]๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
      1
      
           
      
  • ๊ทธ๋ ‡๋‹ค๋ฉด ์ด์ œ ๊ธฐ๋ณธ Reducer๋ฅผ ์‚ฌ์šฉํ•œ ์ฝ”๋“œ๋กœ ์—…๋ฐ์ดํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•ด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
      
    from operator import add
    from typing import Annotated
      
    class State(TypedDict):
        foo: Annotated[list[int], add]
      
    def node_1(state):
        print("---Node 1---")
        return {"foo": [state['foo'][-1] + 1]}
      
    def node_2(state):
        print("---Node 2---")
        return {"foo": [state['foo'][-1] + 1]}
      
    def node_3(state):
        print("---Node 3---")
        return {"foo": [state['foo'][-1] + 1]}
      
    # Build graph
    builder = StateGraph(State)
    builder.add_node("node_1", node_1)
    builder.add_node("node_2", node_2)
    builder.add_node("node_3", node_3)
      
    # Logic
    builder.add_edge(START, "node_1")
    builder.add_edge("node_1", "node_2")
    builder.add_edge("node_1", "node_3")
    builder.add_edge("node_2", END)
    builder.add_edge("node_3", END)
      
    # Add
    graph = builder.compile()
      
    # View
    display(Image(graph.get_graph().draw_mermaid_png()))
    

    • ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ–ˆ๋”๋‹ˆ ์—๋Ÿฌ ์—†์ด ๋Œ์•„๊ฐ€๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ๋ž˜ํ”„์˜ ๋…ธ๋“œ๋“ค์ด None ๊ฐ’์„ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์„ค๊ณ„๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด, TypeError๊ฐ€ ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.

์ด๋•Œ ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ์ด ๋ฐ”๋กœ ์ปค์Šคํ…€ Reducer ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

  • ์ด reduce_list ํ•จ์ˆ˜๋Š” ์‚ฌ์šฉ์ž ์ •์˜ ๋ฆฌ๋“€์„œ(custom reducer)๋กœ, ๋‘ ๋ฆฌ์ŠคํŠธ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฒฐํ•ฉํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.
    • ์ด ํ•จ์ˆ˜์˜ ์ฃผ์š” ํŠน์ง•๊ณผ None ์ฒ˜๋ฆฌ ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:
      • ๋‘ ๊ฐœ์˜ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ํ•˜๋‚˜์˜ ๋ฆฌ์ŠคํŠธ๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค.
      • None ๊ฐ’์„ ํฌํ•จํ•œ ๋‹ค์–‘ํ•œ ์ž…๋ ฅ ์ƒํ™ฉ์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def reduce_list(left: list | None, right: list | None) -> list:
    """Safely combine two lists, handling cases where either or both inputs might be None.

    Args:
        left (list | None): The first list to combine, or None.
        right (list | None): The second list to combine, or None.

    Returns:
        list: A new list containing all elements from both input lists.
               If an input is None, it's treated as an empty list.
    """
    if not left:
        left = []
    if not right:
        right = []
    return left + right
  • ์œ„์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•จ์œผ๋กœ์จ None์ด ๋“ค์–ด์™€๋„ ์ •์ƒ์ ์œผ๋กœ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

SUMMARY

  • ๊ธฐ๋ณธ Reducer: ์ƒํƒœ๋ฅผ ๋ฎ์–ด์“ฐ๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๊ทธ๋ž˜ํ”„์˜ ๋‘ ๋…ธ๋“œ๊ฐ€ ๋™์ผํ•œ ํ‚ค๋ฅผ ๋™์‹œ์— ์—…๋ฐ์ดํŠธํ•˜๋ฉด, ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ๋œ ๊ฐ’์ด ์ตœ์ข… ๊ฐ’์œผ๋กœ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
  • ์ปค์Šคํ…€ Reducer: ์ƒํƒœ๋ฅผ ๋ฎ์–ด์“ฐ์ง€ ์•Š๊ณ  ๊ฐ’์„ ๋ณ‘ํ•ฉํ•˜๊ฑฐ๋‚˜ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํ‚ค๊ฐ€ ๋ฆฌ์ŠคํŠธ๋กœ ์ •์˜๋œ ๊ฒฝ์šฐ Reducer๋ฅผ ์‚ฌ์šฉํ•ด ์ƒˆ๋กœ์šด ๊ฐ’์„ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ์—๋Ÿฌ ์ฒ˜๋ฆฌ: ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์‹œ ๊ฐ’์ด None๊ณผ ๊ฐ™์€ ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฐ’์ด ์ž…๋ ฅ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ปค์Šคํ…€ Reducer๋ฅผ ์ •์˜ํ•˜์—ฌ ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ์—๋„ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฐ’์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Messages

  • CustomMessagesState: TypedDict๋ฅผ ์ƒ์†๋ฐ›์•„ ์ปค์Šคํ…€ ๋”•์…”๋„ˆ๋ฆฌ ํƒ€์ž…์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

    messages ํ‚ค๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ •์˜ํ•˜๊ณ , Annotated๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ add_messages ๋ฆฌ๋“€์„œ๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

    • ์ถ”๊ฐ€์ ์ธ ํ‚ค๋“ค(added_key_1, added_key_2 ๋“ฑ)์„ ์ง์ ‘ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  • ExtendedMessagesState: MessagesState๋ฅผ ์ƒ์†๋ฐ›์Šต๋‹ˆ๋‹ค. ์ด ํด๋ž˜์Šค๋Š” ์ด๋ฏธ messages ํ‚ค์™€ add_messages ๋ฆฌ๋“€์„œ๊ฐ€ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

    • ์ถ”๊ฐ€์ ์ธ ํ‚ค๋“ค(added_key_1, added_key_2 ๋“ฑ)๋งŒ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from typing import Annotated
from langgraph.graph import MessagesState
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages

# Define a custom TypedDict that includes a list of messages with add_messages reducer
class CustomMessagesState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
    added_key_1: str
    added_key_2: str
    # etc

# Use MessagesState, which includes the messages key with add_messages reducer
class ExtendedMessagesState(MessagesState):
    # Add any keys needed beyond messages, which is pre-built 
    added_key_1: str
    added_key_2: str
    # etc
  • MessagesState์€ ๋‚ด์žฅ๋œ messages key๊ฐ€ ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. CustomMessagesState๋Š” ๋ณ„๋„๋กœ messages key๋ฅผ ์ •์˜ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

LangGraph์—์„œ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์ฒ˜๋ฆฌ/๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•

LangGraph์—์„œ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์ฒ˜๋ฆฌ/๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๋‘ ๊ฐ€์ง€ ์ฃผ์š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  • add_messages ๋ฆฌ๋“€์„œ : add_messages ๋ฆฌ๋“€์„œ๋Š” ์ƒํƒœ์˜ messages ํ‚ค์— ์ƒˆ ๋ฉ”์‹œ์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก์„ ์‰ฝ๊ฒŒ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • MessagesState ํด๋ž˜์Šค: MessagesState๋Š” messages ํ‚ค์™€ add_messages ๋ฆฌ๋“€์„œ๊ฐ€ ๋ฏธ๋ฆฌ ๊ตฌํ˜„๋œ ํŽธ๋ฆฌํ•œ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ์ƒ์†ํ•˜์—ฌ ์ถ”๊ฐ€ ํ‚ค๋ฅผ ํฌํ•จํ•˜๋Š” ์ปค์Šคํ…€ ์ƒํƒœ ํด๋ž˜์Šค๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

add_messages ๋ฆฌ๋“€์„œ์˜ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ

  • ๋ฉ”์‹œ์ง€ ๋ฎ์–ด์“ฐ๊ธฐ ๊ธฐ๋Šฅ
    • ๋™์ผํ•œ ID๋ฅผ ๊ฐ€์ง„ ๋ฉ”์‹œ์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ธฐ์กด ๋ฉ”์‹œ์ง€๋ฅผ ๋ฎ์–ด์”๋‹ˆ๋‹ค.
    • ์ด๋ฅผ ํ†ตํ•ด ํŠน์ • ๋ฉ”์‹œ์ง€๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
      1
      2
      3
      4
      5
      6
      7
      8
      
        # Initial state
        initial_messages = [AIMessage(content="Hello! How can I assist you?", name="Model", id="1"),
                            HumanMessage(content="I'm looking for information on marine biology.", name="HUMAN", id="2")
                           ]
        # New message to add
        new_message = HumanMessage(content="I'm looking for information on whales, specifically", name="HUMAN", id="2")
        # Test
        add_messages(initial_messages , new_message)
      

  • ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ ๊ธฐ๋Šฅ
    • RemoveMessage ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŠน์ • ID์˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค1
    • ์ด ๊ธฐ๋Šฅ์€ ๋” ์ด์ƒ ํ•„์š”ํ•˜์ง€ ์•Š์€ ๋ฉ”์‹œ์ง€๋ฅผ ์ƒํƒœ์—์„œ ์ œ๊ฑฐํ•  ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
        # Message list
        messages = [AIMessage("Hello how can I help you.", name="MODEL", id="1")]
        messages.append(HumanMessage("Hi. I am researching ocean mammals", name="HUMAN", id="2"))
        messages.append(AIMessage("That's great! Ocean mammals, also known as marine mammals. How can I help you?", name="MODEL", id="3"))
        messages.append(HumanMessage("Yes, I know about whales. But what others should I learn about?", name="HUMAN", id="4"))
        print("ORIGINAL MESSAGES:")
        print(messages)
        print('---------------')
        # Isolate messages to delete
        delete_messages = [RemoveMessage(id=m.id) for m in messages[:-2]]
        print("DELETED MESSAGES:")
        print(delete_messages)
        print('---------------')
        # Apply the deletion
        updated_messages = add_messages(messages, delete_messages)
        print("UPDATED MESSAGES:")
        print(updated_messages)
        print('---------------')
      


Lesson 3: Multiple Schemas

  • Langraph์—์„œ ๊ทธ๋ž˜ํ”„์˜ ๋…ธ๋“œ ๊ฐ„์—๋Š” ์—ฌ๋Ÿฌ ์Šคํ‚ค๋งˆ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    • ์ด๋Š” ์ž…๋ ฅ(Input) ์Šคํ‚ค๋งˆ์™€ ์ถœ๋ ฅ(Output) ์Šคํ‚ค๋งˆ๋ฅผ ๊ตฌ๋ถ„ํ•˜์—ฌ ๊ทธ๋ž˜ํ”„ ๋‚ด์—์„œ ๊ฐ๊ธฐ ๋‹ค๋ฅธ ํ˜•์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
    • ์ด๋Ÿฌํ•œ ๋‹ค์ค‘ ์Šคํ‚ค๋งˆ ์ ‘๊ทผ ๋ฐฉ์‹์€ ๋ณต์žกํ•œ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๋” ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„์„ ์ •๋ฐ€ํ•˜๊ฒŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.
  • ์ฃผ์š” ์Šคํ‚ค๋งˆ ์œ ํ˜•:

    1. InputState (์ž…๋ ฅ ์Šคํ‚ค๋งˆ): ๊ทธ๋ž˜ํ”„์— ์ „๋‹ฌ๋˜๋Š” ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
    2. OutputState (์ถœ๋ ฅ ์Šคํ‚ค๋งˆ): ๊ทธ๋ž˜ํ”„๊ฐ€ ์ตœ์ข…์ ์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
    3. OverallState (์ „์ฒด ์ƒํƒœ ์Šคํ‚ค๋งˆ): ๊ทธ๋ž˜ํ”„ ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜๋Š” ํฌ๊ด„์ ์ธ ์Šคํ‚ค๋งˆ์ž…๋‹ˆ๋‹ค.
    4. PrivateState (๋น„๊ณต๊ฐœ ์ƒํƒœ ์Šคํ‚ค๋งˆ): ํŠน์ • ๋…ธ๋“œ ๊ฐ„์—๋งŒ ๊ณต์œ ๋˜๋Š” ์ž„์‹œ ๋˜๋Š” ์ค‘๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
    5. User-defined schema (์‚ฌ์šฉ์ž ์ •์˜ ์Šคํ‚ค๋งˆ): ์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž์ถฐ ์ง์ ‘ ์ •์˜ํ•˜๋Š” ์Šคํ‚ค๋งˆ์ž…๋‹ˆ๋‹ค

๐Ÿค– (์ถ”๊ฐ€) ์˜ˆ๋ฅผ ๋“ค์–ด, ๊ณ ๊ฐ ์„œ๋น„์Šค ์ฑ—๋ด‡์„ ๋งŒ๋“ ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๐Ÿค– ์ด ์ฑ—๋ด‡์€ ๊ณ ๊ฐ์˜ ์งˆ๋ฌธ์„ ๋ฐ›์•„ ์ ์ ˆํ•œ ๋‹ต๋ณ€์„ ์ œ๊ณตํ•˜๊ณ , ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์—ฌ๋Ÿฌ ์Šคํ‚ค๋งˆ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. InputState (์ž…๋ ฅ ์Šคํ‚ค๋งˆ): ๊ทธ๋ž˜ํ”„์— ์ „๋‹ฌ๋˜๋Š” ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ •์˜

    1
    2
    3
    
    class InputState(TypedDict):
        customer_query: str
        customer_id: str
    
  2. OutputState (์ถœ๋ ฅ ์Šคํ‚ค๋งˆ): ๊ทธ๋ž˜ํ”„๊ฐ€ ์ตœ์ข…์ ์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ •์˜

    1
    2
    3
    
    class OutputState(TypedDict):
        response: str
        satisfaction_score: int
    
  3. OverallState (์ „์ฒด ์ƒํƒœ ์Šคํ‚ค๋งˆ): ๊ทธ๋ž˜ํ”„ ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜๋Š” ํฌ๊ด„์ ์ธ ์Šคํ‚ค๋งˆ

    1
    2
    3
    4
    5
    6
    7
    
    class OverallState(TypedDict):
        customer_query: str
        customer_id: str
        response: str
        satisfaction_score: int
        internal_notes: str
        database_results: dict
    
  4. PrivateState (๋น„๊ณต๊ฐœ ์ƒํƒœ ์Šคํ‚ค๋งˆ): ํŠน์ • ๋…ธ๋“œ ๊ฐ„์—๋งŒ ๊ณต์œ ๋˜๋Š” ์ž„์‹œ ๋˜๋Š” ์ค‘๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ •์˜

    1
    2
    3
    
    class PrivateState(TypedDict):
        sentiment_analysis: str
        priority_level: int
    
  5. ์ด์ œ ์ด ์Šคํ‚ค๋งˆ๋“ค์„ ์‚ฌ์šฉํ•˜๋Š” ๊ทธ๋ž˜ํ”„๋ฅผ ๊ตฌ์„ฑํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    from langgraph.graph import StateGraph, START, END
       
    def query_analyzer(state: InputState) -> PrivateState:
        # ๊ณ ๊ฐ ์งˆ๋ฌธ ๋ถ„์„
        return {"sentiment_analysis": "positive", "priority_level": 2}
       
    def database_searcher(state: OverallState) -> OverallState:
        # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฒ€์ƒ‰
        return {"database_results": {"product_info": "..."}}
       
    def response_generator(state: OverallState) -> OutputState:
        # ์‘๋‹ต ์ƒ์„ฑ
        return {"response": "Here's the information you requested...", "satisfaction_score": 8}
       
    graph = StateGraph(OverallState, input=InputState, output=OutputState)
       
    graph.add_node("query_analyzer", query_analyzer)
    graph.add_node("database_searcher", database_searcher)
    graph.add_node("response_generator", response_generator)
       
    graph.add_edge(START, "query_analyzer")
    graph.add_edge("query_analyzer", "database_searcher")
    graph.add_edge("database_searcher", "response_generator")
    graph.add_edge("response_generator", END)
       
    compiled_graph = graph.compile()
    

์ด ์˜ˆ์‹œ์—์„œ:

  1. ๊ทธ๋ž˜ํ”„๋Š” InputState๋กœ ์‹œ์ž‘ํ•˜์—ฌ ๊ณ ๊ฐ์˜ ์งˆ๋ฌธ์„ ๋ฐ›์Šต๋‹ˆ๋‹ค.
  2. query_analyzer ๋…ธ๋“œ๋Š” PrivateState๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋‚ด๋ถ€์ ์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  3. database_searcher์™€ response_generator ๋…ธ๋“œ๋Š” OverallState๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž‘์—…ํ•ฉ๋‹ˆ๋‹ค.
  4. ์ตœ์ข…์ ์œผ๋กœ ๊ทธ๋ž˜ํ”„๋Š” OutputState๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ๊ณ ๊ฐ์—๊ฒŒ ์‘๋‹ต์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•จ์œผ๋กœ์จ:

  • ์ž…๋ ฅ๊ณผ ์ถœ๋ ฅ์„ ๋ช…ํ™•ํžˆ ์ œํ•œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋‚ด๋ถ€ ์ž‘์—…์— ํ•„์š”ํ•œ ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํŠน์ • ๋…ธ๋“œ ๊ฐ„์—๋งŒ ๊ณต์œ ๋˜๋Š” ๋น„๊ณต๊ฐœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ตฌ์กฐ๋ฅผ ํ†ตํ•ด ๋ณต์žกํ•œ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉด์„œ๋„ ๊ฐ ๋‹จ๊ณ„์—์„œ ํ•„์š”ํ•œ ์ •๋ณด๋งŒ์„ ๋…ธ์ถœํ•˜๊ณ  ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ๊ฐ์˜ State๋ณ„๋กœ ์ƒ์„ธํ•˜๊ฒŒ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค:

PrivateState ์˜ˆ์‹œ

  • OverallState (์ „์ฒด ์ƒํƒœ ์Šคํ‚ค๋งˆ): ๊ทธ๋ž˜ํ”„ ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜๋Š” ํฌ๊ด„์ ์ธ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  • PrivateState (๋น„๊ณต๊ฐœ ์ƒํƒœ ์Šคํ‚ค๋งˆ): ํŠน์ • ๋…ธ๋“œ ๊ฐ„์—๋งŒ ๊ณต์œ ๋˜๋Š” ์ž„์‹œ ๋˜๋Š” ์ค‘๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
    • ํŠน์ • ๋…ธ๋“œ ๊ฐ„์—๋งŒ ๊ณต์œ ๋˜๋Š” ์ž„์‹œ ๋˜๋Š” ์ค‘๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ •์˜ํ• ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    • ์•„๋ž˜ ์ฝ”๋“œ์—์„œ๋Š” ์ค‘๊ฐ„์— ์žˆ๋Š” Private ๋…ธ๋“œ์—์„œ ์‚ฌ์šฉ๋œ State๋Š” ์™ธ๋ถ€๋กœ ๋“œ๋Ÿฌ๋‚˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from typing_extensions import TypedDict
from IPython.display import Image, display
from langgraph.graph import StateGraph, START, END

class OverallState(TypedDict):
    foo: int

class PrivateState(TypedDict):
    baz: int

def node_1(state: OverallState) -> PrivateState:
    print("---Node 1---")
    return {"baz": state['foo'] + 1}

def node_2(state: PrivateState) -> OverallState:
    print("---Node 2---")
    return {"foo": state['baz'] + 1}

# Build graph
builder = StateGraph(OverallState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)

# Logic
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)

# Add
graph = builder.compile()

# View
display(Image(graph.get_graph().draw_mermaid_png()))

  • baz is only included in PrivateState.
  • node_2 uses PrivateState as input, but writes out to OverallState.

Input / Output Schema

  • InputState (์ž…๋ ฅ ์Šคํ‚ค๋งˆ, ์งˆ๋ฌธ๋งŒ ํฌํ•จ): ๊ทธ๋ž˜ํ”„์— ์ „๋‹ฌ๋˜๋Š” ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  • OutputState (์ถœ๋ ฅ ์Šคํ‚ค๋งˆ, ๋‹ต๋ณ€๋งŒ ํฌํ•จ): ๊ทธ๋ž˜ํ”„๊ฐ€ ์ตœ์ข…์ ์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  • OverallState (์ „์ฒด ์ƒํƒœ ์Šคํ‚ค๋งˆ, ์งˆ๋ฌธ/๋‹ต๋ณ€/๋…ธํŠธ๋ฅผ ๋ชจ๋‘ ํฌํ•จ): ๊ทธ๋ž˜ํ”„ ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜๋Š” ํฌ๊ด„์ ์ธ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class InputState(TypedDict):
    question: str

class OutputState(TypedDict):
    answer: str

class OverallState(TypedDict):
    question: str
    answer: str
    notes: str

def thinking_node(state: InputState):
    return {"answer": "bye", "notes": "... his is name is Chung"}

def answer_node(state: OverallState) -> OutputState:
    return {"answer": "bye Chung"}

graph = StateGraph(OverallState, input=InputState, output=OutputState)
graph.add_node("answer_node", answer_node)
graph.add_node("thinking_node", thinking_node)
graph.add_edge(START, "thinking_node")
graph.add_edge("thinking_node", "answer_node")
graph.add_edge("answer_node", END)

graph = graph.compile()

# View
display(Image(graph.get_graph().draw_mermaid_png()))

graph.invoke({"question":"hi"})


Lesson 4: Trim and Filter Messages

  • ๋ณต์Šต:

    • from langchain_core.messages import AIMessage, HumanMessage: ์šฐ๋ฆฌ๋Š” Langchain์˜ Messages ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์—ฌ AIMessage, HumanMessage๋ฅผ ์ •์˜ํ•˜๊ณ  ์ด๋ฅผ LLM์— ์ „๋‹ฌํ•ด์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

      1
      2
      3
      4
      5
      6
      
      from langchain_core.messages import AIMessage, HumanMessage
      messages = [AIMessage(f"๋‹น์‹ ์ด ํ•ด์–‘ ํฌ์œ ๋ฅ˜๋ฅผ ์—ฐ๊ตฌํ•˜๊ณ  ์žˆ๋‹ค๊ณ  ํ•˜์…จ๋‚˜์š”?", name="AI")]
      messages.append(HumanMessage(f"๋„ค, ์ €๋Š” ๊ณ ๋ž˜์— ๋Œ€ํ•ด ์•Œ๊ณ  ์žˆ์–ด์š”. ํ•˜์ง€๋งŒ ๋‹ค๋ฅธ ์–ด๋–ค ๋™๋ฌผ์— ๋Œ€ํ•ด ๋ฐฐ์›Œ์•ผ ํ• ๊นŒ์š”?", name="HUMAN"))
          
      for m in messages:
          m.pretty_print()
      

    • ์ด์ „์— ๋‚˜๋ˆด๋˜ ๋Œ€ํ™”๋“ค์„ ๋ฐ”ํƒ•์œผ๋กœ LLM์ด ๋‹ต๋ณ€์„ ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

      1
      2
      3
      4
      5
      6
      
      from langchain_openai import ChatOpenAI
      llm = ChatOpenAI(model="gpt-4o")
      messages.append(llm.invoke(messages))
          
      for m in messages:
          m.pretty_print()
      

    • ์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์€ from langgraph.graph import MessagesState์— ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

      • โœจ ์•ž์—์„œ ๋‹ค๋ค˜๋˜ ๊ฒƒ์ฒ˜๋Ÿผ MessagesState์—๋Š” ๋‚ด์žฅ๋œ โ€˜messagesโ€™ ํ‚ค๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์ด ํ‚ค์—๋Š” ๋‚ด์žฅ๋œ add_messages ๋ฆฌ๋“€์„œ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
        1
        2
        3
        4
        
        from typing import Annotated
        from langgraph.graph import MessagesState
        from langchain_core.messages import AnyMessage
        from langgraph.graph.message import add_messages
        
        1
        2
        3
        4
        5
        6
        
        # add_messages ๋ฆฌ๋“€์„œ๋กœ ๋ฉ”์‹œ์ง€ ๋ฆฌ์ŠคํŠธ๋ฅผ ํฌํ•จํ•˜๋Š” ์‚ฌ์šฉ์ž ์ •์˜ TypedDict ์ •์˜
        class CustomMessagesState(TypedDict):
          messages: Annotated[list[AnyMessage], add_messages]
          added_key_1: str
          added_key_2: str
          # ๋“ฑ
        
        1
        2
        3
        4
        5
        6
        
        # MessagesState ์‚ฌ์šฉ, ์ด๋ฏธ add_messages ๋ฆฌ๋“€์„œ๊ฐ€ ์žˆ๋Š” messages ํ‚ค ํฌํ•จ
        class ExtendedMessagesState(MessagesState):
          # messages ์™ธ์— ํ•„์š”ํ•œ ํ‚ค ์ถ”๊ฐ€, messages๋Š” ๋ฏธ๋ฆฌ ๊ตฌ์ถ•๋˜์–ด ์žˆ์Œ 
          added_key_1: str
          added_key_2: str
          # etc
        
    • messages ํ‚ค: AnyMessage ๊ฐ์ฒด์˜ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. (list[AnyMessage])
    • AnyMessage๋Š” HumanMessage, AIMessage, SystemMessage ๋“ฑ ๋‹ค์–‘ํ•œ ๋ฉ”์‹œ์ง€ ํƒ€์ž…์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
    • add_messages ๋ฆฌ๋“€์„œ๊ฐ€ ๊ธฐ๋ณธ์œผ๋กœ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฆฌ๋“€์„œ๋Š” ์ƒˆ ๋ฉ”์‹œ์ง€๋ฅผ ๊ธฐ์กด ๋ฉ”์‹œ์ง€ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      
      from IPython.display import Image, display
      from langgraph.graph import MessagesState
      from langgraph.graph import StateGraph, START, END
        
      # Node
      def chat_model_node(state: MessagesState):
          return {"messages": llm.invoke(state["messages"])}
        
      # Build graph
      builder = StateGraph(MessagesState)
      builder.add_node("chat_model", chat_model_node)
      builder.add_edge(START, "chat_model")
      builder.add_edge("chat_model", END)
      graph = builder.compile()
        
      # View
      display(Image(graph.get_graph().draw_mermaid_png()))
      

      (์ฐธ๊ณ ) add_messages ๋ฆฌ๋“€์„œ Output ์˜ˆ์‹œ

      • add_messages๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ทธ๋ƒฅ str์œผ๋กœ ๋ฉ”์„ธ์ง€๋ฅผ ์ „๋‹ฌํ•ด์ค˜๋„ HumanMessage๋กœ ์ „๋‹ฌ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋จ„์— ๊ทธ๋ƒฅ append๋ฅผ ํ•  ๊ฒฝ์šฐ, ์ผ๋ฐ˜ list append๊ฐ€ ์ˆ˜ํ–‰๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

      • ๊ทธ๋ ‡๋‹ค๋ฉด append๋กœ HumanMessage๋‚˜ AIMessage๋ฅผ ๋„ฃ์–ด์ค„ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์€ ์—†์„๊นŒ์š”? ๋ฌผ๋ก  ์žˆ์Šต๋‹ˆ๋‹ค!

์žฅ๊ธฐ ๋Œ€ํ™”์—์„œ ๋ฉ”์‹œ์ง€ ํžˆ์Šคํ† ๋ฆฌ๊ฐ€ ๊ธธ์–ด์งˆ ๊ฒฝ์šฐ, ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰์ด ๊ธ‰์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด Langraph์—์„œ๋Š” ๋ฉ”์‹œ์ง€ ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฐฉ๋ฒ•๋“ค์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๊ธฐ๋ฒ•์— ๋Œ€ํ•ด์„œ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

    1. ํ•„ํ„ฐ๋ง: ๋ฉ”์‹œ์ง€ ํžˆ์Šคํ† ๋ฆฌ์—์„œ ํ•„์š”ํ•œ ์ตœ๊ทผ ๋ฉ”์‹œ์ง€๋งŒ ๋‚จ๊ธฐ๊ณ  ์ด์ „ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

      • (๋ณต์Šต) ์ด์ „ ์‹œ๊ฐ„์— ๋ฐฐ์šด remove_messages ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜๋Š” ํŠน์ • ๋ฉ”์‹œ์ง€ ID๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ญ์ œํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. -> ์ด๋Š” ๊ทธ๋ž˜ํ”„ state์—์„œ๋„ ํ•ด๋‹น ์ •๋ณด๊ฐ€ ์‚ฌ๋ผ์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค
      • ํ•˜์ง€๋งŒ ์ด๋ฒˆ์— ๋ฐฐ์šธ ํ•„ํ„ฐ๋ง์˜ ๊ฐœ๋…์€ ์ด์™€๋Š” ์กฐ๊ธˆ ๋‹ค๋ฅธ ๊ฐœ๋…์ž…๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ๋ชจ๋ธ์— ๋“ค์–ด๊ฐ€๊ฒŒ๋˜๋Š” ์ •๋ณด๋Š” ํ•œ์ •์ ์ด์ง€๋งŒ, ๊ทธ๋ž˜ํ”„์˜ state์—๋Š” ๋ชจ๋“  ์ •๋ณด๊ฐ€ ๋“ค์–ด๊ฐ€์žˆ์„ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ```

      Node

      def chat_model_node(state: MessagesState): return {โ€œmessagesโ€: [llm.invoke(state[โ€œmessagesโ€][-1:])]} ```

      • ์•„๋ž˜ ๊ทธ๋ฆผ ๋ณด๋ฉด message์—๋Š” ๋ชจ๋“  ์ •๋ณด๊ฐ€ ๋‹ค ๋‹ด๊ฒจ ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

      • ๋ฐ˜๋Œ€๋กœ ์‹ค์ œ llm์— ๋„˜์–ด๊ฐ„ ์งˆ์˜๋ฌธ์€ โ€œํ•ด๋งˆ์— ๋Œ€ํ•ด์„œ ์•Œ๋ ค์•Œ๋ ค์ค˜โ€๋ผ๋Š” ์งˆ๋ฌธ๋งŒ์ด ๋„˜์–ด๊ฐ„ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    2. ํŠธ๋ฆฌ๋ฐ: ๋ฉ”์‹œ์ง€๋ฅผ ํ† ํฐ ์ˆ˜ ๊ธฐ์ค€์œผ๋กœ ์ž˜๋ผ๋‚ด๋Š” ๋ฐฉ๋ฒ•(trim_messages)์ž…๋‹ˆ๋‹ค. ์œ„์˜ ํ•„ํ„ฐ๋ง์ด ์—์ด์ „ํŠธ ๊ฐ„์˜ ๋ฉ”์‹œ์ง€์˜ ์‚ฌํ›„ ํ•˜์œ„ ์ง‘ํ•ฉ๋งŒ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ˜๋ฉด, ํŠธ๋ฆฌ๋ฐ์€ ์ฑ„ํŒ… ๋ชจ๋ธ์ด ์‘๋‹ตํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํ† ํฐ ์ˆ˜๋ฅผ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      
           
       from langchain_core.messages import trim_messages
           
       # Node
       def chat_model_node(state: MessagesState):
           messages = trim_messages(
                   state["messages"],
                   max_tokens=100,
                   strategy="last",
                   token_counter=ChatOpenAI(model="gpt-4o"),
                   allow_partial=False,
               )
           return {"messages": [llm.invoke(messages)]}
      
      • state["messages"]: ํŠธ๋ฆฌ๋ฐํ•  ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค.
      • max_tokens=100: ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€์˜ ์ตœ๋Œ€ ํ† ํฐ ์ˆ˜๋ฅผ 100์œผ๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.
      • strategy="last": ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด ์—ญ์ˆœ์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ํฌํ•จ์‹œํ‚ต๋‹ˆ๋‹ค.
      • token_counter=ChatOpenAI(model="gpt-4"): ํ† ํฐ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ๋ชจ๋ธ์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
      • allow_partial=False: ๋ฉ”์‹œ์ง€๋ฅผ ๋ถ€๋ถ„์ ์œผ๋กœ ์ž๋ฅด๋Š” ๊ฒƒ์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

      • ์—ฌ๊ธฐ๋„ ์œ„์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ LLM ๋ชจ๋ธ์— ๋“ค์–ด๊ฐ€๋Š” ๊ฒƒ์€ token ์กฐ๊ฑด์— ํ•ด๋‹นํ•˜๋Š” ์งˆ์˜๋ฌธ๋งŒ ๋„˜์–ด๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

      • ๊ทธ๋ฆฌ๊ณ  langraph ์ƒ์—๋Š” ์ „์ฒด์˜ message history๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


Lesson 5: Chatbot w/ Summarizing Messages and Memory

์ด๋ฒˆ ๋‚ด์šฉ์€ ๋Œ€ํ™”ํ˜• ์ฑ—๋ด‡์—์„œ ๋Œ€ํ™”๋ฅผ ์š”์•ฝํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์š”์•ฝ ๊ธฐ๋Šฅ์€ ๋Œ€ํ™”๋ฅผ ์˜ค๋ž˜ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ๋ฉ”์‹œ์ง€์˜ ์ „์ฒด ๊ธฐ๋ก์„ ๋‚จ๊ธฐ์ง€ ์•Š๊ณ  ๋Œ€ํ™”๋ฅผ ์••์ถ•ํ•˜๋Š” ๊ธฐ์ˆ ๋กœ, ์•ž์—์„œ ๋‹ค๋ฃฌ ํ•„ํ„ฐ๋ง์ด๋‚˜ ๋ฉ”์‹œ์ง€ ์‚ญ์ œ๋ณด๋‹ค ๋” ๋งŽ์€ ์ •๋ณด๋ฅผ ๋ณด์กดํ•˜๋ ค๋Š” ์‹œ๋„์ž…๋‹ˆ๋‹ค.

  • ์ด ๊ธฐ๋Šฅ์€ LLMs(Large Language Models)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋Œ€ํ™”์˜ ์ฃผ์š” ๋‚ด์šฉ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์š”์•ฝํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

1. ๊ธฐ๋ณธ ์„ค์ • ๋ฐ ๋ฉ”์‹œ์ง€ ์ƒํƒœ ๊ตฌ์„ฑ

  • message state์—๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก์„ ์ €์žฅํ•˜๋Š” messages ํ‚ค๊ฐ€ ์žˆ์ง€๋งŒ, ์—ฌ๊ธฐ์— ๋Œ€ํ™” ์š”์•ฝ์„ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋Š” ์ƒˆ๋กœ์šด summary ํ‚ค๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
1
2
3
from langgraph.graph import MessagesState
class State(MessagesState):
    summary: str
  • call model์ด๋ผ๋Š” ๋…ธ๋“œ์—์„œ๋Š” ๊ธฐ์กด ์š”์•ฝ์ด ์žˆ์œผ๋ฉด ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก์— ์ถ”๊ฐ€ํ•˜๊ณ  ๋ชจ๋ธ์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from langchain_core.messages import SystemMessage, HumanMessage, RemoveMessage

# Define the logic to call the model
def call_model(state: State):
    
    # Get summary if it exists
    summary = state.get("summary", "")

    # If there is summary, then we add it
    if summary:
        
        # Add summary to system message
        system_message = f"Summary of conversation earlier: {summary}"

        # Append summary to any newer messages
        messages = [SystemMessage(content=system_message)] + state["messages"]
    
    else:
        messages = state["messages"]
    
    response = model.invoke(messages)
    return {"messages": response}

2. ์š”์•ฝ ๋…ธ๋“œ ๊ตฌ์„ฑ

  • ๋Œ€ํ™”๋ฅผ ์š”์•ฝํ•˜๋Š” summarize_conversation ๋…ธ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋Œ€ํ™” ์ƒํƒœ๋ฅผ ๋ฐ›์•„ ์š”์•ฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    • ๋งŒ์•ฝ ๊ธฐ์กด ์š”์•ฝ์ด ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ์ƒˆ๋กœ์šด ์š”์•ฝ์— ํฌํ•จํ•˜์—ฌ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
    • ๊ธฐ์กด ์š”์•ฝ์ด ์—†๋‹ค๋ฉด ์ƒˆ๋กœ ์š”์•ฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    • ์š”์•ฝ์„ ์ƒ์„ฑํ•œ ํ›„ ์ƒํƒœ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๊ธฐ ์œ„ํ•ด RemoveMessage๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ€์žฅ ์ตœ๊ทผ ๋ฉ”์‹œ์ง€ ๋‘ ๊ฐœ๋งŒ ๋‚จ๊ธฐ๊ณ  ๋‚˜๋จธ์ง€ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def summarize_conversation(state: State):
    
    # First, we get any existing summary
    summary = state.get("summary", "")

    # Create our summarization prompt 
    if summary:
        
        # A summary already exists
        summary_message = (
            f"This is summary of the conversation to date: {summary}\n\n"
            "Extend the summary by taking into account the new messages above:"
        )
        
    else:
        summary_message = "Create a summary of the conversation above:"

    # Add prompt to our history
    messages = state["messages"] + [HumanMessage(content=summary_message)]
    response = model.invoke(messages)
    
    # Delete all but the 2 most recent messages
    delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
    return {"summary": response.content, "messages": delete_messages}

3. ๋ฉ”์‹œ์ง€ ์‚ญ์ œ์™€ ์š”์•ฝ ์—…๋ฐ์ดํŠธ

  • ๋Œ€ํ™” ๊ธธ์ด์— ๋”ฐ๋ผ ์š”์•ฝ์„ ์ƒ์„ฑํ• ์ง€ ๊ฒฐ์ •ํ•˜๋Š” ์กฐ๊ฑด๋ถ€ ์—ฃ์ง€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
    • len(messages) > 6: ๊ธธ์ด๊ฐ€ 6๋ณด๋‹ค ๊ธธ๋ฉด ์š”์•ฝ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from langgraph.graph import END
# Determine whether to end or summarize the conversation
def should_continue(state: State):
    
    """Return the next node to execute."""
    
    messages = state["messages"]
    
    # If there are more than six messages, then we summarize the conversation
    if len(messages) > 6:
        return "summarize_conversation"
    
    # Otherwise we can just end
    return END

4.์ฒดํฌํฌ์ธํ„ฐ๋กœ ์ƒํƒœ ์œ ์ง€

  • ์ƒํƒœ๋Š” ์ผ์‹œ์ ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋Œ€ํ™”๊ฐ€ ๊ธธ์–ด์ง€๋ฉด ์ƒํƒœ๋ฅผ ๋ณด์กดํ•˜๊ธฐ ์œ„ํ•ด MemorySaver์˜ checkpointer ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ์ด ์ฒดํฌํฌ์ธํ„ฐ๋Š” ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ๋œ ์ƒํƒœ๋ฅผ ๋ณด์กดํ•˜๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ ๋ฒˆ์˜ ๋Œ€ํ™”์—์„œ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ์š”์•ฝ ํ›„ LLM์— ๋Œ€ํ™”๋ฅผ ์š”์ฒญํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ๊ฒฐ๊ณผ๋ฅผ ์–ป์€ ํ›„ output์— ์‚ฐ์ถœํ•˜๊ธฐ ์ „์— ๋ˆ„์ ๋œ ๋Œ€ํ™”๊ฐ€ ๋งŽ๋‹ค๋ฉด ์š”์•ฝ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํ˜•ํƒœ์˜ ๊ทธ๋ž˜ํ”„
    • memory ๊ธฐ๋Šฅ์ด ์žˆ์–ด ๋‹ค์Œ ๋Œ€ํ™”๋ฅผ ์‹œ์ž‘ํ•˜๋Š” ๋‹จ๊ณ„์—์„œ ๋ˆ„์ ๋œ ์ˆ˜์— ๋”ฐ๋ผ ์š”์•ฝ ์—ฌ๋ถ€ ๊ฒฐ์ •ํ•จ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from IPython.display import Image, display
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START

# Define a new graph
workflow = StateGraph(State)
workflow.add_node("conversation", call_model)
workflow.add_node(summarize_conversation)

# Set the entrypoint as conversation
workflow.add_edge(START, "conversation")
workflow.add_conditional_edges("conversation", should_continue)
workflow.add_edge("summarize_conversation", END)

# Compile
memory = MemorySaver()
graph = workflow.compile(checkpointer=memory)
display(Image(graph.get_graph().draw_mermaid_png()))

Threads

์ฒดํฌํฌ์ธํ„ฐ๋Š” ๊ฐ ๋‹จ๊ณ„์—์„œ ์ƒํƒœ๋ฅผ ์ฒดํฌํฌ์ธํŠธ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์ €์žฅ๋œ ์ฒดํฌํฌ์ธํŠธ๋Š” ๋Œ€ํ™”์˜ ์Šค๋ ˆ๋“œ๋กœ ๊ทธ๋ฃนํ™”๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์˜ˆ์‹œ: ChatGPT์—์„œ ์‚ฌ์šฉ์ž์™€ AI ์‚ฌ์ด์˜ ๋Œ€ํ™”๋Š” ์Šค๋ ˆ๋“œ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. ๊ฐ ์Šค๋ ˆ๋“œ๋Š” ํ•˜๋‚˜์˜ ์ฃผ์ œ๋‚˜ ๋Œ€ํ™” ํ๋ฆ„์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
  • ์Šค๋ ˆ๋“œ์˜ ํŠน์ง•:

    • ๊ฐ ๋ฉ”์‹œ์ง€๋Š” ์Šค๋ ˆ๋“œ ๋‚ด์˜ ์ฒดํฌํฌ์ธํŠธ์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.
    • ์ƒˆ๋กœ์šด ๋Œ€ํ™”๋ฅผ ์‹œ์ž‘ํ•˜๋ฉด ์ƒˆ ์Šค๋ ˆ๋“œ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
    • ํ•˜๋‚˜์˜ ์Šค๋ ˆ๋“œ ์•ˆ์—์„œ ๋Œ€ํ™”์˜ ๋งฅ๋ฝ์ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

  • ChatGPT ์ธํ„ฐํŽ˜์ด์Šค์—์„œ๋Š” ์™ผ์ชฝ ์‚ฌ์ด๋“œ๋ฐ”์— ๊ฐ ์Šค๋ ˆ๋“œ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ์ด๋ฅผ ํ†ตํ•ด:
    • ์ด์ „ ๋Œ€ํ™” ์Šค๋ ˆ๋“œ๋กœ ์‰ฝ๊ฒŒ ๋Œ์•„๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ์—ฌ๋Ÿฌ ์ฃผ์ œ์˜ ๋Œ€ํ™”๋ฅผ ๋™์‹œ์— ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ํŠน์ • ๋Œ€ํ™” ๋‚ด์šฉ์„ ๋น ๋ฅด๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Langraph์—์„œ๋Š” ์ด์™€ ๋น„์Šทํ•œ ๊ฐœ๋…์œผ๋กœ configurable์„ ์‚ฌ์šฉํ•˜์—ฌ ์Šค๋ ˆ๋“œ ID๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์ด config = {"configurable": {"thread_id": "1"}} ์„ ์–ธ ํ›„ invoke ์‹œ์— config์œผ๋กœ ๋„ฃ์–ด์ฃผ๊ฒŒ ๋˜๋ฉด ๋”ฐ๋กœ๋”ฐ๋กœ ์‹คํ–‰ํ•ด๋„ message์— ๊ธฐ๋ก์ด ์ œ๋Œ€๋กœ ๋‚จ๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•„์ง๊นŒ์ง€ 6๊ฐœ์˜ ๋Œ€ํ™”๊ฐ€ ๋„˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ์š”์•ฝ(summary)๊ฐ€ ์ˆ˜ํ–‰๋˜์ง€ ์•Š์€ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ํ•˜์ง€๋งŒ ํ•˜๋‚˜์˜ ๋Œ€ํ™”๋ฅผ ์ถ”๊ฐ€ํ•จ์œผ๋กœ์จ 6๊ฐœ๊ฐ€ ๋„˜์–ด์„œ ์š”์•ฝ์ด ๋ฐœ์ƒํ•˜๊ณ  should_continue => summarize_conversation์ด ๋ฐœ์ƒํ•˜๊ณ , ๊ฐ€์žฅ ์ตœ๊ทผ 2๊ฐœ์˜ ๋Œ€ํ™”๋งŒ์ด ๋‚จ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๋˜ํ•œ, ์ด์ „๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ Summary๋„ ์ƒ์„ฑ์ด ๋˜์–ด์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’ก (SUMMARY) ์œ„์— ์ •์˜ํ•œ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค:

  • ์š”์•ฝ์ด ์ƒ์„ฑ๋˜๋ฉด State ๊ฐ์ฒด์˜ summary ํ•„๋“œ์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
  • ์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€๊ฐ€ ๋“ค์–ด์˜ค๋ฉด call_model ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
  • call_model ํ•จ์ˆ˜ ๋‚ด์—์„œ:
    • ์ €์žฅ๋œ ์š”์•ฝ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
    • ์š”์•ฝ์ด ์žˆ๋‹ค๋ฉด, ์ด๋ฅผ ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    • ์ด ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€๋ฅผ ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ์˜ ๋งจ ์•ž์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
    • ๊ทธ ๋‹ค์Œ์— ์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€๋ฅผ ํฌํ•จํ•œ ์ตœ๊ทผ ๋ฉ”์‹œ์ง€๋“ค์ด ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค.
  • ์ด๋ ‡๊ฒŒ ๊ตฌ์„ฑ๋œ ๋ฉ”์‹œ์ง€ ๋ฆฌ์ŠคํŠธ(์š”์•ฝ + ์ตœ๊ทผ ๋ฉ”์‹œ์ง€๋“ค)๊ฐ€ LLM์— ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

Lesson 6: Chatbot w/ Summarizing Messages and External Memory

Langraph๋Š” ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€์˜ ์—ฐ๋™์„ ํ†ตํ•ด ๋” ์˜์†์ ์ธ ๋ฉ”๋ชจ๋ฆฌ ์ €์žฅ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  1. SQL Lite ์‚ฌ์šฉ: Langraph๋Š” SQL Lite์™€ ๊ฐ™์€ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ง€์›ํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์˜๊ตฌ์ ์œผ๋กœ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ ์žฅ๊ธฐ ๋Œ€ํ™”๋ฅผ ์ง€์†์ ์œผ๋กœ ์œ ์ง€ํ•˜๊ณ  ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“Š SQLite (https://www.sqlite.org/)

  • SQLite๋Š” ๊ฐ€๋ณ๊ณ  ์„ค์ •์ด ํ•„์š” ์—†๋Š” ์„œ๋ฒ„๋ฆฌ์Šค RDBMS๋กœ, ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ์—์„œ ์†์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Python์—์„œ๋Š” sqlite3 ๋ชจ๋“ˆ์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ๋ถ€ํ„ฐ ์ฟผ๋ฆฌ ์‹คํ–‰๊นŒ์ง€ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ „์ฒด ๋ฐ์ดํ„ฐ๊ฐ€ ํ•˜๋‚˜์˜ ํŒŒ์ผ์— ์ €์žฅ๋˜์–ด ๊ด€๋ฆฌ์™€ ๋ฐฐํฌ๊ฐ€ ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค.
  • ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ์™€ SQL ํ‘œ์ค€ ์ง€์›์œผ๋กœ ์•ˆ์ •์ ์ธ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  1. LangChain์—์„œ์˜ SqliteSaver ์ฒดํฌํฌ์ธํ„ฐ: ์–ธ๊ธ‰๋œ SqliteSaver ์ฒดํฌํฌ์ธํ„ฐ๋Š” LangChain์—์„œ ์ œ๊ณตํ•˜๋Š” ์ฒดํฌํฌ์ธํŠธ ์ €์žฅ ๋ฐฉ์‹ ์ค‘ ํ•˜๋‚˜๋กœ, ๋Œ€ํ™”๋‚˜ ์ƒํƒœ๋ฅผ SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋Œ€ํ™”์˜ ์ƒํƒœ๋‚˜ ์ง„ํ–‰ ์ƒํ™ฉ์„ ๋ฉ”๋ชจ๋ฆฌ๋‚˜ ํŒŒ์ผ์— ์ €์žฅํ•˜์—ฌ ๋ณต๊ตฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    • ์ฒดํฌํฌ์ธํ„ฐ ์—ญํ• : ์‹œ์Šคํ…œ์ด ์ค‘๋‹จ๋˜๊ฑฐ๋‚˜ ๋Œ€ํ™”๊ฐ€ ๊ธธ์–ด์กŒ์„ ๋•Œ, ์ƒํƒœ๋ฅผ ๋ณด์กดํ•˜๊ฑฐ๋‚˜ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•์Šต๋‹ˆ๋‹ค. SQLite๋Š” ์ด๋Ÿฌํ•œ ์ฒดํฌํฌ์ธํ„ฐ๋กœ ์ ํ•ฉํ•œ ๊ฒฝ๋Ÿ‰ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์ž…๋‹ˆ๋‹ค. ๋ฉ”๋ชจ๋ฆฌ๋ฅผ SQL Lite์— ์ €์žฅํ•จ์œผ๋กœ์จ ์„ธ์…˜์ด ์ข…๋ฃŒ๋˜๋”๋ผ๋„ ๋Œ€ํ™” ๊ธฐ๋ก์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ์„ธ์…˜์ด ์‹œ์ž‘๋˜๋”๋ผ๋„ ์ด์ „ ๋Œ€ํ™”๋ฅผ ๋ถˆ๋Ÿฌ์™€ ์ด์–ด์„œ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    • ๋ฉ”๋ชจ๋ฆฌ ๋‚ด SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

      1
      2
      3
      4
      
           
      import sqlite3
      # In-memory database
      conn = sqlite3.connect(":memory:", check_same_thread=False)
      
      • :memory:: SQLite๋Š” "memory" ๋ฌธ์ž์—ด์„ ์ œ๊ณตํ•˜๋ฉด, ๋ฉ”๋ชจ๋ฆฌ ๋‚ด์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ํœ˜๋ฐœ์„ฑ ๋ฉ”๋ชจ๋ฆฌ(RAM)์—์„œ ๋™์ž‘ํ•˜๋ฉฐ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ข…๋ฃŒ๋˜๊ฑฐ๋‚˜ ์—ฐ๊ฒฐ์ด ๋Š์–ด์ง€๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ…Œ์ŠคํŠธ, ์ž„์‹œ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ, ๋น ๋ฅธ ์—ฐ์‚ฐ์ด ํ•„์š”ํ•œ ์ƒํ™ฉ์—์„œ ๋งค์šฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
      • check_same_thread=False: SQLite๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ์—์„œ์˜ ๋™์‹œ ์ž‘์—…์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š์ง€๋งŒ, ์ด ์˜ต์…˜์„ ์„ค์ •ํ•˜๋ฉด ๋‹ค์ค‘ ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ๋„ ํ•˜๋‚˜์˜ ์—ฐ๊ฒฐ์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ๋กœ์ปฌ SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ

      1
      2
      3
      4
      
           
      # ๋กœ์ปฌ db์— ์ €์žฅ
      db_path = "state_db/example.db"
      conn = sqlite3.connect(db_path, check_same_thread=False)
      
      • ๋กœ์ปฌ ํŒŒ์ผ ๊ธฐ๋ฐ˜ SQLite: ์œ„ ์˜ˆ์‹œ์—์„œ๋Š” db_path = "state_db/example.db"์™€ ๊ฐ™์ด, ๋กœ์ปฌ ํŒŒ์ผ์— ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. SQLite๋Š” ํŒŒ์ผ ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ, ํ•˜๋‚˜์˜ ํŒŒ์ผ์— ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด ํŒŒ์ผ์€ ๋‹ค๋ฅธ ํ™˜๊ฒฝ์œผ๋กœ ์‰ฝ๊ฒŒ ์ด๋™ํ•˜๊ฑฐ๋‚˜ ๋ฐฑ์—…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ๋ฐ ์ฒดํฌํฌ์ธํ„ฐ ์ƒ์„ฑ:

    1
    2
    3
    
      
    from langgraph.checkpoint.sqlite import SqliteSaver
    memory = SqliteSaver(conn)
    
    • SqliteSaver๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ฒดํฌํฌ์ธํ„ฐ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋Œ€ํ™” ์ƒํƒœ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์˜๊ตฌ์ ์œผ๋กœ ์ €์žฅ๋˜๋ฉฐ, ๋‚˜์ค‘์— ์ƒํƒœ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด์ „ ์ฑ•ํ„ฐ์—์„œ ๋งŒ๋“  Agent ์žฌ-์ƒ์„ฑ

    • (๋ณต์Šต) call_model, summarize_conversation, should_continue ๊ธฐ๋ฐ˜ ๋‚ด์šฉ ์š”์•ฝ ๋ฐ ์งˆ์˜๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” LLM agent => Agent ์„ค๋ช…
    • (๋ชฉ์ ) ์ด์ „์—๋Š” in-memory๋กœ ์ž‘๋™ํ–ˆ์ง€๋งŒ, ์ด๋ฒˆ์—์Šค๋Š” external DB์™€ ์—ฐ๋™์„ ํ•˜๊ณ  ์‹ถ์Œ. ```

    from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage, HumanMessage, RemoveMessage

    from langgraph.graph import END from langgraph.graph import MessagesState

    model = ChatOpenAI(model=โ€gpt-4oโ€,temperature=0)

    class State(MessagesState): summary: str

    ๋ชจ๋ธ์„ ํ˜ธ์ถœํ•˜๋Š” ๋กœ์ง ์ •์˜

    def call_model(state: State):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    # ์š”์•ฝ์ด ์žˆ๋‹ค๋ฉด ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค
    summary = state.get("summary", "")
      
    # ์š”์•ฝ์ด ์žˆ๋‹ค๋ฉด ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค
    if summary:
      
        # ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€์— ์š”์•ฝ ์ถ”๊ฐ€
        system_message = f"์ด์ „ ๋Œ€ํ™”์˜ ์š”์•ฝ: {summary}"
      
        # ์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€์— ์š”์•ฝ ์ถ”๊ฐ€
        messages = [SystemMessage(content=system_message)] + state["messages"]
      
    else:
        messages = state["messages"]
      
    response = model.invoke(messages)
    return {"messages": response}
    

    def summarize_conversation(state: State):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    # ๋จผ์ € ๊ธฐ์กด ์š”์•ฝ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค
    summary = state.get("summary", "")
      
    # ์š”์•ฝ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ 
    if summary:
      
        # ์š”์•ฝ์ด ์ด๋ฏธ ์กด์žฌํ•จ
        summary_message = (
            f"์ง€๊ธˆ๊นŒ์ง€์˜ ๋Œ€ํ™” ์š”์•ฝ์ž…๋‹ˆ๋‹ค: {summary}\n\n"
            "์œ„์˜ ์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ์š”์•ฝ์„ ํ™•์žฅํ•˜์„ธ์š”:"
        )
      
    else:
        summary_message = "์œ„์˜ ๋Œ€ํ™”๋ฅผ ์š”์•ฝํ•˜์„ธ์š”:"
      
    # ํ”„๋กฌํ”„ํŠธ๋ฅผ ํžˆ์Šคํ† ๋ฆฌ์— ์ถ”๊ฐ€
    messages = state["messages"] + [HumanMessage(content=summary_message)]
    response = model.invoke(messages)
      
    # ๊ฐ€์žฅ ์ตœ๊ทผ 2๊ฐœ์˜ ๋ฉ”์‹œ์ง€๋งŒ ๋‚จ๊ธฐ๊ณ  ๋ชจ๋‘ ์‚ญ์ œ
    delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
    return {"summary": response.content, "messages": delete_messages}
    

    ๋Œ€ํ™”๋ฅผ ์ข…๋ฃŒํ• ์ง€ ์š”์•ฝํ• ์ง€ ๊ฒฐ์ •

    def should_continue(state: State):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    """๋‹ค์Œ์— ์‹คํ–‰ํ•  ๋…ธ๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
      
    messages = state["messages"]
      
    # ๋ฉ”์‹œ์ง€๊ฐ€ 6๊ฐœ ์ด์ƒ์ด๋ฉด ๋Œ€ํ™”๋ฅผ ์š”์•ฝํ•ฉ๋‹ˆ๋‹ค
    if len(messages) > 6:
        return "summarize_conversation"
      
    # ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค
    return END ```
    

    • ์ฐจ์ด์  :
      • In-Memory: from langgraph.checkpoint.memory import MemorySaver
      • External-DB: from langgraph.checkpoint.sqlite import SqliteSaver ๊ทธ ์™ธ์—๋Š” ๊ณต์‹ ์ง€์›๋˜๋Š” ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฒดํฌํฌ์ธํ„ฐ๋กœ from langgraph.checkpoint.postgres import PostgresSaver์ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋Œ€ํ™” ์ƒํƒœ ๋กœ๋“œ/๋ณต๊ตฌ: SQLite๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋Œ€ํ™” ์ƒํƒœ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋˜๋ฏ€๋กœ, ๋Œ€ํ™”๊ฐ€ ๊ธธ์–ด์ง€๊ฑฐ๋‚˜ ์„ธ์…˜์ด ์ข…๋ฃŒ๋˜๋”๋ผ๋„ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณต๊ตฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    • ์˜ˆ๋ฅผ ๋“ค์–ด, ๋…ธํŠธ๋ถ ์ปค๋„์„ ์žฌ์‹œ์ž‘ํ•˜๋”๋ผ๋„ ๋™์ผํ•œ SQLite DB๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ```

    config = {โ€œconfigurableโ€: {โ€œthread_idโ€: โ€œ1โ€}} graph_state = graph.get_state(config) graph_state

    1
    2
    3
    4
    
    + `config`: ๋Œ€ํ™”์˜ ์Šค๋ ˆ๋“œ ID๋ฅผ ์ง€์ •ํ•˜๋Š” ์„ค์ •์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” "thread\_id": "1"์ด๋ผ๋Š” ์Šค๋ ˆ๋“œ์— ํ•ด๋‹นํ•˜๋Š” ๋Œ€ํ™” ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์„ค์ •์„ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
    + `graph.get_state(config)`: ์ด ๋ฉ”์„œ๋“œ๋Š” ์ฃผ์–ด์ง„ config์— ๋งž๋Š” ์Šค๋ ˆ๋“œ์˜ ๋Œ€ํ™” ์ƒํƒœ๋ฅผ SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.  
        
      ![](https://velog.velcdn.com/images/euisuk-chung/post/40ba3039-f77d-451e-9f18-30c731e4327b/image.png)
    

    ```

  1. ์œ ์šฉํ•œ CODE-SNIPPETS: SQLite DB๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ๋‚˜์„œ, ์•„๋ž˜์™€ ๊ฐ™์€ ํ•จ์ˆ˜๋“ค์„ ํ†ตํ•ด ์ •๋ณด๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
1
2
3
4
5
6
7
8
# StateSnapshot์—์„œ ๋ฉ”์‹œ์ง€ ๋ถ€๋ถ„์„ ์ถ”์ถœํ•˜์—ฌ ๋ณด๊ธฐ ์ข‹๊ฒŒ ์ถœ๋ ฅ
for message in graph_state.values['messages']:
    print(f"Message ID: {message.id}")
    print(f"Message Type: {'Human' if isinstance(message, HumanMessage) else 'AI'}")
    print(f"Content: {message.content}")
    print('-' * 40)

  • ์˜ˆ์‹œ ์ถœ๋ ฅ:

1
2
3
4
# ์š”์•ฝ ์ถœ๋ ฅ
summary = graph_state.values.get('summary', 'No summary available')
print(f"Conversation Summary: {summary}")
  • ์˜ˆ์‹œ ์ถœ๋ ฅ:



-->