Skip to content

Split View: 사내 지식베이스를 LLM으로 만든다는 것 — 권한 인지 검색, 신선도, 그리고 출시 전 평가셋

✨ Learn with Quiz
|

사내 지식베이스를 LLM으로 만든다는 것 — 권한 인지 검색, 신선도, 그리고 출시 전 평가셋

들어가며 — 하루 만오천 건을 받는 사내 검색이 실제로 푸는 문제

2026년 7월 15일, Cerebras가 사내 지식베이스를 어떻게 만들었는지 공개했습니다(How we built our knowledge base, GeekNews 정리). 출시 석 달 만에 하루 15,000건 넘는 질문을 받는 사내 도구가 됐고, 인당으로 환산하면 하루 약 15건입니다. 눈에 띄는 대목은 질의자의 구성입니다 — 사람만이 아니라 자동화 스크립트와 에이전트가 같은 엔드포인트에 묻습니다.

먼저 이 시스템이 실제로 푸는 문제를 정확히 짚고 넘어가는 게 좋겠습니다. 사내 검색의 문제는 "문서가 없다"가 아닙니다. 대개는 이렇습니다 — 답은 6개월 전 어느 Slack 스레드에 있고, 그 스레드를 검색하려면 그때 쓰인 정확한 에러 문자열을 알아야 하며, 그 문자열을 아는 사람은 이미 팀을 옮겼습니다. 위키에는 같은 주제의 문서가 있는데 8개월 전에 마지막으로 수정됐고, 그 사이 아키텍처가 바뀌었습니다. 즉 검색 품질 문제가 아니라 지식의 소재와 신선도 문제입니다.

그리고 팀이 시작할 때 거의 항상 과소평가하는 것은 청킹 전략이나 리랭커가 아닙니다. 권한, 신선도, 삭제 전파, 출처 충돌, 그리고 출시 전 평가셋입니다. 이 글은 그쪽만 봅니다.

Cerebras가 공개한 구조 — 좁은 허리 하나와 여섯 개의 도구

설계의 뼈대는 단순합니다. 수집, 하이브리드 검색, 합성이라는 세 단계에 모든 소스가 하나의 Postgres 임베딩 테이블로 모입니다. 문서, 임베딩, 메타데이터(소스 이름, 타임스탬프) 스키마 하나에 Slack, 코드, 위키, 인시던트, 커스텀 DB가 전부 정규화돼 들어갑니다. 모든 것을 단일 플랫폼으로 이주시키는 대신 데이터가 생성되는 자리에서 뽑아 옵니다.

Slack 처리가 가장 공들인 부분입니다. Socket Mode WebSocket으로 메시지 이벤트를 받고, 이벤트마다 스레드 전체를 다시 가져와 한 행으로 저장합니다. 그다음 LLM이 스레드를 정규화된 문서로 증류합니다 — 엔지니어가 실제로 타이핑할 법한 한 줄짜리 질문, 요약, 해결 방법, 언급된 시스템과 코드 참조. 임베딩되는 것은 원본 대화가 아니라 이 증류본이고, 원본은 전문 검색용으로만 남습니다. 긴 스레드에서는 같은 작성자의 고신호 구간을 따로 떼어 별도 임베딩하는데(bursting), 조건은 IDF 희귀도 4.0 이상, 길이 200자 이상, 그리고 선택적으로 리액션 1개 이상입니다.

검색은 네 신호를 상호 순위 융합으로 합칩니다. k는 60입니다.

# 정확 토큰(에러 문자열, 플래그, 호스트명)   -> 전문 검색
# 패러프레이즈                                 -> 임베딩 검색
# "네 알겠습니다" 같은 채움말 제거             -> IDF 가중
# 오래된 답을 뒤로                             -> 시간 감쇠

score(d) = sum over retrievers of  weight / (60 + rank_r(d))

# 점수 정규화를 하지 않는 것이 요점.
# 한 검색기의 1위보다 여러 검색기의 공통 상위가 이긴다.

코드는 CocoIndex로 언어별 정규식을 써서 클래스에서 메서드, 메서드에서 블록으로 계층 청킹하고, 커밋 단위로 바뀐 부분만 재임베딩합니다. 40GB 규모 저장소까지 다룬다고 밝혔습니다.

