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

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

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

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

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

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

  • Lesson 1: Parallelization
  • Lesson 2: Sub-graphs
  • Lesson 3: Map-reduce
  • Lesson 4: Research Assistant

Lesson 1: Parallelization

๊ฐœ์š”

  • ๋ณ‘๋ ฌํ™”(Parallelization)๋Š” ์ž‘์—…์„ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋…ธ๋“œ๋กœ ๋‚˜๋ˆ„์–ด ๋™์‹œ์— ์ฒ˜๋ฆฌํ•˜์—ฌ ์„ฑ๋Šฅ์„ ๋†’์ด๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
    • ๊ทธ๋Ÿฌ๋‚˜ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋œ ๋…ธ๋“œ๋“ค์ด ๋™์ผํ•œ ์ƒํƒœ ํ‚ค๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๋•Œ ์ƒํƒœ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ฆฌ๋“€์„œ(Reducer)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณ‘ํ•ฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์š” ๊ฐœ๋…

  1. ๋ณ‘๋ ฌํ™” ๊ฐœ๋…
  • ํŒฌ์•„์›ƒ(fan-out): ํ•œ ๋…ธ๋“œ์—์„œ ์—ฌ๋Ÿฌ ๋…ธ๋“œ๋กœ ์ž‘์—…์ด ํ™•์žฅ๋˜๋Š” ๊ณผ์ •์ž…๋‹ˆ๋‹ค.
    • ์˜ˆ๋ฅผ ๋“ค์–ด, ๋…ธ๋“œ A์—์„œ B์™€ C๋ฅผ ๋™์‹œ์— ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์„ ํŒฌ์•„์›ƒ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํŒฌ์ธ(fan-in): ์—ฌ๋Ÿฌ ๋ณ‘๋ ฌ ์ž‘์—…์ด ๋ชจ๋‘ ์™„๋ฃŒ๋œ ํ›„ ํ•˜๋‚˜์˜ ๋…ธ๋“œ๋กœ ๋‹ค์‹œ ํ•ฉ์ณ์ง€๋Š” ๊ณผ์ •์ž…๋‹ˆ๋‹ค.
    • ์˜ˆ๋ฅผ ๋“ค์–ด, B์™€ C์˜ ์ž‘์—…์ด ์™„๋ฃŒ๋˜๋ฉด D ๋…ธ๋“œ๋กœ ํ•ฉ์ณ์ง€๋Š” ๊ฒƒ์ด ํŒฌ์ธ์ž…๋‹ˆ๋‹ค.
  1. ๋ณ‘๋ ฌ ๋…ธ๋“œ ์‹คํ–‰:

    • ์ž‘์—…์„ ์—ฌ๋Ÿฌ ๋…ธ๋“œ๋กœ ๋ถ„๋ฐฐํ•˜์—ฌ ๋™์‹œ์— ์‹คํ–‰ํ•˜๋Š” ์ž‘์—…์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.
    • ์—ฌ๋Ÿฌ ๋…ธ๋“œ๊ฐ€ ๋™์‹œ์— ์‹คํ–‰๋  ๋•Œ ์ƒํƒœ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•œ ์กฐ์น˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
  2. ๋ฆฌ๋“€์„œ(Reducer):

    • ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋œ ๋…ธ๋“œ๊ฐ€ ๋™์ผํ•œ ์ƒํƒœ ํ‚ค๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๋•Œ ์ด๋ฅผ ๋ณ‘ํ•ฉํ•˜๋Š” ์—ญํ• ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.
    • ๋ณ‘๋ ฌ ๋…ธ๋“œ๊ฐ€ ์ƒํƒœ ํ‚ค์— ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ‘ํ•ฉํ•˜๋Š” ๋“ฑ์˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ ์˜ˆ์‹œ

  1. ๊ธฐ๋ณธ์ ์ธ ๋ณ‘๋ ฌํ™” ์ฒ˜๋ฆฌ ์˜ˆ์‹œ:
    • ๊ธฐ๋ณธ์ ์œผ๋กœ ์ง๋ ฌ ์‹คํ–‰๋˜๋Š” ๊ทธ๋ž˜ํ”„์—์„œ A, B, C, D ์ˆœ์„œ๋Œ€๋กœ ๋…ธ๋“œ๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
    • ์•„๋ž˜ ๊ทธ๋ž˜ํ”„๋Š” ๊ฐ ๋‹จ๊ณ„์—์„œ ์ƒํƒœ๋ฅผ ๋ฎ์–ด์“ฐ๋Š” ๊ฐ„๋‹จํ•œ ์„ ํ˜• ๊ทธ๋ž˜ํ”„์ž…๋‹ˆ๋‹ค.
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 IPython.display import Image, display

from typing import Any
from typing_extensions import TypedDict

from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    # operator.add ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜๋Š” ์ด๊ฒƒ์„ ์ถ”๊ฐ€ ์ „์šฉ์œผ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค
    state: str

class ReturnNodeValue:
    def __init__(self, node_secret: str):
        self._value = node_secret

    def __call__(self, state: State) -> Any:
        print(f"{self._value}๋ฅผ {state['state']}์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค")
        return {"state": [self._value]}

# ๋…ธ๋“œ ์ถ”๊ฐ€
builder = StateGraph(State)

# ๊ฐ ๋…ธ๋“œ๋ฅผ node_secret์œผ๋กœ ์ดˆ๊ธฐํ™” 
builder.add_node("a", ReturnNodeValue("๋‚˜๋Š” A์ž…๋‹ˆ๋‹ค"))
builder.add_node("b", ReturnNodeValue("๋‚˜๋Š” B์ž…๋‹ˆ๋‹ค"))
builder.add_node("c", ReturnNodeValue("๋‚˜๋Š” C์ž…๋‹ˆ๋‹ค"))
builder.add_node("d", ReturnNodeValue("๋‚˜๋Š” D์ž…๋‹ˆ๋‹ค"))

# ํ๋ฆ„
builder.add_edge(START, "a")
builder.add_edge("a", "b")
builder.add_edge("b", "c")
builder.add_edge("c", "d")
builder.add_edge("d", END)
graph = builder.compile()

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

  1. ๋ณ‘๋ ฌํ™” ์ฒ˜๋ฆฌ:
    • ์ด์ œ A์—์„œ B์™€ C๊ฐ€ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋˜๋„๋ก ํŒฌ์•„์›ƒ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
builder = StateGraph(State)

# ๊ฐ ๋…ธ๋“œ๋ฅผ node_secret์œผ๋กœ ์ดˆ๊ธฐํ™” 
builder.add_node("a", ReturnNodeValue("๋‚˜๋Š” A์ž…๋‹ˆ๋‹ค"))
builder.add_node("b", ReturnNodeValue("๋‚˜๋Š” B์ž…๋‹ˆ๋‹ค"))
builder.add_node("c", ReturnNodeValue("๋‚˜๋Š” C์ž…๋‹ˆ๋‹ค"))
builder.add_node("d", ReturnNodeValue("๋‚˜๋Š” D์ž…๋‹ˆ๋‹ค"))

# ํ๋ฆ„
builder.add_edge(START, "a")
builder.add_edge("a", "b")
builder.add_edge("a", "c")
builder.add_edge("b", "d")
builder.add_edge("c", "d")
builder.add_edge("d", END)
graph = builder.compile()

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

  1. ์—๋Ÿฌ ๋ฐœ์ƒ
  • ๊ตฌ์กฐ๋Š” ์œ„์™€ ๊ฐ™์ด ์‰ฝ๊ฒŒ ์ •์˜ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๋ง‰์ƒ ์‹คํ–‰ํ•ด๋ณด๋ฉด, B์™€ C๊ฐ€ ๋™์‹œ์— ๋™์ผํ•œ ์ƒํƒœ ํ‚ค๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋ ค ํ•˜๋ฏ€๋กœ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜์—ฌ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  1. ๋ฆฌ๋“€์„œ๋ฅผ ํ†ตํ•œ ์ถฉ๋Œ ํ•ด๊ฒฐ:
    • ๋ณ‘๋ ฌ ๋…ธ๋“œ B์™€ C๊ฐ€ ๋™์‹œ์— ๋ฆฌ์ŠคํŠธ์— ๊ฐ’์„ ์ถ”๊ฐ€ํ•˜๋„๋ก ๋ฆฌ๋“€์„œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํƒœ ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
    • ํŒฌ ์•„์›ƒ์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๋‹จ๊ณ„๊ฐ€ ๋™์ผํ•œ ์ฑ„๋„/ํ‚ค์— ์“ฐ๋Š” ๊ฒฝ์šฐ ๋ฆฌ๋“€์„œ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    • ๋ชจ๋“ˆ 2์—์„œ ์–ธ๊ธ‰ํ–ˆ๋“ฏ์ด operator.add๋Š” Python์˜ ๋‚ด์žฅ operator ๋ชจ๋“ˆ์˜ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
    • operator.add๊ฐ€ ๋ฆฌ์ŠคํŠธ์— ์ ์šฉ๋˜๋ฉด ๋ฆฌ์ŠคํŠธ ์—ฐ๊ฒฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
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
import operator
from typing import Annotated

class State(TypedDict):
    # operator.add ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜๋Š” ์ด๊ฒƒ์„ ์ถ”๊ฐ€ ์ „์šฉ์œผ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค
    state: Annotated[list, operator.add]

# ๋…ธ๋“œ ์ถ”๊ฐ€
builder = StateGraph(State)

# ๊ฐ ๋…ธ๋“œ๋ฅผ node_secret์œผ๋กœ ์ดˆ๊ธฐํ™” 
builder.add_node("a", ReturnNodeValue("๋‚˜๋Š” A์ž…๋‹ˆ๋‹ค"))
builder.add_node("b", ReturnNodeValue("๋‚˜๋Š” B์ž…๋‹ˆ๋‹ค"))
builder.add_node("c", ReturnNodeValue("๋‚˜๋Š” C์ž…๋‹ˆ๋‹ค"))
builder.add_node("d", ReturnNodeValue("๋‚˜๋Š” D์ž…๋‹ˆ๋‹ค"))

# ํ๋ฆ„
builder.add_edge(START, "a")
builder.add_edge("a", "b")
builder.add_edge("a", "c")
builder.add_edge("b", "d")
builder.add_edge("c", "d")
builder.add_edge("d", END)
graph = builder.compile()

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

1
graph.invoke({"state": []})
  • ์œ„์™€ ๊ฐ™์ด ๊ทธ๋ž˜ํ”„๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ฉด ์˜๋„ํ•œ ๊ฒƒ์ฒ˜๋Ÿผ ๋๊นŒ์ง€ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
1
2
3
4
5
๋‚˜๋Š” A์ž…๋‹ˆ๋‹ค๋ฅผ []์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค
๋‚˜๋Š” B์ž…๋‹ˆ๋‹ค๋ฅผ ['๋‚˜๋Š” A์ž…๋‹ˆ๋‹ค']์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค
๋‚˜๋Š” C์ž…๋‹ˆ๋‹ค๋ฅผ ['๋‚˜๋Š” A์ž…๋‹ˆ๋‹ค']์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค
๋‚˜๋Š” D์ž…๋‹ˆ๋‹ค๋ฅผ ['๋‚˜๋Š” A์ž…๋‹ˆ๋‹ค', '๋‚˜๋Š” B์ž…๋‹ˆ๋‹ค', '๋‚˜๋Š” C์ž…๋‹ˆ๋‹ค']์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค
{'state': ['๋‚˜๋Š” A์ž…๋‹ˆ๋‹ค', '๋‚˜๋Š” B์ž…๋‹ˆ๋‹ค', '๋‚˜๋Š” C์ž…๋‹ˆ๋‹ค', '๋‚˜๋Š” D์ž…๋‹ˆ๋‹ค']}
  • ์ด์ œ b์™€ c๊ฐ€ ๋ณ‘๋ ฌ๋กœ ์ˆ˜ํ–‰ํ•œ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ƒํƒœ์— ์ถ”๊ฐ€๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  1. ์ถ”๊ฐ€ ์‹ค์Šต
  • ์ข€ ๋” ๋ณต์žกํ•œ ๋ณ‘๋ ฌ ๊ทธ๋ž˜ํ”„์—์„œ๋„ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด ๊ฒฝ์šฐ b, b2, c๋Š” ๋ชจ๋‘ ๋™์ผํ•œ ๋‹จ๊ณ„์˜ ์ผ๋ถ€์ž…๋‹ˆ๋‹ค.
  • ๊ทธ๋ž˜ํ”„๋Š” d ๋‹จ๊ณ„๋กœ ์ง„ํ–‰ํ•˜๊ธฐ ์ „์— ์ด ๋ชจ๋“  ๊ฒƒ๋“ค์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class State(TypedDict):
    # operator.add ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜๋Š” ์ด๊ฒƒ์„ ์ถ”๊ฐ€ ์ „์šฉ์œผ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค
    state: Annotated[list, operator.add]

# Add nodes
builder = StateGraph(State)

# Initialize each node with node_secret 
builder.add_node("a", ReturnNodeValue("I'm A"))
builder.add_node("b", ReturnNodeValue("I'm B"))
builder.add_node("b2", ReturnNodeValue("I'm B2"))
builder.add_node("c", ReturnNodeValue("I'm C"))
builder.add_node("d", ReturnNodeValue("I'm D"))

# Flow
builder.add_edge(START, "a")
builder.add_edge("a", "b")
builder.add_edge("a", "c")
builder.add_edge("b", "b2")
builder.add_edge(["b2", "c"], "d")
builder.add_edge("d", END)
graph = builder.compile()

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

  • b์™€ c๋Š” ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰์ด ๋˜๊ธฐ๋•Œ๋ฌธ์— ๋จผ์ € ์‹คํ–‰์ด ๋˜๊ณ , .add operator๋กœ ์ธํ•ด์„œ append๋œ ์ˆœ์„œ๋Œ€๋กœ ์ถœ๋ ฅ์ด ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
1
{'state': ["I'm A", "I'm B", "I'm C", "I'm B2", "I'm D"]}
  1. ๊ทธ๋ž˜ํ”„ ๋ณ‘๋ ฌํ™” ์ˆœ์„œ ์ œ์–ด
  • ๊ธฐ๋ณธ์ ์œผ๋กœ LangGraph๋Š” ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋˜๋Š” ๋…ธ๋“œ๋“ค์˜ ์—…๋ฐ์ดํŠธ ์ˆœ์„œ๋ฅผ ์ž๋™์œผ๋กœ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
  • ์œ„์— 4๋ฒˆ ์˜ˆ์ œ์—์„œ๋„ ์ด๋Š” ์šฐ๋ฆฌ๊ฐ€ ์ œ์–ดํ•˜์ง€ ์•Š๋Š” ๊ทธ๋ž˜ํ”„ ํ† ํด๋กœ์ง€์— ๊ธฐ๋ฐ˜ํ•˜์—ฌ LangGraph๊ฐ€ ๊ฒฐ์ •๋ก ์  ์ˆœ์„œ๋ฅผ ์ •ํ•ด์ค€ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    • ์œ„์—์„œ ์šฐ๋ฆฌ๋Š” c๊ฐ€ b2 ์ „์— ์ถ”๊ฐ€๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ํ•˜์ง€๋งŒ, ๋งŒ์•ฝ ์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ์ˆœ์„œ๋กœ ๊ฒฐ๊ณผ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ์ปค์Šคํ…€ ๋ฆฌ๋“€์„œ(Custom 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
def sorting_reducer(left, right):
    """ ๋ฆฌ์ŠคํŠธ์˜ ๊ฐ’๋“ค์„ ๊ฒฐํ•ฉํ•˜๊ณ  ์ •๋ ฌํ•ฉ๋‹ˆ๋‹ค"""
    if not isinstance(left, list):
        left = [left]

    if not isinstance(right, list):
        right = [right]
    
    return sorted(left + right, reverse=False)

class State(TypedDict):
    # sorting_reducer๋Š” ์ƒํƒœ์˜ ๊ฐ’๋“ค์„ ์ •๋ ฌํ•ฉ๋‹ˆ๋‹ค
    state: Annotated[list, sorting_reducer]

# ๋…ธ๋“œ ์ถ”๊ฐ€
builder = StateGraph(State)

# ๊ฐ ๋…ธ๋“œ๋ฅผ node_secret์œผ๋กœ ์ดˆ๊ธฐํ™” 
builder.add_node("a", ReturnNodeValue("๋‚˜๋Š” A์ž…๋‹ˆ๋‹ค"))
builder.add_node("b", ReturnNodeValue("๋‚˜๋Š” B์ž…๋‹ˆ๋‹ค"))
builder.add_node("b2", ReturnNodeValue("๋‚˜๋Š” B2์ž…๋‹ˆ๋‹ค"))
builder.add_node("c", ReturnNodeValue("๋‚˜๋Š” C์ž…๋‹ˆ๋‹ค"))
builder.add_node("d", ReturnNodeValue("๋‚˜๋Š” D์ž…๋‹ˆ๋‹ค"))

# ํ๋ฆ„
builder.add_edge(START, "a")
builder.add_edge("a", "b")
builder.add_edge("a", "c")
builder.add_edge("b", "b2")
builder.add_edge(["b2", "c"], "d")
builder.add_edge("d", END)
graph = builder.compile()

display(Image(graph.get_graph().draw_mermaid_png()))
  • ์ด์ œ ๋ฆฌ๋“€์„œ๊ฐ€ ์—…๋ฐ์ดํŠธ๋œ ์ƒํƒœ ๊ฐ’์„ ์ •๋ ฌํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
1
{'state': ["I'm A", "I'm B", "I'm B2", "I'm C", "I'm D"]}

  1. (์‘์šฉ) ๊ทธ๋ž˜ํ”„ ๋ณ‘๋ ฌํ™” ์ˆœ์„œ ์ œ์–ด
  • ์ข€ ๋” ์–ด๋ ค์šด(?) ๋ชจ์–‘์œผ๋กœ ๊ทธ๋ž˜ํ”„๋ฅผ ๋งŒ๋“ค๊ณ  ํ•œ๋ฒˆ ๋” ์‹ค์Šต์„ ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

1) ๊ธฐ์กด operator.add ๋ฆฌ๋“€์„œ ์‚ฌ์šฉํ•œ ๊ฒฝ์šฐ:

1
2
3
class State(TypedDict):
    # operator.add ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜๋Š” ์ด๊ฒƒ์„ ์ถ”๊ฐ€ ์ „์šฉ์œผ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค
    state: Annotated[list, operator.add]
  • ์ด ๋ฐฉ์‹์—์„œ๋Š” ๋…ธ๋“œ๊ฐ€ ์ˆœ์ฐจ์ ์œผ๋กœ ๊ฐ’์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.
  • ๊ฐ ๋…ธ๋“œ์—์„œ ์ƒ์„ฑ๋œ ๊ฐ’์€ ์ˆœ์„œ๋Œ€๋กœ ์ƒํƒœ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค.
  • "b1"๊ณผ "c1"์ด ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋˜๊ณ , ๊ทธ ํ›„์— "b2"์™€ "c2"๊ฐ€ ์‹คํ–‰๋˜์–ด ๊ทธ ๊ฒฐ๊ณผ๋“ค์ด ํ•ฉ์ณ์ง„ ๋‹ค์Œ "d"๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด๋•Œ๋Š” ์•ฝ๊ฐ„์˜ DFS์ฒ˜๋Ÿผ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ํ•œ ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋ผ ๋๊นŒ์ง€ ์‹คํ–‰ํ•œ ๋‹ค์Œ, ๋‹ค๋ฅธ ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋ผ๊ฐ‘๋‹ˆ๋‹ค.
  • ๋”ฐ๋ผ์„œ ์ถœ๋ ฅ์€ "I'm A", "I'm B1", "I'm C1", "I'm B2", "I'm C2", "I'm D"์™€ ๊ฐ™์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.
  • ์•„๋ž˜ ํ๋ฆ„์„ ์‚ดํŽด๋ณด๋ฉด:

    1. a ์‹คํ–‰ ํ›„ b1๊ณผ c1 ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰.
    2. b1๊ฐ€ ์‹คํ–‰๋˜๊ณ  ๋‚˜์„œ b2๋กœ ์ด๋™.
    3. c1์ด ์‹คํ–‰๋˜๊ณ  ๋‚˜์„œ c2๋กœ ์ด๋™.
    4. b2์™€ c2๊ฐ€ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋˜๊ณ  ๊ทธ ํ›„ d๋กœ ์ด๋™.
  • ๊ฒฐ๊ณผ:

    1
    
    {'state': ["I'm A", "I'm B1", "I'm C1", "I'm B2", "I'm C2", "I'm D"]}
    
  • ์ด๊ฒƒ์€ ๋งˆ์น˜ DFS์—์„œ ํ•œ ๊ฒฝ๋กœ๋ฅผ ์ญ‰ ๋”ฐ๋ผ๊ฐ€๋ฉด์„œ ์‹คํ–‰ํ•˜๊ณ , ๊ทธ ํ›„ ๋‹ค๋ฅธ ๊ฒฝ๋กœ๋กœ ์ง„ํ–‰ํ•˜๋Š” ํ๋ฆ„๊ณผ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

2) sorting_reducer๋ฅผ ์‚ฌ์šฉํ•œ ๊ฒฝ์šฐ:

1
2
3
4
5
6
7
8
9
10
11
12
13
def sorting_reducer(left, right):
    """Combines and sorts the values in a list"""
    if not isinstance(left, list):
        left = [left]
    
    if not isinstance(right, list):
        right = [right]
    
    return sorted(left + right, reverse=False)

class State(TypedDict):
    # sorting_reducer will sort the values in state
    state: Annotated[list, sorting_reducer]
  • sorting_reducer๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋œ ๋…ธ๋“œ๋“ค์˜ ๊ฐ’์ด ์ •๋ ฌ๋˜์–ด ์ƒํƒœ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค.
  • ์ด ๋ฐฉ์‹์€ BFS์ฒ˜๋Ÿผ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ๋™์ผํ•œ ๋ ˆ๋ฒจ์— ์žˆ๋Š” ๋…ธ๋“œ๋“ค์ด ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋˜๊ณ , ๊ทธ ํ›„์— ๋‹ค์Œ ๋ ˆ๋ฒจ๋กœ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค.
  • "b1"๊ณผ "c1"์ด ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋œ ํ›„, "b2"์™€ "c2"๊ฐ€ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
  • ์ƒํƒœ์— ์ถ”๊ฐ€๋  ๋•Œ๋Š” ์•ŒํŒŒ๋ฒณ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— "I'm B1", "I'm B2", "I'm C1", "I'm C2"์˜ ์ˆœ์„œ๋กœ ์ •๋ ฌ๋ฉ๋‹ˆ๋‹ค.
  • ์•„๋ž˜ ํ๋ฆ„์„ ์‚ดํŽด๋ณด๋ฉด:

    1. a ์‹คํ–‰ ํ›„ b1๊ณผ c1์ด ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰.
    2. b1๊ณผ c1์˜ ๊ฒฐ๊ณผ๊ฐ€ ๋ณ‘ํ•ฉ๋˜๊ณ , b2์™€ c2๊ฐ€ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰.
    3. b2์™€ c2์˜ ๊ฒฐ๊ณผ๊ฐ€ ๋ณ‘ํ•ฉ๋˜๊ณ  ์ •๋ ฌ๋จ.
    4. ๊ทธ ํ›„ d๊ฐ€ ์‹คํ–‰๋จ.
  • ๊ฒฐ๊ณผ:

    1
    
    {'state': ["I'm A", "I'm B1", "I'm B2", "I'm C1", "I'm C2", "I'm D"]}
    
  • ์ด๋Š” ๋งˆ์น˜ BFS์—์„œ ํ•œ ๋ ˆ๋ฒจ์˜ ๋…ธ๋“œ๋ฅผ ๋ชจ๋‘ ์‹คํ–‰ํ•œ ํ›„ ๋‹ค์Œ ๋ ˆ๋ฒจ๋กœ ๋„˜์–ด๊ฐ€๋Š” ๋ฐฉ์‹๊ณผ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋œ ๋…ธ๋“œ๋“ค์ด ํ•ญ์ƒ ์ •๋ ฌ๋œ ํ˜•ํƒœ๋กœ ์ƒํƒœ์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

  1. (์‹ฌํ™”) ์ •๋ณด ๊ฒ€์ƒ‰ ๊ทธ๋ž˜ํ”„ ๊ตฌํ˜„
  • ์—ฌ๊ธฐ์„œ ๋‘ ๊ฐœ์˜ ๊ฒ€์ƒ‰ ์ž‘์—…์„ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•˜๋Š” ๊ทธ๋ž˜ํ”„๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค:

    • ์›น ๊ฒ€์ƒ‰(Web Search): ์งˆ๋ฌธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์›น์—์„œ ๊ด€๋ จ ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
    • ์œ„ํ‚คํ”ผ๋””์•„ ๊ฒ€์ƒ‰(Wikipedia Search): ์งˆ๋ฌธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์œ„ํ‚คํ”ผ๋””์•„์—์„œ ์ •๋ณด๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
  • ๋‘ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋Š” ๋ชจ๋‘ context๋ผ๋Š” ์ƒํƒœ ํ‚ค์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
  • ์•„๋ž˜ ์ฝ”๋“œ๋Š” ์›น ๊ฒ€์ƒ‰๊ณผ ์œ„ํ‚คํ”ผ๋””์•„ ๊ฒ€์ƒ‰์„ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•œ ํ›„, ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ํ†ตํ•ฉํ•˜์—ฌ LLM์„ ํ†ตํ•ด ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•˜๋Š” ๊ณผ์ •์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

0) Import Libraries

1
2
3
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_community.document_loaders import WikipediaLoader
from langchain_community.tools.tavily_search import TavilySearchResults

1) search_web ํ•จ์ˆ˜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def search_web(state):
    
    """ ์›น ๊ฒ€์ƒ‰์—์„œ ๋ฌธ์„œ ๊ฒ€์ƒ‰ """

    # ๊ฒ€์ƒ‰
    tavily_search = TavilySearchResults(max_results=3)
    search_docs = tavily_search.invoke(state['question'])

     # ํ˜•์‹ ์ง€์ •
    formatted_search_docs = "\n\n---\n\n".join(
        [
            f'<Document href="{doc["url"]}"/>\n{doc["content"]}\n</Document>'
            for doc in search_docs
        ]
    )

    return {"context": [formatted_search_docs]} 
  • ์—ญํ• : search_web ํ•จ์ˆ˜๋Š” ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์„ ๋ฐ›์•„์„œ ์›น์—์„œ ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.

    • ์—ฌ๊ธฐ์„œ๋Š” TavilySearchResults๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ตœ๋Œ€ 3๊ฐœ์˜ ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ํฌ๋งทํŒ…ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • tavily_search.invoke: ์งˆ๋ฌธ์„ ์ž…๋ ฅ๋ฐ›์•„ ์›น ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒฐ๊ณผ ๋ฌธ์„œ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ฆฌ์ŠคํŠธ ์ปดํ”„๋ฆฌํ—จ์…˜: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋Š” HTML ํ˜•์‹์˜ ํƒœ๊ทธ๋กœ ์ง€์ •๋˜๋ฉฐ, ๊ฐ ๋ฌธ์„œ๋Š” <Document> ํƒœ๊ทธ๋กœ ๊ฐ์‹ธ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.

    • search_docs ๋ฆฌ์ŠคํŠธ์˜ ๊ฐ ์š”์†Œ doc์— ๋Œ€ํ•ด ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค.
    • ๊ฐ doc๋Š” ๋”•์…”๋„ˆ๋ฆฌ๋กœ, url๊ณผ content ํ‚ค๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
    • ๊ฐ doc๋ฅผ <Document> ํƒœ๊ทธ๋กœ ๊ฐ์‹ธ๊ณ , href ์†์„ฑ์— url ๊ฐ’์„, ํƒœ๊ทธ ์•ˆ์— content ๊ฐ’์„ ๋„ฃ์Šต๋‹ˆ๋‹ค.
    • ์ด ๊ณผ์ •์„ ํ†ตํ•ด ๊ฐ doc๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ฌธ์ž์—ด ๊ฒฐํ•ฉ: ๋ฆฌ์ŠคํŠธ ์ปดํ”„๋ฆฌํ—จ์…˜์„ ํ†ตํ•ด ์ƒ์„ฑ๋œ ๋ฌธ์ž์—ด ๋ฆฌ์ŠคํŠธ๋ฅผ "\n\n---\n\n" ๋ฌธ์ž์—ด๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค.

    • ์ด ๊ฒฐํ•ฉ ๋ฌธ์ž์—ด์€ ๊ฐ ๋ฌธ์„œ ์‚ฌ์ด์— ๊ตฌ๋ถ„์„ ์„ ์ถ”๊ฐ€ํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ถœ๋ ฅ: ํฌ๋งทํŒ…๋œ ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ(formatted_search_docs)๋Š” context์— ์ €์žฅ๋˜์–ด ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.

