본문으로 건너뛰기
AIDevOps
  • Learn
  • Learning Paths
  • Practice
  • Open Source
  • Books
  • Engineering

    AI DevOpsAI 서비스 개발·운영 전체 지도LLMOpsLLM 배포·평가·관측실전 프로젝트AI Agent 프로젝트 실습

    Knowledge

    Docs기술 문서 모음Blog엔지니어링 아티클Plogger개발 기록 피드

    Validate

    Certification3단계 역량 인증 · 준비 중
AI Models
LlamaMistralGemmaDeepSeekQwen
🧠 AI Agent 개발
금융 AI AgentLLM API 서버주식 투자 AgentAIOps AI Agent교육 AI Agent코딩 AI Agent
🤖 AI 실전 개발
AI 실전 입문 & 로드맵Hugging FaceLangChainLlamaIndexLLMOps|LangGraphMCPMulti-AgentAgent Evaluation
🧠 AI Core
AI 입문 & 로드맵ML FundamentalsLLM Fundamentals|Python AIC++|PyTorchTensorFlowJAX
🌱 Spring Cloud
Spring 입문 & 로드맵Spring Cloud GatewaySpring BootJava|Spring AISpring SecuritySpring BatchSpring JPA
🐳 DevOps
DevOps 입문 & 로드맵LinuxDockerCI/CD|Kubernetes 기본K8s 심화/실무PrometheusGrafana
🧱 인프라
인프라 입문 & 로드맵NginxRedis
☁️ 클라우드
클라우드 입문 & 로드맵AWSGCPAzureNCPCloudflare
🎨 Frontend
Frontend 입문 & 로드맵JavaScriptTypeScript|ReactNext.js|VueNuxt
📱 Mobile
Mobile 입문 & 로드맵KotlinAndroidFlutter
⚙️ Backend
Backend 입문 & 로드맵Python 기본FastAPIDjangoFlask|CGoGinNode.js
💾 Database
DB 입문 & 로드맵공통 SQLOracleMySQLPostgreSQL|MongoDB벡터 DB
🧪 검증
k6JMeternGrinder
AIDevOps

Engineering AI. From Code to Production.
AI와 AI Agent를 개발하고 운영하기 위한 엔지니어링 학습 플랫폼

Learn

  • 전체 가이드
  • Learning Paths
  • Practice
  • Books

Resources

  • AI DevOps
  • LLMOps
  • 실전 프로젝트
  • Docs
  • Blog
  • Plogger
  • Open Source
  • Certification (준비 중)

Start Here

  • AI Core 로드맵
  • AI 실전 개발 로드맵
  • Spring Cloud 로드맵
  • DevOps 로드맵
  • 인프라 로드맵

 

  • 클라우드 로드맵
  • Frontend 로드맵
  • Mobile 로드맵
  • Backend 로드맵
  • Database 로드맵
© 2026 AI DevOps Korea. All rights reserved.
이용약관개인정보처리방침Sitemaptestforge.kr
금융 AI Agent 03

🔍 투자 리서치 에이전트 RAG 패턴

Visitors

한국 DART 공시·미국 SEC 문서를 수집해 ChromaDB에 저장하고, 자연어 질문에 근거 문서를 기반으로 답하는 RAG 에이전트를 구현합니다. "삼성전자 2024년 영업이익은?", "AAPL 최근 실적 리스크는?" 같은 질문을 처리합니다.

← AI Agent 시리즈
🔍

목차

0 / 8
  1. RAG 아키텍처
  2. 공시 문서 수집
  3. DART 공시 수집
  4. SEC 문서 수집
  5. 임베딩 & 벡터 저장
  6. Retriever 구성
  7. RAG Agent 구성
  8. 실행 예제
목차 8개 섹션
  1. RAG 아키텍처
  2. 공시 문서 수집
  3. DART 공시 수집
  4. SEC 문서 수집
  5. 임베딩 & 벡터 저장
  6. Retriever 구성
  7. RAG Agent 구성
  8. 실행 예제

RAG 아키텍처

TEXT
[문서 수집 파이프라인]
DART API ──┐
SEC API  ──┼─→ 텍스트 청킹 → 임베딩 → ChromaDB
뉴스 크롤 ──┘

