CodeWeaver / DYNAMIC_PARALLEL_SEARCH.md
ใ……ใ…Žใ…‡
Initial commit for Hugging Face Spaces
ea80cdc

A newer version of the Gradio SDK is available: 6.2.0

Upgrade

Dynamic Parallel Search for Multiple Independent Questions

๊ฐœ์š”

CodeWeaver Phase 4๋Š” ๋‹ค์ค‘ ๋…๋ฆฝ ์งˆ๋ฌธ์„ Send API๋กœ ๋™์  ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌํ•˜์—ฌ, ๊ฐ ์งˆ๋ฌธ๋งˆ๋‹ค ๋…๋ฆฝ์ ์ธ ๊ฒ€์ƒ‰ ํŒŒ์ดํ”„๋ผ์ธ์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ์ฒ ํ•™

"๊ธฐ์กด ๊ทธ๋ž˜ํ”„๋ฅผ 100% ์žฌ์‚ฌ์šฉํ•˜๋˜, ์งˆ๋ฌธ ๊ฐœ์ˆ˜๋งŒํผ ๋ณต์ œํ•ด์„œ ๋ณ‘๋ ฌ ์‹คํ–‰ํ•œ๋‹ค"

  • ๊ธฐ์กด ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ๋ฅ : ~95%
  • ์ƒˆ๋กœ์šด ๋…ธ๋“œ: 5๊ฐœ ์ถ”๊ฐ€
  • ์ƒˆ๋กœ์šด edge ํ•จ์ˆ˜: 1๊ฐœ ์ถ”๊ฐ€ (fanout_multi_questions)
  • ์ˆ˜์ •๋œ ๋…ธ๋“œ: 2๊ฐœ ์ˆ˜์ • (create_plan, generate_answer)

์ฃผ์š” ๊ธฐ๋Šฅ

1. ์ž๋™ ์งˆ๋ฌธ ์œ ํ˜• ๊ฐ์ง€

create_plan_node๊ฐ€ ์งˆ๋ฌธ์„ ๋ถ„์„ํ•˜์—ฌ 3๊ฐ€์ง€ ์ผ€์ด์Šค๋กœ ๋ถ„๋ฅ˜:

Case 1: single_topic

  • ์ •์˜: ํ•˜๋‚˜์˜ ์ฃผ์ œ๋ฅผ ๋‹ค๊ฐ๋„๋กœ ๋ฌป๋Š” ๊ฒฝ์šฐ
  • ์˜ˆ์‹œ: "Spring Security JWT ์ธ์ฆ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•"
  • ์„œ๋ธŒ์งˆ๋ฌธ: ["๊ฐœ๋…", "๊ตฌํ˜„", "์˜ˆ์ œ"] (๋‹ต๋ณ€ ์„น์…˜ ๊ตฌ์กฐ์šฉ)
  • ์‹คํ–‰: ๊ธฐ์กด ๊ทธ๋ž˜ํ”„ 1ํšŒ (๊ฒ€์ƒ‰์€ ์›๋ณธ ์งˆ๋ฌธ์œผ๋กœ)

Case 2: multiple_questions

  • ์ •์˜: ์„œ๋กœ ๋ฌด๊ด€ํ•œ ๋…๋ฆฝ ์งˆ๋ฌธ (์ตœ๋Œ€ 2๊ฐœ)
  • ์˜ˆ์‹œ: "JWT๊ฐ€ ๋ญ์•ผ? CORS๋Š”?"
  • ์„œ๋ธŒ์งˆ๋ฌธ: ["JWT๊ฐ€ ๋ญ์•ผ?", "CORS๋Š”?"] (๊ฐ๊ฐ ๋ณ„๋„ ๊ฒ€์ƒ‰)
  • ์‹คํ–‰: Send API๋กœ ๊ธฐ์กด ๊ทธ๋ž˜ํ”„ 2ํšŒ ๋ณ‘๋ ฌ ์‹คํ–‰

Case 3: too_many

  • ์ •์˜: ์งˆ๋ฌธ 3๊ฐœ ์ด์ƒ
  • ์˜ˆ์‹œ: "JWT? CORS? Docker?"
  • ์‹คํ–‰: ์นœ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ, ๋Œ€ํ™” ๊ณ„์† ๊ฐ€๋Šฅ
  • ํ•˜๋“œ ๊ฐ€๋“œ: LLM ๋ถ„๋ฅ˜์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋ฌผ์Œํ‘œ ๊ฐœ์ˆ˜(3๊ฐœ ์ด์ƒ) ๋˜๋Š” ์งˆ๋ฌธ ํ›„๋ณด ๊ฐœ์ˆ˜(3๊ฐœ ์ด์ƒ)๋กœ ๊ฒฐ์ •๋ก ์  ์ฐจ๋‹จ

