포스트

매일 아침 AI 소식 5건 뉴스레터 에이전트

AIFFEL 노드 '매일 아침 AI 소식 5건을 골라 보내는 에이전트'를 1부부터 7부까지 따라가며 LangGraph로 만들고, GitHub Actions로 매일 아침 Discord에 받게 한 학습일지입니다.

매일 아침 AI 소식 5건 뉴스레터 에이전트

10월 28일부터 29일까지 AIFFEL 노드 “매일 아침 AI 소식 5건을 골라 보내는 에이전트”를 진행했다. 노드에 충분한 가이드가 있었고, 그 가이드를 코드로 옮기고 장마다 정리한 것은 Claude Code였다. 나는 노드를 이해하고, 맞게 가고 있는지 묻고, 방향을 정했다. 결과물은 ai-newsletter-agent 저장소에 있다.

이 노드의 핵심

AI 기사는 하루에 100건 가까이 나온다. 그중 개발팀이 알아야 할 5건을 골라 요약하고, 매일 아침 7시 30분에 Discord로 보내는 에이전트를 LangGraph로 만든다. 일은 노드 다섯 개가 나눠 맡는다.

1
2
START → collect → select → report → verify → publish → END
         ① 수집    ② 선별   ③ 요약    ④ 검수    ⑤ 발행

처음에는 장을 하나씩 따라 쳐도 이해가 잘 되지 않았다. Claude Code에게 노드 전체를 분석해 달라고 했고, 정리된 논점은 이랬다. 이 노드는 코드를 가르치기보다 설계 결정을 연습하는 노드다. 46장이 모두 “선택지를 늘어놓고 → 하나를 고르고 → 고른 이유와 포기한 것을 적는” 순서로 되어 있다. 그래서 “이 장은 무슨 질문에 답하고 있나”를 잡고 읽으니 따라갈 수 있었다.

노드 장과 이 글의 대응

부노드가 던지는 질문노드가 고른 답실제로 확인한 것
1부 · 뼈대무엇부터 만들까?빈 노드 다섯 개로 끝까지 도는 틀로그 5줄이 0건으로 끝까지 돌았다
2부 · collect기사를 어디서 가져올까?RSS + 원문 본문 추출수집 0건 → 23건 → (주소 문제 수정 후) 72건
3부 · select100건에서 5건을 어떻게 고를까?예선 → 본선 (서로 비교)따로 점수를 매기면 7점이 6건으로 몰렸다
4부 · report요약을 어떤 모양으로 받을까?headline · summary · why 세 칸Send로 기사 수만큼 펼쳐 동시에 요약
5부 · verify틀린 요약을 무엇으로 잡을까?따로 부른 LLM이 원문과 대조숫자를 바꾼 가짜 요약을 넣어 걸러지는지 확인
6부 · publish어디로 보낼까?Discord 웹훅로컬에서 실제 발송 성공
7부 · 자동 실행매일 아침 누가 돌릴까?GitHub ActionsActions에서 dry-run과 실제 발송 성공

처음에 방향을 바로잡은 것

처음에는 노드 원문 없이 요약 설명만 주고 시작해서, Claude Code가 완성본(newsletter/ 패키지)을 먼저 만들었다. 돌아가기는 했지만, 노드를 읽어 보니 노드가 비교 끝에 접은 방식이 여러 군데 들어 있었다.

부분노드의 결정먼저 만든 완성본
select예선 → 본선, 서로 비교기사마다 따로 점수 (15장이 문제라고 한 방식)
verifyLLM 대조 (숫자 글자 대조는 오탐)숫자 글자 대조부터 (실제로 “2” 하나 때문에 정확한 요약이 떨어졌다)
떨어진 요약버리고 로그에 남기기report로 되돌아가는 엣지 (30장이 접은 안)

그래서 나는 노드의 빈 노드 코드를 메인으로 잡자고 했고, 2부부터 7부까지 한 부씩 같이 채워 갔다. 완성본은 비교용으로만 남겼다.

부마다 따라가며 확인한 것

1부: State와 리듀서

