0Pricing
AI Agents · 강의

다단계 조사 루프 설계

계획 → 검색 → 읽기 → 추출 → 종합 → 충분한 깊이에 도달할 때까지 반복하는 과정을 다룹니다.

다단계 조사 루프 설계은(는) CoddyKit의 무료 AI Agents 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Agents 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.

리서치 반복 과정이란 무엇입니까?

리서치 반복 과정은 LLM이 연구 목표를 달성할 때까지 계획하고, 검색하고, 읽고, 추출하고, 정보 공백을 식별하고, 반복하는 에이전트형 패턴입니다.

한 번만 실행하는 검색과 달리 이 반복 과정은 찾은 내용에 따라 조정됩니다. 예상하지 못한 단서를 따라가고 막다른 길은 버립니다.

1단계: 질문 분해

첫 단계는 리서치 질문을 하위 질문으로 나누는 것입니다. 이렇게 하면 방향이 정해진 검색 계획이 만들어지고 에이전트가 목적 없이 탐색하는 일을 방지할 수 있습니다.

import openai, json

client = openai.OpenAI(api_key='YOUR_OPENAI_KEY')

def decompose_question(question: str) -> list[str]:
    prompt = (
        f'Break this research question into 3-5 focused sub-questions.\n'
        f'Each sub-question should be independently searchable.\n'
        f'Question: "{question}"\n'
        f'Return JSON: {{"sub_questions": ["..."]}}'
    )
    resp = client.chat.completions.create(
        model='gpt-4o',
        messages=[{'role': 'user', 'content': prompt}],
        response_format={'type': 'json_object'}
    )
    return json.loads(resp.choices[0].message.content)['sub_questions']

questions = decompose_question('What are the main causes of inflation in 2024?')
print(questions)

2단계: 웹 검색

각 하위 질문에 대해 웹 검색을 실행합니다. 검색 인터페이스(Serper, Brave, Bing)를 사용해 주소 목록과 검색 요약을 가져옵니다. 모든 페이지를 읽지 말고 먼저 품질이 높은 출처를 우선합니다.

import requests

SERPER_KEY = 'YOUR_SERPER_API_KEY'

def search_web(query: str, num_results: int = 5) -> list[dict]:
    resp = requests.post(
        'https://google.serper.dev/search',
        headers={'X-API-KEY': SERPER_KEY, 'Content-Type': 'application/json'},
        json={'q': query, 'num': num_results}
    )
    resp.raise_for_status()
    results = resp.json().get('organic', [])
    return [
        {'title': r['title'], 'url': r['link'], 'snippet': r.get('snippet', '')}
        for r in results
    ]

3단계: 사실 읽기 및 추출

각 주소의 내용을 가져와 하위 질문과 관련된 핵심 사실을 추출합니다. LLM을 사용해 문서를 읽고 출처 주소와 함께 사실 목록을 생성합니다.

import httpx
from bs4 import BeautifulSoup

def fetch_text(url: str, max_chars: int = 4000) -> str:
    try:
        resp = httpx.get(url, timeout=10, follow_redirects=True,
                         headers={'User-Agent': 'ResearchAgent/1.0'})
        soup = BeautifulSoup(resp.text, 'html.parser')
        for tag in soup(['script', 'style', 'nav', 'footer']):
            tag.decompose()
        return soup.get_text(separator=' ', strip=True)[:max_chars]
    except Exception:
        return ''

def extract_facts(text: str, question: str, url: str) -> list[dict]:
    prompt = (
        f'Extract key facts from the text that answer: "{question}"\n'
        f'Return JSON: {{"facts": ["fact1", ...]}}\n\n'
        f'TEXT:\n{text[:3000]}'
    )
    resp = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[{'role': 'user', 'content': prompt}],
        response_format={'type': 'json_object'}
    )
    facts = json.loads(resp.choices[0].message.content).get('facts', [])
    return [{'fact': f, 'source': url} for f in facts]

4단계: 지식 공백 식별