2. ์งˆ๋ฌธ ๊ฐœ์ˆ˜ ์ œํ•œ

๋น„์šฉ ๋ฐ ํ’ˆ์งˆ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ์ตœ๋Œ€ 2๊ฐœ ์งˆ๋ฌธ์œผ๋กœ ์ œํ•œ:

์ž…๋ ฅ: "JWT? CORS? Docker? Redis?"
์ฒ˜๋ฆฌ: too_many ์ผ€์ด์Šค โ†’ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
์•ˆ๋‚ด: "ํ•˜๋‚˜์˜ ์ฃผ์ œ๋กœ ํ†ตํ•ฉ" ๋˜๋Š” "2๊ฐœ๋งŒ ์„ ํƒ" ๊ถŒ์žฅ

3. Send API ๋™์  ๋ณต์ œ

์ค‘์š”: LangGraph์—์„œ List[Send]๋Š” ๋…ธ๋“œ ๋ฐ˜ํ™˜๊ฐ’์ด ์•„๋‹ˆ๋ผ conditional edge ํ•จ์ˆ˜ ๋ฐ˜ํ™˜๊ฐ’์œผ๋กœ๋งŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

# initiate_dynamic_search_node: state ์ค€๋น„๋งŒ (dict ๋ฐ˜ํ™˜)
def initiate_dynamic_search_node(state: AgentState) -> dict:
    return {"intermediate_steps": [...]}  # Send ๋ฐ˜ํ™˜ ์•ˆ ํ•จ!

# fanout_multi_questions: conditional edge ํ•จ์ˆ˜ (List[Send] ๋ฐ˜ํ™˜)
def fanout_multi_questions(state: AgentState) -> List[Send]:
    sends = []
    for i, question in enumerate(["JWT๊ฐ€ ๋ญ์•ผ?", "CORS๋Š”?"]):
        child_state = state.model_copy(deep=True)
        child_state.user_question = question
        child_state.is_multi_question = True
        # ... ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์„ค์ • ...
        sends.append(Send("run_single_question_worker", child_state))
    return sends

# run_single_question_worker: ๋‚ด๋ถ€ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ ์‹คํ–‰
# ๊ฐ Send๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๋‚ด๋ถ€ ๊ทธ๋ž˜ํ”„๋ฅผ ์‹คํ–‰:
# analyze โ†’ cache โ†’ classify โ†’ search(ร—3) โ†’ collect โ†’ eval โ†’ subgraph โ†’ generate
# โ†’ multi_answers์— ๊ฒฐ๊ณผ ์ถ”๊ฐ€

4. Reducer ์ž๋™ Fan-in (Reset ๊ธฐ๋Šฅ ํฌํ•จ)

# State ์ •์˜ (์ปค์Šคํ…€ reducer ์‚ฌ์šฉ)
multi_answers: Annotated[List[Dict[str, Any]], merge_multi_answers] = []

# merge_multi_answers reducer:
# - ๊ธฐ๋ณธ ๋™์ž‘: old + new (๋ณ‘๋ ฌ worker์—์„œ ๋‹ต๋ณ€์„ ๋™์‹œ์— append)
# - ๋ฆฌ์…‹ ๋™์ž‘: new์˜ ์ฒซ ์›์†Œ๊ฐ€ {"__token__": "__RESET_MULTI_ANS__"}์ด๋ฉด
#   old๋ฅผ ๋ฒ„๋ฆฌ๊ณ  new[1:]๋กœ ๊ต์ฒด (์ด์ „ ํ„ด ๋ˆ„์  ๋ฐฉ์ง€)

# run_single_question_worker 1์ด ๋ฆฌํ„ด:
{"multi_answers": [{"index": 0, "question": "JWT๊ฐ€ ๋ญ์•ผ?", "answer": "..."}]}

# run_single_question_worker 2๊ฐ€ ๋ฆฌํ„ด:
{"multi_answers": [{"index": 1, "question": "CORS๋Š”?", "answer": "..."}]}

# LangGraph Reducer๊ฐ€ ์ž๋™ ๋ณ‘ํ•ฉ:
state.multi_answers = [
    {"index": 0, ...},
    {"index": 1, ...}
]

# combine_answers_node๊ฐ€ ์ด๋ฅผ ํ†ตํ•ฉ Markdown์œผ๋กœ ๋ณ€ํ™˜

๊ทธ๋ž˜ํ”„ ํ๋ฆ„

