스트리밍 API

·3분 읽기

원문: Koog Documentation — streaming-api 이 글은 Koog 공식 문서의 streaming-api 페이지를 한국어로 옮긴 번역본입니다. 문서 구조와 링크 의미를 유지하되, MkDocs 전용 UI 문법은 블로그에서 읽기 좋도록 정리했습니다.

스트리밍 API

Koog의 스트리밍 API를 사용하면 Kotlin에서는 Flow<StreamFrame>, Java에서는 Flow.Publisher<StreamFrame>LLM 출력을 점진적으로 사용할 수 있습니다. 전체 응답을 기다리는 대신 코드는 다음을 수행할 수 있습니다.

  • 보조 텍스트가 도착하면 렌더링하고,
  • 도구 호출을 실시간으로 감지하고 그에 따라 조치를 취합니다.
  • 스트림이 언제 종료되고 그 이유를 알 수 있습니다.

스트림은 두 가지 범주로 구성된 유형화된 프레임을 전달합니다.

코틀린

델타 프레임(증분/부분 콘텐츠):

  • StreamFrame.TextDelta(text: String, index: Int?) — 증분 보조 텍스트
  • StreamFrame.ReasoningDelta(text: String?, summary: String?, index: Int?) — 증분 추론 텍스트 및 요약
  • StreamFrame.ToolCallDelta(id: String?, name: String?, content: String?, index: Int?) — 부분 도구 호출

전체 프레임(전체 콘텐츠):

  • StreamFrame.TextComplete(text: String, index: Int?) — 완전한 보조 텍스트
  • StreamFrame.ReasoningComplete(text: List<String>, summary: List<String>?, encrypted: String?, index: Int?) — 선택적 요약 및 암호화된 콘텐츠를 통한 완전한 추론
  • StreamFrame.ToolCallComplete(id: String?, name: String, content: String, index: Int?) — 완전한 도구 호출

마지막 마커:

  • StreamFrame.End(finishReason: String?, metaInfo: ResponseMetaInfo) — 응답 메타데이터가 있는 스트림 끝 마커

자바

델타 프레임(증분/부분 콘텐츠):

  • StreamFrame.TextDelta — 증분 보조 텍스트. 필드: getText(), getIndex().
  • StreamFrame.ReasoningDelta — 증분 추론 텍스트 및 요약. 필드: getText(), getSummary(), getIndex().
  • StreamFrame.ToolCallDelta — 부분 도구 호출. 필드: getId(), getName(), getContent(), getIndex().

전체 프레임(전체 콘텐츠):

  • StreamFrame.TextComplete — 완전한 보조 텍스트. 필드: getText(), getIndex().
  • StreamFrame.ReasoningComplete — 선택적 요약 및 암호화된 콘텐츠를 통한 완전한 추론. 필드: getText()(List<String> 반환), getSummary()(List<String> 반환), getEncrypted(), getIndex().
  • StreamFrame.ToolCallComplete — 도구 호출을 완료합니다. 필드: getId(), getName(), getContent(), getIndex(). 또한 JSON 구문 분석을 위해 getContentJson()getContentJsonResult()를 제공합니다.

마지막 마커:

  • StreamFrame.End — 스트림 끝 마커. 필드: getFinishReason(), getMetaInfo().

일반 텍스트를 추출하고, 프레임을 Message.Response 객체로 변환하고, 안전하게 청크된 도구 호출을 결합하기 위한 도우미가 제공됩니다.

API 개요

스트리밍을 사용하면 다음을 수행할 수 있습니다.

  • 데이터가 도착하는 대로 처리(UI 응답성 향상)
  • 구조화된 정보를 즉시 구문 분석합니다(Markdown/JSON 등).
  • 완료되면 객체 방출
  • 실시간으로 도구 트리거
  • 실시간으로 모델 추론에 액세스(지원되는 모델의 경우)

프레임 자체 또는 프레임에서 파생된 일반 텍스트에서 작업할 수 있습니다.

델타 대 전체 프레임

스트리밍 API는 두 가지 유형의 프레임을 구별합니다.

  • 델타 프레임 (DeltaFrame) — 청크로 도착하는 증분/부분 콘텐츠입니다. 이는 콘텐츠 스트리밍 시 실시간 표시에 이상적입니다. 예: TextDelta, ReasoningDelta, ToolCallDelta.

  • 전체 프레임(CompleteFrame) — 해당 콘텐츠 유형에 대한 모든 델타가 수신된 후 전체 콘텐츠가 방출됩니다. 이는 최종 처리 및 Message.Response 개체로의 변환에 유용합니다. 예: TextComplete, ReasoningComplete, ToolCallComplete.

