<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>워크플로우 노트</title>
    <link>https://ai-imojumo.tistory.com/</link>
    <description>최신 AI 기술을 직접 재현하고 실무 적용까지 정리하는 블로그</description>
    <language>ko</language>
    <pubDate>Fri, 8 May 2026 12:56:02 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>이천재</managingEditor>
    <image>
      <title>워크플로우 노트</title>
      <url>https://tistory1.daumcdn.net/tistory/6955175/attach/9e39538900db48069367374e6616c248</url>
      <link>https://ai-imojumo.tistory.com</link>
    </image>
    <item>
      <title>n8n Cloud 메모리 오류 진단: 플랜 업그레이드 전에 Code 노드&amp;middot;Merge&amp;middot;실행 저장을 먼저 줄여야 한다</title>
      <link>https://ai-imojumo.tistory.com/entry/n8n-Cloud-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%98%A4%EB%A5%98-%EC%A7%84%EB%8B%A8-%ED%94%8C%EB%9E%9C-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%EC%A0%84%EC%97%90-Code-%EB%85%B8%EB%93%9C%C2%B7Merge%C2%B7%EC%8B%A4%ED%96%89-%EC%A0%80%EC%9E%A5%EC%9D%84-%EB%A8%BC%EC%A0%80-%EC%A4%84%EC%97%AC%EC%95%BC-%ED%95%9C%EB%8B%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;n8n Cloud에서 &lt;code&gt;Workflow did not finish, possible out-of-memory issue&lt;/code&gt;가 나오면 플랜 업그레이드나 self-hosting 전환부터 떠올리기 쉽다. 먼저 볼 지점은 따로 있다. 한 번의 실행에 얼마나 많은 JSON, binary-like payload, Code node 입력, Merge 후보, 실행 저장 데이터가 같이 실리는지부터 줄여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;2026-05-06&lt;/code&gt;에 n8n 공식 문서 경계를 다시 확인했다. Cloud data management docs 기준 Trial과 Starter는 &lt;code&gt;320MiB&lt;/code&gt;, Pro-1은 &lt;code&gt;640MiB&lt;/code&gt;, Pro-2는 &lt;code&gt;1280MiB&lt;/code&gt;, Enterprise는 &lt;code&gt;4096MiB&lt;/code&gt; RAM으로 적혀 있고, n8n 자체도 평균 &lt;code&gt;180MiB&lt;/code&gt; 정도를 쓴다고 설명한다. 같은 문서는 큰 데이터를 작은 chunk로 나누고, 가능한 곳에서는 Code node를 피하고, 큰 데이터는 manual execution으로 돌리지 말고, sub-workflow가 parent workflow에 작은 결과만 돌려주게 하라고 안내한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커뮤니티 글은 수요 신호로만 썼다. 2026-05-01 r/n8n에는 n8n Cloud workflow가 &lt;code&gt;possible out-of-memory issue&lt;/code&gt;로 멈추고 반복 crash 뒤 자동 비활성화까지 걱정된다는 글이 올라왔다. Threads 공개 검색에서는 usable visible result를 찾지 못했다. n8n 동작과 수치, node 기능은 Reddit 댓글이 아니라 n8n 공식 docs로만 잠갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 실험은 live n8n Cloud가 아니라 deterministic local harness로 했다. 18개 synthetic execution을 bulk all-items route와 bounded batch/sub-workflow route로 나눠 돌렸다. 결과는 이렇다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;metric&lt;/th&gt;
&lt;th&gt;bulk all-items route&lt;/th&gt;
&lt;th&gt;bounded batch/sub-workflow route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;executions&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;processed&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OOM failures&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local deactivation-guard skips&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retry attempts&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max estimated peak MiB&lt;/td&gt;
&lt;td&gt;665.23&lt;/td&gt;
&lt;td&gt;181.38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;saved execution MiB total&lt;/td&gt;
&lt;td&gt;16.38&lt;/td&gt;
&lt;td&gt;0.72&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 숫자를 n8n Cloud benchmark로 읽으면 안 된다. 실제 계정, 외부 DB, browser, IMAP, binary storage, LLM API를 호출하지 않았다. 대신 memory pressure가 어디서 커지는지 재현 가능한 모양으로 줄였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커뮤니티 신호: 문제는 실행 한 건이 너무 커지는 쪽이었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주제의 출발점은 &quot;n8n Cloud가 갑자기 나빠졌다&quot;가 아니다. 공개 Reddit 글에서 보인 운영자의 질문은 더 구체적이었다. workflow가 멈췄고, 다시 시도하면 되는 실행도 있지만, 몇몇 workflow는 반복 crash 뒤 다시 켜야 하는 상태가 됐다는 불안이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 중에는 plan limit이나 특정 버전 변화를 단정하는 내용도 있었다. 그 부분은 사실 근거로 쓰지 않았다. 사용자가 실제로 막힌 지점만 demand signal로 남긴다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;demand signal&lt;/th&gt;
&lt;th&gt;글에서 다룰 범위&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;n8n Cloud에서 OOM 비슷한 오류가 반복됨&lt;/td&gt;
&lt;td&gt;한 execution에 실리는 data envelope를 줄이는 순서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재시도해도 같은 문제가 돌아옴&lt;/td&gt;
&lt;td&gt;같은 큰 입력을 다시 보내지 않는 replay ID와 batch gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;자동 비활성화가 걱정됨&lt;/td&gt;
&lt;td&gt;official behavior 단정 없이 반복 crash를 줄이는 운영 gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;plan upgrade가 답인지 모르겠음&lt;/td&gt;
&lt;td&gt;줄일 수 있는 surface를 먼저 줄인 뒤 upgrade 판단&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구분이 중요하다. 커뮤니티 글은 &quot;왜 지금 이 문제를 쓰는가&quot;를 설명한다. 기술 설명은 official docs와 local experiment가 맡는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;n8n 공식 문서로 확인한 줄일 수 있는 지점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n Cloud data management docs는 memory usage와 data storage를 분리해서 본다. 복잡한 workflow가 많은 데이터를 처리하면 memory limit을 넘고 instance가 crash될 수 있으며, execution setting과 volume에 따라 database도 커질 수 있다고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 지점은 payload size다. memory-related errors docs는 n8n이 각 node가 fetch하고 process하는 data amount를 제한하지 않는다고 설명한다. JSON data 양, binary data 크기, workflow node 수, Code node, manual execution, 동시에 도는 workflow 수가 memory usage를 키울 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 Code node다. Code node docs에는 두 mode가 있다. &lt;code&gt;Run Once for All Items&lt;/code&gt;는 input item 수와 관계없이 한 번 실행된다. &lt;code&gt;Run Once for Each Item&lt;/code&gt;은 item마다 실행된다. 둘 중 무엇이 맞는지는 workflow마다 다르지만, 큰 rows를 한 번에 들고 계산하는 Code node는 먼저 의심해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째는 Loop Over Items와 sub-workflow 경계다. Loop Over Items node는 predefined amount of data를 loop output으로 내보내고, 실행이 끝나면 processed data를 done output으로 합친다. Execute Sub-workflow node는 parent workflow에서 다른 workflow를 호출하고 마지막 node의 data를 다시 parent로 돌려준다. Cloud data management docs는 heavy lifting을 batch별 sub-workflow 안에 두고 parent에는 작은 result set만 돌려주는 방식을 memory reduction 예로 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네 번째는 Merge node다. Merge docs는 여러 stream의 data를 합치는 node라고 설명한다. 이 글에서는 Merge가 나쁘다고 말하지 않는다. 다만 rows가 많은 두 stream을 넓게 합치면 실행 한 건 안에서 비교 후보가 커진다. 따라서 Merge 전에 후보를 줄이거나 batch를 나누는 쪽이 더 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다섯 번째는 execution data다. Cloud docs는 큰 data를 처리하는 workflow가 testing stage를 지났다면 successful execution 저장을 끄는 편이 좋다고 설명한다. Execution data docs도 execution setting과 volume에 따라 database가 커질 수 있으니 unnecessary data를 저장하지 말고 pruning을 켜라고 한다. Workflow settings에는 failed/successful/manual execution 저장과 execution progress 저장 설정이 따로 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬 실험: bulk route와 bounded route 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험 script는 &lt;code&gt;experiments/wn121_n8n_cloud_memory_gate.py&lt;/code&gt;다. 입력은 3개 workflow, 18개 synthetic execution이다. 각 execution에는 rows, 평균 JSON 크기, binary-like item 수, 평균 binary 크기, Merge reference rows, success/progress 저장 여부를 넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교한 route는 두 가지다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;route&lt;/th&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;실패 모양&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bulk all-items&lt;/td&gt;
&lt;td&gt;rows와 binary-like payload를 parent workflow가 계속 들고, Code all-items 처리와 broad Merge lookup, success/progress 저장이 한 실행에 붙음&lt;/td&gt;
&lt;td&gt;simulated Starter RAM &lt;code&gt;320MiB&lt;/code&gt;를 넘으면 &lt;code&gt;failed_oom&lt;/code&gt;, 같은 workflow가 3회 crash하면 local guard가 skip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bounded batch/sub-workflow&lt;/td&gt;
&lt;td&gt;200 rows batch, binary는 summary처럼 작게 처리, Merge reference rows를 줄이고 parent에는 작은 result만 반환&lt;/td&gt;
&lt;td&gt;batch peak가 limit을 넘을 때만 manual review로 보낼 수 있게 설계&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 bulk route가 먼저 무너졌다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;th&gt;bulk all-items&lt;/th&gt;
&lt;th&gt;bounded batch/sub-workflow&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;processed executions&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;failed OOM&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;skipped by local crash guard&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rows processed&lt;/td&gt;
&lt;td&gt;6780&lt;/td&gt;
&lt;td&gt;75660&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max estimated peak MiB&lt;/td&gt;
&lt;td&gt;665.23&lt;/td&gt;
&lt;td&gt;181.38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;saved execution MiB total&lt;/td&gt;
&lt;td&gt;16.38&lt;/td&gt;
&lt;td&gt;0.72&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bulk route에서 가장 큰 peak는 &lt;code&gt;665.23MiB&lt;/code&gt;였다. simulated Starter 기준인 &lt;code&gt;320MiB&lt;/code&gt;를 훨씬 넘었다. 반대로 bounded route는 가장 큰 batch peak도 &lt;code&gt;181.38MiB&lt;/code&gt;였다. n8n 자체 memory baseline을 가정해도 parent execution을 작게 유지하는 쪽이 훨씬 덜 흔들렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 &quot;batch size 200이 정답&quot;이 아니다. n8n docs도 예시로 10,000 rows 대신 200 rows를 들지만, 실제 값은 payload와 node에 맞춰 정해야 한다. 중요한 건 한 execution에서 들고 있는 data의 폭을 먼저 줄이는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진단 순서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 단계는 플랜 비교가 아니라 실패한 execution 하나를 고정하는 것이다. execution id, workflow id, 마지막으로 성공한 node, 마지막으로 실행된 node, input rows, binary item 수, manual/production mode, 저장 설정을 기록한다. 이 값이 없으면 같은 큰 workflow를 반복 실행하면서 감으로 고치게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 Code node를 본다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;Run Once for All Items&lt;/code&gt;가 대량 rows 뒤에 붙어 있는지 확인한다.&lt;/li&gt;
&lt;li&gt;Code node가 rows 전체를 복사하거나 새 array로 다시 만드는지 본다.&lt;/li&gt;
&lt;li&gt;Set/Edit Fields 같은 일반 node로 대체 가능한 mapping이면 Code node를 빼본다.&lt;/li&gt;
&lt;li&gt;custom logic이 필요하면 batch 뒤로 보내거나 per-item 경계로 쪼갠다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Merge는 input stream을 줄인 뒤에 걸어야 한다. broad reference table을 그대로 Merge에 넣기 전에 key 후보를 먼저 줄인다. &quot;모든 rows를 합친 뒤 filter&quot;보다 &quot;filter한 rows만 merge&quot;가 낫다. API 호출이나 AI step 앞에 Loop Over Items를 넣었다면 done output으로 다시 합치는 위치도 확인한다. loop body가 끝나기 전에 큰 downstream consumer로 넘기면 batch의 의미가 줄어든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;manual execution도 따로 본다. n8n memory docs는 manual execution에서 frontend용 data copy 때문에 memory usage가 늘 수 있다고 설명한다. 큰 workflow를 editor에서 계속 눌러 보는 동안만 터지는지, production trigger에서도 터지는지 분리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;execution data는 마지막에 정리할 일이 아니다. success execution과 progress data를 모두 저장하면 디버깅은 편하지만, 큰 execution이 많을수록 storage와 운영 부담이 커진다. 운영 단계에 들어간 workflow라면 error/replay에 필요한 최소 데이터와 성공 실행 저장 범위를 나눠야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;upgrade가 필요한 경우와 아닌 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업그레이드가 필요한 경우는 있다. payload를 batch로 줄였고, Code all-items를 없앴고, Merge 후보도 줄였고, success/progress 저장도 조정했는데도 peak가 plan limit 근처에 계속 닿는다면 plan 문제다. binary file 처리 자체가 큰 workflow, 동시에 여러 production workflow가 도는 workspace, sub-workflow로도 parent result를 줄이기 어려운 workflow도 더 큰 plan이나 self-hosting 검토가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 아래 상황에서는 upgrade가 먼저가 아니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;먼저 할 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;manual execution에서만 자주 터짐&lt;/td&gt;
&lt;td&gt;production execution과 분리해 테스트하고 editor preview data를 줄인다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code node가 rows 전체를 한 번에 처리함&lt;/td&gt;
&lt;td&gt;all-items boundary를 batch/per-item/sub-workflow로 나눈다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge 전에 reference rows가 넓게 들어옴&lt;/td&gt;
&lt;td&gt;key 후보를 먼저 줄이고 Merge width를 제한한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;success/progress execution data를 계속 저장함&lt;/td&gt;
&lt;td&gt;error/replay 중심으로 저장 범위를 조정한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;같은 실패를 계속 retry함&lt;/td&gt;
&lt;td&gt;replay ID를 남기고 같은 envelope retry를 막는다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 판단은 &quot;돈을 안 쓰자&quot;가 아니다. 줄일 수 있는 execution envelope를 먼저 줄이고도 계속 limit에 닿는지 봐야 plan 결정이 정확해진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 실험의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 live n8n Cloud incident report가 아니다. 특정 Cloud account, n8n version &lt;code&gt;1.123.33&lt;/code&gt;, 실제 memory metric, real workflow JSON, external database, browser UI, IMAP, binary storage를 검증하지 않았다. local harness의 MiB 값은 산식 기반 추정치다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 운영 노트로 쓸 수 있는 이유는 분명하다. n8n 공식 문서가 이미 줄일 수 있는 surface를 보여 준다. Cloud plan RAM, n8n baseline memory, Code node, Loop Over Items, sub-workflow, Merge, execution data 저장이 모두 서로 다른 knob다. OOM 오류를 plan 하나로만 보면 이 knob를 놓친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n Cloud memory 오류가 났다면 첫 답은 &quot;Pro로 올릴까&quot;가 아니라 &quot;이번 실행 한 건이 왜 이렇게 커졌나&quot;여야 한다. 그 질문에 답할 수 있을 때 upgrade도 덜 감정적인 결정이 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 보면 좋은 글&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/n8n-%EC%9D%B4%EB%A9%94%EC%9D%BC-AI-Agent-%EC%9E%A5%EC%95%A0-%EC%A7%84%EB%8B%A8-50k-%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8%EB%B3%B4%EB%8B%A4-%EB%A9%94%EC%9D%BC-%EC%9B%90%EB%AC%B8%C2%B7%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%B0%BD%C2%B7%EB%8F%84%EA%B5%AC-%EC%B6%9C%EB%A0%A5%EC%9D%84-%EB%A8%BC%EC%A0%80-%EC%9E%98%EB%9D%BC%EC%95%BC-%ED%95%9C%EB%8B%A4&quot;&gt;n8n 이메일 AI Agent 장애 진단: 50k 프롬프트보다 메일 원문&amp;middot;메모리 창&amp;middot;도구 출력을 먼저 잘라야 한다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/n8n-AI-Agent-%EC%9A%B4%EC%98%81-%EA%B8%B0%EC%A4%80-%EB%A9%8B%EC%A7%84-%EB%85%B8%EB%93%9C%EB%B3%B4%EB%8B%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B3%84%EC%95%BD%C2%B7%EC%A4%91%EB%B3%B5-%EB%B0%A9%EC%A7%80%C2%B7%EC%9E%AC%EC%8B%9C%EB%8F%84-%EB%A1%9C%EA%B7%B8%EA%B0%80-%EB%A8%BC%EC%A0%80%EB%8B%A4&quot;&gt;n8n AI Agent 운영 기준: 멋진 노드보다 데이터 계약&amp;middot;중복 방지&amp;middot;재시도 로그가 먼저다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/zapier-make-n8n-solo-business-automation-fit&quot;&gt;Zapier vs Make vs n8n: 1인사업자가 업무 자동화 툴을 고를 때 연결 수보다 실패 처리가 먼저다&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/manage-cloud/cloud-data-management/&quot;&gt;n8n Cloud data management docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/hosting/scaling/memory-errors/&quot;&gt;n8n memory-related errors docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/&quot;&gt;n8n Code node docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.splitinbatches/&quot;&gt;n8n Loop Over Items docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executeworkflow/&quot;&gt;n8n Execute Sub-workflow docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/hosting/scaling/execution-data/&quot;&gt;n8n Execution data docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.merge/&quot;&gt;n8n Merge node docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/workflows/settings/&quot;&gt;n8n Workflow settings docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/Users/sungwoon/Documents/blog-automation/content-planning/trend-radar/2026-05-05-wn-trend-radar.md&quot;&gt;2026-05-05 trend radar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 로그 첨부&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감 경로와 계정 값을 마스킹한 공개용 로그와 실험 스크립트만 아래에 첨부한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;wn121-n8n-cloud-memory-gate-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn121-n8n-cloud-memory-gate-summary-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn121-n8n-cloud-memory-gate-public.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/O6Crf/dJMcahxz8pD/8Kg7Ziz9d31RtpQmR7XGd0/wn121-n8n-cloud-memory-gate-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn121-n8n-cloud-memory-gate-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.02MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/MqxvQ/dJMcajhOhVI/hcjkh5Lv7jkQbmpShyt83k/wn121-n8n-cloud-memory-gate-summary-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn121-n8n-cloud-memory-gate-summary-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/oYgOC/dJMcajhOhVK/KAxdxyejJwaURDBqMGZZQ1/wn121-n8n-cloud-memory-gate-public.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn121-n8n-cloud-memory-gate-public.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>자동화 실전</category>
      <category>n8n</category>
      <category>n8n Cloud</category>
      <category>메모리 오류</category>
      <category>실행 로그</category>
      <category>업무 자동화</category>
      <author>이천재</author>
      <guid isPermaLink="true">https://ai-imojumo.tistory.com/71</guid>
      <comments>https://ai-imojumo.tistory.com/entry/n8n-Cloud-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%98%A4%EB%A5%98-%EC%A7%84%EB%8B%A8-%ED%94%8C%EB%9E%9C-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%EC%A0%84%EC%97%90-Code-%EB%85%B8%EB%93%9C%C2%B7Merge%C2%B7%EC%8B%A4%ED%96%89-%EC%A0%80%EC%9E%A5%EC%9D%84-%EB%A8%BC%EC%A0%80-%EC%A4%84%EC%97%AC%EC%95%BC-%ED%95%9C%EB%8B%A4#entry71comment</comments>
      <pubDate>Thu, 7 May 2026 21:13:27 +0900</pubDate>
    </item>
    <item>
      <title>n8n 이메일 AI Agent 장애 진단: 50k 프롬프트보다 메일 원문&amp;middot;메모리 창&amp;middot;도구 출력을 먼저 잘라야 한다</title>
      <link>https://ai-imojumo.tistory.com/entry/n8n-%EC%9D%B4%EB%A9%94%EC%9D%BC-AI-Agent-%EC%9E%A5%EC%95%A0-%EC%A7%84%EB%8B%A8-50k-%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8%EB%B3%B4%EB%8B%A4-%EB%A9%94%EC%9D%BC-%EC%9B%90%EB%AC%B8%C2%B7%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%B0%BD%C2%B7%EB%8F%84%EA%B5%AC-%EC%B6%9C%EB%A0%A5%EC%9D%84-%EB%A8%BC%EC%A0%80-%EC%9E%98%EB%9D%BC%EC%95%BC-%ED%95%9C%EB%8B%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;n8n 이메일 AI Agent가 몇 주 동안 잘 돌다가 갑자기 parser error나 &lt;code&gt;invalid syntax&lt;/code&gt;로 멈추면, 첫 질문을 모델 변경으로 잡기 쉽다. 실제 진단은 더 작게 시작하는 편이 낫다. 실패한 메일 한 건을 고정해서 다시 실행할 수 있는가. 그 메일 원문과 첨부를 그대로 agent에 넣고 있지 않은가. 메모리가 계정별로 계속 쌓이고 있지 않은가. Google Sheets 같은 도구 출력이 너무 넓게 붙고 있지 않은가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;2026-05-04&lt;/code&gt;에 Reddit 공개 신호를 먼저 봤다. r/n8n에는 6개 메일함을 돌리는 email management agent가 6주 뒤 AI Agent/LangChain node에서 &lt;code&gt;invalid syntax&lt;/code&gt;를 내며 흔들린다는 진단 요청이 올라왔다. 긴 system prompt, 다국어 분류, Google Sheets 제품 조회, Telegram 승인 버튼, 일정 처리, retry worker, error handler가 한 workflow에 붙어 있었다. 댓글 흐름도 payload size, memory, failed execution replay, tool output shape를 먼저 보라는 쪽이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커뮤니티 글은 수요 신호로만 쓴다. n8n 동작과 기능 설명은 n8n 공식 문서로 확인했다. 직접 실험은 live n8n 계정 없이 deterministic local harness로만 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧게 정리하면 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;긴 prompt를 줄이기 전에 실패한 execution fixture를 먼저 고정한다.&lt;/li&gt;
