Spaces:
Sleeping
Sleeping
| # 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 ํจ์ ๋ฐํ๊ฐ**์ผ๋ก๋ง ์ฌ์ฉ๋ฉ๋๋ค. | |
| ```python | |
| # 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 ๊ธฐ๋ฅ ํฌํจ) | |
| ```python | |
| # 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์ผ๋ก ๋ณํ | |
| ``` | |
| ## ๊ทธ๋ํ ํ๋ฆ | |
| ```mermaid | |
| 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 ํ์ฅ | |
| ```python | |
| # 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 | |
| - **์ถ๊ฐ ๋ด์ฉ**: | |
| ```python | |
| # ๊ธฐ์กด ๋ก์ง ๋ง์ง๋ง์ ์ถ๊ฐ | |
| if state.is_multi_question: | |
| updates["multi_answers"] = [{ | |
| "index": state.sub_question_index, | |
| "question": state.sub_question_text, | |
| "answer": final_answer | |
| }] | |
| ``` | |
| ### ๊ทธ๋ํ ์ฌ๊ตฌ์ฑ | |
| ```python | |
| # 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: ๋จ์ผ ์ฃผ์ (๊ธฐ์กด ๋์) | |
| ```python | |
| 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: ๋ค์ค ๋ ๋ฆฝ ์ง๋ฌธ (์ ๊ท) | |
| ```python | |
| 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ํ ๋ณ๋ ฌ ์คํ | |
| # ์ถ๋ ฅ: | |
| ``` | |
| **์ถ๋ ฅ ์์**: | |
| ```markdown | |
| # ๋ค์ค ์ง๋ฌธ ๋ต๋ณ | |
| ์๋ณธ ์ง๋ฌธ: JWT๊ฐ ๋ญ์ผ? CORS ์๋ฌ๋ ์ด๋ป๊ฒ ํด๊ฒฐํด? | |
| --- | |
| ## 1. JWT๊ฐ ๋ญ์ผ? | |
| JWT(JSON Web Token)๋ ์ธ์ฆ ์ ๋ณด๋ฅผ ์์ ํ๊ฒ ์ ์กํ๊ธฐ ์ํ... | |
| [์์ธ ๋ต๋ณ...] | |
| --- | |
| ## 2. CORS ์๋ฌ๋ ์ด๋ป๊ฒ ํด๊ฒฐํด? | |
| CORS(Cross-Origin Resource Sharing) ์๋ฌ๋... | |
| [์์ธ ๋ต๋ณ...] | |
| ``` | |
| ### ์์ 3: ์ง๋ฌธ 3๊ฐ ์ด์ | |
| ```python | |
| 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 ํจ์ ์ฌ์ฉ) | |
| ```python | |
| # โ ์๋ชป๋ ๋ฐฉ๋ฒ: ๋ ธ๋์์ 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 ๊ธฐ๋ฅ ํฌํจ) | |
| ```python | |
| # 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 ๋ณด์ฅ | |
| ```python | |
| # ๋ชจ๋ ๊ฒ์ ๋ ธ๋๊ฐ 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. **์ฐ์ ์์**: ์ค์๋์ ๋ฐ๋ผ ์ง๋ฌธ ์์ ์กฐ์ | |
| ## ์ฐธ๊ณ ์๋ฃ | |
| - [LangGraph Graph API](https://docs.langchain.com/oss/python/langgraph/graph-api) | |
| - [LangGraph Workflows + Agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents) | |
| - [LangGraph Thinking Guide](https://docs.langchain.com/oss/python/langgraph/thinking-in-langgraph) | |
| - CodeWeaver Phase 3: Open Deep Research | |
| ## ๋ฌธ์ | |
| ๊ตฌํ ๊ด๋ จ ์ง๋ฌธ์ด๋ ๋ฒ๊ทธ ๋ฆฌํฌํธ๋ ์ด์๋ก ๋ฑ๋กํด์ฃผ์ธ์. | |