블로그로 돌아가기

LLM 위키 시스템

2026-05-09sungblab

문제

OpenCairn을 처음 만들 때는 "내 자료를 잘 찾아주는 RAG"가 중심이었습니다.

그런데 자료가 많아질수록 단순 검색만으로는 부족했습니다. 사용자는 어떤 용어가 어디서 나왔는지, 비슷한 개념이 이미 있는지, 서로 연결된 note가 무엇인지 알고 싶어집니다.

그래서 OpenCairn 안에는 RAG와 별개로 LLM wiki system이 있습니다.

source note
-> Compiler Agent
-> concepts
-> concept_notes
-> concept_edges
-> wiki_logs
-> evidence bundles

여기서 중요한 점은 wiki system이 LLM의 자유로운 요약문 모음이 아니라는 것입니다. OpenCairn은 concept graph와 audit log를 DB에 명시적으로 남기려고 합니다.

source note에서 시작한다

ingest workflow는 원본 파일을 파싱한 뒤 source note를 만듭니다.

apps/worker/src/worker/activities/note_activity.pycreate_source_note activity는 internal API에 다음 성격의 payload를 보냅니다.

userId
projectId
title
content
sourceType
objectKey
sourceUrl
treeParentNodeId
triggerCompiler: true

여기서 triggerCompiler: true가 LLM wiki system으로 넘어가는 문입니다.

API 쪽 /api/internal/source-notes는 source note를 저장하고, compiler workflow를 시작합니다.

source note created
-> CompilerWorkflow workflowId = compiler-{noteId}
-> compile_note activity
-> CompilerAgent

CompilerWorkflow가 semaphore를 잡는 이유

apps/worker/src/worker/workflows/compiler_workflow.py를 보면 CompilerWorkflow는 바로 agent를 실행하지 않습니다. 먼저 project semaphore를 잡습니다.

CompilerWorkflow
-> acquire_project_semaphore(project, purpose="compiler")
-> compile_note activity
-> release_project_semaphore

이 구조가 필요한 이유는 같은 프로젝트에 여러 source note가 동시에 들어올 수 있기 때문입니다. LLM이 concept를 만들고 merge하는 동안 같은 project concept graph를 동시에 건드리면 중복 concept나 이상한 link가 생길 수 있습니다.

그래서 compiler는 프로젝트 단위로 조심스럽게 직렬화합니다. 느려 보일 수 있지만, 지식 그래프를 만드는 작업에서는 이 편이 더 안전합니다.

CompilerAgent가 하는 일

apps/worker/src/worker/agents/compiler/agent.py의 주석이 이 agent의 역할을 잘 설명합니다.

Compiler는 source note를 읽고, LLM으로 named concept를 추출하고, 기존 concept와 dedupe한 뒤, note와 concept를 연결하고, wiki log를 남깁니다.

흐름은 대략 이렇습니다.

fetch source note
-> LLM extracts concepts
-> embed extracted concept texts
-> search existing concepts
-> merge if similarity is high enough
-> upsert concept
-> link concept to note
-> write wiki_logs
-> emit agent events

여기서 LLM output이 바로 DB write가 되는 것은 아닙니다. Compiler는 API client를 통해 internal Hono route를 호출합니다. worker가 Postgres를 직접 만지지 않고 API boundary를 지키는 구조입니다.

concept graph의 테이블

wiki system의 중심 DB schema는 packages/db/src/schema/concepts.ts입니다.

concepts
-> id
-> projectId
-> name
-> description
-> embedding

concept_notes
-> conceptId
-> noteId

concept_edges
-> sourceId
-> targetId
-> relationType
-> weight
-> evidenceNoteId

concepts는 wiki에서 말하는 개념 노드입니다. concept_notes는 어떤 note가 어떤 concept의 근거인지 연결합니다. concept_edges는 concept 사이의 관계입니다.

이 구조 덕분에 RAG는 나중에 vector search만 하는 것이 아니라 graph expansion도 할 수 있습니다.

seed note
-> concept_notes
-> concepts
-> concept_edges
-> neighboring concepts
-> related notes

merge threshold와 중복 문제

CompilerAgent에는 MERGE_SIMILARITY_THRESHOLD = 0.88이 있습니다.

LLM이 추출한 개념이 이미 프로젝트에 있는 개념과 너무 비슷하면 새로 만들지 않고 기존 concept에 merge/link합니다.

이 부분은 작아 보이지만 중요합니다. LLM은 같은 것을 조금씩 다른 이름으로 자주 뽑습니다.

"Attention Mechanism"
"Self Attention"
"Transformer Attention"

이런 후보를 무조건 새 concept로 만들면 graph가 금방 지저분해집니다. 반대로 너무 공격적으로 merge하면 다른 개념이 하나로 뭉개집니다.