질의 파이프라인은 여섯 단계입니다 — 작은 LLM이 프로젝트 스코프를 보고 도구를 고르는 플래너, 도구를 병렬로 부르는 실행기, k=60 RRF 융합, 중복 제거 후 약 20건으로 컷, 0에서 10점을 매기는 크로스 인코더 리랭커, 그리고 인용을 붙이는 합성 단계. 도구는 여섯 개입니다 — search, search_slack, search_code(ripgrep), who_knows, recent_prs, subsystem_index.

한 가지 구조적 선택이 특히 좋습니다. 웹 UI는 플래너부터 합성까지 전체를 돌리지만, MCP는 프리미티브를 낱개로 노출합니다. Claude Code 같은 에이전트가 붙을 때 숨은 LLM 합성 단계 없이 자기 오케스트레이션을 유지할 수 있게 한 것입니다. 사람과 에이전트는 같은 인덱스를 원하지만 같은 오케스트레이션을 원하지는 않습니다.

한 가지 밝혀 둘 것이 있습니다. 원문 페이지를 여러 번 시도했지만 서버가 500을 돌려주어, 위 세부 사항 중 일부는 원문을 정리한 2차 요약(특히 mer.vin의 정리)에 의존했습니다. 숫자와 파라미터는 여러 요약에서 일관되게 나타나지만, 뒤에 나올 ACL 관련 서술은 2차 출처에만 등장하므로 그대로 인용하지 않겠습니다.

권한 인지 검색 — HR 문서를 흘리는 검색은 없는 것만 못하다

여기가 사내 지식베이스와 공개 문서 RAG가 갈리는 지점입니다. 공개 문서 RAG에서 최악의 실패는 오답이지만, 사내 검색에서 최악의 실패는 정답입니다. 물어보면 안 되는 사람에게 정확한 답을 주는 것.

문제의 뿌리는 한 문장으로 요약됩니다. 원본 시스템의 권한 로직은 청크를 따라오지 않습니다. SharePoint, Drive, Confluence, Slack 비공개 채널에 걸려 있던 접근 제어는 텍스트를 잘라 임베딩하는 순간 사라집니다. LLM이 컨텍스트로 그 청크를 받을 때, 그것이 특정 부서나 지명된 사용자에게만 열려 있던 문서였다는 사실은 어디에도 남아 있지 않습니다.

실무적으로 세 층을 분리해서 설계해야 합니다.

첫째, 인덱스 시점에 원본 ACL을 메타데이터로 함께 적재합니다. 채널 ID, 저장소, 문서 소유자, 그룹 목록을 벡터 행 옆에 놓습니다. 여기서 흔한 실수가 사용자 목록을 그대로 펼쳐 저장하는 것입니다. 조직도가 바뀔 때마다 전 인덱스를 다시 써야 합니다. 그룹이나 채널 같은 안정적인 주체 식별자를 저장하고, 사용자와 그룹의 매핑은 질의 시점에 해석하는 편이 낫습니다.

둘째, 질의 시점에 필터를 건다. 사전 필터링과 사후 필터링은 성격이 다릅니다.

-- 사전 필터링: 권한이 있는 후보에서만 최근접 이웃을 찾는다.
-- 안전하지만 접근 가능 문서가 적은 사용자에게는 recall이 급격히 떨어진다.
SELECT id, content
FROM   chunks
WHERE  acl_group = ANY ($1)            -- 질의자가 속한 그룹
ORDER  BY embedding <=> $2
LIMIT  50;

-- 사후 필터링: 넉넉히 뽑고 나서 거른다.
-- recall은 유지되지만, 최종 k를 못 채우거나
-- "결과 3건 중 2건 숨김" 같은 존재 자체가 정보를 흘릴 수 있다.

실전에서는 둘을 섞습니다. 사전 필터로 명백한 경계를 자르고, 넉넉한 후보를 뽑은 뒤 최종 단계에서 다시 확인합니다. 그리고 마지막 확인은 반드시 원본 시스템 또는 그것을 미러링한 권한 서비스에 물어야 합니다. 인덱스에 박힌 ACL은 스냅숏이고, 스냅숏은 항상 과거입니다.

