Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/fewshot-reviewed-loader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# PM 검수 Few-shot 적재

기존 `fix/fewshot-selection-safety-cache` 작업 브랜치에 로더 보강을 추가했습니다.
기존 dev에 후보 및 질의 임베딩 캐시가 있으므로 캐시는 중복 구현하지 않았습니다.

## 변경

- CSV의 주요 업무·자격요건·우대사항 중 하나 이상 있으면 JD를 허용합니다.
세 항목이 모두 빈 사례는 제외합니다. 누락된 원문 정보를 생성하지 않습니다.
- jobTitle을 보존하고, 없는 기존 CSV만 jobCategorySmall로 대체합니다.
- 우대사항을 예시 프롬프트에 포함합니다.
- approvedAnalysisJson은 단일 JSON 객체여야 합니다. 잘못된 행만 제외하고 다음 행을 읽습니다.
이 검사는 JSON 문법과 최상위 객체 검사이며 분석 내용의 의미 검증은 아닙니다.
- 활성·승인·caseId·문항·비식별 답변 검증을 유지합니다.

## 로컬 평가 설정

검수 후보 CSV는 평가 입력(holdout)과 분리해야 합니다.
후보 JSON과 CSV는 동일 데이터이므로 동시에 로딩하지 않습니다.

```properties
analysis.few-shot.dynamic-selection-enabled=true
analysis.few-shot.dataset-version=fewshot-pm-reviewed-20260914-v2
analysis.few-shot.source.reviewed-evaluation-enabled=true
analysis.few-shot.reviewed-evaluation-resource=
analysis.few-shot.reviewed-evaluation-csv-path=/absolute/path/fewshot_candidates_approved.csv
```

운영 기본 활성화 설정은 변경하지 않았습니다. Python 워커 연결과 실제 API 비용이
발생하는 평가는 이 변경에 포함하지 않습니다.

## 검수 확인

사용자가 PM에게 재확인한 내용: FS-02-S1의 fabricated 변경은 의도된 것이며,
우선순위 1이 최상위입니다. FS-02-S1은 상태 변경 의도 때문에 보류한 것이 아니라
최종 판정 이유와 정책 정합성 보완이 남아 활성 예시에서 제외한 상태입니다.

## 검증

