AI가 없는 뉴스를 지어냈다. 로컬 LLM 할루시네이션 잡기 솔직 후기

카테고리 없음|2026. 4. 28. 23:44

매일 아침 맥미니가 주식 리포트를 자동으로 만든다. Ollama(로컬 LLM)가 뉴스를 분석하고, 종목 추천까지 뽑아준다. 그걸 사이트에 자동 배포한다.

오늘 리포트를 확인했는데 뭔가 이상했다. 삼성전자 주가가 170,500원이었다. 실제 주가는 222,750원이다.

그리고 이런 뉴스가 있었다.

"삼성전자 1분기 매출 76조원 달성, HBM 출하 확대"
"AT&T CEO John Smith, 구독 서비스 확대 발표"

 

두 뉴스 모두 그날 없었던 뉴스다. AI가 만들어낸 거다.

 

 

왜 이런 일이 생기나

 

구조가 잘못됐다. 내가 만든 파이프라인은 이렇다.

 

1. economy_news.py — RSS 피드에서 뉴스 긁어오기
2. stock_report.py — 뉴스 텍스트를 Ollama에 던지기
3. Ollama 응답 — 종목 추천 + 헤드라인 + 시장 분석 JSON
4. git push → Vercel 자동 배포

 

여기서 3번이 문제다.

Ollama(qwen2.5:14b)는 한국 주식 뉴스를 훈련 데이터로 거의 못 봤다. 그러니까 오늘 뉴스에서 중요한 헤드라인을 뽑아줘라고 하면, 뉴스 내용을 이해하는 게 아니라 한국 경제 뉴스에 있을 법한 것을 생성한다.

 

삼성전자 매출 기사, AT&T CEO 발표 — 다 훈련 데이터 기억에서 나온 거다. 그럴듯해 보이지만 사실이 아니다. 이걸 할루시네이션(hallucination)이라고 한다. LLM이 없는 사실을 만들어내는 현상.

 

처음엔 프롬프트 문제라고 생각했다

 

그래서 프롬프트를 강화했다.

- 반드시 수집된 뉴스에서만 인용할 것
- 없는 뉴스를 만들지 말 것
- 헤드라인은 정확한 제목 그대로 쓸 것

 

결과는 이랬다.

 

"삼성전자, 2분기 반도체 수요 회복 전망" — 연합뉴스 경제

 

출처까지 "연합뉴스 경제"라고 붙여서 지어냈다. 프롬프트를 강화해도 소용없었다. AI는 모르는 걸 물어보면 아는 척한다. 그게 LLM의 작동 방식이다.

 

근본 해결 AI에게 뉴스 생성을 맡기지 마라

 

생각을 바꿨다. AI가 헤드라인을 "생성"하게 하지 말고, RSS에서 직접 뽑으면 된다.

IMPACT_KEYWORDS = {
    "high": ["금리", "인플레이션", "CPI", "FOMC", "반도체", "HBM", "earnings"],
    "mid":  ["주가", "상승", "하락", "실적", "전망", "acquisition"],
}

def classify_headlines(parsed):
    def score(item):
        text = item["title"] + " " + item["desc"]
        if any(k in text for k in IMPACT_KEYWORDS["high"]):
            return 2
        if any(k in text for k in IMPACT_KEYWORDS["mid"]):
            return 1
        return 0

    kr_sorted = sorted(parsed["kr"], key=score, reverse=True)
    us_sorted = sorted(parsed["us"], key=score, reverse=True)
    return kr_sorted[:5], us_sorted[:5]

 

RSS에서 받은 기사들을 키워드 기준으로 점수 매겨서 상위 5개씩 뽑는다.

 

그리고 Ollama가 뱉은 헤드라인은 버리고, 진짜 헤드라인을 주입한다.

news_data = call_ollama(news_text)
# AI가 만든 헤드라인 → 버림
news_data["kr_headlines"] = real_headlines_from_rss

 

결과:

📰 오늘의 헤드라인
- 두산테스나, 반도체 장비 수주 확대 — 한국경제 증권
- 삼성전자 가전 OEM 확대 계획 발표 — 매일경제 증권
- GM beats Q1 earnings estimates — CNBC

실제 뉴스다.

 

 

동시에 주가도 틀렸다

 

 

AI가 헤드라인을 지어내는 동안, 주가도 틀렸다. 원인은 yfinance 배치 다운로드였다.

 

# 기존 코드 — 이게 문제
df = yf.download(["005930.KS", "000660.KS", ...], period="1d")

 