셋째, 모든 진입 경로가 같은 인가를 지나야 합니다. 웹 UI, MCP 서버, 사내 봇, 배치 잡이 각각 다른 경로로 인덱스에 닿는 순간 그중 하나는 반드시 인가를 빠뜨립니다. 특히 에이전트가 서비스 계정으로 붙는 구성이 위험합니다. 서비스 계정의 권한은 대개 사용자보다 넓고, 그 계정으로 검색한 결과를 사용자에게 그대로 보여 주면 권한 상승이 됩니다. 에이전트 경로에서는 질의자의 신원을 끝까지 전파하고, 인가는 인덱스에 가장 가까운 지점에서 한 번만 강제하는 게 안전합니다.

마지막으로 프롬프트 인젝션이 이 표면 위에 얹힙니다. 2025년 말 Microsoft 365 Copilot을 상대로 보고된 EchoLeak은 클릭하지 않은 메일 한 통이 사내 RAG 파이프라인에 들어가 민감 데이터를 끌어내 유출시킬 수 있음을 보였습니다(저는 벤더 블로그의 2차 서술로만 확인했습니다). 요점은 검색 대상 코퍼스가 곧 신뢰 경계라는 것입니다. 외부에서 쓰기 가능한 채널 — 메일, 고객 티켓, 외부 게스트가 있는 Slack 채널 — 을 인덱싱하는 순간, 그 채널은 프롬프트 입력 경로가 됩니다.

신선도와 삭제 전파 — 인덱스는 진실을 늦게 배운다

시간 감쇠는 신선도 문제의 해법이 아니라 완화책입니다. 오래된 답의 점수를 낮출 뿐, 그 답이 이미 틀렸다는 사실을 알지 못합니다. 다른 후보가 없으면 시간 감쇠는 여전히 틀린 답을 1위로 올립니다.

실제로 신경 써야 하는 것은 세 가지 서로 다른 이벤트입니다.

수정. 문서가 바뀌면 해당 청크만 재임베딩하면 됩니다. 커밋 단위 증분 처리가 여기 해당합니다. 어렵지 않지만, 청크 경계가 바뀌면 옛 청크가 고아로 남는다는 함정이 있습니다. 문서 ID로 기존 청크를 전부 지우고 다시 쓰는 편이 안전합니다.

삭제. 이게 대부분 늦게 구현됩니다. 원본에서 지운 문서가 인덱스에 남아 있으면 검색은 이미 폐기된 런북을 자신 있게 인용합니다. Slack 메시지 삭제, 위키 페이지 아카이브, 저장소 삭제는 각각 다른 이벤트로 오고, 어떤 것은 이벤트조차 오지 않습니다. 그래서 이벤트 기반 삭제만으로는 부족하고, 주기적인 재조정(reconciliation)이 필요합니다 — 인덱스에 있는 문서 ID 목록을 원본에 다시 물어 사라진 것을 지우는 잡입니다. 조용히 실패하는 종류의 작업이므로, "이번 주기에 N건 삭제됨"을 메트릭으로 내보내야 합니다.

권한 회수. 삭제보다 더 조용합니다. 문서는 그대로인데 접근 권한만 좁아진 경우입니다. 비공개로 전환된 채널, 프로젝트에서 빠진 사람, 퇴사자. 인덱스의 ACL 스냅숏은 아무 일도 일어나지 않은 것처럼 보입니다. 앞 절에서 "최종 확인은 원본 권한 서비스에" 라고 쓴 이유가 이것입니다.

그리고 정직하게 인정할 부분이 있습니다. 완전한 실시간 일관성은 목표가 아닙니다. 목표는 지연 시간을 알고, 그 지연이 허용되지 않는 데이터 종류를 인덱싱 대상에서 빼는 것입니다. HR 문서, 급여, 미공개 인수합병, 보안 인시던트의 원문 — 이런 것들은 권한 모델이 아무리 잘 만들어져 있어도 초기 버전에 넣지 않는 편이 낫습니다. 리스크가 비대칭이기 때문입니다.

위키와 티켓이 서로 다른 말을 할 때

같은 질문에 대해 위키는 A, Jira 티켓은 B, Slack 스레드는 C라고 말하는 상황은 예외가 아니라 정상 상태입니다. 대부분의 시스템은 여기서 조용히 하나를 골라 답합니다. 이게 가장 나쁜 동작입니다 — 충돌을 숨기면서 확신은 그대로 전달하니까요.