&lt;li&gt;Email Trigger에서 원문 format과 attachment download가 agent 입력을 키우는지 본다.&lt;/li&gt;
&lt;li&gt;계정별 memory는 무제한 history가 아니라 window나 summary로 끊는다.&lt;/li&gt;
&lt;li&gt;Google Sheets/Product lookup 결과는 전체 rows가 아니라 top matches만 넘긴다.&lt;/li&gt;
&lt;li&gt;Structured Output Parser는 schema guard다. oversized email envelope를 대신 줄여주지 않는다.&lt;/li&gt;
&lt;li&gt;실패 재시도는 같은 큰 input을 세 번 더 보내는 방식이면 진단이 늦어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커뮤니티 신호는 장애 모양을 고르는 데만 쓴다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택한 demand signal은 n8n 이메일 에이전트 장애였다. 공개 글의 핵심은 &quot;AI Agent가 나쁘다&quot;가 아니다. scope가 큰 email workflow에서 어느 순간부터 실패 입력이 너무 커졌고, 어떤 mailbox, 어떤 sender, 어떤 tool call에서 깨지는지 좁히기 어렵다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 신호가 좋은 WN 주제인 이유는 실험이 가능하기 때문이다. 실제 고객 메일이나 계정이 없어도 아래 조건은 local harness로 재현할 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;장애 재료&lt;/th&gt;
&lt;th&gt;이메일 agent에서 흔한 형태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;긴 system prompt&lt;/td&gt;
&lt;td&gt;분류표, 말투, 금지 규칙, 상품 추천 규칙이 계속 붙음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메일 원문&lt;/td&gt;
&lt;td&gt;HTML, forwarding history, 서명, quoted reply가 함께 들어옴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;첨부&lt;/td&gt;
&lt;td&gt;PDF, 이미지, inline attachment 요약이 agent 입력 앞에 붙음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;memory&lt;/td&gt;
&lt;td&gt;계정별 이전 대화가 session history로 계속 남음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tool output&lt;/td&gt;
&lt;td&gt;Google Sheets 제품 목록이나 sender memory가 넓게 반환됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retry&lt;/td&gt;
&lt;td&gt;같은 oversized input을 다시 보내며 실패만 반복함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커뮤니티 글은 &quot;왜 이 문제를 지금 다루는가&quot;를 설명한다. &quot;n8n이 실제로 이렇게 동작한다&quot;는 근거는 아니다. 그 경계가 흐려지면 글이 장애 분석이 아니라 댓글 요약이 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;n8n 공식 문서로 확인한 줄일 수 있는 지점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n Email Trigger (IMAP) docs는 IMAP Email node가 email server에서 email을 받는 trigger node라고 설명한다. 이 node에는 attachment download option이 있고, docs는 필요할 때만 켜라고 적는다. processing이 늘기 때문이다. format option도 &lt;code&gt;RAW&lt;/code&gt;, &lt;code&gt;Resolved&lt;/code&gt;, &lt;code&gt;Simple&lt;/code&gt;로 나뉜다. &lt;code&gt;Resolved&lt;/code&gt;는 attachments를 binary data로 저장하고, &lt;code&gt;RAW&lt;/code&gt;는 raw field에 base64url body를 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말은 이메일 agent에서 꽤 중요하다. &quot;메일을 받았다&quot;가 곧 &quot;agent에 full email을 다 넣어도 된다&quot;는 뜻은 아니다. 자동 답장 초안에는 보통 본문 일부, 주문번호, 고객 의도, 필요한 attachment summary면 충분하다. forwarding history와 binary payload까지 매번 넣으면 실패 envelope가 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Agent 쪽에서도 줄일 지점이 보인다. n8n Tools AI Agent docs는 prompt를 previous node의 &lt;code&gt;chatInput&lt;/code&gt;에서 자동으로 받거나 직접 정의할 수 있다고 설명한다. &lt;code&gt;Require Specific Output Format&lt;/code&gt;을 켜면 Auto-fixing Output Parser, Item List Output Parser, Structured Output Parser를 연결한다. options에는 &lt;code&gt;System Message&lt;/code&gt;, &lt;code&gt;Max Iterations&lt;/code&gt;, &lt;code&gt;Return Intermediate Steps&lt;/code&gt;, &lt;code&gt;Tracing Metadata&lt;/code&gt;가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Memory docs도 이 글의 핵심 근거다. Simple Memory는 current session의 customizable chat history length를 저장한다. Redis, Postgres, Zep 같은 memory service node도 있다. 더 복잡한 memory 관리는 Chat Memory Manager로 처리할 수 있고, docs는 agent response memory size를 확인하고 줄이는 예를 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parser와 debug surface도 따로 봐야 한다. Structured Output Parser는 JSON Schema 기반 field 반환을 돕지만, generated schema는 field를 mandatory로 취급하고 &lt;code&gt;$ref&lt;/code&gt;는 지원하지 않는다. Error handling docs는 failed execution을 Executions에서 보고 previous execution data를 load할 수 있다고 설명한다. Debug executions docs는 failed execution을 &lt;code&gt;Debug in editor&lt;/code&gt;로 가져와 first node에 data를 pin할 수 있다고 한다. 단, 기능 availability는 n8n Cloud와 registered Community plans라는 caveat가 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬 실험: 42개 메일을 두 route로 돌렸다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 n8n Cloud, self-hosted n8n, IMAP, Google Sheets, Postgres, LLM API는 호출하지 않았다. 대신 n8n-like failure shape를 Python으로 만들었다. 입력은 6개 mailbox와 42개 synthetic message다. 일부 message에는 긴 forwarding history, attachment payload, 제품 추천 요청, ambiguous mixed-language request를 넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교한 route는 두 가지다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;route&lt;/th&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;위험&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;naive long prompt&lt;/td&gt;
&lt;td&gt;50k system prompt, 계정별 전체 memory, raw email payload, 넓은 product-sheet output을 agent envelope에 붙임&lt;/td&gt;
&lt;td&gt;parser error 뒤 같은 큰 입력을 반복 retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bounded replay gate&lt;/td&gt;
&lt;td&gt;plain-text sanitizer, body budget, attachment summary, 6-message memory window, top 8 tool rows, replay ID를 먼저 적용&lt;/td&gt;
&lt;td&gt;위험하거나 애매한 메일은 write 대신 manual review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 크게 갈렸다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;metric&lt;/th&gt;
&lt;th&gt;naive long prompt&lt;/th&gt;
&lt;th&gt;bounded replay gate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;processed messages&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;failed executions&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;manual review routes&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retry attempts&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max envelope chars&lt;/td&gt;
&lt;td&gt;197545&lt;/td&gt;
&lt;td&gt;19970&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max memory items seen&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tool rows per message&lt;/td&gt;
&lt;td&gt;45 또는 637&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;naive route의 첫 failure는 cycle 3의 product thread였다. message body 자체도 길었지만, 계정별 이전 history와 637개 product row surface가 같이 붙으면서 synthetic parser threshold를 넘었다. 실패 뒤에는 같은 envelope를 3회 retry했다. 진단에는 별 도움이 없고 task만 태우는 흐름이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bounded route는 실패를 만들지 않았다. 대신 3개 message를 &lt;code&gt;manual_review&lt;/code&gt;로 보냈다. certified mail with forwarded history, ambiguous cancellation 같은 입력은 자동 초안보다 사람이 먼저 보는 쪽이 맞다. 이 route에서 중요한 값은 &lt;code&gt;failed_executions 0&lt;/code&gt;보다 &lt;code&gt;max_envelope_chars 19970&lt;/code&gt;이다. agent가 보는 봉투를 작게 만들었기 때문에 실패한 입력을 다시 잡기 쉬워졌다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장애 진단 순서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 단계는 prompt rewrite가 아니다. failed execution을 하나 고정한다. n8n Debug executions가 가능한 환경이라면 failed execution을 editor로 가져오고 첫 node data를 pin한다. 기능을 쓸 수 없는 환경이면 execution id, mailbox, message id, subject hash, attachment count, body char estimate를 따로 남긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 Email Trigger 바로 뒤에서 payload를 줄인다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;HTML body를 plain text로 바꾼다.&lt;/li&gt;
&lt;li&gt;quoted reply와 forwarding history를 일정 길이 뒤에서 자른다.&lt;/li&gt;
&lt;li&gt;attachment는 원본 대신 filename, type, size, extracted summary만 넘긴다.&lt;/li&gt;
&lt;li&gt;inline image와 base64 raw body는 agent 입력에서 빼고, 필요한 경우 별도 parser route로 보낸다.&lt;/li&gt;
&lt;li&gt;본문 길이가 기준을 넘으면 자동 답장 대신 manual review로 보낸다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Memory는 mailbox별로 끊는다. 고객 support agent라면 모든 이전 메일이 매번 필요하지 않다. 최근 5-10개 message, sender preference summary, unresolved ticket state 정도로 나눈다. n8n memory docs의 Simple Memory나 memory service node를 쓸 수 있더라도, &quot;저장할 수 있다&quot;와 &quot;agent call마다 다 넣어야 한다&quot;는 다른 판단이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tool output도 줄인다. Google Sheets를 product catalog처럼 쓰면 row 전체를 agent에게 주고 싶어진다. 하지만 agent에게 필요한 것은 보통 top N candidate, price, link, availability, category 정도다. 전체 637 rows를 매번 붙이면 retrieval이 아니라 dump가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parser error는 마지막에 본다. Structured Output Parser와 JSON Schema는 output 모양을 잡는 데 필요하다. 그러나 input envelope가 이미 너무 크면 parser를 더 엄격하게 만들어도 같은 실패가 남는다. schema에는 category enum, allowed action, confidence type, &lt;code&gt;needs_human_review&lt;/code&gt; 같은 field를 두되, 큰 메일은 schema 이전 단계에서 줄인다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 workflow에 넣을 gate&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 workflow에는 아래 순서가 낫다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;위치&lt;/th&gt;
&lt;th&gt;gate&lt;/th&gt;
&lt;th&gt;실패 route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Email Trigger 직후&lt;/td&gt;
&lt;td&gt;mailbox, message id, sender, subject, body length, attachment count 기록&lt;/td&gt;
&lt;td&gt;missing id면 stop/error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sanitizer&lt;/td&gt;
&lt;td&gt;plain text 변환, quoted history cutoff, attachment summary&lt;/td&gt;
&lt;td&gt;body/attachment 과대면 manual review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;memory&lt;/td&gt;
&lt;td&gt;mailbox별 window, sender summary, unresolved state만 유지&lt;/td&gt;
&lt;td&gt;memory overflow면 Chat Memory Manager 또는 review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tool lookup&lt;/td&gt;
&lt;td&gt;query를 먼저 좁히고 top rows만 반환&lt;/td&gt;
&lt;td&gt;너무 많은 rows면 clarification 또는 review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent output&lt;/td&gt;
&lt;td&gt;category enum, allowed action, object consistency 검사&lt;/td&gt;
&lt;td&gt;unsafe action이면 manual review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retry&lt;/td&gt;
&lt;td&gt;transient error만 node-level retry&lt;/td&gt;
&lt;td&gt;parser/input/schema 문제는 같은 envelope retry 금지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;logging&lt;/td&gt;
&lt;td&gt;replay id, execution id/url, route, failure kind 저장&lt;/td&gt;
&lt;td&gt;public artifact는 마스킹 후 하단 첨부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순서의 목표는 agent를 덜 쓰는 것이 아니다. agent에게 줄 수 있는 입력만 남기는 것이다. 이메일 원문 정리, memory 자르기, tool output cap, replay id는 창의적 판단이 아니라 운영 gate다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어디까지 이 실험을 믿어도 되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실험은 live n8n 장애 재현이 아니다. 특정 n8n 버전, LangChain node, OpenAI model, Google Sheets credential, Postgres pool을 검증하지 않았다. 그래서 &quot;n8n 이메일 agent는 42건 중 17건 실패한다&quot; 같은 benchmark로 읽으면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 실패 구조를 작게 보여준다. 긴 system prompt만 문제가 아니라, raw email payload, attachment, growing memory, broad tool output이 같은 envelope에 들어갈 때 실패가 커진다. 같은 큰 입력을 재시도하면 원인을 좁히지 못한다. 반대로 sanitizer, memory window, tool row cap, replay ID를 앞에 두면 자동 처리할 메일과 사람이 봐야 할 메일이 갈린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n 이메일 AI Agent를 운영에 놓는다면 첫 48시간은 자동 전송보다 draft/review mode가 낫다. 실패한 메일 하나를 다시 재현할 수 있을 때 자동화 범위를 넓혀도 늦지 않다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 보면 좋은 글&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/n8n-AI-Agent-%EC%9A%B4%EC%98%81-%EA%B8%B0%EC%A4%80-%EB%A9%8B%EC%A7%84-%EB%85%B8%EB%93%9C%EB%B3%B4%EB%8B%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B3%84%EC%95%BD%C2%B7%EC%A4%91%EB%B3%B5-%EB%B0%A9%EC%A7%80%C2%B7%EC%9E%AC%EC%8B%9C%EB%8F%84-%EB%A1%9C%EA%B7%B8%EA%B0%80-%EB%A8%BC%EC%A0%80%EB%8B%A4&quot;&gt;n8n AI Agent 운영 기준: 멋진 노드보다 데이터 계약&amp;middot;중복 방지&amp;middot;재시도 로그가 먼저다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/zapier-make-n8n-solo-business-automation-fit&quot;&gt;Zapier vs Make vs n8n: 1인사업자가 업무 자동화 툴을 고를 때 연결 수보다 실패 처리가 먼저다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/AI-%EC%97%85%EB%AC%B4-%EC%9E%90%EB%8F%99%ED%99%94-%EC%8B%9C%EC%9E%91-%EC%88%9C%EC%84%9C-ChatGPT%C2%B7Gemini%C2%B7Notion-AI%EB%8A%94-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%B4%88%EC%95%88%EB%B6%80%ED%84%B0-%EB%B6%99%EC%97%AC%EC%95%BC-%EB%8D%9C-%EC%8B%A4%ED%8C%A8%ED%95%9C%EB%8B%A4&quot;&gt;AI 업무 자동화 시작 순서: ChatGPT&amp;middot;Gemini&amp;middot;Notion AI는 이메일 초안부터 붙여야 덜 실패한다&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.emailimap/&quot;&gt;n8n Email Trigger (IMAP) docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent/tools-agent/&quot;&gt;n8n Tools AI Agent docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/advanced-ai/examples/understand-memory/&quot;&gt;n8n memory docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.outputparserstructured/&quot;&gt;n8n Structured Output Parser docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/flow-logic/error-handling/&quot;&gt;n8n Error handling docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/workflows/executions/debug/&quot;&gt;n8n Debug executions docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/hosting/scaling/execution-data/&quot;&gt;n8n Execution data docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/Users/sungwoon/Documents/blog-automation/content-planning/trend-radar/2026-05-04-wn-trend-radar.md&quot;&gt;2026-05-04 trend radar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 로그 첨부&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감 경로와 이메일 주소를 마스킹한 공개용 로그와 실험 스크립트만 아래에 첨부한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;wn120-n8n-email-ai-agent-failure-gate-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn120-n8n-email-ai-agent-failure-gate-summary-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn120-n8n-email-ai-agent-failure-gate-public.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/dntFD2/dJMcagyBdDX/tMlkk2APHd9kw6mM96ttKK/wn120-n8n-email-ai-agent-failure-gate-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn120-n8n-email-ai-agent-failure-gate-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.04MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/dA6dAB/dJMcaaLXe9V/jKvJ3rkR6am5E0T5sKUBjk/wn120-n8n-email-ai-agent-failure-gate-summary-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn120-n8n-email-ai-agent-failure-gate-summary-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/x11Xk/dJMcagyBdEe/4nySBnNjGOAU8OUUbt9PCK/wn120-n8n-email-ai-agent-failure-gate-public.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn120-n8n-email-ai-agent-failure-gate-public.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>자동화 실전</category>
      <category>ai agent</category>
      <category>n8n</category>
      <category>업무 자동화</category>
      <category>이메일 자동화</category>
      <category>장애 진단</category>
      <author>이천재</author>
      <guid isPermaLink="true">https://ai-imojumo.tistory.com/70</guid>
      <comments>https://ai-imojumo.tistory.com/entry/n8n-%EC%9D%B4%EB%A9%94%EC%9D%BC-AI-Agent-%EC%9E%A5%EC%95%A0-%EC%A7%84%EB%8B%A8-50k-%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8%EB%B3%B4%EB%8B%A4-%EB%A9%94%EC%9D%BC-%EC%9B%90%EB%AC%B8%C2%B7%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%B0%BD%C2%B7%EB%8F%84%EA%B5%AC-%EC%B6%9C%EB%A0%A5%EC%9D%84-%EB%A8%BC%EC%A0%80-%EC%9E%98%EB%9D%BC%EC%95%BC-%ED%95%9C%EB%8B%A4#entry70comment</comments>
      <pubDate>Mon, 4 May 2026 17:57:27 +0900</pubDate>
    </item>
    <item>
      <title>n8n AI Agent 운영 기준: 멋진 노드보다 데이터 계약&amp;middot;중복 방지&amp;middot;재시도 로그가 먼저다</title>
      <link>https://ai-imojumo.tistory.com/entry/n8n-AI-Agent-%EC%9A%B4%EC%98%81-%EA%B8%B0%EC%A4%80-%EB%A9%8B%EC%A7%84-%EB%85%B8%EB%93%9C%EB%B3%B4%EB%8B%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B3%84%EC%95%BD%C2%B7%EC%A4%91%EB%B3%B5-%EB%B0%A9%EC%A7%80%C2%B7%EC%9E%AC%EC%8B%9C%EB%8F%84-%EB%A1%9C%EA%B7%B8%EA%B0%80-%EB%A8%BC%EC%A0%80%EB%8B%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;n8n AI Agent workflow가 흔들리는 지점은 agent가 말을 못 해서가 아니다. 보통은 쓰기 전에 멈추지 못해서 흔들린다. Webhook payload key가 바뀌었는데 그대로 CRM에 쓰고, 같은 trigger가 두 번 들어왔는데 메일을 두 번 보내고, 429와 403을 같은 재시도로 묶으면 agent prompt를 고쳐도 운영 문제는 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;2026-05-03&lt;/code&gt;에 Reddit 흐름을 먼저 봤다. r/n8n, r/AI_Agents, r/automation에서 반복된 신호는 비슷했다. &quot;더 많은 agent node&quot;보다 data contract, dedupe key, retry route, execution log가 먼저라는 쪽이다. 이 글에서 Reddit은 수요 신호로만 쓴다. n8n 동작과 기능 설명은 n8n 공식 문서로 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧게 정리하면 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;n8n AI Agent 앞에는 payload contract gate를 둔다.&lt;/li&gt;
