zilliztech/deep-searcher · 언어 Python ≥3.10 · 라이선스 Apache-2.0 · 패키지 버전 0.0.2 · 코어 소스 ~7,900줄 · 최신 커밋 2025-11-19 · PyPI deepsearcher · 제작 Zilliz(Milvus 제작사) · 성숙도 0.0.x 초기·활발)
DeepSearcher는 "내 문서 더미를 벡터DB에 넣어두면, 복잡한 질문에 대해 스스로 여러 번 검색·추론해서 근거 있는 보고서를 써 주는 에이전트"다. 일반 챗봇에게 "우리 회사 2024 회계 보고서 요약해줘"라고 하면 한 번 검색하고 끝이지만, DeepSearcher는 질문을 잘게 쪼개 여러 각도로 검색하고, 나온 자료가 충분한지 스스로 점검한 뒤 부족하면 다시 검색한다.
꼼꼼한 리서치 인턴을 떠올려 보자. "이 주제 조사해 와"라고 시키면, 초보 인턴은 검색 한 번 하고 첫 페이지만 베껴 온다. 반면 잘 훈련된 인턴은 주제를 여러 소주제로 나누고(하위 질문), 자료를 찾은 뒤 "이 근거가 질문과 정말 관련 있나?"를 직접 검토하고, "아직 X에 대한 근거가 없네"를 깨달으면 추가 조사를 나간다. 마지막에 모은 근거로 보고서를 쓴다.
DeepSearcher가 하는 일이 딱 이 "잘 훈련된 인턴"의 절차를 코드로 옮긴 것이다. 다만 인턴이 뒤지는 건 구글이 아니라 네가 넣어둔 벡터DB다.
기술적으로는 두 단계다. ① 오프라인 적재(ingestion) — 로컬 파일이나 웹페이지를 잘게 쪼개(chunk) 임베딩해서 벡터DB에 넣어둔다. ② 온라인 질의(query) — 질문이 들어오면 라우터가 적절한 에이전트를 골라, 하위질문 분해 → 반복 검색 → 반성 → 종합의 에이전트 루프를 돌려 답을 만든다.
2025~2026년 AI 업계의 핫한 키워드가 바로 "Deep Research(딥리서치)"다. OpenAI·구글·퍼플렉시티가 "한 번 묻고 끝"이 아니라 몇 분간 스스로 웹을 뒤져 리포트를 써 주는 기능을 내놨다. 그런데 이들에겐 공통된 한계가 있다:
DeepSearcher는 이 세 가지를 정면으로 뒤집는다. 내 벡터DB(Milvus 등)에 넣은 사내 데이터에 대고, 오픈소스 코드로 절차가 다 보이게, LLM·임베딩·벡터DB를 자유롭게 갈아끼우며 딥리서치를 돌린다. 게다가 Milvus를 만든 Zilliz가 직접 만들어 벡터 검색 부분이 특히 탄탄하다.
보통의 RAG는 검색 1번 → 답 1번이다. DeepSearcher의 DeepSearch 에이전트는 질문을 최대 4개 하위질문으로 쪼개고, 최대 max_iter(기본 3)번까지 반복하며, 매번 "모은 근거에 아직 빈틈이 있나?"를 LLM에게 물어(reflection) 새 질문을 만든다. 빈틈이 없다고 판단되면 조기 종료한다. 이 "스스로 부족함을 찾는" 부분이 딥리서치의 핵심이다.
벡터 검색은 "비슷해 보이는데 사실 무관한" 청크를 자주 물어온다. DeepSearcher는 검색된 각 청크마다 LLM에게 "이거 질문이랑 진짜 관련 있어? YES/NO"를 물어 NO는 버린다. 랭킹 전용 모델 없이 LLM을 재순위기(reranker)로 쓰는 실전 패턴이다.
OpenAI·DeepSeek·Claude·Gemini·Ollama(로컬)·Bedrock·GLM 등 17종 LLM, 15종 임베딩, Milvus/Qdrant/Oracle/Azure Search 4종 벡터DB를 전부 같은 방식으로 갈아끼운다. config.yaml에서 provider 이름 한 줄만 바꾸면 된다. "공급자 추상화(provider abstraction)"의 교과서 예제다.
"클라우드 딥리서치는 좋은데 내 사내 데이터엔 못 쓴다"는 빈틈을, 오픈소스 + 벡터DB + 에이전트 루프로 메운 게 인기 이유다. 특히 Milvus 제작사가 만들어 벡터 검색·하이브리드 검색이 견고하다.
패키지 버전이 0.0.2인 초기 프로젝트다. README가 스스로 경고하듯, 이 파이프라인은 LLM이 리스트·JSON 형식을 정확히 뱉어야 돌아간다. 그래서 작은 모델을 쓰면 파싱이 깨져 자주 실패하고, deepseek-r1·OpenAI o-시리즈·Claude 4 Sonnet 같은 큰 추론 모델을 권장한다. 즉 저비용 로컬 모델로는 품질이 크게 떨어질 수 있으니, 실제 데이터로 반드시 검증하고 모델을 골라야 한다.
| 역할 | 기술 | 메모 |
|---|---|---|
| 언어 | Python ≥3.10 | .python-version=3.10. Ruff 린트(line-length 100, target py310) |
| 패키지/의존성 | uv + hatchling | uv.lock(~1.16MB)로 잠금. 빌드 백엔드는 hatchling |
| 웹 서비스 | FastAPI + uvicorn | main.py가 REST 서버. Swagger UI는 /docs |
| 벡터DB 기본 | pymilvus ≥2.5.8 | Milvus Lite(로컬 파일 ./milvus.db)부터 Zilliz Cloud까지 |
| 청킹 | langchain-text-splitters | RecursiveCharacterTextSplitter + 자체 문장-윈도우 전략 |
| 문서 파싱 | pdfplumber · unstructured · docling | PDF/오피스/HTML 등 로컬 파일 로더 |
| 테스트 | pytest ≥8.3.5 | tests/가 패키지 구조를 미러링 |
DeepSearcher의 진짜 힘은 "무엇이든 갈아끼운다"는 데 있다. 아래는 소스의 __init__.py export 기준(README 표는 일부 낡음)이다. 다섯 축 전부가 플러그인처럼 교체된다.
| 축 | 개수 | 구현된 공급자 (일부) |
|---|---|---|
| LLM | 17 | OpenAI, DeepSeek, Anthropic(Claude), Gemini, XAI(Grok), Ollama(로컬), Bedrock, GLM, TogetherAI, SiliconFlow, Volcengine, Aliyun, WatsonX, Novita, PPIO, AzureOpenAI, JiekouAI |
| 임베딩 | 15 | OpenAIEmbedding, VoyageEmbedding, MilvusEmbedding, GeminiEmbedding, BedrockEmbedding, OllamaEmbedding, SentenceTransformerEmbedding, FastEmbedEmbedding, GLMEmbedding … |
| 벡터DB | 4 | Milvus(+Zilliz Cloud), Qdrant, OracleDB, AzureSearch |
| 파일 로더 | 5 | PDFLoader(pdfplumber), TextLoader, JsonFileLoader, UnstructuredLoader, DoclingLoader |
| 웹 크롤러 | 4 | FireCrawlCrawler, JinaCrawler, Crawl4AICrawler, DoclingCrawler |
pip install "deepsearcher[ollama]"처럼 필요한 공급자만 골라 설치하는 방식. Claude를 쓰려면 [anthropic], Qdrant를 쓰려면 [qdrant]만 깔면 된다. 17개 LLM SDK를 전부 강제로 설치시키지 않는 영리한 구조.DeepSearcher엔 성격이 다른 에이전트가 둘 있다: DeepSearch(넓은 보고서형)와 ChainOfRAG(콕 집은 사실·멀티홉). 어느 걸 쓸지 사람이 정하지 않고 LLM이 고른다. 각 에이전트는 @describe_class(...) 데코레이터로 "나는 이런 질문에 강해"라는 설명을 달고 있고, 라우터는 그 설명들을 LLM에게 보여주며 최적의 하나를 고르게 한다.
# agent/chain_of_rag.py — 데코레이터가 라우팅의 근거가 된다
@describe_class(
"This agent can decompose complex queries and gradually find the "
"fact information of sub-queries. It is very suitable for handling "
"concrete factual queries and multi-hop questions."
)
class ChainOfRAG(RAGAgent):
...
병원 접수 데스크와 같다. 환자(질문)가 오면 접수원(RAGRouter)이 증상을 보고 "이건 내과, 저건 정형외과"로 보낸다. 각 과(에이전트)는 자기 전문 분야 설명을 문 앞에 붙여뒀고, 접수원은 그 설명을 읽고 배정한다.
DeepSearch의 진짜 알맹이는 _generate_gap_queries()다. 한 바퀴 검색한 뒤, 지금까지 모은 근거를 LLM에게 보여주며 "이 질문에 답하기엔 아직 뭐가 빠졌지? 빠진 걸 채울 새 질문을 만들어"라고 시킨다. 빈 리스트가 나오면(=충분함) 루프를 빠져나가고, 새 질문이 나오면 그걸 들고 다시 검색한다. 이 "검색→반성→재검색"이 단순 RAG와 딥리서치를 가르는 결정적 차이다.
# agent/deep_search.py — 병렬 검색 + LLM 재순위(요지)
# 하위 gap 질문들을 한꺼번에 비동기 검색
search_tasks = [self._search_chunks_from_vectordb(q, sub_gap_queries)
for q in sub_gap_queries]
search_results = await asyncio.gather(*search_tasks)
# 각 청크를 LLM이 YES/NO로 걸러 accept
if "YES" in response_content and "NO" not in response_content:
all_retrieved_results.append(retrieved_result)
references.add(retrieved_result.reference)
"A의 상사가 세운 회사의 본사는 어디?" 같은 멀티홉 질문은 한 번에 검색이 안 된다. ChainOfRAG(논문 arXiv 2501.14342 기반)는 매 홉마다 ① 다음에 무엇을 물어야 할지 후속 질문 생성(FOLLOWUP_QUERY_PROMPT) → ② 검색해 중간 답 도출 → ③ 근거 문서 선별 → ④ "이제 최종 답을 낼 만큼 정보가 모였나?"(early stopping) 판단을 반복한다. 마지막에 중간 Q-A 사슬을 엮어 최종 답을 만든다.
17개 LLM을 if provider == "OpenAI": ... elif ...로 나열하지 않는다. config.yaml의 provider 문자열(=클래스 이름)을 그대로 받아 동적으로 import·인스턴스화한다. 새 공급자를 추가해도 이 팩토리는 손댈 필요가 없다.
# configuration.py — ModuleFactory 요지
class_name = self.config.provide_settings[feature]["provider"]
module = __import__(module_name, fromlist=[class_name])
class_ = getattr(module, class_name)
return class_(**self.config.provide_settings[feature]["config"])
추론 모델은 <think>...</think> 같은 사고 과정을 답 앞에 붙이거나, 리스트를 ```python ... ``` 코드펜스로 감싸 뱉는다. BaseLLM.remove_think는 사고 블록을 잘라내고, literal_eval은 코드펜스를 벗겨 첫 리스트/딕셔너리만 안전하게 추출한다. "LLM 출력은 못 믿는다"를 전제로 한 방어 코드다.
# llm/base.py
@staticmethod
def remove_think(response_content: str) -> str:
if "<think>" in response_content and "</think>" in response_content:
end = response_content.find("</think>") + len("</think>")
response_content = response_content[end:]
return response_content.strip()
검색은 좁은 청크로 정확히 하되, 답을 합성할 땐 그 청크 주변 ±300자를 담아둔 wider_text(문장 윈도우)를 함께 넘겨 문맥 손실을 막는다. 또 Milvus 백엔드는 밀집 벡터 + BM25 희소 벡터를 RRFRanker로 합치는 하이브리드 검색도 지원한다 — 의미 검색과 키워드 검색의 장점을 동시에.
코드 무게중심을 줄 수가 말해준다. 가장 무거운 건 벡터DB 백엔드(oracle.py 536줄)와 두 핵심 에이전트(chain_of_rag.py 326, deep_search.py 319)다. 즉 이 레포의 지식은 agent/와 vector_db/에 몰려 있다.
① configuration.py(공급자가 어떻게 조립되는지) → ② online_query.py(질의 진입점) → ③ agent/rag_router.py(누가 답할지 고르는 법) → ④ agent/deep_search.py(반복·반성 루프의 실체) → ⑤ loader/splitter.py(문장-윈도우 청킹). 이 5개면 전체 그림이 잡힌다. 무거운 vector_db/*.py는 나중에.
질문 분해 → 병렬 검색 → LLM 재순위 → 반성/빈틈 채우기 → 종합. 요즘 "AI 에이전트"의 핵심인 계획-실행-반성 루프가 deep_search.py 한 파일에 응축돼 있다. RAG를 넘어서고 싶은 사람의 필독 코드.
추상 베이스 클래스(BaseLLM·BaseEmbedding·BaseVectorDB) + 동적 import 팩토리로 17개 공급자를 하나의 인터페이스 뒤에 숨기는 법. "if-elif 지옥" 없이 확장 가능한 설계의 실물 답안.
literal_eval(코드펜스 벗기기), remove_think(사고 블록 제거), YES/NO 파싱 폴백. 실제 LLM 앱을 프로덕션에 올리면 반드시 마주치는 "출력 형식 깨짐" 문제의 실전 처방전.
RecursiveCharacterTextSplitter + 문장-윈도우, Milvus의 밀집+BM25 하이브리드(RRFRanker), 컬렉션 라우팅. 벡터DB를 "그냥 넣고 뺀다" 수준을 넘어 검색 품질을 끌어올리는 기법들을 코드로 익힌다.
하위질문들을 asyncio.gather로 동시에 검색하는 팬아웃, 그리고 모든 LLM 호출의 토큰 사용량을 ChatResponse로 끝까지 추적하는 토큰 회계. 비용 민감한 LLM 앱의 필수 습관.
같은 코어를 Python API·deepsearcher CLI·REST 서버 세 가지로 노출하는 패키징. 오픈소스 도구를 여러 사용 방식으로 배포하는 표준 구조를 배운다.
uri: "./milvus.db")로 벡터DB 세팅 없이 PDF 몇 개 넣고 질문해 보기.DeepSearch에 로그를 추가해 하위질문·gap질문·YES/NO 판정을 눈으로 추적하기.naive_rag_query와 query로 각각 돌려 답 품질을 비교하기.| 항목 | 요구 |
|---|---|
| 런타임 | Python 3.10+. GPU 불필요 — 기본은 API 기반(LLM·임베딩). 원하면 Ollama·SentenceTransformers로 로컬 실행도 가능 |
| 설치 | pip install deepsearcher (또는 git clone && uv sync). 공급자별 extras: pip install "deepsearcher[ollama]" 등 |
| API 키 | 쓰는 공급자에 맞춰 환경변수(OPENAI_API_KEY, ANTHROPIC_API_KEY, FIRECRAWL_API_KEY 등, env.example 참고) |
| 벡터DB | 가장 쉬운 건 Milvus Lite(로컬 파일 ./milvus.db) — 서버 설치 불필요. 규모가 커지면 Milvus 서버/Zilliz Cloud/Qdrant로 전환 |
| 모델 선택 | 파이프라인이 엄격한 형식 출력에 의존 → 큰 추론 모델 권장(deepseek-r1, OpenAI o-시리즈, Claude 4 Sonnet). 작은 모델은 파싱 실패 잦음 |
| 실행 방식 | ① Python API ② CLI(deepsearcher load … / query "…") ③ FastAPI 서버(python main.py, 8000포트). Docker 이미지 제공. Jupyter에선 nest_asyncio.apply() 필요 |
LLM만 로컬(Ollama)로 두고 임베딩·벡터DB도 로컬로 구성하면, 사내 문서가 외부 클라우드로 나가지 않는 완전 오프라인 딥리서치가 된다. 규제·보안이 중요한 조직에 결정적 장점이다.
pip install deepsearcher 후 config.set_provider_config로 LLM/임베딩만 지정하고, load_from_local_files로 PDF 몇 개를 넣은 뒤 query("..."). "적재 → 질의"의 전체 왕복을 벡터DB 서버 없이 체험.
config.yaml의 llm.provider를 OpenAI→Ollama(로컬)로, embedding.provider를 바꿔 본다. 코드 한 줄 안 고치고 백엔드가 통째로 교체되는 공급자 추상화를 체감.
deep_search.py에 로그를 심어 하위질문(SUB_QUERY), 청크 YES/NO 판정, gap질문(REFLECT)을 한 번의 질의에서 전부 출력. "왜 이 질문이 3번이나 검색됐는지"를 데이터로 추적한다.
같은 질문 세트를 naive_rag_query와 query로 돌려 답의 근거 수·정확도·토큰 비용을 표로 비교. evaluation/의 멀티홉 QA 데이터셋을 활용하면 정량 평가도 가능.
BaseLoader를 상속해 예: .hwp(한글) 또는 Notion export 로더를 작성하고 config.yaml에 등록. 공급자 인터페이스를 진짜로 이해했는지 검증하는 최종 과제. 나아가 FINAL_ANSWER_PROMPT를 한국어 보고서 스타일로 튜닝해 보기.
| 주차 | 주제 | 할 것 |
|---|---|---|
| 1주 | 임베딩 & 벡터 검색 기초 | 임베딩이 뭔지, 코사인 유사도, top-k 검색. Milvus Lite로 직접 넣고 조회 |
| 2주 | 기본 RAG 파이프라인 | 청킹→임베딩→검색→프롬프트 합성. naive_rag.py 정독 후 최소 RAG 직접 구현 |
| 3주 | 청킹 전략 | RecursiveCharacterTextSplitter, 문장-윈도우, 오버랩. 청크 크기가 답 품질에 미치는 영향 실험 |
| 4주 | 하이브리드 검색 | 밀집 벡터 + BM25 희소, RRFRanker. vector_db/milvus.py의 하이브리드 코드 재현 |
| 5주 | 에이전트 루프 | 계획-실행-반성. deep_search.py를 뜯어 반성 루프를 자신의 작은 에이전트로 이식 |
| 6주 | 멀티홉 추론 | ChainOfRAG 논문(arXiv 2501.14342) 읽고 chain_of_rag.py와 대조 |
| 7주 | LLM 출력 신뢰성 | 구조화 출력, 파싱 폴백, 재시도. literal_eval/remove_think 패턴 확장 |
| 8주 | 서빙 & 평가 | FastAPI로 RAG 서비스화, evaluation/로 정량 벤치, 토큰·지연 모니터링 |
wider_text)까지 담아 정확도를 높인다.RRFRanker로 합쳐 각각의 약점을 보완하는 검색.config.yamldocs/(mkdocs-material) · DeepWiki deepwiki.com/zilliztech/deep-searcherpip install deepsearcher (v0.0.2, Apache-2.0)