feat(rag): rag-v4 — RAG provenance 라벨 (source/trust + 신뢰도 우선 지시)
RAG 프롬프트의 각 [근거] 청크 머리에 출처/trust 라벨을 붙이고 (`[#n] source=jira trust=secondary doc=…`), system prompt 에 "저신뢰 출처를 권위 출처와 충돌 시 discount 하고 [#번호]로 귀속" 2규칙을 더한다. 출처 필터 (`--source`/`--trust-min`)가 못 잡는 생성 측 실패 — 저신뢰(jira) 청크가 권위(wiki) 청크를 답변에서 덮어쓰는 것 — 를 다룬다. - SearchHit 에 source_id/trust_level(additive optional). lexical/vector build_hit 가 documents 조인에서 채움(both 동일: trust_level lowercase TEXT → serde lowercase round-trip, doc_summary read-back 과 동형). hybrid fusion 전파. - pack_context 라벨 렌더(버전 무관 항상). SYSTEM_PROMPT_RAG_V4 = rag-v3 8규칙 verbatim + 2규칙. config 기본 rag-v3→rag-v4. multi-hop synth 도 2규칙 → rag-multi-hop-v1→v2(prompt 변경 = 버전 bump, design §9). - wire: search_hit.v1 에 두 필드 optional additive(required 아님, skip_serializing_if=None → 구 소비자 무영향, v2 bump 아님). - source_id 는 RAG 헤더에 렌더되므로 validate_sources 에 [A-Za-z0-9._-] char 검증. - opt-out: rag-v3 핀 = v3 system prompt 선택(discount 지시 빠짐, 라벨은 무해히 잔존). 검증: kebab-core/search/rag/config/eval 전 테스트 green(25 바이너리), clippy 0. 독립 코드 리뷰 APPROVE(7위험 PASS — trust round-trip 실 DB 확인; MEDIUM rag-v3 opt-out doc + multi-hop 버전 / LOW source_id 검증 반영). 도그푸딩: 라벨 메커니즘 end-to-end 검증(search --json 이 competing 쿼리에 wiki/primary + jira/secondary 둘 다 정확 라벨로 노출). LLM-judge(답변 비교)는 instruction LLM 부재로 보류(.2/.47 다운 + lemonade /api/generate it-model template 미적용) — 인프라, .2 복구 시 측정. 버전 bump 은 follow-up 들과 배치 릴리스에서 일괄. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
This commit is contained in:
@@ -940,7 +940,7 @@ impl Config {
|
||||
stale_threshold_days: 30,
|
||||
},
|
||||
rag: RagCfg {
|
||||
prompt_template_version: "rag-v3".to_string(),
|
||||
prompt_template_version: "rag-v4".to_string(),
|
||||
score_gate: 0.30,
|
||||
explain_default: false,
|
||||
max_context_tokens: 8000,
|
||||
@@ -1212,6 +1212,20 @@ impl Config {
|
||||
if s.id.trim().is_empty() {
|
||||
return Err("workspace.sources: an entry has an empty `id`".to_string());
|
||||
}
|
||||
// rag-provenance-label: source.id renders verbatim into the RAG
|
||||
// prompt context header (`[#n] source=<id> trust=… …`), where the
|
||||
// newline is the per-chunk line terminator and `[#n]` is the
|
||||
// citation grammar. Constrain ids to a safe set so a stray newline
|
||||
// / bracket can't split or spoof a chunk header. Also keeps the
|
||||
// `--source <id>` CLI filter ergonomics clean. (Single-user local
|
||||
// tool — config is self-authored, so this is defense-in-depth.)
|
||||
if !s.id.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) {
|
||||
return Err(format!(
|
||||
"workspace.sources: source id `{}` has invalid characters \
|
||||
(allowed: ASCII letters, digits, `.` `_` `-`)",
|
||||
s.id
|
||||
));
|
||||
}
|
||||
if s.root.trim().is_empty() {
|
||||
return Err(format!(
|
||||
"workspace.sources: source `{}` has an empty `root`",
|
||||
@@ -1844,9 +1858,9 @@ max_pixels = 1600
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_rag_prompt_template_version_is_rag_v3() {
|
||||
fn defaults_rag_prompt_template_version_is_rag_v4() {
|
||||
let c = Config::defaults();
|
||||
assert_eq!(c.rag.prompt_template_version, "rag-v3");
|
||||
assert_eq!(c.rag.prompt_template_version, "rag-v4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2454,6 +2468,24 @@ max_context_tokens = 8000
|
||||
assert!(cfg.validate_sources().is_err(), "empty id must fail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_id_with_invalid_chars_rejected() {
|
||||
// rag-provenance-label: source.id renders into the RAG prompt header;
|
||||
// a newline / bracket / space must be rejected at load.
|
||||
for bad in ["a b", "a\nb", "a]b", "a/b", "한글"] {
|
||||
let mut cfg = Config::defaults();
|
||||
cfg.workspace.sources = vec![source_cfg(bad, "/a")];
|
||||
assert!(
|
||||
cfg.validate_sources().is_err(),
|
||||
"source id {bad:?} must be rejected"
|
||||
);
|
||||
}
|
||||
// The allowed set still passes.
|
||||
let mut ok = Config::defaults();
|
||||
ok.workspace.sources = vec![source_cfg("wiki-v2.notes_1", "/a")];
|
||||
ok.validate_sources().expect("clean id must pass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_stale_threshold_is_30() {
|
||||
let c = Config::defaults();
|
||||
|
||||
Reference in New Issue
Block a user