graph TD
    START[START] --> plan[create_plan]
    
    plan -->|single_topic| analyze[analyze_question]
    plan -->|multiple_questions 2๊ฐœ| dynamic[initiate_dynamic_search]
    plan -->|too_many 3+| tooMany[handle_too_many_questions]
    
    tooMany --> END
    
    analyze --> cache[check_cache]
    cache -->|hit| returnCache[return_cached_answer]
    cache -->|miss| classify[classify_intent]
    
    returnCache --> END
    
    classify --> searchSO[search_stackoverflow]
    classify --> searchGH[search_github]
    classify --> searchDocs[search_official_docs]
    
    searchSO --> collect[collect_results]
    searchGH --> collect
    searchDocs --> collect
    
    collect --> eval[evaluate_results]
    
    eval -->|needs_refinement| refine[refine_search]
    eval -->|sufficient| filterNode[filter_and_score]
    
    refine --> classify
    
    filterNode --> summarize[summarize_results]
    summarize --> generate[generate_answer]
    
    generate -->|is_multi_question| combine[combine_answers]
    generate -->|single_topic| END
    
    combine --> END
    
    dynamic --> fanout[fanout_multi_questions<br/>conditional edge]
    fanout -.Send Q1.-> worker1[run_single_question_worker<br/>๋‚ด๋ถ€ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„]
    fanout -.Send Q2.-> worker2[run_single_question_worker<br/>๋‚ด๋ถ€ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„]
    worker1 --> combine
    worker2 --> combine

ํ๋ฆ„ ์„ค๋ช…

Single Topic (๊ธฐ์กด ๋™์ž‘ ์œ ์ง€)

START โ†’ create_plan (case: single_topic)
      โ†’ analyze โ†’ cache โ†’ classify โ†’ search(ร—3) โ†’ collect โ†’ eval โ†’ subgraph โ†’ generate โ†’ END

Multiple Questions (์‹ ๊ทœ)

START โ†’ create_plan (case: multiple_questions)
      โ†’ initiate_dynamic_search (state ์ค€๋น„)
      โ†’ fanout_multi_questions (conditional edge)
          โ”œโ”€ Send("run_single_question_worker", Q1) โ†’ [๋‚ด๋ถ€ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ] โ†’ multi_answers[0]
          โ””โ”€ Send("run_single_question_worker", Q2) โ†’ [๋‚ด๋ถ€ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ] โ†’ multi_answers[1]
      โ†’ combine_answers (์ž๋™ fan-in) โ†’ END

Too Many (์‹ ๊ทœ)

START โ†’ create_plan (case: too_many)
      โ†’ handle_too_many_questions โ†’ END
(์‚ฌ์šฉ์ž๋Š” ์ฆ‰์‹œ ๋‹ค์‹œ ์งˆ๋ฌธ ๊ฐ€๋Šฅ)

๊ตฌํ˜„ ์ƒ์„ธ

State ํ™•์žฅ

# src/agent/state.py

class AgentState(BaseModel):
    # ... ๊ธฐ์กด ํ•„๋“œ ...
    
    # Phase 4: Dynamic Parallel Search
    is_multi_question: bool = False
    sub_question_index: int = 0
    sub_question_text: Optional[str] = None
    original_multi_question: Optional[str] = None
    multi_answers: Annotated[List[Dict[str, Any]], merge_multi_answers] = []

์ƒˆ๋กœ์šด ๋…ธ๋“œ (5๊ฐœ)

1. create_plan_node (์ˆ˜์ •)

  • ์œ„์น˜: src/agent/nodes.py ๋ผ์ธ 206
  • ์—ญํ• : ์งˆ๋ฌธ ์œ ํ˜• ๋ฐ ๊ฐœ์ˆ˜ ํŒ๋‹จ
  • ๋ณ€๊ฒฝ:
    • case ํ•„๋“œ ์ถ”๊ฐ€ (single_topic/multiple_questions/too_many)
    • ํ•˜๋“œ ๊ฐ€๋“œ ์ถ”๊ฐ€: _hard_guard_too_many ํ•จ์ˆ˜๋กœ 3๊ฐœ ์ด์ƒ ์งˆ๋ฌธ ๊ฒฐ์ •๋ก ์  ์ฐจ๋‹จ
      • ๋ฌผ์Œํ‘œ ๊ฐœ์ˆ˜(3๊ฐœ ์ด์ƒ) ๋˜๋Š” ์งˆ๋ฌธ ํ›„๋ณด ๊ฐœ์ˆ˜(3๊ฐœ ์ด์ƒ) ๊ฐ์ง€
      • LLM ๋ถ„๋ฅ˜์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ too_many๋กœ ๊ฐ•์ œ

