본문으로 바로 가기

지식 그래프

지식 그래프는 문서를 수집할 때 엔티티와 관계를 추출하고, 질의응답 시 연결 관계를 따라 관련 청크를 추가로 검색합니다. 벡터 및 키워드 검색과 함께 사용하여 답변에 관계 맥락을 보충할 수 있습니다.

이 기능은 인물, 조직, 제품 또는 조항 사이의 관계가 많은 자료에 적합합니다. 활성화하면 수집 단계의 모델 호출이 늘어나며 Neo4j를 배포해야 합니다.

스크린샷 준비 중
지식 그래프 뷰: 엔티티와 관계

지식 베이스의 그래프 탭에 엔티티 관계도를 표시합니다. 노드를 클릭하면 연결된 문서를 볼 수 있습니다.

website-docs/public/screenshots/kg-graph.png
지식 그래프 뷰: 엔티티와 관계

그래프 저장소는 Neo4j를 사용하며 APOC 플러그인에 의존합니다.

활성화 설정

그래프 기능은 두 단계의 스위치를 모두 켜야 합니다.

전역 스위치: Neo4j 환경 변수

NEO4J_ENABLE은 지식 그래프의 유일한 전역 스위치입니다(docker-compose.yml 주석에 명시된 대로 v0.1.6부터 ENABLE_GRAPH_RAGNEO4J_ENABLE로 대체했으며 Go 메인 애플리케이션은 더 이상 이전 변수를 읽지 않습니다).

이름타입기본값설명
NEO4J_ENABLEstring비어 있음(비활성화)true로 설정하면 그래프를 활성화합니다. internal/container/container.goinitNeo4jClient와 작업 큐 등록 / 검색 파이프라인 모두 이 값을 확인합니다
NEO4J_URIstringbolt://neo4j:7687Neo4j 연결 주소
NEO4J_USERNAMEstringneo4j사용자 이름
NEO4J_PASSWORDstringpassword비밀번호

initNeo4jClient는 시작 시 연결을 맺고 검증하기 위해 최대 30회(2s 간격) 재시도합니다. 비활성화 상태에서는 nil driver를 반환하며, 이때 Neo4jRepository의 모든 메서드는 no-op으로 동작합니다(로그: NOT SUPPORT RETRIEVE GRAPH). GET /system 정보 API는 getGraphDatabaseEngine()을 통해 "Neo4j" 또는 "Not Enabled"를 보고합니다(internal/handler/system.go).

docker-compose의 neo4j 서비스에는 APOC가 사전 설치되어 있습니다. NEO4JLABS_PLUGINS=["apoc"]로 설정되며, 그래프 쓰기는 apoc.merge.node / apoc.merge.relationship에, 삭제는 apoc.periodic.iterate에 의존합니다.

지식 베이스 수준 스위치: IndexingStrategy + ExtractConfig

internal/types/knowledgebase.go:

go
// IsGraphEnabled는 지식 그래프 추출의 활성화 여부를 확인합니다.
// IndexingStrategy 플래그와 유효한 ExtractConfig가 모두 필요합니다.
func (kb *KnowledgeBase) IsGraphEnabled() bool {
    return kb != nil && kb.IndexingStrategy.GraphEnabled &&
        kb.ExtractConfig != nil && kb.ExtractConfig.Enabled
}
  • IndexingStrategy.GraphEnabled(internal/types/indexing_strategy.go): 지식 베이스 인덱싱 전략의 그래프 스위치로, 기본값은 false입니다. 이전 필드인 ExtractConfig.Enabled는 읽을 때 IndexingStrategy.GraphEnabled로 단방향 동기화됩니다(knowledgebase.go 635행 부근의 legacy sync).
  • ExtractConfig(internal/types/knowledgebase.go)는 추출용 few-shot 설정을 담습니다.
이름타입기본값설명
enabledboolfalse추출 활성화 여부
textstring비어 있음few-shot 예제 원문
tags[]stringnil관계 유형 태그 집합
nodes[]*GraphNodenil예제 엔티티 노드(name / attributes)
relations[]*GraphRelationnil예제 관계(node1 / node2 / type)
custom_instructionsstring비어 있음도메인별 사용자 정의 추출 지침(시스템 프롬프트에 추가되며, 구조화된 출력 프로토콜은 여전히 시스템이 제어합니다)

