키워드 추출 정확성 확보

Posted: Updated:
#NLP##Forge##KeywordExtraction

Doculink은 Confluence 문서 간의 관계를 그래프로 시각화해주는 Atlassian Forge 앱입니다. 문서 관계를 정립하려면 각 문서에서 핵심 키워드를 뽑아내고, 키워드가 겹치는 문서끼리 연결해야 합니다. 문제는 이 키워드 추출의 품질이 곧 전체 서비스의 품질이라는 점이었습니다.

문제 분석

기존에는 retext-keywords 라이브러리를 사용해 키워드를 추출하고 있었습니다. 그런데 테스트를 진행해보니 내용적으로 전혀 관련 없는 인문학 문서와 컴퓨터과학 문서가 서로 연결되는 현상이 발생하고 있었습니다.

(임계값 미설정) 거의 모든 문서가 서로 연결되어 있는 것을 확인할 수 있음
(임계값 미설정) 거의 모든 문서가 서로 연결되어 있는 것을 확인할 수 있음

원인을 분석하기 위해 실제 추출되는 키워드를 로그로 확인해보았습니다.

Data Structures Overview
[
  { score: 1, stem: 'data' },
  { score: 0.75, stem: 'structur' },
  { score: 0.375, stem: 'algorithm' },
  { score: 0.375, stem: 'time' },
  { score: 0.25, stem: 'element' },
  { score: 0.25, stem: 'arrai' },
  { score: 0.25, stem: 'list' },
  { score: 0.25, stem: 'access' },
  { score: 0.25, stem: 'memori' },
  { score: 0.25, stem: 'task' },
  { score: 0.25, stem: 'databas' },
  { score: 0.25, stem: 'tree' },
  { score: 0.25, stem: 'graph' },
  { score: 0.25, stem: 'effici' }
]

Computer Networks
[
  { score: 1, stem: 'network' },
  { score: 0.75, stem: 'data' },
  { score: 0.75, stem: 'ip' },
  { score: 0.5, stem: 'comput' },
  { score: 0.5, stem: 'model' },
  { score: 0.5, stem: 'applic' },
  { score: 0.5, stem: 'tcp' },
  { score: 0.5, stem: 'http' },
  { score: 0.5, stem: 'dn' },
  { score: 0.5, stem: 'web' },
  { score: 0.5, stem: 'system' }
]

로그를 코드와 대조해 본 결과, 구체적으로 다음 3가지 문제를 발견했습니다.

  1. 모든 키워드를 관계로 성립시키고 있었습니다. retext-keywords는 0~1 사이의 중요도 점수(score)를 반환하는데, 기존 코드에서는 이 값을 완전히 무시하고 추출된 모든 키워드를 그대로 관계 연결에 사용하고 있었습니다. support, collect 같은 범용 어근도 키워드로 잡히면서 데이터 노이즈가 발생했습니다.
  2. 어근(stem) 대신 최초 등장 단어를 기준으로 매칭하고 있었습니다. keyword.matches[0].node 값을 사용하고 있어서, 같은 개념이라도 문서마다 다른 형태로 등장하면 매칭이 안 되는 경우가 발생했습니다.
  3. 플랫폼 자체의 제약도 컸습니다. Atlassian Forge의 백그라운드 함수 실행 시간 제한이 25초이고, 보안 정책상 외부 API 호출이 제한적이어서 GPU 기반의 AI 모델을 직접 쓰기 어려운 환경이었습니다.

기술 분석

최적의 키워드 추출 방식을 결정하기 위해 먼저 접근 가능한 방법들을 전체적으로 조사했습니다.

비학습 기반 방식

TF-IDF는 빠르고 간단하지만 문맥을 고려하지 않고, TextRankYAKE! 같은 그래프 알고리즘은 문서 하나만 있어도 동작하지만 긴 키워드(문구) 추출에 제한적이었습니다. 문법 기반 방식은 비교적 단순하고 직관적이나 도메인에 따라 과잉 또는 과소 추출이 발생할 수 있었습니다.

AI 기반 방식

