ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [OpenAI][LLM 설계] structured output과 function calling을 언제 나누고 JSON 스키마는 어디까지 강제하나
    기타개발지식/풀스택개발 2026. 7. 12. 09:20

    IT 리서치 노트

    [OpenAI][LLM 설계] structured output과 function calling을 언제 나누고 JSON 스키마는 어디까지 강제하나

    LLM 앱에서 JSON schema를 붙이다 보면 어디까지를 structured output으로 묶고, 어디부터를 function calling으로 나눠야 하는지가 금방 헷갈린다. 2026년 7월 12일 기준 OpenAI 공식 문서를 다시 보면 Structured Outputs는 `strict: true`와 `additionalProperties: false`로 최종 응답 계약을 강하게 고정하는 쪽이고, Function calling은 JSON schema로 정의한 tool 입력을 애플리케이션에 넘기는 쪽이다. 여기에 TypeScript reference는 tool-call item을 message만으로 축약하면 다음 요청이 실패할 수 있다고 경고한다. 이 글은 그래서 최종 답변 스키마, 외부 작업 입력 스키마, 자유 형식 custom tool을 어떤 기준으로 나누는 편이 실무적으로 맞는지 정리한 것이다.

    1. 개요

    결론부터 말하면 Structured output은 모델의 최종 답변을 우리 UI나 저장 로직이 바로 소비할 때 쓰는 계약이고, Function calling은 모델이 외부 시스템을 호출하기 위해 애플리케이션에 넘길 입력 계약이다. 둘 다 JSON schema를 쓰지만 책임이 다르다. 최종 답을 고정하려는 문제와 외부 작업 입력을 검증하려는 문제를 한 스키마로 합치면, 장애가 났을 때 어느 층에서 깨졌는지 판단이 늦어진다.

    또 JSON schema를 강하게 걸었다고 해서 function calling 운영이 자동으로 쉬워지지는 않는다. tool item은 별도 타입으로 남겨야 하고, strict한 입력 검증이 중요하면 parallel tool calls 같은 호출 정책도 함께 조정해야 한다. 이미 Structured Outputs 차이 글이 최종 응답 계약을 설명했다면, 이번 글은 그 계약을 function calling과 어디서 끊어야 하는지에 초점을 둔다.

    2. 어디서 실제로 막히는가

    현장에서 가장 흔한 실수는 세 가지다. 첫째, 외부 조회나 변경이 필요한데도 최종 답변 JSON만 예쁘게 만들면 된다고 보고 Structured output 하나로 설계를 시작한다. 둘째, function parameters와 final answer schema를 같은 객체 구조 안에 욱여넣는다. 셋째, tool item을 assistant text 한 덩어리로 저장해 function_call과 function_call_output을 별도 복기 단위로 남기지 않는다.

    이렇게 되면 장애가 났을 때 질문이 뒤섞인다. 지금 실패가 최종 답변 JSON에 필수 필드가 빠진 것인지, 모델이 잘못된 함수 인자를 만든 것인지, 도구 호출은 성공했는데 그 결과를 다시 최종 응답 스키마에 넣는 과정에서 깨진 것인지가 로그 한 항목 안에서 섞인다. JSON schema는 강해졌는데 원인 분리는 오히려 약해지는 이유가 여기에 있다.

    OpenAI Function calling 가이드는 function을 JSON schema로 정의한 tool이라고 설명한다. 반대로 Structured Outputs 가이드는 text.format 또는 response_format으로 최종 응답 구조를 강제하는 흐름을 보여 준다. TypeScript reference는 output items를 message만으로 축약하면 다음 요청에 필요한 reasoning이나 tool-call item이 사라질 수 있다고 경고한다. 이 세 문서를 같이 보면 '최종 응답 계약'과 '외부 작업 입력 계약'과 '대화 item 보존 계약'이 서로 다른 층이라는 점이 분명해진다.

    • 증상: JSON schema를 강하게 걸었는데도 함수 호출 장애 원인이 불명확하다.
    • 실패: 최종 답변 필드와 외부 작업 입력 필드를 한 스키마에 같이 넣는다.
    • 막힘: function_call과 function_call_output을 assistant text 안에 묻어 둔다.
    • 누락: strict한 함수 입력 검증이 필요한데 parallel tool calls 정책을 같이 보지 않는다.
    질문 먼저 써야 할 패턴 이유
    최종 응답을 기계가 바로 읽나 Structured output 최종 JSON 계약을 고정해야 한다
    외부 시스템을 호출하나 Function calling 입력 파라미터를 별도 검증해야 한다
    긴 자유 텍스트 입력이 더 자연스러운가 Custom tool 억지 JSON 포장을 줄일 수 있다

    3. 실무에서 적용하는 순서

    가장 덜 꼬이는 순서는 네 단계다. 먼저 최종 답변이 우리 시스템에서 어디로 흘러가는지 본다. UI 렌더링, 저장, 후속 자동화처럼 기계가 바로 소비하면 Structured output을 붙인다. 두 번째로 모델이 외부 함수나 내부 비즈니스 액션을 호출해야 하는지 본다. 호출이 필요하면 Function calling을 별도 축으로 분리한다. 세 번째로 함수 입력 검증이 정말 엄격해야 하면 strict: true와 additionalProperties: false를 함수 parameters에 걸고, 동시에 parallel tool calls 정책을 보수적으로 가져간다. 마지막으로 대화 history를 직접 관리하는 경우 output items를 타입 그대로 보존한다.

    1. 최종 응답이 기계 소비 대상이면 Structured output부터 정한다.
    2. 외부 조회·변경이 있으면 Function calling을 별도 축으로 둔다.
    3. strict 입력 검증이 중요하면 parallel tool calls를 같이 조정한다.
    4. tool-call item은 message text로 축약하지 않고 타입 그대로 남긴다.

    이 순서가 좋은 이유는 실패 의미를 서로 다른 로그로 분리할 수 있기 때문이다. Structured output 실패는 보통 최종 answer schema mismatch다. Function calling 실패는 함수 이름 선택, 인자 구조, 호출 순서, 외부 시스템 응답 축으로 갈린다. history 보존 실패는 reasoning item이나 tool-call item 축약에서 온다. 세 실패를 같은 필드 집합으로 기록하면 복구가 느려진다.

    운영 메모 예시
    final_answer_schema=invoice_summary_v3
    tool_input_schema=lookup_customer_v2
    parallel_tool_calls=false
    tool_item_log=function_call,function_call_output
    final_answer_validator=strict_json_schema

    특히 고객 조회, 재고 확인, 결제 실행처럼 외부 상태를 읽거나 바꾸는 단계는 function calling으로 빼고, 그 결과를 사용자에게 보여 주는 최종 요약만 structured output으로 받는 편이 가장 안정적이다. 반대로 함수 호출이 없고 최종 답변만 곧바로 저장하거나 화면에 뿌리면 structured output만으로 충분할 때가 많다. custom tool은 여기에 넣기 애매한 긴 텍스트 질의나 CFG 기반 자유 형식 입력이 필요할 때 검토하면 된다.

    • 최종 응답 JSON과 함수 입력 JSON은 다른 계약으로 기록한다.
    • tool 호출이 있는 경우 item 타입 보존을 운영 기본값으로 둔다.
    • strict한 함수 입력 검증은 호출 정책과 함께 설계한다.

    핵심은 JSON schema를 하나 더 붙이는 일이 아니라, 어떤 계약이 어떤 실패를 설명하는지 먼저 자르는 일이다. 이 경계가 선명해야 API 응답과 tool orchestration을 함께 다루는 앱에서도 디버깅 시간이 짧아진다.

    4. 공식 문서와 예시 화면으로 확인하기

    첫 자료는 OpenAI Structured Outputs 가이드의 JSON Schema 팁 구간이다. 여기서는 `strict: true`와 `additionalProperties: false`를 실제 응답 계약에 어떻게 걸어야 하는지 한 번에 보여 준다. 오늘 글에서 중요한 점은 이 스키마가 '모델의 최종 응답을 우리 시스템이 바로 소비할 때' 쓰는 계약이라는 점이다.

    Structured Outputs 가이드는 응답 스키마를 `strict: true`와 `additionalProperties: false`로 명시해 최종 응답 계약을 강하게 고정하는 패턴을 보여 준다.
    Structured Outputs 가이드는 응답 스키마를 `strict: true`와 `additionalProperties: false`로 명시해 최종 응답 계약을 강하게 고정하는 패턴을 보여 준다.

    즉 이 스키마는 답변 JSON의 모양을 고정하는 층이다. 외부 시스템을 실제로 호출하는 단계까지 이 스키마 하나로 해결하려고 하면, 호출 입력 검증과 최종 응답 검증이 한 덩어리로 섞여 장애 지점이 불명확해진다.

    두 번째 자료는 Function calling 가이드의 핵심 정의다. OpenAI는 function을 JSON schema로 정의된 tool이라고 설명하고, 그 함수 입력을 모델이 애플리케이션에 넘기게 만든다. 즉 함수 스키마는 최종 답변 모양이 아니라 외부 작업 입력 계약이다.

    Function calling 문서는 function tool을 JSON schema로 정의된 외부 작업 입력 계약으로 설명한다.
    Function calling 문서는 function tool을 JSON schema로 정의된 외부 작업 입력 계약으로 설명한다.

    이 차이를 분리해야 한다. Structured output은 UI나 저장 파이프라인이 읽을 최종 답을 고정하는 층이고, function calling은 모델이 애플리케이션에 넘길 작업 입력을 고정하는 층이다. 둘 다 JSON schema를 쓰지만 실패 의미가 다르다.

    세 번째 자료는 function calling 가이드의 parallel tool calls 관련 구간이다. OpenAI는 strict schema adherence가 중요한 경우 parallel tool calls를 끄라고 안내한다. 여러 함수를 한 번에 병렬로 고르게 두면, 어떤 스키마 실패가 어느 호출에서 났는지 추적 비용이 커지기 때문이다.

    Function calling 가이드는 strict한 함수 입력 검증이 중요할 때 parallel tool calls를 비활성화하는 선택지를 보여 준다.
    Function calling 가이드는 strict한 함수 입력 검증이 중요할 때 parallel tool calls를 비활성화하는 선택지를 보여 준다.

    따라서 함수 호출이 핵심인 워크플로우에서는 '최종 응답 JSON을 더 엄격하게 만들자'보다 먼저 '도구 호출을 한 번에 몇 개 허용할지, 한 호출이 끝난 뒤 다음 호출을 열지'를 정하는 편이 낫다. 이 축을 잘못 잡으면 스키마를 강하게 걸어도 운영은 더 어려워진다.

    네 번째 자료는 OpenAI TypeScript reference의 multi-turn conversations 경고다. 여기서는 `response.output`에서 message만 남기도록 필터링하면 reasoning 또는 tool-call item이 빠져 다음 요청이 실패할 수 있다고 명시한다. 즉 tool item을 단순 텍스트 뒤에 숨기면 안 된다는 뜻이다.

    TypeScript reference는 tool-call과 reasoning item을 message 텍스트만으로 축약하면 다음 요청이 실패할 수 있다고 경고한다.
    TypeScript reference는 tool-call과 reasoning item을 message 텍스트만으로 축약하면 다음 요청이 실패할 수 있다고 경고한다.

    이 경고는 오늘 주제와 직접 닿아 있다. 최종 응답 JSON 계약을 잘 만들었더라도, tool item을 별도 타입으로 보존하지 않으면 함수 호출 복기와 재시도 판단이 어려워진다. 스키마 강제 범위를 나눌 때 item 보존 범위도 같이 나눠야 한다.

    실무에서는 이 경계를 비교표로 먼저 고정해 두는 편이 좋다. 어떤 JSON schema가 최종 응답 계약인지, 어떤 JSON schema가 외부 작업 입력 계약인지, 어떤 경우에 custom tool처럼 자유 형식을 허용할지를 한 장으로 정리하면 설계가 흔들리지 않는다.

    Structured output, function calling, custom tool의 역할과 스키마 강제 범위를 나눈 비교표다.
    Structured output, function calling, custom tool의 역할과 스키마 강제 범위를 나눈 비교표다.

    이 표처럼 보면 같은 JSON schema라도 목적이 다르다는 점이 눈에 들어온다. 이미 JSON mode와 Structured Outputs 차이 글이 최종 응답 계약에 초점을 뒀다면, 이번 글은 함수 입력 계약과의 경계까지 확장하는 후속편이다.

    마지막 자료는 응답 계약과 도구 계약을 분리한 요청 예시다. 한 요청 안에 두 계약이 같이 있더라도 필드 의미를 다르게 적어 두면, 어디서 실패했는지 로그가 빨리 갈린다.

    최종 응답 스키마와 tool 입력 스키마를 분리해 적는 Responses 요청 예시다.
    최종 응답 스키마와 tool 입력 스키마를 분리해 적는 Responses 요청 예시다.

    이 형태를 유지하면 built-in tool이나 manual function call을 섞을 때도 경계를 잃지 않는다. 특히 built-in tools 구조 글, function_call_output 검증 글과 바로 이어 읽기 좋다.

    5. 주의사항과 리스크

    첫 번째 리스크는 Structured output을 강하게 걸어 두고 function calling 문제까지 같은 층에서 해결할 수 있다고 기대하는 것이다. 두 번째 리스크는 함수 입력과 최종 응답을 같은 schema version으로 묶어 배포마다 동시에 흔드는 것이다. 세 번째 리스크는 tool item을 message text만 남기도록 축약해 다음 turn failure와 감사 로그 누락을 동시에 만드는 것이다.

    운영 전에 확인할 때는 최소한 final_answer_schema, tool_input_schema, parallel_tool_calls, tool_item_log, history_replay_mode 다섯 칸을 같은 표에 남겨 두는 편이 좋다. 이 다섯 칸이 없으면 스키마가 강해졌는데도 실제 장애가 어느 층인지 다시 추적해야 한다.

    • Structured output은 최종 답변 계약이다.
    • Function calling은 외부 작업 입력 계약이다.
    • 대화 item 보존은 또 다른 운영 계약이다.

    6. 결론

    OpenAI 앱 설계에서 structured output과 function calling은 같은 JSON schema 도구를 쓰더라도 역할이 다르다. 최종 답변을 기계가 읽는다면 structured output을, 외부 작업 입력을 검증해야 한다면 function calling을, 자유 형식 입력이 더 자연스럽다면 custom tool을 고르는 편이 맞다. 이 경계를 먼저 나눠야 스키마는 더 엄격해지고 운영 복기는 오히려 더 쉬워진다.

    • 최종 응답 계약과 도구 입력 계약을 분리한다.
    • strict 스키마와 호출 정책을 함께 설계한다.
    • tool item은 텍스트로 축약하지 않고 타입 그대로 남긴다.

    7. 참고 링크

    1. https://developers.openai.com/api/docs/guides/structured-outputs
    2. https://developers.openai.com/api/docs/guides/function-calling
    3. https://developers.openai.com/api/reference/typescript/
Designed by Tistory.