"Completions.create() got an unexpected keyword argument 'query'"
지난 시간에 점수화 시스템을 완성했지만, 실제 배포 환경에서 예상치 못한 문제가 터져나왔습니다. 특히 API 레이어와 Tool 레이어 간의 LLM 파라미터 전달 과정에서 문제가 발생하면서 전체 시스템이 먹통이 되는 상황이 발생했어요.
오늘은 이런 레이어 간 데이터 전달 시 발생하는 문제를 어떻게 해결했는지, 그리고 Langfuse 트레이싱을 통해 디버깅하는 과정을 공유하겠습니다!
# 단독 실행시에는 완벽하게 작동
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에서 완전한 트레이스를 분석해보니:
🎯 발견된 문제:
- LangGraph: ✅ 정상 실행
- Google Search: ✅ 뉴스 수집 성공
- Report Assistant: ✅ 보고서 생성 성공
- US Financial Analyzer: ❌ ChatOpenAI LLM 호출 실패
이상한 점: 다른 노드들은 ChatOpenAI 에러가 없는데, 왜 내 노드만?
트레이스를 자세히 보니 문제의 핵심을 발견했습니다:
// 정상 작동하는 노드들의 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에 바인딩되어 있었습니다!
@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로 전달되어 에러!
request.model_dump()로 모든 필드 바인딩query 매개변수도 함께 전달"Completions.create() got an unexpected keyword argument 'query'" 에러API 코드를 수정하면 다른 부분에 영향을 줄 수 있으니, Tool에서만 방어적으로 처리하기로 했습니다.
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()
# Before: 더러운 LLM (query 포함)
"kwargs": {"query": "AAPL", "model": "gpt-4o-mini", "temperature": 0.2}
# After: 깨끗한 LLM (유효한 매개변수만)
"kwargs": {"model": "gpt-4o-mini", "temperature": 0.2}
<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)
이제 에러 없는 완전한 워크플로우를 볼 수 있습니다:

✅ LangGraph 실행: 28초
✅ US Financial Analyzer: LLM 자동 정리로 성공!
✅ Google Search: 뉴스 수집 성공
✅ Report Assistant: 종합 보고서 생성 성공
# 나쁜 예: 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) # 정리 후 사용
문제: 상위 레이어의 "편의상" 처리가 하위 레이어에 부작용을 일으킴
Option 1: API 레벨에서 수정
Option 2: Tool 레벨에서 방어적 처리 ✅
선택 기준: 안정성 > 성능, 기존 코드 보존
결론: 복잡한 시스템에서는 각 레이어가 독립적으로 안전하게 동작할 수 있도록 설계하는 것이 중요합니다. 특히 외부 API 호출이 있는 Tool 레벨에서는 상위 레이어의 데이터를 맹신하지 말고 방어적으로 정리해서 사용해야 합니다! 🛡️