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
3 changes: 3 additions & 0 deletions ai/src/ai_server/chain/followup_generation_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async def generate(
answer_text: str,
context: str = "(none)",
parent_category: str = "UNKNOWN",
expected_signal: str = "(none)",
history: str = "(none)",
) -> FollowupResult: ...

Expand All @@ -46,6 +47,7 @@ async def generate(
answer_text: str,
context: str = "(none)",
parent_category: str = "UNKNOWN",
expected_signal: str = "(none)",
history: str = "(none)",
) -> FollowupResult:
result = await self._chain.ainvoke(
Expand All @@ -56,6 +58,7 @@ async def generate(
"answer_text": answer_text,
"context": context,
"parent_category": parent_category,
"expected_signal": expected_signal,
"history": history,
}
)
Expand Down
2 changes: 2 additions & 0 deletions ai/src/ai_server/chain/prompts/feedback_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
" - 점수를 매기기 **전에** 강점/약점의 근거를 먼저 정리한 뒤 점수를 산정하세요(즉흥 점수 금지).\n"
" - strengths_summary·weaknesses_summary 에는 **어느 질문/순간 때문인지 구체적으로** 적으세요 "
"(예: 'Q3에서 동시성 처리를 RDB 락으로만 설명해 분산 환경 고려가 빠짐').\n"
" - 질문에 '기대 신호(평가 기준)' 가 붙어 있으면, 각 답변이 그 핵심을 얼마나 짚었는지를 "
"technical_accuracy 판단의 핵심 근거로 삼고, 빗나간 지점은 weaknesses 에 구체적으로 적으세요.\n"
" - 답변이 짧거나 비어 있으면 해당 점수는 낮게 또는 null.\n"
" - 컨텍스트 청크(분석 문서 일부) 가 있다면 사실 검증에만 활용 (직접 인용 X).\n"
"- 응답은 반드시 지정된 JSON 스키마를 따릅니다."
Expand Down
4 changes: 4 additions & 0 deletions ai/src/ai_server/chain/prompts/followup_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"PROJECT_DEEP_DIVE 같은 기술형이면 structure 는 참고만 하고 정확성·깊이를 우선한다.\n"
" - correctness (0~5): 답변이 '검색 문서 컨텍스트'의 사실과 일치하는가. "
"**컨텍스트가 '(none)' 이면 판단 불가 → correctness 는 null**. 추측으로 채우지 마세요.\n"
"- '기대 신호(expected_signal)' 가 주어지면, 그 질문이 드러내려던 핵심을 답변이 "
"얼마나 짚었는지를 specificity·correctness 채점의 핵심 기준으로 삼으세요 "
"(기대 신호를 정확히 충족하면 높게, 빗나가거나 누락하면 낮게). '(none)' 이면 무시.\n"
"- 꼬리질문은 가장 약한 축을 겨냥합니다 (예: 구체성 낮음→수치/사례 요구, "
"correctness 의심→자료와의 불일치 확인).\n"
"- '이미 나눈 대화'에서 다룬 내용을 그대로 반복하지 말고 새로운 각도로 파고드세요.\n"
Expand All @@ -30,6 +33,7 @@
"- INTEGRATED: 기술 질문과 인성 질문의 관점을 균형 있게 반영합니다.\n\n"
"이미 나눈 대화 (반복 금지):\n{history}\n\n"
"직전 질문:\n{previous_question}\n\n"
"직전 질문의 기대 신호(평가 기준):\n{expected_signal}\n\n"
"지원자 답변:\n{answer_text}\n\n"
"검색 문서 컨텍스트:\n---\n{context}\n---\n\n"
"{format_instructions}"
Expand Down
2 changes: 2 additions & 0 deletions ai/src/ai_server/messaging/consumers/feedback_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ def _build_transcript(messages: list[FeedbackMessageItem]) -> str:
else ("지원자" if m.role == "INTERVIEWEE" else m.role)
)
line = f"[{m.sequence_number}] {speaker}: {m.content}"
if m.role == "INTERVIEWER" and m.expected_signal:
line += f"\n └ 기대 신호(평가 기준): {m.expected_signal}"
if m.role == "INTERVIEWEE" and m.evaluation is not None:
line += f"\n └ 답변평가: {_format_evaluation(m.evaluation)}"
lines.append(line)
Expand Down
1 change: 1 addition & 0 deletions ai/src/ai_server/messaging/consumers/followup_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ async def handle(self, message: AbstractIncomingMessage) -> None:
answer_text=req.answer_text,
context=await self._build_rag_context(req),
parent_category=req.parent_category or "UNKNOWN",
expected_signal=req.parent_expected_signal or "(none)",
history=_format_history(req.history),
)

Expand Down
2 changes: 2 additions & 0 deletions ai/src/ai_server/model/messages/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class FeedbackMessageItem(BaseModel):
role: Literal["INTERVIEWER", "INTERVIEWEE", "SYSTEM"]
content: str
parent_message_id: int | None = None
# 질문(INTERVIEWER) 메시지에만 채워짐. 답변이 짚어야 할 핵심(평가 기준).
expected_signal: str | None = None
# 답변(INTERVIEWEE) 메시지에만 채워짐. 피드백 종합 채점의 근거.
evaluation: MessageEvaluation | None = None