`./gradlew test --tests '*fewshot.*'`: 27개 성공.
선택 서비스의 기존 캐시·입력 제외 회귀 테스트와 CSV 로더의 선택 JD 필드,
직무명 호환, 우대사항 보존, 잘못된 JSON 행 이후 정상 행 적재를 확인했습니다.
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,6 @@ private List<FewShotCase> loadReviewedEvaluationCsvCases(String csvPath) {
Set<String> ids = new HashSet<>();
for (Map<String, String> row : rows) {
String id = value(row, "caseId");
if (!ids.add(id)) {
log.warn("reviewed evaluation few-shot row skipped. reason=duplicate_case_id, caseId={}", id);
continue;
}
if (!"true".equalsIgnoreCase(value(row, "fewShotEnabled"))) {
continue;
}
Expand All @@ -169,22 +165,40 @@ private List<FewShotCase> loadReviewedEvaluationCsvCases(String csvPath) {
String sanitizedAnswer = value(row, "sanitizedAnswer");
String approvedAnalysisJson = value(row, "approvedAnalysisJson");
if (!StringUtils.hasText(id)
|| !StringUtils.hasText(value(row, "mainTasks"))
|| !StringUtils.hasText(value(row, "qualifications"))
|| !(StringUtils.hasText(value(row, "mainTasks"))
|| StringUtils.hasText(value(row, "qualifications"))
|| StringUtils.hasText(value(row, "preferences")))
|| !StringUtils.hasText(value(row, "question"))
|| !StringUtils.hasText(sanitizedAnswer)
|| !StringUtils.hasText(approvedAnalysisJson)) {
log.warn("reviewed evaluation few-shot row skipped. reason=missing_required_field, caseId={}", id);
continue;
}
try {
var analysis = objectMapper.reader()
.with(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_TRAILING_TOKENS)
.readTree(approvedAnalysisJson);
if (analysis == null || !analysis.isObject()) {
log.warn("reviewed evaluation few-shot row skipped. reason=invalid_analysis_object, caseId={}", id);
continue;
}
} catch (IOException e) {
log.warn("reviewed evaluation few-shot row skipped. reason=invalid_analysis_json, caseId={}", id);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
continue;
}
if (!ids.add(id)) {
log.warn("reviewed evaluation few-shot row skipped. reason=duplicate_case_id, caseId={}", id);
continue;
}
result.add(new FewShotCase(
id,
FewShotSource.REVIEWED_EVALUATION,
FewShotReviewStatus.APPROVED,
true,
parseInt(value(row, "fewShotPriority")),
value(row, "jobCategorySmall"),
value(row, "jobCategorySmall"),
StringUtils.hasText(value(row, "jobTitle"))
? value(row, "jobTitle") : value(row, "jobCategorySmall"),
splitLines(value(row, "mainTasks")),
splitLines(value(row, "qualifications")),
value(row, "question"),
Expand Down Expand Up @@ -250,6 +264,7 @@ private static String buildReviewedPromptBlock(
관련 JD 요구사항:
- mainTask: %s
- qualification: %s
- preference: %s

평가 대상 답변:
- questionId: 1
Expand All @@ -262,6 +277,7 @@ private static String buildReviewedPromptBlock(
id,
value(row, "mainTasks"),
value(row, "qualifications"),
value(row, "preferences"),
value(row, "question"),
sanitizedAnswer,
approvedAnalysisJson
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,64 @@ class FewShotCaseStoreTest {
@TempDir
Path tempDir;

@Test
@DisplayName("무효 행은 ID를 선점하지 않고 같은 ID의 첫 유효 행만 적재한다")
void retainsFirstValidRowAfterInvalidRowsWithSameCaseId() throws Exception {
Path csv = tempDir.resolve("duplicate-id.csv");
Files.writeString(csv, """
caseId,mainTasks,question,sanitizedAnswer,approvedAnalysisJson,fewShotEnabled,reviewStatus
EV-01,API 개발,경험,비활성 답변,{},false,APPROVED
EV-01,API 개발,경험,미승인 답변,{},true,IN_REVIEW
EV-01,API 개발,경험,,{},true,APPROVED
EV-01,API 개발,경험,잘못된 JSON 답변,{,true,APPROVED
EV-01,API 개발,경험,배열 JSON 답변,[],true,APPROVED
EV-01,API 개발,경험,첫 유효 답변,{},true,APPROVED
EV-01,API 개발,경험,중복 유효 답변,{},true,APPROVED
""");
FewShotProperties properties = new FewShotProperties();
properties.getSource().setFixedEnabled(false);
properties.getSource().setCuratedEnabled(false);
properties.getSource().setReviewedEvaluationEnabled(true);
properties.setReviewedEvaluationResource("");
properties.setReviewedEvaluationCsvPath(csv.toString());

var loaded = new FewShotCaseStore(new FewShotPromptProvider(), properties, new ObjectMapper())
.loadActiveCases();

assertThat(loaded).extracting(FewShotCase::id).containsExactly("EV-01");
assertThat(loaded.getFirst().sanitizedAnswer()).isEqualTo("첫 유효 답변");
}

@Test
void preservesOptionalJdSectionsAndSkipsMalformedJsonPerRow() throws Exception {
Path csv = tempDir.resolve("optional-jd.csv");
Files.writeString(csv, """
caseId,jobCategorySmall,jobTitle,mainTasks,qualifications,preferences,question,sanitizedAnswer,approvedAnalysisJson,fewShotEnabled,reviewStatus
FS-05,PA,Project Assistant,,협업 능력,Adobe,경험,답변,{},true,APPROVED
FS-09,BX,Brand Designer,IP 관리,,,경험,답변,{},true,APPROVED
PREF,디자인,,, ,Adobe,경험,답변,{},true,APPROVED
EMPTY,디자인,,,,,경험,답변,{},true,APPROVED
BROKEN,디자인,,IP 관리,,,경험,답변,{,true,APPROVED
ARRAY,디자인,,IP 관리,,,경험,답변,[],true,APPROVED
TRAILING,디자인,,IP 관리,,,경험,답변,{} garbage,true,APPROVED
LAST,개발,,API 개발,,,경험,답변,{},true,APPROVED
""");
FewShotProperties properties = new FewShotProperties();
properties.getSource().setFixedEnabled(false);
properties.getSource().setCuratedEnabled(false);
properties.getSource().setReviewedEvaluationEnabled(true);
properties.setReviewedEvaluationResource("");
properties.setReviewedEvaluationCsvPath(csv.toString());
var loaded = new FewShotCaseStore(new FewShotPromptProvider(), properties, new ObjectMapper())
.loadActiveCases();
assertThat(loaded).extracting(FewShotCase::id).containsExactly("FS-05", "FS-09", "PREF", "LAST");
assertThat(loaded.getFirst().jobTitle()).isEqualTo("Project Assistant");
assertThat(loaded.getFirst().mainTasks()).isEmpty();
assertThat(loaded.getFirst().promptBlock()).contains("- preference: Adobe");
assertThat(loaded.get(1).qualifications()).isEmpty();
assertThat(loaded.getLast().jobTitle()).isEqualTo("개발");
}

@Test
@DisplayName("reviewed evaluation CSV는 승인, 활성, 비식별 답변, 승인 분석이 있는 행만 후보로 적재한다")
void loadsOnlyApprovedReviewedEvaluationRows() throws Exception {
Expand Down
Loading