설정 마법사 보조 API(internal/handler/initialization.go, 라우트: internal/router/router.go 914-916행):

  • POST /initialization/extract/text-relation(ExtractTextRelations): 텍스트 한 구간(≤5000자)에 대해 선택한 태그로 관계 추출을 시험 실행하여 결과를 미리 확인합니다.
  • POST /initialization/extract/fabri-text / fabri-tag(FabriText / FabriTag): LLM이 예제 텍스트 / 추천 태그를 생성하여 사용자가 ExtractConfig를 빠르게 구성하도록 돕습니다.

엔티티 관계 추출 흐름(구축)

트리거와 작업 오케스트레이션

문서 파싱이 완료되면 internal/application/service/knowledge_post_process.go가 보강 작업 분산 단계에서 텍스트 청크 수를 셉니다(eff.GraphEnabled일 때 graphChunkCount = len(textChunks)). 이어서 internal/application/service/extract.goNewChunkExtractTask를 호출하여 청크별로 큐에 등록합니다.

go
func NewChunkExtractTask(...) (bool, error) {
    if strings.ToLower(os.Getenv("NEO4J_ENABLE")) != "true" {
        logger.Warn(ctx, "NEO4J is not enabled, skip chunk extract task")
        return false, nil
    }
    ...
    task := asynq.NewTask(types.TypeChunkExtract, payload,
        asynq.Queue(types.QueueGraph), asynq.MaxRetry(3), asynq.Timeout(30*time.Minute))
    ...
}

작업은 독립적인 asynq QueueGraph 큐에서 처리되며, 청크마다 LLM을 한 번 호출합니다(소스 주석에서는 이를 "파이프라인에서 가장 비용이 큰 보강 작업 분산"이라고 설명합니다). 모델 수준의 백그라운드 동시 실행 제한기(limiter)가 적용됩니다. 취소 / 삭제되었거나 새 파싱 시도로 대체된(attemptSuperseded) 작업은 실행을 건너뛰고 부모 작업의 pending_subtasks_count를 차감합니다.

추출 실행(ChunkExtractService.Handle)

internal/application/service/extract.go:

  1. 청크, 지식 베이스, 파일 수준 ProcessOverrides를 불러오고 ResolveProcessConfig로 실제 적용할 ExtractConfig를 결정합니다(비활성화된 경우 건너뜁니다).
  2. 구조화된 프롬프트 템플릿을 구성합니다. 시스템 프로토콜 부분은 config.ExtractManager.ExtractGraph(config/config.yamlextract.extract_graph, 엔티티 추출 + 속성 보강 + 관계 추출로 이루어진 다단계 지침)에서 가져오고, 지식 베이스의 custom_instructions, tags, ExtractConfig의 few-shot 예제(Text/Nodes/Relations)를 더합니다.
  3. chatpipeline.NewExtractor(chatModel, template).Extract(ctx, chunk.Content)가 Chat 모델을 호출합니다(temperature 0.3, max_tokens 4096, thinking 비활성화). Formater.ParseGraph가 결과를 types.GraphData(internal/types/extract_graph.go)로 파싱합니다.
go
type GraphNode struct {
    Name       string   `json:"name,omitempty"`
    Chunks     []string `json:"chunks,omitempty"`
    Attributes []string `json:"attributes,omitempty"`
}
type GraphRelation struct {
    Node1 string `json:"node1,omitempty"`
    Node2 string `json:"node2,omitempty"`
    Type  string `json:"type,omitempty"`
}
  1. 각 노드에 node.Chunks = []string{chunk.ID}를 채운 뒤 graphEngine.AddGraph(ctx, NameSpace{KnowledgeBase, Knowledge}, ...)로 Neo4j에 저장합니다.
  2. 전체 과정을 SpanTracker로 추적합니다(postprocess.graph.chunk[i] 하위 span에 nodes/relations 수와 예시를 기록합니다).

저장소 백엔드: Neo4j

internal/application/repository/retriever/neo4j/repository.gointerfaces.RetrieveGraphRepository(AddGraph / DelGraph / SearchNode)를 구현합니다.

  • 네임스페이스가 곧 라벨: NameSpace{KnowledgeBase, Knowledge}는 노드 라벨 ENTITY<kb_id>, ENTITY<knowledge_id>로 매핑됩니다(하이픈은 밑줄로 바꿉니다). 노드 속성에는 name, kg(knowledge_id), attributes, chunks가 포함됩니다.
  • 쓰기에는 APOC의 멱등 병합을 사용하고, 이름이 같은 엔티티의 chunks는 합집합으로 합칩니다.
