- Phase 12: FeedbackRepository + td_feedback 테이블, Gradio 👍/👎 이벤트, run_id 추적, LangSmith create_feedback() 연동 - Phase 13: 커스텀 _SemanticSplitter 제거 → langchain_experimental.SemanticChunker 교체, buffer_size/threshold_type 환경변수 적용 - Phase 13-B: RerankService (Cross-Encoder), RetrieverService.search()에 reranker 통합, tools.py as_retriever() → search() 전환 - Bug 5: mlx_chat_model enable_thinking 런타임 오버라이드, agent_service stream_mode=["messages","custom"] 이중 스트림, thinking 토큰 custom 이벤트로 emit - ROADMAP: LLM 모델명 8B 반영, RAG에 Reranker 추가, 추천 진행 순서 갱신 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.5 KiB
사고 과정 표시 기능 분석 보고서
테스트 일시: 2026-05-28
테스트 질문: "논문 결과가 어떻게 돼?"
앱 버전: http://localhost:7860
테스트 결과 요약
| 항목 | 사고 과정 OFF | 사고 과정 ON |
|---|---|---|
| 총 소요 시간 | 200.5s | 233.7s |
| 1단계 (질문 분석) | 59.8s | 77.4s |
| 사고 과정 블록 표시 | 없음 | 없음 (버그) |
| 최종 답변 내용 | 6개 섹션, 동일 | 6개 섹션, 동일 |
| 답변 차이 | 없음 | 없음 |
결론: ON/OFF 체크박스가 현재 아무런 시각적 차이를 만들지 않는다.
실제 응답 (두 경우 모두 동일)
[LangGraph → agent: 질문 분석 중] (59.84s)
문서 검색 중... ("어머니의 반응성 상호작용이 아동의 중심축 행동과 지능 및 다중지능 발달에 미치는 영향")
[LangGraph → tools: 도구 실행 중] (71.18s)
[결과: 3개 문서 반환 → agent 복귀]
[문서 검색: "어머니의 반응성 상호작용이 아동의 중심축 행동과 지능 및 다중지능 발달에 미치는 영향"]
→ [문서 1] 1, 81-99 어머니의 반응성 상호작용이 아동의 중심축 행동...
→ [문서 2] 김정미․정은주/ 어머니의반응성상호작용이...
→ [문서 3] 김정미․정은주/ 어머니의반응성상호작용이...
[LangGraph → agent: 검색 결과 반영 중] (132.91s)
[LangGraph → agent: 최종 답변 생성]
본 연구의 결과는 다음과 같이 요약할 수 있습니다:
1. 어머니의 반응성 상호작용과 아동의 중심축 행동 간의 관계
...
사고 과정 ON을 선택했을 때 기대되는 [사고 과정]...[/사고 과정] 블록이 나타나지 않음.
원인 분석
구조적 문제
LLM 생성 흐름:
<think>사고 내용...</think> → 최종 답변 텍스트
↓ ↓
AIMessageChunk AIMessageChunk
content="" content="본 연구의..."
additional_kwargs= additional_kwargs={}
{"thinking": "..."}
핵심 병목: call_model 내부 누적 방식
agent_service.py:111의 call_model 함수는 LLM 청크를 내부에서 모두 누적한 뒤 단일 AIMessage로 반환한다:
async for chunk in llm_with_tools.astream(msgs, config):
thinking_acc += chunk.additional_kwargs.get("thinking", "")
content_acc += chunk.content or ""
...
return {"messages": [AIMessage(content=content_acc, additional_kwargs={"thinking": thinking_acc})]}
LangGraph stream_mode="messages"는 내부 LLM 청크를 외부로 통과시키지만,
사고 청크(content="", additional_kwargs={"thinking":"..."})는
빈 content로 인해 LangGraph 스트림에서 필터링되거나 전달되지 않는 것으로 보인다.
결과적으로 stream_response가 수신하는 청크:
| 수신되는 것 | 수신 안 되는 것 |
|---|---|
content가 있는 AIMessageChunk |
thinking이 있는 AIMessageChunk |
최종 AIMessage (thinking 포함) |
왜 최종 AIMessage의 thinking도 표시 안 되는가
stream_response:221의 조건이 이를 차단한다:
elif node == "agent" and isinstance(chunk, AIMessage):
if not content_started and not thinking_open: # ← content_started=True면 전체 스킵
thinking = chunk.additional_kwargs.get("thinking", "")
if thinking and _think_verbose:
yield "\n[사고 과정]\n"
...
content AIMessageChunk들이 먼저 처리되면서 content_started = True가 세팅됨.
최종 AIMessage가 도착할 때는 이미 content_started=True라 전체 블록이 실행되지 않는다.
적용된 버그 수정 (2026-05-28)
수정 1: agent_service.py:223 — 인스턴스 변수 참조 오류
- if thinking and self._think_verbose: # 항상 False (config 기본값)
+ if thinking and _think_verbose: # 체크박스 값 사용
이 수정은 엣지케이스(content 스트리밍 없이 최종 AIMessage만 도달하는 경우)에서 체크박스를 올바르게 반영한다.
그러나 정상 스트리밍 경로에서는 content_started=True 조건이 여전히 블록을 막는다.
제안하는 추가 수정
stream_response에서 최종 AIMessage의 thinking을 저장해두고,
스트리밍 루프 종료 후 표시하는 방식이 가장 간단하다:
# 루프 내 - AIMessage 처리 시 thinking 저장
elif node == "agent" and isinstance(chunk, AIMessage):
if not thinking_open:
deferred_thinking = chunk.additional_kwargs.get("thinking", "")
if chunk.content and not content_started:
...
# 루프 종료 후
if deferred_thinking and _think_verbose:
yield "\n\n---\n**[사고 과정]**\n\n"
yield deferred_thinking
yield "\n\n**[/사고 과정]**\n"
단, thinking이 답변 뒤에 표시되는 UX 트레이드오프가 있다.
답변 전에 표시하려면call_model을 리팩토링해 thinking을 먼저 스트리밍해야 한다.
소요 시간 비교 참고
ON이 OFF보다 약 33초 더 걸린 점은 주목할 만하다.
enable_thinking=True(config 설정)로 모델이 항상 thinking을 생성하므로,
ON/OFF 간 소요 시간 차이는 모델 비결정성(temperature)에 의한 자연 편차로 보인다.
체크박스는 표시 여부만 제어하며 모델 동작 자체는 바꾸지 않는다.