2. handle_too_many_questions_node (์‹ ๊ทœ)

  • ์œ„์น˜: src/agent/nodes.py ๋ผ์ธ 1068
  • ์—ญํ• : 3๊ฐœ ์ด์ƒ ์งˆ๋ฌธ ์‹œ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€
  • ํŠน์ง•: ๋Œ€ํ™” ์ข…๋ฃŒํ•˜์ง€ ์•Š์Œ (์ฆ‰์‹œ ์žฌ์งˆ๋ฌธ ๊ฐ€๋Šฅ)

3. initiate_dynamic_search_node (์‹ ๊ทœ)

  • ์œ„์น˜: src/agent/nodes.py ๋ผ์ธ 1092
  • ์—ญํ• : ๋‹ค์ค‘ ์งˆ๋ฌธ ์ฒ˜๋ฆฌ ์ง„์ž…์ , state ์ค€๋น„
  • ํ•ต์‹ฌ: dict๋งŒ ๋ฐ˜ํ™˜ (Send๋Š” ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์Œ)

4. fanout_multi_questions (์‹ ๊ทœ - Edge ํ•จ์ˆ˜)

  • ์œ„์น˜: src/agent/nodes.py ๋ผ์ธ 1110
  • ์—ญํ• : conditional edge ํ•จ์ˆ˜๋กœ List[Send] ๋ฐ˜ํ™˜
  • ํ•ต์‹ฌ: ๊ฐ ์„œ๋ธŒ ์งˆ๋ฌธ์„ run_single_question_worker๋กœ Send

5. run_single_question_worker_node (์‹ ๊ทœ)

  • ์œ„์น˜: src/agent/nodes.py ๋ผ์ธ 1306
  • ์—ญํ• : ๋‚ด๋ถ€ ์„œ๋ธŒ๊ทธ๋ž˜ํ”„๋ฅผ ์‹คํ–‰ํ•˜์—ฌ state ์ถฉ๋Œ ๋ฐฉ์ง€
  • ํ•ต์‹ฌ:
    • ๋…๋ฆฝ๋œ ๋‹จ์ผ ์งˆ๋ฌธ ๊ทธ๋ž˜ํ”„๋ฅผ ๋‚ด๋ถ€์—์„œ ์‹คํ–‰
    • outer graph์˜ scalar state ์ฑ„๋„ ์ถฉ๋Œ ๋ฐฉ์ง€
    • ๊ฒฐ๊ณผ๋ฅผ multi_answers reducer์—๋งŒ ์ถ”๊ฐ€

6. combine_answers_node (์‹ ๊ทœ)

  • ์œ„์น˜: src/agent/nodes.py ๋ผ์ธ 1168
  • ์—ญํ• : multi_answers๋ฅผ ํ†ตํ•ฉ Markdown ํฌ๋งท์œผ๋กœ ๋ณ€ํ™˜
  • ํŠน์ง•: ์ž๋™ fan-in (๋ชจ๋“  Send ์™„๋ฃŒ ๋Œ€๊ธฐ)

์ˆ˜์ •๋œ ๋…ธ๋“œ (1๊ฐœ)

generate_answer_node (5์ค„ ์ถ”๊ฐ€)

  • ์œ„์น˜: src/agent/nodes.py ๋ผ์ธ 726
  • ์ถ”๊ฐ€ ๋‚ด์šฉ:
# ๊ธฐ์กด ๋กœ์ง ๋งˆ์ง€๋ง‰์— ์ถ”๊ฐ€
if state.is_multi_question:
    updates["multi_answers"] = [{
        "index": state.sub_question_index,
        "question": state.sub_question_text,
        "answer": final_answer
    }]

๊ทธ๋ž˜ํ”„ ์žฌ๊ตฌ์„ฑ

# src/agent/graph.py

# 1. START ์ง„์ž…์  ๋ณ€๊ฒฝ
graph.add_edge(START, "create_plan")  # ๊ธฐ์กด: analyze_question

# 2. create_plan ํ›„ ๋ถ„๊ธฐ ์ถ”๊ฐ€
graph.add_conditional_edges(
    "create_plan",
    route_after_plan,
    {
        "analyze_question": "analyze_question",
        "initiate_dynamic_search": "initiate_dynamic_search",
        "handle_too_many_questions": "handle_too_many_questions"
    }
)

# 3. initiate_dynamic_search ํ›„ fan-out
graph.add_conditional_edges(
    "initiate_dynamic_search",
    fanout_multi_questions,  # List[Send] ๋ฐ˜ํ™˜
)

# 4. run_single_question_worker ํ›„ fan-in
graph.add_edge("run_single_question_worker", "combine_answers")

# 5. generate_answer ํ›„ ๋ถ„๊ธฐ ์ถ”๊ฐ€
graph.add_conditional_edges(
    "generate_answer",
    route_after_generate,
    {
        "combine_answers": "combine_answers",
        END: END
    }
)