2) search_wikipedia ํ•จ์ˆ˜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def search_wikipedia(state):
    
    """ ์œ„ํ‚คํ”ผ๋””์•„์—์„œ ๋ฌธ์„œ ๊ฒ€์ƒ‰ """

    # ๊ฒ€์ƒ‰
    search_docs = WikipediaLoader(query=state['question'], 
                                  load_max_docs=2).load()

     # ํ˜•์‹ ์ง€์ •
    formatted_search_docs = "\n\n---\n\n".join(
        [
            f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
            for doc in search_docs
        ]
    )

    return {"context": [formatted_search_docs]} 
  • ์—ญํ• : search_wikipedia ํ•จ์ˆ˜๋Š” ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์„ ๋ฐ›์•„์„œ ์œ„ํ‚คํ”ผ๋””์•„์—์„œ ๊ด€๋ จ ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
  • WikipediaLoader: ์ฃผ์–ด์ง„ ์งˆ๋ฌธ์„ ์‚ฌ์šฉํ•˜์—ฌ ์œ„ํ‚คํ”ผ๋””์•„์—์„œ ์ตœ๋Œ€ 2๊ฐœ์˜ ๋ฌธ์„œ๋ฅผ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.
  • ํ˜•์‹ ์ง€์ •: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋Š” <Document> ํƒœ๊ทธ๋กœ ํฌ๋งทํŒ…๋˜์–ด ๋ฌธ์„œ์˜ ์ถœ์ฒ˜์™€ ํŽ˜์ด์ง€ ์ •๋ณด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

    • formatted_search_docs ๋ณ€์ˆ˜๋Š” ๋ฆฌ์ŠคํŠธ ์ปดํ”„๋ฆฌํ—จ์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ ๋ฌธ์„œ๋ฅผ ํŠน์ • XML ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•œ ๋ฌธ์ž์—ด๋“ค์„ ๊ฒฐํ•ฉํ•œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.
    • ๊ฐ ๋ฌธ์„œ๋Š” <Document> ํƒœ๊ทธ๋กœ ๊ฐ์‹ธ์ ธ ์žˆ์œผ๋ฉฐ, ๋ฌธ์„œ์˜ ์ถœ์ฒ˜์™€ ํŽ˜์ด์ง€ ์ •๋ณด๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ์ด ์ •๋ณด๋Š” doc.metadata ๋”•์…”๋„ˆ๋ฆฌ์—์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
    • ๋ฌธ์„œ์˜ ์‹ค์ œ ๋‚ด์šฉ์€ doc.page_content์—์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  • ์ถœ๋ ฅ: ํฌ๋งทํŒ…๋œ ์œ„ํ‚คํ”ผ๋””์•„ ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ๊ฐ€ context์— ์ €์žฅ๋˜์–ด ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.

3) generate_answer ํ•จ์ˆ˜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def generate_answer(state):
    
    """ ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜๋Š” ๋…ธ๋“œ """

    # ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ธฐ
    context = state["context"]
    question = state["question"]

    # ํ…œํ”Œ๋ฆฟ
    answer_template = """์ด ๋งฅ๋ฝ์„ ์‚ฌ์šฉํ•˜์—ฌ {question} ์งˆ๋ฌธ์— ๋‹ตํ•˜์„ธ์š”: {context}"""
    answer_instructions = answer_template.format(question=question, 
                                                       context=context)    
    
    # ๋‹ต๋ณ€
    answer = llm.invoke([SystemMessage(content=answer_instructions)]+[HumanMessage(content=f"์งˆ๋ฌธ์— ๋‹ตํ•˜์„ธ์š”.")])
      
    # ์ƒํƒœ์— ์ถ”๊ฐ€
    return {"answer": answer}
  • ์—ญํ• : generate_answer ํ•จ์ˆ˜๋Š” ์›น ๊ฒ€์ƒ‰ ๋ฐ ์œ„ํ‚คํ”ผ๋””์•„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์— ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ํ…œํ”Œ๋ฆฟ: ๊ฒ€์ƒ‰๋œ ์ปจํ…์ŠคํŠธ์™€ ์งˆ๋ฌธ์„ ๊ฒฐํ•ฉํ•˜์—ฌ LLM(Large Language Model)์— ์ „๋‹ฌํ•  ํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • LLM ์‚ฌ์šฉ: ์ƒ์„ฑ๋œ ํ…œํ”Œ๋ฆฟ์„ SystemMessage์™€ HumanMessage๋กœ LLM์— ์ „๋‹ฌํ•˜์—ฌ ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ์ถœ๋ ฅ: ์ƒ์„ฑ๋œ ๋‹ต๋ณ€์€ ์ƒํƒœ์— ์ถ”๊ฐ€๋˜์–ด ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.

4) ๊ทธ๋ž˜ํ”„ ๋…ธ๋“œ ์ถ”๊ฐ€ ๋ฐ ํ๋ฆ„ ์„ค์ •

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ๋…ธ๋“œ ์ถ”๊ฐ€
builder = StateGraph(State)

# ๊ฐ ๋…ธ๋“œ๋ฅผ node_secret์œผ๋กœ ์ดˆ๊ธฐํ™” 
builder.add_node("search_web",search_web)
builder.add_node("search_wikipedia", search_wikipedia)
builder.add_node("generate_answer", generate_answer)

# ํ๋ฆ„
builder.add_edge(START, "search_wikipedia")
builder.add_edge(START, "search_web")
builder.add_edge("search_wikipedia", "generate_answer")
builder.add_edge("search_web", "generate_answer")
builder.add_edge("generate_answer", END)
graph = builder.compile()

display(Image(graph.get_graph().draw_mermaid_png()))
  • ํ๋ฆ„: ์ด ๊ทธ๋ž˜ํ”„๋Š” ๋ณ‘๋ ฌ๋กœ ๊ฒ€์ƒ‰ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๊ณ , ๊ทธ ํ›„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์ด์šฉํ•˜์—ฌ ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ์‹คํ–‰ ํ๋ฆ„ ์„ค๋ช…:

    • ์‚ฌ์šฉ์ž๊ฐ€ ์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜๋ฉด, ์œ„ํ‚คํ”ผ๋””์•„์™€ ์›น ๊ฒ€์ƒ‰์ด ๋™์‹œ์— ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
    • ๊ฒ€์ƒ‰๋œ ๋ฌธ์„œ๋“ค์ด ๊ฐ๊ฐ ํฌ๋งทํŒ…๋˜์–ด context ์ƒํƒœ์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
    • ๊ทธ ํ›„, ๋‘ ๊ฒ€์ƒ‰์˜ ๊ฒฐ๊ณผ๊ฐ€ ๋ณ‘ํ•ฉ๋˜์–ด generate_answer ๋…ธ๋“œ์—์„œ LLM์„ ํ†ตํ•ด ๋‹ต๋ณ€์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
  • ๊ฒฐ๊ณผ์ ์œผ๋กœ, ๊ฒ€์ƒ‰๋œ ๋ฌธ์„œ๋“ค์„ ๋ฐ”ํƒ•์œผ๋กœ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ์ตœ์ข… ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•˜๋Š” ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค.

5) ์งˆ๋ฌธ ์ž…๋ ฅ

  • ์•„๋ž˜์™€ ๊ฐ™์ด ์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜๋ฉด TAVILY์™€ WIKIPEDIA SEARCH๋ฅผ ํ†ตํ•ด์„œ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
1
2
result = graph.invoke({"question": "LG์ „์ž์˜ 2024๋…„ 3๋ถ„๊ธฐ ์‹ค์ ์€ ์–ด๋• ๋‚˜์š”?"})
print(result['answer'].content)
  • ์•„๋ž˜๋Š” ์œ„ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค:
1
2
3
4
5
6
LG์ „์ž์˜ 2024๋…„ 3๋ถ„๊ธฐ ์‹ค์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

- ๋งค์ถœ: 22์กฐ1769์–ต์›, ์ „๋…„ ๋™๊ธฐ ๋Œ€๋น„ 10.7% ์ฆ๊ฐ€
- ์˜์—…์ด์ต: 7511์–ต์›, ์ „๋…„ ๋™๊ธฐ ๋Œ€๋น„ 20.9% ๊ฐ์†Œ

๋งค์ถœ์€ 3๋ถ„๊ธฐ ์ตœ๋Œ€์น˜๋ฅผ ๊ธฐ๋กํ–ˆ์œผ๋ฉฐ, ์ด๋Š” ๊ฐ€์ „์ œํ’ˆ๊ณผ ์ „์žฅ ๋ถ€๋ฌธ์˜ ์ง€์†์ ์ธ ์„ฑ์žฅ ๋•๋ถ„์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์˜์—…์ด์ต์€ ๋ฌผ๋ฅ˜๋น„ ๊ธ‰๋“ฑ ๋ฐ ๋งˆ์ผ€ํŒ…๋น„ ์ฆ๊ฐ€๋กœ ์ธํ•ด ๊ฐ์†Œํ–ˆ์Šต๋‹ˆ๋‹ค.

Lesson 2: Sub-graphs

๊ฐœ์š”

  • ์„œ๋ธŒ๊ทธ๋ž˜ํ”„(Sub-graph)๋Š” ํฐ ๊ทธ๋ž˜ํ”„์˜ ์ผ๋ถ€๋ถ„์œผ๋กœ, ๋…๋ฆฝ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ž‘์—… ๋‹จ์œ„์ž…๋‹ˆ๋‹ค.

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

    • ๋กœ๊ทธ๋ฅผ ๋ฐ›๋Š” ์‹œ์Šคํ…œ์ด ์žˆ์Šต๋‹ˆ๋‹ค
    • ์ด ์‹œ์Šคํ…œ์€ ์„œ๋กœ ๋‹ค๋ฅธ ๊ฐ๊ฐ์˜ ์—์ด์ „ํŠธ(Summary Agent, Failure Analysis Agent)์— ์˜ํ•ด ๋‘ ๊ฐ€์ง€ ๋ณ„๋„์˜ ํ•˜์œ„ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค(๋ฌธ์„œ์š”์•ฝ, ์˜ค๋ฅ˜๋ถ„์„)
    • ์ด ๋‘ ๊ฐ€์ง€ ์ž‘์—…์„ ๋‘ ๊ฐœ์˜ ๋‹ค๋ฅธ ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„์—์„œ ์ˆ˜ํ–‰ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

  • ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฒƒ์€ ๊ทธ๋ž˜ํ”„๋“ค์ด โ€œ์–ด๋–ป๊ฒŒ ํ†ต์‹ ์„ ํ• ๊นŒโ€๋ผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
  • ๊ฐ„๋‹จํžˆ ๋งํ•ด, ํ†ต์‹ ์€ ๊ฒน์น˜๋Š” ํ‚ค๋ฅผ ํ†ตํ•ด ์ด๋ฃจ์–ด์ง‘๋‹ˆ๋‹ค:

    • ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„๋Š” ๋ถ€๋ชจ๋กœ๋ถ€ํ„ฐ docs์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
    • ๋ถ€๋ชจ๋Š” ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„๋กœ๋ถ€ํ„ฐ Summary Agent ๋˜๋Š” Failure Analysis Agent์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

์ฃผ์š” ๊ฐœ๋…

  1. ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„์™€ ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„ ๊ฐ„์˜ ์ƒํƒœ ๊ณต์œ :

    • ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„์™€ ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„ ๊ฐ„์— ๋ฐ์ดํ„ฐ๋ฅผ ๊ตํ™˜ํ•˜๊ธฐ ์œ„ํ•ด ์ƒํƒœ ํ‚ค๊ฐ€ ์ค‘๋ณต๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ํŠน์ • ์ƒํƒœ๋Š” ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ ๋‚ด์—์„œ๋งŒ ๊ด€๋ฆฌ๋˜๋ฉฐ ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„์—๋Š” ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  2. ์ถœ๋ ฅ ์Šคํ‚ค๋งˆ:

    • ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„์˜ ์ถœ๋ ฅ ์ƒํƒœ๋ฅผ ์ œํ•œํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ์ƒํƒœ ํ‚ค๋ฅผ ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„์— ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ ์˜ˆ์‹œ

  • ์ด๋ฒˆ ์ฝ”๋“œ ์‹ค์Šต์€ ๋‘ ๊ฐœ์˜ ์„œ๋ธŒ-๊ทธ๋ž˜ํ”„(Failure Analysis ์„œ๋ธŒ-๊ทธ๋ž˜ํ”„์™€ Question Summarization ์„œ๋ธŒ-๊ทธ๋ž˜ํ”„)๋ฅผ ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„(EntryGraph)๋กœ ํ†ตํ•ฉํ•˜์—ฌ, ์ฃผ์–ด์ง„ ๋กœ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ๋ถ„์„ํ•˜๋Š” ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ•œ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.
    • ์ด ์‹œ์Šคํ…œ์€ ๋ณ‘๋ ฌ๋กœ ๋‘ ๊ฐœ์˜ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋ฅผ ์‹คํ–‰ํ•œ ํ›„, ๊ฐ๊ฐ์˜ ๊ฒฐ๊ณผ๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ์ตœ์ข… ๋ณด๊ณ ์„œ์™€ ์š”์•ฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
# ํ•„์š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ž„ํฌํŠธ
from operator import add
from typing_extensions import TypedDict
from typing import List, Optional, Annotated

1. ๋กœ๊ทธ ๊ตฌ์กฐ ์ •์˜:

  • ๋จผ์ €, Log ํด๋ž˜์Šค๋Š” ๋กœ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๊ฐ ๋กœ๊ทธ๋Š” ์งˆ๋ฌธ๊ณผ ๋‹ต๋ณ€, ํ‰๊ฐ€ ์ •๋ณด ๋ฐ ํ”ผ๋“œ๋ฐฑ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
class Log(TypedDict):
    id: str
    question: str
    docs: Optional[List]
    answer: str
    grade: Optional[int]
    grader: Optional[str]
    feedback: Optional[str]

  • id: ๋กœ๊ทธ์˜ ๊ณ ์œ  ์‹๋ณ„์ž.
  • question: ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ.
  • docs: ์งˆ๋ฌธ์— ๋Œ€ํ•ด ๊ฒ€์ƒ‰๋œ ๋ฌธ์„œ๋“ค.
  • answer: ์ œ๊ณต๋œ ๋‹ต๋ณ€.
  • grade: ๋กœ๊ทธ์˜ ํ‰๊ฐ€ (์˜ˆ: ํ’ˆ์งˆ ์ ์ˆ˜).
  • grader: ํ‰๊ฐ€์ž ์ •๋ณด.
  • feedback: ํ‰๊ฐ€์™€ ๊ด€๋ จ๋œ ํ”ผ๋“œ๋ฐฑ.

2. Failure Analysis ์„œ๋ธŒ๊ทธ๋ž˜ํ”„:

  • Failure Analysis ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์˜ ๋ชฉํ‘œ๋Š” ์‹คํŒจํ•œ ๋กœ๊ทธ๋ฅผ ์‹๋ณ„ํ•˜๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์š”์•ฝํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  • FailureAnalysisState๋Š” ์‹คํŒจ ๋ถ„์„ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ๊ด€๋ฆฌํ•  ์ƒํƒœ๋ฅผ ์ •์˜ํ•˜๋ฉฐ, ์ฃผ๋กœ ์‹คํŒจํ•œ ๋กœ๊ทธ์— ๋Œ€ํ•œ ์š”์•ฝ๊ณผ ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
class FailureAnalysisState(TypedDict):
    cleaned_logs: List[Log]   # ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ
    failures: List[Log]       # ์‹คํŒจํ•œ ๋กœ๊ทธ ๋ชฉ๋ก
    fa_summary: str           # ์‹คํŒจ ๋ถ„์„ ์š”์•ฝ
    processed_logs: List[str] # ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ ์ •๋ณด

  • FailureAnalysisOutputState๋Š” ์‹คํŒจ ๋ถ„์„ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์˜ ์ถœ๋ ฅ ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ์ถœ๋ ฅ ์Šคํ‚ค๋งˆ๋Š” ์œ„์—์™€๋Š” ๋‹ค๋ฅด๊ฒŒ ์œ ์ €์—๊ฒŒ ์ „ํ•˜๊ณ  ์‹ถ์€ ์ •๋ณด๋งŒ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
1
2
3
4
class FailureAnalysisOutputState(TypedDict):
    fa_summary: str
    processed_logs: List[str]

  • get_failures: ์‹คํŒจํ•œ ๋กœ๊ทธ(grade๊ฐ€ ํฌํ•จ๋œ ๋กœ๊ทธ)๋ฅผ ํ•„ํ„ฐ๋งํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
def get_failures(state):
    """ ์‹คํŒจ๋ฅผ ํฌํ•จํ•˜๋Š” ๋กœ๊ทธ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค """
    cleaned_logs = state["cleaned_logs"]
    failures = [log for log in cleaned_logs if "grade" in log]
    return {"failures": failures}

  • generate_summary: ํ•„ํ„ฐ๋ง๋œ ์‹คํŒจ ๋กœ๊ทธ๋“ค์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์š”์•ฝ์„ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ processed_logs๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
def generate_summary(state):
    """ ์‹คํŒจ์˜ ์š”์•ฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค """
    failures = state["failures"]
    # ํ•จ์ˆ˜ ์ถ”๊ฐ€: fa_summary = summarize(failures)
    fa_summary = "Chroma ๋ฌธ์„œ์˜ ๊ฒ€์ƒ‰ ํ’ˆ์งˆ์ด ๋‚ฎ์Œ."
    return {"fa_summary": fa_summary, 
            "processed_logs": [f"failure-analysis-on-log-{failure['id']}" for failure in failures]}

  • sub-graph ์ •์˜: ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด์„œ sub-graph๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
fa_builder = StateGraph(input=FailureAnalysisState,output=FailureAnalysisOutputState)
fa_builder.add_node("get_failures", get_failures)
fa_builder.add_node("generate_summary", generate_summary)
fa_builder.add_edge(START, "get_failures")
fa_builder.add_edge("get_failures", "generate_summary")
fa_builder.add_edge("generate_summary", END)

graph = fa_builder.compile()
display(Image(graph.get_graph().draw_mermaid_png()))

3. Question Summarization ์„œ๋ธŒ๊ทธ๋ž˜ํ”„

  • Question Summarization ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋Š” ๋กœ๊ทธ๋ฅผ ์š”์•ฝํ•˜๊ณ , ์ตœ์ข… ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

  • QuestionSummarizationState๋Š” ์งˆ๋ฌธ ์š”์•ฝ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ์‚ฌ์šฉํ•  ์ƒํƒœ๋ฅผ ์ •์˜ํ•˜๋ฉฐ, ์ฃผ๋กœ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ์š”์•ฝ๊ณผ ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
class QuestionSummarizationState(TypedDict):
    cleaned_logs: List[Log]   # ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ
    qs_summary: str           # ์งˆ๋ฌธ ์š”์•ฝ
    report: str               # ๋ณด๊ณ ์„œ
    processed_logs: List[str] # ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ ์ •๋ณด

  • QuestionSummarizationOutputState๋Š” ์งˆ๋ฌธ ์š”์•ฝ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์˜ ์ถœ๋ ฅ ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ์ถœ๋ ฅ ์Šคํ‚ค๋งˆ๋Š” ์œ„์—์™€๋Š” ๋‹ค๋ฅด๊ฒŒ ์œ ์ €์—๊ฒŒ ์ „ํ•˜๊ณ  ์‹ถ์€ ์ •๋ณด๋งŒ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
1
2
3
4
class QuestionSummarizationOutputState(TypedDict):
    report: str
    processed_logs: List[str]

  • generate_summary: ์งˆ๋ฌธ ๋กœ๊ทธ๋ฅผ ์š”์•ฝํ•˜๊ณ , ๊ฐ ๋กœ๊ทธ์˜ ์š”์•ฝ ๊ฒฐ๊ณผ๋ฅผ processed_logs์— ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
def generate_summary(state):
    cleaned_logs = state["cleaned_logs"]
    # ํ•จ์ˆ˜ ์ถ”๊ฐ€: summary = summarize(generate_summary)
    summary = "์งˆ๋ฌธ์€ ChatOllama์™€ Chroma ๋ฒกํ„ฐ ์ €์žฅ์†Œ์˜ ์‚ฌ์šฉ์— ์ดˆ์ ์„ ๋งž์ท„์Šต๋‹ˆ๋‹ค."
    return {"qs_summary": summary, 
            "processed_logs": [f"summary-on-log-{log['id']}" for log in cleaned_logs]}
  • send_to_slack: ์ƒ์„ฑ๋œ ์š”์•ฝ์„ ๋ณด๊ณ ์„œ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค(๋‹จ์ˆœํ•œ ๋”๋ฏธ ์˜ˆ์‹œ์—์„œ โ€œfoo bar bazโ€๋ผ๋Š” ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜).
1
2
3
4
5
6
def send_to_slack(state):
    qs_summary = state["qs_summary"]
    # ํ•จ์ˆ˜ ์ถ”๊ฐ€: report = report_generation(qs_summary)
    report = "foo bar baz"
    return {"report": report}

  • sub-graph ์ •์˜: ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด์„œ sub-graph๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
qs_builder = StateGraph(input=QuestionSummarizationState,output=QuestionSummarizationOutputState)
qs_builder.add_node("generate_summary", generate_summary)
qs_builder.add_node("send_to_slack", send_to_slack)
qs_builder.add_edge(START, "generate_summary")
qs_builder.add_edge("generate_summary", "send_to_slack")
qs_builder.add_edge("send_to_slack", END)

graph = qs_builder.compile()
display(Image(graph.get_graph().draw_mermaid_png()))

๐Ÿ“š (์ฐธ๊ณ ) EntryGraphStates

  • ์šฐ๋ฆฌ๋Š” EntryGraphState ํด๋ž˜์Šค๋กœ ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์•„๋ž˜์ฒ˜๋Ÿผ ์ •์˜ํ•ด์„œ ์„œ๋ธŒ ๋…ธ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • entry_builder.add_node("question_summarization", qs_builder.compile())
    • entry_builder.add_node("failure_analysis", fa_builder.compile())
  • EntryGraphState ํด๋ž˜์Šค๋ฅผ ์‚ดํŽด๋ณด๋ฉด, cleaned_logs์™€ processed_logs์— Annotated[List[Log], add]๊ฐ€ ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
# ์ง„์ž… ๊ทธ๋ž˜ํ”„
class EntryGraphState(TypedDict):
    raw_logs: List[Log]
    cleaned_logs: Annotated[List[Log], add] # ์ด๊ฒƒ์€ ๋‘ ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„์—์„œ ๋ชจ๋‘ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค
    fa_summary: str # ์ด๊ฒƒ์€ FA ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„์—์„œ๋งŒ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค
    report: str # ์ด๊ฒƒ์€ QS ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„์—์„œ๋งŒ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค
    processed_logs: Annotated[List[int], add] # ์ด๊ฒƒ์€ ๋‘ ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„์—์„œ ๋ชจ๋‘ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค
  • ์œ„์˜ EntryGraphState์—์„œ ๊ฐ๊ฐ cleaned_logs์™€ processed_logs์— Annotated[List[Log], add] ๋ฆฌ๋“€์„œ๊ฐ€ ์‚ฌ์šฉ๋˜๋Š” ์ด์œ ๋Š”, ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๊ฐ€ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋˜๋ฉด์„œ ๋™์ผํ•œ ์ƒํƒœ ํ‚ค๋ฅผ ์ฐธ์กฐํ•˜๊ฑฐ๋‚˜ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
  • ๊ฐ๊ฐ์˜ ๋ฆฌ๋“€์„œ๋ฅผ ์ •์˜ํ•ด์คŒ์œผ๋กœ์จ Annotated[List[int], add] ๋ณ‘๋ ฌ ์‹คํ–‰ ์‹œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํƒœ ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•˜๊ณ , ์—ฌ๋Ÿฌ ๊ฐ’์„ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณ‘ํ•ฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.

๐Ÿ“š (์‹ฌํ™”) cleaned_logs: Annotated[List[Log], add]

1
cleaned_logs: Annotated[List[Log], add]
  • ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ: cleaned_logs๋Š” Failure Analysis ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์™€ Question Summarization ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๊ฐ€ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ์ƒํƒœ๋Š” ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ์ˆ˜์ •๋˜์ง€ ์•Š๊ณ  ์ฐธ์กฐ๋งŒ ๋ฉ๋‹ˆ๋‹ค.
  • ๋ณ‘๋ ฌ ์‹คํ–‰ ์‹œ์˜ ์ƒํƒœ ์ถฉ๋Œ ๋ฐฉ์ง€: ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋˜๋Š” ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๊ฐ€ ๊ฐ™์€ ์ƒํƒœ ํ‚ค(cleaned_logs)๋ฅผ ์ฐธ์กฐํ•˜์ง€๋งŒ, ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•  ๋•Œ ๋™์ผํ•œ ํ‚ค๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ด ํ‚ค์— ๋Œ€ํ•œ ์ƒํƒœ ๋ณ‘ํ•ฉ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌ๋“€์„œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ƒํƒœ๊ฐ€ ์ถฉ๋Œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • add ๋ฆฌ๋“€์„œ์˜ ์—ญํ• : add ๋ฆฌ๋“€์„œ๋Š” ๊ฐ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ๋ฐ˜ํ™˜๋œ cleaned_logs ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ณ‘ํ•ฉํ•ด ํ•˜๋‚˜์˜ ๋ฆฌ์ŠคํŠธ๋กœ ๋งŒ๋“ค๋ฉฐ, ์ด๋•Œ ์›๋ณธ ๋ฆฌ์ŠคํŠธ๋Š” ์ˆ˜์ •๋˜์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๊ฐ€ ๊ฐ™์€ cleaned_logs๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋”๋ผ๋„, add ๋ฆฌ๋“€์„œ๋ฅผ ํ†ตํ•ด ์•ˆ์ „ํ•˜๊ฒŒ ๋ณ‘ํ•ฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“š (์‹ฌํ™”) processed_logs: Annotated[List[int], add]

