처음에는 단순한 문제라고 생각했습니다.
“어제 나온 뉴스만 골라서 아침 9시에 보내줘.”
요구사항은 이 한 줄이면 충분해 보였습니다. 매일 새벽에 뉴스를 모으고, LLM이 그중 중요한 기사를 고르고, 보기 좋게 요약해서 텔레그램으로 보내면 됩니다. 실제로 처음 며칠은 그럴듯하게 잘 돌아갔습니다. 메시지도 왔고, 형식도 맞았고, 요약도 자연스러웠습니다.
그런데 운영을 해보니 진짜 문제는 “브리핑이 안 오는 것”이 아니었습니다. 더 곤란한 문제는 브리핑은 정상적으로 왔는데, 안에 들어간 기사 날짜가 틀린 경우였습니다.
겉으로는 성공, 안쪽으로는 실패
아침 뉴스 브리핑은 전날 뉴스를 기준으로 만들어야 합니다. 예를 들어 5월 5일 아침에 발송되는 브리핑이라면, 커버리지 날짜는 5월 4일입니다. 그런데 어느 날 브리핑을 확인해보니 며칠 전 기사가 섞여 있었습니다.
이게 운영 입장에서 애매한 이유는 겉으로 보기에는 모든 단계가 성공했기 때문입니다.
- 크론은 제 시간에 실행됐습니다.
- LLM은 브리핑을 생성했습니다.
- 텔레그램 메시지도 정상적으로 도착했습니다.
- 본문 요약도 읽기에는 자연스러웠습니다.
하지만 사용자가 받는 정보의 기준일이 틀렸다면, 이건 성공이 아닙니다. 특히 뉴스 자동화에서는 날짜가 품질의 핵심입니다. 어제 뉴스 브리핑에 사흘 전 기사가 섞이면, 아무리 문장이 좋아도 신뢰가 깨집니다.
실제로 운영 중 5월 1일에는 6건, 5월 4일에는 5건의 과거 기사 혼입이 확인됐습니다. 이 정도면 “프롬프트를 조금 더 세게 쓰면 되겠지” 수준의 문제가 아니었습니다.
처음에는 프롬프트를 고쳤습니다
처음 대응은 당연히 프롬프트 수정이었습니다.
반드시 커버리지 날짜에 발행된 기사만 포함하세요.
각 URL의 실제 발행일을 확인하세요.
발행일을 확인할 수 없는 기사는 제외하세요.
이런 문장을 더 넣었습니다. “반드시”, “절대”, “확인하세요” 같은 표현도 늘렸습니다. 하지만 결과적으로 이 방식은 근본 해결이 아니었습니다.
LLM은 지시를 이해합니다. 문제는 그 지시를 매번 같은 기준으로 검증해주는 시스템이 아니라는 점입니다. 검색 결과에 제목이 좋아 보이는 기사가 있으면 선택할 수 있고, 페이지 안의 날짜 메타 정보를 놓칠 수도 있습니다. Google News 리디렉트 URL처럼 실제 원문 검증이 어려운 링크도 섞일 수 있습니다.
여기서 결론이 나왔습니다.
LLM에게 “검증까지 잘해줘”라고 맡기는 구조 자체가 잘못됐습니다.
LLM이 못한다기보다, 맡길 일이 아니었습니다
이번 문제를 겪으면서 생각이 조금 바뀌었습니다. “LLM이 날짜 검증을 못한다”라고만 말하면 반은 맞고 반은 틀립니다. 더 정확히는 그 일을 LLM에게 맡기면 운영 시스템이 불안정해진다가 맞습니다.
기사의 중요도를 판단하고, 내용을 요약하고, 읽기 좋은 문장으로 바꾸는 일은 LLM이 잘합니다. 반대로 아래 같은 일은 코드가 더 잘합니다.
- URL 안에 날짜 패턴이 있는지 확인하기
article:published_time메타 태그 읽기og:published_time,datePublished,time datetime값 확인하기- 커버리지 날짜와 실제 발행일 차이를 계산하기
- 실패한 URL만 찾아 대체 기사로 바꾸기
이건 창의력이나 추론이 필요한 일이 아닙니다. 같은 기준을 반복해서 적용해야 하는 일입니다. 그러면 LLM보다 코드가 맞습니다.
그래서 역할을 나눴습니다
개선 후에는 파이프라인을 이렇게 나눴습니다.
| 단계 | 담당 | 이유 |
|---|---|---|
| 기사 후보 수집 | Python 수집기 | RSS와 뉴스 소스를 반복 가능하게 모읍니다. |
| 기사 선택과 요약 | LLM | 중요도 판단과 문장 정리에 강합니다. |
| 발행일 검증 | verify_article_dates.py |
날짜 확인은 코드로 하는 편이 안정적입니다. |
| 오류 기사 교체 | fix_briefing_dates.py |
실패한 기사만 찾아 같은 카테고리 후보로 바꿉니다. |
| 최종 발송 | Telegram API | 검증 후 정해진 채널로 전달합니다. |
핵심은 LLM을 빼는 것이 아닙니다. 오히려 LLM을 더 잘 쓰기 위해 역할을 줄였습니다. LLM은 선택과 요약에 집중하고, 검증과 복구는 코드가 맡습니다.
실제 운영 흐름
현재 뉴스 브리핑 파이프라인은 대략 이런 흐름입니다.
1. collect_daily_news.py
- RSS / Google News / 언론사 소스에서 기사 후보 수집
2. LLM 브리핑 생성
- 수집된 JSON 안에서 카테고리별 기사 선택
- 제목과 요약 작성
3. verify_article_dates.py
- 브리핑 파일 안의 URL 추출
- 각 URL의 발행일 확인
- 커버리지 날짜와 비교
4. fix_briefing_dates.py
- 실패한 URL이 있으면 같은 섹션에서 대체 기사 검색
- 브리핑 파일 자동 수정
5. Telegram 발송
- 최종 브리핑 전달
여기서 중요한 기준은 하나입니다. 이제는 “브리핑 파일이 생성됐는가?”만 보지 않습니다. 브리핑 안의 기사 URL들이 날짜 검증을 통과했는가?를 봅니다.
검증 스크립트가 보는 것
verify_article_dates.py는 브리핑 본문에서 • URL: 형식의 링크를 뽑아냅니다. 그리고 각 URL에 대해 먼저 페이지 메타 정보를 확인합니다.
article:published_time
og:published_time
datePublished
<time datetime="...">
메타 정보가 부족하면 URL 자체의 날짜 패턴도 봅니다. 예를 들어 연합뉴스 URL의 AKR20260501... 같은 패턴은 날짜를 추정하는 데 사용할 수 있습니다.
이렇게 확인한 실제 발행일을 커버리지 날짜와 비교합니다. 운영 중에는 주말이나 휴일처럼 기사 수가 적은 날도 있어서, 현재는 수집기·프롬프트·검증 스크립트 모두 ±1일 허용 범위로 맞췄습니다.
MAX_AGE_DAYS = 1
def is_within_tolerance(actual, expected, max_days):
ad = datetime.strptime(actual[:10], '%Y-%m-%d').date()
ed = datetime.strptime(expected[:10], '%Y-%m-%d').date()
return abs((ad - ed).days) <= max_days
여기서도 한 번 실수가 있었습니다. MAX_AGE_DAYS = 1이라고 써놓고, 실제 판정 로직에서는 그 값을 쓰지 않는 상태였던 적이 있습니다. 겉으로 보기에는 설정이 반영된 것처럼 보였지만 실제로는 exact match만 하고 있었습니다.
이런 버그는 운영 자동화에서 꽤 위험합니다. 설정값이 존재한다는 것과 그 설정값이 실제 판정에 쓰인다는 것은 전혀 다른 문제입니다.
복구는 재생성이 아니라 교체입니다
검증에 실패했다고 매번 브리핑 전체를 다시 만들지는 않습니다. 그러면 같은 문제가 반복될 수도 있고, 멀쩡한 기사까지 흔들릴 수 있습니다.
대신 fix_briefing_dates.py는 실패한 URL이 어느 섹션에 있었는지 찾습니다.
- 국내 뉴스인지
- IT/과학 뉴스인지
- 게임 뉴스인지
- 국제 뉴스인지
그리고 수집된 JSON에서 같은 카테고리의 다른 후보를 찾아 교체합니다. 예를 들어 게임 섹션의 기사가 실패했다면 게임 후보 안에서만 대체합니다. 국내 뉴스가 실패했는데 국제 뉴스로 채우는 식의 복구는 하지 않습니다.
이 방식이 마음에 드는 이유는 복구 범위가 작기 때문입니다. 전체를 다시 만드는 것이 아니라, 실패한 조각만 바꿉니다. 운영 자동화에서는 이런 작은 복구가 더 안전합니다.
운영하면서 추가로 막은 것들
날짜 검증만으로 끝나지는 않았습니다. 실제로 돌려보면 URL 품질 문제도 같이 따라옵니다.
news.google.com/rss/articles/...형태의 리디렉트 URL- 발행일 메타 태그가 없는 일부 언론사 페이지
- 인스타그램 같은 SNS 링크
- 기사 페이지가 아니라 언론사 메인 도메인으로만 잡힌 링크
특히 Google News 리디렉트 URL은 조심해야 합니다. 원문 출처처럼 보이지만 실제 브리핑에 넣기에는 좋지 않습니다. 그래서 선별 단계에서 제외하고, 생성 후에도 브리핑 파일 안에 news.google.com이 남아 있는지 한 번 더 검사하도록 했습니다.
rg 'news\.google\.com|instagram\.com' \
/root/.hermes/news_workspace/news_editor/briefing_YYYY-MM-DD.txt
이 구조로 바꾼 뒤 좋아진 점
가장 큰 변화는 마음이 편해졌다는 점입니다. 이전에는 브리핑이 도착해도 “오늘은 또 날짜가 섞였을까?”를 사람이 봐야 했습니다. 지금은 최소한 날짜 검증 실패가 로그로 드러납니다.
| 구분 | 이전 | 이후 |
|---|---|---|
| 품질 기준 | 메시지가 오면 성공 | URL 검증까지 통과해야 성공 |
| 날짜 확인 | LLM 지시문에 의존 | 메타 태그와 URL 패턴으로 확인 |
| 오류 대응 | 사람이 수동 확인 | 실패 URL만 자동 교체 |
| 모델 변경 영향 | 모델이 바뀌면 품질도 흔들림 | 검증 기준은 코드로 고정 |
이 차이는 생각보다 큽니다. 모델을 바꾸더라도 검증 기준은 그대로 남습니다. 프롬프트를 조금 고치더라도 마지막 문턱은 코드가 지킵니다.
이번 일로 정리한 원칙
LLM 자동화를 운영하면서 이제는 이렇게 나누려고 합니다.
- LLM에게 맡길 일: 읽고, 고르고, 요약하고, 사람이 보기 좋게 정리하는 일
- 코드에게 맡길 일: 날짜, URL, 중복, 형식, 실패 여부처럼 기준이 명확한 일
- 운영자가 볼 일: 어떤 예외가 반복되는지, 기준을 완화할지 강화할지 결정하는 일
자동화는 “사람이 안 봐도 된다”가 아니라, 사람이 봐야 할 지점을 줄이는 것에 가깝습니다. LLM이 만든 결과를 매번 사람이 처음부터 끝까지 검수해야 한다면 자동화의 의미가 줄어듭니다. 반대로 코드가 잡을 수 있는 오류를 코드가 잡아주면, 사람은 더 중요한 판단에 집중할 수 있습니다.
마무리
이번 문제를 겪고 나서 LLM 자동화를 조금 다르게 보게 됐습니다.
처음에는 “어떤 모델을 쓰면 더 잘할까?”를 많이 생각했습니다. 그런데 운영을 해보니 더 중요한 질문은 따로 있었습니다.
모델이 틀렸을 때, 시스템은 어디서 그것을 잡아낼 수 있는가?
이 질문에 답하지 못하면 자동화는 데모에 가깝습니다. 반대로 이 질문에 답할 수 있으면, 조금 부족한 모델을 쓰더라도 시스템 전체는 훨씬 안정적으로 돌아갑니다.
아침 뉴스 브리핑 파이프라인에서 신뢰를 만든 것은 거창한 프롬프트가 아니었습니다. 작은 검증 스크립트와, 실패한 기사를 조용히 바꿔주는 복구 로직이었습니다.
LLM은 요약을 잘합니다. 하지만 운영 시스템에서는 요약보다 검증이 더 중요할 때가 있습니다. 이번 사례가 그걸 다시 확인시켜준 경험이었습니다. 🤖✍️