Expand Down
2 changes: 2 additions & 0 deletions ai/src/ai_server/model/messages/followup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class GenerateFollowupRequest(BaseModel):
job_category: Literal["FRONTEND", "BACKEND", "INFRA", "DBA"]
context_document_ids: list[int] = Field(default_factory=list)
parent_category: str | None = None # 직전 질문 카테고리 (루브릭 선택)
# 직전 질문이 기대하는 핵심(평가 관점). correctness/specificity 충족도 채점에 사용.
parent_expected_signal: str | None = None
history: list[HistoryItem] = Field(default_factory=list) # 최근 대화 (중복 회피)


Expand Down
16 changes: 16 additions & 0 deletions ai/tests/test_feedback_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ai_server.messaging.consumers.feedback_consumer import (
FeedbackConsumer,
_build_score_basis,
_build_transcript,
)
from ai_server.messaging.idempotency import LruIdempotencyStore
from ai_server.model.messages.feedback import (
Expand Down Expand Up @@ -231,6 +232,21 @@ def test_build_score_basis_empty_when_no_evaluations():
assert "per-answer 평가 없음" in _build_score_basis([])


def test_transcript_includes_expected_signal_on_question():
msgs = [
FeedbackMessageItem(
id=1,
sequence_number=1,
role="INTERVIEWER",
content="동시성을 어떻게 제어하나요?",
expected_signal="DB 락/격리수준까지 설명하는지",
),
_answer(2, spec=3.0, logic=3.0, structure="NONE"),
]
transcript = _build_transcript(msgs)
assert "기대 신호(평가 기준): DB 락/격리수준까지 설명하는지" in transcript


def test_feedback_prompt_has_score_anchor_and_slot():
assert "점수 앵커" in SYSTEM_PROMPT
assert "±15" in SYSTEM_PROMPT
Expand Down
2 changes: 2 additions & 0 deletions ai/tests/test_followup_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ async def test_consumer_passes_parent_category_and_history_to_generator():
"mode": "TECHNICAL",
"jobCategory": "BACKEND",
"parentCategory": "PROJECT_DEEP_DIVE",
"parentExpectedSignal": "동시성 제어를 DB 레벨까지 설명하는지",
"history": [
{"role": "INTERVIEWER", "content": "이전 질문"},
{"role": "INTERVIEWEE", "content": "이전 답변"},
Expand All @@ -256,6 +257,7 @@ async def test_consumer_passes_parent_category_and_history_to_generator():

kwargs = generator.generate.await_args.kwargs
assert kwargs["parent_category"] == "PROJECT_DEEP_DIVE"
assert kwargs["expected_signal"] == "동시성 제어를 DB 레벨까지 설명하는지"
assert "이전 질문" in kwargs["history"]
assert "면접관:" in kwargs["history"]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ private MessageItem toItem(InterviewMessage m) {
m.getRole().name(),
m.getContent(),
parentId,
m.getExpectedSignal(),
evaluation
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public void onAnswerSubmitted(AnswerSubmittedEvent event) {
session.getJobCategory(),
contextDocumentIds,
parent.getCategory(),
parent.getExpectedSignal(),
history
);
publisher.publishToAi(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public record MessageItem(
String role,
String content,
Long parentMessageId,
String expectedSignal, // INTERVIEWER 질문에만(평가 기준). 답변은 null
MessageEvaluation evaluation // INTERVIEWEE 답변에만(없으면 null)
) {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public record GenerateFollowupPayload(
JobCategory jobCategory,
List<Long> contextDocumentIds, // RAG(자료 근거/correctness) 용
String parentCategory, // 직전 질문 카테고리 (루브릭 선택)
String parentExpectedSignal, // 직전 질문이 기대하는 핵심(평가 관점) — 충족도 채점용
List<HistoryItem> history // 최근 대화 (중복 회피)
) {
public record HistoryItem(String role, String content) {
Expand Down
12 changes: 10 additions & 2 deletions docs/messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,22 @@
"messageType": "generate.followup",
"payload": {
"sessionId": 99,
"questionMessageId": 501,
"parentMessageId": 501,
"answerMessageId": 502,
"previousQuestion": "...",
"answerText": "...",
"audioS3Key": "session/99/audio/502.webm"
"mode": "TECHNICAL",
"jobCategory": "BACKEND",
"contextDocumentIds": [12, 13],
"parentCategory": "PROJECT_DEEP_DIVE",
"parentExpectedSignal": "동시성 제어를 DB 레벨까지 설명하는지",
"history": [{ "role": "INTERVIEWER", "content": "..." }]
}
}
```

> `parentExpectedSignal` = 직전 질문 생성 시 만든 기대 신호(평가 관점). AI 가 specificity/correctness 채점의 핵심 기준으로 사용(없으면 무시). `contextDocumentIds` 로 RAG 검색 → correctness 판정.

### 5.9 `callback.questions` (꼬리질문)
```json
{
Expand Down