State는 노드 다섯 개가 같이 쓰는 딕셔너리 하나다. 노드는 자기가 바꾼 칸만 돌려준다. drafted와 log에만 operator.add(이어 붙이기)를 붙이는데, 이유가 처음엔 와닿지 않았다. 4부에서 요약 워커 다섯 개가 drafted에 동시에 한 건씩 쓰는 걸 보고 이해됐다. 리듀서가 없으면 마지막 워커 한 건만 남는다.

2부: collect와 조용한 실패

노드 13장 코드를 넣으니 수집이 23건이었다. 소스별로 세어 보니 AI타임스가 1건뿐이었다. 원인은 같은 글 거르기 한 줄이었다.

1
key = e.link.split("?")[0].rstrip("/")   # 추적용 꼬리표를 떼고 비교

AI타임스 주소는 articleView.html?idxno=216753처럼 기사 번호가 ? 뒤에 있다. ? 뒤를 떼면 50건이 모두 같은 주소가 된다. 에러 없이 숫자만 줄어서, 14장이 말한 조용한 실패 그대로였다. Claude Code가 노드 코드 그대로 둘지 고칠지 물었고, 나는 고치기로 했다. 추적용 꼬리표(utm_* 등)만 떼도록 바꿨다. 수집이 72건이 됐다.

3부: select는 순서를 정하는 문제

15장과 17장 실험을 같은 12건으로 돌렸다.

  • 따로 점수(15장): 7점이 6건, 서로 다른 점수가 4개뿐이라 5건을 자를 수 없었다.
  • 한 화면 비교(17장): 따로 채점에서 4점이던 기사가 3위로 올라왔다.

“이 기사가 몇 점인가”가 아니라 “이 중 무엇이 더 중요한가”를 물어야 한다는 게 3부의 요점이다. 목록이 길면 중간을 놓치니(lost in the middle) 40건씩 묶어 예선을 치르고, 본선에서 다시 비교한다. 이번 실행은 72건 → 예선 16건 → 5건이었다.

4부: report와 Send

select 다음 고정 엣지 한 줄을 add_conditional_edges("select", fan_report, ["report"])로 바꾸면, 기사 수만큼 report 워커가 펼쳐진다. 결과는 노드 25장 예시와 같은 모양이었다. 본문이 600자가 안 되는 기사는 취재 제외로 빠졌다.

24장 실험에서는 “헤드라인 20자 내외”, “why 한 문장” 지시가 지켜지지 않았다. 헤드라인은 34-51자였다. 스키마는 칸이 채워지는 것까지만 보장하고, 나머지는 부탁이라는 걸 확인했다.

5부: verify는 가짜를 넣어 봐야 안다

같은 영어 원문에 정확한 요약 A와 숫자를 바꾼 요약 B를 넣었다.

 숫자 글자 대조(27장)LLM 대조(28장)
A · 정확함불합격 (3, 6000이 원문에 없음)합격
B · 5억 → 50억불합격불합격, “50억 달러로 과장됨”

실제 실행은 “5 → 5건”이 통과였다. 29장 말대로 불합격 0건은 확인해 볼 신호라서, 실제 요약 하나의 “macOS 27”을 “macOS 270”으로 바꾼 가짜를 섞었다. 그 한 건만 정확히 걸러졌다.

6-7부: Discord와 GitHub Actions

로컬에서 DRY_RUN=0으로 실제 발송에 성공했다. 7부대로 graph.py와 run.py로 나눴다. 이전에는 파일을 import만 해도 브리핑이 발행되는 구조였다. Secrets에 키 두 개를 넣고 Actions에서 dry-run → 실제 발송 순서로 확인했다.

첫 실제 발송은 Discord가 404로 거절했다. Secret에 넣은 웹훅 값이 잘못 들어간 것이었고, .env의 값으로 다시 등록하니 성공했다.

노드와 다르게 정한 것