읽은 후 LLM에 누적된 사실을 검토하고 아직 알려지지 않은 내용을 식별하도록 요청합니다. 이러한 공백이 다음 검색 차수의 검색어가 됩니다.

def identify_gaps(original_question: str, facts: list[dict]) -> list[str]:
    fact_text = '\n'.join(f'- {f["fact"]}' for f in facts[:20])
    prompt = (
        f'Original question: "{original_question}"\n'
        f'Facts gathered so far:\n{fact_text}\n\n'
        f'What key aspects are still unanswered? '
        f'Return 0-3 follow-up search queries (0 if research is complete).\n'
        f'JSON: {{"gaps": ["search query 1", ...]}}'
    )
    resp = client.chat.completions.create(
        model='gpt-4o',
        messages=[{'role': 'user', 'content': prompt}],
        response_format={'type': 'json_object'}
    )
    return json.loads(resp.choices[0].message.content).get('gaps', [])

종료 조건

반복 과정이 영원히 실행되지 않도록 명확한 중지 조건이 필요합니다. 다음과 같은 경우 중지합니다.

  • 새로운 공백이 식별되지 않음
  • 마지막 반복에서 새로운 사실이 추가되지 않음
  • 최대 반복 횟수에 도달함(안전 제한)
  • 전체 사실 수가 기준을 초과함(충분한 깊이)
def should_continue(gaps: list[str], new_facts_this_round: int,
                    iteration: int, total_facts: int) -> bool:
    if iteration >= 5:
        return False          # Hard cap: 5 iterations
    if new_facts_this_round == 0:
        return False          # No new information found
    if not gaps:
        return False          # LLM says research is complete
    if total_facts >= 50:
        return False          # Sufficient depth reached
    return True

if __name__ == '__main__':
    print('Continue (has gaps, new facts)?', should_continue(['gap1'], 4, iteration=1, total_facts=10))
    print('Continue (no new facts)?', should_continue(['gap1'], 0, iteration=1, total_facts=10))

사실 중복 제거

여러 출처가 같은 사실을 보고하는 경우가 많습니다. 의미상 같은 사실을 합치고 가장 신뢰할 수 있는 출처가 있는 버전만 남기도록 LLM에 요청해 중복을 제거합니다.

def deduplicate_facts(facts: list[dict]) -> list[dict]:
    if len(facts) <= 3:
        return facts
    fact_text = '\n'.join(
        f'{i}: {f["fact"]} (source: {f["source"]})' for i, f in enumerate(facts)
    )
    prompt = (
        f'Remove duplicate or near-duplicate facts. Keep the most informative version.\n'
        f'Return JSON: {{"keep_indices": [0, 1, ...]}}\n\n'
        f'FACTS:\n{fact_text}'
    )
    resp = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[{'role': 'user', 'content': prompt}],
        response_format={'type': 'json_object'}
    )
    indices = json.loads(resp.choices[0].message.content).get('keep_indices', [])
    return [facts[i] for i in indices if i < len(facts)]

전체 리서치 반복 과정

모든 단계를 하나의 research() 함수로 결합하고, 종료될 때까지 반복 과정을 실행합니다.

def research(question: str) -> dict:
    all_facts = []
    iteration = 0

    # Phase 1: Decompose into sub-questions
    queries = decompose_question(question)

    while True:
        new_facts_this_round = 0

        for query in queries:
            results = search_web(query, num_results=3)
            for result in results:
                text = fetch_text(result['url'])
                if not text:
                    continue
                facts = extract_facts(text, query, result['url'])
                all_facts.extend(facts)
                new_facts_this_round += len(facts)

        all_facts = deduplicate_facts(all_facts)
        gaps = identify_gaps(question, all_facts)
        iteration += 1

        if not should_continue(gaps, new_facts_this_round, iteration, len(all_facts)):
            break

        queries = gaps  # next iteration searches for the gaps

    return {'facts': all_facts, 'iterations': iteration}

출처 추적

최종 보고서에 인용을 포함할 수 있도록 모든 사실에 출처 주소가 있어야 합니다. 처리 중에 출처 메타데이터를 절대 삭제하지 마십시오. 인용 계층에 필요합니다.