그래서 이 threshold는 완벽한 정답이라기보다 운영하면서 조정해야 하는 제품 감각에 가깝습니다.

evidence bundle을 남기는 이유

Compiler는 concept를 만들고 끝내지 않습니다. 현재 compile_note activity에는 concept extraction evidence를 best-effort로 기록하는 경로도 있습니다.

대략 이런 흐름입니다.

Compiler output concept_ids
-> list source note chunks
-> create evidence bundle
-> record concept extraction evidence
-> record relation claims

이게 필요한 이유는 나중에 "이 concept가 왜 생겼지?"라는 질문에 답해야 하기 때문입니다.

LLM이 concept를 뽑았다는 사실만으로는 부족합니다. 어떤 source note chunk를 근거로 뽑았는지, 어떤 run이 만들었는지, 어떤 quote를 봤는지 남겨야 합니다.

저는 이 부분이 LLM wiki system에서 특히 중요하다고 봅니다. AI가 만든 지식 구조는 편리하지만, 근거가 없으면 유지보수가 어렵습니다.

wiki_links는 concept graph와 다르다

OpenCairn에는 wiki_links도 있습니다. 이것은 Compiler가 만든 concept graph와 다른 층입니다.

wiki_links는 editor 안의 wiki-link Plate node에서 나오는 명시적 노트 링크입니다.

note body contains wiki-link node
-> Hocuspocus/Yjs persistence sync
-> wiki_links row
-> backlinks query

반면 concept graph는 LLM Compiler가 source note에서 추출한 의미 그래프입니다.

LLM extracts concept
-> concepts / concept_notes / concept_edges

둘 다 "wiki"라는 말로 부를 수 있지만 용도가 다릅니다.

wiki_links
-> 사용자가 노트 안에서 명시적으로 연결한 graph

concept graph
-> LLM이 source note에서 추출한 semantic graph

이 둘을 섞어버리면 제품이 헷갈립니다. 사용자가 직접 건 링크와 모델이 추론한 개념 관계는 신뢰 수준이 다르기 때문입니다.

Librarian과 Curator

Compiler가 새 source note를 graph에 편입하는 역할이라면, Librarian/Curator는 나중에 graph 품질을 보는 축입니다.

예를 들어 Curator agent는 orphan concept, duplicate concept, contradiction 후보를 찾습니다. Librarian은 project knowledge health를 관리하는 방향으로 이어집니다.

Compiler
-> source note에서 concept를 뽑고 graph에 편입

Librarian / Curator
-> 만들어진 knowledge graph의 품질과 유지보수 확인

이렇게 역할을 나눠야 ingest path가 너무 무거워지지 않습니다. 업로드 직후에는 source note와 concept extraction이 중요하고, 전체 graph 청소는 별도 maintenance workflow로 돌리는 편이 맞습니다.

RAG와 만나는 지점

LLM wiki system은 RAG와 분리된 기능이지만, 검색 품질에는 직접 영향을 줍니다.

retrieval-graph-expansion.ts는 concept graph를 따라 related note/chunk 후보를 찾습니다.

RAG seed hit
-> concept graph expansion
-> related note chunks
-> permission filter
-> rerank/context

즉 Compiler가 만든 concept graph는 나중에 RAG의 graph channel이 됩니다. source note에서 concept를 잘 뽑지 못하면 graph expansion도 약해집니다.

그래서 ingest, Compiler, RAG는 따로 떨어진 기능이 아닙니다.

parse quality
-> source note quality
-> concept extraction quality
-> graph expansion quality
-> RAG answer quality

구현하며 배운 점

LLM wiki system을 만들면서 느낀 점은, "위키"라는 말이 생각보다 여러 층을 포함한다는 것입니다.

OpenCairn에서는 최소한 이 층들을 분리해서 봐야 했습니다.

  • source note: 원본 자료에서 나온 본문
  • note chunk: RAG가 검색하는 근거 단위
  • wiki link: 사용자가 노트 안에서 명시적으로 건 링크
  • concept: LLM이 추출한 의미 단위
  • concept edge: concept 사이의 관계
  • wiki log: 어떤 agent가 무엇을 만들었는지 남기는 감사 기록
  • evidence bundle: concept나 relation claim이 어떤 source chunk에 기대는지 남기는 근거

이 구조는 단순 RAG보다 복잡합니다. 하지만 개인/팀 지식 OS에서 AI가 지식을 계속 정리하려면, 검색 결과만으로는 부족했습니다.

제가 지금 보는 OpenCairn의 wiki system은 이렇습니다.

LLM wiki = source notes + concept graph + explicit links + evidence + audit log

검색은 그 위에 올라가는 소비자 중 하나이고, agentic workflow는 그 evidence가 fresh한지 확인하는 다음 소비자입니다.