cypher
UNWIND $data AS row
CALL apoc.merge.node(row.labels, {name: row.name, kg: row.knowledge_id}, row.props, {}) YIELD node
SET node.chunks = apoc.coll.union(node.chunks, row.chunks)
  • 지식 / 지식 베이스를 삭제할 때(knowledge_delete.go, knowledgebase.go) DelGraph를 호출하여 apoc.periodic.iterate로 1000개씩 배치 처리하면서 간선과 노드를 병렬로 삭제합니다.

검색 시 그래프 보강(GraphRAG)

기존 채팅 파이프라인(internal/application/service/chat_pipeline)에는 두 가지 플러그인이 있습니다.

  1. PluginExtractEntity(extract_entity.go, QUERY_UNDERSTAND 이벤트에 연결): NEO4J_ENABLE=true일 때 먼저 ExtractConfig.Enabled인 지식 베이스를 추립니다(chatManage.EntityKBIDs / EntityKnowledge에 저장). 이어서 ExtractManager.ExtractEntity 템플릿과 Chat 모델로 사용자 질의에서 엔티티 이름을 추출해 chatManage.Entity에 저장합니다.
  2. PluginSearchEntity(search_entity.go, ENTITY_SEARCH 이벤트에 연결): 그래프가 활성화된 각 지식 베이스 / 파일에 대해 graphRepo.SearchNode를 병렬 호출합니다. Cypher는 n.name CONTAINS nodeText로 엔티티를 부분 일치 검색하고 1홉 이웃과 관계를 반환하며, 이를 chatManage.GraphResult로 병합합니다. 이후 filterSeenChunk가 그래프 노드의 chunks를 가져오고(벡터 검색에서 이미 찾은 항목은 제외), chunkRepo에서 원문을 불러와 SearchResult로 변환한 뒤 후보 집합에 합칩니다. 이를 통해 "엔티티 → 연결된 청크" 방식의 그래프 보충 검색을 수행합니다.

Agent 모드에서는 query_knowledge_graph 도구(internal/agent/tools/query_knowledge_graph.go)를 제공합니다. 각 지식 베이스의 그래프 설정 여부(ExtractConfig.Nodes/Relations가 비어 있지 않은지)를 검증하고, 여러 지식 베이스를 동시에 검색하여 청크별로 중복을 제거하고 정렬합니다. 출력에는 각 지식 베이스의 그래프 설정 상태(엔티티 유형 / 관계 유형 목록)가 포함됩니다. 그래프가 설정되지 않은 지식 베이스는 일반 하이브리드 검색 결과로 대체합니다.

흐름도

구축 흐름

질의 흐름

시각화

  • Mermaid 그래프 생성: internal/application/service/graph.gographBuildertypes.GraphBuilder 인터페이스의 메모리 기반 구현입니다(LLM 엔티티 추출 → 관계 추출 → PMI×0.6 + Strength×0.4로 관계 가중치를 계산하고 1-10으로 정규화 → 엔티티 차수 계산 → 청크 연결 그래프 구축). generateKnowledgeGraphDiagram은 DFS로 연결 요소를 찾아 Mermaid graph TD 하위 그래프를 출력합니다(빈도가 높은 엔티티를 강조하고, 강도 >7인 관계에는 굵은 화살표를 사용합니다). 참고: NewGraphBuilder는 현재 컨테이너 구성에서 호출되지 않습니다(저장소 내 다른 참조 없음). 독립적/레거시 그래프 구축 및 시각화 구현이며, 생성된 Mermaid 그래프는 로그에 출력됩니다.
  • 외부 제공 API: 지식 그래프 자체에는 전용 시각화 REST 엔드포인트가 없습니다. query_knowledge_graph 도구의 구조화된 출력(graph_configs, 결과 목록)을 Agent 프런트엔드가 렌더링합니다. GET /wiki/graph(wikiHandler.GetGraph)는 Wiki 기능 자체의 그래프 API로, 이 문서의 엔티티 관계 그래프와는 무관합니다.
  • prompt 템플릿: config/prompt_templates/graph_extraction.yamldefault_extract_entities 등의 템플릿(엔티티 유형 열거값 Person/Organization/Location/... 및 JSON 출력 프로토콜)을 제공합니다. internal/config/config.goextract_entities_prompt_id / extract_relationships_prompt_id를 통해 Conversation.ExtractEntitiesPrompt / ExtractRelationshipsPrompt로 파싱되어 위 메모리 기반 graphBuilder에서 사용됩니다. 프로덕션 비동기 추출 경로는 config.yamlextract.extract_graph / extract.extract_entity 템플릿(ExtractManagerConfig)을 사용합니다.

WeKnora v0.8.0 소스 코드 기반 한국어 번역 · MIT License