# 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 트레이싱(선택)