DeepSeek Harness: Protocolo StreamChunk e Tratamento de Erros

Última atualização: 2026-08-31

Saída em streaming é a experiência central de interação com o Agent — usuários não querem esperar 30 segundos por uma resposta completa, querem ver o Agent pensar palavra por palavra. O protocolo StreamChunk é a abstração unificada do DSH para saída em streaming, e tratamento de erros garante degradação graciosa quando exceções ocorrem.

💡 Dica: A parte difícil de saída em streaming não é "enviar" — é "o que fazer quando erros acontecem." O protocolo StreamChunk trata erros como um tipo de chunk, deixando consumers tratar casos normais e anormais uniformemente.

📋 Pré-requisitos: Ter completado 24-llm-adapter.md, entender adaptadores LLM

1. O Que Você Vai Aprender

Sequência StreamChunk


2. Detalhes do Protocolo StreamChunk

(1) Definição Completa

TYPESCRIPT
type StreamChunk =
  | TextChunk
  | ToolCallChunk
  | ToolResultChunk
  | ErrorChunk
  | DoneChunk

interface TextChunk {
  type: 'text'
  content: string
}

interface ToolCallChunk {
  type: 'tool_call'
  id: string
  name: string
  arguments: string
  index: number
}

interface ToolResultChunk {
  type: 'tool_result'
  id: string
  toolCallId: string
  result: any
  isError: boolean
}

interface ErrorChunk {
  type: 'error'
  error: Error
  recoverable: boolean
  retryAfter?: number
}

interface DoneChunk {
  type: 'done'
  reason: 'stop' | 'tool_use' | 'length' | 'cancel' | 'error'
  usage?: TokenUsage
}

interface TokenUsage {
  promptTokens: number
  completionTokens: number
  totalTokens: number
}

(2) Fluxo Típico

100%
graph LR
    START[Stream inicia] --> TEXT[TextChunk × N]
    TEXT --> TC[ToolCallChunk]
    TC --> TR[ToolResultChunk]
    TR --> TEXT2[TextChunk × N]
    TEXT2 --> DONE[DoneChunk]

Um fluxo típico de conversa de Agent:

  1. LLM emite texto → TextChunk
  2. LLM decide chamar uma ferramenta → ToolCallChunk
  3. Execução da ferramenta completa → ToolResultChunk
  4. LLM continua saída → TextChunk
  5. Stream termina → DoneChunk

(3) Exemplo de Consumo Completo

▶ Exemplo 1: Consumo Completo de Stream com Switch

TYPESCRIPT
const stream = ctx.llm.stream({
  messages: [{ role: 'user', content: 'list project files' }],
  tools: availableTools
})

for await (const chunk of stream) {
  switch (chunk.type) {
    case 'text':
      process.stdout.write(chunk.content)
      break

    case 'tool_call':
      console.log(`\n🔧 Chamada de ferramenta: ${chunk.name}`)
      console.log(`   Argumentos: ${chunk.arguments}`)
      break

    case 'tool_result':
      if (chunk.isError) {
        console.log(`   ❌ Erro da ferramenta: ${chunk.result}`)
      } else {
        console.log(`   ✅ Resultado: ${JSON.stringify(chunk.result)}`)
      }
      break

    case 'error':
      console.error(`\n⚠️ Erro: ${chunk.error.message}`)
      if (chunk.recoverable) {
        console.log(`   Tentará novamente em ${chunk.retryAfter}ms`)
      }
      break

    case 'done':
      console.log(`\n✅ Concluído (${chunk.reason})`)
      if (chunk.usage) {
        console.log(`   Uso de tokens: ${chunk.usage.totalTokens}`)
      }
      break
  }
}

3. Tipos de Chunk em Detalhe

(1) TextChunk

Fragmentos de conteúdo de texto, emitidos incrementalmente:

TYPESCRIPT
// LLM emite "Hello, world!"
// Pode produzir múltiplos TextChunks:
// chunk 1: { type: 'text', content: 'Hello' }
// chunk 2: { type: 'text', content: ', ' }
// chunk 3: { type: 'text', content: 'world' }
// chunk 4: { type: 'text', content: '!' }

Consumers devem concatenar todos os TextChunks em vez de exibi-los individualmente.

(2) ToolCallChunk

LLM requisita uma chamada de ferramenta:

TYPESCRIPT
{
  type: 'tool_call',
  id: 'call_abc123',
  name: 'file_edit',
  arguments: '{"action":"read","path":"src/index.ts"}',
  index: 0
}

Nota: arguments é uma string JSON que precisa parsing.

(3) ToolResultChunk

Resultados após execução da ferramenta:

