diff --git a/journey/README.md b/journey/README.md index 700020f..7a59028 100644 --- a/journey/README.md +++ b/journey/README.md @@ -64,6 +64,10 @@ - 로빙 에이전트 루프·스킬 훅·LLM 실행 구조 리서치 – `research/orchestration_tools/260312_로빙_에이전트루프_스킬훅_LLM실행구조_리서치.md` - 로빙 LLM API·Agent API·모델 선정·비용 비교 리서치 – `research/orchestration_tools/260312_로빙_LLM_API_Agent_API_모델선정_비용비교_리서치.md` +### LLM 출력 계약 / 안정화 + +- Pydantic AI 도입 기반 LLM 출력 안정화 종료 리서치 – `research/260313_pydantic_ai_도입_기반_llm_출력_안정화_종료_리서치.md` + ### HITL / 리뷰 큐 - Human-in-the-Loop Intent Learning 아키텍처 – `../300_architecture/390_human_in_the_loop_intent_learning.md` diff --git a/journey/ideas/260313_pydantic_ai_도입_기반_llm_출력_안정화_아이디어.md b/journey/ideas/260313_pydantic_ai_도입_기반_llm_출력_안정화_아이디어.md new file mode 100644 index 0000000..bed14e6 --- /dev/null +++ b/journey/ideas/260313_pydantic_ai_도입_기반_llm_출력_안정화_아이디어.md @@ -0,0 +1,109 @@ +tags: [ideas, pydantic-ai, langgraph, llm, structured-output, ir-analysis] + +# Pydantic AI 도입 기반 LLM 출력 안정화 아이디어 + +## 아이디어 한 줄 +- `LangGraph`는 유지하고, 핵심 구조화 출력 경로에는 우선 `Pydantic` 기반 typed validation을 도입하고, 필요 시 다음 단계에서 `Pydantic AI`까지 확장하자는 아이디어입니다. + +## 왜 이 아이디어를 떠올렸는가 +- 현재 일부 평가 경로는 LLM이 반환한 구조화 결과를 거의 그대로 신뢰하고 있습니다. +- 오늘 IR Deck 분석에서는 회사소개서 자체는 제대로 선택됐지만, LLM이 `total_score=100`, `grade=B`처럼 서로 맞지 않는 값을 함께 반환했고, 이 값이 그대로 사용자에게 노출됐습니다. +- 그래서 지금 필요한 것은 워크플로우를 갈아엎는 것보다, `Pydantic` 계열 typed output 계층을 붙여 출력 계약을 더 강하게 만드는 방향이라고 봤습니다. + +## 이 아이디어가 겨냥하는 현재 상태 +- 지금 구조에서는 LLM이 JSON 비슷한 출력을 하면 파싱 후 일부 필드만 범위 제한하고 저장합니다. +- 그 결과: + - 점수와 등급의 정합성이 깨져도 통과할 수 있습니다. + - `story_scores` 평균과 `total_score`가 맞지 않아도 그대로 저장될 수 있습니다. + - 사용자 메시지, DB 저장, 후속 의사결정이 모두 흔들릴 수 있습니다. +- 즉 현재 문제는 "LLM이 들쭉날쭉하다"보다, "들쭉날쭉한 출력을 막는 typed validation 계층이 약하다"에 가깝습니다. + +## 핵심 아이디어 +- `LangGraph`는 유지하되, LLM 출력이 중요한 구간에는 `Pydantic` 기반 typed validation 계층을 먼저 부분 도입합니다. +- 이후 retry, output validator, agent화가 더 필요해지면 `Pydantic AI`로 확장할 수 있게 경계를 잡습니다. +- 목표는 워크플로우를 갈아엎는 것이 아니라, **LLM이 반환하는 평가/분류/판정 결과를 검증 가능한 모델로 강제**하는 것입니다. +- 즉 이 아이디어의 중심 문장은 `Pydantic 계열 typed validation을 안정화 계층으로 도입하자`입니다. + +## 왜 하필 Pydantic 계열인가 + +### 1. 지금 문제는 워크플로우보다 출력 계약 문제다 +- 콜드메일처럼 `중단/재개`, `체크포인트`, `사람 확인 후 계속 진행`이 필요한 경로는 여전히 LangGraph가 잘 맞습니다. +- 하지만 IR 평가처럼 "점수/등급/요약/추천"을 구조화해서 내야 하는 경로는 LangGraph보다 출력 모델 검증이 더 중요합니다. + +### 2. LangGraph는 괜찮지만, 복잡한 지점이 분명히 있다 +- 장점: + - 상태 저장 + - interrupt / resume + - 장기 실행 흐름 + - 사람 확인 후 재개 같은 운영 흐름 +- 복잡한 점: + - 단순 구조화 응답 문제까지 워크플로우 문제처럼 보이게 만들 수 있습니다. + - 출력 검증이 약하면, 그래프는 잘 돌아도 잘못된 값이 오래 살아남습니다. +- 따라서 지금 문제를 보고 `LangGraph가 나쁘다`고 결론내리는 것은 과도합니다. +- 더 정확한 해석은 `LangGraph는 유지할 가치가 있지만, typed output 안정화 계층이 별도로 필요하다`입니다. + +## Pydantic 계열을 쓰면 기대할 수 있는 것 + +### 1. 출력 모델 강제 +- 예: `IRDeckEvaluationResult` 같은 모델로 아래를 강제할 수 있습니다. + - `total_score: int` + - `grade: Literal["S", "A", "B", "C"]` + - `story_scores: list[...]` + - `summary: str` + - `investment_opinion: ...` +- 구조가 맞지 않으면 바로 실패시킬 수 있습니다. + +### 2. 후처리 검증을 명시적으로 둘 수 있음 +- 예: + - `total_score`와 `grade` 정합성 검증 + - `story_scores` 평균과 `total_score` 차이 제한 + - 필수 필드 길이/개수 검증 +- 즉 "파싱만 성공하면 통과"가 아니라 "의미적으로도 맞아야 통과"가 가능합니다. + +### 3. 실패를 성공처럼 포장하지 않기 쉬움 +- 지금처럼 `100점인데 B등급` 같은 모순을 조용히 저장하는 대신, + - 재질의 + - 재계산 + - 실패 가시화 + 중 하나로 강제할 수 있습니다. + +## 이 아이디어의 권장 방향 + +### 1. LangGraph 전면 교체는 하지 않는다 +- 콜드메일, 브리핑, 인터럽트 워크플로우는 LangGraph가 계속 필요합니다. +- 장기 상태/체크포인트/사람 확인 흐름은 LangGraph의 실제 강점입니다. + +### 2. 1차는 `Pydantic-only`, 2차 후보는 `Pydantic AI`로 둔다 +- 우선 후보: + - IR Deck 종합 평가 출력 + - 브리핑 인사이트 한 줄 출력 + - 콜드메일 후보 판정 이유 요약 + - 투자 의견 구조화 출력 +- 즉 LLM이 말만 잘하면 되는 경로보다, **출력값이 제품 데이터가 되는 경로**에 먼저 붙입니다. +- 첫 단계는 기존 `call_llm()` 뒤에 `Pydantic model_validate()`와 semantic validator를 붙이는 방식이 가장 현실적입니다. +- 이후 재질의, output validator, retry를 경로 내부에 더 강하게 묶고 싶을 때 `Pydantic AI`로 확장합니다. + +### 3. 점수는 LLM에게 전부 맡기지 않는다 +- 이상적인 방향은: + - LLM은 `story_scores`, 요약, 강점/약점 같은 해석을 반환 + - `total_score`, `grade`, `recommendation` 일부는 서버가 재계산 또는 재검증 +- 이렇게 하면 LLM의 자유도는 유지하면서도 최종 사용자 값은 더 안정화할 수 있습니다. + +## 이 아이디어가 답하려는 질문 +1. `Pydantic-only`만으로도 현재 모순값 차단을 시작할 수 있는가? +2. `LangGraph`는 유지하면서 `Pydantic` 계열로 출력 안정화 계층만 강화할 수 있는가? +3. `Pydantic-only`로 시작한 뒤 필요하면 `Pydantic AI`로 확장하는 순서가 맞는가? + +## 아직 아이디어 단계인 이유 +- 실제로 `Pydantic-only`와 `Pydantic AI`의 경계를 어디까지 둘지 범위가 정해지지 않았습니다. +- 기존 FastAPI/Pydantic 모델과 어떤 경계로 겹칠지 설계가 필요합니다. +- 검증 실패 시 재질의, 서버 재계산, 실패 노출 중 어떤 정책을 기본으로 할지 아직 정하지 않았습니다. +- 비용과 지연 시간도 실제 경로에서 측정이 필요합니다. + +## 바로 실행 결론으로 넘기지 않은 이유 +- 이번 문서는 `Pydantic 계열 typed validation을 부분 도입하자`는 방향을 여는 아이디어 문서입니다. +- 다만 아이디어 단계에서는 `도입 범위`, `도입 경계`, `실패 정책`이 아직 열려 있으므로 바로 실행 결론으로 넘기지 않았습니다. +- 실제 계획에서는 `어느 경로부터`, `어떤 필드부터`, `어떤 실패 정책으로` 도입할지를 좁혀야 합니다. + +## 한 줄 결론 +- 방향은 `LangGraph 유지 + Pydantic-only 먼저`입니다. 현재 출력 검증이 약한 경로에 typed validation 계층을 먼저 붙이고, 필요하면 다음 단계에서 `Pydantic AI`로 확장하는 것이 이 아이디어의 핵심입니다. diff --git a/journey/plans/260313_pydantic_ai_부분도입_llm_출력안정화_계획.md b/journey/plans/260313_pydantic_ai_부분도입_llm_출력안정화_계획.md new file mode 100644 index 0000000..73e17ee --- /dev/null +++ b/journey/plans/260313_pydantic_ai_부분도입_llm_출력안정화_계획.md @@ -0,0 +1,179 @@ +--- +tags: [plans, pydantic-ai, langgraph, llm, structured-output, ir-analysis] +--- + +# Pydantic AI 부분 도입 LLM 출력안정화 계획 + +**작성일**: 2026-03-13 +**상태**: planned +**목표**: `LangGraph`는 유지하면서 `IR Deck` 종합 평가 경로에 우선 `Pydantic-only` 기반 typed validation 계층을 도입해, 모순된 점수/등급/추천이 저장·노출되지 않게 합니다. + +## 관련 문서 +- [Pydantic AI 도입 기반 LLM 출력 안정화 아이디어](../ideas/260313_pydantic_ai_도입_기반_llm_출력_안정화_아이디어.md) +- [Pydantic AI 도입 기반 LLM 출력 안정화 종료 리서치](../research/260313_pydantic_ai_도입_기반_llm_출력_안정화_종료_리서치.md) +- [콜드메일 IR 분석이 첫 번째 PDF를 잘못 선택하는 상태](../debug/260313_coldmail_ir_분석대상_오선택_디버그.md) + +## 1. 이번 계획의 결정 +- 1차는 `Pydantic-only`로 갑니다. +- `Pydantic AI`는 버리지 않되, 후속 2차 후보로 둡니다. +- 전면 교체는 하지 않고, `IR Deck 종합 평가` 경로에만 부분 도입합니다. +- `LangGraph` 워크플로는 유지합니다. +- 첫 단계에서는 `raw output -> Pydantic model_validate -> semantic validation -> final output -> save` 경계를 추가합니다. + +## 2. 왜 지금 이 계획으로 가는가 +- 실측 기준으로 현재 `_evaluate_comprehensive()`는 `total_score=100`, `grade=B` 같은 모순값을 그대로 통과시킵니다. +- 실측 기준으로 `Pydantic` validator만으로도 같은 입력을 차단했습니다. +- 따라서 지금 필요한 것은 `Pydantic AI`까지 바로 올리는 것보다, 먼저 가장 작은 변경으로 `IR Deck` 종합 평가 출력에 검증 경계를 추가하는 것입니다. + +## 3. 범위 +- 포함: + - `rb8001/app/services/ir_deck_analyzer.py` 종합 평가 출력 경로 + - 필요 시 `rb8001/app/services/ir_deck_output_models.py` 또는 동등한 전용 모델 파일 추가 + - `rb8001/tests/test_ir_deck_json_parsing.py` 보강 + - `rb8001/tests/test_ir_deck_workflow.py` 보강 + - `Pydantic` typed output 모델 및 validator 추가 + - 저장 전 `final_evaluation_result` 생성 + - 모순 출력 테스트 추가 +- 제외: + - `LangGraph` 제거 또는 재설계 + - `Pydantic AI` 의존성 즉시 추가 + - 브리핑, 콜드메일, 투자의견 전 경로 동시 적용 + - 운영용 공통 출력 정책 프레임워크 전면화 + +## 4. 구현 원칙 +- `LangGraph`는 상태 관리 계층으로 유지합니다. +- `Pydantic-only`는 종합 평가 결과 검증 계층으로만 붙입니다. +- `raw`와 `final` 결과를 분리합니다. +- `grade`, `recommendation`은 서버가 최종 소유합니다. +- 검증 실패는 성공처럼 저장하지 않습니다. + +## 5. 구현 단계 + +### A. 의존성 추가 +- 추가 의존성 없이 현재 `pydantic` 자산을 먼저 사용합니다. +- 첫 적용 범위는 `rb8001` 한 경로로 제한합니다. + +### B. 출력 모델 추가 +- 권장 위치: + - 1순위: `rb8001/app/services/ir_deck_output_models.py` + - 대안: `rb8001/app/services/ir_deck_analyzer.py` 내부 보조 모델 +- 이유: + - validator 로직과 LLM 호출 로직을 분리해 테스트 가능성을 높이기 위함입니다. +- `IRDeckEvaluationResult`에 해당하는 typed output 모델을 추가합니다. +- 최소 필드: + - `total_score` + - `grade` + - `story_scores` + - `summary` + - `strengths` + - `weaknesses` + - `risks` + - `investment_opinion` +- 함께 둘 보조 모델: + - `StoryScore` + - `InvestmentOpinion` +- validator 권장 항목: + - `grade == assign_grade(total_score)` + - `summary` 최소 길이 + - `story_scores` 개수/범위 + - `recommendation` 허용값 또는 서버 재계산 대상 표시 + +### C. `Pydantic-only` 검증 경로 추가 +- `_evaluate_comprehensive()` 내부에서 기존 자유형 JSON 파싱 뒤 `Pydantic model_validate()`를 수행합니다. +- model validator에서 의미 검증을 수행합니다. +- 필요 시 검증 함수는 `parse_raw_evaluation_result()`와 `finalize_evaluation_result()`로 분리합니다. +- 권장 함수 분리: + - `_extract_raw_evaluation_dict(response: str) -> dict` + - `_validate_evaluation_result(raw: dict) -> IRDeckEvaluationResult` + - `_finalize_evaluation_result(validated: IRDeckEvaluationResult) -> dict` +- 이유: + - 재질의 정책과 validator 테스트를 각각 독립적으로 검증하기 위함입니다. + +### D. 서버 최종값 재계산 +- `grade`는 `total_score` 기반으로 서버가 다시 계산합니다. +- `recommendation`도 서버 정책으로 다시 계산합니다. +- 필요 시 `story_scores` 기반 총점 재계산 또는 허용 오차 검증을 추가합니다. +- 권장 고정안: + - 1차에서는 `grade`, `recommendation`만 서버 최종값으로 고정 + - `total_score`는 우선 validator 통과값을 사용하되, 후속 단계에서 재계산 여부 검토 +- 이유: + - 변경 범위를 줄이면서도 사용자에게 보이는 핵심 모순을 먼저 닫기 위함입니다. + +### E. 저장 경계 분리 +- 저장은 `raw_evaluation_result`가 아니라 `final_evaluation_result`만 사용합니다. +- 검증 실패 시 저장하지 않거나, 실패 상태를 명시적으로 반환합니다. +- 권장 구현: + - raw dict는 지역 변수로만 유지 + - repository에는 `final_evaluation_result`에서 꺼낸 값만 전달 + - 실패 시 `ValueError` 또는 명시적 커스텀 예외를 발생시켜 상위 경로에서 실패 처리 + +### E-1. 라우터/워크플로 영향 범위 확인 +- 확인 대상: + - `rb8001/app/services/workflows/ir_deck_workflow.py` + - `rb8001/app/router/ir_deck.py` +- 확인 항목: + - `_evaluate_comprehensive()` 반환 구조가 바뀌어도 기존 `evaluate_node`, `save_node`, `EvaluationResponse`가 깨지지 않는지 + - 실패 시 비동기 평가 잡이 침묵 성공으로 끝나지 않는지 +- 이유: + - 이번 수정은 analyzer 내부가 중심이지만, 실제 저장/응답 경로는 workflow/router까지 이어지기 때문입니다. + +### F. 테스트 추가 +- 최소 테스트: + - `100점 + B등급` 입력 차단 + - 잘못된 enum 차단 + - `story_scores`와 총점 불일치 차단 또는 보정 + - `summary` 최소 길이 미달 처리 +- 권장 반영 위치: + - `rb8001/tests/test_ir_deck_json_parsing.py` + - validator 단위 테스트 + - raw dict -> model_validate 실패 테스트 + - 재질의 필요 판정 테스트 + - `rb8001/tests/test_ir_deck_workflow.py` + - validator 통과 결과가 workflow save까지 정상 전달되는지 + - validator 실패 시 workflow가 실패를 드러내는지 +- 가능하면 `_evaluate_comprehensive()` 파싱 경로와 validator를 분리해 단위 테스트를 우선 추가합니다. + +## 6. 실패 정책 +- 1차 원칙: + - validator 실패 시 조용히 저장하지 않습니다. +- 기본안: + - 기존 `call_llm()` 응답 검증 실패 시 직접 재질의 1회 + - 1회 재시도 후에도 실패하면 명시적 실패 처리 +- 보조안: + - `grade`, `recommendation`은 서버 재계산으로 보정 가능 + - 구조 자체가 깨진 경우는 실패 처리 +- 권장 세부 순서: + 1. 첫 응답 JSON 추출 + 2. `Pydantic model_validate()` + 3. 실패 시 같은 프롬프트로 1회 재호출 + 4. 2차도 실패하면 저장하지 않고 실패 로그 남김 + 5. 단, `grade/recommendation` 같은 서버 소유 필드는 validator 통과 후에도 서버가 다시 계산 +- 이렇게 두는 이유: + - 재질의는 최소 1회만 허용해 비용과 지연을 제한하고 + - 서버 소유 필드는 보정 가능하지만 구조 실패는 숨기지 않기 위함입니다. + +## 7. 완료 판정 기준 +1. `IR Deck` 종합 평가 경로가 `Pydantic` typed validation을 사용합니다. +2. `total_score=100`, `grade=B` 같은 모순 입력은 저장되지 않습니다. +3. 저장 전 `final_evaluation_result` 경계가 코드에 존재합니다. +4. `grade`, `recommendation`은 서버 정책을 거친 값만 저장됩니다. +5. 관련 테스트가 추가되고 통과합니다. + +## 7-1. 검증 실행 기준 +- 코드 수정 후 최소 검증: + - `pytest rb8001/tests/test_ir_deck_json_parsing.py` + - `pytest rb8001/tests/test_ir_deck_workflow.py` +- 가능하면 추가 검증: + - 기존 IR Deck 관련 대표 테스트 1회 +- 배포 후 확인 기준: + - 실제 평가 1건에서 `100점 + B등급` 같은 모순 저장이 발생하지 않는지 + - 실패 케이스가 있으면 성공처럼 `completed`로만 남지 않는지 로그 확인 +- 이유: + - 이번 수정의 핵심은 parser/validator/workflow 경계이므로 이 세 축을 직접 검증해야 합니다. + +## 8. 후속 경계 +- 이번 계획이 닫혀도 브리핑, 콜드메일, 투자 의견 경로는 별도 확대 계획으로 다룹니다. +- 2차 단계에서 retry, output validator, 생성-검증 일체화를 더 강하게 원하면 `Pydantic AI` 확장 계획을 별도로 검토합니다. + +## 9. 이번 계획의 한 줄 결론 +- 지금은 `LangGraph를 바꾸는 단계`가 아니라, `IR Deck 종합 평가에 Pydantic-only를 먼저 붙여 typed validation 계약을 실제 코드에 고정하는 단계`입니다. diff --git a/journey/research/260313_pydantic_ai_도입_기반_llm_출력_안정화_종료_리서치.md b/journey/research/260313_pydantic_ai_도입_기반_llm_출력_안정화_종료_리서치.md new file mode 100644 index 0000000..cde7952 --- /dev/null +++ b/journey/research/260313_pydantic_ai_도입_기반_llm_출력_안정화_종료_리서치.md @@ -0,0 +1,265 @@ +--- +tags: [research, pydantic-ai, langgraph, llm, structured-output, ir-analysis, closure] +--- + +# Pydantic AI 도입 기반 LLM 출력 안정화 종료 리서치 + +## 관련 문서 +- [Pydantic AI 도입 기반 LLM 출력 안정화 아이디어](../ideas/260313_pydantic_ai_도입_기반_llm_출력_안정화_아이디어.md) +- [자가수정 에이전트 프레임워크 및 Workspace CLI 검증 리서치](./260311_자가수정_에이전트_프레임워크_및_workspace_cli_검증_리서치.md) +- [로빙 에이전트 루프, 스킬 훅, LLM 실행 구조 리서치](./orchestration_tools/260312_로빙_에이전트루프_스킬훅_LLM실행구조_리서치.md) +- [NAVER WORKS 브리핑 인사이트 서두 누출 종료 리서치](./260311_naverworks_briefing_insight_preamble_leak_closure_research.md) +- [콜드메일 IR 분석이 첫 번째 PDF를 잘못 선택하는 상태](../debug/260313_coldmail_ir_분석대상_오선택_디버그.md) + +## 목적 +- `Pydantic` 계열 typed validation 도입 아이디어가 실제 계획으로 넘어갈 수 있는지 검증합니다. +- 이번 문서는 `Pydantic-only 먼저, 필요 시 Pydantic AI 확장`이라는 방향을 기준으로, 어디에 붙이는 것이 맞는지와 왜 이 순서가 타당한지를 좁히는 데 집중합니다. +- 구현 순서, 배포, 세부 수정 항목은 다음 `plans` 문서에서 다룹니다. + +## 조사 질문 +1. `Pydantic-only`를 지금 로빙에 붙인다면 어디가 첫 도입 지점으로 가장 적절한가 +2. 현재 IR Deck 평가 경로에서 `Pydantic-only`가 바로 메울 수 있는 공백은 정확히 무엇인가 +3. `LangGraph`, 기존 `Pydantic`, `Pydantic AI`의 역할 경계는 어떻게 나누는 것이 맞는가 +4. 다음 `plans` 문서가 바로 받아야 할 `Pydantic-only 우선 도입` 결정 포인트는 무엇인가 + +## 확인된 사실 +1. 현재 아이디어 문서가 겨냥하는 직접 문제는 `LLM이 total_score=100, grade=B 같은 모순값을 반환해도 저장·노출될 수 있는 상태`입니다. +2. 현재 IR Deck 평가는 `LangGraph` 워크플로를 통해 `extract -> evaluate -> analyze_pages -> save` 순서로 진행됩니다. + - [ir_deck_workflow.py](../../../rb8001/app/services/workflows/ir_deck_workflow.py) +3. 종합 평가 출력은 `IRDeckAnalyzer._evaluate_comprehensive()`에서 LLM JSON 응답을 파싱한 뒤 일부 필드만 범위 제한합니다. + - [ir_deck_analyzer.py](../../../rb8001/app/services/ir_deck_analyzer.py#L591) +4. 현재 파싱 단계는 아래만 직접 보정합니다. + - `total_score`를 `0~100`으로 clamp + - `grade`가 `S/A/B/C`가 아니면 점수 기반으로 재계산 + - `story_scores`는 리스트/점수 범위만 제한 + - `investment_opinion.recommendation`은 값이 없을 때만 점수 기반 기본값 채움 +5. 반대로 현재 파싱 단계에는 아래 의미 검증이 없습니다. + - `total_score`와 `grade`의 정합성 검증 + - `story_scores` 평균/분포와 `total_score`의 허용 오차 검증 + - `recommendation`과 `grade/score` 정합성 검증 + - 검증 실패 시 저장 차단 +6. 저장 레이어는 전달받은 `total_score`, `grade`, `story_scores`, `investment_opinion`을 그대로 DB에 넣습니다. + - [ir_valuation_repository.py](../../../rb8001/app/state/ir_valuation_repository.py#L130) +7. 현재 `EvaluationResponse`도 저장값을 그대로 API 응답으로 노출하는 구조입니다. + - [ir_deck.py](../../../rb8001/app/router/ir_deck.py#L44) +8. 현재 `rb8001` 의존성에는 `pydantic`과 `pydantic-settings`는 있지만 `pydantic-ai`는 없습니다. + - [requirements.txt](../../../rb8001/requirements.txt) +9. 기존 로빙 리서치 문서도 `LangGraph 전면 교체`보다 `typed output validation 요구가 큰 경로만 부분 도입`이 맞다고 이미 정리했습니다. + - [260311_자가수정_에이전트_프레임워크_및_workspace_cli_검증_리서치.md](./260311_자가수정_에이전트_프레임워크_및_workspace_cli_검증_리서치.md) +10. NAVER WORKS 브리핑 종료 리서치에서도 같은 패턴이 확인됐습니다. + - 프롬프트만으로 닫지 못했고 + - `generate -> validate -> regenerate/fail-visible` 계약이 닫힘 조건이었습니다. + +## 2026-03-13 실측 테스트 + +### 테스트 1. 현재 `_evaluate_comprehensive()`는 모순값을 그대로 통과시킵니다 +- 방법: + - `ir_deck_analyzer.py`를 최소 stub 의존성으로 import했습니다. + - `call_llm()`이 아래 JSON을 반환하도록 고정했습니다. + - `total_score=100` + - `grade=B` + - `investment_opinion.recommendation=투자 보류` + - 실제 `_evaluate_comprehensive()`를 호출했습니다. +- 결과: + - 반환값은 아래처럼 그대로 통과했습니다. + - `{'total_score': 100, 'grade': 'B', ..., 'investment_opinion': {'recommendation': '투자 보류', ...}}` +- 해석: + - 현재 구현에는 `total_score`와 `grade`, `recommendation`의 의미 정합성 검증이 없다는 것이 실측으로 확인됐습니다. + +### 테스트 2. 순수 `Pydantic` 모델 validator는 같은 입력을 즉시 차단합니다 +- 방법: + - 임시 경로에 `pydantic==2.12.5`를 설치했습니다. + - `Evaluation` 모델에 `grade == assign_grade(total_score)` validator를 넣었습니다. + - 같은 입력(`100점 + B등급`)을 검증했습니다. +- 결과: + - `VALIDATION_FAILED_AS_EXPECTED` + - `Value error, grade mismatch: total_score=100, grade=B, expected=S` +- 해석: + - 최소한의 typed validator만 있어도 현재 통과 중인 모순값을 막을 수 있습니다. + +### 테스트 3. `Pydantic AI` Agent + output validator도 같은 입력을 차단합니다 +- 방법: + - `pydantic-ai==1.68.0`를 임시 경로에 설치했습니다. + - `TestModel(custom_output_args=...)`로 동일한 모순 출력(`100점 + B등급`)을 주입했습니다. + - `Agent(output_type=Evaluation)`에 output validator를 붙여 실행했습니다. +- 결과: + - `UnexpectedModelBehavior` + - `Exceeded maximum retries (1) for output validation` +- 해석: + - `Pydantic AI`는 로빙 같은 경로에서 `typed output + validator + retry/fail-visible` 계약을 구현하는 실제 후보로 볼 수 있습니다. + +### 테스트 4. `Pydantic AI` 도입은 경량 추가가 아닙니다 +- 방법: + - `python3 -m pip install --target /tmp/pydantic_ai_pkg pydantic-ai`를 실행했습니다. +- 결과: + - 설치는 성공했지만 `openai`, `anthropic`, `google-genai`, `mcp`, `fastmcp`, `logfire`, `temporalio` 등 매우 많은 의존성이 함께 내려왔습니다. +- 해석: + - `Pydantic AI`는 기능적으로는 유효하지만, 의존성/이미지 크기/빌드 시간 영향까지 고려해야 합니다. + - 따라서 첫 도입 결정에는 `기능 적합성`뿐 아니라 `의존성 비용`도 함께 포함돼야 합니다. + +## 해석 +- 현재 로빙에는 `Pydantic` 계열 typed validation을 붙일 명확한 도입 이유가 있습니다. +- 그 이유는 `LangGraph`를 버려야 해서가 아니라, 현재 코드가 `형식 파싱`과 `범위 제한`까지만 하고 `제품값 정합성 검증`은 하지 않기 때문입니다. +- 따라서 1차 해법은 `현재 call_llm() 유지 + Pydantic model_validate() + semantic validator 추가`가 가장 현실적입니다. +- `IR Deck 종합 평가`는 이 경계를 가장 먼저 붙여볼 가치가 높은 경로입니다. +- `Pydantic AI`는 유효한 후보지만, 실측 결과 기준으로 의존성 무게가 작지 않으므로 첫 단계 기본값으로 두기보다 2차 확장 후보로 두는 편이 더 타당합니다. + +## `Pydantic-only` vs `Pydantic AI` 비교 + +### 1. `Pydantic-only` +- 구조: + - 기존 `call_llm()` 유지 + - 응답 JSON 추출 후 `Pydantic model_validate()` 수행 + - model validator에서 점수/등급 정합성 검사 +- 장점: + - 현재 구조 변경이 가장 작습니다. + - `LangGraph`, `call_llm()`, 저장 흐름을 거의 그대로 유지할 수 있습니다. + - 실측상 현재 문제(`100점 + B등급`)를 막는 데 충분합니다. +- 단점: + - 재질의, retry, output validator orchestration은 직접 붙여야 합니다. + - 생성과 검증이 분리돼 있어 호출 경계가 느슨합니다. + +### 2. `Pydantic AI` +- 구조: + - `Agent(output_type=...)` + - output validator + - retry / fail-visible +- 장점: + - 생성과 검증을 한 계층으로 묶기 쉽습니다. + - retry 정책을 더 일관되게 넣기 좋습니다. + - 향후 tool calling, structured output, agent 경계 확장에 유리합니다. +- 단점: + - 의존성 증가가 큽니다. + - 현재 로빙 경로에 바로 붙이기엔 변경 범위가 더 큽니다. + - 지금 당장 필요한 `모순값 차단`에는 과할 수 있습니다. + +### 3. 현재 기준 판단 +- 현재 단계의 기본값은 `Pydantic-only`가 더 적절합니다. +- 이유: + 1. 문제를 바로 막는 데 충분합니다. + 2. 기존 경계를 거의 유지할 수 있습니다. + 3. 실측상 `Pydantic` validator만으로도 현재 모순값을 차단했습니다. +- 따라서 지금 리서치의 결론은 `Pydantic AI를 버리자`가 아니라 `1차는 Pydantic-only, 2차는 Pydantic AI`입니다. + +## 선택지 비교 + +### 선택지 A. 현재 구조 유지 + 프롬프트 보강만 추가 +- 장점: + - 가장 빠릅니다. + - 의존성 추가가 없습니다. +- 단점: + - 이미 출력 일탈이 한 번 발생했습니다. + - 저장 전 계약 검증이 없어 같은 문제를 다시 막지 못합니다. +- 판단: + - `Pydantic AI 도입 아이디어`를 검증한 결과, 이 선택지는 충분하지 않습니다. + +### 선택지 B. `LangGraph`를 버리고 `Pydantic AI` 중심으로 재구성 +- 장점: + - 출력 계약을 새로 설계하기 쉽습니다. + - `Agent` + `result_type` + validator 조합을 일관되게 가져갈 수 있습니다. +- 단점: + - 현재 `interrupt`, checkpoint, 비동기 평가 흐름과 분리되지 않습니다. + - 이번 문제보다 범위가 과도하게 큽니다. + - 기존 리서치 결론과도 충돌합니다. +- 판단: + - `Pydantic AI 도입` 아이디어와는 연결되지만, 부분 도입 아이디어의 범위를 벗어납니다. + +### 선택지 C. `LangGraph`는 유지하고, 종합 평가 출력 직후에 `Pydantic-only` typed contract 계층을 추가 +- 장점: + - 현재 문제 지점을 직접 닫습니다. + - `LangGraph`의 상태 관리 가치는 유지합니다. + - `Pydantic-only`만으로 바로 시작할 수 있습니다. +- 단점: + - retry와 fail-visible 일부는 직접 구현해야 합니다. + - 서버 재계산 정책을 따로 정해야 합니다. +- 판단: + - 지금 바로 실행할 첫 단계로 가장 적절합니다. + +### 선택지 D. `LangGraph`는 유지하고, 바로 `Pydantic AI`를 붙인다 +- 장점: + - output validator, retry, typed output을 한 계층으로 묶기 쉽습니다. + - 다음 단계 확장성은 좋습니다. +- 단점: + - 현재 필요한 문제 크기보다 변경 범위가 큽니다. + - 의존성 증가가 즉시 들어옵니다. +- 판단: + - 후속 2차 후보로는 좋지만, 첫 단계 기본안으로 두기엔 무겁습니다. + +## 결론 +- 이번 리서치 기준으로 가장 맞는 결론은 `선택지 C`, 즉 `LangGraph 유지 + Pydantic-only 우선 도입`입니다. +- 정확한 실행 문장은 아래와 같습니다. + - `LangGraph`는 유지합니다. + - IR Deck 종합 평가 경로를 `Pydantic-only` 첫 도입 후보로 잡습니다. + - 이 경로에 `LLM raw output -> Pydantic model_validate -> semantic validation -> server-derived final fields -> save` 계층을 추가하는 방향이 맞습니다. + - `Pydantic AI`는 2차 확장 후보로 남깁니다. + - 실측 테스트 기준으로도 현재 구현의 공백은 재현됐고, 순수 `Pydantic` validator만으로도 이 공백을 차단할 수 있다는 점이 확인됐습니다. + +## 이번 도입 아이디어를 닫는 권장 계약 + +### 1. Raw Output과 Final Output을 분리해야 합니다 +- LLM이 처음 반환한 값은 `raw_evaluation_result`로 취급합니다. +- 사용자와 DB에 저장되는 값은 `final_evaluation_result`로 분리해야 합니다. +- 이 둘을 같은 dict로 다루는 현재 구조가 문제의 핵심입니다. + +### 2. Typed Contract는 구조 검증과 의미 검증을 둘 다 가져야 합니다 +- 구조 검증: + - 필수 필드 존재 + - 타입 일치 + - 리스트 길이, 문자열 길이, enum 범위 +- 의미 검증: + - `grade == assign_grade(total_score)` 이어야 함 + - `recommendation`은 `total_score/grade` 정책과 충돌하면 안 됨 + - `story_scores`가 있으면 `total_score`는 허용 오차 내에서 재계산 가능해야 함 + - 필수 설명 필드가 너무 비어 있으면 통과시키지 않아야 함 + +### 3. 핵심 제품값은 서버가 최종 소유해야 합니다 +- `grade`는 서버가 `total_score`로 재계산하는 쪽이 더 안전합니다. +- `recommendation`도 서버 정책에서 파생시키는 쪽이 더 안전합니다. +- `total_score`는 이상적으로 `story_scores` 또는 서버 스코어링 규칙으로 재산출 가능한 구조로 옮겨야 합니다. +- 즉 LLM은 `해석값`을 내고, 서버는 `최종 노출값`을 소유해야 합니다. + +### 4. 검증 실패는 저장 금지 또는 명시적 실패로 다뤄야 합니다 +- 검증 실패 시 조용히 clamp 후 저장하는 현재 접근은 부족합니다. +- 최소한 아래 중 하나가 필요합니다. + - 재질의 1회 후 실패 가시화 + - 서버 재계산 후 `raw`와 차이를 로그로 남기고 저장 + - 기준 미달이면 저장 자체를 실패 처리 +- 어떤 정책을 택하든 `성공처럼 저장`은 금지돼야 합니다. + +## 다음 `plans` 문서가 바로 결정해야 할 포인트 +1. typed contract 구현을 어떤 `Pydantic` 모델 구조로 둘지 +2. `final`로 서버가 소유할 필드를 어디까지로 할지 + - 최소 후보: `grade`, `recommendation` + - 확장 후보: `total_score` +3. 검증 실패 정책을 무엇으로 할지 + - 재질의 직접 구현 1회 + - 서버 재계산 우선 + - 저장 차단 +4. 첫 적용 범위를 어디로 자를지 + - 1순위: IR Deck 종합 평가 + - 이후: 브리핑 인사이트, 콜드메일 후보 판정, 투자 의견 출력 + - 2차: 같은 경계를 `Pydantic AI`로 올릴지 검토 +5. 어떤 로그와 테스트를 닫힘 기준으로 삼을지 + +## 닫힘 기준 +1. IR Deck 종합 평가 경로에 `raw -> validate -> finalize -> save` 경계가 코드로 존재해야 합니다. +2. `total_score=100, grade=B` 같은 모순 입력은 더 이상 그대로 저장되지 않아야 합니다. +3. 저장되는 `grade`, `recommendation`은 LLM 원문이 아니라 서버 정책을 통과한 최종값이어야 합니다. +4. 검증 실패 케이스는 성공처럼 응답하거나 DB에 남지 않아야 합니다. +5. 아래 테스트가 최소 포함돼야 합니다. + - 점수/등급 모순 + - `story_scores` 평균과 총점 불일치 + - 잘못된 enum/누락 필드 + - summary/strengths/risks 최소 품질 미달 + +## 미확정 항목 +- `Pydantic-only` 1차 적용 후 어느 시점에 `Pydantic AI`로 올릴지는 아직 미정입니다. +- `total_score`를 완전히 서버 재계산으로 가져갈지, LLM 점수를 허용 오차 내에서만 수용할지는 아직 미정입니다. +- 검증 실패 시 사용자에게 `평가 실패`를 직접 노출할지, 내부 재시도 후만 노출할지는 아직 미정입니다. +- `raw output`을 DB나 로그에 별도 저장할지는 아직 미정입니다. +- `Pydantic AI`의 의존성 증가를 나중에 운영 이미지에서 수용할지, 별도 실험 브랜치로 먼저 검증할지도 아직 미정입니다. + +## 이번 리서치의 결론 문장 +- 이 리서치는 `Pydantic` 계열 typed validation 도입 아이디어를 반박하지 않습니다. +- 오히려 `IR Deck 종합 평가`에 `Pydantic-only`를 먼저 붙이는 쪽이 현재 구조와 가장 잘 맞는다는 쪽으로 결론이 모입니다. +- 따라서 다음 단계 `plans`는 `Pydantic-only를 어떤 모델과 정책으로 붙일지`를 고정하는 문서가 되어야 합니다. diff --git a/journey/worklog/260313_ir_deck_pydantic_only_출력검증_구현및배포검증.md b/journey/worklog/260313_ir_deck_pydantic_only_출력검증_구현및배포검증.md new file mode 100644 index 0000000..1bb33f4 --- /dev/null +++ b/journey/worklog/260313_ir_deck_pydantic_only_출력검증_구현및배포검증.md @@ -0,0 +1,46 @@ +--- +tags: [worklog, ir-deck, pydantic, validation, rb8001, deployment] +--- + +# IR Deck Pydantic-only 출력검증 구현 및 배포검증 + +## 관련 문서 +- [Pydantic AI 도입 기반 LLM 출력 안정화 아이디어](../ideas/260313_pydantic_ai_도입_기반_llm_출력_안정화_아이디어.md) +- [Pydantic AI 도입 기반 LLM 출력 안정화 종료 리서치](../research/260313_pydantic_ai_도입_기반_llm_출력_안정화_종료_리서치.md) +- [Pydantic AI 부분 도입 LLM 출력안정화 계획](../plans/260313_pydantic_ai_부분도입_llm_출력안정화_계획.md) + +## 완료 요약 +- `rb8001`의 `IR Deck` 종합 평가 경로에 `Pydantic-only` typed validation을 추가했습니다. +- `100점 + B등급` 같은 모순 출력은 1회 재질의 후에도 해결되지 않으면 저장하지 않도록 변경했습니다. +- `grade`, `recommendation`은 서버 최종값으로 재계산하도록 고정했습니다. + +## 구현 내용 +- 추가: + - `rb8001/app/services/ir_deck_output_models.py` +- 수정: + - `rb8001/app/services/ir_deck_analyzer.py` + - `rb8001/tests/test_ir_deck_json_parsing.py` +- 핵심 변경: + - `raw -> Pydantic model_validate -> finalize -> save` 경계 추가 + - `story_scores` 정규화 함수 추가 + - `summary` fallback 생성 후 최소 길이 검증 추가 + - validator 실패 시 같은 프롬프트 재호출 1회 추가 + +## 검증 결과 +- 컨테이너 내부 테스트: + - `pytest /code/tests/test_ir_deck_json_parsing.py` → `15 passed` + - `pytest /code/tests/test_ir_deck_workflow.py` → `5 passed` +- 배포 검증: + - `git push origin main` 완료 + - `rb8001` 컨테이너 재시작 확인 + - 재시작 후 `curl localhost:8001/health` 정상 응답 확인 + +## 배포 확인 값 +- 푸시 커밋: `cc8f281` +- 배포 후 `rb8001` 시작 시각: + - `2026-03-13T09:28:41Z` +- 배포 후 상태: + - `rb8001 Up 22 seconds (healthy)` 확인 후 헬스 응답 정상 + +## 한 줄 결론 +- 이번 작업으로 `IR Deck` 종합 평가는 기존 `LangGraph`를 유지한 채, `Pydantic-only` 검증 계층을 통해 모순된 구조화 출력 저장을 차단하는 상태로 전진했습니다.