Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/WORKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,5 @@
| 2026-08-03 | 하루에 유형마다 서너 번씩 난 **반복 사고 세 유형**을 트러블슈팅으로 남겼다(`S15P11A705-293`, T70~T72). **개별 사고의 해결이 아니라 반복 유형을 적는 문서다** — 각 건의 처리는 자기 티켓·PR·이슈에 있고 여기에는 「무엇이 반복됐는가」만 남는다. **유형마다 마지막 사고는 앞선 사고의 대응이 이미 있는 상태에서 났다** — 07-31 색인 사고 다섯 건과 같은 모양이고, 차이는 성실성이 아니라 강제 장치다. ① **사본에서 꺼낸 값**(T70) — 철회된 단정이 그 문서를 근거로 인용하며 되살아나고, 코드 한 행을 인용하며 **바로 다음 행**을 놓친 오독이 **이미 병합된 작업의 설계 근거**가 되고, 원장에 적어 둔 정정이 계약에서 원래 문구로 되돌아오고, 계약에 실은 수치가 여러 번 틀렸다. **넷 다 사본이 아니라 원문을 연 다른 사람이 잡았다** — 「쓰기 직전 재조회」 규칙은 이미 있었는데도 났다. 사본은 자기가 낡았다고 말하지 않으므로 대응을 읽는 쪽이 아니라 **쓰는 쪽**에 뒀다(`R-18` 「값 대신 출처」). 철회하는 쪽도 대상을 좁힌다 — **폐기된 것은 값이 아니라 값의 항상성이다.** ② **반만 덮는 자동화**(T71) — 「성공했다」와 「실제로 반영됐다」가 다르고 실패가 조용하다. **성공 판정을 네 층으로 가른다**(프로세스 생존 ≠ 실행 성공 ≠ 실제 변경 발생 ≠ 최종 전달 성공). 감시 고아는 heartbeat 로 고쳤고(판정은 사람이 한다), 배포 자동화 둘은 인프라 소관이라 이슈로 기록만 했다. **「다음 스케줄이 복구한다」는 전제 자체가 약하다.** ③ **공개 저장소 실사용자 기록**(T72) — 훅이 **바깥으로 나가는 발화**만 검사해 스크립트가 만들고 사람이 `git add` 한 산출물은 검사 지점을 한 번도 안 거쳤고, `.gitignore` 예외가 「커밋하라」고 **적극적으로 지시**하고 있었다. **대응 미정** — 선택지 넷 중 마스킹 문구의 경로 안내 제거만 즉시 가능하다. 위치·규모는 적지 않는다(위치 비공개 선례). **이 문서 자신이 T70 의 첫 시험이라 수치·코드 행 번호를 옮기지 않고 좌표만 가리킨다** | [T70~T72](troubleshooting/2026-08-03-repeat-incidents.md) |
| 2026-08-03 | 표시명 개정(`S15P11A705-292`)이 코드에서 운영 화면까지 간 **배포 체인을 기록했다**(I50). **체인 여섯 단계의 소관을 갈랐다** — 우리(`ai#102` dev · `ai#104` 릴리스) · 자동화(`image-publish` 는 별도 워크플로가 아니라 CI 안의 job 이고 `main` push 조건이라 **`dev` 병합만으로는 이미지가 안 만들어진다**) · 인프라 소관(`infra#185`) · 클러스터 sync · 화면. **핵심은 미결 판정 하나를 실측으로 닫은 것이다** — 프리셋 봉인 값이 개정 전 값 그대로인 채 배포됐는데 새 표시명이 화면에 나왔다. 따라서 그 값은 **판 표기이지 부트스트랩 재실행 조건이 아니다**(`BeforeHookCreation` 이 같은 이름 Job 을 지우고 다시 만들고, 이미지 태그가 바뀌면 sync 가 돌며, 적재가 멱등이다). 그 전에는 중앙이 「값이 같으면 재실행되지 않는다」로 판정했고 독립 검증이 논거를 반증했으나 **클러스터 관측 경로가 없어 「확인 불가」로 끝나 있었다** — 배포가 답을 냈다. **그래도 표기가 낡은 것은 기록 정합성 문제로 남는다**(배포를 막지 않을 뿐이고 틀려도 실패하는 검사가 없다). **릴리스가 두 번 막혔고 둘 다 다음에 그대로 다시 만난다** — base 검사는 `main` base 를 `release/*`·`hotfix/*` 로 한정하므로 `dev` 를 그대로 열면 CI 가 막고(`ai#103` 이 그렇게 닫혔다), strict 상태 검사는 BEHIND 를 거부하므로 `main` 을 먼저 병합해 해소한다(병합 커밋 `032e391f`). 갱신 자동화의 예정 회차가 릴리스 병합 **직전**에 돌아 변경 없이 끝났고, 갱신을 만든 것은 수동 실행이라 **`success` 두 번의 뜻이 다르다.** 확인 수단이 화면뿐이라 「아직인가 실패인가」가 갈리지 않아 한 번 오판했다가 재확인에서 뒤집혔다. **값은 옮기지 않고 좌표만 적었다**(`T70` 적용) | [배포 체인 리포트](implements/2026-08-03-preset-display-name-deploy-chain.md), [I48](implements/2026-08-03-dev-deploy-gap.md), [T70~T72](troubleshooting/2026-08-03-repeat-incidents.md) |
| 2026-08-03 | 외부 리뷰 문서를 **P47 제안 문서로 다시 썼다** — 프리셋의 표시 라벨·축 정의·스키마 개정안(`S15P11A705-228`·`-269`·`-270`·`-271`·`-292`, `ai#90` 후속). **proposals 최초의 `Proposed`** 다 — §7 이후가 미실행이고 §11 실측이 없다. **다만 §6(표시 라벨)은 문서를 쓰는 사이에 반영이 끝났다** — `-292` 가 `display_name` 을 명사·명사구로 통일해 `ai#102`(`dev`)·`ai#104`(`main`)로 나갔고, 그래서 그 절만 제안이 아니라 **기록**으로 다시 썼다(제목에 「반영 완료」 · 표의 「권장 표시명」을 「반영값」으로 · 정본은 문서가 아니라 `data/keyword_preset.yaml` 임을 명시). **초안과 실제가 갈린 셋을 확정으로 남겼다** — `ALONE` 은 초안의 `1인` 을 기각하고 `혼자` 를 유지했고(축의 나머지가 관계어인데 수량 표현만 이질적이다), `TRENDY` 는 `examples` 가 유행이 아니라 미감을 가리키므로 `감성` 으로 확정했으며, `RETRO` 는 **초안에 없던 결정**으로 `복고` 가 됐다(같은 축의 어휘 층위 통일 · `description` 과 정합). **선결 조건이 통째로 바뀌었다** — 「프리셋을 고치면 기존 판정을 전부 재처리한다」로 정해지면서 「판을 구분할 경로」(행 `version` 을 올릴 경로가 없다는 사실, `-269`)가 선결에서 §3-F 로 내려갔고, 그 자리에 **「재처리할 수단이 없다」**가 들어갔다: 완료 State 를 되돌리는 코드가 없고 · 재스캔이 잡는 상태에 완료가 없으며 · 한 번에 밀면 게이트웨이 한도에 걸려 재시도 예산이 소진되면 실패로 굳는다(부재의 범위는 `ai#100`). **「정할 것」이 아니라 「만들 것」이라 §13 0단계가 구현 항목이 됐고**, 같은 이유로 §10.3 세트 release 는 운영 용도가 빠지고 평가 재현만 남아 우선순위가 내려갔다. **원본의 다섯 곳을 정정했다**: ① Feed 표시 정렬을 절째로 빼고 `back` P46 을 가리켰다(원본 제안이 그 결정이 **명시적으로 기각한** 방향이었다), ② 「CI 에서 검증되지 않는 것」 목록을 없애고 방향과 「테스트 코드가 정본」만 남겼다(이미 있는 테스트를 없다고 적고 있었다), ③ 배포 서술을 hook 메커니즘 존재라는 구조 사실로 줄였다(나머지는 `infra` 소관), ④ `version` 을 「세트 release 와 분리」가 아니라 판 식별 문제로 재배치, ⑤ 표시명 오염을 「임베딩 입력」이 아니라 **임베딩·판정 프롬프트 양쪽**으로 고쳤다 — 그래서 이 변경은 임베딩 실험이 아니라 프롬프트 실험 대상이다. **구조 사실 일곱을 보강했다**(개정에 기존 판정 재처리가 따라붙는다 · 후보 슬롯 경쟁으로 프리셋 단위 개선이 전역에서 상쇄된다 · 저빈도 프리셋 처분이 라벨 개정보다 앞선다 · 표시명 개정이 백엔드 조회의 정렬과 중복 흡수에 파급된다 · **`is_active` 를 끌 경로가 없어 tombstone 은 구멍 메우기다** · 공용 계약 개정 단계 · 임베딩 비결정성 때문에 1회 측정으로 채택을 가르지 않는다). **값을 쓰지 않았다** — 측정 수치·현행 상수·행 번호·배포 상태를 전부 출처 참조로 바꿨고 남은 숫자는 §6.2 의 반영값뿐인데 그것도 YAML 이 정본임을 표 앞에 못박았다. 문서가 값을 안고 있으면 값이 바뀔 때 문서만 낡는다. **`§6` 이 실측 없이 먼저 나간 것은 §14 에 감수 항목으로 남겼다** — 표시 계층만 손댄 범위였기 때문이고, 대가로 라벨 개정이 판정에 얼마나 파급됐는지는 재지 않은 채 §11 에 남는다 | [P47](proposals/P47-keyword-preset-label-axis.md), [근거 리포트](implements/2026-08-03-preset-description.md), [T68·T69](troubleshooting/2026-08-03-preset-description.md) |
| 2026-08-07 | 결합 신뢰도 게이트(중앙 조정 세션 인계 문서 `OFFTOPIC-CONFIDENCE-GATE-HANDOFF-DRAFT.md` §4) 의 threshold 후보 0.35 를 오프라인으로 재측정했다(`S15P11A705-401`). 0.35 는 원래 `SEARCH_KEYWORD_RERANK_FLOOR`(질의-Preset 코사인 판정값)에서 가져온 값이라 이 용도(S1 단독·저유사도 결과를 숨기는 게이트)로는 검증된 적이 없었다. `gate_sweep.py` 로 문장형 정답 12·단어형 정답 66·진단프로브 22·무관 60·타인소유 96건에 게이트 규칙을 적용해 보니 threshold 0.35 에서 정답 손실 0 을 유지하면서 무관 노출이 크게 줄었고(단어무관 23/45·타인소유 55/96 신규 침묵), 손실 0 구간의 최적값(0.36)과도 사실상 같았다. **최초 실행에서는 프로브 6 건이 손실로 나왔는데, 원인은 `recall_probe.json` 이 질의별이 아니라 최상위에 `user_id` 를 두는 형식을 놓친 것이었다** — 고치자 손실이 threshold 0.38 까지 0 건으로 나왔다. 0.35 채택을 권고한다 | [리포트](implements/2026-08-07-gate-threshold-remeasure.md) |
| 2026-08-07 | 401 리포트 작성 뒤 `test_docs_index.py`가 `docs/implements/README.md`의 색인 두 표(개별 리포트·구현·산출 전수) 모두에서 I58(`2026-08-07-gate-threshold-remeasure.md`) 행이 빠진 것을 잡았다 — 리포트를 커밋하면서 색인 갱신을 놓친 것. 두 표 모두에 행을 추가해 정정했다 | [gate_sweep 리포트](implements/2026-08-07-gate-threshold-remeasure.md) |
52 changes: 52 additions & 0 deletions docs/implements/2026-08-07-gate-threshold-remeasure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 결합 신뢰도 게이트 임계값 오프라인 재측정