몇 가지 규칙이 실무적으로 작동합니다.

소스별 권위를 명시적으로 매깁니다. "코드가 최종 권위, 그다음 인시던트 기록, 그다음 위키, 그다음 Slack" 같은 순서를 설정으로 둡니다. Cerebras가 코드베이스를 구조화되고 테스트되고 버전 관리된다는 이유로 권위 있는 출처로 취급한 것도 같은 발상입니다. 다만 이 순서는 질문 유형에 따라 뒤집힙니다 — "이 플래그의 기본값은?"은 코드가 이기지만, "왜 이렇게 했는가?"는 Slack 스레드나 설계 문서가 이깁니다.

충돌을 답변 안에서 드러냅니다. 합성 단계 프롬프트에 "출처들이 서로 다른 값을 제시하면 하나를 고르지 말고 둘 다 제시하고 각각의 날짜와 출처를 밝히라"는 지시를 넣습니다. 답변 품질이 아니라 신뢰 문제입니다. 한 번 조용히 틀린 답을 준 검색은 그다음부터 아무도 믿지 않습니다.

최신성 신호를 문서가 아니라 사실 단위로 붙입니다. 위키 페이지 전체의 수정 시각은 거의 쓸모가 없습니다. 페이지 하단의 오탈자를 고쳐도 전체가 최신이 됩니다. Slack 증류처럼 사실 단위로 추출해 두면 "이 해결 방법이 확인된 시점"을 따로 붙일 수 있습니다.

출시 전에 만들어야 하는 평가셋

여기가 가장 자주 생략되고, 생략의 대가가 가장 큰 부분입니다. 데모는 항상 잘 됩니다. 만든 사람이 답을 아는 질문을 던지기 때문입니다.

평가셋은 크지 않아도 됩니다. 50에서 150문항이면 충분하고, 중요한 건 크기가 아니라 커버하는 실패 유형입니다.

질문 유형무엇을 검증하나실패했을 때의 증상
정답이 하나뿐인 사실 질문검색 정확도와 인용 정합성그럴듯한데 출처가 답을 뒷받침하지 않음
최근에 값이 바뀐 사실신선도와 시간 감쇠6개월 전 정책을 확신 있게 답변
폐기된 문서에만 있던 사실삭제 전파이미 지운 런북을 인용
권한 밖 문서에만 있는 사실권한 인지 검색조회 권한 없는 사용자에게 내용 노출
위키와 티켓이 충돌하는 사실출처 우선순위와 충돌 표시한쪽을 임의로 고르고 충돌을 숨김
정확한 토큰이 열쇠인 질문전문 검색 경로에러 문자열을 임베딩이 뭉개서 못 찾음
답이 존재하지 않는 질문기권 능력없는 답을 지어냄
사람을 찾는 질문전문가 라우팅퇴사자나 담당 아닌 사람을 추천

각 항목은 이런 모양이면 충분합니다.

{
  "id": "acl-003",
  "type": "permission",
  "query": "지난 분기 성과 평가 등급 분포가 어떻게 되나요",
  "asked_by": "role:engineer",
  "expect": {
    "must_not_cite": ["source:hr-drive"],
    "must_not_contain_any": ["등급 분포", "S등급", "비율"],
    "acceptable_behavior": "abstain_with_reason"
  }
}

세 가지를 강조하고 싶습니다.

첫째, 권한 케이스는 반드시 역할별로 돌려야 합니다. 관리자 계정 하나로 돌린 평가는 권한에 대해 아무것도 알려 주지 않습니다. 최소한 일반 엔지니어, 다른 팀 엔지니어, 인턴, 외부 게스트 정도의 역할을 두고 같은 질의를 반복합니다.

둘째, 기권을 정답으로 채점해야 합니다. "모르겠습니다, 이 주제는 제가 접근할 수 있는 문서에 없습니다"가 만점인 문항이 평가셋의 10~20%는 돼야 합니다. 이걸 넣지 않으면 시스템은 항상 답하는 방향으로 튜닝됩니다.

