설계서 3 + 절차서 1. - README.md: 기능 설계서 (15 케이스 corpus, 임계 5/15, 경로 A/B) - fn-corpus_logger.md: optional debug logger (kDebugMode + dart-define 가드) - fn-userTurn_partial_push.md: chat_providers.dart 의 break 분기 수정안 (경로 A/B) - corpus-procedure.md: 빌드/캡처/15 프롬프트/임계 판정 절차 R1-R5 모두 해소 (Architect 채택안). ADR-0006 슬롯 = 경로 B 채택 시 작성 (Developer 단계). Refs #312
6.0 KiB
6.0 KiB
함수 설계서: ChatSessionController.userTurn partial push 분기 (#312)
부모 설계서: ./README.md · 상태: Draft 작성: [AI] Architect · 구현:
app/lib/state/chat_providers.dart:144-153수정 · 테스트:app/test/state/chat_session_prefix_test.dart(신규)
1. 시그니처
변경 없음 (메서드 시그니처 유지):
Future<void> userTurn(String text, BuildContext context) async;
본 설계서는 메서드 내부 event 루프의 LlmFunctionCall 분기만 다룬다.
2. 책임 (단일 책임, 1줄)
Event 루프가 LlmFunctionCall 을 받았을 때, corpus 결과 (≥5/15) 가 prefix 보존을 정당화한 경우에만 accumulated 를 ModelChatMessage 로 push 한 뒤 tool 처리로 break.
3. 입력
| 파라미터 | 타입 | 제약/검증 | 설명 |
|---|---|---|---|
(loop local) accumulated |
String | non-null, 빈 가능 | LlmTextChunk 누적 결과. |
(loop local) event |
LlmFunctionCall | non-null | Gemma 의 함수 호출 이벤트. |
(instance) logger |
CorpusLogger? |
nullable | optional 진단. corpus 단계에서만 활성. |
(instance) state.messages |
List<ChatMessage> |
non-null | 누적 메시지 history. |
4. 출력
- 반환: 없음 (loop 내부 분기).
- 부수효과:
logger?.onFunctionCall(...)(corpus 활성 시).- 경로 A:
state.messages에ModelChatMessage(accumulated)append (단 trim 후 non-empty). - 양 경로 공통:
toolCall = event; break;.
5. 동작 / 알고리즘
경로 A (corpus 결과 ≥5/15 → push 채택)
} else if (event is LlmFunctionCall) {
toolCall = event;
// #312 — corpus 결과 X/15 (≥5) 가 의미있는 prefix → push.
logger?.onFunctionCall(turn, accumulated, event.name, event.args);
final trimmed = accumulated.trim();
if (trimmed.isNotEmpty) {
state = state.copyWith(
messages: [
...state.messages,
ModelChatMessage(trimmed),
],
);
}
break;
}
경로 B (corpus 결과 ≤4/15 → 폐기)
} else if (event is LlmFunctionCall) {
toolCall = event;
// #312 — corpus 측정 결과 X/15 만 의미있는 prefix → 의도적 폐기.
// ADR-0006 (docs/adr/0006-tool-call-prefix-discard.md) 참조.
// accumulated 는 버린다 — 회귀 가드는
// app/test/state/chat_session_prefix_test.dart 의 "폐기 회귀" 테스트.
logger?.onFunctionCall(turn, accumulated, event.name, event.args);
break;
}
logger?.onTextChunk(turn, event.text) 는 LlmTextChunk 분기에 동일하게 추가 (양 경로 공통).
양 경로 공통 추가 사항
- 컨트롤러 생성자에 optional
CorpusLogger? logger추가. - Riverpod provider 가
DebugCorpusLogger.maybeCreate()를 호출해 inject (production 에서는 null).
6. 에러 & 실패 모드
| 조건 | 처리 | 반환/예외 |
|---|---|---|
accumulated.trim() 이 빈 문자열 |
경로 A 의 if 가드 → push 안 함 | 정상 break |
state.copyWith 가 빈 messages 로 호출 |
정상 (no-op equivalent) | 정상 |
| logger 가 throw | logger 구현체 내부에서 swallow (fn-corpus_logger §6) | 정상 |
event.args 가 null |
LlmFunctionCall 계약상 non-null — 발생 시 LlmService 버그. catch 없음 (fail-fast). |
LlmService 단에서 처리 |
7. 엣지케이스
- 빈 prefix 후 tool: accumulated="" → 경로 A 의 trim guard 가 push 차단. ChatScreen 에 빈 버블 노출 안 됨.
- whitespace only prefix (
"\n\n "): trim 후 empty → push 안 함. - prefix 가 multi-turn 루프의 turn 1+ 에서 발생: 첫 turn 에서 tool 호출, 두 번째 turn 에서 LLM 이 또 prefix 후 tool 호출. 이때도 동일 로직 — accumulated 가 turn 별로 reset 되어 있음 (
var accumulated = '';가 for 루프 내부) 이므로 OK. - 마지막 turn 의 prefix + 자연어 종료: tool call 이 안 들어오고
toolCall == null분기로 빠지면 기존 코드가ModelChatMessage(accumulated)push — 본 설계와 무관. - prefix 가 그대로 사용자 입력 echo: 운영 정의상 corpus 에서 N 으로 판정되나 구현은 echo 감지 안 함 (false positive 위험). 코드는 단순 trim/length 만.
8. 복잡도 / 성능
- 시간: O(accumulated.length) for trim. 무시 가능.
- 공간:
ModelChatMessage1개 (trim 된 prefix 길이). - 호출 빈도: tool call 당 1회. userTurn 당 최대
kChatMaxTurns(4) 회.
9. 의존성
- 본 파일 (
chat_providers.dart) 내 sealedChatMessage(UserChatMessage/ModelChatMessage/ToolCallChatMessage). LlmChatEventsealed class (LlmTextChunk/LlmFunctionCall).CorpusLogger?(fn-corpus_logger.md).
10. 테스트 케이스
- 경로 A happy:
- Given: fake LlmService emit
[Text("수면 카탈로그를 보여드릴게요 "), FunctionCall("search_catalog", {"category":"sleep"})]. - When:
userTurn("수면 습관 추천"). - Then:
state.messages의 마지막 3 =[UserChatMessage("수면 습관 추천"), ModelChatMessage("수면 카탈로그를 보여드릴게요"), ToolCallChatMessage("search_catalog", {category:"sleep"}, _)].
- Given: fake LlmService emit
- 경로 A trim guard:
- Given: fake emit
[Text("\n\n "), FunctionCall(...)]. - Then:
state.messages에 ModelChatMessage 추가 안 됨. 마지막 2 =[User, ToolCall].
- Given: fake emit
- 경로 A 빈 prefix:
- Given: fake emit
[FunctionCall(...)](text chunk 없음). - Then: state.messages 마지막 2 =
[User, ToolCall].
- Given: fake emit
- 경로 B 폐기 회귀 (경로 B 채택 시):
- Given: fake emit
[Text("의미있는 한국어 prefix"), FunctionCall(...)]. - Then: state.messages 에 ModelChatMessage 없음 — corpus 결과로 폐기가 정당화됨을 명시적으로 assert.
- Given: fake emit
모든 케이스는 mocked
LlmService+ 실제 ChatSessionController. 실 Gemma 통합은 corpus 수집 (Phase A) 에서 검증.
11. 추적성
- 인수조건: AC2 (조건부 구현), AC3 (단위 테스트).
- 관련 ADR: ADR-0006 (조건부 — 경로 B 채택 시).