CodeWeaver / DYNAMIC_PARALLEL_SEARCH.md
ใ……ใ…Žใ…‡
Initial commit for Hugging Face Spaces
ea80cdc
# 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
## ๋ฌธ์˜
๊ตฌํ˜„ ๊ด€๋ จ ์งˆ๋ฌธ์ด๋‚˜ ๋ฒ„๊ทธ ๋ฆฌํฌํŠธ๋Š” ์ด์Šˆ๋กœ ๋“ฑ๋กํ•ด์ฃผ์„ธ์š”.