Spaces:
Sleeping
A newer version of the Gradio SDK is available:
6.2.0
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_answersreducer์๋ง ์ถ๊ฐ
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๊ฐ ๋ญ์ผ?" ์ง๋ฌธ โ ๋ต๋ณ ํ์ธ โ ๋ค์ ์ง๋ฌธ
์ด๋ป๊ฒ ๋์๋๋ฆด๊น์?
ํ ์คํธ
ํ ์คํธ ํ์ผ์ ํ๋ก์ ํธ ๋ฃจํธ์ ์์ต๋๋ค. (์ญ์ ๋จ - ํ์์ ์ฌ์์ฑ)
ํ ์คํธ ์๋๋ฆฌ์ค
โ ๋จ์ผ ์ฃผ์ : "Spring Security JWT ์ธ์ฆ ๊ตฌํ ๋ฐฉ๋ฒ"
- ๊ธฐ์กด ๊ทธ๋ํ 1ํ ์คํ
- multi_answers ๋น์ด์์
- ์ผ๋ฐ ๋ต๋ณ ํ์
โ ๋ค์ค ์ง๋ฌธ 2๊ฐ: "JWT๊ฐ ๋ญ์ผ? CORS๋?"
- Send API๋ก ๊ทธ๋ํ 2ํ ๋ณ๋ ฌ ์คํ
- multi_answers์ 2๊ฐ ํญ๋ชฉ
- ์น์ ๊ตฌ๋ถ๋ ํตํฉ ๋ต๋ณ
โ ์ง๋ฌธ 3๊ฐ ์ด์: "JWT? CORS? Docker?"
- handle_too_many_questions๋ก ๋ถ๊ธฐ
- ์น์ ํ ์๋ฌ ๋ฉ์์ง
- ๋ํ ๊ณ์ ๊ฐ๋ฅ
โ ์ฃ์ง ์ผ์ด์ค: "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๋ก ๋ณํฉ ์๋ํ
ํ๊ณ ๋ฐ ํฅํ ๊ฐ์
ํ์ฌ ํ๊ณ
์ง๋ฌธ ๊ฐ์ ์ ํ: ์ต๋ 2๊ฐ
- ๋น์ฉ vs ํ์ง ํธ๋ ์ด๋์คํ
- ํฅํ 3-4๊ฐ๋ก ํ์ฅ ๊ฐ๋ฅ
์บ์ฑ ์ ๋ต: ํตํฉ ๋ต๋ณ์ ์บ์ ์ ๋จ
- ๊ฐ ์๋ธ ์ง๋ฌธ์ ๊ฐ๋ณ ์บ์๋จ
- ๋์ผํ ๋ค์ค ์ง๋ฌธ ์ฌ์ ๋ ฅ ์ ๊ฐ๋ณ ์บ์ ํํธ
Refinement ๋ฃจํ: ๋ค์ค ์ง๋ฌธ์์๋ ๊ฐ๊ฐ ๋ ๋ฆฝ์ ์ผ๋ก ์๋
- ํ ์ง๋ฌธ refine ์ ๋ค๋ฅธ ์ง๋ฌธ์ ์ํฅ ์์
ํฅํ ๊ฐ์ ๋ฐฉํฅ
- ๋ ๋ง์ ์ง๋ฌธ ์ง์: 3-4๊ฐ๊น์ง ํ์ฅ
- ํผํฉ ์ง๋ฌธ ๊ฐ์ง: "JWT๊ฐ ๋ญ์ผ? ๊ทธ๊ฑธ Spring์ ์ ์ฉํ๋ ค๋ฉด?" (์์ฐจ ์์กด)
- ์คํธ๋ฆฌ๋ฐ ๋ต๋ณ: ๊ฐ ์๋ธ ์ง๋ฌธ ์๋ฃ ์ฆ์ ์คํธ๋ฆฌ๋ฐ
- ์ฐ์ ์์: ์ค์๋์ ๋ฐ๋ผ ์ง๋ฌธ ์์ ์กฐ์
์ฐธ๊ณ ์๋ฃ
- LangGraph Graph API
- LangGraph Workflows + Agents
- LangGraph Thinking Guide
- CodeWeaver Phase 3: Open Deep Research
๋ฌธ์
๊ตฌํ ๊ด๋ จ ์ง๋ฌธ์ด๋ ๋ฒ๊ทธ ๋ฆฌํฌํธ๋ ์ด์๋ก ๋ฑ๋กํด์ฃผ์ธ์.