Alpha Vantage API에서 미국 주식 분석기 만들기 12)

Tasker_Jang·2025년 7월 5일
0

"Completions.create() got an unexpected keyword argument 'query'"

지난 시간에 점수화 시스템을 완성했지만, 실제 배포 환경에서 예상치 못한 문제가 터져나왔습니다. 특히 API 레이어와 Tool 레이어 간의 LLM 파라미터 전달 과정에서 문제가 발생하면서 전체 시스템이 먹통이 되는 상황이 발생했어요.

오늘은 이런 레이어 간 데이터 전달 시 발생하는 문제를 어떻게 해결했는지, 그리고 Langfuse 트레이싱을 통해 디버깅하는 과정을 공유하겠습니다!

🚨 문제 상황: API 레이어의 잘못된 파라미터 바인딩

지난 편까지의 성과

# 단독 실행시에는 완벽하게 작동
python -c "from src.tools.us_stock.tool import USFinancialStatementTool; print(USFinancialStatementTool()._run('AAPL'))"

# 결과: 65/100점 완벽한 분석 ✅

하지만 LangGraph supervisor를 통한 호출에서는 완전히 다른 결과가...

# Supervisor를 통한 호출  
curl -X "POST" "http://localhost:8000/api/query" -d '{"query": "AAPL"}'

# 결과: "Completions.create() got an unexpected keyword argument 'query'" ❌

🔍 Langfuse로 정확한 에러 위치 추적

트레이스 분석 결과

Langfuse에서 완전한 트레이스를 분석해보니:

🎯 발견된 문제:
- LangGraph: ✅ 정상 실행
- Google Search: ✅ 뉴스 수집 성공  
- Report Assistant: ✅ 보고서 생성 성공
- US Financial Analyzer: ❌ ChatOpenAI LLM 호출 실패

이상한 점: 다른 노드들은 ChatOpenAI 에러가 없는데, 왜 내 노드만?

Langfuse 로그 상세 분석

트레이스를 자세히 보니 문제의 핵심을 발견했습니다:

// 정상 작동하는 노드들의 LLM
"llm": {
  "kwargs": {"model": "openai/gpt-4o-mini", "temperature": 0.2}
}

// 문제가 되는 노드의 LLM  
"llm": {
  "kwargs": {"query": "AAPL", "model": "openai/gpt-4o-mini", "temperature": 0.2}
}

핵심 발견: query 매개변수가 LLM에 바인딩되어 있었습니다!

🔍 근본 원인 분석: API Route의 잘못된 바인딩

문제의 발단: api/route.py

@router.post("/query")
async def process_query(request: QueryRequest, llm: ChatOpenAI):
    # ❌ 문제가 되는 코드
    _llm = llm.bind(**request.model_dump())
    
    # request.model_dump() 결과:
    # {"query": "AAPL", "model": "gpt-4o-mini", "temperature": 0.2}
    # 모든 필드가 LLM에 바인딩됨!

왜 다른 노드는 괜찮았을까?

다른 노드들: 바인딩된 LLM을 그대로 사용

# 일반적인 노드
class GoogleSearchNode:
    def _run(self, state):
        llm = state["llm"]  # 바인딩된 LLM 그대로 사용
        # LLM을 직접 호출하지 않아서 문제 없음

US Financial Analyzer: Tool 내부에서 별도 LLM 호출

# 내가 구현한 노드
class USFinancialAnalyzerNode:
    def _run(self, state):
        llm = state["llm"]  # 바인딩된 LLM 받음
        self.tools[0].llm = llm  # Tool에 전달
        
        # Tool 내부에서 LLM 호출
        response = self.llm.invoke([HumanMessage(content=prompt)])
        # 여기서 'query' 매개변수가 OpenAI API로 전달되어 에러!

에러 발생 과정

  1. API Route: request.model_dump()로 모든 필드 바인딩
  2. Node: 바인딩된 LLM을 Tool에 전달
  3. Tool: LLM 호출 시 query 매개변수도 함께 전달
  4. OpenAI API: "Completions.create() got an unexpected keyword argument 'query'" 에러

🛠️ 해결책: Tool 레벨에서 방어적 LLM 정리

핵심 아이디어: API는 건드리지 말고 Tool에서 해결

API 코드를 수정하면 다른 부분에 영향을 줄 수 있으니, Tool에서만 방어적으로 처리하기로 했습니다.

1. LLM Setter에서 파라미터 정리

