diff --git a/.gitignore b/.gitignore index 2ceca6e..d7757d6 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ wheels/ *.mmd .idea .omc + +# assignments +assignments/latteeea/week3/cache/ \ No newline at end of file diff --git a/assignments/latteeea/week3/data/X-Username.md b/assignments/latteeea/week3/data/X-Username.md new file mode 100644 index 0000000..755b60d --- /dev/null +++ b/assignments/latteeea/week3/data/X-Username.md @@ -0,0 +1,142 @@ +## **왜 고치니까 잘 됐는지 (상세 정리)** + +### **1. 클라이언트가 하고 있던 일** + +이전 코드는 대략 이렇게 동작했습니다. + +- **`GET /api/users/profile`** + - `Authorization: Bearer <액세스토큰>` + - 여기에 더해 **`X-Username: `** 를 붙였습니다. +- **`GET /api/users/me/agreements`** + - URL 안에 이미 **`/me/`** 가 있어서 “**지금 로그인한 나**”를 가리키는 전형적인 REST 패턴인데, + - 마찬가지로 **`X-Username`** 을 넘기는 경우가 있었습니다 (`getUserAgreements({ username })`). + +즉, **“나 자신”을 조회하는 API**인데도, **헤더로 또 한 번 “이 사용자 이름이다”라고 보내는 이중 정보**가 있었습니다. + +--- + +### **2. 서버 입장에서 어떻게 해석될 수 있는지** + +백엔드 구현에 따라 다르지만, 흔한 패턴은 다음과 같습니다. + +1. **JWT만 본다** + - 토큰 안의 `sub` / `user_id` 등으로 “누구인지” 결정하고, 그 사용자의 프로필·약관 동의를 돌려준다. +2. **`X-Username`이 있으면 그걸 우선한다 / 검증한다** + - “헤더에 적힌 username으로 프로필을 찾는다” + - 또는 “JWT의 사용자와 **헤더의 username이 같은지** 검사한다” +3. **불일치·잘못된 값이면 404** + - “해당 사용자를 찾을 수 없음”으로 처리하는 API도 있습니다 (401이 아니라 404인 경우). + +그래서 **토큰은 유효한데** (`/api/auth/me` 200), **프로필/약관만 404**가 나는 현상이 나올 수 있습니다. + +--- + +### **3. 왜 iOS는 되고 안드로이드만 안 됐을 가능성이 큰지** + +**같은 JS 코드**인데 한쪽만 깨지는 이유는, 보통 **“보내는 문자열이 미묘하게 다르다”** 쪽입니다. + +- **`X-Username`에 실려 가는 값**은 `AsyncStorage`에서 꺼낸 `username`입니다. +- 저장 시점은 로그인 직후 `setItem('@username', trimmedUsername)` 등인데, + - 예전에 다른 키(`username`, `@insole_app:username`)에 **다른 형태**로 저장된 값이 남았거나, + - **앞뒤 공백**, **보이지 않는 문자**, **대소문자** 등이 OS/스토리지 구현 차이로 달라질 수 있습니다. +- `getUsername()`은 **여러 키를 순서대로** 보는데, **첫 번째로 읽힌 값**이 “로그인한 사람과 같은 사람”이 아닐 수도 있습니다. + +이때: + +- JWT 안의 사용자 = **방금 로그인한 계정** (서버는 이걸로 `/auth/me`는 성공) +- `X-Username` = **다른 문자열** 또는 **서버 DB에 없는 조합** + +이면 서버가 “이 username으로는 프로필을 못 찾겠다” → **404**를 줄 수 있습니다. + +iOS에서는 우연히 **같은 키·같은 문자열**이 잘 맞았고, 안드로이드에서는 **스토리지/키 우선순위/문자열**이 달라져서 **불일치**가 났을 가능성이 큽니다. + +(재설치 후에도, 앱이 쓰는 키가 여러 개라면 **다른 경로로 예전 값이 들어가는** 경우도 생깁니다.) + +--- + +### **4. 이번 수정이 맞는 이유 (본질적인 정리)** + +| **API** | **의미** | +| --- | --- | +| `/api/users/profile` (본인 조회) | 보통 **JWT만**으로 “현재 사용자”를 특정합니다. | +| `/api/users/me/agreements` | URL의 **`me`** 가 이미 “토큰의 나”를 의미합니다. | + +여기에 **`X-Username`을 추가로 보내는 것**은: + +- 서버가 이를 **무시**하면 다행이지만, +- **검증/우선 적용**에 쓰이면, JWT와 **한 글자라도 어긋날 때** 이상 동작(404 등)을 유발할 수 있습니다. + +그래서 이번에 한 것은: + +1. **`getProfile()`** + - **`Authorization`만** 사용 → “토큰이 가리키는 사용자”의 프로필만 조회. + - AsyncStorage username에 의존하지 않음 → **OS별 저장값 차이**에 덜 흔들림. +2. **`getUserAgreements()`** + - **`/users/me/...`** 는 **JWT 기준 ‘나’**만 보면 되므로 `username` 옵션(→ `X-Username`) 제거. +3. **`getUsername()`의 `trim()`** + - 나중에 다른 API에서 `X-Username`이 필요할 때, **공백만 다른 값**으로 잘못 나가는 것을 줄이기 위한 방어. + +정리하면, **문제의 핵심은 “네트워크가 안드로이드만 막혔다”가 아니라, “내 정보 조회인데 `X-Username`으로 인한 사용자 식별 불일치 가능성”**이었고, **`/me`·본인 프로필은 JWT만 쓰도록 맞춘 것**이 해결책이 된 것입니다. + +- 기존 → 변경 코드 + - utils/api/profile.ts + + ```tsx + export async function getProfile(): Promise> { + const username = await getUsername(); + if (!username) { + throw new Error('사용자 이름을 찾을 수 없습니다.'); + } + + return apiGet(ENDPOINTS.USERS.PROFILE, { + username, + }); + } + + // 바꾼 버전 + export async function getProfile(): Promise> { + return apiGet(ENDPOINTS.USERS.PROFILE); + ``` + + - app/(tabs)/profile/index.tsx + + ```tsx + if (foundUsername) { + try { + const agreementsResponse = await getUserAgreements({ username: foundUsername }); + if (agreementsResponse.success && agreementsResponse.data) { + const agreedTerms = agreementsResponse.data.agreements.filter( + (agreement) => agreement.agreed + ); + setUserAgreements(agreedTerms); + } + } catch (error) { + const err = error as ApiError; + if (err?.requiresAdditionalInfo === true && Array.isArray(err.missingFields) && err.missingFields.length > 0) { + setAdditionalInfoResume(err.missingFields); + router.replace(getFirstAdditionalInfoRoute(err.missingFields) as any); + return; + } + console.error('약관 동의 상태 조회 실패:', error); + } + + + // 바꾼 버전 + + try { + const agreementsResponse = await getUserAgreements(); + if (agreementsResponse.success && agreementsResponse.data) { + const agreedTerms = agreementsResponse.data.agreements.filter( + (agreement) => agreement.agreed + ); + setUserAgreements(agreedTerms); + } + } catch (error) { + const err = error as ApiError; + if (err?.requiresAdditionalInfo === true && Array.isArray(err.missingFields) && err.missingFields.length > 0) { + setAdditionalInfoResume(err.missingFields); + router.replace(getFirstAdditionalInfoRoute(err.missingFields) as any); + return; + ``` + + - X-Username 검증 부분 제거 \ No newline at end of file diff --git a/assignments/latteeea/week3/data/android_build_error.md b/assignments/latteeea/week3/data/android_build_error.md new file mode 100644 index 0000000..9a89392 --- /dev/null +++ b/assignments/latteeea/week3/data/android_build_error.md @@ -0,0 +1,97 @@ +> +> +> - 원인: 일부 파일/폴더가 다른 소유자/권한 상태로 남아있어서(복사, 압축 해제, 깃/툴, 권한 상속 끊김 등) EAS가 접근 실패 +> - 해결: 프로젝트 루트에 대해 **권한 상속 활성화 + 내 계정 Full Control 재부여** + +ondrive/desktop 에 프로젝트 폴더 사용하다가 + +onedrive 동기화 되는게 귀찮고 폴더 경로에 따른 권한 에러가 많이 나서 ondrive 동기화를 중지 시킴 + +하고 ondrive/desktop 에 있던 프로젝트를 c:/users/kimta/desktop 에 옮김 + +ios 빌드와 auto submit 까지는 잘 됐는데 안드로이드 빌드할 때 prepare project 단계에서 경로를 못 찾는 에러 발생 (권한) + +```jsx +tar: .idea/.gitignore: Cannot open: Permission denied +tar: .idea/insole-app.iml: Cannot open: Permission denied +tar: .idea/inspectionProfiles: Cannot mkdir: Permission denied +tar: .idea/misc.xml: Cannot open: Permission denied +tar: .idea/modules.xml: Cannot open: Permission denied +tar: .idea/vcs.xml: Cannot open: Permission denied +tar: .idea/workspace.xml: Cannot open: Permission denied +tar: .vscode/extensions.json: Cannot open: Permission denied +tar: .vscode/settings.json: Cannot open: Permission denied +tar: app/(tabs): Cannot mkdir: Permission denied +tar: app/account-exists.tsx: Cannot open: Permission denied +tar: app/find-account.tsx: Cannot open: Permission denied +tar: app/index.tsx: Cannot open: Permission denied +tar: app/login.tsx: Cannot open: Permission denied +tar: app/modal.tsx: Cannot open: Permission denied +tar: app/register: Cannot mkdir: Permission denied +tar: app/social: Cannot mkdir: Permission denied +``` + +tar 압축을 푸는 것에 대한 에러였는데 일단 권한 에러라서 desktop이 아닌 C:/dev/{프로젝트} 로 아예 C 하위로 옮겨봤다 (desktop이 보호 폴더인 경우가 많다고 지피티가 그럼) + +powershell 관리자 권한으로 실행해서 다시 해봤는데 또 같은 에러로 실패 → desktop 문제 아님 + +`tar -cf test.tar .` ← 이 명령어로 tar 이 되는지 확인해봤는데 잘되는 것 확인 + +로컬에서 permission 에러 안나니까 이제 되겠지 → eas build 시도 → but 실패 + +⇒ EAS CLI의 문제다! EAS가 내부적으로 다른 tar 를 쓰거나 EAS가 만드는 임시 폴더/아카이브 과정에서 권한이 막히는 것 + +→ EAS CLI가 프로젝트를 임시 작업 폴더에 풀어놓고(스테이징) tar 작업을 하는데 그 임시 폴더 경로가 쓰기 권한이 없는(구: onedrive 경로)곳으로 잡혀있어서 tar가 디렉토리 생성/파일 쓰기를 못 하는 상황 + +TEMP/TMP 경로 확인해봤더니 onedrive에 있는게 아니라 C 하위의 appdata/local에 정상으로 위치해 있음. 이게 문제가 아니고 + +⇒ tar 에서 permission denied가 되는게 아니라, 파일/폴더의 NTFS 권한/속성(ACL/읽기전용/소유자) 이 꼬여있는 상태 + +→ powershell 관리자 권한으로 + +1. 읽기 전용 속성 제거 `attrib -R * /S /D` +2. 소유권을 현재 사용자로 강제 지정 `takeown /F . /R /D Y` +3. 권한을 현재 사용자 full control로 재부여 + 상속 켜기 `icacls . /inheritance:e /grant:r "$env:USERNAME:(OI)(CI)F" /T` + +하고 eas build했더니 성공 + +### 정리하자면 + +ondrive/desktop이 바탕화면에 보이게 해둔 상태에서 동기화를 끊으니까 바탕화면에 바로가기로 되어 있던 폴더나 프로그램들이 실행할 수 없다고 뜬 적이 있음 → 여기서 계정에 대한 상속 체인이 변경되었을 것으로 예측 + +동기화를 끊을 시 윈도우는 + +- 경로 재지정을 원래 위치로 되돌리거나 +- ondrive 폴더를 분리하거나 +- 로컬 복사하거나 하는데 + +이 과정에서 내가 겪은 증상을 보자면 두가지 가능성이 있음 + +- 대상 경로가 바뀌어서 링크가 깨짐 (onedrive 경로 기반으로 바로가기면, 동기화 해제 시 대상이 사라져서 실행 불가) +- 파일은 있는데 접근 권한이 달라짐 (SID/ACL 문제로 엑세스 거부된 상태) + +이게 eas 문제로 이어진 이유 + +- onedrive desktop 하위에서 생성된 파일들을 +- OneDrive 리디렉션 해제 과정에서 파일/폴더의 소유자 SID 또는 ACL 상속 구조가 변경되었고, +- 이후 동일 볼륨 내 이동(C:/dev 하위)은 그 상태를 그대로 유지했을 가능성이 높다. + +## 📌 문제 발생 흐름 정리 + +1. OneDrive가 Desktop을 리디렉션한 상태에서 프로젝트를 생성 +2. OneDrive 동기화를 해제하면서: + - Desktop 경로 재지정 해제 + - 파일 재배치/복사/분리 과정 발생 + - 이 과정에서 일부 파일의 **소유자 SID 또는 ACL 상속 체인이 변경되었을 가능성** +3. 이후 동일 C: 드라이브 내에서 `C:\dev`로 이동 + - NTFS는 같은 볼륨 이동 시 ACL을 그대로 유지 + - 즉, 이미 비정상 상태였던 권한 구조가 그대로 유지됨 +4. EAS Build가 프로젝트를 스테이징하면서 디렉토리 생성/복사 단계에서 `Permission denied` 발생 + +--- + +## 📌 왜 일반 tar는 되고 EAS만 실패했을까? + +- 일반 `tar`는 “읽기 중심” +- EAS는 스테이징 디렉토리 생성 + 복사 + 권한 처리 수행 +- 소유자/ACL 상속 구조가 비정상이면 mkdir 단계에서 실패 \ No newline at end of file diff --git a/assignments/latteeea/week3/data/android_network_security.md b/assignments/latteeea/week3/data/android_network_security.md new file mode 100644 index 0000000..2bb10a6 --- /dev/null +++ b/assignments/latteeea/week3/data/android_network_security.md @@ -0,0 +1,44 @@ +개발 환경: react native로 크로스 플랫폼 개발 중, 하지만 네이티브 모듈(블루투스 통신)을 사용해서 expo go 앱을 통해서는 빌드하지 못하고 android/ios 각각 빌드를 진행하고 있었다. profile development로 해서 기기 시뮬을 돌릴 때는 개발 기기의 네트워크와 시뮬 기기의 네트워크가 같은 것에 연결되어 있어야 되는 것은 알고 있었음 (다만 이유는 뭔지 모르겠다) + +문제 상황: 클라이언트측과 QA를 위해 profile preview로 apk 파일을 뽑았음. 로컬 프로세스 돌려가지고 같은 네트워크 연결할 때는 몰랐는데 apk 파일 뽑고 같은 네트워크 유지하면서 버튼 누르는데 ‘서버에 연결할 수 없습니다. 실제 기기를 이용하는 경우 로컬 IP를 사용해야 합니다) 라고 뜸 ????? 상황을 정리하자면 + +- ios ipa 파일을 testflight에 임시 배포했을때 다른 사람 폰에서 시뮬 돌린건 잘 됐음 (코드 로직의 문제는 아님) +- 다만 도메인 주소를 연결할 수 없는 상황이고 서버를 이관 혹은 폐기하고 새롭게 만들 예정이라 임시 IP 주소를 https 로 전환하지 않은 상태임 → 이게 개크게 의심됨 (왜냐면 여태 프젝할때는 항상 https 로 배포된 서버에 연결해서 실제 기기 시뮬 돌렸기 때문) + +해결을 시도해보자 + +- 안드로이드 9부터 HTTP 요청 차단이 기본 정책 → usesCleartextTraffic : True 인지 확인하기 (응 true 야~) +- http 배포된 서버는 80 포트 디폴트로 사용하는데 앱이 지 혼자서 8000포트로 가고 있는거 아닌가? (포트를 명시적으로 고정해볼까? 생각함) 8000으로 고정해서 브라우저 열어보려고 하면 안되는거 확인 (ㄹㅇ 포트를 8000으로 쓰나?) +- 안드로이드 브라우저에서 http://{IP주소} 로 들어가는건 들어가짐! 이건 서버의 방화벽이나 보안그룹 문제는 아니고 앱 문제가 맞다! 판단 +- 근데 usesClearTextTraffic 적용되어 있는데 왜 안되지? androidmanifest에 안들어갔나? (빌드할때 설정을 건너뛴다던가 다른 설정이 얘를 막는다던가) +1. 포트 80으로 고정하기 (nginx 설정) + 1. 명령어를 계속 쳐보려고 했지만 ios 시뮬 돌리는 과정에서 api 요청이 8000으로 가는건 아니고 포트를 지정안하고 api 요청을 날리는 것을 확인함 (포트 문제가 아닐 수 있겠다) 앱이 8000을 찍는 건 아님 판단 +2. **cleartext 허용이 빌드에 반영이 안된 것이 궁극적인 문제임!!** + 1. **network security config 강제 넣기 + manifest도 수동으로 넣어주기 (app.json 이 아니라 android 폴더 내의 파일에 설정하는 거기 때문에)** + +해결책 정리 + +- 일단 이게 ios가 아닌 안드로이드에서만 터진 이유 : Cleartext (HTTP) 차단 정책 + - Network Security Policy : 앱이 HTTP로 통신하는 걸 기본적으로 금지하는 방향 +- 브라우저로 열리는데 앱이 통신 못하는 이유는? + - 브라우저는 : 사용자 앱이라 HTTP 허용 정책이 다름 + - 우리 앱 : OS가 cleartext 허용했나? 물어보고 막아버림 → 연결 실패 +- usesCleartextTraffic 이 적혀있는데도 안된 이유? + - android 에서 cleartext 허용을 결정하는 방식은 ‘레이어’가 있다 + - manifest의 usesCleartextTraffic = “true” + - Network Security Config + - 기기/OS 정책, 라이브러리 동작 등 + + ⇒ usesCleartextTraffic=true면 풀릴때가 많고 나의 경우에는 그게 최종 apk 파일에 제대로 반영이 안된 경우임. + + - 특히 expo/eas 환경에서는 + - app.json 설정이 빌드 과정에서 네이티브 파일을 생성/수정하면서 반영되는데, + - 프로젝트 상태(프리빌드, 플러그인, 빌드 캐시, 다른 매니패스트 병합 결과)에 따라 내가 켰다고 생각한 값이 최종 androidManifest 에 안들어가는 일이 생긴 것 +- 결국에는 HTTPS로 가는 게 정석이다!! (android/ios 둘다 정책 신경 쓸 일이 줄어들긔) +- 이 방식은 QA 같은 곳에서 임시로 사용하기 +- expo prebuild 를 돌리면 파일이 다시 날아갈 수 있으니 이건 유의하기! + +결론 + +- app.json 에 usesclear 옵션이 적용되어 있는데 왜 빌드할때 적용이 안됐냐? → 빌드 시점에 androidManifest를 생성/패치하는데 prebuild를 이미 해둔 상태거나 다른 플러그인이 manifest를 덮어쓰는 경우 안들어갈 수 있음 +- https 로 바꾸면 거의 백프로 해결되는 문제긴 해~ 결국엔 해결될 수 있는 문제였지만 네트워크에 대해 좀 알게된 경험이었다! \ No newline at end of file diff --git a/assignments/latteeea/week3/data/android_network_security2.md b/assignments/latteeea/week3/data/android_network_security2.md new file mode 100644 index 0000000..12c77b0 --- /dev/null +++ b/assignments/latteeea/week3/data/android_network_security2.md @@ -0,0 +1,99 @@ +개발중인 서버가 ssl 을 붙이지 못해서 https가 아닌 http로 운영되고 있고 + +react native expo 및 eas build 로 android와 ios 동시 빌드를 하고 있다면 + +android 의 네이티브 network config 설정이 필요함 + +android 9+ 이상부터는 HTTP를 기본적으로 차단하기 때문에 이를 풀어주지 않으면 apk 파일로 다운받았을때 + +서버에 연결할 수 없습니다. ([https://15.~~~](https://15.~~~/)) 실제 기기를 사용하는 경우 컴퓨터의 로컬 IP 주소를 사용해야 합니다. 예:[http://192~~](http://192~~/) + +이러한 에러가 나옴 + +이때 HTTP로 테스트하기 위해서는 network_security_config / usesCleartextTraffic이 필요함 + +- app.json에 http 허용 켜기 (usesCleartextTraffic: true) + + ```jsx + { + "expo": { + "android": { + "usesCleartextTraffic": true + } + } + } + ``` + +- network_security_config.xml 만들기 + + ```jsx + + + + + + ``` + +- android manifest의 application 태그에 연결하기 + + ```jsx + android:usesCleartextTraffic="true" + + android:networkSecurityConfig="@xml/network_security_config" + ``` + + +이걸 하고 바로 prebuild를 다시 돌리면 androidmanifest 가 덮어씌워질 수 있음 이걸 방지하기 위해서 config plugin 을 통해 빌드 때마다 자동 주입되게 만드는 방법이 있음 + +```jsx +const { withAndroidManifest, withDangerousMod } = require("@expo/config-plugins"); +const fs = require("fs"); +const path = require("path"); + +module.exports = function withNetworkSecurityConfig(config) { + // 1) res/xml/network_security_config.xml 생성/갱신 + config = withDangerousMod(config, [ + "android", + async (cfg) => { + const projectRoot = cfg.modRequest.projectRoot; + const xmlDir = path.join(projectRoot, "android", "app", "src", "main", "res", "xml"); + const xmlPath = path.join(xmlDir, "network_security_config.xml"); + + fs.mkdirSync(xmlDir, { recursive: true }); + + const xml = ` + + + +`; + fs.writeFileSync(xmlPath, xml, "utf8"); + return cfg; + }, + ]); + + // 2) AndroidManifest.xml application에 속성 강제 주입 + config = withAndroidManifest(config, (cfg) => { + const app = cfg.modResults.manifest.application?.[0]; + if (app) { + app.$["android:usesCleartextTraffic"] = "true"; + app.$["android:networkSecurityConfig"] = "@xml/network_security_config"; + } + return cfg; + }); + + return config; +}; + +``` + +ios에서는 기본적으로 차단을 안하나? ios에서는 서버 연결 잘 된 이유 + +- 기본적으로 보안 안된 HTTP를 막긴 함 +- 하지만 나는 iOS에서 NSExceptionDomains에 http ip 주소를 넣어놨고 NSExceptionAllowsInsecureHTTPLoads : true 로 HTTP 예외를 허용해둔 상태 +- android 에서는 cleartext 트래픽을 차단하는 정책이 생겼고 +- iOS처럼 info.plist 한 덩어리로 예외가 잡히는게아니라 usesCleartextTraffic, network_security_config 같은걸로 명시적 허용 필요 + +### 정리하자면 + +- iOS에서는 NSExceptionDomains로 예외 허용해놨고 (app.json) +- android 에서는 app.json 말고도 네이티브 폴더에 설정되어야 하는 파일이 있는데 prebuild를 할때 없어져버려서 에러가 다시 생긴 거임 \ No newline at end of file diff --git a/assignments/latteeea/week3/data/atomic_facts.md b/assignments/latteeea/week3/data/atomic_facts.md new file mode 100644 index 0000000..4c552cc --- /dev/null +++ b/assignments/latteeea/week3/data/atomic_facts.md @@ -0,0 +1,262 @@ +구조화/문서화를 하는 과정에서 내 의도와 다르게 구현된 부분 발견 (cursor와 할때) + +evidence 에서 rule로 가는거에서 rule이 제대로 안나오길래 중간에 의미 단위로 끊는 atomic facts 레이어를 넣으려고 한건데 알고보니까 assoication rules는 5-tuple중에 2-tuple만 반영하고 있었음 (ingredient, state_tag) + +실제 5-tuple은 final_guidelines(just 가시화 위한 가이드라인)에서만 쓰이고 2-tuple만 쓰이고 있었음. 이게 버그는 아니지만 5-tuple vs 2-tuple 그룹화하는 기준이 다르기 때문에 final_guidelines와 최종 추천 결과가 상충될 수 있다는 점이 문제임 + +→ 이렇게 되면 atomic_facts 를 넣으면서 분리하려고 했던 것들 (population_segment, dose_route, outcome_normalized, claim_subject)를 전부 날리게 되는 구조임, 결과적으로 evidence→2-tuple grouping은 atomic facts를 도입하기 전의 상태와 크게 다르지 않는 것. + +final_guidelines는 성분별 리포트(테스트위함)를 생성할때만 읽히고 추천 엔진에서는 쓰이지 않음. 결론적으로 이걸 유의미하게 쓰기 위해서는 atomic_facts를 입력으로 받도록 해야하는것 (rule이) + +원인 추정 + +- atomic facts를 설계하는것까진 했으나 이를 코드로 구현할때 association이 ingrdient-state_tag 기반의 룰이라 cursor가 이 두개의 튜플 필드만 매핑하도록 구현했고 이게 클로드로 넘어오면서까지 유지됐는데 클로드가 내가 이미 설계한 의도가 있다고 생각해서 이상한점을 지적하지 않은듯함. +- 결론적으로 인간의 전체적인 플로우 인지가 중요하다… + +--- + +근데 보니까 5-tuple 필드 다 가지고 그룹핑을 하면 너무 파편화된 rule이 많이 생겨서 score를 매기기 힘듦 (n의 값이 거의 1아니면 2) claim_subject는 필터링을 진행하고 있어서 괜찮은 상태인데(behavior_exposure같은거는 제외하는 방식으로) population-segment가 그룹핑이 안되고 있음을 발견함 + +ingrdeint, state_tag 2-tuple만 반영하고 있는데 이렇게되면 누구한테 적용되는지에 대한 게 안 들어가서 고령자든 어린이든 똑같은 효과를 설명하는 rule이 나오게 됨. 이건 특히 dose rule에서 엄격하게 다뤄야하는문제임. + +![{48F5CFC2-79C0-433A-8C1A-6A50EDF074CA}.png](attachment:badf46c4-a263-4b1a-85fe-1a0e85444c40:48F5CFC2-79C0-433A-8C1A-6A50EDF074CA.png) + +이걸 클로드가 파악하지 못한 이유 추정 : 처음 클로드로 갈아타면서 프로젝트에 대한 전반적인 알고리즘의 설계적 보완점을 말해달라고 했는데 프로젝트가 너무 커서 기억을 다 못했거나 판단을 제대로 하지 못했거나 했을까? + +![{E51234EE-DE26-4BDF-9522-F29E881CD9C9}.png](attachment:9ccf8221-34da-4ad6-9cd5-9080f9e10fb8:E51234EE-DE26-4BDF-9522-F29E881CD9C9.png) + +```tsx + 설계 검토 요청 때 체크되지 않은 이유 + + 솔직하게 말하면 세 가지 구조적 한계가 있습니다. + + 1. Claude Code는 파일을 순서대로 읽습니다 + + atomic_facts.py를 읽을 때 generate_rules.py의 association_rules 코드가 컨텍스트에 없으면, 두 + 파이프라인이 단절돼 있다는 걸 파악하기 어렵습니다. "이 함수가 제대로 작동하는가"는 파악하지만 "이 + 함수 결과가 다음 단계에 연결되어 있는가"는 놓칩니다. + + 2. 설계 검토 요청이 "맞는지 봐줘" 형식이면 검증 편향이 생깁니다 + + "이 설계 어떻게 생각해?"라고 물으면 Claude는 코드가 동작하는지 확인하고 "잘 구현됐다"고 답하는 + 경향이 있습니다. "뭐가 잘못됐냐"는 관점으로 보지 않습니다. + + 3. 도메인 지식 없이는 문제를 발견 못 합니다 + + "임산부 용량과 일반 성인 용량이 분리돼야 한다"는 건 영양학 지식입니다. 코드에서 population_segment를 + groupby에 안 넣은 것 자체는 문법 오류가 아닙니다. 도메인 맥락이 있어야 문제로 보입니다. + + --- + 다음부터 어떻게 명령을 내려야 하는가 + + 효과적인 방식: + + ❌ "이 설계 검토해줘" + ✅ "atomic_facts가 association_rules의 입력으로 쓰이는지 확인해줘. + 쓰이지 않는다면 왜 문제인지 설명해줘." + + ❌ "dose_rules 괜찮아 보여?" + ✅ "임산부와 일반성인의 dose_rule이 분리되어 있는지 확인해줘. + cholecalciferol으로 예시를 보여줘." + + 패턴으로 요약하면: + 1. 체크할 불변 조건을 명시 → "A는 반드시 B를 입력으로 받아야 한다. 맞는지 확인해줘" + 2. 반례를 직접 제시 → "임산부 + 철분 용량이 20mg으로 나오면 문제야. 지금 얼마 나오는지 확인해줘" + 3. "뭐가 잘못됐냐"로 질문 → "이 파이프라인에서 영양학적으로 틀릴 수 있는 부분 3가지만 찾아줘" + + 도메인 지식을 가진 쪽은 사용자입니다. Claude는 그 지식을 구체적인 체크 조건으로 받아서 코드에서 + 확인하는 역할입니다. +``` + +```tsx +실제 회사에서 개발자는 어떻게 설계하나? + + 솔직하게 말하면 개발자 혼자 이걸 설계하면 안 됩니다. 그리고 실제로 그렇게 하지 않습니다. + + 일반적인 헬스테크 회사의 구조: + + 개발자 역할 도메인 전문가 역할 + ──────────────────── ──────────────────────────── + 스키마 설계 ←→ 어떤 차원이 있어야 하는지 정의 + config 파일 구조 만들기 ←→ config 파일 내용 채우기 + 알고리즘 로직 구현 ←→ 수치/기준의 임상적 타당성 검토 + 파이프라인 오류 디버깅 ←→ 아웃풋 결과 임상적 검토 + + 실제로 어떻게 협업하냐: + + 초기 스타트업 기준으로 보면: + - CTO/개발자가 "population_segment라는 차원이 있어야 하나요?"라고 물으면 + - 영양사/임상 전문가가 "임산부와 일반성인은 철분 안전 용량이 완전히 다르니 반드시 분리해야 + 합니다"라고 답하는 구조입니다 + + 개발자가 "임산부에게 비타민A가 기형을 유발한다"는 걸 먼저 알아야 population_segment를 넣는 게 + 아닙니다. 개발자는 "안전성이 집단마다 다를 수 있는가?"라는 구조적 질문을 하고, 전문가가 "그렇다"고 + 하면 그 구조를 만드는 겁니다. + + 한국 헬스테크 스타트업 현실: + + 초기에는 대부분 둘 중 하나입니다. + 1. 공동창업자 중 임상영양사나 의사가 있어서 처음부터 같이 설계 + 2. 외부 임상 자문위원(컨설팅 계약)이 알고리즘 로직을 주기적으로 검토 + + 지금 이 프로젝트에서 prior_facts.yaml, clinical_guarantees 같은 YAML 파일이 있는 게 사실 올바른 + 방향입니다. 개발자가 스키마(구조)를 만들고 도메인 전문가가 내용을 채우는 구조가 되어 있으니까요. + 문제는 그 내용을 지금 개발자가 직접 채우고 있다는 겁니다. + + 지금 상황에서 현실적인 조언: + + 1. 지금 구조 설계(population_segment를 키로 넣을지 등)는 개발자가 해도 됩니다 + 2. clinical_guarantees.yaml, prior_facts.yaml에 들어가는 수치와 임상 규칙은 영양사나 임상 전문가의 + 검토가 필수입니다 + 3. 규모가 커지기 전에 임상 자문 1명(파트타임이라도)을 확보하는 게 이 프로젝트의 가장 큰 리스크 + 해소입니다 +``` + +** 심각성에 대한 필드를 추가해야한다는 클로드 + +evidence → rule 로 나오는 과정에서 임상학 도메인을 가진채로 검증 요청 + +![{54B26BF2-EA15-47FB-A1BD-664625CBFD1E}.png](attachment:49a42203-b21c-43b0-aa07-a5d9761e8eec:54B26BF2-EA15-47FB-A1BD-664625CBFD1E.png) + +심각성에 대한 필드를 rule에 추가해야한다고 했지만 내가 보기에 문제는 그 쪽 구조가 아니었음 + +![{9C21FE53-9AC4-4DBA-9AFF-9D23D011809F}.png](attachment:9533b5cb-c1fc-4b18-a3dc-8a47371e4a6f:9C21FE53-9AC4-4DBA-9AFF-9D23D011809F.png) + +금기로 나온 논문결과라면 그 금기는 따로 추출하도록 프롬프트가 짜여져 있는 상태였음 내 기억상으로. + +![{F5B1A751-93B9-40F4-9DBE-67CBED5C4802}.png](attachment:f51e9678-e56f-4469-91c4-0ea9d8868c03:F5B1A751-93B9-40F4-9DBE-67CBED5C4802.png) + +그게 맞았다는 게 클로드의 재입장. 하지만 rule에서는 그 금기에대한 분기가 적용되어있지 않다는게 문제였고 그걸 중심으로 다시 분석 요청했더니 어느 레이어에서 mixed로뭉개지는지 확인을 함. (evidence score를 기준으로 rule을 뽑아내는 쪽이 아니라 evidence를 집계하는 과정에서 warning_type이 여러개면 금기든 뭐든 mixed가 되는것이 금기 필터링이 느슨했던 이유였고) 금기 여부에 대한 boolean 필드를 해당 레이어에 추가하기로 함 + +state_tag에 명시되지 않았지만 논문 근거에서 나온 효과가 있다면 이것은 어떻게 처리하는지? 예를 들어 두뇌 건강에 대한 태그는 없지만 논문 근거에서 두뇌 건강에 효과를 주는 성분을 얘기했다면 이 데이터는 버려지는건가? 기타 데이터로 활용할 수 있는 방안이 없는가? + +지금 단계에서는 evidence, association rules 단계에서 버려지지 않음. 다만 추천 엔진에서 죽음. needs_vector의 키와 rule의 state_tag를 매칭하기 때문에. 근데 이건 데이터가 사라지는게아니라 rule 데이터 자체로는 있지만 추천에서만 안쓰이는거니까 추천 엔진의 입력단에서 뭔가를 조정하면 바로 쓸수 있음 + +### 개큰 문제 == LLM과 데이터 + +- S16 ↔ S16B + + 논문 주제가 “칼슘 함유 약물 사용이 고령, 수축기 혈압 등과 함께 PWV(맥파속도)를 독립적으로 증가시키는 요인으로 확인되었다” 인데 맥파속도가 증가하는건 → 동맥경직도 증가 → 심혈관에 나쁜거임 + 논문은 칼슘이 심혈관에 해롭다는 내용인데 LLM이 cardiovascular_risk 에 direction을 positive로 추출함. 완전 방향이 반전된 추출. 이게 칼슘에 대한 추천 score를 가중시켰고 supporting에 들어가야하는게 core로 진입한 문제 + +- S06 ↔ S06B + + 마그네슘이 워낙 많은 태그를 커버하는 성분이다 보니까 top_n_rules = 5 (성분당 규칙 수를 제한) 했음에도 불구하고 빈혈로 내원한 환자(페르소나)에게 마그네슘이 철분보다 강하게 추천됨 (임상적으로 맞지 않음) + +- S07 ↔ S07B + - 멜라토닌이 immune_vulnerability에 supporting으로 등장 + 논문에서 “멜라토닌+비타민D 병용이 유방암 세포에서 세포자멸사를 유도” 했다고 했는데 항암 연구를 면역 지원으로 오분류함 (LLM이) + 이 페르소나는 45세 남성 일반 면역 취약에 대한 사람인데 멜라토닌 추천은 부적절하고 논문 자체도 엉뚱함 +- S20 ↔ S20B + - 신장경계 환자 페르소나인데 calcitriol(활성형 비타민D)와 phosphate(인 보충제)가 추천됨 + 신장 기능이 떨어지면 소변으로 배출되어야 하는 ‘인’이 혈액 속에 쌓이는데 인 보충제를 추천하면 안됨. 근데 문제는 이게 사전 임상 지식에 없는거고 논문에서도 나오지 않은 거라 필터링이 안된것. 세상 모든 지식을 어떻게 끌어와야하지? 그게 가능한가? + - calcitriol은 OTC 보충제가 아닌 처방 의약품이기 때문에 보충제 서비스에서는 추천하면 안됨 +- LLM이 방향을 틀리는 이유 (프롬프트 개선만으로는 해결이 안되는 이유) + - 논문은 ‘칼슘이 PWV를 높인다’고 했는데, LLM 입장에서 이 문장의 구조는 ‘성분 → 심혈관 지표 → 유의미한 결과’로 본다. 어떤 방향이든 연구 대상이 되면 positive로 분류하는 편향이 있고 “치료 목적으로 연구됨”, “위험 요인으로 연구됨”을 구분하려면 논문의 전체 맥락, 연구 설계 의도, 결론의 임상적 함의를 이해해야 함. + - claim_subject로 필터링되고 있는데도 이러는 이유 : 걍 direction을 잘못 추출한 경우임 + - LLM에서 추출하고 임상학적으로 맞는건지 확인하는 llm하나 더 붙여서 multi-agent (LangGraph)구조로 가기 + +### 260508 회의 (키오스크 단 설문) + +1. 당일 컨디션 설문 키워드 (8가지) - state_tag 하루치 변동 여부 + - 에너지/활력 : 오늘 복용할 보충제에서 보통 사용자가 즉각적으로 기대하는 효과. + - 어젯밤 수면 : 전날 결과가 당일 컨디션 전반에 미치는 ‘어제’ 질문. ‘평소 수면’이라는 초기 설문의 패턴 질문은 어젯밤의 급성 부족을 잡지 못할 가능성 큼 + - 오늘 스트레스 : 하루 단위로 급변하는 요소. 당일 상황이 마그네슘/비타민C/테아닌의 필요를 높일 수 있기 때문에. 초기 설문에서는 지난 1주일 기준이라 오늘의 급성 스트레스를 반영하지 못함 + - 운동 계획 : 운동 전후 복용 타이밍과 필요 성분이 완전히 달라짐. 강도 높은 운동 당일이면 단백질/크레아틴/BCAA 신호 올라가야하니까. + - 운동 타이밍 (전/후) x 운동 강도 (고강도/저강도) → 4가지 + - 감기 기운 : 감기 초기증상에는 비타민C 같은게 필요할 수 있음. 초기 설문에서는 평소에 대한 상태, 즉 면역력 자체에 대한 질문이므로 지금 감기 걸린 사람한테나 안 걸린 사람한테나 동일한 추천이 나옴. + - 장소마다 필요성 다름 + - 소화 상태 : 소화가 안 좋은 ‘날’에는 고용량 철분이나 비타민C같은 자극 성분을 줄여야함. 프로바이오틱스 추천을 앞당길 수 있기 때문에 + - 오늘 수분 섭취 : 탈수 상태에서는 전해질 우선순위 오리고, 크레아틴 같은 일부 성분은 충분한 섭취 없이 복용하면 신장 부담이 되기 때문에 중요 → 질문 고민 (보수적으로 해도 ok) + - 전날 음주 여부 : 고용량 비타민 A나 오메가3 고용량 등은 급성 음주 이후에 간 독성 시너지나 항응고 효과를 증폭시키는 등 음주 시 피해야하는 성분이 있기 때문에 constraint 용으로 필요 → constraint (회복하는 걸 더 기대함) + - 추가하면 되고 + state_tag 배치 +2. 건강검진데이터 포함 시나리오 20가지 시뮬레이션 (20가지는 바이오마커 포함, 4가지는 설문만) + + 비교 페어로 볼 수 있는 것들: + + - S06 vs S06B: 빈혈 — 혈액 수치가 추천 강도에 얼마나 영향 주는지 + - S16 vs S16B: 당뇨 전단계 — 공복혈당/HbA1c 없을 때 크롬·마그네슘 순위 차이 + - S07 vs S07B: 면역 — 바이오마커 VitD 12가 vitamin_d_deficiency 태그 추가로 추천 바뀌는지 + - S20 vs S20B: 신장 경계 — 가장 중요, 설문만으로는 renal_impairment constraint가 안 걸리는지 확인 + - 20(바이오마커)+4가지(설문만) 시나리오 정리 + + + | ID | 시나리오 | 사용자 페르소나 | 설문 응답 | 바이오마커 | Core 추천 | Supporting 추천 | 활성 Constraint | 경고 | 포커싱 포인트 | + | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | + | S01 | 수면 부족 중심 | 남성 28세, 70kg | 수면 낮음/부족, 피로 보통, 수분 보통 | VitD 21.0, Hgb 15.0, 총콜 188 | L-테아닌, 마그네슘 205.52mg/day | 비타민D, vitamin_e, 아연 7.4mg/day, 비타민C | 없음 | 비타민D 부족. 보충 권장 | 수면 문제 중심. 마그네슘·L-테아닌 위주로 추천되는지 확인 | + | S02 | 스트레스·신경근 긴장 | 남성 35세, 75kg | 신경근긴장 높음, 피로 보통, 단백질 높음 | VitD 22.0, 수축기BP 128, 총콜 195 | 마그네슘 205.52mg/day, 비타민D | 비타민C, potassium 3400mg/day, 칼슘 500mg/day | 없음 | 비타민D 부족. 보충 권장 | 경미한 심혈관 신호가 있어도 주 추천이 흔들리지 않는지 | + | S03 | 피로 + 단백질 섭취 부족 | 남성 30세, 72kg | 피로 높음, 단백질 낮음, 운동 보통 | Hgb 13.2, ferritin 16.0, VitD 20.0 | 단백질 보충제, 마그네슘 205.52mg/day | 비타민D, 비타민C, B-Complex, 철분 18mg/day | 없음 | 비타민D 부족. 보충 권장 | 피로·단백질 부족 중심, 철분이 supporting으로 올라오는지 | + | S04 | 소화 불편 + 당류 과다 | 여성 32세, 58kg | 소화 높음, 당류 높음, 채소·과일 보통 | 공복혈당 102, TG 148, Hgb 12.8 | 크로뮴, 프로바이오틱스 | 마그네슘 205.52mg/day, 비타민C, 단백질 보충제, B-Complex 1000mcg/day | 없음 | 공복혈당 당뇨 전단계. 혈당 보조 검토 | 장 불편 + 혈당 변동 리스크가 함께 잡히는지 | + | S05 | 수분 부족·탈수 위험 | 남성 25세, 68kg | 수분 낮음, 카페인 보통, 운동 높음 | 요산 6.5, Cr 1.05, Hgb 15.5 | 단백질 보충제, 아연 7.4mg/day | 마그네슘, 비타민C, 프로바이오틱스, B-Complex, 크레아틴 | high_caffeine_exposure | 고카페인 감지. 미네랄 흡수 저해 가능 | 탈수·전해질 니즈와 요산 경계 신호 확인 | + | S06 | 빈혈 의심·진단 경험 | 여성 26세, 52kg | 빈혈 진단경험, 피로 높음, 단백질 보통 | Hgb 9.5, ferritin 8.0, VitD 22.0 | 철분 18mg/day, 마그네슘 205.52mg/day | 비타민D, 비타민C, B-Complex | 없음 | 중등도 이상 빈혈. 전문의 상담 필요 / 페리틴 결핍 | 빈혈 신호가 강하게 증폭되어 철분 core가 되는지 | + | S07 | 면역 취약 + 채소/햇빛 부족 | 남성 45세, 85kg | 면역 높음, 채소·과일 낮음, 햇빛 낮음 | VitD 12.0, HDL 38, 총콜 215 | 비타민D, 칼슘 500mg/day | 프로바이오틱스, 비타민C, 멜라토닌, B-Complex, 아연 10mg/day, K2 | 없음 | 비타민D 결핍. 적극 보충 필요 / HDL 낮음 | 면역·햇빛 부족 신호와 VitD 결핍이 합산되는지 | + | S08 | 고강도 운동·회복 니즈 | 남성 28세, 82kg | 운동 높음, 단백질 낮음, 피로 보통 | ferritin 18.0, 요산 6.2, VitD 23.0 | 단백질 보충제, 아연 34mg/day | 마그네슘 205.52mg/day, 비타민C, B-Complex, 비타민D, 크레아틴 | 없음 | 비타민D 부족. 보충 권장 | 운동 회복 중심 추천이 유지되는지 | + | S09 | 생선 부족 + 피로 | 여성 40세, 60kg | 생선 낮음, 피로 높음, 카페인 보통 | TG 165, Hgb 11.9, VitD 21.0 | 오메가-3, 마그네슘 205.52mg/day | 비타민D, 비타민C, 프로바이오틱스, 철분 18mg/day | high_caffeine_exposure | 고카페인 경고 / 비타민D 부족 | 오메가3 강화 + 철분 supporting 등장 여부 | + | S10 | 수면 부족 + 고카페인 | 여성 29세, 55kg | 수면 낮음, 카페인 높음, 신경근긴장 보통 | VitD 20.0, Hgb 12.5, 총콜 178 | L-테아닌, 마그네슘 205.52mg/day | vitamin_e, 아연 7.4mg/day, 비타민D, 프로바이오틱스 | high_caffeine_exposure | 고카페인 경고 / 비타민D 부족 | 수면 추천과 카페인 constraint가 모순 없이 처리되는지 | + | S11 | 노인 여성 - 근감소 + 골밀도 | 여성 68세, 57kg | 피로 높음, 단백질 낮음, 햇빛 낮음, 운동 낮음 | VitD 18.0, Hgb 11.5, 총콜 220, Cr 0.88 | 비타민D, 칼슘 500mg/day, 단백질 보충제 | 마그네슘 205.52mg/day, 비타민C, B-Complex, 프로바이오틱스, K2 | 없음 | 비타민D 결핍. 적극 보충 필요 | 고령 보정으로 단백질·칼슘·비타민D가 core인지 | + | S12 | 비만 남성 - 비타민D 흡수 저하 | 남성 42세, 102kg | 햇빛 낮음, 피로 보통, 면역 보통, 운동 낮음 | BMI 33.7, VitD 15.0, TG 185, 공복혈당 103, HDL 38, BP 135 | 비타민D, 칼슘 500mg/day | 프로바이오틱스, vitamin_a, 마그네슘 205.52mg/day, B-Complex, 아연 10mg/day, K2 | 없음 | 심혈관 위험 / BMI 비만 2단계 | BMI 계수로 비타민D 추천 강도가 올라가는지 | + | S13 | 고강도 운동 남성 - 단백질 + 오메가3 | 남성 25세, 80kg | 운동 높음, 단백질 낮음, 생선 낮음, 피로 보통 | Hgb 15.8, 요산 5.5, VitD 26.0 | 단백질 보충제, 오메가-3, 아연 10mg/day | 마그네슘 205.52mg/day, B-Complex, 비타민C, 프로바이오틱스, 크레아틴 | 없음 | 비타민D 부족. 보충 권장 | 정상 바이오마커에서 기준선 추천만 유지되는지 | + | S14 | 임신 2분기 여성 - 엽산·철분·칼슘 | 여성 31세, 60kg | 임신중, 피로 높음, 빈혈 의심, 단백질/햇빛/채소 보통 | Hgb 10.5, ferritin 10.0, VitD 24.0 | 비타민D, B-Complex, 철분 18mg/day, 칼슘 500mg/day | 마그네슘 205.52mg/day, 비타민C, K2 | 없음 | 페리틴 결핍 / 비타민D 부족 | 임신 보장 로직으로 엽산·철분이 core에 반영되는지 | + | S15 | 비건 남성 - B12·철분·오메가3·칼슘 결핍 패턴 | 남성 28세, 65kg | 단백질 낮음, 생선 낮음, 유제품 낮음, 채소 높음, 피로 보통 | Hgb 12.6, ferritin 11.0, VitD 25.0 | 철분 18mg/day, 오메가-3, 단백질 보충제 | 마그네슘 205.52mg/day, 비타민C, 비타민D, B-Complex, 칼슘 500mg/day, K2 | 없음 | 페리틴 결핍 / 비타민D 부족 | 동물성 식품 부족 패턴에서 철분·오메가3·단백질이 올라오는지 | + | S16 | 당뇨 전단계 중년 남성 | 남성 52세, 88kg | 당류 높음, 운동 낮음, 피로 보통, 채소 낮음, 소화 보통 | 공복혈당 112, HbA1c 6.0, TG 195, BMI 29.1, LDL 145, BP 132, VitD 19.0 | 비타민D, 크로뮴, 칼슘 500mg/day | 마그네슘 205.52mg/day, B-Complex, vitamin_a, K2 | 없음 | 심혈관 위험 / 공복혈당 당뇨 전단계 | 혈당·대사 관리 성분이 최상위로 올라오는지 | + | S17 | 교대근무 야간 간호사 | 여성 34세, 57kg | 수면 낮음, 햇빛 낮음, 면역 높음, 카페인 높음, 피로 높음 | VitD 10.0, Hgb 11.8 | 비타민D, 칼슘 500mg/day, L-테아닌 | 마그네슘 205.52mg/day, 프로바이오틱스, 비타민C, B-Complex, 아연 10mg/day, K2 | high_caffeine_exposure | 고카페인 경고 / 비타민D 심한 결핍 | 수면·햇빛·면역 신호가 동시에 강화되는지 | + | S18 | 흡연·음주 병행 중년 남성 | 남성 48세, 78kg | 흡연 매일, 음주 높음, 피로 높음, 소화 보통, 면역 보통 | γ-GTP 95, ALT 55, HDL 36, 총콜 210, TG 175 | 마그네슘 205.52mg/day, 프로바이오틱스 | 비타민C, 비타민D, B-Complex | smoking_oxidative_load, high_alcohol_exposure | 고위험 음주 경고 / 흡연 경고 | 음주 constraint가 설문과 간효소 양쪽 경로로 발동되는지 | + | S19 | 완경 후 여성 - 골밀도·수면·피로 복합 | 여성 56세, 62kg | 유제품 낮음, 햇빛 낮음, 수면 낮음, 운동 낮음, 피로 보통 | VitD 17.0, Hgb 12.2, 총콜 225, LDL 140, BMI 24.2 | 비타민D, 칼슘 500mg/day, L-테아닌 | 마그네슘 205.52mg/day, 아연 10mg/day, 프로바이오틱스, B-Complex, K2 | 없음 | 비타민D 결핍. 적극 보충 필요 | 칼슘+비타민D core와 심혈관 리스크 추가 신호 확인 | + | S20 | 통풍·신장 기능 경계 중년 남성 | 남성 55세, 85kg | 단백질 높음, 음주 보통, 피로 높음, 운동 낮음, 소화 보통 | 요산 7.8, Cr 1.35, eGFR 62, 총콜 225, TG 190, BP 138, BMI 29.4 | 마그네슘 205.52mg/day, 비타민C | B-Complex, 비타민D, phosphate | high_alcohol_exposure, renal_impairment | 고위험 음주 경고 / 신장 질환 감지, 전문의 상담 필요 | 신장 constraint, 요산 경고, 안전 필터 작동 여부 | + | S06B | 비교군: 빈혈 의심 - 설문만 | 여성 26세, 52kg | 빈혈 진단경험, 피로 높음, 단백질 보통 | 없음 | 철분 18mg/day, 마그네슘 205.52mg/day | 비타민C, 비타민D, B-Complex | 없음 | 없음 | S06 대비 바이오마커 없을 때 철분 추천 강도 변화 | + | S16B | 비교군: 당뇨 전단계 - 설문만 | 남성 52세, 88kg | 당류 높음, 운동 낮음, 피로 보통, 채소 낮음, 소화 보통 | 없음 | 크로뮴, 마그네슘 205.52mg/day | 비타민C, 프로바이오틱스, B-Complex 1000mcg/day | 없음 | 없음 | S16 대비 혈당 바이오마커 없을 때 크롬·마그네슘 순위 변화 | + | S07B | 비교군: 면역 취약 + 햇빛 부족 - 설문만 | 남성 45세, 85kg | 면역 높음, 채소·과일 낮음, 햇빛 낮음 | 없음 | 비타민D, 칼슘 500mg/day | 프로바이오틱스, 비타민C, 멜라토닌, B-Complex, 아연 10mg/day, K2 | 없음 | 없음 | S07 대비 VitD 결핍 태그 미발동 시 추천 강도 변화 | + | S20B | 비교군: 통풍·신장 경계 - 설문만 | 남성 55세, 85kg | 단백질 높음, 음주 보통, 피로 높음, 운동 낮음, 소화 보통 | 없음 | 마그네슘 205.52mg/day, 비타민C | 비타민D, 철분 18mg/day, 프로바이오틱스 | high_alcohol_exposure | 고위험 음주 경고 | S20 대비 renal_impairment constraint가 미발동해야 함 | +3. 알고리즘 구조도 문서화 + + https://docs.google.com/document/d/1fC-jJwyLyAwVRgOv6DV64rCmjcDs_L8d7GB-O6X3xr0/edit?tab=t.0 + +4. 건강 니즈 관련 설문 (최대 3가지 선택 제한 필요 - 9가지 다 선택하면 추천 결과가 무의미하게 분산될 수 있기 때문) → 원료랑 같이 정해져야 + - 피로/에너지 회복 (한국 보충제 구매 동기 1위) + - 면역력 강화 + - 근력/운동 회복 + - 수면 개선 + - 뼈/관절 건강 + - 소화/장 건강 + - 심혈관/혈압 + - 눈 건강 + - 피부 관리 + - 체중/체형 관리 + + ![{474465DA-3E15-4CBA-B5CB-31B658639A95}.png](attachment:e5a303f2-f8cf-45cf-ac25-eb4bb96b7e0a:474465DA-3E15-4CBA-B5CB-31B658639A95.png) + + ![{9EDF8B0B-1A09-4586-B6B3-00AD190440C8}.png](attachment:980940b4-e86e-40cd-ada9-571c3e9800d4:9EDF8B0B-1A09-4586-B6B3-00AD190440C8.png) + + +### 260515 회의 + +1. E2R 엔진 검증에 대한 피드백 + 1. (제일 중요) 이 시스템은 두 단계가 완전 분리되어 있는 구조임 + 1. LLM이 실제 추천 시점에도 관여하는 end-to-end 시스템이 아니라 + 2. Step 1 : 오프라인 배치 (1회성) - LLM이 pdf 읽고 evidence 추출하기 + rule 생성하기 + 3. Step 2: 실시간 추천 - 사용자 입력을 통해 니즈벡터를 계산하고 rule 조회해서 추천 결과 내기 + 4. T3 일관성 테스트 아예 새로 파야함(설계 자체가 다름), T4 구조적 재설계 필요 + 2. T3. 일관성 테스트 - Jaccard ≥0.99 너무 낮음 + 1. 추천 엔진은 LLM을 실시간으로 호출하는게 아니라 이미 추출된 것을 바탕으로 순수 rule 기반이기 때문에 + 2. 동일 입력 → 동일 출력이 보장되어야 함. 100%가 되어야 함 + 3. temperature 조정 하는건 제거 하는걸로 (실제 측정 대상이 없음) + + ⇒ 동일 논문 텍스트 → LLM 추출 5회 반복 → evidence 필드별 일치율을 확인하는 쪽으로 해야함 (study_type, outcome_normalized, effect_direction, confidence_score) + + 3. T4. 5-fold CV가 아님 지금 5000편 논문이 아니라 1,000편 논문 상태다. 근데 이게 문제가 아니라 rule coverage는 달라질수밖에 없음 subset 마다 + 1. 성분 분포가 달라져서 표준편차가 5%p 이내로 나오기 어려움. 논문 수를 조정하거나 표준편차를 조정하거나 + 2. 5-fold 검증을 하려면 rule 생성 코드를 5번 다른 subset으로 돌려야 함. 기술적으로는 가능하지만 fold당 200편인 지금 규모에서는 통계적 의미가 없음. + 3. 또한 검증 fold로 무엇을 측정하는지 정의 필요 - held-out 논문 클레임이 생성된 rule에 포함되는지, 이 설계는 rule 커버리지 테스트이지 추천 정확도 테스트가 아님 + 4. **fold마다 coverage가 달라지는 건 시스템의 불안정성이 아닌 당연한 결과임** + 1. 오메가3 논문이 fold 1에 몰려있으면 오메가3 rule 만 많이 나오는거고. 이걸 비교해서 얻는 게 없다 + 2. k-fold는 ML에서 “학습 데이터로 본 적 없는 레이블을 예측할 수 있는가”를 텍스트하는 건데 이 시스템엔 예측 대상 레이블이 없음. 측정할 가치도 없고 + + ⇒ Rule Score 안정성 측정으로 (부트스트랩 방법) + + - 논문 중 무작위 20%를 제거하고 rule 생성 코드를 돌려서 핵심 ingredient-outcome 쌍의 rule_score 변동이 얼마나 되는지 N회 반복해서 표준편차 + - 오메가3 + cardiovascular_risk 가 전체 논문에서는 rule score가 0.82였고 80% bootstrap 20회 평균으로는 0.79가 나왔다면 0.04가 표준편차(안정적) + - 마그네슘 + sleep_problem 이 전체 논문에서는 0.61 이었고 80% 부트스트랩 20회 평균 냈더니 0.58 나왔다면 0.12가 불안정 (논문 수 부족으로) + - 코엔자임, 크롬, 비타민, 전해질 폴더 논문들 수 적음 (20개이하) - 부트스트랩 취약 + 4. T1. Adversarial 케이스 중 Rule DB 커버리지 확인 + 1. 와파린 + 비타민 K, 레보티록신 + 칼슘 등은 safety_rules에 있어야 차단이 됨. 논문에서 말하지 않았거나 사전 임상 지식 레이어에 없으면 차단이 안되기 때문에 + 2. 차단되어야하는것들을 전문가를 통해 리스트업 요청하고 그걸 프로젝트 내에 넣은 후에 차단되는지 확인하는 방식으로 가야함 → 검증 방법 자체는 good \ No newline at end of file diff --git a/assignments/latteeea/week3/data/claude_cursor.md b/assignments/latteeea/week3/data/claude_cursor.md new file mode 100644 index 0000000..fc3da96 --- /dev/null +++ b/assignments/latteeea/week3/data/claude_cursor.md @@ -0,0 +1,752 @@ +이 프로젝트의 전체 구조를 분석해줘. 특히 논문 데이터를 Atomic Facts로 추출해서 최종 Rule을 생성하는 'split-mapping-reduce' 파이프라인의 핵심 파일들과 로직이 어떻게 연결되어 있는지 요약해줘. 해당 파이프라인이 아닌 다른 구조라면 뭐라고 명명할 수 있는지도 알려주고. + +- cursor + + 프로젝트를 보면 큰 흐름이 **`수집/추출 → 정규화/저장 → 룰 생성 → 추천/검증 → 외부 동기화`**로 나뉩니다. + + 질문하신 “논문 데이터 → Atomic Facts → 최종 Rule” 경로는 실제로 존재하고, 핵심은 `pipeline/generate_rules.py`를 중심으로 연결되어 있습니다. + + ## **전체 구조 (상위 관점)** + + - **논문 처리(ingest)** + - `pipeline/process_drive_sample_batch.py`: 구글 드라이브에서 PDF 샘플 배치 처리 + - `pipeline/process_one_paper.py`: 단일 PDF 처리 오케스트레이션 + - `services/pdf_service.py`: PDF 텍스트 추출 + - `services/llm_service.py`: 논문 텍스트를 구조화된 `evidence_rows`로 변환 + - `services/csv_service.py`: `paper_registry.csv`, `evidence_db.csv` 저장 + - **룰 엔진(rule engine)** + - `pipeline/generate_rules.py`: 전체 룰 생성 파이프라인 엔트리 + - `pipeline/rule_engine/*`: 점수화, 세그먼트화, 충돌감지, atomic fact 생성, 정책 문장 생성 + - 산출: `evidence_with_scores.csv`, `conflicts_report.csv`, `atomic_facts.csv`, `final_guidelines.csv`, 그리고 유형별 룰 CSV들 + - **추천/QA** + - `services/recommendation_service.py`, `pipeline/recommend.py`: state tag 기반 기본 추천 + - `services/recommendation_mvp.py`, `pipeline/recommend_mvp.py`: 설문 기반 니즈 벡터 + 안전/상호작용 정책 반영 + - `pipeline/run_qa_scenarios.py`: QA 시나리오 일괄 실행 + - **외부 연동** + - `pipeline/sync_rules_to_notion.py`, `services/notion_rules_service.py`: 룰 CSV를 노션으로 업서트 + - `services/notion_service.py`: 논문/근거 페이지 저장 + + --- + + ## **split-mapping-reduce 파이프라인 연결 (핵심)** + + 질문하신 흐름을 파일 기준으로 연결하면: + + 1. **Split (논문을 근거 단위로 분해)** + - `process_one_paper.py`가 `llm_service.analyze_paper_with_llm()` 호출 + - `llm_service.py`에서 논문 1편을 최대 6개 `evidence_rows`로 분해/정제 + - 결과를 `csv_service.append_evidence_rows()`로 `evidence_db.csv`에 적재 + 2. **Mapping/Normalization (근거를 분석 가능한 축으로 매핑)** + - `generate_rules.py`에서 순차 실행: + - `evidence_scoring.enrich_evidence_with_scores()`→ `study_type × confidence` 기반 `evidence_score` + - `population_segment.enrich_evidence_with_population_segments()`→ `condition_population` 텍스트를 세그먼트 태그로 매핑 + - `dosage_normalize.enrich_evidence_with_dosage_normalization()`→ 투여경로(IV/Oral) 및 용량 단위 정규화 + - 그리고 `conflicts.identify_conflicts()`로 동일 축 내 상충 근거 탐지 + 3. **Reduce (집계/합성)** + - `atomic_facts.generate_atomic_facts()`에서 + - 그룹 키: `rule_ingredient + outcome_normalized + population_segment + dose_route + claim_subject` + - 집계값: 근거 수, 점수 합, 방향별 점수, 지배 방향 + - 이 단계가 사실상 질문하신 “Atomic Facts 생성”의 핵심 reduce + 4. **Final Rule/Guideline 생성** + - `policy_language.build_final_guidelines(atomic_facts, conflicts)`에서 + - conflict verdict 조인 + - recommendation tier 계산 + - 최종 한국어 policy statement 생성 + - 별도로 `generate_rules.py` 내 `generate_association_rules / generate_dose_rules / generate_safety_rules / generate_interaction_rules` + - 실서비스용 구조화 룰 CSV 생성 + + --- + + ## **이걸 꼭 split-mapping-reduce라고 부를 수 있나?** + + 가능은 하지만, 코드 구조상 더 정확한 명명은 아래가 맞습니다. + + - **Evidence-to-Policy Aggregation Pipeline** (가장 정확) + - **Extract-Normalize-Aggregate-Compose (ENAC) Pipeline** + - **Rule Synthesis Pipeline from Evidence Graph** + - 한국어로는 **“근거 집계 기반 규칙 합성 파이프라인”** + + 이유: + + - 단순 split-map-reduce보다, 실제로는 + - 상충 탐지(conflict mediation), + - 신뢰도 가중치(scoring), + - 정책 문장 합성(policy language generation) 이 포함된 **다단계 합성 파이프라인**이기 때문입니다. + + 원하시면 다음 답변에서 이 구조를 **한 장짜리 아키텍처 다이어그램 형태**(입력/출력 CSV와 함수 호출 순서 포함)로 정리해드릴게요. + +- claude code + + ```markdown + 1. 프로젝트 전체 구조 + + health_algorithm/ + ├── main.py (빈 엔트리 — 실제 진입점은 pipeline/*) + ├── config.py, create_notion_schema.py + ├── models/evidence_models.py (Pydantic: EvidenceRow / PaperAnalysisResult) + ├── services/ (외부 시스템 + 비즈니스 로직) + │ ├── drive_service.py (Google Drive 목록·다운로드) + │ ├── pdf_service.py (PDF → 텍스트) + │ ├── llm_service.py (OpenAI 호출 + 프롬프트) + │ ├── normalization_service.py + │ ├── csv_service.py (paper_registry / evidence_db I/O) + │ ├── notion_service.py, notion_rules_service.py + │ ├── recommendation_service.py / recommendation_mvp.py + │ ├── survey_needs.py, policy_config.py + ├── pipeline/ (배치 오케스트레이션) + │ ├── process_drive_sample_batch.py (성분 폴더 루프) + │ ├── process_one_paper.py (1논문 → Evidence rows 저장) + │ ├── generate_rules.py (⭐ Rule 생성 엔트리) + │ ├── recommend.py / recommend_mvp.py + │ ├── run_qa_scenarios.py, summary_report.py, sync_rules_to_notion.py + │ └── rule_engine/ (순수 함수 변환 모듈) + │ ├── common.py (claim_subject, rule_ingredient 추론) + │ ├── evidence_scoring.py (study_type × confidence → evidence_score) + │ ├── population_segment.py, dosage_normalize.py + │ ├── conflicts.py (동일 성분·결과 positive/negative 충돌) + │ ├── atomic_facts.py (⭐ 중간 레이어: 그룹 집계) + │ └── policy_language.py (Atomic Fact → 한국어 가이드라인) + └── data/ + ├── paper_registry.csv, evidence_db.csv (1차 산출) + ├── evidence_with_scores.csv, conflicts_report.csv (파생) + ├── atomic_facts.csv (중간 레이어) + ├── final_guidelines.csv (정책 문장) + └── association_rules.csv, dose_rules.csv, safety_rules.csv, interaction_rules.csv + + 2. 파이프라인 연결 흐름 (논문 → Rule) + + [Drive PDF] + │ process_drive_sample_batch.py (성분별 루프, N개 샘플링) + ▼ + process_one_paper.py + │ pdf_service.extract_full_text + │ llm_service.analyze_paper_with_llm → PaperAnalysisResult (evidence_rows: List[EvidenceRow]) + │ csv_service.append_evidence_rows → evidence_db.csv + │ notion_service.save_analysis_to_notion + ▼ + [evidence_db.csv] ← 행 단위 원자 근거 (논문 × 클레임) + │ + │ generate_rules.main() (pipeline/generate_rules.py) + │ + ├─ enrich_evidence_with_scores (rule_engine/evidence_scoring.py) + │ study_type·confidence → evidence_score + ├─ enrich_evidence_with_population_segments + ├─ enrich_evidence_with_dosage_normalization + │ → evidence_with_scores.csv + │ + ├─ identify_conflicts (rule_engine/conflicts.py) + │ (rule_ingredient, outcome_normalized, claim_subject) 그룹의 + │ positive vs negative 가중합 비교 → conflicts_report.csv + │ + ├─ generate_atomic_facts (rule_engine/atomic_facts.py) ⭐ 중간 레이어 + │ 그룹키: (rule_ingredient, outcome_normalized, population_segment, + │ dose_route, claim_subject) + │ 집계: n_evidence, score_sum, 방향별 점수, dominant_direction + │ → atomic_facts.csv + │ + ├─ build_final_guidelines (rule_engine/policy_language.py) + │ atomic_facts ⋈ conflicts → recommendation_tier + 한국어 policy_statement + │ → final_guidelines.csv + │ + └─ generate_{association|dose|safety|interaction}_rules (generate_rules.py) + evidence_with_scores 에서 각 evidence_type별 재그룹 → 4종 rules CSV + + [final_guidelines.csv + *_rules.csv] + │ recommendation_service / recommend.py + ▼ + state_tags 쿼리 → 추천 JSON + + 핵심 중간 레이어 도입 이유 (commit c13510f): Evidence(서술형 LLM 추출) ↔ Rule(의사결정용 집계) 사이 간극이 커서 atomic_facts 레이어를 추가, 토큰화·집계·방향 결정을 여기로 모음. + + 3. "Split–Map–Reduce"로 부를 수 있는가 + + 형태적으로는 MapReduce와 부분적으로 일치합니다 — (a) 논문을 Evidence 행으로 쪼개기(Split/Flat-Map by LLM), (b) 각 행에 점수·세그먼트·claim_subject enrichment(Map), (c) 그룹바이 집계(Reduce) 구조를 갖습니다. + 다만 atomic_facts → final_guidelines는 조인 + 자연어 템플릿 합성이라 순수 Reduce는 아닙니다. + + 더 정확한 명명 후보 + - Evidence Synthesis Pipeline (체계적 고찰 용어로 가장 자연스러움) + - Medallion / Bronze→Silver→Gold 아키텍처 — paper_registry · evidence_db(Bronze) → evidence_with_scores · atomic_facts(Silver) → final_guidelines · *_rules(Gold) + - Extract → Normalize → Aggregate → Publish (ENAP) — LLM 기반 논문 추출 파이프라인의 일반 패턴 + - Claim Distillation Pipeline — 개별 클레임(Evidence)을 신뢰도 가중으로 증류해 Rule/가이드라인으로 승격시키는 관점 강조 + + 현재 코드베이스가 가장 가까운 건 "LLM-Extracted Evidence → Atomic Fact Aggregation → Policy/Rule Synthesis" 형태의 단계적 증거 증류 파이프라인이며, 구현 실체는 pandas 기반 groupby 파이프라인이라 엄밀한 분산 + MapReduce라기보단 ELT 스타일 Medallion 아키텍처로 부르는 편이 가장 맞습니다. + ``` + +- 차이 + + 커서는 해당 파이프라인을 근거 집계 기반 규칙 합성 파이프라인이라고 했고, 클로드는 Medallion 아키텍처 (bronze → silver → gole) : 데이터 엔지니어링 용어를 사용함 (지능이 다르다…) → 철학이 담긴다. 데이터의 증류를 아키텍처 관점에서 봄 + claim distillation (증류 파이프라인 : 파편화된 주장을 순도 높은 규칙으로 도출) + + +현재 pipeline/rule_engine/atomic_facts.py와 generate_rules.py에서 가중치(Scoring)를 부여하고 근거 간의 충돌을 해결하는 로직을 집중적으로 봐줘. 지금 방식에서 논리적으로 보완이 필요하거나, 영양소 조합 레시피를 산출할 때 정확도를 떨어뜨릴 만한 위험 요소가 있다면 우선순위 순으로 찾아줘. 최대 10가지, 10개를 다 채울 필요는 없음. + +- cursor + + 우선순위 높은 순으로, **정확도/레시피 품질 저하 가능성**만 추렸습니다. + + - **[높음] 충돌 판정이 레시피 산출 경로에 반영되지 않음** + - `generate_rules.py`에서 충돌 리포트를 만들지만, 추천 엔진은 `association/dose/safety` CSV만 사용해 충돌 정보를 직접 소비하지 않습니다. 결과적으로 상충 근거가 있는 성분도 조합 추천에 그대로 들어갈 수 있습니다. + + generate_rules.pyLines 872-893 + + df_scored = enrich_evidence_with_scores(df) + + ... + + conflicts_df = identify_conflicts(df_scored) + + ... + + association_df = generate_association_rules(df_scored) + + dose_df = generate_dose_rules(df_scored) + + safety_df = generate_safety_rules(df_scored) + + interaction_df = generate_interaction_rules(df_scored) + + - **[높음] `atomic_facts` 집계에 paper 단위 cap이 없어 단일 논문 과대표 가능** + - `association_rules`는 논문별 기여도 cap(`_capped_signed_score_by_paper`)을 쓰는데, `atomic_facts`/`conflicts`는 단순 합산이라 같은 논문 다건이 방향을 밀어버릴 수 있습니다. + + generate_rules.pyLines 108-127 + + def _capped_signed_score_by_paper(...): + + ... + + by_paper = by_paper.clip(lower=-per_paper_cap, upper=per_paper_cap) + + atomic_facts.pyLines 149-166 + + n = len(g) + + total = float(g["_w"].sum()) + + ... + + dom = _weighted_dominant_direction(g) + + - **[높음] 충돌 판단이 `positive/negative`만 사용해 `mixed/null` 근거를 버림** + - 실제로는 혼합/무효 근거가 많은데도 충돌 비율 계산에서 제외되어, 양/음의 점수 분율이 과장될 수 있습니다. + + conflicts.pyLines 82-95 + + pos = g[g["_dir"] == "positive"] + + neg = g[g["_dir"] == "negative"] + + mixed = g[g["_dir"] == "mixed"] + + ... + + total = s_pos + s_neg + + share = s_pos / total + + - **[중간] `dominant_direction`이 건수 기반(count), 점수 기반(weight)과 불일치** + - `association_rules`의 대표 방향은 가중치가 아니라 건수로 결정됩니다. 저품질 다건이 고품질 소수보다 방향 판정에 더 큰 영향 줄 수 있습니다. + + generate_rules.pyLines 311-324 + + positive_count = int(...) + + negative_count = int(...) + + ... + + dominant_direction = compute_dominant_direction( + + positive_count, negative_count, mixed_count, null_count + + ) + + - **[중간] `atomic_facts` 대표 방향 tie-break가 `positive` 편향** + - 가중치 합이 동률일 때 `max()`가 dict 순서상 먼저 나온 `positive`를 선택할 수 있어, 경계 케이스에서 체계적 편향이 생깁니다. + + atomic_facts.pyLines 41-55 + + for d in ["positive", "negative", "mixed", "null", "unclear"]: + + sums[d] = ... + + best = max(sums, key=lambda k: sums[k]) + + ... + + return best + + - **[중간] `atomic_facts`가 evidence type을 분리하지 않아 이질 근거가 같은 fact로 합쳐질 수 있음** + - 그룹 키에 `evidence_type`이 없어, 동일 `outcome_normalized`를 쓰는 관찰/기전/안전 문맥이 합쳐질 여지가 있습니다(특히 데이터 품질이 흔들릴 때). + + atomic_facts.pyLines 65-66 + + 그룹 키: rule_ingredient, outcome_normalized, population_segment, dose_route, claim_subject + + - **[중간] 스코어 체계가 이원화되어 파이프라인 해석 일관성 저하** + - 충돌/atomic은 `evidence_score`(0~1 스케일), association는 `confidence_weight*direction_sign`(정수 스케일)이라 동일 데이터에 대해 중요도 판단 기준이 달라질 수 있습니다. + + evidence_scoring.pyLines 14-42 + + study_type_weight * confidence_weight -> evidence_score + + generate_rules.pyLines 86-101 + + def confidence_weight(...) -> int + + def direction_sign(...) -> int + + - **[낮음~중간] 음수 근거 정보가 `weighted_score` floor 처리로 일부 손실** + - `weighted_score_raw`가 음수여도 `weighted_score = max(0, raw)`로 잘려 support 계산에 반영력이 줄고, 이후 규칙 소비자가 raw 대신 floor 값을 보면 리스크 신호가 약해집니다. + + generate_rules.pyLines 317-319 + + weighted_score_raw = _capped_signed_score_by_paper(group) + + weighted_score = max(EFFICACY_WEIGHTED_SCORE_FLOOR, weighted_score_raw) + + --- + + 확인하고 싶은 가정 2가지: + + - 레시피 추천이 현재 `final_guidelines`나 `conflicts_report`를 전혀 읽지 않는 구조가 맞는지 + - `atomic_facts`를 실제 추천/랭킹 입력으로도 쓸 계획인지(현재는 주로 guideline 생성용으로 보임) +- claude + + ```tsx + ● 전체 파이프라인을 파악했습니다. 우선순위 순으로 정리합니다. + + --- + + 스코어링·충돌 로직 위험 요소 (우선순위 순) + + --- + + 1. confidence_weight 함수가 두 곳에 따로 존재하는 이중 스케일 문제 — 최우선 + + evidence_scoring.py와 generate_rules.py에 같은 이름의 함수가 별도로 있는데 반환 타입과 스케일이 다릅니다. + + # evidence_scoring.py (float, 0~1 범위) + + def confidence_weight(confidence: str) -> float: + high=1.0, medium=0.7, low=0.4, default=0.6 + + # generate_rules.py (int, 1~3 범위) + + def confidence_weight(confidence: str) -> int: + high=3, medium=2, low=1 + + atomic_facts·conflicts는 float 기반 evidence_score를 쓰고, association_rules·dose_rules·safety_rules는 integer 기반 weighted_score를 씁니다. 두 파이프라인이 같은 데이터에서 출발해도 수치 체계가 달라 + "final_guidelines가 권장, association_rule은 weak" 같은 모순이 구조적으로 발생할 수 있습니다. + + --- + + 1. 충돌(conflict) 탐지 키가 atomic_facts 집계 키보다 거칠어서 오염되는 문제 — 최우선 + + # [conflicts.py](http://conflicts.py/): 3-tuple + + for (ri, out, cs), g in d.groupby(["_ri", "_out", "_cs"], ...) + + # atomic_facts.py: 5-tuple + + for (ri, out, pop, route, cs), g in d.groupby(["_ri", "_out", "_pop", "_route", "_cs"], ...) + + final_guidelines 빌드 시 (ri, ou, cs) 키로 conflict lookup을 합니다. 즉, "고령자"에서 발견된 positive↔negative 충돌이 "건강 성인" atomic fact에도 그대로 적용됩니다. 인구 집단별로 효과 방향이 다를 수 있는 + 영양소(예: 철분, 비타민 D)에서 잘못된 tier_mixed_review 판정을 유발합니다. + + --- + + 1. _weighted_dominant_direction에 최소 우세 마진 기준이 없는 문제 + + best = max(sums, key=lambda k: sums[k]) + if sums[best] <= 0: + return "unclear" + return best + + positive=0.41, negative=0.40이어도 "positive" dominant를 반환합니다. 이 값이 compute_recommendation_tier에 들어가 tier_strong_positive까지 도달할 수 있습니다. 레시피에서 거의 상충되는 근거를 강한 권장으로 + 오분류하는 가장 직접적인 위험입니다. 최소한 (sp - sn) / (sp + sn) > 0.15 같은 최소 마진 기준이 필요합니다. + + --- + + 1. dose 규칙과 safety 규칙에 논문(paper) 단위 상한이 없는 문제 + + association_rules와 interaction_rules에는 _capped_signed_score_by_paper(cap=2)가 있지만: + + # dose_rules (generate_rules.py:461) + + weighted_score = sum(confidence_weight(str(c)) for c in group["confidence"].tolist()) + + # safety_rules (generate_rules.py:811) + + weighted_score = sum(confidence_weight(str(c)) for c in group["confidence"].tolist()) + + 한 논문이 여러 용량 또는 부작용 항목을 보고하면 그 논문의 기여가 무제한으로 축적됩니다. 논문 1편이 3개 dose entry를 내면 weighted_score=9 → strong으로 판정될 수 있습니다. + + --- + + 1. compute_recommendation_tier에서 share 분기가 누락된 케이스 + + if share >= 0.65 and dom == "positive": # ✅ + ... + if share >= 0.55 and dom == "positive": # ✅ + ... + if share <= 0.35 and dom == "negative": # ✅ + ... + if share <= 0.35 and dom == "positive": # ✅ + ... + + # share >= 0.65인데 dom == "mixed"나 "unclear"이면? → 아무 분기도 안 걸림 + + share >= 0.65이지만 dominant_direction이 "mixed"인 경우 어떤 분기도 통과하지 못하고 하단의 score 기반 로직(s >= 1.5 → moderate)으로 낙하합니다. share 정보가 완전히 무시됩니다. policy_language.py:166 참조. + + --- + + 1. atomic_facts와 association_rules가 같은 데이터를 병렬로 집계하지만 조율 없이 독립 출력되는 구조 + + main()에서: + atomic_facts_df = generate_atomic_facts(df_scored) # → final_guidelines + association_df = generate_association_rules(df_scored) # → association_rules.csv + + 같은 데이터에서 final_guidelines와 association_rules가 별개로 만들어지는데 그룹 키가 다릅니다 (outcome vs state_tag). 레시피 로직이 두 파일을 모두 참조할 때 **성분 A가 final_guidelines에서는 "강력 권장", + association_rules에서는 "weak"**으로 나오는 모순을 reconcile하는 코드가 없습니다. + + --- + + 1. choose_representative_text가 품질이 아닌 길이 기준으로 대표 문장을 선택 + + def choose_representative_text(series: pd.Series) -> str: + values.sort(key=len, reverse=True) + return values[0] + + 가장 긴 문장 = 가장 verbose한 문장이지, 가장 high-confidence나 high-score인 문장이 아닙니다. 레시피 출력에 표시되는 representative_statement가 case report 수준의 장황한 서술에서 왔을 가능성이 있습니다. + + --- + + 1. LLM이 출력한 direction="mixed"와 내부 집계로 산출한 "mixed" 개념이 구분되지 않음 + + compute_recommendation_tier에서 tier_mixed_review는 0.35 < share < 0.65 조건에서만 발동합니다. 그런데 LLM이 단일 논문 결과 자체를 "mixed"라 표기한 경우, share 계산에 분자(positive)도 분모(negative)도 없어 + share=None이 됩니다. 결과적으로 "mixed" 방향의 논문 3편이 있어도 tier_mixed_review가 아닌 tier_reference나 tier_limited로 분류됩니다. + + --- + + 1. evidence_score_sum에 null/unclear 근거가 포함되지만 weighted_positive_share 분모에는 제외되는 분모 불일치 + + atomic_facts.evidence_score_sum은 모든 방향의 score 합입니다. 그러나: + + # policy_language.py:122 + + share = sp / (sp + sn) # null, unclear 무시 + + null/unclear evidence가 많을수록 evidence_score_sum은 크지만 share 계산에는 반영되지 않아, score_sum이 높게 나오지만 실제 유효 방향 비율은 낮은 음영 구간이 생깁니다. + + --- + + 1. dose 규칙에서 신뢰도 낮은 논문이 min/max 용량을 결정할 수 있는 문제 + + "dose_min": float(group["dose_value"].min()), + "dose_max": float(group["dose_value"].max()), + + 집계 전에 confidence 기준 필터링이 없습니다. low-confidence case report 1건의 극단 용량이 dose_min 또는 dose_max로 노출됩니다. 레시피에서 최소/최대 용량 범위 표시에 직접 영향을 줍니다. + ``` + +- claude 충돌 브리핑 내용 확인 + 1. confidence weight 스케일 통일 안됨 (evidence_scoring과 generate_rules 다름) → float로 채택 + + ![{2A5378E3-786B-4CFE-A97B-08E3B9F29A60}.png](attachment:7dd6160d-3964-40bd-a08d-56bcc5badcfb:2A5378E3-786B-4CFE-A97B-08E3B9F29A60.png) + + ![{571C6A33-F0C4-4514-BF28-01B994160345}.png](attachment:e8b011e0-234a-4df6-b2be-03fd194cd598:571C6A33-F0C4-4514-BF28-01B994160345.png) + + 2. 우리가 원하는 방향성과 코드의 방향성의 일치성 점검 및 보완점 확인 + 1. 좋은 구조 + 1. LLM 출력 행 제한 준수 (evidence 개수 초과한 것 없음) + 2. dose 수치 필드 완결성 (dose_value/unit 채워짐) + 3. 핵심 설문 state_tag 커버 (14개 중 9개 커버함) - 좋은 게 아닌데?? + 4. 파이프라인 아키텍처 (Bronze → Silver → Gold 단계 분리 명확) + 5. conflict 탐지 및 tier 산출 (claude 수정으로 구조적 차단 완료) + 2. 보완 필요 + 1. target_ingredient 정규화 없음 + : magnesium이 17가지가 나와서 atomic_facts가 636개 있으면 evidence 567개가 나와서 거의 모든 게 잠재적 유익성(근거 부족 tier)로 강등됨. + → target_ingredient→canonical ingredient 매핑 테이블을 config에 추가하고 enrich_evidence_with_scores 단계에서 rule_ingredient를 canonical form으로 정규화 + 2. state_tags 미입력된 것들 (햇빛 노출, 유제품, 생선, 당류, 신경계 없음) + 3. confidence를 llm에게 과대하게 부여하는 것 → bias 유도 + evidence scoring에서 RCT = 1.0이 atomic_facts weight에 들어가는데 llm이 rct 처리할때 기계적으로 high를 부여하면 근거 강도가 훨씬 빡세짐 (과평가), 다변량 보정 여부, 표본 수, 선택 평향을 confidence 판정 기준으로 넣었지만 실제 반영 안되고 있는 문제 + 4. dose_route = unknown 이 너무 많음 (똑같은 oral 복용인데 dose_route가 달라서 다른 그룹이 되는 것, n_evidence = 1 fragmentation 의 원인. unknown 추론 로직이나 병합하는 전처리 필요 (groupby 단에서) + 5. dose_unit이 known이면 dose_rules 제외되는 거 아까움. 단위 추론 로직 보완이 필요 + 6. +- 차이 + + 커서는 이 파일은 이걸 쓰고, 저 파일은 이걸 안 쓰고의 가시적인 surface 차이점 위주, 구조적인 불일치를 찾는 데에서 그침 + + 클로드는 1번 (이중 스케일 문제)에서 같은 함수이름인데 타입과 범위가 달라서 발생하는 모순을 찾아냄 로직의 충돌… 시한폭탄 같은 것 + 2번 (충돌 탐지 키 오염) : 고령자가데이터가 성인 데이터에 영향을 줄 수 있다는 것을 파악한것. 코드를 단순히 읽는 게 아니라 데이터의 흐름과 비지니스 로직을 이해하고 있는 것. + + → 이게 왜 레시피에서 문제가 되는지, 비지니스 임팩트를 연결해서 설명함 + + +같은 질문을 던졌을 때, 단순 코드 불일치를 찾는 Cursor와 **데이터 스케일의 모순과 도메인 지식(영양소 충돌)의 허점**을 찾아낸 Claude Code의 차이. + +지적해준 10가지 포인트가 정말 날카로워. 특히 1번(이중 스케일)과 2번(충돌 키 오염)은 지금 당장 해결해야겠어. + +1. `evidence_scoring.py`와 `generate_rules.py`의 `confidence_weight` 스케일을 하나로 통일하는 리팩토링을 진행해줘. 어떤 스케일이 더 합리적일지 먼저 제안해줘. +2. 충돌 탐지 키를 `atomic_facts`와 동일하게 5-tuple로 맞춰서 인구 집단별 데이터 오염을 막는 코드를 수정해줘. + +이 작업을 마친 뒤에, 전체 파이프라인을 다시 돌려서 `conflicts_report.csv`에 어떤 변화가 생기는지 확인해보고 싶어. + +![{46FBCF66-9539-4EA9-A5A7-4B372B9D50FC}.png](attachment:358720f5-135d-45d2-876a-54def5087408:46FBCF66-9539-4EA9-A5A7-4B372B9D50FC.png) + +클로드가 있더라도 판단하고 실행하는 건 우리의 몫… 무작정 실행하는것보다 실행의 이유를 따지는 게 중요함 + +- DRI 테이블 생성하기 (베이스 레시피 도출용) + +![{7B81DE38-6EFE-433B-B057-72C34DE40F52}.png](attachment:c058d7be-0ff0-4cc6-b428-ca7a48aa4511:7B81DE38-6EFE-433B-B057-72C34DE40F52.png) + +- Dose 와 관련된 UL 넘지 않도록 클램핑 하고 BMI 계산하는 것도 설문에 추가 +- 13개 시나리오 업데이트 및 실행 완료 → 정확히 동작함 + - S11 노인 여성 68세 57kg: 단백질 1.0g/kg (노인 보정) × 57kg = 57g, 마그네슘 228mg + - S12 비만 남성 BMI 33.7: 비타민D BMI 2.0x 계수 → 20μg (표준 10μg의 2배) + - S13 고강도 운동 80kg: 단백질 1.6g/kg × 80kg = 128g, 마그네슘 cap 350mg + +![{7A427CD8-8C69-447D-8FA9-51D28F76A6BA}.png](attachment:e9ad87c5-8c95-4715-a6c7-01f372fe1b81:7A427CD8-8C69-447D-8FA9-51D28F76A6BA.png) + +![{8B2E1E2C-BE45-4935-86D4-ED88C1F67947}.png](attachment:6587c599-edc2-4405-8606-b89ab8b41212:8B2E1E2C-BE45-4935-86D4-ED88C1F67947.png) + +아까 전에는 논문 다시 돌릴 필요 없다고 했는데 안 돌렸더니 누락된 필드 → 이게 하필 품질 테스트 측면에서 중요한 거였고 (canonical 반영) 이걸 모르고 코드를 수정할뻔했는데 중간에 작업을 막아서 괜찮았음 + +![{F8951F17-ADFD-4CD6-BA07-67C5AA0F84C2}.png](attachment:ba5a33b3-5e97-463b-a5bc-b2534ebfbdd6:F8951F17-ADFD-4CD6-BA07-67C5AA0F84C2.png) + +![{CD9C8E1C-E2F7-4298-A64B-532209217806}.png](attachment:e68a7fd7-7e3b-48c4-ae90-2c2f2108de52:CD9C8E1C-E2F7-4298-A64B-532209217806.png) + +품질 테스트를 진행하면서 잘못 나온 게 있으면 역추적 할 수 있어야 함 +빈혈 경험이 있다고 입력했는데 iron 의 추천 점수가 0.52 (논문 간 상충이 있었음) + +빈혈 경험과 iron은 필연적이고 상충되면 안되는 기본적인 지식인데 상충이 됐다는 건 논문에서 evidence를 추출하는 과정에서 에러가 있다는 것 → 확인해봤더니 ‘철분 보충이 혈색소에 부정적’이 아니라 ‘운동 강도가 높으면 철이 소모됨’을 관찰한 연구인데 ‘철 농도가 낮아졌다’는 결과를 보고 direction=negative로 추출한 것. + +![{27B74567-77F1-4353-BB6D-955B454B1EEE}.png](attachment:c874dd9a-aa22-4258-b3f3-cce55ebefdfe:27B74567-77F1-4353-BB6D-955B454B1EEE.png) + +- 역상관관계에 대한 내용을 direction = negative로 추출하는 llm 추출 로직 + - 엽산 상태와 호모시스테인이 역상관관계라는 내용인데 이를 direction = negative로 추출해서 빈혈 경험이 있음에도 불구하고 엽산이 제외된 성분으로 나옴. + → 실제 의미 : 엽산을 많이 먹으면 호모시스테인이 낮아져서 빈혈 위험이 감소하는것 = positive 임 + - 이 구조 (역상관관계를 negative로 잘못 해석)가 전체를 뒤집고 있음 → 음수 룰을 처리하는 로직 수정 필요 + - 문제가 두 층으로 겹쳐 있음 + - 1층 (데이터) : 엽산-호모시스테인 역상관관계를 direction=negative로 해서 folatexanemia_risk 룰이 음수로 나옴 + - 2층 (로직) : score ≤0 (음수)이면 hard exclude → prior_knowledge를 추가해도 negative(-1.3) + prior_knowledge(1.3) = 0.0 여전히 excluded 인것 + + ⇒ prior_knowledge weight를 weak보다 높게 설정해서 llm 단일 오류를 이길 수 있도록 + + +![{6E9CB3DD-3926-43C3-8389-147DC41F88A8}.png](attachment:bad633e0-6e5b-47ad-a590-901ec48038de:6E9CB3DD-3926-43C3-8389-147DC41F88A8.png) + +### 파이프라인 칭찬 + +![{F6EBED4E-903B-4281-9024-BC14125BF0AA}.png](attachment:a045e127-c2d5-498f-89ab-51e9888806f7:F6EBED4E-903B-4281-9024-BC14125BF0AA.png) + +![{42100BB8-6F72-4435-9EE2-0F1389ABF598}.png](attachment:041b2df1-be34-415f-8f5f-0d9063f721e2:42100BB8-6F72-4435-9EE2-0F1389ABF598.png) + +![{BD4556D3-0233-4C1D-8983-642141E5A765}.png](attachment:bbb5b3e6-c8f7-4089-ac79-de1cd93a5608:BD4556D3-0233-4C1D-8983-642141E5A765.png) + +![{B0892FAE-CEEC-476E-BD35-9104CC2B5903}.png](attachment:e4d0b695-c580-4ad1-ab21-25949d526329:B0892FAE-CEEC-476E-BD35-9104CC2B5903.png) + +![{9378BAF9-19F5-4DD9-87E2-3D5C50190CE3}.png](attachment:f275c5b7-b334-4867-ba47-f8bda0db3cee:9378BAF9-19F5-4DD9-87E2-3D5C50190CE3.png) + +| **평가 차원** | **항목** | **등급** | **상세 평가 및 상용화 관점의 분석** | +| --- | --- | --- | --- | +| **영양학적 정밀도** | **개인화 분석력** | **S+** | 사용자의 BMI, 식습관(고당 식이), 증상(피로, 빈혈)을 벡터화하여 1~8위까지 정교하게 도출함. | +| | **시너지 배합** | **S** | 비타민 C와 철분의 3배 흡수 로직 등 영양소 간의 상호작용을 계산에 반영함. | +| **임상적 안전성** | **고위험군 배제** | **S+** | **칼륨(Potassium)** 등 심장/신장에 민감한 성분을 'Hard Safety' 로직으로 자동 차단하여 법적 리스크 관리. | +| | **제형 최적화** | **A+** | 저체중 사용자를 위해 위장 부담이 적은 **Bisglycinate** 형태의 미네랄을 우선 매칭함. | +| **H/W 연동 적합성** | **투입량 변환** | **B+** | **순도/벌크 밀도** 개념이 도입되었으나, 실제 하드웨어의 오거(Auger) 제어값으로의 최종 매핑은 미팅 후 보완 필요. | +| | **용해 환경 제어** | **A** | 포화용해도에 따른 물 양 산출 로직이 설계되어 물리적 '떡짐'이나 미용해 현상을 예방 가능함. | +| **상업적 신뢰도** | **복약 지도력** | **S** | LLM 기반의 전문가 페르소나 총평이 도입되어, 단순 자판기를 넘어 '디지털 약사'의 경험을 제공함. | +| | **데이터 투명성** | **A** | DRI(권장량), UL(상한량), 논문 근거 용량을 모두 표기하여 전문가용 신뢰도 확보. | + +## 파이프라인 최종 구조 + +### LLM 기반 임상 근거 증류 파이프라인 (LLM-Augmented Clinical Evidence Distillation Pipeline) + +- LLM-Augmented : AI가 추천 주체가 아닌 근거 도출 도구로 사용 → 과장 없음 +- Clinical Evidence : 논문 근거 기반 → 규제 대응 근거 가능 +- Distillation : 수백 편 논문 → 단일 방향성 규칙으로 증류 - 정보 손실 없이 압축 + +### 아키텍처 구조 : Medallion Evidence Architecture (3-tier) + +1. Bronze : 논문 원자 근거 + 1. pdf → LLM 추출 → 행 단위 저장 +2. Silver : 정규화/점수화 근거 + 1. study_type 가중, confidence 보정, 결과 표준화 (atomic facts + evidence with scores) +3. gold : 의사결정 룰 + 1. association, dose, safety, interaction ruels + 2. 성분 x state_tag 집계 x 방향성/신뢰도 확정 + +⇒ Databricks가 정의한 Medallion Architecture와 동일한 계층 논리이며, 의약 데이터 파이프라인(FDA Sentinel, EMA 약물 감시)에서도 채택하는 표준 방식임 + +### 5가지 핵심 구조적 특징 + +① Claim Distillation (클레임 증류) + +논문 A: "마그네슘 → 수면 개선 (positive, RCT, high)" +논문 B: "마그네슘 → 수면 개선 (positive, RCT, medium)" +논문 C: "마그네슘 → 수면 개선 (mixed, Observational)" +↓ +association_rule: sleep_problem, dominant=positive, +positive_count=5, weighted_score=6.2, support=strong + +경쟁 차별점: 단순 논문 수 집계가 아닌 study_type × confidence × direction 가중 합산. 저신뢰 연구가 고신뢰 RCT를 이길 수 없습니다. + +--- + +② Dual-Source Knowledge Injection (이중 지식 주입) + +논문 근거 (Bottom-up) 임상 지식 (Top-down) +↓ ↓ +association_rules clinical_guarantees.yaml +prior_facts.yaml +↘ ↙ +추천 엔진 (병합) + +- Bottom-up: 논문에서 귀납적으로 추출된 규칙 +- Top-down: WHO·KDRIs·생리학 교과서 수준의 연역적 임상 지식 +- 두 레이어가 충돌하지 않고 보완: 논문 근거 부족 시 임상 지식이 fallback + +의미: 근거 공백(Evidence Gap)을 임상 지식으로 메우면서도 두 근거 출처를 혼합하지 않고 추적 가능하게 분리 관리. + +--- + +③ Multi-Signal Needs Vector (다중 신호 니즈 벡터) + +needs_vector = { +"fatigue_state": 1.63, # q_fatigue(direct) + q_anemia(supporting) +"anemia_risk": 1.50, # q_anemia(direct) + q_fatigue(supporting) +"glucose_variability": 1.10, # q_sugar(direct) +... +} + +설문 응답 → 단순 카테고리 매칭이 아닌 가중 연속값 벡터로 변환. 동일한 q_anemia=diagnosed 응답도 q_fatigue=high가 동반되면 anemia_risk 점수가 더 높아집니다. + +의미: 추천 결과가 설문의 조합에 반응 — 단일 응답이 아닌 사용자 전체 상태를 반영. + +--- + +④ Layered Safety Architecture (계층적 안전 필터링) + +Layer 1 — LLM 추출 Safety Rules +논문에서 추출된 contraindication/AE → hard/soft 분류 + +Layer 2 — 상호작용 검사 +추천 성분 간 cross-interaction pair 검사 + +Layer 3 — DRI UL Clamping +한국인 섭취기준 상한선 자동 조정 + +Layer 4 — Constraint Tags +흡연·음주·카페인 → 니즈벡터 아닌 별도 constraint 채널 + +의미: 일반 앱의 블랙리스트 방식과 달리 논문 근거가 있는 안전 필터 — 규제 대응 시 각 필터의 근거를 제시 가능. + +--- + +⑤ Self-Updating Rule Generation (자기갱신 룰 엔진) + +새 논문 추가 +↓ +process_one_paper (LLM 분석) +↓ evidence_db 자동 추가 +generate_rules (파이프라인 재실행) +↓ +새 association_rule 자동 생성 or 기존 룰 강화 + +논문 30편 추가 → 배치 실행 → 규칙 자동 갱신. 수작업 룰 편집 없음. + +의미: 논문 DB가 곧 경쟁 해자(Moat). 데이터가 쌓일수록 정밀도가 올라가는 플라이휠 구조. + +![{7B228EC3-A1C8-4DEC-AF20-B01B8092195F}.png](attachment:7dde4630-2372-4b0e-964f-9c8d08aca6cd:7B228EC3-A1C8-4DEC-AF20-B01B8092195F.png) + +### RAG의 구조를 정형 데이터 파이프라인으로 치환 (AI의 불안정성 제거) + +- 구조도 Mermaid 코드 + + ```tsx + flowchart TB + classDef input fill:#dbeafe,stroke:#3b82f6,color:#1e40af + classDef process fill:#d1fae5,stroke:#10b981,color:#065f46 + classDef store fill:#fef3c7,stroke:#f59e0b,color:#78350f + classDef config fill:#f3e8ff,stroke:#8b5cf6,color:#5b21b6 + classDef output fill:#fce7f3,stroke:#ec4899,color:#9d174d + + PDF["☁️ Google Drive\nPDF 논문 (성분별 폴더)"]:::input + USER["👤 사용자\n설문 응답 19개 + 신체정보"]:::input + + subgraph BATCH["🔄 배치 파이프라인 (오프라인 · 논문 → 규칙)"] + direction TB + A["pdf_service\nPDF → 텍스트"]:::process + B["llm_service · GPT-4.1-mini\n논문 → evidence rows (최대 6개/편)\nclaim_subject · direction + · confidence 추출"]:::process + C[("evidence_db.csv\n원자 근거 행")]:::store + D["evidence_scoring\npopulation_segment · dosage_normalize\nstudy_type × confidence → + evidence_score"]:::process + E[("evidence_with_scores.csv")]:::store + F["atomic_facts.py\n5-tuple 그룹 집계\ndominant_direction · + weighted_positive_share"]:::process + G["conflicts.py\n긍·부 충돌 감지\nconflict_verdict 판정"]:::process + H["policy_language.py\n추천 등급 8단계 판정\n한국어 가이드라인 생성"]:::process + I["generate_rules.py\n4종 규칙 CSV 생성"]:::process + + A --> B --> C --> D --> E + E --> F + E --> G + F --> H + G --> H + H --> I + end + + RULES[("association_rules.csv\ndose_rules.csv · safety_rules.csv\ninteraction_rules.csv · + final_guidelines.csv")]:::store + + subgraph CONFIG["⚙️ 설정 레이어 (data/config/)"] + direction LR + C1["algorithm_params.yaml\n점수·등급 임계값 18개"]:::config + C2["prior_facts.yaml\n임상 사전 지식\n(논문 없는 조합 fallback)"]:::config + C3["clinical_guarantees.yaml\n임상 보장 규칙 25개"]:::config + C4["ingredient_canonical_groups.yaml\n16개 성분 그룹·형태"]:::config + C5["dri_korean.csv\n한국인 영양소 섭취기준"]:::config + C6["constraint_warnings.yaml\n제약 조건 경고 매핑"]:::config + end + + subgraph ONLINE["⚡ 온라인 추천 엔진 (실시간)"] + direction TB + P1["survey_needs.py\n설문 → needs_vector + constraint_tags\nstate_weight × + question_importance"]:::process + P2["성분 점수 계산\nassociation_rules × needs_vector\nTop-5 rule 합산 + + dose_bonus"]:::process + P3["임상 보장 규칙 적용\nclinical_guarantees.yaml\nforce_core 성분 강제 포함"]:::process + P4["안전성 필터\nhard_exclude · soft_warning · soft_penalty\nconstraint_tag 경고 문구 + 추가"]:::process + P5["canonical 중복 제거\nform_selection 자동 선택\n소화 불편 → bisglycinate 등"]:::process + P6["DRI 용량 산출\n성별 × 연령 × 체중 기반\nUL 초과 시 자동 보정"]:::process + + P1 --> P2 --> P3 --> P4 --> P5 --> P6 + end + + OUT["📋 최종 추천 결과\ncore 성분 + supporting 성분\n권장 용량 · 추천 등급 · 안전 + 경고"]:::output + + %% ── 주요 데이터 흐름 ── + PDF --> A + USER --> P1 + I --> RULES + RULES --> P2 + RULES --> P4 + + %% ── 설정 레이어 → 파이프라인 ── + C1 --> F + C1 --> G + C1 --> H + C2 --> I + + %% ── 설정 레이어 → 추천 엔진 ── + C3 --> P3 + C4 --> P5 + C5 --> P6 + C6 --> P4 + + %% ── 출력 ── + P6 --> OUT + ``` \ No newline at end of file diff --git a/assignments/latteeea/week3/data/cursor_claude.md b/assignments/latteeea/week3/data/cursor_claude.md new file mode 100644 index 0000000..4b1511a --- /dev/null +++ b/assignments/latteeea/week3/data/cursor_claude.md @@ -0,0 +1,234 @@ +### 소재 + +- Cursor가 ‘멍청하게’ 느껴지기 시작했다 +- 앱/웹 4부를 만들며 느낀 한계 : 왜 나는 클로드로 갈아타려 하는가 (부제: 나는 커서를 클로드 처럼 쓰고 있었다) +- IDE의 안락함 vs CLI의 강력함 + +### 개요 + +- 커서로 만든 4개의 프로젝트 (23세, 4학년 학생이자 스타트업 개발자로서 커서를 어떻게 활용했는지, 구체적인 스택 포함) + - 나는 커서를 마스터했고, 그 한계를 명확히 느꼈다. +- 커서가 멍청하게 느껴진 순간들 + - 프로젝트 규모가 커졌을 때 맥락을 놓치던 경험 (아주 쉬운 것인데도 빙빙 둘러감) + - 반복이 계속되다보니 무한루프 에러가 채팅에서도 발견됨 + + ![image.png](attachment:e943e591-e963-4634-9995-349f3795119f:image.png) + + - 파일 안을 못봐서 직접 열어주고 코드 복사해줬던 경험 + - 설계를 하고 싶은데, 기본적인 타입 에러 하나 못 잡는 커서 때문에 코드 한 줄 수정에 매달리게 되는 상황 → 근본적인 문제를 해결하지 못하고 surface-level의 bug만 잡아냄. 근데 근본이 아니라 다른 곳에서도 같은 문제 발생 (아이디 변경 시 측정 상세 데이터 조회 불가 에러 - X-Username으로 인증하는 백엔드 문제였는데 items.name이 아니라 id를 써라는 이상한 말을 함) +- 나는 커서를 클로드처럼 쓰고 있었다 + - 커서와 클로드의 차이 (내가 생각하는) + - 나는 코드 한 줄을 부탁하는 게 아니라, 백엔드 로직 설계와 검토 사항을 먼저 던지며 ‘전체 그림’을 그리고 있었는데 + + ![image.png](attachment:6644fe71-765a-4a61-9690-e3490cc492c3:image.png) + + ![image.png](attachment:490b4921-a097-4c69-bd93-ce8923dcdd38:image.png) + + ![image.png](attachment:c776a63f-79ed-4240-9039-6b925d1535a5:image.png) + + ![image.png](attachment:d5bb6ee9-a820-4b74-9061-700b5a47dbe1:image.png) + + - 머릿속은 이미 에이전트를 원하고 있었는데, 손은 아직 도구에 갇혀 있었다. 그래서 갈아탄다 +- Claude Code를 사용후 깨달은 것 + - 프로젝트 맥락 파악에 대한 성능 + - 환경적 차이와 내가 느낀 효율성 (커서는 코드의 어느 부분이 변하는지 실시간으로 보이니까 보기에 확실히 편했다. 또한 클로드보다 커서가 훨씬 싸니까 학생 입장인 나는 좀 돌아가더라도 커서를 쓸 수밖에 없었다. 가시성과 가성비가 프로젝트를 하는데의 효율성보다 크다고 생각했던 것) → (claude code 써보고 적어보기) + + ![{BFF2B375-594E-4BC9-8EC2-AFB5875FFE1E}.png](attachment:eb87ff55-6497-44c8-b34e-f900b03c1396:BFF2B375-594E-4BC9-8EC2-AFB5875FFE1E.png) + + - 멍청한 도구를 쓰는 게 더 똑똑한 게 아닌가 라고 생각했지만, 똑똑한 도구가 나를 더 큰 문제에 집중하게 만든다는 확신. + +--- + +그동안 claude code를 알고 좋다는 걸 알면서도 cursor에 머물렀던 이유는 + +- GUI의 직관적인 편안함 : 코드가 실시간으로 바뀌는 게 보이니까 어디가 어떻게 변했다는 것을 바로 확인하는 건 심리적으로 꽤 큰 안정감을 줌 +- 학생 입장에서는 성능보다 가성비가 중요했음 : claude code의 api 비용이 부담스러울수밖에 없었다. 커서의 정액제는 조금 돌아가더라도 경제적으로 훨씬 이득이었다. + +→ 가시성과 가성비가 주는 효율이 개발의 속도보다 더 크다고 생각해왔다. 하지만 프로젝트가 고도화되고 관리하는 범위가 넓어질수록, ai를 달래고 수동으로 코드롤 옮기는 시간이 누적되면서 ‘아, 내가 아낀 돈보다 내 집중력과 시간이 더 빠르게 소모되고 있구나’ 라는 것을 느꼈다. + +→ cluade 써보고 난 후에 + +- 실시간으로 코드 변경 사항이 안보여서 불편할줄 알았는데, 오히려 결과물에 집중하게 되지는 않았나? +- 돈은 더 나가지만, 작업 시간이 얼마나 줄었는지 + +--- + +## 📝 블로그 제목 + +- **메인:** Cursor가 ‘멍청하게’ 느껴지기 시작했다: Claude Code로 갈아타는 이유 +- **부제:** 나는 이미 커서를 클로드처럼 쓰고 있었다 (IDE의 안락함을 넘어 CLI의 강력함으로) + +## 1. 프롤로그: 커서와 함께한 2년, 4개의 결과물 + +23살, 컴공 4학년이자 스타트업 개발자로서 지난 2년간 Cursor는 제 최고의 파트너였습니다. Jetpack Compose로 안드로이드 앱을, React Native(Expo)와 FastAPI로 풀스택 웹/앱/서버(aws ec2, rds, s3)을 6개나 뽑아냈죠. 저는 제가 커서를 마스터했다고 생각했습니다. 하지만 프로젝트 규모가 커질수록, 어느 순간부터 커서가 **'멍청하게'** 느껴지기 시작했습니다. + +## 2. 커서의 뇌절: "나는 AI를 달래는 유치원 선생님인가?" + +가장 답답했던 건 프로젝트의 **근본적인 맥락**을 놓칠 때였습니다. + +- **Surface-level Bug Fix의 한계:** 아이디 변경 시 데이터 조회가 안 되는 문제였는데, 백엔드의 `X-Username` 인증 로직을 보는 게 아니라 "변수명을 `name` 대신 `id`로 써보세요" 같은 엉뚱한 소리만 하더군요. 근본 원인은 못 찾고 겉핥기식 코드만 뱉어내는 거죠. +- **무한 루프와 멈춤 현상:** (이미지 1 첨부) 똑같은 말을 반복하며 대화창을 도배할 때는 정말 당혹스러웠습니다. +- **에이전트의 한계:** (이미지 5, 6 첨부) "탐색 에이전트 호출 불가" 메시지가 뜨며 채팅이 멈출 때, 제가 AI에게 했던 말은 **"한번에 다 안 해도 되니까 조금씩이라도 해봐"**였습니다. 제가 개발을 하는 건지, 과부하 걸린 AI를 달래는 상담가인지 헷갈리더군요. +- **‘파일 단위’로 코드를 짜는 비효율적인 경향** : 특히 프론트에서 심했는데 회원가입 단에 이름 입력 → 생년월일 입력 등 디자인이 일관되어 있는게 있어도 컴포넌트를 분리해서 재사용하지 못하고 페이지를 다 나눠서 파일을 만듦. 뒤로 가기 버튼 위치나 확인 버튼 디자인이 변경되면 두 파일 모두 수정해야하는 정말 안좋은 코드였음. 클로드로 리팩토링하기 (surface-level 개발 : 가장 큰 한계, 특히 유지보수 어려운 코드는 정말 안 좋은 코드니까) + +## 3. 깨달음: "나는 이미 커서를 클로드처럼 쓰고 있었다" + +돌이켜보니 저는 커서를 단순한 코드 편집기로 쓰지 않았습니다. + +- (이미지 2, 3, 4 첨부) 저는 항상 **백엔드 로직 설계와 비즈니스 흐름**을 먼저 던지고, AI에게 **"구현 전에 내가 검토해야 할 사항이나 설계상 헷갈리는 게 있는지"**를 먼저 물어봤습니다. +- 저는 '설계자'로서 전체 그림을 그리고 있었는데, 커서는 '에디터'라는 틀에 갇혀 제가 열어준 파일 안에서만 맴돌고 있었던 거죠. **제 머릿속은 이미 에이전트(Agent)를 원하고 있었는데, 제 손은 GUI라는 낡은 도구에 묶여 있었습니다.** + +## 4. 그럼에도 참았던 이유 (가성비, 가시성) + +그동안 claude code를 알고 좋다는 걸 알면서도 cursor에 머물렀던 이유는 + +- GUI의 직관적인 편안함 : 코드가 실시간으로 바뀌는 게 보이니까 어디가 어떻게 변했다는 것을 바로 확인하는 건 심리적으로 꽤 큰 안정감을 줌 **(근데 이제 claude code desktop 나와서 이것도 해당안됨)** +- 학생 입장에서는 성능보다 **가성비가 중요(*여전히 아쉬운점)** 했음 : claude code의 api 비용이 부담스러울수밖에 없었다. 커서의 정액제는 조금 돌아가더라도 경제적으로 훨씬 이득이었다. + - 그래서 제미나이, 지피티, 커서를 총동원해 5개의 프로젝트와 클라우드 배포까지 달려왔는데 이제는 구현을 넘어 관리와 설계로 갈 때라는 걸 느끼고 있다. 설계한 그림을 정확하게 실행해줄 에이전트의 필요성을 느낀다. + +→ 가시성과 가성비가 주는 효율이 개발의 속도보다 더 크다고 생각해왔다. 하지만 프로젝트가 고도화되고 관리하는 범위가 넓어질수록, ai를 달래고 수동으로 코드롤 옮기는 시간이 누적되면서 ‘아, 내가 아낀 돈보다 내 집중력과 시간이 더 빠르게 소모되고 있구나’ 라는 것을 느꼈다. + +→ cluade 써보고 난 후에 + +- 실시간으로 코드 변경 사항이 안보여서 불편할줄 알았는데, 오히려 결과물에 집중하게 되지는 않았나? +- 돈은 더 나가지만, 작업 시간이 얼마나 줄었는지 + +## 5. 클로드로 갈아타게 된 결정적 이유 + +도전 과제 : 수천 개의 논문을 분석해 영양소 시너지와 부작용 및 효과적인 레시피 산출 AI 엔진 구축 + +프로젝트 구조 : 논문 데이터 → Atomic Facts → Rule Mapping → Reduce 데이터 파이프라인 설계 + +나의 방식 : 읽는 양이 워낙 방대하다 보니까 커서에 소모되는 토큰양이 너무 많았음 월정액이지만 하루에 거의 반을 다 쓸정도로. +GPT/제미나이로 로직 설계 및 보완점 확인 → 커서로 프롬프트 작성 및 코드 작성 + +느낀점 : 1개월만에 mvp를 뽑아냈지만, 가중치를 미세하게 조정하고 레시피 산출 로직을 고도화하려니 커서와의 대화가 너무 파편화되어있었다. 엔진의 전체 설계도를 보고 대화하고 싶은데 커서는 그 많은 부분 중 한 칸을 닦고 있는 느낌 + +커서의 한계 : 전체 데이터가 흐르는 규칙이 깨지지 않는 게 중요한데, 부분적인 코드 수정에 집중하기 때문에 전체 파이프라인 일관성을 놓쳐서 고쳤던 부분을 또 고치고 하는 문제점이 있었음. + +클로드의 강점 : 복잡한 룰과 컨텍스트를 기억하고 가중치 조정이나 파인튜닝 시에 전체 로직의 충돌 여부를 확인하는 데 성능이 높음 + +- **Before (Cursor + Chat):** GPT에게 로직 물어보기 → 답변 복사 → 커서에 붙여넣기 → 에러 나면 다시 GPT에게 질문 → 무한 반복 (컨텍스트 분절) +- **After (Claude Code):** 터미널에서 엔진 코드 전체를 열어두고 "현재 영양소 충돌 로직에서 가중치 부여 방식이 논문 A의 기준과 안 맞아. 전체적으로 다시 매핑해줘."라고 한마디로 해결. + +## 6. Claude Code (CLI)로의 전향: "인턴이 아니라 파트너가 필요하다" + +그래서 저는 Claude Code로 갈아타기로 했습니다. IDE의 안락한 UI를 포기하고 검은 터미널(CLI)로 돌아가는 이유는 명확합니다. + +1. **자율적인 맥락 파악:** 내가 파일을 일일이 열어주지 않아도, 클로드는 스스로 프로젝트 전체를 탐색합니다. +2. **실행 중심의 에이전트:** 코드를 제안만 하는 게 아니라, 직접 실행해보고 에러를 확인하며 '완성'까지 달립니다. +3. **설계에 집중하는 환경:** 멍청한 도구와 씨름하며 오타를 고치는 시간 대신, 더 큰 아키텍처를 고민할 시간을 벌어줍니다. + +## 5. 마치며: 도구에 휘둘리지 않는 똑똑함 + +"더 똑똑한 도구를 쓰면 내 실력이 줄어들지 않을까?"라는 고민도 했습니다. 하지만 결론은 명확합니다. **멍청한 도구를 붙잡고 시간을 쓰는 게 실력이 아닙니다.** 똑똑한 도구를 활용해 더 큰 문제를 해결하고, AI가 짠 코드를 날카롭게 검수하며 시스템을 설계하는 능력이 현대 개발자의 진짜 실력입니다. 이제 저는 Cursor라는 편한 의자를 떠나, Claude Code라는 강력한 엔진에 올라타 보려 합니다. + +--- + +### **비지니스 속도에 개발 속도 맞추기 (뒤꽁무니 따라가기)** + +스타트업의 특성상 코드의 품질보다는 만들어내는 속도가 중요하다. 비지니스의 속도에 따라가야하는데 혼자 모든 걸 하려고 하니 많이 벅찼던 것 같다. 출시 날은 정해져있는데 모든 일이 그렇듯 단축할 수 없는 base time이 존재하고 개발도 그 타임이 긴 편인데 이 base time으로도 촉박했다. 커서한테 기능을 설명하고 코드를 구현하고 디버깅을 하기에 급급해서 유지보수하기 좋은 코드, 깔끔한 코드는 신경 쓸 겨를이 없었고 결과는 이렇게 나왔다. + +### 좋은 팀과 나라는 사람의 괴리 + +솔직히 우리팀은 평균 나이가 적고 다 학부생임에도 불구하고 성과가 좋았다. 여러 선배 기업들로부터 인정도 받고 여러 계약을 따내기도 했고 지원사업도 많이 붙었고 어디에 속하든 항상 막내였다. 이렇게 좋은 팀에 있었지만 나의 기술에 대한 확신, 내 지분에 대한 확신이 없었던 이유는 어차피 커서가 짰는데? 내가 직접 코드를 작성하지 않았는데? 디버깅을 할땐 커서한테 물어보는데? 이런 AI에 의존하는 나의 모습 때문에 확신이 없었던 것 같다. (전문지식이 없는 게 한 몫) + +### 정체성의 혼란과 과도한 책임감 + +더군다나 반년전까지만 해도 그저 춤추기 좋아하는, 이것저것 하고 싶은 게 많은 컴공생이었고 아이들 좋아하는 초등영어강사였는데 갑자기 하루아침에 일과의 대부분이 개발이 되어버리니 그 간극을 못 견뎌서 힘들기도 했고 사람들이 사용하는 서비스이다 보니 에러가 나는 것도 나의 책임, 그걸 빠르게 고쳐야하는 것도 나의 책임이라 생각했다. 학교에서 수업듣고 친구들과 점심 먹다가 에러 발생해서 택시 잡고 회사 가면서 디버깅하기도 했고 출시 n일 전까지는 다 되어야 한다는 생각에 10일 연속으로 대표님과 회사에서 밤을 새기도 했다 (공용사무실에는 리클라이너 의자가 딱 두개 있다) + +### AI만 쓴 건 아니긴 해~ + +하지만 우리 팀의 메인 서비스(안드로이드 키오스크 앱)는 커서를 20% 정도만 썼다. 대부분의 파일들은 코드 한줄한줄 내가 다 넣어가면서 만들었고 설정 파일이나 디버깅을 커서로 돌렸다. (아무래도 메인이다보니 코드가 그래도 중복을 최소화한 유지보수 가능한 것으로 만들고 싶었어서 시간이 없는데도 이렇게 했다, 키오스크 앱이라 가능했다 모바일 앱이었으면 시간내에 못했을 것) + +### 가짜 실력 증후군 + +![{49D513DA-0481-4D0F-9E40-F3B4FD08B73E}.png](attachment:e9250056-bc3b-4d11-93f2-e3abf7f16a25:49D513DA-0481-4D0F-9E40-F3B4FD08B73E.png) + +가짜 실력 증후군이 있었던 것 같다. 주변 친구들은 일을 시작한 나에게 응원을 많이 해줬고 부러움을 표현하는 친구들도 있었지만 난 정말 진심으로 내가 그럴 자격이 되나?를 많이 생각했던 것 같다. 왜냐면 우연히 친한 언니로부터 구인 소식을 들었고 혹시 모르니 넣어보자는 생각에 연락을 했고 운좋게 어쩌다보니 들어가게돼서 AI 돌려가며 일하고 있는건데 내가 어떤 부러움을 살 사람이 되는건가? 싶었다. + +### 운도 실력이다 라는 말에 대한 나의 생각 + +literally 운은 사주상 운세 흐름이 좋아서 나에게 좋은 기운을 주는 거고 실력은 내가 내 의지로 노력해서 쌓아온 전문 지식이나 지혜를 말하는거잖아 둘이 다름. 근데 그 운을 본인의 것으로 가져오는 건 실력이 맞는 것 같다. 아직도 생각한다. 그때 언니가 나한테 그 공고문을 보여줬을때 내 머리를 지배하던 ‘이거 했다가 이 서비스 망하면 어떡해 내가 잘 못하면 어떡해?’라는 생각을 계속 가지고 있었다면 이런 경험들 이런 생각들 다 못했을 것. 운 좋게 공고문을 봤고 일단 내 포트폴리오만 내 보자 판단은 그쪽이 하겠지 라는 용기를 가진 건 내 의도였고 그 포트폴리오를 그동한 준비해왔던 건 내 노력과 실력이었다. + +### 그래서 지금은? + +사실 그때 당시에 앱, 웹, 서버 이렇게 만들어서 테스트베드 출시 했을때는 아 내가 해냈다 라기 보다는 에러 언제 뜰라나 라는 불안감이 먼저였다. 하지만 점점 내가 맡는 태스크가 늘어나고 참석하는 미팅도 다양해지고 다른 회사 사람들도 만나고 의견을 내는 과정이 생기다보니 자연스레 자리를 잡았다고 느껴진것같다. 또한 세상이 빠르게 바뀌면서 ‘코드를 직접 타이핑하는 게 실력’이라고 여기던 개발자 시장에서 ‘AI를 활용해 복잡한 문제를 해결하는 게 실력’이라고 바뀌는 걸 보면서 AI를 사용하면서 서비스를 만든 내가 그렇게 잘못된 것은 아니구나 라고 생각이 들었던 것 같다. + +### 내가 부족한 것 + +난 지금 실전 근육은 있는 상태인데 시험용 잔근육이 부족하다 (제미나이 피셜) 실무에서 겪은 에러 대응을 좀 더 논리적으로 설명할 수 있고 더 좋은 솔루션을 제시할 수 있는 전문지식을 길러야 함을 계속해서 느끼고 있다. + +--- + +코드 실행 전 별개로 나눠져 있는 것들 (커서 작품) + +![{FFCDB22B-0CC0-4A8B-9722-3AD06EF5F23F}.png](attachment:a129e95a-06f0-436a-a4ad-3144491a563b:FFCDB22B-0CC0-4A8B-9722-3AD06EF5F23F.png) + +--- + +**트랙 A: [시리즈] AI 시대, '서비스 제조 기계'에서 '엔지니어'로목적:** 사용자님의 **커리어 브랜딩**과 **정체성 여정**을 다룸. (네이버/링크드인/티스토리 공통) +• **1편: 9개월간 6개의 서비스를 쏟아내며 느낀 '가짜 실력'의 공포** + ◦ 내용: 춤추던 학생에서 하루아침에 1인 개발자로. 택시 안 디버깅 에피소드. AI 의존에 대한 불안함과 '가짜 실력 증후군'. +• **2편: Cursor가 멍청해 보인 순간: 나는 왜 'IDE의 안락함'을 버렸나?** + ◦ 내용: 커서의 뇌절 경험(이미지 활용). 가성비/가시성을 포기하고 클로드 코드를 선택한 경제적·기술적 이유. +• **3편: 운을 실력으로 바꾸는 용기: 텍스코어 합격과 미국행 티켓** + ◦ 내용: 기회를 잡은 용기, 실전 근육과 시험용 잔근육의 차이. "AI를 다루는 설계자"라는 새로운 정체성 정의. + +**트랙 B: [개별 포스팅] AI 에이전트로 해결하는 기술 난제 (Technical Deep-dive)목적:** 사용자님의 **순수 기술 역량** 증명. (티스토리/링크드인 중심) +• **포스팅 1: [Data] 수천 개의 영양학 논문을 씹어먹는 Split-Mapping-Reduce 파이프라인** + ◦ 내용: 보내주신 상세 구조도 활용. 복잡한 룰 엔진 설계와 클로드 코드를 활용한 가중치 파인튜닝 과정. +• **포스팅 2: [App] 음악 편집 UI 리팩토링: 시간축($ms$)과 공간축($px$)의 늪에서 탈출하기** + ◦ 내용: **음악 앱 UI 이야기입니다.** 기존 스파게티 코드를 보여주고, 클로드 에이전트를 통해 어떻게 선언적이고 재사용 가능한 컴포넌트로 리팩토링했는지 상세 기술. + +[Claude 사용기] + +![{CCD3B9C8-1038-4E52-AD78-23FC0A0DBB01}.png](attachment:965e7d3d-b68e-46c4-9f0c-62cba07ff991:CCD3B9C8-1038-4E52-AD78-23FC0A0DBB01.png) + +이게 진짜 협력이지 ㅜㅜㅜ 나와 다르게 생각하는 뇌랑 같이 의견을 주고받는 느낌 +커서는 내가 구조적으로 실수로 놓친 부분을 캐치해주는 거에서 그치는데 클로드는 철학적인 면을 파고들어서 의미상으로 틀린 부분을 잡아줌 (일관성), + +기존 : share가 0.65보다 큰데 dom이 positive가 안되는 경우가 한 가지 더 있음 (3번을 수정했는데도 불구하고 _ dominant_direction에 최소 우세 마진 기준 없는 거). → llm이 dirction은 mixed로 표기한 게 많고 여기에는 positive evidence도 섞인게 많다. dom은 mixed/positive 중에서 큰 걸 선택하는데 mixed가 positive보다 크고 share가 0.83 이어도 미발동 되는거야. 참고 권장이 되어버리는 것. + +수정 : dom = postitive 조건을 제거하거나 dom별 전용 분기를 추가하거나 (후자가 낫겠다) mixed/unclear 인 경우에는 share 값 보고 조정하는 게 낮을듯 (조건부 권장 으로) + +![{4C1CE093-8B76-446A-9EC0-AA66C90B5331}.png](attachment:3f3b208b-b1c6-43a8-b30d-6131a308e7ef:4C1CE093-8B76-446A-9EC0-AA66C90B5331.png) + +하 나의 고민을 이해하는 동료를 만난 기분이다 ㅜㅜ 트레이드오프 사항에 대한 의견을 주고받을 수 있음 철학적인 의미에 관해서 (커서랑 대화할 때는 구조적이고 문법적인 것에 대한 질문과 답변이었음) + +가장 큰 문제 : target_ingredient의 canonical 매핑 문제였음 (트레이드오프임) +염화 마그네슘과 구연산 마그네슘은 마그네슘이라서 피로 회복이 된다는 공통적인 효과는 같지만, safety 측면에서 복용해도 되는 사람이 있고 안되는 사람이 있을 수 있음. 하지만 이런 기본적 영양학 지식은 논문에 있지 않은 이상 safety rule로 처리할 수 없는데 논문이 수백개가 추가될 때마다 새로운 target_ingredient가 들어올때마다 canonical 테이블에 추가를 해야하는건지? + +클로드 답 : + +1. 정규화 기준은 2계층 구조가 답이다. + 1. canoical_ingredient를 부모로 하고 target을 자식으로 하는데, association은 canonical로 하고 dose, safety, interaction은 target을 기반으로 한다. 논문에서 명확히 구분하지 않아도 form-specific 차이가 있으면 target 레벨에서 유지하고 일반 효능은 canonical 레벨로 roll up (정보 손실 없이 fragmentation 가능) +2. 매핑 테이블은 사람이 업데이트해야하나? + 1. llm 추출 시점에 직접 채우게 하자 (제어하는 어휘만 명시해두면 됨) +3. 사전 지식 세팅 : 별도 레이어로 분리하는 게 맞다 + 1. 논문에서 기본 영양학 지식이 나오지 않을 수 있는 건 맞지만 사전 지식을 논문 evidence와 섞어선 안된다. 신뢰도 체계가 무너짐 + 2. config/prior_facts 에 state_tag, direction, source, tier_floor 라고 해서 넣어두기 + 1. 논문 evidence 충분할 때는 prior_facts 무시, + 2. n_evidence가 적거나 state_tag 미커버일 시에 prior_facts를 fallback 으로 + 3. 출처 항상 명시 → 논문 기반이 아닌 공공 지침 기반이다 라는것 + +구글 드라이브 폴더를 통한 코드 매핑은 rule이나 추천 로직에 전혀 영향이 없기 때문에 트래킹용으로는 신규 폴더 자동 코드 생성을 추천, (config에서 관리하되 자동 확장되도록 하는 게 맞다) + +가중치에 대한 컨펌 + +- 논문 종류 위계는 근거 기반 국제 표준이기 때문에 건드리지 x +- 데이터 쌓이면서 조정할 것은 : + - genearte_rules : per_paper_max_contribution (논문 수 늘면 올릴 수있고), support level count 에 대한 strong/moderate/weak(논문 1000건 넘으면 기준 상향 필요), + - atomic_facts : min_dominance_margin : 너무 많이 mixed 나오면 낮추고, weak 하면 올리고 + - 서비스 정책 결정 policy_language : floor weak, very_weak : 최소 근거 기준(얼마나 보수적으로 갈지) + - conflicts : verdict_positive_dominant_hare : 충돌 판정 기준 + +⇒ algorithm_params.yaml 에 모아두기!! + +논문 수가 커지면 SQLite 로 전환하기 (서버 없이 파일 하나로 운영 가능) + +max thinking tokens = 10000 으로 충분한가? no 32000 ㄱㄱ + +![{E19DFE9E-9737-4778-87FF-A028BE58D988}.png](attachment:28c974b6-a8ae-47df-ae64-95e15bbc3847:E19DFE9E-9737-4778-87FF-A028BE58D988.png) + +![{9DAC1BCD-25DD-4BBE-9916-875EC15CC703}.png](attachment:e59f6c11-cf2f-4ef6-9133-69b79d726611:9DAC1BCD-25DD-4BBE-9916-875EC15CC703.png) \ No newline at end of file diff --git a/assignments/latteeea/week3/data/llm_verify1.md b/assignments/latteeea/week3/data/llm_verify1.md new file mode 100644 index 0000000..c9cd91c --- /dev/null +++ b/assignments/latteeea/week3/data/llm_verify1.md @@ -0,0 +1,40 @@ +### Adversarial Injection : 의도적으로 오류 케이스를 넣어서 검증 에이전트가 이를 잡아내는지 확인하는 방법 + +Evidence 추출 오류율 추정 결과 (50편, 166행 표본 결과) : 95% 신뢰구간에서 16.3% 정도 (최대 22.6%) +하지만 validator가 체크하는 영역 (direction, 처방약 잘못 classify, causal attribution)에서는 100% 잡아내는데 validator가 모르는 오류 유형 (미묘한 수치 과장이나 맥락 오독 등)은 측정 불가능함. 이 유형 기준 약 16% (95% CI:11~23%)를 자동으로 플래그하고 해당 오류 유형에 대한 캐치율은 100%(n=15) + +fn_review_sample_v2.csv 수동 검토가 필요하긴 함. 해당 방식은 우리가 이미 아는 오류를 자동 테스트하는 것이기에 우리가 아직 정의하지 못한 패턴이 항상 존재한다는 것이 문제이다. 수동 검토는 그걸 발견하는 유일한 방법임. + +수동 검토 → 미지의 오류 발견 → adversarial 케이스 추가 → 자동화 범위 확대 → 다음 수동 검토로 미지의 오류 발견 의 반복 (수동 검토를 없애는 게 목표가 아니라 수동 검토의 빈도와 범위를 줄이는 게 목표) + +### 수동 검토를 없애는 건 불가능 + Adversarial Injection 자동화로 오류 범위 늘려가기 + +### 레전드레전드!! + +50편 샘플 + 기존 체크 3가지는 16.3% 였는데 새로 추가한 체크 3가지 (outcome_normalized 누락, state_tags 누락, applicability 오분류) 에다가 1175편을 했더니 → 48.9%로 올랐다 + +reject 5%, correct 44% 인데 대부분은 데이터 제외가 아니라 자동 수정 (outcome_normalized 채움, state_tags 추가) 인것임. 실제 오염 제거보다 누락 필드 보완이 훨씬 많았음. + +5%는 실제 근본적으로 정보를 추출한것에 오류가 있었던 것이고 (잘못된 인과 귀속, 방향 반전 등) +44%는 누락 필드를 자동으로 보완한 것들임. → 잘못 나온게아닌것 + +![{6A543A56-CA8D-440F-A102-2FB6D20B182A}.png](attachment:ccf1a1f1-6aa6-4cfe-9493-7ced346bfcf0:6A543A56-CA8D-440F-A102-2FB6D20B182A.png) + +전체 evidence 중에서 rule로 만들어지는 게 34% 정도밖에 없었고, 66%가 안쓰이는 이유는 target_effect_Relation = unrelated_to_target인게 전체의 68%이고 rule 생성에 들어가는건 supports_target인것만 쓰이기 때문. + +direct이면서 unrelated_to_target인 행이 1400개인것은 논리적으로 모순이다라고 하는데, rule 생성되는 범위를 supports_to_taget으로 좁혔던건 interacts까지 포함하니 rule 이 이상하게 나오는것을 발견했기 때문임. 지금 상황에서 범위를 늘리는게 얼마나 유의미할지 확인하기 + +⇒ 핵심 패턴이 뭐였냐면 interacts_partners가 비어있어도 llm이 논문 내 복합 맥락이 있다는 이유로 interacts로 붙이는 경우가 235개였고, 진짜 성분간 상호작용이어서 interacts로 나온게 19개였음. 235개는 처리해야 rule 품질이 올라감. 즉, interaction_partners가 비어있고 interacts-to-target인 경우는 association rule과 동일하다는 것임. → 이거 보정하는 로직 추가 + +### 품질 개선 이유 + +![{7D7F955E-E93D-47E3-9D45-0A6301ED4D98}.png](attachment:4b0d2c3f-cbca-423c-a996-23909aa5858b:7D7F955E-E93D-47E3-9D45-0A6301ED4D98.png) + +수정 전 : 7/36 = 19% 이고 수정 후 : 31/36 = 86% → +67%p (4.4배 향상) + + CKD(6/8)은 처음부터 잘 동작 → 이거 포함하면 전체 수치가 눌리고 CKD 제외하고 “수정 전에 깨져 있던 항목”만 보면 → 1/28=4% ⇒ 25/29=89% 로 22배 개선됨 + +버그 3개가 독립적인 문제였고 이걸 다 고쳐서 개선함 + +- BMI 주입 + 가중치 상향 같이 묶어야 효과가 남 : needs_vector 에 BMI 신호 추가, 그 신호가 guarantee 임계값에 도달해서 가중치 상향했더니 이게 효과가 있었고 +- 오타 수정 (pregnant → pregnancy). 임상 로직은 처음부터 있었는데 임신 태그 자체가 아예 생성됨 \ No newline at end of file diff --git a/assignments/latteeea/week3/data/llm_verify2.md b/assignments/latteeea/week3/data/llm_verify2.md new file mode 100644 index 0000000..8d6210f --- /dev/null +++ b/assignments/latteeea/week3/data/llm_verify2.md @@ -0,0 +1,195 @@ +### llm 추출 프롬프트 수동 검증 테스트 + +1. EVD-PAPER-ZN-080-005이나 EVD-PAPER-FE-008-004 같은 fact sheet들을 커버할 수 있는 study_type 필요 (fact_sheet) + confidence는 high로 고정 +2. EVD-PAPER-PR-043-001 이 논문은 표본 크기가 24이므로 medium으로 낮추기 (지금은 high) -> 표본크기 기준은 30으로 +3. EVD-PAPER-VIT2-054-004 이거는 동물실험(생쥐 대상)이기 때문에 RCT가 아니라 mechanistic으로 분류해야함 -> confidence는 medium으로(상한선 맞추기) +4. EVD-PAPER-BET-005-002는 논문 안에서 Our study integrated functional, metabolic, transcriptomic, and +epigenetic data to elucidate the molecular mechanisms underpinning +C1q-induced innate immune tolerance. + +이렇게 되어있기 때문에 observational 보다는 mechanistic 인것같음. 다변량 보정 모델 있기 때문에 medium으로 유지 + +1. EVD-PAPER-CHR-009-001 같은 경우는 고강도 운동인에게 구리 농도가 떨어진다는것으로 negative로 갔는데 +이 말은 고강도 운동인에게 구리가 안 좋게 작용한다는 말이 아님 +그렇다고 구리가 고강도 운동인에게 좋게 반영된다는 것은(positive) 확신할 수 없으므로 direction은 null이 되어야 하고 고강도 운동이라는 행동을 함으로써 구리라는 성분이 줄어들었다는 것은 행동에 의한 상태 변화이기 때문에 behavior_exposure 이 되어야하고, 성분에 의한 상태변화가 아니라 생리적 매커니즘이기 때문에 evidence_type은 mechanism이 되어야 함 (association rule이 될 수 없음), observational 이고 다변량모델이 없기 때문에 low로 낮추기 +2. EVD-PAPER-COE-002-004 다변량 보정 모델 없으므로 confidence는 low +3. EVD-PAPER-FOL-011-001 는 필드로만 보면 엽산이 과일섭취가 부족한 사람에게 추천될 수 있다 <- 라는 건데 +내용은 "과일이 아닌 채소의 섭취 부족은 영양소 결핍으로 이어진다" +-> evidence_type = mechanism 으로 가야함. 다변량 보정 모델 있지만 상한선 안지켰으므로 medium이 되어야 하고 state_tag는 없는것으로 하는게 맞는것 같다. +4. EVD-PAPER-BCA-004-001 는 빈혈과 직접적 내용이 아니라 간접적 매핑이 된거기 때문에 state_tag 없는걸로 해야한다(BCAA가 조혈 작용에 중요한 역할을 해서 llm이 간접적 연결로 인식하고 태깅한듯함 +실제 논문에서 헤모글로빈이나 줄기세포 기능에 대한 직접적 언급이 없으면 연결하지 않도록 방지 필요), 다변량 보정 모델 없으니까 low로 +5. EVD-PAPER-POT-015-003은 다변량 보정 모델 없으므로 confidence=low +6. EVD-PAPER-CAL-028-001에서 상한선 medium 맞추기 +7. EVD-PAPER-EL-014-004 에서 duration=unknown으로 들어가있는데 이는 kg/day 라는 단위가 없어서 -> 추가 필요 +-> " 67.9 mg/kg 체중/일" 에서 체중/일에 해당되는 단위를 못찾아서 unknown으로 들어감. 그리고 리뷰 논문이므로 narrative 가 되어야 함 (study_type) +8. EVD-PAPER-PB-032-001 이것도 리뷰 논문임. (confidence는 medium으로) +9. EVD-PAPER-FE-004-004, EVD-PAPER-DIE-055-001 두개는 다변량 보정 모델 없으니까 low가 되어야 하고 EVD-PAPER-VIT-054-004 이건 선형 혼합 모델 사용되었으나 주 결과에 보정된 것 아님 -> low +4.2 Unfortunately, the sample size of this pilot study was too small to include CRP and the SOFA score in the mixed model analysis + +[수동 검증 필요(llm추출_QA시나리오) - llm 추출 수동 검증표.csv](attachment:8b33e5b9-0efe-47c0-8dfc-b7d5e630145d:수동_검증_필요(llm추출_QA시나리오)_-_llm_추출_수동_검증표.csv) + +### 프롬프트 수정 리스트업 + +현재 코드 상태와 대조한 결과입니다. + +A. STEP 0 — study_type 판별 체크리스트 신설 [코드에 없음, 완전 신규] + +현재: study_type 허용값 목록만 있고 판별 기준 없음 (line 280-288) + +추가 위치: study_type 허용값 목록 바로 위 + +STEP 0 — study_type 판별 시 반드시 아래 순서를 먼저 확인하십시오: + +① 논문 텍스트에서 제목 바로 앞/위에 "Review", "Review Article", +"Narrative Review", "Mini-Review"가 단독으로 있으면 +→ Abstract 내용에 관계없이 study_type = Narrative Review로 즉시 분류하십시오. +(저자가 자신의 이전 실험 데이터를 인용하더라도, 저널 부여 article type이 우선합니다.) + +② Methods 섹션이 존재하지 않으면 → Narrative Review 강력 의심. +Abstract에 "this work reviews", "we review", "we summarize", "this paper examines" +가 있으면 Narrative Review로 분류하십시오. + +③ 실험 대상이 동물(mice, rat, rodent, in vitro, cell line)이면 +→ "RCT"라는 단어가 있더라도 study_type = Mechanistic + +④ 주목적이 분자 경로 규명이면 → Mechanistic +키워드: transcriptomic, epigenetic, proteomic, signaling pathway, +molecular mechanism, multi-omics, innate immune tolerance + +⑤ ①~④ 해당 없으면 RCT / Observational / 기타로 판단하십시오. + +--- + +B. study_type 허용값에 Fact_Sheet 신설 [코드에 없음] + +현재: 8개 값 (line 281-288), Fact_Sheet 없음 + +추가: + +- Fact_Sheet ← 국가 기관, WHO, 보건부 공식 영양 섭취 기준표·지침서 +→ confidence는 반드시 high로 고정 + +--- + +C. confidence 규칙 강화 [기존 있으나 "권고" → "필수"로 격상] + +현재 line 116: "낮출 수 있음", "권고" 표현 → LLM이 무시함 + +수정 1: line 128 "표본 수 < 30이면 medium 이하 권고" +→ "표본 수 < 30이면 high 불가, 반드시(must) medium 이하" + +수정 2: line 125 "다변량 보정 모델이 명시된 경우에만 medium 허용" +→ "다변량 보정(adjusted model)이 논문에 명시되지 않으면 +Observational은 반드시 low. 혼합 모델(linear mixed model)이 있어도 +주 결과에 실제 적용되지 않았으면 low." + +추가: "evidence_type=mechanism이고 claim_subject=behavior_exposure이면 +confidence는 반드시 low (보충 효과 근거 아님)" + +--- + +D. evidence_type=mechanism 판정 기준 신설 [정의가 현재 전혀 없음] + +현재 line 431-439: 허용값 목록만 있음 + +추가 위치: evidence_type 허용값 위 + +[mechanism 판정 기준] +다음 경우 evidence_type = mechanism을 사용하십시오: + +- 성분이 개입(독립변수)이 아닌 측정 결과(종속변수·바이오마커) 위치에 있을 때 +- 행동·식이 패턴 → 성분 수준 변화를 설명하는 생리 경로일 때 +(성분을 투여한 것이 아니라, 행동 결과로 성분 농도가 변한 것) +- 성분 투여 없이 결핍/유병률만 관찰한 역학 연구 +(이것은 association이 아닙니다) +- 분자 경로 규명이 목적인 연구 (multi-omics, transcriptomic, epigenetic) + +association과의 구분: + +- association: 성분 투여·섭취 → 결과 변화가 중심 +- mechanism: 왜 그런지 경로 설명, 또는 성분이 결과의 측정값 + +--- + +E. behavior_exposure 규칙 강화 [기존 있으나 direction=null 명시 없음] + +현재 line 82-99: claim_subject 정의만 있고 direction=null 강제 없음 + +수정 1: "claim_subject=behavior_exposure 또는 covariate_pattern이면 +direction은 반드시(must) null" +(통계 수치가 있어도 성분 보충 효과가 아니므로 direction 기록 금지) + +수정 2: 정의 확장 — 현재 "운동·훈련·신체 부하"만 예시 +→ "식이 행동(특정 식품군 섭취 부족, 식이 패턴 불균형)도 behavior_exposure" + +수정 3: line 90 "target_effect_relation은 보통(usually) unrelated_to_target" +→ "반드시(must) unrelated_to_target" + +추가: few-shot 예시 2개 + +- "고강도 운동 그룹에서 구리 농도 유의하게 감소" +→ claim_subject=behavior_exposure, direction=null, +evidence_type=mechanism, target_effect_relation=unrelated_to_target, confidence=low +- "채소 섭취 부족 인구에서 엽산 결핍 비율 높음" +→ claim_subject=behavior_exposure, direction=null, evidence_type=mechanism + +--- + +F. state_tag 과태깅 방지 강화 [기존 규칙 보완] + +현재 line 369-413: 간접 매핑 허용 기준이 있으나 2단계 추론 금지 명시 없음 + +추가: [매핑 금지 및 주의 사항] 항목에 추가 + +- 2단계 이상 추론으로 state_tag 부여 금지 +(예: "BCAA → 조혈 기능 → 빈혈" 처럼 논문에 헤모글로빈·빈혈이 +직접 언급되지 않으면 anemia_risk 태깅 금지) +- evidence_type=mechanism이면 state_tag 부여에 매우 신중할 것 +(성분 보충 효과가 아닌 생리 관찰이므로 추천 엔진 연결 부적절) + +--- + +G. dose_unit 허용값 확장 [현재 mg/kg만 있음] + +현재 line 454-458: mg/kg, g/kg, IU/kg만 있고 /day 복합 단위 없음 + +추가: + +- mg/kg/day ← "mg/kg 체중/일", "mg/kg BW/d", "mg/kg body weight/day" 모두 이걸로 파싱 +- g/kg/day +- mcg/kg/day + +그리고 _normalize_analysis_dict 함수의 allowed_dose_units set (line 1063-1077)에도 동일하게 추가 필요. + +### 가치 평가 + +포트폴리오 가치 평가 + +결론부터: 헬스테크/AI 프로덕트 포지션에서는 상위 10% 수준의 포트폴리오 소재입니다. 단, 어떻게 포장하느냐에 따라 편차가 큽니다. + +강점 — 진짜 차별점 + +1. End-to-End LLM 파이프라인 구축 경험 +논문 PDF → LLM 추출 → evidence DB → atomic facts → 추천 규칙까지 직접 설계하고 구현한 사람은 드뭅니다. "LLM 써봤다" 수준이 아니라 LLM 출력의 신뢰도를 구조적으로 제어하는 수준. +2. 체계적 품질 관리 (QA 프레임워크) +- 2-pass 임상 검증 레이어 (approve/correct/reject) +- 팀원 수동 검증 → 13개 케이스 → 7개 카테고리 오류 분류 +- confidence 기준, study_type 체계, state_tag 정밀도를 직접 설계 + +이게 포트폴리오에서 가장 강한 부분입니다. "LLM이 틀리는 걸 어떻게 잡아내고 개선했는가"를 데이터로 보여줄 수 있음. + +1. 도메인 × 기술 동시 보유 +영양학적 판단(mechanism vs association, behavior_exposure 구분)을 직접 해서 프롬프트 설계에 반영했다는 점. 대부분의 ML 엔지니어는 이 판단을 도메인 전문가에게 위임합니다. +2. Medallion 아키텍처 +Bronze → Silver → Gold 레이어 설계는 데이터 엔지니어링 관점에서 명확한 설계 능력을 보여줌. + +--- + +약점 — 솔직하게 + +1. 임팩트 수치가 없음 +논문 몇 편 처리했는지, 성분 몇 개 커버하는지, 실제 사용자가 몇 명인지 없으면 채용 담당자가 규모를 가늠하기 어렵습니다. +2. 아직 제품 수준이 아님 +QA 시나리오 116개, 검증 13개 케이스 — 이건 "개발 중인 시스템"의 규모입니다. 실제 배포·운영 경험이 없으면 ML 엔지니어링 포지션에서 가산점이 크지 않음. +3. 기술 스택 폭이 좁음 +인프라(MLOps, 배포), 모니터링, A/B 테스트 경험이 드러나지 않습니다. \ No newline at end of file diff --git a/assignments/latteeea/week3/data/llm_verify3.md b/assignments/latteeea/week3/data/llm_verify3.md new file mode 100644 index 0000000..d938373 --- /dev/null +++ b/assignments/latteeea/week3/data/llm_verify3.md @@ -0,0 +1,327 @@ +### 1차 오류율 측정 및 개선 결과 +- 1차 수동 검증 후 수정 필요 사항들 + 1. EVD-PAPER-ZN-080-005이나 EVD-PAPER-FE-008-004 같은 fact sheet들을 커버할 수 있는 study_type 필요 (fact_sheet) + confidence는 high로 고정 + 2. EVD-PAPER-PR-043-001 이 논문은 표본 크기가 24이므로 medium으로 낮추기 (지금은 high) -> 표본크기 기준은 30으로 + 3. EVD-PAPER-VIT2-054-004 이거는 동물실험(생쥐 대상)이기 때문에 RCT가 아니라 mechanistic으로 분류해야함 -> confidence는 medium으로(상한선 맞추기) + 4. EVD-PAPER-BET-005-002는 논문 안에서 Our study integrated functional, metabolic, transcriptomic, and + epigenetic data to elucidate the molecular mechanisms underpinning + C1q-induced innate immune tolerance. + + 이렇게 되어있기 때문에 observational 보다는 mechanistic 인것같음. 다변량 보정 모델 있기 때문에 medium으로 유지 + + 5. EVD-PAPER-CHR-009-001 같은 경우는 고강도 운동인에게 구리 농도가 떨어진다는것으로 negative로 갔는데 + 이 말은 고강도 운동인에게 구리가 안 좋게 작용한다는 말이 아님 + 그렇다고 구리가 고강도 운동인에게 좋게 반영된다는 것은(positive) 확신할 수 없으므로 direction은 null이 되어야 하고 고강도 운동이라는 행동을 함으로써 구리라는 성분이 줄어들었다는 것은 행동에 의한 상태 변화이기 때문에 behavior_exposure 이 되어야하고, 성분에 의한 상태변화가 아니라 생리적 매커니즘이기 때문에 evidence_type은 mechanism이 되어야 함 (association rule이 될 수 없음), observational 이고 다변량모델이 없기 때문에 low로 낮추기 + 6. EVD-PAPER-COE-002-004 다변량 보정 모델 없으므로 confidence는 low + 7. EVD-PAPER-FOL-011-001 는 필드로만 보면 엽산이 과일섭취가 부족한 사람에게 추천될 수 있다 <- 라는 건데 + 내용은 "과일이 아닌 채소의 섭취 부족은 영양소 결핍으로 이어진다" + -> evidence_type = mechanism 으로 가야함. 다변량 보정 모델 있지만 상한선 안지켰으므로 medium이 되어야 하고 state_tag는 없는것으로 하는게 맞는것 같다. + 8. EVD-PAPER-BCA-004-001 는 빈혈과 직접적 내용이 아니라 간접적 매핑이 된거기 때문에 state_tag 없는걸로 해야한다(BCAA가 조혈 작용에 중요한 역할을 해서 llm이 간접적 연결로 인식하고 태깅한듯함 + 실제 논문에서 헤모글로빈이나 줄기세포 기능에 대한 직접적 언급이 없으면 연결하지 않도록 방지 필요), 다변량 보정 모델 없으니까 low로 + 9. EVD-PAPER-POT-015-003은 다변량 보정 모델 없으므로 confidence=low + 10. EVD-PAPER-CAL-028-001에서 상한선 medium 맞추기 + 11. EVD-PAPER-EL-014-004 에서 duration=unknown으로 들어가있는데 이는 kg/day 라는 단위가 없어서 -> 추가 필요 + -> " 67.9 mg/kg 체중/일" 에서 체중/일에 해당되는 단위를 못찾아서 unknown으로 들어감. 그리고 리뷰 논문이므로 narrative 가 되어야 함 (study_type) + 12. EVD-PAPER-PB-032-001 이것도 리뷰 논문임. (confidence는 medium으로) + 13. EVD-PAPER-FE-004-004, EVD-PAPER-DIE-055-001 두개는 다변량 보정 모델 없으니까 low가 되어야 하고 EVD-PAPER-VIT-054-004 이건 선형 혼합 모델 사용되었으나 주 결과에 보정된 것 아님 -> low + 4.2 Unfortunately, the sample size of this pilot study was too small to include CRP and the SOFA score in the mixed model analysis +- 프롬프트 수정 전 + + Evidence 추출 오류율 측정 결과 + + 샘플: 50편 논문, 166개 evidence rows (무작위 추출, seed=42) + + 전체 오류율 + + 전체 플래그율 (reject + correct): 17.5% [95% CI: 12.4% ~ 24.0%] + ├── reject (데이터 제외): 9.0% [95% CI: 5.6% ~ 14.4%] + └── correct (자동 수정): 8.4% [95% CI: 5.1% ~ 13.7%] + + → 현재 추출된 evidence 약 6개 중 1개에 오류 + + 에러 유형별 (현행 validator 6-check 기준) + + causal_attribution_error 13.9% (n=23) ← 압도적 1위 + direction_inversion 3.0% (n=5) + outcome_direction_mismatch 0.6% (n=1) + + causal_attribution_error가 전체 오류의 79%를 차지합니다. behavior_exposure(고강도 운동, 식이 패턴)를 substance_intervention으로 혼동하는 문제 — 이번 수정에서 직접 타깃. + + +> 50편 무작위 표본 기준 evidence 추출 오류율 17.5% (95% CI: 12.4%–24.0%) 측정. 가장 큰 원인은 causal attribution 오분류(13.9%)로, 팀원 수동 검증 13케이스를 통해 validator가 잡지 못하는 4개 추가 오류 카테고리를 체계적으로 발굴 → 7개 카테고리 프롬프트 수정안 도출. +> +- 프롬프트 1차 수정 후 + + applicability_misclassified가 갑자기 40.1%로 튀었음 - 프롬프트가 아닌 비교 구조 문제였음 + 근데 프롬프트 문제도 하나 발견 + + - BEFORE 검증기는 3check, AFTER 검증기는 6CHECK + - 기존에 존재했는데 원래 몰랐다가 (구 validator가 못 잡다가) 새 프롬프트를 적용하고 나니 is_strict_direct_match 버그 발견함 + - BEFORE: target="ascorbic_acid" → "ascorbic acid" ≠ "vitamin c supplement" → unrelated (원래도 틀림, 구 validator가 못 잡은 것) + - AFTER: target="vitamin_c" → underscore 때문에 "vitamin_c" ≠ "vitamin c supplementation" → unrelated (틀린 매칭) + - 새 프롬프트가 더 표준적인 target_ingredient("vitamin_c")를 쓰면서 버그가 수면 위로 올라옴 + + | | BEFORE | AFTER(1차 프롬프트) | DELTA | + | --- | --- | --- | --- | + | 전체 플래그율 | 48.2% | 52.0% | +3.8pp (CI 겹침 → 통계적 동등) | + | causal_attribution_error | 12.0% | 8.6% | -3.4pp 개선 | + | direction_inversion | 2.4% | 0.7% | -1.7pp 개선 | + | appicability_mismatch | 9.6% | 40.1% | +30.5pp ← 버그 노출 !! | + - causal_attribution_error 감소, direction_inversion 감소는 프롬프트 수정 효과인데 + - applicability_misclassified 급증 → 새 프롬프트로 못 보던 버그가 수면 위로 올라온 것 +- 프롬프트 2차 수정 후 (applicability_misclassified 얼마나 줄었는지) + + 모두 같은 50편으로 전체 측정 결과 요약 + + | | BEFORE | AFTER(2차 프롬프트) | DELTA | + | --- | --- | --- | --- | + | 전체 플래그율 | 48.2% | 25.0% | -23.2 개선 | + | causal_attribution_error | 12.0% | 6.6% | -5.4pp 개선 | + | direction_inversion | 2.4% | 0.7% | -1.7pp 개선 | + | appicability_mismatch | 40.1% (1차 프롬프트) | 13.8% | -26.3pp 개선 | + +- 프롬프트 2차 수정까지 한 후 정리 + + 측정 불가 항목 (validator가 현재 체크하지 않음) + + ┌─────────────────────────────────────────────────────┬──────────────────────────────────────────────┬──────────────────────────────────────────┐ + │ 수정 항목 │ 기대 효과 │ 측정 방법 │ + ├─────────────────────────────────────────────────────┼──────────────────────────────────────────────┼──────────────────────────────────────────┤ + │ CAT-A STEP 0 study_type 체크리스트 (리뷰 논문 감지) │ 리뷰 논문 Narrative 오분류 감소 │ 수동 검증 또는 validator Check 추가 필요 │ + ├─────────────────────────────────────────────────────┼──────────────────────────────────────────────┼──────────────────────────────────────────┤ + │ CAT-C confidence 권고 → 필수(must) │ 표본수<30·다변량보정 없는 케이스 과산정 감소 │ 동상 │ + ├─────────────────────────────────────────────────────┼──────────────────────────────────────────────┼──────────────────────────────────────────┤ + │ CAT-F state_tag 2단계 추론 금지 │ 간접 연결 과태깅 감소 │ 동상 │ + ├─────────────────────────────────────────────────────┼──────────────────────────────────────────────┼──────────────────────────────────────────┤ + │ CAT-G dose_unit mg/kg/day 파싱 추가 │ unknown 오기입 감소 │ 동상 │ + ├─────────────────────────────────────────────────────┼──────────────────────────────────────────────┼──────────────────────────────────────────┤ + │ CAT-B Fact_Sheet 신설 │ 공식 지침서 분류 가능 │ 동상 │ + └─────────────────────────────────────────────────────┴──────────────────────────────────────────────┴──────────────────────────────────────────┘ + + --- + + 부작용으로 새로 나타난 오류 + + ┌──────────────────────────────────┬──────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ 오류 유형 │ 발생률 │ 원인 │ + ├──────────────────────────────────┼──────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ missing_state_tags │ 2.0% (n=3) │ CAT-F state_tag 기준 강화로 필요한 태그도 보수적으로 적용하는 케이스 발생 │ + ├──────────────────────────────────┼──────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ missing_outcome_normalized │ 2.0% (n=3) │ 새 추출 데이터 특성 (BEFORE에도 존재했을 가능성 있음) │ + ├──────────────────────────────────┼──────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ prescription_drug_misclassified │ 2.0% (n=3) │ 동상 │ + ├──────────────────────────────────┼──────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ applicability_misclassified 잔존 │ 13.8% (n=21) │ 버그 수정 후에도 남은 건 새 프롬프트가 behavior_exposure를 더 적극 분류하면서 생긴 진짜 차이 (BEFORE 대비 +4.2pp) │ + └──────────────────────────────────┴──────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + + --- + + 한 줄 요약 + + ▎ CAT-E + CAT-D(behavior_exposure 규칙 강화)가 causal_attribution_error 12.0%→6.6%, direction_inversion 2.4%→0.7%를 만들었고, _is_strict_direct_match 버그 수정이 applicability_misclassified 40.1%→13.8%를 + ▎ 만들었습니다. 나머지 5개 카테고리(A·B·C·F·G)의 효과는 현재 validator로는 측정 불가 — 수동 검증 또는 validator Check 확장 필요합니다. + +- 그 다음으로 validator 에 추가해야하는 check 항목 + 1. Check 7 — behavior_exposure + direction≠null [최상] — 구현 가장 쉬움 (CHR-009 내용) + + 왜 최상: Check 3이 substance_intervention → behavior_exposure 방향만 잡고, 반대 (behavior_exposure인데 direction=positive/negative는 통과)는 못 잡음. 이번 측정에서 causal_attribution_error가 줄었지만 완전히 + 없어지지 않은 원인 중 하나. + + claim_subject = "behavior_exposure" 또는 "covariate_pattern"인데 + direction = "positive" 또는 "negative"인 row: + → correct, error_type: behavior_exposure_direction_error + → correction_direction: "null" + + 구현 난이도: 매우 쉬움 — 필드값만 비교 + + + --- + + 1. Check 8 — confidence 과산정 [최상] — 임팩트 가장 큼 (7/13) + + 왜 최상: 수동 검증 7/13 케이스에서 발견, 현재 전혀 측정 안 됨. 이게 없으면 confidence 규칙을 프롬프트에서 아무리 강화해도 효과를 측정할 수 없음. + + [Rule 1] study_type = Mechanistic 또는 Narrative Review인데 confidence = "high" + → correct, error_type: confidence_overcalibrated + → correction_confidence: "medium" + + [Rule 2] study_type = Observational인데 + source_snippet에 "adjusted", "multivariate", "controlled for", + "regression model", "cox model" 키워드 없고 confidence = "medium" 또는 "high" + → correct, error_type: confidence_overcalibrated + → correction_confidence: "low" + + [Rule 3] evidence_type = "mechanism" + claim_subject = "behavior_exposure"인데 + confidence ≠ "low" + → correct, error_type: confidence_overcalibrated + → correction_confidence: "low" + + 구현 난이도: 보통 — Rule 1,3은 필드값만 비교, Rule 2는 source_snippet 키워드 검색 필요 + + 2. Check 9 — study_type 오분류 [상] (3/13) + + 왜 상: confidence 과산정의 upstream 원인. study_type이 틀리면 Check 8 Rule 1도 작동 안 함. + + [Rule 1] condition_population 또는 source_snippet에 + "mice", "rat", "mouse", "in vitro", "cell line", "murine", "rodent"가 있는데 + study_type = "RCT" 또는 "Observational" + → correct, error_type: study_type_misclassified + → correction_study_type: "Mechanistic" + + [Rule 2] source_snippet 또는 evidence_statement에 + "this review", "we review", "narrative review", "literature review", + "systematic search", "PRISMA"가 있는데 + study_type ≠ "Narrative Review" 또는 "Systematic Review" + → correct, error_type: study_type_misclassified + → correction_study_type: "Narrative Review" (또는 "Systematic Review") + + 구현 난이도: 쉬움 — keyword matching + +- 새 프롬프트(추출 프롬프트)로 v5 sample 돌린 결과 + + 수동 검증에서 수정할사항으로 나왔던 16가지 논문 중 5가지만 pass (31%) + + FAIL (11개) — 패턴별 분류 + + 패턴 1: confidence 과산정 (7개) — 가장 많음 + COE-002, POT-015, FE-004, DIE-055, VIT-054 → medium으로 나옴 (low여야) + BCA-004 → high/medium 혼재 (low여야) + PR-043 → high 그대로 (medium이어야, n=24) + CHR-009 → medium 그대로 (low여야) + 프롬프트에 "반드시(must) low"를 넣었지만 LLM이 여전히 무시. + + 패턴 2: study_type 오분류 (2개) + VIT2-054: 동물실험인데 RCT 그대로 + BET-005: multi-omics인데 RCT 그대로 + STEP 0 체크리스트 추가했지만 이 두 논문은 통과 못 함. + + 패턴 3: behavior_exposure direction (1개) + CHR-009: 4개 row 중 2개는 direction=null 됐지만 2개는 여전히 negative + + 패턴 4: mechanism 미분류 (1개) + FOL-011: 식이패턴→결핍 경로인데 여전히 association 4개 + + - 31%는 낮아보이지만 11개 FAIL의 대부분은 validator가 자동 수정 가능함 + - confidence 과산정 7개 → check 8로 수정 가능 + - study_type 오분류 2개 → check 9로 수정 가능 + - direction=negative → check7로 수정 가능 + - FOL-011 mechanism 미분류 → 수정가능한 check 없음 (수동 수정 필요) + - 2-pass 기준(검증 프롬프트 추가된) 예상 → 14~15/16 → 실제 측정 결과 : 5/16 (그대로다 문제임… 밑에서) + - 리뷰 논문/fact sheet 판단 여부는 프롬프트로 해결되지만, confidence 과산정은 추출 프롬프트로 한계가 있어, validator에 의존하는 게 현실적이다. + - 일단 mechanism 미분류한거 few_shot 예시를 좀 더 구체적으로 조정해보고 + - confidence 과산정 경우는, 프롬프트 must 강화로도 안되는 상황이고. validator가 사후에 잡아주는것으로 집중 필요 +- check-9(검증 프롬프트) 추가했는데도 5/16(31%)이 나온 이유 + + validator decision 분석 결과 → 모든 케이스에서 corr_conf = None 이 나왔는데 3가지 구조적 문제가 있음 + + 1. 1행 1결정 제약 - validator는 행당 하나의 error_type만 반환해서 check 8보다 check3 이 먼저 걸리면 confidence(check8) 는 수정이 안됨 + 1. COE-002 row0: behavior_exposure_direction_error 잡힘 → confidence는 못 고침 + 2. FE-004 row0: causal_attribution_error 잡힘 → confidence는 못 고침 + 2. check8 자체가 미발동됨 - POT-015, PR-043은 다른 오류도 없는데 confidence가 그냥 approve됨. LLM이 "multivariate 키워드 없으면 low"를 Observational에서 일관되게 적용 못 함 + 3. Check 8에 RCT n<30 규칙이 없음 — PR-043 (n=24인데 high) 케이스는 Check 8에 아예 없는 규칙 + + 어떻게 고쳐야 하냐면 : confidence 과산정 → LLM이 아닌 코드 레벨로 이동하도록 + → LLM 기반 validator로는 결정론적 규칙(다변량 여부, 표본 크기)를 일관되게 적용하기 어렵구나… 코드로 박는 게 어느정도 필요함 + + - fact sheet는 confidence high로 고정하는 거 필요하고 (postpreocessor로) + - study_type에 따른 상한선 강제하는것도 필요하고 (Observational인데 계속 high로 나옴 ㅜㅜ 상한선 medium이라 해놨는데) + - (이건 반영안함) 클로드가 source_snippet (논문 문장 그대로 가져온 필드)에 다변량 관련된 키워드가 없으면 low로 (observational + 다변량 모델 x → low 라는 우리만의 규칙) 하자고 하는데, 우리가 수동 검증할때는 source_snippet에서 다변량에 관련된 말이 있었던게 아니라 논문 전체를 읽고 다변량 모델이 아니라고 판단해서 해당 근거 사례의 confidence를 low로 해야한다고 결정한거기 때문에 검증 프롬프트가 아닌 추출 프롬프트에서 해결이 되어야 하는것임 → 클로드 말 반영 안함 + - evidence의 필드에 다변량 여부를 boolean으로 명시적으로 기록하도록 추가함. 여기서 postprocessor가 이 필드를 보고 결정론적으로 적용하도록 하기. (표본 크기 필드도 추가함) + - RCT + 표본 n<30 → medium 상한으로 하는것도 필요 (n=24인데 high로 하는건 표본 크기가 너무 작아서 신뢰도 높지 않은 것임) +- 검증 프롬프트 및 추출 프롬프트 약간 수정(다변량 필드 추가 후)한 이후 결과 (9/16 → 56%) + + 5/16(31%) → 9/16(56%), 4 pass 개선됨 + + 결과 분석 + + 새로 PASS된 6개 (confidence 보정 작동) + + ✓ PR-043 n=24 → Rule D (RCT n<30 → medium) 작동 + ✓ COE-002 Observational + 다변량 없음 → Rule B (→ low) 작동 + ✓ POT-015 동상 + ✓ FE-004 동상 + ✓ DIE-055 동상 + ✓ VIT-054 혼합모델 미적용 → has_multivariate_adjustment=False → low + + 기존 PASS인데 이번에 FAIL된 2개 (regression) + + ✗ EL-014 study_type=Narrative Review → Unknown으로 퇴행 + (이번 재추출에서 STEP 0이 안 먹힘) + ✗ PB-032 한 row만 confidence=high 남음 + (나머지 3개는 medium인데 1개 row study_type이 달랐을 가능성) + + 여전히 FAIL (7개) + + ┌──────────┬───────────────────────────────────────────────────────────────────────┐ + │ 케이스 │ 남은 문제 │ + ├──────────┼───────────────────────────────────────────────────────────────────────┤ + │ CHR-009 │ direction=null 2/3 미적용, evidence_type=association (mechanism 아님) │ + ├──────────┼───────────────────────────────────────────────────────────────────────┤ + │ FOL-011 │ evidence_type=population_note (mechanism 아님) │ + ├──────────┼───────────────────────────────────────────────────────────────────────┤ + │ VIT2-054 │ study_type=RCT 그대로 (동물 실험 미인식) │ + ├──────────┼───────────────────────────────────────────────────────────────────────┤ + │ BET-005 │ study_type=RCT/Observational 혼재 (Mechanistic 미인식) │ + ├──────────┼───────────────────────────────────────────────────────────────────────┤ + │ BCA-004 │ 1개 row만 medium 잔존 │ + ├──────────┼───────────────────────────────────────────────────────────────────────┤ + │ EL-014 │ 이번 추출에서 Narrative Review → Unknown 퇴행 │ + ├──────────┼───────────────────────────────────────────────────────────────────────┤ + │ PB-032 │ 1개 row confidence=high │ + └──────────┴───────────────────────────────────────────────────────────────────────┘ + + 패턴 정리 + + 확실히 해결된 것: confidence 과산정 (다변량 없음, n<30) — Rule B·D가 결정론적으로 잡아줌 + + 아직 LLM이 일관성 없는 것 2가지: + + 1. study_type 오분류 (동물실험, multi-omics → RCT/Observational): STEP 0 추가했지만 논문 내용에 따라 여전히 틀림. LLM이 "mice", "rodent" 키워드를 보고도 RCT로 분류하는 것 → 이건 Check 9 (validator 자동 수정) + 에 의존하거나, 추출 후 코드 레벨 키워드 체크 추가 고려 + 2. mechanism 미분류 (CHR-009, FOL-011): 행동→성분 경로를 여전히 association/population_note로 분류. 예시를 추가했지만 이 두 논문에서 안 통함 → 예시를 더 강화하거나 논문 내용 자체가 모호할 수 있음 + + EL-014·PB-032 퇴행: LLM 비결정성으로 이번 추출에서 다르게 나온 것. 여러 번 돌리면 결과가 달라질 수 있음. + + - 다음으로는 study_type오분류(VIT2-054, BET-005)는 postprocess 부분에 condition_population 키워드 체크를 코드 레벨로 추가해서 비결정성 없이 llm이 잡도록 하고 + - mechanism 미분류는 llm 의존이라 더 많은 예시가 필요할듯 → 다음 수동 검증으로 넘기자 +- study_type 키워드 보정 후 결과 (14/16 pass → 88%) + + 14/16 PASS (88%) 입니다. + + --- + + 전체 진행 경과 + + 최초 (프롬프트 수정 전): 5/16 (31%) + + - 프롬프트 7개 카테고리: 5/16 (31%) ← confidence 미해결 + - n_sample/has_adj 필드: 9/16 (56%) ← confidence 해결 + - study_type 키워드 보정: 14/16 (88%) ← study_type 해결 + + 새로 PASS된 것 (이번 최종 run) + + ┌────────────┬───────────────────────────────────────────────────────────────────────────┐ + │ 케이스 │ 해결 원인 │ + ├────────────┼───────────────────────────────────────────────────────────────────────────┤ + │ CHR-009 ✓ │ behavior_exposure + direction=null + mechanism + confidence=low 모두 통과 │ + ├────────────┼───────────────────────────────────────────────────────────────────────────┤ + │ FOL-011 ✓ │ evidence_type=mechanism 분류 성공 (예시 추가 효과) │ + ├────────────┼───────────────────────────────────────────────────────────────────────────┤ + │ VIT2-054 ✓ │ condition_population 키워드(C57BL, mice 등) → Mechanistic 자동 보정 │ + ├────────────┼───────────────────────────────────────────────────────────────────────────┤ + │ BET-005 ✓ │ source_snippet 키워드(transcriptomic, epigenetic) → Mechanistic 자동 보정 │ + ├────────────┼───────────────────────────────────────────────────────────────────────────┤ + │ PB-032 ✓ │ Narrative Review 인식 + confidence 상한선 적용 │ + └────────────┴───────────────────────────────────────────────────────────────────────────┘ + + 여전히 FAIL (2개) + + ┌─────────┬───────────────────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ 케이스 │ 문제 │ 원인 │ + ├─────────┼───────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ BCA-004 │ 1개 row만 medium 잔존 ['medium', 'low', 'low'] │ 해당 row의 has_multivariate_adjustment가 null로 나와서 Rule B 미발동, Rule C만 적용(→ medium 상한) │ + ├─────────┼───────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ EL-014 │ study_type=Observational/Mechanistic ['Observational', 'Mechanistic'] │ 이번엔 Mechanistic 키워드 체크가 과도하게 걸려서 Narrative Review를 못 잡음 │ + └─────────┴───────────────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────┘ + + BCA-004는 LLM이 has_multivariate_adjustment=null로 반환한 row 1개 문제, EL-014는 Narrative Review 감지 불안정(LLM 비결정성)입니다. 둘 다 다음 수동 검증 회차로 넘겨도 무방한 수준입니다. 88%면 충분히 진행할 수 + 있을 것 같습니다. \ No newline at end of file diff --git a/assignments/latteeea/week3/data/long_polling.md b/assignments/latteeea/week3/data/long_polling.md new file mode 100644 index 0000000..18c2d6d --- /dev/null +++ b/assignments/latteeea/week3/data/long_polling.md @@ -0,0 +1,241 @@ +### 요구사항 및 구현 + +요구사항 : 기기/서버 점검 중이라는 창을 키오스크에 띄워야 하고 이는 관리자웹에서 제어 가능해야함 + +기술적 고민 : polling vs long polling vs websocket + +- 프론트와 백 서버가 주기적으로 통신할 수 있는 것은 크게 세 가지가 있고 각각의 장단점을 보면 + - polling : N초마다 요청 ↔ 요청 받는 즉시 응답 / N초만큼 지연되고 트래픽과 쿼리가 반복됨 + - long polling : 클라이언트가 요청 → 서버가 ‘변화가 생길 때까지’ 응답을 안 하고 대기, 변화가 생기면 즉시 응답 → 클라이언트는 응답 받자마자 바로 다음 long - polling 요청을 다시 걸기 / **동시 접속자가 많으면 ‘열려 있는 요청’이 계속 쌓임 (그때는 이 단점을 미처 파악하지 못했고 이 문제로 인해 서버가 종료되는 사건 발생)** + - websocket : 연결 1개를 계속 유지하면서 push → 찐 실시간 / 인프라 쪽에서 신경쓸 게 좀 있음 무거우니까 +- 채팅 같은 곳에서나 쓰이는 게 websocket인데 기기 점검 여부에 대해 표시하기 위해 연결을 하나 아예 열어두는 건 너무 투머치이다. 왜냐면 거의 쓰지 않는 기능이기 때문에. 근데 일단 그 여부가 수정되면 최대한 빠르게 바뀌는 게 좋다. 간격이 1분을 넘어가진 않았으면 좋겠다 → Long Polling 결정 + +### 문제 상황 + +```c +sqlalchemy.exc.TimeoutError: QueuePool limit of size 5 overflow 10 reached, connection timed out, timeout 30.00 (Background on this error at: https://sqlalche.me/e/20/3o7r) +ERROR: Exception in ASGI application +Traceback (most recent call last): + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py", line 416, in run_asgi + result = await app( # type: ignore[func-returns-value] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + self.scope, self.receive, self.send + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__ + return await self.app(scope, receive, send) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/fastapi/applications.py", line 1135, in __call__ + await super().__call__(scope, receive, send) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/starlette/applications.py", line 107, in __call__ + await self.middleware_stack(scope, receive, send) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/starlette/middleware/errors.py", line 186, in __call__ + raise exc + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/starlette/middleware/errors.py", line 164, in __call__ + await self.app(scope, receive, _send) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py", line 63, in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app + raise exc + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app + await app(scope, receive, sender) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__ + await self.app(scope, receive, send) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/starlette/routing.py", line 716, in __call__ + await self.middleware_stack(scope, receive, send) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/starlette/routing.py", line 736, in app + await route.handle(scope, receive, send) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/starlette/routing.py", line 290, in handle + await self.app(scope, receive, send) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/fastapi/routing.py", line 115, in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app + raise exc + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app + await app(scope, receive, sender) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/fastapi/routing.py", line 101, in app + response = await f(request) + ^^^^^^^^^^^^^^^^ + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/fastapi/routing.py", line 355, in app + raw_response = await run_endpoint_function( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ...<3 lines>... + ) + ^ + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/fastapi/routing.py", line 243, in run_endpoint_function + return await dependant.call(**values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/opt/render/project/src/app/apis/endpoints/system_status.py", line 183, in poll_system_status + new_statuses = query.order_by(models.SystemStatus.created_at.desc()).all() + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/orm/query.py", line 2704, in all + return self._iter().all() # type: ignore + ~~~~~~~~~~^^ + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/orm/query.py", line 2857, in _iter + result: Union[ScalarResult[_T], Result[_T]] = self.session.execute( + ~~~~~~~~~~~~~~~~~~~~^ + statement, + ^^^^^^^^^^ + params, + ^^^^^^^ + execution_options={"_sa_orm_load_options": self.load_options}, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/orm/session.py", line 2351, in execute + return self._execute_internal( + ~~~~~~~~~~~~~~~~~~~~~~^ + statement, + ^^^^^^^^^^ + ...<4 lines>... + _add_event=_add_event, + ^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/orm/session.py", line 2239, in _execute_internal + conn = self._connection_for_bind(bind) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/orm/session.py", line 2108, in _connection_for_bind + return trans._connection_for_bind(engine, execution_options) + ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "", line 2, in _connection_for_bind + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/orm/state_changes.py", line 137, in _go + ret_value = fn(self, *arg, **kw) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/orm/session.py", line 1187, in _connection_for_bind + conn = bind.connect() + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 3285, in connect + return self._connection_cls(self) + ~~~~~~~~~~~~~~~~~~~~^^^^^^ + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 143, in __init__ + self._dbapi_connection = engine.raw_connection() + ~~~~~~~~~~~~~~~~~~~~~^^ + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 3309, in raw_connection + return self.pool.connect() + ~~~~~~~~~~~~~~~~~^^ + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/pool/base.py", line 447, in connect + return _ConnectionFairy._checkout(self) + ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^ + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/pool/base.py", line 1264, in _checkout + fairy = _ConnectionRecord.checkout(pool) + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/pool/base.py", line 711, in checkout + rec = pool._do_get() + File "/opt/render/project/src/.venv/lib/python3.13/site-packages/sqlalchemy/pool/impl.py", line 166, in _do_get + raise exc.TimeoutError( + ...<4 lines>... + ) +sqlalchemy.exc.TimeoutError: QueuePool limit of size 5 overflow 10 reached, connection timed out, timeout 30.00 (Background on this error at: https://sqlalche.me/e/20/3o7r) +2026-01-30 12:30:29,926 +``` + +## 제미나이 정리본 (참고) + +### 1. 도입: 왜 Long Polling을 선택했는가? + +- 실시간성 데이터 업데이트가 필요했고, WebSocket 도입 비용 대비 효율적인 Long Polling을 선택했다. + +### 2. 위기: 트래픽 증가와 서버 다운 + +- 클라이언트 수가 늘어나자 서버 리소스가 급증하며 프로세스가 Kill 되는 현상 발생. + +### 3. 원인 파악 (이 부분이 핵심!) + +- AI(커서)가 생성해 준 코드는 **'기능적 구현'**에만 충실했을 뿐, **'동시 접속자 수에 따른 커넥션 유지 비용'**이나 **'타임아웃 처리'**가 미흡했음을 발견. +- 구체적으로 어떤 코드 라인이 무한 대기를 유발했거나 메모리 누수를 일으켰는지 분석. + +### 4. 해결 및 개선 + +- 서버가 요청을 무한정 붙잡지 않도록 `Max Timeout` 설정. +- 비어있는 응답을 보낼 때의 오버헤드 최적화. +- (만약 했다면) Node.js의 경우 Event Loop가 차단되지 않도록 비동기 로직 수정. + +--- + +## 💡 "AI만 쓰는 사람"처럼 안 보이려면? + +글 마무리나 중간에 이런 뉘앙스를 한 줄만 섞어주세요. + +> "AI는 구현 속도를 비약적으로 높여주지만, 시스템의 안정성과 확장성(Scalability)은 결국 개발자가 도메인 지식을 바탕으로 검증해야 한다는 것을 깨달았다." +> + +| **지표 (Metrics)** | **수정 전 (AS-IS)** | **수정 후 (TO-BE)** | **개선 결과** | +| --- | --- | --- | --- | +| **최대 동시 접속 수** | 약 30 ~ 40명 | 200명 이상 테스트 통과 | **500% 향상** | +| **평균 메모리 사용량** | 800MB (임계치 도달) | 250MB (안정권) | **68% 감소** | +| **장애 발생 주기** | 1일 3~4회 강제 종료 | 장애 발생 없음 | **안정성 확보** | + +### 임시 트러블슈팅 + +- 문제 : 키오스크 기기 하나 매장에 더 들어갔는데 몇 분 안돼서 서버가 강제 종료됨 (아무것도 이용 불가;;) +- 원인 : 관리자웹에서 기기/서버 점검 창을 키오스크 기기에 띄울 수 있도록 하는 기능을 long-polling 으로 구현했었는데 이걸 사용하는 기기가 하나 더 늘어나니가 db 점유율이 너무 많아져서 그런 것. + - long-polling + SQLAlchemy Session이 커넥션을 오래 물고 있었음 + - long-polling 이 트랜잭션으로 작동되는데 session이 처음 db 작업을 하는 순간 트랜잭션이 자동 시작되고 그 트랜잭션/커넥션이 commit/rollback/close 되기 전까지 풀에 안 돌아가 버리는 케이스임. (긴 요청에 while로 계속 도는거지) + - 폴링 요청이 여러 개 동시에 걸려버리면 session 1개씩 + 커넥션 1개씩 장시간 점유 → 풀 고갈 → QueuePooltimeout 이 터짐 +- 해결 + - check_interval 1s → 3s + - 폴링 간격을 1초 → 3초로 늘렸음 + - pool_size/overflow 키우기 땜빵용으로 + - 동시 커넥션 한도 늘려서 조금 더 버티게 하기 + - 하지만 long - polling 요청이 커넥션을 너무 오래 점유하면 pool_size를 키워도 언젠가 또 고갈남 (클라이언트 수가 늘수록 선형적 증가) + - **근본적으로 끝내려면 long - poll 루프에서 매번 트랜잭션을 끝내서 커넥션 반환** + +## 타겟 트러블슈팅 + +원인: 임시로 동시 커넥션 제한 수 늘린 것 안정적으로 고치기 + +문제: long polling 구현 방식이 db 커넥션 풀과 궁합이 좋지 않음 + +- 일단 기본적으로 사용하는 리소스 양은 long polling > websocket임 +- websocket을 안 쓰고 long polling 을 사용한 이유가 websocket 같이 연결 하나를 아예 열어두고 있을만한 기능이 아니라고 생각함. 별로 바뀌는 게 잦지 않고 매우 드문 거고 1분 이상의 간격으로 요청하기에는 변경이 일어날 시 빠르게 알아차리는 게 중요하다고 생각해서 long polling 을 한건데 long polling 을 사용하는 기기가 여러개라면 어쩌면 websocket보다 그 리소스 잡아먹는 양이 더 안좋을수도 있는 것임. +- 즉, 데이터 변경이 드문 기능이라서 효율적일 줄 알았으나, HTTP 오버헤드의 반복, 동시 접속자 수 비례 리소스 점유 라는 치명적인 단점이 트래픽 증가 시점에 서버 다운이라는 결과로 이어짐 + +해결 : SSE를 써도 되긴 하나 기기 갯수가 제한되어 있기 때문에 long polling 을 제대로 쓰는 쪽으로 해보자 + +### system-status 로그가 계속 찍히는 이유 + +클라이언트가 응답을 받자마자 즉시 재요청하고, 서버가 실제로는 바로 응답해버림 → since가 매번 None으로 들어옴 → 즉시 전체 반환하는 분기 + +→ since가 실제로 들어오는지 확인 (None으로 들어와서 값 넣어주는 걸로 했더니 바로 로그 갯수 확연히 줄어들음 → 1초에 6개씩은 나오던게 6초에 3개 정도 나온다) + +### 정상적인 long polling 방식 + +⇒ ‘”대기 동안의 비용이 거의 0 이어야 한다.” + +long polling 요청이 오래 살아있으니까 서버 리소스는 기본적으로 더 사용하게 됨(열려있는 http 요청 자체가 서버 자원을 먹음). 특히 스레드/커넥션 풀 같은 제한이 있으면 클라이언트 수에 비례해서 항상 떠있는 요청 수가 늘어나는 것. → 변경이 드물어도 long polling 이 cheap 하지 않을 수 있다 + +⇒ 보통 점검 상태 같은건 서버 → 클라이언트 단방향 push 인 SSE를 많이 사용한다. + +### 효율을 나눠서 보기 + +1. 네트워크/HTTP 오버헤드 + 1. polling : 주기마다 요청/응답 반복 → 오버헤드 일정하게 발생 + 2. long polling : 요청 오래 열려있다가 timeout 때만 재요청 → 요청 횟수 줄어들고 변경이 매우 드물면 요청 수 확실히 줄음 +2. 서버 리소스 (이게 제일 중요) + 1. polling : 요청은 짧게 끝남 → 동시 요청 수 낮고 예측 가능 + 2. long polling : 항상 열려있는 요청이 클라이언트 수만큼 발생 → 동시 요청 수가 클라이언트 수와 거의 동일하게 유지 + + ⇒ polling이 유리함 (커넥션/메모리/서버 워커 관점에서) + +3. DB 부하 + 1. polling : 주기마다 DB 조회 → 변경 없어도 쿼리 계속 + 2. long polling : 구현에 따라 다른데 + 1. 나쁜 구현(내 코드) : polling이랑 똑같이 쿼리 계속 + 2. 좋은 구현(이벤트 기반) : 변경 발생 시에만 DB 조회 → DB 부하 극적으로 줄어듦 + + ⇒ DB 측면에서 이득을 보려면 이벤트 기반에 가까워야 한다. + + +### 내 상황에서는 뭐가 효율적이냐? + +1. 3~10초 polling + 1. 클라이언트가 3~10초마다 최신 상태 요청 + 2. 즉시성은 떨어지지만 + 3. 서버는 요청 짧고 안정적이어서 좋음 + 4. DB도 인덱스 + latest 같은 endpoint로 최적화하면 ㄱㅊ +2. SSE (보통 점검 상태는 이걸 사용) + 1. websocket처럼 양방향/복잡하지 않음 + 2. HTTP 기반 단방향 스트림이므로 투머치 느낌 덜하고 + 3. 서버가 변경시점에만 push 하면 됨 + 4. long polling 보다 열려있는 연결을 효율적으로 다루는 편 +3. long polling 유지하되 **제대로** 만들기 + 1. 매 반복마다 DB 세션을 닫거나 rollback 해서 커넥션을 반환 + 2. 가능하면 DB 폴링을 줄이고 캐시/이벤트 사용 + 3. 동시 접속자 증가에 따라 열린 요청 수는 늘어나긴 함 (본질적) \ No newline at end of file diff --git a/assignments/latteeea/week3/data/multi_agent.md b/assignments/latteeea/week3/data/multi_agent.md new file mode 100644 index 0000000..baeece0 --- /dev/null +++ b/assignments/latteeea/week3/data/multi_agent.md @@ -0,0 +1,56 @@ +### 멀티 에이전트 구조 채택 이유 + +기존에는 paper → evidence llm이 추출하고 생성한 rule → 바로 추천까지 가서 + +추천 결과에서 임상학적으로 잘못 나온게 있는지 하나하나 확인했음 → 지피티한테 페르소나 먹여서 제대로 나오는지 확인했는데 이렇게 하니까 의약품이 들어가는 경우도 있고 맥파속도 증가(안좋은거)인데 해당 성분이 어떤 state를 변화하는데 positive 하게 작용했다고 잘못 evidence direction 필드가 채워져서 룰 품질이 너무 손상되는 경우가 꽤 있었다. 그래서 임상 지식을 빡세게 걸고 추후 필터링 로직에서 잘못 결과가 나올때마다 각 경우를 사전에 정의해서 추천때 막는걸로 계속 했는데 + +이건 진정한 자동화가 아니고 코드도 더러워지고 유지보수할때 너무 힘들것 같다는 생각을 했음 + +지금 문제인건 evidence 필드 누락 혹은 할루시네이션 발생 +→ 이로 인한 데이터 품질 하락 및 rule 생성 시에도 결합하는 구조에서 supports_to_target만 반ㅇ여하고 interacts_with_target 은 아예 rule로 안들어가는 evidence로 처리하는 등 할루시네이션으로 인한 룰 품질 저하를 막기 위해 일부로 설정한 몇몇 보수적인 로직때문에 rule 이 적게 나오는 경우 발생 +→ 데이터 품질 저하 뿐만 아니라 rule 생성에서도 데이터가 많이 탈락되는 게 보였고 이런 사전 정의나 필터링을 아무리 걸어도 rule 이 제대로 나오는게 맞는지에 대한 확신은 여전히 부족했음 + +⇒ 멀티 에이전트 구조로 가서 이중으로 검증하자 (추출보다 중요한게 raw data를 정형화했을때 잘 들어갔냐에 대한 검증이라고 생각했고) + +evidence 필드에 대한 데이터 적합성 검증 + rule 자체의 임상학적 커버력 검증 + +두개를 추가했고 이게 성능 및 추천 결과상 어떻게 작용했냐면 + +- 새 논문 추출시 사전이나 필터링 로직 추가한 횟수가 몇번에서 → 몇번으로 절감 +- 이로 인한 추천 결과 품질이 어느정도 향상 (정확도) + +Adversarial Injection 을 하기로 결정한 이유 + +모든 것을 에이전트가 검증하고 그 검증한 걸 모두 믿기에는, 할루시네이션 때문에 검증 에이전트를 추가한 거기 때문에 영원히 할루시네이션의 두려움에서 벗어날 수 없음 →인간의 수동 검증을 필수적으로 들어가게 하되, 수동 검증시 발견한 오류 패턴을 프롬프트에 집어넣어서 계속해서 검증하는 패턴을 추가해나가는것 (FN Rate가 안정될때까지) + +수동검증을 하는 목적은 2개가 있는데 + +1. 캘리브레이션 : validator 가 못 잡는 새 오류 패턴이 있는지? - 새 패턴 발견이 멈출 때 수동 검증 중지 +2. 모니터링 : 현재 FN rate 가 허용 수준인지? - 수치가 안정될 때 수동 검증 중지 + +FN Rate 수치는 어떻게 하면 될까 (FN rate 95% 신뢰구간 측면에서) + +1. 20개 검토, 0개 오류 → FN Rate가 14% 정도 → 너무 불확실 +2. 60개 검토, 0개 오류 → FN Rate가 6% 정도 → 아직 부족 +3. 100개 검토, 0개 오류 → FN Rate가 4% 정도 → FN rate 5% 이하를 목표로 하면 80개 연속 clean이 나와야 통계적 근거가 생김 + +![{7A20F08F-94F5-478A-82F7-E3143ADC5CC5}.png](attachment:de4574ae-c8b2-4dc7-a136-2a32a236f22f:7A20F08F-94F5-478A-82F7-E3143ADC5CC5.png) + +### 확인 필요 필드 + +- study_type : evidence 의 점수를 결정짓는 필드이기 때문에 +- state_tags : state tag가 “억지없이” 수용가능할 정도로 들어갔는가 +- ~~human_review_status~~ + - ~~unreviewed : validator(검증 llm)가 approve(문제없음)으로 남긴 것 → validator가 놓친 오류가 있는지~~ + - ~~validator_correct : 오류 있어서 (direction, 의약품, 인과관계 혹은 state_tag 등의 누락) 고친 것 → validator가 수정을 제대로 했는지? 확인~~ +- ingredient_applicability : ingredient_name (해당 근거에서 중심을 두고 있는 성분)과 state_tag와 직접적 관련이 없으면 unrelated로 되어있는가 (관련이 없는데 direct로 되어 있진 않은가) +- claim_subject : 성분-효과의 주체 + - substance_intervention : 해당 성분이 상태를 변화시켰을때 (룰이 만들어지는 evidence) + - covariate_pattern : 공변량 패턴으로 성분이 성분을 변화시키는 등 다른 변수가 있는 경우 (룰 생성 x) + - behavior_exposure : 특정 행동을 통해 상태가 변화한 경우 (룰 x → just 매커니즘 노트로 들어감) +- direction : 성분-상태의 관계 벡터 + - positive : 성분이 상태의 상황을 “좋은 방향으로” 변화시켰는가 + - negative : 성분이 상태의 상황을 “나쁜 방향으로” 변화시켰는가 + - unclear/mixed : postive와 negative가 충돌했거나 하나의 벡터로 확정짓기 어려움 + +paper → evidence → rule \ No newline at end of file diff --git a/assignments/latteeea/week3/data/nginx_deploy.md b/assignments/latteeea/week3/data/nginx_deploy.md new file mode 100644 index 0000000..a4fb41f --- /dev/null +++ b/assignments/latteeea/week3/data/nginx_deploy.md @@ -0,0 +1,98 @@ +## 1. 포트 점유 충돌: "왜 내 서비스가 8000번 포트를 못 쓰지?" + +### **문제 상황** + +신규 FastAPI 서비스를 실행하려는데 `Address already in use` 에러가 발생하며 8000번 포트를 사용하지 못함. + +### **원인** + +과거에 `/srv/insole-server/` 경로에서 실행했던 옛날 서비스 프로세스가 시스템 서비스(systemd)로 등록되어 이미 8000번 포트를 점유하고 있었음. + +### **해결 과정** + +1. `ps -ef | grep 8000`으로 범인 프로세스 PID 확인. +2. `grep -r`을 이용해 `/etc/systemd/system/`에서 해당 프로세스를 실행 중인 서비스 파일명 탐색. +3. `/srv` 경로를 바라보던 유령 서비스를 중단. + +### **최종 해결** + +Bash + +`sudo systemctl stop [옛날서비스명] +sudo systemctl disable [옛날서비스명] +sudo systemctl restart insole-api # 진짜 서비스 실행` + +--- + +## 2. Nginx 405 Method Not Allowed: "POST 요청이 왜 안 돼?" + +### **문제 상황** + +로그인 버튼을 눌렀는데 `405 Not Allowed` 에러 발생. + +### **원인** + +Nginx가 `/api/` 요청을 백엔드(FastAPI)로 넘겨주지 않고, 자신의 정적 파일 폴더에서 `/api`라는 파일을 찾으려 함. Nginx는 기본적으로 정적 파일에 대한 `POST` 요청을 허용하지 않음. + +### **해결 과정** + +Vite의 `proxy` 설정은 로컬 개발용일 뿐, 배포 환경에서는 Nginx가 그 역할을 대신해야 함을 인지. + +### **최종 해결** + +Nginx 설정 파일(`sites-available`)에 `location /api/` 블록을 추가하고 `proxy_pass`로 8000번 포트 연결. + +--- + +## 3. Nginx 500/403 Permission Denied: "파일은 있는데 왜 못 읽니?" + +### **문제 상황** + +웹사이트 접속 시 흰 화면이 뜨거나 이미지(assets) 로딩 시 `500 Internal Server Error` 또는 `403 Forbidden` 발생. + +### **원인** + +Nginx 실행 계정(`www-data`)이 사용자의 홈 디렉토리(`/home/ubuntu`) 내부에 있는 빌드 파일에 접근할 권한이 없었음. 권한이 막히자 Nginx가 `try_files` 설정에 의해 무한 루프에 빠짐. + +### **해결 과정** + +1. Nginx 에러 로그(`/var/log/nginx/error.log`)에서 `Permission denied` 확인. +2. 상위 폴더인 `/home/ubuntu`부터 실행 권한(`+x`)이 있는지 체크. + +### **최종 해결** + +상위 폴더부터 하위 빌드 파일까지 Nginx가 접근할 수 있도록 권한 부여. + +Bash + +`sudo chmod 755 /home/ubuntu +sudo chmod -R 755 /var/www/manage/dist` + +--- + +## 4. 경로 미스터리: "내가 알던 dist가 그 dist가 아니다" + +### **문제 상황** + +Nginx 설정을 마쳤는데도 계속 404가 뜨거나, 뜬금없이 AWS CLI 관련 파일들만 보임. + +### **원인** + +서버 내에 `dist`라는 이름의 폴더가 여러 개 존재함. 특히 `/home/ubuntu/aws/dist`는 AWS CLI 설치 파일이었는데, Nginx가 이곳을 웹사이트 루트로 바라보고 있었음. + +### **해결 과정** + +1. `find / -name "index.html"` 명령어로 서버 전체에서 진짜 관리자 페이지 빌드물이 어디 있는지 수색. +2. `/var/www/manage/dist`라는 진짜 경로를 찾아냄. + +### **최종 해결** + +Nginx 설정 파일의 `root` 경로를 가짜 폴더(`~/aws/dist`)에서 진짜 폴더(`/var/www/manage/dist`)로 전격 수정. + +--- + +### **💡 블로그 포스팅** + +- **제목 추천:** "EC2에서 React + FastAPI 배포하며 겪은 Nginx 삽질기 (403, 404, 405, 500 완벽 정리)" +- **핵심 교훈:** "Nginx 설정을 바꿨다면 반드시 에러 로그(`tail -f /var/log/nginx/error.log`)를 먼저 확인하자!" +- **마무리:** 백엔드는 `git push` 자동 배포, 프론트는 `scp` 수동 배포로 이원화하여 관리하는 효율적인 워크플로우 소개. \ No newline at end of file diff --git a/assignments/latteeea/week3/data/pagination.md b/assignments/latteeea/week3/data/pagination.md new file mode 100644 index 0000000..781f6ab --- /dev/null +++ b/assignments/latteeea/week3/data/pagination.md @@ -0,0 +1,17 @@ +## 🗂 오늘 작업 + +고객정보 조회 api 호출 시 offset과 limit 확인하는 습관 장착하기… + +## ⚙ 기술 / 개념 + +🐛 트러블슈팅 + +- 문제: 고객정보와 주문 데이터 동기화 과정에서 고객정보의 성별/생년월일이 동기화되지 않는 문제 발생 +- 원인: 아임웹 API 응답값을 확인해 봤는데 total_count는 아임웹 페이지에 있는 고객 그대로 들어오는 것으로 확인함. 디비 접속해서 회원 정보 업데이트 안된 사람들 확인해봤더니 성별과 생년월일이 아임웹에서 제대로 들어온사람은 딱 100명이었다.. 100명 초과가 됐을 때 아임웹 회원가입 시 설문을 통해 성별을 입력받는 시기가 약간 겹쳤었어서 이 설문 때문에 정보가 제대로 안들어오는건지, 백엔드 코드 문제인지 헷갈렸는데 아임웹 api 응답값 직접 확인해보니 제대로 들어오고 있음에도 불구하고 우리 서버에 적용이 안되는 경우가 있었음. +- 해결: 아임웹 api reference를 열심히 뒤진 결과 offset과 limit 요청 파라미터로 데이터가 페이지네이션 돼서 들어오고 있었음. offset인 걸 모르고 page로 임의로 넣고 있었는데 이게 아니었지… limit 최댓값이 100이었고 아 이것 때문이구나 해서 데이터 다 받을때까지 offset 하나씩 늘려서 계속 데이터 받는 방식으로 동기화 로직을 변경했다. + +## 🧠 인사이트 + +외부 API 연동 시에는 응답 규격과 목록 요청 파라미터, 페이지네이션 정책을 꼭 확인하자. + +아임웹에서 고객 정보 조회/주문 조회 두개 쓰고 있었는데 주문 조회는 제일 최근꺼 몇개만 받아오고 우리 디비에 업데이트하는 식이었어서 offset, limit 이 소용 없었음. 고객 조회는 제일 처음 사람부터 보내줘가지고 이게 중요했다. \ No newline at end of file diff --git a/assignments/latteeea/week3/data/pgadmin_killed.md b/assignments/latteeea/week3/data/pgadmin_killed.md new file mode 100644 index 0000000..3d63b40 --- /dev/null +++ b/assignments/latteeea/week3/data/pgadmin_killed.md @@ -0,0 +1,529 @@ +### 에러 로그 + +```jsx +pgAdmin Runtime Environment +-------------------------------------------------------- +Python Path: "C:\Program Files\PostgreSQL\18\pgAdmin 4\python\python.exe" +Runtime Config File: "C:\Users\Kimta\AppData\Roaming\pgadmin4\config.json" +Webapp Path: "C:\Program Files\PostgreSQL\18\pgAdmin 4\web\pgAdmin4.py" +pgAdmin Command: "C:\Program Files\PostgreSQL\18\pgAdmin 4\python\python.exe -s C:\Program Files\PostgreSQL\18\pgAdmin 4\web\pgAdmin4.py" +Environment: + - ALLUSERSPROFILE: C:\ProgramData + - ANDROID_HOME: C:\Users\Kimta\AppData\Local\Android\Sdk + - APPDATA: C:\Users\Kimta\AppData\Roaming + - CommonProgramFiles: C:\Program Files\Common Files + - CommonProgramFiles(x86): C:\Program Files (x86)\Common Files + - CommonProgramW6432: C:\Program Files\Common Files + - COMPUTERNAME: TAERAN + - ComSpec: C:\WINDOWS\system32\cmd.exe + - Desktop: C:\Users\Kimta\Desktop + - DriverData: C:\Windows\System32\Drivers\DriverData + - EFC_18504_1262719628: 1 + - EFC_18504_1592913036: 1 + - EFC_18504_2283032206: 1 + - EFC_18504_2775293581: 1 + - EFC_18504_3789132940: 1 + - ELECTRON_ENABLE_SECURITY_WARNINGS: false + - FPS_BROWSER_APP_PROFILE_STRING: Internet Explorer + - FPS_BROWSER_USER_PROFILE_STRING: Default + - HOMEDRIVE: C: + - HOMEPATH: \Users\Kimta + - JAVA_HOME: C:\Program Files\Android\Android Studio\jbr + - LOCALAPPDATA: C:\Users\Kimta\AppData\Local + - LOGONSERVER: \\TAERAN + - NUMBER_OF_PROCESSORS: 16 + - NVM_HOME: C:\nvm + - NVM_SYMLINK: C:\nvm4w\nodejs + - OneDrive: C:\Users\Kimta\OneDrive + - ORIGINAL_XDG_CURRENT_DESKTOP: undefined + - OS: Windows_NT + - Path: C:\Program Files\PostgreSQL\18\pgAdmin 4\runtime;C:\nvm4w\nodejs;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files\Git\cmd;C:\Users\Kimta\AppData\Local\Android\Sdk\platform-tools;C:\nvm;C:\nvm;C:\Program Files\PostgreSQL\18\bin;C:\Users\Kimta\OneDrive\Desktop\ffmpeg-7.1.1-full_build\ffmpeg-7.1.1-full_build\bin;C:\Users\Kimta\AppData\Local\Programs\Python\Python310\Scripts\;C:\Users\Kimta\AppData\Local\Programs\Python\Python310\;C:\Users\Kimta\AppData\Local\Programs\Python\Python311\Scripts\;C:\Users\Kimta\AppData\Local\Programs\Python\Python311\;C:\Users\Kimta\AppData\Local\Programs\Python\Launcher\;C:\nvm4w\nodejs;C:\Users\Kimta\AppData\Local\Microsoft\WindowsApps;C:\Users\Kimta\AppData\Local\Android\Sdk\platform-tools;C:\Users\Kimta\AppData\Local\Android\Sdk\emulator;C:\Users\Kimta\AppData\Local\Android\Sdk\tools;C:\Users\Kimta\AppData\Local\Android\Sdk\tools\bin;C:\nvm;C:\nvm;C:\Users\Kimta\AppData\Local\Programs\Microsoft VS Code\bin;C:\Users\Kimta\AppData\Local\Programs\cursor\resources\app\bin;C:\Users\Kimta\OneDrive\Desktop\ffmpeg-7.1.1-full_build\ffmpeg-7.1.1-full_build\bin; + - PATHEXT: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC + - PGADMIN_INT_KEY: 42071496-44f8-44fa-b5cb-1901b1b3cfe6 + - PGADMIN_INT_PORT: 63414 + - PGADMIN_SERVER_MODE: OFF + - PROCESSOR_ARCHITECTURE: AMD64 + - PROCESSOR_IDENTIFIER: Intel64 Family 6 Model 154 Stepping 3, GenuineIntel + - PROCESSOR_LEVEL: 6 + - PROCESSOR_REVISION: 9a03 + - ProgramData: C:\ProgramData + - ProgramFiles: C:\Program Files + - ProgramFiles(x86): C:\Program Files (x86) + - ProgramW6432: C:\Program Files + - PSModulePath: C:\Program Files\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules + - PUBLIC: C:\Users\Public + - SESSIONNAME: Console + - SystemDrive: C: + - SystemRoot: C:\WINDOWS + - TEMP: C:\Users\Kimta\AppData\Local\Temp + - TMP: C:\Users\Kimta\AppData\Local\Temp + - USERDOMAIN: TAERAN + - USERDOMAIN_ROAMINGPROFILE: TAERAN + - USERNAME: Kimta + - USERPROFILE: C:\Users\Kimta + - windir: C:\WINDOWS + - ZES_ENABLE_SYSMAN: 1 +-------------------------------------------------------- + +Total spawn time to start the pgAdmin4 server: 0.008 Sec +2026-02-26 15:38:31,689: WARNING werkzeug: Werkzeug appears to be used in a production deployment. Consider switching to a production web server instead. + + * Serving Flask app 'pgadmin' + * Debug mode: off + +------------------------------------------ +Total time taken to ping pgAdmin4 server: 93.901 Sec +------------------------------------------ +Total launch time of pgAdmin4: 94.51 Sec +------------------------------------------ +Application Server URL: http://127.0.0.1:63414/?key=42071496-44f8-44fa-b5cb-1901b1b3cfe6 +2026-02-26 15:38:55,467: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 + +2026-02-26 15:39:02,277: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 + +2026-02-26 15:39:05,024: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 + +2026-02-26 15:39:08,378: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 + +2026-02-26 15:39:14,702: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:39:14,721: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:39:14,833: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 + +2026-02-26 15:39:18,532: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:39:24,923: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:39:28,864: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:39:49,206: ERROR pgadmin: connection timeout expired +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5433', hostaddr: '::1': connection timeout expired +- host: 'localhost', port: '5433', hostaddr: '127.0.0.1': connection timeout expired + +2026-02-26 15:39:49,318: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:39:50,895: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:39:51,008: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:39:52,414: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:39:52,434: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:39:52,550: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:39:52,823: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:39:52,917: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:39:53,019: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:39:53,021: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:39:53,128: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:39:53,169: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:39:53,298: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:39:57,466: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 + +2026-02-26 15:40:12,523: ERROR pgadmin: connection timeout expired +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5433', hostaddr: '::1': connection timeout expired +- host: 'localhost', port: '5433', hostaddr: '127.0.0.1': connection timeout expired + +2026-02-26 15:40:12,634: ERROR pgadmin: connection timeout expired +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5433', hostaddr: '::1': connection timeout expired +- host: 'localhost', port: '5433', hostaddr: '127.0.0.1': connection timeout expired + +2026-02-26 15:40:17,615: ERROR pgadmin: connection timeout expired +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5433', hostaddr: '::1': connection timeout expired +- host: 'localhost', port: '5433', hostaddr: '127.0.0.1': connection timeout expired + +2026-02-26 15:42:28,077: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:42:28,901: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:42:29,090: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:42:52,529: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:42:53,001: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:42:55,111: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 + +2026-02-26 15:42:59,965: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:00,259: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:00,259: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:00,583: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:00,690: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:00,799: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:03,945: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:04,526: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:09,928: ERROR pgadmin: Failed to execute query (execute_async) for the server #3 - CONN:3449827(Query-id: 200001): +Error Message:ERROR: consuming input failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:10,068: ERROR pgadmin: Failed to execute query (execute_async) for the server #3 - CONN:428388(Query-id: 268034): +Error Message:ERROR: consuming input failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:10,160: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:43:10,759: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:13,475: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:14,207: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:14,240: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:43:14,273: ERROR pgadmin: Failed to execute query (execute_2darray) for the server #3 - CONN:5900114 (Query-id: 5116705): +Error Message:consuming input failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:14,391: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:43:14,509: ERROR pgadmin: Failed to execute query (execute_async) for the server #3 - CONN:6008019(Query-id: 1615208): +Error Message:ERROR: consuming input failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:14,652: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:43:15,016: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:15,208: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:43:15,423: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:43:15,672: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:43:16,669: ERROR pgadmin: Failed to execute query (execute_async) for the server #3 - CONN:5372890(Query-id: 7718987): +Error Message:ERROR: consuming input failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:18,301: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 + +2026-02-26 15:43:18,328: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:18,846: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 + +2026-02-26 15:43:19,543: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:20,159: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:21,648: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:22,734: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 + +2026-02-26 15:43:22,826: ERROR pgadmin: connection timeout expired +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5433', hostaddr: '::1': connection timeout expired +- host: 'localhost', port: '5433', hostaddr: '127.0.0.1': connection timeout expired + +2026-02-26 15:43:23,426: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:24,613: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 + +2026-02-26 15:43:25,099: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: "uwellnow" 데이터베이스 없음 + +2026-02-26 15:43:25,403: ERROR pgadmin: connection timeout expired +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5433', hostaddr: '::1': connection timeout expired +- host: 'localhost', port: '5433', hostaddr: '127.0.0.1': connection timeout expired + +2026-02-26 15:43:25,784: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:43:26,607: ERROR pgadmin: Failed to execute query (execute_2darray) for the server #3 - CONN:9510520 (Query-id: 9621522): +Error Message:consuming input failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:27,594: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:43:27,648: ERROR pgadmin: connection timeout expired +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5433', hostaddr: '::1': connection timeout expired +- host: 'localhost', port: '5433', hostaddr: '127.0.0.1': connection timeout expired + +2026-02-26 15:43:28,695: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:28,859: ERROR pgadmin: Could not find the specified database. + +2026-02-26 15:43:29,225: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:29,596: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:29,730: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:29,843: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:29,935: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:30,139: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:30,201: ERROR pgadmin: connection timeout expired +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5433', hostaddr: '::1': connection timeout expired +- host: 'localhost', port: '5433', hostaddr: '127.0.0.1': connection timeout expired + +2026-02-26 15:43:30,341: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:30,370: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:43:30,472: ERROR pgadmin: Failed to execute query (execute_2darray) for the server #3 - CONN:3376031 (Query-id: 5432376): +Error Message:consuming input failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:30,488: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:43:30,559: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:30,683: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: fe_sendauth: no password supplied +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: fe_sendauth: no password supplied + +2026-02-26 15:43:31,387: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:31,815: ERROR pgadmin: Failed to execute query (execute_2darray) for the server #3 - CONN:8838435 (Query-id: 421063): +Error Message:consuming input failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:31,848: ERROR pgadmin: Failed to execute query (execute_2darray) for the server #3 - CONN:7618602 (Query-id: 8882867): +Error Message:consuming input failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:31,867: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:32,066: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:32,138: ERROR pgadmin: Failed to execute query (execute_2darray) for the server #3 - CONN:6847692 (Query-id: 4360272): +Error Message:consuming input failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:32,521: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:32,647: ERROR pgadmin: connection failed: connection to server at "35.227.164.209", port 5432 failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:34,294: ERROR pgadmin: Failed to execute query (execute_2darray) for the server #3 - CONN:436281 (Query-id: 8485337): +Error Message:consuming input failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:34,296: ERROR pgadmin: Failed to execute query (execute_2darray) for the server #3 - CONN:1575895 (Query-id: 8881322): +Error Message:consuming input failed: SSL connection has been closed unexpectedly + +2026-02-26 15:43:40,973: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. + +2026-02-26 15:43:43,750: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. + +2026-02-26 15:43:43,848: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. + +2026-02-26 15:43:43,993: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. + +2026-02-26 15:43:45,750: ERROR pgadmin: connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. +Multiple connection attempts failed. All failures were: +- host: 'localhost', port: '5432', hostaddr: '::1': connection failed: connection to server at "::1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. +- host: 'localhost', port: '5432', hostaddr: '127.0.0.1': connection failed: connection to server at "127.0.0.1", port 5432 failed: 치명적오류: 최대 동시 접속자 수를 초과했습니다. + +``` + +### 문제 상황 + +- 치명적 오류: 최대 동시 접속자 수를 초과했습니다. +→ max_connections 한도 초과 (커넥션을 많이 열었거나, 닫혔는데도 세션이 남아있거나, pgadmin이 재시도 폭주) +- “uwellnow” 데이터베이스 없음 → Could not find the specifed database +→ pgadmin 서버 등록에서 ‘maintenance DB’ 또는 기본 DB가 uwellnow로 되어 있는데, 실제 로컬 Postgres에 그 DB가 없음 +- fe_sendauth: no password supplied → 인증 필요한데 비번 입력 안된 상태에서 재시도 계속 +- 35.227.164.209 … SSL connection has been closed unexpectedly → 원격 DB 쪽 SSL 설정/네트워크 문제로 원격도 계속 재시도 + +⇒ 합쳐져서 폭주 더 심해짐 + +### 트러블슈팅 + +- 최종 원인 + - pgadmin에 등록된 서버 중 하나가 존재하지 않는 db를 기본 db (maintenance DB)로 들고 있음 + - fe_sendauth: no password supplied → pgadmin이 비번을 못 가져오거나 저장 안 함 + - 5433 포트 나오는 거 봐서 등록된 서버 여러 개고 그 중 하나가 5433 +- 시도 + - pgadmin 관련 워커 작업 끝내기 → postgres 서비스 재시작 ⇒ 소용 없음 (uwellnow 디비를 자꾸 찾으려고 함) + - postgres=# select usename, datname, application_name, count(*) from pg_stat_activity group by 1,2,3 order by 4 desc; → psql에서 현재 붙어있는 연결 보기 (pgadmin 다라락 찍히면 폭주하는 거 맞음) + - pg_stat_activity 겨과 보면 pgadmin 폭주로 db 커넥션 쌓이는 거 아님 + - psql 1개 말고 application_name 비어있는 거 8개 뿐임. 본체는 postgres가 문제가 아니라 pgadmin 쪽 로컬 설정/자격증명/등록 서버 꼬여서 에러 반복하는 것 +- 해결 + - dbeaver로 갈아탔음 → 버벅임 없고 완전 편안 + +### 인사이트 + +DBeaver과 PgAdmin의 공통점 + +- libpq 프로토콜 (TCP/IP)로 서버에 접속함 + +Pgadmin : PostgreSQL 전용 공식 툴 + +- PgAdmin → PostgreSQL driver → PostgreSQL server 구조인데 +- replication, vacuum, extension, role 관리, pg_stat 관련 기능에서 postgre 기능에 최적화되어 있다 +- 내부가 웹앱 구조 : Electron/Browser UI → Python backend (Flask) → DB 연결 + - 느릴 수 있고 + - 메모리 많이 먹고 + - 로딩 버벅임 있음 + +DBeaver : 범용 DB 툴 + +- Dbeaver(Java desktop app) → JDBC driver → postgreSQL server 구조인데 +- PostgreSQL JDBC driver로 연결하기 때문에 + - Java 기반 + - 연결 속도 빠름 + - 안정적 +- 모든 DB 지원 + +DBeaver이 더 안정적인 이유 + +> 레이어 수 + 실행 방식 + 프로토콜 처리 방식 차이 +> + +단순히 Java 라서가 아니라 중간계층이 훨씬 적고 직접 DB driver를 호출하는 구조임. + +PgAdmin은 + +```bash +Browser / Electron UI + ↓ +Python Flask backend + ↓ +psycopg driver + ↓ +libpq + ↓ +PostgreSQL +``` + +UI ↔ backend 통신이 있는 반면, (레이어가 한 개 더 있는 것) + +DBeaver은 + +```bash +Java Desktop App + ↓ +PostgreSQL JDBC driver + ↓ +PostgreSQL +``` + +바로 DB driver를 호출하는 구조이다. (중간에 백엔드가 없음) + +PgAdmin은 Python backend 에서 crash가 날 수 있다 (내가 겪은 흰 화면이 이런 경우) + +connection pool 이 제한에 도달했다거나 하는 면에서 백엔드 서버가 터질 수 있음 \ No newline at end of file diff --git a/assignments/latteeea/week3/data/pipeline_agent.md b/assignments/latteeea/week3/data/pipeline_agent.md new file mode 100644 index 0000000..5adfb7e --- /dev/null +++ b/assignments/latteeea/week3/data/pipeline_agent.md @@ -0,0 +1,28 @@ +- 지금의 structured field기반이 아닌 multi-agent로 가야하지 않나? + + 근데 진짜 건강에 대한 신뢰성 있는 추천 시스템이 되려면 지금 같은 코드 기반 다중 필터링이 아니라 + agent가 여러개 결합돼서 서로 상태를 공유하고 검증하는게 자동화된걸로 가야할것같아. 예를 들어 + paper에서 mechanism에 대한 근거가 나왔으면 그 메커니즘을 학습해서 다른 rule들에서 추천하는 성분을 뽑아낼때 그 메커니즘에서 참고해야할게 있으면 알아서 참고하는 식으로. 이런 건 어때? 이런 서비스가 있나? 이런 서비스랑 지금 프로젝트 구조랑 비교해봐봐 + + ![{3B056F45-70B7-4AD6-A702-42C637CF5E46}.png](attachment:d17e7b37-ccc5-4d94-9b16-02add3a8806e:3B056F45-70B7-4AD6-A702-42C637CF5E46.png) + + 아직 이런 서비스는 없긴 함. 논문 지식을 누적하면서 추천까지 가는 건 없는데 + + **논문 PDF 가 있으면** + + **→ 추출 에이전트 : EvidenceRow + 메커니즘 노드 → 지식 그래프 + → 집계 에이전트 : 근거 점수화 + 메커니즘 일관성 검증 + → 추천 에이전트 : 후보 성분 생성 + “이 메커니즘에서 지지되는가” 자기 검증 + → 검증 에이전트 : 추천 이유가 저장된 메커니즘과 모순되지 않는지 교차 검증** + + **→ 추천 JSON + 추론 체인 출력** + + ![{775DB48A-50F5-472E-A587-D43EAE7BCFD5}.png](attachment:68d6bd62-1471-4440-9b0b-a3e807fd1d3b:775DB48A-50F5-472E-A587-D43EAE7BCFD5.png) + + ### 다만 멀티에이전트는 일관성 관점에서 좋지 않음 (같은 인풋 다른 출력 많음) + + ### 근데 고려해야할점은 멀티 에이전트 + 강화학습 궁합 나쁨 + + RL은 고정된 상태 공간에서 명확한 행동을 반복 학습해야하는데 멀티에이전트 같이 출력이 계속 다른거에 붙여버리면 강화학습의 의미가 없음. + + ### 걍 base policy 레이어를 추가하는걸로… (mechanism evidence가 겹치면 bonus 점수 획득하는걸로) \ No newline at end of file diff --git a/assignments/latteeea/week3/data/pipeline_review.md b/assignments/latteeea/week3/data/pipeline_review.md new file mode 100644 index 0000000..1e48cdf --- /dev/null +++ b/assignments/latteeea/week3/data/pipeline_review.md @@ -0,0 +1,403 @@ +## 회고 정리 + +### 회고1 (기록 흐름) + +개발 흐름으로 보면 + +기록 범위는 2026-04-24 ~ 2026-05-20 약 4주입니다. + +[04-24~27] 정밀도 진단 + 대규모 수정 (12개 항목) +→ association rule 오염·confidence bias·dose 필터 등 기반 품질 개선 + +[04-28~29] 임상 지식 레이어 확장 + 배치 651편 +→ cross-nutrient 필터, 설문 재보정, 임신부 DRI, 안전성 로직 + +[05-06~07] 다중 소스 아키텍처 + 바이오마커 +→ 당일 컨디션 설문, biomarker_service, 약물 충돌 체크 + +[05-13~14] LLM 임상 검증 레이어 +→ 2-pass 검증, 소급 1,175개, QA 116개 완성 + +[05-20] 프롬프트 구조 결함 분석 +→ mechanism/association 분류 문제, support_level cap 코드 수정 + +### 회고2 (트러블슈팅 리스트업) + +포트폴리오 관점 정리 + +### 1순위 — "이런 문제를 발견하고 고쳤다" ⇒ 기능이 동작하는 상태에서 발견한 문제 (신뢰성 문제) + +가장 강하게 말할 수 있는 것들입니다. 버그 수정이 아니라 구조적 결함을 스스로 진단한 케이스들입니다. + +1. 거짓 정밀도(False Precision) 문제 발견과 분석 +파이프라인이 내놓는 숫자들이 실제로는 신뢰하기 어렵다는 것을 스스로 파악하고 7가지 구조적 문제로 분류했습니다. "LLM 추출 노이즈가 집계에서 걸러지는 게 아니라 그대로 증폭된다"는 진단은 시스템을 피상적으로 보지 않았다는 걸 보여줍니다. + - llm이 같은 개념을 매번 다르게 표기 (sleep_quality vs sleep duration) → 다른 그룹으로 분산돼서 각각 n=1짜리 근거가 됨 + - direction이 positive/negative/mixed로만 환원 → p=0.04 논문과 p<0.001 논문이 동등하게 집계됨 (효과 크기 손실) → confidence 필요하겠다 생각 + - confidence 가 llm의 주관적 판단 → 같은 논문도 추출 실행마다 달라질 수 있어서 재현성이 부족 + + ⇒ 시스템이 동작한 이후 ‘이 숫자를 믿을 수 있는가’를 스스로 질문하고 구조화해서 분석함. 파이프라인이 ‘강력 권장’ , ‘조건부 권장’ 같은 숫자를 내놓는데, 그 숫자 자체를 신뢰할 수 있는지 분석함 + +2. share 분모 버그 발견 +sp/(sp+sn) → sp/(sp+sn+sm) 수정. mixed 점수가 분모에서 빠져 tier가 과상향되고 있었는데, 이를 발견해서 실제 3개 그룹의 tier를 내렸습니다. 수식의 의미를 깊이 이해하지 않으면 보이지 않는 버그입니다. 동작하는데 틀린 케이스이고 이 버그는 신뢰성 문제로 직결되기 때문에 겉으로는 문제가 없어 보여서 테스트로도 잡히지 않는 유형임. + - 추천 tier를 결정하는 핵심 수식에 버그가 있었는데 **`share = sp / (sp+sn)`** mixed 점수가 분모에서 빠져있어서 tier가 과상향되는 부분이 있었음 + - share = sp / (sp+sn+sm) 으로 mixed를 포함하니 tier가 내려갔음 + - 예를 들어 positive 점수 3, negative 점수 1, mixed 점수 2가 있으면: + - 버그: 3/(3+1) = 0.75 → 강력권장 기준(0.65) 초과 + - 수정: 3/(3+1+2) = 0.50 → 조건부권장으로 내려감 + + ⇒ 논문에서 evidence 뽑아내는데 positive/negative로만 뽑는 코드일때 추가한거라 이렇게 되어있었는데 evidence 가 한 벡터방향으로만은 나오지 않는다는 것을 확인하고 mixed를 추가했고 이 수식을 차마 건드릴 생각을 못했는데 추천 결과에서 나온 tier 점수를 보다가 모 아니면 도로 나오기도 하고 근거가 꽤 적게 나온 주요 성분들이 있어서 추천의 출처 evidence를 하나씩 보다가 계산하는 부분에서 맞지 않는 것을 확인했다. 내가 계산한거랑 코드에서 나온거랑 달랐음 + +3. confidence 이중 스케일 불일치 수정 +evidence_scoring.py(float)와 generate_rules.py(int 1~3)가 서로 다른 스케일을 쓰고 있었습니다. 두 파일이 같은 개념을 다루는 것처럼 보이지만 실제로는 다르게 동작하고 있던 케이스입니다. + - evidence_scoring 에서는 high/medium/low를 1.0/0.7/0.4로 매기고 있고 + - generate_rules 에서는 high/medium/low를 3/2/1로 매기고 있었는데 + + ⇒ 만들어진 rule을 보면서 계산이 잘 나온건지 확인하고 있었는데(confidence_weight으로) 소숫점으로 나온 점수가 하나도 없길래 뭔가 이상하다 생각했고 반올림 하는 로직도 없었어서 이 부분을 뒤졌음. 다른 파일에서 아예 다른 숫자를 쓰고 있었어서 가중치 계산이 틀어졌던 것. + +4. survey → association rules 태그 단절 발견 +설문 24개 태그 중 9개만 추천 엔진에 연결되고 있었습니다. 사용자가 설문을 다 채워도 대부분이 추천에 반영이 안 되는 상태였는데, 이를 발견하고 prior_facts 14개 추가 + 허용 태그 확장으로 해결했습니다. + - cursor로 설문 질문들을 다 주고 이걸 state_tag로 변경하라고 했음 대충 5개 봤는데 잘 들어갔길래 넘어갔는데 + - claude로 하다가 거의 완성됐을때 추천을 돌려봤는데 ‘생선 거의 안 먹음’을 하든 ‘생선 자주 먹음’을 하든 결과가 같게 나오는 게 이상했고 state_tag 부분을 보니 + - 설문 24개 중에서 9개의 질문만 state_tag가 반영됐던 것이다… + - 설문에서 아무리 입력을 받아도 state_tag없는 15개는 추천 엔진에서 무시됐떤 것. + + ⇒ 전체 흐름을 end-to-end로 추적해서 확인하게 된것. 설문을 채웠는데도 이상하게 뜨는 증상에서 출발해 어디서 신호가 소실되는지 역추적한 과정이다. 추가하니까 오메가3 제대로 나오더라 + + +--- + +### 2순위 — "이런 설계 결정을 내렸다" + +단순 구현이 아니라 왜 이렇게 만들었는지를 설명할 수 있는 것들입니다. + +1. mixed 개념 분리: within_paper_mixed vs cross_paper_mixed (개념 모델링 문제) +"한 논문 안에서 상반된 결과"와 "여러 논문 간 불일치"는 다른 임상 의미인데 같은 라벨로 처리되고 있었습니다. 이를 두 개념으로 분리하고 policy_statement 문구까지 다르게 가져갔습니다. 코드보다 도메인 이해가 앞선 판단입니다. + - 한 rule의 direction 필드에서 mixed는 두가지를 표현하고 있었음 + - 논문 하나 안에서 “같은 실험자 대상으로 아침에는 효과 있고 저녁에는 효과 없다” + - 논문 A는 postivie, 논문 B는 negative로 상충 + + ⇒ rule 에서 mixed 된 게 있어서 정확히 positive랑 negative가 비슷한 비율로 있어서 mixed가 됐는지 확인해보려고 근거들을 쫙 봤는데 논문 안에서 상충과 논문 간의 상충 둘다 보이길래 이건 룰베이스 데이터로서 구분하는 게 낫겠다 싶었음. + +2. 2-pass LLM 임상 검증 레이어 설계 +단일 LLM 추출에서 반복되는 오류(calcium→PWV 증가→positive, calcitriol→otc_supplement)를 프롬프트 수정만으로는 막을 수 없다는 판단 후, 두 번째 LLM이 첫 번째 추출을 검증하는 구조를 도입했습니다. +비용(+15~20%)과 안전성의 트레이드오프를 인식하고 도입한 결정입니다. + - 반복적으로 나타나는 두 가지 오류 + - 칼슘 → 맥파 속도 증가 → direction = postive로 들어감 + (PWV 증가는 심혈관 위험 신호인데 증가만 보고 좋다고 판단함) + - 처방 의약품 → 성분 카테고리가 = otc_supplement + (의사 처방 없이 살 수 없는 약이 일반 보충제로 분류됨) + - 프롬프트를 아무리 수정해도 llm하나로는 논문을 다 읽고 evidence의 여러 필드를 채우는 과정에서 다양한 오류가 나왔지만 이 둘이가 추천 결과의 품질에 제일 큰 영향을 미치는 것들이었음 → 프롬프트 가이드라인으로는 통제할 수 있는 한계가 있구나 판단 + - LLM을 두번 쓰는 것으로 설계함 + - 첫번째 LLM이 논문에서 evidence와 필드들을 추출하고 + - 두번째 LLM이 그 추출 결과를 임상적으로 검증하는 것 (approve / correct / reject) → 저장 + - 검증 범위는 direction 정합성(outcome이 해당 state_tag를 가진 사람에게 좋은건지 나쁜건지) + - 처방약이 보충제로 분류됐는지의 여부 + - 자연 발생 혈중 농도를 개입 효과로 오인했는지 (자연 발생은 성분 개입으로 인한 효과가 아님 이걸 구분했어야했음) + - 핵심 설계 결정 : Phase 1 soft-reject + - reject 된 행도 삭제하진 않고 저장하지만 모든 exclude 플래그를 true해서 rule 생성에서 배제하도록 함 + - 검증 LLM도 결국 프롬프트로 하는거기 때문에 조정하지 않은 초반에는 틀릴 수 있음 + - 나중에 골든 데이터셋으로 FN 검증을 위해 남겨둠 + - 데이터가 남아 있어야 hard-reject 전환 여부 결정할 수 있으니까 + - 비용 최적화 + - 두번째 LLM에 논문 풀텍스트가 아니라 이미 추출된 JSON과 요약 500자만 넣어서 추출 비용을 15% 정도로 줄임. + + ⇒ LLM이 틀리기 때문에 프롬프트를 더 잘쓰자고 생각했지만 하면 할수록 한계가 존재함을 느끼고 LLM으로 LLM을 검증하는 구조를 만들자고 접근했고 전자 경우에서 데인 경험이 있기 때문에 후자 검증 LLM도 틀릴 수 있음을 인지하고 soft-reject로 안전하게 시작함. 의료 도메인이기 때문에 처음부터 hard-reject를 걸진 않았음 + +3. 다중 소스 아키텍처 (3개 신호 소스 병합) +초기 설문 + 당일 컨디션 설문 + 건강검진 바이오마커를 독립적으로 받아서 하나의 needs_vector로 합산하는 구조를 설계했습니다. 각 소스의 신선도 할인율(6개월 이내=1.0, 2년 초과=0.0)까지 반영했습니다. + - 처음 기획된 건 설문 하나에 의존하는 구조였는데 건강검진결과, 오늘의 컨디션 질문이 추가되어야 하는것으로 변경되었고 추후 인바디 결과 등 추가될 인풋이 있었음 + - 세 가지 소스를 독립적으로 받고 하나의 needs_vector로 합산하는 아키텍처 설계 + - 초기 설문 (기본 1.0~1.5) + 당일 컨디션 설문(보조 0.7~0.8) + 건강검진 바이오마커(선택, 신선도 할인율 적용) = 최종 needs vector → 추천 엔진 + - 다만 합산 시, needs_vector는 sum 방식으로 했고 (여러 소스에서 같은 신호 시 강화), constraint_tags는 max 방식으로 한 소스에서라도 금기가 있으면 적용하는 것으로 + - 신장 질환있다는 바이오마커가 있는데 설문에서 아무 문제 없다고 해도 금기 유지되어야 함 + + ⇒ 각 소스의 성격과 중요도가 다르다는 것을 인지하고 합산 방식을 다르게 가져감. 바이오마커에서는 시간에 가중치를 두어 “오래된 검사 결과는 덜 신뢰한다”는 실제 임상의 감각을 코드로 표현함 + +4. potassium hard_exclude 조건화 +신장 질환 환자에게만 적용되어야 할 금기가 모든 사람에게 적용되고 있었습니다. constraint_tags 파라미터를 추가해 "신장 질환 있을 때만 hard_exclude, 없으면 soft_warning"으로 분기했습니다. 의료 도메인에서 과도한 +안전 제한도 문제가 된다는 판단입니다. + - 신장 질환에만 적용되어야 하는 potassium 금기가 모든 사람에게 적용되고 있었음 + - 논문 하나에서 potassium에 대한 금기 점수가 커서 hard_exclude로 들어갔는데 로직이 신장 질환에 대한 금기가 아니라 그냥 모든 사람 적용 되는 contraindication_cluster로 들어가고 있었던것 + - 신잘 질환 있을때만 hard_exclude, 일반인은 soft_warning으로 경고 문구만 빼는것으로 변경했음 (신장질환 있는데 없다고 체크했을수도있으니까 알리긴 해야함) + + ⇒ 과도한 안전 제한도 문제구나… 느꼈음. 의료쪽이라 안전한 방향으로 제한하려다가 과도하게 제한하게 된점을 인지하고 신장 질환 없는 사람에게 필요한 추천을 못 받게 만드는 것도 나쁜결과라고 생각이 들었고 의학적 근거에 따라 분기하는것으로 변경함. + + +--- + +3순위 — "이런 결정을 의도적으로 하지 않았다" + +오히려 이게 포트폴리오에서 차별점이 될 수 있습니다. 안 만든 이유를 말할 수 있다는 것은 1년차에게 드문 역량입니다. + +1. atomic_facts ↔ association_rules 조율 보류 +두 경로가 다른 grouping key를 사용해서 동일 성분에 다른 결론이 날 수 있는 문제를 발견했지만, 고치지 않았습니다. 이유: 추천 엔진은 association_rules만 읽고 atomic_facts는 정책 문장으로만 연결되어 실제 추천에 +영향이 없고, 수정하려면 파이프라인 아키텍처 전체를 바꿔야 하기 때문입니다. "지금 건드리면 더 위험하다"는 판단입니다. + - 파이프라인에 두가지 집계 경로가 있는데 + - evidence_db → atomic_facts (5-tuple) → final_guidelines(정책 문장) + - evidence_db → association_rules (2-tuple) → 추천 엔진 + - atomic facts (rule_ingredient, outcome_normalized, population_segment, dose_route, claim_subject) + - association_rules (ingredient_name, state_tag) + - 왜 안고쳤냐면 3가지 이유 + - 실제 추천 결과에 영향이 없었음 (추천 엔진에 5 tuple이 안쓰이는 것) + - 그룹핑 된 후의 단계가 너무 길기 때문에 두 경로를 통합하거나 동기화하는 건 전체 데이터 흐름을 재설계해야함 → 아키텍처 전체를 바뀌어야 하는것에서 지금 논문 수에서는 그 비용이 얻는것보다 크다는 점 + - 재검토 시점이 명시했음 - 논문 수가 대폭 늘거나 추천 엔진때 final guidelines를 직접사용하는걸로 전환할때 + + ⇒ 이 문제자 존재한다는 것 자체를 인식했다는 게 핵심인게 실제 추천 결과에서는 안 쓰여서 테스트를 해도 보이지 않는 부분인데 임시 검증용으로 만든 final_guidelines의 로직을 점검하다가 이 두 흐름이 잘못 들어갔다는 걸 인식하고 지금 고치면 안되는 이유를 정리한 것은 기술 부채를 방치한 게 아니라 의도적으로 관리한 것 (커서로 개발하다가 막 나누던 얘기에서 5tuple을 쓰기로 했다가 너무 규칙이 안 모여서 2tuple로 줄였는데 이 과정에서 final_guidelines 쪽이 2tuple로 안바뀌고 5tuple로 남아있었던 것) + +2. RL 레이어 도입 보류 +강화학습 레이어를 나중에 붙일 계획인데, agent network 구조로 전면 전환하지 않기로 했습니다. 이유: RL은 결정론적 환경에서 credit assignment가 명확해야 하는데 agent network의 비결정성이 훈련을 불안정하게 +만들기 때문입니다. 기술 트렌드보다 구현 안정성을 택한 판단입니다. + - 사용자 복용 피드백을 받아서 강화학습으로 동적 리도징하는것을 기획해야했고 agent network 구조로 전면 전환하는 안도 검토를 했지만 + - LLM agent들이 네트워크로 연결 → 각자 역할 분담 → 추천 생성 구조를 검토했는데 + - agent network가 비결정적이었는데 + - llm이기 때문에 같은 입력에 다른 출력이 나올 수 있고 + - RL 훈련에서 credit assignment가 불가능해진다는 것 + - 강화학습이 제대로 작동하려면 이 행동이 이 결과를 만들었다 라는 인과관계가 명확해야 하는데, agent network는 내부 흐름이 llm에 의존하므로 동일 조건에서 다른 결정을 내릴 수 있고, 어느 agent의 어느 결정이 최종 결과에 영향을 줬는지 추적하기 어렵다. 이 상태에서 RL을 붙이면 훈련이 불안정해짐 + - 결정적으로 내린 결론은 + - 현재 결정론적 파이프라인 구조 유지하고 RL은 나중에 별도 레이어로 추가하고 agent는 설명/검증용으로만 사용 + - 현재는 같은 입력에 항상 같은 출력이 나오고 있고, pandas groupby + 수식기반 집계이기 때문에 여기에 RL 레이어 붙이면 credit assignment 가 명확함. rule의 가중치를 올렸더니 이 추천이 나왔고, 사용자 반응이 이랬다를 추적가능 + + ⇒ RL을 쓰고 싶다고 생각하고 RL이 제대로 작동하기 위한 필요 조건을 생각함. 멀티 에이전트 구조가 유행이기도 하고 지금 파이프라인이 복잡해서 트렌드에 따라가는 구조를 고려했지만 RL 훈련 안정성과 비결정성이 충돌할 수 있기 때문에 구현 요건을 우선하여 판단함. + +3. effect_size 필드 추출 보류 +정밀도 문제의 근본 해결책이지만, 지금 규모에서 우선순위가 아닌 것으로 판단했습니다. 대신 outcome_normalized 통제 어휘 도입이라는 더 현실적인 개선을 먼저 했습니다. + - 현재는 direction = positive / negative / mixed ← 범주형으로 구분하는데 + - p=0.04, HR=0.99 논문 = positive 이고 + - p<0.001, HR=0.30 논문 = positive 로 둘이 동등하게 집계됨 + - 이상적인 건 effect_size = 0.30 (실제 효과 크기) → 얼마나 postive 한가에 대한 수치화 필요성 느꼈고 effect_size, CI_lower, CI_upper 필드 추가하면 direction 대신 실제 효과 크기로 집계할 수 있었는데 하지 않은 이유 + - 추출 자체가 어려움. 논문마다 다른 효과 크기 지표가 있고 LLM이 통일된 스케일로 추출하는건 현재 구조에서 신뢰하기 어려움을 느낌 (llm의 한계를 느낀 상황에서 더더욱) 잘못 추출된 사이즈는 direction 오류보다 훨씬 심각한 오류를 만들 수 있다고 생각 + - 현재 데이터 규모에서 제한적. 성분당 평균 10~20편 논문에서 사이즈를 메타분석 수준의 집계를 하기에는 표본이 부족하고 논문 수가 훨씬 많아져야 effect_size 집계가 의미 있어짐 + - 더 즉각적인 개선책이 있었는데 그건 outcome_normalized 통제 어휘를 도입하는 것. 같은 개념이 다르게 표기되어 그룹이 분산되는 문제가 있었어서 effect_size보다 더 큰 임팩트를 줄 수 있었고 더 효과적이라고 생각해서 이걸로 우선 개선을 선택함. + + ⇒ effect_size가 더 좋다는 걸 알았지만 지금 당장 실용적인 해결책을 구분하고, 현재 데이터 규모와 추출 신뢰성을 고려하여 우선순위를 조정했고 기술적으로 완벽한 방법보다는 지금 환경에 맞는 방법을 선택했음 + + +--- + +4순위 — 개발 흐름에서 보이는 성장 지점 + +처음에 잡지 못했다가 나중에 고친 것들 + +초기에는 "동작하게 만들기" 수준이었다면, 갈수록 "왜 이런 결과가 나오는지 역추적"하는 방향으로 바뀌는 게 보입니다. 틀린 걸 발견하고 고치는 사이클을 빨리게 돈다는 점이 핵심. + +1. confidence 이중 스케일 발견 (4/24) + 1. 파이프라인을 처음 만들 때 두 파일이 따로 작성됐음. evidence_scoring을 먼저 만들고 나중에 generate_rules에서 confidence 가중치를 다시 구현했음. 클로드가. 합리적으로 보이는 스케일을 썼는데 서로 달랐고 + 2. 파이프라인 정밀도 전체 리뷰 하면서 점수가 어떻게 계산되는가를 끝에서 끝까지 추적하다가 발견한것. 개별 파일만 보면 둘다 멀쩡해보임. (두 파일의 일관성을 확인한것) + 3. 새로운 파일을 만들 때 기존 파일과의 스케일 일치를 먼저 확인하는 습관이 생겼다 +2. cross-nutrient 오염 (4/28) + 1. get_canonical_ingredient()가 target_ingredient(논문이 있는 구글 드라이브 폴더 이름:영양성분 이름임)를 우선으로 사용했음. 하지만 실제로 rule의 주어가 되어야 하는 건 ingredient_name이고 구글 드라이브에 팀원이 직접 iron을 검색해서 거기에 있는 논문을 구글 드라이브에 넣고 그걸 분석하는거기 때문에 실제 논문에서 다루는건 iron 성분이 아닐수도 있음을 파악함 + 2. 철분 부족하면 비타민B 흡수에 영향이 간다는 논문인데 논문 폴더가 비타민B 였고, 논문이 실제로 다루는 타깃은 iron인데 rule 생성시 target_ingredient를 우선 사용하여 iron x immune_vulnerability 룰이 비타민B 근거로 오염되는것을 발견함 + 3. association rule을 검토하다가 iron 룰의 근거 논문이 이상하게 섞여있는걸 발견했고 근거를 역추적하니 비타민B폴더 논문이 iron 룰에 들어가있었음 + + ⇒ canonical_ingredient → ingredient_name → target_ingredient 3계층 우선순위 구조를 이해하지 못했을 때 생긴 문제임. 이 수정 이후로 어느 성분의 관점에서 룰을 만드는지와 어느 성분이 이 연구에서 개입 물질인가를 명확히 구분해야한다는 것을 알았음. + +3. study_type 가중치 누락 + 1. evidence_score = study_type_weight x confidence_weight인데 association 룰 생성 시 코드에서 가중치 계산할 때 evidence score 대신 confidence weight만 쓰고 있었고 + 2. 결과적으로 RCT와 narrative review가 confidence가 같으면 동등한 가중치로 집계됐던 것. 연구 설계 수준을 기껏 llm한테서 추출하게 해놓고 룰 생성할때 안쓰고 있었던것 + 3. 알고리즘 문서 작성하면서 실제 추천 결정 흐름이 어떻게 되는가를 다시 정리하다가 발견함. 문서 쓰면서 코드 다시 읽는 과정에서 나옴 + + ⇒ 코드를 짜다가 발견한 게 아니라 문서를 쓰면서 발견함. 코드를 설명하려고 하면 실제로 어떻게 동작하는가를 더 정밀하게 보게 되고 이후 중요한 로직 변경 후에는 문서나 주석을 업데이트하는것자체가 검증수단이 된 계기. + +4. BMI 신호 미반영 버그 + 1. 체중/키 입력을 추천에서 추가할때 DRI 계산에 필요하다는 용도에만 집중했고 BMI가 needs_vector에도 기여해야한다는걸 연결하지 못했음. + 2. DRI 용량 계산에만 사용하다가 저체중인 사람에게는 low_protein_intake, 뭐 이런 신호가 들어가야하는데 체중/키를 입력해도 이 신호가 생성되지 않음 + 3. QA 시나리오 확장하면서 저체중 여성 페르소나를 만들고 돌려봤는데 protein 추천이 나오지 않았고 입력값이 달라지는데 반영이 안된다는걸 시나리오 테스트로 발견 + 4. BMI 가중치가 저체중의 low_protein_intake에서 0.4→1.0으로, 비만2의 cardiovascular_risk도 0.5→1.2로, 기존 가중치가 clinical guarantee threshold(1.0)에 미치지 못해 신호가 생성돼도 추천에 발동이 안됐기 때문. + + ⇒ 입력을 추가할때 이 데이터가 시스템의 어느 경로에 영향을 줘야 하는가를 전부 확인하지 않으 ㄴ실수. 수정 후에는 새로운 입력 파라미터가 추가될 때 이 값이 DRI계산/needs_vector/constraint_tags 중 어디에 영향을 줘야하는가를 체크리스트로 확인하게 됨 + +5. mechanism/association 분류 오염 (5/20) + 1. 프롬프트에 evidence_type=mechanism을 언제 쓰는지 기준이 없었음. 허용 값 목록으로만 있고 정의가 없어서 llm이 성분이 관련된 생리 연구면 전부 association 으로 분류함 + 2. 고강도 운동→구리농도 감소가 llm이 판단하기에는 구리와 관련된 연구여서 association으로 판단했지만 실제 분류는 행동에 의한 성분 변화 경로이기 때문에 mechanism으로 들어가야함. direction도 negative였는데 운동강도가 높으면 → 구리가 낮아진다 상관계수 그대로 반비례라 negative로 들어갔지만 구리를 보충하는것은 부정적 효과라는 것은 잘못된 해석이다 + 3. 수동 검증 과정에서 특정 근거를 임상적으로 해석하려고 source_snippet까지 직접 읽다가 발견함. 필드 값과 실제 내용 사이의 불일치를 육안으로 발견함 + + ⇒ 이전에는 파이프라인 출력(rule, score)이 이상할 때 코드를 먼저 봤는데 이 케이스는 코드는 맞고, llm입력(프롬프트)가 잘못된 것으로, 코드가 아니라 데이터 생성 과정이 문제일 수 있겠다는 관점의 확장이 일어남. 추출 에이전트의 정확도를 검증하려고 수동 검증을 하다가 발견하게 되었고 룰이 이상하면 코드보다 evidence_db의 개별 행을 확인하는 방향이 자리잡힘. + + +4순위에서 결론은 초기에는 ‘기능이 동작하는가’를 보다가 시간이 지나면서 ‘기능이 정확하게 동작하는가’와 ‘데이터가 올바르게 만들어지고 있는가’를 보게 된 변화임. + +### 회고3 (평가1) + +바깥으로 나갈 숫자가 필요하다. ‘좋은 엔지니어링 판단력을 가진 사람’이라는 걸 보여주는 프로젝트이고, 그걸로도 충분히 유의미하다. + +1. 객관적 평가 + +강점 — 1년차치고 명확히 위에 있는 것들 + +일반적인 1년차 포트폴리오는 CRUD 앱, API 연동, 간단한 ML 모델입니다. 이 프로젝트는 그 레벨이 아닙니다. + +- 시스템을 "동작하게"가 아니라 "신뢰하게" 만드는 데 집중했습니다. share 분모 버그, confidence 이중 스케일, false precision 진단 모두 기능이 동작한 이후에 발견한 구조 문제입니다. 이걸 스스로 찾는 사람은 +드뭅니다. +- 안 만든 이유를 설명할 수 있습니다. atomic_facts 보류, RL 도입 보류 — 1년차가 "지금은 이게 더 위험하다"고 판단하고 기록까지 남기는 건 보기 드문 태도입니다. +- 도메인 지식과 엔지니어링이 같이 성장했습니다. DRI 기준, GRADE 시스템, 임상 인과귀속 — 코드를 짜면서 임상 개념도 함께 쌓았습니다. + +약점 — 솔직히 말하면 + +첫째, "배포해서 사람들이 쓴다"는 증거가 없습니다. +메모리 전체를 봐도 실제 사용자, 실제 추천 결과에 대한 피드백, 비즈니스 지표가 없습니다. QA 116개 시나리오는 내부 검증이고, "1,199편 논문 처리"는 입력 규모이지 출력 품질이 아닙니다. 면접에서 "그래서 추천 +정확도가 얼마나 됩니까?" 라는 질문에 대답하기 어렵습니다. + +둘째, 인상적인 작업 대부분이 "안에서만" 일어납니다. +share 분모 수정, confidence cap, mechanism-only rule 필터링 — 이걸 이해하는 데 시스템 전체를 알아야 합니다. 5분 데모로 보여줄 수 없는 것들입니다. 면접관이 코드를 깊이 읽지 않으면 평가받기 어렵습니다. + +셋째, LLM 파이프라인이 이미 흔해졌습니다. +2025~2026년 기준으로 "논문에서 LLM으로 정보 추출해서 규칙 생성"은 차별점이 되기 어렵습니다. 임상 도메인 깊이가 차별점인데, 그걸 설명하는 데 시간이 걸립니다. + +넷째, 팀 프로젝트인지 솔로인지가 불명확합니다. +"팀원들이 수동 검증 중"이라는 맥락이 있는데, 이 프로젝트에서 본인이 설계·구현한 부분과 팀 기여를 구분해서 설명할 수 있어야 합니다. + +--- + +1. 이 경험이 커리어에 어떤 영향을 주는가 + +유리한 방향: + +- AI/ML 엔지니어링, 데이터 파이프라인 포지션에서 강한 신호입니다 +- 헬스케어·바이오텍 스타트업에서는 도메인 지식이 희소하므로 차별점이 됩니다 +- "코드만 짜는 사람"이 아니라는 것을 보여줄 수 있습니다 + +현실적인 한계: + +- 일반 백엔드/풀스택 포지션 면접에서는 설명 비용이 높습니다 +- 대기업 공채에서는 "실제 서비스에 적용된 경험"을 더 보는 경향이 있어서 불리할 수 있습니다 +- 1년차 시장에서 경쟁자 대부분이 배포된 서비스를 가지고 있다면 상대적으로 약해집니다 + +--- + +결론 + +차별점이 되는 프로젝트입니다. 단, 조건부입니다. + +지금 상태 그대로는 "복잡한 내부가 있는 미완성 프로젝트"로 읽힐 수 있습니다. 아래 두 가지를 추가하면 레벨이 달라집니다. + +1. 배포 + 실사용 데이터: 실제 사용자 N명에게 추천하고 "의사/영양사의 조언과 일치율 X%" 같은 숫자 하나만 있어도 서사가 완성됩니다. +2. 비교 전후: "이 수정 전/후로 추천 품질이 어떻게 달라졌는가"를 정량화하면, 내부 개선 작업들이 모두 근거 있는 작업이 됩니다. + +지금은 "좋은 엔지니어링 판단력을 가진 사람"이라는 걸 보여주는 프로젝트이고, 그걸로도 충분히 유의미합니다. 다만 거기서 한 단계 올라가려면 바깥으로 나갈 숫자가 필요합니다. + +### 회고4 (설계 평가2) + +그리고 원래 이 프로젝트는 비개발자 팀원이 LLM한테 논문 수천개 넣어서 각 성분별 rule이 나오는 걸로 개발을 해놔서 할루시네이션 가능성이 많은 프로젝트였는데 내가 이걸 새로 개발하게 되면서 이런 논문에서 +evidence를 뽑아내는 그런 구조를 생각한거거든. 이것도 포트폴리오가 될수있겠지? 어떤식으로 가져가면 좋을까 이걸 + +● 이게 사실 가장 강력한 포트폴리오 서사입니다. 지금까지 말한 것보다 훨씬 더 중요한 컨텍스트입니다. + +--- + +왜 이게 더 강한가 + +지금까지 정리한 것들은 모두 "좋은 엔지니어링 판단"의 사례입니다. 그런데 이 맥락은 다릅니다. 아키텍처 문제를 진단하고 처음부터 다시 설계한 것입니다. + +원래 구조를 한 줄로 표현하면: + +논문 수천 개 → LLM → 규칙 (블랙박스) + +이 구조의 문제는 단순히 "할루시네이션이 많다"가 아닙니다. 더 근본적인 문제가 있습니다. + +- 어느 논문에서 나온 규칙인지 추적 불가 +- 틀린 규칙이 나와도 왜 틀렸는지 알 수 없음 +- 규칙의 신뢰도를 측정할 방법 없음 +- 논문이 추가되면 전체를 다시 돌려야 함 + +현재 구조는 이걸 이렇게 바꾼 겁니다: + +논문 → Evidence 행 (근거 단위 분해, 출처 추적 가능) +→ 신뢰도 스코어링 (study_type × confidence) +→ 충돌 탐지 (positive vs negative 근거) +→ 집계 → 규칙 (검증 가능, 역추적 가능) + +--- + +이게 실제로 어떤 문제를 푼 건지 + +업계에서 LLM 할루시네이션을 줄이는 접근법은 크게 두 가지입니다. + +RAG (Retrieval Augmented Generation) — "답변할 때 관련 문서를 찾아서 근거로 쓴다" + +이 프로젝트의 접근법 — "LLM이 주장을 만들지 못하게 하고, 논문에서 사실만 뽑아 사람이 집계한다" + +후자가 임상·의료 도메인에서 더 적합합니다. RAG는 여전히 LLM이 최종 문장을 만들기 때문에 할루시네이션이 완전히 사라지지 않습니다. 이 구조는 LLM을 "정보 추출기"로만 쓰고, 규칙을 만드는 건 결정론적 코드가 +합니다. + +--- + +포트폴리오 서사 구성 방법 + +세 파트로 나누면 됩니다. + +Problem (기존 시스템이 왜 위험했는가) + +▎ "LLM에 논문을 직접 넣어 규칙을 생성하는 구조였습니다. 영양제 추천 규칙이 어느 논문에서 나왔는지 알 수 없었고, 규칙의 신뢰도를 측정할 방법이 없었습니다. 의료 도메인에서 출처 없는 규칙은 단순한 버그가 아니라 +▎ 안전 문제입니다." + +Architecture Decision (왜 이 구조를 선택했는가) + +▎ "LLM을 규칙 생성자가 아닌 정보 추출기로만 사용하기로 했습니다. 논문 한 편에서 여러 개의 evidence 행을 뽑고, 각 행마다 논문 ID·페이지·원문 인용을 보존합니다. 규칙 생성은 이 evidence들의 통계적 집계로만 +▎ 이루어져, 출처 역추적이 항상 가능합니다." + +What I learned about LLM reliability (이 작업을 통해 배운 것) + +▎ "LLM 자체의 할루시네이션보다 더 큰 문제는 잘못된 아키텍처였습니다. LLM에게 너무 많은 결정을 맡기면 어디서 오류가 났는지 알 수 없게 됩니다. 각 단계를 분리해서 오류가 발생할 수 있는 레이어를 명시적으로 +▎ 만드는 것이, 단순히 좋은 프롬프트를 쓰는 것보다 훨씬 강력한 신뢰성 보장 방법이라는 걸 깨달았습니다." + +--- + +이 서사가 차별점이 되는 이유 + +1년차 개발자가 포트폴리오에서 말하기 어려운 것 중 하나가 "기존 시스템의 문제를 진단하고 더 나은 구조로 바꿨다" 입니다. 새로 만드는 것보다 기존 것을 비판적으로 보는 게 더 어렵기 때문입니다. + +그리고 이 케이스는 LLM 신뢰성이라는 지금 업계 전체가 고민하는 문제를 다루고 있습니다. "LLM을 어떻게 믿을 수 있게 쓸 것인가"는 2025~2026년 AI 엔지니어링의 핵심 질문입니다. 이 프로젝트의 답이 그 질문에 대한 +하나의 실용적 사례가 됩니다. + +앞서 말한 "배포 + 실사용 데이터" 문제와 합치면, 이 아키텍처 전환 스토리만으로도 포트폴리오의 중심축이 될 수 있습니다. + +### 해야하는것 + +어떤 숫자를 받아야 포트폴리오가 되는가 + +전문가에게 뭘 시키느냐가 중요합니다. 두 가지를 같이 받으면 좋습니다. + +1. 추천 적절성 평가 +QA 116개 시나리오 중 샘플 20~30개를 전문가에게 보여주고 "임상적으로 적절 / 수용 가능 / 부적절"로 평가받습니다. 결과: "전문가 검토 결과 X%가 임상적으로 적절 수준으로 평가됨." +2. 이전 방식과 비교 +현재 구조 이전의 "LLM 직접 생성 방식"으로 같은 케이스에 대해 규칙을 뽑아서 함께 보여주면 됩니다. 지금도 프롬프트를 단순화해서 5~10개 케이스만 돌려봐도 충분합니다. 결과: "기존 방식 대비 부적절 추천 X% 감소." + +2번이 있으면 포트폴리오 서사가 "전/후 비교가 있는 개선 스토리"가 됩니다. 이게 없으면 "좋은 시스템을 만들었다"로 끝나고, 있으면 "나쁜 시스템을 진단하고 좋은 시스템으로 바꿨다"가 됩니다. + +필터 설계는 원칙적으로 되어있음 + +behavior_exposure / covariate_pattern 이면 association 에서 제외라는 게 존재하는데 + +mechanism 타입 근거 **408개 중 368개(90%)**가 claim_subject = substance_intervention 으로 들어가서 association에 들어가는것임 + +성분 → 상태 변화가 association인데 mechanism의 ‘행동→성분 변화’에 대한 케이스도 substance_intervention으로 필터를 통과하게 되는것임 + +→ LLM 분류 품질이 문제인 것으로 확인 + +![{7882603A-FE59-4ACE-89B3-4C3D0A39E05D}.png](attachment:f7d299b3-930e-46e8-bfce-a3e1c3608309:7882603A-FE59-4ACE-89B3-4C3D0A39E05D.png) + +### 프롬프트 수정 필요 + +1. mechanism 판정 기준 추가 +2. behavior_exposure → direction=null 강제하도록 (direction 오염 직접 방어) + 1. 통계적 수치가 있더라도 성분의 보충 효과가 아니니까 direction 기록 금지 +3. behavior_exposure 에시 추가 및 정의 확장 + 1. 현재는 운동/훈련에만 정의가 되어있는데 식이 행동도 추가해야함 + 2. substance_intervention 케이스만 예시가 있기 때문에 이 claim_subject도 예시 필요 +4. target_effect_relation 강도 강화 + 1. ‘보통 unrelated_to_target이 되어야 하며’ ⇒ 보통→반드시로 수정 + 2. behavior_exposure 이나 covariate_pattern 이면 unrelated_to_target이 되도록 +5. 역학 연구 direction 처리 + 1. 역학 조사, 국가 식이 조사, 단면 연구에서 성분이 개입한 경우가 아니면 direction 기록 안하도록 + +![{E422A074-D4E1-402E-A896-1218A9214C8E}.png](attachment:5c3ba664-8b01-4a0e-a429-e2063d5232e2:E422A074-D4E1-402E-A896-1218A9214C8E.png) + +![{E624DB22-05A9-4FF8-8BBB-CCEA45ED6770}.png](attachment:fc45f313-de59-4ed6-9e3c-be892c86dc93:E624DB22-05A9-4FF8-8BBB-CCEA45ED6770.png) + +넌 핵심을 찔렀어! (computational burden 지적) + +### evidence_type 조잡다리 3가지 + +- mechanism : 생물학적 인과 경로 설명. 성분이 원인이든 결과든 경로 안에 있는 거 (association과 함께 rule 생성에 관여함) +- population_note : 특정 집단이 어떤 상태인지 기술. 경로 없이 관찰만 +- context : 연구 배경 정보. 성분 효과와 직접 무관 \ No newline at end of file diff --git a/assignments/latteeea/week3/data/spotify_api.md b/assignments/latteeea/week3/data/spotify_api.md new file mode 100644 index 0000000..4c2dff9 --- /dev/null +++ b/assignments/latteeea/week3/data/spotify_api.md @@ -0,0 +1,22 @@ +## 🗂 오늘 작업 + +spotify API 지원 중지됨 → 배포된 앱에서 에러 및 기능 사용 불가 상태 → 다른 API로 갈아타기 + +## ⚙ 기술 / 개념 + +🐛 트러블슈팅 + +- 문제: 음악 검색을 spotify api 를 사용해서 구현한 상태였는데 배포된 앱들에서 공통적으로 음악 검색이 안되는 문제 발생 +- 원인: 403 에러가 뜨는데 우리쪽에서 spotify 관련 인증 처리하는 건 엑세스 키 및 클라이언트 ID 밖에 없었고 기존에 쓰던 건 무료 버전임을 알고 있었음. + - 우리 코드 문제일리가 없음 → 배포된 앱에서 에러가 난 거기 때문에 + - 백엔드 문제일리도 없음 → 백엔드 코드를 바꾼 적이 없었고 DB 문제라기엔 다른 기능들은 잘 운영되고 있었음 + + ⇒ spotify 문서 찾아보기 + + → 바로 전날에 프리미엄 계정을 가진 클라이언트의 ID여야 API가 사용가능하도록 변경되었고 이게 2월부터 알려졌지만 우리는 메일 같은 걸 받지 못해서 모르는 상황이었던 것 + +- 해결: 제목이나 가수 키워드를 검색해서 그와 관련된 노래를 검색하는 기능을 구현해야했는데 스포티파이 말고 해당 기능을 제공하는 다른 API를 찾아보았고 애플에서 제공하는 아이튠즈 API는 별도의 인증이나 제한 없이 사용 가능해서 이걸로 빨리 교체했다. + +## 🧠 인사이트 + +내가 외부 API를 사용하고 있다면 그 업체에 대한 메일 수신 동의나 SNS를 팔로우 하고 있어야겠다.. 다른 프로젝트에서 아임웹 API도 사용하고 있는데 거기는 메일 수신 동의를 해놨는데 스포티파이는 안해놔서 차마 이 정보를 알지 못했다. 다음부터는 이를 예방하기 위해 팔로우를 잘 하고 있어야겠다.. \ No newline at end of file diff --git a/assignments/latteeea/week3/data/system_font_setting.md b/assignments/latteeea/week3/data/system_font_setting.md new file mode 100644 index 0000000..73874be --- /dev/null +++ b/assignments/latteeea/week3/data/system_font_setting.md @@ -0,0 +1,41 @@ +## 🗂 오늘 작업 + +학산 QA : 시스템 폰트 사이즈 반영하지 않고 앱에서 설정한 크기 그대로 보이기 + +## ⚙ 기술 / 개념 + +🐛 트러블슈팅 + +- 문제: +`Component.defaultProps.allowFontScaling = false;` + - 루트 파일의 해당 설정이 먹지 않음 +- 과정 + - https://github.com/facebook/react-native/issues/51113?utm_source=chatgpt.com → index.js에서 해당 설정이 특정 환경에서 먹히지 않는 케이스 다수 발생 + - 적용 타이밍/엔트리포인트 문제 또는 일부 RN 버전/신 아키텍처 관련 이슈 의심 + - 엔트리포인트 : index.js 생성 후에 메인엔트리를 index.js 로해서 + index.js → expo runtime → expo router 초기화 → app/_layout.tsx + 이렇게 했지만 defaultProps 방식 여전히 안 먹음 + + ```jsx + import { Text, TextInput } from "react-native"; + + Text.defaultProps = Text.defaultProps || {}; + Text.defaultProps.allowFontScaling = false; + Text.defaultProps.maxFontSizeMultiplier = 1; + + TextInput.defaultProps = TextInput.defaultProps || {}; + TextInput.defaultProps.allowFontScaling = false; + TextInput.defaultProps.maxFontSizeMultiplier = 1; + + // 그 다음 expo-router 실행 + import "expo-router/entry"; + ``` + + - 런타임이 아닌 컴파일 타임 치환 방식을 사용하자! (공장에서 조립하기 전에 부품 자체를 바꿔 끼워 생산하는 것) + - Text.defaultProps 는 RN이 가진 Text라는 객체의 속성을 런타임에 수정하는 건데 Text가 아닌 AppText로 코드를 바꾸면 RN 내부 구조나 Fabric 영향 없이 안정적으로 적용할 수 있음 + - AppHeader의 텍스트에서는 시스템 설정에 영향을 받지 않음 (allowFontScaling = false 로 되어 있기 때문에) +- 원인: iOS/Android/Expo 정책 때문이 아니고 엔트리포인트도 아니고 RN 최신 구조에서의 전역 defaultProps 패치가 더 이상 안전하지 않은 것. + + ⇒ 전역 defaultProps로 Text를 막는 방식은 공식적으로 보장되지 않는다. + +- 해결: AppText 래퍼 만들어서 전부 교체하기,, \ No newline at end of file diff --git a/assignments/latteeea/week3/graph.py b/assignments/latteeea/week3/graph.py new file mode 100644 index 0000000..29a0512 --- /dev/null +++ b/assignments/latteeea/week3/graph.py @@ -0,0 +1,39 @@ +from typing import Annotated, TypedDict + +from langchain_core.messages import BaseMessage +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph, START +from langgraph.graph.message import add_messages +from langgraph.prebuilt import ToolNode, tools_condition + +from state import AgentState +from tools import TOOLS +from memory import checkpointer +from nodes import extract_hypotheses_nodes, narrative_interrupt_node + + +llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) +llm_with_tools = llm.bind_tools(TOOLS) + +def agent_node(state: AgentState): + response = llm_with_tools.invoke(state["messages"]) + return {"messages": [response]} + +def build_graph(): + graph = StateGraph(AgentState) + + graph.add_node("extract_hypothesis", extract_hypotheses_nodes) + graph.add_node("narrative_interrupt", narrative_interrupt_node) + graph.add_node("agent", agent_node) + graph.add_node("tools", ToolNode(TOOLS)) + + graph.add_edge(START,"extract_hypothesis") + graph.add_edge("extract_hypothesis", "narrative_interrupt") + graph.add_edge("narrative_interrupt", "agent") + + graph.add_conditional_edges("agent", tools_condition) + graph.add_edge("tools", "agent") + + return graph.compile( + checkpointer=checkpointer + ) \ No newline at end of file diff --git a/assignments/latteeea/week3/nodes.py b/assignments/latteeea/week3/nodes.py new file mode 100644 index 0000000..7f16f50 --- /dev/null +++ b/assignments/latteeea/week3/nodes.py @@ -0,0 +1,73 @@ +from langgraph.types import interrupt +from state import AgentState + + +HYPOTHESES_KEYWORDS = [ + "설계 문제", + "구조 문제", + "boundary 문제", + "단순 버그가 아니다", + "단순 버그", + "경계", + "원인" +] + +def extract_hypotheses_nodes(state): + last_messages = state["messages"][-1] + content = getattr(last_messages, "content","") + + existing = state.get("hypotheses", []) + + matched_keywords = [ + keyword for keyword in HYPOTHESES_KEYWORDS + if keyword.lower() in content.lower() + ] + + if not matched_keywords: + return {} + + new_hypothesis = { + "claim": content, + "type": "user_interpretation", + "matched_keywords": matched_keywords, + "status": "active", + } + + return { + "hypotheses": existing + [new_hypothesis] + } + +def narrative_interrupt_node(state: AgentState): + last_message = state["messages"][-1] + content = getattr(last_message, "content", "") + + if state.get("narrative_intent"): + return {} + + should_interrupt = ( + "글" in content + or "블로그" in content + or "아웃라인" in content + ) + + if not should_interrupt: + return {} + + choice = interrupt({ + "question": "어떤 narrative 방향으로 정리할까요?", + "options": { + "1": "디버깅 흐름 중심", + "2": "구조적 설계 문제 중심", + "3": "운영 장애 리뷰 중심", + } + }) + + intent_map = { + "1": "debugging_flow", + "2": "architectural_problem", + "3": "operation_error_review" + } + + return { + "narrative_intent": intent_map.get(str(choice), "debugging_flow") + } \ No newline at end of file diff --git a/assignments/latteeea/week3/rag/__init__.py b/assignments/latteeea/week3/rag/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/assignments/latteeea/week3/rag/embaddings.py b/assignments/latteeea/week3/rag/embaddings.py new file mode 100644 index 0000000..e69de29 diff --git a/assignments/latteeea/week3/rag/indexing.py b/assignments/latteeea/week3/rag/indexing.py new file mode 100644 index 0000000..caca04a --- /dev/null +++ b/assignments/latteeea/week3/rag/indexing.py @@ -0,0 +1,51 @@ +from loader import load_md_documents +from splitter import split_recursive, split_md_header + +from vectorstore import ( + build_faiss_vectorstore, + save_faiss_vectorstore, + reset_faiss_index, +) + +RECURSIVE_INDEX_DIR = "./cache/faiss_recursive" +MARKDOWN_INDEX_DIR = "./cache/faiss_markdown" + +def build_recursive_index(): + docs = load_md_documents("data") + + chunks = split_recursive(docs) + + print(f"[Recursive] chunk count: {len(chunks)}") + + vectorstore = build_faiss_vectorstore(chunks) + + reset_faiss_index(RECURSIVE_INDEX_DIR) + + save_faiss_vectorstore( + vectorstore, + RECURSIVE_INDEX_DIR, + ) + + print(f"Saved recursive index -> {RECURSIVE_INDEX_DIR}") + +def build_markdown_index(): + docs = load_md_documents("data") + + chunks = split_md_header(docs) + + print(f"[Markdown Header] chunk count: {len(chunks)}") + + vectorstore = build_faiss_vectorstore(chunks) + + reset_faiss_index(MARKDOWN_INDEX_DIR) + + save_faiss_vectorstore( + vectorstore, + MARKDOWN_INDEX_DIR, + ) + + print(f"Saved markdown index -> {MARKDOWN_INDEX_DIR}") + +if __name__ == "__main__": + build_recursive_index() + build_markdown_index() \ No newline at end of file diff --git a/assignments/latteeea/week3/rag/loader.py b/assignments/latteeea/week3/rag/loader.py new file mode 100644 index 0000000..30c5f8a --- /dev/null +++ b/assignments/latteeea/week3/rag/loader.py @@ -0,0 +1,39 @@ +from pathlib import Path +from langchain_community.document_loaders import TextLoader +from langchain_core.documents import Document + +def load_md_documents(data_dir: str = "data") -> list[Document]: + """ + week3/data 폴더 아래의 모든 .md 파일을 로드합니다. + """ + base_path = Path(data_dir) + + if not base_path.exists(): + raise FileNotFoundError(f"Data directory not found: {data_dir}") + + docs: list[Document] = [] + + for file_path in base_path.rglob("*.md"): + loader = TextLoader(str(file_path), encoding="utf-8") + loaded_docs = loader.load() + + for doc in loaded_docs: + doc.metadata.update( + { + "source": str(file_path), + "filename": file_path.name, + "stem": file_path.stem, + } + ) + docs.append(doc) + + return docs + +if __name__ == "__main__": + documents = load_md_documents() + print(f"Loaded {len(documents)} documents") + + for doc in documents[:3]: + print("-" * 50) + print(doc.metadata) + print(doc.page_content[:400]) \ No newline at end of file diff --git a/assignments/latteeea/week3/rag/qa.py b/assignments/latteeea/week3/rag/qa.py new file mode 100644 index 0000000..12fec09 --- /dev/null +++ b/assignments/latteeea/week3/rag/qa.py @@ -0,0 +1,106 @@ +from dotenv import load_dotenv +from langchain_openai import ChatOpenAI +from langchain_core.prompts import ChatPromptTemplate + +from retriever import retrieve + +load_dotenv() + +def format_docs(docs): + formatted = [] + + for i, doc in enumerate(docs, start=1): + source = doc.metadata.get("source", "unknown") + chunk_id = doc.metadata.get("chunk_id", "unknown") + + formatted.append( + f"[문서 {i}]\n" + f"source: {source}\n" + f"chunk_id: {chunk_id}\n" + f"content:\n{doc.page_content}" + ) + + return "\n\n".join(formatted) + +def answer_question( + question: str, + strategy: str = "markdown", + k: int = 4, +) -> dict: + docs = retrieve(query=question, strategy=strategy, k=k) + context = format_docs(docs) + + llm = ChatOpenAI(model="gpt-4o-mini", temperature = 0) + prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + """ + 너는 사용자의 기술 트러블슈팅 기록을 기반으로 답변하는 RAG assistant 이다. + + 규칙: + - 제공된 context 안의 내용을 근거고 대답하되, 중간 단계가 비어있는 경우 적당히 추측해서 채워넣는다. 이때 추측한 내용은 [추측] 이라고 무조건 표시한다. + - 답변에는 문제 상황, 해결, 인사이트를 가능한 한 구분해서 정리한다. + - 마지막에 참고 문서 source를 반드시 포함한다. + + """, + ), + ( + "human", + """ + 질문: {question} + + 검색된 context: {context} + """, + ), + ] + ) + + chain = prompt | llm + response = chain.invoke( + { + "question": question, + "context": context, + } + ) + + sources = [ + { + "source": doc.metadata.get("source"), + "filename": doc.metadata.get("filename"), + "chunk_id": doc.metadata.get("chunk_id"), + "strategy": doc.metadata.get("chunk_strategy"), + } + for doc in docs + ] + + return { + "question": question, + "strategy": strategy, + "answer": response.content, + "sources": sources, + } + +def print_qa_result(result: dict): + print("=" * 100) + print(f"Question: {result['question']}") + print(f"Strategy: {result['strategy']}") + print("-" * 100) + print(result["answer"]) + print("-" * 100) + print("Sources:") + for source in result["sources"]: + print( + f"- {source['filename']} | " + f"{source['chunk_id']} | " + f"{source['strategy']}" + ) + +if __name__ == "__main__": + result = answer_question( + question="외부 API 변경으로 장애가 발생한 사례를 원인과 해결 중심으로 정리해줘", + strategy="markdown", + k=4, + ) + + print_qa_result(result) \ No newline at end of file diff --git a/assignments/latteeea/week3/rag/retriever.py b/assignments/latteeea/week3/rag/retriever.py new file mode 100644 index 0000000..e5ee94e --- /dev/null +++ b/assignments/latteeea/week3/rag/retriever.py @@ -0,0 +1,40 @@ +from vectorstore import load_faiss_vectorstore + +RECURSIVE_INDEX_DIR = "./cache/faiss_recursive" +MARKDOWN_INDEX_DIR = "./cache/faiss_markdown" + +def get_retriever(strategy: str = "recursive", k: int = 4): + if strategy == "recursive": + vectorstore = load_faiss_vectorstore(RECURSIVE_INDEX_DIR) + elif strategy == "markdown": + vectorstore = load_faiss_vectorstore(MARKDOWN_INDEX_DIR) + else: + raise ValueError("strategy must be either 'recursive" or 'markdown') + + return vectorstore.as_retriever( + search_kwargs={"k": k} + ) + +def retrieve(query: str, strategy: str = "recursive", k: int = 4): + retriever = get_retriever(strategy=strategy, k=k) + return retriever.invoke(query) + +def print_results(query: str, strategy: str = "recursive", k: int = 4): + docs = retrieve(query = query, strategy=strategy, k=k) + + print(f"\nQuery: {query}") + print(f"Strategy: {strategy}") + print(f"Result count: {len(docs)}") + + for i, doc in enumerate(docs, start=1): + print("=" * 80) + print(f"[{i}] source={doc.metadata.get('source')}") + print(f"filename={doc.metadata.get('filename')}") + print(f"chunk_id={doc.metadata.get('chunk_id')}") + print(doc.page_content[:1000]) + +if __name__ == "__main__": + test_query = "외부 API 변경으로 장애가 발생한 사례" + + print_results(test_query, strategy="recursive", k=3) + print_results(test_query, strategy="markdown",k=3) \ No newline at end of file diff --git a/assignments/latteeea/week3/rag/splitter.py b/assignments/latteeea/week3/rag/splitter.py new file mode 100644 index 0000000..e5b0a79 --- /dev/null +++ b/assignments/latteeea/week3/rag/splitter.py @@ -0,0 +1,84 @@ +from langchain_text_splitters import RecursiveCharacterTextSplitter, MarkdownHeaderTextSplitter +from langchain_core.documents import Document + +def split_recursive( + docs: list[Document], + chunk_size: int = 700, + chunk_overlap: int = 100, +) -> list[Document]: + """ + 일반적인 문자 기반 chunking으로, 문맥이 길게 이어진 문서를 일정 길이로 자르기 + """ + + splitter = RecursiveCharacterTextSplitter( + chunk_size = chunk_size, + chunk_overlap = chunk_overlap, + separators=["\n\n", "\n", " ", ""], + ) + + chunks = splitter.split_documents(docs) + + for i, chunk in enumerate(chunks): + chunk.metadata.update( + { + "chunk_strategy": "recursive", + "chunk_id": f"recursive-{i}", + } + ) + + return chunks + +def split_md_header(docs: list[Document]) -> list[Document]: + """ + md header 기준 chunking. 해결하려는 문제를 적은 경우가 있어서 구조 보존용. + """ + + headers_to_split_on = [ + ("#", "h1"), + ("##", "h2"), + ("###", "h3"), + ] + + splitter = MarkdownHeaderTextSplitter( + headers_to_split_on=headers_to_split_on, + strip_headers=False, + ) + + chunks: list[Document] = [] + + for doc in docs: + split_docs = splitter.split_text(doc.page_content) + + for idx, split_doc in enumerate(split_docs): + split_doc.metadata.update(doc.metadata) + split_doc.metadata.update( + { + "chunk_strategy": "md_header", + "chunk_id": f"{doc.metadata.get('stem', 'doc')}-header-{idx}", + } + ) + chunks.append(split_doc) + + return chunks + +def preview_chunks(chunks: list[Document], limit: int = 5) -> None: + print(f"Total chunk: {len(chunks)}") + + for chunk in chunks[:limit]: + print("=" * 80) + print(chunk.metadata) + print(chunk.page_content[:500]) + +if __name__ == "__main__": + from loader import load_md_documents + + docs = load_md_documents("data") + + recursive_chunks = split_recursive(docs) + md_chunks = split_md_header(docs) + + print("\n[Recursive chunking]") + preview_chunks(recursive_chunks, limit=3) + + print("\n[Markdown Header chunking]") + preview_chunks(md_chunks, limit=3) \ No newline at end of file diff --git a/assignments/latteeea/week3/rag/vectorstore.py b/assignments/latteeea/week3/rag/vectorstore.py new file mode 100644 index 0000000..a2cc7ab --- /dev/null +++ b/assignments/latteeea/week3/rag/vectorstore.py @@ -0,0 +1,56 @@ +from pathlib import Path +import shutil + +from dotenv import load_dotenv + +from langchain_community.vectorstores import FAISS +from langchain_openai import OpenAIEmbeddings +from langchain_core.documents import Document + +load_dotenv() + +def get_embedding_model() -> OpenAIEmbeddings: + return OpenAIEmbeddings(model="text-embedding-3-small") + +def build_faiss_vectorstore(chunks: list[Document]) -> FAISS: + """ + chunks 리스트를 embedding해서 FAISS vector store를 생성한다. + """ + embeddings = get_embedding_model() + + vectorstore = FAISS.from_documents( + documents=chunks, + embedding=embeddings, + ) + + return vectorstore + +def save_faiss_vectorstore(vectorstore: FAISS, index_dir: str) -> None: + """ + FAISS index를 로컬 디렉토리에 저장한다. + """ + path = Path(index_dir) + path.mkdir(parents=True, exist_ok=True) + + vectorstore.save_local(str(path)) + +def load_faiss_vectorstore(index_dir: str) -> None: + """ + 저장된 FAISS index 로드하기 + """ + embeddings = get_embedding_model() + + return FAISS.load_local( + folder_path=index_dir, + embeddings=embeddings, + allow_dangerous_deserialization=True, + ) + +def reset_faiss_index(index_dir: str) -> None: + """ + 기존 FAISS index 디렉토리 삭제 + """ + path = Path(index_dir) + + if path.exists(): + shutil.rmtree(path) \ No newline at end of file diff --git a/assignments/latteeea/week3/run.py b/assignments/latteeea/week3/run.py new file mode 100644 index 0000000..afa9aad --- /dev/null +++ b/assignments/latteeea/week3/run.py @@ -0,0 +1,44 @@ +from dotenv import load_dotenv + +load_dotenv(override=True) + +from langchain_core.messages import HumanMessage +from graph import build_graph +from langgraph.types import Command + + +app = build_graph() + +config = { + "configurable": { + "thread_id" : "week2-interrupt-thread" + } +} + +inputs = [ + "long polling 관련 사례를 글로 만들어줘", +] + +for text in inputs: + result = app.invoke( + {"messages": [HumanMessage(content=text)]}, + config = config, + ) + + print("\n--- latteeea ---") + print(text) + + print("\n--- AI ---") + print(result["messages"][-1]) + +result = app.invoke( + Command(resume="2"), + config = config, +) + +print("\n--- INTERRUPT TEST ---") +print("Agent asked for narrative direction.") +print("Resume with choice: 2") + +print(result["messages"][-1]) +print("narrative_intent:", result.get("narrative_intent")) \ No newline at end of file diff --git a/assignments/latteeea/week3/schemas.py b/assignments/latteeea/week3/schemas.py new file mode 100644 index 0000000..4009ef6 --- /dev/null +++ b/assignments/latteeea/week3/schemas.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel, Field + +class AgentResponse(BaseModel): + answer: str = Field(description="질문에 대한 에이전트 답변") + used_tools: list[str] = Field(description="응답 생성 시 사용한 tool 목록") + evidence: list[str] = Field(description="답변 근거가 된 노트(노션)") + confidence: list[str] = Field(description="0.0~1.0 사이의 llm이 부여한 신뢰도") \ No newline at end of file diff --git a/assignments/latteeea/week3/state.py b/assignments/latteeea/week3/state.py new file mode 100644 index 0000000..700dfb7 --- /dev/null +++ b/assignments/latteeea/week3/state.py @@ -0,0 +1,14 @@ +from typing import Annotated, TypedDict +from langgraph.graph.message import add_messages +from langchain_core.messages import BaseMessage + +class AgentState(TypedDict): + messages: Annotated[list[BaseMessage], add_messages] + + active_note_id: str | None + current_topic: str | None + + hypotheses: list[dict] + + narrative_intent: str | None + evidence_plan: dict | None \ No newline at end of file diff --git a/assignments/latteeea/week3/tests/chunk_compare.py b/assignments/latteeea/week3/tests/chunk_compare.py new file mode 100644 index 0000000..320059d --- /dev/null +++ b/assignments/latteeea/week3/tests/chunk_compare.py @@ -0,0 +1,54 @@ +# tests/chunk_compare.py + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +RAG_DIR = ROOT_DIR / "rag" +sys.path.append(str(RAG_DIR)) + +from retriever import retrieve + + +COMPARE_QUERIES = [ + "외부 API 변경으로 장애가 발생한 사례", + "사용자 인증 정보 불일치로 문제가 발생한 사례", + "수동 검토와 자동 검증을 함께 사용한 사례", + "안드로이드 빌드 시 배포 에러가 발생한 사례", + "long polling 으로 인한 서버 다운 사례" +] + + +def summarize_doc(doc): + return { + "source": doc.metadata.get("source"), + "filename": doc.metadata.get("filename"), + "chunk_id": doc.metadata.get("chunk_id"), + "preview": doc.page_content[:120].replace("\n", " "), + } + + +def compare_query(query: str, k: int = 3): + recursive_docs = retrieve(query=query, strategy="recursive", k=k) + markdown_docs = retrieve(query=query, strategy="markdown", k=k) + + print("=" * 120) + print(f"QUERY: {query}") + print("=" * 120) + + print("\n[Recursive Chunking Results]") + for i, doc in enumerate(recursive_docs, start=1): + item = summarize_doc(doc) + print(f"{i}. {item['filename']} | {item['chunk_id']}") + print(f" {item['preview']}") + + print("\n[Markdown Header Chunking Results]") + for i, doc in enumerate(markdown_docs, start=1): + item = summarize_doc(doc) + print(f"{i}. {item['filename']} | {item['chunk_id']}") + print(f" {item['preview']}") + + +if __name__ == "__main__": + for query in COMPARE_QUERIES: + compare_query(query, k=3) \ No newline at end of file diff --git a/assignments/latteeea/week3/tools.py b/assignments/latteeea/week3/tools.py new file mode 100644 index 0000000..da3a949 --- /dev/null +++ b/assignments/latteeea/week3/tools.py @@ -0,0 +1,127 @@ +from langchain_core.tools import tool +from typing import Optional +from mock_data import TROUBLESHOOTING_NOTES + +NOTE_BY_ID = {note["id"]: note for note in TROUBLESHOOTING_NOTES} + +def _tokenize(query: str) -> list[str]: + return [token.lower() for token in query.split() if token.strip()] + +@tool +def search_notes( + query: str, + category: Optional[str] = None, + min_blog_value: Optional[int] = None, +) -> list[dict]: + """keyword를 바탕으로 트러블슈팅 노트를 찾는다.""" + + query_lower = query.lower() + results = [] + + for note in TROUBLESHOOTING_NOTES: + if category and note["category"] != category: + continue + + if min_blog_value and note["blog_value"] < min_blog_value: + continue + + searchable_fields = { + "title": note.get("title", ""), + "category": note.get("category", ""), + "tech_stack": " ".join(note.get("tech_stack", [])), + "symptom": note.get("symptom", ""), + "root_cause": note.get("root_cause", ""), + "content_angle": note.get("content_angle", ""), + "structural_insight": note.get("structural_insight", ""), + } + + query_tokens = _tokenize(query) + + matched_fields = [ + field for field, value in searchable_fields.items() + if any(token in value.lower() for token in query_tokens) + ] + + if matched_fields: + results.append({ + "id": note["id"], + "title": note["title"], + "category": note["category"], + "matched_fields": matched_fields, + "summary": note["structural_insight"], + "blog_value": note["blog_value"], + }) + + return results + +@tool +def get_note_detail(note_id: str) -> dict: + """note_id를 가지고 트러블슈팅 노트의 자세한 정보를 가져오기""" + + note = NOTE_BY_ID.get(note_id) + + if not note: + return { + "error": f"Note not found: {note_id}", + "available_ids": list(NOTE_BY_ID.keys()), + } + + return { + "id": note["id"], + "title": note["title"], + "timeline": note["timeline"], + "investigation_steps": note["investigation_steps"], + "root_cause": note["root_cause"], + "solution": note["solution"], + "structural_insight": note["structural_insight"], + "evidence": note["evidence"], + "content_angle": note["content_angle"], + "blog_value": note["blog_value"], + } + +@tool +def generate_narrative_outline( + note_id: str, + target_reader: str = "junior_backend_engineer" +) -> dict: + """트러블 슈팅 노트에서 블로그 스타일의 narrative 아웃라인을 생성하기""" + + note = NOTE_BY_ID.get(note_id) + + if not note: + return { + "error": f"Note not found: {note_id}", + "available_ids": list(NOTE_BY_ID.keys()), + } + + wrong_or_partial_hypothesis = [ + step["hypothesis"] + for step in note["investigation_steps"] + if step["result"] in ["rejected", "'partial"] + ] + + confirmed_finidings = [ + step["observation"] + for step in note["investigation_steps"] + if step["result"] == "confirmed" + ] + + return { + "note_id": note["id"], + "target_reader": target_reader, + "title": note["title"], + "hook": note["symptom"], + "problem_context": note["impact"], + "debugging_flow": { + "timeline": note["timeline"], + "wrong_or_partial_hypothesis": wrong_or_partial_hypothesis, + "confirmed_findings": confirmed_finidings, + }, + "root_cause_section": note["root_cause"], + "solution_section": note["solution"], + "lessons": note["structural_insight"], + "closing": note["content_angle"], + "evidence": note["evidence"], + } + +TOOLS = [search_notes, get_note_detail, generate_narrative_outline] \ No newline at end of file