[질의 파이프라인]
사용자 질문 → 질문 임베딩 → 유사 문서 검색(Top-K)
           → LLM 컨텍스트 주입 → 근거 기반 답변
구성요소선택이유
임베딩 모델jhgan/ko-sroberta-multitask한국어 특화, 로컬 실행
벡터 DBChromaDB (로컬)설치 간단, 영속성 지원
LLMLlama 3.1 8B (Ollama)CPU 추론, 무료
청크 크기512 토큰, 64 오버랩공시 문단 단위 최적

공시 문서 수집

DART 공시 수집

collectors/dart_collector.pyPYTHON
import requests
import os
import zipfile
import io
from pathlib import Path
from dotenv import load_dotenv

load_dotenv()
DART_KEY = os.getenv("DART_API_KEY")

class DartCollector:
    BASE = "https://opendart.fss.or.kr/api"

    def get_corp_code(self, company_name: str) -> str | None:
        """기업명 → DART 고유번호"""
        res = requests.get(
            f"{self.BASE}/company.json",
            params={"crtfc_key": DART_KEY, "corp_name": company_name},
        )
        items = res.json().get("list", [])
        return items[0]["corp_code"] if items else None

    def get_financial_report(self, corp_code: str, year: int, report_type: str = "11011") -> list[dict]:
        """
        사업보고서 주요 재무 항목 조회
        report_type: 11011=사업보고서, 11012=반기보고서, 11013=분기보고서
        """
        res = requests.get(
            f"{self.BASE}/fnlttSinglAcnt.json",
            params={
                "crtfc_key": DART_KEY,
                "corp_code": corp_code,
                "bsns_year": str(year),
                "reprt_code": report_type,
            },
        )
        data = res.json()
        if data.get("status") != "000":
            return []
        return data.get("list", [])

    def get_disclosures(self, corp_code: str, bgn_date: str, end_date: str) -> list[dict]:
        """공시 목록 조회"""
        res = requests.get(
            f"{self.BASE}/list.json",
            params={
                "crtfc_key": DART_KEY,
                "corp_code": corp_code,
                "bgn_de": bgn_date,
                "end_de": end_date,
                "sort": "date",
                "sort_mth": "desc",
                "count": 20,
            },
        )
        return res.json().get("list", [])

    def get_disclosure_text(self, rcept_no: str) -> str:
        """공시 원문 다운로드 및 텍스트 추출"""
        res = requests.get(
            f"{self.BASE}/document.xml",
            params={"crtfc_key": DART_KEY, "rcept_no": rcept_no},
        )
        # ZIP 해제 후 텍스트 추출
        try:
            z = zipfile.ZipFile(io.BytesIO(res.content))
            texts = []
            for name in z.namelist():
                if name.endswith(".xml") or name.endswith(".html"):
                    content = z.read(name).decode("utf-8", errors="ignore")
                    # 태그 제거
                    import re
                    texts.append(re.sub(r"<[^>]+>", " ", content))
            return " ".join(texts)[:10000]  # 10,000자 제한
        except Exception:
            return ""

def build_dart_documents(company_name: str, years: list[int]) -> list[dict]:
    """
    기업의 사업보고서 데이터를 문서 리스트로 변환
    반환: [{"content": str, "metadata": dict}]
    """
    collector = DartCollector()
    corp_code = collector.get_corp_code(company_name)
    if not corp_code:
        return []

    documents = []
    for year in years:
        items = collector.get_financial_report(corp_code, year)
        for item in items:
            content = (
                f"[{company_name} {year}년 재무데이터]\n"
                f"계정명: {item.get('account_nm')}\n"
                f"당기: {item.get('thstrm_amount')}원\n"
                f"전기: {item.get('frmtrm_amount')}원\n"
            )
            documents.append({
                "content": content,
                "metadata": {
                    "company": company_name,
                    "year": year,
                    "account": item.get("account_nm"),
                    "source": "DART",
                },
            })
    return documents

SEC 문서 수집 (미국)

collectors/sec_collector.pyPYTHON
import requests
from langchain_core.tools import tool

SEC_HEADERS = {"User-Agent": "aidevops contact@aidevops.kr"}