- **티켓**: S15P11A705-401
- **날짜**: 2026-08-07
- **하네스**: `tools/search_cut/gate_sweep.py` — 실행 절차·데이터 원본은 그 파일 docstring
- **기준 문서**: `OFFTOPIC-CONFIDENCE-GATE-HANDOFF-DRAFT.md`(중앙 조정 세션 인계 문서) §4 · [P49](../proposals/P49-multi-signal-search.md) §9(0.35의 원출처) · 선행 실측 [I57](2026-08-06-rerank-adoption.md)
- **성격**: 재기만 한다. 앱 런타임 코드를 바꾸지 않는다. 기존에 커밋된 행렬만 읽었다(DB·GMS 호출 0회).

## 한눈에 보는 결과

| 항목 | 확인한 내용 | 판단 |
|---|---|---|
| 0.35 재사용 안전성 | 문장형 정답 12건·단어형 정답 66건·진단프로브 22건 어디에서도 threshold=0.35에서 정답 손실이 0건이다 | 이 데이터에서는 **안전하다** — 재사용해도 된다 |
| 0.35의 최적성 | 정답 손실 0을 유지하는 구간(0.25~0.37)에서 최적값은 0.36이고, 0.35는 그보다 침묵 2건(타인소유 1·단어무관 1) 적을 뿐이다 | 0.35는 최적에 근접한다 — 별도 값을 채택할 실익이 없다 |
| 무관 노출 감소 | 0.35에서 문장무관 3/15·단어무관 23/45·타인소유 55/96이 추가로 침묵한다(현재 0건 대비) | 게이트가 실제로 노출을 크게 줄인다 |
| 재사용 판단 근거 | P48→P49 구조 전환 때 우연히 같은 숫자가 재확정됐던 선례(I57)와 달리, 이번엔 **구조가 또 달라** 실제로 다시 쟀다 | "값이 같다고 검증을 생략하지 않는다"는 이 프로젝트 관행을 그대로 따랐다 |