1
processed_logs: Annotated[List[int], add]
  • ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ์ƒ์„ฑ๋˜๋Š” ๊ฐ’: processed_logs๋Š” Failure Analysis ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์™€ Question Summarization ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๊ฐ€ ๊ฐ๊ธฐ ๋‹ค๋ฅธ ๊ฐ’์„ ์ƒ์„ฑํ•˜๋Š” ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ๊ฐ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋Š” ์ž์ฒด์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ์˜ ID ๋˜๋Š” ๊ด€๋ จ ์ •๋ณด๋ฅผ processed_logs์— ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.
  • ๋ณ‘๋ ฌ ์‹คํ–‰ ์‹œ ๊ฐ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ๊ฐ’ ์ถ”๊ฐ€: ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๊ฐ€ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋˜๋ฉด์„œ, ๊ฐ๊ฐ์˜ processed_logs ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋ฆฌ๋“€์„œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด, ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๊ฐ€ ๋ฐ˜ํ™˜ํ•œ ๊ฐ’์ด ์ถฉ๋Œํ•˜์—ฌ ํ•œ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์˜ ๊ฐ’๋งŒ ๋ฐ˜์˜๋˜๊ฑฐ๋‚˜ ์ƒํƒœ๊ฐ€ ๋ฎ์–ด์”Œ์›Œ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • add ๋ฆฌ๋“€์„œ์˜ ์—ญํ• : add ๋ฆฌ๋“€์„œ๋Š” ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ๋ฐ˜ํ™˜๋œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ณ‘ํ•ฉํ•˜์—ฌ, ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์˜ ๋ชจ๋“  ๋กœ๊ทธ๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ๊ธฐ๋ก๋˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๊ฐ€ ๊ฐ๊ธฐ ๋‹ค๋ฅธ ๊ฐ’์„ ์ƒ์„ฑํ•ด๋„, add ๋ฆฌ๋“€์„œ๊ฐ€ ๊ฐ ๊ฐ’๋“ค์„ ๊ฒฐํ•ฉํ•˜์—ฌ processed_logs์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค” (์‹ฌํ™”) ๊ฐ๊ฐ ๋‘˜๋‹ค ๊ณตํ†ต์ ์ธ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๊ณต์œ ํ•˜๋‹ค๋ณด๋‹ˆ ๊ทธ๋Ÿฐ๊ฑฐ๋ผ๊ณ  ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ ๋‹ค๋ฅธ ํด๋ž˜์Šค๋ผ์„œ ์ƒ๊ด€์—†์ง€ ์•Š๋‚˜?

  • ์ข‹์€ ์งˆ๋ฌธ์ž…๋‹ˆ๋‹ค! ๊ฒ‰๋ณด๊ธฐ์—๋Š” FailureAnalysisState์™€ QuestionSummarizationState๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ํด๋ž˜์Šค์ด๊ณ , ๊ฐ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ ๋‚ด์—์„œ ๋…๋ฆฝ์ ์œผ๋กœ ์ •์˜๋œ ์ƒํƒœ์ด๊ธฐ ๋•Œ๋ฌธ์— cleaned_logs์™€ processed_logs๊ฐ€ ๊ฒน์น˜๋”๋ผ๋„ ๋ฌธ์ œ๊ฐ€ ์—†์–ด ๋ณด์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋“ค์ด ๋™์ผํ•œ ์ƒ์œ„ ๊ทธ๋ž˜ํ”„(Entry Graph)์—์„œ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋  ๋•Œ๋Š” ์ƒํ™ฉ์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค.
  • cleaned_logs์™€ processed_logs๊ฐ€ ๋…๋ฆฝ๋œ ํด๋ž˜์Šค์—์„œ ์ •์˜๋˜์—ˆ์ง€๋งŒ, Entry Graph(์ƒ์œ„ ๊ทธ๋ž˜ํ”„)์—์„œ๋Š” ์ด๋“ค ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๊ฐ€ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์˜ ์ƒํƒœ๊ฐ€ ์ƒ์œ„ ๊ทธ๋ž˜ํ”„์˜ ์ƒํƒœ๋กœ ๋ณ‘ํ•ฉ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋Š” EntryGraphState์— ์ •์˜๋œ cleaned_logs์™€ processed_logs๋ฅผ ๊ณต์œ ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๊ฐ€ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋  ๋•Œ, ๋™์ผํ•œ cleaned_logs์™€ processed_logs ์ƒํƒœ๋ฅผ ์ฐธ์กฐํ•˜๊ฑฐ๋‚˜ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋˜๋ฏ€๋กœ, ์ด ์ƒํƒœ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณ‘ํ•ฉํ•˜์ง€ ์•Š์œผ๋ฉด ์ƒํƒœ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • cleaned_logs: ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ์ฐธ์กฐ๋งŒ ํ•˜๋ฏ€๋กœ ์‹ค์ œ๋กœ ์ˆ˜์ •๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๊ฐ€ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋œ ํ›„, ๊ฐ๊ฐ cleaned_logs๋ฅผ ๋™์ผํ•œ ํ‚ค๋กœ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ, ์ƒ์œ„ ๊ทธ๋ž˜ํ”„์—์„œ๋Š” ์ด๋ฅผ ๋ณ‘ํ•ฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ add ๋ฆฌ๋“€์„œ๊ฐ€ ๋ณ‘ํ•ฉ ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
    • processed_logs: ๊ฐ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ์ƒ์„ฑํ•œ ์ฒ˜๋ฆฌ ๋กœ๊ทธ ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฏ€๋กœ, ๋ณ‘๋ ฌ ์‹คํ–‰ ํ›„ ๊ฐ๊ฐ์˜ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ๊ฐ€ ์ƒ์œ„ ๊ทธ๋ž˜ํ”„์˜ processed_logs์— ๋ˆ„์ ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ๋„ add ๋ฆฌ๋“€์„œ๊ฐ€ ์žˆ์–ด์•ผ ๋ณ‘ํ•ฉ ์ถฉ๋Œ ์—†์ด ๊ฐ๊ฐ์˜ ๊ฒฐ๊ณผ๊ฐ€ ํ•œ ๋ฆฌ์ŠคํŠธ๋กœ ๋ชจ์•„์ง‘๋‹ˆ๋‹ค.

4. ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„ (EntryGraph)

  • ์ด์ œ ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„์—์„œ๋Š” ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋ฅผ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์ข…ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

  • (์ฐธ๊ณ ) xray=0 ๊ฐ’์œผ๋กœ ์ฃผ์—ˆ์„ ๋•Œ์˜ ์‹œ๊ฐํ™” ๊ฒฐ๊ณผ

  • EntryGraphState๋Š” ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„์—์„œ ๊ด€๋ฆฌํ•  ์ „์ฒด ์ƒํƒœ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
class EntryGraphState(TypedDict):
    raw_logs: List[Log]                        # ์›์‹œ ๋กœ๊ทธ ๋ฐ์ดํ„ฐ
    cleaned_logs: List[Log]                    # ์ •์ œ๋œ ๋กœ๊ทธ (๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ์‚ฌ์šฉ)
    fa_summary: str                            # ์‹คํŒจ ๋ถ„์„ ์š”์•ฝ (FA ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ์ƒ์„ฑ)
    report: str                                # ์งˆ๋ฌธ ์š”์•ฝ ๋ณด๊ณ ์„œ (QS ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ์ƒ์„ฑ)
    processed_logs: Annotated[List[int], add]  # ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ ๋ชฉ๋ก (๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ์ƒ์„ฑ)

  • fa_summary: Failure Analysis ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ์ƒ์„ฑ๋œ ์‹คํŒจ ๋ถ„์„ ์š”์•ฝ.
  • report: Question Summarization ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ์ƒ์„ฑ๋œ ๋ณด๊ณ ์„œ.
  • processed_logs: ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ๊ณตํ†ต์œผ๋กœ ์ƒ์„ฑ๋œ ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ ๋ชฉ๋ก์„ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
  • cleaned_logs: ์›์‹œ ๋กœ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ์ •์ œํ•˜์—ฌ cleaned_logs ์ƒํƒœ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ƒํƒœ๋Š” ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

    1
    2
    3
    4
    5
    6
    7
    
    def cleaned_logs(state):
        # ๋กœ๊ทธ ๊ฐ€์ ธ์˜ค๊ธฐ
        raw_logs = state["raw_logs"]
        # raw_logs -> docs ๋ฐ์ดํ„ฐ ์ •์ œ 
        cleaned_logs = raw_logs
        return {"cleaned_logs": cleaned_logs}
      
    

๐Ÿคฆโ€โ™‚๏ธ ์ž ๊น!! ์–ด!? ์—ฌ๊ธฐ cleaned_logs์—๋Š” ๋ฆฌ๋“€์„œ๊ฐ€ ์—†๋Š”๋ฐ์š”?

1
2
3
4
5
6
7
# ์ง„์ž… ๊ทธ๋ž˜ํ”„
class EntryGraphState(TypedDict):
    raw_logs: List[Log]
    cleaned_logs: List[Log]
    fa_summary: str # ์ด๊ฒƒ์€ FA ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„์—์„œ๋งŒ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค
    report: str # ์ด๊ฒƒ์€ QS ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„์—์„œ๋งŒ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค
    processed_logs: Annotated[List[int], add] # ์ด๊ฒƒ์€ ๋‘ ์„œ๋ธŒ ๊ทธ๋ž˜ํ”„์—์„œ ๋ชจ๋‘ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค
  • ์ด๊ฒƒ์€ ํŠน์ด ์ผ€์ด์Šค๋กœ, ์•ž์—์„œ ๊ฐ๊ฐ FailureAnalysis์™€ QuestionSummarization์˜ OutputState(FailureAnalysisOutputState, QuestionSummarizationOutputState)๋ฅผ ์ •์˜ํ•˜์—ฌ, clean_logs๋ฅผ output์œผ๋กœ ๋ณด๋‚ด์ง€ ์•Š๊ณ  processed_logs๋งŒ ๋ณด๋‚ด์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์ด์— processed_logs: Annotated[List[int], add]๋งŒ ๋ณ„๋„๋กœ ๋ฆฌ๋“€์„œ ์ฒ˜๋ฆฌํ•ด์ค€ ๊ฒƒ์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.


5. ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์˜ ํ†ตํ•ฉ ๋ฐ ๋ณ‘๋ ฌ ์‹คํ–‰

๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋Š” clean_logs๊ฐ€ ์™„๋ฃŒ๋œ ํ›„ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ด ๋•Œ ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋Š” ๊ฐ™์€ cleaned_logs ์ƒํƒœ๋ฅผ ์ฐธ์กฐํ•˜์ง€๋งŒ ์ˆ˜์ •ํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
entry_builder.add_node("cleaned_logs", cleaned_logs)
entry_builder.add_node("question_summarization", qs_builder.compile())
entry_builder.add_node("failure_analysis", fa_builder.compile())

entry_builder.add_edge("cleaned_logs", "failure_analysis")
entry_builder.add_edge("cleaned_logs", "question_summarization")
entry_builder.add_edge("failure_analysis", END)
entry_builder.add_edge("question_summarization", END)

graph = entry_builder.compile()
  • cleaned_logs: ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„์—์„œ ์›์‹œ ๋กœ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ์ •์ œํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.
  • question_summarization: Question Summarization ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋ฅผ ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  • failure_analysis: Failure Analysis ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋ฅผ ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

6. ์‹คํ–‰ ์˜ˆ์ œ

  • ๋”๋ฏธ ๋กœ๊ทธ ์˜ˆ์‹œ:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ๋”๋ฏธ ๋กœ๊ทธ
question_answer = Log(
    id="1",
    question="ChatOllama๋ฅผ ์–ด๋–ป๊ฒŒ ์ž„ํฌํŠธํ•  ์ˆ˜ ์žˆ๋‚˜์š”?",
    answer="ChatOllama๋ฅผ ์ž„ํฌํŠธํ•˜๋ ค๋ฉด ๋‹ค์Œ์„ ์‚ฌ์šฉํ•˜์„ธ์š”: 'from langchain_community.chat_models import ChatOllama.'",
)

question_answer_feedback = Log(
    id="2",
    question="Chroma ๋ฒกํ„ฐ ์ €์žฅ์†Œ๋ฅผ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?",
    answer="Chroma๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •์˜ํ•˜์„ธ์š”: rag_chain = create_retrieval_chain(retriever, question_answer_chain).",
    grade=0,
    grader="๋ฌธ์„œ ๊ด€๋ จ์„ฑ ํšŒ์ƒ",
    feedback="๊ฒ€์ƒ‰๋œ ๋ฌธ์„œ๋“ค์€ ๋ฒกํ„ฐ ์ €์žฅ์†Œ์— ๋Œ€ํ•ด ์ผ๋ฐ˜์ ์œผ๋กœ ๋…ผ์˜ํ•˜์ง€๋งŒ, Chroma์— ๋Œ€ํ•ด ๊ตฌ์ฒด์ ์œผ๋กœ ๋‹ค๋ฃจ์ง€ ์•Š์Šต๋‹ˆ๋‹ค",
)
  • ์˜ˆ์‹œ-1:
1
2
raw_logs = [question_answer, question_answer_feedback]
graph.invoke({"raw_logs": [question_answer]})
  • ์ถœ๋ ฅ ๊ฒฐ๊ณผ-1:
1
2
3
4
5
6
7
8
9
10
11
{'raw_logs': [{'id': '1',
   'question': 'ChatOllama๋ฅผ ์–ด๋–ป๊ฒŒ ์ž„ํฌํŠธํ•  ์ˆ˜ ์žˆ๋‚˜์š”?',
   'answer': "ChatOllama๋ฅผ ์ž„ํฌํŠธํ•˜๋ ค๋ฉด ๋‹ค์Œ์„ ์‚ฌ์šฉํ•˜์„ธ์š”: 'from langchain_community.chat_models import ChatOllama.'"}],
   
 'cleaned_logs': [{'id': '1',
   'question': 'ChatOllama๋ฅผ ์–ด๋–ป๊ฒŒ ์ž„ํฌํŠธํ•  ์ˆ˜ ์žˆ๋‚˜์š”?',
   'answer': "ChatOllama๋ฅผ ์ž„ํฌํŠธํ•˜๋ ค๋ฉด ๋‹ค์Œ์„ ์‚ฌ์šฉํ•˜์„ธ์š”: 'from langchain_community.chat_models import ChatOllama.'"}],
   
 'fa_summary': 'Chroma ๋ฌธ์„œ์˜ ๊ฒ€์ƒ‰ ํ’ˆ์งˆ์ด ๋‚ฎ์Œ.',
 'report': 'foo bar baz',
 'processed_logs': ['summary-on-log-1']}
  • ์˜ˆ์‹œ-2:
1
2
ans = graph.invoke({"raw_logs":  [question_answer_feedback]})
ans
  • ์ถœ๋ ฅ ๊ฒฐ๊ณผ-2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{'raw_logs': [{'id': '2',
   'question': 'Chroma ๋ฒกํ„ฐ ์ €์žฅ์†Œ๋ฅผ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?',
   'answer': 'Chroma๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •์˜ํ•˜์„ธ์š”: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',
   'grade': 0,
   'grader': '๋ฌธ์„œ ๊ด€๋ จ์„ฑ ํšŒ์ƒ',
   'feedback': '๊ฒ€์ƒ‰๋œ ๋ฌธ์„œ๋“ค์€ ๋ฒกํ„ฐ ์ €์žฅ์†Œ์— ๋Œ€ํ•ด ์ผ๋ฐ˜์ ์œผ๋กœ ๋…ผ์˜ํ•˜์ง€๋งŒ, Chroma์— ๋Œ€ํ•ด ๊ตฌ์ฒด์ ์œผ๋กœ ๋‹ค๋ฃจ์ง€ ์•Š์Šต๋‹ˆ๋‹ค'}],
   
 'cleaned_logs': [{'id': '2',
   'question': 'Chroma ๋ฒกํ„ฐ ์ €์žฅ์†Œ๋ฅผ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?',
   'answer': 'Chroma๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •์˜ํ•˜์„ธ์š”: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',
   'grade': 0,
   'grader': '๋ฌธ์„œ ๊ด€๋ จ์„ฑ ํšŒ์ƒ',
   'feedback': '๊ฒ€์ƒ‰๋œ ๋ฌธ์„œ๋“ค์€ ๋ฒกํ„ฐ ์ €์žฅ์†Œ์— ๋Œ€ํ•ด ์ผ๋ฐ˜์ ์œผ๋กœ ๋…ผ์˜ํ•˜์ง€๋งŒ, Chroma์— ๋Œ€ํ•ด ๊ตฌ์ฒด์ ์œผ๋กœ ๋‹ค๋ฃจ์ง€ ์•Š์Šต๋‹ˆ๋‹ค'}],
   
 'fa_summary': 'Chroma ๋ฌธ์„œ์˜ ๊ฒ€์ƒ‰ ํ’ˆ์งˆ์ด ๋‚ฎ์Œ.',
 'report': 'foo bar baz',
 'processed_logs': ['summary-on-log-2', 'failure-analysis-on-log-2']}

๐Ÿ“‹ ์˜ˆ์ œ ํ๋ฆ„ ์ •๋ฆฌ

    1. ๋ถ€๋ชจ ๊ทธ๋ž˜ํ”„์—์„œ ์›๋ณธ ๋กœ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ์ •์ œํ•˜์—ฌ cleaned_logs๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
      • ์˜ˆ์ œ์—์„œ๋Š” ์ด๋ฅผ ์œ„ํ•ด โ€œ๋”๋ฏธ ๋กœ๊ทธโ€๋ฅผ ์ƒ์„ฑ ํ›„ ๋„ฃ์–ด์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค
    1. cleaned_logs๋Š” Failure Analysis ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์™€ Question Summarization ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์— ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.
      • Failure Analysis ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋Š” ์‹คํŒจํ•œ ๋กœ๊ทธ(question_answer_feedback)๋ฅผ ์‹๋ณ„ํ•˜๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋กœ ์‹คํŒจ ์š”์•ฝ(fa_summary)์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
      • Question Summarization ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋Š” ์งˆ๋ฌธ ๋กœ๊ทธ(question_answer)๋ฅผ ์š”์•ฝํ•˜๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋กœ ๋ณด๊ณ ์„œ(report)๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    1. ๋‘ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์—์„œ ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ ์ •๋ณด๋Š” processed_logs์— ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค.
    1. ์ตœ์ข… ๊ฒฐ๊ณผ๋กœ, ๊ฐ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„์˜ ๊ฒฐ๊ณผ(fa_summary์™€ report)์™€ ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ ๋ชฉ๋ก์ด ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.

Lesson 3: Map-reduce

๊ฐœ์š”

MapReduce๋Š” ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ์…‹์„ ๋ณ‘๋ ฌ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๋‘ ๊ฐ€์ง€ ๊ธฐ๋ณธ ๋‹จ๊ณ„์ธ Map ๋‹จ๊ณ„์™€ Reduce ๋‹จ๊ณ„๋กœ ๋‚˜๋‰˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๊ฐ ๋‹จ๊ณ„์˜ ํ•ต์‹ฌ ๋‚ด์šฉ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • Map ๋‹จ๊ณ„: ์ฃผ์–ด์ง„ ์ž‘์—…์„ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ž‘์€ ์ž‘์—…์œผ๋กœ ๋ถ„ํ• ํ•˜๊ณ  ์ด๋ฅผ ๋ณ‘๋ ฌ๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ ์ค‘๊ฐ„ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • Reduce ๋‹จ๊ณ„: Map ๋‹จ๊ณ„์˜ ์ค‘๊ฐ„ ๊ฒฐ๊ณผ๋“ค์„ ํ•˜๋‚˜๋กœ ํ†ตํ•ฉํ•˜๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜์—ฌ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ์–ป์Šต๋‹ˆ๋‹ค.

์ฃผ์š” ๊ฐœ๋…

LangGraph์—์„œ์˜ Map ๋‹จ๊ณ„ ๊ตฌํ˜„

  • ์˜ˆ์ œ์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ˆœ์„œ๋กœ Map ๋‹จ๊ณ„๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:

    1. ์ฃผ์ œ ์ƒ์„ฑ:

      • ์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ ์ฃผ์ œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ LangGraph๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ด€๋ จ ์ฃผ์ œ(subject)๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
      • ์ด ๊ณผ์ •์€ LangGraph์˜ send API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—ฌ๋Ÿฌ ๊ฐœ์˜ generate joke ๋…ธ๋“œ๋กœ ํ™•์žฅ๋ฉ๋‹ˆ๋‹ค.
      • ์ด๋•Œ ๊ฐ ์ฃผ์ œ๋ณ„๋กœ LangGraph๊ฐ€ ๋ณ„๋„์˜ ๋…ธ๋“œ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜์—ฌ ๋ณ‘๋ ฌ์ ์œผ๋กœ joke์„ ์ƒ์„ฑํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
    2. joke ์ƒ์„ฑ:

      • ๊ฐ ์ฃผ์ œ์— ๋Œ€ํ•ด LangGraph๋Š” generate joke ๋…ธ๋“œ์—์„œ ํ•ด๋‹น ์ฃผ์ œ์— ๋งž๋Š” joke์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
      • ์ด ๋…ธ๋“œ๋Š” ์ž…๋ ฅ์œผ๋กœ ์ฃผ์ œ๋ฅผ ๋ฐ›๊ณ , ํ•ด๋‹น ์ฃผ์ œ์— ๋งž๋Š” joke์„ ์ƒ์„ฑํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ jokes ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
      • LangGraph์˜ add reducer ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ์ƒ์„ฑ๋œ joke๋“ค์ด ํ•˜๋‚˜์˜ jokes ๋ฆฌ์ŠคํŠธ๋กœ ํ†ตํ•ฉ๋ฉ๋‹ˆ๋‹ค.

LangGraph์—์„œ์˜ Reduce ๋‹จ๊ณ„ ๊ตฌํ˜„

  • Reduce ๋‹จ๊ณ„์—์„œ๋Š” ์ƒ์„ฑ๋œ ๋ชจ๋“  joke ์ค‘์—์„œ ๊ฐ€์žฅ ์šฐ์ˆ˜ํ•œ ๊ฒƒ์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์š” ๊ณผ์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    1. ์ตœ๊ณ ์˜ joke ์„ ํƒ:
      • ๋ชจ๋“  joke์ด jokes ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€๋˜๋ฉด, LangGraph๋Š” ํ•ด๋‹น ๋ฆฌ์ŠคํŠธ๋ฅผ ํ•œ ๋ฌธ์ž์—ด๋กœ ํ•ฉ์ณ best joke prompt๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
      • ๋ชจ๋ธ์€ ์ „์ฒด joke ๋ฆฌ์ŠคํŠธ๋ฅผ ํ‰๊ฐ€ํ•˜๊ณ , ๊ฐ€์žฅ ์žฌ๋ฏธ์žˆ๋Š” joke์˜ ์ธ๋ฑ์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
      • ๋ฐ˜ํ™˜๋œ ์ธ๋ฑ์Šค๋Š” best joke๋กœ์„œ ์ตœ์ข… ๊ฒฐ๊ณผ์— ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ ์˜ˆ์‹œ

  1. LLM ๋ฐ State ์ •์˜
    • ๋†๋‹ด ์ƒ์„ฑ ๋ฐ ์„ ํƒ์„ ์œ„ํ•œ LLM์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
    • ๊ทธ๋ž˜ํ”„์˜ ์ง„์ž…์ ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ง„์ž…์ ์€:
      • ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ฃผ์ œ(topic)๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค
      • ๊ทธ๋กœ๋ถ€ํ„ฐ ๋†๋‹ด ์ฃผ์ œ ๋ชฉ๋ก(subject)์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค
      • ๊ฐ ๋†๋‹ด ์ฃผ์ œ๋ฅผ ์œ„์˜ ๋†๋‹ด ์ƒ์„ฑ ๋…ธ๋“œ๋กœ ๋ณด๋ƒ…๋‹ˆ๋‹ค
    • ์ด๋•Œ ์ƒ์„ฑ๋œ ๋†๋‹ด์„ ๋ฆฌ๋“€์„œ๋กœ ์ €์žฅํ•ด๋‘˜ jokes ํ‚ค ์ •์˜
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from langchain_openai import ChatOpenAI

# ์‚ฌ์šฉํ•  ํ”„๋กฌํ”„ํŠธ
# subjects_prompt = """๋‹ค์Œ ์ „์ฒด ์ฃผ์ œ์™€ ๊ด€๋ จ๋œ 3๊ฐœ์˜ sub-topics ๋ชฉ๋ก์„ ์ƒ์„ฑํ•˜์„ธ์š”: {topic}."""
# joke_prompt = """{subject}์— ๋Œ€ํ•œ ๋†๋‹ด์„ ์ƒ์„ฑํ•˜์„ธ์š”"""
# best_joke_prompt = """์•„๋ž˜๋Š” {topic}์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ ๋†๋‹ด์ž…๋‹ˆ๋‹ค. ๊ฐ€์žฅ ์ข‹์€ ๊ฒƒ์„ ์„ ํƒํ•˜์„ธ์š”! ๊ฐ€์žฅ ์ข‹์€ ๋†๋‹ด์˜ ID๋ฅผ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”, ์ฒซ ๋ฒˆ์งธ ๋†๋‹ด์˜ ID๋Š” 0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๋†๋‹ด๋“ค: \n\n {jokes}"""

subjects_prompt = """Generate a list of 3 sub-topics that are all related to this overall topic: {topic}."""
joke_prompt = """Generate a joke about {subject}"""
best_joke_prompt = """Below are a bunch of jokes about {topic}. Select the best one! Return the ID of the best one, starting 0 as the ID for the first joke. Jokes: \n\n  {jokes}"""

# LLM
model = ChatOpenAI(model="gpt-4", temperature=0)
joke_model = ChatOpenAI(model="gpt-4", temperature=1.0)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import operator
from typing import Annotated
from typing_extensions import TypedDict
from pydantic import BaseModel

class Subjects(BaseModel):
    subjects: list[str]

class BestJoke(BaseModel):
    id: int
    