def add_fact(fact_list: list, fact_text: str, source_url: str, iteration: int):
    fact_list.append({
        'fact':      fact_text,
        'source':    source_url,
        'iteration': iteration,
        'verified':  False   # set to True after cross-referencing
    })

# Example:
facts_store = []
add_fact(facts_store, 'Global inflation peaked at 9.1% in June 2022',
         'https://bls.gov/news.release/cpi.htm', iteration=1)

print(f'Logged {len(facts_store)} fact(s):')
for f in facts_store:
    print(f"  - {f['fact']} (source: {f['source']})")

속도를 위한 병렬 검색

순차 검색은 느립니다. 검색어 5개 × 주소 3개 × 각 1회 가져오기 = 순차적인 웹 요청 15회입니다. 각 반복 안에서 가져오기를 병렬화하려면 concurrent.futures를 사용합니다.

from concurrent.futures import ThreadPoolExecutor, as_completed

def parallel_research_round(queries: list[str]) -> list[dict]:
    collected = []

    def process_query(query):
        results = search_web(query, num_results=3)
        facts = []
        for r in results:
            text = fetch_text(r['url'])
            if text:
                facts.extend(extract_facts(text, query, r['url']))
        return facts

    with ThreadPoolExecutor(max_workers=4) as ex:
        futures = {ex.submit(process_query, q): q for q in queries}
        for future in as_completed(futures):
            collected.extend(future.result())

    return collected

반복 과정 진행 상황 모니터링

반복이 중지된 이유나 예상보다 오래 실행된 이유를 파악할 수 있도록 각 반복을 기록합니다. 검색어 수, 추가된 사실, 식별된 공백을 포함합니다.

import logging

log = logging.getLogger('research_loop')
import sys
logging.basicConfig(level=logging.INFO, stream=sys.stdout)

def log_iteration(iteration: int, queries: list[str],
                  new_facts: int, total_facts: int, gaps: list[str]):
    log.info(
        'Iteration %d | Queries: %d | New facts: %d | Total: %d | Gaps: %d',
        iteration, len(queries), new_facts, total_facts, len(gaps)
    )
    if gaps:
        for g in gaps:
            log.debug('  Gap query: %s', g)

if __name__ == '__main__':
    log_iteration(iteration=2, queries=['who are our top competitors?'], new_facts=4, total_facts=12, gaps=['pricing data'])

새로운 정보가 발견되지 않을 때 리서치 반복 과정을 종료하는 것은 무엇입니까?

종료 조건을 이해하면 무한 반복을 방지하고 에이전트가 합리적인 시간 안에 결과를 제공하도록 할 수 있습니다.

리서치 반복 과정 설계 복습

다단계 리서치 반복 과정은 분해 → 검색 → 읽기 → 추출 → 공백 식별 → 반복의 순서로 진행됩니다. 공백이 없어지거나, 새로운 사실이 발견되지 않거나, 정해진 반복 상한에 도달하면 중지합니다.

항상 모든 사실에 출처 주소를 기록하고, 속도를 위해 검색을 병렬화하며, 종합하기 전에 사실의 중복을 제거합니다.

자주 묻는 질문

“다단계 조사 루프 설계” 강의는 무료인가요?

네 — “다단계 조사 루프 설계” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Agents 강의 전체를 잠금 해제할 수 있습니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.

“다단계 조사 루프 설계”에서 뭘 배우나요?

계획 → 검색 → 읽기 → 추출 → 종합 → 충분한 깊이에 도달할 때까지 반복하는 과정을 다룹니다. 브라우저에서 직접 실행하는 실습 코드로 AI Agents을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

AI Agents을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 AI Agents은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“다단계 조사 루프 설계” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 AI Agents 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 AI Agents 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 다단계 조사 루프 설계
  2. 출처 검증 및 인용
  3. 구조화된 보고서 생성
  4. 사실 확인 및 환각 방지
← AI Agents(으)로 돌아가기