## 배경

`OFFTOPIC-CONFIDENCE-GATE-HANDOFF-DRAFT.md` §4.3은 새 게이트(S1 단독·유사도<threshold면 결과 제외)의 threshold 후보로 0.35를 들면서, 그 값이 `SEARCH_KEYWORD_RERANK_FLOOR`(질의-Preset 코사인이 같은 의미인지 판단하는 값)에서 가져온 것이라 **이 용도로는 검증된 적이 없다**고 스스로 명시했다(§4.3 "이 재사용이 실제로 타당한지는 아직 검증되지 않았다"). 이 리포트는 그 검증이다.

## 측정 조건

- 데이터: `matrix.json`(문장형 정답 12·무관 15) · `word_grid.json`(단어형 정답 66·무관 45·타인소유 96) · `recall_probe.json`(진단프로브 22) — 전부 기존에 커밋된 행렬.
- S2(문자열 매치): `lexical_matrix.json` + 채택 게이트 G1(단어형 한정·부분일치, I54).
- S3(키워드 매치): `keyword_matrix.json` + 채택 파라미터 binary·floor 0.35·top_k 3(I57, `app/core/config.py` 정본). S3 신호는 "재정렬 순서"가 아니라 "신호가 0보다 큰가"만 쓴다 — 이 게이트는 순서와 무관하다.
- 게이트 규칙: 컷 통과 후보 중 S2도 S3도 없고 유사도가 `threshold` 미만이면 제외. threshold 격자는 0.0(게이트 없음) 및 0.16~0.40을 0.01 간격으로 훑었다.
- 결정성: 같은 입력으로 두 번 돌려 출력이 완전히 같음을 확인했다(입력이 파일뿐이고 난수·시각을 쓰지 않는다).