BERT, GPT 같은 사전 학습 모델을 활용하면 문맥 이해가 가능하지만, GPU 서버 비용이 발생하고 외부 API를 사용할 경우 Confluence 내부 문서를 전송하는 보안 이슈가 문제였습니다. Google Cloud NL, Amazon Comprehend, HuggingFace 등을 검토했지만, Forge 환경의 보안 정책과 비용 문제가 발목을 잡았습니다.

Node.js 라이브러리 비교

결국 Forge의 Node.js 런타임에서 직접 동작하는 라이브러리를 찾아야 했습니다. 5종의 라이브러리를 컴퓨터과학 문서 10개, 인문학 문서 10개(각 2~3문단 분량의 영문서)로 직접 테스트했습니다.

라이브러리방식다국어Forge 호환비고
retext-keywords구문 분석 + 빈도 + WordNet영어 중심O기존 사용 중
wink-nlpPOS 기반 명사 추출영어 중심O빈도 기반, 빠름
compromise규칙 기반 POS영어 전용Ximport만으로 전체 기능 마비
rake-jsRAKE 알고리즘영어 전용Xwebpack 오류
node-nlpTF-IDF, NER다국어X타임아웃

compromise는 라이브러리를 추가하는 코드만 넣어도 Forge의 모든 기능이 아예 작동하지 않는 문제가 발생했고, rake-js는 import 시 webpack 오류가 발생했습니다. 두 라이브러리 모두 별도 Node 환경에서 dummy 데이터로 실행하면 정상 구동되었기에, Forge 환경에서 Node의 일부 함수를 지원하지 않는 것이 원인으로 추정됩니다. node-nlp은 Forge에서 timeout 문제로 사용이 불가능했습니다.

node-nlp 테스트 결과: Forge에서 timeout 문제로 사용 불가
node-nlp 테스트 결과: Forge에서 timeout 문제로 사용 불가

Forge 환경에서 실제로 빌드되고 실행까지 가능한 라이브러리는 retext-keywordswink-nlp 둘뿐이었습니다. wink-nlp은 단순 빈도수 기반으로 상위 5개 단어를 추출했는데, 결과를 확인해보면 다음과 같았습니다.

Data Structures Overview [ 'data', 'structures', 'time', 'like', 'provide' ]
Computer Networks [ 'networks', 'data', 'like', 'ip', 'computer' ]
Version Control with Git [ 'developers', 'git', 'distributed', 'version', 'control' ]
Basics of Machine Learning [ 'machine', 'learning', 'field', 'artificial', 'intelligence' ]
The Origins of Greek Mythology [ 'stories', 'human', 'greek', 'mythology', 'collection' ]
History of the Printing Press [ 'invention', 'printing', 'press', 'johannes', 'gutenberg' ]
Ethics and Moral Philosophy [ 'ethics', 'branch', 'philosophy', 'concerned', 'right' ]
Greek Philosophy [ 'greek', 'philosophy', 'plato', 'aristotle', 'knowledge' ]
Introduction to Linguistics [ 'language', 'linguistics', 'scientific', 'study', 'structure' ]
wink-nlp 테스트 결과: retext-keywords임계값 0.75 적용 결과와 유사
wink-nlp 테스트 결과: retext-keywords임계값 0.75 적용 결과와 유사

결과가 retext-keywords에서 임계값 0.75를 적용했을 때와 유사한 수준이었지만, 중요도 계산이 없어 정밀도 면에서 한계가 있었고 TF-IDF나 BM25를 자체 구현해야 하는 추가 부담이 있었습니다.

문제 해결

분석 결과를 종합하여, 라이브러리의 호환성 문제와 성능 제약을 극복하기 위해 전통적 NLP와 AI 에이전트를 활용한 이중 트랙 전략을 수립했습니다.

1. NLP 기반 점수 필터링

retext-keywords를 유지하되, 사용 방식을 근본적으로 바꿨습니다. 키워드 기준을 keyword.matches[0].node에서 keyword.stem으로 변경했습니다. 어근 기반 매칭으로 전환하면서 같은 개념의 다른 표현 형태도 하나로 묶이게 되었습니다. 또한, 중요도 점수에 임계값을 설정했습니다. 기존에는 score 값을 완전히 무시하고 있었는데, 임계값별로 결과를 비교해보았습니다.