class USFinancialStatementTool(BaseTool):
    _llm = None

    @property
    def llm(self):
        return self._llm

    @llm.setter
    def llm(self, value):
        """LLM 설정 시 깨끗한 LLM으로 재생성"""
        if value is None:
            self._llm = None
            return
            
        try:
            # 원본 LLM 설정 추출
            original_model = getattr(value, 'model_name', 'gpt-4o-mini')
            original_temperature = getattr(value, 'temperature', 0.2)
            original_base_url = getattr(value, 'openai_api_base', None)
            original_api_key = getattr(value, 'openai_api_key', None)
            
            # 바인딩된 kwargs에서 유효한 것만 추출
            bound_kwargs = getattr(value, 'kwargs', {})
            clean_model = bound_kwargs.get('model', original_model)
            clean_temperature = bound_kwargs.get('temperature', original_temperature)
            
            print(f"Creating clean LLM - model: {clean_model}, temperature: {clean_temperature}")
            
            # ✅ 깨끗한 LLM 생성 (query 등의 잘못된 매개변수 제외)
            clean_llm_params = {
                "model": clean_model,
                "temperature": clean_temperature,
                "openai_api_key": original_api_key or os.getenv("OPENAI_API_KEY"),
            }
            
            # base_url 설정
            if original_base_url:
                clean_llm_params["base_url"] = original_base_url
            elif os.getenv("OPENAI_BASE_URL"):
                clean_llm_params["base_url"] = os.getenv("OPENAI_BASE_URL")
                
            # OpenRouter 헤더 (필요한 경우)
            base_url = clean_llm_params.get("base_url", "")
            if "openrouter" in base_url.lower():
                clean_llm_params["default_headers"] = {
                    "HTTP-Referer": os.getenv("HTTP_REFERER", "http://localhost:8000"),
                    "X-Title": os.getenv("X_TITLE", "Market Analysis Team"),
                }
            
            self._llm = ChatOpenAI(**clean_llm_params)
            print(f"Successfully created clean LLM: {type(self._llm).__name__}")
            
        except Exception as e:
            print(f"Error creating clean LLM, falling back to default: {e}")
            # 실패 시 기본 LLM 생성
            self._llm = self._create_default_llm()

2. 핵심 해결 과정

# Before: 더러운 LLM (query 포함)
"kwargs": {"query": "AAPL", "model": "gpt-4o-mini", "temperature": 0.2}

# After: 깨끗한 LLM (유효한 매개변수만)  
"kwargs": {"model": "gpt-4o-mini", "temperature": 0.2}

🎉 수정 후 완벽한 결과

ChatOpenAI 에러 완전 해결!

<curl -X "POST" "http://localhost:8000/api/query" \
  -H "Content-Type: application/json" \
  -d '{"query":"AAPL"}'

결과:

### Financial Statement Analysis for Apple Inc (Ticker: AAPL)

## 📊 **Overall Financial Health Score**
- **Total Score**: 65/100 points (Grade: B)
- **Profitability Score**: 100/100 points (Grade: A+)
- **Stability Score**: 13/100 points (Grade: D)

#### **Profitability Metrics:**
- **ROE**: 138.0% | **Score**: 100/100 (Exceptional)
- **ROA**: 23.8% | **Score**: 100/100 (Excellent)  
- **Operating Margin**: 31.51% | **Score**: 100/100 (Elite)

Langfuse 트레이스 완성

이제 에러 없는 완전한 워크플로우를 볼 수 있습니다:

✅ LangGraph 실행: 28초
✅ US Financial Analyzer: LLM 자동 정리로 성공!
✅ Google Search: 뉴스 수집 성공
✅ Report Assistant: 종합 보고서 생성 성공

💡 핵심 교훈과 설계 원칙

1. 방어적 프로그래밍의 중요성

# 나쁜 예: API 레이어를 완전히 신뢰
def set_llm(self, llm):
    self._llm = llm  # 그대로 사용 → 에러 위험

# 좋은 예: Tool 레벨에서 방어적 처리
def set_llm(self, llm):
    if self._is_llm_clean(llm):
        self._llm = llm
    else:
        self._llm = self._create_clean_llm(llm)  # 정리 후 사용

2. 레이어 간 데이터 전달의 함정

  • API 레이어: 사용자 요청의 모든 데이터 처리
  • Node 레이어: 워크플로우 조정
  • Tool 레이어: 실제 작업 수행

문제: 상위 레이어의 "편의상" 처리가 하위 레이어에 부작용을 일으킴

3. 디버깅 도구의 가치

  • Langfuse 트레이싱 없었다면 이 문제를 찾기 매우 어려웠을 것
  • 레이어별 데이터 흐름을 시각적으로 추적 가능
  • 다른 노드와의 비교를 통해 문제 패턴 발견

4. 해결책 선택의 철학

Option 1: API 레벨에서 수정

  • 장점: 근본 원인 해결
  • 단점: 다른 부분에 영향 가능성

Option 2: Tool 레벨에서 방어적 처리 ✅

  • 장점: 기존 코드 안전, 확장성 좋음
  • 단점: 약간의 성능 오버헤드

선택 기준: 안정성 > 성능, 기존 코드 보존


결론: 복잡한 시스템에서는 각 레이어가 독립적으로 안전하게 동작할 수 있도록 설계하는 것이 중요합니다. 특히 외부 API 호출이 있는 Tool 레벨에서는 상위 레이어의 데이터를 맹신하지 말고 방어적으로 정리해서 사용해야 합니다! 🛡️

profile
ML Engineer 🧠 | AI 모델 개발과 최적화 경험을 기록하며 성장하는 개발자 🚀 The light that burns twice as bright burns half as long ✨

0개의 댓글