## 확인한 결과

**threshold 0.24 이하는 아무 효과가 없다.** 이 데이터의 무관/타인소유 세그먼트 유사도 상한이 0.25 부근에 몰려 있어, 그 아래 threshold는 게이트가 걸리는 후보 자체가 없다.

**threshold 0.25~0.37은 정답 손실 없이 무관 노출을 줄인다.** 0.35에서: 문장무관 3/15, 단어무관 23/45, 타인소유 55/96이 새로 침묵한다 — 게이트 도입 전(전부 0건)과 비교해 상당한 개선이다. 같은 구간에서 문장형·단어형·진단프로브 어디에서도 정답이 하나도 빠지지 않았다(1위 손실도 0건).

**threshold 0.38 이상부터 진단프로브에서 손실이 시작된다.** 0.38에서 프로브 1건이 처음으로 전량 손실된다 — 그 질의는 S2·S3 신호가 없고 유사도가 0.35~0.38 사이인 파라프레이즈다.

**구간 안에서 0.36이 근소하게 더 낫다** — 같은 손실 0 조건에서 0.35보다 타인소유 1건·단어무관 1건을 더 침묵시킨다. 차이가 작고(전체 96·45건 대비 각 1건), 인접 threshold 간 차이가 임베딩 재현성 상한(T68, |Δsim| 0.0044)보다 작은 격자 간격(0.01)에서 나온 값이라 이 차이 자체를 신뢰하지는 않는다 — 결론은 "0.35도 0.36도 이 안전 구간 안에 있다"이지 "0.36이 더 우수하다"가 아니다.

