Langchain GPT-OSS 구조화된 출력 트러블슈팅

Updated:

Databricks-gpt-oss

사용한 LLM은 gpt-oss-20b 모델이고 databricks workspace에 통합된 모델을 사용했습니다. huggingface에 업로드된 gpt-oss 모델을 사용해본 경험은 없지만 같은 에러가 발생할 것 같습니다. 만약 똑같은 문제가 일어났다면 아래 해결 방법이 도움될 수 있을 것 같습니다.

from databricks_langchain import ChatDatabricks

llm = ChatDatabricks(
    model='databricks-gpt-oss-20b',
    temperature=0.,
    extra_params={"reasoning_effort": "low"}
)
llm.invoke("hello?")

v1 이전 Structured Output

Langchain이 최근 v1.0으로 업데이트되면서 기존 애플리케이션을 마이그레이션해야 하는 상황에 놓였습니다.

업데이트 이전 Langchain 애플리케이션에선 아래 코드처럼 ResponseSchema를 정의하여 Prompt에 통합하여 사용했습니다.

from langchain.output_parsers import StructuredOutputParser, ResponseSchema

response_schemas = [
    ResponseSchema(
        name="answer",
        description="Answer",
        type="string"
    ),
    ResponseSchema(
        name="reason",
        description="Reason",
        type="string"
    ),
]

parser = StructuredOutputParser.from_response_schemas(response_schemas)

chain = prompt | llm
response = chain.invoke({
    "question": question,
    "format_instruction": parser.get_format_instructions()
})

content = json.loads(response.content)[-1]['text']
parsed_content = parser.parse(content)

v1 이후 Structured Output

최신 Langchain에선 Pydantic BaseModel을 이용해 표준화된 형태로만 사용 가능하도록 변경되었습니다. 아래는 Langchain 공식 문서의 예제입니다.

from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy, ProviderStrategy
from pydantic import BaseModel

class OutputSchema(BaseModel):
    summary: str
    sentiment: str

# Using ToolStrategy
agent = create_agent(
    model="gpt-4o-mini",
    tools=tools,
    # explicitly using tool strategy
    response_format=ToolStrategy(OutputSchema)  
)

gpt-oss의 구조화된 출력 문제

위의 예시처럼 response_format을 구성하면 모델의 출력을 제어할 수 있어 편리한 점이 있었지만 gpt-oss의 경우 제대로 적용되지 않았습니다.

랭체인에선 구조화된 출력을 제어할 수 있도록 langchain.agents.structured_output.ToolStrategy라는 것을 제공하고 있지만 gpt-oss에선 오히려 아래와 같은 에러가 발생하여 제대로 출력되지 않습니다.

BadRequestError: Error code: 400 - {'error_code': 'BAD_REQUEST', 'message': 'Model output is not in expected format, adjust parameters or prompt and try again.\n'}

Solution

이를 해결하기 위해 랭체인에서 제공하는 ToolStrategy를 사용하지 않고 스키마를 직접 작성한 뒤 LLM에 bind하여 해결할 수 있습니다.

from pydantic import BaseModel, Field

class ResponseFormat(BaseModel):
    answer: str = Field("Answer for the question")
    reason: str = Field("Reason for the answer")

response_format = {
    "type": "json_schema",
    "json_schema": {
        "strict": True,
        "name": ResponseFormat.__name__,
        "schema": ResponseFormat.model_json_schema(),
    },
}

llm.bind(response_format=response_format).invoke("hello?")

이를 이용하면 langchain.agents.create_agent에도 사용할 수 있습니다. create_agent의 response_schema를 이용하지 않고 bind된 LLM 모델을 넣어 사용하여 구조화된 출력을 기대할 수 있습니다.

from langchain.agents import create_agent
from langchain.tools import tool

from pydantic import BaseModel, Field

class ResponseFormat(BaseModel):
    answer: str = Field("Answer for the question")
    reason: str = Field("Reason for the answer")

response_format = {
    "type": "json_schema",
    "json_schema": {
        "strict": True,
        "name": ResponseFormat.__name__,
        "schema": ResponseFormat.model_json_schema(),
    },
}

agent = create_agent(
    model=llm.bind(response_format=response_format),
    tools=[],
    system_prompt="You are helpful agent and your output should be in korean",
)

response = agent.invoke({"messages": [{"role": "human", "content": "What is aEtbdwetq?"}]})

import json
json.loads(response['messages'][-1].content)
Output:
[
    {
        'type': 'reasoning',
        'summary': [
            {
                'type': 'summary_text',
                'text': 'Need to respond in Korean, JSON format with answer and reason. Unknown term. Probably explain unknown.'
            }
        ]
    },
    {
        'type': 'text',
        'text': '{
            "answer":"aEtbdwetq는 일반적으로 알려진 용어나 개념이 아니며, 현재로서는 특정한 의미를 파악하기 어렵습니다. 혹시 오타이거나 특정 분야에서 사용되는 전문 용어일 가능성이 있으니, 추가적인 맥락이나 정확한 철자를 알려주시면 보다 정확한 답변을 드릴 수 있습니다.",
            "reason":"사용자가 제시한 용어가 표준 사전이나 일반적인 지식 데이터베이스에 존재하지 않으며, 문맥이 부족해 정확한 정의를 제공하기 어려웠습니다. 따라서 추가 정보를 요청하는 것이 가장 합리적인 접근입니다."
        }'
    }
]

그러나 agent의 tool을 사용하면 이 구조가 깨지는 문제가 또 발생합니다. 이 경우에는 이전 방법처럼 프롬프트에 출력 스키마를 포함시켜 최종 출력에 스키마 구조를 따르도록 지시해야 합니다.

from langchain.agents import create_agent
from langchain.tools import tool

from pydantic import BaseModel, Field

class ResponseFormat(BaseModel):
    answer: str = Field("Answer for the question")
    reason: str = Field("Reason for the answer")
    search: str = Field("Search result")

response_format = {
    "type": "json_schema",
    "json_schema": {
        "strict": True,
        "name": ResponseFormat.__name__,
        "schema": ResponseFormat.model_json_schema(),
    },
}

@tool
def search(query: str):
    "anything related to the query"
    return {"result": "Not found :("}

agent = create_agent(
    model=llm.bind(response_format=response_format),
    tools=[search],
    system_prompt=(
        "You are helpful agent and your output should be in korean."
        "you should to use structed format." 
        "output should be respected:\n{response_format}".format(
            response_format=ResponseFormat.model_json_schema()
        )
    )
)

response = agent.invoke({"messages": [{"role": "human", "content": "What is aEtbdwetq?"}]})

import json
json.loads(response['messages'][-1].content)
Output
{
    'answer': 'aEtbdwetq는 일반적으로 알려진 용어나 개념이 아니며, 검색 결과에서도 해당 단어에 대한 정보를 찾을 수 없었습니다.',
    'reason': '검색 결과가 없었고, 해당 문자열이 특정 약어, 코드, 혹은 오타일 가능성이 높아 명확한 정의를 제공하기 어려웠습니다.',
    'search': 'Not found :('
}

Reference

Comments