바꾼 것계기근거
주소 비교 때 추적용 꼬리표만 떼기AI타임스 50건이 1건으로 줄었다14장 조용한 실패
발송에 실패하면 run.py가 exit 1404로 실패했는데 Actions는 초록불이었다41장 ① 실패 메일의 구멍
⑤ 발행 줄을 보냄 / dry-run / 실패로 구분발송 실패가 “dry-run”으로 찍혔다(노드에 없는 보강)
선별이 0건이면 verify로 바로0건인 날 “오늘은 조용합니다”도 안 갔다44장에 적힌 고치는 법
소스 5곳 → 15곳AI타임스가 403으로 막히자 수집이 20건이 됐다8장 실습 데이터(11곳·하루 50-120건), 16장 예선 구조
403도 응답 없음에 기록막힌 소스가 로그에 안 보였다14장 raise_for_status()
예선 전 소스 섞기, AI 무관 기사 버리기, 같은 사건·매체 상한한국어 종합 IT 피드가 5건 중 4건을 차지했다20장 라벨 세기·매체 상한
영어 요약이면 한 번 다시 쓰고, 그래도 영어면 버리기영어 헤드라인이 실제로 나왔다24장 한글 검사
AI타임스는 본선 진출만 보장아래 따로 정리9장 tier, 17장 서로 비교

AI타임스 비중을 정한 과정

소스를 15곳으로 늘리자 국내 AI 전문지인 AI타임스가 종합 IT 피드에 밀려 0건이 될 수 있었다. 비중이 너무 낮으면 안 된다고 보고, 처음에는 최소 2건 발행 보장을 골랐다. 시험 브랜치로 Actions에서 돌려 보니 2건이 들어갔다.

그런데 2건을 무조건 보장하면, 더 좋은 기사가 있는 날에도 자리를 밀어낸다. 그래서 발행 자리는 보장하지 않고 본선 진출만 보장하는 쪽으로 바꿨다. 예선에서 묶음 운으로 떨어져도 AI타임스 기사 2건은 본선에 올라가 다른 기사와 한 화면에서 비교받는다. 다시 돌려 보니 158건 → 본선 32건 중 AI타임스 16건 → 발행 2건이었다. 보장 없이 경쟁으로 얻은 2건이다.

푸시 전에 문서와 대조한 것

푸시 직전에 멈추고, 노드 문서와 코드를 처음부터 다시 대조해 달라고 했다. 노드의 코드 블록 37개를 한 줄씩 비교한 결과는 이랬다.

  • 노드 본문 코드는 모두 들어가 있었다.
  • 빠진 것: 14장 준비 단계의 brotli 설치. 영향이 있는지 기사 30건을 뽑아 확인했고, 결과에는 차이가 없었다. 그래도 문서대로 넣었다.
  • 잘못 적힌 것: README, .env.example, docs/sources.md가 먼저 만든 완성본 기준으로 남아 있었다. 공개 저장소에 그대로 올라가 있던 상태라 지금 코드 기준으로 다시 썼다.

아직 남은 것

  • 10월 29일 아침 7시 30분 첫 자동 발송이 잘 오는지 확인하기
  • 평가셋이 없어서 프롬프트를 바꾼 뒤 나빠졌는지 알 수 없다 (44장)
  • 실행 기록(소스별 건수, 단계별 탈락)이 파일로 쌓이지 않는다 (12·21·44장)
  • AI타임스가 하루 50건으로 가장 많아서, 본선 후보의 절반이 AI타임스로 채워진다. 며칠 지켜보고 판단한다.

정리

  • 노드는 코드보다 결정의 순서를 가르친다. 장마다 “이 장의 질문이 무엇인가”를 잡으니 따라갈 수 있었다.
  • 빈 노드로 뼈대를 먼저 세우고 함수 하나씩 갈아 끼우니, 매번 로그 숫자로 어디까지 됐는지 보였다.
  • 노드 코드를 그대로 넣고 돌려 보니 노드가 경고한 문제(조용한 실패, 영어 요약, 0건인 날)가 실제로 나왔다. 고칠 때는 노드의 다른 장에서 근거를 찾았다.
  • 실패가 보이게 만드는 장치를 따로 챙겼다: 403 기록, 발송 실패 시 빨간불, 0건인 날에도 한 줄 발송.
  • 완료라고 하기 전에 문서와 다시 대조했고, 빠진 것과 잘못 적힌 것을 찾았다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.