TYPESCRIPT
{
  type: 'tool_result',
  id: 'result_xyz789',
  toolCallId: 'call_abc123',
  result: { content: 'export const name = ...' },
  isError: false
}

isError: true indica falha na execução da ferramenta; result contém informação de erro.

(4) Múltiplas Chamadas de Ferramenta

Uma única conversa pode chamar múltiplas ferramentas:

TEXT 📖 Somente leitura
TextChunk: "Vou verificar dois arquivos"
ToolCallChunk: { name: "file_edit", id: "call_1", index: 0 }
ToolCallChunk: { name: "file_edit", id: "call_2", index: 1 }
ToolResultChunk: { toolCallId: "call_1", result: ... }
ToolResultChunk: { toolCallId: "call_2", result: ... }
TextChunk: "O conteúdo de ambos os arquivos acima..."
DoneChunk: { reason: "stop" }

4. Mecanismo de Recuperação de Erros

(1) Classificação de Erros

TYPESCRIPT
// Erro não-recuperável (ex.: chave de API inválida)
{
  type: 'error',
  error: new Error('Invalid API key'),
  recoverable: false
}

// Erro recuperável (ex.: limite de taxa)
{
  type: 'error',
  error: new Error('Rate limit exceeded'),
  recoverable: true,
  retryAfter: 5000
}

// Erro recuperável (ex.: instabilidade de rede)
{
  type: 'error',
  error: new Error('Connection timeout'),
  recoverable: true,
  retryAfter: 2000
}

(2) Fluxo de Auto-Recovery

100%
graph TD
    ERR[ErrorChunk] --> CHECK{recuperável?}
    CHECK -->|Não| FAIL[Encerrar stream + DoneChunk<br/>reason: error]
    CHECK -->|Sim| WAIT[Aguardar retryAfter]
    WAIT --> RETRY[Tentar novamente requisição]
    RETRY --> SUCCESS{Sucesso?}
    SUCCESS -->|Sim| CONTINUE[Continuar stream]
    SUCCESS -->|Não| ERR2[Outro ErrorChunk]
    ERR2 --> CHECK2{Tentativas esgotadas?}
    CHECK2 -->|Não| WAIT
    CHECK2 -->|Sim| FAIL

(3) Recuperação Manual

▶ Exemplo 2: Tratamento Manual de Erros de Stream

TYPESCRIPT
for await (const chunk of stream) {
  if (chunk.type === 'error') {
    if (chunk.recoverable) {
      ctx.logger.warn(`Erro de stream, recuperável: ${chunk.error.message}`)
      // Framework auto-tenta novamente
    } else {
      ctx.logger.error(`Erro de stream, não-recuperável: ${chunk.error.message}`)
      break
    }
  }
}

(4) Tratamento de Erros de Ferramenta

Quando a execução de uma ferramenta falha, o framework retorna o erro como ToolResultChunk para o LLM:

TYPESCRIPT
{
  type: 'tool_result',
  id: 'result_1',
  toolCallId: 'call_1',
  result: { error: true, message: 'Permission denied: /etc/passwd' },
  isError: true
}

O LLM pode então escolher:


5. Interrupção e Cancelamento

(1) Cancelamento pelo Usuário

Quando um usuário clica no botão "Parar" na Web UI, o stream é cancelado:

TYPESCRIPT
const controller = new AbortController()

const stream = ctx.llm.stream(request, { signal: controller.signal })

// Usuário cancela
controller.abort()

// Stream produz DoneChunk
// { type: 'done', reason: 'cancel' }

(2) Cancelamento por Timeout

TYPESCRIPT
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 60000)

try {
  for await (const chunk of ctx.llm.stream(request, { signal: controller.signal })) {
    // Processar chunk
  }
} finally {
  clearTimeout(timer)
}

(3) Cancelamento Condicional

TYPESCRIPT
let tokenCount = 0

for await (const chunk of stream) {
  if (chunk.type === 'text') {
    tokenCount += chunk.content.length
    if (tokenCount > 10000) {
      controller.abort()
      break
    }
  }
}

(4) Limpeza Após Cancelamento

Após cancelamento, o framework garante:


6. Estratégias de Tentativa

(1) Estratégias de Tentativa Built-in

Tipo de Erro Tentativas Estratégia de Backoff
Limite de taxa (429) 3 Backoff exponencial
Timeout de rede 2 Intervalo fixo
Erro de servidor (5xx) 2 Backoff exponencial
Erro de autenticação (401) 0 Sem tentativa
Erro de requisição (400) 0 Sem tentativa

(2) Configurando Tentativas