class OverallState(TypedDict):
    topic: str
    subjects: list
    jokes: Annotated[list, operator.add]
    best_selected_joke: str

  • ์ฝ”๋“œ ์„ค๋ช…

    • Subjects: ํ•˜์œ„ ์ฃผ์ œ๋“ค์„ ํฌํ•จํ•˜๋Š” ๊ตฌ์กฐ์ฒด๋กœ, ๊ฐ ์ฃผ์ œ๋ณ„๋กœ joke๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด Map ๋‹จ๊ณ„์—์„œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
    • BestJoke: ๊ฐ€์žฅ ์žฌ๋ฏธ์žˆ๋Š” joke์˜ ์ธ๋ฑ์Šค๋ฅผ ์ €์žฅํ•˜์—ฌ, Reduce ๋‹จ๊ณ„์—์„œ ์ตœ์ƒ์˜ joke๋ฅผ ์„ ํƒํ•  ๋•Œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
    • OverallState: ์ „์ฒด MapReduce ๊ณผ์ •์˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ตฌ์กฐ์ฒด๋กœ, ์ฃผ์–ด์ง„ ์ฃผ์ œ์—์„œ ํ•˜์œ„ ์ฃผ์ œ, ์ƒ์„ฑ๋œ joke ๋ฆฌ์ŠคํŠธ, ์ตœ์ข… ์„ ํƒ๋œ joke๊นŒ์ง€์˜ ๋ชจ๋“  ์ƒํƒœ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  1. ์ฃผ์ œ์— ๋Œ€ํ•œ ํ•˜์œ„ ์ž‘์—… ์ƒ์„ฑ (Map ์ด์ „ ๋‹จ๊ณ„) - generate_topics ํ•จ์ˆ˜

  • ์—ญํ• : generate_topics ํ•จ์ˆ˜๋Š” ์ฃผ์–ด์ง„ ์ฃผ์ œ(topic)์— ๋Œ€ํ•ด ๊ด€๋ จ ์žˆ๋Š” ํ•˜์œ„ ์ฃผ์ œ ๋ชฉ๋ก์„ ์ƒ์„ฑํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

    1
    2
    3
    4
    5
    
    def generate_topics(state: OverallState):
        prompt = subjects_prompt.format(topic=state["topic"])
        response = model.with_structured_output(Subjects).invoke(prompt)
        return {"subjects": response.subjects}
      
    
    • ๋™์ž‘:

      • subjects_prompt๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ ์ฃผ์š” ์ฃผ์ œ(topic)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ LangChain ๋ชจ๋ธ์— ์ „๋‹ฌํ•  ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
      • model.with_structured_output(Subjects).invoke(prompt)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ LangChain ๋ชจ๋ธ์—์„œ Subjects ์Šคํ‚ค๋งˆ์— ๋งž๋Š” ์‘๋‹ต(๋ฆฌ์ŠคํŠธ, ์•„๋ž˜ ์ฐธ๊ณ )์„ ์ƒ์„ฑํ•˜๋„๋ก ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ```

      class Subjects(BaseModel): subjects: list[str] ```

      • ์ด๋ ‡๊ฒŒ ํ•จ์œผ๋กœ์จ ๋ชจ๋ธ์˜ ์‘๋‹ต์€ Subjects ์Šคํ‚ค๋งˆ์— ์ •์˜๋œ ํ˜•ํƒœ(subjects: list[str])๋ฅผ ๊ฐ€์ง€๋ฉฐ, ์ •ํ™•ํ•œ ๋ฆฌ์ŠคํŠธ ํ˜•์‹์œผ๋กœ ์ฃผ์ œ๋“ค์ด ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.
      • ๋ฐ˜ํ™˜๋œ ์ฃผ์ œ ๋ชฉ๋ก์€ subjects๋กœ ์ €์žฅ๋˜์–ด ์ดํ›„ Map ๋‹จ๊ณ„์—์„œ ๊ฐ ์ฃผ์ œ์— ๋Œ€ํ•œ joke ์ƒ์„ฑ ์ž‘์—…์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
    • ์ƒํƒœ: ํ˜„์žฌ ์ƒํƒœ(state)๋Š” subjects: list[str]์˜ ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. langgraph์˜ Send API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ continue_to_jokes ํ•จ์ˆ˜์— ๊ฐ๊ฐ์˜ subjects ๋ฆฌ์ŠคํŠธ ์›์†Œ๋“ค์„ ๋„˜๊ฒจ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • Send์™€ continue_to_jokes ํ•จ์ˆ˜

    1
    2
    3
    4
    5
    
    from langgraph.constants import Send
      
    def continue_to_jokes(state: OverallState):
        return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
      
    

    • Send๋Š” ๊ฐ ์ฃผ์ œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ generate_joke ๋…ธ๋“œ๋กœ ์ž‘์—…์„ ์ „๋‹ฌํ•˜์—ฌ ๋ณ‘๋ ฌ์ ์œผ๋กœ joke๋ฅผ ์ƒ์„ฑํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
    • continue_to_jokes๋Š” generate_joke ๋…ธ๋“œ๋กœ์˜ ์ „์†ก์„ ์„ค์ •ํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.
    • ๋™์ž‘:

      • state["subjects"] ๋ฆฌ์ŠคํŠธ์—์„œ ๊ฐ ์ฃผ์ œ s์— ๋Œ€ํ•ด Send ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
      • Send("generate_joke", {"subject": s})๋Š” generate_joke ๋…ธ๋“œ์— { "subject": s } ํ˜•ํƒœ๋กœ ์ƒํƒœ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
        • ์ด ์ „๋‹ฌ๋˜๋Š” ์ƒํƒœ๊ฐ€ OverallState์™€ ์ผ์น˜ํ•  ํ•„์š”๋Š” ์—†์œผ๋ฉฐ, generate_joke ๋…ธ๋“œ๋Š” ์ž์‹ ์˜ ์ƒํƒœ์— ๋งž๊ฒŒ ์ด ์ •๋ณด๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
      • ๊ฒฐ๊ณผ์ ์œผ๋กœ Send๋ฅผ ํ†ตํ•ด ๊ฐ ์ฃผ์ œ๋ฅผ ๋ณ‘๋ ฌ๋กœ generate_joke์— ์ „๋‹ฌํ•˜์—ฌ ๊ฐ ์ฃผ์ œ์— ๋Œ€ํ•œ joke๋ฅผ ์ƒ์„ฑํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
  1. ํ•˜์œ„ ์ฃผ์ œ์— ๋Œ€ํ•œ ๋†๋‹ด ์ƒ์„ฑ (Map ๋‹จ๊ณ„) - generate_joke ํ•จ์ˆ˜

    • ์—ญํ• : generate_joke๋Š” ๊ฐ ์ฃผ์ œ์— ๋Œ€ํ•ด joke๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ OverallState์˜ jokes ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      class JokeState(TypedDict):
          subject: str
           
      class Joke(BaseModel):
          joke: str
           
      def generate_joke(state: JokeState):
          prompt = joke_prompt.format(subject=state["subject"])
          response = model.with_structured_output(Joke).invoke(prompt)
          return {"jokes": [response.joke]}
           
      
    • ๋™์ž‘:

      • state["subject"] ๊ฐ’์„ ์ด์šฉํ•ด joke_prompt๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ์ด๋ฅผ ํ”„๋กฌํ”„ํŠธ๋กœ ์‚ฌ์šฉํ•˜์—ฌ LangChain ๋ชจ๋ธ์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
      • model.with_structured_output(Joke).invoke(prompt)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Joke ์Šคํ‚ค๋งˆ์— ๋งž๋Š” ์‘๋‹ต์„ ์ƒ์„ฑํ•˜๋„๋ก ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

        • ๋ชจ๋ธ์˜ ์‘๋‹ต์€ Joke ์Šคํ‚ค๋งˆ์— ์ •์˜๋œ ํ˜•ํƒœ(joke: str)๋กœ ๋ฐ˜ํ™˜๋˜๋ฉฐ, ์ •ํ™•ํ•œ ๋ฌธ์ž์—ด ํ˜•์‹์˜ joke๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
      • ๋ฐ˜ํ™˜๋œ joke๋Š” ๋ฆฌ์ŠคํŠธ๋กœ ๊ฐ์‹ธ์ ธ { "jokes": [response.joke] } ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.

        • ์ด๋Š” OverallState์˜ jokes ํ‚ค์— ์ถ”๊ฐ€๋˜๋ฉฐ, ์—ฌ๊ธฐ์„œ jokes ํ‚ค๋Š” add reducer๋ฅผ ํ†ตํ•ด ๋ฆฌ์ŠคํŠธ๋กœ ์ž๋™ ์ง‘๊ณ„๋ฉ๋‹ˆ๋‹ค.
  2. ์ตœ๊ณ ์˜ ๋†๋‹ด ์„ ํƒ (Reduce ๋‹จ๊ณ„) - best_joke ํ•จ์ˆ˜

    • ์—ญํ• : best_joke ํ•จ์ˆ˜๋Š” OverallState์— ์žˆ๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ joke ์ค‘์—์„œ ๊ฐ€์žฅ ์žฌ๋ฏธ์žˆ๋Š” joke๋ฅผ ์„ ํƒํ•˜๋Š” ๊ธฐ๋Šฅ์„ ํ•ฉ๋‹ˆ๋‹ค.

      1
      2
      3
      4
      5
      6
      
      # Reduce ๋‹จ๊ณ„: ๊ฐ€์žฅ ์žฌ๋ฏธ์žˆ๋Š” ๋†๋‹ด ์„ ํƒ
      def best_joke(state: OverallState):
          jokes = "\n\n".join(state["jokes"])
          prompt = best_joke_prompt.format(topic=state["topic"], jokes=jokes)
          response = model.with_structured_output(BestJoke).invoke(prompt)
          return {"best_selected_joke": state["jokes"][response.id]}
      
    • ๋™์ž‘:

      • state["jokes"] ๋ฆฌ์ŠคํŠธ์— ์ €์žฅ๋œ ๋ชจ๋“  joke๋ฅผ \n\n๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ํ•˜๋‚˜์˜ ๋ฌธ์ž์—ด๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค.
      • best_joke_prompt๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฃผ์–ด์ง„ topic๊ณผ ๊ฒฐํ•ฉ๋œ joke ๋ฆฌ์ŠคํŠธ๋ฅผ LangChain ๋ชจ๋ธ์˜ ํ”„๋กฌํ”„ํŠธ๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
      • with_structured_output(BestJoke).invoke(prompt)๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ LangChain ๋ชจ๋ธ์ด BestJoke ์Šคํ‚ค๋งˆ์— ๋งž๋Š” ์‘๋‹ต์„ ์ƒ์„ฑํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
        • ์ด ๋ชจ๋ธ์€ ๊ฐ€์žฅ ์žฌ๋ฏธ์žˆ๋Š” joke์˜ ์ธ๋ฑ์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ, response.id์— ํ•ด๋‹น ์ธ๋ฑ์Šค๊ฐ€ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
      • ์ตœ์ข…์ ์œผ๋กœ state["jokes"][response.id]์— ํ•ด๋‹นํ•˜๋Š” joke๋ฅผ best_selected_joke ํ‚ค์— ์ €์žฅํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  3. StateGraph ๊ตฌ์„ฑ ๋ฐ ์ปดํŒŒ์ผ ๋‹จ๊ณ„

    • ์—ญํ• : StateGraph๋Š” ์ „์ฒด MapReduce ํ๋ฆ„์„ ํ•œ ๊ทธ๋ž˜ํ”„ ๋‚ด์—์„œ ๊ตฌ์„ฑํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      
      from IPython.display import Image
      from langgraph.graph import END, StateGraph, START
           
      # ๊ทธ๋ž˜ํ”„ ๊ตฌ์„ฑ
      graph = StateGraph(OverallState)
      graph.add_node("generate_topics", generate_topics)
      graph.add_node("generate_joke", generate_joke)
      graph.add_node("best_joke", best_joke)
      graph.add_edge(START, "generate_topics")
      graph.add_conditional_edges("generate_topics", continue_to_jokes, ["generate_joke"])
      graph.add_edge("generate_joke", "best_joke")
      graph.add_edge("best_joke", END)
      
    • ๋™์ž‘:

      • OverallState๋ฅผ ์ƒํƒœ๋กœ ์‚ฌ์šฉํ•˜๋Š” StateGraph ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
      • generate_topics, generate_joke, best_joke ๋…ธ๋“œ๋ฅผ ๊ฐ๊ฐ ๊ทธ๋ž˜ํ”„์— ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฐ ์ž‘์—… ๋‹จ๊ณ„๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
      • ์‹œ์ž‘์ (START)์—์„œ generate_topics ๋…ธ๋“œ๋กœ ์ด๋™ํ•˜๋„๋ก add_edge ๋ฉ”์„œ๋“œ๋กœ ์—ฃ์ง€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
      • generate_topics ๋‹จ๊ณ„ ์ดํ›„ ๊ฐ ์ฃผ์ œ์— ๋Œ€ํ•ด ์กฐ๊ฑด๋ถ€ ์—ฃ์ง€(add_conditional_edges)๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ generate_joke๋กœ ์—ฐ๊ฒฐ๋˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
      • ๊ฐ generate_joke ์ž‘์—… ์ดํ›„ best_joke ๋‹จ๊ณ„๋กœ ์—ฐ๊ฒฐ๋˜๋ฉฐ, best_joke์—์„œ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์ข…๋ฃŒ์ (END)์œผ๋กœ ์—ฐ๊ฒฐ๋ฉ๋‹ˆ๋‹ค.

์‹คํ–‰ ์˜ˆ์ œ

  • ์•„๋ž˜์™€ ๊ฐ™์ด science๋ผ๋Š” topic์„ ์ฃผ๋ฉด, LLM์ด ์œ ์‚ฌ ์ฃผ์ œ 3๊ฐœ๋ฅผ ๋งŒ๋“ค๊ณ , ์ด์™€ ๊ด€๋ จ๋œ joke ์ƒ์„ฑ ํ›„ best joke๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
1
2
3
# ๊ทธ๋ž˜ํ”„ ํ˜ธ์ถœ: ์—ฌ๊ธฐ์„œ ๋†๋‹ด ๋ชฉ๋ก์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค
for s in app.stream({"topic": "science"}):
    print(s)
1
2
3
4
5
6
7
8
9
10
11
12
Generated Subjects: ['Physics', 'Biology', 'Chemistry']

{'generate_topics': {'subjects': ['Physics', 'Biology', 'Chemistry']}}

{'generate_joke': {
         'jokes': ["Why did the biologist look forward to casual Fridays? Because they're allowed to wear genes to work!"]}}
{'generate_joke': {
         'jokes': ["Why can't you trust an atom? Because they make up everything!"]}}
{'generate_joke': {
         'jokes': ["Why do chemists like nitrates so much? Because they're cheaper than day rates!"]}}
         
{'best_joke': {'best_selected_joke': "Why can't you trust an atom? Because they make up everything!"}}


Lesson 4: Research Assistant

๊ฐœ์š”

Research Assistant ์‹œ์Šคํ…œ์€ ์—ฌ๋Ÿฌ AI ์—์ด์ „ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—ฐ๊ตฌ ์ž‘์—…์„ ์ž๋™ํ™”ํ•˜๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. ์ฃผ์ œ์— ๋”ฐ๋ผ ๊ฐ ์—์ด์ „ํŠธ๊ฐ€ ํ•˜์œ„ ์ฃผ์ œ๋ฅผ ๋‹ด๋‹นํ•˜๊ณ , ์™ธ๋ถ€ ์†Œ์Šค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์—ฐ๊ตฌ๋ฅผ ์ˆ˜ํ–‰ํ•œ ํ›„ ๊ฒฐ๊ณผ๋ฅผ ๋ณ‘ํ•ฉํ•˜์—ฌ ์ตœ์ข… ๋ณด๊ณ ์„œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์š” ๊ฐœ๋…

์ด๋ฒˆ ์ฑ•ํ„ฐ์˜ ๋ชฉํ‘œ๋Š” ์ฑ„ํŒ… ๋ชจ๋ธ์„ ์ค‘์‹ฌ์œผ๋กœ ๊ฒฝ๋Ÿ‰ ๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜์—ฌ ์—ฐ๊ตฌ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ตœ์ ํ™”ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํ•ด๋‹น ๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ ์‹œ์Šคํ…œ์€ ์•„๋ž˜ ๋ชจ๋“ˆ๋“ค๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์†Œ์Šค ์„ ํƒ

    • ์‚ฌ์šฉ์ž๋Š” ์—ฐ๊ตฌ์— ์‚ฌ์šฉํ•  ์ž…๋ ฅ ์†Œ์Šค ์„ธํŠธ๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ณ„ํš

    • ์‚ฌ์šฉ์ž๊ฐ€ ์ฃผ์ œ๋ฅผ ์ œ๊ณตํ•˜๋ฉด ์‹œ์Šคํ…œ์€ AI ๋ถ„์„๊ฐ€ ํŒ€์„ ์ƒ์„ฑํ•˜๊ณ , ๊ฐ ๋ถ„์„๊ฐ€๋Š” ํ•˜๋‚˜์˜ ํ•˜์œ„ ์ฃผ์ œ์— ์ง‘์ค‘ํ•ฉ๋‹ˆ๋‹ค.
    • ์—ฐ๊ตฌ ์‹œ์ž‘ ์ „์— ์ด๋Ÿฌํ•œ ํ•˜์œ„ ์ฃผ์ œ๋ฅผ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด Human-in-the-loop๊ฐ€ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • LLM ํ™œ์šฉ

    • ๊ฐ ๋ถ„์„๊ฐ€๋Š” ์„ ํƒ๋œ ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ „๋ฌธ๊ฐ€ AI์™€ ์‹ฌ์ธต ์ธํ„ฐ๋ทฐ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
    • ์ธํ„ฐ๋ทฐ๋Š” STORM ๋…ผ๋ฌธ์—์„œ ๋ณด์—ฌ์ง„ ๊ฒƒ์ฒ˜๋Ÿผ ์ƒ์„ธํ•œ ํ†ต์ฐฐ์„ ์ถ”์ถœํ•˜๊ธฐ ์œ„ํ•œ ๋‹ค์ค‘ ํ„ด ๋Œ€ํ™”๊ฐ€ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
    • ์ด๋Ÿฌํ•œ ์ธํ„ฐ๋ทฐ๋Š” ๋‚ด๋ถ€ ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ sub-graphs๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์บก์ฒ˜๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
  • ์—ฐ๊ตฌ ํ”„๋กœ์„ธ์Šค

    • ์ „๋ฌธ๊ฐ€๋“ค์€ ๋ถ„์„๊ฐ€์˜ ์งˆ๋ฌธ์— ๋‹ตํ•˜๊ธฐ ์œ„ํ•ด parallel๋กœ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค.
    • ๋ชจ๋“  ์ธํ„ฐ๋ทฐ๋Š” map-reduce๋ฅผ ํ†ตํ•ด ๋™์‹œ์— ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค.
  • ์ถœ๋ ฅ ํ˜•์‹

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

๐ŸŒช๏ธ โ€œ์œ„ํ‚คํ”ผ๋””์•„์™€ ์œ ์‚ฌํ•œ ๊ธ€ ์ž‘์„ฑ์„ ์œ„ํ•œ ๋Œ€ํ˜• ์–ธ์–ด ๋ชจ๋ธ ์ง€์› ์‹œ์Šคํ…œ, STORMโ€

  • ๋…ผ๋ฌธ ๋งํฌ : https://arxiv.org/pdf/2402.14207
  • ํ•ด๋‹น ๋…ผ๋ฌธ์€ ๋Œ€ํ˜• ์–ธ์–ด ๋ชจ๋ธ(LLM)์„ ํ™œ์šฉํ•˜์—ฌ ์œ„ํ‚คํ”ผ๋””์•„ ์ˆ˜์ค€์˜ ์žฅ๋ฌธ ๊ธ€ ์ž‘์„ฑ์„ ์ง€์›ํ•˜๋Š” STORM ์‹œ์Šคํ…œ์„ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

  • STORM์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ ์ฃผ์ œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์œ„ํ‚คํ”ผ๋””์•„ ์Šคํƒ€์ผ์˜ ์•„ํ‹ฐํด์„ ์ƒ์„ฑํ•˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ์‹œ์Šคํ…œ์€ ๋ณด๋‹ค ์กฐ์ง์ ์ด๊ณ  ํฌ๊ด„์ ์ธ ์•„ํ‹ฐํด์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ๋‹ค์Œ ๋‘ ๊ฐ€์ง€ ์ฃผ์š” ์ธ์‚ฌ์ดํŠธ๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค:
    • ์œ ์‚ฌํ•œ ์ฃผ์ œ์— ๋Œ€ํ•œ ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ๊ฐœ์š”๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ด ์ฃผ์ œ์˜ ํฌ๊ด„์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.
    • ๋‹ค์–‘ํ•œ ๊ด€์ ์„ ๋ฐ˜์˜ํ•˜๊ณ  ๊ฒ€์ƒ‰์— ๊ธฐ๋ฐ˜ํ•œ ๋Œ€ํ™” ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ํ†ตํ•ด ์ฐธ์กฐ ์ž๋ฃŒ์˜ ์ˆ˜์™€ ์ •๋ณด ๋ฐ€๋„๋ฅผ ๋†’์ž…๋‹ˆ๋‹ค.
  • ์œ„ ๊ทธ๋ฆผ์€ ๋…ผ๋ฌธ์—์„œ ์ œ์‹œํ•œ flow diagram์ด๋ฉฐ, ์•„๋ž˜ ๊ทธ๋ฆผ์€ langchain ์ธก์—์„œ ์ข€ ๋” ๋””ํ…Œ์ผํ•˜๊ฒŒ ๊ทธ๋ฆฐ flow diagram์ž…๋‹ˆ๋‹ค.
  • ์ž๋ฃŒ ๋งํฌ : https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/storm/storm.ipynb

๐ŸŒช๏ธ STORM ๋…ผ๋ฌธ์—์„œ ์ˆ˜ํ–‰ํ•˜๋Š” ๋‹ค์ค‘ ํ„ด ๋Œ€ํ™”(multi-turn conversation)๋Š” ์œ„ํ‚คํ”ผ๋””์•„ ์ž‘์„ฑ์ž์™€ ์ฃผ์ œ ์ „๋ฌธ๊ฐ€ ๊ฐ„์˜ ๊ฐ€์ƒ ๋Œ€ํ™”๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜์—ฌ ์ •๋ณด๋ฅผ ์–ป๋Š” ๊ณผ์ •์ž…๋‹ˆ๋‹ค.

  • ์ด ๊ณผ์ •์—์„œ STORM์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹จ๊ณ„๋ฅผ ๊ฑฐ์นฉ๋‹ˆ๋‹ค:
    1. ์งˆ๋ฌธ ์ƒ์„ฑ: ํŠน์ • ๊ด€์ ์„ ๊ฐ€์ง„ ์œ„ํ‚คํ”ผ๋””์•„ ์ž‘์„ฑ์ž๊ฐ€ ์ฃผ์ œ์— ๋Œ€ํ•ด ์งˆ๋ฌธ์„ ๋˜์ง‘๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, โ€œ2022๋…„ ๋™๊ณ„ ์˜ฌ๋ฆผํ”ฝ ๊ฐœ๋ง‰์‹์˜ ๊ตํ†ต ์ค€๋น„๋Š” ์–ด๋–ป๊ฒŒ ๋˜์—ˆ๋Š”๊ฐ€?โ€์™€ ๊ฐ™์€ ์„ธ๋ถ€์ ์ธ ์งˆ๋ฌธ์ด ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋‹จ์ˆœํ•œ โ€œ๋ฌด์—‡โ€์ด๋‚˜ โ€œ์–ธ์ œโ€์™€ ๊ฐ™์€ ํ‘œ๋ฉด์ ์ธ ์งˆ๋ฌธ์—์„œ ๋ฒ—์–ด๋‚˜, ์ฃผ์ œ์— ๋Œ€ํ•œ ์‹ฌ์ธต์ ์ธ ํƒ๊ตฌ๋ฅผ ์œ ๋„ํ•ฉ๋‹ˆ๋‹ค.
    2. ์งˆ๋ฌธ ์„ธ๋ถ„ํ™” ๋ฐ ๊ฒ€์ƒ‰: ๊ฐ ์งˆ๋ฌธ์€ ์„ธ๋ถ€ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋กœ ๋ถ„ํ• ๋˜๊ณ , ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์ธํ„ฐ๋„ท ์ถœ์ฒ˜๋ฅผ ์‚ฌ์šฉํ•ด ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋‹จ๊ณ„์—์„œ๋Š” ์œ„ํ‚คํ”ผ๋””์•„์˜ ์‹ ๋ขฐ๋„ ๊ธฐ์ค€์— ๋งž๋Š” ์ถœ์ฒ˜๋งŒ ์„ ํƒํ•˜์—ฌ, ์ •ํ™•ํ•œ ์ •๋ณด๋งŒ์ด ๋‹ต๋ณ€์— ๋ฐ˜์˜๋˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
    3. ๋Œ€๋‹ต ์ƒ์„ฑ ๋ฐ ๋Œ€ํ™” ๋ฐ˜๋ณต: ์ˆ˜์ง‘๋œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ฐ€์ƒ์˜ ์ „๋ฌธ๊ฐ€๊ฐ€ ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜๊ณ , ์ด ๋‹ต๋ณ€์„ ๋ฐ”ํƒ•์œผ๋กœ ์ž‘์„ฑ์ž๊ฐ€ ์ƒˆ๋กœ์šด ํ›„์† ์งˆ๋ฌธ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ฒซ ๋ฒˆ์งธ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์—์„œ ์ถ”๊ฐ€์ ์ธ ๊ตํ†ต ํŽธ์˜์— ๊ด€ํ•œ ์–ธ๊ธ‰์ด ์žˆ๋‹ค๋ฉด, ์ž‘์„ฑ์ž๋Š” ์ด์™€ ๊ด€๋ จ๋œ ์ƒˆ๋กœ์šด ์งˆ๋ฌธ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    4. ์ž๋ฃŒ ์ถ•์  ๋ฐ ๊ตฌ์กฐ ํ˜•์„ฑ: ์—ฌ๋Ÿฌ ๋ผ์šด๋“œ์— ๊ฑธ์นœ ๋Œ€ํ™”๊ฐ€ ์ง„ํ–‰๋˜๋ฉด์„œ, STORM์€ ์ฃผ์ œ์™€ ๊ด€๋ จ๋œ ๊นŠ์ด ์žˆ๋Š” ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์–ป์–ด์ง„ ์ •๋ณด๋Š” ๋‚˜์ค‘์— ์ „์ฒด ๋ฌธ์„œ์˜ ๊ฐœ์š”๋ฅผ ์ •๋ฆฌํ•˜๋Š” ๋ฐ ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ ์˜ˆ์‹œ

โœ”๏ธ Generate Analysts: Human-In-The-Loop

Generate Analysts ์„น์…˜์—์„œ๋Š” LangGraph์˜ Human-in-the-loop๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—ฐ๊ตฌ ์ฃผ์ œ์— ๋งž๋Š” AI ๋ถ„์„๊ฐ€๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ธ๊ฐ„์˜ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ˜์˜ํ•˜์—ฌ ๋ถ„์„๊ฐ€๋ฅผ ๊ฒ€ํ† ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

  • ์ด๋ฅผ ํ†ตํ•ด ์—ฐ๊ตฌ ์ฃผ์ œ๋ฅผ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•œ ์ตœ์ ์˜ ๋ถ„์„๊ฐ€ ํŒ€์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

1. ๊ธฐ๋ณธ ์ฝ”๋“œ ๊ตฌ์„ฑ

๋จผ์ € ํ•„์š”ํ•œ ๋ชจ๋“ˆ๊ณผ ๊ธฐ๋ณธ ํด๋ž˜์Šค ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๋ถ„์„๊ฐ€๋Š” ์—ฐ๊ตฌ ์ฃผ์ œ์™€ ์—ญํ• ์— ๋”ฐ๋ผ ๊ฐ๊ธฐ ๋‹ค๋ฅธ ํŽ˜๋ฅด์†Œ๋‚˜๋ฅผ ๊ฐ€์ง€๋ฉฐ, ์ด ํŽ˜๋ฅด์†Œ๋‚˜๋Š” ์ฃผ์ œ์— ๋งž๋Š” ์งˆ๋ฌธ๊ณผ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฐ ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค.

1) Analyst ํด๋ž˜์Šค ์Šคํ‚ค๋งˆ ์ •์˜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from typing import List
from pydantic import BaseModel, Field

