docs: 게이트웨이 입력게이트·도구게이팅·출력가드 전체 지도 도표 (guard-map.html)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
kim
2026-09-23 12:16:27 +09:00
co-authored by Claude Code
parent 9283df99d8
commit a815721976
2 changed files with 702 additions and 0 deletions
+351
View File
@@ -0,0 +1,351 @@
<title>홈클로 가드 지도</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+KR:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap">
<style>
:root {
--bg: #f2f4f6;
--surface: #ffffff;
--ink: #1b2733;
--muted: #5d6d7c;
--line: #d5dce2;
--c-input: #0d7c85;
--c-input-soft: #e2f1f2;
--c-tool: #4653b4;
--c-tool-soft: #e7e9f8;
--c-out: #b0512a;
--c-out-soft: #f8ebe3;
--c-note: #7a6a1f;
--c-note-soft: #f4efdd;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #12171c;
--surface: #1a2129;
--ink: #dfe6ec;
--muted: #93a3b2;
--line: #2b3540;
--c-input: #4fb6be;
--c-input-soft: #14343a;
--c-tool: #8d97e3;
--c-tool-soft: #232647;
--c-out: #e0885d;
--c-out-soft: #3a2417;
--c-note: #c9b45e;
--c-note-soft: #332d14;
}
}
:root[data-theme="dark"] {
--bg: #12171c;
--surface: #1a2129;
--ink: #dfe6ec;
--muted: #93a3b2;
--line: #2b3540;
--c-input: #4fb6be;
--c-input-soft: #14343a;
--c-tool: #8d97e3;
--c-tool-soft: #232647;
--c-out: #e0885d;
--c-out-soft: #3a2417;
--c-note: #c9b45e;
--c-note-soft: #332d14;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: 'IBM Plex Sans KR', sans-serif;
font-size: 14px;
line-height: 1.65;
padding: 28px 20px 56px;
}
.wrap { max-width: 880px; margin: 0 auto; }
.eyebrow {
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
letter-spacing: 0.14em;
color: var(--muted);
text-transform: uppercase;
margin: 0 0 6px;
}
h1 { font-size: 26px; font-weight: 700; margin: 0 0 8px; letter-spacing: -0.01em; }
.lede { color: var(--muted); margin: 0 0 24px; max-width: 60ch; }
.lede code, td code, .stage p code {
font-family: 'IBM Plex Mono', monospace;
font-size: 12px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 4px;
padding: 1px 5px;
white-space: nowrap;
}
/* ── 실측 수치 스트립 ── */
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-bottom: 36px;
}
.metric {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
padding: 12px 14px 10px;
}
.metric .num {
font-family: 'IBM Plex Mono', monospace;
font-size: 24px;
font-weight: 500;
font-variant-numeric: tabular-nums;
line-height: 1.2;
}
.metric .lbl { font-size: 12px; color: var(--muted); }
h2 {
font-size: 17px;
font-weight: 700;
margin: 40px 0 4px;
padding-top: 20px;
border-top: 1px solid var(--line);
}
h2 .src {
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
font-weight: 400;
color: var(--muted);
margin-left: 8px;
}
.sub { color: var(--muted); font-size: 13px; margin: 0 0 16px; }
/* ── 파이프라인 흐름도 ── */
.flow { display: flex; flex-direction: column; }
.stage {
background: var(--surface);
border: 1px solid var(--line);
border-left: 4px solid var(--stage-color, var(--muted));
border-radius: 8px;
padding: 13px 16px 11px;
}
.stage .tag {
display: inline-block;
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
color: var(--stage-color);
letter-spacing: 0.08em;
margin-bottom: 3px;
}
.stage h3 { font-size: 14.5px; margin: 0 0 4px; }
.stage p { margin: 0; color: var(--muted); font-size: 13px; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.chip {
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
padding: 2px 8px;
border-radius: 20px;
border: 1px solid color-mix(in srgb, var(--stage-color) 40%, var(--line));
background: color-mix(in srgb, var(--stage-color) 9%, var(--surface));
color: var(--stage-color);
}
.arrow {
align-self: flex-start;
margin-left: 26px;
width: 1px;
height: 22px;
background: var(--line);
position: relative;
}
.arrow::after {
content: '';
position: absolute;
bottom: -1px; left: -3.5px;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 6px solid var(--line);
}
.loop-note {
align-self: flex-start;
margin: 2px 0 2px 40px;
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
color: var(--c-out);
}
/* ── 표 ── */
.tbl-wrap { overflow-x: auto; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; }
table { border-collapse: collapse; width: 100%; min-width: 620px; }
th, td { text-align: left; padding: 9px 14px; vertical-align: top; border-bottom: 1px solid var(--line); }
th {
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
letter-spacing: 0.08em;
color: var(--muted);
font-weight: 500;
}
tr:last-child td { border-bottom: none; }
td.gate { font-family: 'IBM Plex Mono', monospace; font-size: 12px; white-space: nowrap; }
tr.in td.gate { color: var(--c-input); }
tr.tl td.gate { color: var(--c-tool); }
tr.out td.gate { color: var(--c-out); }
td .eff { font-size: 12px; color: var(--muted); }
/* ── 실측 노트 ── */
.notes { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 10px; }
.note {
background: var(--c-note-soft);
border: 1px solid color-mix(in srgb, var(--c-note) 35%, var(--line));
border-radius: 8px;
padding: 11px 14px;
font-size: 13px;
}
.note b { color: var(--c-note); }
/* ── 진행 중 ── */
.wip {
background: var(--surface);
border: 1px dashed var(--c-tool);
border-radius: 8px;
padding: 14px 16px;
margin-top: 10px;
}
.wip .lbl {
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
color: var(--c-tool);
letter-spacing: 0.1em;
margin-bottom: 4px;
}
.wip p { margin: 0 0 8px; font-size: 13px; color: var(--muted); }
.wip p:last-child { margin-bottom: 0; }
footer { margin-top: 44px; font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--muted); }
</style>
<div class="wrap">
<p class="eyebrow">HOMECLAW GATEWAY · 2026-09-23 기준</p>
<h1>홈클로 가드 지도</h1>
<p class="lede">
한 발이 사용자에게 닿기까지 지나가는 문(게이트) 전체 — <code>src/gateway/guards/prompt-gates.ts</code>의 입장 분류,
<code>tool-scope.ts</code>의 도구 게이팅, 모델 응답을 검사하는 출력 가드까지 한 장으로.
</p>
<div class="metrics">
<div class="metric"><div class="num">1,011</div><div class="lbl">감사 대상 발화 (09-04)</div></div>
<div class="metric"><div class="num">21%</div><div class="lbl">가드 발동률</div></div>
<div class="metric"><div class="num">80~92%</div><div class="lbl">도구 토큰 게이팅 절감</div></div>
<div class="metric"><div class="num">7종</div><div class="lbl">휴면 가드 (0건 — 제거 금지)</div></div>
</div>
<h2>파이프라인<span class="src">handle-chat.ts · prompt-gates.ts · tool-scope.ts</span></h2>
<p class="sub">발화 하나가 지나는 7단. ④→⑥ 사이에서 문제가 잡히면 모델 호출로 되돌아간다(강제 재검색·재프롬프트, 최대 1회).</p>
<div class="flow">
<div class="stage" style="--stage-color: var(--c-input)">
<span class="tag">① INPUT</span>
<h3>입력 분류</h3>
<p>발화를 종류별로 판별해 이후 단계의 스위치를 켠다. 뉴스·재난·실시간·사실확인·인사·실행형·메시징.</p>
<div class="chips">
<span class="chip">isLiveDataRequest</span><span class="chip">isNewsRequest</span><span class="chip">DISASTER_PATTERN</span><span class="chip">isFactualInfoRequest</span><span class="chip">isGreetingLike</span><span class="chip">isExecutionLike</span>
</div>
</div>
<div class="arrow"></div>
<div class="stage" style="--stage-color: var(--c-tool)">
<span class="tag">② TOOLS</span>
<h3>도구 게이팅</h3>
<p>정의 77개(<code>build-tools.ts</code>)를 세션·턴 종류별로 거른다. 부팅 턴은 2개만, 번역·proj 세션은 0개, 코드AI는 블록리스트 제외, 나머지는 스킬 키워드 게이트.</p>
<div class="chips"><span class="chip">selectToolsForTurn</span><span class="chip">buildSkillToolFilter</span><span class="chip">codeAiBlockedTools</span></div>
</div>
<div class="arrow"></div>
<div class="stage" style="--stage-color: var(--c-tool)">
<span class="tag">③ PROMPT</span>
<h3>프롬프트 조립</h3>
<p>시스템 프롬프트 + 성격 컨텍스트(USER.md·메모리 스니펫·카테고리 감지) + 도구 스키마. 게이팅 덕에 도구 프롬프트 비용이 17.4K 토큰에서 80~92% 절감된다.</p>
<div class="chips"><span class="chip">buildChatSystemPrompt</span><span class="chip">personality-context</span></div>
</div>
<div class="arrow"></div>
<div class="stage" style="--stage-color: var(--muted)">
<span class="tag">④ MODEL</span>
<h3>모델 호출 · 라운드 루프</h3>
<p>primary <code>glm-5.3-flash:cloud</code> (09-22 교체). 라운드 0에서 실시간·사실형 질의면 <code>tool_choice: required</code> 강제 — 전역 ON(07-29).</p>
<div class="chips"><span class="chip">forceToolChoiceOnLiveData</span><span class="chip">round loop</span></div>
</div>
<div class="arrow"></div>
<div class="stage" style="--stage-color: var(--muted)">
<span class="tag">⑤ SPLIT</span>
<h3>thinking 분리</h3>
<p><code>separateThinkingFromContent</code> — think 태그 제거 + 키워드 휴리스틱 + 영어 셀프토크 구조 기반 분리(GLM 노출 대응, 진행 중).</p>
</div>
<div class="arrow"></div>
<div class="stage" style="--stage-color: var(--c-out)">
<span class="tag">⑥ GUARDS</span>
<h3>출력 가드</h3>
<p>답변을 다시 검사한다. 실패하면 검색 강제·재프롬프트로 ④로 되돌아감 — 아래 표.</p>
<div class="chips">
<span class="chip">NUMERIC-GROUNDING</span><span class="chip">SEARCH-ABANDONMENT</span><span class="chip">INTENT-ONLY</span><span class="chip">DEFERRAL</span><span class="chip">STALE-VERSION</span><span class="chip">SYCOPHANTIC-CONFIRM</span>
</div>
</div>
<div class="arrow"></div>
<div class="stage" style="--stage-color: var(--muted)">
<span class="tag">⑦ REPLY</span>
<h3>응답</h3>
<p>세션 저장 + thinking은 UI에서 별도 표시.</p>
</div>
</div>
<h2>입력 게이트<span class="src">guards/prompt-gates.ts</span></h2>
<p class="sub">발화를 보고 "이건 어떤 요청인가"를 판별하는 술어들. 출력 가드와 달리 발화가 통과하는 문이 아니라 라우팅 스위치 역할.</p>
<div class="tbl-wrap">
<table>
<tr><th>게이트</th><th>판별 대상</th><th>효과</th></tr>
<tr class="in"><td class="gate">isLiveDataRequest</td><td>날씨·실시간·시세</td><td>도구 강제, 실시간 검색 경로 <span class="eff">— round 0 tool_choice required</span></td></tr>
<tr class="in"><td class="gate">isNewsRequest · DISASTER_PATTERN</td><td>뉴스·재난 키워드</td><td>뉴스 라우팅, 재난 시 전건수 답변</td></tr>
<tr class="in"><td class="gate">isFactualInfoRequest / ThreadFollowUp</td><td>사실형 질문·후속질문</td><td>검증 경로 진입</td></tr>
<tr class="in"><td class="gate">isExecutionLikeRequest</td><td>실행형 요청</td><td>사실검증·가드 면제</td></tr>
<tr class="in"><td class="gate">isGreetingLikeMessage</td><td>인사말</td><td>검증 면제</td></tr>
<tr class="in"><td class="gate">isMessagingRequest + claimsMessageSent</td><td>"보냈다"는 주장</td><td>전송 사실 검증</td></tr>
<tr class="in"><td class="gate">assumedGenerationTokenInQuery</td><td>검색어에 학습시점 세대 박음("M4" 등)</td><td>세대 추정 쿼리 차단</td></tr>
<tr class="in"><td class="gate">shouldForceSessionScopeForTemporalClaim</td><td>시점 주장</td><td>세션 범위 강제</td></tr>
<tr class="in"><td class="gate">isExemptFromVerification</td><td>면제 목록</td><td>가드 전체 스킵</td></tr>
</table>
</div>
<h2>출력 가드<span class="src">handle-chat.ts · retry-decisions.ts · numeric-grounding.ts</span></h2>
<p class="sub">모델이 이미 낸 답을 검사해 실패 시 모델 호출로 되돌린다. 가드 발동률 실측 21%.</p>
<div class="tbl-wrap">
<table>
<tr><th>가드</th><th>잡는 것</th><th>동작</th></tr>
<tr class="out"><td class="gate">NUMERIC-GROUNDING</td><td>검증 안 된 스펙·가격 숫자</td><td>강제 재검색 (최대 1회). <code>sourceCoversQuery</code> 관문으로 "맞는 답을 지우는" 오탐 차단(09-04)</td></tr>
<tr class="out"><td class="gate">SEARCH-ABANDONMENT</td><td>도구 0개로 턴 종료</td><td>재검색 재프롬프트</td></tr>
<tr class="out"><td class="gate">DEFERRAL-PROMISE · INTENT-ONLY</td><td>"잠시만 기다려 주세요"·의지만 말고 종료</td><td>턴 강제 지속</td></tr>
<tr class="out"><td class="gate">SYCOPHANTIC-CONFIRMATION</td><td>근거 없이 사용자 추측 맞다고 확인</td><td>재검증</td></tr>
<tr class="out"><td class="gate">STALE-VERSION</td><td>최신판 요청에 버전 확인 누락</td><td>버전 확인 재요청</td></tr>
<tr class="out"><td class="gate">EMPTY-GROUNDING · DEAD-END</td><td>빈 검색결과·막다른 길</td><td>폴백 체인 (searxng→ollama_cloud→tavily→…)</td></tr>
<tr class="out"><td class="gate">SAFETY-REFUSAL</td><td>안전 거부 오탐</td><td>재시도 판단</td></tr>
<tr class="out"><td class="gate">MSG-SENT</td><td>"보냈다" 주장</td><td>전송 사실 검증</td></tr>
</table>
</div>
<h2>감사 실측 노트<span class="src">2026-09-02 ~ 09-04</span></h2>
<div class="notes">
<div class="note"><b>발화 1,011건 전수</b> — 전부 게이트 통과. 로그 기반 감사로 보정한 유일한 기준선.</div>
<div class="note"><b>휴면 가드 7종</b> — 감사 시점 0건이지만 의도적 휴면. 제거하면 안 됨.</div>
<div class="note"><b>호출 47%가 출력 60토큰 미만</b> — 가드·게이트 오버헤드가 큰 비중을 차지하지 않는 이유.</div>
<div class="note"><b>한글 정규식 함정</b> — <code>\b</code>는 한글 뒤에서 절대 매칭 안 됨 → <code>rankResults</code>가 한국어 검색 관련성을 0으로 만들었던 진범. 한글 정규식은 문자클래스부터 의심.</div>
<div class="note"><b>가드는 실측 데이터로만 보정</b> — 가상 케이스로 임계값 잡으면 3번 연속 틀림. SSE tool_result는 잘려 있음.</div>
<div class="note"><b>가드의 전제 검증</b> — NUMERIC-GROUNDING이 맞는 답을 5턴 연속 지운 사례 → <code>sourceCoversQuery</code> 관문 신설.</div>
</div>
<h2>진행 중</h2>
<div class="wip">
<div class="lbl">09-22 · GLM 전환 + 영어 셀프토크 노출</div>
<p>primary를 <code>gemma4:31b-cloud</code>에서 <code>glm-5.3-flash:cloud</code>로 교체하자 답변 앞에 영어 내부 계획("Key message:", "Answer format:")이 노출되고 마지막 단락에서 한글과 무공백 융합("No emoji habit.아쉽게도…").</p>
<p>대응: <code>separateThinkingFromContent</code>를 <code>thinking-separation.ts</code>로 추출 + 구조 기반 <code>stripEnglishScratchPrefix</code> 추가. 단위 테스트 6건 중 4건 실패 — 한글이 섞인 영어 계획 단락 허용하도록 수정 필요 (미해결).</p>
</div>
<footer>/srv/homeclaw · src/gateway/chat/ · src/gateway/guards/prompt-gates.ts</footer>
</div>
+351
View File
@@ -0,0 +1,351 @@
<title>홈클로 가드 지도</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+KR:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap">
<style>
:root {
--bg: #f2f4f6;
--surface: #ffffff;
--ink: #1b2733;
--muted: #5d6d7c;
--line: #d5dce2;
--c-input: #0d7c85;
--c-input-soft: #e2f1f2;
--c-tool: #4653b4;
--c-tool-soft: #e7e9f8;
--c-out: #b0512a;
--c-out-soft: #f8ebe3;
--c-note: #7a6a1f;
--c-note-soft: #f4efdd;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #12171c;
--surface: #1a2129;
--ink: #dfe6ec;
--muted: #93a3b2;
--line: #2b3540;
--c-input: #4fb6be;
--c-input-soft: #14343a;
--c-tool: #8d97e3;
--c-tool-soft: #232647;
--c-out: #e0885d;
--c-out-soft: #3a2417;
--c-note: #c9b45e;
--c-note-soft: #332d14;
}
}
:root[data-theme="dark"] {
--bg: #12171c;
--surface: #1a2129;
--ink: #dfe6ec;
--muted: #93a3b2;
--line: #2b3540;
--c-input: #4fb6be;
--c-input-soft: #14343a;
--c-tool: #8d97e3;
--c-tool-soft: #232647;
--c-out: #e0885d;
--c-out-soft: #3a2417;
--c-note: #c9b45e;
--c-note-soft: #332d14;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: 'IBM Plex Sans KR', sans-serif;
font-size: 14px;
line-height: 1.65;
padding: 28px 20px 56px;
}
.wrap { max-width: 880px; margin: 0 auto; }
.eyebrow {
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
letter-spacing: 0.14em;
color: var(--muted);
text-transform: uppercase;
margin: 0 0 6px;
}
h1 { font-size: 26px; font-weight: 700; margin: 0 0 8px; letter-spacing: -0.01em; }
.lede { color: var(--muted); margin: 0 0 24px; max-width: 60ch; }
.lede code, td code, .stage p code {
font-family: 'IBM Plex Mono', monospace;
font-size: 12px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 4px;
padding: 1px 5px;
white-space: nowrap;
}
/* ── 실측 수치 스트립 ── */
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-bottom: 36px;
}
.metric {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
padding: 12px 14px 10px;
}
.metric .num {
font-family: 'IBM Plex Mono', monospace;
font-size: 24px;
font-weight: 500;
font-variant-numeric: tabular-nums;
line-height: 1.2;
}
.metric .lbl { font-size: 12px; color: var(--muted); }
h2 {
font-size: 17px;
font-weight: 700;
margin: 40px 0 4px;
padding-top: 20px;
border-top: 1px solid var(--line);
}
h2 .src {
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
font-weight: 400;
color: var(--muted);
margin-left: 8px;
}
.sub { color: var(--muted); font-size: 13px; margin: 0 0 16px; }
/* ── 파이프라인 흐름도 ── */
.flow { display: flex; flex-direction: column; }
.stage {
background: var(--surface);
border: 1px solid var(--line);
border-left: 4px solid var(--stage-color, var(--muted));
border-radius: 8px;
padding: 13px 16px 11px;
}
.stage .tag {
display: inline-block;
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
color: var(--stage-color);
letter-spacing: 0.08em;
margin-bottom: 3px;
}
.stage h3 { font-size: 14.5px; margin: 0 0 4px; }
.stage p { margin: 0; color: var(--muted); font-size: 13px; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.chip {
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
padding: 2px 8px;
border-radius: 20px;
border: 1px solid color-mix(in srgb, var(--stage-color) 40%, var(--line));
background: color-mix(in srgb, var(--stage-color) 9%, var(--surface));
color: var(--stage-color);
}
.arrow {
align-self: flex-start;
margin-left: 26px;
width: 1px;
height: 22px;
background: var(--line);
position: relative;
}
.arrow::after {
content: '';
position: absolute;
bottom: -1px; left: -3.5px;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 6px solid var(--line);
}
.loop-note {
align-self: flex-start;
margin: 2px 0 2px 40px;
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
color: var(--c-out);
}
/* ── 표 ── */
.tbl-wrap { overflow-x: auto; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; }
table { border-collapse: collapse; width: 100%; min-width: 620px; }
th, td { text-align: left; padding: 9px 14px; vertical-align: top; border-bottom: 1px solid var(--line); }
th {
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
letter-spacing: 0.08em;
color: var(--muted);
font-weight: 500;
}
tr:last-child td { border-bottom: none; }
td.gate { font-family: 'IBM Plex Mono', monospace; font-size: 12px; white-space: nowrap; }
tr.in td.gate { color: var(--c-input); }
tr.tl td.gate { color: var(--c-tool); }
tr.out td.gate { color: var(--c-out); }
td .eff { font-size: 12px; color: var(--muted); }
/* ── 실측 노트 ── */
.notes { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 10px; }
.note {
background: var(--c-note-soft);
border: 1px solid color-mix(in srgb, var(--c-note) 35%, var(--line));
border-radius: 8px;
padding: 11px 14px;
font-size: 13px;
}
.note b { color: var(--c-note); }
/* ── 진행 중 ── */
.wip {
background: var(--surface);
border: 1px dashed var(--c-tool);
border-radius: 8px;
padding: 14px 16px;
margin-top: 10px;
}
.wip .lbl {
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
color: var(--c-tool);
letter-spacing: 0.1em;
margin-bottom: 4px;
}
.wip p { margin: 0 0 8px; font-size: 13px; color: var(--muted); }
.wip p:last-child { margin-bottom: 0; }
footer { margin-top: 44px; font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--muted); }
</style>
<div class="wrap">
<p class="eyebrow">HOMECLAW GATEWAY · 2026-09-23 기준</p>
<h1>홈클로 가드 지도</h1>
<p class="lede">
한 발이 사용자에게 닿기까지 지나가는 문(게이트) 전체 — <code>src/gateway/guards/prompt-gates.ts</code>의 입장 분류,
<code>tool-scope.ts</code>의 도구 게이팅, 모델 응답을 검사하는 출력 가드까지 한 장으로.
</p>
<div class="metrics">
<div class="metric"><div class="num">1,011</div><div class="lbl">감사 대상 발화 (09-04)</div></div>
<div class="metric"><div class="num">21%</div><div class="lbl">가드 발동률</div></div>
<div class="metric"><div class="num">80~92%</div><div class="lbl">도구 토큰 게이팅 절감</div></div>
<div class="metric"><div class="num">7종</div><div class="lbl">휴면 가드 (0건 — 제거 금지)</div></div>
</div>
<h2>파이프라인<span class="src">handle-chat.ts · prompt-gates.ts · tool-scope.ts</span></h2>
<p class="sub">발화 하나가 지나는 7단. ④→⑥ 사이에서 문제가 잡히면 모델 호출로 되돌아간다(강제 재검색·재프롬프트, 최대 1회).</p>
<div class="flow">
<div class="stage" style="--stage-color: var(--c-input)">
<span class="tag">① INPUT</span>
<h3>입력 분류</h3>
<p>발화를 종류별로 판별해 이후 단계의 스위치를 켠다. 뉴스·재난·실시간·사실확인·인사·실행형·메시징.</p>
<div class="chips">
<span class="chip">isLiveDataRequest</span><span class="chip">isNewsRequest</span><span class="chip">DISASTER_PATTERN</span><span class="chip">isFactualInfoRequest</span><span class="chip">isGreetingLike</span><span class="chip">isExecutionLike</span>
</div>
</div>
<div class="arrow"></div>
<div class="stage" style="--stage-color: var(--c-tool)">
<span class="tag">② TOOLS</span>
<h3>도구 게이팅</h3>
<p>정의 77개(<code>build-tools.ts</code>)를 세션·턴 종류별로 거른다. 부팅 턴은 2개만, 번역·proj 세션은 0개, 코드AI는 블록리스트 제외, 나머지는 스킬 키워드 게이트.</p>
<div class="chips"><span class="chip">selectToolsForTurn</span><span class="chip">buildSkillToolFilter</span><span class="chip">codeAiBlockedTools</span></div>
</div>
<div class="arrow"></div>
<div class="stage" style="--stage-color: var(--c-tool)">
<span class="tag">③ PROMPT</span>
<h3>프롬프트 조립</h3>
<p>시스템 프롬프트 + 성격 컨텍스트(USER.md·메모리 스니펫·카테고리 감지) + 도구 스키마. 게이팅 덕에 도구 프롬프트 비용이 17.4K 토큰에서 80~92% 절감된다.</p>
<div class="chips"><span class="chip">buildChatSystemPrompt</span><span class="chip">personality-context</span></div>
</div>
<div class="arrow"></div>
<div class="stage" style="--stage-color: var(--muted)">
<span class="tag">④ MODEL</span>
<h3>모델 호출 · 라운드 루프</h3>
<p>primary <code>glm-5.3-flash:cloud</code> (09-22 교체). 라운드 0에서 실시간·사실형 질의면 <code>tool_choice: required</code> 강제 — 전역 ON(07-29).</p>
<div class="chips"><span class="chip">forceToolChoiceOnLiveData</span><span class="chip">round loop</span></div>
</div>
<div class="arrow"></div>
<div class="stage" style="--stage-color: var(--muted)">
<span class="tag">⑤ SPLIT</span>
<h3>thinking 분리</h3>
<p><code>separateThinkingFromContent</code> — think 태그 제거 + 키워드 휴리스틱 + 영어 셀프토크 구조 기반 분리(GLM 노출 대응, 진행 중).</p>
</div>
<div class="arrow"></div>
<div class="stage" style="--stage-color: var(--c-out)">
<span class="tag">⑥ GUARDS</span>
<h3>출력 가드</h3>
<p>답변을 다시 검사한다. 실패하면 검색 강제·재프롬프트로 ④로 되돌아감 — 아래 표.</p>
<div class="chips">
<span class="chip">NUMERIC-GROUNDING</span><span class="chip">SEARCH-ABANDONMENT</span><span class="chip">INTENT-ONLY</span><span class="chip">DEFERRAL</span><span class="chip">STALE-VERSION</span><span class="chip">SYCOPHANTIC-CONFIRM</span>
</div>
</div>
<div class="arrow"></div>
<div class="stage" style="--stage-color: var(--muted)">
<span class="tag">⑦ REPLY</span>
<h3>응답</h3>
<p>세션 저장 + thinking은 UI에서 별도 표시.</p>
</div>
</div>
<h2>입력 게이트<span class="src">guards/prompt-gates.ts</span></h2>
<p class="sub">발화를 보고 "이건 어떤 요청인가"를 판별하는 술어들. 출력 가드와 달리 발화가 통과하는 문이 아니라 라우팅 스위치 역할.</p>
<div class="tbl-wrap">
<table>
<tr><th>게이트</th><th>판별 대상</th><th>효과</th></tr>
<tr class="in"><td class="gate">isLiveDataRequest</td><td>날씨·실시간·시세</td><td>도구 강제, 실시간 검색 경로 <span class="eff">— round 0 tool_choice required</span></td></tr>
<tr class="in"><td class="gate">isNewsRequest · DISASTER_PATTERN</td><td>뉴스·재난 키워드</td><td>뉴스 라우팅, 재난 시 전건수 답변</td></tr>
<tr class="in"><td class="gate">isFactualInfoRequest / ThreadFollowUp</td><td>사실형 질문·후속질문</td><td>검증 경로 진입</td></tr>
<tr class="in"><td class="gate">isExecutionLikeRequest</td><td>실행형 요청</td><td>사실검증·가드 면제</td></tr>
<tr class="in"><td class="gate">isGreetingLikeMessage</td><td>인사말</td><td>검증 면제</td></tr>
<tr class="in"><td class="gate">isMessagingRequest + claimsMessageSent</td><td>"보냈다"는 주장</td><td>전송 사실 검증</td></tr>
<tr class="in"><td class="gate">assumedGenerationTokenInQuery</td><td>검색어에 학습시점 세대 박음("M4" 등)</td><td>세대 추정 쿼리 차단</td></tr>
<tr class="in"><td class="gate">shouldForceSessionScopeForTemporalClaim</td><td>시점 주장</td><td>세션 범위 강제</td></tr>
<tr class="in"><td class="gate">isExemptFromVerification</td><td>면제 목록</td><td>가드 전체 스킵</td></tr>
</table>
</div>
<h2>출력 가드<span class="src">handle-chat.ts · retry-decisions.ts · numeric-grounding.ts</span></h2>
<p class="sub">모델이 이미 낸 답을 검사해 실패 시 모델 호출로 되돌린다. 가드 발동률 실측 21%.</p>
<div class="tbl-wrap">
<table>
<tr><th>가드</th><th>잡는 것</th><th>동작</th></tr>
<tr class="out"><td class="gate">NUMERIC-GROUNDING</td><td>검증 안 된 스펙·가격 숫자</td><td>강제 재검색 (최대 1회). <code>sourceCoversQuery</code> 관문으로 "맞는 답을 지우는" 오탐 차단(09-04)</td></tr>
<tr class="out"><td class="gate">SEARCH-ABANDONMENT</td><td>도구 0개로 턴 종료</td><td>재검색 재프롬프트</td></tr>
<tr class="out"><td class="gate">DEFERRAL-PROMISE · INTENT-ONLY</td><td>"잠시만 기다려 주세요"·의지만 말고 종료</td><td>턴 강제 지속</td></tr>
<tr class="out"><td class="gate">SYCOPHANTIC-CONFIRMATION</td><td>근거 없이 사용자 추측 맞다고 확인</td><td>재검증</td></tr>
<tr class="out"><td class="gate">STALE-VERSION</td><td>최신판 요청에 버전 확인 누락</td><td>버전 확인 재요청</td></tr>
<tr class="out"><td class="gate">EMPTY-GROUNDING · DEAD-END</td><td>빈 검색결과·막다른 길</td><td>폴백 체인 (searxng→ollama_cloud→tavily→…)</td></tr>
<tr class="out"><td class="gate">SAFETY-REFUSAL</td><td>안전 거부 오탐</td><td>재시도 판단</td></tr>
<tr class="out"><td class="gate">MSG-SENT</td><td>"보냈다" 주장</td><td>전송 사실 검증</td></tr>
</table>
</div>
<h2>감사 실측 노트<span class="src">2026-09-02 ~ 09-04</span></h2>
<div class="notes">
<div class="note"><b>발화 1,011건 전수</b> — 전부 게이트 통과. 로그 기반 감사로 보정한 유일한 기준선.</div>
<div class="note"><b>휴면 가드 7종</b> — 감사 시점 0건이지만 의도적 휴면. 제거하면 안 됨.</div>
<div class="note"><b>호출 47%가 출력 60토큰 미만</b> — 가드·게이트 오버헤드가 큰 비중을 차지하지 않는 이유.</div>
<div class="note"><b>한글 정규식 함정</b> — <code>\b</code>는 한글 뒤에서 절대 매칭 안 됨 → <code>rankResults</code>가 한국어 검색 관련성을 0으로 만들었던 진범. 한글 정규식은 문자클래스부터 의심.</div>
<div class="note"><b>가드는 실측 데이터로만 보정</b> — 가상 케이스로 임계값 잡으면 3번 연속 틀림. SSE tool_result는 잘려 있음.</div>
<div class="note"><b>가드의 전제 검증</b> — NUMERIC-GROUNDING이 맞는 답을 5턴 연속 지운 사례 → <code>sourceCoversQuery</code> 관문 신설.</div>
</div>
<h2>진행 중</h2>
<div class="wip">
<div class="lbl">09-22 · GLM 전환 + 영어 셀프토크 노출</div>
<p>primary를 <code>gemma4:31b-cloud</code>에서 <code>glm-5.3-flash:cloud</code>로 교체하자 답변 앞에 영어 내부 계획("Key message:", "Answer format:")이 노출되고 마지막 단락에서 한글과 무공백 융합("No emoji habit.아쉽게도…").</p>
<p>대응: <code>separateThinkingFromContent</code>를 <code>thinking-separation.ts</code>로 추출 + 구조 기반 <code>stripEnglishScratchPrefix</code> 추가. 단위 테스트 6건 중 4건 실패 — 한글이 섞인 영어 계획 단락 허용하도록 수정 필요 (미해결).</p>
</div>
<footer>/srv/homeclaw · src/gateway/chat/ · src/gateway/guards/prompt-gates.ts</footer>
</div>