yf.download()는 여러 종목을 한 번에 받는 함수인데, 네트워크 오류 하나만 생겨도 전체가 빈 DataFrame으로 반환된다. 에러 메시지도 없다. 주가 없음 → AI가 훈련 데이터 기억에서 채움 → 2~3년 전 주가.

개별 조회로 바꿨다.

 

def fetch_prices(tickers):
    prices = {}
    for tk in tickers:
        try:
            hist = yf.Ticker(tk).history(period="5d")
            if not hist.empty:
                prices[tk] = float(hist["Close"].iloc[-1])
        except Exception as e:
            print(f"[{tk}] 실패: {e}")
    return prices

 

하나가 실패해도 나머지는 정상으로 받아온다. 그리고 종목별 유효 범위도 현실화했다.

 

# 기존 → 수정
삼성전자: (30000, 180000) → (30000, 500000)  # 실제 22만원, 범위 초과로 버려지던 것
SK하이닉스: (60000, 500000) → (60000, 2000000)
TSMC: (50, 300) → (50, 700)

 

PRICE_VALID 범위가 2022년 기준이었다. 그 사이 주가가 많이 올랐다.

 

RSS 소스도 절반이 죽어있었다

 

국내 RSS 피드 12개 중 3개만 정상이었다.

 

이데일리: XML 파싱 에러
머니투데이: 410 Gone
조선비즈: 404 Not Found
파이낸셜뉴스: 404 Not Found

 

죽은 피드 지우고 살아있는 거 추가했다.

  • 한국경제 증권 피드 추가
  • 매일경제 증권 피드 추가
  • 동아닷컴 경제 추가

국내 기사 수: 45건 → 80건.

 

RSS 피드는 가끔 확인해야 한다. 조용히 죽어있어도 아무도 안 알려준다.

 

수치 계산도 AI에게 맡기지 않는다

매수구간/목표가/손절가도 원래 AI가 계산했다. 현재 주가를 모르는 AI가 계산하면 당연히 틀린다. 후처리로 직접 계산하도록 바꿨다.

price = prices[ticker]  # yfinance 실제 주가
pick["buy_zone"]  = f"{price * 0.97:,.0f}~{price * 1.01:,.0f}원"  # -3%~+1%
pick["target"]    = f"{price * 1.12:,.0f}원"                        # +12%
pick["stop_loss"] = f"{price * 0.93:,.0f}원"                        # -7%

 

비율이 고정이라 섬세하진 않다. 그래도 AI가 지어내는 것보단 낫다.

 

결론 AI에게 사실 생성을 맡기지 마라

 

이번 작업에서 배운 건 하나다.

LLM이 뭔가를 생성하게 시키면 안 되는 경우가 있다.

  • 사실(Fact) — 오늘 뉴스, 실시간 주가, 특정 날짜의 이벤트 → 외부 데이터 직접 사용
  • 판단(Judgment) — 뉴스 감성, 시장 분위기 요약, 테마 분류 → AI에게 맡겨도 됨

 

AI는 모르는 걸 물어보면 아는 척한다. 그게 버그가 아니라 설계다. 훈련 데이터에 없는 정보를 물어보면, 그럴듯한 걸 만들어낸다. 구조를 바꿔서, 사실은 외부에서 가져오고 판단만 AI에게 맡기면 할루시네이션을 막을 수 있다.

 

수정 전 수정 후

삼성전자 주가 170,500원 (틀림) 222,750원 (실제)
헤드라인 AI 생성 (없는 뉴스) RSS 원문 직접
국내 기사 45건 80건
매수구간 AI 임의 계산 현재가 -3%~+1%

 

내일 리포트가 어떻게 나오는지 지켜볼 예정이다. AI로 무언갈 만든다는 건 재밌다. 

댓글()

AI랑 3시간 삽질하고 내린 결론 Base 파밍은 결국 손가락이 답

코인 훈수|2026. 4. 27. 07:16

"수수료 $10으로 볼륨 $10만 만들기."

 

 

이 문장 보고 당연히 자동화 생각했죠. 저는 봇 만드는 사람이니까. 결론부터 말할게요. 3시간 삽질 끝에 "손가락으로 눌러라"는 결론 나왔습니다. 그 과정 솔직하게 씁니다.

 

발단: 크립토메이지님 블로그

네이버 블로그에서 Base 에어드랍 파밍 가이드를 봤습니다. 핵심은 이거였어요.

  • USDC → USDT: Aerodrome에서 스왑 (수수료 0.05%)
  • USDT → USDC: Base 앱 인앱 스왑 (수수료 0%)
  • 이걸 반복하면 $10만 볼륨에 수수료 $10~25

 

