CodeWeaver / ARCHITECTURE.md
ใ……ใ…Žใ…‡
Initial commit for Hugging Face Spaces
ea80cdc
# CodeWeaver ์•„ํ‚คํ…์ฒ˜ (์‹ค์ œ ์ฝ”๋“œ ๊ธฐ์ค€)
์ด ๋ฌธ์„œ๋Š” ํ˜„์žฌ ์ €์žฅ์†Œ์˜ CodeWeaver๊ฐ€ **์–ด๋–ค ์ˆœ์„œ๋กœ ๋™์ž‘ํ•˜๋Š”์ง€**, ๊ทธ๋ฆฌ๊ณ  ๊ทธ ์›๋ฆฌ๊ฐ€ ๋ฌด์—‡์ธ์ง€(์ƒํƒœ/๋ผ์šฐํŒ…/๋ณ‘๋ ฌํ™”/์บ์‹œ)๋ฅผ **์ฝ”๋“œ์™€ 1:1๋กœ ์ •ํ•ฉ**๋˜๊ฒŒ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.
## ์ „์ฒด ๊ตฌ์„ฑ ์š”์†Œ
- **UI**: Gradio ์ฑ„ํŒ… UI (`CodeWeaver/ui/app.py`)
- ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ `AgentState`๋กœ ํฌ์žฅํ•œ ๋’ค `agent.ainvoke(..., config={"configurable": {"thread_id": ...}})`๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
- **์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜(๊ทธ๋ž˜ํ”„)**: LangGraph `StateGraph` (`CodeWeaver/src/agent/graph.py`)
- `START โ†’ create_plan`๋กœ ์ง„์ž… ํ›„, ์งˆ๋ฌธ ์œ ํ˜•/๊ฐœ์ˆ˜์— ๋”ฐ๋ผ ๋ถ„๊ธฐํ•ฉ๋‹ˆ๋‹ค.
- ์ฒดํฌํฌ์ธํŒ…: `MemorySaver` ์‚ฌ์šฉ(์Šค๋ ˆ๋“œ/์„ธ์…˜ ๋‹จ์œ„ ์ƒํƒœ ์œ ์ง€).
- **๋…ธ๋“œ ๊ตฌํ˜„**: (`CodeWeaver/src/agent/nodes.py`)
- ์งˆ๋ฌธ ๋ถ„์„, ์บ์‹œ ์กฐํšŒ, ์˜๋„ ๋ถ„๋ฅ˜, 3์†Œ์Šค ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰, ๊ฒฐ๊ณผ ํ‰๊ฐ€/๋ฆฌํŒŒ์ธ, ํ•„ํ„ฐ๋ง/์š”์•ฝ, ๋‹ต๋ณ€ ์ƒ์„ฑ, ๋‹ค์ค‘ ์งˆ๋ฌธ ๊ฒฐํ•ฉ ๋“ฑ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
- **์ƒํƒœ ๋ชจ๋ธ(Reducer ํฌํ•จ)**: (`CodeWeaver/src/agent/state.py`)
- `search_results`๋Š” `Annotated[List[SearchResult], add]`๋กœ **๋ณ‘๋ ฌ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์ž๋™ ๋ณ‘ํ•ฉ**๋ฉ๋‹ˆ๋‹ค.
- `intermediate_steps`, `multi_answers`๋Š” **๋ฆฌ์…‹ ํ† ํฐ์„ ์ง€์›ํ•˜๋Š” ์ปค์Šคํ…€ reducer**๋กœ, ์ฒดํฌํฌ์ธํŒ…/์Šค๋ ˆ๋“œ ์œ ์ง€ ์‹œ ์ด์ „ ํ„ด์˜ ๋ˆ„์ ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
- **์บ์‹œ(Vector DB)**: Qdrant Cloud (`CodeWeaver/src/vector_db/qdrant_client.py`)
- ์ž„๋ฒ ๋”ฉ์€ ๋กœ์ปฌ `BAAI/bge-m3`(`sentence-transformers`)๋กœ ์ƒ์„ฑ, Qdrant์— ์ €์žฅ/๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
- **๊ฒ€์ƒ‰ ์†Œ์Šค**: (`CodeWeaver/src/tools/search_tools.py`)
- Stack Overflow(๊ณต์‹ StackExchange API), GitHub Code Search API, Tavily(๊ณต์‹๋ฌธ์„œ ๋„๋ฉ”์ธ ์ œํ•œ) ์‚ฌ์šฉ.
## ์‚ฌ์šฉ์ž ์ œ๊ณต ๊ทธ๋ž˜ํ”„์™€์˜ ์ •ํ•ฉ์„ฑ
์‚ฌ์šฉ์ž๊ป˜์„œ ์ œ๊ณตํ•œ Mermaid ๊ทธ๋ž˜ํ”„๋Š” ์ด ํ”„๋กœ์ ํŠธ์˜ ์˜๋„์™€ **๋Œ€๋ถ€๋ถ„ ์ผ์น˜**ํ•ฉ๋‹ˆ๋‹ค.
### ์ผ์น˜ํ•˜๋Š” ๋ถ€๋ถ„(ํ•ต์‹ฌ ํŒŒ์ดํ”„๋ผ์ธ)
- `create_plan`์—์„œ **single_topic / multiple_questions(2๊ฐœ) / too_many(3+)** ๋ถ„๊ธฐ
- ๋‹จ์ผ ์งˆ๋ฌธ(ํ˜น์€ ๋‹จ์ผ ์ฃผ์ œ)์—์„œ:
- `analyze_question โ†’ check_cache โ†’ (hit๋ฉด return_cached_answer) / (miss๋ฉด classify_intent)`
- `classify_intent` ์ดํ›„ 3์†Œ์Šค ๊ฒ€์ƒ‰์„ Send API๋กœ ๋ณ‘๋ ฌ ์‹คํ–‰(fan-out)ํ•˜๊ณ  `collect_results`์—์„œ fan-in
- `evaluate_results โ†’ (ํ•„์š” ์‹œ refine_search 1ํšŒ) โ†’ filter_and_score โ†’ summarize_results โ†’ generate_answer`
- `evaluate_results`๊ฐ€ ๋ถ€์กฑํ•˜๋ฉด `refine_search โ†’ classify_intent`๋กœ **์ตœ๋Œ€ 1ํšŒ ๋ฃจํ”„**
### ์‹ค์ œ ์ฝ”๋“œ์—์„œ ์ถ”๊ฐ€/๋ณ€ํ˜•๋œ ๋ถ€๋ถ„(์ค‘์š”)
1) **clarification(๋ณด์ถฉ ์š”์ฒญ) ์ „์šฉ ๊ฒฝ๋กœ๊ฐ€ ์กด์žฌ**
- `analyze_question` ๊ฒฐ๊ณผ๊ฐ€ `clarification`์ด๋ฉด
- **์บ์‹œ/๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๊ณ **
- `generate_with_history`๋กœ ๋ฐ”๋กœ ๋‹ต๋ณ€ํ•˜๊ณ  ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.
2) **multiple_questions fan-out์€ `analyze_question`๋กœ ์ง์ ‘ ๋“ค์–ด๊ฐ€์ง€ ์•Š์Œ**
์‚ฌ์šฉ์ž ๊ทธ๋ž˜ํ”„๋Š” โ€œdynamic์—์„œ Send๋กœ analyze_question์„ 2๋ฒˆ ํ˜ธ์ถœโ€ ํ˜•ํƒœ์— ๊ฐ€๊น์ง€๋งŒ, ์‹ค์ œ ๊ตฌํ˜„์€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.
- ์‹ค์ œ ๊ตฌํ˜„์€ `fanout_multi_questions`๊ฐ€ `Send("run_single_question_worker", child_state)`๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
- ์ด์œ : outer graph์—์„œ ์งˆ๋ฌธ 2๊ฐœ๋ฅผ ๋™์‹œ์— ๋™์ผ ํŒŒ์ดํ”„๋ผ์ธ(analyze/cache/intent/โ€ฆ)์œผ๋กœ ๋Œ๋ฆฌ๋ฉด
- `question_type`, `cached_result` ๊ฐ™์€ **scalar ์ฑ„๋„(state ํ•„๋“œ)**์ด ๋ณ‘๋ ฌ ์—…๋ฐ์ดํŠธ ์ถฉ๋Œ์„ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
- ๋”ฐ๋ผ์„œ **worker ๋‚ด๋ถ€์—์„œ ๋ณ„๋„์˜ โ€˜๋‹จ์ผ ์งˆ๋ฌธ ๊ทธ๋ž˜ํ”„โ€™๋ฅผ ์‹คํ–‰**ํ•˜๊ณ ,
- outer graph์—๋Š” reducer ์ฑ„๋„์ธ `multi_answers`๋งŒ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์ถฉ๋Œ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
## ์‹ค์ œ ์‹คํ–‰ ํ๋ฆ„(์ฝ”๋“œ ๊ธฐ์ค€)
### 1) UI โ†’ Agent ์‹คํ–‰(์—”ํŠธ๋ฆฌ)
`CodeWeaver/ui/app.py`์—์„œ:
- ์ž…๋ ฅ ๋ฌธ์ž์—ด `message`๋ฅผ `AgentState(user_question=..., messages=[HumanMessage(...)], ...)`๋กœ ๋งŒ๋“ค๊ณ 
- `thread_id`๋ฅผ `config={"configurable":{"thread_id": thread_id}}`๋กœ ์ „๋‹ฌํ•˜์—ฌ `agent.ainvoke()` ์‹คํ–‰
- `MemorySaver`๊ฐ€ `thread_id` ๋‹จ์œ„๋กœ ์ƒํƒœ๋ฅผ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค.
### 2) ๋ฉ”์ธ ๊ทธ๋ž˜ํ”„(Top-level) ํ๋ฆ„
`CodeWeaver/src/agent/graph.py` ๊ธฐ์ค€ ๋ฉ”์ธ ํ๋ฆ„์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.
```mermaid
graph TD
startNode[START] --> createPlan[create_plan]
createPlan -->|single_topic| analyzeQuestion[analyze_question]
createPlan -->|multiple_questions_2| initiateDynamic[initiate_dynamic_search]
createPlan -->|too_many_3plus| tooMany[handle_too_many_questions]
tooMany --> endNode[END]
analyzeQuestion -->|clarification| withHistory[generate_with_history]
withHistory --> endNode
analyzeQuestion -->|new_topic_or_independent| checkCache[check_cache]
checkCache -->|hit| returnCached[return_cached_answer]
returnCached --> endNode
checkCache -->|miss| classifyIntent[classify_intent]
classifyIntent --> searchSO[search_stackoverflow]
classifyIntent --> searchGH[search_github]
classifyIntent --> searchDocs[search_official_docs]
searchSO --> collect[collect_results]
searchGH --> collect
searchDocs --> collect
collect --> evalNode[evaluate_results]
evalNode -->|needs_refinement_and_lt1| refine[refine_search]
refine --> classifyIntent
evalNode -->|sufficient_or_ge1| searchSubgraph[search_subgraph]
searchSubgraph --> generateAnswer[generate_answer]
generateAnswer --> routeAfterGen[route_after_generate]
routeAfterGen -->|single| endNode
routeAfterGen -->|multi| combine[combine_answers]
combine --> endNode
initiateDynamic --> fanout[fanout_multi_questions]
fanout --> worker[run_single_question_worker]
worker --> combine
```
### 3) `create_plan`: ์งˆ๋ฌธ ๊ฐœ์ˆ˜/ํ˜•ํƒœ ํŒ๋ณ„ + โ€œ3๊ฐœ ์ด์ƒโ€ ํ•˜๋“œ ๊ฐ€๋“œ
`create_plan_node`๋Š” ์ž…๋ ฅ์„ ์•„๋ž˜ 3๊ฐ€์ง€๋กœ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค.
- **single_topic**: ํ•˜๋‚˜์˜ ์ฃผ์ œ๋ฅผ ๋‹ค์–‘ํ•œ ๊ด€์ ์œผ๋กœ ๋ฌป๋Š” ํ˜•ํƒœ
- **multiple_questions**: ๋…๋ฆฝ ์งˆ๋ฌธ 2๊ฐœ
- **too_many**: ๋…๋ฆฝ ์งˆ๋ฌธ 3๊ฐœ ์ด์ƒ
์ถ”๊ฐ€๋กœ, LLM ๋ถ„๋ฅ˜์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋‹ค์Œ ์กฐ๊ฑด์ด๋ฉด **๊ฒฐ์ •๋ก ์ ์œผ๋กœ too_many**๋กœ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
- ๋ฌผ์Œํ‘œ๊ฐ€ 3๊ฐœ ์ด์ƒ
- ๋˜๋Š” โ€œ์งˆ๋ฌธ ํ›„๋ณดโ€๊ฐ€ 3๊ฐœ ์ด์ƒ(์ค„๋ฐ”๊ฟˆ/๋ฒˆํ˜ธ/๊ตฌ๋ถ„์ž ๋“ฑ์œผ๋กœ ์ถ”์ •)
๋˜ํ•œ ์ฒดํฌํฌ์ธํŒ… ์ƒํƒœ ๋ˆ„์ ์„ ๋ง‰๊ธฐ ์œ„ํ•ด, ๋งค ์‹คํ–‰ ์‹œ์ž‘ ์‹œ `multi_answers`๋ฅผ ๋ฆฌ์…‹ ํ† ํฐ์œผ๋กœ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
### 4) `analyze_question`: ์งˆ๋ฌธ ํƒ€์ž…(clarification/new_topic/independent) + ์บ์‹œ ์ ๊ฒฉ์„ฑ ํŒ๋‹จ
`analyze_question_node`๊ฐ€ LLM์œผ๋กœ ์•„๋ž˜ ๊ฐ’์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
- `question_type`: `clarification | new_topic | independent`
- `should_cache`: ์บ์‹œ ์ €์žฅ ์—ฌ๋ถ€
- `canonical_question`: ์บ์‹œ์šฉ ์ •๊ทœํ™” ์งˆ๋ฌธ(should_cache=true์ผ ๋•Œ)
๋ผ์šฐํŒ…์€ `graph.py`์˜ `route_after_analysis`์—์„œ:
- `clarification` โ†’ `generate_with_history` (๊ฒ€์ƒ‰/์บ์‹œ ์ƒ๋žต)
- ๋‚˜๋จธ์ง€ โ†’ `check_cache`
### 5) ์บ์‹œ(`check_cache` / `return_cached_answer`)
`check_cache_node`๋Š” Qdrant์—์„œ ์œ ์‚ฌ ์งˆ๋ฌธ์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
- ์ž„๋ฒ ๋”ฉ: ๋กœ์ปฌ `BAAI/bge-m3` (1024์ฐจ์›)
- ์ž„๊ณ„๊ฐ’: cosine score **0.85 ์ด์ƒ**์ด๋ฉด hit๋กœ ๊ฐ„์ฃผ
hit๋ฉด `return_cached_answer_node`๊ฐ€ ์ €์žฅ๋œ ๋‹ต๋ณ€์„ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
### 6) ์˜๋„ ๋ถ„๋ฅ˜(`classify_intent`)
`classify_intent_node`๊ฐ€ ์งˆ๋ฌธ์„ `debugging | learning | code_review`๋กœ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค.
์ด ๊ฐ’์€ ๊ฒ€์ƒ‰ ๊ฐœ์ˆ˜ ๋“ฑ ์ผ๋ถ€ ์ •์ฑ…์— ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค(์˜ˆ: StackOverflow๋Š” debugging์ด๋ฉด ๋” ๋งŽ์ด ๊ฐ€์ ธ์˜ด).
### 7) ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰(fan-out) โ†’ ์ˆ˜์ง‘(fan-in)
`classify_intent` ์ดํ›„ conditional edge ํ•จ์ˆ˜๊ฐ€ `Send(...)` 3๊ฐœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
- `search_stackoverflow_node`
- `search_github_node`
- `search_official_docs_node`
๊ฐ ๋…ธ๋“œ๋Š” `{"search_results": [..]}`๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , `AgentState.search_results`์˜ reducer(`add`)๊ฐ€ ์ด๋ฅผ ์ž๋™ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
`collect_results_node`๋Š” ๋ณ‘ํ•ฉ๋œ ์ด ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜๋งŒ ์ง‘๊ณ„ํ•ฉ๋‹ˆ๋‹ค.
### 8) ๊ฒฐ๊ณผ ํ‰๊ฐ€(`evaluate_results`)์™€ ์ฟผ๋ฆฌ ๋ฆฌํŒŒ์ธ(`refine_search`)
`evaluate_results_node`๋Š” ๋‹ค์Œ ๊ธฐ์ค€์œผ๋กœ โ€œ๊ฐœ์„  ํ•„์š”โ€๋ฅผ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.
- ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜ < 2 โ†’ ๊ฐœ์„  ํ•„์š”
- (relevance_score๊ฐ€ ์žˆ๋‹ค๋ฉด) ํ‰๊ท  ์ ์ˆ˜ < 0.5 โ†’ ๊ฐœ์„  ํ•„์š”
`refine_search_node`๋Š” LLM์ด `MORE_SPECIFIC | MORE_GENERAL | TRANSLATE` ์ „๋žต์„ ์„ ํƒํ•ด ์ฟผ๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.
- ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€: `refinement_count < 1`์ผ ๋•Œ๋งŒ 1ํšŒ ํ—ˆ์šฉ
- ์žฌ๊ฒ€์ƒ‰์„ ์œ„ํ•ด `search_results`๋ฅผ ๋นˆ ๋ฆฌ์ŠคํŠธ๋กœ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  `classify_intent`๋กœ ๋˜๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค.
### 9) `search_subgraph`: ํ•„ํ„ฐ๋ง + ์š”์•ฝ
๋ฉ”์ธ ๊ทธ๋ž˜ํ”„์—๋Š” `search_subgraph`๊ฐ€ โ€œํ•˜๋‚˜์˜ ๋…ธ๋“œโ€์ฒ˜๋Ÿผ ๋ถ™์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
- `filter_and_score`: ์ตœ์†Œ ๊ธธ์ด/URL ์กฐ๊ฑด์œผ๋กœ ํ•„ํ„ฐ ํ›„, ์ƒ์œ„ ์ผ๋ถ€์— ๋Œ€ํ•ด ๊ด€๋ จ๋„ ์ ์ˆ˜ ๋ถ€์—ฌ
- `summarize_results`: ๊ฐ ๊ฒฐ๊ณผ๋ฅผ 2~3๋ฌธ์žฅ์œผ๋กœ ์š”์•ฝ
### 10) `generate_answer`: ๋‹ต๋ณ€ ์ƒ์„ฑ + (์กฐ๊ฑด๋ถ€) ์บ์‹œ ์ €์žฅ
`generate_answer_node`๋Š” ์˜๋„์— ๋”ฐ๋ผ ํ…œํ”Œ๋ฆฟ์„ ๋ฐ”๊ฟ” ์ตœ์ข… ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
์บ์‹œ ์ €์žฅ ์ •์ฑ…:
- `question_type`๊ฐ€ `new_topic` ๋˜๋Š” `independent`์ด๊ณ  `should_cache`๊ฐ€ true์ด๋ฉด ์ €์žฅ
- `clarification`์€ ์ €์žฅํ•˜์ง€ ์•Š์Œ(๋ผ์šฐํŒ…์ƒ ๋ณดํ†ต ์—ฌ๊ธฐ๋กœ ์˜ค์ง€ ์•Š์ง€๋งŒ ๋ฐฉ์–ด์ ์œผ๋กœ ์ฒดํฌ)
### 11) ๋‹ค์ค‘ ์งˆ๋ฌธ(multiple_questions) ์ฒ˜๋ฆฌ ์›๋ฆฌ
๋‹ค์ค‘ ์งˆ๋ฌธ์˜ ํ•ต์‹ฌ์€ โ€œouter graph๋Š” ์ถฉ๋Œ ์—†์ด orchestration๋งŒ, ์‹ค์ œ ํŒŒ์ดํ”„๋ผ์ธ์€ worker ๋‚ด๋ถ€์—์„œ ์‹คํ–‰โ€์ž…๋‹ˆ๋‹ค.
#### ํ๋ฆ„
- `create_plan(case=multiple_questions)` โ†’ `initiate_dynamic_search` (์ค€๋น„)
- `fanout_multi_questions`(conditional edge)์ด ์งˆ๋ฌธ 2๊ฐœ๋ฅผ ๊ฐ๊ฐ `run_single_question_worker`๋กœ Send
- `run_single_question_worker_node` ๋‚ด๋ถ€์—์„œ **๋‹จ์ผ ์งˆ๋ฌธ์šฉ ๊ทธ๋ž˜ํ”„๋ฅผ ๋ณ„๋„ compile/์‹คํ–‰**
- worker ๊ฒฐ๊ณผ๋Š” `multi_answers`์— append(reducer๋กœ ๋ณ‘ํ•ฉ)
- ๋ชจ๋“  worker๊ฐ€ ๋๋‚˜๋ฉด `combine_answers_node`๊ฐ€ Markdown์œผ๋กœ ๊ฒฐํ•ฉ
#### ์™œ worker๊ฐ€ ํ•„์š”ํ•œ๊ฐ€?
outer graph์—์„œ ๋™์ผํ•œ state๋ฅผ ๋ณต์ œํ•ด `analyze_question`๋ถ€ํ„ฐ ๋™์‹œ์— ๋Œ๋ฆฌ๋ฉด,
scalar ์ฑ„๋„(`question_type`, `cached_result` ๋“ฑ)์ด ์„œ๋กœ ๋ฎ์–ด์“ฐ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๊ทธ๋ž˜์„œ ์‹ค์ œ ๊ตฌํ˜„์€:
- worker ๋‚ด๋ถ€์—์„œ ๋‹จ์ผ ์งˆ๋ฌธ ๊ทธ๋ž˜ํ”„๋ฅผ ๋Œ๋ฆฌ๊ณ 
- outer state์—๋Š” **reducer ์ฑ„๋„์ธ `multi_answers`๋งŒ** ์—…๋ฐ์ดํŠธ
์ด ๋ฐฉ์‹์œผ๋กœ ๋ณ‘๋ ฌ ์‹คํ–‰ ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ•ฉ๋‹ˆ๋‹ค.
## ํ™˜๊ฒฝ ๋ณ€์ˆ˜(์‹คํ–‰์— ํ•„์š”ํ•œ ์‹ค์ œ ๊ฐ’)
ํ•„์ˆ˜:
- `GOOGLE_API_KEY`: Gemini ํ˜ธ์ถœ(`langchain-google-genai`)
- `QDRANT_URL`, `QDRANT_API_KEY`: Qdrant Cloud ์บ์‹œ
- `TAVILY_API_KEY`: ๊ณต์‹ ๋ฌธ์„œ ๊ฒ€์ƒ‰(Tavily)
์„ ํƒ:
- `GITHUB_TOKEN`: GitHub API rate limit ์™„ํ™”(์—†์œผ๋ฉด 60 req/hr ์ˆ˜์ค€)
- `LANGCHAIN_TRACING_V2`, `LANGCHAIN_API_KEY`: LangSmith ํŠธ๋ ˆ์ด์‹ฑ(์„ ํƒ)