์‚ฌ์šฉ ์˜ˆ์‹œ

์˜ˆ์‹œ 1: ๋‹จ์ผ ์ฃผ์ œ (๊ธฐ์กด ๋™์ž‘)

from CodeWeaver.src.agent.graph import create_agent
from langchain_core.messages import HumanMessage

agent = create_agent()

result = await agent.ainvoke({
    "user_question": "React hooks ์™„๋ฒฝ ๊ฐ€์ด๋“œ",
    "messages": [HumanMessage(content="React hooks ์™„๋ฒฝ ๊ฐ€์ด๋“œ")]
})

# ๊ฒฐ๊ณผ
# plan.case: "single_topic"
# plan.sub_questions: ["hooks๋ž€", "์ฃผ์š” hooks", "์‹ค๋ฌด ํŒจํ„ด"]
# ํ๋ฆ„: ๊ธฐ์กด ๊ทธ๋ž˜ํ”„ 1ํšŒ ์‹คํ–‰
# ์ถœ๋ ฅ: ์ผ๋ฐ˜ ๋‹ต๋ณ€ ํ˜•์‹

์˜ˆ์‹œ 2: ๋‹ค์ค‘ ๋…๋ฆฝ ์งˆ๋ฌธ (์‹ ๊ทœ)

result = await agent.ainvoke({
    "user_question": "JWT๊ฐ€ ๋ญ์•ผ? CORS ์—๋Ÿฌ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•ด?",
    "messages": [HumanMessage(content="JWT๊ฐ€ ๋ญ์•ผ? CORS ์—๋Ÿฌ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•ด?")]
})

# ๊ฒฐ๊ณผ
# plan.case: "multiple_questions"
# plan.sub_questions: ["JWT๊ฐ€ ๋ญ์•ผ?", "CORS ์—๋Ÿฌ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•ด?"]
# ํ๋ฆ„: Send API๋กœ ๊ทธ๋ž˜ํ”„ 2ํšŒ ๋ณ‘๋ ฌ ์‹คํ–‰
# ์ถœ๋ ฅ:

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

# ๋‹ค์ค‘ ์งˆ๋ฌธ ๋‹ต๋ณ€

์›๋ณธ ์งˆ๋ฌธ: JWT๊ฐ€ ๋ญ์•ผ? CORS ์—๋Ÿฌ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•ด?

---

## 1. JWT๊ฐ€ ๋ญ์•ผ?

JWT(JSON Web Token)๋Š” ์ธ์ฆ ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ „์†กํ•˜๊ธฐ ์œ„ํ•œ...

[์ƒ์„ธ ๋‹ต๋ณ€...]

---

## 2. CORS ์—๋Ÿฌ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•ด?

CORS(Cross-Origin Resource Sharing) ์—๋Ÿฌ๋Š”...

[์ƒ์„ธ ๋‹ต๋ณ€...]

์˜ˆ์‹œ 3: ์งˆ๋ฌธ 3๊ฐœ ์ด์ƒ

result = await agent.ainvoke({
    "user_question": "JWT? CORS? Docker?",
    "messages": [HumanMessage(content="JWT? CORS? Docker?")]
})

# ๊ฒฐ๊ณผ
# plan.case: "too_many"
# ์ถœ๋ ฅ:

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

์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ•œ ๋ฒˆ์— ์ตœ๋Œ€ 2๊ฐœ์˜ ์งˆ๋ฌธ๊นŒ์ง€๋งŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•ด์„œ ๋‹ค์‹œ ์งˆ๋ฌธํ•ด ์ฃผ์„ธ์š”:

1. **ํ•˜๋‚˜์˜ ์ฃผ์ œ๋กœ ํ†ตํ•ฉํ•ด์„œ ์งˆ๋ฌธ**
   ์˜ˆ: "JWT ์ธ์ฆ๊ณผ CORS ์„ค์ •์„ ํ•จ๊ป˜ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•"

2. **๊ฐ€์žฅ ์ค‘์š”ํ•œ 2๊ฐœ ์งˆ๋ฌธ๋งŒ ์„ ํƒ**
   ์˜ˆ: "JWT๊ฐ€ ๋ญ์•ผ? ๋‚ด ์ฝ”๋“œ์— ์–ด๋–ป๊ฒŒ ์ ์šฉํ•ด?"

3. **์งˆ๋ฌธ์„ ๋‚˜๋ˆ ์„œ ์ˆœ์ฐจ์ ์œผ๋กœ ์งˆ๋ฌธ**
   ์˜ˆ: ๋จผ์ € "JWT๊ฐ€ ๋ญ์•ผ?" ์งˆ๋ฌธ โ†’ ๋‹ต๋ณ€ ํ™•์ธ โ†’ ๋‹ค์Œ ์งˆ๋ฌธ