왜 이게 의미 있냐면, Base 재단이 온체인 활동량을 나중에 인센티브 배정에 쓸 가능성이 높거든요. 트랜잭션 수, 스왑 볼륨, 활동 지속 일수. 이 세 가지를 쌓으면 됩니다. 읽자마자 생각했죠. "이거 봇으로 돌리면 되겠다."

 

1차 시도: iPhone 미러링으로 화면 클릭

 

맥북에서 iPhone Mirroring으로 아이폰 화면을 제어하는 방법입니다. PyAutoGUI로 스왑 버튼 좌표 찍고 자동 클릭하는 거죠. 좌표 찾고, PIN 자동 입력 스크립트 만들고... 근데 문제가 생겼습니다.

 

스왑마다 PIN이 나옵니다. 매번요.

 

그리고 금액이랑 방향(USDC→USDT 또는 반대)은 손으로 설정해야 합니다. 반자동도 아니고 그냥 PIN 입력만 자동화한 거였어요. 이게 무슨 자동화야 싶었습니다.

 

2차 시도: web3.py로 직접 컨트랙트 호출

 

발상을 바꿨습니다.

 

"Base 앱 통해서 스왑하나, 직접 Aerodrome 컨트랙트 부르나, 온체인에서 결과는 같지 않나?"

 

맞는 말이었습니다. 그래서 web3.py로 Aerodrome Router를 직접 호출하는 스크립트를 짰습니다. 3개 계정 순환, USDC↔USDT 왕복, 랜덤 딜레이까지. 테스트 돌려보니 실제로 작동했습니다. 트랜잭션 성공.

근데 여기서 중요한 걸 발견했습니다.

 

Aerodrome은 금액이 커지면 슬리피지가 폭발합니다.

 

스왑 금액 실제 수수료

 

$100 $0.05
$500 $2.20
$1,000 $38.40

 

$1,000 단위로 100번 스왑하면 수수료만 $3,800 나옵니다. 볼륨 $10만 만들려다가 파산할 뻔했어요. 반드시 $100 이하 소액으로 해야 합니다.

 

진짜 문제: Base 앱 인앱 스왑의 정체

 

그러면 Base 앱 인앱 스왑은 왜 수수료가 0%야?

궁금해서 실제 트랜잭션을 뜯어봤습니다. Blockscout에서 확인한 결과:

컨트랙트: 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
method:   handleOps

 

이게 뭔지 아세요? ERC-4337 EntryPoint입니다.

쉽게 말하면 Base 앱 인앱 스왑은 일반 트랜잭션이 아닙니다. "Account Abstraction(AA)"이라는 구조로 작동하는데, 번들러라는 중개자가 가스비를 대신 내줍니다. 그래서 유저 입장에서 수수료가 0%인 겁니다.

 

문제는 이 구조를 web3.py로 복제할 수 없다는 거예요. AA 번들러랑 페이마스터 연동이 필요한데, 그냥 개인키로 컨트랙트 직접 부르는 방식으로는 안 됩니다.

 

결국 Base 앱 인앱 스왑은 반드시 Base 앱에서 손으로 눌러야 합니다.

 

결론: 뭘 선택할 것인가

 

두 가지 선택지만 남았습니다.

1. Aerodrome 양방향 완전 자동화

  • 수수료 ~$53 / $10만 볼륨
  • 한 번 실행하면 완전 무인 운영
  • SWAP_AMOUNT_USD = 100 설정 필수

2. 블로그 원래 전략 (인앱 + Aerodrome 편도)

  • 수수료 ~$25 / $10만 볼륨
  • 인앱 스왑은 손으로 눌러야 함
  • $28 아끼려고 손가락 수고

 

저는 지금 1번으로 보류해뒀습니다. 솔직히 에어드랍 확정도 아니고, Base 재단이 이 데이터를 실제로 쓸지도 모르거든요. $53 태워서 볼륨 쌓을 만큼 확신이 없어요.

 

배운 것

삽질했지만 건진 게 있습니다.

  • AA(Account Abstraction): 가스비 대납 구조, 점점 많은 앱이 씁니다. 알아두면 나중에 도움 됩니다.
  • Aerodrome 슬리피지: stable pool도 금액 커지면 슬리피지 폭발. $100 단위가 기본입니다.
  • 온체인 트랜잭션 분석: Blockscout에서 컨트랙트 주소랑 method 보면 앱이 어떻게 작동하는지 알 수 있습니다.

자동화가 항상 답은 아닙니다. 근데 시도해보지 않으면 왜 안 되는지도 모르죠.

댓글()
개인정보처리방침