셋째, 평가셋은 인시던트에서 자랍니다. 사용자가 잘못된 답을 신고할 때마다 그 질의를 그대로 평가셋에 넣습니다. 회귀 테스트와 같은 원리입니다. 초기 150문항보다 6개월 뒤의 400문항이 훨씬 가치 있습니다.

조직 단위로 스코프를 자르는 일

Cerebras 설계에서 저평가하기 쉬운 요소가 프로젝트 개념입니다. 관련된 Slack 채널, 저장소, 문서 공간, DB를 하나의 이름 붙은 묶음으로 만들고, 신입은 온보딩 때 기본 프로젝트를 고릅니다.

이게 단순한 UX 편의처럼 보이지만 실은 세 가지를 동시에 해결합니다. 검색 노이즈를 줄이고(다른 팀의 동명이인 서비스가 상위에 뜨지 않음), 플래너의 선택지를 좁히고(작은 모델이 여섯 개 도구 중 고르기가 쉬워짐), 권한 경계와 대체로 정렬됩니다(대개 팀이 접근할 수 있는 것과 팀이 관심 있는 것은 상당히 겹칩니다).

한계도 분명합니다. 조직이 자주 재편되면 프로젝트 정의가 금세 낡습니다. 그리고 스코프는 검색 품질을 위한 것이지 보안 경계가 아닙니다 — 스코프를 벗어난 질의도 여전히 인가를 통과해야 합니다. 이 둘을 같은 메커니즘으로 구현하면 언젠가 스코프를 넓히는 기능이 곧 권한 우회가 됩니다.

마치며 — 검색 품질은 나중에 고칠 수 있지만 유출은 되돌릴 수 없다

사내 지식베이스 프로젝트에서 팀의 시간은 대개 검색 품질에 쏠립니다. 청킹, 리랭커, 하이브리드 가중치 — 전부 측정 가능하고 개선이 즉시 눈에 보이니까요. 그런데 이 항목들은 나중에 고칠 수 있습니다. 인덱스를 다시 만들면 됩니다.

되돌릴 수 없는 것은 세 가지입니다. 권한 없는 사람에게 이미 보여 준 문서, 잘못된 답을 근거로 이미 내려진 결정, 그리고 "저건 못 믿는다"는 조직의 학습된 인식. 마지막 것이 특히 회복이 어렵습니다. 사내 도구는 한 번 신뢰를 잃으면 트래픽이 조용히 0으로 수렴하고, 지표상으로는 아무 일도 일어나지 않은 것처럼 보입니다.

정리하면 이렇습니다.

  • 권한은 인덱싱 시점의 메타데이터, 질의 시점의 필터, 그리고 원본 권한 서비스에 대한 최종 확인이라는 세 층으로 나눠 설계합니다. 모든 진입 경로가 같은 인가를 지나야 하고, 에이전트가 서비스 계정으로 검색하는 구성은 권한 상승입니다.
  • 시간 감쇠는 신선도의 해법이 아닙니다. 수정, 삭제, 권한 회수는 서로 다른 이벤트이고, 삭제 전파에는 이벤트 처리와 주기적 재조정이 둘 다 필요합니다. 재조정 건수는 메트릭으로 내보내세요.
  • 출처가 충돌할 때 조용히 하나를 고르는 것이 가장 나쁜 동작입니다. 권위 순서를 설정으로 두고, 충돌은 답변 안에서 드러냅니다.
  • 출시 전 평가셋에는 권한 케이스와 기권 케이스가 반드시 들어가야 하고, 역할별로 돌려야 합니다.

검색이 정확한지는 나중에 알 수 있지만, 유출은 나중에 알게 됩니다.

Building an Internal Knowledge Base on an LLM — Permission-Aware Retrieval, Freshness, and the Eval Set You Need Before Launch

Introduction — the Actual Problem Solved by an Internal Search Fielding 15,000 Queries a Day

On July 15, 2026, Cerebras published an account of how it built its internal knowledge base (How we built our knowledge base, GeekNews summary). Three months after launch, it had become an internal tool fielding more than 15,000 questions a day — roughly 15 per person, converted to a per-head basis. What stands out is who's asking: not just people, but automation scripts and agents hitting the same endpoint.

