AI 기능 아키텍처 설계 (Claude LLM 통합)
Hereby에 Claude 기반 AI 기능을 도입하기 위한 설계 문서. 대상 기능: ① 대화형 HR 어시스턴트 ② 시트 자동 구성 ③ 자동 스케줄링 ④ 사용자 가이드 지원.
상태: 초기 설계 v3 기록 (2026-07-10, 이후 일부 후속 기록 포함).
2026-10-14 현행 정정: 이 문서의 현황 표·단계별 계획은 작성 당시 기준이며 현재 구현 목록이 아닙니다. 사용자 가이드 안내의 기준은 AI 문서 안내의 현재 구조와 유지보수입니다. 현재 해당 경로는 Voyage
voyage-context-4와 S3 Vectors를 사용하며, 색인 대상 15개 문서와 안내 대상으로 연결한 10개 기능을 구분합니다. 아래 pgvector·voyage-3·증분 색인 계획이 현재 안내 경로에 적용됐다고 해석하지 않습니다.
0. 현황 요약 — 이미 있는 것과 없는 것
| 축 | 현재 상태 |
|---|---|
| LLM 클라이언트 | packages/api/src/utils/openai-client.ts — SDK 없는 수제 fetch 클라이언트 (OpenAI / Ollama / vLLM 3-provider). 소비처는 간호사 스케줄러 1곳 (schedule-ai-generator.ts). Anthropic/Claude 참조는 코드베이스 전체에 0건 |
| 스케줄 자동 생성 | ✅ 이미 존재 — schedule-generator.ts(결정론적 제약 솔버) + schedule-ai-generator.ts(LLM, opt-in). nurseScheduler.schedule.generate가 AI 시도 → 실패 시 솔버 폴백 (generationMethod: constraint-fallback) |
| 스트리밍 인프라 | ✅ tRPC WS 구독 /trpc-ws 가동 중 (applyWSSHandler, 메신저 이벤트에 사용). SSE는 없음 |
| pgvector | Docker 이미지에 설치됨(0.8.2, shared_preload_libraries) — 그러나 사용처 0건. 엔티티/쿼리/임베딩 없음. 마이그레이션 설계는 §7 |
| 시트 프로그래밍 표면 | ✅ 완비 — sheetView.create/update(columnLayout JSONB) · setAutoPopulate · setComputeConfig · sheetTemplate.* · sheetRow/sheetCell.*. Zod 스키마(sheet-view.schema.ts)가 전체 형태를 이미 검증 |
| 사용자 가이드 | VitePress docs/manual/*.md (EN+KO, 앱별 매뉴얼 ~13편). 인앱 도움말은 단축키 모달 3개뿐 — 온보딩/투어 시스템 없음 |
| 조직별 자격증명 | ✅ 패턴 존재 — ExternalIntegrationCredential 엔티티(organizationId + provider + status) + maskCredentialSecret. BYOK(§5)에 재사용 |
| 조직 설정 | ✅ Organization.settings jsonb 존재 — AI enable 플래그를 얹을 자리 |
| 백그라운드 워커 | Redis 클라이언트/큐 없음. in-process setInterval(sheet-materializer) + 이벤트 이미터 패턴 |
결론: AI 기능 4개를 각각 만드는 게 아니라, 하나의 에이전트 코어 + 프로바이더 추상화를 만들고 기존 표면(라우터·솔버·문서·시트 API)에 tool로 연결하는 구조.
1. 핵심 아키텍처 결정
1-1. Claude API + tool use, self-hosted 루프, packages/api 내부
apps/admin ─┐ (shared chat widget: ┌────────────────────────────────────┐
apps/hr-sys ─┼── packages/components) ──▶│ packages/api │
employee-app ┘ 모든 앱 탑재 가능 │ ai.router.ts (tRPC + WS 구독) │
│ └▶ services/ai/agent.service.ts │──▶ Anthropic API
│ │ tool-use 루프 │ (모듈별 모델, §1-3)
│ ▼ tool은 호출자 ctx로 실행 │
│ employee/leave/payroll 라우터 │
│ sheetView.* · nurseScheduler.* │
│ docs 검색 (pgvector, §7) │
└────────────────────────────────────┘- Managed Agents(서버 관리형)를 쓰지 않는 이유: tool이 전부 내부 DB/서비스 호출이며, HR·급여 데이터가 모델 API 호출 이상으로 외부에 나가면 안 된다. 에이전트 루프를 우리 서버에서 돌린다.
- 별도 서비스로 분리하지 않는 이유: 에이전트가 tRPC context(인증·조직·권한), TypeORM 리포지토리, scope 헬퍼를 그대로 써야 한다 — 전부
packages/api에 있다. - SDK:
@anthropic-ai/sdk. tool 정의는betaZodTool— 입력 스키마가 이미 전부 Zod(@hereby/core)라서 상당수 tool 스키마를 기존 스키마에서 파생 가능.
1-2. 보안 불변식 (멀티테넌시 — 가장 중요한 설계 원칙)
모든 tool은 기존 서비스/프로시저의 얇은 래퍼이며, 요청한 사용자의 ctx로 실행된다.
- 에이전트에게 raw SQL, unscoped repository를 절대 주지 않는다.
scopedProcedure/applyScopeFilter/organizationId필터가 UI 클릭과 동일하게 적용된다. - 팀장이 "우리 팀 연차 현황"을 물으면 team-scope 데이터만 나온다. 모델이 사용자 권한 이상을 조회하는 것이 구조적으로 불가능해야 한다.
- 프롬프트 인젝션 대비: 셀 값·문서 내용 등 DB에서 읽어온 텍스트는 신뢰 불가 입력으로 취급 → v1 tool은 전부 read-only, 변경(mutation)은 "모델이 제안 → UI 확인 카드 → 사용자 클릭이 실행"의 confirm 패턴만 허용.
- 감사 추적: 모든 대화·tool 호출을
ai_messages에 기록 (BaseEntity, soft delete — 하우스 룰 준수).
1-3. 프로바이더 추상화 & 모듈별 모델 설정 [결정 반영]
openai-client.ts는 신규 프로바이더 추상화로 흡수한다 (레거시 병존 없음). 스케줄러 경로도 Phase 3에서 이 추상화로 이전.
packages/api/src/services/ai/providers/
types.ts # AiProvider 인터페이스: chat(stream, tools), 프로바이더 무관 usage 리포트
anthropic.ts # @anthropic-ai/sdk (1차 시민: tool use, streaming, caching)
openai-compat.ts # 기존 openai-client 이관 (OpenAI / Ollama / vLLM — 동일 wire format)AI 모듈(기능 단위)별로 모델을 갈아끼울 수 있어야 한다 [결정 4]:
// packages/core/src/enums/ai.enum.ts
export enum AiModule {
CHAT = "chat", // 기능① HR 어시스턴트
DOCS = "docs", // 기능④ 가이드 지원
SHEET_CONFIG = "sheet_config", // 기능② 시트 구성
SCHEDULER = "scheduler", // 기능③ 스케줄링
}모델 해석 순서 (모듈 단위): 조직 설정 오버라이드 → env 설정 → 모듈 기본값
| 모듈 | 기본 모델 | 경량 대안 (설정으로 전환) |
|---|---|---|
chat | claude-opus-4-8 | — (품질 민감) |
docs | claude-opus-4-8 | claude-haiku-4-5 ($1/$5) — 검색+요약은 경량 후보 1순위 |
sheet_config | claude-opus-4-8 | — (정확성 민감, structured output) |
scheduler | claude-opus-4-8 | claude-haiku-4-5 — 선호 추출은 경량 후보 |
env: AI_MODEL_DEFAULT=claude-opus-4-8, AI_MODEL_<MODULE>=... 오버라이드. config.ai 블록 확장.
1-4. 스트리밍 · 캐싱
| 항목 | 결정 |
|---|---|
| 스트리밍 | 항상 스트리밍. 기존 tRPC WS 구독 채널로 delta 전달 — 신규 인프라 불필요 |
| 프롬프트 캐싱 | 시스템 프롬프트 + tool 정의를 byte-stable하게 유지, 그 뒤에 cache_control breakpoint. 턴별 컨텍스트(사용자·조직·현재 앱/탭)는 breakpoint 뒤에 배치. 캐시 읽기 ≈ 0.1× — 채팅 기능의 최대 비용 레버 |
| thinking | {type: "adaptive"} |
1-5. 권한
권한 상수에 ai 리소스 추가 (resource:action.scope 규격): 예) ai:execute.self, ai:admin.all(사용량 대시보드·조직 설정) — 역할/조직 단위로 기능 게이팅. default-permissions 시드에 반영.
2. 조직별 AI 설정 · BYOK · 사용량 측정 [신규 요구사항]
2-1. 조직별 enable 플래그 [결정 1]
Organization.settings.ai(기존 jsonb 확장 — 마이그레이션 불필요):
settings: {
ai?: {
enabled: boolean; // 기본 false — 명시적으로 켠 조직만 사용
keySource?: "platform" | "byok"; // 기본 platform
modelOverrides?: Partial<Record<AiModule, string>>; // 모듈별 모델 오버라이드 (§1-3)
monthlyTokenLimit?: number; // 선택: 조직별 월 사용량 상한
};
}- 모든
ai.*프로시저 진입점에서enabled체크 (미들웨어 1곳). system-admin(tools/system-admin)에서 조직 프로비저닝 시 토글.
2-2. BYOK — 고객사 API 키 직접 입력
기존 ExternalIntegrationCredential 패턴 재사용 (신규 엔티티 불필요):
IntegrationProvider에ANTHROPIC추가. 조직별 1건 (organizationId + provider + status=ACTIVE).- at-rest 암호화는 기존 인프라로 해결됨:
apiKey/apiSecret컬럼이 이미encryptedColumn()transformer(AES-256-GCM,packages/core/src/utils/encryption.utils.ts,FIELD_ENCRYPTION_KEY,v1:버전 프리픽스로 키 로테이션 대비)를 사용 — BYOK 키는 이 엔티티 재사용만으로 암호화 저장된다. - 표시할 때는 기존
maskCredentialSecret사용. 저장/수정은 admin 설정 UI +ai:admin권한. - 키 해석 순서: 조직
keySource=byok&& ACTIVE 자격증명 존재 → 고객 키 / 그 외 → 플랫폼 키(ANTHROPIC_API_KEY). - BYOK여도 사용량 측정(§2-3)은 동일하게 기록한다 — 과금 주체만 다를 뿐 관측은 플랫폼 책임.
- 키 유효성: 저장 시 1회 검증 호출(models.retrieve), 런타임 401 시 자격증명 status를 FAILED로 전환하고 플랫폼 키로 폴백하지 않는다(고객 의도 존중, 에러 노출).
2-3. 조직별 사용량 측정 (metering)
모든 모델 호출의 usage를 메시지 단위로 기록하고, 조직/월 단위로 집계한다.
ai_messages에 usage 컬럼 포함:model,module,inputTokens,outputTokens,cacheCreationTokens,cacheReadTokens,latencyMs,keySource.- 집계는 쿼리로 시작(월별 GROUP BY) — 물리 롤업 테이블(
ai_usage_monthly)은 데이터가 커지면 도입. TimescaleDB가 이미 있으므로 필요 시 hypertable 전환 여지. ai.router에usageSummary프로시저 (ai:admin스코프) — 조직 관리자용 대시보드(admin) + 전체 조직 뷰(system-admin).monthlyTokenLimit설정 시 초과하면 요청 차단 + 관리자 알림 (기존 notification 파이프라인 재사용).
3. 패키지 구조 (기존 컨벤션 준수)
packages/api/src/services/ai/
client.ts # 프로바이더/키/모델 해석 (조직 설정 → env → 기본값)
agent.service.ts # tool-use 루프, 스트리밍, 대화 조립, usage 기록
providers/ # §1-3 추상화 (anthropic, openai-compat)
tools/
employee.tools.ts # 기능① — read-only HR 집계
docs.tools.ts # 기능④ — 가이드 검색
sheet.tools.ts # 기능② — 시트 구성 제안/적용
scheduler.tools.ts # 기능③ — 자연어 선호 추출
prompts/
system.ts # 안정 시스템 프롬프트 (캐시 친화)
versions.ts # 프롬프트 버전 태그 (§8 평가용)
evals/ # §8 오프라인 평가 하네스
packages/api/src/routers/ai.router.ts # chat, history, usageSummary, 구독
packages/database/src/entities/
ai-conversation.entity.ts # BaseEntity + organizationId
ai-message.entity.ts # role, content, toolCalls, usage, promptVersion, feedback
doc-embedding.entity.ts # §7 pgvector
packages/core/src/schemas/ai.schema.ts # Zod (스키마는 core에 — 하우스 룰)
packages/core/src/enums/ai.enum.ts # AiModule 등
packages/components/src/components/AiAssistant.ts # 공용 Alpine 채팅 위젯 (§4-1)4. 기능① 대화형 HR 어시스턴트 — 가장 먼저 구현
파운데이션(§1~3) 자체가 이 기능이다.
- v1 tool 팩 (전부 read-only):
searchEmployees,getEmployeeDetail,getLeaveBalance,getAttendanceSummary,getPayrollSummary— 각각 기존 라우터/서비스에 위임. - 변경(mutation) tool은 후속 단계에서 confirm 패턴으로만 추가.
4-1. 공용 위젯 [결정 3]
처음부터 packages/components 공용 컴포넌트로 만들어 모든 앱에서 탑재 가능하게 한다 (NotificationBell과 동일 패턴):
- SDK의
ai서비스 + WS 구독으로 통신,Alpine.store("hereby")에서 조직/사용자 컨텍스트 획득. - 마운트 시 앱 컨텍스트 주입:
AiAssistant({ app: "admin", getContext: () => ({ tab, ... }) })— 어느 앱·화면에서 열렸는지 턴별 컨텍스트로 전달 (가이드 지원 §5의 핵심 입력). - 노출 조건: 조직
settings.ai.enabled&& 사용자ai:execute권한. 플래그 off면 렌더 자체를 안 함. - 롤아웃 순서는 admin → hr-system → employee-app (탑재는 앱별 한 줄이므로 순서는 운영 판단).
5. 기능④ 사용자 가이드 지원 — 두 번째, 최저 비용 승리
같은 채팅 에이전트에 tool 하나 추가.
searchDocstool: pgvector 유사도 검색(§7) — 코퍼스는docs/manual/*.md를 청크 단위로 인제스트. 코퍼스가 작은 초기에는 full-text 하이브리드(tsvector)를 병행해 리콜 보완.- 위젯이 주입한 현재 앱·탭 컨텍스트로 "이 화면에서 뭘 해야 해요?" 대응.
- 당시 임베딩 계획은
Voyage이었습니다. 현재 문서 안내 구현은voyage-3확정voyage-context-4, 1024차원이며 현재 색인 구조를 따릅니다.
6. 기능② 시트 자동 구성 / 기능③ 자동 스케줄링
6-1. 시트 자동 구성 — 최고 가치·최고 리스크, propose → approve 필수
프로그래밍 표면은 이미 완성돼 있다: sheetView.create → update(columnLayout) → setAutoPopulate/컬럼별 sourceTable → setComputeConfig → runCompute.
proposeSheetConfigtool: 출력이 기존sheetColumn*Zod 스키마로 검증되는 structured output(strict tool use) — 잘못된 column kind, 미지의SourceTable, 깨진 수식은 DB에 닿기 전에 검증 실패.- 도메인 어휘를 시스템 프롬프트에 그라운딩:
SourceTableenum, column kind(identity/literal/ref/formula/computed/aggregate), 수식 문법([colKey],[a]:[b]범위, SUM/ROUND/IF —sheet-rows/resolver.ts의 그래머) 요약 스펙. - 자동 적용 금지. 흐름: 자연어 요청 → 에이전트가 columnLayout 제안 → 프런트가 프리뷰 렌더(워크북은 레이아웃 렌더를 이미 안다) → 사용자 승인 → 프런트엔드가 통상의
sheetView뮤테이션 호출.commitToRecords(급여 커밋 경로)는 인간 전용 유지 — silent breakage 정책상 데이터 손실이 치명적인 구간. - 템플릿(
sheetTemplate.applyToPeriod)은 더 안전한 상위 동사: "7월 수당 시트 만들어줘"는 from-scratch 레이아웃이 아니라 템플릿 적용으로 처리.
6-2. 자동 스케줄링 — 재구축이 아니라 업그레이드
솔직한 평가: LLM은 hard-constraint 근무표 풀이에 부적합. 결정론적 솔버(schedule-generator.ts)가 계속 권위자여야 하고, AI의 승부처는 그 주변이다.
- 자연어 → 제약 변환: "김OO 목요일 나이트 불가, 이번 달 연차 3일" → 구조화된
NursePreferencerows (기존 스키마 검증, 관리자 확인 후 저장). 실제 마찰은 풀이가 아니라 선호 입력이다. - 설명 & 수선: 생성 후
entry.validate위반 사항을 에이전트가 설명하고 최소 스왑을 제안. - 기존 LLM 경로 마이그레이션:
schedule-ai-generator.ts를 신규 프로바이더 추상화(§1-3)로 이전, 수제parseAndValidate를 structured outputs로 대체. constraint-fallback 패턴은 유지 — 이미 올바른 설계.
7. pgvector 마이그레이션 설계 [당시 계획]
이 절의
doc_embeddings검색·변경 청크만 임베딩하는 설계는 현재 사용자 가이드 안내의 검색 구현이 아닙니다. 현재 색인과 검색 저장소는 S3 Vectors입니다. 아래 초안은 설계 이력으로 보존합니다.
pgvector 0.8.2는 Docker 이미지에 이미 설치·프리로드되어 있다(docker/postgres/Dockerfile). 필요한 것은 extension 활성화 + 테이블 + 인덱스.
7-1. 선행 결정
| 항목 | 내용 |
|---|---|
| 차원(N) | 1024 확정 — Voyage voyage-3 (§9 결정 9). vector(N)은 테이블 생성 시 고정되므로 프로바이더 변경 시 재인제스트 필요 |
| 인덱스 | HNSW + cosine (vector_cosine_ops) — 재빌드 없이 증분 삽입 가능, 수만 청크 규모에 적합. IVFFlat은 대량 삽입 후 빌드가 유리하므로 현 규모에선 불필요 |
| TypeORM 제약 | TypeORM은 vector 컬럼 타입을 모른다 → 마이그레이션은 raw SQL, 엔티티는 임베딩 컬럼을 select: false + raw 타입으로 선언하고 유사도 검색은 dataSource.query() raw SQL로 수행 (ORDER BY embedding <=> $1) |
7-2. 마이그레이션 초안 (컨벤션: snake_case 테이블 / camelCase 컬럼 / gen_random_uuid)
// packages/database/src/migrations/<ts>-AddAiFoundation.ts
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS vector`);
await queryRunner.query(`
CREATE TABLE "doc_embeddings" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"organizationId" uuid, -- NULL = 플랫폼 공용(매뉴얼) 문서
"source" varchar(50) NOT NULL, -- 'manual' (향후 'notice' 등 확장)
"docPath" varchar(255) NOT NULL, -- 예: manual/nurse-scheduler.md
"locale" varchar(10) NOT NULL DEFAULT 'ko',
"chunkIndex" int NOT NULL,
"heading" varchar(255),
"content" text NOT NULL,
"contentHash" varchar(64) NOT NULL, -- 재인제스트 시 변경 감지
"embedding" vector(1024) NOT NULL, -- voyage-3 = 1024차원 (확정)
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now(),
"deletedAt" timestamptz
)
`);
await queryRunner.query(
`CREATE UNIQUE INDEX "uq_doc_embeddings_chunk"
ON "doc_embeddings" ("source", "docPath", "locale", "chunkIndex")
WHERE "deletedAt" IS NULL`,
);
await queryRunner.query(
`CREATE INDEX "idx_doc_embeddings_hnsw"
ON "doc_embeddings" USING hnsw ("embedding" vector_cosine_ops)`,
);
}같은 마이그레이션(또는 인접 마이그레이션)에 ai_conversations / ai_messages 테이블 포함 (usage 컬럼 §2-3).
7-3. 인제스트 & 검색
- 인제스트 스크립트:
packages/api/src/scripts/ingest-docs.ts(기존 scripts 패턴) —docs/manual/*.md를 헤딩 기준 청크(≈500 토큰) →contentHash비교로 변경분만 임베딩 → upsert. 배포 파이프라인 또는 수동 실행. - 검색 쿼리 (docs.tools.ts에서 raw SQL):
SELECT "docPath", "heading", "content",
1 - ("embedding" <=> $1::vector) AS score
FROM "doc_embeddings"
WHERE "deletedAt" IS NULL AND "locale" = $2
AND ("organizationId" IS NULL OR "organizationId" = $3)
ORDER BY "embedding" <=> $1::vector
LIMIT 8- 초기엔 tsvector full-text와 hybrid(RRF 병합)로 리콜 보완 — 한국어 형태소 한계가 있으므로 벡터가 주, full-text는 보조.
8. 성능 평가 & 프롬프트 고도화 [신규 요구사항]
프롬프트를 "고치면 좋아졌는지"를 감으로 판단하지 않기 위한 최소 장치 3층.
8-1. 관측 (모든 호출에 자동 기록)
ai_messages에promptVersion(프롬프트 파일의 버전 태그),model,module, usage,latencyMs저장 — §2-3 metering과 동일 테이블.- tool 호출 성공/검증실패(Zod reject)/에러를
toolCallsJSONB에 기록 → 모듈별 "structured output 검증 실패율"이 프롬프트 품질의 1차 지표. - 실패한 턴도 행으로 남긴다(2026-09-23, 마이그레이션 1921).
ai_messages.status(ok|error) +errorjsonb(AiErrorCode분류 · 메시지 · 프로바이더 status/type/requestID · 우리 requestId · 반복 수). 프로바이더 예외는agent.service가AiTurnFailure(분류 + 부분 본문·부분 usage·toolCalls)로 던지고ai.router가 status=error 행으로 저장한 뒤 TRPCError 로 바꾼다(data.detail.aiMessageId를 위젯에 돌려 「문제 신고」에 첨부). 부분 usage 는 과금된 토큰이므로 집계에 넣고, 실패 행은 모델 히스토리에서 뺀다. 이전에는 실패가 로그에만 남아 오류율이 항상 0% 였다. - 운영 화면: system-admin 「AI 모니터링」 탭(
systemAdminRead.aiSummary/aiConversations/aiConversationDetail/aiErrors, 서비스system-admin-ai.service.ts, 권한ai:read). 모듈별·프롬프트 버전×모델별 턴/오류율/평균·p95 지연/턴당 토큰/도구 실패율/👍👎, 대화 상세(도구 호출 입력·소요), 오류 목록. 티켓 MED-177 ①의 「모듈별 품질 지표 리포트」가 이 화면이다.
8-2. 사용자 피드백 (온라인 평가)
- 채팅 위젯에 응답별 👍/👎 + 선택 코멘트 →
ai_messages.feedback컬럼. 시트 제안은 "승인/수정 후 승인/거부" 자체가 암묵 피드백 — 제안 대비 최종 적용 layout의 diff 크기를 기록. admin 대시보드(usageSummary 확장)에서system-admin AI 모니터링 요약에서 모듈·프롬프트 버전별 피드백 비율 조회(2026-09-23). 위젯의 👍/👎 UI 는 2026-09-04 결정으로 감춰져 있어 값이 쌓이려면 되살려야 한다.
8-3. 오프라인 평가 하네스 (services/ai/evals/)
- 모듈별 golden set (JSON fixture): 예)
sheet_config— 자연어 요청 → 기대 columnLayout(핵심 필드만 부분 매칭),scheduler— 자연어 선호 문장 → 기대 NursePreference rows,docs— 질문 → 기대 docPath 포함 여부. - Vitest 러너로 실행하되 CI 기본 제외(비용·비결정성) —
pnpm eval:ai수동/주기 실행. 결과를 프롬프트 버전과 함께 기록해 회귀 비교. - 프롬프트 변경 절차:
prompts/versions.ts버전 bump → eval 실행 → 통과율 비교 후 머지. 판정이 필요한 항목(응답 품질)은 LLM-judge 채점, 구조 비교(스키마 일치)는 코드 채점. - 모델 비교(경량 모델 전환 판단, MED-177 ②):
pnpm eval:ai -- --org=HEREBY_DEMO --model=<모델 id>가 조직 오버라이드·env 를 건너뛰고 그 모델로 돌린다(runChatTurn.modelOverride). 결과 파일evals/results/<date>-<promptVersion>-<model>.json의summary(통과율·평균 토큰·평균 지연)를 기본 모델 결과와 나란히 놓고 결정을 기록한다.
9. 결정 사항 (전체 확정, 2026-07-10)
- 조직별
settings.ai.enabled플래그, 기본 off — Phase 0부터 포함 (§2-1). - openai-client는 신규 프로바이더 추상화로 흡수 — 레거시 병존 없음 (§1-3).
- 위젯은 공용 컴포넌트 — packages/components, 모든 앱 탑재 가능, 앱 컨텍스트 주입 (§4-1).
- 모듈별 경량 모델 전환 가능 — AiModule 단위 모델 해석, 조직 오버라이드 포함 (§1-3).
- 조직별 사용량 측정 — 메시지 단위 usage 기록 + 집계 + 상한 (§2-3).
- BYOK — ExternalIntegrationCredential 재사용, 고객 키 우선 (§2-2).
- 평가/프롬프트 고도화 체계 — 관측 + 피드백 + 오프라인 eval (§8).
- pgvector 도입 — §7 마이그레이션 설계대로.
- **당시 임베딩 결정: Voyage
voyage-3(1024차원)**이었습니다. 현재 문서 안내는voyage-context-4를 사용하므로 현행 문서를 기준으로 합니다. - BYOK 키 암호화: 기존 인프라 재사용으로 해결 —
encryptedColumn()transformer(AES-256-GCM)가 이미 적용되어 있어 추가 작업 없음 (§2-2). - 모델은 전 모듈
claude-opus-4-8로 시작 — 경량(Haiku) 전환은 eval(§8) 데이터를 보고 판단.
10. 단계별 계획
| 단계 | 범위 | 산출물 |
|---|---|---|
| 0 | AI 파운데이션 | SDK + 프로바이더 추상화 + services/ai/ + 엔티티(conversation/message/embedding) + 마이그레이션(§7) + ai.router + ai 권한 + org 플래그 + 사용량 기록 + WS 스트리밍 |
| 1 | 기능①+④ | 공용 채팅 위젯(전 앱 탑재 가능) + read-only tool 팩 + docs 인제스트/검색 + 피드백 수집 + admin 사용량/피드백 뷰 |
| 2 | 기능② + BYOK | 시트 propose→preview→approve 흐름 / BYOK 설정 UI + 키 해석 / eval 하네스 1차(golden set) |
| 3 | 기능③ | 자연어 선호 추출 + schedule-ai-generator 프로바이더 추상화 이전 + 설명/수선 tool. openai-client 삭제 |
0+1이 기존 기능 PR 1개 규모. 2가 프리뷰 UX 때문에 가장 크다.
11. 관련 문서
- 시트 아키텍처: docs/guide/sheet-architecture.md
- 사용자 매뉴얼(RAG 코퍼스): docs/manual/
- 급여 스키마 리팩터: docs/packages/reference/payroll-schema-refactor.md