Files
AI/오픈소스분석자료/OpenDeepResearcher_분석_및_기능명세.md
LASTA_DEV01\lasta 9e88f4c7ad ontology
2026-05-13 19:57:34 +09:00

1328 lines
41 KiB
Markdown

# OpenDeepResearcher 분석 및 기능명세
분석 대상: `C:\Users\lasta\MyProject\AI\참고\OpenDeepResearcher-main`
작성 목적: 오픈 프로젝트 `OpenDeepResearcher-main`을 범용 온톨로지 구축 플랫폼의 기본 소스로 재사용하기 위해, 구조와 기능을 상세히 분석하고 현재 프로젝트에 이식 가능한 기능 명세를 정의한다.
## 1. 프로젝트 개요
`OpenDeepResearcher`는 사용자의 연구 질문을 입력받아 LLM이 검색어를 만들고, 검색 API로 웹 문서를 찾고, 각 문서의 유용성을 LLM으로 평가한 뒤, 필요한 정보만 추출하고, 추가 검색 필요 여부를 다시 판단하는 반복형 딥리서치 노트북이다.
저장소는 라이브러리/패키지 형태가 아니라 Jupyter Notebook 중심의 예제 프로젝트다. 핵심 코드는 두 노트북 안에 거의 동일하게 포함되어 있다.
| 파일 | 역할 |
|---|---|
| `README.md` | 프로젝트 개요, 요구 API, 사용법 설명 |
| `open_deep_researcher.ipynb` | CLI/input 기반 딥리서치 루프 원본 |
| `open_deep_researcher_gradio.ipynb` | Gradio UI가 붙은 변형 |
| `LICENSE` | MIT License |
이 프로젝트의 본질은 완성된 제품이라기보다 “검색 기반 연구 루프의 최소 구현”이다. 따라서 현재 범용 온톨로지 구축 플랫폼에서는 코드를 그대로 복사하기보다, 루프 구조와 프롬프트 역할, 비동기 처리 방식, 링크 중복 제거 로직, 검색 확장 판단 로직을 거의 원형에 가깝게 모듈화해 흡수하는 것이 적합하다.
## 2. 라이선스 및 재사용 조건
라이선스는 MIT License다.
재사용 가능 범위:
- 소스 복사, 수정, 병합, 배포, 상업적 사용 가능
- 단, 저작권 고지와 MIT 라이선스 문구를 소프트웨어의 주요 복사본 또는 실질적 일부에 포함해야 함
- “AS IS” 조건이므로 품질, 정확성, 특정 목적 적합성에 대한 보증은 없음
내 프로젝트에서 기본 소스로 사용할 경우 권장 조치:
- `THIRD_PARTY_NOTICES.md` 또는 `NOTICE``OpenDeepResearcher`, 원 저작권자 `mshumer`, MIT License를 명시
- 원본에서 차용한 모듈 파일 상단에 간단한 출처 주석 추가
- API 키, 모델명, 엔드포인트는 하드코딩하지 않고 기존 설정 체계로 이동
## 3. 기술 스택
| 영역 | 사용 기술 | 설명 |
|---|---|---|
| 실행 형태 | Jupyter Notebook, Google Colab 가정 | 독립 패키지가 아니라 노트북 셀 실행 방식 |
| 비동기 처리 | `asyncio`, `aiohttp`, `nest_asyncio` | 검색, 페이지 fetch, LLM 평가/추출을 병렬 처리 |
| 검색 | SERPAPI Google Search | 질의별 검색 결과 URL 수집 |
| 페이지 텍스트화 | Jina Reader API `https://r.jina.ai/` | URL을 텍스트로 변환해 LLM 입력으로 사용 |
| LLM 호출 | OpenRouter Chat Completions | 검색어 생성, 유용성 평가, 컨텍스트 추출, 추가 검색 판단, 최종 보고서 생성 |
| UI | Gradio | Gradio 노트북에서 입력/출력 화면 제공 |
필수 API 키:
| 환경 값 | 원본 변수명 | 용도 |
|---|---|---|
| OpenRouter API Key | `OPENROUTER_API_KEY` | LLM 호출 |
| SERPAPI API Key | `SERPAPI_API_KEY` | Google 검색 |
| Jina API Key | `JINA_API_KEY` | 웹페이지 텍스트 추출 |
기본 모델:
```text
anthropic/claude-3.5-haiku
```
현재 프로젝트 이식 시에는 OpenRouter에 고정하지 않고 기존 `crawler_platform.app.core.extractor.ai_provider`의 OpenAI-compatible 구조와 맞춰 `provider`, `base_url`, `model`, `api_key_env`로 일반화하는 것이 좋다.
## 4. 전체 아키텍처
원본의 전체 흐름은 다음과 같다.
```mermaid
flowchart TD
A["사용자 연구 질문"] --> B["LLM: 초기 검색어 생성"]
B --> C["SERPAPI: 검색어별 Google 검색"]
C --> D["검색 결과 URL 통합 및 중복 제거"]
D --> E["Jina: URL별 웹페이지 텍스트 추출"]
E --> F["LLM: 페이지 유용성 Yes/No 평가"]
F --> G{"유용한가?"}
G -- "Yes" --> H["LLM: 관련 컨텍스트 추출"]
G -- "No" --> I["폐기"]
H --> J["컨텍스트 누적"]
J --> K["LLM: 추가 검색 필요 여부 판단"]
K -- "검색어 리스트" --> C
K -- "<done>" --> L["LLM: 최종 보고서 생성"]
```
구성 요소를 책임 기준으로 나누면 다음과 같다.
| 구성 요소 | 원본 함수 | 책임 |
|---|---|---|
| LLM 클라이언트 | `call_openrouter_async` | Chat Completions API 호출 |
| 검색어 생성기 | `generate_search_queries_async` | 사용자 질문을 검색 질의 목록으로 변환 |
| 검색 클라이언트 | `perform_search_async` | 검색어를 URL 리스트로 변환 |
| 웹 텍스트 fetcher | `fetch_webpage_text_async` | URL 본문을 텍스트로 변환 |
| 관련성 평가기 | `is_page_useful_async` | 페이지가 질문에 유용한지 판정 |
| 컨텍스트 추출기 | `extract_relevant_context_async` | 페이지에서 질문 관련 정보만 추출 |
| 반복 계획기 | `get_new_search_queries_async` | 누적 컨텍스트를 보고 다음 검색어 또는 종료 판단 |
| 보고서 생성기 | `generate_final_report_async` | 누적 컨텍스트 기반 최종 답변 작성 |
| 링크 처리 파이프라인 | `process_link` | fetch → 평가 → 추출을 URL 단위로 수행 |
| 메인 루프 | `async_main`, `async_research` | 반복 실행, 상태 누적, 종료 제어 |
## 5. 노트북별 상세 분석
### 5.1 `open_deep_researcher.ipynb`
CLI형 또는 콘솔 입력형 구현이다.
주요 특징:
- `input()`으로 사용자 질문과 최대 반복 횟수를 받음
- 초기 검색어를 LLM으로 생성
- 반복마다 검색어별 SERPAPI 요청을 동시에 실행
- 검색 결과 URL을 딕셔너리로 중복 제거
- URL별 Jina fetch, LLM 유용성 평가, LLM 컨텍스트 추출을 동시에 실행
- 누적 컨텍스트를 LLM에 제공해 다음 검색어 또는 `<done>` 판단
- 종료 후 최종 보고서를 생성하고 출력
상태 변수:
| 변수 | 의미 |
|---|---|
| `aggregated_contexts` | 모든 반복에서 추출한 유용 컨텍스트 누적 |
| `all_search_queries` | 지금까지 사용한 모든 검색어 |
| `new_search_queries` | 현재 반복에서 실행할 검색어 |
| `iteration_limit` | 최대 반복 횟수 |
| `unique_links` | 한 반복 안에서 중복 제거된 URL과 URL을 발견한 검색어 매핑 |
핵심 장점:
- 구조가 단순하고 이해하기 쉽다.
- 모든 외부 I/O를 비동기로 처리해 속도상 이점이 있다.
- 페이지 fetch, 관련성 판정, 컨텍스트 추출을 기능별로 분리했다.
- 검색이 부족한지 LLM이 판단하는 자기 확장 루프가 있다.
핵심 한계:
- `eval(response)`로 LLM 출력을 파싱하므로 보안상 위험하다.
- 검색 결과 URL의 전역 중복 제거가 없다. 반복 간 동일 URL 재처리 가능성이 있다.
- 출처 URL, 제목, 검색어, 평가 결과가 최종 컨텍스트와 함께 구조화 저장되지 않는다.
- 실패 재시도, rate limit, timeout, backoff가 없다.
- 토큰 예산 관리가 단순하다. 페이지 본문은 앞 20,000자만 사용한다.
- 최종 보고서에 인용/근거 링크가 구조적으로 연결되지 않는다.
- 온톨로지 엔티티/관계 추출 기능은 없다.
### 5.2 `open_deep_researcher_gradio.ipynb`
Gradio UI를 붙인 구현이다. 연구 루프 자체는 원본과 거의 동일하다.
추가된 함수:
| 함수 | 역할 |
|---|---|
| `async_research(user_query, iteration_limit)` | 콘솔 입력 없이 연구 루프 실행 후 결과와 로그 반환 |
| `run_research(user_query, iteration_limit=10)` | `asyncio.run`으로 비동기 루프 실행 |
| `gradio_run(user_query, iteration_limit)` | Gradio 이벤트 핸들러, 예외 처리 |
UI 구성:
| Gradio 컴포넌트 | 용도 |
|---|---|
| `Textbox(lines=2)` | 연구 질문 입력 |
| `Number(value=10)` | 최대 반복 횟수 입력 |
| `Textbox(label="Final Report")` | 최종 보고서 출력 |
| `Textbox(label="Intermediate Steps Log")` | 실행 로그 출력 |
현재 프로젝트에는 이미 FastAPI와 프론트엔드가 있으므로 Gradio 코드는 직접 이식 대상은 아니다. 다만 `async_research`처럼 UI에서 호출 가능한 순수 함수형 API로 연구 루프를 분리한 점은 참고할 가치가 있다.
## 6. 함수별 기능 명세
### 6.1 `call_openrouter_async`
목적: OpenRouter Chat Completions API를 비동기로 호출한다.
입력:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| `session` | `aiohttp.ClientSession` | 공유 HTTP 세션 |
| `messages` | `list[dict]` | Chat Completions 메시지 |
| `model` | `str` | 사용할 모델명, 기본값 `DEFAULT_MODEL` |
처리:
1. `Authorization: Bearer {OPENROUTER_API_KEY}` 헤더 구성
2. `OPENROUTER_URL`로 POST 요청
3. 응답이 200이면 `choices[0].message.content` 반환
4. 구조 오류 또는 HTTP 오류이면 `None` 반환
출력:
| 성공 | 실패 |
|---|---|
| assistant 메시지 문자열 | `None` |
이식 시 개선 명세:
- OpenRouter 전용 함수가 아니라 `AsyncLLMClient.complete(messages, model, response_schema=None)` 형태로 추상화
- timeout, retry, backoff, rate limit 처리
- 오류를 `print`하지 않고 구조화 로그와 DB 실행 기록에 저장
- 비용/토큰 사용량 저장
- JSON 모드 또는 스키마 응답 옵션 지원
### 6.2 `generate_search_queries_async`
목적: 사용자 질문에서 최대 4개의 검색어를 생성한다.
입력:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| `session` | `aiohttp.ClientSession` | HTTP 세션 |
| `user_query` | `str` | 원본 연구 질문 |
원본 프롬프트 요구:
- 전문 연구 보조자 역할
- 최대 4개 distinct, precise search query
- Python 문자열 리스트만 출력
출력:
```python
["query1", "query2", "query3"]
```
원본 한계:
- `eval(response)` 사용
- 검색어 품질 기준이 약함
- 검색어 언어, 도메인, 시간 범위, 출처 유형 제약이 없음
온톨로지 플랫폼용 개선 명세:
검색어 객체를 문자열이 아닌 구조로 반환해야 한다.
```json
{
"queries": [
{
"query": "perfume note taxonomy ontology extraction",
"purpose": "Find ontology classes and relation candidates",
"target_entity_types": ["Perfume", "Note", "Accord"],
"expected_source_type": "reference"
}
]
}
```
필수 검증:
- `queries`는 1개 이상 4개 이하
- 중복 검색어 제거
- 빈 문자열 제거
- 이전 검색어와 의미적으로 거의 동일한 검색어 제거
### 6.3 `perform_search_async`
목적: 검색어 하나를 SERPAPI Google 검색에 보내 URL 리스트를 얻는다.
입력:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| `session` | `aiohttp.ClientSession` | HTTP 세션 |
| `query` | `str` | 검색어 |
요청 파라미터:
| 키 | 값 |
|---|---|
| `q` | 검색어 |
| `api_key` | `SERPAPI_API_KEY` |
| `engine` | `google` |
출력:
- `organic_results[*].link`만 추출한 URL 리스트
- 실패 시 빈 리스트
이식 시 개선 명세:
- `SearchProvider` 인터페이스 도입
- SERPAPI, Tavily, Bing, Google CSE, 로컬 색인 등을 교체 가능하게 구성
- 검색 결과에 URL만 남기지 말고 제목, snippet, rank, source, query를 함께 저장
- 도메인 allow/block list 지원
- PDF, HTML, GitHub, 논문, 문서 등 source type 태깅
권장 데이터 모델:
```json
{
"url": "https://example.com/page",
"title": "Page title",
"snippet": "Search result snippet",
"rank": 1,
"query": "original search query",
"provider": "serpapi",
"retrieved_at": "ISO-8601"
}
```
### 6.4 `fetch_webpage_text_async`
목적: Jina Reader API로 URL의 웹페이지 텍스트를 가져온다.
입력:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| `session` | `aiohttp.ClientSession` | HTTP 세션 |
| `url` | `str` | 원본 URL |
처리:
- `full_url = f"{JINA_BASE_URL}{url}"`
- Jina API에 GET 요청
- 성공 시 텍스트 반환
출력:
| 성공 | 실패 |
|---|---|
| 페이지 텍스트 | 빈 문자열 |
현재 프로젝트와의 관계:
현재 프로젝트에는 이미 다음 기능이 있다.
- `crawler_platform.app.core.crawler.fetchers.make_fetcher`
- `crawler_platform.app.core.crawler.plugins.ParserRegistry`
- `SiteCrawler`
- `GraphResearchLoop`
따라서 Jina fetcher를 반드시 그대로 쓸 필요는 없다. 다만 외부 웹 텍스트 추출 대체 경로로 `JinaTextFetcher` 어댑터를 추가하면 좋다.
이식 시 개선 명세:
- 기존 fetcher/parser와 동일한 결과 객체로 변환
- URL, final_url, title, raw_text, clean_text, markdown, status_code, warnings 포함
- robots.txt 정책을 기존 `RobotsPolicy`와 통합
- 실패 시 fallback fetcher 사용 가능
### 6.5 `is_page_useful_async`
목적: 웹페이지 본문이 사용자 질문에 유용한지 LLM으로 이진 판정한다.
입력:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| `session` | `aiohttp.ClientSession` | HTTP 세션 |
| `user_query` | `str` | 원본 질문 |
| `page_text` | `str` | 페이지 본문 |
원본 프롬프트:
- critical research evaluator
- 질문과 페이지 내용을 보고 유용성 판단
- 정확히 `Yes` 또는 `No`만 출력
- 본문은 앞 20,000자만 사용
출력:
```text
Yes
No
```
한계:
- 이유, 점수, 불확실성이 없다.
- 현재 온톨로지에서 어떤 gap을 채우는지 판단하지 않는다.
- 페이지 품질, 신뢰도, 중복성, 출처 유형을 반영하지 않는다.
온톨로지 플랫폼용 개선 명세:
```json
{
"useful": true,
"score": 0.82,
"reason": "Contains explicit product-note relationships relevant to target predicates.",
"matched_entity_types": ["Perfume", "Note"],
"matched_predicates": ["hasTopNote", "hasBaseNote"],
"fills_gaps": ["missing note relationships for product pages"],
"source_quality": "primary|secondary|low",
"recommended_action": "extract|crawl_links|skip"
}
```
이 기능은 현재 프로젝트의 `RelevanceEngine`과 결합하는 것이 좋다. 원본의 Yes/No 판정은 LLM 기반 의미 판정으로 유지하되, 기존 점수 기반 링크 우선순위와 함께 사용한다.
### 6.6 `extract_relevant_context_async`
목적: 유용하다고 판단된 페이지에서 질문 답변에 필요한 관련 컨텍스트만 추출한다.
입력:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| `session` | `aiohttp.ClientSession` | HTTP 세션 |
| `user_query` | `str` | 원본 질문 |
| `search_query` | `str` | 해당 페이지를 발견한 검색어 |
| `page_text` | `str` | 페이지 본문 |
원본 출력:
- 일반 plain text
- 별도 구조 없음
온톨로지 플랫폼용 개선 명세:
컨텍스트 추출 결과는 반드시 출처와 증거 범위를 포함해야 한다.
```json
{
"source": {
"url": "https://example.com/item",
"search_query": "query used",
"title": "Page title"
},
"contexts": [
{
"text": "short extracted evidence",
"summary": "what this evidence supports",
"entity_candidates": [
{"name": "Bergamot", "type": "Note"}
],
"relation_candidates": [
{
"subject": "Product A",
"predicate": "hasTopNote",
"object": "Bergamot"
}
],
"confidence": 0.78
}
]
}
```
현재 프로젝트에서는 이 결과를 다음 두 경로 중 하나로 연결할 수 있다.
1. `ExtractionPageContext`로 변환해 기존 `Extractor.extract_from_context`에 전달
2. `ExtractedEntity`, `ExtractedClaim`, `ExtractionBundle`로 직접 변환
권장 방향은 1번이다. 원본의 컨텍스트 추출기는 “정보 압축기”로 두고, 실제 온톨로지 엔티티/클레임 추출은 기존 `LLMJsonExtractor`와 validation 계층을 쓰는 편이 일관성이 높다.
### 6.7 `get_new_search_queries_async`
목적: 지금까지 수행한 검색어와 누적 컨텍스트를 보고 추가 검색이 필요한지 판단한다.
입력:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| `session` | `aiohttp.ClientSession` | HTTP 세션 |
| `user_query` | `str` | 원본 질문 |
| `previous_search_queries` | `list[str]` | 이전 검색어 |
| `all_contexts` | `list[str]` | 누적 컨텍스트 |
출력:
| 상황 | 출력 |
|---|---|
| 추가 검색 필요 | Python list 형식의 검색어 목록 |
| 충분함 | `<done>` |
| 실패 | 빈 리스트 |
중요성:
이 함수가 OpenDeepResearcher의 핵심이다. 단순 검색 파이프라인을 “자기 확장형 연구 루프”로 만드는 역할을 한다.
한계:
- 연구 완료 기준이 모호하다.
- 검색어 중복 방지가 약하다.
- 온톨로지 gap, 충돌, 엔티티 커버리지와 연결되어 있지 않다.
- 누적 컨텍스트가 길어지면 토큰 초과 가능성이 크다.
온톨로지 플랫폼용 개선 명세:
```json
{
"status": "continue|done",
"completion_reason": "sufficient evidence for requested ontology gaps",
"coverage": {
"entity_types": {
"Perfume": 0.8,
"Brand": 0.6,
"Note": 0.9
},
"predicates": {
"hasBrand": 0.7,
"hasTopNote": 0.5
}
},
"new_queries": [
{
"query": "site:example.com perfume top notes bergamot",
"purpose": "Fill missing hasTopNote claims",
"priority": 0.86
}
],
"stop_conditions_met": [
"max useful source diversity reached",
"no unresolved high-priority gaps"
]
}
```
완료 판단 기준:
- 대상 엔티티 타입별 최소 수집량 충족
- 핵심 predicate별 evidence coverage 충족
- 최근 반복에서 신규 유용 컨텍스트가 일정 수 이하
- 동일 URL/도메인 반복 비율 증가
- LLM이 명시적으로 추가 gap을 찾지 못함
- 최대 반복/최대 비용/최대 시간 도달
### 6.8 `generate_final_report_async`
목적: 누적 컨텍스트를 기반으로 최종 연구 보고서를 생성한다.
입력:
| 파라미터 | 타입 | 설명 |
|---|---|---|
| `session` | `aiohttp.ClientSession` | HTTP 세션 |
| `user_query` | `str` | 원본 질문 |
| `all_contexts` | `list[str]` | 모든 컨텍스트 |
출력:
- 구조화된 문자열 보고서
온톨로지 플랫폼에서의 위치:
최종 보고서는 사용자 설명용 산출물이다. 범용 온톨로지 구축 플랫폼의 핵심 산출물은 보고서가 아니라 다음이어야 한다.
- 후보 엔티티
- 후보 관계/클레임
- 증거 텍스트
- 출처 URL
- 신뢰도
- 충돌/중복 판정
- 온톨로지 gap 변화
- 다음 수집 과제
따라서 보고서 생성기는 부가 기능으로 두고, 연구 세션 요약 또는 운영 리포트 생성에 활용한다.
권장 보고서 구조:
```text
1. 연구 목표
2. 수집 범위
3. 핵심 발견
4. 온톨로지 후보
5. 증거 기반 클레임 요약
6. 남은 지식 공백
7. 출처 목록
8. 다음 크롤링/검색 제안
```
### 6.9 `process_link`
목적: URL 하나에 대해 fetch → 유용성 평가 → 컨텍스트 추출을 수행한다.
입력:
| 파라미터 | 설명 |
|---|---|
| `link` | 처리할 URL |
| `user_query` | 원본 질문 |
| `search_query` | URL을 발견한 검색어 |
처리:
1. Jina로 페이지 텍스트 fetch
2. 빈 텍스트면 `None`
3. LLM으로 유용성 평가
4. `Yes`이면 관련 컨텍스트 추출
5. 컨텍스트가 있으면 반환
출력:
| 상황 | 출력 |
|---|---|
| 유용한 페이지 | 추출 컨텍스트 문자열 |
| 무용/실패 | `None` |
이식 시 권장 반환 타입:
```json
{
"url": "https://example.com/page",
"status": "extracted|skipped|failed",
"search_query": "query",
"usefulness": {
"useful": true,
"score": 0.82,
"reason": "..."
},
"context_count": 3,
"contexts": [],
"error": null
}
```
## 7. 반복 루프 기능 명세
### 7.1 입력
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `user_query` | `str` | 필수 | 연구 목표 또는 온톨로지 구축 목표 |
| `iteration_limit` | `int` | `10` | 최대 반복 횟수 |
| `max_queries_per_iteration` | `int` | `4` | 반복당 최대 검색어 수 |
| `max_results_per_query` | `int` | 검색 API 기본값 | 검색어당 결과 수 |
| `max_pages_per_iteration` | `int` | 별도 없음 | 원본에는 없음, 추가 필요 |
| `model` | `str` | `anthropic/claude-3.5-haiku` | LLM 모델 |
| `search_provider` | `str` | `serpapi` | 검색 제공자 |
| `fetch_provider` | `str` | `jina` | 텍스트 fetch 제공자 |
### 7.2 출력
원본 출력:
- 최종 보고서 문자열
- Gradio 버전은 중간 로그 문자열 추가
내 프로젝트용 출력:
```json
{
"session_id": 123,
"status": "completed|partial|failed|cancelled",
"query": "original user query",
"iterations": 3,
"search_queries": [],
"visited_urls": [],
"useful_sources": [],
"extracted_contexts": [],
"ontology_candidates": {
"entities": [],
"claims": []
},
"coverage": {},
"remaining_gaps": [],
"final_report": "..."
}
```
### 7.3 상태 전이
```mermaid
stateDiagram-v2
[*] --> Initialized
Initialized --> QueryGenerated
QueryGenerated --> Searching
Searching --> LinksDeduplicated
LinksDeduplicated --> FetchingPages
FetchingPages --> EvaluatingPages
EvaluatingPages --> ExtractingContexts
ExtractingContexts --> PlanningNext
PlanningNext --> Searching: new queries
PlanningNext --> Reporting: done
Searching --> PartialFailure: provider error
FetchingPages --> PartialFailure: fetch error
PartialFailure --> PlanningNext
Reporting --> Completed
Completed --> [*]
```
### 7.4 종료 조건
원본 종료 조건:
- LLM이 `<done>` 반환
- LLM이 새 검색어를 반환하지 않음
- 반복 횟수가 `iteration_limit`에 도달
추가해야 할 종료 조건:
- 유용 컨텍스트 증가량이 낮음
- 새 URL 발견률이 낮음
- 비용/토큰 제한 도달
- 사용자 취소
- 검색 API quota 소진
- 동일 도메인 반복 과다
- 온톨로지 gap coverage 목표 달성
## 8. 현재 프로젝트와의 통합 분석
현재 프로젝트에는 이미 범용 온톨로지 구축 플랫폼의 뼈대가 상당히 들어 있다.
관련 기존 모듈:
| 현재 프로젝트 모듈 | 역할 | OpenDeepResearcher와의 연결 |
|---|---|---|
| `crawler_platform.app.core.research.graph_research_loop.GraphResearchLoop` | 그래프/엔티티 중심 탐색 루프 | 외부 검색 기반 gap 탐색을 추가할 핵심 위치 |
| `ResearchMemoryStore` | 연구 세션, 히스토리, 큐, memory 저장 | OpenDeepResearcher의 로그/컨텍스트 누적을 구조화 저장 |
| `ExplorationQueue` | 탐색 대상 우선순위 큐 | 검색 결과 URL을 queue item으로 변환 가능 |
| `RelevanceEngine` | URL/엔티티 관련성 점수화 | LLM page usefulness 판정과 결합 |
| `GapTaskPlanner` | 지식 공백 기반 작업 생성 | 추가 검색어 생성 입력으로 사용 |
| `SiteCrawler` | 사이트 내부 크롤링 | 검색으로 발견한 seed URL을 사이트 크롤링으로 확장 |
| `LLMJsonExtractor` | 온톨로지 엔티티/클레임 JSON 추출 | 관련 컨텍스트에서 구조화 지식 추출 |
| `KnowledgeRepository` | 프로젝트, source, page, entity, claim 저장 | 연구 결과 영속화 |
통합 방향:
1. OpenDeepResearcher를 독립 노트북이 아니라 `WebResearchPlanner` 또는 `ExternalResearchLoop` 모듈로 분리한다.
2. 검색어 생성과 추가 검색 판단은 기존 `GapTaskPlanner`의 출력과 `ResearchMemoryStore.memory`를 입력으로 받는다.
3. 검색 결과 URL은 `ExplorationQueue``target_type="url"`로 넣는다.
4. URL fetch와 파싱은 가능하면 기존 `make_fetcher`/`ParserRegistry`를 사용한다.
5. Jina는 별도 fetch provider로 추가한다.
6. 페이지 유용성 판정은 기존 `RelevanceEngine.score_url` 결과와 LLM 의미 판정을 합산한다.
7. 컨텍스트 추출 후 `ExtractionPageContext`를 만들고 기존 extractor로 엔티티/클레임을 저장한다.
8. 최종 보고서는 연구 세션 요약 API에서 생성한다.
## 9. 권장 신규 모듈 설계
### 9.1 패키지 위치
권장 파일 구조:
```text
crawler_platform/app/core/research/
external_research_loop.py
search_provider.py
async_llm_client.py
context_extractor.py
research_planner.py
source_usefulness.py
```
### 9.2 핵심 클래스
#### `AsyncResearchLLMClient`
책임:
- OpenAI-compatible 또는 OpenRouter API 비동기 호출
- JSON 응답 파싱
- retry/timeout/backoff
- token/cost metadata 수집
주요 메서드:
```python
async def complete(self, messages: list[dict[str, str]], *, model: str | None = None) -> str
async def complete_json(self, messages: list[dict[str, str]], *, schema: dict | None = None) -> dict
```
#### `SearchProvider`
책임:
- 검색어를 검색 결과 목록으로 변환
주요 메서드:
```python
async def search(self, query: str, *, limit: int = 10) -> list[SearchResult]
```
#### `ResearchPlanner`
책임:
- 초기 검색어 생성
- 추가 검색 필요 판단
- 종료 판단
- gap 기반 query 생성
주요 메서드:
```python
async def initial_queries(self, goal: str, ontology: dict) -> list[ResearchQuery]
async def next_step(self, state: ResearchState) -> ResearchDecision
```
#### `SourceUsefulnessEvaluator`
책임:
- 페이지가 연구 목표/온톨로지 gap에 유용한지 판단
- LLM 판정과 rule score 결합
주요 메서드:
```python
async def evaluate(self, goal: str, page: ParsedPage, gaps: list[dict]) -> UsefulnessDecision
```
#### `ResearchContextExtractor`
책임:
- 페이지 텍스트에서 연구 관련 컨텍스트 추출
- 출처와 evidence를 유지
주요 메서드:
```python
async def extract(self, goal: str, page: ParsedPage, query: ResearchQuery) -> list[ResearchContext]
```
#### `ExternalResearchLoop`
책임:
- OpenDeepResearcher의 전체 반복 루프를 현재 프로젝트 구조로 실행
- 검색 결과를 저장소와 큐에 반영
- 연구 세션 히스토리와 memory 업데이트
주요 메서드:
```python
async def run(self, request: ExternalResearchRequest) -> ExternalResearchResult
```
## 10. 데이터 모델 명세
### 10.1 `ResearchQuery`
```json
{
"query": "string",
"purpose": "string",
"priority": 0.0,
"target_entity_types": ["string"],
"target_predicates": ["string"],
"source_type": "web|paper|docs|github|unknown",
"iteration": 0
}
```
필수 필드:
- `query`
- `purpose`
- `priority`
검증:
- `query`는 3자 이상
- `priority`는 0.0 이상 1.0 이하
- 동일 세션 내 normalized query 중복 금지
### 10.2 `SearchResult`
```json
{
"url": "string",
"title": "string|null",
"snippet": "string|null",
"rank": 1,
"query": "string",
"provider": "serpapi",
"metadata": {}
}
```
검증:
- URL scheme은 `http` 또는 `https`
- fragment 제거
- trailing slash normalization
- 같은 normalized URL은 한 세션에서 한 번만 처리
### 10.3 `UsefulnessDecision`
```json
{
"useful": true,
"score": 0.0,
"reason": "string",
"recommended_action": "extract|crawl_links|skip",
"matched_entity_types": [],
"matched_predicates": [],
"fills_gaps": []
}
```
검증:
- `score`는 0.0 이상 1.0 이하
- `recommended_action`은 enum
- `useful=false`이면 `score < min_relevance` 권장
### 10.4 `ResearchContext`
```json
{
"url": "string",
"title": "string|null",
"search_query": "string",
"text": "string",
"summary": "string",
"evidence_text": "string",
"entity_candidates": [],
"relation_candidates": [],
"confidence": 0.0
}
```
검증:
- `text` 또는 `evidence_text`는 비어 있으면 안 됨
- 긴 본문 복사를 막기 위해 evidence는 짧게 제한
- relation candidate는 subject/predicate/object 중 최소 subject와 predicate 필요
### 10.5 `ResearchDecision`
```json
{
"status": "continue|done",
"reason": "string",
"new_queries": [],
"coverage": {},
"remaining_gaps": [],
"stop_conditions": []
}
```
## 11. API 기능 명세
현재 FastAPI에 추가할 수 있는 API 명세다.
### 11.1 외부 딥리서치 실행
```http
POST /research/external/run
```
요청:
```json
{
"project_name": "perfume",
"source_name": "web",
"goal": "Build ontology candidates for perfume notes and accords",
"iteration_limit": 5,
"max_queries_per_iteration": 4,
"max_results_per_query": 10,
"min_usefulness": 0.45,
"search_provider": "serpapi",
"fetch_provider": "existing|jina",
"llm_provider": "openai_compatible",
"llm_model": "model-name",
"llm_base_url": "http://localhost:1234/v1",
"same_domain_only": false
}
```
응답:
```json
{
"session_id": 1,
"status": "completed",
"iterations": 3,
"visited_count": 42,
"useful_count": 11,
"context_count": 25,
"entity_count": 18,
"claim_count": 37,
"remaining_gaps": [],
"report": "..."
}
```
### 11.2 연구 세션 조회
기존 `/research/sessions/{job_id}`에 외부 검색 세션의 상세 항목을 포함한다.
추가 필드:
```json
{
"search_queries": [],
"search_results": [],
"usefulness_decisions": [],
"contexts": [],
"coverage": {},
"cost": {
"llm_calls": 0,
"search_calls": 0,
"fetch_calls": 0
}
}
```
### 11.3 연구 세션 취소
```http
POST /research/sessions/{job_id}/cancel
```
필요 이유:
- OpenDeepResearcher 원본에는 취소 기능이 없다.
- 웹 UI에서 긴 리서치 작업을 실행할 경우 필수다.
## 12. 프롬프트 명세
### 12.1 검색어 생성 프롬프트
목표:
- 온톨로지 구축 목표를 검색 가능한 질의로 분해
- entity type, predicate, source type을 명시
출력은 JSON only:
```json
{
"queries": [
{
"query": "string",
"purpose": "string",
"priority": 0.0,
"target_entity_types": [],
"target_predicates": [],
"source_type": "web"
}
]
}
```
필수 규칙:
- 최대 4개
- 서로 다른 의도를 가져야 함
- 이미 수행한 검색어와 중복 금지
- target ontology와 직접 관련 없는 일반 검색어 금지
### 12.2 페이지 유용성 평가 프롬프트
목표:
- 페이지가 온톨로지 gap을 채우는 데 유용한지 판단
출력은 JSON only:
```json
{
"useful": true,
"score": 0.0,
"reason": "string",
"recommended_action": "extract",
"matched_entity_types": [],
"matched_predicates": [],
"fills_gaps": []
}
```
평가 기준:
- 명시적 사실 또는 관계가 있는가
- 대상 entity/predicate와 연결되는가
- 출처가 신뢰 가능한가
- 중복 정보가 아닌가
- 페이지 본문이 충분히 추출되었는가
### 12.3 컨텍스트 추출 프롬프트
목표:
- 전체 페이지 요약이 아니라 온톨로지 구축에 필요한 evidence만 추출
출력은 JSON only:
```json
{
"contexts": [
{
"text": "string",
"summary": "string",
"evidence_text": "string",
"entity_candidates": [],
"relation_candidates": [],
"confidence": 0.0
}
]
}
```
규칙:
- 본문에 없는 사실 생성 금지
- 네비게이션, 광고, 푸터, 배송/정책 문구 제외
- evidence는 짧게 유지
- ontology predicate와 매핑 가능한 relation candidate 우선
### 12.4 다음 검색 판단 프롬프트
목표:
- 더 검색할지, 종료할지, 어떤 gap을 더 채울지 결정
출력은 JSON only:
```json
{
"status": "continue",
"reason": "string",
"new_queries": [],
"remaining_gaps": [],
"coverage": {},
"stop_conditions": []
}
```
규칙:
- 충분하면 `status="done"`과 빈 `new_queries`
- 계속할 경우 최대 4개 query
- 새 query는 이전 query와 중복 금지
- 검색 목적과 우선순위를 포함
## 13. 원본 코드의 위험 요소와 수정 필요 사항
| 위험 요소 | 원본 위치 | 문제 | 수정 방향 |
|---|---|---|---|
| `eval(response)` | 검색어 파싱, 추가 검색어 파싱 | LLM 출력 실행 위험 | `json.loads`, `ast.literal_eval`, JSON schema 사용 |
| API 키 하드코딩 | 설정 상수 | 보안 및 운영 부적합 | 환경 변수/설정 파일/프로젝트 설정으로 이동 |
| 전역 중복 URL 관리 없음 | 메인 루프 | 반복 간 URL 재처리 가능 | 세션 단위 `visited_urls` 저장 |
| 컨텍스트 구조 없음 | `extract_relevant_context_async` | 출처/근거 추적 어려움 | 구조화 JSON과 evidence 필드 |
| 출처 인용 없음 | 최종 보고서 | 검증 어려움 | URL, title, evidence 연결 |
| retry/backoff 없음 | 모든 API 호출 | 일시적 실패에 취약 | retry 정책 추가 |
| rate limit 제어 없음 | `asyncio.gather` | API 제한 초과 가능 | semaphore/concurrency limit |
| 토큰 예산 단순 절단 | `page_text[:20000]` | 중요 정보 손실 가능 | content chunking, source zone 기반 선별 |
| 품질 필터 약함 | Yes/No 평가 | 낮은 품질 페이지 통과 가능 | score, source quality, duplication check |
| 온톨로지 저장 없음 | 전체 | 보고서 생성에 그침 | 기존 repository/entity/claim 저장과 연결 |
## 14. 거의 변형 없이 사용할 수 있는 부분
다음은 원본 구조를 거의 유지해도 되는 부분이다.
1. 반복형 연구 루프 개념
- 초기 검색어 생성
- 검색 실행
- 링크 중복 제거
- 페이지 평가
- 관련 컨텍스트 추출
- 추가 검색 판단
- 최종 요약
2. 비동기 실행 패턴
- 검색어별 검색을 `asyncio.gather`로 병렬 처리
- URL별 fetch/evaluate/extract를 병렬 처리
3. 검색 확장 프롬프트의 역할 분리
- 검색어 생성 프롬프트
- 페이지 유용성 평가 프롬프트
- 컨텍스트 추출 프롬프트
- 추가 검색 판단 프롬프트
4. Gradio 버전의 로그 누적 방식
- UI에 중간 진행 로그를 보여주는 개념은 현재 웹 프론트엔드 progress 표시로 재사용 가능
5. `<done>` 또는 종료 토큰 개념
- 다만 실제 구현은 JSON `status="done"`으로 바꾸는 것이 안전하다.
## 15. 변형이 필요한 부분
다음은 반드시 현재 프로젝트 방식에 맞춰 수정해야 한다.
| 원본 방식 | 변경 필요 방식 |
|---|---|
| Notebook 내부 상수 | 프로젝트 설정/환경변수/DB 저장 설정 |
| OpenRouter 전용 호출 | OpenAI-compatible async client |
| SERPAPI 고정 | 검색 provider 인터페이스 |
| Jina 고정 | fetch provider 또는 기존 fetcher fallback |
| plain text context | evidence 포함 구조화 context |
| 최종 보고서 중심 | entity/claim/evidence 중심 |
| print 로그 | DB research session history |
| Gradio UI | 기존 FastAPI + 웹 프론트엔드 |
| `eval` 파싱 | JSON schema/validator |
| 반복 내 중복 제거 | 세션 전체 URL/query 중복 제거 |
## 16. 범용 온톨로지 구축 플랫폼에서의 목표 기능명세
### 16.1 기능명: 외부 검색 기반 온톨로지 연구 루프
설명:
사용자가 입력한 연구 목표 또는 자동 감지된 온톨로지 gap을 기반으로 외부 웹 검색을 수행하고, 관련 문서에서 엔티티/관계 후보를 추출하여 지식 그래프 구축에 반영하는 반복형 연구 기능.
사용자 가치:
- seed URL 없이도 외부 웹에서 지식 후보를 발견할 수 있다.
- 지식 공백을 기반으로 자동 검색 계획을 세울 수 있다.
- 검색 결과가 단순 보고서가 아니라 온톨로지 엔티티/클레임으로 이어진다.
- 연구 과정과 근거를 세션 단위로 추적할 수 있다.
### 16.2 주요 사용자 시나리오
#### 시나리오 A: 새 도메인 온톨로지 후보 수집
1. 사용자가 도메인과 목표를 입력한다.
2. 시스템이 도메인 ontology seed를 바탕으로 검색어를 생성한다.
3. 외부 검색을 수행한다.
4. 유용한 페이지를 선별한다.
5. 컨텍스트와 엔티티/관계 후보를 추출한다.
6. 부족한 entity type/predicate를 파악해 추가 검색한다.
7. 최종적으로 후보 ontology와 출처 목록을 제시한다.
#### 시나리오 B: 기존 지식 그래프의 공백 보완
1. `GapTaskPlanner`가 부족한 predicate 또는 entity type을 찾는다.
2. 시스템이 gap별 검색어를 생성한다.
3. 검색 결과를 수집하고 유용성을 평가한다.
4. 기존 그래프에 없는 claim 후보만 우선 저장한다.
5. 충돌 가능 claim은 review 상태로 남긴다.
#### 시나리오 C: 특정 엔티티 확장 연구
1. 사용자가 특정 entity를 선택한다.
2. 시스템이 entity 이름, 타입, 기존 관계를 기반으로 검색어를 만든다.
3. 관련 source에서 추가 속성/관계를 추출한다.
4. entity 중심 neighborhood graph를 확장한다.
### 16.3 기능 요구사항
| ID | 요구사항 |
|---|---|
| ODR-FR-001 | 시스템은 연구 목표에서 최대 N개의 초기 검색어를 생성해야 한다. |
| ODR-FR-002 | 시스템은 검색어별 외부 검색을 비동기로 실행해야 한다. |
| ODR-FR-003 | 시스템은 검색 결과 URL을 세션 단위로 중복 제거해야 한다. |
| ODR-FR-004 | 시스템은 각 URL의 본문을 fetch/parser 계층을 통해 텍스트화해야 한다. |
| ODR-FR-005 | 시스템은 각 페이지의 유용성을 점수와 이유로 평가해야 한다. |
| ODR-FR-006 | 시스템은 유용한 페이지에서 관련 evidence context를 추출해야 한다. |
| ODR-FR-007 | 시스템은 추출 context를 온톨로지 extractor에 전달해 entity/claim 후보를 생성해야 한다. |
| ODR-FR-008 | 시스템은 entity/claim 후보를 기존 repository에 저장해야 한다. |
| ODR-FR-009 | 시스템은 누적 결과와 gap을 기반으로 추가 검색 여부를 결정해야 한다. |
| ODR-FR-010 | 시스템은 반복 종료 후 연구 세션 요약 보고서를 생성해야 한다. |
| ODR-FR-011 | 시스템은 모든 검색어, URL, 평가, 추출 결과, 오류를 세션 history에 저장해야 한다. |
| ODR-FR-012 | 시스템은 사용자가 실행 중인 연구 세션을 취소할 수 있어야 한다. |
### 16.4 비기능 요구사항
| ID | 요구사항 |
|---|---|
| ODR-NFR-001 | 외부 API 호출은 timeout과 retry를 가져야 한다. |
| ODR-NFR-002 | 동시 요청 수는 provider별로 제한 가능해야 한다. |
| ODR-NFR-003 | API 키는 코드에 하드코딩하지 않는다. |
| ODR-NFR-004 | LLM JSON 출력은 schema validation을 통과해야 한다. |
| ODR-NFR-005 | 연구 세션은 partial failure를 허용하고 가능한 결과를 보존해야 한다. |
| ODR-NFR-006 | 최종 entity/claim은 evidence와 source URL을 잃지 않아야 한다. |
| ODR-NFR-007 | 동일 URL과 동일 query는 세션 내 중복 실행하지 않는다. |
| ODR-NFR-008 | 긴 페이지는 chunking 또는 content zone 기반으로 처리한다. |
| ODR-NFR-009 | 비용, 호출 수, 처리 시간 지표를 기록한다. |
## 17. 구현 우선순위
### Phase 1: 원본 루프의 안전한 모듈화
- `eval` 제거
- 검색/LLM/fetch 클라이언트 분리
- FastAPI에서 호출 가능한 `ExternalResearchLoop` 추가
- 검색 결과와 로그를 `ResearchMemoryStore`에 저장
- 최종 보고서 문자열 생성까지 구현
### Phase 2: 온톨로지 추출 연결
- 유용 페이지를 `ExtractionPageContext`로 변환
- 기존 `LLMJsonExtractor` 호출
- `KnowledgeRepository.save_extraction_bundle`로 저장
- extracted claim과 source/evidence 연결 확인
### Phase 3: gap-aware research
- `GapTaskPlanner` 결과를 검색어 생성 프롬프트에 포함
- entity type/predicate coverage 산출
- 추가 검색 판단을 gap 기반으로 변경
### Phase 4: UI/운영 기능
- 웹 프론트엔드에 외부 연구 실행 화면 추가
- 세션 로그, 검색어, 유용성 평가, 추출 claim 표시
- 취소, 재시도, 결과 승인/반려 기능 추가
## 18. 테스트 명세
### 18.1 단위 테스트
| 테스트 | 검증 내용 |
|---|---|
| 검색어 JSON 파싱 | 잘못된 LLM 응답을 안전하게 거부 |
| URL normalization | fragment/trailing slash 중복 제거 |
| 검색 결과 dedupe | 같은 URL이 한 번만 처리됨 |
| usefulness parser | 점수/enum/schema 검증 |
| next decision parser | `continue`/`done` 처리 |
| context extractor parser | evidence 없는 결과 거부 |
### 18.2 통합 테스트
| 테스트 | 검증 내용 |
|---|---|
| mock search provider loop | 검색 → fetch → 평가 → 추출 → 종료 전체 흐름 |
| partial API failure | 일부 URL 실패해도 세션이 partial/completed로 보존 |
| repository 저장 | page, entity, claim, history 저장 확인 |
| duplicate iteration | 반복 간 동일 URL 재처리 방지 |
| gap-aware query | gap 입력이 query purpose에 반영됨 |
### 18.3 운영 테스트
| 테스트 | 검증 내용 |
|---|---|
| concurrency limit | 동시 호출 수 제한 |
| timeout | 오래 걸리는 provider 호출 중단 |
| cancellation | 세션 취소 요청 반영 |
| cost tracking | LLM/search/fetch 호출 수 기록 |
## 19. 결론
`OpenDeepResearcher`는 코드 규모는 작지만, 범용 온톨로지 구축 플랫폼에 매우 중요한 “외부 검색 기반 자기 확장 연구 루프”의 핵심 패턴을 제공한다. 현재 프로젝트는 이미 크롤러, parser, extractor, repository, graph research loop, memory store를 갖고 있으므로, 원본을 그대로 제품 코드로 넣기보다는 다음 부분을 원형에 가깝게 차용하는 것이 가장 효율적이다.
- LLM 기반 검색어 생성
- 검색 결과 병렬 수집
- URL 중복 제거
- 페이지 유용성 LLM 판정
- 관련 컨텍스트 추출
- 누적 컨텍스트 기반 추가 검색 판단
- 최종 연구 요약 생성
다만 현재 플랫폼의 목표는 보고서 생성이 아니라 온톨로지 지식 구축이므로, 최종 산출물은 반드시 `entity`, `claim`, `evidence`, `source`, `confidence`, `gap coverage` 중심으로 재설계해야 한다. 이 관점에서 OpenDeepResearcher는 “최종 보고서 도구”가 아니라 “외부 지식 발견 엔진”의 시작점으로 활용하는 것이 가장 적절하다.