145b0cc96f
- 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>
151 lines
5.5 KiB
Markdown
151 lines
5.5 KiB
Markdown
# 사고 과정 표시 기능 분석 보고서
|
|
|
|
**테스트 일시**: 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`로 반환**한다:
|
|
|
|
```python
|
|
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`의 조건이 이를 차단한다:
|
|
|
|
```python
|
|
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` — 인스턴스 변수 참조 오류
|
|
|
|
```diff
|
|
- if thinking and self._think_verbose: # 항상 False (config 기본값)
|
|
+ if thinking and _think_verbose: # 체크박스 값 사용
|
|
```
|
|
|
|
이 수정은 엣지케이스(content 스트리밍 없이 최종 AIMessage만 도달하는 경우)에서 체크박스를 올바르게 반영한다.
|
|
그러나 정상 스트리밍 경로에서는 `content_started=True` 조건이 여전히 블록을 막는다.
|
|
|
|
---
|
|
|
|
## 제안하는 추가 수정
|
|
|
|
`stream_response`에서 최종 `AIMessage`의 thinking을 저장해두고,
|
|
스트리밍 루프 종료 후 표시하는 방식이 가장 간단하다:
|
|
|
|
```python
|
|
# 루프 내 - 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)에 의한 자연 편차로 보인다.
|
|
체크박스는 표시 여부만 제어하며 모델 동작 자체는 바꾸지 않는다.
|