class SecCollector:
    BASE = "https://data.sec.gov"

    def get_cik(self, ticker: str) -> str | None:
        """티커 → CIK 번호"""
        res = requests.get(
            "https://www.sec.gov/files/company_tickers.json",
            headers=SEC_HEADERS,
        )
        data = res.json()
        ticker_upper = ticker.upper()
        for item in data.values():
            if item["ticker"] == ticker_upper:
                return str(item["cik_str"]).zfill(10)
        return None

    def get_recent_10k(self, cik: str, count: int = 3) -> list[dict]:
        """최근 10-K(연간보고서) 목록"""
        res = requests.get(
            f"{self.BASE}/submissions/CIK{cik}.json",
            headers=SEC_HEADERS,
        )
        data = res.json()
        filings = data.get("filings", {}).get("recent", {})

        results = []
        forms = filings.get("form", [])
        dates = filings.get("filingDate", [])
        accs  = filings.get("accessionNumber", [])

        for i, form in enumerate(forms):
            if form == "10-K" and len(results) < count:
                results.append({
                    "form": form,
                    "date": dates[i],
                    "accession": accs[i],
                    "url": f"https://www.sec.gov/Archives/edgar/full-index/{dates[i][:4]}/",
                })
        return results

    def get_filing_text(self, cik: str, accession: str) -> str:
        """10-K 텍스트 추출 (앞 8,000자)"""
        acc_clean = accession.replace("-", "")
        url = f"{self.BASE}/Archives/edgar/data/{int(cik)}/{acc_clean}/{accession}.txt"
        try:
            res = requests.get(url, headers=SEC_HEADERS, timeout=10)
            import re
            text = re.sub(r"<[^>]+>", " ", res.text)
            text = re.sub(r"\s+", " ", text)
            return text[:8000]
        except Exception:
            return ""

def build_sec_documents(ticker: str) -> list[dict]:
    """미국 기업 10-K 데이터를 문서 리스트로 변환"""
    collector = SecCollector()
    cik = collector.get_cik(ticker)
    if not cik:
        return []

    documents = []
    for filing in collector.get_recent_10k(cik, count=2):
        text = collector.get_filing_text(cik, filing["accession"])
        if text:
            documents.append({
                "content": f"[{ticker} 10-K {filing['date']}]\n{text}",
                "metadata": {
                    "ticker": ticker,
                    "form": "10-K",
                    "date": filing["date"],
                    "source": "SEC",
                },
            })
    return documents

임베딩 & 벡터 저장

rag/vectorstore.pyPYTHON
import chromadb
from chromadb.config import Settings
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from pathlib import Path

# 한국어 특화 임베딩 모델 (로컬, 무료)
EMBED_MODEL = "jhgan/ko-sroberta-multitask"
DB_PATH = "D:/ai-agent-finance/chroma_db"

def get_embeddings():
    return HuggingFaceEmbeddings(
        model_name=EMBED_MODEL,
        model_kwargs={"device": "cpu"},
        encode_kwargs={"normalize_embeddings": True},
    )

def get_vectorstore(collection: str = "finance") -> Chroma:
    return Chroma(
        collection_name=collection,
        embedding_function=get_embeddings(),
        persist_directory=DB_PATH,
    )

def ingest_documents(documents: list[dict], collection: str = "finance") -> int:
    """
    문서를 청킹해 벡터 DB에 저장합니다.
    documents: [{"content": str, "metadata": dict}]
    반환: 저장된 청크 수
    """
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=512,
        chunk_overlap=64,
        separators=["\n\n", "\n", "。", ". ", " "],
    )

    chunks, metadatas = [], []
    for doc in documents:
        splits = splitter.split_text(doc["content"])
        chunks.extend(splits)
        metadatas.extend([doc["metadata"]] * len(splits))

    vs = get_vectorstore(collection)
    vs.add_texts(texts=chunks, metadatas=metadatas)
    vs.persist()
    print(f"✓ {len(chunks)}개 청크를 '{collection}' 컬렉션에 저장했습니다.")
    return len(chunks)

# 사용 예시
if __name__ == "__main__":
    from collectors.dart_collector import build_dart_documents
    from collectors.sec_collector import build_sec_documents

    # 한국 기업 인제스트
    docs = build_dart_documents("삼성전자", years=[2023, 2024])
    ingest_documents(docs, collection="kr_finance")

    # 미국 기업 인제스트
    docs = build_sec_documents("AAPL")
    ingest_documents(docs, collection="us_finance")