### 최초 실행에서 발견하고 고친 버그

첫 실행에서는 threshold 0.25부터 진단프로브 손실이 나타났고(0.35에서 6건, 「스팟」 포함), 원인은 `recall_probe.json`이 질의별이 아니라 **최상위**에 `user_id`를 두는 형식인데 이를 놓쳐 프로브 전 질의의 S2·S3 신호가 전부 빈 값으로 계산된 것이었다(`lexical_sweep.py`가 `default_uid`로 이미 처리하던 문제와 같다). 수정 후 프로브 손실이 threshold 0.38까지 0건으로 나왔다 — 「스팟」은 실제로 본문에 문자열이 그대로 있어(`record_id=287`, `substring=True`) S2 신호가 항상 잡힌다.

## 남은 것

- 이 측정은 S1/S2/S3 신호를 **오프라인으로 재구성**한 것이다. S15P11A705-400(back 게이트 구현) 이후에는 실서버 on/off 대조로 한 번 더 검증해야 한다(`rerank_verify_live.py`류 절차).
- Record 42건·소유자 3명 규모의 결론이다. 절대 채택값이 아니라 방향과 상대 비교로 읽는다.
- 유사도 값 자체의 회차 간 흔들림(T68)은 이번에 재측정하지 않고 기존 실측을 인용했다. 0.35~0.36처럼 격자 간격(0.01)보다 좁은 인접 값 비교는 그 흔들림 안에 있으므로 우열을 주장하지 않는다.

## 채택

**threshold 0.35를 채택한다.** 이 값은 이 데이터에서 정답 손실 0을 유지하면서 무관 노출을 크게 줄이고, 안전 구간 안의 최적값(0.36)과도 사실상 같다. `SEARCH_KEYWORD_RERANK_FLOOR`와 값은 같지만, 이번에도 I57과 같은 이유로 값을 가정하지 않고 이 용도로 별도 재측정한 결과다 — 우연히 같은 숫자가 다시 나온 것이지 재사용을 전제하지 않았다.
Loading
Loading