TYPESCRIPT
export const Config = Schema.object({
  maxRetries: Schema.number().default(3).description('Máximo de tentativas'),
  retryDelay: Schema.number().default(1000).description('Atraso inicial de tentativa (ms)'),
  retryMultiplier: Schema.number().default(2).description('Multiplicador de atraso para backoff exponencial')
})

(3) Lógica de Tentativa Customizada

▶ Exemplo 3: Tentativa com Backoff Exponencial

TYPESCRIPT
async function streamWithRetry(
  ctx: Context,
  request: LLMRequest,
  maxRetries = 3
): Promise<void> {
  let attempt = 0
  
  while (attempt <= maxRetries) {
    try {
      for await (const chunk of ctx.llm.stream(request)) {
        if (chunk.type === 'error' && chunk.recoverable) {
          attempt++
          const delay = Math.min(1000 * Math.pow(2, attempt), 30000)
          ctx.logger.warn(`tentativa ${attempt}/${maxRetries} em ${delay}ms`)
          await sleep(delay)
          break  // Re-entrar no loop while
        }
        // Processar chunks normais
      }
      return  // Sucesso
    } catch (error) {
      attempt++
      if (attempt > maxRetries) throw error
    }
  }
}

7. Logging e Observabilidade

(1) Logging de Stream

TYPESCRIPT
ctx.on('llm/stream/start', (request) => {
  ctx.logger.info(`stream iniciado: model=${request.model}`)
})

ctx.on('llm/stream/chunk', (chunk) => {
  ctx.logger.debug(`chunk: type=${chunk.type}`)
})

ctx.on('llm/stream/end', (done) => {
  ctx.logger.info(`stream encerrado: reason=${done.reason}, tokens=${done.usage?.totalTokens}`)
})

(2) Métricas de Desempenho

TYPESCRIPT
interface StreamMetrics {
  ttfb: number              // Time to First Byte
  totalDuration: number     // Duração total
  chunkCount: number        // Contagem de chunks
  toolCallCount: number     // Contagem de chamadas de ferramenta
  retryCount: number        // Contagem de tentativas
  tokenUsage: TokenUsage    // Uso de tokens
}

(3) Logging Estruturado

TYPESCRIPT
ctx.logger.info('llm_request', {
  model: request.model,
  messageCount: request.messages.length,
  toolCount: request.tools?.length || 0,
  stream: true
})

ctx.logger.info('llm_response', {
  model: response.model,
  duration: Date.now() - startTime,
  tokens: response.usage?.totalTokens
})

❓ Perguntas Frequentes

P Após cancelamento do stream, ferramentas já chamadas são revertidas?
R Não. Execução de ferramenta entra em vigor imediatamente (ex.: arquivos já criados). Cancelamento apenas para saída LLM subsequente.
P O stream sempre termina após um ErrorChunk?
R Não necessariamente. Se o erro é recuperável, o framework tenta novamente e o stream pode continuar. Apenas erros não-recuperáveis ou tentativas esgotadas produzem DoneChunk para encerrar o stream.
P Como calcular TTFB para saída em streaming?
R Registre o horário em que o primeiro chunk chega: typescript const start = Date.now() for await (const chunk of stream) { if (chunk.type === 'text') { const ttfb = Date.now() - start ctx.logger.info(`TTFB: ${ttfb}ms`) break } }
P Para que serve o index em múltiplos ToolCallChunks?
R index identifica a ordem de chamadas de ferramenta paralelas. Uma única resposta LLM pode conter múltiplos tool_calls; index incrementa a partir de 0.
P Streaming e saída completa podem ser suportados simultaneamente?
R Sim. Adaptadores implementam ambos os métodos complete() e stream(). Chamadores escolhem conforme necessário.
P Como mockar saída em streaming para testes?
R typescript async function* mockStream(): AsyncIterable<StreamChunk> { yield { type: 'text', content: 'Hello' } yield { type: 'text', content: ', world!' } yield { type: 'done', reason: 'stop' } }

📖 Resumo


📝 Exercícios

1. ⭐ Básico: Escreva um consumer que itera sobre um stream StreamChunk, contando chunks text/tool_call/tool_result/error/done separadamente. Teste com um stream mock.

2. ⭐⭐ Intermediário: Implemente um consumer de stream com timeout — se nenhum chunk chegar em 30 segundos, automaticamente cancela o stream. Use AbortController para cancelamento.

3. ⭐⭐⭐ Desafio: Implemente um wrapper de tentativa customizado em torno de ctx.llm.stream() que auto-tenta em erros recuperáveis (backoff exponencial, máx. 3 tentativas). Registre o atraso e resultado de cada tentativa. Teste com um adaptador LLM mock que alterna entre falha e sucesso.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%