&lt;li&gt;중복 trigger는 write action 전에 event key나 idempotency key로 막는다.&lt;/li&gt;
&lt;li&gt;agent output은 &quot;JSON처럼 보인다&quot;가 아니라 allowed action, ticket/order ID, confidence, business rule을 따로 검증한다.&lt;/li&gt;
&lt;li&gt;429 같은 transient failure와 403 같은 auth failure는 같은 retry route로 보내지 않는다.&lt;/li&gt;
&lt;li&gt;운영자가 나중에 찾을 수 있도록 execution metadata나 trace ID를 남긴다.&lt;/li&gt;
&lt;li&gt;Structured Output Parser는 도움이 되지만, n8n docs도 agent parsing caveat를 따로 둔다. parser가 business validation을 대신하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커뮤니티 신호는 문제를 고르는 데만 쓴다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 radar에서 가장 강한 신호는 r/n8n 글이었다. 글과 댓글은 agent workflow가 production에서 버티려면 data contracts, retries, idempotency, observability가 먼저라고 반복했다. r/automation에서는 duplicate data를 어떻게 막느냐는 질문이 올라왔고, 답변은 unique ID, hash, early dedupe, insert 전 검증 쪽으로 모였다. r/AI_Agents 쪽도 messy input, confirmation step, 좁은 scope, eval set 같은 운영 지점을 강조했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 글은 공식 근거가 아니다. 다만 독자가 실제로 막히는 질문을 보여준다. 그래서 주제를 &quot;n8n AI Agent가 좋은가&quot;가 아니라 &quot;AI Agent workflow가 쓰기 전에 무엇을 잠가야 하는가&quot;로 좁혔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;n8n 공식 문서로 확인한 안전장치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n Tools AI Agent docs는 Tools Agent가 external tools와 APIs를 사용해 action을 수행하거나 정보를 가져오는 node라고 설명한다. 같은 문서에는 &lt;code&gt;Require Specific Output Format&lt;/code&gt; 설정이 있고, 이 설정을 켜면 Auto-fixing Output Parser, Item List Output Parser, Structured Output Parser 같은 output parser를 붙이게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Structured Output Parser common issues 문서는 agent workflow에서 structured parsing이 자주 안정적이지 않다고 따로 적고 있다. n8n은 agent output을 별도 LLM-chain으로 받아 parse하는 방식을 더 일관된 결과로 권한다. 이 말은 중요하다. &quot;parser를 붙였으니 안전하다&quot;가 아니라 &quot;agent output과 parser를 분리해도 business validation은 남는다&quot;로 읽어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 gate로 쓸 수 있는 공식 surface도 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지점&lt;/th&gt;
&lt;th&gt;n8n에서 볼 기능&lt;/th&gt;
&lt;th&gt;운영상 역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;입력 중복&lt;/td&gt;
&lt;td&gt;Remove Duplicates&lt;/td&gt;
&lt;td&gt;current input 또는 previous executions 기준으로 중복 item 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;node 실패&lt;/td&gt;
&lt;td&gt;Retry On Fail&lt;/td&gt;
&lt;td&gt;실패한 node 재시도. 429처럼 transient failure에 맞다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;오류 분기&lt;/td&gt;
&lt;td&gt;On Error, Error Trigger&lt;/td&gt;
&lt;td&gt;stop, continue, error output, error workflow route 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 추적&lt;/td&gt;
&lt;td&gt;Execution Data&lt;/td&gt;
&lt;td&gt;execution list에서 찾을 metadata 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rate limit&lt;/td&gt;
&lt;td&gt;Retry On Fail, Loop Over Items, Wait&lt;/td&gt;
&lt;td&gt;request 간격과 재시도 간격을 명시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 표가 agent 설계를 대신하지는 않는다. 다만 &quot;AI가 알아서 판단&quot;하기 전에 workflow가 먼저 막을 수 있는 지점이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬 실험: agent-first와 contract-first 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 n8n Cloud나 Zapier 계정은 쓰지 않았다. 대신 n8n-like workflow failure를 deterministic Python harness로 만들었다. 입력은 support ticket webhook 7개다. scenario는 정상 입력, duplicate webhook, field rename, 잘못된 agent action, rate limit, auth failure, missing trace로 나눴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교한 route는 두 가지다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;route&lt;/th&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;기대 위험&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;naive agent-first&lt;/td&gt;
&lt;td&gt;payload와 agent output을 거의 그대로 write action에 넘김&lt;/td&gt;
&lt;td&gt;duplicate write, invalid action, generic retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contract-first workflow gate&lt;/td&gt;
&lt;td&gt;payload contract, dedupe key, agent output gate, retry route, trace ID를 먼저 봄&lt;/td&gt;
&lt;td&gt;일부 run은 write 전에 block/review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 아래처럼 갈렸다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;metric&lt;/th&gt;
&lt;th&gt;naive agent-first&lt;/th&gt;
&lt;th&gt;contract-first gate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;total write&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;duplicate write&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;invalid action write&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;explicit route decision&lt;/td&gt;
&lt;td&gt;3 failures after write&lt;/td&gt;
&lt;td&gt;7 decisions before or at write gate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;contract-first route의 decision 분포는 &lt;code&gt;block_before_agent 1&lt;/code&gt;, &lt;code&gt;dedupe_skip 1&lt;/code&gt;, &lt;code&gt;manual_review 2&lt;/code&gt;, &lt;code&gt;retry_node_only_with_idempotency_key 1&lt;/code&gt;, &lt;code&gt;stop_and_alert 1&lt;/code&gt;, &lt;code&gt;write_once 1&lt;/code&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 수치는 write가 줄었다는 점이다. 좋은 자동화는 많이 쓰는 workflow가 아니다. 써도 되는 것만 쓰고, 애매한 것은 작게 멈추는 workflow다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gate 1: Webhook 바로 뒤에서 payload contract를 본다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webhook이 들어오면 바로 AI Agent로 넘기지 않는다. 먼저 event ID, business object ID, recipient, priority, message 같은 최소 key를 본다. 이번 실험에서는 &lt;code&gt;customer_email&lt;/code&gt;이 &lt;code&gt;email&lt;/code&gt;로 바뀐 scenario가 있었다. 사람이 보면 같은 뜻 같지만 workflow contract에서는 다른 payload다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서 할 일은 거창하지 않다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;필수 key가 있는지 본다.&lt;/li&gt;
&lt;li&gt;값 type과 빈 값 여부를 본다.&lt;/li&gt;
&lt;li&gt;action에 필요한 business object ID가 있는지 본다.&lt;/li&gt;
&lt;li&gt;빠진 key가 있으면 Stop And Error 또는 manual review route로 보낸다.&lt;/li&gt;
&lt;li&gt;이 실패를 error workflow가 받을 수 있게 message를 남긴다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n에서는 Code node, IF node, Stop And Error node, Error Trigger를 조합할 수 있다. 중요한 것은 AI Agent가 &quot;없는 값을 추측&quot;하지 못하게 하는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gate 2: 중복은 write action 전에 막는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;duplicate webhook은 실무에서 이상한 사건이 아니다. upstream retry, browser double-submit, polling overlap, replay가 모두 같은 결과를 만든다. 이때 &quot;agent가 같은 답을 했으니 괜찮다&quot;는 판단은 위험하다. CRM update, email send, invoice create 같은 side effect는 두 번 실행되면 비용이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n Remove Duplicates docs는 current input 안의 duplicate 제거와 previous executions 기준 duplicate 제거를 설명한다. &lt;code&gt;Value Is New&lt;/code&gt;에서는 unique ID나 field combination이 dedupe 기준이 된다. 이 기능만으로 모든 idempotency가 해결되지는 않지만, write action 앞에서 첫 번째 guard로 쓰기 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 다음 순서가 낫다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;upstream event ID가 있으면 그 값을 dedupe key로 쓴다.&lt;/li&gt;
&lt;li&gt;event ID가 없으면 &lt;code&gt;source + object_id + action + timestamp bucket&lt;/code&gt;처럼 조합 key를 만든다.&lt;/li&gt;
&lt;li&gt;Remove Duplicates나 DB unique constraint로 write 전에 막는다.&lt;/li&gt;
&lt;li&gt;skip된 run도 trace에 남긴다.&lt;/li&gt;
&lt;li&gt;replay를 해야 한다면 새 run인지 retry인지 구분한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실험에서 naive route는 같은 ticket/action 조합이 두 번 쓰였다. contract-first route는 duplicate event를 &lt;code&gt;dedupe_skip&lt;/code&gt;으로 끝냈다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gate 3: agent output은 parser 뒤에서도 한 번 더 본다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n AI Agent에 Structured Output Parser를 붙이면 output 모양은 나아질 수 있다. 하지만 n8n docs는 agent structured parsing caveat를 따로 둔다. 그래서 parser 통과와 업무 허용은 분리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실험의 &lt;code&gt;S4-GREEN-200-BAD-ACTION&lt;/code&gt;은 syntactically valid object를 반환했다. 문제는 action이 &lt;code&gt;refund_now&lt;/code&gt;였다는 점이다. support reply workflow에서 환불 실행은 허용 action이 아니었다. naive route는 이 action을 write 대상으로 받아들였고, gated route는 &lt;code&gt;manual_review&lt;/code&gt;로 보냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;agent output gate는 최소한 아래를 본다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;검사&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;allowed action&lt;/td&gt;
&lt;td&gt;&lt;code&gt;draft_reply&lt;/code&gt;, &lt;code&gt;route_to_human&lt;/code&gt;, &lt;code&gt;classify_ticket&lt;/code&gt;처럼 whitelist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;object consistency&lt;/td&gt;
&lt;td&gt;output의 ticket/order ID가 input과 같은지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;confidence/type&lt;/td&gt;
&lt;td&gt;숫자 type인지, 최소 기준 이상인지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;side effect class&lt;/td&gt;
&lt;td&gt;email draft인지, 실제 전송인지, 결제/환불인지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;human review 필요 여부&lt;/td&gt;
&lt;td&gt;긴급, 금액, 권한 변경, 고객 영향 action&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 gate를 prompt에만 맡기면 안 된다. prompt는 agent에게 방향을 준다. gate는 workflow가 실제 action을 막는 장치다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gate 4: 429와 403은 같은 재시도가 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n node settings에는 &lt;code&gt;Retry On Fail&lt;/code&gt;이 있고, rate-limit docs는 429 상황에서 Retry On Fail 또는 Loop Over Items + Wait를 쓸 수 있다고 설명한다. 이 기능은 transient failure에 맞다. 잠깐 기다렸다가 같은 node를 다시 시도하면 되는 경우다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 403 auth failure는 재시도로 풀리는 문제가 아니다. credential, scope, account 권한을 봐야 한다. 이것을 full workflow retry로 태우면 같은 side effect를 반복하거나, task만 소모하고, 운영자는 실패 원인을 늦게 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분기 기준은 단순하게 둔다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;failure&lt;/th&gt;
&lt;th&gt;route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;429, timeout&lt;/td&gt;
&lt;td&gt;node-level retry with backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400 bad request&lt;/td&gt;
&lt;td&gt;payload/schema review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;401/403&lt;/td&gt;
&lt;td&gt;stop and alert, credential/scope check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;parser/business validation fail&lt;/td&gt;
&lt;td&gt;manual review or fail-closed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;duplicate event&lt;/td&gt;
&lt;td&gt;skip and log&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실험에서 rate-limit scenario는 &lt;code&gt;retry_node_only_with_idempotency_key&lt;/code&gt;로 갔다. auth failure는 &lt;code&gt;stop_and_alert&lt;/code&gt;였다. 둘을 같은 retry로 묶지 않은 것이 핵심이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gate 5: 나중에 찾을 수 있는 execution metadata를 남긴다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zapier 커뮤니티 글에서도 run ID를 어떻게 operations에게 넘길지 묻는 신호가 있었다. n8n에서도 같은 문제가 생긴다. 실패했을 때 &quot;어떤 run이 무엇을 썼는지&quot;를 30초 안에 못 찾으면, 자동화가 아니라 수동 수사에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n Error handling docs의 error data에는 execution id, url, retryOf, lastNodeExecuted 같은 정보가 포함될 수 있다. 단, execution id와 url은 execution이 database에 저장되어야 한다는 caveat가 있다. Execution Data node는 workflow execution metadata를 저장해 execution list에서 검색할 수 있게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영용 metadata는 길게 쓰지 않는 편이 낫다. n8n Execution Data docs에는 key/value length 제한도 있다. 예를 들어 아래 정도면 충분하다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;key&lt;/th&gt;
&lt;th&gt;value 예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trace_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sha256(event_id + action)&lt;/code&gt;의 짧은 값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;object_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ticket/order/customer ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;route&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;write_once&lt;/code&gt;, &lt;code&gt;dedupe_skip&lt;/code&gt;, &lt;code&gt;manual_review&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failure_kind&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;payload_contract_missing&lt;/code&gt;, &lt;code&gt;auth_403&lt;/code&gt;, &lt;code&gt;rate_limit_429&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;idempotency_key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;write action dedupe에 쓴 key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그는 장식이 아니다. 재시도, 고객 문의, 내부 승인, 장애 회고에서 같은 사건을 다시 찾는 색인이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 n8n workflow에 넣는 순서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Agent workflow를 만들 때는 아래 순서로 시작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Webhook 또는 trigger 바로 뒤에 payload contract gate를 둔다.&lt;/li&gt;
&lt;li&gt;event ID 또는 business object ID로 dedupe key를 만든다.&lt;/li&gt;
&lt;li&gt;Remove Duplicates 또는 DB unique constraint를 write action 앞에 둔다.&lt;/li&gt;
&lt;li&gt;AI Agent output은 parser 이후 allowed action과 object consistency를 다시 본다.&lt;/li&gt;
&lt;li&gt;side effect가 있는 action은 draft, preview, approval, execute를 나눈다.&lt;/li&gt;
&lt;li&gt;429/timeout은 node-level retry로 보내고, 401/403/400은 다른 route로 보낸다.&lt;/li&gt;
&lt;li&gt;Error Trigger workflow를 별도로 두고 Slack/email alert에는 trace ID와 execution URL을 넣는다.&lt;/li&gt;
&lt;li&gt;Execution Data에는 운영자가 검색할 metadata만 짧게 저장한다.&lt;/li&gt;
&lt;li&gt;처음 48시간은 자동 execute보다 draft/review mode로 둔다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순서는 agent를 덜 쓰자는 뜻이 아니다. agent가 판단해야 할 부분만 남기자는 뜻이다. payload shape, duplicate trigger, credential failure, trace lookup은 agent가 창의적으로 해결할 문제가 아니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어디까지 이 실험을 믿어도 되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 run은 live n8n Cloud, Zapier, LLM API를 호출하지 않았다. 특정 n8n 버전이나 paid plan의 실제 UI를 검증한 것도 아니다. 그래서 이 글은 &quot;n8n에서 정확히 몇 퍼센트 실패한다&quot;는 benchmark가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 운영 실패를 작게 재현했다. duplicate event가 있으면 write 전에 key로 막는가. payload key가 바뀌면 agent 전에 멈추는가. agent가 valid-looking but unsafe action을 내면 manual review로 보내는가. 429와 403을 구분하는가. 나중에 execution을 찾을 trace를 남기는가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다섯 질문에 답하지 못하면 agent prompt를 고쳐도 workflow는 계속 흔들린다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 보면 좋은 글&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/zapier-make-n8n-solo-business-automation-fit&quot;&gt;Zapier vs Make vs n8n: 1인사업자가 업무 자동화 툴을 고를 때 연결 수보다 실패 처리가 먼저다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/AI-%EC%97%85%EB%AC%B4-%EC%9E%90%EB%8F%99%ED%99%94-%EC%8B%9C%EC%9E%91-%EC%88%9C%EC%84%9C-ChatGPT%C2%B7Gemini%C2%B7Notion-AI%EB%8A%94-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%B4%88%EC%95%88%EB%B6%80%ED%84%B0-%EB%B6%99%EC%97%AC%EC%95%BC-%EB%8D%9C-%EC%8B%A4%ED%8C%A8%ED%95%9C%EB%8B%A4&quot;&gt;AI 업무 자동화 시작 순서: ChatGPT&amp;middot;Gemini&amp;middot;Notion AI는 이메일 초안부터 붙여야 덜 실패한다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI-Computer-Use%EC%99%80-Playwright-CLI-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%9E%90%EB%8F%99%ED%99%94-%EB%B9%84%EA%B5%90-%ED%99%94%EB%A9%B4-%ED%8C%90%EB%8B%A8%EC%9D%80-%EB%AA%A8%EB%8D%B8-%EB%A1%9C%EA%B7%B8%EC%9D%B8%C2%B7%EC%B2%A8%EB%B6%80%C2%B7%EA%B2%80%EC%A6%9D%EC%9D%80-selector-%ED%95%98%EB%84%A4%EC%8A%A4%EA%B0%80-%EB%A7%A1%EB%8A%94%EB%8B%A4&quot;&gt;OpenAI Computer Use와 Playwright CLI 브라우저 자동화 비교: 화면 판단은 모델, 로그인&amp;middot;첨부&amp;middot;검증은 selector 하네스가 맡는다&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent/tools-agent/&quot;&gt;n8n Tools AI Agent docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.outputparserstructured/common-issues/&quot;&gt;n8n Structured Output Parser common issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.removeduplicates/&quot;&gt;n8n Remove Duplicates docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/flow-logic/error-handling/&quot;&gt;n8n Error handling docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executiondata/&quot;&gt;n8n Execution Data docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://help.zapier.com/hc/en-us/articles/19220226086797-What-is-replay&quot;&gt;Zapier replay docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/Users/sungwoon/Documents/blog-automation/content-planning/trend-radar/2026-05-03-wn-trend-radar.md&quot;&gt;2026-05-03 trend radar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 로그 첨부&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감 경로와 이메일 주소를 마스킹한 공개용 로그와 실험 스크립트만 아래에 첨부한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;wn119-n8n-agent-workflow-gate-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn119-n8n-agent-workflow-gate-summary-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn119-n8n-agent-workflow-gate-public.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/rsoOe/dJMcagk3JSy/Yo9IgE3nAZaYcrewe1zaMk/wn119-n8n-agent-workflow-gate-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn119-n8n-agent-workflow-gate-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/RGPWD/dJMcafmbdgM/cy1B9rPkBgckE7fsNW1q30/wn119-n8n-agent-workflow-gate-summary-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn119-n8n-agent-workflow-gate-summary-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bp2Br6/dJMcacQveDK/yZBcUjOKhOd7uHksufISr0/wn119-n8n-agent-workflow-gate-public.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn119-n8n-agent-workflow-gate-public.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.02MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>자동화 실전</category>
      <category>ai agent</category>
      <category>n8n</category>
      <category>업무 자동화</category>
      <category>재시도</category>
      <category>중복 방지</category>
      <author>이천재</author>
      <guid isPermaLink="true">https://ai-imojumo.tistory.com/69</guid>
      <comments>https://ai-imojumo.tistory.com/entry/n8n-AI-Agent-%EC%9A%B4%EC%98%81-%EA%B8%B0%EC%A4%80-%EB%A9%8B%EC%A7%84-%EB%85%B8%EB%93%9C%EB%B3%B4%EB%8B%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B3%84%EC%95%BD%C2%B7%EC%A4%91%EB%B3%B5-%EB%B0%A9%EC%A7%80%C2%B7%EC%9E%AC%EC%8B%9C%EB%8F%84-%EB%A1%9C%EA%B7%B8%EA%B0%80-%EB%A8%BC%EC%A0%80%EB%8B%A4#entry69comment</comments>
      <pubDate>Sun, 3 May 2026 20:49:07 +0900</pubDate>
    </item>
    <item>
      <title>SCM SAP split-expedite approval evaluation: PO000318 승인 callback과 REBEC shadow ledger를 닫은 SCM-047</title>
      <link>https://ai-imojumo.tistory.com/entry/SCM-SAP-split-expedite-approval-evaluation-PO000318-%EC%8A%B9%EC%9D%B8-callback%EA%B3%BC-REBEC-shadow-ledger%EB%A5%BC-%EB%8B%AB%EC%9D%80-SCM-047</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PO000318&lt;/code&gt;은 이제 단순 경보가 아니라 승인 callback이 들어온 주문이다. 해야 할 일은 queue 숫자를 보는 것이 아니라, 84개 critical kit를 지금 예약할지 결정하는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 운영자가 먼저 보는 주문 case file&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;주문 번호&lt;/th&gt;
