knowledge-base
# knowledge-base
여러 소스(문서, git 로그, Slack, …)의 팀 지식을 **하나의 Postgres 임베딩 테이블**로
수집하고, **하이브리드 검색**(전문검색 + 벡터 + RRF 융합)을 **MCP 도구**로 노출하는
지식 베이스 서버.
> Inspired by Cerebras’ ["How We Built Our Knowledge Base"](https://www.cerebras.ai/blog/how-we-built-our-knowledge-base).
핵심 원칙:
- **정보가 사는 곳에서 그대로 추출** — 커넥터가 소스별로 행을 정규화해 같은 테이블에 쓴다.
테이블에 들어가면 즉시 같은 인터페이스로 검색된다.
- **하이브리드 검색** — 전문검색(정확 토큰: 에러 문자열·설정 키)과 벡터 검색(개념 질의)을
RRF(k=60)로 융합. Slack 문서에는 나이 감쇠("옛 답변은 만료된다") 적용.
- **MCP에는 LLM-free 검색 프리미티브만** — 합성된 답이 아니라 원시 증거 행을 반환.
오케스트레이션과 답변 생성은 Claude Code 같은 클라이언트 에이전트의 몫.
- **에이전트가 쓰는 지식 베이스** — 개발 중 내린 의사결정(`record_decision`)과
삽질에서 얻은 교훈(`record_learning`)을 MCP로 직접 기록하면 즉시 팀 전체가 검색 가능.
작업 전 `pitfalls`로 과거에 밟은 문제를 확인해 **같은 실수를 반복하지 않는다**
→ [팀 워크플로 가이드](docs/05-team-workflow.md).
```
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. 설치
```bash
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 예시):
```bash
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가 다 뜬다.
```bash
# 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에 걸면 된다:
```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)
```bash
claude mcp add kb --env KB_CONFIG=/absolute/path/kb.toml -- /absolute/path/.venv/bin/kb mcp
```
또는 프로젝트의 `.mcp.json`:
```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로 한 번에)
```bash
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):**
```bash
claude mcp add --transport http kb https://kb.example.com/mcp \
--header "Authorization: Bearer <KB_AUTH_TOKEN>"
```
프로젝트 공유는 `.mcp.json`으로 (토큰은 각자 환경변수로 — 커밋 금지):
```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](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](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`)도 같은 흐름을 탄다.
동작 확인:
```bash
# 무인증 요청은 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](docs/05-team-workflow.md) §3에 있다.
## 5. kb.toml 레퍼런스
```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](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 기반)
1. Slack 워크스페이스 관리자 메뉴에서 export를 받아 압축을 푼다
(`users.json`, `<채널>/YYYY-MM-DD.json` 구조).
2. `[[connector]] type="slack_export"`를 설정하고 `kb sync`.
3. 스레드 하나가 문서 한 행이 된다. 답글·수정(`edited.ts`)이 생긴 스레드는 다음
sync에서 **전체가 재수집**되어 같은 행에 갱신된다.
### 7.2 실시간 수집 (Socket Mode)
Slack 앱을 만들어 Socket Mode를 켜고(앱 토큰 `xapp-`, `connections:write`),
봇 토큰(`xoxb-`)에 `channels:history`, `channels:read`, `users:read`,
`reactions:read` 스코프를 준 뒤 봇을 추적할 채널에 초대한다.
```bash
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`가 이긴다.
```toml
[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)
```toml
[[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단계:
1. [notion.so/profile/integrations](https://www.notion.so/profile/integrations)에서
내부 통합 생성 → 토큰을 `NOTION_TOKEN`에.
2. 인덱싱할 최상위 페이지/스페이스에서 **⋯ → 연결(Connections) → 통합 추가**.
하위 페이지는 자동 포함된다. **공유 범위가 곧 인덱싱 범위** — 민감 스페이스는
공유하지 않으면 절대 수집되지 않는다.
3. 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과 동일한 증류·버스트 파이프라인. 설정:
1. 인증 수단을 고른다 (아래 표). **OAuth client_credentials 권장.**
2. 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)의 이름 붙은 번들. 같은 구획을 여러
프로젝트가 공유할 수 있다.
```sql
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에 사람/에이전트별 토큰을 발급해 **프로젝트 스코프와 쓰기 권한을 강제**할 수 있다:
```bash
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
```toml
[embedding]
backend = "openai" # OpenAI 호환 API면 무엇이든 (OPENAI_BASE_URL)
model = "text-embedding-3-small"
dim = 1024
```
```bash
export OPENAI_API_KEY=...
kb reindex # 전 문서 재임베딩 (차원이 바뀌면 kb migrate를 새 DB에 다시)
```
### AWS Bedrock
```toml
[embedding]
backend = "bedrock"
model = "global.cohere.embed-v4:0" # 한국어 포함 다국어 권장 (실측: docs/04 §S2)
dim = 1024
region = "ap-northeast-2"
```
```bash
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](docs/04-spike-results.md)의
S2 항목 참고 — `spikes/`의 골든 세트 평가를 재실행해 비교할 수 있다. 어떤 백엔드든
**모델/차원을 바꾸면 `kb reindex`가 필수**이고, 차원이 바뀌면 chunks 테이블의 vector
차원도 바뀌어야 하므로 새 DB에 `kb migrate`부터 다시 하는 것이 안전하다.
## 10. 새 커넥터 만들기
커넥터는 "cursor 이후 변경분을 `SourceRow`로 방출"만 하면 된다. 증류·청킹·임베딩·
업서트는 엔진이 처리한다.
```python
# 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](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](docs/08-cicd.md).
## 12. 개발
```bash
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.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](docs/02-spikes.md) | 구현 전 스파이크 계획 (S0~S7) |
| [docs/03-plan.md](docs/03-plan.md) | 데이터 모델, MCP 도구 설계, Phase 0~4 로드맵 |
| [docs/04-spike-results.md](docs/04-spike-results.md) | 스파이크 실측 결과와 Decision Log — 왜 이렇게 만들었는지의 근거 |
| [docs/05-team-workflow.md](docs/05-team-workflow.md) | 팀 배포·연결 절차와 "같은 실수 두 번 안 밟기" 워크플로 (CLAUDE.md 스니펫 포함) |
| [docs/06-dashboard.md](docs/06-dashboard.md) | 웹 대시보드 + API 비용 계측 설계 (D8~D11, 밟은 함정 포함) |
| [docs/07-deep-dive.md](docs/07-deep-dive.md) | 설계 재점검(D12~D15 교정) + 참고문헌 10편·유사 시스템·최신 기법 조사 + 기능 로드맵(Tier 0~5) |
| [docs/08-oauth.md](docs/08-oauth.md) | OAuth 인증 설계 (D16~D18) — kb가 AS·Google이 IdP인 이유, 서명 state, 도메인 게이트, 밟은 함정 |
| [docs/08-cicd.md](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]` 가중치, 재순위 등)를 바꾸기 전에 골든셋부터 만드는 것을 권장한다.
## 15. 라이선스
[MIT](LICENSE)
TDQS
Scored across 15 tools
Most tools serve clearly distinct purposes, with detailed descriptions clarifying edge cases (e.g., search_code vs. search vs. subsystem_index). Minor overlap exists between search and pitfalls, but their intents are well explained.
The majority follow a verb_noun pattern (record_*, search_code, get_document), but 'search' and 'pitfalls' deviate, and 'subsystem_index' uses a noun_noun format. This mixing makes the convention less predictable.
15 tools is at the upper boundary of a well-scoped set. Each tool has a defined role, though a few meta-tools (search_feedback, stale_notes) add complexity without being strictly necessary for core knowledge-base operations.
Covers search, retrieval, creation/updating via record_* idempotency, verification, and catalog listing. Missing an explicit delete tool, but supersedes mechanism partially addresses retirement. Overall, the surface is quite complete for its stated purpose.