Retriever 구성

rag/retriever.pyPYTHON
from langchain_core.tools import tool
from rag.vectorstore import get_vectorstore

def _search(query: str, collection: str, k: int = 4) -> str:
    vs = get_vectorstore(collection)
    docs = vs.similarity_search(query, k=k)
    if not docs:
        return "관련 문서를 찾을 수 없습니다."
    return "\n\n---\n\n".join(
        f"[출처: {d.metadata.get('source','?')} {d.metadata.get('date','')}]\n{d.page_content}"
        for d in docs
    )

@tool
def search_kr_disclosure(query: str) -> str:
    """
    한국 기업 DART 공시 데이터베이스에서 관련 내용을 검색합니다.
    query: 검색할 내용 (예: "삼성전자 2024 영업이익", "SK하이닉스 반도체 재고")
    """
    return _search(query, "kr_finance")

@tool
def search_us_filing(query: str) -> str:
    """
    미국 기업 SEC 10-K 데이터베이스에서 관련 내용을 검색합니다.
    query: 검색할 내용 (예: "Apple revenue 2024", "NVDA data center risk")
    """
    return _search(query, "us_finance")

RAG Agent 구성

agents/research_agent.pyPYTHON
from langchain_ollama import ChatOllama
from langchain_anthropic import ChatAnthropic
from langchain.agents import create_react_agent, AgentExecutor
from langchain_core.prompts import PromptTemplate
from rag.retriever import search_kr_disclosure, search_us_filing
from tools.us_stock import get_us_stock_price
from tools.kr_stock import get_kr_stock_price
import os

RESEARCH_TOOLS = [
    search_kr_disclosure,
    search_us_filing,
    get_kr_stock_price,
    get_us_stock_price,
]

SYSTEM = """당신은 금융 투자 리서치 전문가입니다.
공시 문서와 재무 데이터를 기반으로 정확한 분석을 제공합니다.

중요 원칙:
- 검색된 문서의 내용만 근거로 답변합니다
- 출처(DART/SEC)를 명시합니다
- 수치는 원문 그대로 인용합니다
- 투자 권유는 하지 않습니다"""

def build_research_agent(use_claude: bool = False) -> AgentExecutor:
    if use_claude:
        llm = ChatAnthropic(
            model="claude-sonnet-4-6",
            api_key=os.getenv("ANTHROPIC_API_KEY"),
            temperature=0,
        )
    else:
        llm = ChatOllama(model="llama3.1:8b", temperature=0)

    prompt = PromptTemplate.from_template(
        SYSTEM + """

사용 가능한 도구:
{tools}

도구 이름: {tool_names}

질문: {input}
{agent_scratchpad}"""
    )

    agent = create_react_agent(llm, RESEARCH_TOOLS, prompt)
    return AgentExecutor(
        agent=agent,
        tools=RESEARCH_TOOLS,
        verbose=True,
        max_iterations=6,
        handle_parsing_errors=True,
    )
💡

Claude vs Ollama 선택: DART/SEC 문서는 길고 복잡합니다. 로컬 Llama 3.1 8B로 시작하되, 분석 품질이 낮다면 use_claude=True로 전환하세요. Claude는 긴 컨텍스트 처리와 한국어 이해도가 뛰어납니다.

실행 예제

PYTHON
from agents.research_agent import build_research_agent

agent = build_research_agent(use_claude=False)  # 로컬 Ollama

questions = [
    "삼성전자 2024년 영업이익은 얼마인가요?",
    "SK하이닉스의 주요 재무 리스크는 무엇인가요?",
    "Apple의 2024 10-K에서 언급된 주요 사업 리스크는?",
    "삼성전자 반도체 부문 실적 추이를 설명해주세요",
]

for q in questions:
    print(f"\nQ: {q}")
    result = agent.invoke({"input": q})
    print(f"A: {result['output']}")
BASH
# 먼저 문서 인제스트 실행
python rag/vectorstore.py

# 에이전트 테스트
python -c "
from agents.research_agent import build_research_agent
agent = build_research_agent()
r = agent.invoke({'input': '삼성전자 최근 영업이익은?'})
print(r['output'])
"