score ≥ 0.75 적용. 관계없는 문서 간 연결이 확연히 줄어듦
score ≥ 0.75 적용. 관계없는 문서 간 연결이 확연히 줄어듦
score ≥ 0.5 적용 결과
score ≥ 0.5 적용 결과
score ≥ 0.25 적용. 임계값이 없을 때와 차이가 없음
score ≥ 0.25 적용. 임계값이 없을 때와 차이가 없음

테스트에 사용된 문서의 내용이 적어 0.5와 0.75 외에는 임계값이 없을 때와 차이가 없었지만, 문서 양이 많아지면 유효한 점수 분포가 나올 것으로 판단했습니다. 최종적으로 score ≥ 0.75 기준으로 상위 5개 핵심 키워드만 추출하도록 제한하여 무분별한 관계 생성을 방지하고 연산 복잡도를 낮췄습니다.

2. Rovo Agent 프롬프트 엔지니어링

NLP 기반 필터링은 빈도와 희소성만으로 중요도를 판단하기 때문에 문맥 이해에 한계가 있었습니다. 이를 보완하기 위해 Atlassian 플랫폼 내부의 AI 에이전트인 Rovo를 활용하는 방법을 설계했습니다.

처음 시도: Rovo에게 관계 추출까지 맡기기

처음에는 Rovo Agent에게 문서를 분석해서 관계까지 직접 추출하도록 프롬프트를 설계했습니다. 문서 간 유사도를 분석하고 graph 형태의 edges로 출력하라는 방식이었습니다.

# 초기 프롬프트 (관계 추출 시도)
prompt: >
  Role: You are responsible for analyzing documents 
  and determining their relationships.
  
  A. Fetch all Confluence pages using 'fetch-confluence-pages' action.
  B. Analyze relationships between pages based on content.
     Represent as graph format with nodes and edges:
     { "source": "Page1 Id", "target": "Page2 Id", "type": "rovo" }
  C. Register relationships using 'register-relationships' action.

그런데 문제가 발생했습니다. 관계 자체는 추출하는 것처럼 보였지만, 실제로는 거의 모든 문서를 단순히 엮어서 출력하고 있었습니다. JSON 리턴 형식을 graph 구조로 바꾸고, 유사도 기반 필터링(similarity score > 0.5)을 프롬프트에 명시하고, 작업을 A/B/C 3단계로 잘게 쪼개 서술해도 결과는 동일했습니다.

여러 차례 프롬프트를 수정했지만, 모든 문서를 단순히 엮는 결과가 반복
여러 차례 프롬프트를 수정했지만, 모든 문서를 단순히 엮는 결과가 반복

keyword 추출 → 유사 키워드 기반 관계 추출을 하나의 프롬프트 안에서 순차적으로 처리하게 하는 방식, 관계 강도(relationship_strength)를 수치로 출력하게 하는 방식 등 여러 변형을 시도했지만, Rovo Agent는 계속 같은 형태의 결과만 반환했습니다.

또한 저장 로직도 문제였습니다. 분석(Job A)과 등록(Job B)을 분리해서 프롬프트를 작성했음에도, Agent가 분석 결과를 출력한 뒤 등록 액션을 호출하지 않는 현상이 반복되었습니다. register-relationships 액션의 input 스키마를 JSON 형식으로 명확히 정의하고, Job 간 데이터 전달을 명시적으로 서술한 뒤에야 등록이 실행되기 시작했습니다.

방향 전환: 키워드 추출에 집중 여러 차례 시도 끝에 핵심적인 깨달음을 얻었습니다. Rovo에게 "관계 추출"이라는 복합 판단을 맡기는 것이 아니라, "키워드 추출"이라는 단일 작업에 집중시키는 것이 훨씬 효과적이라는 점이었습니다. 관계 정립은 기존 알고리즘이 처리하고, Rovo는 NLP가 놓치는 문맥 기반 핵심어를 뽑아내는 역할만 담당하게 했습니다.