&lt;th&gt;lane&lt;/th&gt;
&lt;th&gt;현재 shipping status&lt;/th&gt;
&lt;th&gt;original due date&lt;/th&gt;
&lt;th&gt;외부 신호&lt;/th&gt;
&lt;th&gt;영향 이유&lt;/th&gt;
&lt;th&gt;아무것도 안 했을 때 결과&lt;/th&gt;
&lt;th&gt;추천 액션&lt;/th&gt;
&lt;th&gt;tradeoff&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PO000318&lt;/td&gt;
&lt;td&gt;LANE-KR-US-AI-SEA&lt;/td&gt;
&lt;td&gt;operator approved split expedite for 84 critical kits; expedited split is in capacity reservation simulation, remaining 336 kits stay on ocean ETA 2026-05-15, terminal appointment still pending&lt;/td&gt;
&lt;td&gt;2026-05-16&lt;/td&gt;
&lt;td&gt;UNCTAD 2026-04-07, WTO 2026-03-19, IMF 2026-04-14 official signals were rechecked at 2026-05-03T01:02:07+09:00; mapped as capacity and transport-cost pressure, not as a direct lane shutdown.&lt;/td&gt;
&lt;td&gt;The AI server control board is critical, the ocean ETA is still one day before due date, and the approved split must be reserved before the decision window closes.&lt;/td&gt;
&lt;td&gt;If the callback is left as a queue item, no-action remains at 38% residual stockout risk and the modeled line-stop risk cost stays at USD 155800.&lt;/td&gt;
&lt;td&gt;Accept the approval callback into the shadow ledger, reserve 84 critical kits under ALT-PO000318-SPLIT-EXPEDITE-20PCT, and keep external SAP write false until human execution.&lt;/td&gt;
&lt;td&gt;Pay USD 18400 premium freight and handle partial-kit complexity, but reduce modeled stockout risk to 10% and save USD 96400.0 versus no action.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 화면의 결론은 이렇다. &lt;code&gt;PO000318&lt;/code&gt;은 전체 air 전환 대상이 아니다. 승인 callback을 받아 84개 critical kit만 split expedite로 예약하고, 나머지 336개는 기존 ocean shipment에 둔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 이번에 만든 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;scm_sap_split_expedite_approval_evaluation.py&lt;/code&gt;를 추가했다. 전 단계 &lt;code&gt;SCM-046&lt;/code&gt; action ticket을 받아 operator approval callback, no-action 대비 평가 보드, REBEC shadow ledger, order casefile을 한 번에 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 SAP write는 여전히 &lt;code&gt;false&lt;/code&gt;다. 이번 단계는 실제 SAP 변경이 아니라 승인된 액션을 실행 전에 shadow ledger로 닫고 비용과 리스크를 비교하는 evaluation harness다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. callback을 어떻게 처리했나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;callback id는 &lt;code&gt;CB-PO000318-APPROVE-SPLIT-20260503-0104&lt;/code&gt;이고 decision은 &lt;code&gt;approved_with_guardrails&lt;/code&gt;이다. premium freight cap은 &lt;code&gt;USD 20000&lt;/code&gt;이고 전체 air 전환은 금지했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. no-action 대비 평가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평가식은 &lt;code&gt;expected_total_cost = incremental_cost + stockout_probability * line_stop_cost&lt;/code&gt;다. line-stop cost는 synthetic 값 &lt;code&gt;USD 410000&lt;/code&gt;으로 두었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;SCN-PO000318-NO-ACTION&lt;/code&gt;: reject, 잔여 stockout &lt;code&gt;38%&lt;/code&gt;, 예상 late &lt;code&gt;1.8&lt;/code&gt;일, 추가 비용 &lt;code&gt;$0&lt;/code&gt;, 기대 총비용 &lt;code&gt;$155800.0&lt;/code&gt;. 승인 packet을 실행하지 않고 기존 ocean monitoring만 유지한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SCN-PO000318-SPLIT-EXPEDITE-20PCT&lt;/code&gt;: selected, 잔여 stockout &lt;code&gt;10%&lt;/code&gt;, 예상 late &lt;code&gt;0.2&lt;/code&gt;일, 추가 비용 &lt;code&gt;$18400&lt;/code&gt;, 기대 총비용 &lt;code&gt;$59400.0&lt;/code&gt;. 84 critical kits를 split expedite로 예약하고 336 kits는 ocean에 둔다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SCN-PO000318-FULL-AIR&lt;/code&gt;: reserve_only, 잔여 stockout &lt;code&gt;4%&lt;/code&gt;, 예상 late &lt;code&gt;0.0&lt;/code&gt;일, 추가 비용 &lt;code&gt;$76500&lt;/code&gt;, 기대 총비용 &lt;code&gt;$92900.0&lt;/code&gt;. 420 kits 전체를 air expedite로 전환한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SCN-PO000318-PLANT-REBALANCE&lt;/code&gt;: fallback, 잔여 stockout &lt;code&gt;19%&lt;/code&gt;, 예상 late &lt;code&gt;0.7&lt;/code&gt;일, 추가 비용 &lt;code&gt;$9200&lt;/code&gt;, 기대 총비용 &lt;code&gt;$87100.0&lt;/code&gt;. Plant-MX-01 buffer를 Plant-US-02로 임시 재배정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택된 시나리오는 &lt;code&gt;SCN-PO000318-SPLIT-EXPEDITE-20PCT&lt;/code&gt;다. 모델상 절감액은 &lt;code&gt;USD 96400.0&lt;/code&gt;다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. REBEC shadow ledger&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;operator_callback_consumer&lt;/code&gt;: &lt;code&gt;approval_decision_received&lt;/code&gt; - approved_with_guardrails&lt;/li&gt;
&lt;li&gt;&lt;code&gt;action_simulator&lt;/code&gt;: &lt;code&gt;simulate_post_approval_execution&lt;/code&gt; - capacity reservation simulated for 84 critical kits&lt;/li&gt;
&lt;li&gt;&lt;code&gt;evaluation_board&lt;/code&gt;: &lt;code&gt;outcome_candidates_scored&lt;/code&gt; - net expected savings USD 96400.0&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sap_shadow_ledger&lt;/code&gt;: &lt;code&gt;shadow_event_appended&lt;/code&gt; - no external SAP write; human execution packet remains required&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ledger가 실행 결과가 아니라 실행 전 안전장치라는 점이 핵심이다. 외부 SAP write는 하지 않았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 실패한 것과 조심할 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 TMS booking confirmation은 아직 없다. 그래서 &lt;code&gt;2026-05-14T10:00:00-07:00&lt;/code&gt; split ETA는 실행 결과가 아니라 simulation이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UNCTAD, WTO, IMF 신호는 macro pressure다. 이 신호만으로 lane closure를 판정하지 않았고 승인 평가의 배경 신호로만 썼다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 다음에 붙일 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계는 human execution handoff packet을 만들거나, booking confirmation이 들어온 뒤 simulated ETA와 실제 ETA를 비교해 evaluation score를 갱신하는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 로그 첨부&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감 경로와 내부 식별자는 마스킹한 공개용 아티팩트만 아래에 첨부한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;scm-047-run-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-047-publish-check-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-047-official-signal-boundary-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-047-approval-callback-event-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-047-split-expedite-evaluation-board-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-047-shadow-action-ledger-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-047-order-casefile-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-047-summary-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-047-official-signal-verification-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-047-split-expedite-approval-evaluation-spec-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-sap-split-expedite-approval-evaluation-public.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/boYiIJ/dJMcagFphzM/e2yuBJCrfr5P1a3rrSVYlk/scm-047-run-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-047-run-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bomNs8/dJMcabjNEty/JlwYlWPCBPwDjvGaaAk451/scm-047-publish-check-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-047-publish-check-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/eixmQ3/dJMcagejTQX/mMs7IqLvoycXrHuLt2RKD0/scm-047-official-signal-boundary-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-047-official-signal-boundary-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/G15cU/dJMcagejTQZ/ehbW1yp4VysOyKA0DxLyF0/scm-047-approval-callback-event-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-047-approval-callback-event-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bcGh59/dJMcabjNEtD/qUk4NMKcFrRDjYHPlVPk41/scm-047-split-expedite-evaluation-board-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-047-split-expedite-evaluation-board-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bjSYVR/dJMcabjNEtH/auCHmvtuePuiIbKijYPTVK/scm-047-shadow-action-ledger-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-047-shadow-action-ledger-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/ChVop/dJMcaiwoM9L/Fyq3HspFgE75KV9BDQN1VK/scm-047-order-casefile-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-047-order-casefile-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/LMZQk/dJMcagejTRc/6MEXpUYNT1P8MxtWKocfMk/scm-047-summary-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-047-summary-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/liLnb/dJMcabjNEtP/SO6ZoawbG3126NddgLCurk/scm-047-official-signal-verification-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-047-official-signal-verification-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/oJTca/dJMcacprONQ/otPvvyCA7T6r3IBsFfvMmK/scm-047-split-expedite-approval-evaluation-spec-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-047-split-expedite-approval-evaluation-spec-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cBxemN/dJMcacJLR1b/Kyp3EQkLVrNG7jxPbHn8n0/scm-sap-split-expedite-approval-evaluation-public.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-sap-split-expedite-approval-evaluation-public.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.04MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Enterprise AI Systems</category>
      <category>Enterprise AI</category>
      <category>Rebec</category>
      <category>SAP</category>
      <category>SCM</category>
      <category>Split Expedite</category>
      <author>이천재</author>
      <guid isPermaLink="true">https://ai-imojumo.tistory.com/68</guid>
      <comments>https://ai-imojumo.tistory.com/entry/SCM-SAP-split-expedite-approval-evaluation-PO000318-%EC%8A%B9%EC%9D%B8-callback%EA%B3%BC-REBEC-shadow-ledger%EB%A5%BC-%EB%8B%AB%EC%9D%80-SCM-047#entry68comment</comments>
      <pubDate>Sun, 3 May 2026 20:37:20 +0900</pubDate>
    </item>
    <item>
      <title>OpenAI Structured Outputs 사용 기준: JSON 모드보다 strict schema&amp;middot;refusal&amp;middot;검증 실패 처리를 먼저 잠가야 한다</title>
      <link>https://ai-imojumo.tistory.com/entry/OpenAI-Structured-Outputs-%EC%82%AC%EC%9A%A9-%EA%B8%B0%EC%A4%80-JSON-%EB%AA%A8%EB%93%9C%EB%B3%B4%EB%8B%A4-strict-schema%C2%B7refusal%C2%B7%EA%B2%80%EC%A6%9D-%EC%8B%A4%ED%8C%A8-%EC%B2%98%EB%A6%AC%EB%A5%BC-%EB%A8%BC%EC%A0%80-%EC%9E%A0%EA%B0%80%EC%95%BC-%ED%95%9C%EB%8B%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI API 응답을 서비스 코드에 바로 넣을 계획이라면 첫 질문은 &quot;JSON으로 받을 수 있나&quot;가 아니다. 코드가 기대하는 key, type, enum, 실패 branch가 고정되어 있는지가 먼저다. JSON mode는 JSON 파싱 문제를 줄여 주지만, 원하는 schema와 맞는다는 뜻은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;2026-05-02&lt;/code&gt; 기준 OpenAI 공식 문서로 보면 기본 선택지는 분명하다. 사용자가 보는 답변 자체를 구조화해야 하면 Structured Outputs를 보고, 외부 시스템에서 환불, 검색, 업데이트 같은 action을 실행해야 하면 function calling으로 분리한다. Responses API에서는 Structured Outputs를 &lt;code&gt;text.format&lt;/code&gt;으로 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧게 정리하면 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 schema가 필요한 추출, 분류, UI payload는 JSON mode보다 Structured Outputs를 먼저 본다.&lt;/li&gt;
&lt;li&gt;JSON mode를 써야 하는 경우에도 local validation, retry, fail-closed branch를 둔다.&lt;/li&gt;
&lt;li&gt;모든 field는 &lt;code&gt;required&lt;/code&gt;로 두고, optional은 &lt;code&gt;[&quot;string&quot;, &quot;null&quot;]&lt;/code&gt; 같은 nullable union으로 표현한다.&lt;/li&gt;
&lt;li&gt;모든 object에 &lt;code&gt;additionalProperties: false&lt;/code&gt;를 넣는다.&lt;/li&gt;
&lt;li&gt;root schema는 object여야 하며 root &lt;code&gt;anyOf&lt;/code&gt;를 피한다.&lt;/li&gt;
&lt;li&gt;refusal, incomplete output, content filter는 schema parser 앞에서 먼저 분기한다.&lt;/li&gt;
&lt;li&gt;외부 action은 Structured Outputs answer payload가 아니라 function calling contract로 분리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSON 파싱과 schema 계약은 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI Structured Outputs guide는 Structured Outputs를 supplied JSON Schema에 맞는 model response를 만들기 위한 기능으로 설명한다. 이 기능의 쟁점은 &quot;중괄호가 닫혔는가&quot;가 아니라 &quot;내 코드가 기대하는 구조를 지키는가&quot;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON mode는 더 기본적인 기능이다. OpenAI docs는 JSON mode가 valid JSON을 보장하지만 특정 schema와 맞는지는 보장하지 않는다고 설명한다. 따라서 invoice 추출에서 &lt;code&gt;invoice_id&lt;/code&gt;, &lt;code&gt;total&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;를 기대했는데 &lt;code&gt;unexpected_note&lt;/code&gt;가 붙거나 &lt;code&gt;currency&lt;/code&gt;가 빠져도, JSON mode 관점에서는 여전히 valid JSON일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;production parser 앞에서는 이 차이가 크다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;선택지&lt;/th&gt;
&lt;th&gt;맞는 상황&lt;/th&gt;
&lt;th&gt;남는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON mode&lt;/td&gt;
&lt;td&gt;단순 JSON 형태만 필요하거나 Structured Outputs를 못 쓰는 경우&lt;/td&gt;
&lt;td&gt;schema validation, retry, incomplete/refusal 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured Outputs&lt;/td&gt;
&lt;td&gt;응답 key와 type을 고정해야 하는 추출, 분류, UI payload&lt;/td&gt;
&lt;td&gt;schema preflight, refusal branch, business validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Function calling&lt;/td&gt;
&lt;td&gt;외부 tool, DB, 결제, 환불, 검색 action이 필요한 경우&lt;/td&gt;
&lt;td&gt;tool output과 &lt;code&gt;call_id&lt;/code&gt; correlation, side effect 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &quot;JSON으로 줘&quot;라는 prompt는 계약이 아니다. 계약은 schema와 실패 처리에서 만들어진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Responses API에서는 text.format을 본다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Chat Completions 예제를 보면 &lt;code&gt;response_format&lt;/code&gt;을 기억하는 사람이 많다. 하지만 OpenAI migration guide는 Responses API에서 Structured Outputs 정의가 &lt;code&gt;response_format&lt;/code&gt;에서 &lt;code&gt;text.format&lt;/code&gt;으로 이동했다고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 프로젝트라면 이 경계를 먼저 맞춰 두는 편이 낫다. Responses API는 tool call, reasoning item, message item처럼 output을 여러 item으로 다룬다. 여기에 structured answer를 붙일 때는 &lt;code&gt;text.format&lt;/code&gt; 쪽으로 schema를 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 외부 action은 다른 문제다. OpenAI docs는 tool, function, application data에 모델을 연결하려면 function calling을 쓰고, 사용자에게 줄 답변 자체를 구조화하려면 structured output을 쓰는 식으로 나눈다. 환불 실행, 계정 조회, 내부 문서 검색 같은 작업을 단순 structured answer로 처리하면 side effect와 audit trail이 흐려진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;schema는 API 호출 전에 막아야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 local gate에서 10개 scenario를 만들었다. OpenAI API는 호출하지 않았다. 공식 문서의 Structured Outputs 제한을 preflight rule로 바꾸고, 각 schema가 parser 앞까지 갈 수 있는지 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 흔한 실패는 optional field였다. Structured Outputs에서는 모든 field 또는 function parameter가 &lt;code&gt;required&lt;/code&gt;여야 한다. optional 값을 표현하려면 field를 빼는 것이 아니라 &lt;code&gt;null&lt;/code&gt;을 허용하는 union으로 설계해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 아래처럼 &lt;code&gt;due_date&lt;/code&gt;를 properties에는 넣고 required에서 빼면 gate가 막는다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;object&quot;,
  &quot;properties&quot;: {
    &quot;title&quot;: { &quot;type&quot;: &quot;string&quot; },
    &quot;owner&quot;: { &quot;type&quot;: &quot;string&quot; },
    &quot;due_date&quot;: { &quot;type&quot;: [&quot;string&quot;, &quot;null&quot;] }
  },
  &quot;required&quot;: [&quot;title&quot;, &quot;owner&quot;],
  &quot;additionalProperties&quot;: false
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 방향은 단순하다. &lt;code&gt;due_date&lt;/code&gt;도 required에 넣고 값이 없을 때 &lt;code&gt;null&lt;/code&gt;을 받는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;root anyOf와 additionalProperties를 먼저 확인한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zod나 Pydantic을 쓰더라도 generated schema가 OpenAI subset에 맞는지는 따로 봐야 한다. OpenAI docs는 root schema가 object여야 하며 root &lt;code&gt;anyOf&lt;/code&gt;는 사용할 수 없다고 설명한다. Zod discriminated union을 그대로 response format으로 변환하면 top-level &lt;code&gt;anyOf&lt;/code&gt;가 나오는 경우가 있어 preflight에서 막아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나는 &lt;code&gt;additionalProperties: false&lt;/code&gt;다. OpenAI docs는 Structured Outputs가 지정된 key/value만 생성하는 방식을 지원하므로 object에 이 설정을 요구한다고 설명한다. 이 설정이 없으면 &quot;모델이 extra key를 만들었을 때 어떻게 할지&quot;가 모호해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 gate에서는 &lt;code&gt;missing-additional-properties&lt;/code&gt; scenario가 API 호출 전 blocked됐다. schema가 간단해도 이 설정이 빠지면 production parser 앞에서 실패하는 편이 맞다. API까지 보내고 에러를 받는 것보다 CI나 local preflight에서 막는 쪽이 운영하기 쉽다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;schema 크기와 enum은 실제 장애가 된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Structured Outputs는 많은 JSON Schema 기능을 지원하지만 무제한은 아니다. OpenAI docs는 schema에 총 5,000 object properties, 10 levels nesting, 120,000 characters string budget, 1,000 enum values 제한이 있다고 설명한다. 단일 string enum이 250개를 넘으면 enum values 전체 string length 15,000 characters 제한도 붙는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 gate에서 두 가지가 걸렸다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;scenario&lt;/th&gt;
&lt;th&gt;막힌 이유&lt;/th&gt;
&lt;th&gt;먼저 할 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deep-analytics-payload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12단계 nested object&lt;/td&gt;
&lt;td&gt;payload를 단계별 schema로 쪼개거나 UI state를 별도 object로 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;large-enum-catalog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SKU enum 1,100개&lt;/td&gt;
&lt;td&gt;enum을 줄이거나 tool lookup, 검색, 후처리 검증으로 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 schema는 모델보다 운영 코드에서 먼저 무너진다. 타입 정의와 JSON Schema가 어긋나고, enum이 제품 catalog 변경을 따라가지 못하고, UI payload가 한 번에 너무 많은 일을 하게 된다. OpenAI docs도 schema와 programming language type divergence를 막기 위해 Pydantic/Zod SDK support나 CI rule을 권장한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;fine-tuned model에는 keyword 제한이 더 붙는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Structured Outputs guide에는 supported keyword와 unsupported keyword가 나뉘어 있다. composition 쪽에서는 &lt;code&gt;allOf&lt;/code&gt;, &lt;code&gt;not&lt;/code&gt;, &lt;code&gt;dependentRequired&lt;/code&gt;, &lt;code&gt;dependentSchemas&lt;/code&gt;, &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;then&lt;/code&gt;, &lt;code&gt;else&lt;/code&gt;가 지원되지 않는다고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fine-tuned model에서는 제한이 더 늘어난다. docs는 strings의 &lt;code&gt;minLength&lt;/code&gt;, &lt;code&gt;maxLength&lt;/code&gt;, &lt;code&gt;pattern&lt;/code&gt;, &lt;code&gt;format&lt;/code&gt;, numbers의 &lt;code&gt;minimum&lt;/code&gt;, &lt;code&gt;maximum&lt;/code&gt;, &lt;code&gt;multipleOf&lt;/code&gt;, objects의 &lt;code&gt;patternProperties&lt;/code&gt;, arrays의 &lt;code&gt;minItems&lt;/code&gt;, &lt;code&gt;maxItems&lt;/code&gt; 등을 추가로 지원하지 않는다고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 gate의 &lt;code&gt;fine-tuned-regex-user&lt;/code&gt; scenario는 username에 &lt;code&gt;pattern&lt;/code&gt;, &lt;code&gt;minLength&lt;/code&gt;, email에 &lt;code&gt;format&lt;/code&gt;을 둔 schema였다. base model 기준 예시에서는 이런 keyword를 볼 수 있지만, fine-tuned model 조건에서는 blocked로 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 결론은 &quot;정규식을 쓰지 말라&quot;가 아니다. 모델 출력 schema에 모든 검증을 맡기지 말라는 쪽에 가깝다. 계정명, 이메일, SKU, 금액 범위 같은 business validation은 Structured Outputs 뒤의 application validator에도 남겨야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;refusal은 schema parser 앞에서 분기한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Structured Outputs를 켜도 모든 응답이 supplied schema 그대로 오는 것은 아니다. OpenAI docs는 user-generated input에서 safety refusal이 생길 수 있고, refusal은 supplied &lt;code&gt;response_format&lt;/code&gt; schema를 따르지 않을 수 있다고 설명한다. Responses API JSON mode 예시도 refusal content type과 incomplete output을 별도로 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 branch를 parser 뒤에 두면 장애가 난다. parser는 &lt;code&gt;category&lt;/code&gt;, &lt;code&gt;priority&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt;를 기다리는데 실제 content가 refusal이면, 사용자는 안전 거절을 받은 것이 아니라 &quot;응답 파싱 실패&quot;를 보게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리 순서는 이렇다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;API response status가 incomplete인지 본다.&lt;/li&gt;
&lt;li&gt;content type이 refusal인지 본다.&lt;/li&gt;
&lt;li&gt;content filter나 max output token으로 잘렸는지 본다.&lt;/li&gt;
&lt;li&gt;그 다음에 structured JSON을 parse한다.&lt;/li&gt;
&lt;li&gt;business rule validation을 별도로 통과시킨다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 gate의 &lt;code&gt;refusal-sensitive-request&lt;/code&gt;는 schema 자체는 valid였지만 route가 &lt;code&gt;ready_but_needs_refusal_branch&lt;/code&gt;로 잡혔다. schema가 맞아도 parser 앞 branch가 없으면 아직 production-ready가 아니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;user input이 schema와 안 맞을 때도 정해야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI docs는 user-generated input이 schema와 호환되지 않을 때 모델이 schema를 맞추려다 hallucination할 수 있다고 설명한다. 예를 들어 &quot;이 글에서 환불 사유를 뽑아줘&quot;라고 했는데 글 안에 환불 정보가 없으면, 모델이 빈 값을 넣을지, null을 넣을지, &quot;정보 없음&quot;을 넣을지 정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 schema만으로 해결되지 않는다. prompt에 incompatible input 처리 규칙을 넣고, schema에도 &lt;code&gt;null&lt;/code&gt;, empty array, &lt;code&gt;unknown&lt;/code&gt; enum처럼 실패를 표현할 자리를 만들어야 한다. 그래도 최종 business validation은 남긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Structured Outputs는 parser를 덜 흔들리게 해준다. 하지만 입력이 작업과 맞지 않는 문제, safety refusal, partial output, 업무 규칙 위반까지 없애지는 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬 gate 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 run은 OpenAI API를 호출하지 않았다. live model reliability를 측정한 것도 아니다. 공식 문서의 schema subset과 response handling 조건을 deterministic planning gate로 바꿔 10개 scenario를 평가했다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;scenario&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;th&gt;route&lt;/th&gt;
&lt;th&gt;primary action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;support-ticket-strict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ready_strict_structured_output&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_structured_outputs_with_strict_schema&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;json-mode-invoice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;&lt;code&gt;json_mode_requires_validator_retry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prefer_structured_outputs_or_add_local_validation_and_retry&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;optional-field-not-required&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blocked_schema_before_api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fix_schema_subset_before_calling_api&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;root-discriminated-union&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blocked_schema_before_api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fix_schema_subset_before_calling_api&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;missing-additional-properties&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blocked_schema_before_api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fix_schema_subset_before_calling_api&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deep-analytics-payload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blocked_schema_before_api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fix_schema_subset_before_calling_api&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;large-enum-catalog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blocked_schema_before_api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fix_schema_subset_before_calling_api&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fine-tuned-regex-user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blocked_schema_before_api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fix_schema_subset_before_calling_api&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refusal-sensitive-request&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ready_but_needs_refusal_branch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;branch_refusal_before_schema_parser&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refund-action-tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;function_calling_tool_contract&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_function_calling_for_external_action&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 단순했다. schema가 valid인지, JSON이 parse되는지, refusal을 처리하는지, 외부 action인지가 모두 다른 gate였다. 이 네 가지를 한 parser 함수에 몰아넣으면 장애 원인을 구분하기 어렵다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현 전 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI Structured Outputs를 붙이기 전에 아래를 먼저 본다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;결과가 사용자에게 보여줄 structured answer인가, 외부 action을 실행할 tool call인가.&lt;/li&gt;
&lt;li&gt;Responses API라면 &lt;code&gt;text.format&lt;/code&gt;으로 Structured Outputs를 정의했는가.&lt;/li&gt;
&lt;li&gt;JSON mode를 쓰는 이유가 명확한가. Structured Outputs를 쓸 수 있다면 그쪽이 먼저인가.&lt;/li&gt;
&lt;li&gt;root schema가 object이고 root &lt;code&gt;anyOf&lt;/code&gt;가 없는가.&lt;/li&gt;
&lt;li&gt;모든 field가 required인가.&lt;/li&gt;
&lt;li&gt;optional 값은 nullable union으로 표현했는가.&lt;/li&gt;
&lt;li&gt;모든 object에 &lt;code&gt;additionalProperties: false&lt;/code&gt;가 있는가.&lt;/li&gt;
&lt;li&gt;schema가 5,000 properties, 10 depth, 120,000 string budget, enum limit 안에 있는가.&lt;/li&gt;
&lt;li&gt;fine-tuned model에서 지원하지 않는 keyword를 schema에 넣지 않았는가.&lt;/li&gt;
&lt;li&gt;refusal과 incomplete output을 parser 전에 분기하는가.&lt;/li&gt;
&lt;li&gt;user input이 schema와 맞지 않을 때 null, empty array, unknown enum, refusal 중 어떤 경로로 처리할지 정했는가.&lt;/li&gt;
&lt;li&gt;schema와 app type이 갈라지지 않도록 Pydantic/Zod helper, code generation, CI check 중 하나를 쓰는가.&lt;/li&gt;
&lt;li&gt;Structured Outputs 뒤에도 business validation을 남겨 두었는가.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 4번부터 9번은 API 호출 전 gate로 막을 수 있다. 10번부터 13번은 runtime branch다. 둘을 나눠 두면 실패가 훨씬 빨리 보인다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 보면 좋은 글&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI-API-%EB%B0%B0%ED%8F%AC-%EC%B2%B4%ED%81%AC%EB%A6%AC%EC%8A%A4%ED%8A%B8-Responses-API%EB%8A%94-%EC%8B%9C%EC%9E%91%EC%A0%90%EC%9D%B4%EA%B3%A0-eval%C2%B7rate-limit%C2%B7background%EB%B6%80%ED%84%B0-%EC%9E%A0%EA%B0%80%EC%95%BC-%ED%95%9C%EB%8B%A4&quot;&gt;OpenAI API 배포 체크리스트: Responses API는 시작점이고 eval&amp;middot;rate limit&amp;middot;background부터 잠가야 한다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI-Batch-API-%EC%82%AC%EC%9A%A9-%EA%B8%B0%EC%A4%80-50-%ED%95%A0%EC%9D%B8%EB%B3%B4%EB%8B%A4-24%EC%8B%9C%EA%B0%84-SLA%C2%B7JSONL%C2%B7%EC%9E%AC%EC%8B%9C%EB%8F%84-%ED%81%90%EA%B0%80-%EB%A8%BC%EC%A0%80%EB%8B%A4&quot;&gt;OpenAI Batch API 사용 기준: 50% 할인보다 24시간 SLA&amp;middot;JSONL&amp;middot;재시도 큐가 먼저다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI-Apps-SDK%EC%99%80-MCP-%EC%95%B1-%EC%84%A4%EA%B3%84-%EC%9C%84%EC%A0%AF-%EC%BD%94%EB%93%9C%EB%B3%B4%EB%8B%A4-tool-metadata%C2%B7auth%C2%B7%EC%83%81%ED%83%9C-%EA%B2%BD%EA%B3%84%EB%A5%BC-%EB%A8%BC%EC%A0%80-%EC%9E%A0%EA%B0%80%EC%95%BC-%ED%95%9C%EB%8B%A4&quot;&gt;OpenAI Apps SDK와 MCP 앱 설계: 위젯 코드보다 tool metadata&amp;middot;auth&amp;middot;상태 경계를 먼저 잠가야 한다&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/structured-outputs&quot;&gt;OpenAI Structured model outputs guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/function-calling&quot;&gt;OpenAI Function calling guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/migrate-to-responses&quot;&gt;OpenAI Migrate to the Responses API guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 로그 첨부&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감정보를 제거한 공개용 로그와 실험 스크립트만 아래에 첨부한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;wn118-structured-outputs-schema-gate-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn118-structured-outputs-schema-gate-summary-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn118-structured-outputs-schema-gate-public.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cL9haw/dJMcaiDaB1q/RkgoPhk7HcHD8xaY4qocG1/wn118-structured-outputs-schema-gate-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn118-structured-outputs-schema-gate-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.05MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/AewbI/dJMcabqyayR/9Qy7yMHbAOIW1SMpE5D490/wn118-structured-outputs-schema-gate-summary-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn118-structured-outputs-schema-gate-summary-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cJPcyt/dJMcaiDaB1s/RCKxHyezbe3RlQGkKOEWkK/wn118-structured-outputs-schema-gate-public.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn118-structured-outputs-schema-gate-public.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.02MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Model APIs</category>
      <category>JSON Schema</category>
      <category>Model APIs</category>
      <category>openAI API</category>
      <category>Responses API</category>
      <category>structured outputs</category>
      <author>이천재</author>
      <guid isPermaLink="true">https://ai-imojumo.tistory.com/67</guid>
      <comments>https://ai-imojumo.tistory.com/entry/OpenAI-Structured-Outputs-%EC%82%AC%EC%9A%A9-%EA%B8%B0%EC%A4%80-JSON-%EB%AA%A8%EB%93%9C%EB%B3%B4%EB%8B%A4-strict-schema%C2%B7refusal%C2%B7%EA%B2%80%EC%A6%9D-%EC%8B%A4%ED%8C%A8-%EC%B2%98%EB%A6%AC%EB%A5%BC-%EB%A8%BC%EC%A0%80-%EC%9E%A0%EA%B0%80%EC%95%BC-%ED%95%9C%EB%8B%A4#entry67comment</comments>
      <pubDate>Sat, 2 May 2026 18:15:35 +0900</pubDate>
    </item>
    <item>
      <title>OpenAI File Search 사용 기준: Vector Store 비용&amp;middot;만료&amp;middot;청킹을 먼저 잠가야 RAG 운영이 덜 흔들린다</title>
      <link>https://ai-imojumo.tistory.com/entry/OpenAI-File-Search-%EC%82%AC%EC%9A%A9-%EA%B8%B0%EC%A4%80-Vector-Store-%EB%B9%84%EC%9A%A9%C2%B7%EB%A7%8C%EB%A3%8C%C2%B7%EC%B2%AD%ED%82%B9%EC%9D%84-%EB%A8%BC%EC%A0%80-%EC%9E%A0%EA%B0%80%EC%95%BC-RAG-%EC%9A%B4%EC%98%81%EC%9D%B4-%EB%8D%9C-%ED%9D%94%EB%93%A4%EB%A6%B0%EB%8B%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI File Search를 붙일 때 첫 질문은 &quot;RAG를 직접 만들까, API tool을 쓸까&quot;가 아니다. 같은 문서를 반복해서 검색할 것인지, 그 문서를 며칠 보관할 것인지, 파일이 제한 안에 들어오는지, 검색 결과를 나중에 감사할 수 있어야 하는지가 먼저다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;2026-05-01&lt;/code&gt; 기준 OpenAI 공식 문서로 보면 경계는 꽤 분명하다. File Search는 Responses API에서 쓰는 hosted tool이다. Vector Store는 그 tool이 검색할 수 있도록 파일을 chunking, embedding, indexing해 두는 저장/index layer다. 반복 질의가 있는 문서 지식베이스라면 Vector Store가 맞다. 반대로 회의록 파일 하나를 두 번 요약하는 작업이라면 persistent Vector Store를 만드는 게 오히려 운영 부담이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧게 정리하면 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자주 검색할 제품 문서, FAQ, 정책 문서는 File Search + Vector Store부터 본다.&lt;/li&gt;
