From 1b3a0a56323362986a8840e62b8ea733114a83c9 Mon Sep 17 00:00:00 2001 From: happybell80 Date: Mon, 16 Mar 2026 09:33:58 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20LLM=20=EB=AA=A8=EB=8D=B8=20SSOT=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20=EA=B3=84=ED=9A=8D=20=EB=B0=8F=20workspace?= =?UTF-8?q?-config=20=EB=A6=AC=EC=84=9C=EC=B9=98=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- ...260316_llm_model_ssot_transition_계획.md | 62 ++++++++++++- ...ace_config_로컬이식_통합리서치.md | 92 +++++++++++++++++++ 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/journey/plans/260316_llm_model_ssot_transition_계획.md b/journey/plans/260316_llm_model_ssot_transition_계획.md index 92ab7bd..b1e8533 100644 --- a/journey/plans/260316_llm_model_ssot_transition_계획.md +++ b/journey/plans/260316_llm_model_ssot_transition_계획.md @@ -24,8 +24,16 @@ - `rb8001`의 모델 기본값/직접 handler 생성/직접 Gemini SDK 호출/fallback 배열 정리 - `skill-slack`의 하드코딩 모델 문자열 정리 +- 전수 확인으로 드러난 `rb8001`의 추가 직접 생성 경로 정리 +- `skill-news`의 독자 `GEMINI_MODEL` SSOT와 직접 Gemini SDK 호출 정리 - 모델 변경 검증 체크리스트 고정 +### 포함 근거 + +- 리서치 기준 `skill-news`는 단순 모델 문자열 1~2개가 아니라, `README`, `.env.example`, `docker-compose.yml`, 서비스 코드, API 모듈 import 시점 초기화가 함께 `.env`의 `GEMINI_MODEL`을 독자 SSOT로 강화하고 있습니다. +- 따라서 `skill-news`를 범위 밖으로 두면 로빙 전체 관점에서 "주 모델 변경이 SSOT 1회 수정으로 닫힌다"는 목표를 충족할 수 없습니다. +- 특히 `skill-news/app/api/news_endpoints.py`의 `news_summarizer = NewsSummarizer()`는 프로세스 시작 시점에 현재 모델 계약을 고정하므로, 단순 문서 수정이나 compose 수정만으로는 충분하지 않습니다. + ### 제외 - 새 provider registry 파일 추가 @@ -53,6 +61,10 @@ - 직접 `genai.GenerativeModel(...)` 생성 제거 - [rb8001/app/services/ir_analyzer.py](../../../../rb8001/app/services/ir_analyzer.py) - 파일 내부 fallback 배열 제거 또는 중앙 정책으로 이동 +- [rb8001/app/services/diary/generator.py](../../../../rb8001/app/services/diary/generator.py) + - 직접 `GeminiHandler()` 생성 제거 검토 +- [rb8001/app/services/workflows/headlines_workflow.py](../../../../rb8001/app/services/workflows/headlines_workflow.py) + - `or GeminiHandler(default_model)` 우회 제거 ### 3. 스킬 계층 정리 @@ -63,6 +75,38 @@ - [skill-slack/app/core/config.py](../../../../skill-slack/app/core/config.py) - 필요 최소한의 기본 모델 설정만 추가 검토합니다. +### 4. skill-news 정리 + +- [skill-news/app/services/news_summarizer.py](../../../../skill-news/app/services/news_summarizer.py) +- [skill-news/app/services/companyx_news_summarizer.py](../../../../skill-news/app/services/companyx_news_summarizer.py) +- [skill-news/app/services/sea_news_filter.py](../../../../skill-news/app/services/sea_news_filter.py) + - 직접 `genai.GenerativeModel(...)` 생성과 `GEMINI_MODEL` 독자 SSOT를 정리합니다. +- [skill-news/app/api/news_endpoints.py](../../../../skill-news/app/api/news_endpoints.py) + - import 시점 `NewsSummarizer()` 생성이 어떤 설정 계약을 고정하는지 정리합니다. +- [skill-news/docker-compose.yml](../../../../skill-news/docker-compose.yml) + - 모델 주입을 `workspace-config/runtime.env` 기준으로 재해석할지 확정합니다. +- [skill-news/README.md](../../../../skill-news/README.md) +- [skill-news/.env.example](../../../../skill-news/.env.example) + - 문서 SSOT 표현을 코드/운영 계약과 맞춥니다. + +## 수정 규모 기준 + +### 1. 최소 직접 수정 범위 + +- `rb8001` 9개 내외 +- `skill-slack` 4개 내외 +- `skill-news` 6개 내외 + - 서비스 코드 4개 + - compose 1개 + - 문서/예시 2개 중 계약상 필수 범위 포함 +- 문서/테스트 제외 최소 직접 수정 예상은 `19개 내외`로 봅니다. + +### 2. 완전 종결 범위 + +- 위 최소 범위 외에도 compose의 `.env` 오버라이드, `workspace-config` 주입 순서, 서비스별 예외를 더 확인해야 합니다. +- `skill-news`는 독자 제품 SSOT처럼 운영돼 온 흔적이 있어, 계획 실행 중 예외 선언으로 분리할지 통합 전환할지 정책 판단이 추가로 필요할 수 있습니다. +- 따라서 이 계획은 `핵심 SSOT 경로 복구 계획`이고, `로빙 전체 workspace-config 완전 종결 계획`과는 범위를 구분합니다. + ## 정책 판단 ### 1. 유지 가능한 정책 @@ -77,6 +121,8 @@ - router/service의 직접 handler 생성 - 서비스 파일 안의 직접 Gemini/OpenAI SDK 생성 - `skill-slack`의 고정 모델 문자열 +- `skill-news`의 `.env` 기반 독자 모델 SSOT 표현 +- import 시점 초기화가 특정 모델 계약을 고정하는 구조 ### 3. 추가하지 않을 것 @@ -104,25 +150,31 @@ - `genai.GenerativeModel(...)` 직접 생성이 공용 경로 밖에서 사라집니다. - `ir_analyzer.py`의 파일 내부 fallback 배열이 제거되거나 중앙 정책으로 이동합니다. - `skill-slack`의 하드코딩 모델 문자열이 제거됩니다. +- `diary/generator.py`, `headlines_workflow.py`의 직접 `GeminiHandler` 생성이 제거됩니다. +- `skill-news`의 직접 Gemini SDK 생성과 `GEMINI_MODEL` 독자 SSOT가 정리됩니다. +- `skill-news/app/api/news_endpoints.py`의 import 시점 초기화가 새 계약과 모순되지 않게 정리됩니다. ## 적용 순서 1. `rb8001` 공용 경로 정리 - `config.py`, `llm_service.py`, `gemini_handler.py` 2. `rb8001` 우회 경로 제거 - - `llm_endpoint.py`, `coldmail_llm_classifier.py`, `ir_analyzer.py` + - `llm_endpoint.py`, `coldmail_llm_classifier.py`, `ir_analyzer.py`, `diary/generator.py`, `workflows/headlines_workflow.py` 3. `skill-slack` 모델 선택 정리 - `digest.py`, `action_extractor.py`, `summarizer.py`, 필요 시 `app/core/config.py` -4. 기본값 변경 검증 +4. `skill-news` 모델 경로 정리 + - `news_summarizer.py`, `companyx_news_summarizer.py`, `sea_news_filter.py`, `docker-compose.yml`, `README.md`, `.env.example` +5. 기본값 변경 검증 - `workspace-config/runtime.env` - 과도기 `rb8001/.env` - API 입력 `model` override -5. worklog 작성 후 닫힘 선언 +6. worklog 작성 후 닫힘 선언 ## 레포별 경계 - 1차 대상 레포: `robeing/rb8001` - 2차 대상 레포: `robeing/skill-slack` +- 3차 대상 레포: `robeing/skill-news` - 문서/검증 기록 레포: `robeing/DOCS` - 한 번에 여러 레포를 한 커밋으로 묶지 않습니다. @@ -130,6 +182,7 @@ - `rb8001` 변경은 `rb8001` 레포 안에서만 롤백 판단합니다. - `skill-slack` 변경은 `skill-slack` 레포 안에서만 롤백 판단합니다. +- `skill-news` 변경은 `skill-news` 레포 안에서만 롤백 판단합니다. - 문서 변경은 `DOCS` 레포 안에서만 롤백 판단합니다. - 과도기에는 `workspace-config/runtime.env`와 `rb8001/.env`를 이전 값으로 되돌리는 것이 가장 작은 운영 롤백 단위입니다. @@ -139,4 +192,5 @@ 2. 과도기에도 `workspace-config/runtime.env`와 `rb8001/.env`까지만 수정하면 됩니다. 3. API 입력 모델명으로 요청 단위 override가 가능합니다. 4. 리서치 문서에 적힌 직접 우회 경로가 실제 코드에서 제거됩니다. -5. 닫힘 선언은 `worklog`에서만 합니다. +5. `skill-news`까지 포함해 독자 모델 SSOT 표현이 정리됩니다. +6. 닫힘 선언은 `worklog`에서만 합니다. diff --git a/journey/research/260315_모델SSOT_하드코딩_분산과_workspace_config_로컬이식_통합리서치.md b/journey/research/260315_모델SSOT_하드코딩_분산과_workspace_config_로컬이식_통합리서치.md index ac811f5..6db97a3 100644 --- a/journey/research/260315_모델SSOT_하드코딩_분산과_workspace_config_로컬이식_통합리서치.md +++ b/journey/research/260315_모델SSOT_하드코딩_분산과_workspace_config_로컬이식_통합리서치.md @@ -229,6 +229,47 @@ tags: [research, llm, ssot, workspace-config, hardcoding, robeing] - [skill-slack/app/core/config.py](../../../../skill-slack/app/core/config.py) - 현재 LLM 기본 모델 설정 자체가 없습니다. +### 6. 전수 확인으로 추가 확인된 누락 범위 + +- [rb8001/app/services/diary/generator.py](../../../../rb8001/app/services/diary/generator.py) + - `GeminiHandler()`를 직접 생성합니다. + - 기본 모델은 간접적으로 따라가더라도, 호출 진입점이 `LLMService` 단일 경로로 닫히지 않았습니다. +- [rb8001/app/services/workflows/headlines_workflow.py](../../../../rb8001/app/services/workflows/headlines_workflow.py) + - `llm_service.handlers.get(default_model) or GeminiHandler(default_model)` 경로가 남아 있습니다. + - 기본값은 `settings.DEFAULT_LLM_MODEL`을 보더라도 handler 직접 생성 우회가 살아 있습니다. +- [skill-news/app/services/news_summarizer.py](../../../../skill-news/app/services/news_summarizer.py) + - `os.getenv("GEMINI_MODEL", "gemini-2.5-flash-lite")` 기본값 문자열과 직접 `genai.GenerativeModel(...)` 생성이 남아 있습니다. +- [skill-news/app/services/companyx_news_summarizer.py](../../../../skill-news/app/services/companyx_news_summarizer.py) + - `os.getenv("GEMINI_MODEL", "gemini-2.5-flash-lite")` 기본값 문자열과 직접 `genai.GenerativeModel(...)` 생성이 남아 있습니다. +- [skill-news/app/services/sea_news_filter.py](../../../../skill-news/app/services/sea_news_filter.py) + - `GEMINI_MODEL`을 독자 SSOT처럼 요구하고 직접 `genai.GenerativeModel(...)`를 생성합니다. +- [skill-news/app/api/news_endpoints.py](../../../../skill-news/app/api/news_endpoints.py) + - 모듈 import 시점에 `news_summarizer = NewsSummarizer()`가 즉시 생성됩니다. + - 즉 `skill-news`는 단순 설정 조회가 아니라, 프로세스 시작 시점부터 `.env`의 `GEMINI_MODEL` 계약을 전제로 동작합니다. +- [skill-news/docker-compose.yml](../../../../skill-news/docker-compose.yml) + - 현재 `workspace-config/runtime.env`를 읽지 않고 `${GEMINI_MODEL}`을 직접 전달합니다. +- [skill-news/README.md](../../../../skill-news/README.md) + - 모델명 SSOT를 `.env`의 `GEMINI_MODEL`로 선언하고 있어, 현재 로빙 전체 목표 SSOT와 충돌합니다. +- [skill-news/.env.example](../../../../skill-news/.env.example) + - `GEMINI_MODEL` 예시값을 별도 SSOT처럼 유지합니다. + +### 7. compose / env_file 계층 확인 결과 + +- [rb8001/docker-compose.yml](../../../../rb8001/docker-compose.yml) + - `env_file`에 `.env`, `/home/admin/workspace-config/runtime.env`, `/home/admin/workspace-config/secrets.env`를 함께 읽습니다. + - 즉 구조는 workspace-config를 쓰지만, 서비스별 `.env` 오버라이드가 아직 실제 경로에 남아 있습니다. +- [skill-slack/docker-compose.yml](../../../../skill-slack/docker-compose.yml) + - `env_file`에 `.env`, `/home/admin/workspace-config/runtime.env`, `/home/admin/workspace-config/secrets.env`를 함께 읽습니다. +- [skill-rag-file/docker-compose.yml](../../../../skill-rag-file/docker-compose.yml) + - `env_file` 3중 구조와 별도 `./.env:/app/.env:ro` 마운트가 함께 있습니다. + - 로빙 전체 SSOT 문제를 닫을 때 `.env` 오버라이드가 실제 런타임 우선순위를 어떻게 바꾸는지 별도 점검이 필요합니다. +- [skill-calendar/docker-compose.yml](../../../../skill-calendar/docker-compose.yml) +- [skill-email/docker-compose.yml](../../../../skill-email/docker-compose.yml) +- [robeing-monitor/docker-compose.yml](../../../../robeing-monitor/docker-compose.yml) +- [robeing-gateway/docker-compose.yml](../../../../robeing-gateway/docker-compose.yml) + - 공통으로 `workspace-config`와 서비스별 `.env`를 함께 읽는 구조가 남아 있습니다. + - 따라서 `LLM 모델 SSOT`를 넘어 `서비스별 .env 오버라이드` 문제까지 완전 종결하려면 후속 점검 범위가 더 큽니다. + ## 확인 사실 요약표 | 항목 | 확인 사실 | 해석 | @@ -242,6 +283,10 @@ tags: [research, llm, ssot, workspace-config, hardcoding, robeing] | API 입력 model | `rb8001/app/services/llm/models.py`의 `LLMRequest.model` 존재 | override 구조는 이미 있음 | | OpenAI handler | `rb8001/app/services/llm/openai_handler.py` 존재 | provider 추상화 일부 존재 | | skill-slack 모델 선택 | `digest.py`, `action_extractor.py`, `summarizer.py`에 모델 문자열 존재 | 설정화 또는 정책화 필요 | +| rb8001 추가 직접 생성 | `diary/generator.py`, `workflows/headlines_workflow.py`에 `GeminiHandler` 직접 생성 잔존 | 계획 누락 | +| skill-news 독자 SSOT | `skill-news`가 `.env`의 `GEMINI_MODEL`을 자체 SSOT로 유지 | 로빙 전체 SSOT와 충돌 | +| skill-news 초기화 시점 | `news_endpoints.py`가 import 시점에 `NewsSummarizer()` 생성 | 런타임 계약이 프로세스 시작 시점에 고정 | +| compose env 계층 | 여러 서비스가 `workspace-config`와 `.env`를 함께 읽음 | 구조는 있으나 오버라이드 잔존 | ## 최소 변경 원칙 @@ -288,6 +333,49 @@ tags: [research, llm, ssot, workspace-config, hardcoding, robeing] - [skill-slack/app/services/action_extractor.py](../../../../skill-slack/app/services/action_extractor.py) - [skill-slack/app/services/summarizer.py](../../../../skill-slack/app/services/summarizer.py) - 하드코딩 모델 문자열을 설정값 또는 API 입력값으로 승격 +- [rb8001/app/services/diary/generator.py](../../../../rb8001/app/services/diary/generator.py) + - 직접 `GeminiHandler()` 생성 제거 검토 +- [rb8001/app/services/workflows/headlines_workflow.py](../../../../rb8001/app/services/workflows/headlines_workflow.py) + - `or GeminiHandler(default_model)` 우회 제거 +- [skill-news/app/services/news_summarizer.py](../../../../skill-news/app/services/news_summarizer.py) +- [skill-news/app/services/companyx_news_summarizer.py](../../../../skill-news/app/services/companyx_news_summarizer.py) +- [skill-news/app/services/sea_news_filter.py](../../../../skill-news/app/services/sea_news_filter.py) + - `GEMINI_MODEL` 독자 SSOT와 직접 Gemini SDK 생성을 정리 +- [skill-news/app/api/news_endpoints.py](../../../../skill-news/app/api/news_endpoints.py) + - import 시점 인스턴스 생성이 어떤 설정 계약을 고정하는지 함께 정리 +- [skill-news/docker-compose.yml](../../../../skill-news/docker-compose.yml) + - `workspace-config/runtime.env` 기준 모델 주입으로 맞출지 검토 +- [skill-slack/app/core/config.py](../../../../skill-slack/app/core/config.py) + - LLM 기본 모델 설정 경로를 둘지 여부 확정 필요 + +### 1-1. 현재 전수 확인 기준 최소 직접 수정 규모 + +- `rb8001` 9개 내외 + - `app/core/config.py` + - `app/services/llm/llm_service.py` + - `app/services/llm/gemini_handler.py` + - `app/router/llm_endpoint.py` + - `app/services/coldmail_llm_classifier.py` + - `app/services/ir_analyzer.py` + - `app/services/diary/generator.py` + - `app/services/workflows/headlines_workflow.py` + - 필요 시 `app/services/llm/models.py` +- `skill-slack` 4개 내외 + - `app/services/digest.py` + - `app/services/action_extractor.py` + - `app/services/summarizer.py` + - `app/core/config.py` +- `skill-news` 6개 내외 +- `skill-news`는 서비스 코드 4개 + compose 1개 + 문서/예시 2개까지 걸쳐 있어, 코드 수정만으로 끝나지 않고 계약 문서도 같이 바뀌어야 합니다. + - `app/services/news_summarizer.py` + - `app/services/companyx_news_summarizer.py` + - `app/services/sea_news_filter.py` + - `app/api/news_endpoints.py` + - `docker-compose.yml` + - `README.md` + - `.env.example` +- 따라서 2026-03-16 전수 확인 기준으로, 문서/테스트 제외 최소 직접 수정 예상은 `19개 내외`로 보는 편이 맞습니다. +- 특히 `skill-news`는 `.env` 기반 독자 SSOT를 README와 compose, 서비스 초기화 코드가 함께 강화하고 있으므로, 단순 코드 치환보다 "계약 전환"으로 다루는 편이 맞습니다. ### 2. 굳이 지금 추가하지 않아도 되는 것 @@ -310,6 +398,8 @@ tags: [research, llm, ssot, workspace-config, hardcoding, robeing] ### 2. 로빙의 현재 모델 구조 문제도 같은 유형입니다 - `DEFAULT_LLM_MODEL`은 선언돼 있지만, 실제 경로는 `GeminiHandler` 직접 생성, Gemini SDK 직접 호출, `gemini-*` fallback 리스트, 서비스별 개별 모델 하드코딩이 섞여 있습니다. +- `skill-news`는 이 문제의 바깥 예외가 아니라, 별도 제품 SSOT를 이미 갖고 있는 하위 서비스입니다. +- 그래서 로빙 전체 모델 SSOT를 하나로 묶으려면 `skill-news`를 예외로 선언하거나, 아니면 README/compose/service 초기화까지 함께 계약 전환해야 합니다. - 그래서 `주 모델 변경`이 `SSOT 1개 값 변경`으로 닫히지 않고, 여러 서비스의 코드 경로를 다시 건드려야 하는 구조가 됩니다. - 즉 지금의 직접 원인은 `gpt-5-mini` 전환 작업이고, 근본 원인은 `모델 호출 계약의 SSOT`와 `실제 호출 구현`이 분리되지 않은 점입니다. - 이 해석은 서버와 로컬 모두 동일합니다. 서버별 모델 차이가 필요해도 그것은 코드 차이가 아니라 `runtime.env` 값 차이로만 표현돼야 합니다. @@ -322,6 +412,7 @@ tags: [research, llm, ssot, workspace-config, hardcoding, robeing] - 이 기준이 닫혀야 `DEFAULT_LLM_MODEL` 같은 값이 실제 SSOT가 됩니다. - 닫혀야 할 최종 경로는 `runtime.env -> settings.DEFAULT_LLM_MODEL -> 단일 LLM factory/service -> handler`입니다. - `GEMINI_MODEL`, 코드 기본값 문자열, 요청별 모델 직접 주입, 서비스별 fallback 배열은 모두 이 경로의 우회로로 봐야 합니다. +- `skill-news`처럼 README와 `.env.example`, compose, import 시점 초기화 코드가 함께 특정 키를 독자 SSOT로 강화하는 경우도 같은 우회로로 봐야 합니다. - 사용자 작업 기준으로는 목표 수정 지점과 과도기 최소 수정 지점을 함께 고정해야 합니다. - 목표 수정 지점: `workspace-config/runtime.env` - 과도기 최소 수정 지점: `workspace-config/runtime.env`와 `rb8001/.env` @@ -334,6 +425,7 @@ tags: [research, llm, ssot, workspace-config, hardcoding, robeing] - 서비스별 `.env`와 코드 fallback 중 무엇이 아직 실제 런타임 경로에 남아 있는지 전체 인벤토리 - 로컬 기준 공용 파일만 읽도록 바꿀 때, 프로젝트별 예외가 필요한 서비스가 있는지 여부 - 서버 기준 공용 파일만 읽도록 바꿀 때, 운영상 예외가 필요한 서비스가 있는지 여부 +- `skill-news`를 로빙 전체 모델 SSOT 전환 범위에 포함할지, 아니면 별도 제품 SSOT 예외로 둘지 최종 정책 확정 ## workspace-config 로컬 이식 기준