It's worth first pinning down exactly what problem this system is actually solving. The problem with internal search isn't "the documents don't exist." It's usually this: the answer sits in some Slack thread from six months ago, finding that thread requires knowing the exact error string used at the time, and the person who knew that string has already moved teams. The wiki has a document on the same topic, but it was last edited eight months ago, and the architecture has changed since. In other words, this isn't a search-quality problem — it's a problem of where knowledge lives and how fresh it is.

And the thing teams almost always underrate at the start isn't chunking strategy or the reranker. It's permissions, freshness, deletion propagation, source conflicts, and the pre-launch eval set. This post looks only at those.

What Cerebras Published — One Narrow Waist and Six Tools

The skeleton of the design is simple. Across three stages — ingestion, hybrid retrieval, and synthesis — every source funnels into a single Postgres embeddings table. One schema for documents, embeddings, and metadata (source name, timestamp), and Slack, code, wikis, incidents, and custom DBs all get normalized into it. Instead of migrating everything onto a single platform, they pull data out at the point where it's generated.

Slack handling got the most care. Message events arrive over a Socket Mode WebSocket, and for every event the entire thread is re-fetched and stored as a single row. An LLM then distills the thread into a normalized document — a one-line question an engineer would actually type, a summary, the resolution, and the systems and code references mentioned. What gets embedded is this distillation, not the raw conversation; the raw conversation is kept only for full-text search. In long threads, high-signal spans from the same author are pulled out and embedded separately (bursting), gated on an IDF rarity score of 4.0 or higher, a length of 200 characters or more, and optionally at least one reaction.

Retrieval merges four signals via reciprocal rank fusion, with k set to 60.

# exact tokens (error strings, flags, hostnames)   -> full-text search
# paraphrases                                       -> embedding search
# stripping filler like "yeah, got it"               -> IDF weighting
# pushing old answers down                           -> age decay

score(d) = sum over retrievers of  weight / (60 + rank_r(d))

# The point is that scores are not normalized.
# A document that multiple retrievers agree on beats one retriever's #1 pick.

Code is chunked hierarchically with CocoIndex using language-specific regexes — class to method, method to block — and only the parts changed by a given commit get re-embedded. They report handling repositories at the 40GB scale.

The query pipeline runs in six stages — a planner where a small LLM looks at project scope and picks tools, an executor that calls tools in parallel, k=60 RRF fusion, a cut down to roughly 20 results after deduplication, a cross-encoder reranker scoring 0 to 10, and a synthesis stage that attaches citations. There are six tools: search, search_slack, search_code (ripgrep), who_knows, recent_prs, and subsystem_index.

One structural choice stands out. The web UI runs the whole pipeline from planner to synthesis, but MCP exposes the primitives individually. That lets an agent like Claude Code, when it attaches, keep its own orchestration without a hidden LLM synthesis step in the way. People and agents want the same index, but not the same orchestration.