์–ด๋–ป๊ฒŒ ๋„์™€๋“œ๋ฆด๊นŒ์š”?

ํ…Œ์ŠคํŠธ

ํ…Œ์ŠคํŠธ ํŒŒ์ผ์€ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— ์žˆ์Šต๋‹ˆ๋‹ค. (์‚ญ์ œ๋จ - ํ•„์š”์‹œ ์žฌ์ƒ์„ฑ)

ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค

  1. โœ… ๋‹จ์ผ ์ฃผ์ œ: "Spring Security JWT ์ธ์ฆ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•"

    • ๊ธฐ์กด ๊ทธ๋ž˜ํ”„ 1ํšŒ ์‹คํ–‰
    • multi_answers ๋น„์–ด์žˆ์Œ
    • ์ผ๋ฐ˜ ๋‹ต๋ณ€ ํ˜•์‹
  2. โœ… ๋‹ค์ค‘ ์งˆ๋ฌธ 2๊ฐœ: "JWT๊ฐ€ ๋ญ์•ผ? CORS๋Š”?"

    • Send API๋กœ ๊ทธ๋ž˜ํ”„ 2ํšŒ ๋ณ‘๋ ฌ ์‹คํ–‰
    • multi_answers์— 2๊ฐœ ํ•ญ๋ชฉ
    • ์„น์…˜ ๊ตฌ๋ถ„๋œ ํ†ตํ•ฉ ๋‹ต๋ณ€
  3. โœ… ์งˆ๋ฌธ 3๊ฐœ ์ด์ƒ: "JWT? CORS? Docker?"

    • handle_too_many_questions๋กœ ๋ถ„๊ธฐ
    • ์นœ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
    • ๋Œ€ํ™” ๊ณ„์† ๊ฐ€๋Šฅ
  4. โœ… ์—ฃ์ง€ ์ผ€์ด์Šค: "JWT? CORS? Docker? Redis?"

    • ํ•˜๋“œ ๊ฐ€๋“œ๋กœ ๋ฌด์กฐ๊ฑด too_many ์ฐจ๋‹จ (๋ฌผ์Œํ‘œ 4๊ฐœ ๊ฐ์ง€)
    • LLM ๋ถ„๋ฅ˜์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ์ฐจ๋‹จ ๋ณด์žฅ

์„ฑ๋Šฅ ๊ณ ๋ ค์‚ฌํ•ญ

๋ณ‘๋ ฌ ์‹คํ–‰

  • ๋‹จ์ผ ์ฃผ์ œ: 3๊ฐœ ๊ฒ€์ƒ‰ ๋…ธ๋“œ ๋ณ‘๋ ฌ (๊ธฐ์กด)
  • ๋‹ค์ค‘ ์งˆ๋ฌธ (2๊ฐœ): 2ร—3=6๊ฐœ ๊ฒ€์ƒ‰ ๋…ธ๋“œ ๋ณ‘๋ ฌ
  • LangGraph Send API๊ฐ€ ์ž๋™ ๋ณ‘๋ ฌํ™” ๊ด€๋ฆฌ

๋น„์šฉ ๊ด€๋ฆฌ

  • ์งˆ๋ฌธ ๊ฐœ์ˆ˜ ์ œํ•œ: ์ตœ๋Œ€ 2๊ฐœ
  • ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜: ์†Œ์Šค๋‹น 3-5๊ฐœ
  • ๋‹ค์ค‘ ์งˆ๋ฌธ ์‹œ ์˜๋„ ๋ถ„๋ฅ˜ ์ƒ๋žต (๊ธฐ๋ณธ๊ฐ’ "learning" ์‚ฌ์šฉ)

์บ์‹ฑ

  • ๋‹จ์ผ ์ฃผ์ œ: ์ „์ฒด ๋‹ต๋ณ€ ์บ์‹œ โœ…
  • ๋‹ค์ค‘ ์งˆ๋ฌธ: ๊ฐ ์„œ๋ธŒ ์งˆ๋ฌธ ๋‹ต๋ณ€ ๊ฐœ๋ณ„ ์บ์‹œ โœ…
    • Q1 ๋‹ต๋ณ€ โ†’ Q1 ์งˆ๋ฌธ์œผ๋กœ ์บ์‹œ
    • Q2 ๋‹ต๋ณ€ โ†’ Q2 ์งˆ๋ฌธ์œผ๋กœ ์บ์‹œ
  • ๋‹ค์Œ๋ฒˆ ๋™์ผ ์งˆ๋ฌธ ์‹œ ๊ฐœ๋ณ„ ์บ์‹œ ํžˆํŠธ ๊ฐ€๋Šฅ

