mcp server
knowledge-base
Hybrid-search knowledge base for teams — ingests docs, git logs, Slack, code, and PRs into one Postgres+pgvector table, served over MCP, CLI, and a web dashboard
Set up
Runs on your computer. Pick your agent and operating system.
Before you start
- uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
{
"mcpServers": {
"riemannulus-knowledge-base": {
"command": "uvx",
"args": [
"knowledge-base"
]
}
}
}same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12
Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs
README
synced Oct 21, 2026knowledge-base
여러 소스(문서, git 로그, Slack, …)의 팀 지식을 하나의 Postgres 임베딩 테이블로 수집하고, 하이브리드 검색(전문검색 + 벡터 + RRF 융합)을 MCP 도구로 노출하는 지식 베이스 서버.
Inspired by Cerebras’ "How We Built Our Knowledge Base".
핵심 원칙:
- 정보가 사는 곳에서 그대로 추출 — 커넥터가 소스별로 행을 정규화해 같은 테이블에 쓴다. 테이블에 들어가면 즉시 같은 인터페이스로 검색된다.
- 하이브리드 검색 — 전문검색(정확 토큰: 에러 문자열·설정 키)과 벡터 검색(개념 질의)을 RRF(k=60)로 융합. Slack 문서에는 나이 감쇠("옛 답변은 만료된다") 적용.
- MCP에는 LLM-free 검색 프리미티브만 — 합성된 답이 아니라 원시 증거 행을 반환. 오케스트레이션과 답변 생성은 Claude Code 같은 클라이언트 에이전트의 몫.
- 에이전트가 쓰는 지식 베이스 — 개발 중 내린 의사결정(
record_decision)과 삽질에서 얻은 교훈(record_learning)을 MCP로 직접 기록하면 즉시 팀 전체가 검색 가능. 작업 전pitfalls로 과거에 밟은 문제를 확인해 같은 실수를 반복하지 않는다 → 팀 워크플로 가이드.
SOURCES markdown · git log · Slack(export/실시간) · 코드 저장소 · GitHub PR · (커스텀)
│ sync (커넥터가 SourceRow 방출) ┌ 에이전트/사람이 MCP로 직접 기록
PIPELINE 해시 dedup → [대화형: LLM 증류] → 청킹(헤딩 인지) → [문맥 생성] → 임베딩
│ upsert └ record_decision / learning / note
POSTGRES documents(원문+FTS) + chunks(임베딩+문맥) — 단일 임베딩 테이블
│ query
SEARCH FTS(희귀토큰 OR/접두) + 벡터 + IDF + trigram + 문맥FTS → 가중 RRF(k=60)
│ → 나이 감쇠 · boost/숨김 · supersede 감점 · 소스별 중복 상한
INTERFACES kb CLI · MCP 서버(stdio 로컬 / Streamable HTTP 원격 — 토큰 또는 OAuth 인증)
읽기: search·get_document·list_sources·who_knows·recent_changes
·search_code·subsystem_index·recent_prs
쓰기: record_decision·learning·note·verify_note·search_feedback / 예방: pitfalls
웹 대시보드(:8080) — 운영 헬스 · 모델/API 비용 · 쿼리 분석 · 질문(/ask)
· 노트 재검증/중복 큐 · 토큰/프로젝트 관리
1. 요구사항
| 항목 | 버전 | 비고 |
|---|---|---|
| Python | 3.11+ | tomllib 사용 |
| PostgreSQL | 14+ (16에서 테스트) | |
| pgvector | 0.5+ (0.6에서 테스트) | HNSW 인덱스 사용. halfvec(2000차원 초과·메모리 절감)은 0.7+, 필터 검색 리콜 보정(iterative_scan)은 0.8+에서 자동 활성 |
| pg_trgm | 선택 권장 | 한국어 복합어 내부 매칭(trigram 신호). Postgres 기본 동봉 확장 |
ripgrep (rg) | 선택 | search_code 도구용. 없으면 grep 폴백 |
claude CLI 또는 ANTHROPIC_API_KEY 또는 AWS 자격증명 | 선택 | LLM 기능(증류/요약/재순위/문맥/질문)용. 컨테이너 환경은 backend="anthropic"(공식 SDK) 또는 backend="bedrock"(Bedrock의 Claude, API 키 불필요) — §7.3 |
2. 설치
git clone https://github.com/riemannulus/knowledge-base && cd knowledge-base
python3 -m venv .venv && source .venv/bin/activate
pip install -e . # 개발용은 pip install -e ".[dev]"
선택 기능은 extras로: [aws](Bedrock 임베딩) · [slack](실시간 수집) ·
[dashboard](웹 대시보드, §11) · [anthropic](LLM 기능 API 백엔드, §7.3).
Docker 이미지는 네 개를 모두 포함한다.
Postgres에 pgvector가 없다면 (Debian/Ubuntu 예시):
sudo apt-get install postgresql-16-pgvector
createdb kb # 원하는 이름의 DB 생성
3. 빠른 시작 (5분)
Postgres 설치 없이 바로 띄우려면 §4.1의 Docker Compose 경로를 쓰면 된다 —
cp .env.example .env && docker compose up -d --build한 번으로 DB·MCP 서버·sync가 다 뜬다.
# 1) 설정 파일 작성
cp kb.toml.example kb.toml
$EDITOR kb.toml # dsn, 커넥터 경로 수정
# 2) 스키마 마이그레이션 (Alembic — 멱등, 여러 번 실행해도 안전)
kb migrate
# 3) 동기화 (커넥터 전체 실행, 증분)
kb sync
# {"connector": "markdown:kb-docs", "emitted": 4, "upserted": 4, ...}
# {"connector": "gitlog:kb-repo", "emitted": 12, "upserted": 12, ...}
# 4) 검색해 보기
kb search "배포 락 타임아웃"
kb search "ERR_MANIFEST_TIMEOUT" --source slack --limit 5
kb search "재색인 주기" --json
kb sync는 커서 기반 증분이다: 다시 실행하면 변경된 것만 처리하고,
내용이 같으면(content_hash) 재임베딩도 건너뛴다. 주기 실행은 cron에 걸면 된다:
*/10 * * * * cd /path/to/kb && .venv/bin/kb sync >> /var/log/kb-sync.log 2>&1
4. Claude Code / Claude Desktop에 MCP로 연결
두 가지 모드가 있다:
- 로컬(stdio) — 혼자 쓸 때. 아래처럼 명령으로 등록.
- 원격(Streamable HTTP) — 팀 공유. §4.1의 Docker 배포 후 URL로 등록.
Claude Code (로컬 stdio)
claude mcp add kb --env KB_CONFIG=/absolute/path/kb.toml -- /absolute/path/.venv/bin/kb mcp
또는 프로젝트의 .mcp.json:
{
"mcpServers": {
"kb": {
"command": "/absolute/path/.venv/bin/kb",
"args": ["mcp"],
"env": { "KB_CONFIG": "/absolute/path/kb.toml" }
}
}
}
Claude Desktop (로컬 stdio)
claude_desktop_config.json의 mcpServers에 위와 동일한 블록을 추가한다.
4.1 팀 공유 원격 서버 (Docker Compose로 한 번에)
cp .env.example .env
$EDITOR .env # KB_AUTH_TOKEN만 채우면 됨 (openssl rand -hex 24)
docker compose up -d --build
# 확인: 401이 나오면 정상 (인증이 걸린 상태)
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:8000/mcp -d '{}'
db(pgvector) + migrate(Alembic 스키마 마이그레이션 — 1회 실행 후 종료) +
mcp(HTTP :8000/mcp, 헬스체크 포함) + sync(주기 동기화, KB_SYNC_INTERVAL
기본 300초) + dashboard(웹 UI :8080, §11) 서비스가 뜨고 재시작 정책이 걸려 있다.
앱 서비스들은 migrate가 성공해야 시작되므로 스키마 업그레이드가 배포에 자동
포함된다 (k8s도 동일 — mcp/dashboard의 initContainer, sync CronJob의 선행 단계).
기본값 그대로면 이 저장소 자신(docs/ + git log)을 인덱싱하는 데모로 즉시 동작하고,
실데이터는 .env의 KB_DOCS_DIR/KB_REPO_DIR와 deploy/kb.docker.toml을 수정한다.
커넥터 하나가 실패해도 나머지 sync는 계속된다(실패 격리). DB 데이터는
kb-pgdata 볼륨에 보존되므로 docker compose down(-v 없이)으로는 유실되지 않는다.
수동 실행도 가능: kb mcp --transport http --host 0.0.0.0 --port 8000
(KB_AUTH_TOKEN 환경변수가 있으면 Authorization: Bearer 검사).
팀원 등록 (Claude Code):
claude mcp add --transport http kb https://kb.example.com/mcp \
--header "Authorization: Bearer <KB_AUTH_TOKEN>"
프로젝트 공유는 .mcp.json으로 (토큰은 각자 환경변수로 — 커밋 금지):
{
"mcpServers": {
"kb": {
"type": "http",
"url": "https://kb.example.com/mcp",
"headers": { "Authorization": "Bearer ${KB_AUTH_TOKEN}" }
}
}
}
플러그인으로 한 번에 (권장): 수동 mcp add 대신 kb-workflow 플러그인을 설치하면
KB 연결(+워크플로 스킬 kb-brief/kb-log/kb-wrap + 기록 유실 방지 훅)이 함께 배선된다.
설치 시 서버 URL/토큰을 물어본다 — 로컬(compose)은 기본값 엔터, 원격은 팀 서버
URL + kbt_ 토큰 입력(마스킹·보안 저장소 저장). 이후 전환·재설정은 /plugin 메뉴의
플러그인 설정 또는 /kb-workflow:setup 참고. 프로젝트 .mcp.json에 동명 kb 서버를
정의하면 플러그인 정의를 덮는다(완전 오버라이드).
상세: plugins/kb-workflow/README.md.
/plugin marketplace add riemannulus/knowledge-base
/plugin install kb-workflow@knowledge-base
claude.ai / Claude Desktop 커스텀 커넥터: 설정 → 커넥터 → 커스텀 커넥터에
https://kb.example.com/mcp 등록. 웹 커넥터 UI는 커스텀 헤더를 붙일 수 없으므로
OAuth로 붙인다 — §4.2. 자동화/봇은 Anthropic API MCP connector의
authorization_token에 kbt_ 토큰을 쓰면 된다.
상세 절차와 운영 팁: docs/05-team-workflow.md.
4.2 claude.ai 웹 커넥터 연결 (Google OAuth)
kb가 **스스로 OAuth 2.1 인가 서버(AS)**가 되고 "누구인가"만 Google에 위임한다. claude.ai가 커넥터를 연결할 때 브라우저로 Google 로그인을 띄우고, 허용 도메인 계정이면 kb가 자기 액세스 토큰을 발급한다. 팀원은 토큰을 복사·붙여넣을 일이 없다.
claude.ai ──OAuth 2.1(PKCE + 동적 등록)──▶ kb(/mcp)
└─OIDC─▶ Google (허용 도메인만 통과)
Google이 아니라 kb가 AS인 이유: Google은 임의 리소스 서버를 위한 RFC 9728 리소스 메타데이터도, MCP 클라이언트의 동적 등록(RFC 7591)도 제공하지 않는다. MCP 스펙이 명시한 3rd-party IdP 위임 패턴이 이 구조다.
1) Google OAuth 클라이언트 만들기 (하나로 커넥터·대시보드 모두 커버)
Google Cloud Console → API 및 서비스 → 사용자 인증 정보 → OAuth 클라이언트 ID → 애플리케이션 유형 웹 애플리케이션. 승인된 리디렉션 URI에 둘을 등록한다 (외부에서 실제로 접속하는 주소여야 하고, 문자 하나까지 일치해야 한다):
https://kb.example.com/oauth/google/callback # MCP 서버 (= KB_PUBLIC_URL + 고정 경로)
https://kb-dash.example.com/auth/callback # 대시보드 (= KB_DASHBOARD_URL + 고정 경로)
2) 서버 환경변수 (5개가 모두 있어야 켜진다 — 일부만 채우면 기동 시 예외로 알려준다)
| 환경변수 | 값 |
|---|---|
KB_GOOGLE_CLIENT_ID / KB_GOOGLE_CLIENT_SECRET | 위에서 만든 클라이언트 |
KB_OAUTH_ALLOWED_DOMAINS | 허용 Google Workspace 도메인 (예: example.com) |
KB_OAUTH_SECRET | state·세션 쿠키 서명 키. openssl rand -hex 32 (레플리카 전체 동일) |
KB_PUBLIC_URL | MCP 서버 공개 주소 (예: https://kb.example.com) |
선택: KB_OAUTH_ALLOWED_EMAILS(도메인 밖 개별 허용), KB_OAUTH_TOKEN_TTL(기본 3600),
KB_OAUTH_REFRESH_TTL(기본 30일). 설치는 pip install -e ".[oauth]"
(Docker 이미지에는 포함, compose는 .env만 채우면 된다).
3) 팀원 연결: claude.ai 설정 → 커넥터 → 커스텀 커넥터에 https://kb.example.com/mcp
입력 → 연결 → Google 계정 선택. 끝. Claude Desktop·MCP Inspector·Claude Code
(claude mcp add --transport http kb https://kb.example.com/mcp)도 같은 흐름을 탄다.
동작 확인:
# 무인증 요청은 401 + AS 위치를 알려주는 헤더를 돌려준다 (클라이언트가 이걸로 발견)
curl -si -X POST https://kb.example.com/mcp -d '{}' | grep -i www-authenticate
# WWW-Authenticate: Bearer error="invalid_token",
# resource_metadata="https://kb.example.com/.well-known/oauth-protected-resource/mcp"
curl -s https://kb.example.com/.well-known/oauth-authorization-server | jq .issuer
노출되는 엔드포인트: /.well-known/oauth-authorization-server(RFC 8414),
/.well-known/oauth-protected-resource/mcp(RFC 9728), /register(RFC 7591 동적 등록),
/authorize, /token, /revoke(RFC 7009), /oauth/google/callback.
권한 모델: 허용 도메인 계정 = 팀 전체 접근(프로젝트 스코프 없음), 쓰기는
kb.write 스코프에 달려 있다(기본 발급에 포함 — 읽기 전용 클라이언트는 kb.read만
요청하면 된다). 신원 이름은 이메일이라 record_*의 author와 query_log에 그대로 남는다.
세부 스코프가 필요하면 kbt_ 토큰(§8.1)을 계속 쓰면 된다 — 두 경로는 공존한다.
운영: 등록된 커넥터와 활성 세션은 대시보드 토큰 페이지에서 보고 폐기한다
(kb oauth clients|sessions|revoke-client|revoke-user도 같은 일을 한다).
퇴사자 처리는 Google 계정 정지 + kb oauth revoke-user <email>.
액세스 토큰은 만료가 짧고(기본 1시간), 리프레시는 회전할 때 옛 쌍이 함께 폐기된다.
⚠ OAuth를 켜면 "등록 토큰 0개 = 무인증 admin" 완화가 꺼진다 (§8.1) — 토큰 없는 요청은 항상 401이다. HTTPS는 여전히 reverse proxy/터널 몫이다: OAuth 토큰은 베어러라 평문 HTTP로 노출하면 그대로 유출된다.
연결 후 이렇게 물어보면 된다:
"kb에서 검색해줘: staging 배포가 락 때문에 멈추는 문제 예전에 어떻게 해결했지?"
에이전트가 search → get_document를 호출해 근거 문서(document_id, URL)를 인용하며 답한다.
MCP 도구 레퍼런스
| 도구 | 파라미터 | 설명 |
|---|---|---|
search | query, source?, project?, limit=10, rerank=false, expand=0 | 하이브리드 검색. expand=1~2면 매치 청크의 이웃을 context로 복원. 노트 결과의 project는 기록 시 명시한 소속 프로젝트(null=팀 공용) — 스코프 검색에서 타 프로젝트 노트는 하향 + other-project 표시. 반환: document_id/score/title/url/snippet/matched/context/project |
get_document | document_id, context=0 | 원문 전체 + (Slack이면) 증류 결과. context=1이면 헤딩 청크 목록 포함. 열람은 감사 로그에 남는다 |
list_sources | — | 소스/구획별 문서 수·최근 갱신·설명("무엇을 잘 답하나"), 프로젝트 목록. 도구 선택 전 상황 파악용 |
subsystem_index | query, limit=10 | 파일별 요약 인덱스 — "어느 파일에 구현돼 있나" 류 질문 (file_summary/file_head 대상) |
recent_prs | query?, days=14, limit=10 | 최근 PR 목록/검색 (github_prs 커넥터 필요) |
who_knows | topic, limit=5 | 주제 관련 스레드 참여자·커밋 작성자 집계 → 실증 전문가 |
recent_changes | source?, days=7, limit=20 | 최근 갱신 문서 (커밋/스레드/문서) |
search_code | pattern, repo?, regex=false, limit=30 | [code_repos] 위 ripgrep 정확 매칭 |
search_feedback | document_id, useful, query? | 검색 결과 품질 피드백 — 골든셋/큐레이션 재료 |
record_decision | title, decision, context?, alternatives?, tags?, author?, project?, code_refs?, supersedes? | 쓰기: 의사결정 기록. supersedes로 옛 결정 대체(번복 추적), code_refs의 파일이 바뀌면 재검증 대상 표시. 프로젝트 한정 결정이면 project 명시(미지정 = 팀 공용) |
record_learning | title, problem, root_cause?, solution?, prevention?, tags?, author?, project?, code_refs?, supersedes? | 쓰기: 삽질/버그 교훈 기록. prevention이 핵심. 특정 프로젝트에서만 성립하는 교훈이면 project 명시 |
record_note | title, content, tags?, author?, project? | 쓰기: 일반 지식 메모 |
stale_notes | days=90, limit=20, reason? | 재검증 큐 (대시보드와 동일): 참조 코드 변경(code) 우선, 90일+ 미검증(age) 다음. counts로 전체 규모 동봉 — 일일 재검증 루틴의 입력 |
verify_note | document_id | 쓰기: 노트 재검증 표시 (stale 플래그 해제) |
pitfalls | topic, limit=5, project? | 작업 전 사전 점검: 주제 관련 과거 교훈·결정 조회. 대체된 노트 제외, stale=true는 참조 코드가 변경됨 표시. 결과의 project는 소속 프로젝트(null=팀 공용), 스코프와 다른 프로젝트의 노트는 하향 + other_project=true |
모든 읽기 도구는 LLM을 호출하지 않아 빠르고 싸다. 쓰기 도구의 기록은 즉시
청킹·임베딩되어 바로 검색된다.
record_* 응답의 similar_existing은 비슷한 기존 기록이 이미 있다는 신호다 — 같은
문제라면 새 제목 대신 기존 제목으로 갱신하거나 supersedes를 쓰라. 단 후보의
project가 다르면 같은 증상이라도 원인이 다를 수 있다 — 힌트에 경고가 붙으니
같은 근본 원인임을 확인한 경우에만 갱신하고, 아니면 별도 기록을 유지하라.
record_*의 project는 등록된 프로젝트 이름 기준으로 해석된다: 등록명은 그대로,
저장소 이름(모노레포 하위 저장소 등 project_sources의 source_key)은 소유
프로젝트로 자동 교정, 등록되지 않았거나 여러 프로젝트에 걸려 모호한 이름은
라벨을 버리고 팀 공용으로 기록한다 — 잘못된 라벨은 자기 프로젝트 스코프 검색에서
억울한 감점을 만들기 때문. 결과는 응답의 project(최종 저장값)와
project_hint(교정/폐기 사유)로 알 수 있고, 같은 제목으로 다시 기록하면
갱신되므로 즉시 교정 가능하다.
에이전트가 이 루프(작업 전 pitfalls → 작업 후 record)를 스스로 돌게 만드는
CLAUDE.md 스니펫은 docs/05-team-workflow.md §3에 있다.
5. kb.toml 레퍼런스
[database]
dsn = "host=127.0.0.1 port=5432 user=kb dbname=kb" # $KB_DSN이 있으면 그것이 우선
[embedding]
backend = "hashing" # 'hashing' | 'openai' | 'bedrock'
dim = 1024
model = "" # openai: 기본 text-embedding-3-small / bedrock: 기본 amazon.titan-embed-text-v2:0
region = "" # bedrock 전용 AWS 리전 (비우면 $AWS_REGION)
[distill]
backend = "none" # 'none' | 'claude-cli'
model = "claude-haiku-4-5-20251001"
[[connector]] # 소스 하나당 하나. type: markdown | gitlog | slack_export
type = "markdown"
root = "docs" # 상대 경로는 kb.toml 위치 기준
source_key = "kb-docs"
url_prefix = "https://github.com/org/repo/blob/main/docs" # 선택: 결과 URL 생성
[[connector]]
type = "gitlog"
repo = "."
source_key = "kb-repo"
url_prefix = "https://github.com/org/repo/commit"
[[connector]]
type = "slack_export"
root = "/data/slack-export" # 워크스페이스 export(zip 해제) 디렉터리
channels = ["deploy", "infra"] # 생략 시 전체 채널
workspace_url = "https://your.slack.com" # 선택: 스레드 딥링크 생성
[[connector]]
type = "github_prs" # PR 본문+코멘트를 대화형 문서로 수집 (증류 경로 적용)
repo = "org/repo" # $GITHUB_TOKEN 권장
source_key = "prs"
[code_repos] # search_code(ripgrep) 대상
platform = "/srv/repos/platform"
이 밖에 [context](청크 문맥 생성 — Contextual Retrieval), [ask](대시보드 질문
페이지), [rerank] pool, [chunking] max_chars, [search](RRF 가중치·다양성 캡),
[pricing](단가 오버라이드), 커넥터 description(소스 카탈로그) 옵션이 있다 —
전체 주석은 kb.toml.example 참고. LLM 백엔드는 공통으로
'none' | 'claude-cli' | 'anthropic' | 'bedrock'이며, API 백엔드는 컨테이너/k8s에서도
동작한다 (pip install '.[anthropic]', bedrock은 [anthropic,aws]).
[context]는 프롬프트 캐싱으로 동작한다: 문서 본문을 캐시 프리픽스로 한 번
기록(1.25×)한 뒤 나머지 청크가 캐시 적중(0.1×)으로 돌아, 청크마다 문서를
풀프라이스로 재전송하는 것 대비 첫 백필 비용이 ~85% 절감된다. 캐시 프라이밍
후 청크 호출은 4-way 병렬이라 백필 속도도 그만큼 빨라진다 (API 백엔드 한정 —
claude-cli는 캐싱 제어가 없어 프리픽스를 이어붙여 실행).
환경변수: KB_CONFIG(설정 파일 경로), KB_DSN(DSN 오버라이드),
OPENAI_API_KEY/OPENAI_BASE_URL(openai 임베딩), ANTHROPIC_API_KEY(anthropic LLM
백엔드), GITHUB_TOKEN(github_prs 커넥터).
6. CLI 레퍼런스
| 명령 | 설명 |
|---|---|
kb migrate (별칭 init-db) | 스키마를 최신 Alembic 리비전으로 (멱등). 구 init-db 스키마는 자동으로 baseline stamp 후 편입. compose/k8s 배포는 자동 실행 |
kb sync [--connector 이름] | 전체(또는 특정) 커넥터 증분 동기화. 이름은 markdown:kb-docs 형식 |
kb search "질의" [--source S] [--project P] [--limit N] [--rerank] [--expand N] [--json] | 하이브리드 검색. --expand는 매치 청크 이웃 복원, --rerank는 LLM 재채점 |
kb eval golden.jsonl [--k 10] | 골든셋 검색 품질 평가 (Recall@k/MRR/nDCG). 모든 검색 튜닝의 판정 기준 |
kb eval --bootstrap out.jsonl [--days 30] | query_log에서 골든셋 스켈레톤 생성 (expect는 사람이 라벨링) |
kb notes stale [--days 90] | 재검증 필요 노트 (참조 코드 변경 또는 장기 미검증) |
kb notes dedup [--threshold 0.85] | 유사 노트 쌍(병합 후보) — 자동 병합 안 함 |
kb notes verify <id> / kb notes supersede <old> <new> | 노트 재검증 / 대체 표시 |
kb token create/list/revoke [--default-project P] | 사용자별 접근 토큰 관리 (§8.1). default-project는 soft 기본 스코프 |
kb oauth clients|sessions|revoke-client|revoke-user|prune | OAuth 커넥터 등록·사용자 세션 조회/폐기 (§4.2). 대시보드 토큰 페이지와 같은 조작의 헤드리스 경로 |
kb stats [--days N] | 쿼리 로그 분석 — 무응답 질의(커넥터/골든세트 백로그 후보), 상위 질의, 클라이언트별 사용량 |
kb reindex | 전 문서 재청킹·재임베딩 — 임베딩 백엔드/차원 변경 후 필수 |
kb gc [--connector 이름] [--dry-run] | 소스에서 사라진 문서(유령) 수거. sync에도 포함되므로 평소엔 불필요 — 미리보기/수동용 |
kb slack-listen | Slack Socket Mode 실시간 수집 (SLACK_BOT_TOKEN, SLACK_APP_TOKEN 필요, §7.2) |
kb mcp [--transport stdio|http] [--host H] [--port P] | MCP 서버 실행. http면 /mcp 경로, KB_AUTH_TOKEN 설정 시 베어러 인증 |
kb dashboard [--host H] [--port P] | 웹 대시보드 (기본 :8080) — 운영 헬스/모델·비용/검색/관리 (§11) |
공통 옵션: --config /path/kb.toml.
6.1 원본이 바뀌거나 사라지면 (드리프트 처리)
| 원본 변화 | 동작 |
|---|---|
| 내용 수정 | 커서에 걸려 재방출 → content_hash 비교 후 변경분만 재청킹·재임베딩 (같으면 스킵) |
| 파일 삭제 | sync 말미의 reconcile이 소스의 현재 목록(current_ids)과 대조해 유령 행 삭제 |
| 파일 이름변경/이동 | 새 경로로 재인덱싱 + 옛 경로 유령 삭제 (파일별 mtime 커서라 rename도 감지) |
| Slack 메시지 수정 (답글 없이) | edited.ts가 커서에 반영되어 스레드 전체 재수집 |
| git 히스토리 리라이트 (force push) | 무효 커서 감지 → 전체 재스캔으로 자가 복구, 사라진 커밋은 reconcile로 제거 |
안전장치: reconcile은 소스 목록이 비어 있으면 삭제하지 않는다 — 진짜 빈 소스와
마운트/경로 오설정을 구분할 수 없어, 설정 실수 한 번으로 인덱스가 전멸하는 사고를 막는다.
커스텀 커넥터도 current_ids()(현재 전체 source_id 반환) 하나만 구현하면 같은 수거를 받는다.
7. Slack·코드 데이터 넣기
7.1 백필 (과거 이력, export 기반)
- Slack 워크스페이스 관리자 메뉴에서 export를 받아 압축을 푼다
(
users.json,<채널>/YYYY-MM-DD.json구조). [[connector]] type="slack_export"를 설정하고kb sync.- 스레드 하나가 문서 한 행이 된다. 답글·수정(
edited.ts)이 생긴 스레드는 다음 sync에서 전체가 재수집되어 같은 행에 갱신된다.
7.2 실시간 수집 (Socket Mode)
Slack 앱을 만들어 Socket Mode를 켜고(앱 토큰 xapp-, connections:write),
봇 토큰(xoxb-)에 channels:history, channels:read, users:read,
reactions:read 스코프를 준 뒤 봇을 추적할 채널에 초대한다.
export SLACK_BOT_TOKEN=xoxb-... SLACK_APP_TOKEN=xapp-...
kb slack-listen # 또는: docker compose --profile slack up -d
-
이벤트 도착 → 즉시 ack → event_id 중복 제거 → 스레드 전체 재수집 → 행 1개 upsert.
-
새 메시지·답글·수정·리액션 모두 스레드 재수집을 트리거한다.
-
삭제 전파: 스레드가 Slack에서 지워지면 재수집 결과가 비므로 행을 삭제한다 — export 백필이 못 하던 유령 정리가 실시간 경로에서 해결된다.
-
백필과 같은
source_id(채널명/thread_ts)를 쓰므로 export → 실시간 전환 시 중복 없이 이어진다. -
채널 필터:
[slack] channels(추적할 채널 allowlist, 빈 값 = 전체) ·[slack] exclude_channels(봇이 초대됐지만 인덱싱하지 않을 채널). 봇이 초대된 채널의 이벤트는 전부 도착하므로, 잡담·알림 채널은 초대를 유지하면서 여기서 제외한다. 두 목록 다 문자열 완전 일치와 정규식 둘 다 받는다 —/…/로 감싸면 정규식(/…/i는 대소문자 무시)이고,re.search라 부분 일치이니 전체 일치는^…$로 앵커한다. 겹치면exclude_channels가 이긴다.[slack] channels = [] # 전체 채널 추적 exclude_channels = ["random", '/^tmp-/', '/-notifications$/']정규식은 TOML 리터럴 문자열(
'…')로 쓰면 백슬래시 이스케이프가 필요 없다. 잘못된 정규식은 리스너 시작 시 즉시 에러다 (조용히 무시해서 필터가 새는 것보다 빨리 실패하는 게 낫다). 필터는 수집 대상만 정하므로reply응답은 제외 채널에서도 동작한다 — 멘션은 사람이 직접 부른 요청이다. -
봇 응답:
[slack] reply = true면 @봇 멘션에 지식 베이스 검색 상위 3건(제목+링크+스니펫)을 스레드로 회신한다. 의도적으로 LLM-free — 봇은 증거를 주고 판단은 사람이 한다. 합성 답변은 대시보드 질문 페이지(§11) 몫. 추가 스코프:app_mentions:read,chat:write.
7.3 증류와 버스트
LLM 증류: [distill] backend = "claude-cli"(로컬), "anthropic"(API,
컨테이너 가능), "bedrock"(Amazon Bedrock의 Claude — API 키 없이 AWS 자격증명
재사용, 임베딩을 Bedrock으로 쓰는 배포에 적합. 리전은 [llm] region) 중 하나로
두면 대화형 문서마다 {question, summary, resolution, systems, code_refs}를 추출해 distilled 청크로 임베딩한다. bedrock 백엔드에서 model에
1P 모델명을 쓰면 추론 프로파일 ID(global.anthropic.…)로 자동 변환되고, Claude 외
Bedrock 모델도 지정할 수 있다 — model = "nova-2-lite"(Amazon Nova 2 Lite,
Converse API 자동 라우팅)는 Haiku 대비 입력가 1/3.3에 프롬프트 캐싱도 지원한다.
전환 전 scripts/model_ab.py로 실데이터 A/B를 권장. 잡담 스레드는 LLM이 skip
판정하고, 그 전에 비용 게이트(기본: 메시지 4개 미만이고 800자 미만이면 증류
생략, [distill] min_messages/min_chars)와 재증류 델타(redistill_after,
기본 5 — 리스너의 답글당 전체 재증류 방지)가 백필·실시간 폭주를 막는다. 증류물이 있으면 대화 원문은 임베딩하지 않는다 (D12 비대칭 인덱싱:
raw는 FTS로만 검색, 벡터 공간은 정규화된 증류물+버스트만 — 자체 A/B에서도 증류물이
우세). 증류가 실패하거나 꺼져 있으면 원문 임베딩으로 폴백해 검색 공백을 막는다.
버스트: 긴 스레드 속 개별 답변(같은 저자의 연속 메시지 묶음)을 스레드 주제를 앞에 붙여 별도 임베딩한다 — 스레드 요약에 어휘가 안 들어가는 탄젠트 답변도 검색된다. 저신호 차단 게이트: 희귀 토큰(IDF ≥ 4.0) + 200자 이상 + 리액션의 가중 결합 점수가 임계값을 넘어야 임베딩된다.
7.4 코드 저장소 인덱싱 (Phase 3)
[[connector]]
type = "code"
repo = "/srv/repos/platform"
source_key = "platform"
include = ["src/*"] # 파일 경로 allowlist/denylist
exclude = ["*/generated/*"] # node_modules, vendor, dist 등은 기본 제외
- 청킹: 언어별 경계 정규식으로 거친 단위 → 세밀한 단위 분할 (클래스/최상위 함수 → 메서드 → 고정 크기 폴백). Python/JS·TS/Go/Rust/JVM/C 계열 지원.
- 증분: cursor = HEAD sha,
git diff로 변경 파일만 재임베딩. 삭제·이름변경은 reconcile, 히스토리 리라이트는 자가 복구. 바이너리·256KB 초과 파일 자동 제외. - file_summary:
[code_summary] backend를 켜면 파일마다 목적·핵심 API를 LLM이 요약해 별도 청크로 임베딩한다. "환불 웹훅 처리 어디 있지?"처럼 문제는 아는데 구현 위치를 모르는 자연어 질의가 코드에 닿는 다리다. 에러 문자열·심볼 정확 매칭은search_code(ripgrep)가 담당 — grep과 시맨틱은 보완재다 — 둘 다 유지한다 (Cursor 실측: 병용이 최고 성과, docs/07 §3). - file_head (LLM 없는 파일 레벨 레코드): 심볼이 3개 이상인 코드 파일은 경로+심볼
목록이 자동으로 임베딩된다 — LLM 요약 없이도
subsystem_index도구가 동작한다.
7.5 GitHub PR 인덱싱
[[connector]] type = "github_prs"는 PR 제목·본문·이슈 코멘트를 스레드처럼 합쳐
대화형 문서로 수집한다 — Slack과 동일한 증류·버스트 파이프라인을 탄다.
커서는 updated_at 워터마크 증분이고, recent_prs
MCP 도구가 "최근에 누가 뭘 바꿨지" 질문에 답한다. GITHUB_TOKEN 권장.
7.6 Notion 인덱싱
[[connector]] type = "notion"은 Notion 통합(integration)에 공유된 페이지를
문서로 수집한다. 설정 3단계:
- notion.so/profile/integrations에서
내부 통합 생성 → 토큰을
NOTION_TOKEN에. - 인덱싱할 최상위 페이지/스페이스에서 ⋯ → 연결(Connections) → 통합 추가. 하위 페이지는 자동 포함된다. 공유 범위가 곧 인덱싱 범위 — 민감 스페이스는 공유하지 않으면 절대 수집되지 않는다.
- kb.toml에 커넥터 블록 추가 (kb.toml.example 참고) 후
kb sync.
동작: 검색 API를 last_edited_time 역순 워터마크로 증분 순회하고(타임스탬프가
분 단위 반올림이라 워터마크−5분 오버랩 창으로 되감아 스캔 — 재방출은 해시 dedup이
흡수), 블록 트리를
마크다운풍 텍스트(헤딩/리스트/코드 펜스)로 렌더링해 헤딩 인지 청킹을 그대로 태운다.
하위 페이지는 각자 별도 문서로 인덱싱된다. 아카이브/휴지통 페이지는 건너뛰며,
공유 해제·삭제된 페이지는 kb gc(reconcile)가 수거한다. 레이트리밋(≈3 req/s)은
429 Retry-After 재시도로 흡수. 1회 sync는 max_pages(기본 10,000)까지 — 상한에
잘리면 재개 커서를 저장해 다음 sync가 잘린 지점부터 이어서 백필하므로 큰
워크스페이스도 몇 주기에 걸쳐 전량 인덱싱된다.
7.7 Linear 인덱싱
[[connector]] type = "linear"는 이슈 제목·설명·코멘트를 스레드처럼 합쳐
대화형 문서로 수집한다 — GitHub PR과 동일한 증류·버스트 파이프라인. 설정:
- 인증 수단을 고른다 (아래 표). OAuth client_credentials 권장.
- kb.toml에 커넥터 블록 추가 (kb.toml.example 참고) 후
kb sync.
| OAuth client_credentials (권장) | 개인 API 키 | |
|---|---|---|
| 환경변수 | LINEAR_CLIENT_ID + LINEAR_CLIENT_SECRET | LINEAR_API_KEY |
| 발급 | linear.app/settings/api/applications/new 에서 OAuth 앱 생성 후 client credentials 토글 on | Settings → Security & access → Personal API keys |
| 신원 | 워크스페이스 소유 app actor (사람 계정과 무관) | 키 소유자 |
| 인덱싱 범위 | 워크스페이스의 public 팀 — private 팀은 구조적으로 제외 | 키 소유자가 볼 수 있는 전부 (private 팀 포함) |
| 헤더 | Authorization: Bearer <token> | 키를 접두 없이 그대로 |
인덱싱된 것은 KB 검색 사용자 전원이 볼 수 있으므로 범위가 곧 노출 범위다.
개인 키를 쓰면 소유자의 private 팀 이슈까지 들어오고, 이를 막는 건 teams
allowlist를 사람이 유지하는 것뿐이다 — 새 팀이 생기면 새는 쪽으로 기운다.
OAuth 쪽은 앱 상세 페이지에서 app user의 팀 접근을 더 조일 수도 있다.
어느 쪽이든 teams = ["ENG"]로 커넥터 단에서 더 좁힐 수 있다.
app actor 토큰은 30일 만료에 refresh_token이 없다. 커넥터는 sync 프로세스마다 새로 발급하므로(동일 스코프 병렬 토큰은 서로 무효화하지 않는다) 갱신 운영이 없다 — 영속 저장이 필요한 건 client id/secret뿐이다. 둘 중 하나만 설정하면 기동이 아니라 첫 호출에서 예외다(다른 수단으로 조용히 폴백하지 않는다). 둘 다 설정된 경우 OAuth가 개인 키를 이긴다 — 마이그레이션이 조용히 무효가 되지 않도록.
동작: GraphQL issues(filter: {updatedAt: {gt: 워터마크−5분}}) — 증분 필터가
서버측이라 무변경 sync는 요청 1회로 끝난다. 오버랩 창 5분은 필터 경계·복제
지연 보정이고 재방출은 해시 dedup이 흡수한다(§7.6 Notion과 동일 원칙). 아카이브·
휴지통 이슈는 건너뛰며 kb gc(reconcile)가 수거한다. source_id는 UUID라
이슈가 팀을 옮겨 식별자(ENG-123)가 바뀌어도 중복 문서가 생기지 않는다.
8. 프로젝트 스코프 (검색 범위 묶기)
프로젝트 = 소스 구획(source, source_key)의 이름 붙은 번들. 같은 구획을 여러 프로젝트가 공유할 수 있다.
INSERT INTO projects VALUES ('ml-infra', 'ML 학습 인프라');
INSERT INTO project_sources VALUES
('ml-infra', 'slack', '#ml-infra'),
('ml-infra', 'markdown', 'kb-docs'),
('ml-infra', 'gitlog', 'kb-repo');
이후 kb search "..." --project ml-infra 또는 MCP search(project="ml-infra").
8.1 사용자별 접근 토큰 (Phase 4 authz)
원격 MCP에 사람/에이전트별 토큰을 발급해 프로젝트 스코프와 쓰기 권한을 강제할 수 있다:
kb token create june --projects ml-infra # ml-infra 소스만 검색/조회 가능
kb token create ci-bot --read-only # record_* 도구 금지
kb token list / kb token revoke june
- 인증 규칙:
KB_AUTH_TOKEN(관리자, 전체 접근) →api_tokens등록 토큰(스코프 적용) → OAuth로 발급된kba_액세스 토큰(§4.2, 신원 = Google 이메일) → 셋 다 없으면 무인증 admin(사설망 모드). 등록 토큰이 하나라도 생기거나 OAuth가 켜져 있으면 무인증은 차단된다. - 스코프 토큰은 search/get_document/list_sources/recent_changes/who_knows 전부에서
허용 프로젝트의 소스만 본다. 예외:
note(팀 교훈·결정)는 항상 보인다 — pitfalls/record 루프는 팀 공용이기 때문. author를 생략한 record_* 기록에는 토큰 이름이 자동으로 들어가고, query_log에도 토큰 이름이 남는다(사용 분석·감사).- 평문 토큰은 발급 시 한 번만 출력된다 (DB에는 sha256만 저장).
9. 임베딩 백엔드 교체 (중요)
기본 hashing 백엔드는 외부 의존 없는 자리표시자다. 문자 n-gram 기반이라
정확 토큰·한국어 조사 변형은 잘 잡지만, 어휘가 겹치지 않는 패러프레이즈는 못 잡는다
(예: "학습 재개가 멈춰요" → "checkpoint restore stalls" 스레드).
실서비스에서는 의미 임베딩으로 교체할 것. 두 가지 API 백엔드를 지원한다:
OpenAI 호환 API
[embedding]
backend = "openai" # OpenAI 호환 API면 무엇이든 (OPENAI_BASE_URL)
model = "text-embedding-3-small"
dim = 1024
export OPENAI_API_KEY=...
kb reindex # 전 문서 재임베딩 (차원이 바뀌면 kb migrate를 새 DB에 다시)
AWS Bedrock
[embedding]
backend = "bedrock"
model = "global.cohere.embed-v4:0" # 한국어 포함 다국어 권장 (실측: docs/04 §S2)
dim = 1024
region = "ap-northeast-2"
pip install "knowledge-base[aws]" # boto3 (Docker 이미지에는 이미 포함)
kb reindex
| Bedrock 모델 | 차원 | 비고 |
|---|---|---|
global.cohere.embed-v4:0 | 256~1536 (dim 반영) | 다국어(한국어) 권장 — 실측 최고 성적(docs/04 §S2). 추론 프로파일 ID라 global. 접두 필수. 글로벌 라우팅이라 데이터 지역성 요건이 있으면 부적합 |
amazon.titan-embed-text-v2:0 | 256/512/1024 | 리전 내 상주. 한↔영 교차 어휘는 약함(실측). 텍스트 1건/호출 |
cohere.embed-multilingual-v3 | 1024 | 리전에 따라 미제공(ap-northeast-2 없음). 96건 배치 |
가용 모델은 리전마다 다르다 — aws bedrock list-foundation-models --by-output-modality EMBEDDING으로 확인하고, INFERENCE_PROFILE 타입이면
모델 ID 대신 프로파일 ID(global.…/apac.…)를 쓴다.
- 자격 증명: AWS 표준 체인을 그대로 쓴다 — EC2/ECS의 IAM 롤이면 키 설정이
필요 없고, 아니면
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY(+AWS_REGION) 환경변수. Bedrock API 키(AWS_BEARER_TOKEN_BEDROCK)도 boto3가 자동 인식한다. compose 사용 시.env와compose.yaml의 AWS_* 주석을 해제한다. - 실측 참고 (합성 골든 세트, docs/04 §S2): Titan v2로 교체 시 벡터 MRR 0.817→0.875, 증류물 임베딩이 원문 대비 우세(MRR 1.0 vs 0.927 — 증류를 켜는 게 좋다). 단 어휘가 전혀 안 겹치는 한↔영 교차 질의는 Titan v2도 약하다 — 다국어가 중요하면 Cohere multilingual 모델 접근을 활성화해 비교할 것.
- IAM 권한: 해당 모델에 대한
bedrock:InvokeModel하나면 된다. Bedrock 콘솔에서 모델 접근(Model access) 활성화가 선행되어야 한다. - 스로틀링: adaptive 재시도(최대 8회)가 내장되어 있어 대량 백필도 견딘다.
모델 선정 근거와 평가 하네스는 docs/04-spike-results.md의
S2 항목 참고 — spikes/의 골든 세트 평가를 재실행해 비교할 수 있다. 어떤 백엔드든
모델/차원을 바꾸면 kb reindex가 필수이고, 차원이 바뀌면 chunks 테이블의 vector
차원도 바뀌어야 하므로 새 DB에 kb migrate부터 다시 하는 것이 안전하다.
10. 새 커넥터 만들기
커넥터는 "cursor 이후 변경분을 SourceRow로 방출"만 하면 된다. 증류·청킹·임베딩·
업서트는 엔진이 처리한다.
# src/kb/connectors/jira.py (예시)
from datetime import datetime, timezone
from typing import Iterator
from .base import SourceRow, SyncState
class JiraConnector:
def __init__(self, base_url: str, project: str):
self.name = f"jira:{project}"
...
def sync(self, state: SyncState) -> Iterator[SourceRow]:
last = state.cursor.get("updated_after") # 증분 커서
for issue in self._fetch_issues(updated_after=last):
yield SourceRow(
source="jira", source_key=self.project,
source_id=issue["key"], # upsert 키
title=issue["summary"],
text=f'{issue["summary"]}\n\n{issue["description"]}',
updated_at=issue["updated"],
url=f"{self.base_url}/browse/{issue['key']}",
doc_type="document", # document | conversation | code
metadata={"participants": issue["participants"]},
)
state.cursor["updated_after"] = datetime.now(timezone.utc).isoformat()
doc_type 의미: document → 헤딩 인지 청킹 / conversation → 증류 경로(설정 시) /
code → 통짜 청킹. metadata.participants를 채우면 who_knows가 활용한다.
등록은 src/kb/connectors/registry.py의 build_connectors에 분기 추가 +
kb.toml에 [[connector]] 엔트리 — 이 두 곳 외에는 수정할 파일이 없다.
11. 웹 대시보드 (운영 · 비용 · 관리)
kb dashboard(기본 :8080)로 뜨는 팀용 웹 UI. compose에선 dashboard 서비스로
함께 올라온다 (http://localhost:8080).
| 페이지 | 내용 |
|---|---|
| 개요 | 사용 중인 모델(역할별 백엔드·단가), 수집 상태 요약(source 단위 롤업 + 동기화 지연 경고 → 상세는 소스 페이지), 임베딩 무결성(차원 불일치·NULL 청크 감지), 노트 요약(종류별 개수 · 재검증 필요 · 대체됨 + 최근 5건 → 상세는 노트 페이지) |
| 검색 | MCP search와 동일한 하이브리드 검색 + matched 신호 분해(FTS/벡터/IDF/trigram/문맥 어느 신호로 잡혔는지), 재순위 토글, 결과별 👍/👎 피드백(품질 신호로 축적, MCP search_feedback과 같은 테이블) |
| 질문 | /ask — 소형 LLM 플래너가 도구(search/subsystem_index/recent_prs)를 골라 증거를 모으고 [n] 인용이 달린 답변을 생성. kb.toml [ask] backend 필요 (미설정 시 안내만 표시) |
| 노트 | 기록 브라우저 — 제목·태그 검색 + 종류/상태(유효·재검증 필요·대체됨)/작성자/태그/프로젝트 필터, 정렬(최근 갱신·검증 오래된 순), 50건 페이지네이션(총 건수 표시). 뷰가 분리된다: 재검증 큐(?view=queue — 참조 코드 변경 먼저, 사유별 탭, 체크박스 일괄 확인) · 중복 후보(?view=dedup — 유사쌍을 supersede로 승인) — 큐 조작은 관리자 |
| 소스 | /sources — 커넥터별 마지막 동기화 시각·경과·커서(워터마크), source 롤업(구획 수·문서·청크·임베딩 누락), 구획(source/source_key)별 문서 수·최근 갱신(카탈로그 설명·프로젝트 매핑 포함), 구획을 누르면 그 안의 문서 목록(페이지네이션). source 탭 + source_key 부분 검색으로 좁힌다 |
| 쿼리 분석 | 일별 검색량·문서 열람 수, 무응답 쿼리(커넥터 백로그 후보), 인기 쿼리, 클라이언트별 사용량, 지연 p50/p95, 피드백 요약(👍/👎 비율·부정 피드백 쿼리). 쿼리 지표는 tool='search'만 센다 — get_document 열람 감사가 같은 테이블에 쌓이기 때문 |
| 운영·비용 | API 사용량·비용 집계 (§11.1) |
| 토큰/프로젝트 | §8.1 토큰 발급·폐기(평문 1회 표시), OAuth 커넥터 등록·사용자 세션 폐기(§4.2), 프로젝트-소스 구획 매핑 편집 — 관리자 전용 |
문서 상세 페이지에서는 관리자가 큐레이션을 할 수 있다: boost(검색 순위 ±, [-0.9, 2.0] 범위) 조정과 숨김(검색에서 제외, 원문은 유지) 토글. Onyx/Guru의 큐레이션 패턴을 따른 것으로, 자주 낡은 문서가 상위에 뜨면 지우는 대신 내리면 된다.
11.1 API 사용량·비용 계측
임베딩(bedrock/openai)과 LLM 기능(증류/코드 요약/재순위/문맥 생성/질문 —
claude-cli·anthropic·bedrock 백엔드)의 모든 호출이 api_usage 테이블에 자동
적재된다 — 호출 수, 입출력 토큰, 비용, 기능(kind)별 구분.
- 비용 = 공시 단가 × 토큰. 단가표는
src/kb/pricing.py, 변경·추가는 kb.toml[pricing]오버라이드 (모델 ID 부분 문자열 매칭이라global.프로파일 접두 무시 가능). - 토큰 수의 출처: Titan·OpenAI·anthropic·bedrock(Claude) 백엔드는 API가
돌려주는 실측값. Bedrock Cohere 임베딩은 토큰 수를 안 주므로 문자 수 기반
추정(대시보드에
≈표시). claude CLI는 응답의total_cost_usd를 그대로 기록 — 구독 인증으로 돌려도 "API로 돌렸다면"의 환산 비용이다. Bedrock Claude의 리전 고정 프로파일(us. 등)은 +10% 프리미엄이 있는데 단가표는 1P 공시가 기준이다. - 운영·비용 페이지: 일별 비용 차트, 기능·모델별 집계, 이번 달 누적(MTD)·일할 예상. 추정치이며 청구서가 아니다 — 실제 청구는 AWS/OpenAI 콘솔이 기준.
- hashing 백엔드·비활성 기능은 호출 비용이 없으므로 계측하지 않는다.
11.2 인증 — Google 로그인 (권장) 또는 Cloudflare Access
대시보드는 사람용이므로 브라우저 SSO를 쓴다. 모드는 환경변수로 결정된다: Google OAuth > CF Access > 개발 모드(무인증).
(A) Google 로그인 — MCP 커넥터와 같은 인증. §4.2의 Google 클라이언트를 그대로
쓰고 리디렉션 URI만 하나 더 등록하면 된다(<대시보드 주소>/auth/callback).
Zero Trust 앱 설정이 필요 없고, 앱이 직접 검증하므로 "오리진 우회" 전제도 없다.
| 환경변수 | 의미 |
|---|---|
KB_GOOGLE_CLIENT_ID / KB_GOOGLE_CLIENT_SECRET | §4.2와 동일한 클라이언트 |
KB_OAUTH_ALLOWED_DOMAINS / KB_OAUTH_ALLOWED_EMAILS | 허용 도메인/이메일 (§4.2와 공용) |
KB_OAUTH_SECRET | 세션 쿠키·state 서명 키 (32자 이상, 레플리카 전체 동일) |
KB_DASHBOARD_URL | 대시보드 공개 주소 (리디렉션 URI 계산) |
KB_DASHBOARD_SESSION_TTL | 세션 수명(초, 기본 43200 = 12시간) |
동작: /auth/login → Google → /auth/callback에서 id_token을 검증하고 허용 도메인을
확인한 뒤 서명된 세션 쿠키(HttpOnly·SameSite=Lax, HTTPS면 Secure)를 굽는다.
서버 측 세션 저장소가 없어 대시보드도 레플리카 N개로 뜰 수 있다. 로그아웃은
헤더의 링크(/auth/logout).
(B) Cloudflare Access.
| 환경변수 | 의미 |
|---|---|
KB_CF_TEAM_DOMAIN | Zero Trust 팀 이름 (myteam 또는 myteam.cloudflareaccess.com) |
KB_CF_AUD | Access 앱의 Application Audience(AUD) 태그 |
동작: CF Access가 SSO 통과 요청에 붙이는 Cf-Access-Jwt-Assertion 헤더(JWT)를
팀 도메인 JWKS로 서명 검증하고 aud/iss/exp를 확인한 뒤 email 클레임을 신원으로 쓴다.
⚠ CF 모드 전제: 오리진이 Cloudflare를 우회해서 접근될 수 없어야 한다. cloudflared tunnel(§11.3)로만 노출하거나 오리진 방화벽으로 잠글 것 — 헤더는 위조 가능하므로 우회 경로가 있으면 JWT 검증도 소용없다.
공통
| 환경변수 | 의미 |
|---|---|
KB_DASHBOARD_ADMINS | 관리 페이지(토큰/프로젝트/OAuth 커넥터) 허용 이메일, 쉼표 구분 |
KB_DASHBOARD_DEV_USER | 개발 모드에서 표시할 사용자명 (기본 dev@local) |
둘 다 비어 있으면 개발 모드(무인증, 전원 관리자, 배너 표시) — 사설망 밖 노출 금지.
11.3 Kubernetes 배포
deploy/k8s/에 kustomize 매니페스트 일체가 있다: Postgres StatefulSet(운영은 관리형
DB 권장), mcp Deployment(stateless ×2), dashboard, sync CronJob(git clone
initContainer 예시), slack-listener(선택), cloudflared tunnel(공인 LB/Ingress
불필요, CF Access 우회 원천 차단). mcp/dashboard에는 kb migrate initContainer가
붙어 있어 롤아웃마다 스키마가 자동으로 head까지 올라간다 (Alembic, advisory
lock으로 동시 기동 직렬화 — 레플리카 N개가 함께 떠도 안전). 빌드→시크릿→Zero
Trust 설정→kubectl apply -k 절차는 deploy/k8s/README.md.
이미지는 손으로 빌드하지 않는다 — main 머지 시 CD(.github/workflows/cd.yml)가
게이트(ruff + 실 Postgres pytest)를 통과한 커밋으로 멀티아치(amd64/arm64) 이미지를
빌드해 AWS ECR에 push하고, sha-<커밋> 좌표를 Actions summary에 남긴다. 자격증명은
GitHub OIDC 단기 토큰이라 저장된 AWS 키가 없다. 롤아웃은 그 좌표를
kustomization.yaml에 넣고 apply하는 수동 절차다 — 셋업·lifecycle 정책·롤백은
docs/08-cicd.md.
12. 개발
pip install -e ".[dev,dashboard,oauth]"
KB_TEST_DSN="host=127.0.0.1 port=5432 user=kb dbname=kb_test" pytest
ruff check src tests # 린트 (import 정렬·흔한 실수 검사)
개발 규칙·아키텍처 지도·과거에 밟은 함정은 CLAUDE.md에 정리돼 있다 (Claude Code로 작업 시 자동 로드).
DB 계층은 SQLAlchemy 2.0이다: 스키마는 ORM 모델(kb/models.py)이 원본이고
마이그레이션은 Alembic(kb/migrations/) — 스키마를 바꾸려면 모델 수정 +
새 리비전 파일 한 쌍으로 (CLAUDE.md 불변 원칙 참고). 검색 신호 SQL은 의도적으로
text()를 유지한다 (pgvector/FTS/trgm 튜닝 문장 — 골든셋 재검증 없는 변형 금지).
테스트는 실제 Postgres에 붙는다(스키마는 매 세션 실제 Alembic 마이그레이션으로 생성 — 마이그레이션 경로가 상시 검증된다). MCP 왕복, 대시보드(TestClient), CF Access JWT 검증(RSA 실서명), OAuth 전 구간(동적 등록 → PKCE → Google 위임 → 토큰 발급·회전·폐기, 도메인 게이트, 대시보드 세션 쿠키 — Google HTTP 왕복만 대역), 사용량 계측, 검색 신호별 회귀(D12~D15 교정 포함), 노트 수명주기, ask 파이프라인(스크립트 LLM), 레거시 DB stamp 편입까지 포함.
13. 설계 문서
| 문서 | 내용 |
|---|---|
| docs/02-spikes.md | 구현 전 스파이크 계획 (S0~S7) |
| docs/03-plan.md | 데이터 모델, MCP 도구 설계, Phase 0~4 로드맵 |
| docs/04-spike-results.md | 스파이크 실측 결과와 Decision Log — 왜 이렇게 만들었는지의 근거 |
| docs/05-team-workflow.md | 팀 배포·연결 절차와 "같은 실수 두 번 안 밟기" 워크플로 (CLAUDE.md 스니펫 포함) |
| docs/06-dashboard.md | 웹 대시보드 + API 비용 계측 설계 (D8~D11, 밟은 함정 포함) |
| docs/07-deep-dive.md | 설계 재점검(D12 |
| docs/08-oauth.md | OAuth 인증 설계 (D16~D18) — kb가 AS·Google이 IdP인 이유, 서명 state, 도메인 게이트, 밟은 함정 |
| docs/08-cicd.md | main 머지 → ECR push CD: OIDC/IAM 셋업, 멀티아치 태그 규칙, lifecycle 함정, 롤백 (D16~D19 — oauth 문서와 번호 중복) |
spikes/는 검증에 쓴 버리는 코드다 (한국어 FTS 실험, RRF 랭킹 비교, 증류 A/B 등).
14. 현재 한계와 로드맵
- 임베딩: 기본 hashing 백엔드는 의미 검색 불가 — §9대로 교체 권장. Bedrock Titan v2는 실호출로 검증·측정 완료(docs/04 §S2), OpenAI 백엔드는 코드만 있고 미검증.
- 재순위는 opt-in: 기본은 RRF 순위 그대로 (LLM-free 원칙).
--rerank/rerank=true[rerank] backend를 켜야 소형 LLM 재채점이 동작한다.
- 인증: 공유/사용자별 베어러 토큰(§8.1)과 Google OAuth(§4.2)가 공존한다. TLS는
여전히 reverse proxy/터널 몫이다. OAuth 신원은 허용 도메인 = 팀 전체 접근이라
프로젝트 스코프를 걸 수 없다(세부 스코프가 필요하면
kbt_토큰을 쓴다 — 이메일별 스코프는 백로그). 스코프는 검색·조회 접근 제어일 뿐 DB 접근 권한이 아니므로 극도로 민감한 데이터는 여전히 인덱싱하지 않는 것이 원칙. - 쓰기 도구에 사전 검토 절차 없음: record_*는 즉시 반영된다. 대신 사후 장치가
있다 — 기록 시 유사 노트 힌트(
similar_existing), 대시보드 중복 병합 큐,supersedes/kb notes supersede로 대체(삭제가 아니라 감점+이력 보존). - 한국어 복합어:
simple파서 + 희귀 토큰 접두(:*) 질의가 어절 수준을 커버하고, pg_trgm trigram 신호(word_similarity 임계 0.2)가 띄어쓰기 없는 복합어 내부 매칭을 보완한다. 형태소 수준이 필요해지면 pgroonga 도입. - pgvector 0.8 경로는 실측 미완: 필터 검색 리콜 보정(iterative_scan)과
halfvec(0.7+)은 버전 게이트로 구현했지만 개발 환경이 0.6이라 폴백 경로만 실검증됐다.
compose/k8s의
pgvector/pgvector:pg16이미지는 0.8+이므로 거기서 자동 활성된다. - 실서비스 미검증 기능: github_prs(§7.5)·notion(§7.6)·linear(§7.7) 커넥터,
Slack 멘션 자동응답(§7.2)은 페이크 API로 로직만 테스트했다 — 실제 토큰·워크스페이스
환경에서 첫 확인이 필요하다. bedrock 백엔드는 실호출 검증 완료(2026-08-05, Claude
Haiku·Nova 2 Lite Converse+캐싱·Cohere embed v4 — 실측 A/B에서 Nova가 동일 작업
기준 3.0× 저렴). LLM 모델 전환은
scripts/model_ab.py로 실데이터 A/B 후 결정할 것. - export 전용 운영 시 Slack 삭제 전파 안 됨: 실시간 리스너(§7.2)는 삭제를 전파하지만, export 백필만 쓰는 경우 export에 남아 있는 스레드는 지워졌는지 알 수 없다.
- 비용 계측은 추정치: 단가표가 하드코딩(+
[pricing]오버라이드)이라 공시가 변동을 자동 추적하지 않고, Bedrock Cohere는 토큰 수를 추정한다.api_usage는 계속 쌓이므로 수년 운영 시 오래된 행 정리(파티셔닝/DELETE)가 필요할 수 있다. - 평가 골든셋은 비어 있음:
kb eval하네스와--bootstrap(쿼리 로그 기반 스켈레톤 생성)은 있지만, 기대 문서(expect)를 채우는 건 팀의 몫이다. 검색 파라미터 ([search]가중치, 재순위 등)를 바꾸기 전에 골든셋부터 만드는 것을 권장한다.