우선 이 시스템이 어떻게 생겼는지 알아야 합니다:
사용자 → API → LangGraph → 여러 노드들 → 실제 작업
구체적으로는:
1. 사용자: curl -X POST /api/query -d '{"query": "AAPL"}'
요청
2. API: 요청을 받아서 LangGraph에 전달
3. LangGraph: 여러 노드들을 순서대로 실행
4. 노드들: GoogleSearch, USFinancialAnalyzer 등이 각자 작업 수행
# 이렇게 API를 통해 호출하면 에러
curl -X POST "http://localhost:8000/api/query" -d '{"query": "AAPL"}'
# 결과: "Completions.create() got an unexpected keyword argument 'query'" ❌
# api/route.py
@router.post("/query")
async def process_query(request: QueryRequest, llm: ChatOpenAI):
# 여기가 문제의 시작점!
_llm = llm.bind(**request.model_dump())
# 사용자 요청: {"query": "AAPL"}
# QueryRequest 모델에서 기본값들이 추가되어 이렇게 됨:
request.model_dump() = {
"query": "AAPL", # ← 이게 문제의 원인!
"model": "gpt-4o-mini", # ← 정상
"temperature": 0.2 # ← 정상
}
llm.bind()
는 LLM에 추가 파라미터를 "묶어주는" 기능입니다:
# 원래 LLM
original_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2)
# bind() 후
bound_llm = original_llm.bind(query="AAPL", model="gpt-4o-mini", temperature=0.2)
# 이제 bound_llm.kwargs에는 이게 들어있음:
bound_llm.kwargs = {
"query": "AAPL", # ← 이건 ChatOpenAI가 모르는 파라미터!
"model": "gpt-4o-mini",
"temperature": 0.2
}
class GoogleSearchNode:
def _run(self, state):
llm = state["llm"] # 바인딩된 LLM 받음
# 하지만 이 노드는 LLM을 직접 호출하지 않음!
# 대신 검색 API만 호출하고 끝
search_results = google_search_api(query)
return search_results
class USFinancialAnalyzerNode:
def _run(self, state):
llm = state["llm"] # 바인딩된 LLM 받음 (query 파라미터 포함!)
# 문제: 이 노드는 LLM을 실제로 호출함!
self.tools[0].llm = llm # Tool에 바인딩된 LLM 전달
# Tool 내부에서...
response = self.llm.invoke([HumanMessage(content="AAPL 분석해줘")])
# ↑ 여기서 ChatOpenAI API에 query="AAPL" 파라미터도 함께 전달됨!
# 사용자 요청
{"query": "AAPL"}
# API에서 모든 필드를 LLM에 바인딩
llm.bind(query="AAPL", model="gpt-4o-mini", temperature=0.2)
# 모든 노드가 같은 바인딩된 LLM을 받음
state["llm"] = bound_llm # query="AAPL" 포함
# GoogleSearchNode: LLM 호출 안 함 → 문제없음 ✅
# ReportNode: LLM 호출 안 함 → 문제없음 ✅
# USFinancialAnalyzer: LLM 호출 함 → 문제발생! ❌
# USFinancialStatementTool 내부에서
self.llm.invoke([HumanMessage(content="AAPL 분석해줘")])
# 이게 내부적으로 OpenAI API 호출:
openai.chat.completions.create(
model="gpt-4o-mini",
temperature=0.2,
query="AAPL", # ← 이 파라미터를 OpenAI가 모름!
messages=[{"role": "user", "content": "AAPL 분석해줘"}]
)
# OpenAI API 응답: "unexpected keyword argument 'query'" ❌
Option 1: API 수정 (하지 않은 이유)
# API에서 유효한 파라미터만 바인딩
valid_params = {k: v for k, v in request.model_dump().items()
if k in ['model', 'temperature']}
_llm = llm.bind(**valid_params)
문제점: 다른 노드들이 query
파라미터를 사용할 수도 있어서 위험
Option 2: Tool에서 방어적 처리 (채택한 이유)
# Tool에서만 깨끗한 LLM 재생성
# API 코드는 건드리지 않아서 안전
class USFinancialStatementTool:
@llm.setter
def llm(self, value):
# 받은 LLM을 그대로 사용 → 'query' 파라미터 포함된 더러운 LLM
self._llm = value
class USFinancialStatementTool:
@llm.setter
def llm(self, value):
"""LLM 설정 시 깨끗한 LLM으로 재생성"""
if value is None:
self._llm = None
return
try:
# 1. 원본 LLM에서 기본 설정 추출
original_model = getattr(value, 'model_name', 'gpt-4o-mini')
original_temperature = getattr(value, 'temperature', 0.2)
# 2. 바인딩된 kwargs에서 유효한 것만 추출
bound_kwargs = getattr(value, 'kwargs', {})
clean_model = bound_kwargs.get('model', original_model)
clean_temperature = bound_kwargs.get('temperature', original_temperature)
print(f"더러운 LLM kwargs: {bound_kwargs}")
print(f"깨끗한 파라미터 - model: {clean_model}, temperature: {clean_temperature}")
# 3. 새로운 깨끗한 LLM 생성 (query 등 제외!)
self._llm = ChatOpenAI(
model=clean_model,
temperature=clean_temperature,
openai_api_key=os.getenv("OPENAI_API_KEY")
)
print("✅ 깨끗한 LLM 생성 완료!")
except Exception as e:
print(f"LLM 정리 실패, 기본 LLM 사용: {e}")
# 실패시 안전한 기본 LLM 생성
self._llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.2,
openai_api_key=os.getenv("OPENAI_API_KEY")
)
# Tool이 받은 LLM의 상태
llm.kwargs = {
"query": "AAPL", # ← 이게 OpenAI API로 전달되어 에러!
"model": "gpt-4o-mini",
"temperature": 0.2
}
# OpenAI API 호출시
openai.chat.completions.create(
query="AAPL", # ← OpenAI가 모르는 파라미터
model="gpt-4o-mini",
temperature=0.2,
messages=[...]
)
# → "unexpected keyword argument 'query'" ❌
# Tool이 만든 새로운 깨끗한 LLM
clean_llm.kwargs = {} # 아무것도 없음!
# OpenAI API 호출시
openai.chat.completions.create(
model="gpt-4o-mini", # ← 유효한 파라미터만
temperature=0.2,
messages=[...]
)
# → ✅ 정상 실행, "Apple 65/100점" 결과 생성
따라서..
OpenAI API가 정확히 어떤 파라미터를 받고 거부하는지 알아야합니다!
REQUIRED_PARAMS = [
"model", # 사용할 모델명 (gpt-4o-mini, gpt-4o, gpt-3.5-turbo 등)
"messages" # 대화 메시지 배열 [{"role": "user", "content": "..."}]
]
COMMON_OPTIONAL_PARAMS = [
"temperature", # 0.0-2.0, 응답의 창의성 조절
"max_tokens", # 최대 출력 토큰 수
"top_p", # 0.0-1.0, 누적 확률 샘플링
"frequency_penalty", # -2.0-2.0, 반복 단어 억제
"presence_penalty", # -2.0-2.0, 새로운 주제 촉진
"stop", # 생성 중단할 단어/문구 리스트
"stream", # true/false, 스트리밍 응답 여부
"seed", # 재현 가능한 출력을 위한 시드
"logit_bias", # 특정 토큰 확률 조정
"logprobs", # 로그 확률 반환 여부
"top_logprobs", # 반환할 로그 확률 개수
"n", # 생성할 응답 개수
"user" # 사용자 식별자 (남용 모니터링용)
]
ADVANCED_PARAMS = [
"tools", # 함수 호출 도구 정의
"tool_choice", # 도구 선택 방식
"functions", # (deprecated) 함수 정의
"function_call", # (deprecated) 함수 호출 방식
"response_format", # JSON 모드 등 응답 형식
"parallel_tool_calls" # 병렬 도구 호출 허용 여부
]
FORBIDDEN_USER_PARAMS = [
"query", # 사용자 쿼리 (messages로 전달해야 함)
"user_input", # 사용자 입력
"search_term", # 검색어
"prompt", # 프롬프트 (messages로 전달해야 함)
"question", # 질문
"request", # 요청 내용
"input", # 입력값
"task", # 작업 내용
"instruction", # 지시사항
"context", # 컨텍스트
"data", # 데이터
]
LIBRARY_SPECIFIC_PARAMS = [
"openai_api_key", # LangChain 전용, API 직접 호출시 안 됨
"openai_api_base", # LangChain 전용
"model_name", # LangChain 내부용, API에서는 "model" 사용
"callbacks", # LangChain 콜백
"tags", # LangChain 태그
"metadata", # LangChain 메타데이터
"run_name", # LangChain 실행명
]
OTHER_PLATFORM_PARAMS = [
"anthropic_version", # Anthropic Claude 전용
"system", # Anthropic Claude 전용 (OpenAI는 messages 안에)
"max_tokens_to_sample", # Anthropic Claude 전용
"endpoint", # 기타 플랫폼
"api_version", # Azure OpenAI 등에서 사용
]
import openai
# 정상적인 호출
response = openai.chat.completions.create(
model="gpt-4o-mini", # ✅ 필수
messages=[ # ✅ 필수
{"role": "user", "content": "AAPL 분석해줘"}
],
temperature=0.2, # ✅ 선택적
max_tokens=1000, # ✅ 선택적
top_p=0.9, # ✅ 선택적
stop=["END", "STOP"] # ✅ 선택적
)
# 케이스 1: 사용자 정의 파라미터
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "분석해줘"}],
query="AAPL", # ❌ TypeError!
user_input="재무분석", # ❌ TypeError!
task="financial_analysis" # ❌ TypeError!
)
# 케이스 2: LangChain 전용 파라미터
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "분석해줘"}],
openai_api_key="sk-xxx", # ❌ TypeError!
model_name="gpt-4o-mini", # ❌ TypeError!
callbacks=[] # ❌ TypeError!
)
# 케이스 3: 잘못된 형식
response = openai.chat.completions.create(
model="gpt-4o-mini",
prompt="AAPL 분석해줘", # ❌ messages 써야 함!
temperature=0.2
)
# API에서 이렇게 바인딩됨
request.model_dump() = {
"query": "AAPL", # ❌ OpenAI가 모르는 파라미터
"model": "gpt-4o-mini", # ✅ OpenAI가 아는 파라미터
"temperature": 0.2 # ✅ OpenAI가 아는 파라미터
}
_llm = llm.bind(**request.model_dump())
# 결과: _llm.kwargs = {"query": "AAPL", "model": "gpt-4o-mini", "temperature": 0.2}
# LangChain에서 내부적으로 이렇게 호출
openai.chat.completions.create(
**_llm.kwargs, # 모든 kwargs 전달
messages=[{"role": "user", "content": "Extract ticker from: AAPL"}]
)
# 실제로 전달된 파라미터
{
"query": "AAPL", # ❌ OpenAI: "이게 뭔가요?"
"model": "gpt-4o-mini", # ✅ OpenAI: "알겠습니다"
"temperature": 0.2, # ✅ OpenAI: "알겠습니다"
"messages": [...] # ✅ OpenAI: "알겠습니다"
}
# OpenAI 응답:
# TypeError: create() got an unexpected keyword argument 'query'
OpenAI API 공식 문서에서 정확한 파라미터 목록을 확인할 수 있습니다:
결론: OpenAI API는 자신이 정의한 파라미터만 받고, 나머지는 모두 거부합니다!