๊ธฐ์ˆ ์  ํ•ต์‹ฌ

1. Send API ํŒจํ„ด (Conditional Edge ํ•จ์ˆ˜ ์‚ฌ์šฉ)

# โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ•: ๋…ธ๋“œ์—์„œ Send ๋ฐ˜ํ™˜
def initiate_dynamic_search_node(state):
    return [Send(...), Send(...)]  # ์—๋Ÿฌ ๋ฐœ์ƒ!

# โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•: conditional edge ํ•จ์ˆ˜์—์„œ Send ๋ฐ˜ํ™˜
def fanout_multi_questions(state: AgentState) -> List[Send]:
    sends = []
    for i, question in enumerate(sub_questions):
        child_state = state.model_copy(deep=True)
        child_state.user_question = question
        sends.append(Send("run_single_question_worker", child_state))
    return sends

# ๊ทธ๋ž˜ํ”„ ์„ค์ •
graph.add_conditional_edges(
    "initiate_dynamic_search",
    fanout_multi_questions,  # List[Send] ๋ฐ˜ํ™˜
)

# LangGraph๊ฐ€ ์ž๋™์œผ๋กœ:
# 1. ๋‘ Send๋ฅผ ๋ณ‘๋ ฌ ์‹คํ–‰
# 2. ๊ฐ Send์˜ ๋ชจ๋“  ๋…ธ๋“œ ์‹คํ–‰ ๋Œ€๊ธฐ
# 3. ๋‹ค์Œ ๊ณตํ†ต ๋…ธ๋“œ๋กœ ์ด๋™ (combine_answers)

2. Reducer ์ž๋™ ๋ณ‘ํ•ฉ (Reset ๊ธฐ๋Šฅ ํฌํ•จ)

# State ์ •์˜ (์ปค์Šคํ…€ reducer)
multi_answers: Annotated[List[Dict[str, Any]], merge_multi_answers] = []

# merge_multi_answers reducer:
def merge_multi_answers(old: List[Dict], new: List[Dict]) -> List[Dict]:
    if not new:
        return old
    # Reset ํ† ํฐ ์ฒดํฌ
    if new[0].get("__token__") == "__RESET_MULTI_ANS__":
        return new[1:]  # ์ด์ „ ํ„ด ๋ˆ„์  ๋ฐฉ์ง€
    return old + new  # ๊ธฐ๋ณธ ๋ณ‘ํ•ฉ

# create_plan_node์—์„œ ๋งค ์‹คํ–‰ ์‹œ์ž‘ ์‹œ ๋ฆฌ์…‹:
updates["multi_answers"] = [{"__token__": "__RESET_MULTI_ANS__"}]

# ๋ณ‘๋ ฌ ์‹คํ–‰ ์‹œ:
# [Q1_answer] + [Q2_answer] = [Q1_answer, Q2_answer]

3. Fan-in ๋ณด์žฅ

# ๋ชจ๋“  ๊ฒ€์ƒ‰ ๋…ธ๋“œ๊ฐ€ collect_results๋กœ ์—ฐ๊ฒฐ
graph.add_edge("search_stackoverflow", "collect_results")
graph.add_edge("search_github", "collect_results")
graph.add_edge("search_official_docs", "collect_results")

# LangGraph๊ฐ€ ์ž๋™์œผ๋กœ:
# 1. 3๊ฐœ ๊ฒ€์ƒ‰ ๋ชจ๋‘ ์™„๋ฃŒ ๋Œ€๊ธฐ
# 2. collect_results 1ํšŒ๋งŒ ์‹คํ–‰

์ฝ”๋“œ ๋ณ€๊ฒฝ ์š”์•ฝ

ํŒŒ์ผ๋ณ„ ๋ณ€๊ฒฝ์‚ฌํ•ญ

ํŒŒ์ผ ์ถ”๊ฐ€ ์ˆ˜์ • ์‚ญ์ œ
state.py 5 ํ•„๋“œ, 1 reducer ํ•จ์ˆ˜ - -
nodes.py 5 ๋…ธ๋“œ + 1 edge ํ•จ์ˆ˜ (~300์ค„) 2 ๋…ธ๋“œ (create_plan ํ•˜๋“œ ๊ฐ€๋“œ ์ถ”๊ฐ€, generate_answer 5์ค„) -
graph.py 3 routing ํ•จ์ˆ˜, ์—ฃ์ง€ ์žฌ๊ตฌ์„ฑ build_agent_graph -

์ด ๋ณ€๊ฒฝ๋Ÿ‰: ~350์ค„ ์ถ”๊ฐ€, ~100์ค„ ์ˆ˜์ •

