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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.swyp.picke.domain.classroom.service.debate;

import com.swyp.picke.domain.battle.enums.BattleDifficulty;
import com.swyp.picke.domain.classroom.enums.ClassDebateSpeaker;
import com.swyp.picke.domain.user.enums.PhilosopherType;

Expand All @@ -12,7 +13,7 @@ public final class ClassDebatePrompts {
private ClassDebatePrompts() {
}

public static final String PROMPT_VERSION = "debate-v1";
public static final String PROMPT_VERSION = "debate-v2";

/** 과금 대상 출력 합계 1,000 토큰 상한 (PRD §11 길이와 전달 방식) */
public static final int MAX_OUTPUT_TOKENS = 1_000;
Expand All @@ -31,8 +32,14 @@ private ClassDebatePrompts() {
주제에서 벗어났으면 관련 논점으로 돌아오도록 이끕니다.
3. 출처 없는 직접 인용·통계·연구 결과를 만들지 않습니다. 실존 인물의 말을 따옴표로 인용하지 않습니다.
4. 참가자를 비난하거나 조롱하지 않습니다. 이름을 부르지 않고 "~해요" 체로 씁니다.
5. argument_text 는 50자 이상 500자 이하로 씁니다.
5. argument_text 는 50자 이상 500자 이하로 씁니다. 그 안에서 [수준]의 권장 길이를 따릅니다.
6. focus 는 이번 주장의 초점 하나를 EVIDENCE(근거), ASSUMPTION(가정), COUNTEREXAMPLE(반례), CONDITION(조건), VALUE_CONFLICT(가치충돌) 중에서 고릅니다.
[수준]별 반론 방식 (모임이 정한 참가자 수준입니다. 논리의 엄밀함은 낮추지 않고 표현과 깊이를 맞춥니다):
- 초급: 일상 경험이나 친숙한 예시로 설명하고 쉬운 말을 씁니다. 개념어는 꼭 필요할 때만 풀어서 씁니다.
한 번에 논점 하나만 다루고 문장을 짧게 씁니다. 반박을 이어 갈 수 있도록 생각을 묻는 질문 하나로 마칩니다. 권장 120~250자.
- 중급: 근거와 숨은 가정을 짚고 구체적인 예를 하나 듭니다. 개념어를 쓰면 한 번 짧게 풀어 줍니다. 권장 200~350자.
- 고급: 개념을 구분하고 조건·반례·가치 충돌을 정밀하게 다룹니다. 철학 개념어를 설명 없이 써도 되지만
출처 없는 인용은 쓰지 않습니다. 참가자 논증의 전제나 일반화 범위를 직접 문제 삼습니다. 권장 300~500자.
출력은 아래 형식의 JSON 객체 하나만 씁니다. 라운드 번호, 종료 여부, 점수 같은 다른 필드는 넣지 않습니다.
{"argument_text": "50~500자", "focus": "EVIDENCE"}
""";
Expand All @@ -46,6 +53,7 @@ public static String user(DebatePromptInput input) {
PhilosopherType p = input.philosopher();
sb.append("[철학자] ").append(p.getLabel()).append(" · ").append(p.getTypeName()).append(" · ")
.append(p.getDescription()).append(" · ").append(String.join(" ", p.getKeywordTags())).append("\n\n");
sb.append("[수준] ").append(difficultyLabel(input.difficulty())).append('\n');
sb.append("[라운드] ").append(input.round()).append(" / 3\n\n");
sb.append("[대화]\n");
if (input.history().isEmpty()) {
Expand All @@ -68,6 +76,14 @@ public static String correctionUser(String originalUser, String previousResponse
+ "위 문제를 고친 JSON 을 다시 출력합니다.\n";
}

static String difficultyLabel(BattleDifficulty difficulty) {
return switch (difficulty) {
case BEGINNER -> "초급";
case INTERMEDIATE -> "중급";
case ADVANCED -> "고급";
};
}

private static String side(DebatePromptInput.Side side) {
return side.position() + ". " + side.label() + " — " + side.rationale();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.swyp.picke.domain.battle.entity.Battle;
import com.swyp.picke.domain.battle.entity.BattleOption;
import com.swyp.picke.domain.battle.enums.BattleDifficulty;
import com.swyp.picke.domain.classroom.dto.request.ClassDebateReplyRequest;
import com.swyp.picke.domain.classroom.dto.response.ClassDebateResponse;
import com.swyp.picke.domain.classroom.entity.AiJob;
Expand Down Expand Up @@ -279,10 +280,17 @@ private DebatePromptInput promptInput(ClassDebateSession session) {
promptSide(session.getParticipantOption()),
promptSide(session.getOpponentOption()),
session.getPhilosopher(),
difficultyOf(session.getClassroom()),
session.getRound(),
history);
}

/** 커스텀 배틀의 대상 수준 = 주제 버전의 수준 (기획 Q&A: 커스텀은 AI 철학자 반론 수준). 없으면 중급으로 본다 */
private static BattleDifficulty difficultyOf(Classroom classroom) {
ClassTopicVersion version = classroom.getTopicVersion();
return version == null || version.getDifficulty() == null ? BattleDifficulty.INTERMEDIATE : version.getDifficulty();
}

private static DebatePromptInput.Side promptSide(BattleOption option) {
return new DebatePromptInput.Side(position(option), option.getTitle(), option.getStance());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.swyp.picke.domain.classroom.service.debate;

import com.swyp.picke.domain.battle.enums.BattleDifficulty;
import com.swyp.picke.domain.classroom.enums.ClassDebateSpeaker;
import com.swyp.picke.domain.user.enums.PhilosopherType;
import java.util.List;
Expand All @@ -13,6 +14,7 @@ public record DebatePromptInput(
Side participant,
Side opponent,
PhilosopherType philosopher,
BattleDifficulty difficulty,
int round,
List<Turn> history
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ void fullDebate() throws Exception {
.andExpect(jsonPath("$.data.messages[0].focus").value("EVIDENCE"))
.andExpect(jsonPath("$.data.messages[0].focusLabel").value("근거"));
verify(aiClient).chatJson(eq(ClassDebatePrompts.SYSTEM),
argThat(p -> p.contains("[라운드] 1 / 3") && p.contains("(아직 없음)")
argThat(p -> p.contains("[라운드] 1 / 3") && p.contains("(아직 없음)") && p.contains("[수준] 중급")
&& p.contains("[상대 입장 - 당신이 대변] B. 반대 2") && p.contains("[철학자] 아리스토텔레스")),
anyInt());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.swyp.picke.domain.classroom.service.debate;

import static org.assertj.core.api.Assertions.assertThat;

import com.swyp.picke.domain.battle.enums.BattleDifficulty;
import com.swyp.picke.domain.user.enums.PhilosopherType;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class ClassDebatePromptsTest {

@ParameterizedTest
@DisplayName("토론 입력에 클래스 수준이 한글 이름으로 들어간다")
@CsvSource({"BEGINNER,초급", "INTERMEDIATE,중급", "ADVANCED,고급"})
void userPromptCarriesDifficulty(BattleDifficulty difficulty, String label) {
String prompt = ClassDebatePrompts.user(input(difficulty));

assertThat(prompt).contains("[수준] " + label + "\n");
}

@Test
@DisplayName("시스템 프롬프트에 수준별 반론 방식이 모두 있고, 권장 길이는 출력 검증 범위(50~500자) 안이다")
void systemPromptDescribesEveryDifficulty() {
assertThat(ClassDebatePrompts.SYSTEM)
.contains("- 초급:", "- 중급:", "- 고급:")
.contains("권장 120~250자", "권장 200~350자", "권장 300~500자");
assertThat(ClassDebatePrompts.PROMPT_VERSION).isEqualTo("debate-v2");
}

private static DebatePromptInput input(BattleDifficulty difficulty) {
return new DebatePromptInput(
"표현하지 않은 감정까지 타인이 판단해도 될까요?",
"감정 표현의 자유와 공동체의 기대를 비교해 봐요.",
new DebatePromptInput.Side("A", "판단할 수 있다", "표현은 관계에 대한 책임의 일부다"),
new DebatePromptInput.Side("B", "판단하기 어렵다", "내면과 표현은 다를 수 있다"),
PhilosopherType.values()[0],
difficulty,
1,
List.of());
}
}
Loading