일반적으로 UI 업데이트에는 델타 프레임을 사용하고 구조화된 최종 데이터를 추출하려면 전체 프레임을 사용합니다.


용법

프레임을 직접 사용하여 작업

이것이 가장 일반적인 접근 방식입니다. 즉, 각 프레임 종류에 반응합니다.

코틀린

1llm.writeSession {2    appendPrompt { user("Tell me a joke, then call a tool with JSON args.") }34    val stream = requestLLMStreaming() // Flow<StreamFrame>56    stream.collect { frame ->7        when (frame) {8            is StreamFrame.TextDelta -> print(frame.text)9            is StreamFrame.ReasoningDelta -> print("[Reasoning] text=${frame.text} summary=${frame.summary}")10            is StreamFrame.ToolCallComplete -> {11                println("\n🔧 Tool call: ${frame.name} args=${frame.content}")12                // Optionally parse lazily:13                // val json = frame.contentJson14            }15            is StreamFrame.End -> println("\n[END] reason=${frame.finishReason}")16            else -> {} // Handle other frame types (TextComplete, ToolCallDelta, etc.)17        }18    }19}

자바

1ctx.getLlm().writeSession(session -> {2    session.appendPrompt(prompt -> {3        prompt.user("Tell me a joke, then call a tool with JSON args.");4        return null;5    });67    Flow.Publisher<StreamFrame> stream = session.requestLLMStreaming();89    stream.subscribe(new Flow.Subscriber<>() {10        @Override11        public void onSubscribe(Flow.Subscription subscription) {12            subscription.request(Long.MAX_VALUE);13        }1415        @Override16        public void onNext(StreamFrame frame) {17            if (frame instanceof StreamFrame.TextDelta delta) {18                System.out.print(delta.getText());19            } else if (frame instanceof StreamFrame.ReasoningDelta reasoning) {20                System.out.print("[Reasoning] text=" + reasoning.getText()21                    + " summary=" + reasoning.getSummary());22            } else if (frame instanceof StreamFrame.ToolCallComplete toolCall) {23                System.out.println("\nTool call: " + toolCall.getName()24                    + " args=" + toolCall.getContent());25            } else if (frame instanceof StreamFrame.End end) {26                System.out.println("\n[END] reason=" + end.getFinishReason());27            }28            // Handle other frame types (TextComplete, ToolCallDelta, etc.)29        }3031        @Override32        public void onError(Throwable throwable) {33            System.err.println("Stream error: " + throwable.getMessage());34        }3536        @Override37        public void onComplete() {38        }39    });4041    return null;42});

원시 문자열 스트림으로 직접 작업하여 출력을 구문 분석할 수 있다는 점에 유의하는 것이 중요합니다. 이 접근 방식을 사용하면 구문 분석 프로세스에 대한 더 많은 유연성과 제어력을 얻을 수 있습니다.

다음은 출력 구조의 Markdown 정의가 포함된 원시 문자열 스트림입니다.

코틀린

1fun markdownBookDefinition(): MarkdownStructureDefinition {2    return MarkdownStructureDefinition("name", schema = { /*...*/ })3}45val mdDefinition = markdownBookDefinition()67llm.writeSession {8    val stream = requestLLMStreaming(mdDefinition)9    // Access the raw string chunks directly10    stream.collect { chunk ->11        // Process each chunk of text as it arrives12        println("Received chunk: $chunk") // The chunks together will be structured as a text following the mdDefinition schema13    }14}

자바

1StructureDefinition mdDefinition = markdownBookDefinition();23ctx.getLlm().writeSession(session -> {4    session.appendPrompt(prompt -> {5        prompt.user(input);6    });78    Flow.Publisher<StreamFrame> stream = session.requestLLMStreaming(mdDefinition);910    // Access the raw frames directly11    stream.subscribe(new Flow.Subscriber<>() {12        @Override13        public void onSubscribe(Flow.Subscription subscription) {14            subscription.request(Long.MAX_VALUE);15        }1617        @Override18        public void onNext(StreamFrame frame) {19            // Process each frame as it arrives20            System.out.println("Received frame: " + frame);21        }2223        @Override24        public void onError(Throwable throwable) {25            System.err.println("Stream error: " + throwable.getMessage());26        }2728        @Override29        public void onComplete() {30        }31    });3233    return null;34});

추론 프레임 작업

추론을 지원하는 모델(예: Claude Sonnet 4.5 또는 GPT-o1)은 스트리밍 중에 추론 프레임을 내보냅니다. 추론 과정과 요약에 모두 액세스할 수 있습니다.

코틀린

1llm.writeSession {2    appendPrompt { user("Solve this complex problem: ...") }34    val stream = requestLLMStreaming()5    val reasoningSteps = mutableListOf<String>()6    val summarySteps = mutableListOf<String>()78    stream.collect { frame ->9        when (frame) {10            is StreamFrame.ReasoningDelta -> {11                frame.text?.let { 12                    reasoningSteps.add(it)13                    print(frame.text) // Display reasoning as it arrives14                }15                frame.summary?.let {16                    summarySteps.add(it)17                    print(frame.summary) // Display reasoning summary as it arrives18                }19            }20            is StreamFrame.ReasoningComplete -> {21                // Access complete reasoning22                println("\nComplete reasoning: ${frame.text.joinToString("")}")23                println("Summary: ${frame.summary?.joinToString("") ?: "N/A"}")24            }25            is StreamFrame.TextDelta -> print(frame.text)26            is StreamFrame.End -> println("\n[END]")27            else -> {}28        }29    }30}

자바

1ctx.getLlm().writeSession(session -> {2    session.appendPrompt(prompt -> {3        prompt.user("Solve this complex problem: ...");4        return null;5    });67    Flow.Publisher<StreamFrame> stream = session.requestLLMStreaming();8    List<String> reasoningSteps = new ArrayList<>();9    List<String> summarySteps = new ArrayList<>();1011    stream.subscribe(new Flow.Subscriber<StreamFrame>() {12        @Override13        public void onSubscribe(Flow.Subscription subscription) {14            subscription.request(Long.MAX_VALUE);15        }1617        @Override18        public void onNext(StreamFrame frame) {19            if (frame instanceof StreamFrame.ReasoningDelta reasoning) {20                if (reasoning.getText() != null) {21                    reasoningSteps.add(reasoning.getText());22                    System.out.print(reasoning.getText());23                }24                if (reasoning.getSummary() != null) {25                    summarySteps.add(reasoning.getSummary());26                    System.out.print(reasoning.getSummary());27                }28            } else if (frame instanceof StreamFrame.ReasoningComplete complete) {29                // Access complete reasoning30                System.out.println("\nComplete reasoning: "31                    + String.join("", complete.getText()));32                System.out.println("Summary: "33                    + (complete.getSummary() != null34                        ? String.join("", complete.getSummary()) : "N/A"));35            } else if (frame instanceof StreamFrame.TextDelta delta) {36                System.out.print(delta.getText());37            } else if (frame instanceof StreamFrame.End) {38                System.out.println("\n[END]");39            }40        }4142        @Override43        public void onError(Throwable throwable) { }4445        @Override46        public void onComplete() { }47    });4849    return null;50});

원시 텍스트 스트림 작업(파생)

Flow<String>을 예상하는 기존 스트리밍 파서가 있는 경우, filterTextOnly()을 통해 텍스트 청크를 파생하거나 collectText()로 수집합니다.

코틀린

1llm.writeSession {2    val frames = requestLLMStreaming()34    // Stream text chunks as they come:5    frames.filterTextOnly().collect { chunk -> print(chunk) }67    // Or, gather all text into one String after End:8    val fullText = frames.collectText()9    println("\n---\n$fullText")10}

자바

1ctx.getLlm().writeSession(session -> {2    Flow.Publisher<StreamFrame> frames = session.requestLLMStreaming();34    // Stream text chunks as they come (equivalent of filterTextOnly):5    StringBuilder fullText = new StringBuilder();6    frames.subscribe(new Flow.Subscriber<>() {7        @Override8        public void onSubscribe(Flow.Subscription subscription) {9            subscription.request(Long.MAX_VALUE);10        }1112        @Override13        public void onNext(StreamFrame frame) {14            if (frame instanceof StreamFrame.TextDelta delta) {15                System.out.print(delta.getText());16                fullText.append(delta.getText());17            }18        }1920        @Override21        public void onError(Throwable throwable) { }2223        @Override24        public void onComplete() {25            // fullText now contains all text (equivalent of collectText)26            System.out.println("\n---\n" + fullText);27        }28    });2930    return null;31});

이벤트 핸들러에서 스트림 이벤트 수신

agent event handlers에서 스트림 이벤트를 들을 수 있습니다.

코틀린

1handleEvents {2    onToolCallStarting { context ->3        println("\n🔧 Using ${context.toolName} with ${context.toolArgs}... ")4    }56    onLLMStreamingFrameReceived { context ->7        when (val frame = context.streamFrame) {8            is StreamFrame.TextDelta -> print(frame.text)9            is StreamFrame.ReasoningDelta -> print("[Reasoning] text=${frame.text} summary=${frame.summary}")10            else -> {} // Handle other frame types if needed11        }12    }1314    onLLMStreamingFailed { context ->15        println("❌ Error: ${context.error}")16    }1718    onLLMStreamingCompleted {19        println("🏁 Done")20    }21}

자바

1.install(EventHandler.Feature, config -> {2    config.onToolCallStarting(ctx -> {3        System.out.println("\nUsing " + ctx.getToolName() + " with " + ctx.getToolArgs() + "... ");4    });56    config.onLLMStreamingFrameReceived(ctx -> {7        StreamFrame frame = ctx.getStreamFrame();8        if (frame instanceof StreamFrame.TextDelta delta) {9            System.out.print(delta.getText());10        } else if (frame instanceof StreamFrame.ReasoningDelta reasoning) {11            System.out.print("[Reasoning] text=" + reasoning.getText()12                + " summary=" + reasoning.getSummary());13        }14    });1516    config.onLLMStreamingFailed(ctx -> {17        System.out.println("Error: " + ctx.getError());18    });1920    config.onLLMStreamingCompleted(ctx -> {21        System.out.println("Done");22    });23})

프레임을 Message.Response로 변환

수집된 프레임 목록을 표준 메시지 객체로 변환할 수 있습니다.

  • toAssistantMessageOrNull() — 텍스트 프레임에서 Message.Assistant을 추출합니다.
  • toReasoningMessageOrNull() — 추론 프레임에서 Message.Reasoning을 추출합니다.
  • toToolCallMessages() — 도구 호출 프레임에서 Message.Tool.Call을 추출합니다.
  • toMessageResponses() — 모든 완전한 프레임을 해당 Message.Response 개체로 변환합니다.

스트리밍 중 구조화된 데이터(마크다운 예)

원시 문자열 스트림으로 작업하는 것이 가능하지만 structured data을 사용하여 작업하는 것이 더 편리한 경우가 많습니다.

구조화된 데이터 접근 방식에는 다음과 같은 주요 구성요소가 포함됩니다.

  1. MarkdownStructureDefinition: 구조화된 데이터에 대한 스키마와 예를 정의하는 데 도움이 되는 클래스입니다. 마크다운 형식.
  2. markdownStreamingParser: 마크다운 청크 스트림을 처리하고 내보내는 파서를 생성하는 함수 이벤트.

아래 섹션에서는 구조화된 데이터 스트림 처리와 관련된 단계별 지침과 코드 샘플을 제공합니다.

1. 데이터 구조 정의

먼저 구조화된 데이터를 나타내는 데이터 클래스를 정의합니다.

코틀린

1@Serializable2data class Book(3    val title: String,4    val author: String,5    val description: String6)

자바

1// TODO not yet supported in Java

2. 마크다운 구조 정의

다음을 사용하여 Markdown에서 데이터를 구성하는 방법을 지정하는 정의를 만듭니다. MarkdownStructureDefinition 클래스:

코틀린

1fun markdownBookDefinition(): MarkdownStructureDefinition {2    return MarkdownStructureDefinition("bookList", schema = {3        markdown {4            header(1, "title")5            bulleted {6                item("author")7                item("description")8            }9        }10    }, examples = {11        markdown {12            header(1, "The Great Gatsby")13            bulleted {14                item("F. Scott Fitzgerald")15                item("A novel set in the Jazz Age that tells the story of Jay Gatsby's unrequited love for Daisy Buchanan.")16            }17        }18    })19}

자바

1// TODO not yet supported in Java

3. 데이터 구조에 대한 파서를 만듭니다.

markdownStreamingParser은 다양한 Markdown 요소에 대한 여러 처리기를 제공합니다.

코틀린

1markdownStreamingParser {2    // Handle level 1 headings (level ranges from 1 to 6)3    onHeader(1) { headerText -> }4    // Handle bullet points5    onBullet { bulletText -> }6    // Handle code blocks7    onCodeBlock { codeBlockContent -> }8    // Handle lines matching a regex pattern9    onLineMatching(Regex("pattern")) { line -> }10    // Handle the end of the stream11    onFinishStream { remainingText -> }12}

자바

1// TODO not yet supported in Java

정의된 핸들러를 사용하면 Markdown 스트림을 구문 분석하고 데이터 객체를 내보내는 함수를 구현할 수 있습니다. markdownStreamingParser 함수를 사용합니다.

코틀린

1fun parseMarkdownStreamToBooks(markdownStream: Flow<StreamFrame>): Flow<Book> {2   return flow {3      markdownStreamingParser {4         var currentBookTitle = ""5         val bulletPoints = mutableListOf<String>()67         // Handle the event of receiving the Markdown header in the response stream8         onHeader(1) { headerText ->9            // If there was a previous book, emit it10            if (currentBookTitle.isNotEmpty() && bulletPoints.isNotEmpty()) {11               val author = bulletPoints.getOrNull(0) ?: ""12               val description = bulletPoints.getOrNull(1) ?: ""13               emit(Book(currentBookTitle, author, description))14            }1516            currentBookTitle = headerText17            bulletPoints.clear()18         }1920         // Handle the event of receiving the Markdown bullets list in the response stream21         onBullet { bulletText ->22            bulletPoints.add(bulletText)23         }2425         // Handle the end of the response stream26         onFinishStream {27            // Emit the last book, if present28            if (currentBookTitle.isNotEmpty() && bulletPoints.isNotEmpty()) {29               val author = bulletPoints.getOrNull(0) ?: ""30               val description = bulletPoints.getOrNull(1) ?: ""31               emit(Book(currentBookTitle, author, description))32            }33         }34      }.parseStream(markdownStream.filterTextOnly())35   }36}

자바

1// TODO not yet supported in Java

4. 에이전트 전략에 파서를 사용하세요

코틀린

1val agentStrategy = strategy<String, List<Book>>("library-assistant") {2   // Describe the node containing the output stream parsing3   val getMdOutput by node<String, List<Book>> { booksDescription ->4      val books = mutableListOf<Book>()5      val mdDefinition = markdownBookDefinition()67      llm.writeSession {8         appendPrompt { user(booksDescription) }9         // Initiate the response stream in the form of the definition `mdDefinition`10         val markdownStream = requestLLMStreaming(mdDefinition)11         // Call the parser with the result of the response stream and perform actions with the result12         parseMarkdownStreamToBooks(markdownStream).collect { book ->13            books.add(book)14            println("Parsed Book: ${book.title} by ${book.author}")15         }16      }1718      books19   }20   // Describe the agent's graph making sure the node is accessible21   edge(nodeStart forwardTo getMdOutput)22   edge(getMdOutput forwardTo nodeFinish)23}

자바

1// TODO not yet supported in Java

고급 사용법: 도구를 사용한 스트리밍

또한 스트리밍 API를 도구와 함께 사용하여 데이터가 도착할 때 이를 처리할 수도 있습니다. 다음 섹션에서는 도구를 정의하고 이를 스트리밍 데이터와 함께 사용하는 방법에 대한 간략한 단계별 가이드를 제공합니다.

1. 데이터 구조에 대한 도구 정의

코틀린

1@Serializable2data class Book(3   val title: String,4   val author: String,5   val description: String6)78class BookTool(): SimpleTool<Book>(9    argsType = typeToken<Book>(),10    name = NAME,11    description = "A tool to parse book information from Markdown"12) {1314    companion object { const val NAME = "book" }1516    override suspend fun execute(args: Book): String {17        println("${args.title} by ${args.author}:\n ${args.description}")18        return "Done"19    }20}

자바

1class BookTool implements ToolSet {2    @Tool3    @LLMDescription("A tool to parse book information from Markdown")4    public String book(5        @LLMDescription("Title of the book") String title,6        @LLMDescription("Author of the book") String author,7        @LLMDescription("Description of the book") String description8    ) {9        System.out.println(title + " by " + author + ":\n " + description);10        return "Done";11    }12}

2. 스트리밍 데이터와 함께 도구 사용

코틀린

1val agentStrategy = strategy<String, Unit>("library-assistant") {2   val getMdOutput by node<String, Unit> { input ->3      val mdDefinition = markdownBookDefinition()45      llm.writeSession {6         appendPrompt { user(input) }7         val markdownStream = requestLLMStreaming(mdDefinition)89         parseMarkdownStreamToBooks(markdownStream).collect { book ->10            callToolRaw(BookTool.NAME, book)11            /* Other possible options:12                callTool(BookTool::class, book)13                callTool<BookTool>(book)14                findTool(BookTool::class).execute(book)15            */16         }1718         // We can make parallel tool calls19         parseMarkdownStreamToBooks(markdownStream).toParallelToolCallsRaw(toolClass=BookTool::class).collect {20            println("Tool call result: $it")21         }22      }23   }2425   edge(nodeStart forwardTo getMdOutput)26   edge(getMdOutput forwardTo nodeFinish)27 }

자바

1var strategy = AIAgentGraphStrategy.builder("library-assistant")2    .withInput(String.class)3    .withOutput(Void.class);45var getMdOutput = AIAgentNode.builder("getMdOutput")6    .withInput(String.class)7    .withOutput(Void.class)8    .withAction((input, ctx) -> {9        StructureDefinition mdDefinition = markdownBookDefinition();1011        ctx.getLlm().writeSession(session -> {12            session.appendPrompt(prompt -> {13                prompt.user(input);14                return null;15            });1617            Flow.Publisher<StreamFrame> markdownStream = session.requestLLMStreaming(mdDefinition);1819            // Process streamed frames and invoke tools on ToolCallComplete frames20            markdownStream.subscribe(new Flow.Subscriber<StreamFrame>() {21                @Override22                public void onSubscribe(Flow.Subscription subscription) {23                    subscription.request(Long.MAX_VALUE);24                }2526                @Override27                public void onNext(StreamFrame frame) {28                    if (frame instanceof StreamFrame.ToolCallComplete toolCall) {29                        System.out.println("Tool call: " + toolCall.getName()30                            + " args=" + toolCall.getContent());31                    }32                }3334                @Override35                public void onError(Throwable throwable) { }3637                @Override38                public void onComplete() { }39            });4041            return null;42        });4344        return null;45    })46    .build();4748strategy.edge(strategy.nodeStart, getMdOutput);49strategy.edge(getMdOutput, strategy.nodeFinish);

3. 에이전트 구성에 도구를 등록하세요.

코틀린

1val toolRegistry = ToolRegistry {2    tool(BookTool())3}45val runner = AIAgent(6    promptExecutor = simpleOpenAIExecutor("OPENAI_API_KEY"),7    llmModel = OpenAIModels.Chat.GPT4o,8    toolRegistry = toolRegistry9)

자바

1ToolRegistry toolRegistry = ToolRegistry.builder()2    .tools(new BookTool())3    .build();45AIAgent<String, String> runner = AIAgent.<String, String>builder()6    .promptExecutor(PromptExecutor.builder().openAI("OPENAI_API_KEY").build())7    .llmModel(OpenAIModels.Chat.GPT4o)8    .toolRegistry(toolRegistry)9    .build();

모범 사례

  1. 명확한 구조 정의: 데이터에 대한 명확하고 명확한 마크다운 구조를 만듭니다.

  2. 좋은 사례 제공: LLM을 안내하기 위해 MarkdownStructureDefinition에 포괄적인 사례를 포함합니다.

  3. 불완전한 데이터 처리: 스트림에서 데이터를 구문 분석할 때 항상 null 또는 빈 값을 확인하세요.

  4. 리소스 정리: onFinishStream 핸들러를 사용하여 리소스를 정리하고 나머지 데이터를 처리합니다.

  5. 오류 처리: 잘못된 마크다운이나 예상치 못한 데이터에 대한 적절한 오류 처리를 구현합니다.

  6. 테스트: 부분 청크 및 잘못된 입력을 포함한 다양한 입력 시나리오로 파서를 테스트합니다.

  7. 병렬 처리: 독립적인 데이터 항목의 경우 더 나은 성능을 위해 병렬 도구 호출을 사용하는 것이 좋습니다.