์žฌ์‚ฌ์šฉ๋ฅ 

  • ๊ธฐ์กด ๋…ธ๋“œ ์žฌ์‚ฌ์šฉ: 12/16 (75%)
  • ๊ธฐ์กด ๋กœ์ง ์žฌ์‚ฌ์šฉ: ~95% (๊ฒ€์ƒ‰, ํ‰๊ฐ€, ํ•„ํ„ฐ๋ง, ์š”์•ฝ ๋“ฑ)
  • ์ƒˆ๋กœ์šด ๊ฐœ๋…: Send API + Reducer๋งŒ

LangGraph ๊ณต์‹ ๊ฐ€์ด๋“œ๋ผ์ธ ์ค€์ˆ˜

โœ… Graph API

  • StateGraph ์‚ฌ์šฉ
  • Pydantic BaseModel state
  • START/END ๋ช…์‹œ

โœ… Workflows + Agents

  • Send API๋กœ ๋™์  ๋ณ‘๋ ฌํ™”
  • Conditional edges๋กœ ๋ผ์šฐํŒ…
  • Fan-out/Fan-in ํŒจํ„ด

โœ… Thinking in LangGraph

  • ๋…ธ๋“œ๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜ (ํ•œ ๊ฐ€์ง€ ์ผ๋งŒ)
  • State๋Š” ๋ถˆ๋ณ€ ์—…๋ฐ์ดํŠธ
  • Reducer๋กœ ๋ณ‘ํ•ฉ ์ž๋™ํ™”

ํ•œ๊ณ„ ๋ฐ ํ–ฅํ›„ ๊ฐœ์„ 

ํ˜„์žฌ ํ•œ๊ณ„

  1. ์งˆ๋ฌธ ๊ฐœ์ˆ˜ ์ œํ•œ: ์ตœ๋Œ€ 2๊ฐœ

    • ๋น„์šฉ vs ํ’ˆ์งˆ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„
    • ํ–ฅํ›„ 3-4๊ฐœ๋กœ ํ™•์žฅ ๊ฐ€๋Šฅ
  2. ์บ์‹ฑ ์ „๋žต: ํ†ตํ•ฉ ๋‹ต๋ณ€์€ ์บ์‹œ ์•ˆ ๋จ

    • ๊ฐ ์„œ๋ธŒ ์งˆ๋ฌธ์€ ๊ฐœ๋ณ„ ์บ์‹œ๋จ
    • ๋™์ผํ•œ ๋‹ค์ค‘ ์งˆ๋ฌธ ์žฌ์ž…๋ ฅ ์‹œ ๊ฐœ๋ณ„ ์บ์‹œ ํžˆํŠธ
  3. Refinement ๋ฃจํ”„: ๋‹ค์ค‘ ์งˆ๋ฌธ์—์„œ๋„ ๊ฐ๊ฐ ๋…๋ฆฝ์ ์œผ๋กœ ์ž‘๋™

    • ํ•œ ์งˆ๋ฌธ refine ์‹œ ๋‹ค๋ฅธ ์งˆ๋ฌธ์— ์˜ํ–ฅ ์—†์Œ

ํ–ฅํ›„ ๊ฐœ์„  ๋ฐฉํ–ฅ

  1. ๋” ๋งŽ์€ ์งˆ๋ฌธ ์ง€์›: 3-4๊ฐœ๊นŒ์ง€ ํ™•์žฅ
  2. ํ˜ผํ•ฉ ์งˆ๋ฌธ ๊ฐ์ง€: "JWT๊ฐ€ ๋ญ์•ผ? ๊ทธ๊ฑธ Spring์— ์ ์šฉํ•˜๋ ค๋ฉด?" (์ˆœ์ฐจ ์˜์กด)
  3. ์ŠคํŠธ๋ฆฌ๋ฐ ๋‹ต๋ณ€: ๊ฐ ์„œ๋ธŒ ์งˆ๋ฌธ ์™„๋ฃŒ ์ฆ‰์‹œ ์ŠคํŠธ๋ฆฌ๋ฐ
  4. ์šฐ์„ ์ˆœ์œ„: ์ค‘์š”๋„์— ๋”ฐ๋ผ ์งˆ๋ฌธ ์ˆœ์„œ ์กฐ์ •

์ฐธ๊ณ  ์ž๋ฃŒ

๋ฌธ์˜

๊ตฌํ˜„ ๊ด€๋ จ ์งˆ๋ฌธ์ด๋‚˜ ๋ฒ„๊ทธ ๋ฆฌํฌํŠธ๋Š” ์ด์Šˆ๋กœ ๋“ฑ๋กํ•ด์ฃผ์„ธ์š”.