# 최종 프롬프트 (키워드 추출에 집중)
prompt: >
  Role: You are responsible for analyzing documents 
  and extracting key keywords.
  
  A. Fetch all Confluence pages using 'fetch-confluence-pages' action.
  B. Extract top 5 most representative keywords from each page.
     Response format:
     { "documents": [{ "id": "PageId", "keywords": [...] }] }
  C. Register keywords using 'register-keywords' action.

이렇게 하면 NLP 트랙(retext-keywords)과 AI 트랙(Rovo)이 각각 독립적으로 키워드를 추출하고, 기존 관계 정립 알고리즘이 두 결과를 종합해서 문서 간 연결을 만드는 구조가 됩니다. Rovo에게 관계 판단이라는 복잡한 추론을 맡기지 않으면서도, 문맥 기반의 고품질 키워드를 확보할 수 있었습니다.

플랫폼 내부 도구이기 때문에 Confluence 문서를 외부로 전송하는 보안 이슈도 발생하지 않았습니다.

3. 비동기 액션 워크플로우 설계

단일 백그라운드 함수에서 모든 처리를 하면 25초 제한에 걸리는 문제를 해결하기 위해, Rovo Agent가 필요한 개별 함수를 직접 호출하는 비동기 액션 워크플로우를 설계했습니다. 하나의 긴 작업을 여러 개의 짧은 함수 호출로 분산시켜 실행 시간 문제를 해결했습니다.

추가로 Promise.all을 통한 병렬 처리, 배치 단위 요청, 긴 문서의 문단 단위 분할 등의 최적화도 적용했습니다.

결과

Before: 거의 모든 문서가 거미줄처럼 연결
Before: 거의 모든 문서가 거미줄처럼 연결
After: 핵심 주제 기반의 의미 있는 연결만 남음
After: 핵심 주제 기반의 의미 있는 연결만 남음

이번 최적화 과정을 통해 다음과 같은 성과를 얻을 수 있었습니다.

  • 정확도 및 가독성 향상: 단순 연결이 아닌 문서의 핵심 주제를 관통하는 의미 있는 연결 중심의 그래프 시각화를 구현했습니다. 컴퓨터과학 문서는 컴퓨터과학 문서끼리, 인문학 문서는 인문학 문서끼리 클러스터가 형성되었습니다.
  • 시스템 안정성 확보: 데이터 처리량 조절을 통해 제한된 자원 내에서도 서비스할 수 있는 문서량 임계치를 확대했습니다.
  • 엔지니어링 역량 증명: GPU도 못 쓰고, 외부 API도 제한되고, 실행 시간도 25초뿐인 환경에서 플랫폼의 물리적 제약을 아키텍처 설계와 알고리즘 최적화로 극복한 경험을 확보했습니다.

마치며

이번 작업에서 가장 큰 배움은 AI에게 무엇을 시키느냐의 문제였습니다. 처음에는 Rovo Agent에게 관계 추출이라는 복합적인 판단을 맡기려 했지만, 여러 차례의 프롬프트 수정에도 원하는 결과를 얻지 못했습니다. 결국 AI에게는 "핵심 키워드 추출"이라는 단일 작업만 맡기고, 관계 정립은 기존 알고리즘에 위임하는 구조로 전환하면서 문제가 해결되었습니다.

NLP 트랙의 score 임계값 적용도 마찬가지였습니다. retext-keywords가 이미 제공하고 있던 중요도 점수를 활용하지 않고 있었다는 사실 자체가 문제의 핵심이었고, 이 값을 무시하지 않는 것만으로도 결과가 크게 달라졌습니다.

다만 아직 해결해야 할 과제도 남아 있습니다. Confluence의 방대한 문서 특성상 일부 환경에서는 여전히 실행 시간 제약을 완전히 해결하지 못했고, Rovo Agent가 프롬프트 지시와 다른 형식을 반환하는 현상이 간헐적으로 발생해 스키마 정의나 응답 파싱 로직의 강화가 필요합니다.

Comments (0)

댓글을 불러오는 중...