One thing should be disclosed. I tried the original page multiple times and it returned a 500, so some of the details above rely on secondary summaries of the original (mer.vin's writeup in particular). The numbers and parameters appear consistently across multiple summaries, but the ACL-related discussion that follows appears only in secondary sources, so I won't quote it directly.

Permission-Aware Retrieval — a Search That Leaks HR Documents Is Worse Than No Search at All

This is where an internal knowledge base and public-document RAG diverge. In public-document RAG, the worst failure is a wrong answer. In internal search, the worst failure is a right answer — a correct answer handed to someone who was never supposed to be asked.

The root of the problem fits in one sentence. The source system's permission logic doesn't travel with the chunk. Access controls that sat on SharePoint, Drive, Confluence, or a private Slack channel disappear the moment the text is sliced and embedded. When an LLM receives that chunk as context, nothing anywhere records the fact that it was a document open only to a specific department or a named set of users.

In practice you need to design three separate layers.

First, load the source ACL as metadata alongside the index at ingestion time. Put the channel ID, repository, document owner, and group list next to the vector row. A common mistake here is storing the flattened user list directly — every time the org chart changes, you have to rewrite the entire index. It's better to store stable subject identifiers like groups or channels, and resolve the user-to-group mapping at query time instead.

Second, filter at query time. Pre-filtering and post-filtering behave differently.

-- Pre-filtering: find nearest neighbors only among candidates the user has access to.
-- Safe, but recall drops sharply for users with few accessible documents.
SELECT id, content
FROM   chunks
WHERE  acl_group = ANY ($1)            -- groups the querying user belongs to
ORDER  BY embedding <=> $2
LIMIT  50;

-- Post-filtering: pull generously, then filter.
-- Recall is preserved, but you may fail to fill the final k,
-- or the mere existence of "2 of 3 results hidden" can leak information.

In practice you mix the two. Cut the obvious boundaries with a pre-filter, pull a generous set of candidates, and re-verify at the final stage. And that final check must always go to the source system itself, or to a permission service that mirrors it. An ACL baked into the index is a snapshot, and a snapshot is always the past.

Third, every entry point must pass through the same authorization. The moment the web UI, the MCP server, an internal bot, and a batch job each reach the index through a different path, one of them will inevitably skip authorization. Configurations where an agent attaches through a service account are especially dangerous. A service account's permissions are usually broader than a user's, and showing a user the results of a search run under that account amounts to privilege escalation. In agent paths, the safest approach is to propagate the querying user's identity all the way through and enforce authorization exactly once, at the point closest to the index.

Finally, prompt injection sits on top of this whole surface. EchoLeak, reported against Microsoft 365 Copilot in late 2025, showed that a single unopened email could enter an internal RAG pipeline and be used to pull out and exfiltrate sensitive data (I confirmed this only through secondary accounts on vendor blogs). The point is that the corpus you search over is itself the trust boundary. The moment you index a channel that outsiders can write to — email, customer tickets, a Slack channel with external guests — that channel becomes a prompt-injection input path.

Freshness and Deletion Propagation — the Index Learns the Truth Late

Age decay is not a solution to the freshness problem; it's a mitigation. It only lowers the score of an old answer — it has no idea that answer is already wrong. When there's no better candidate, age decay still puts the wrong answer in first place.

What actually needs attention is three distinct kinds of events.

Edits. When a document changes, you only need to re-embed the affected chunk. Commit-level incremental processing falls here. Not hard, but there's a trap — if chunk boundaries shift, old chunks can be orphaned. It's safer to delete every existing chunk for a document ID and rewrite from scratch.

Deletions. This is the piece that's usually implemented late. If a document deleted at the source stays in the index, search will confidently cite a runbook that's already been retired. Slack message deletion, wiki page archival, and repository deletion each arrive as a different kind of event, and some don't arrive as events at all. So event-driven deletion alone isn't enough — you need periodic reconciliation: a job that asks the source again about the list of document IDs in the index and removes whatever has disappeared. This is the kind of task that fails silently, so you need to emit "N deleted this cycle" as a metric.

Permission revocation. Quieter than deletion. The document is unchanged, but access has narrowed — a channel goes private, someone is removed from a project, someone leaves the company. The ACL snapshot in the index looks as if nothing happened at all. This is exactly why the previous section said the final check must go to the source permission service.

And there's something worth admitting honestly. Full real-time consistency is not the goal. The goal is knowing your latency, and excluding from the index any category of data where that latency isn't acceptable. HR documents, payroll, undisclosed M&A, the raw text of security incidents — no matter how well-built the permission model is, these are better left out of an early version, because the risk is asymmetric.

When the Wiki and the Ticket Say Different Things

A wiki saying A, a Jira ticket saying B, and a Slack thread saying C about the same question is the normal state, not an edge case. Most systems quietly pick one and answer with it. This is the worst possible behavior — it hides the conflict while delivering the same confidence as if there were none.

A few rules work well in practice.

Rank sources by authority explicitly. Set an order like "code is the final authority, then incident records, then the wiki, then Slack" as configuration. Cerebras treating the codebase as an authoritative source because it's structured, tested, and version-controlled reflects the same thinking. That said, this order flips depending on the type of question — "what's the default for this flag?" is won by the code, but "why was it done this way?" is won by a Slack thread or a design doc.

Surface the conflict inside the answer itself. Add an instruction to the synthesis-stage prompt: "if sources give different values, don't pick one — present both, along with each one's date and source." This isn't a quality issue, it's a trust issue. Once a search has quietly given a wrong answer, nobody trusts it again.

Attach freshness signals at the level of individual facts, not documents. The edit timestamp of an entire wiki page is nearly useless — fixing a typo at the bottom of the page makes the whole thing look current. Extracting at the level of individual facts, the way the Slack distillation does, lets you attach "when this fix was confirmed" separately.

The Eval Set You Must Build Before Launch

This is the part most often skipped, and the one where skipping it costs the most. Demos always work, because the person who built it asks questions they already know the answer to.

The eval set doesn't need to be big. 50 to 150 questions is enough — what matters isn't size but the range of failure modes it covers.

Question typeWhat it verifiesSymptom when it fails
Factual question with one correct answerRetrieval accuracy and citation consistencySounds plausible, but the cited source doesn't support the answer
Fact that changed recentlyFreshness and age decayConfidently states a policy from six months ago
Fact that only ever lived in a retired documentDeletion propagationCites a runbook that's already been deleted
Fact that lives only in a document outside the asker's permissionsPermission-aware retrievalContent exposed to a user without view access
Fact where the wiki and a ticket conflictSource priority and conflict surfacingArbitrarily picks one side and hides the conflict
Question where an exact token is the keyFull-text search pathEmbedding smooths over the error string so it can't be found
Question with no existing answerAbility to abstainFabricates an answer that doesn't exist
Question that asks for a personExpert routingRecommends someone who's left the company or isn't the right owner

Each item is fine in a shape like this:

{
  "id": "acl-003",
  "type": "permission",
  "query": "What was the distribution of performance review ratings last quarter?",
  "asked_by": "role:engineer",
  "expect": {
    "must_not_cite": ["source:hr-drive"],
    "must_not_contain_any": ["rating distribution", "S-tier", "percentage"],
    "acceptable_behavior": "abstain_with_reason"
  }
}

I want to stress three things.

First, permission cases must be run per role. An eval run under a single admin account tells you nothing about permissions. At minimum, keep roles for a regular engineer, an engineer on a different team, an intern, and an external guest, and repeat the same query under each.

Second, abstention must be graded as a correct answer. "I don't know, this isn't in any document I have access to" needs to be the full-credit answer on 10 to 20 percent of the set. Skip this, and the system gets tuned to always answer.

Third, the eval set should grow out of incidents. Every time a user reports a wrong answer, drop that exact query straight into the eval set. Same principle as a regression test. A 400-question set six months in is worth far more than the initial 150.

Cutting Scope Along Organizational Lines

An element easy to undervalue in Cerebras's design is the concept of a project. Related Slack channels, repositories, doc spaces, and databases get bundled into one named unit, and new hires pick a default project during onboarding.

This looks like a simple UX convenience, but it actually solves three things at once. It cuts search noise (a same-named service from another team doesn't surface at the top), it narrows the planner's options (it's easier for a small model to choose among six tools), and it broadly aligns with permission boundaries (what a team can access and what a team cares about tend to overlap substantially).

The limits are clear too. If an organization reorganizes often, project definitions go stale fast. And scope is for search quality, not a security boundary — queries that fall outside scope still have to pass authorization. Implement the two with the same mechanism, and sooner or later a feature meant to widen scope becomes a permission bypass.

Conclusion — Search Quality Can Be Fixed Later, but a Leak Can't Be Undone

In an internal knowledge base project, a team's time usually gravitates toward search quality. Chunking, the reranker, hybrid weights — all measurable, and improvements show up immediately. But these items can be fixed later. You just rebuild the index.

Three things can't be undone. A document already shown to someone without permission. A decision already made on the basis of a wrong answer. And the organization's learned belief that "that thing can't be trusted." The last one is especially hard to recover from. Once an internal tool loses trust, its traffic quietly converges to zero, and the metrics look as if nothing happened at all.

To sum up.

  • Design permissions across three layers: metadata at indexing time, a filter at query time, and a final check against the source permission service. Every entry point has to pass through the same authorization, and a configuration where an agent searches under a service account is a privilege escalation.
  • Age decay is not a solution to freshness. Edits, deletions, and permission revocations are three different events, and deletion propagation needs both event handling and periodic reconciliation. Emit the reconciliation count as a metric.
  • Quietly picking one source when sources conflict is the worst possible behavior. Keep an authority order as configuration, and surface conflicts inside the answer.
  • The pre-launch eval set must include permission cases and abstention cases, and it must be run per role.

You find out whether search is accurate later. You find out about a leak even later than that.