class Analyst(BaseModel):
    affiliation: str = Field(
        description="๋ถ„์„๊ฐ€์˜ ์ฃผ์š” ์†Œ์†.",
    )
    name: str = Field(
        description="๋ถ„์„๊ฐ€์˜ ์ด๋ฆ„."
    )
    role: str = Field(
        description="์ฃผ์ œ์™€ ๊ด€๋ จ๋œ ๋ถ„์„๊ฐ€์˜ ์—ญํ• .",
    )
    description: str = Field(
        description="๋ถ„์„๊ฐ€์˜ ์ดˆ์ , ๊ด€์‹ฌ์‚ฌ, ๋™๊ธฐ์— ๋Œ€ํ•œ ์„ค๋ช….",
    )
    
    # ํŽ˜๋ฅด์†Œ๋‚˜ ์†์„ฑ: ๋ถ„์„๊ฐ€์˜ ๋ฐฐ๊ฒฝ ๋ฐ ์—ญํ• ์„ ์š”์•ฝํ•œ ํ…์ŠคํŠธ
    @property
    def persona(self) -> str:
        return f"์ด๋ฆ„: {self.name}\n์—ญํ• : {self.role}\n์†Œ์†: {self.affiliation}\n์„ค๋ช…: {self.description}\n"
  • Analyst ํด๋ž˜์Šค: ๋ถ„์„๊ฐ€์— ๋Œ€ํ•œ ์ฃผ์š” ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ์œผ๋ฉฐ, affiliation, name, role, description ์†์„ฑ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
    • affiliation: ๋ถ„์„๊ฐ€์˜ ์ฃผ์š” ์†Œ์†์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค.
    • name: ๋ถ„์„๊ฐ€์˜ ์ด๋ฆ„์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค.
    • role: ์ฃผ์ œ์™€ ๊ด€๋ จ๋œ ๋ถ„์„๊ฐ€์˜ ์—ญํ• ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค.
    • description: ๋ถ„์„๊ฐ€์˜ ์ดˆ์ , ๊ด€์‹ฌ์‚ฌ, ๋™๊ธฐ์— ๋Œ€ํ•œ ์„ค๋ช…์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค.
  • persona ์†์„ฑ: ๋ถ„์„๊ฐ€์˜ ๋ฐฐ๊ฒฝ์„ ์š”์•ฝํ•˜์—ฌ ํŽ˜๋ฅด์†Œ๋‚˜๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
    • ์ด๋ฅผ ํ†ตํ•ด ๋ถ„์„๊ฐ€๊ฐ€ ์ˆ˜ํ–‰ํ•  ์—ญํ• ์— ๋งž๋Š” ์บ๋ฆญํ„ฐ๋ฅผ ๊ฐ–์ถœ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • persona๋Š” @property ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•œ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋Š” ๋ถ„์„๊ฐ€์˜ ์ •๋ณด๋ฅผ ์š”์•ฝ๋œ ๋ฌธ์ž์—ด ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ, ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์†์„ฑ์ฒ˜๋Ÿผ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค (์˜ˆ: analyst.persona).

2) Perspectives ํด๋ž˜์Šค ์ •์˜

1
2
3
4
class Perspectives(BaseModel):
    analysts: List[Analyst] = Field(
        description="๋ถ„์„๊ฐ€๋“ค์˜ ์—ญํ• ๊ณผ ์†Œ์†์„ ํฌํ•จํ•œ ์ข…ํ•ฉ์ ์ธ ๋ชฉ๋ก.",
    )
  • Perspectives ํด๋ž˜์Šค: ๋ถ„์„๊ฐ€ ์ง‘ํ•ฉ ๋ฆฌ์ŠคํŠธ (List[Analyst])

3) GenerateAnalystsState ํด๋ž˜์Šค ์ •์˜

1
2
3
4
5
class GenerateAnalystsState(TypedDict):
    topic: str  # ์—ฐ๊ตฌ ์ฃผ์ œ
    max_analysts: int  # ์ƒ์„ฑํ•  ๋ถ„์„๊ฐ€ ์ˆ˜
    human_analyst_feedback: str  # ์ธ๊ฐ„ ํ”ผ๋“œ๋ฐฑ
    analysts: List[Analyst]  # ๋ถ„์„๊ฐ€ ๋ชฉ๋ก
  • GenerateAnalystsState ํด๋ž˜์Šค: ์ฃผ์ œ์™€ ๋ถ„์„๊ฐ€ ์ˆ˜, ์ธ๊ฐ„ ํ”ผ๋“œ๋ฐฑ ๋“ฑ์„ ํฌํ•จํ•˜์—ฌ ์—ฐ๊ตฌ ์ฃผ์ œ์— ๋งž๋Š” ๋ถ„์„๊ฐ€ ํŒ€์„ ์„ค์ •ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
    • GenerateAnalystsState ํด๋ž˜์Šค๋Š” ์ฃผ์ œ์™€ ๋ถ„์„๊ฐ€ ํŒ€์„ ์ƒ์„ฑํ•˜๊ณ  ์ธ๊ฐ„์˜ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ˜์˜ํ•˜์—ฌ ์ฃผ์–ด์ง„ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

4) ๋ถ„์„๊ฐ€ ์ƒ์„ฑ ์ง€์นจ (Analyst Instructions) ์ •์˜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
analyst_instructions = """๋‹น์‹ ์€ AI ๋ถ„์„๊ฐ€ ํŽ˜๋ฅด์†Œ๋‚˜ ์„ธํŠธ๋ฅผ ๋งŒ๋“œ๋Š” ์ž„๋ฌด๋ฅผ ๋งก์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์ง€์นจ์„ ์ฃผ์˜ ๊นŠ๊ฒŒ ๋”ฐ๋ฅด์„ธ์š”:

1. ๋จผ์ € ์—ฐ๊ตฌ ์ฃผ์ œ๋ฅผ ๊ฒ€ํ† ํ•˜์„ธ์š”:
{topic}
        
2. ๋ถ„์„๊ฐ€ ์ƒ์„ฑ์„ ์•ˆ๋‚ดํ•˜๊ธฐ ์œ„ํ•ด ์„ ํƒ์ ์œผ๋กœ ์ œ๊ณต๋œ ํŽธ์ง‘ ํ”ผ๋“œ๋ฐฑ์„ ๊ฒ€ํ† ํ•˜์„ธ์š”:
{human_analyst_feedback}
    
3. ์œ„์˜ ๋ฌธ์„œ ๋ฐ/๋˜๋Š” ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ”ํƒ•์œผ๋กœ ๊ฐ€์žฅ ํฅ๋ฏธ๋กœ์šด ์ฃผ์ œ๋ฅผ ๊ฒฐ์ •ํ•˜์„ธ์š”.
                    
4. ์ƒ์œ„ {max_analysts}๊ฐœ์˜ ์ฃผ์ œ๋ฅผ ์„ ํƒํ•˜์„ธ์š”.

5. ๊ฐ ์ฃผ์ œ์— ํ•œ ๋ช…์˜ ๋ถ„์„๊ฐ€๋ฅผ ํ• ๋‹นํ•˜์„ธ์š”.
"""
  • analyst_instructions ์ง€์นจ: ์—ฐ๊ตฌ ์ฃผ์ œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋ถ„์„๊ฐ€์˜ ํŽ˜๋ฅด์†Œ๋‚˜๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์ผ๋ จ์˜ ๋‹จ๊ณ„๋“ค์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
    • ๋ถ„์„๊ฐ€๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€์ธ analyst_instructions๋Š” ์—ฐ๊ตฌ ์ฃผ์ œ์™€ ํ”ผ๋“œ๋ฐฑ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํŠน์ •ํ•œ ๋ถ„์„๊ฐ€๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ง€์นจ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
    • ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ”ํƒ•์œผ๋กœ ํฅ๋ฏธ๋กœ์šด ์ฃผ์ œ๋ฅผ ๊ฒฐ์ •ํ•˜๊ณ , ๊ฐ๊ฐ์˜ ์ฃผ์ œ์— ๋งž๋Š” ๋ถ„์„๊ฐ€๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

5) create_analysts ํ•จ์ˆ˜ ์ •์˜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def create_analysts(state: GenerateAnalystsState):
    topic = state['topic']
    max_analysts = state['max_analysts']
    human_analyst_feedback = state.get('human_analyst_feedback', '')
        
    # ๊ตฌ์กฐํ™”๋œ ์ถœ๋ ฅ ๊ฐ•์ œ
    structured_llm = llm.with_structured_output(Perspectives)

    # ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€
    system_message = analyst_instructions.format(topic=topic,
                                                 human_analyst_feedback=human_analyst_feedback, 
                                                 max_analysts=max_analysts)

    # ์งˆ๋ฌธ ์ƒ์„ฑ
    analysts = structured_llm.invoke(
                                 [SystemMessage(content=system_message)] 
                                 + 
                                 [HumanMessage(content="๋ถ„์„๊ฐ€ ์ง‘๋‹จ(set of analysts)์„ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”.")]
                                 )

    # ๋ถ„์„ ๋ชฉ๋ก์„ ์ƒํƒœ์— ๊ธฐ๋ก
    return {"analysts": analysts.analysts}
  • create_analysts ํ•จ์ˆ˜: ์ฃผ์–ด์ง„ ์ƒํƒœ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ถ„์„๊ฐ€๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. llm.invoke ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ๊ตฌ์กฐํ™”๋œ ์ถœ๋ ฅ์œผ๋กœ ๋ถ„์„๊ฐ€ ๋ชฉ๋ก์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    • structured_llm: llm.with_structured_output(Perspectives)์„ ํ†ตํ•ด ๋ถ„์„๊ฐ€์˜ ์ถœ๋ ฅ ํ˜•์‹์„ ๊ตฌ์กฐํ™”ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. (2 Perspective ํด๋ž˜์Šค ์ฐธ๊ณ )
    • system_message: ์ฃผ์ œ์™€ ํ”ผ๋“œ๋ฐฑ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ถ„์„๊ฐ€์˜ ํŽ˜๋ฅด์†Œ๋‚˜๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€์ž…๋‹ˆ๋‹ค.
    • invoke: ๊ตฌ์กฐํ™”๋œ ์ถœ๋ ฅ์œผ๋กœ ๋ถ„์„๊ฐ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋ฉฐ, ์ด ๋ฆฌ์ŠคํŠธ๋Š” ์ƒํƒœ์— ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค.

6-1) Human Feedback ํ•จ์ˆ˜ ์ •์˜

1
2
3
def human_feedback(state: GenerateAnalystsState):
    """ ์ค‘๋‹จ๋˜์–ด์•ผ ํ•˜๋Š” no-op ๋…ธ๋“œ """
    pass
  • human_feedback: ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•˜๋Š” ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ˜์˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ํ”ผ๋“œ๋ฐฑ์„ ์ฃผ๊ณ ๋ฐ›๋Š” ๊ณผ์ •์„ ์œ„ํ•ด ์ž„์‹œ ์ค‘๋‹จ ์ƒํƒœ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
    • ์ƒ์„ฑ๋œ ๋ถ„์„๊ฐ€์— ๋Œ€ํ•œ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ˜์˜ํ•˜๊ธฐ ์œ„ํ•ด human_feedback ํ•จ์ˆ˜์™€ should_continue ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    • ํ”ผ๋“œ๋ฐฑ์ด ์žˆ์„ ๊ฒฝ์šฐ, create_analysts ํ•จ์ˆ˜๊ฐ€ ๋‹ค์‹œ ํ˜ธ์ถœ๋˜์–ด ํ”ผ๋“œ๋ฐฑ์— ๋งž๋Š” ์ƒˆ๋กœ์šด ๋ถ„์„๊ฐ€๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
    • should_continue ํ•จ์ˆ˜๋Š” ์•„๋ž˜๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”.

6-2) should_continue ํ•จ์ˆ˜ ์ •์˜

1
2
3
4
5
6
def should_continue(state: GenerateAnalystsState):
    # ์ธ๊ฐ„ ํ”ผ๋“œ๋ฐฑ ํ™•์ธ
    human_analyst_feedback = state.get('human_analyst_feedback', None)
    if human_analyst_feedback:
        return "create_analysts"
    return END
  • should_continue: ํ”ผ๋“œ๋ฐฑ์ด ์žˆ๋‹ค๋ฉด create_analysts๋กœ ๋Œ์•„๊ฐ€ ๋ถ„์„๊ฐ€๋ฅผ ๋‹ค์‹œ ์ƒ์„ฑํ•˜๋ฉฐ, ์—†๋‹ค๋ฉด ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.

(์ถ”๊ฐ€) ์ž‘๋™๋ฐฉ์‹ ๋ถ€์—ฐ ์„ค๋ช…

  • LangGraph์—์„œ ๋ถ„์„๊ฐ€๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ณผ์ • ์ค‘, human_feedback ๋…ธ๋“œ๊ฐ€ ์‚ฌ์šฉ์ž์˜ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ์ค‘๋‹จ ์ง€์ ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ๋™์•ˆ ์‚ฌ์šฉ์ž๋Š” ์ƒ์„ฑ๋œ ๋ถ„์„๊ฐ€ ๋ชฉ๋ก์„ ๋ณด๊ณ , ํ•„์š”ํ•œ ๊ฒฝ์šฐ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ดํ›„, ์‚ฌ์šฉ์ž์˜ ํ”ผ๋“œ๋ฐฑ์ด should_continue ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ํ™•์ธ๋˜๊ณ , ํ”ผ๋“œ๋ฐฑ์ด ์žˆ๋Š” ๊ฒฝ์šฐ ๋ถ„์„๊ฐ€ ์ƒ์„ฑ ๋‹จ๊ณ„(create_analysts)๋กœ ๋‹ค์‹œ ๋Œ์•„๊ฐ€ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ˜์˜ํ•œ ์ƒˆ๋กœ์šด ๋ถ„์„๊ฐ€๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

6-3) Graph ์„ ์–ธ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langgraph.graph import START, END, StateGraph
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage

builder = StateGraph(GenerateAnalystsState)
builder.add_node("create_analysts", create_analysts)
builder.add_node("human_feedback", human_feedback)
builder.add_edge(START, "create_analysts")
builder.add_edge("create_analysts", "human_feedback")
builder.add_conditional_edges("human_feedback", should_continue, ["create_analysts", END])

# ์ปดํŒŒ์ผ
memory = MemorySaver()
graph = builder.compile(interrupt_before=['human_feedback'], checkpointer=memory)

# ๋ณด๊ธฐ
display(Image(graph.get_graph(xray=1).draw_mermaid_png()))
  • ์ด์ œ StateGraph ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋…ธ๋“œ์™€ ์—ฃ์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์ „์ฒด์ ์ธ ํ๋ฆ„์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
    • builder: ๊ทธ๋ž˜ํ”„ ๋นŒ๋”๋ฅผ ์„ค์ •ํ•˜๊ณ  ๊ฐ ๋…ธ๋“œ์™€ ์—ฃ์ง€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
    • add_node: create_analysts ๋ฐ human_feedback ๋…ธ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ถ„์„๊ฐ€ ์ƒ์„ฑ๊ณผ ํ”ผ๋“œ๋ฐฑ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
    • add_edge: ์‹œ์ž‘๊ณผ ์ข…๋ฃŒ ๋…ธ๋“œ๋ฅผ ์—ฐ๊ฒฐํ•˜๋ฉฐ, ํ”ผ๋“œ๋ฐฑ์ด ์žˆ์„ ๊ฒฝ์šฐ ๋ถ„์„๊ฐ€๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋…ธ๋“œ๋กœ ๋Œ์•„๊ฐ€๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
    • MemorySaver: ์‹คํ–‰ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜์—ฌ ํ•„์š”ํ•  ๋•Œ ๋ณต์›ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
    • display: ๊ทธ๋ž˜ํ”„๋ฅผ ์‹œ๊ฐํ™”ํ•˜์—ฌ ๊ตฌ์กฐ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

(์ค‘๊ฐ„ ์ ๊ฒ€) ๊ทธ๋Ÿผ ์ด์ œ ์ž˜ ๋งŒ๋“ค์–ด์กŒ๋‚˜ ํ™•์ธํ•ด๋ด์•ผ๊ฒ ์ฃ ?

  • ์•„๋ž˜์™€ ๊ฐ™์ด LLMOps ์†”๋ฃจ์…˜ ์ œ์ž‘์ด๋ผ๋Š” ์ฃผ์ œ๋กœ ์ „๋ฌธ๊ฐ€๋“ค์„ 3๋ช… ๊ณ ์šฉํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ์ž…๋ ฅ
max_analysts = 3
topic = "LLMOps ์†”๋ฃจ์…˜ ์ œ์ž‘"
thread = {"configurable": {"thread_id": "1"}}
# ์ฒซ ๋ฒˆ์งธ ์ค‘๋‹จ๊นŒ์ง€ ๊ทธ๋ž˜ํ”„ ์‹คํ–‰
for event in graph.stream({"topic":topic,"max_analysts":max_analysts,}, thread, stream_mode="values"):
    # ๊ฒ€ํ† 
    analysts = event.get('analysts', '')
    if analysts:
        for analyst in analysts:
            print(f"์ด๋ฆ„: {analyst.name}")
            print(f"์†Œ์†: {analyst.affiliation}")
            print(f"์—ญํ• : {analyst.role}")
            print(f"์„ค๋ช…: {analyst.description}")
            print("-" * 50)  

(๊ฒฐ๊ณผ ํ™•์ธ) ์‹ค์ œ๋กœ ์ „๋ฌธ๊ฐ€๋“ค์ด ์ œ๋Œ€๋กœ ์ƒ์„ฑ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

  • Lead Research Scientist, Data Engineer, AI Ethics Consultant ์ง์ฑ…์œผ๋กœ ๊ณ„์‹  ๋ถ„๋“ค์„ ๋งŒ๋“ค์–ด๋ƒˆ๊ตฐ์š”.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
์ด๋ฆ„: Dr. Emily Carter
์†Œ์†: Tech Innovators Inc.
์—ญํ• : Lead Research Scientist
์„ค๋ช…: Dr. Carter focuses on the development and optimization of LLMOps solutions, with a particular interest in scalability and efficiency.
--------------------------------------------------
์ด๋ฆ„: Michael Thompson
์†Œ์†: AI Solutions Ltd.
์—ญํ• : Data Engineer
์„ค๋ช…: Michael specializes in data pipeline architecture and is passionate about integrating LLMOps solutions to streamline data processing workflows.
--------------------------------------------------
์ด๋ฆ„: Sophia Martinez
์†Œ์†: FutureTech Analytics
์—ญํ• : AI Ethics Consultant
์„ค๋ช…: Sophia is dedicated to ensuring that LLMOps solutions are developed and deployed ethically, with a focus on transparency and fairness.
--------------------------------------------------

์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•ด๋ณด๋ฉด ํ˜„์žฌ ('human_feedback',)์ด์ „์—์„œ ๋Œ€๊ธฐ ์ค‘์ด๋ผ๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

1
2
3
# ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ณ  ๋‹ค์Œ ๋…ธ๋“œ ํ™•์ธ
state = graph.get_state(thread)
state.next

๊ทธ๋ฆฌ๊ณ  ๋‚˜์„œ ์•„๋ž˜์™€ ๊ฐ™์€ ์ฝ”๋“œ๋กœ human_feedback์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

1
2
3
# ์ด์ œ human_feedback ๋…ธ๋“œ์ธ ๊ฒƒ์ฒ˜๋Ÿผ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค
graph.update_state(thread, {"human_analyst_feedback": 
                            "๊ธฐ์—…๊ฐ€์  ๊ด€์ ์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด gen ai ๋„ค์ดํ‹ฐ๋ธŒ ์Šคํƒ€ํŠธ์—…์˜ CEO๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”"}, as_node="human_feedback")

๊ทธ๋Ÿผ human_feedback ๋…ธ๋“œ๋ฅผ ํ†ตํ•ด ์ด์ œ ๋‹ค์‹œ create_analyst ๋…ธ๋“œ๋กœ ๋„˜์–ด๊ฐ€์„œ ์ถ”๊ฐ€์ ์œผ๋กœ analyst๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์œ„ ๊ทธ๋ฆผ์„ ๊ธ€๋กœ ์ •๋ฆฌํ•˜์ž๋ฉด,

  1. ์ฒซ ๋ฒˆ์งธ create_analysts ํ˜ธ์ถœ:

    • ์ฃผ์ œ์— ๋งž๋Š” ๋ถ„์„๊ฐ€ 3๋ช…์„ ์ƒ์„ฑํ•˜์—ฌ analysts ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
    • ์ด ๋ฆฌ์ŠคํŠธ๋Š” state์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
  2. human_feedback๋ฅผ ํ†ตํ•œ ํ”ผ๋“œ๋ฐฑ ๋ฐ˜์˜:

    • human_feedback ๋…ธ๋“œ์—์„œ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›๊ณ , create_analysts๊ฐ€ ์žฌํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
    • ์ด๋•Œ, state['analysts']์— ๊ธฐ์กด 3๋ช…์˜ ๋ถ„์„๊ฐ€๊ฐ€ ์ด๋ฏธ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
    • ์ƒˆ๋กœ์šด 3๋ช…์ด ์ถ”๊ฐ€๋˜๋ฉด์„œ ์ตœ์ข…์ ์œผ๋กœ 6๋ช…์˜ ๋ถ„์„๊ฐ€ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ์ถœ๋ ฅ์— ์ง€๊ธˆ 6๋ช…์œผ๋กœ ๋œฌ ์ด์œ ๋Š” MemorySaver()๊ฐ€ checkpointer๋กœ ์‚ฌ์šฉ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด์ „ ์‹คํ–‰ ์ƒํƒœ๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

  • ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ create_analysts๊ฐ€ ์žฌํ˜ธ์ถœ๋˜๋ฉด์„œ ๊ธฐ์กด ๋ถ„์„๊ฐ€ ๋ชฉ๋ก(์ด์ „์— ํ˜ธ์ถœ๋œ 3๋ช…์˜ ๋ถ„์„๊ฐ€)์— ์ƒˆ๋กœ์šด ๋ถ„์„๊ฐ€๋“ค(์ƒˆ๋กญ๊ฒŒ ์ด๋ฒˆ์— ์ถ”๊ฐ€๋œ 3๋ช…์˜ ์ถ”๊ฐ€ ๋ถ„์„๊ฐ€)์ด ๊ณ„์† ์ถ”๊ฐ€๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค(์ด 6๋ช…์˜ ๋ถ„์„๊ฐ€).

ํ˜„์žฌ state ์ •๋ณด๋งŒ ๋ณด๋ ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ถœ๋ ฅํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
state = graph.get_state(thread)
# state.values

# ํ˜„์žฌ ์ƒํƒœ ์ถœ๋ ฅ
print(f"์ฃผ์ œ: {state.values['topic']}")
print(f"์ตœ๋Œ€ ๋ถ„์„๊ฐ€ ์ˆ˜: {state.values['max_analysts']}")
print(f"ํ”ผ๋“œ๋ฐฑ: {state.values['human_analyst_feedback']}")

print('======================')
print('      PRINT INFO ')
print('======================')

# analysts ๋ชฉ๋ก ์ถœ๋ ฅ
analysts = state.values['analysts']
if analysts:
    for analyst in analysts:
        print(f"\n์ด๋ฆ„: {analyst.name}")
        print(f"์†Œ์†: {analyst.affiliation}")
        print(f"์—ญํ• : {analyst.role}")
        print(f"์„ค๋ช…: {analyst.description}")
        print("-" * 50)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
์ฃผ์ œ: LLMOps ์†”๋ฃจ์…˜ ์ œ์ž‘
์ตœ๋Œ€ ๋ถ„์„๊ฐ€ ์ˆ˜: 3
ํ”ผ๋“œ๋ฐฑ: ๊ธฐ์—…๊ฐ€์  ๊ด€์ ์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด gen ai ๋„ค์ดํ‹ฐ๋ธŒ ์Šคํƒ€ํŠธ์—…์˜ CEO๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”
======================
      PRINT INFO 
======================

์ด๋ฆ„: Dr. Emily Zhang
์†Œ์†: Tech Innovators Inc.
์—ญํ• : AI Research Scientist
์„ค๋ช…: Dr. Zhang focuses on the development and optimization of large language models (LLMs). She is particularly interested in the technical challenges and innovations in LLMOps.
--------------------------------------------------

์ด๋ฆ„: Alex Kim
์†Œ์†: GenAI Startups
์—ญํ• : CEO
์„ค๋ช…: Alex is the CEO of a GenAI-native startup. He brings an entrepreneurial perspective to the table, focusing on the business applications and market potential of LLMOps solutions.
--------------------------------------------------

์ด๋ฆ„: Priya Natarajan
์†Œ์†: Data Security Solutions
์—ญํ• : Data Privacy Analyst
์„ค๋ช…: Priya specializes in data privacy and security. Her interest lies in ensuring that LLMOps solutions comply with data protection regulations and maintain user privacy.
--------------------------------------------------

๋”์ด์ƒ human-in-the-loop (ํœด๋จผ-ํ”ผ๋“œ๋ฐฑ)์ด ํ•„์š”์—†๋‹ค๊ณ  ํŒ๋‹จ์ด ๋˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด None์„ ํ”ผ๋“œ๋ฐฑ์œผ๋กœ ์ œ๊ณตํ•˜์—ฌ ๊ทธ๋ž˜ํ”„๊ฐ€ ์ข…๋ฃŒ๋  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

1
2
3
4
# ๋งŒ์กฑํ•œ๋‹ค๋ฉด ํ”ผ๋“œ๋ฐฑ ์—†์Œ์„ ์ œ๊ณต
further_feedack = None
graph.update_state(thread, {"human_analyst_feedback": 
                            further_feedack}, as_node="human_feedback")

์ด๋ ‡๊ฒŒ ๊ทธ๋ž˜ํ”„๋ฅผ ๋๊นŒ์ง€ ์‹คํ–‰์ด ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์ถœ๋ ฅ ๊ฒฐ๊ณผ: ())

1
2
3
4
5
6
7
8
9
10
# ๊ทธ๋ž˜ํ”„ ์‹คํ–‰์„ ๋๊นŒ์ง€ ๊ณ„์†ํ•ฉ๋‹ˆ๋‹ค
for event in graph.stream(None, thread, stream_mode="updates"):
    print("--๋…ธ๋“œ--")
    node_name = next(iter(event.keys()))
    print(node_name)

final_state = graph.get_state(thread)
analysts = final_state.values.get('analysts')

final_state.next

์ตœ์ข… ์„ ์ • Analyst๋„ ์œ„์—์„œ 1๋ฒˆ์˜ ํ”ผ๋“œ๋ฐฑ์„ ๊ฑฐ์นœ ๋ถ„์„๊ฐ€๋“ค์ž„์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


โœ”๏ธ Conduct Interview: ์งˆ๋ฌธ ์ƒ์„ฑ ๋ฐ ์ธํ„ฐ๋ทฐ ์ˆ˜ํ–‰

Conduct Interview ๋‹จ๊ณ„์—์„œ๋Š” ์ƒ์„ฑ๋œ ๋ถ„์„๊ฐ€๊ฐ€ ์ „๋ฌธ๊ฐ€ AI์™€ ๋Œ€ํ™”ํ•˜๋ฉฐ ์ฃผ์ œ์— ๋Œ€ํ•œ ํ†ต์ฐฐ์„ ์–ป๊ธฐ ์œ„ํ•ด ์งˆ๋ฌธ์„ ๋˜์ง€๊ณ  ๋‹ต๋ณ€์„ ์ˆ˜์ง‘ํ•˜๋Š” ๊ณผ์ •์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

  • ์ด ๊ณผ์ •์€ ๋ถ„์„๊ฐ€์˜ ์—ญํ• ์— ๋”ฐ๋ผ ์„ค๊ณ„๋œ ์งˆ๋ฌธ์„ ์ƒ์„ฑํ•˜๊ณ , ๋‹ค์–‘ํ•œ ์†Œ์Šค์—์„œ ์ •๋ณด๋ฅผ ๋ณ‘๋ ฌ๋กœ ์ˆ˜์ง‘ํ•œ ๋’ค ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.
