ADR-0009: 뉴스 브리프 후속(follow-up) 링크를 엔티티 인덱스 + precision-first LLM 판정으로 붙인다
- Status: Accepted
- Date: 2026-09-11
- 관련: HMH-9231 (이 ADR) · 백엔드 follow-up API
PUT /admin/v1/news-brief/{id}/follow-up-of— PR #4807 / HMH-9223 (koreanproptech/howmuchhome) · 파이프라인은 ADR-0006 · 데이터 소유 경계는 ADR-0001
Context
정비사업은 한 사업장이 구역지정 → 조합설립 → 사업시행인가 → 착공처럼 여러 단계로 진행되고, 각 단계가 별도 기사·브리프가 된다. 유저가 사업 흐름을 따라가게 하려면 같은 사업장의 후속 브리프를 이어 붙여야 한다. 백엔드가 이 링크용 API(follow_up_of_id)를 추가했다(#4807).
후속은 merge(같은 사건 중복, HMH-9228)와 다르다. merge 는 같은 시점의 같은 사건이라 요약 시 최근 브리프 컨텍스트로 잡지만, 후속은 단계 사이 간격이 몇 주~몇 달일 수 있어 시간창으로는 못 잡는다.
두 제약이 설계를 강하게 좌우한다:
- 백엔드는 엔티티/사업장 검색 API 가 없다. 브리프 read 는 최신순 페이지네이션뿐이라, “같은 사업장의 과거 브리프”를 백엔드에 물어볼 수 없다.
- 미탐(후속을 못 잡음)이 오탐(무관한 브리프를 잘못 이음)보다 낫다. 오탐은 유저에게 “엉뚱한 연결”로 바로 보이고, 미탐은 단독 브리프로 남을 뿐 — 손해가 비대칭이다. 그래서 보수적으로 설계한다.
검토한 대안
| 결정축 | 대안 | 채택 안 한 이유 |
|---|---|---|
| 후보 탐색 | 시간창(최근 N일 내 유사) | 후속은 단계 간격이 커 시간창을 벗어난다 — 구조적으로 못 잡음 |
| 임베딩(pgvector + 외부 임베딩 모델) | “의미 유사”는 잡지만 유사가 곧 후속은 아니라, 같은 지역 다른 사업까지 후보로 쏟아져 오탐 압력이 커진다. 외부 임베딩 의존·pgvector 확장·상시 비용도 추가. precision-first 에 역행 | |
| 엔티티 매칭 | pg_trgm 퍼지 매칭 | 느슨하면 “도림동” 과 “도림천” 이 엮이는 오매칭. 정규화 후 정확 일치가 precision-first 에 맞고, 놓치는 변형은 감수 |
| 인덱스 위치 | 백엔드에 조회 위임 | 엔티티 검색 API 자체가 없음. 크롤·보조 상태는 프론트가 소유(ADR-0001)하므로 우리 Neon 에 둔다 |
| 판정 | 엔티티 일치만으로 자동 링크 | 유사가 곧 후속이 아니므로 자동 링크는 오탐 직행. LLM 판정을 반드시 거친다 |
| 확인 | 판정 1콜로 끝 | precision 이 한 겹뿐. 판정이 후보를 고른 경우에 한해 refute 1콜로 재검증해 오탐을 한 번 더 거른다 |
Decision
엔티티 인덱스(우리 Neon) → 대표 엔티티 정확 일치 후보 top-3 → LLM 판정 → refute 확인 → 링크. precision-first 라 기본값은 “링크 안 함”이다.
배선:
- 엔티티 추출 — 요약 콜에서 브리프마다 사업장 엔티티(대표 1 + 부차 ≤2)를 함께 출력한다(추가 비용 없음). 지시는 코드가 넣고(
packages/news-crawler/src/prompt.tsbuildUserMessage),entities는 백엔드 계약 밖이라 POST 전에 벗겨낸다(postBrief.ts). - 인덱싱 — 게시 성공한 브리프의 엔티티를
news_brief_entity(우리 Neon)에 적재한다.entity_key는 정규화 매칭키(공백·조사·사업유형 접미 제거),entity_name은 표시 원형. (packages/db/schema/newsBriefEntity.ts,services/newsBriefEntity.ts) - 후보 수집 — 새 브리프의 대표 엔티티와
entity_key정확 일치하는 다른 브리프를 primary∩primary 우선·최신순 top-3 로 뽑는다. 대표 아닌 것끼리(secondary-only)는 애초에 후보에서 뺀다. - 판정 + 확인 — 후보를 LLM 에 한 번에 넘겨 명백한 순차 후속만 고르게 하고(확신 없으면 null), 골랐으면 refute 콜로 “후속 아님”을 반박해 살아남을 때만
PUT .../follow-up-of로 링크한다. (packages/news-crawler/src/followup.ts,run.ts)
비용 상한: 새 브리프당 후보 없으면 0콜, 있으면 판정 1콜, 후보를 골랐을 때만 refute 1콜 — 최대 2콜.
Consequences
좋아지는 것
- 단계 간격이 아무리 커도 같은 사업장이면 후속을 잇는다 — 시간창의 근본 한계를 피한다
- 엔티티(넓게) → LLM 판정 → refute(반박)의 3중 게이트로 오탐을 강하게 억제한다. “미탐이 낫다”는 제약과 정렬
- 임베딩 인프라(pgvector·외부 임베딩 모델·상시 비용) 없이 시작한다. 비용은 후보 유무로 상한이 걸린다(대부분 0~1콜)
감수하는 것
- 미탐을 감수한다. 정규화 정확 일치 + 보수적 판정이라 표기가 크게 다른 같은 사업이나 애매한 전개는 놓친다. 이는 의도된 트레이드오프다
- Phase 2 이후 게시분만 인덱스에 있다. 과거 브리프는 후속 대상이 못 된다(백필은 후속 과제)
- 엔티티 인덱스와 브리프가 이원화된다(인덱스=우리 Neon, 브리프=백엔드). 백엔드에서 브리프가 삭제되면 인덱스에 고아 행이 남아, 후보로 뽑혀도 링크가 없거나 실패할 수 있다(링크 실패는 삼키고 로그만)
이 결정을 다시 열어야 할 때
- recall 이 아쉬우면(진짜 후속을 자주 놓치면) 정규화 완화·K 증가로 후보를 넓히거나, 그래도 부족하면 임베딩을 후보 단계에만 얹는다(판정·refute 는 유지)
- 백엔드가 엔티티/후속 검색이나 사업장 식별자를 제공하면 우리 Neon 인덱스를 걷어낼 수 있다
- 브리프 삭제와 인덱스 동기화가 필요해지면(고아 행이 문제 되면) 정리 잡을 추가한다
Last updated on