&lt;li&gt;한두 번 읽고 끝나는 파일은 prompt context, file input, 별도 임시 처리로 충분한지 먼저 본다.&lt;/li&gt;
&lt;li&gt;Vector Store를 만들기 전에 storage 비용, tool call 비용, model token 비용을 분리해서 계산한다.&lt;/li&gt;
&lt;li&gt;임시 store는 &lt;code&gt;expires_after&lt;/code&gt;를 잡고, 오래 사는 store는 stale file review와 cost owner를 둔다.&lt;/li&gt;
&lt;li&gt;업로드 전 파일당 512 MB, 5,000,000 tokens, batch 500 files, chunk overlap 제한을 확인한다.&lt;/li&gt;
&lt;li&gt;검색 품질을 확인해야 하면 &lt;code&gt;include=[&quot;file_search_call.results&quot;]&lt;/code&gt;와 작은 &lt;code&gt;max_num_results&lt;/code&gt;부터 쓴다.&lt;/li&gt;
&lt;li&gt;테넌트, 지역, 문서 종류를 나눠야 하면 ingestion 단계에서 attributes와 filter key를 먼저 설계한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;File Search는 tool이고 Vector Store는 저장소다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI File Search guide는 File Search를 Responses API에서 사용할 수 있는 tool로 설명한다. model이 답변을 만들기 전에 vector store에 있는 파일을 semantic search와 keyword search로 찾아볼 수 있게 해준다. 직접 검색 executor를 구현하지 않아도 되는 hosted tool이라는 점이 장점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 File Search만 켠다고 문서 검색 시스템이 완성되는 것은 아니다. Retrieval guide는 vector store를 semantic search index로 설명한다. 파일을 vector store에 추가하면 OpenAI 쪽에서 chunking, embedding, indexing을 처리한다. 이 말은 곧 저장 수명, 파일 크기, chunk 설정, metadata, 비용을 운영자가 먼저 정해야 한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 나누면 이렇다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;먼저 볼 질문&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;File Search&lt;/td&gt;
&lt;td&gt;Responses API에서 모델이 호출하는 검색 tool&lt;/td&gt;
&lt;td&gt;답변 전에 파일 검색이 필요한가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector Store&lt;/td&gt;
&lt;td&gt;검색 가능한 파일 index와 storage&lt;/td&gt;
&lt;td&gt;어떤 파일을 얼마나 오래 저장할 것인가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector store file&lt;/td&gt;
&lt;td&gt;원본 file을 store에 붙인 indexed wrapper&lt;/td&gt;
&lt;td&gt;metadata, chunking, batch 상태가 맞는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct file/prompt 처리&lt;/td&gt;
&lt;td&gt;persistent index 없이 한 번 처리&lt;/td&gt;
&lt;td&gt;반복 검색 없이 끝나는가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &quot;문서가 있다&quot;는 이유만으로 Vector Store를 만들면 안 된다. 문서를 반복해서 검색하고, 결과를 citation과 함께 답변에 넣고, 나중에 같은 store를 재사용할 때 가치가 커진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 번 볼 파일이면 Vector Store가 과할 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 local gate에서 &lt;code&gt;one-off-board-minutes&lt;/code&gt; scenario는 persistent Vector Store가 아니라 &lt;code&gt;direct_file_or_prompt_context&lt;/code&gt; route로 분리했다. 0.02 GB 파일 하나를 하루 동안 두 번만 볼 예정이었기 때문이다. 비용 estimate 자체는 작았지만, index를 만들고 만료를 관리하고 결과 품질을 확인하는 운영 비용이 더 크다고 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 &lt;code&gt;temporary-support-faq&lt;/code&gt;는 7일짜리 FAQ 초안이었지만 File Search + Vector Store route로 갔다. 42개 파일을 일주일 동안 280회 검색할 예정이었고, 답변 근거를 확인해야 했기 때문이다. 이런 경우는 storage free tier 안에 들어가더라도 tool call cost와 결과 inspection을 같이 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이는 실제 구현에서 자주 놓친다. &quot;RAG니까 vector store&quot;가 아니라 &quot;같은 지식을 여러 번 검색하고 관리할 필요가 있는가&quot;가 기준이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비용은 storage와 tool call을 나눠서 본다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI pricing page는 File Search storage를 &lt;code&gt;$0.10 / GB per day (1 GB free)&lt;/code&gt;, tool call을 &lt;code&gt;$2.50 / 1k calls&lt;/code&gt;로 설명한다. 또 built-in tools에 쓰인 token은 선택한 model의 token rate로 과금된다는 note가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 비용은 적어도 세 갈래로 봐야 한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;무엇을 뜻하나&lt;/th&gt;
&lt;th&gt;운영자가 할 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;storage&lt;/td&gt;
&lt;td&gt;vector store에 저장된 parsed chunks와 embeddings&lt;/td&gt;
&lt;td&gt;보관 기간과 삭제 정책을 정한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tool call&lt;/td&gt;
&lt;td&gt;File Search tool 호출 횟수&lt;/td&gt;
&lt;td&gt;기능별 호출량을 로그로 남긴다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;model token&lt;/td&gt;
&lt;td&gt;검색 결과가 model context에 들어가 답변을 만들 때의 token&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max_num_results&lt;/code&gt;와 응답 길이를 조절한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 gate의 &lt;code&gt;evergreen-product-docs&lt;/code&gt; scenario는 8.0 GB corpus, 350 calls/day, 30일 retention으로 estimate했다. storage estimate는 &lt;code&gt;$21.00&lt;/code&gt;, tool call estimate는 &lt;code&gt;$26.25&lt;/code&gt;, 합계는 &lt;code&gt;$47.25&lt;/code&gt;였다. 이 숫자는 실제 청구서가 아니다. OpenAI 문서도 storage가 parsed chunks와 embeddings 기준이라고 설명하므로, local gate는 운영자가 사전 budget을 잡기 위한 planning approximation이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 store도 공짜라고 보면 안 된다. &lt;code&gt;temporary-support-faq&lt;/code&gt;는 0.35 GB라 storage는 free tier 안에 있었지만 7일 동안 280번 호출되면 tool call estimate가 &lt;code&gt;$0.70&lt;/code&gt;으로 남았다. 규모가 작을 때는 큰돈이 아니어도, 기능이 여러 개로 늘어나면 call 수가 먼저 튄다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;임시 store는 expires_after를 먼저 넣는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retrieval guide는 vector store에 &lt;code&gt;expires_after&lt;/code&gt; expiration policy를 설정할 수 있다고 설명한다. vector store가 expire되면 associated &lt;code&gt;vector_store.file&lt;/code&gt; objects가 삭제되고 더 이상 charge되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션은 &quot;나중에 정리하자&quot;로 미루면 잘 안 된다. 실험용 문서, 고객사 PoC 문서, 캠페인 FAQ처럼 수명이 짧은 corpus는 만들 때부터 만료를 넣어야 한다. 이번 gate에서도 short-lived scenario는 checklist에 &lt;code&gt;set expires_after last_active_at N days&lt;/code&gt;를 넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오래 사는 제품 문서 store는 다르게 봐야 한다. 자동 만료를 짧게 잡으면 필요한 문서가 사라질 수 있다. 대신 stale file review, owner, budget threshold, 재색인 주기를 따로 둔다. File Search가 hosted tool이어도 지식베이스 운영 책임까지 사라지는 것은 아니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;업로드 전 파일과 batch 제한을 막아야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retrieval guide는 파일당 최대 512 MB, 5,000,000 tokens 제한을 설명한다. 또한 vector store file batches는 최대 500개 파일을 한 요청에 넣을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 gate에서 세 가지 scenario가 ingestion 전에 막혔다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;scenario&lt;/th&gt;
&lt;th&gt;막힌 이유&lt;/th&gt;
&lt;th&gt;먼저 할 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;oversized-pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;640 MB PDF가 512 MB file limit을 넘음&lt;/td&gt;
&lt;td&gt;파일 분할, 원본 압축, 필요한 장만 추출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;huge-token-contract&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;한 파일이 6,000,000 tokens로 5M token limit을 넘음&lt;/td&gt;
&lt;td&gt;문서 단위 분리, appendix 분리, ingestion 전 token estimate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bad-static-chunking&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;300 token chunk에 200 token overlap 지정&lt;/td&gt;
&lt;td&gt;overlap을 chunk size의 절반 이하로 낮춤&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;bulk-600-file-ingest&lt;/code&gt;는 blocked는 아니었지만 warning을 받았다. 한 batch에 600개 파일을 넣으려 했기 때문이다. 이 경우 최소 두 batch로 나누고, 각 batch 상태를 polling하거나 실패 파일을 따로 재시도해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 gate는 단순하지만 중요하다. upload를 먼저 시도한 뒤 에러를 읽는 방식은 자동화에서 비싸다. 파일 크기, token estimate, batch size, chunk setting은 upload 전 validator로 막는 편이 낫다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;청킹은 품질 knob이면서 비용 knob이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retrieval guide의 default chunking은 &lt;code&gt;max_chunk_size_tokens=800&lt;/code&gt;, &lt;code&gt;chunk_overlap_tokens=400&lt;/code&gt;이다. &lt;code&gt;chunking_strategy&lt;/code&gt;로 조정할 수 있지만 max chunk size는 100~4096 사이여야 하고, overlap은 non-negative이며 chunk size의 절반을 넘지 않아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chunk를 작게 만들면 검색 단위가 촘촘해질 수 있다. 대신 chunk 수가 늘고, overlap이 크면 같은 내용이 여러 chunk에 반복된다. 반대로 chunk를 너무 크게 만들면 문맥은 넓어지지만 필요한 문장만 정확히 뽑기 어려울 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 gate는 largest file 기준 chunk estimate도 남겼다. &lt;code&gt;evergreen-product-docs&lt;/code&gt;의 largest file은 650,000 tokens였고 default 800/400 설정에서 1,624 chunks로 추정됐다. 이 역시 OpenAI 내부 처리 결과가 아니라 planning estimate다. 하지만 &quot;문서 하나가 대략 몇 검색 단위로 쪼개질지&quot;를 업로드 전에 보는 데에는 충분하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;metadata filter는 ingestion 전에 설계한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;File Search guide는 metadata filtering 예시를 제공하고, Retrieval guide는 vector store file에 attributes를 붙여 semantic search filtering에 사용할 수 있다고 설명한다. 이 기능은 테넌트, 국가, 문서 종류, 제품 버전이 섞이는 순간 중요해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 같은 store에 한국 정책, 미국 정책, 일본 정책 문서가 같이 들어가는데 filter key가 없다면, 질문은 한국어여도 검색 결과가 다른 지역 문서로 섞일 수 있다. 나중에 파일명 parsing으로 때우는 것보다 ingestion 때 attributes를 붙이는 편이 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 gate의 &lt;code&gt;tenant-policy-kb&lt;/code&gt; scenario는 2.2 GB corpus, 180 calls/day, 14일 retention이었다. route는 File Search + Vector Store였지만 checklist에는 &lt;code&gt;attach attributes for filter keys before ingestion&lt;/code&gt;을 넣었다. 이 scenario의 핵심은 storage보다 filter였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;검색 결과를 보려면 include를 켜야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;File Search guide는 output text의 annotation으로 file reference를 볼 수 있지만, file search call은 search results를 기본으로 반환하지 않는다고 설명한다. 검색 결과 자체를 response에 포함하려면 &lt;code&gt;include=[&quot;file_search_call.results&quot;]&lt;/code&gt;를 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션은 초기에 꼭 켜 보는 편이 좋다. 답변이 맞아 보여도 실제로 어떤 chunk가 들어왔는지 모르면, hallucination인지 retrieval miss인지 prompt 문제인지 구분하기 어렵다. 다만 운영 트래픽에서 항상 full result를 저장할지는 별도 결정이다. privacy, log volume, token/cost 경계를 같이 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;max_num_results&lt;/code&gt;도 같은 맥락이다. File Search guide는 결과 수 제한이 token usage와 latency를 줄일 수 있지만 answer quality와 tradeoff가 있다고 설명한다. 처음부터 많이 가져오는 것보다 작은 값으로 시작하고, 실패 case를 보며 올리는 방식이 안전하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬 gate 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 run에서는 OpenAI API를 호출하지 않았다. 파일도 업로드하지 않았고 retrieval 품질도 측정하지 않았다. 공식 문서의 제한과 가격 조건을 deterministic planning gate로 바꿔 8개 scenario를 평가했다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;scenario&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;th&gt;route&lt;/th&gt;
&lt;th&gt;cost window USD&lt;/th&gt;
&lt;th&gt;primary action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;temporary-support-faq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;file_search_vector_store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.7000&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_file_search_with_vector_store&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;evergreen-product-docs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;td&gt;&lt;code&gt;file_search_vector_store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;47.2500&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_file_search_with_vector_store&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;one-off-board-minutes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;td&gt;&lt;code&gt;direct_file_or_prompt_context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.0050&lt;/td&gt;
&lt;td&gt;&lt;code&gt;avoid_persistent_vector_store_for_one_off_low_reuse_task&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tenant-policy-kb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;file_search_vector_store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7.9800&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_file_search_with_vector_store&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bulk-600-file-ingest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;92&lt;/td&gt;
&lt;td&gt;&lt;code&gt;file_search_vector_store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.0500&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_file_search_with_vector_store&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;oversized-pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blocked_before_ingestion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.4375&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fix_file_or_chunking_limits_before_vector_store_ingestion&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;huge-token-contract&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blocked_before_ingestion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.5000&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fix_file_or_chunking_limits_before_vector_store_ingestion&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bad-static-chunking&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blocked_before_ingestion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.1125&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fix_file_or_chunking_limits_before_vector_store_ingestion&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 결과는 두 가지였다. 첫째, 반복 검색과 evidence inspection이 필요한 문서는 File Search + Vector Store가 맞았다. 둘째, File Search를 쓰기로 한 뒤에도 upload 전 gate가 필요했다. 파일 크기, token 수, batch size, chunk overlap이 틀리면 retrieval 품질을 보기 전에 막힌다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현 전 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI File Search를 붙이기 전에 아래 질문을 먼저 본다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이 문서를 같은 기능에서 반복 검색하는가.&lt;/li&gt;
&lt;li&gt;한두 번 처리하고 끝나는 파일이라면 persistent Vector Store 없이 해결할 수 있는가.&lt;/li&gt;
&lt;li&gt;corpus size와 예상 retention으로 storage budget을 계산했는가.&lt;/li&gt;
&lt;li&gt;예상 query call 수로 File Search tool call cost를 따로 계산했는가.&lt;/li&gt;
&lt;li&gt;model token cost가 &lt;code&gt;max_num_results&lt;/code&gt;와 응답 길이에 따라 늘어나는 구조를 이해했는가.&lt;/li&gt;
&lt;li&gt;임시 corpus라면 &lt;code&gt;expires_after&lt;/code&gt;를 만들 때 넣었는가.&lt;/li&gt;
&lt;li&gt;오래 사는 store라면 stale file review, owner, budget threshold가 있는가.&lt;/li&gt;
&lt;li&gt;파일당 512 MB와 5,000,000 tokens 제한을 upload 전에 확인했는가.&lt;/li&gt;
&lt;li&gt;500개가 넘는 파일은 batch를 나눌 계획이 있는가.&lt;/li&gt;
&lt;li&gt;static chunking을 쓴다면 chunk size와 overlap이 허용 범위 안에 있는가.&lt;/li&gt;
&lt;li&gt;테넌트, 지역, 제품 버전, 문서 종류 filter가 필요하면 attributes를 ingestion 전에 붙였는가.&lt;/li&gt;
&lt;li&gt;품질 점검 때 &lt;code&gt;include=[&quot;file_search_call.results&quot;]&lt;/code&gt;로 실제 검색 결과를 확인했는가.&lt;/li&gt;
&lt;li&gt;운영 기본값은 작은 &lt;code&gt;max_num_results&lt;/code&gt;에서 시작하고 실패 case를 보며 올릴 계획인가.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 1번이 비어 있으면 Vector Store를 만들 이유가 약하다. 6번과 7번이 비어 있으면 비용이 새기 쉽다. 8번부터 10번이 비어 있으면 업로드 자동화가 중간에 깨질 가능성이 높다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 보면 좋은 글&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/multimodal-document-rag-text-chunking-limit&quot;&gt;멀티모달 문서 RAG는 왜 텍스트 청킹만으로 안 되는가: 표&amp;middot;차트&amp;middot;레이아웃에서 깨지는 지점&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI-API-%EB%B0%B0%ED%8F%AC-%EC%B2%B4%ED%81%AC%EB%A6%AC%EC%8A%A4%ED%8A%B8-Responses-API%EB%8A%94-%EC%8B%9C%EC%9E%91%EC%A0%90%EC%9D%B4%EA%B3%A0-eval%C2%B7rate-limit%C2%B7background%EB%B6%80%ED%84%B0-%EC%9E%A0%EA%B0%80%EC%95%BC-%ED%95%9C%EB%8B%A4&quot;&gt;OpenAI API 배포 체크리스트: Responses API는 시작점이고 eval&amp;middot;rate limit&amp;middot;background부터 잠가야 한다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI-Batch-API-%EC%82%AC%EC%9A%A9-%EA%B8%B0%EC%A4%80-50-%ED%95%A0%EC%9D%B8%EB%B3%B4%EB%8B%A4-24%EC%8B%9C%EA%B0%84-SLA%C2%B7JSONL%C2%B7%EC%9E%AC%EC%8B%9C%EB%8F%84-%ED%81%90%EA%B0%80-%EB%A8%BC%EC%A0%80%EB%8B%A4&quot;&gt;OpenAI Batch API 사용 기준: 50% 할인보다 24시간 SLA&amp;middot;JSONL&amp;middot;재시도 큐가 먼저다&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/tools-file-search&quot;&gt;OpenAI File search guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/retrieval&quot;&gt;OpenAI Retrieval guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/reference/resources/vector_stores&quot;&gt;OpenAI Vector Stores API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/reference/resources/vector_stores/methods/search&quot;&gt;OpenAI Vector Store search API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/pricing&quot;&gt;OpenAI Pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 로그 첨부&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감정보를 제거한 공개용 로그와 실험 스크립트만 아래에 첨부한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;wn117-file-search-vector-store-gate-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn117-file-search-vector-store-gate-summary-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn117-file-search-vector-store-gate-public.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/xClxC/dJMcaffqHtv/6sNFcNHzTeDb6mbnabi15K/wn117-file-search-vector-store-gate-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn117-file-search-vector-store-gate-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/dOFTgR/dJMcahqKTsQ/BHkYKwTkFHmWtSr7f7oWlk/wn117-file-search-vector-store-gate-summary-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn117-file-search-vector-store-gate-summary-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bWLiqb/dJMcaffqHtw/IUOvEi9qYLteIlPatkCTqk/wn117-file-search-vector-store-gate-public.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn117-file-search-vector-store-gate-public.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Model APIs</category>
      <category>File Search</category>
      <category>Model APIs</category>
      <category>openAI API</category>
      <category>Rag</category>
      <category>Vector Stores</category>
      <author>이천재</author>
      <guid isPermaLink="true">https://ai-imojumo.tistory.com/66</guid>
      <comments>https://ai-imojumo.tistory.com/entry/OpenAI-File-Search-%EC%82%AC%EC%9A%A9-%EA%B8%B0%EC%A4%80-Vector-Store-%EB%B9%84%EC%9A%A9%C2%B7%EB%A7%8C%EB%A3%8C%C2%B7%EC%B2%AD%ED%82%B9%EC%9D%84-%EB%A8%BC%EC%A0%80-%EC%9E%A0%EA%B0%80%EC%95%BC-RAG-%EC%9A%B4%EC%98%81%EC%9D%B4-%EB%8D%9C-%ED%9D%94%EB%93%A4%EB%A6%B0%EB%8B%A4#entry66comment</comments>
      <pubDate>Fri, 1 May 2026 21:32:16 +0900</pubDate>
    </item>
    <item>
      <title>SCM PDF identity watch executor: PO000225 WEO PDF hash가 baseline과 일치해 watch를 clean으로 닫은 SCM-045</title>
      <link>https://ai-imojumo.tistory.com/entry/SCM-PDF-identity-watch-executor-PO000225-WEO-PDF-hash%EA%B0%80-baseline%EA%B3%BC-%EC%9D%BC%EC%B9%98%ED%95%B4-watch%EB%A5%BC-clean%EC%9C%BC%EB%A1%9C-%EB%8B%AB%EC%9D%80-SCM-045</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PO000225&lt;/code&gt;의 WEO PDF watch는 이제 닫아도 된다. 이번에는 웹 표면만 본 것이 아니라 PDF binary까지 다시 받아 &lt;code&gt;SCM-042&lt;/code&gt; baseline과 맞췄다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 운영자가 먼저 보는 문서 case file&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;문서 엔티티&lt;/th&gt;
