문제
OpenCairn의 RAG는 그냥 "문서 몇 개 찾아서 LLM에 넣기"가 아닙니다.
OpenCairn의 기본 데이터 경계는 다음과 같습니다.
Workspace
-> Project
-> Page
-> Note / Block / File / Import / Comment
여기서 workspace가 isolation boundary입니다. 사용자가 어떤 note를 볼 수 없다면, 그 note는 검색 결과에도 나오면 안 되고, citation에도 나오면 안 되고, reranker의 후보에도 들어가면 안 됩니다.
naive RAG는 보통 이렇게 구현됩니다.
const chunks = await vectorSearch(queryEmbedding, { workspaceId });
const answer = await llm.generate({ query, context: chunks });
이 방식은 개인 프로젝트에서는 빠르게 동작하지만, 팀 지식 OS에서는 바로 문제가 됩니다.
- workspace 안에서도 project/page 단위 권한이 갈릴 수 있다.
- import된 파일이 아직 permission mapping을 완료하지 않았을 수 있다.
- stale chunk가 최신 note 상태와 달라질 수 있다.
- 모델에게 넘긴 context는 UI에서 숨길 수 없다.
그래서 OpenCairn에서는 검색을 "LLM 앞단 helper"가 아니라 permission-aware retrieval boundary로 취급합니다.
현재 코드의 중심
핵심 진입점은 apps/api/src/lib/chat-retrieval.ts입니다.
실제 파일에는 retrieve, retrieveWithPolicy, RagMode, RetrievalScope 같은 경계가 있고, 검색 후보는 permission helper를 통과합니다.
구조를 단순화하면 이렇습니다.
// apps/api/src/lib/chat-retrieval.ts
export type RagMode = "auto" | "focused" | "off";
export type RetrievalScope = {
workspaceId: string;
projectId?: string;
pageId?: string;
};
export async function retrieveWithPolicy(input: {
db: DbClient;
userId: string;
query: string;
scope: RetrievalScope;
mode: RagMode;
}) {
const candidates = await expandGraphCandidates(input);
const readable = [];
for (const candidate of candidates) {
if (await canRead(input.db, input.userId, candidate)) {
readable.push(candidate);
}
}
return rerankAndEvaluate(readable, input.query);
}
이 코드는 요약본입니다. 중요한 것은 vectorSearch -> prompt가 아니라:
query
-> scoped candidate expansion
-> permission filtering
-> rerank
-> quality/evidence evaluation
-> prompt context
순서가 된다는 점입니다.
왜 canRead가 검색 안에 들어가야 하나
권한 검사를 route handler에서만 하면 늦습니다.
예를 들어 사용자가 /api/chat에 접근할 권한은 있지만, 특정 project의 note는 읽을 권한이 없을 수 있습니다. 이때 route guard는 통과하지만 retrieval result는 제한되어야 합니다.
그래서 RAG 파이프라인은 다음 둘을 분리해야 합니다.
API를 사용할 수 있는가?
-> chat endpoint auth
이 자료를 LLM context로 사용할 수 있는가?
-> retrieval candidate permission
OpenCairn에서는 apps/api/src/lib/permissions.ts 계열 helper가 이 역할을 맡습니다. 코드에서 검색해보면 canReadNote, canReadProject, readableProjectsInWorkspace 같은 함수들이 RAG와 API route의 공통 경계로 쓰입니다.
단순화한 permission check는 이런 모양입니다.
async function canReadCandidate(db: DbClient, userId: string, candidate: Candidate) {
if (candidate.kind === "note") {
return canReadNote(db, userId, candidate.noteId);
}
if (candidate.kind === "project") {
return canReadProject(db, userId, candidate.projectId);
}
if (candidate.kind === "file") {
return canReadFile(db, userId, candidate.fileId);
}
return false;
}
핵심은 "검색 결과를 숨긴다"가 아니라 "처음부터 prompt context가 될 수 없는 자료를 제거한다"입니다.
graph expansion이 필요한 이유
문서 검색은 벡터 하나로 끝나지 않습니다. 사용자가 "저번 회의에서 나온 API quota 얘기"라고 물으면, 직접 chunk match보다 다음 연결이 더 중요할 수 있습니다.
현재 page
-> linked note
-> imported Google Doc
-> related file
-> comment thread
그래서 OpenCairn의 retrieval은 graph candidate expansion을 둡니다.
const candidates = await expandGraphCandidates({
workspaceId,
projectId,
pageId,
query,
});
이 단계는 검색 품질을 올리지만, 동시에 위험합니다. 링크를 따라가다가 사용자가 볼 수 없는 자료까지 후보로 들어올 수 있기 때문입니다. 그래서 expansion 다음에는 반드시 permission filter가 와야 합니다.
expand first
-> permission filter
-> rank
반대로 permission filter 없이 rank를 먼저 하면, LLM prompt에는 안 넣더라도 scoring/debug/log 단계에서 private data가 새어 나갈 여지가 생깁니다.
evidence quality
RAG 품질은 "검색 결과가 있냐"보다 "이 답을 해도 되냐"에 가깝습니다.
OpenCairn에서는 retrieval result를 answer context로 넘기기 전에 품질 판단을 둡니다.
type RetrievalQuality = {
hasEnoughEvidence: boolean;
confidence: "low" | "medium" | "high";
missingEvidenceReason?: string;
};
이 판단이 있어야 chat layer에서 다음 결정을 할 수 있습니다.
충분한 근거가 있음
-> grounded answer
근거가 약함
-> "현재 workspace 자료만으로는 확답하기 어렵다"
근거가 없음
-> 일반 답변 또는 follow-up question
AI 제품에서 중요한 것은 답을 항상 만들어내는 것이 아니라, 근거가 약할 때 약하다고 말하는 것입니다.
OpenCairn에서 RAG가 제품 기능이 되는 지점
OpenCairn의 RAG는 chat 답변만 위한 기능이 아닙니다.
같은 retrieval boundary가 다음 기능들로 확장됩니다.
Chat answer
-> citation
-> /factcheck
-> /cite
-> note.update preview
-> document generation sources
-> agentic plan evidence
즉 RAG는 하나의 endpoint가 아니라 OpenCairn의 AI 기능들이 공유하는 evidence layer입니다.
이 구조가 있으면 document generation agent도 같은 방식으로 source를 모으고, workflow planner도 같은 방식으로 fresh evidence를 확인할 수 있습니다.
구현하며 배운 점
가장 큰 교훈은 RAG를 retrieval algorithm으로만 보면 부족하다는 것입니다.
OpenCairn에서 RAG는 다음 조건을 동시에 만족해야 했습니다.
- permission boundary를 지킨다.
- graph 관계를 따라가되 private data를 노출하지 않는다.
- stale evidence를 구분한다.
- citation 가능한 단위로 context를 만든다.
- chat, note update, document generation, agentic plan이 재사용할 수 있다.
그래서 제가 생각하는 OpenCairn식 RAG의 정의는 이렇습니다.
RAG = search + permission + freshness + citation + recovery
이 중 하나라도 빠지면 실제 협업 제품에서는 문제가 됩니다.