Skip to content

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는 없음
pgvectorDocker 이미지에 설치됨(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]:

ts
// packages/core/src/enums/ai.enum.ts
export enum AiModule {
  CHAT = "chat",              // 기능① HR 어시스턴트
  DOCS = "docs",              // 기능④ 가이드 지원
  SHEET_CONFIG = "sheet_config", // 기능② 시트 구성
  SCHEDULER = "scheduler",    // 기능③ 스케줄링
}

모델 해석 순서 (모듈 단위): 조직 설정 오버라이드 → env 설정 → 모듈 기본값

모듈기본 모델경량 대안 (설정으로 전환)
chatclaude-opus-4-8— (품질 민감)
docsclaude-opus-4-8claude-haiku-4-5 ($1/$5) — 검색+요약은 경량 후보 1순위
sheet_configclaude-opus-4-8— (정확성 민감, structured output)
schedulerclaude-opus-4-8claude-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 확장 — 마이그레이션 불필요):
ts
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 하나 추가.

  • searchDocs tool: 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.

  • proposeSheetConfig tool: 출력이 기존 sheetColumn* Zod 스키마로 검증되는 structured output(strict tool use) — 잘못된 column kind, 미지의 SourceTable, 깨진 수식은 DB에 닿기 전에 검증 실패.
  • 도메인 어휘를 시스템 프롬프트에 그라운딩: SourceTable enum, 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일" → 구조화된 NursePreference rows (기존 스키마 검증, 관리자 확인 후 저장). 실제 마찰은 풀이가 아니라 선호 입력이다.
  • 설명 & 수선: 생성 후 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) ​

ts
// 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):
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)/에러를 toolCalls JSONB에 기록 → 모듈별 "structured output 검증 실패율"이 프롬프트 품질의 1차 지표.
  • 실패한 턴도 행으로 남긴다(2026-09-23, 마이그레이션 1921). ai_messages.status(ok|error) + error jsonb(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) ​

  1. 조직별 settings.ai.enabled 플래그, 기본 off — Phase 0부터 포함 (§2-1).
  2. openai-client는 신규 프로바이더 추상화로 흡수 — 레거시 병존 없음 (§1-3).
  3. 위젯은 공용 컴포넌트 — packages/components, 모든 앱 탑재 가능, 앱 컨텍스트 주입 (§4-1).
  4. 모듈별 경량 모델 전환 가능 — AiModule 단위 모델 해석, 조직 오버라이드 포함 (§1-3).
  5. 조직별 사용량 측정 — 메시지 단위 usage 기록 + 집계 + 상한 (§2-3).
  6. BYOK — ExternalIntegrationCredential 재사용, 고객 키 우선 (§2-2).
  7. 평가/프롬프트 고도화 체계 — 관측 + 피드백 + 오프라인 eval (§8).
  8. pgvector 도입 — §7 마이그레이션 설계대로.
  9. **당시 임베딩 결정: Voyage voyage-3 (1024차원)**이었습니다. 현재 문서 안내는 voyage-context-4를 사용하므로 현행 문서를 기준으로 합니다.
  10. BYOK 키 암호화: 기존 인프라 재사용으로 해결 — encryptedColumn() transformer(AES-256-GCM)가 이미 적용되어 있어 추가 작업 없음 (§2-2).
  11. 모델은 전 모듈 claude-opus-4-8로 시작 — 경량(Haiku) 전환은 eval(§8) 데이터를 보고 판단.

10. 단계별 계획 ​

단계범위산출물
0AI 파운데이션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. 관련 문서 ​