feat(ask): Phase 3.5a guardrails (classifier + refusal gate + grounding + partial)
신규 파일: - classifier_service.py: exaone binary classifier (sufficient/insufficient) parallel with evidence, circuit breaker, timeout 5s - refusal_gate.py: multi-signal fusion (score + classifier) AND 조건, conservative fallback 3-tier (classifier 부재 시) - grounding_check.py: strong/weak flag 분리 strong: fabricated_number + intent_misalignment(important keywords) weak: uncited_claim + low_overlap + intent_misalignment(generic) re-gate: 2+ strong → refuse, 1 strong → partial - sentence_splitter.py: regex 기반 (Phase 3.5b KSS 업그레이드) - classifier.txt: exaone Y+ prompt (calibration examples 포함) - search_synthesis_partial.txt: partial answer 전용 프롬프트 - 102_ask_events.sql: /ask 관측 테이블 (completeness 3-분리 지표) - queries.yaml: Phase 3.5 smoke test 평가셋 10개 수정 파일: - search.py /ask: classifier parallel + refusal gate + grounding re-gate + defense_layers 로깅 + AskResponse completeness/aspects/confirmed_items - config.yaml: classifier model 섹션 (exaone3.5:7.8b GPU Ollama) - config.py: classifier optional 파싱 - AskAnswer.svelte: 4분기 렌더 (full/partial/insufficient/loading) - ask.ts: Completeness + ConfirmedItem 타입 P1 실측: exaone ternary 불안정 → binary gate 축소. partial은 grounding이 담당. 토론 9라운드 확정. plan: quiet-meandering-nova.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,10 +63,14 @@
|
||||
};
|
||||
|
||||
let tokens = $derived(data?.ai_answer ? splitAnswer(data.ai_answer) : []);
|
||||
let showAnswer = $derived(
|
||||
!!data && !!data.ai_answer && data.synthesis_status === 'completed' && !data.refused,
|
||||
let showFullAnswer = $derived(
|
||||
!!data && !!data.ai_answer && data.completeness === 'full'
|
||||
&& data.synthesis_status === 'completed' && !data.refused,
|
||||
);
|
||||
let showWarning = $derived(!!data && !showAnswer);
|
||||
let showPartial = $derived(
|
||||
!!data && data.completeness === 'partial' && !data.refused,
|
||||
);
|
||||
let showWarning = $derived(!!data && !showFullAnswer && !showPartial);
|
||||
</script>
|
||||
|
||||
<section class="bg-surface border border-default rounded-card p-5">
|
||||
@@ -107,7 +111,7 @@
|
||||
근거 기반 답변 생성 중… 약 15초 소요
|
||||
</p>
|
||||
</div>
|
||||
{:else if showAnswer && data}
|
||||
{:else if showFullAnswer && data}
|
||||
<div class="text-sm leading-7 text-text">
|
||||
{#each tokens as tok}
|
||||
{#if tok.type === 'cite'}
|
||||
@@ -124,6 +128,67 @@
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{:else if showPartial && data}
|
||||
<!-- Phase 3.5a: question-aligned partial structure -->
|
||||
<div>
|
||||
<Badge tone="warning" size="sm">일부 답변</Badge>
|
||||
|
||||
{#if data.ai_answer}
|
||||
<div class="mt-3 text-sm leading-7 text-text">
|
||||
{#each tokens as tok}
|
||||
{#if tok.type === 'cite'}
|
||||
<button
|
||||
type="button"
|
||||
class="inline-block align-baseline text-accent font-semibold hover:underline rounded px-0.5"
|
||||
onclick={() => onCitationClick(tok.n)}
|
||||
>{tok.raw}</button>
|
||||
{:else}
|
||||
<span>{tok.value}</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{:else if data.confirmed_items?.length}
|
||||
<div class="mt-3">
|
||||
<h4 class="text-xs font-semibold text-dim uppercase tracking-wider">✓ 답변 가능</h4>
|
||||
<ul class="mt-2 space-y-2">
|
||||
{#each data.confirmed_items as item}
|
||||
<li class="text-sm text-text">
|
||||
<strong class="text-accent">{item.aspect}:</strong>
|
||||
<span>{item.text}</span>
|
||||
{#each item.citations as n}
|
||||
<button
|
||||
type="button"
|
||||
class="text-accent font-semibold hover:underline px-0.5"
|
||||
onclick={() => onCitationClick(n)}
|
||||
>[{n}]</button>
|
||||
{/each}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if data.missing_aspects?.length}
|
||||
<div class="mt-4 border-t border-default pt-3">
|
||||
<h4 class="text-xs font-semibold text-dim uppercase tracking-wider">✗ 답변 불가</h4>
|
||||
<ul class="mt-2 space-y-1">
|
||||
{#each data.missing_aspects as aspect}
|
||||
<li class="text-sm text-dim">{aspect} <span class="text-[10px]">(근거 없음)</span></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
href={`/documents?q=${encodeURIComponent(data.query)}`}
|
||||
>
|
||||
검색 결과 확인하기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if showWarning && data}
|
||||
<EmptyState
|
||||
icon={AlertTriangle}
|
||||
|
||||
@@ -50,6 +50,14 @@ export interface SearchResult {
|
||||
rerank_score: number | null;
|
||||
}
|
||||
|
||||
export type Completeness = 'full' | 'partial' | 'insufficient';
|
||||
|
||||
export interface ConfirmedItem {
|
||||
aspect: string;
|
||||
text: string;
|
||||
citations: number[];
|
||||
}
|
||||
|
||||
export interface AskResponse {
|
||||
results: SearchResult[];
|
||||
ai_answer: string | null;
|
||||
@@ -61,4 +69,9 @@ export interface AskResponse {
|
||||
no_results_reason: string | null;
|
||||
query: string;
|
||||
total: number;
|
||||
/** Phase 3.5a */
|
||||
completeness: Completeness;
|
||||
covered_aspects: string[] | null;
|
||||
missing_aspects: string[] | null;
|
||||
confirmed_items: ConfirmedItem[] | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user