&lt;th&gt;연결 주문&lt;/th&gt;
&lt;th&gt;lane&lt;/th&gt;
&lt;th&gt;현재 문서 상태&lt;/th&gt;
&lt;th&gt;원래 due date&lt;/th&gt;
&lt;th&gt;외부 신호&lt;/th&gt;
&lt;th&gt;영향 이유&lt;/th&gt;
&lt;th&gt;아무것도 안 했을 때 결과&lt;/th&gt;
&lt;th&gt;추천 액션&lt;/th&gt;
&lt;th&gt;tradeoff&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DOC-WEO-2026APR-COMPILED-PDF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PO000225&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PO000225 docs branch -&amp;gt; IMF WEO compiled PDF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;closed_by_direct_asset_capture&lt;/code&gt;; scheduled watch result &lt;code&gt;identity_match_clean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-04-30&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-05-01T16:17:44+09:00&lt;/code&gt; 기준 IMF direct PDF를 다시 받아 SHA-256 &lt;code&gt;5c281721761f2ac8f8ae313ea977a0039539aa760039ea3717cf54002d82432a&lt;/code&gt;, byte size &lt;code&gt;8298182&lt;/code&gt;, PDF magic &lt;code&gt;%PDF-1.6&lt;/code&gt;, page count &lt;code&gt;180&lt;/code&gt;이 &lt;code&gt;SCM-042&lt;/code&gt; baseline과 모두 일치함을 확인했다&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SCM-042&lt;/code&gt; baseline은 URL/date만이 아니라 binary identity까지 요구한다. 이번 비교로 same-URL silent republish 의심을 닫을 수 있다&lt;/td&gt;
&lt;td&gt;watch verdict를 반영하지 않으면 &lt;code&gt;PO000225&lt;/code&gt; docs branch가 이미 검증된 PDF를 계속 open risk로 들고 간다&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ALT-KEEP-CLOSED-AFTER-IDENTITY-MATCH&lt;/code&gt;를 선택한다. watch queue를 닫고 &lt;code&gt;PO000225&lt;/code&gt; 문서 branch를 닫힌 상태로 유지한다&lt;/td&gt;
&lt;td&gt;빠르게 닫을 수 있지만, 이번 판단은 현재 PDF hash가 baseline과 같다는 범위 안에서만 유효하다. 이후 errata나 PDF 재배포가 생기면 새 watch가 필요하다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 화면의 결론은 분명하다. &lt;code&gt;PO000225&lt;/code&gt;는 다시 열 일이 아니라, watch queue를 닫을 일이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 이번에 만든 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 실행기 &lt;code&gt;scm_sap_pdf_identity_watch_executor.py&lt;/code&gt;를 추가했다. 이 스크립트는 &lt;code&gt;SCM-042&lt;/code&gt;의 PDF identity baseline queue를 읽고 scheduled window 이후 현재 PDF를 다시 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교 항목은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;direct URL&lt;/li&gt;
&lt;li&gt;content type&lt;/li&gt;
&lt;li&gt;PDF magic&lt;/li&gt;
&lt;li&gt;page count&lt;/li&gt;
&lt;li&gt;byte size&lt;/li&gt;
&lt;li&gt;SHA-256&lt;/li&gt;
&lt;li&gt;errata update date&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 실행에서는 로컬 DNS 실패로 &lt;code&gt;ALT-MANUAL-PDF-EVIDENCE-HOLD&lt;/code&gt;가 나왔다. 이번 재시도에서는 PDF binary capture가 성공했고, hash와 byte size가 baseline과 일치했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 공식 근거는 어디까지 잠갔나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 IMF 표면은 &lt;code&gt;2026-05-01T16:17:44+09:00&lt;/code&gt; 기준으로 다시 확인했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IMF WEO issue page: &lt;code&gt;2026-04-14&lt;/code&gt; URL의 April 2026 WEO page&lt;/li&gt;
&lt;li&gt;IMF direct PDF: &lt;code&gt;application/pdf&lt;/code&gt;, PDF version &lt;code&gt;1.6&lt;/code&gt;, page count &lt;code&gt;180&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IMF Data WEO dataset: &lt;code&gt;April 2026&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IMF WEO press briefing transcript: &lt;code&gt;2026-04-14&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PDF metadata도 baseline과 맞았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SHA-256: &lt;code&gt;5c281721761f2ac8f8ae313ea977a0039539aa760039ea3717cf54002d82432a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;byte size: &lt;code&gt;8298182&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;PDF magic: &lt;code&gt;%PDF-1.6&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pdfinfo&lt;/code&gt; page count: &lt;code&gt;180&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;PDF modified date: &lt;code&gt;2026-04-22 02:49:30 KST&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 PDF 원문은 저장하거나 첨부하지 않았다. identity metadata만 run log에 남겼다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 선택한 대안&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택한 대안은 &lt;code&gt;ALT-KEEP-CLOSED-AFTER-IDENTITY-MATCH&lt;/code&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 대안은 세 가지를 동시에 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;PO000225&lt;/code&gt; docs branch를 닫힌 상태로 유지한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WATCH-PO000225-WEO-PDF-IDENTITY-20260430&lt;/code&gt; queue를 clean verdict로 닫는다.&lt;/li&gt;
&lt;li&gt;외부 SAP write는 하지 않고 내부 action workflow에만 결과를 기록한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ALT-BINARY-DELTA-REVIEW&lt;/code&gt;와 &lt;code&gt;ALT-REOPEN-ON-ERRATA-DELTA&lt;/code&gt;는 이번에는 열지 않았다. SHA-256, byte size, page count, visible errata date가 baseline과 모두 같았기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. REBEC 흐름&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;watch_scheduler -&amp;gt; pdf_identity_watch_executor&lt;/code&gt;: &lt;code&gt;2026-04-30 09:00 KST&lt;/code&gt; 이후 watch window가 열렸다고 전달&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pdf_identity_watch_executor -&amp;gt; official_signal_collector&lt;/code&gt;: IMF issue page, PDF, data, transcript 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pdf_identity_watch_executor -&amp;gt; binary_identity_comparator&lt;/code&gt;: current PDF binary를 baseline과 비교&lt;/li&gt;
&lt;li&gt;&lt;code&gt;binary_identity_comparator -&amp;gt; sap_order_state_coordinator&lt;/code&gt;: 외부 SAP write 없이 keep-closed action을 기록&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름의 의미는 간단하다. 이 주문은 더 이상 &quot;PDF를 못 봐서 남은 위험&quot;이 아니라 &quot;PDF identity가 맞아서 닫을 수 있는 문서 branch&quot;가 됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 실패한 것과 조심할 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 실행에서는 DNS 실패 때문에 current hash를 만들지 못했다. 그래서 manual hold가 맞았다. 이번에는 같은 PDF URL을 다시 받아 hash 비교가 성공했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 주의점은 하나다. clean verdict는 &lt;code&gt;2026-05-01T16:17:44+09:00&lt;/code&gt;에 받은 PDF가 &lt;code&gt;SCM-042&lt;/code&gt; baseline과 같다는 뜻이다. 이후 IMF가 새 errata를 올리거나 같은 URL로 PDF를 다시 배포하면 새 watch를 열어야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 그래서 언제 끝나나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 &lt;code&gt;PO000225&lt;/code&gt; WEO PDF watch arc는 여기서 끝내도 된다. &lt;code&gt;SCM-037&lt;/code&gt;부터 이어진 &quot;compiled PDF가 아직 안 잡혔다&quot;는 꼬리는 &lt;code&gt;SCM-045&lt;/code&gt;에서 hash까지 맞추고 닫혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 SCM/SAP applied AI series는 끝이라기보다 운영 기능을 하나씩 붙이는 장기 빌드다. 다만 이 특정 IMF WEO PDF closure branch는 다음 run에서 또 끌고 갈 이유가 없다. 다음부터는 새 장애 축이나 평가/운영 자동화로 넘어가면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 로그 첨부&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감 경로와 내부 식별자는 마스킹한 공개용 아티팩트만 아래에 첨부한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;scm-045-run-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-045-publish-check-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-045-pdf-identity-watch-result-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-045-action-workflow-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-045-pdf-identity-watch-casefile-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-045-pdf-identity-watch-summary-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-045-imf-pdf-identity-watch-verification-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-045-pdf-identity-watch-executor-spec-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-sap-pdf-identity-watch-executor-public.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bbgD3C/dJMcaad5QbO/ZadIdh1ozGvLMIEJotAZLK/scm-045-run-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-045-run-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/AMd8r/dJMcaad5QbP/Xt5QoW4eRwB5pgDVKEBMr1/scm-045-publish-check-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-045-publish-check-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cjjFvE/dJMcaaZq3Yg/GGmEK2KIWzC8B3Fc7AzVw1/scm-045-pdf-identity-watch-result-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-045-pdf-identity-watch-result-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cqcbDw/dJMcaad5QbQ/MdMbLE3gkjUzxfhRKF0Qak/scm-045-action-workflow-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-045-action-workflow-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/ckl2Rd/dJMcaaZq3Yh/huljCfgsCGmpwpUeFKADZK/scm-045-pdf-identity-watch-casefile-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-045-pdf-identity-watch-casefile-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bJqXt7/dJMcaad5QbR/854OH6vHpv8DEQF0I51Wk0/scm-045-pdf-identity-watch-summary-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-045-pdf-identity-watch-summary-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/buGXue/dJMcaad5QbS/y80dGY7pMb1pMsnvyIMNfK/scm-045-imf-pdf-identity-watch-verification-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-045-imf-pdf-identity-watch-verification-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/dMSC0e/dJMcaad5QbU/wiv01DbaKK2aAHofmgui01/scm-045-pdf-identity-watch-executor-spec-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-045-pdf-identity-watch-executor-spec-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/lGhMN/dJMcaaZq3Yk/mtMtXqQi6RkQEL394FpJKK/scm-sap-pdf-identity-watch-executor-public.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-sap-pdf-identity-watch-executor-public.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.03MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Enterprise AI Systems</category>
      <category>Enterprise AI</category>
      <category>IMF</category>
      <category>PDF Identity</category>
      <category>SAP</category>
      <category>SCM</category>
      <author>이천재</author>
      <guid isPermaLink="true">https://ai-imojumo.tistory.com/65</guid>
      <comments>https://ai-imojumo.tistory.com/entry/SCM-PDF-identity-watch-executor-PO000225-WEO-PDF-hash%EA%B0%80-baseline%EA%B3%BC-%EC%9D%BC%EC%B9%98%ED%95%B4-watch%EB%A5%BC-clean%EC%9C%BC%EB%A1%9C-%EB%8B%AB%EC%9D%80-SCM-045#entry65comment</comments>
      <pubDate>Fri, 1 May 2026 16:21:38 +0900</pubDate>
    </item>
    <item>
      <title>OpenAI Realtime API 연결 기준: 브라우저 음성은 WebRTC, 서버 로직은 WebSocket&amp;middot;sideband로 나눠야 한다</title>
      <link>https://ai-imojumo.tistory.com/entry/OpenAI-Realtime-API-%EC%97%B0%EA%B2%B0-%EA%B8%B0%EC%A4%80-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%9D%8C%EC%84%B1%EC%9D%80-WebRTC-%EC%84%9C%EB%B2%84-%EB%A1%9C%EC%A7%81%EC%9D%80-WebSocket%C2%B7sideband%EB%A1%9C-%EB%82%98%EB%88%A0%EC%95%BC-%ED%95%9C%EB%8B%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI Realtime API를 붙일 때 첫 질문은 &quot;WebRTC가 좋나, WebSocket이 좋나&quot;가 아니다. 마이크와 스피커가 어디에 있고, 표준 API key가 어디에 남아야 하며, 주문 조회나 정책 판단 같은 서버 로직을 누가 처리해야 하는지가 먼저다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;2026-04-30&lt;/code&gt; 기준 OpenAI 공식 문서로 보면 기본 경계는 꽤 분명하다. 브라우저나 모바일에서 사용자가 음성으로 대화한다면 WebRTC부터 본다. 서버 worker가 음성 스트림과 이벤트를 직접 처리한다면 WebSocket이 맞다. 전화번호로 들어오는 통화는 SIP path다. 답변 생성 없이 실시간 자막만 필요하면 speech-to-speech session이 아니라 transcription session을 따로 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧게 정리하면 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저/모바일 음성 에이전트는 WebRTC를 기본값으로 잡는다.&lt;/li&gt;
