LLM 파라미터 바인딩 문제: 왜 생겼고 어떻게 해결했는가?

Tasker_Jang·2025년 8월 2일
0

🏗️ 전체 시스템 구조부터 이해하기

우선 이 시스템이 어떻게 생겼는지 알아야 합니다:

사용자 → API → LangGraph → 여러 노드들 → 실제 작업

구체적으로는:
1. 사용자: curl -X POST /api/query -d '{"query": "AAPL"}' 요청
2. API: 요청을 받아서 LangGraph에 전달
3. LangGraph: 여러 노드들을 순서대로 실행
4. 노드들: GoogleSearch, USFinancialAnalyzer 등이 각자 작업 수행

🚨 문제 발생: API를 통한 호출에서 에러

API 호출시 에러 발생

# 이렇게 API를 통해 호출하면 에러
curl -X POST "http://localhost:8000/api/query" -d '{"query": "AAPL"}'

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

🔍 문제의 뿌리: API에서의 잘못된 처리

API 코드 살펴보기

# 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"}
# QueryRequest 모델에서 기본값들이 추가되어 이렇게 됨:

request.model_dump() = {
    "query": "AAPL",                    # ← 이게 문제의 원인!
    "model": "gpt-4o-mini",            # ← 정상
    "temperature": 0.2                 # ← 정상
}

llm.bind()가 뭘 하는 건지 이해하기

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
}

🎯 핵심 문제: 왜 다른 노드는 괜찮고 USFinancialAnalyzer만 에러?

다른 노드들의 동작 방식

class GoogleSearchNode:
    def _run(self, state):
        llm = state["llm"]  # 바인딩된 LLM 받음
        
        # 하지만 이 노드는 LLM을 직접 호출하지 않음!
        # 대신 검색 API만 호출하고 끝
        search_results = google_search_api(query)
        return search_results

USFinancialAnalyzer의 동작 방식

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" 파라미터도 함께 전달됨!

💥 에러 발생 과정을 단계별로 추적

1단계: API에서 잘못된 바인딩

# 사용자 요청
{"query": "AAPL"}

# API에서 모든 필드를 LLM에 바인딩
llm.bind(query="AAPL", model="gpt-4o-mini", temperature=0.2)

2단계: LangGraph에서 노드들에 전달

# 모든 노드가 같은 바인딩된 LLM을 받음
state["llm"] = bound_llm  # query="AAPL" 포함

3단계: 노드별 처리

# GoogleSearchNode: LLM 호출 안 함 → 문제없음 ✅
# ReportNode: LLM 호출 안 함 → 문제없음 ✅  
# USFinancialAnalyzer: LLM 호출 함 → 문제발생! ❌

4단계: 실제 에러 발생 지점

# 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'" ❌

🛠️ 해결 방법: Tool에서 깨끗한 LLM 만들기

해결 전략: 왜 API가 아닌 Tool에서 해결했나?

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가 정확히 어떤 파라미터를 받고 거부하는지 알아야합니다!

🟢 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" # 병렬 도구 호출 허용 여부
]

🔴 OpenAI API가 거부하는 파라미터들

사용자 정의 파라미터 (절대 안 됨)

FORBIDDEN_USER_PARAMS = [
    "query",           # 사용자 쿼리 (messages로 전달해야 함)
    "user_input",      # 사용자 입력
    "search_term",     # 검색어
    "prompt",          # 프롬프트 (messages로 전달해야 함)
    "question",        # 질문
    "request",         # 요청 내용
    "input",           # 입력값
    "task",            # 작업 내용
    "instruction",     # 지시사항
    "context",         # 컨텍스트
    "data",            # 데이터
]

잘못된 LangChain/라이브러리 특화 파라미터

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 등에서 사용
]

💡 실제 사용 예시

✅ 올바른 OpenAI API 호출

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}

실제 OpenAI API 호출시

# 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는 자신이 정의한 파라미터만 받고, 나머지는 모두 거부합니다!

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

0개의 댓글