1
2
3
import operator
from typing import  Annotated
from langgraph.graph import MessagesState
  1. InterviewState ํด๋ž˜์Šค:

    • MessagesState๋ฅผ ์ƒ์†๋ฐ›์•„ ์ธํ„ฐ๋ทฐ ๋Œ€ํ™”์—์„œ ํ•„์š”ํ•œ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

      1
      2
      3
      4
      5
      6
      
      class InterviewState(MessagesState):
          max_num_turns: int # ๋Œ€ํ™” ํ„ด ์ˆ˜
          context: Annotated[list, operator.add] # ์†Œ์Šค ๋ฌธ์„œ
          analyst: Analyst # ์งˆ๋ฌธํ•˜๋Š” ๋ถ„์„๊ฐ€
          interview: str # ์ธํ„ฐ๋ทฐ ๊ธฐ๋ก
          sections: list # Send() API๋ฅผ ์œ„ํ•ด ์™ธ๋ถ€ ์ƒํƒœ์—์„œ ๋ณต์ œํ•˜๋Š” ์ตœ์ข… ํ‚ค
      
    • ์ด ํด๋ž˜์Šค๋Š” ๋ถ„์„๊ฐ€(Analyst)์™€ ์ธํ„ฐ๋ทฐ ์ง„ํ–‰ ๊ณผ์ •์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

      • max_num_turns: ์ธํ„ฐ๋ทฐ์˜ ์ตœ๋Œ€ ํ„ด ์ˆ˜๋ฅผ ์„ค์ •ํ•˜์—ฌ, ์ธํ„ฐ๋ทฐ๊ฐ€ ๋ช‡ ์ฐจ๋ก€์˜ ์งˆ๋ฌธ-์‘๋‹ต ์Œ ์ดํ›„ ์ข…๋ฃŒ๋ ์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
      • context: Annotated ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜์—ฌ ์†Œ์Šค ๋ฌธ์„œ์˜ ๋ชฉ๋ก์„ ์„ค์ •ํ•˜๊ณ , operator.add๋ฅผ ์ ์šฉํ•˜์—ฌ ๊ฐ ์ธํ„ฐ๋ทฐ์—์„œ ์ˆ˜์ง‘๋œ ์†Œ์Šค ๋ฌธ์„œ๋ฅผ ์ ์ง„์ ์œผ๋กœ ๋ˆ„์ ํ•ฉ๋‹ˆ๋‹ค.
      • analyst: ํ˜„์žฌ ์งˆ๋ฌธ์„ ํ•˜๋Š” ๋ถ„์„๊ฐ€๋ฅผ ๋‚˜ํƒ€๋‚ด๋ฉฐ, Analyst ๊ฐ์ฒด๋กœ ๊ด€๋ฆฌ๋ฉ๋‹ˆ๋‹ค.
      • interview: ์ธํ„ฐ๋ทฐ ๋‚ด์šฉ์„ ๋ฌธ์ž์—ด๋กœ ์ €์žฅํ•˜์—ฌ, ๋Œ€ํ™” ์ „์ฒด์˜ ๊ธฐ๋ก์„ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค.
      • sections: Send() API์—์„œ ์™ธ๋ถ€ ์ƒํƒœ์™€ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” ํ‚ค์ž…๋‹ˆ๋‹ค.
  2. SearchQuery ํด๋ž˜์Šค:

    • search_query ์†์„ฑ๋งŒ์„ ๊ฐ€์ง€๋Š” ๊ฐ„๋‹จํ•œ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ๋กœ, ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋ฅผ ๋ฌธ์ž์—ด๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

      1
      2
      
      class SearchQuery(BaseModel):
          search_query: str = Field(None, description="๊ฒ€์ƒ‰์„ ์œ„ํ•œ ์ฟผ๋ฆฌ.")
      
    • search_query๋Š” Field ํ•จ์ˆ˜์™€ ํ•จ๊ป˜ ์„ค์ •๋˜์–ด, ๊ฒ€์ƒ‰์–ด์˜ ์šฉ๋„๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

      • Field ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ์˜ ์šฉ๋„๋ฅผ ์„ค๋ช…ํ•˜์—ฌ search_query๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
      • ๊ฒ€์ƒ‰ ๊ณผ์ •์—์„œ ๋ถ„์„๊ฐ€์˜ ์งˆ๋ฌธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒ€์ƒ‰์–ด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์ €์žฅํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  3. question_instructions ์ฟผ๋ฆฌ:

  • question_instructions๋Š” ์ธํ„ฐ๋ทฐ๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๋ถ„์„๊ฐ€๊ฐ€ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ํฅ๋ฏธ๋กญ๊ณ  ๊ตฌ์ฒด์ ์ธ ์งˆ๋ฌธ์„ ๋˜์ง€๋„๋ก ๋•๋Š” ๊ฐ€์ด๋“œ๋ผ์ธ์ž…๋‹ˆ๋‹ค.
    • ์ด ํ”„๋กฌํ”„ํŠธ๋Š” ๋ถ„์„๊ฐ€๊ฐ€ ์ธํ„ฐ๋ทฐ์˜ ์ดˆ์ ๊ณผ ๋ชฉํ‘œ์— ๋งž์ถฐ ๋Œ€ํ™”๋ฅผ ์ด๋Œ์–ด๋‚˜๊ฐ€๋„๋ก ์œ ๋„ํ•˜๋ฉฐ, ์ „์ฒด ์ธํ„ฐ๋ทฐ ๋Œ€ํ™”์˜ ์งˆ๊ณผ ๋ฐฉํ–ฅ์„ฑ์„ ์œ ์ง€ํ•˜๋Š” ๋ฐ ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ๋ถ„์„๊ฐ€๊ฐ€ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ํฅ๋ฏธ๋กญ๊ณ  ๊ตฌ์ฒด์ ์ธ ์งˆ๋ฌธ์„ ๋˜์ง€๋„๋ก ๋•๋Š” ๊ฐ€์ด๋“œ๋ผ์ธ
question_instructions = """

๋‹น์‹ ์€ ํŠน์ • ์ฃผ์ œ์— ๋Œ€ํ•ด ์ „๋ฌธ๊ฐ€๋ฅผ ์ธํ„ฐ๋ทฐํ•˜๋Š” ๋ถ„์„๊ฐ€์ž…๋‹ˆ๋‹ค. 

๋‹น์‹ ์˜ ๋ชฉํ‘œ๋Š” ์ฃผ์ œ์™€ ๊ด€๋ จ๋œ ํฅ๋ฏธ๋กญ๊ณ  ๊ตฌ์ฒด์ ์ธ ํ†ต์ฐฐ์„ ์–ป๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  1. Interesting: ์‚ฌ๋žŒ๋“ค์ด ๋†€๋ž๊ฑฐ๋‚˜ ๋œป๋ฐ–์ด๋ผ๊ณ  ์ƒ๊ฐํ•  ํ†ต์ฐฐ.
        
  2. Specific: ์ผ๋ฐ˜๋ก ์„ ํ”ผํ•˜๊ณ  ์ „๋ฌธ๊ฐ€์˜ ๊ตฌ์ฒด์ ์ธ ์˜ˆ์‹œ๋ฅผ ํฌํ•จํ•˜๋Š” ํ†ต์ฐฐ.

๋‹ค์Œ์€ ๋‹น์‹ ์˜ topic of focus์™€ set of goals์ž…๋‹ˆ๋‹ค: {goals}
        
๋‹น์‹ ์˜ ํŽ˜๋ฅด์†Œ๋‚˜์— ๋งž๋Š” ์ด๋ฆ„์œผ๋กœ ์ž์‹ ์„ ์†Œ๊ฐœํ•˜๊ณ  ์งˆ๋ฌธ์„ ์‹œ์ž‘ํ•˜์„ธ์š”.

์ฃผ์ œ์— ๋Œ€ํ•œ ์ดํ•ด๋ฅผ ์‹ฌํ™”ํ•˜๊ณ  ๊ตฌ์ฒดํ™”ํ•˜๊ธฐ ์œ„ํ•ด ๊ณ„์†ํ•ด์„œ ์งˆ๋ฌธํ•˜์„ธ์š”.
        
์ดํ•ด๊ฐ€ ์ถฉ๋ถ„ํ•˜๋‹ค๊ณ  ํŒ๋‹จ๋˜๋ฉด "๋„์›€ ์ฃผ์…”์„œ ์ •๋ง ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!"๋ผ๊ณ  ๋งํ•˜๋ฉฐ ์ธํ„ฐ๋ทฐ๋ฅผ ๋งˆ๋ฌด๋ฆฌํ•˜์„ธ์š”.

์ œ๊ณต๋œ ํŽ˜๋ฅด์†Œ๋‚˜์™€ ๋ชฉํ‘œ๋ฅผ ๋ฐ˜์˜ํ•˜๋ฉฐ ์‘๋‹ต ์ „์ฒด์— ๊ฑธ์ณ ์บ๋ฆญํ„ฐ๋ฅผ ์œ ์ง€ํ•˜์„ธ์š”.
"""

1) ์งˆ๋ฌธ ์ƒ์„ฑ (Generate Question)

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

  • ์งˆ๋ฌธ ์ƒ์„ฑ์€ generate_question ํ•จ์ˆ˜์—์„œ ์ด๋ฃจ์–ด์ง€๋ฉฐ, ์ด ํ•จ์ˆ˜๋Š” ๋ถ„์„๊ฐ€์˜ ํŽ˜๋ฅด์†Œ๋‚˜์™€ ๋ชฉํ‘œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ ์ ˆํ•œ ์งˆ๋ฌธ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
def generate_question(state: InterviewState):
    # ๋ถ„์„๊ฐ€์™€ ์ด์ „ ๋ฉ”์‹œ์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ
    analyst = state["analyst"]
    messages = state["messages"]
    
    # ๋ถ„์„๊ฐ€์˜ ๋ชฉํ‘œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์งˆ๋ฌธ ์ƒ์„ฑ
    system_message = question_instructions.format(goals=analyst.persona)
    question = llm.invoke([SystemMessage(content=system_message)] + messages)
    
    # ์ƒ์„ฑ๋œ ์งˆ๋ฌธ์„ ์ƒํƒœ์— ๊ธฐ๋กํ•˜์—ฌ ๋‹ค์Œ ํ”„๋กœ์„ธ์Šค๋กœ ์ „๋‹ฌ
    return {"messages": [question]}
  • analyst: ๋ถ„์„๊ฐ€์˜ ํŽ˜๋ฅด์†Œ๋‚˜ ๋ฐ ๋ชฉํ‘œ๋ฅผ ํฌํ•จํ•œ ์ธ์Šคํ„ด์Šค๋กœ, ์—ฐ๊ตฌ ์ฃผ์ œ์™€ ๊ด€๋ จ๋œ ๊ตฌ์ฒด์ ์ธ ์งˆ๋ฌธ์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ ๋‹ด๊น๋‹ˆ๋‹ค.
  • question_instructions: ๋ถ„์„๊ฐ€๊ฐ€ ์งˆ๋ฌธ์„ ํ•  ๋•Œ ๋”ฐ๋ผ์•ผ ํ•  ์ง€์นจ์œผ๋กœ, ์งˆ๋ฌธ์ด ํฅ๋ฏธ๋กญ๊ณ  ๊ตฌ์ฒด์ ์ธ ํ†ต์ฐฐ์„ ์œ ๋„ํ•˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    • (์ฐธ๊ณ ) question_instructions์€ ๋ฐ”๋กœ ์œ„ 3. question_instructions ์ฟผ๋ฆฌ์—์„œ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • question = llm.invoke ๋ฉ”์„œ๋“œ๋Š” ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€์™€ ์ด์ „ ๋Œ€ํ™” ๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅ์œผ๋กœ ๋ฐ›์•„ ์ƒˆ๋กœ์šด ์งˆ๋ฌธ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    • llm.invoke์— ๋“ค์–ด๊ฐ€๋Š” ์ธ์ž๋Š” [SystemMessage(content=system_message)] + messages๋กœ, ์œ„์—์„œ ์ •์˜ํ•œ question_instructions๊ณผ messages์ž…๋‹ˆ๋‹ค.
    • ์ฐธ๊ณ ๋กœ, messages๋Š” ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ ์ธํ„ฐ๋ทฐ ๋Œ€ํ™”์˜ ์ „์ฒด ๋ฉ”์‹œ์ง€ ๊ธฐ๋ก์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ์ด๋Š” ๋ถ„์„๊ฐ€์™€ ์ „๋ฌธ๊ฐ€ ๊ฐ„์˜ ์ด์ „ ์งˆ๋ฌธ๊ณผ ๋‹ต๋ณ€์ด ์ˆœ์„œ๋Œ€๋กœ ํฌํ•จ๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.

2) ๋ณ‘๋ ฌ ๋‹ต๋ณ€ ์ˆ˜์ง‘ (Parallelized Answer Collection)

LangGraph์˜ ๋ณ‘๋ ฌํ™” ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•ด ์ „๋ฌธ๊ฐ€๊ฐ€ ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์–‘ํ•œ ์†Œ์Šค์—์„œ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค.

  • ๋ณธ ์˜ˆ์ œ์—์„œ๋Š” tavily_search์™€ search_wikipedia ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๊ฐ ์†Œ์Šค๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
# ์›น ๊ฒ€์ƒ‰ ๋„๊ตฌ
from langchain_community.tools.tavily_search import TavilySearchResults
tavily_search = TavilySearchResults(max_results=3)

# ์œ„ํ‚คํ”ผ๋””์•„ ๊ฒ€์ƒ‰ ๋„๊ตฌ
from langchain_community.document_loaders import WikipediaLoader

search_instructions

  • search_instructions๋Š” ๋Œ€ํ™” ๋งฅ๋ฝ์„ ๋ฐ”ํƒ•์œผ๋กœ ์ตœ์ ์˜ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    • ์ด ํ”„๋กฌํ”„ํŠธ๋Š” ํŠน์ •ํ•œ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๊ฒ€์ƒ‰ํ•  ๋•Œ ๊ตฌ์ฒด์ ์ด๊ณ  ์œ ์šฉํ•œ ์ฟผ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ์œ ๋„ํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
# ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ์ž‘์„ฑ
search_instructions = SystemMessage(content=f"""
๋ถ„์„๊ฐ€์™€ ์ „๋ฌธ๊ฐ€ ์‚ฌ์ด์˜ ๋Œ€ํ™”๊ฐ€ ์ฃผ์–ด์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค. 

๋‹น์‹ ์˜ ๋ชฉํ‘œ๋Š” ๋Œ€ํ™”์™€ ๊ด€๋ จ๋œ ๊ฒ€์ƒ‰ ๋ฐ/๋˜๋Š” ์›น ๊ฒ€์ƒ‰์— ์‚ฌ์šฉํ•  ์ž˜ ๊ตฌ์กฐํ™”๋œ ์ฟผ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
        
๋จผ์ € ์ „์ฒด ๋Œ€ํ™”๋ฅผ ๋ถ„์„ํ•˜์„ธ์š”.

๋ถ„์„๊ฐ€๊ฐ€ ๋งˆ์ง€๋ง‰์œผ๋กœ ์ œ๊ธฐํ•œ ์งˆ๋ฌธ์— ํŠนํžˆ ์ฃผ์˜๋ฅผ ๊ธฐ์šธ์ด์„ธ์š”.

์ด ๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ์„ ์ž˜ ๊ตฌ์กฐํ™”๋œ ์›น ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋กœ ๋ณ€ํ™˜ํ•˜์„ธ์š”
""")

์›น ๊ฒ€์ƒ‰ (search_web)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def search_web(state: InterviewState):
    """ ์›น ๊ฒ€์ƒ‰์—์„œ ๋ฌธ์„œ ๊ฒ€์ƒ‰ """
    # ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ์ƒ์„ฑ
    structured_llm = llm.with_structured_output(SearchQuery)
    search_query = structured_llm.invoke([search_instructions] + state["messages"])
    
    # Tavily ๊ฒ€์ƒ‰ API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ด
    search_docs = tavily_search.invoke(search_query.search_query)
    
    # ํ˜•์‹ ์ง€์ •๋œ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
    formatted_search_docs = "\n\n---\n\n".join(
        [f'<Document href="{doc["url"]}"/>\n{doc["content"]}\n</Document>' for doc in search_docs]
    )
    return {"context": [formatted_search_docs]} 
  • search_query: ๋ถ„์„๊ฐ€์˜ ์งˆ๋ฌธ์„ ๋ฐ”ํƒ•์œผ๋กœ ์›น ๊ฒ€์ƒ‰์— ์ ํ•ฉํ•œ ์ฟผ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • tavily_search: Tavily API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์›น ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

    • ์ด ๊ฒฐ๊ณผ๋Š” ์ถ”ํ›„ ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • formatted_search_docs: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ HTML ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ณด๊ณ ์„œ ์ž‘์„ฑ ์‹œ ์ถœ์ฒ˜๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