&lt;li&gt;표준 OpenAI API key는 브라우저에 넣지 않는다. client에는 ephemeral key 또는 unified server initialization 경계를 둔다.&lt;/li&gt;
&lt;li&gt;WebSocket은 server-to-server에 맞지만 audio buffer와 JSON event 처리를 직접 책임져야 한다.&lt;/li&gt;
&lt;li&gt;tool 호출, 주문 조회, 내부 정책, guardrail은 client가 아니라 server sideband로 빼는 편이 안전하다.&lt;/li&gt;
&lt;li&gt;실시간 자막만 필요하면 &lt;code&gt;type=transcription&lt;/code&gt; session을 쓴다. 이 mode는 보통 model response를 만들지 않는다.&lt;/li&gt;
&lt;li&gt;전화 상담처럼 phone number가 entry point라면 SIP trunk와 incoming webhook accept/reject flow를 별도로 설계한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Realtime API는 음성 채팅 하나만 뜻하지 않는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI Realtime API overview는 Realtime API를 low-latency multimodal application용으로 설명한다. 음성 입력과 음성 출력만 있는 것이 아니라 audio, image, text input과 audio, text output을 다룰 수 있고, realtime audio transcription에도 쓰인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &quot;Realtime API를 쓴다&quot;는 말만으로는 구현 방식이 정해지지 않는다. 브라우저에서 바로 음성 대화를 만들 수도 있고, 서버에서 WebSocket으로 event를 처리할 수도 있다. 전화망에서 들어오는 call을 SIP로 받을 수도 있고, 모델 답변 없이 transcript만 stream할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이 네 가지를 한 코드 path에 섞을 때 생긴다. 브라우저 데모에서 편하다는 이유로 표준 API key를 넣거나, WebSocket으로 서버를 만들면서 base64 audio buffer 처리를 빼먹거나, transcription-only 요구사항에 speech-to-speech session을 붙이면 나중에 고치기 어렵다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;브라우저 음성은 WebRTC부터 본다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI WebRTC guide는 browser나 mobile client가 Realtime model에 연결할 때 WebSocket보다 WebRTC를 권장한다. 이유는 단순하다. 브라우저의 microphone input, remote audio playback, peer connection, data channel이 모두 WebRTC의 영역에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC path에도 두 가지 초기화 방식이 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;흐름&lt;/th&gt;
&lt;th&gt;조심할 점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ephemeral key&lt;/td&gt;
&lt;td&gt;서버가 &lt;code&gt;/v1/realtime/client_secrets&lt;/code&gt;로 short-lived key를 만들고 browser가 SDP를 Realtime API에 보낸다&lt;/td&gt;
&lt;td&gt;key 발급 endpoint와 session config를 서버에서 관리해야 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unified interface&lt;/td&gt;
&lt;td&gt;browser가 SDP를 app server에 보내고 server가 session config와 함께 &lt;code&gt;/v1/realtime/calls&lt;/code&gt;에 보낸다&lt;/td&gt;
&lt;td&gt;구현은 단순해지지만 session initialization에 서버가 critical path가 된다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 중 무엇을 고르든 표준 API key를 browser에 넣으면 안 된다. Realtime API reference도 client secret은 client-side environment에서 쓰는 ephemeral key이고, standard API token은 server-side only라고 설명한다. 같은 reference는 현재 client secret token expiry를 one minute로 설명한다. 이 숫자는 변할 수 있으니 배포 전 다시 확인해야 하지만, 구조상 &quot;브라우저에 오래 살아 있는 비밀값을 둔다&quot;는 설계는 맞지 않다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서버 worker는 WebSocket이 맞다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket guide는 server-to-server Realtime integration에 WebSocket이 좋은 선택이라고 설명한다. backend system이 Realtime API에 직접 WebSocket으로 연결하고, 표준 API key는 secure backend server에만 남긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 WebSocket은 낮은 수준의 인터페이스다. Realtime conversations guide는 WebRTC가 audio send/receive에 필요한 media handling을 많이 도와주지만, WebSocket audio는 input audio buffer에 base64-encoded audio를 직접 보내야 한다고 설명한다. 즉 서버 worker를 만들려면 event loop, reconnect, audio chunk append, commit, response event 처리까지 운영 코드가 가져가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 나누면 이렇게 된다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;먼저 볼 path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;사용자가 브라우저에서 말하고 바로 듣는다&lt;/td&gt;
&lt;td&gt;WebRTC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;backend가 call audio stream을 받아 모델 event를 직접 처리한다&lt;/td&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;browser media는 직접 연결하되 tool 실행은 서버에 둔다&lt;/td&gt;
&lt;td&gt;WebRTC + sideband&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전화번호로 들어오는 통화를 받는다&lt;/td&gt;
&lt;td&gt;SIP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모델 답변 없이 transcript만 필요하다&lt;/td&gt;
&lt;td&gt;Realtime transcription&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket을 고르면 &quot;서버니까 안전하다&quot;에서 끝나지 않는다. audio buffer를 어떻게 append하고 commit할지, &lt;code&gt;response.create&lt;/code&gt;를 언제 보낼지, &lt;code&gt;response.done&lt;/code&gt;과 usage를 어디에 기록할지 정해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;tool과 내부 정책은 sideband로 분리한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에 WebRTC를 붙였다고 해서 모든 로직을 브라우저에 둬야 하는 것은 아니다. Server controls guide는 client가 WebRTC나 SIP로 Realtime API server에 직접 연결하더라도 tool use와 business logic은 application server에 남기는 것이 좋다고 설명한다. 이를 위해 sideband control channel을 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sideband는 같은 Realtime session에 두 개의 연결이 붙는 구조다. 하나는 사용자의 client connection이고, 다른 하나는 application server connection이다. 서버 connection은 session을 monitor하고, instructions를 업데이트하고, tool call에 응답할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경계는 실제 서비스에서 중요하다. 예를 들어 브라우저 voice agent가 &quot;내 주문 상태 알려줘&quot;라는 요청을 받는다고 하자. 음성 media 자체는 WebRTC가 처리해도 된다. 하지만 주문 DB credential, policy rule, refund 가능 여부 판단은 browser code에 두면 안 된다. client에는 음성 UX를 맡기고, 서버 sideband가 tool call을 받아 검증한 뒤 결과만 session에 돌려주는 구조가 더 낫다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실시간 자막만 필요하면 transcription session이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Realtime transcription guide는 transcription-only use case를 따로 설명한다. 마이크나 file input에서 realtime subtitles나 transcripts를 만들 수 있지만, transcription-only mode에서는 model response를 생성하지 않는다. session type도 &lt;code&gt;transcription&lt;/code&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말은 구현 선택에서 꽤 큰 차이를 만든다. 회의 화면에 실시간 자막만 띄우려는 기능에 speech-to-speech assistant를 붙이면 불필요한 response event와 비용 경계가 생긴다. 반대로 상담 봇처럼 사용자의 말을 듣고 답변까지 해야 하는 경우라면 transcription-only session만으로는 부족하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI docs는 transcription session에서 &lt;code&gt;conversation.item.input_audio_transcription.delta&lt;/code&gt;와 &lt;code&gt;conversation.item.input_audio_transcription.completed&lt;/code&gt; event를 받을 수 있다고 설명한다. &lt;code&gt;gpt-4o-transcribe&lt;/code&gt;와 &lt;code&gt;gpt-4o-mini-transcribe&lt;/code&gt;는 incremental transcript를 stream할 수 있고, &lt;code&gt;whisper-1&lt;/code&gt;은 delta event에도 full turn transcript가 들어간다고 설명한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전화번호가 출발점이면 SIP다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전화 상담처럼 phone number가 entry point라면 WebRTC와 WebSocket만으로는 부족하다. SIP guide는 SIP trunking provider를 통해 phone call을 IP traffic으로 바꾸고, OpenAI SIP endpoint와 incoming call webhook을 연결하는 흐름을 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 inbound call을 webhook으로 받고, &lt;code&gt;call_id&lt;/code&gt;를 기준으로 accept 또는 reject를 결정한다는 점이다. accept할 때 model, voice, instructions 같은 Realtime session config를 넘긴다. 세션이 열린 뒤에는 usual monitoring path를 붙일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SIP는 &quot;브라우저 음성 에이전트를 전화로도 열어두자&quot; 정도의 작은 옵션이 아니다. 번호 구매, carrier, webhook 검증, accept/reject 정책, hangup, monitoring이 같이 들어간다. 이 글의 local router도 incoming phone support scenario는 별도 &lt;code&gt;sip&lt;/code&gt; route로 분리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비용은 연결이 아니라 Response에서 주로 갈린다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Realtime costs guide는 현재 network bandwidth나 connection 자체 비용은 없고, Response가 생성될 때 input/output token 기준으로 비용이 발생한다고 설명한다. 또 Realtime conversation에서는 이전 turn의 item들이 다음 Response의 input으로 들어가므로 뒤 turn이 더 비싸질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 연결 방식만 바꾼다고 비용 문제가 끝나지 않는다. WebRTC든 WebSocket이든 사용자가 계속 대화하고 Response가 계속 만들어지면 usage가 쌓인다. 비용을 보려면 &lt;code&gt;response.done&lt;/code&gt; event의 usage를 저장하고, session이 길어질 때 어떤 context를 유지할지 따로 정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나의 작은 함정은 voice 설정이다. API reference는 model이 audio output을 한 번 낸 뒤에는 그 session에서 voice를 변경할 수 없다고 설명한다. 운영 UI에서 voice를 바꾸게 하려면 첫 audio output 전에 설정을 끝내거나 새 session을 열어야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬 routing gate 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 run에서는 OpenAI API를 실제 호출하지 않았다. 마이크도 열지 않았고, WebRTC peer connection이나 SIP trunk도 만들지 않았다. 대신 공식 문서의 연결 조건을 deterministic routing gate로 바꿔 8개 scenario를 평가했다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;scenario&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;th&gt;route&lt;/th&gt;
&lt;th&gt;recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;browser-voice-agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;webrtc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_webrtc_with_ephemeral_or_unified_server_initialization&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;browser-voice-agent-with-order-tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;&lt;code&gt;webrtc_plus_sideband&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_webrtc_for_media_and_sideband_for_private_tool_control&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;backend-audio-worker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;websocket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_server_to_server_websocket&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;backend-worker-no-audio-buffer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;td&gt;&lt;code&gt;websocket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_server_to_server_websocket&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;live-caption-browser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;realtime_transcription&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_realtime_transcription_session_without_model_response&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public-demo-leaks-standard-key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;&lt;code&gt;webrtc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fix_blockers_before_realtime&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;incoming-phone-support&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_sip_with_webhook_accept_reject_and_optional_sideband&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;next-day-podcast-transcript&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;&lt;code&gt;not_realtime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_non_realtime_audio_transcription_or_batch_pipeline&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 blocker는 &lt;code&gt;public-demo-leaks-standard-key&lt;/code&gt;였다. 브라우저에서 표준 API key를 쓰는 설계는 WebRTC와 WebSocket을 비교하기 전에 막아야 한다. &lt;code&gt;backend-worker-no-audio-buffer&lt;/code&gt;는 WebSocket route로 남았지만, base64 audio buffer 처리가 없어서 warning을 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 &lt;code&gt;browser-voice-agent-with-order-tool&lt;/code&gt;은 WebRTC가 틀린 것이 아니었다. media path는 WebRTC가 맞지만, 주문 조회 tool과 내부 정책은 sideband server control로 분리해야 했다. 같은 Realtime API라도 media plane과 control plane을 나눠야 한다는 뜻이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현 전 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI Realtime API를 붙이기 전에 아래 질문을 먼저 본다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 브라우저나 모바일에서 마이크와 스피커를 직접 쓰는가.&lt;/li&gt;
&lt;li&gt;client에 표준 API key가 들어가지 않는 구조인가.&lt;/li&gt;
&lt;li&gt;ephemeral key 또는 unified interface를 발급하는 server endpoint가 있는가.&lt;/li&gt;
&lt;li&gt;서버가 직접 audio stream과 event를 처리해야 한다면 WebSocket audio buffer 처리가 준비됐는가.&lt;/li&gt;
&lt;li&gt;tool, DB credential, policy rule, guardrail이 client code에 들어가지 않는가.&lt;/li&gt;
&lt;li&gt;WebRTC/SIP direct session에 server control이 필요하면 sideband를 설계했는가.&lt;/li&gt;
&lt;li&gt;답변 생성이 필요한가, transcript만 필요한가.&lt;/li&gt;
&lt;li&gt;transcript-only라면 &lt;code&gt;type=transcription&lt;/code&gt; session과 delta/completed event 처리를 따로 잡았는가.&lt;/li&gt;
&lt;li&gt;전화번호가 entry point라면 SIP trunk, incoming webhook, accept/reject policy가 있는가.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;response.done&lt;/code&gt; usage를 저장하고 session이 길어질 때 비용이 늘어나는 구조를 이해했는가.&lt;/li&gt;
&lt;li&gt;voice를 session 중간에 바꿔야 하는 UI라면 첫 audio output 전 설정 또는 새 session 정책을 정했는가.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 2번이 비어 있으면 구현을 멈추는 편이 낫다. 4번이 비어 있으면 WebSocket은 아직 이르다. 5번과 6번이 비어 있으면 voice demo는 돌아가도 실제 서비스의 tool 실행 경계가 흔들린다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 보면 좋은 글&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI-Computer-Use%EC%99%80-Playwright-CLI-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%9E%90%EB%8F%99%ED%99%94-%EB%B9%84%EA%B5%90-%ED%99%94%EB%A9%B4-%ED%8C%90%EB%8B%A8%EC%9D%80-%EB%AA%A8%EB%8D%B8-%EB%A1%9C%EA%B7%B8%EC%9D%B8%C2%B7%EC%B2%A8%EB%B6%80%C2%B7%EA%B2%80%EC%A6%9D%EC%9D%80-selector-%ED%95%98%EB%84%A4%EC%8A%A4%EA%B0%80-%EB%A7%A1%EB%8A%94%EB%8B%A4&quot;&gt;OpenAI Computer Use와 Playwright CLI 브라우저 자동화 비교: 화면 판단은 모델, 로그인&amp;middot;첨부&amp;middot;검증은 selector 하네스가 맡는다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI-API-%EB%B0%B0%ED%8F%AC-%EC%B2%B4%ED%81%AC%EB%A6%AC%EC%8A%A4%ED%8A%B8-Responses-API%EB%8A%94-%EC%8B%9C%EC%9E%91%EC%A0%90%EC%9D%B4%EA%B3%A0-eval%C2%B7rate-limit%C2%B7background%EB%B6%80%ED%84%B0-%EC%9E%A0%EA%B0%80%EC%95%BC-%ED%95%9C%EB%8B%A4&quot;&gt;OpenAI API 배포 체크리스트: Responses API는 시작점이고 eval&amp;middot;rate limit&amp;middot;background부터 잠가야 한다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI-Batch-API-%EC%82%AC%EC%9A%A9-%EA%B8%B0%EC%A4%80-50-%ED%95%A0%EC%9D%B8%EB%B3%B4%EB%8B%A4-24%EC%8B%9C%EA%B0%84-SLA%C2%B7JSONL%C2%B7%EC%9E%AC%EC%8B%9C%EB%8F%84-%ED%81%90%EA%B0%80-%EB%A8%BC%EC%A0%80%EB%8B%A4&quot;&gt;OpenAI Batch API 사용 기준: 50% 할인보다 24시간 SLA&amp;middot;JSONL&amp;middot;재시도 큐가 먼저다&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/realtime&quot;&gt;OpenAI Realtime API overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/realtime-webrtc&quot;&gt;OpenAI Realtime API with WebRTC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/realtime-websocket&quot;&gt;OpenAI Realtime API with WebSocket&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/realtime-server-controls&quot;&gt;OpenAI Webhooks and server-side controls&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/realtime-transcription&quot;&gt;OpenAI Realtime transcription&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/reference/resources/realtime&quot;&gt;OpenAI Realtime API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/realtime-sip&quot;&gt;OpenAI Realtime API with SIP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/realtime-costs&quot;&gt;OpenAI Realtime managing costs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 로그 첨부&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감정보를 제거한 공개용 로그와 실험 스크립트만 아래에 첨부한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;wn116-realtime-connection-router-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn116-realtime-connection-router-summary-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn116-realtime-connection-router-public.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bXvHUv/dJMcajhJU1m/mIr6tSWLdeyJ82yc5UNkZK/wn116-realtime-connection-router-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn116-realtime-connection-router-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/PIhsh/dJMcaiC9QAd/TcXtsKgeTcJHZNkdIX7wyK/wn116-realtime-connection-router-summary-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn116-realtime-connection-router-summary-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/rTDdr/dJMcajhJU1n/RvX1Pay1SQ55KR8i4c56K1/wn116-realtime-connection-router-public.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn116-realtime-connection-router-public.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Model APIs</category>
      <category>openAI API</category>
      <category>Realtime API</category>
      <category>Voice Agents</category>
      <category>WebRTC</category>
      <category>websocket</category>
      <author>이천재</author>
      <guid isPermaLink="true">https://ai-imojumo.tistory.com/64</guid>
      <comments>https://ai-imojumo.tistory.com/entry/OpenAI-Realtime-API-%EC%97%B0%EA%B2%B0-%EA%B8%B0%EC%A4%80-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%9D%8C%EC%84%B1%EC%9D%80-WebRTC-%EC%84%9C%EB%B2%84-%EB%A1%9C%EC%A7%81%EC%9D%80-WebSocket%C2%B7sideband%EB%A1%9C-%EB%82%98%EB%88%A0%EC%95%BC-%ED%95%9C%EB%8B%A4#entry64comment</comments>
      <pubDate>Thu, 30 Apr 2026 22:47:29 +0900</pubDate>
    </item>
    <item>
      <title>SCM watch-window guard: PO000225 WEO PDF watch를 09시 전에 완료 처리하지 않게 막은 SCM-044</title>
      <link>https://ai-imojumo.tistory.com/entry/SCM-watch-window-guard-PO000225-WEO-PDF-watch%EB%A5%BC-09%EC%8B%9C-%EC%A0%84%EC%97%90-%EC%99%84%EB%A3%8C-%EC%B2%98%EB%A6%AC%ED%95%98%EC%A7%80-%EC%95%8A%EA%B2%8C-%EB%A7%89%EC%9D%80-SCM-044</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PO000225&lt;/code&gt;는 아직 watch 완료로 닫을 주문이 아니다. 날짜는 &lt;code&gt;2026-04-30&lt;/code&gt;이 맞지만, 실행 기준은 &lt;code&gt;09:00 KST&lt;/code&gt;다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 운영자가 먼저 보는 문서 case file&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;문서 엔티티&lt;/th&gt;
&lt;th&gt;연결 주문&lt;/th&gt;
&lt;th&gt;lane&lt;/th&gt;
&lt;th&gt;현재 문서 상태&lt;/th&gt;
&lt;th&gt;원래 due date&lt;/th&gt;
&lt;th&gt;외부 신호&lt;/th&gt;
&lt;th&gt;영향 이유&lt;/th&gt;
&lt;th&gt;아무것도 안 했을 때 결과&lt;/th&gt;
&lt;th&gt;추천 액션&lt;/th&gt;
&lt;th&gt;tradeoff&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DOC-WEO-2026APR-COMPILED-PDF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PO000225&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PO000225 docs branch -&amp;gt; IMF WEO compiled PDF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;closed_by_direct_asset_capture&lt;/code&gt;; April 30 watch date reached but &lt;code&gt;09:00 KST&lt;/code&gt; window is not open yet&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-04-30&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-04-30T01:04:45+09:00&lt;/code&gt; 기준 IMF issue page, direct PDF surface, WEO dataset, press transcript를 공식 웹 경로로 확인했다. direct PDF는 &lt;code&gt;180&lt;/code&gt; page surface지만 이번 확인은 &lt;code&gt;09:00&lt;/code&gt; scheduled verdict가 아니다&lt;/td&gt;
&lt;td&gt;date-only automation은 4월 30일 01시 실행을 09시 watch와 혼동할 수 있다&lt;/td&gt;
&lt;td&gt;guard 없이 완료 처리하면 09:00에 해야 할 SHA-256, byte size, errata date 비교가 누락될 수 있다&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PO000225&lt;/code&gt;를 닫힌 상태로 유지하고 &lt;code&gt;2026-04-30 09:00 KST&lt;/code&gt;에 PDF identity watch를 다시 실행한다&lt;/td&gt;
&lt;td&gt;조기 완료 판정을 막지만, 실제 변경 판정은 09:00까지 보류된다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 화면의 결론은 단순하다. 오늘 실행할 수 있는 일은 완료 판정이 아니라 &lt;code&gt;09:00 KST&lt;/code&gt;로 다시 넘기는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 만든 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 스크립트 &lt;code&gt;scm_sap_watch_window_guard.py&lt;/code&gt;를 추가했다. 이 스크립트는 &lt;code&gt;SCM-042&lt;/code&gt;의 PDF identity watch queue를 읽고 현재 시각이 scheduled window 안에 들어왔는지 먼저 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실행 시각은 &lt;code&gt;2026-04-30T01:04:45+09:00&lt;/code&gt;이었다. scheduled watch는 &lt;code&gt;2026-04-30T09:00:00+09:00&lt;/code&gt;이라서 &lt;code&gt;7.92&lt;/code&gt;시간 남아 있었다. 그래서 결과는 &lt;code&gt;blocked_before_scheduled_watch_window&lt;/code&gt;다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 왜 막았나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서 가장 위험한 실수는 날짜만 보고 watch를 완료 처리하는 것이다. &lt;code&gt;2026-04-30&lt;/code&gt;이라는 날짜는 맞지만, &lt;code&gt;SCM-041&lt;/code&gt;부터 잡아 둔 실행 시각은 &lt;code&gt;09:00 KST&lt;/code&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;01시에 PDF를 봤다고 해서 09시 watch가 끝난 것은 아니다. 01시와 09시 사이에 PDF가 조용히 다시 올라오거나 errata date가 바뀌면, date-only 자동화는 그 차이를 놓친다. 반대로 01시에 네트워크가 실패했는데 그것을 09시 watch 실패로 기록하면 운영자가 불필요하게 수동 hold를 열 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. official surface는 어디까지 확인했나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 IMF 표면은 다시 확인했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IMF WEO issue page: &lt;code&gt;2026-04-14&lt;/code&gt; URL의 April 2026 WEO page와 Full Report surface&lt;/li&gt;
&lt;li&gt;IMF direct PDF: &lt;code&gt;application/pdf&lt;/code&gt;, &lt;code&gt;180&lt;/code&gt; page surface&lt;/li&gt;
&lt;li&gt;IMF Data WEO dataset: April 2026 WEO dataset surface&lt;/li&gt;
&lt;li&gt;IMF WEO press briefing transcript: &lt;code&gt;2026-04-14&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 확인은 scheduled PDF identity verdict가 아니다. &lt;code&gt;SCM-042&lt;/code&gt; baseline SHA-256 &lt;code&gt;5c281721761f2ac8f8ae313ea977a0039539aa760039ea3717cf54002d82432a&lt;/code&gt;와 byte size &lt;code&gt;8298182&lt;/code&gt;는 09시에 다시 비교해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. REBEC 흐름&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;watch_scheduler -&amp;gt; watch_window_guard&lt;/code&gt;: &lt;code&gt;2026-04-30T01:04:45+09:00&lt;/code&gt; 실행 시도&lt;/li&gt;
&lt;li&gt;&lt;code&gt;watch_window_guard -&amp;gt; sap_order_state_coordinator&lt;/code&gt;: &lt;code&gt;PO000225&lt;/code&gt; 닫힌 상태 유지, 외부 SAP write 없음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;watch_window_guard -&amp;gt; operator_handoff_builder&lt;/code&gt;: 09시용 case-file-first handoff 갱신&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operator_handoff_builder -&amp;gt; watch_scheduler&lt;/code&gt;: exact &lt;code&gt;2026-04-30 09:00 KST&lt;/code&gt; PDF identity executor 재큐잉&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름을 넣은 이유는 운영자가 &quot;4월 30일이니까 이미 끝났겠지&quot;가 아니라 &quot;아직 09시 전이라 완료 처리는 금지, 09시에 hash까지 다시 비교&quot;로 읽게 만들기 위해서다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 실패한 것과 남은 위험&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 run은 의도적으로 PDF hash를 새 verdict로 쓰지 않았다. 01시 hash는 scheduled watch 증거가 아니기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tistory 발행도 제한이 있다. 현재 도구 표면에는 Computer Use browser-control 명령이 노출되지 않았다. Playwright MCP로 우회하지 않고, helper session이 유효하지 않으면 publish-ready 상태로 둔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 다음 단계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;2026-04-30 09:00 KST&lt;/code&gt; 이후에는 &lt;code&gt;ALT-RUN-PDF-IDENTITY-WATCH&lt;/code&gt;를 실행한다. direct PDF fetch가 성공하면 URL, content type, PDF magic, page count, byte size, SHA-256, errata date를 모두 비교한다. hash나 byte size가 바뀌면 &lt;code&gt;ALT-BINARY-DELTA-REVIEW&lt;/code&gt;, errata date나 page count가 바뀌면 &lt;code&gt;ALT-REOPEN-ON-ERRATA-DELTA&lt;/code&gt;, fetch가 실패하면 &lt;code&gt;ALT-MANUAL-PDF-EVIDENCE-HOLD&lt;/code&gt;로 멈춘다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 로그 첨부&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감 경로와 내부 식별자는 마스킹한 공개용 아티팩트만 아래에 첨부한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;scm-044-run-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-044-publish-check-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-044-watch-window-guard-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-044-watch-window-handoff-update-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-044-action-simulation-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-044-watch-window-casefile-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-044-watch-window-summary-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-044-imf-watch-window-verification-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-044-watch-window-guard-spec-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scm-sap-watch-window-guard-public.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/biR3LI/dJMcagrOwCc/K57U33gMVOHbdR1nHdYdg0/scm-044-run-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-044-run-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.02MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/duPshx/dJMcagrOwCf/8OYhldPUrQuZiBjyLRKmjk/scm-044-publish-check-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-044-publish-check-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/Qorz4/dJMcaiiRhOI/k5jDd2UCAkRhtnzlaF0Nvk/scm-044-watch-window-guard-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-044-watch-window-guard-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cYYl1u/dJMcafsTVFQ/YcthCkBIZvbxaczEwVAdQ1/scm-044-watch-window-handoff-update-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-044-watch-window-handoff-update-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/mjViQ/dJMcafsTVFS/XBWKpAObKF9WLr36O4mVS0/scm-044-action-simulation-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-044-action-simulation-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/lK9ZW/dJMcaiiRhPg/TjMthX9967fvKCurMtvKi1/scm-044-watch-window-casefile-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-044-watch-window-casefile-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cbmUsK/dJMcafsTVFW/fiMa5oiOwG8vNK1THSzhQ1/scm-044-watch-window-summary-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-044-watch-window-summary-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/wVh4s/dJMcafsTVFY/lKvMiFSYO5SCKmKIlMUaT1/scm-044-imf-watch-window-verification-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-044-imf-watch-window-verification-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/VzetB/dJMcafsTVF2/56CmkIP2hDEUB0WlkUjQa0/scm-044-watch-window-guard-spec-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-044-watch-window-guard-spec-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cjINPr/dJMcafsTVF5/xVZ9jJNPsyvsmmOC5mNsWk/scm-sap-watch-window-guard-public.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;scm-sap-watch-window-guard-public.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.02MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Enterprise AI Systems</category>
      <category>Enterprise AI</category>
      <category>IMF</category>
      <category>SAP</category>
      <category>SCM</category>
      <category>Watch Window</category>
      <author>이천재</author>
      <guid isPermaLink="true">https://ai-imojumo.tistory.com/63</guid>
      <comments>https://ai-imojumo.tistory.com/entry/SCM-watch-window-guard-PO000225-WEO-PDF-watch%EB%A5%BC-09%EC%8B%9C-%EC%A0%84%EC%97%90-%EC%99%84%EB%A3%8C-%EC%B2%98%EB%A6%AC%ED%95%98%EC%A7%80-%EC%95%8A%EA%B2%8C-%EB%A7%89%EC%9D%80-SCM-044#entry63comment</comments>
      <pubDate>Thu, 30 Apr 2026 21:23:32 +0900</pubDate>
    </item>
    <item>
      <title>OpenAI Batch API 사용 기준: 50% 할인보다 24시간 SLA&amp;middot;JSONL&amp;middot;재시도 큐가 먼저다</title>
      <link>https://ai-imojumo.tistory.com/entry/OpenAI-Batch-API-%EC%82%AC%EC%9A%A9-%EA%B8%B0%EC%A4%80-50-%ED%95%A0%EC%9D%B8%EB%B3%B4%EB%8B%A4-24%EC%8B%9C%EA%B0%84-SLA%C2%B7JSONL%C2%B7%EC%9E%AC%EC%8B%9C%EB%8F%84-%ED%81%90%EA%B0%80-%EB%A8%BC%EC%A0%80%EB%8B%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI Batch API는 싸게 호출하는 버튼이 아니다. 대량 요청을 비동기로 맡기고, 결과 파일과 에러 파일을 나중에 회수하는 운영 방식이다. 사용자가 채팅창에서 답을 기다리는 흐름이라면 Batch가 아니라 Standard, 필요하면 Priority 쪽을 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;2026-04-29&lt;/code&gt; 기준 OpenAI 공식 문서로 보면 판단 기준은 분명하다. Batch API는 synchronous API 대비 50% 낮은 비용, 별도 rate-limit pool, 24시간 turnaround를 전제로 한다. 대신 &lt;code&gt;.jsonl&lt;/code&gt; 파일, &lt;code&gt;purpose=batch&lt;/code&gt; 업로드, unique &lt;code&gt;custom_id&lt;/code&gt;, status polling, error file 처리, expired retry queue를 직접 설계해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 결론만 적으면 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대량 eval, 대량 분류, embedding backfill, offline image/video job처럼 당장 사용자 응답이 필요 없는 작업은 Batch 후보가 된다.&lt;/li&gt;
