File size: 10,586 Bytes
ea80cdc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# 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 ํŠธ๋ ˆ์ด์‹ฑ(์„ ํƒ)