o2o-site-AEO/solution/backend/services/prompts/place_research.py
hbyang 7b238efffb [feat] solution/backend: 업소 조사 — 소개문이 쓸 재료를 출처와 함께 찾아온다
소개문이 "군산시에 있는 스테이,머뭄입니다. 주차 가능." 한 줄이었다. 생성기 잘못이 아니라
**쓸 재료가 그것뿐**이었다 — 네이버 플레이스가 준 fact 3건이 전부이고, TourAPI 는 미등록,
예약 페이지와 인스타그램은 robots 가 자동 수집을 금지한다. 그런데 이 업소의 내력
(1925년 적산가옥 · 히로쓰 가옥 후문 옆 · A동 B동 컨셉)은 블로그·기사에 공개돼 있다.
그걸 가져오는 단계가 없었을 뿐이다.

★ 문장을 검색모델에게 시키지 않는다. 재료만 모으고 소개문은 지금처럼 Gemini 가 쓴다 —
  소개문을 바로 시키면 그 문장의 근거를 우리가 못 갖고, ground_check 가 전부 반려한다.

- services/prompts/place_research.py: 사실 조각을 **출처와 함께** 요구한다. 요금·객실 수·
  체크인·취소 규정은 묻지 않는다 — 그건 fact 이고 블로그의 옛값이 섞이면 예약 클레임이다
- services/grounding/place_research.py: 출처 없는 항목은 버린다(story 와 같은 규율) +
  **상호 대조**를 더한다. 지역 이야기는 틀려도 지역 이야기지만, 업소 조사가 틀리면 남의
  가게 이야기가 이 사장님 소개문이 된다 — 이 레포에서 가장 비싼 실수다
- services/place_research.py: 조사 → place_channels.raw 에 근거 적재. **확정하지 않는다** —
  남이 쓴 글이라 공식 채널·sameAs 로 나가면 안 된다. 새 표를 만들지 않았다
- copy_service: 확정 링크만 읽던 근거를 raw.kind=research 까지 넓혔다. 확정 여부는
  "화면에 채널로 낼 것인가" 의 판단이지 "근거로 읽을 것인가" 의 판단이 아니다
- prompts/copy: 조사 기록을 fact 목록이 아니라 **별도 절**로 준다(fact 자리에 섞으니
  모델이 값 하나로 읽고 안 썼다). 소개문 분량 100~250자 → 200~600자·2~3문단 —
  옛 길이로는 확인된 사실을 나열하면 끝나 기록이 들어갈 자리가 없었다
- collect_service: 수집이 끝난 **뒤** 조사한다. 앞에 두면 네이버·TourAPI 가 이미 준 것을
  다시 묻는 꼴이라 검색 요금이 헛돈다

실측(스테이,머뭄): 조사 8건 채택·0건 버림 → 소개문이
"1925년에 지어진 100년 된 적산가옥을 리노베이션한 숙소 … 히로쓰 가옥 후문 바로 옆" 으로.
발행본 본문 10,798자 → 11,100자. 생성물은 여전히 PENDING_OWNER 로 들어가 사장님이 확인해야
노출된다(절대규칙 1).
2026-09-10 14:17:44 +09:00

61 lines
3.7 KiB
Python

"""업소 조사 프롬프트 — 소개문을 쓸 **근거**를 공개 웹에서 찾아온다.
★ 무엇을 요구하나
"소개문을 써 달라"가 아니다. **사실 조각을 출처와 함께** 달라고 한다.
소개문을 모델에게 바로 시키면 그 문장이 어디서 왔는지 알 수 없고, 우리 규칙은
근거 없는 문장을 발행하지 않는다(`copy_service.ground_check`). 그래서 이 단계는
재료만 모으고, 문장은 기존 생성기(Gemini)가 그 재료로 쓴다.
★ 왜 이게 필요한가 (실측 2026-09-10, 스테이,머뭄)
네이버 플레이스가 주는 fact 는 3건(주차·와이파이·휠체어)뿐이고 TourAPI 는 미등록,
예약 페이지와 인스타그램은 robots 가 자동 수집을 금지한다. 그 상태로 소개문을 생성하면
"군산시에 있는 스테이,머뭄입니다. 주차 가능." 한 줄이 나온다 — 쓸 재료가 그것뿐이라
생성기 잘못이 아니다. 그런데 이 업소에 대한 사실(1920년대 고택 · 히로쓰 가옥 옆 ·
2024년 리모델링 · A동/B동)은 블로그·기사에 공개돼 있다. 그걸 **출처와 함께** 가져오는
자리가 없었을 뿐이다.
★ 지어내게 두지 않는다
- 항목마다 출처 URL 을 요구한다. 없으면 버린다(`grounding/place_research.py`).
- 확인할 수 없는 것은 비우라고 명시한다. 모델은 빈칸을 싫어해서, 안 그러면 채운다.
- **가격·객실 수·운영 규정은 묻지 않는다.** 그건 fact 이고, 틀리면 예약 클레임이 난다 —
출처가 블로그면 옛 요금이 그대로 올라온다. 이 단계가 모으는 것은 **소개문의 재료**다.
"""
SYSTEM_PROMPT = (
"당신은 지역 업소를 조사하는 사람이다. 웹에서 확인되는 사실만 적는다. "
"확인되지 않으면 그 항목을 아예 빼라 — 추측하거나 일반론으로 채우지 마라. "
"출력은 JSON 하나뿐이고 코드펜스를 두르지 않는다."
)
# ★ 최대 개수를 둔다. 많이 받아 봐야 소개문 한 문단이고, 길수록 옛 정보가 섞인다.
MAX_ITEMS = 8
def build_prompt(name: str, address: str, category_label: str) -> str:
"""조사 프롬프트. 상호와 주소를 **둘 다** 준다 — 동명 업소를 가르는 유일한 단서다."""
return f"""다음 업소에 대해 웹에서 확인되는 사실을 모아라.
상호: {name}
주소: {address}
업종: {category_label}
[무엇을 찾나]
- 이 업소만의 특징: 건물의 내력·연식, 공간 구성, 주변 랜드마크와의 관계, 운영 방식
- 손님이 실제로 겪는 것: 어떤 사람이 어떤 목적으로 오는가, 무엇이 인상적이라고 말하는가
- 시기: 문을 연 때, 고쳐 지은 때
[적지 않을 것]
- 요금·객실 수·체크인 시각·취소 규정 — 이건 다른 경로로 확인한다. 옛 값이 섞이면 위험하다
- "아름다운", "최고의" 같은 형용사만 있는 문장
- 다른 업소 이야기. 상호와 주소가 위와 일치하는 곳만이다
[문장 쓰는 법]
- **각 문장에 업소 이름을 넣어라.** "이 숙소는…" 처럼 쓰지 마라 — 문장만 떼어 놔도 어느
업소 이야기인지 알 수 있어야 한다. 뒤에서 이 문장들만 모아 근거로 쓰기 때문이다.
- 한 문장에 사실 하나. 두 가지를 이어 붙이면 한쪽이 틀렸을 때 통째로 버리게 된다.
[형식] 아래 JSON 만 출력한다. 각 항목에 **그 사실이 적힌 페이지 주소**를 단다.
{{"items": [{{"text": "한 문장으로 적은 사실", "source": {{"name": "출처 이름", "url": "https://..."}}}}]}}
항목은 최대 {MAX_ITEMS}개. 출처를 댈 수 없는 항목은 넣지 마라 — 적게 주는 편이 낫다."""