&lt;li&gt;실시간 채팅, streaming UI, 사용자 앞 blocking action은 Batch 후보가 아니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;completion_window&lt;/code&gt;는 현재 &lt;code&gt;24h&lt;/code&gt;만 지원된다. 5분 안에 끝나야 하는 작업이면 다른 path를 잡아야 한다.&lt;/li&gt;
&lt;li&gt;결과 파일 순서는 입력 순서와 다를 수 있다. &lt;code&gt;custom_id&lt;/code&gt; 없이 row index로 맞추는 설계는 위험하다.&lt;/li&gt;
&lt;li&gt;expired request는 error file로 돌아온다. 완료된 요청의 토큰은 과금되므로 실패분만 다시 넣는 retry queue가 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Batch API가 맞는 작업은 대량 비동기 작업이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI Batch guide는 Batch API의 예시로 eval 실행, 대량 dataset classification, content repository embedding, large offline video-render job을 든다. 공통점은 사용자가 지금 화면에서 답을 기다리지 않는다는 점이다. 밤에 돌리고 다음날 리포트에 붙여도 되는 작업, 과거 데이터를 한 번에 다시 분류하는 작업, embedding 저장소를 다시 채우는 작업이 여기에 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 사용자가 버튼을 누른 뒤 바로 결과를 봐야 하는 작업은 Batch와 맞지 않는다. Batch는 &lt;code&gt;24h&lt;/code&gt; completion window를 잡고 상태를 확인한 뒤 결과 파일을 받는 구조다. 고객 상담창의 한 문장 답변, 결제 전 moderation, form submit 직후 검증처럼 즉시성이 필요한 흐름은 동기 API로 남겨야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;할인보다 먼저 보는 것은 24시간 경계다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch 문서의 50% lower costs는 눈에 잘 띈다. 하지만 운영에서는 24시간 경계가 더 먼저다. API reference는 &lt;code&gt;completion_window&lt;/code&gt;가 현재 &lt;code&gt;24h&lt;/code&gt;만 지원된다고 설명한다. 즉 &quot;가능하면 몇 분 안에 끝났으면 좋겠다&quot;가 아니라, &quot;24시간 안에 결과가 와도 업무가 깨지지 않는다&quot;는 조건이 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경계가 맞으면 Batch는 유용하다. 예를 들어 밤 11시에 eval 12,000건을 돌리고 다음날 오전에 regression report를 보는 workflow라면 24시간 경계가 자연스럽다. CRM 과거 메모 42,000건을 분류해 다음날 영업 리포트에 붙이는 것도 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 상담원이 고객과 대화 중이라면 다르다. 50% 비용 절감이 있어도 고객이 기다릴 수 없다. 이런 경우는 Standard 또는 latency가 중요한 high-value traffic이면 Priority processing을 검토해야 한다. Priority docs도 latency가 중요한 user-facing regular traffic에 맞고, data processing이나 eval에는 쓰지 말라고 선을 긋는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSONL 파일 계약을 지켜야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch는 request를 바로 보내는 방식이 아니다. 먼저 &lt;code&gt;.jsonl&lt;/code&gt; 파일을 만들고 Files API에 &lt;code&gt;purpose=batch&lt;/code&gt;로 업로드한 뒤, 그 file id로 batch를 생성한다. 각 줄에는 &lt;code&gt;method&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;, &lt;code&gt;custom_id&lt;/code&gt; 같은 request 단위 정보가 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 자주 놓치는 부분이 세 가지다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;필요한 이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;unique &lt;code&gt;custom_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;결과 line order가 입력 order와 다를 수 있어서 매핑 키가 필요하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;one model per input file&lt;/td&gt;
&lt;td&gt;Batch guide는 input file 하나가 single model requests만 포함해야 한다고 설명한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;supported endpoint 확인&lt;/td&gt;
&lt;td&gt;endpoint가 Batch 지원 목록에 없으면 파일을 잘 만들어도 create 단계에서 막힌다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;2026-04-29&lt;/code&gt; 기준 Batch 지원 endpoint는 &lt;code&gt;/v1/responses&lt;/code&gt;, &lt;code&gt;/v1/chat/completions&lt;/code&gt;, &lt;code&gt;/v1/embeddings&lt;/code&gt;, &lt;code&gt;/v1/completions&lt;/code&gt;, &lt;code&gt;/v1/moderations&lt;/code&gt;, &lt;code&gt;/v1/images/generations&lt;/code&gt;, &lt;code&gt;/v1/images/edits&lt;/code&gt;, &lt;code&gt;/v1/videos&lt;/code&gt;다. 영상 batch는 JSON request를 써야 하고, multipart upload를 그대로 넣는 방식은 맞지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과는 output file과 error file로 나뉜다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch를 만들면 상태가 바로 완료되는 것이 아니다. Batch object는 &lt;code&gt;validating&lt;/code&gt;, &lt;code&gt;in_progress&lt;/code&gt;, &lt;code&gt;finalizing&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, &lt;code&gt;cancelling&lt;/code&gt;, &lt;code&gt;cancelled&lt;/code&gt; 같은 상태를 가진다. 그래서 운영 코드에는 status polling 또는 완료 이벤트 처리 흐름이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완료된 뒤에는 &lt;code&gt;output_file_id&lt;/code&gt;로 결과 &lt;code&gt;.jsonl&lt;/code&gt;을 받는다. 실패한 request는 &lt;code&gt;error_file_id&lt;/code&gt;에서 확인한다. 여기서도 입력 순서에 기대면 안 된다. OpenAI guide는 output line order가 input line order와 다를 수 있으므로 &lt;code&gt;custom_id&lt;/code&gt;로 결과를 매핑하라고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 결과 테이블을 이렇게 잡는 편이 안전하다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;column&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;custom_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;원본 입력 row와 결과 row를 연결한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;batch_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;어떤 batch run에서 나온 결과인지 남긴다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;completed, failed, expired 등을 구분한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;error_code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;재시도할 오류와 폐기할 오류를 나눈다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retry_batch_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실패분만 다시 넣었을 때 lineage를 남긴다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조가 없으면 Batch가 끝난 뒤가 더 복잡해진다. 싸게 돌렸는데 어떤 입력이 어떤 결과인지 사람이 다시 맞추는 상황이 생긴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;숫자 제한은 파일 분할 기준이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch API에는 파일과 queue 경계가 있다. API reference는 input file이 최대 50,000 requests, 200 MB까지 가능하다고 설명한다. &lt;code&gt;/v1/embeddings&lt;/code&gt;는 여기에 더해 batch 전체 embedding inputs도 50,000개로 제한된다. Batch creation도 시간당 2,000 batches 제한이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 대량 작업은 &quot;Batch를 쓸까 말까&quot; 다음에 &quot;몇 개 파일로 나눌까&quot;가 바로 나온다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;판단&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;42,000 classification requests, 155 MB&lt;/td&gt;
&lt;td&gt;single batch 후보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;35,000 embedding requests, embedding inputs 68,000개&lt;/td&gt;
&lt;td&gt;Batch는 맞지만 embedding input 기준으로 split 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60,000 requests&lt;/td&gt;
&lt;td&gt;request count 기준으로 split 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;210 MB JSONL&lt;/td&gt;
&lt;td&gt;file size 기준으로 split 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 나누면 retry도 쉬워진다. 실패한 shard만 다시 넣을 수 있고, 전체 job을 처음부터 다시 태우지 않아도 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;expired를 retry queue로 받아야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch가 24시간 안에 끝나지 않으면 &lt;code&gt;expired&lt;/code&gt; 상태로 갈 수 있다. OpenAI guide는 완료되지 않은 요청은 취소되고, 완료된 요청의 responses는 output file로 제공되며, 완료된 요청에서 소비된 token은 과금된다고 설명한다. expired requests는 error file에 &lt;code&gt;batch_expired&lt;/code&gt;로 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문장은 운영상 중요하다. &quot;실패했으니 전체 batch를 다시 실행&quot;하면 이미 완료된 요청까지 다시 과금될 수 있다. 안전한 방식은 error file을 읽고 실패분만 새 JSONL로 만드는 것이다. 이때도 &lt;code&gt;custom_id&lt;/code&gt;를 새로 만들지, 원본 id를 유지하고 retry suffix를 붙일지 정책을 정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 retry 원칙은 이렇다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;output file에서 성공한 &lt;code&gt;custom_id&lt;/code&gt;를 먼저 닫는다.&lt;/li&gt;
&lt;li&gt;error file에서 retry 가능한 code만 고른다.&lt;/li&gt;
&lt;li&gt;expired request만 별도 retry batch로 묶는다.&lt;/li&gt;
&lt;li&gt;원본 &lt;code&gt;custom_id&lt;/code&gt;, retry attempt, new &lt;code&gt;custom_id&lt;/code&gt;를 같이 남긴다.&lt;/li&gt;
&lt;li&gt;같은 입력이 두 번 반영되지 않게 downstream upsert key를 고정한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch의 비용 장점은 이 retry queue가 있을 때 살아난다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Batch가 아니면 Standard, Priority, Flex를 본다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch와 비교할 선택지는 하나가 아니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;workload&lt;/th&gt;
&lt;th&gt;먼저 볼 path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;사용자가 응답을 기다리는 일반 API 호출&lt;/td&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;latency가 중요한 고가치 user-facing regular traffic&lt;/td&gt;
&lt;td&gt;Priority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;낮은 우선순위의 단발 긴 요청&lt;/td&gt;
&lt;td&gt;Flex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대량 async job&lt;/td&gt;
&lt;td&gt;Batch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flex processing은 Batch와 헷갈리기 쉽다. Flex docs는 Responses 또는 Chat Completions request에서 낮은 비용을 위해 slower response time과 occasional resource unavailability를 감수하는 beta tier라고 설명한다. 요청 한 건짜리 긴 분석처럼 JSONL batch로 묶을 이유가 약하지만 즉시성이 낮은 작업이면 Flex가 더 단순할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch는 파일을 만들고, 업로드하고, 상태를 확인하고, 결과 파일을 해석해야 한다. 요청 한 건이라면 이 구조가 오히려 무겁다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬 routing gate 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 run에서는 OpenAI API를 실제 호출하지 않았다. 대신 공식 문서의 Batch/Flex/Priority 조건을 deterministic workload-routing gate로 바꿔 7개 후보를 평가했다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;workload&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;th&gt;route&lt;/th&gt;
&lt;th&gt;recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nightly-eval-suite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;batch_ready&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_batch_single_file&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;crm-backfill-classification&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;td&gt;&lt;code&gt;batch_ready&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_batch_single_file&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user-chat-answer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;&lt;code&gt;not_batch_ready&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;keep_standard_or_priority_sync_path&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;embedding-backfill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;&lt;code&gt;batch_ready_after_split&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_batch_with_split_files&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;one-off-long-analysis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;flex_candidate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;consider_flex_not_batch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;offline-video-render-queue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;batch_ready&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_batch_single_file&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mixed-model-batch-file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;&lt;code&gt;not_batch_ready&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;block_until_batch_file_contract_is_fixed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과에서 중요한 것은 할인율이 아니다. &lt;code&gt;user-chat-answer&lt;/code&gt;는 비용을 줄일 여지가 있어도 streaming user-facing flow라서 Batch에서 빠졌다. &lt;code&gt;mixed-model-batch-file&lt;/code&gt;은 대량 비동기 작업처럼 보였지만 one model per input file 조건을 깨서 blocked가 됐다. &lt;code&gt;embedding-backfill&lt;/code&gt;은 Batch가 맞지만 embedding inputs 68,000개가 걸려 split이 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 &lt;code&gt;nightly-eval-suite&lt;/code&gt;, &lt;code&gt;crm-backfill-classification&lt;/code&gt;, &lt;code&gt;offline-video-render-queue&lt;/code&gt;는 공통점이 있다. 사용자가 기다리지 않고, supported endpoint 안에 있고, JSONL 크기가 제한 안에 있으며, &lt;code&gt;custom_id&lt;/code&gt;와 error file 처리가 준비돼 있다. 이런 작업이 Batch의 자리다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Batch 투입 전 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI Batch API로 넘기기 전에 아래 질문을 먼저 본다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 지금 응답을 기다리지 않는가.&lt;/li&gt;
&lt;li&gt;24시간 안에 끝나면 업무가 깨지지 않는가.&lt;/li&gt;
&lt;li&gt;endpoint가 Batch 지원 목록에 있는가.&lt;/li&gt;
&lt;li&gt;input file을 &lt;code&gt;purpose=batch&lt;/code&gt;로 올리는 구조인가.&lt;/li&gt;
&lt;li&gt;파일 하나에 single model request만 들어가는가.&lt;/li&gt;
&lt;li&gt;각 line에 unique &lt;code&gt;custom_id&lt;/code&gt;가 있는가.&lt;/li&gt;
&lt;li&gt;결과를 output order가 아니라 &lt;code&gt;custom_id&lt;/code&gt;로 매핑하는가.&lt;/li&gt;
&lt;li&gt;request count 50,000개와 200 MB file limit 안에 있는가.&lt;/li&gt;
&lt;li&gt;embeddings라면 embedding inputs 50,000개 limit도 봤는가.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error_file_id&lt;/code&gt;와 &lt;code&gt;batch_expired&lt;/code&gt;를 읽어 실패분만 retry하는가.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 1번과 2번이 아니면 Batch가 아니다. 6번과 7번이 비어 있으면 Batch를 만들 수는 있어도 결과 운영이 흔들린다. 10번이 없으면 만료와 부분 실패가 비용 문제로 돌아온다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 보면 좋은 글&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI%C2%B7Anthropic%C2%B7LangSmith-%EB%AC%B8%EC%84%9C%EB%A5%BC-%EA%B0%99%EC%9D%B4-%EB%B3%B4%EB%A9%B4-AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-eval%EC%9D%80-%EC%99%9C-%EA%B8%B0%EB%8A%A5-%EB%8D%B0%EB%AA%A8%EB%B3%B4%EB%8B%A4-%EB%A8%BC%EC%A0%80-%EC%9E%88%EC%96%B4%EC%95%BC-%ED%95%98%EB%82%98&quot;&gt;OpenAI&amp;middot;Anthropic&amp;middot;LangSmith 문서를 같이 보면: AI 에이전트 eval은 왜 기능 데모보다 먼저 있어야 하나&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI-GPT-54%C2%B7Anthropic-Claude-Opus-47%C2%B7Google-Gemini-25-prompt-caching-%EB%B9%84%EA%B5%90-%EC%9E%90%EB%8F%99-%ED%95%A0%EC%9D%B8%EB%B3%B4%EB%8B%A4-prefix%C2%B7TTL%C2%B7%EC%9A%94%EA%B8%88-%EC%9C%84%EC%B9%98%EB%A5%BC-%EB%A8%BC%EC%A0%80-%EB%B4%90%EC%95%BC-%ED%95%9C%EB%8B%A4&quot;&gt;OpenAI GPT-5.4&amp;middot;Anthropic Claude Opus 4.7&amp;middot;Google Gemini 3.1 Preview/2.5 prompt caching 비교: 자동 할인보다 prefix&amp;middot;TTL&amp;middot;preview/stable을 먼저 봐야 한다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI-API-%EB%B0%B0%ED%8F%AC-%EC%B2%B4%ED%81%AC%EB%A6%AC%EC%8A%A4%ED%8A%B8-Responses-API%EB%8A%94-%EC%8B%9C%EC%9E%91%EC%A0%90%EC%9D%B4%EA%B3%A0-eval%C2%B7rate-limit%C2%B7background%EB%B6%80%ED%84%B0-%EC%9E%A0%EA%B0%80%EC%95%BC-%ED%95%9C%EB%8B%A4&quot;&gt;OpenAI API 배포 체크리스트: Responses API는 시작점이고 eval&amp;middot;rate limit&amp;middot;background부터 잠가야 한다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai-imojumo.tistory.com/entry/OpenAI-Realtime-API-%EC%97%B0%EA%B2%B0-%EA%B8%B0%EC%A4%80-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%9D%8C%EC%84%B1%EC%9D%80-WebRTC-%EC%84%9C%EB%B2%84-%EB%A1%9C%EC%A7%81%EC%9D%80-WebSocket%C2%B7sideband%EB%A1%9C-%EB%82%98%EB%88%A0%EC%95%BC-%ED%95%9C%EB%8B%A4&quot;&gt;OpenAI Realtime API 연결 기준: 브라우저 음성은 WebRTC, 서버 로직은 WebSocket&amp;middot;sideband로 나눠야 한다&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/batch&quot;&gt;OpenAI Batch API guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/reference/resources/batches/methods/create&quot;&gt;OpenAI Batch create API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/flex-processing&quot;&gt;OpenAI Flex processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/priority-processing&quot;&gt;OpenAI Priority processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/guides/cost-optimization&quot;&gt;OpenAI Cost optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/pricing&quot;&gt;OpenAI Pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 로그 첨부&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감정보를 제거한 공개용 로그와 실험 스크립트만 아래에 첨부한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;wn115-openai-batch-api-router-public.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn115-openai-batch-api-router-summary-public.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wn115-openai-batch-api-router-public.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/duxYoj/dJMcaarCCEC/6UPB7vZlCnYv8cxJKDQTU1/wn115-openai-batch-api-router-public.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn115-openai-batch-api-router-public.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bZkM7x/dJMcaiwnB5c/y13arn3PKk14FnAoksCyi0/wn115-openai-batch-api-router-summary-public.md?attach=1&amp;amp;knm=tfile.md&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn115-openai-batch-api-router-summary-public.md&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.00MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cotbQ0/dJMcaiwnB5p/ZeRq5V5nlBQb9czw6qgTEK/wn115-openai-batch-api-router-public.py?attach=1&amp;amp;knm=tfile.py&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;wn115-openai-batch-api-router-public.py&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Model APIs</category>
      <category>api cost</category>
      <category>Batch API</category>
      <category>Flex Processing</category>
      <category>Model APIs</category>
      <category>openAI API</category>
      <author>이천재</author>
      <guid isPermaLink="true">https://ai-imojumo.tistory.com/62</guid>
      <comments>https://ai-imojumo.tistory.com/entry/OpenAI-Batch-API-%EC%82%AC%EC%9A%A9-%EA%B8%B0%EC%A4%80-50-%ED%95%A0%EC%9D%B8%EB%B3%B4%EB%8B%A4-24%EC%8B%9C%EA%B0%84-SLA%C2%B7JSONL%C2%B7%EC%9E%AC%EC%8B%9C%EB%8F%84-%ED%81%90%EA%B0%80-%EB%A8%BC%EC%A0%80%EB%8B%A4#entry62comment</comments>
      <pubDate>Wed, 29 Apr 2026 21:52:11 +0900</pubDate>
    </item>
  </channel>
</rss>