์œ„ํ‚คํ”ผ๋””์•„ ๊ฒ€์ƒ‰ (search_wikipedia)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def search_wikipedia(state: InterviewState):
    """ ์œ„ํ‚คํ”ผ๋””์•„์—์„œ ๋ฌธ์„œ ๊ฒ€์ƒ‰ """
    # ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ์ƒ์„ฑ
    structured_llm = llm.with_structured_output(SearchQuery)
    search_query = structured_llm.invoke([search_instructions] + state["messages"])
    
    # WikipediaLoader๋กœ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ฐ€์ ธ์˜ค๊ธฐ
    search_docs = WikipediaLoader(query=search_query.search_query, load_max_docs=2).load()
    
    # ํ˜•์‹ ์ง€์ •๋œ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
    formatted_search_docs = "\n\n---\n\n".join(
        [f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>' for doc in search_docs]
    )
    return {"context": [formatted_search_docs]} 
  • WikipediaLoader: ์œ„ํ‚คํ”ผ๋””์•„์—์„œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์—ญํ• ์„ ํ•˜๋ฉฐ, ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด ๊ด€๋ จ ๋ฌธ์„œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • formatted_search_docs: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ HTML ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ณด๊ณ ์„œ ์ž‘์„ฑ ์‹œ ์ถœ์ฒ˜๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

answer_instructions

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

    • ์ด ๊ฐ€์ด๋“œ๋ผ์ธ์€ ๋‹ต๋ณ€์˜ ์ผ๊ด€์„ฑ๊ณผ ์ •ํ™•์„ฑ์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
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
answer_instructions = """
๋‹น์‹ ์€ ๋ถ„์„๊ฐ€์— ์˜ํ•ด ์ธํ„ฐ๋ทฐ๋ฅผ ๋ฐ›๊ณ  ์žˆ๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ๋ถ„์„๊ฐ€์˜ ๊ด€์‹ฌ ์˜์—ญ์ž…๋‹ˆ๋‹ค: 
{goals}. 
        
๋‹น์‹ ์˜ ๋ชฉํ‘œ๋Š” ์ธํ„ฐ๋ทฐ์–ด๊ฐ€ ์ œ๊ธฐํ•œ ์งˆ๋ฌธ์— ๋‹ตํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์งˆ๋ฌธ์— ๋‹ตํ•˜๊ธฐ ์œ„ํ•ด ์ด ์ปจํ…์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”:        
{context}.

์งˆ๋ฌธ์— ๋Œ€๋‹ตํ•  ๋•Œ ์•„๋ž˜ ์ง€์นจ์„ ๋”ฐ๋ฅด์„ธ์š”:
  1. ์ œ๊ณต๋œ ์ปจํ…์ŠคํŠธ์˜ ์ •๋ณด๋งŒ ์‚ฌ์šฉํ•˜์„ธ์š”. 
        
  2. ์ปจํ…์ŠคํŠธ์— ๋ช…์‹œ์ ์œผ๋กœ ์–ธ๊ธ‰๋˜์ง€ ์•Š์€ ์™ธ๋ถ€ ์ •๋ณด๋ฅผ ๋„์ž…ํ•˜๊ฑฐ๋‚˜ ๊ฐ€์ •ํ•˜์ง€ ๋งˆ์„ธ์š”.

  3. ์ปจํ…์ŠคํŠธ์—๋Š” ๊ฐ ๊ฐœ๋ณ„ ๋ฌธ์„œ ์ƒ๋‹จ์— ์†Œ์Šค๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

  4. ๊ด€๋ จ ์ง„์ˆ  ์˜†์— ์ด๋Ÿฌํ•œ ์†Œ์Šค๋ฅผ ๋‹ต๋ณ€์— ํฌํ•จํ•˜์„ธ์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, ์†Œ์Šค #1์˜ ๊ฒฝ์šฐ [1]์„ ์‚ฌ์šฉํ•˜์„ธ์š”. 

  5. ๋‹ต๋ณ€ ํ•˜๋‹จ์— ์†Œ์Šค๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ๋‚˜์—ดํ•˜์„ธ์š”. [1] ์†Œ์Šค 1, [2] ์†Œ์Šค 2 ๋“ฑ
        
  6. ์†Œ์Šค๊ฐ€ '<Document source="assistant/docs/llama3_1.pdf" page="7"/>'์ธ ๊ฒฝ์šฐ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‚˜์—ดํ•˜์„ธ์š”: 
        
  [1] assistant/docs/llama3_1.pdf, 7ํŽ˜์ด์ง€ 
        
  ์ธ์šฉ ์‹œ ๊ด„ํ˜ธ์™€ Document source ์ „๋ฌธ์„ ์ƒ๋žตํ•˜์„ธ์š”.
"""
  1. ๋‹ต๋ณ€ ์ƒ์„ฑ (Generate Answer)

๋ถ„์„๊ฐ€์˜ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์€ ์›น ๋ฐ ์œ„ํ‚คํ”ผ๋””์•„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉํ•ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

  • generate_answer ํ•จ์ˆ˜๋Š” ๋ฌธ๋งฅ(context)๊ณผ ์งˆ๋ฌธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ „๋ฌธ๊ฐ€์˜ ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
def generate_answer(state: InterviewState):
    # ๋ถ„์„๊ฐ€์™€ ์ปจํ…์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
    analyst = state["analyst"]
    context = state["context"]
    
    # ์งˆ๋ฌธ์„ ๋ฐ”ํƒ•์œผ๋กœ ๋‹ต๋ณ€ ์ƒ์„ฑ
    system_message = answer_instructions.format(goals=analyst.persona, context=context)
    answer = llm.invoke([SystemMessage(content=system_message)] + state["messages"])
    answer.name = "expert"  # ์ „๋ฌธ๊ฐ€์˜ ์ด๋ฆ„ ์ง€์ •
    
    return {"messages": [answer]}
  • context: ์ด์ „์— ์ˆ˜์ง‘๋œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋“ค์ด ํฌํ•จ๋œ ๋ฌธ๋งฅ ๋ฐ์ดํ„ฐ๋กœ, ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์„ ์ž‘์„ฑํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • answer_instructions: ์ „๋ฌธ๊ฐ€๊ฐ€ ์ปจํ…์ŠคํŠธ์™€ ๋ถ„์„๊ฐ€์˜ ๋ชฉํ‘œ๋ฅผ ๋ฐ˜์˜ํ•˜์—ฌ ์ •ํ™•ํ•˜๊ณ  ๊ด€๋ จ ์žˆ๋Š” ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    • (์ฐธ๊ณ ) answer_instructions์€ ์œ„์—์„œ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • answer.name: ๋‹ต๋ณ€์„ ์ „๋ฌธ๊ฐ€๋กœ๋ถ€ํ„ฐ ์˜จ ๋ฉ”์‹œ์ง€๋กœ ์ง€์ •ํ•˜์—ฌ ์ธํ„ฐ๋ทฐ์˜ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  1. ์ธํ„ฐ๋ทฐ ์ €์žฅ ๋ฐ ์š”์•ฝ (Save Interview)

์ธํ„ฐ๋ทฐ๊ฐ€ ๋๋‚˜๋ฉด ๋ชจ๋“  ๋ฉ”์‹œ์ง€๋ฅผ ํ•˜๋‚˜์˜ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅํ•˜๋Š” save_interview ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ์ด ์ธํ„ฐ๋ทฐ ๊ธฐ๋ก์€ ์ดํ›„ ์ตœ์ข… ๋ณด๊ณ ์„œ ์ž‘์„ฑ์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

1
2
3
4
5
6
7
8
9
def save_interview(state: InterviewState):
    # ๋ฉ”์‹œ์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ
    messages = state["messages"]
    
    # ์ธํ„ฐ๋ทฐ ๋‚ด์šฉ์„ ํ•˜๋‚˜์˜ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
    interview = get_buffer_string(messages)
    
    # ์ธํ„ฐ๋ทฐ ๋‚ด์šฉ์„ ์ƒํƒœ์— ์ €์žฅ
    return {"interview": interview}
  • get_buffer_string: ์ธํ„ฐ๋ทฐ ๋Œ€ํ™”๋ฅผ ํ•˜๋‚˜์˜ ํ…์ŠคํŠธ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ดํ›„ ๋‹จ๊ณ„์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค” get_buffer_string

  • get_buffer_string ํ•จ์ˆ˜๋Š” LangChain ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ผ๋ถ€์ž…๋‹ˆ๋‹ค. ๊ตฌ์ฒด์ ์œผ๋กœ, ์ด ํ•จ์ˆ˜๋Š” langchain_core.messages.utils ๋ชจ๋“ˆ์— ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋ชฉ์ : ์ผ๋ จ์˜ ๋ฉ”์‹œ์ง€(Messages)๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ํ•˜๋‚˜์˜ ๋ฌธ์ž์—ด๋กœ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค
    • ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
      1
      2
      3
      4
      5
      6
      7
      
      from langchain_core import AIMessage, HumanMessage
      messages = [
        HumanMessage(content="Hi, how are you?"),
        AIMessage(content="Good, how are you?"),
      ]
      result = get_buffer_string(messages)
      # ๊ฒฐ๊ณผ: "Human: Hi, how are you?\nAI: Good, how are you?"
      
    • ์œ„์˜ ์˜ˆ์ œ์™€ ๊ฐ™์ด get_buffer_string ํ•จ์ˆ˜๋Š” ์ฃผ์–ด์ง„ ๋ฉ”์‹œ์ง€ ๋ฆฌ์ŠคํŠธ๋ฅผ ํ•˜๋‚˜์˜ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๋ฉฐ, ๊ฐ ๋ฉ”์‹œ์ง€์— ๋Œ€ํ•ด prefix์™€ content๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
  1. ๋ผ์šฐํŒ… ํ•จ์ˆ˜ (Route Messages)

์งˆ๋ฌธ๊ณผ ๋‹ต๋ณ€์„ ์ฃผ๊ณ ๋ฐ›๋Š” ํ”„๋กœ์„ธ์Šค๋ฅผ ์กฐ์œจํ•˜๊ธฐ ์œ„ํ•ด route_messages ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • ์ด ํ•จ์ˆ˜๋Š” ์ „๋ฌธ๊ฐ€๊ฐ€ ํŠน์ •ํ•œ ์‘๋‹ต ์ˆ˜์— ๋„๋‹ฌํ–ˆ๊ฑฐ๋‚˜ ๋ถ„์„๊ฐ€๊ฐ€ ๋Œ€ํ™”๋ฅผ ๋งˆ์น˜๊ณ ์ž ํ•˜๋Š” ์‹ ํ˜ธ(โ€œ๋„์›€ ์ฃผ์…”์„œ ์ •๋ง ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹คโ€)๋ฅผ ๋ณด๋ƒˆ์„ ๋•Œ ์ธํ„ฐ๋ทฐ๋ฅผ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def route_messages(state: InterviewState, name: str = "expert"):
    # ๋ฉ”์‹œ์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ
    messages = state["messages"]
    max_num_turns = state.get('max_num_turns', 2)
    
    # ์ „๋ฌธ๊ฐ€ ์‘๋‹ต ์ˆ˜ ํ™•์ธ
    num_responses = len(
        [m for m in messages if isinstance(m, AIMessage) and m.name == name]
    )
    
    # ์‘๋‹ต์ด ์ตœ๋Œ€ ํ„ด ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ๊ฑฐ๋‚˜ ๊ฐ์‚ฌ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ์ธํ„ฐ๋ทฐ ์ข…๋ฃŒ
    if num_responses >= max_num_turns:
        return 'save_interview'
    
    last_question = messages[-2]  # ๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ ํ™•์ธ
    if "๋„์›€ ์ฃผ์…”์„œ ์ •๋ง ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค" in last_question.content:
        return 'save_interview'
    
    return "ask_question"
  • ์ข…๋ฃŒ ์กฐ๊ฑด:

    • num_responses: ์ „๋ฌธ๊ฐ€์˜ ์‘๋‹ต ํšŸ์ˆ˜๋ฅผ ํ™•์ธํ•˜์—ฌ ์ตœ๋Œ€ ์‘๋‹ต ์ˆ˜์— ๋„๋‹ฌํ•˜๋ฉด ์ธํ„ฐ๋ทฐ๋ฅผ ์ข…๋ฃŒํ•˜๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
    • last_question: ๋ถ„์„๊ฐ€์˜ ๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ์„ ํ™•์ธํ•˜์—ฌ ๋Œ€ํ™” ์ข…๋ฃŒ๋ฅผ ์š”์ฒญํ•˜๋Š” ๋ฉ”์‹œ์ง€๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ ์ธํ„ฐ๋ทฐ๋ฅผ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.
  1. write_section ํ•จ์ˆ˜

section_writer_instructions

  • section_writer_instructions๋Š” ์ „๋ฌธ ๊ธฐ์ˆ  ์ž‘๊ฐ€๊ฐ€ ๋ณด๊ณ ์„œ ์„น์…˜์„ ์ž‘์„ฑํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ์„ธ๋ถ€ ์ง€์นจ์„ ์ œ๊ณตํ•˜์—ฌ, ์ธํ„ฐ๋ทฐ์—์„œ ์ˆ˜์ง‘ํ•œ ํ†ต์ฐฐ์„ ๋ช…ํ™•ํ•˜๊ณ  ์ผ๊ด€๋œ ํ˜•์‹์œผ๋กœ ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•์Šต๋‹ˆ๋‹ค.

    • ์ด ๊ณผ์ •์€ Conduct Interview ๋‹จ๊ณ„์—์„œ ์ˆ˜์ง‘๋œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ, ๋ณด๊ณ ์„œ์˜ ๊ฐ ์„น์…˜์„ ์ž‘์„ฑํ•˜๋Š” ๋ฐ ์ค‘์ ์„ ๋‘ก๋‹ˆ๋‹ค. ์ฃผ์š” ์ง€์นจ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
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
46
47
48
49
50
51
52
section_writer_instructions = """
๋‹น์‹ ์€ ์ „๋ฌธ ๊ธฐ์ˆ  ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค. 
            
๋‹น์‹ ์˜ ์ž„๋ฌด๋Š” ์†Œ์Šค ๋ฌธ์„œ ์„ธํŠธ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋ณด๊ณ ์„œ์˜ ์งง๊ณ  ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šด ์„น์…˜์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

1. ์†Œ์Šค ๋ฌธ์„œ์˜ ๋‚ด์šฉ ๋ถ„์„: 
- ๊ฐ ์†Œ์Šค ๋ฌธ์„œ์˜ ์ด๋ฆ„์€ <Document ํƒœ๊ทธ์™€ ํ•จ๊ป˜ ๋ฌธ์„œ ์‹œ์ž‘ ๋ถ€๋ถ„์— ์žˆ์Šต๋‹ˆ๋‹ค.
        
2. ๋งˆํฌ๋‹ค์šด ํ˜•์‹์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ณด๊ณ ์„œ ๊ตฌ์กฐ ์ƒ์„ฑ:
- ์„น์…˜ ์ œ๋ชฉ์—๋Š” ## ์‚ฌ์šฉ
- ํ•˜์œ„ ์„น์…˜ ํ—ค๋”์—๋Š” ### ์‚ฌ์šฉ
        
3. ๋‹ค์Œ ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ผ ๋ณด๊ณ ์„œ ์ž‘์„ฑ:
a. ์ œ๋ชฉ (## ํ—ค๋”)
b. ์š”์•ฝ (### ํ—ค๋”)
c. ์ถœ์ฒ˜ (### ํ—ค๋”)

4. ๋ถ„์„๊ฐ€์˜ ๊ด€์‹ฌ ์˜์—ญ์„ ๋ฐ”ํƒ•์œผ๋กœ ํฅ๋ฏธ๋กœ์šด ์ œ๋ชฉ ๋งŒ๋“ค๊ธฐ: 
{focus}

5. ์š”์•ฝ ์„น์…˜:
- ๋ถ„์„๊ฐ€์˜ ๊ด€์‹ฌ ์˜์—ญ๊ณผ ๊ด€๋ จ๋œ ์ผ๋ฐ˜์ ์ธ ๋ฐฐ๊ฒฝ/๋งฅ๋ฝ์œผ๋กœ ์š”์•ฝ ์‹œ์ž‘
- ์ธํ„ฐ๋ทฐ์—์„œ ์–ป์€ ํ†ต์ฐฐ ์ค‘ ์ƒˆ๋กญ๊ฑฐ๋‚˜ ํฅ๋ฏธ๋กญ๊ฑฐ๋‚˜ ๋†€๋ผ์šด ์  ๊ฐ•์กฐ
- ์‚ฌ์šฉํ•œ ์†Œ์Šค ๋ฌธ์„œ์˜ ๋ฒˆํ˜ธ ๋ชฉ๋ก ์ƒ์„ฑ
- ์ธํ„ฐ๋ทฐ์–ด๋‚˜ ์ „๋ฌธ๊ฐ€์˜ ์ด๋ฆ„ ์–ธ๊ธ‰ํ•˜์ง€ ์•Š๊ธฐ
- ์ตœ๋Œ€ 400๋‹จ์–ด๋ฅผ ๋ชฉํ‘œ๋กœ ํ•จ
- ์†Œ์Šค ๋ฌธ์„œ์˜ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋ณด๊ณ ์„œ์— ๋ฒˆํ˜ธ๊ฐ€ ๋งค๊ฒจ์ง„ ์ถœ์ฒ˜ ์‚ฌ์šฉ (์˜ˆ: [1], [2])
        
6. ์ถœ์ฒ˜ ์„น์…˜:
- ๋ณด๊ณ ์„œ์— ์‚ฌ์šฉ๋œ ๋ชจ๋“  ์ถœ์ฒ˜ ํฌํ•จ
- ๊ด€๋ จ ์›น์‚ฌ์ดํŠธ๋‚˜ ํŠน์ • ๋ฌธ์„œ ๊ฒฝ๋กœ์˜ ์ „์ฒด ๋งํฌ ์ œ๊ณต
- ๊ฐ ์ถœ์ฒ˜๋ฅผ ์ค„๋ฐ”๊ฟˆ์œผ๋กœ ๊ตฌ๋ถ„. ๊ฐ ์ค„ ๋์— ๋‘ ๊ฐœ์˜ ๊ณต๋ฐฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋งˆํฌ๋‹ค์šด์—์„œ ์ค„๋ฐ”๊ฟˆ ์ƒ์„ฑ.
- ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณด์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค:

### ์ถœ์ฒ˜
[1] ๋งํฌ ๋˜๋Š” ๋ฌธ์„œ ์ด๋ฆ„
[2] ๋งํฌ ๋˜๋Š” ๋ฌธ์„œ ์ด๋ฆ„

7. ์ถœ์ฒ˜๋ฅผ ๋ฐ˜๋“œ์‹œ ๊ฒฐํ•ฉํ•˜์„ธ์š”. ์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ์€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค:

[3] https://ai.meta.com/blog/meta-llama-3-1/
[4] https://ai.meta.com/blog/meta-llama-3-1/

์ค‘๋ณต๋œ ์ถœ์ฒ˜๊ฐ€ ์—†์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฐ„๋‹จํžˆ ํ‘œ์‹œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

[3] https://ai.meta.com/blog/meta-llama-3-1/
        
8. ์ตœ์ข… ๊ฒ€ํ† :
- ๋ณด๊ณ ์„œ๊ฐ€ ํ•„์š”ํ•œ ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ฅด๋Š”์ง€ ํ™•์ธ
- ๋ณด๊ณ ์„œ ์ œ๋ชฉ ์ „์— ์„œ๋ฌธ์„ ํฌํ•จํ•˜์ง€ ์•Š์Œ
- ๋ชจ๋“  ์ง€์นจ์„ ๋”ฐ๋ž๋Š”์ง€ ํ™•์ธ
"""
  1. write_section ํ•จ์ˆ˜
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def write_section(state: InterviewState):
    """ ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜๋Š” ๋…ธ๋“œ """

    # ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ธฐ
    interview = state["interview"]
    context = state["context"]
    analyst = state["analyst"]
   
    # ์ธํ„ฐ๋ทฐ์—์„œ ์ˆ˜์ง‘ํ•œ ์†Œ์Šค ๋ฌธ์„œ(context) ๋˜๋Š” ์ธํ„ฐ๋ทฐ ์ž์ฒด(interview)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„น์…˜ ์ž‘์„ฑ
    system_message = section_writer_instructions.format(focus=analyst.description)
    
    section = llm.invoke(
    			[SystemMessage(content=system_message)]
                +
                [HumanMessage(content=f"์ด ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„น์…˜์„ ์ž‘์„ฑํ•˜์„ธ์š”: {context}")]) 
                
    # ์ƒํƒœ์— ์ถ”๊ฐ€
    return {"sections": [section.content]}
  • ๋ชฉ์ : ์ด ํ•จ์ˆ˜๋Š” InterviewState์—์„œ ์ˆ˜์ง‘ํ•œ ์ธํ„ฐ๋ทฐ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ตœ์ข… ๋ณด๊ณ ์„œ์˜ ๊ฐ ์„น์…˜์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ์ƒํƒœ ์š”์†Œ ๊ฐ€์ ธ์˜ค๊ธฐ:

    • interview: ์ธํ„ฐ๋ทฐ ๊ธฐ๋ก์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
    • context: ์ธํ„ฐ๋ทฐ์—์„œ ์ƒ์„ฑ๋œ ์†Œ์Šค ๋ฌธ์„œ๋‚˜ ์ฐธ๊ณ  ์ž๋ฃŒ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
    • analyst: ๋ถ„์„๊ฐ€์˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™€์„œ ํ•ด๋‹น ๋ถ„์„๊ฐ€๊ฐ€ ๊ฐ€์ง„ ํŠน์ • ๋ชฉํ‘œ(analyst.description)๋ฅผ ์„น์…˜ ์ž‘์„ฑ์— ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค.
  • ์„น์…˜ ์ž‘์„ฑ:

    • section_writer_instructions์˜ focus๋ฅผ ๋ถ„์„๊ฐ€์˜ ์„ค๋ช…(analyst.description)์œผ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
    • section_writer_instructions๋Š” ๋ณด๊ณ ์„œ๊ฐ€ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ž‘์„ฑ๋˜๋„๋ก ์ง€์นจ์„ ํฌํ•จํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
    • SystemMessage๋Š” ์„น์…˜ ์ž‘์„ฑ์„ ์œ„ํ•œ ์ง€์นจ์„ ์ „๋‹ฌํ•˜๊ณ , HumanMessage๋Š” ์ƒ์„ฑ๋œ context๋ฅผ ํฌํ•จํ•˜์—ฌ AI ๋ชจ๋ธ์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
      • ์ด๋ฅผ ํ†ตํ•ด ์ธํ„ฐ๋ทฐ ๋‚ด์šฉ์„ ๋ฐ˜์˜ํ•œ ์„น์…˜์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
  • ์„น์…˜ ์ถ”๊ฐ€:

    • sections๋ผ๋Š” ํ‚ค์— ํ•ด๋‹น ์„น์…˜์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ƒํƒœ์— ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    • ์ด ์„น์…˜์€ ๋ณด๊ณ ์„œ ์ž‘์„ฑ์˜ ์ตœ์ข… ๋‹จ๊ณ„๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค.

  1. StateGraph ์ •์˜ ๋ฐ ๋…ธ๋“œ ์ถ”๊ฐ€
1
2
3
4
5
6
7
interview_builder = StateGraph(InterviewState)
interview_builder.add_node("ask_question", generate_question)
interview_builder.add_node("search_web", search_web)
interview_builder.add_node("search_wikipedia", search_wikipedia)
interview_builder.add_node("answer_question", generate_answer)
interview_builder.add_node("save_interview", save_interview)
interview_builder.add_node("write_section", write_section)
  • StateGraph ์ •์˜: InterviewState๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํƒœ ๊ทธ๋ž˜ํ”„๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์ด ๊ทธ๋ž˜ํ”„๋Š” write_section๊นŒ์ง€ ์ด์–ด์ง€๋Š” ์ธํ„ฐ๋ทฐ ํ”„๋กœ์„ธ์Šค์˜ ๋ชจ๋“  ๋‹จ๊ณ„๋ณ„ ๋…ธ๋“œ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
  • ๋…ธ๋“œ ์ถ”๊ฐ€: ๊ฐ ๋‹จ๊ณ„์˜ ์ฃผ์š” ๊ธฐ๋Šฅ์„ ๋‹ด๋‹นํ•˜๋Š” ๋…ธ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
    • ask_question: ์ธํ„ฐ๋ทฐ ์งˆ๋ฌธ์„ ์ƒ์„ฑํ•˜๋Š” ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค.
    • search_web ๋ฐ search_wikipedia: ํ•„์š”ํ•œ ์ž๋ฃŒ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋Š” ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค.
    • answer_question: ์ „๋ฌธ๊ฐ€์˜ ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    • save_interview: ์ธํ„ฐ๋ทฐ ๋‚ด์šฉ์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
    • write_section: ์ธํ„ฐ๋ทฐ์˜ ์š”์ ์„ ์„น์…˜์œผ๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

  1. ์ธํ„ฐ๋ทฐ ๋‹จ๊ณ„์˜ ํ๋ฆ„ ์ •์˜
1
2
3
4
5
6
7
8
9
# ํ๋ฆ„
interview_builder.add_edge(START, "ask_question")
interview_builder.add_edge("ask_question", "search_web")
interview_builder.add_edge("ask_question", "search_wikipedia")
interview_builder.add_edge("search_web", "answer_question")
interview_builder.add_edge("search_wikipedia", "answer_question")
interview_builder.add_conditional_edges("answer_question", route_messages, ['ask_question','save_interview'])
interview_builder.add_edge("save_interview", "write_section")
interview_builder.add_edge("write_section", END)
  • ํ๋ฆ„ ์ •์˜:
    • START์—์„œ ask_question๋กœ ์—ฐ๊ฒฐํ•˜์—ฌ ์ธํ„ฐ๋ทฐ ์งˆ๋ฌธ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
    • ์งˆ๋ฌธ ํ›„ search_web ๋˜๋Š” search_wikipedia๋กœ ์ง„ํ–‰ํ•ด ๊ด€๋ จ ์ •๋ณด๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
    • ๊ฐ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋Š” answer_question ๋…ธ๋“œ๋กœ ์ „๋‹ฌ๋˜์–ด ์ธํ„ฐ๋ทฐ์˜ ๋‹ต๋ณ€์œผ๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค.
  • ์กฐ๊ฑด๋ถ€ ์—ฃ์ง€:
    • answer_question์—์„œ route_messages๋ฅผ ํ†ตํ•ด ๋Œ€ํ™”๋ฅผ ๊ณ„์† ์ง„ํ–‰ํ• ์ง€(ask_question์œผ๋กœ ๋Œ์•„๊ฐ) ์•„๋‹ˆ๋ฉด ์ธํ„ฐ๋ทฐ ์ €์žฅ ๋‹จ๊ณ„(save_interview)๋กœ ๋„˜์–ด๊ฐˆ์ง€๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
  • ๋งˆ์ง€๋ง‰ ๋‹จ๊ณ„:
    • ์ธํ„ฐ๋ทฐ๊ฐ€ ์ €์žฅ๋˜๋ฉด write_section์œผ๋กœ ๋„˜์–ด๊ฐ€ ๋ณด๊ณ ์„œ์˜ ์„น์…˜์„ ์ž‘์„ฑํ•œ ํ›„ ์ข…๋ฃŒ(END)๋ฉ๋‹ˆ๋‹ค.

  1. ๊ทธ๋ž˜ํ”„ ์ปดํŒŒ์ผ ๋ฐ ์ธํ„ฐ๋ทฐ ์‹คํ–‰
1
2
3
# ์ธํ„ฐ๋ทฐ 
memory = MemorySaver()
interview_graph = interview_builder.compile(checkpointer=memory).with_config(run_name="์ธํ„ฐ๋ทฐ ์ˆ˜ํ–‰")
  • MemorySaver: ์ธํ„ฐ๋ทฐ ์ค‘๊ฐ„ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜์—ฌ, ์ธํ„ฐ๋ทฐ ์ง„ํ–‰ ์ค‘์— ๋ฐœ์ƒํ•˜๋Š” ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ์ฒดํฌํฌ์ธํŠธ๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.
  • ๊ทธ๋ž˜ํ”„ ์ปดํŒŒ์ผ: ์ธํ„ฐ๋ทฐ ์ˆ˜ํ–‰์„ ์œ„ํ•ด ์ „์ฒด ๊ทธ๋ž˜ํ”„๋ฅผ ์ปดํŒŒ์ผํ•˜๋ฉฐ, ์ด๋ฅผ interview_graph๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

  1. ๊ทธ๋ž˜ํ”„ ์‹œ๊ฐํ™”
1
2
# ๋ณด๊ธฐ
display(Image(interview_graph.get_graph().draw_mermaid_png()))
  • ๊ทธ๋ž˜ํ”„ ์‹œ๊ฐํ™”: draw_mermaid_png() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํƒœ ๊ทธ๋ž˜ํ”„๋ฅผ ์‹œ๊ฐํ™”ํ•˜์—ฌ ํ๋ฆ„์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

(์ค‘๊ฐ„์ ๊ฒ€) ๋ถ„์„๊ฐ€ ํ•œ๋ช…์„ ์„ ํƒํ•ด์„œ ๊ธ€ ์ž‘์„ฑ์„ ์š”์ฒญํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ํ•œ ๋ช…์„ ์„ ํƒํ•ด์„œ ํ•ด๋‹น ์ฃผ์ œ์— ๋Œ€ํ•œ ๊ธ€์„ ์š”์ฒญํ•ด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
1
2
# ๋ถ„์„๊ฐ€ ํ•œ ๋ช… ์„ ํƒ
analysts[0]
1
2
3
4
5
6
Analyst(
affiliation='Tech Innovators Inc.', 
name='Dr. Emily Zhang', 
role='AI Research Scientist', 
description='Dr. Zhang focuses on the development and optimization of large language models (LLMs). Her interest lies in improving model efficiency and scalability.'
)
  • ๊ธ€ ์ž‘์„ฑ ์š”์ฒญ ํ›„ ๊ฒฐ๊ณผ๋ฅผ ํ•œ๋ฒˆ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
1
2
3
4
5
from IPython.display import Markdown
messages = [HumanMessage(f"๋‹น์‹ ์ด {topic}์— ๋Œ€ํ•œ ๊ธ€์„ ์“ฐ๊ณ  ์žˆ๋‹ค๊ณ  ํ–ˆ๋‚˜์š”?")]
thread = {"configurable": {"thread_id": "1"}}
interview = interview_graph.invoke({"analyst": analysts[0], "messages": messages, "max_num_turns": 3}, thread)
Markdown(interview['sections'][0])

  • ์—„์ฒญ ๊ทธ๋Ÿด ๋“ฏํ•˜๊ฒŒ LLMOps์— ๋Œ€ํ•œ ๊ธ€์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ์•ž์— ์„ธ์šด ๋ผ์šฐํŒ… ๊ทœ์น™์— ์˜ํ•ด max_num_turns 3ํšŒ๊นŒ์ง€ ๋Œ๊ณ , ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

(๊ฒฐ๊ณผํ•ด์„) ์ถœ๋ ฅ ๊ฒฐ๊ณผ๋ฅผ ์ข€ ๋” ์„ธ๋ถ„ํ™”ํ•ด์„œ ํ•ด์„ํ•ด๋ณผ๊นŒ์š”?

  • ์ถœ๋ ฅ๋œ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด, ๋Œ€ํ™” ์‹œ๋‚˜๋ฆฌ์˜ค์™€ ๋ฌธ์„œ ๊ธฐ๋ฐ˜ ์„น์…˜ ์ž‘์„ฑ์ด ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • ๊ตฌ์ฒด์ ์œผ๋กœ ํ™•์ธํ•ด๋ณด๋ฉด, ์ฃผ์š” ์ ˆ์ฐจ๊ฐ€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
    • ๋Œ€ํ™” ์ง„ํ–‰: ๋Œ€ํ™” ํ๋ฆ„์ด HumanMessage์™€ AIMessage์˜ ๋ฐ˜๋ณต์œผ๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ด์–ด์กŒ์Šต๋‹ˆ๋‹ค. ๊ฐ HumanMessage์— ๋Œ€ํ•ด AIMessage๊ฐ€ ๋…ผ๋ฆฌ์ ์ธ ๋‹ต๋ณ€์„ ์ œ๊ณตํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, LLMOps ๊ด€๋ จ ์ฃผ์š” ๊ฐœ๋…๊ณผ ๊ตฌํ˜„ ๋„์ „ ๊ณผ์ œ๋“ค์ด ์„ค๋ช…๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๋ฌธ์„œ ์ปจํ…์ŠคํŠธ ํ™œ์šฉ: context๋กœ ์ง€์ •๋œ ๋ฌธ์„œ๋“ค์ด section_writer_instructions์— ๋”ฐ๋ผ ์š”์•ฝ ๋ฐ ๋ฌธ์„œ ๊ตฌ์„ฑ์„ ์œ„ํ•ด ์‚ฌ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. write_section ํ•จ์ˆ˜์—์„œ ๋ฌธ์„œ ๋‚ด์šฉ์ด ์„น์…˜์— ์ ์ ˆํžˆ ๋ฐ˜์˜๋˜์–ด, LLMOps์˜ ์ฃผ์š” ๊ฐœ๋…๊ณผ ์ ์šฉ ์‚ฌ๋ก€ ๋ฐ ๋ชจ๋ฒ” ์‚ฌ๋ก€๊ฐ€ ํฌํ•จ๋œ ์š”์•ฝ์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์„น์…˜ ์ž‘์„ฑ: sections ํ•„๋“œ์— ์ €์žฅ๋œ ๋ณด๊ณ ์„œ๋Š” ##์™€ ### ํ˜•์‹์„ ์‚ฌ์šฉํ•ด ๊ตฌ์กฐ์ ์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ๊ณ , ์ถœ์ฒ˜ ๋ชฉ๋ก์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๊ฐ ์ถœ์ฒ˜๊ฐ€ [1], [2] ๋“ฑ์œผ๋กœ ์ œ๋Œ€๋กœ ๋งํฌ๋˜์—ˆ์œผ๋ฉฐ, ๋ชจ๋ฒ” ์‚ฌ๋ก€์™€ LLMOps์˜ ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ๋„ ์š”์•ฝ์— ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ด์ œ ๊ฐ๊ฐ์˜ Analyst๋“ค์„ ํ‰ํ–‰ํ•˜๊ฒŒ ๊ทธ๋ž˜ํ”„ ์ƒ์—์„œ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โœ”๏ธ Parallelize ์ธํ„ฐ๋ทฐ: Map-Reduce ํŒจํ„ด ์ ์šฉ

์ด ๊ตฌ์กฐ์—์„œ๋Š” ์ธํ„ฐ๋ทฐ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์ˆ˜ํ–‰ํ•˜์—ฌ ์‹œ๊ฐ„์„ ์ ˆ์•ฝํ•˜๊ณ , ์ตœ์ข…์ ์œผ๋กœ ๋ฆฌํฌํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•ด ๊ฒฐ๊ณผ๋ฅผ ๊ฒฐํ•ฉํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • ๋งต(Map) ๋‹จ๊ณ„:

    • initiate_all_interviews ํ•จ์ˆ˜๊ฐ€ Send() API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ณ‘๋ ฌ๋กœ ์ธํ„ฐ๋ทฐ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.
    • Send()๋Š” ๊ฐ ๋ถ„์„๊ฐ€์™€ ์ „๋ฌธ๊ฐ€ ๊ฐ„ ์ธํ„ฐ๋ทฐ ์„ธ์…˜์„ ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰ํ•˜๋ฉฐ, ๊ฐ ์ธํ„ฐ๋ทฐ๊ฐ€ ๋ณ„๋„๋กœ ์ง„ํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— ์—ฌ๋Ÿฌ ์ธํ„ฐ๋ทฐ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•˜์—ฌ ์‹œ๊ฐ„ ํšจ์œจ์„ฑ์„ ๋†’์ž…๋‹ˆ๋‹ค.
    • ์ด ๋‹จ๊ณ„์—์„œ๋Š” ๊ฐ ์ธํ„ฐ๋ทฐ์˜ ๊ฒฐ๊ณผ๊ฐ€ ResearchGraphState์— sections ๋ฆฌ์ŠคํŠธ์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
  • ๋ฆฌ๋“€์Šค(Reduce) ๋‹จ๊ณ„:

    • write_report ํ•จ์ˆ˜๋Š” sections ๋ฆฌ์ŠคํŠธ์— ์ €์žฅ๋œ ๋ชจ๋“  ์ธํ„ฐ๋ทฐ ์„ธ์…˜์„ ๊ฒฐํ•ฉํ•˜์—ฌ ์ตœ์ข… ๋ณด๊ณ ์„œ ๋‚ด์šฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    • ์ดํ›„ write_introduction๊ณผ write_conclusion ํ•จ์ˆ˜๊ฐ€ ๊ฐ๊ฐ ์„œ๋ก ๊ณผ ๊ฒฐ๋ก ์„ ์ž‘์„ฑํ•˜์—ฌ ์ตœ์ข… ๋ณด๊ณ ์„œ์— ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.
    • ์ตœ์ข… ๋‹จ๊ณ„์ธ finalize_report์—์„œ๋Š” ์„œ๋ก , ๋ณธ๋ฌธ, ๊ฒฐ๋ก ์„ ํ•˜๋‚˜์˜ ์™„์„ฑ๋œ ๋ณด๊ณ ์„œ๋กœ ํ•ฉ์ณ ๋ฆฌํฌํŠธ๋ฅผ ์™„์„ฑํ•ฉ๋‹ˆ๋‹ค.
  1. ResearchGraphState ํด๋ž˜์Šค ์ •์˜
  • ์—ญํ• : ์—ฐ๊ตฌ ์ฃผ์ œ, ๋ถ„์„๊ฐ€ ์ˆ˜, ํ”ผ๋“œ๋ฐฑ, ๋ณด๊ณ ์„œ ์„œ๋ก  ๋ฐ ๊ฒฐ๋ก  ๋“ฑ ์ƒํƒœ ์ •๋ณด๋ฅผ ์ถ”์ ํ•˜์—ฌ, ์ „์ฒด ์‹คํ–‰ ํ๋ฆ„ ์ค‘ ๋ฐ์ดํ„ฐ๋ฅผ ์‰ฝ๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ  ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import operator
from typing import List, Annotated
from typing_extensions import TypedDict

class ResearchGraphState(TypedDict):
    topic: str # ์—ฐ๊ตฌ ์ฃผ์ œ
    max_analysts: int # ๋ถ„์„๊ฐ€ ์ˆ˜
    human_analyst_feedback: str # ์ธ๊ฐ„ ํ”ผ๋“œ๋ฐฑ
    analysts: List[Analyst] # ์งˆ๋ฌธํ•˜๋Š” ๋ถ„์„๊ฐ€
    sections: Annotated[list, operator.add] # Send() API ํ‚ค
    introduction: str # ์ตœ์ข… ๋ณด๊ณ ์„œ์˜ ์„œ๋ก 
    content: str # ์ตœ์ข… ๋ณด๊ณ ์„œ์˜ ๋‚ด์šฉ
    conclusion: str # ์ตœ์ข… ๋ณด๊ณ ์„œ์˜ ๊ฒฐ๋ก 
    final_report: str # ์ตœ์ข… ๋ณด๊ณ ์„œ

  • TypedDict๋กœ ์ง€์ •ํ•˜์—ฌ ์ƒํƒœ๊ฐ€ ํŠน์ •ํ•œ ํ‚ค์™€ ๊ฐ’์„ ๊ฐ–๋„๋ก ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
  • Annotated๋กœ ์—ฌ๋Ÿฌ sections๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋กœ ๋ณ‘ํ•ฉํ•˜์—ฌ ์ €์žฅํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
  1. initiate_all_interviews ํ•จ์ˆ˜: ์ธํ„ฐ๋ทฐ ๋ณ‘๋ ฌํ™” ์‹คํ–‰
  • ์—ญํ• : ์ธํ„ฐ๋ทฐ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์ˆ˜ํ–‰ํ•˜๋Š” โ€œ๋งตโ€ ๋‹จ๊ณ„.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def initiate_all_interviews(state: ResearchGraphState):
    """ ์ด๋Š” Send API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ ์ธํ„ฐ๋ทฐ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋ฅผ ์‹คํ–‰ํ•˜๋Š” "๋งต" ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค """    

    # ์ธ๊ฐ„ ํ”ผ๋“œ๋ฐฑ ํ™•์ธ
    human_analyst_feedback=state.get('human_analyst_feedback')
    if human_analyst_feedback:
        # create_analysts๋กœ ๋Œ์•„๊ฐ€๊ธฐ
        return "create_analysts"

    # ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด Send() API๋ฅผ ํ†ตํ•ด ์ธํ„ฐ๋ทฐ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์‹œ์ž‘
    else:
        topic = state["topic"]
        return [Send("conduct_interview", {"analyst": analyst,
                                           "messages": [HumanMessage(
                                               content=f"๋‹น์‹ ์ด {topic}์— ๋Œ€ํ•œ ๊ธ€์„ ์“ฐ๊ณ  ์žˆ๋‹ค๊ณ  ํ–ˆ๋‚˜์š”?"
                                           )
                                                       ]}) for analyst in state["analysts"]]
  • human_analyst_feedback์ด ์žˆ์„ ๊ฒฝ์šฐ, ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ˜์˜ํ•ด create_analysts๋กœ ๋Œ์•„๊ฐ€ ๋ถ„์„๊ฐ€๋ฅผ ์ƒˆ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ํ”ผ๋“œ๋ฐฑ์ด ์—†์œผ๋ฉด Send() API๋ฅผ ์‚ฌ์šฉํ•ด ์ธํ„ฐ๋ทฐ๋ฅผ ๊ฐ ๋ถ„์„๊ฐ€์™€ ๋…๋ฆฝ์ ์œผ๋กœ ๋ณ‘๋ ฌ๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

report_writer_instructions

  • ํ•ด๋‹น ์ง€์นจ์€ ๋ถ„์„๊ฐ€ ํŒ€์ด ์ œ๊ณตํ•œ ๋ฉ”๋ชจ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋ณด๊ณ ์„œ ๋ณธ๋ฌธ์„ ์ž‘์„ฑํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ์„ธ๋ถ€์‚ฌํ•ญ์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค.
    • ์ด ์ง€์นจ์€ ๋ณด๊ณ ์„œ ์ž‘์„ฑ์ž๊ฐ€ ์ผ๊ด€์„ฑ ์žˆ๊ณ  ์ถœ์ฒ˜๊ฐ€ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ๋œ ๋ณด๊ณ ์„œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋ฉฐ, ์ œ๊ณต๋œ ์ž๋ฃŒ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ†ต์ฐฐ๋ ฅ ์žˆ๋Š” ์š”์•ฝ์„ ๋งŒ๋“ค๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
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
report_writer_instructions = """

๋‹น์‹ ์€ ๋‹ค์Œ ์ „์ฒด ์ฃผ์ œ์— ๋Œ€ํ•œ ๋ณด๊ณ ์„œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ธฐ์ˆ  ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค: 

{topic}
    
๋‹น์‹ ์—๊ฒŒ๋Š” ๋ถ„์„๊ฐ€ ํŒ€์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ๋ถ„์„๊ฐ€๋Š” ๋‘ ๊ฐ€์ง€ ์ผ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค: 

1. ํŠน์ • ํ•˜์œ„ ์ฃผ์ œ์— ๋Œ€ํ•ด ์ „๋ฌธ๊ฐ€์™€ ์ธํ„ฐ๋ทฐ๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.
2. ๊ทธ๋“ค์˜ ๋ฐœ๊ฒฌ์„ ๋ฉ”๋ชจ๋กœ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

๋‹น์‹ ์˜ ์ž„๋ฌด: 

1. ๋ถ„์„๊ฐ€๋“ค์˜ ๋ฉ”๋ชจ ๋ชจ์Œ์ด ์ฃผ์–ด์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค.
2. ๊ฐ ๋ฉ”๋ชจ์˜ ํ†ต์ฐฐ์„ ์‹ ์ค‘ํžˆ ์ƒ๊ฐํ•ด๋ณด์„ธ์š”.
3. ๋ชจ๋“  ๋ฉ”๋ชจ์˜ ์ค‘์‹ฌ ์•„์ด๋””์–ด๋ฅผ ํ•˜๋‚˜์˜ ๊ฐ„๊ฒฐํ•œ ์ „์ฒด ์š”์•ฝ์œผ๋กœ ํ†ตํ•ฉํ•˜์„ธ์š”. 
4. ๊ฐ ๋ฉ”๋ชจ์˜ ์ค‘์‹ฌ์ ์„ ํ•˜๋‚˜์˜ ์ผ๊ด€๋œ ๋‚ด๋Ÿฌํ‹ฐ๋ธŒ๋กœ ์š”์•ฝํ•˜์„ธ์š”.

๋ณด๊ณ ์„œ ํ˜•์‹:
 
1. ๋งˆํฌ๋‹ค์šด ํ˜•์‹์„ ์‚ฌ์šฉํ•˜์„ธ์š”. 
2. ๋ณด๊ณ ์„œ์— ์„œ๋ฌธ์„ ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”.
3. ์†Œ์ œ๋ชฉ์„ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”. 
4. ๋‹จ์ผ ์ œ๋ชฉ ํ—ค๋”๋กœ ๋ณด๊ณ ์„œ๋ฅผ ์‹œ์ž‘ํ•˜์„ธ์š”: ## ํ†ต์ฐฐ
5. ๋ณด๊ณ ์„œ์— ๋ถ„์„๊ฐ€ ์ด๋ฆ„์„ ์–ธ๊ธ‰ํ•˜์ง€ ๋งˆ์„ธ์š”.
6. ๋ฉ”๋ชจ์— ์žˆ๋Š” ์ธ์šฉ์„ ๋ณด์กดํ•˜์„ธ์š”. ์ธ์šฉ์€ [1] ๋˜๋Š” [2]์™€ ๊ฐ™์ด ๋Œ€๊ด„ํ˜ธ ์•ˆ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
7. ์ตœ์ข… ํ†ตํ•ฉ ์ถœ์ฒ˜ ๋ชฉ๋ก์„ ๋งŒ๋“ค๊ณ  '## ์ถœ์ฒ˜' ํ—ค๋”๊ฐ€ ์žˆ๋Š” ์ถœ์ฒ˜ ์„น์…˜์— ์ถ”๊ฐ€ํ•˜์„ธ์š”.
8. ์ถœ์ฒ˜๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ๋‚˜์—ดํ•˜๊ณ  ๋ฐ˜๋ณตํ•˜์ง€ ๋งˆ์„ธ์š”.

[1] ์ถœ์ฒ˜ 1
[2] ์ถœ์ฒ˜ 2

๋‹ค์Œ์€ ๋ณด๊ณ ์„œ ์ž‘์„ฑ์— ์‚ฌ์šฉํ•  ๋ถ„์„๊ฐ€๋“ค์˜ ๋ฉ”๋ชจ์ž…๋‹ˆ๋‹ค: 

{context}
"""
  1. write_report ํ•จ์ˆ˜: ๋ณด๊ณ ์„œ ์ž‘์„ฑ
  • ์—ญํ• : sections์— ์žˆ๋Š” ์ธํ„ฐ๋ทฐ ๋‚ด์šฉ์„ ๊ฒฐํ•ฉํ•˜์—ฌ ์ตœ์ข… ๋ณด๊ณ ์„œ๋ฅผ ์ž‘์„ฑ.
1
2
3
4
5
6
7
8
9
10
11
12
def write_report(state: ResearchGraphState):
    # ์ „์ฒด ์„น์…˜ ์„ธํŠธ
    sections = state["sections"]
    topic = state["topic"]

    # ๋ชจ๋“  ์„น์…˜์„ ํ•˜๋‚˜๋กœ ์—ฐ๊ฒฐ
    formatted_str_sections = "\n\n".join([f"{section}" for section in sections])
    
    # ์„น์…˜์„ ์ตœ์ข… ๋ณด๊ณ ์„œ๋กœ ์š”์•ฝ
    system_message = report_writer_instructions.format(topic=topic, context=formatted_str_sections)    
    report = llm.invoke([SystemMessage(content=system_message)]+[HumanMessage(content=f"์ด ๋ฉ”๋ชจ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋ณด๊ณ ์„œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”.")]) 
    return {"content": report.content}
  • report_writer_instructions์—์„œ ์ฃผ์–ด์ง„ ์–‘์‹์— ๋”ฐ๋ผ llm.invoke()๋ฅผ ํ†ตํ•ด ๋ณด๊ณ ์„œ ๋ณธ๋ฌธ์„ ์ƒ์„ฑ.
  • ์ตœ์ข… ๋ณด๊ณ ์„œ ๋ณธ๋ฌธ์€ "content" ํ•„๋“œ์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

intro_conclusion_instructions

  • ํ•ด๋‹น ์ง€์นจ์€ ๋ณด๊ณ ์„œ์— ์„œ๋ก  ๋˜๋Š” ๊ฒฐ๋ก ์„ ์ž‘์„ฑํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ์„ธ๋ถ€ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋ณด๊ณ ์„œ ์ „์ฒด๋ฅผ ์š”์•ฝํ•˜๊ณ  ๋ฐฉํ–ฅ์„ฑ์„ ์ œ๊ณตํ•˜๊ฑฐ๋‚˜, ์ „์ฒด ๋ณด๊ณ ์„œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
    • ์ด ์ง€์นจ์„ ํ†ตํ•ด ์ž‘์„ฑ์ž๋Š” ๋ณด๊ณ ์„œ ์ „์ฒด ๋‚ด์šฉ์„ ๊ฐ„๊ฒฐํ•˜๊ณ  ์„ค๋“๋ ฅ ์žˆ๊ฒŒ ์ •๋ฆฌํ•˜์—ฌ, ๋…์ž๋“ค์ด ๋ณด๊ณ ์„œ์˜ ์ฃผ์š” ๋‚ด์šฉ์„ ๋น ๋ฅด๊ฒŒ ํŒŒ์•…ํ•˜๊ฑฐ๋‚˜ ๊ฒฐ๋ก ์„ ํ†ตํ•ด ์ •๋ฆฌ๋œ ํ†ต์ฐฐ์„ ์–ป๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
intro_conclusion_instructions = """
๋‹น์‹ ์€ {topic}์— ๋Œ€ํ•œ ๋ณด๊ณ ์„œ๋ฅผ ๋งˆ๋ฌด๋ฆฌํ•˜๋Š” ๊ธฐ์ˆ  ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค.

๋ณด๊ณ ์„œ์˜ ๋ชจ๋“  ์„น์…˜์ด ์ฃผ์–ด์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋‹น์‹ ์˜ ์ž„๋ฌด๋Š” ๊ฐ„๊ฒฐํ•˜๊ณ  ์„ค๋“๋ ฅ ์žˆ๋Š” ์„œ๋ก  ๋˜๋Š” ๊ฒฐ๋ก  ์„น์…˜์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž๊ฐ€ ์„œ๋ก  ๋˜๋Š” ๊ฒฐ๋ก  ์ค‘ ์–ด๋А ๊ฒƒ์„ ์ž‘์„ฑํ• ์ง€ ์ง€์‹œํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋‘ ์„น์…˜ ๋ชจ๋‘ ์„œ๋ฌธ์„ ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”.

์•ฝ 100๋‹จ์–ด๋ฅผ ๋ชฉํ‘œ๋กœ ํ•˜์—ฌ ๋ณด๊ณ ์„œ์˜ ๋ชจ๋“  ์„น์…˜์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋ฏธ๋ฆฌ ๋ณด์—ฌ์ฃผ๊ฑฐ๋‚˜(์„œ๋ก ์˜ ๊ฒฝ์šฐ) ์š”์•ฝํ•˜์„ธ์š”(๊ฒฐ๋ก ์˜ ๊ฒฝ์šฐ).

๋งˆํฌ๋‹ค์šด ํ˜•์‹์„ ์‚ฌ์šฉํ•˜์„ธ์š”. 

์„œ๋ก ์˜ ๊ฒฝ์šฐ, ๋งค๋ ฅ์ ์ธ ์ œ๋ชฉ์„ ๋งŒ๋“ค๊ณ  # ํ—ค๋”๋ฅผ ์ œ๋ชฉ์— ์‚ฌ์šฉํ•˜์„ธ์š”.

์„œ๋ก ์˜ ๊ฒฝ์šฐ, ## ์„œ๋ก ์„ ์„น์…˜ ํ—ค๋”๋กœ ์‚ฌ์šฉํ•˜์„ธ์š”. 

๊ฒฐ๋ก ์˜ ๊ฒฝ์šฐ, ## ๊ฒฐ๋ก ์„ ์„น์…˜ ํ—ค๋”๋กœ ์‚ฌ์šฉํ•˜์„ธ์š”.

๋‹ค์Œ์€ ์ž‘์„ฑ ์‹œ ์ฐธ๊ณ ํ•  ์„น์…˜๋“ค์ž…๋‹ˆ๋‹ค: {formatted_str_sections}
"""
  1. write_introduction ๋ฐ write_conclusion ํ•จ์ˆ˜: ์„œ๋ก  ๋ฐ ๊ฒฐ๋ก  ์ž‘์„ฑ
  • ์—ญํ• : ๋ณด๊ณ ์„œ์— ์„œ๋ก  ๋ฐ ๊ฒฐ๋ก ์„ ์ถ”๊ฐ€ํ•ด ๊ตฌ์กฐํ™”.
1
2
3
4
5
6
7
8
9
10
11
12
def write_introduction(state: ResearchGraphState):
    # ์ „์ฒด ์„น์…˜ ์„ธํŠธ
    sections = state["sections"]
    topic = state["topic"]

    # ๋ชจ๋“  ์„น์…˜์„ ํ•˜๋‚˜๋กœ ์—ฐ๊ฒฐ
    formatted_str_sections = "\n\n".join([f"{section}" for section in sections])
    
    # ์„น์…˜์„ ์ตœ์ข… ๋ณด๊ณ ์„œ๋กœ ์š”์•ฝ
    instructions = intro_conclusion_instructions.format(topic=topic, formatted_str_sections=formatted_str_sections)    
    intro = llm.invoke([instructions]+[HumanMessage(content=f"๋ณด๊ณ ์„œ ์„œ๋ก ์„ ์ž‘์„ฑํ•˜์„ธ์š”")]) 
    return {"introduction": intro.content}
1
2
3
4
5
6
7
8
9
10
11
12
def write_conclusion(state: ResearchGraphState):
    # ์ „์ฒด ์„น์…˜ ์„ธํŠธ
    sections = state["sections"]
    topic = state["topic"]

    # ๋ชจ๋“  ์„น์…˜์„ ํ•˜๋‚˜๋กœ ์—ฐ๊ฒฐ
    formatted_str_sections = "\n\n".join([f"{section}" for section in sections])
    
    # ์„น์…˜์„ ์ตœ์ข… ๋ณด๊ณ ์„œ๋กœ ์š”์•ฝ
    instructions = intro_conclusion_instructions.format(topic=topic, formatted_str_sections=formatted_str_sections)    
    conclusion = llm.invoke([instructions]+[HumanMessage(content=f"๋ณด๊ณ ์„œ ๊ฒฐ๋ก ์„ ์ž‘์„ฑํ•˜์„ธ์š”")]) 
    return {"conclusion": conclusion.content}
  • intro_conclusion_instructions์— ๋”ฐ๋ผ, ์„œ๋ก ์€ ๋ณด๊ณ ์„œ์˜ ๊ฐ„๊ฒฐํ•œ ์š”์•ฝ์„, ๊ฒฐ๋ก ์€ ์š”์•ฝ ๋ฐ ๋งˆ๋ฌด๋ฆฌ ๋‚ด์šฉ์„ ๋‹ด์Šต๋‹ˆ๋‹ค.
  • write_introduction ๋ฐ write_conclusion์€ ๊ฐ๊ฐ introduction, conclusion ํ•„๋“œ์— ์ €์žฅ.
  1. finalize_report ํ•จ์ˆ˜: ์ตœ์ข… ๋ณด๊ณ ์„œ ๊ฒฐํ•ฉ ๋ฐ ์™„๋ฃŒ
  • ์—ญํ• : ์„œ๋ก , ๋ณธ๋ฌธ, ๊ฒฐ๋ก ์„ ํ•˜๋‚˜์˜ ์™„์„ฑ๋œ ๋ณด๊ณ ์„œ๋กœ ๊ฒฐํ•ฉํ•˜๋Š” โ€œ๋ฆฌ๋“€์Šคโ€ ๋‹จ๊ณ„.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def finalize_report(state: ResearchGraphState):
    """ ์ด๋Š” ๋ชจ๋“  ์„น์…˜์„ ์ˆ˜์ง‘ํ•˜๊ณ  ๊ฒฐํ•ฉํ•œ ๋‹ค์Œ ์„œ๋ก /๊ฒฐ๋ก ์„ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•ด ๊ทธ๋“ค์„ ๋ฐ˜์˜ํ•˜๋Š” "๋ฆฌ๋“€์Šค" ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค """
    # ์ „์ฒด ์ตœ์ข… ๋ณด๊ณ ์„œ ์ €์žฅ
    content = state["content"]
    if content.startswith("## ํ†ต์ฐฐ"):
        content = content.strip("## ํ†ต์ฐฐ")
    if "## ์ถœ์ฒ˜" in content:
        try:
            content, sources = content.split("\n## ์ถœ์ฒ˜\n")
        except:
            sources = None
    else:
        sources = None

    final_report = state["introduction"] + "\n\n---\n\n" + content + "\n\n---\n\n" + state["conclusion"]
    if sources is not None:
        final_report += "\n\n## ์ถœ์ฒ˜\n" + sources
    return {"final_report": final_report}
  • content, introduction, conclusion, sources๋ฅผ ํ•ฉ์ณ ์ตœ์ข… ๋ณด๊ณ ์„œ(final_report)์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

  1. StateGraph ๋…ธ๋“œ ๋ฐ ์—ฃ์ง€ ์ •์˜
  • ์—ญํ• : ์ธํ„ฐ๋ทฐ ์ƒ์„ฑ, ํ”ผ๋“œ๋ฐฑ ์ฒ˜๋ฆฌ, ์ธํ„ฐ๋ทฐ ์ง„ํ–‰, ๋ณด๊ณ ์„œ ์ž‘์„ฑ, ์„œ๋ก  ๋ฐ ๊ฒฐ๋ก  ์ž‘์„ฑ, ์ตœ์ข… ๋ณด๊ณ ์„œ ์™„์„ฑ์„ ์œ„ํ•œ ๋…ธ๋“œ์™€ ์—ฃ์ง€ ์ •์˜.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ๋…ธ๋“œ์™€ ์—ฃ์ง€ ์ถ”๊ฐ€ 
builder = StateGraph(ResearchGraphState)
builder.add_node("create_analysts", create_analysts)
builder.add_node("human_feedback", human_feedback)
builder.add_node("conduct_interview", interview_builder.compile())
builder.add_node("write_report",write_report)
builder.add_node("write_introduction",write_introduction)
builder.add_node("write_conclusion",write_conclusion)
builder.add_node("finalize_report",finalize_report)

# ๋กœ์ง
builder.add_edge(START, "create_analysts")
builder.add_edge("create_analysts", "human_feedback")
builder.add_conditional_edges("human_feedback", initiate_all_interviews, ["create_analysts", "conduct_interview"])
builder.add_edge("conduct_interview", "write_report")
builder.add_edge("conduct_interview", "write_introduction")
builder.add_edge("conduct_interview", "write_conclusion")
builder.add_edge(["write_conclusion", "write_report", "write_introduction"], "finalize_report")
builder.add_edge("finalize_report", END)
  • add_conditional_edges๋กœ human_feedback ์กฐ๊ฑด์„ ์ถฉ์กฑํ•  ๊ฒฝ์šฐ ์ƒˆ๋กœ์šด ๋ถ„์„๊ฐ€๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ์ธํ„ฐ๋ทฐ๋ฅผ ์‹œ์ž‘ํ•˜๋„๋ก ๊ตฌ์„ฑ.
  • ์—ฌ๊ธฐ์„œ interview_builder๋ฅผ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋กœ ์ฒ˜๋ฆฌํ•œ ๋ฐฉ์‹์€ ์ธํ„ฐ๋ทฐ ์ˆ˜ํ–‰ ๊ณผ์ •์„ ๋…๋ฆฝ์ ์ธ ๊ทธ๋ž˜ํ”„๋กœ ๊ตฌ์„ฑํ•˜์—ฌ ๋ชจ๋“ˆํ™”ํ•˜๊ณ , ์ด๋ฅผ ์ฃผ ๊ทธ๋ž˜ํ”„์ธ StateGraph์˜ ํŠน์ • ๋…ธ๋“œ๋กœ ์ปดํŒŒ์ผํ•˜์—ฌ ์žฌ์‚ฌ์šฉํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.
    • ์ด ๊ณผ์ •์€ ๋ณต์žกํ•œ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ณ , StateGraph์˜ ๋…ธ๋“œ ๊ฐ„ ๋‹จ๊ณ„๋ณ„ ํ๋ฆ„์„ ๋” ์‰ฝ๊ฒŒ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.
    • interview_builder๋Š” ์ธํ„ฐ๋ทฐ ์ˆ˜ํ–‰์˜ ๊ฐ ๋‹จ๊ณ„๋ฅผ ํฌํ•จํ•˜๋Š” ์ž‘์€ ๊ทธ๋ž˜ํ”„๋กœ, ๋…๋ฆฝ์ ์ธ StateGraph ์ธ์Šคํ„ด์Šค๋กœ ์ž‘์„ฑ๋ฉ๋‹ˆ๋‹ค.
    • interview_builder ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋Š” ์ด์ „ ๋‹จ๊ณ„์—์„œ ๋งŒ๋“  ask_question, search_web, search_wikipedia, answer_question, save_interview, write_section ๋…ธ๋“œ์™€ ์ด๋“ค์„ ์ž‡๋Š” ์—ฃ์ง€๋กœ ๊ตฌ์„ฑ๋˜์–ด, ์ธํ„ฐ๋ทฐ์™€ ๊ด€๋ จ๋œ ๋ชจ๋“  ๋กœ์ง์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  1. ๊ทธ๋ž˜ํ”„ ์‹คํ–‰ ๋ฐ ํ™•์ธ
  • ์—ญํ• : ๊ทธ๋ž˜ํ”„๋ฅผ ์ปดํŒŒ์ผํ•˜๊ณ  ๊ฐ ๋‹จ๊ณ„์˜ ์ƒํƒœ๋ฅผ ์‹œ๊ฐํ™”ํ•˜์—ฌ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.
1
2
3
4
# ์ปดํŒŒ์ผ
memory = MemorySaver()
graph = builder.compile(interrupt_before=['human_feedback'], checkpointer=memory)
display(Image(graph.get_graph(xray=1).draw_mermaid_png()))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ์ž…๋ ฅ
max_analysts = 3 
topic = "LLMOps ํ”Œ๋žซํผ ๊ตฌ์ถ•"
thread = {"configurable": {"thread_id": "1"}}

# ์ฒซ ๋ฒˆ์งธ ์ค‘๋‹จ๊นŒ์ง€ ๊ทธ๋ž˜ํ”„ ์‹คํ–‰
for event in graph.stream({"topic":topic,
                           "max_analysts":max_analysts}, 
                          thread, 
                          stream_mode="values"):
    
    analysts = event.get('analysts', '')
    if analysts:
        for analyst in analysts:
            print(f"์ด๋ฆ„: {analyst.name}")
            print(f"์†Œ์†: {analyst.affiliation}")
            print(f"์—ญํ• : {analyst.role}")
            print(f"์„ค๋ช…: {analyst.description}")
            print("-" * 50)  
  • graph.stream์œผ๋กœ ๊ทธ๋ž˜ํ”„๊ฐ€ ํ๋ฆ„์— ๋”ฐ๋ผ ๋‹จ๊ณ„๋ณ„๋กœ ์ง„ํ–‰๋˜๋ฉฐ, ์ƒํƒœ์— ์ €์žฅ๋œ ๋ถ„์„๊ฐ€ ์ •๋ณด๋„ ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
# ๋‚˜๋จธ์ง€๋„ ๋๊นŒ์ง€ ์‹คํ–‰ํ•ด์ค๋‹ˆ๋‹ค
graph.update_state(thread, {"human_analyst_feedback": 
                            None}, as_node="human_feedback")
                            
# ๊ณ„์†
for event in graph.stream(None, thread, stream_mode="updates"):
    print("--๋…ธ๋“œ--")
    node_name = next(iter(event.keys()))
    print(node_name)
    
  • ์˜๋„ํ•œ ๊ฒƒ์ฒ˜๋Ÿผ conduct_interview๋“ค์ด ๋ณ‘๋ ฌ์ ์œผ๋กœ ์ˆ˜ํ–‰๋˜๊ณ , write ์ž‘์—…๋“ค์ด ์ˆ˜ํ–‰๋œ ๋’ค์—, finalize_report์ด ์ˆ˜ํ–‰๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
--๋…ธ๋“œ--
conduct_interview
--๋…ธ๋“œ--
conduct_interview
--๋…ธ๋“œ--
conduct_interview
--๋…ธ๋“œ--
write_introduction
--๋…ธ๋“œ--
write_conclusion
--๋…ธ๋“œ--
write_report
--๋…ธ๋“œ--
finalize_report
  1. ์ตœ์ข… ๋ณด๊ณ ์„œ ๊ฒฐ๊ณผ ์ถœ๋ ฅ
  • ์—ญํ• : ์ตœ์ข… ๋ณด๊ณ ์„œ๋ฅผ Markdown ํ˜•์‹์œผ๋กœ ์ถœ๋ ฅ.
1
2
3
4
from IPython.display import Markdown
final_state = graph.get_state(thread)
report = final_state.values.get('final_report')
Markdown(report)
  • final_report์— ์ €์žฅ๋œ ์ตœ์ข… ๋ณด๊ณ ์„œ๋ฅผ ์‹œ๊ฐํ™”ํ•˜์—ฌ, ๊ตฌ์กฐ๊ฐ€ ์™„์„ฑ๋œ ๋ณด๊ณ ์„œ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.



-->