Vercel AI SDK দিয়ে Response Streaming বানানো — যাতে ইউজারকে Wait করতে না হয়

Z
জাহিদ হাসান তন্ময়
১৬ অক্টোবর, ২০২৬
13 মিনিটের পড়া
🇬🇧 Read in English
|
0 views
অডিও ওভারভিউ ও পডকাস্ট
~১ মিনিট কুইক ওভারভিউ
0%

আমাদের আগের পর্বে আমরা যখন Next.js প্রজেক্টে OpenAI ও Claude API যুক্ত করে আমাদের প্রথম Chatbot Agent চালু করেছিলাম, তখন একটি ইউনিফাইড অ্যাডাপ্টার দিয়ে সফলভাবে GPT-4o এবং Claude 3.5 Sonnet কানেক্ট করেছিলাম। কিন্তু অ্যাপটি চালানোর সময় আপনি নিশ্চয়ই একটা বিরক্তিকর জিনিস খেয়াল করেছেন—ইউজার যখনই একটু বড় বা জটিল কোনো টেকনিক্যাল প্রশ্ন করে, তখন স্ক্রিনে টানা ৫ থেকে ৮ সেকেন্ড শুধু 'Thinking...' লেখা ভেসে থাকে এবং সবশেষে পুরো তিন প্যারাগ্রাফের উত্তর ধপ করে একসাথে স্ক্রিনে হাজির হয়!

ইউজারের এই দীর্ঘ অপেক্ষার সমস্যা সমাধান করতে Next.js প্রজেক্টে Vercel AI SDK (ai প্যাকেজ)-এর streamText এবং createStreamableValue ব্যবহার করে রিয়েল-টাইম Response Streaming তৈরি করা হয়। এতে মডেল পুরো উত্তর শেষ করার জন্য সার্ভারকে ব্লক করে রাখে না; বরং OpenAI বা Claude-এর সার্ভারে একেকটি শব্দ (Token) তৈরি হওয়ার সাথে সাথে মাত্র ২৫০ থেকে ৪৫০ মিলিসেকেন্ডের মধ্যে তা ব্রাউজারে পাঠানো শুরু করে এবং স্ট্রিম শেষ হলে onFinish কলব্যাকের মাধ্যমে মোট টোকেন খরচ ও সময়ের হিসাব সেভ করে রাখে।

সাধারণ ওয়েব অ্যাপ্লিকেশনে ডেটাবেস থেকে ডেটা আসতে সময় লাগে বড়জোর ১০০-২০০ মিলিসেকেন্ড। কিন্তু LLM কাজ করে একদম ভিন্নভাবে—এটি একটার পর একটা টোকেন (মোটামুটি সেকেন্ডে ৫০ থেকে ৯০টি টোকেন) জেনারেট করে। আপনার DevPulse এজেন্ট যদি ৪০০ টোকেনের একটি বাগ ট্রায়াজ গাইড লেখে, তবে পুরো লেখা শেষ হতে সময় লাগবে প্রায় ৬ সেকেন্ড। এই ৬ সেকেন্ড ইউজারকে সাদা স্ক্রিনের সামনে বসিয়ে না রেখে প্রথম সেকেন্ড থেকেই অক্ষরগুলো লাইভ দেখানোই হলো স্ট্রিমিংয়ের মূল উদ্দেশ্য।

সাধারণ (Blocking) API কল কেন ইউজার এক্সপেরিয়েন্স ও সার্ভারলেস ফাংশন নষ্ট করে?

স্ট্রিমিং না থাকাটা শুধু দেখতেই খারাপ লাগে তা নয়, প্রোডাকশন Next.js অ্যাপে এটি দুটি বড় টেকনিক্যাল বিপদ ডেকে আনে:

১. ডুপ্লিকেট রিকোয়েস্ট ও বাড়তি API খরচ: ইউজার যখন প্রশ্ন সাবমিট করার পর ৫-৬ সেকেন্ড কোনো নড়াচড়া দেখতে পায় না, তখন অনেকেই ভাবেন নেটওয়ার্ক কাজ করেনি বা অ্যাপ হ্যাং করেছে। ফলে তারা বারবার বাটনে ক্লিক করেন এবং ব্যাকএন্ডে একই প্রশ্নের জন্য ডাবল API কল চলে যায়। ২. Serverless Gateway Timeout: Vercel বা ক্লাউডফ্লেয়ারের মতো সার্ভারলেস প্ল্যাটফর্মে প্রথম বাইট পাঠানোর (Time to First Byte - TTFB) একটি নির্দিষ্ট সময়সীমা থাকে। আপনি যখন স্ট্রিমিং চালু করেন, তখন প্রথম ৩০০ মিলিসেকেন্ডেই সার্ভার রেসপন্স পাঠানো শুরু করে দেয়, ফলে উত্তর যত বড়ই হোক কানেকশন কখনোই মাঝপথে টাইমআউট হয় না।

আগে যখন আমরা ম্যানুয়ালি স্ট্রিমিং করতাম, তখন ব্রাউজারের ReadableStream, TextEncoder বাইট বাফার এবং OpenAI ও Anthropic-এর সম্পূর্ণ আলাদা দুটি SSE (Server-Sent Events) ইভেন্ট ফরম্যাট পার্স করার জন্য শত শত লাইনের জটিল কোড লিখতে হতো। কিন্তু Vercel AI SDK এই পুরো জটিলতাকে মাত্র একটি streamText ফাংশনের ভেতরে নিয়ে এসেছে।

Vercel AI SDK-তে স্ট্রিমিং পাইপলাইন কীভাবে কাজ করে?

আমরা কিন্তু আগের পর্বগুলোতে (পর্ব ১২, ১৩ ও ১৪) যে টাইপ-সেফ আর্কিটেকচার এবং sanitizeConversationHistory() বানিয়েছিলাম, তা মোটেও ফেলে দিচ্ছি না। বরং আমাদের LLM লেয়ারটিকে @ai-sdk/openai এবং @ai-sdk/anthropic দিয়ে আপগ্রেড করছি।

নিচের ডায়াগ্রামে দেখুন কীভাবে LLM থেকে প্রতিটি টোকেন ডেল্টা (Delta Chunk) আমাদের Next.js ব্যাকএন্ড হয়ে রিয়েল-টাইমে ক্লায়েন্টে পৌঁছায়:

`mermaid sequenceDiagram participant Client as React Client UI participant SA as Next.js Server Action / Route participant SDK as Vercel AI SDK (streamText) participant LLM as OpenAI / Anthropic API

Client->>SA: স্যানিটাইজড messages[] + modelId পাঠানো হলো
SA->>SDK: কল করা হলো streamText(model, system, messages)
SDK->>LLM: স্ট্রিমিং HTTP কানেকশন ওপেন
LLM-->>SDK: প্রথম টোকেন চাঙ্ক জেনারেট (~300ms TTFT)
SDK-->>SA: StreamableValue / DataStream-এ চাঙ্ক পুশ
SA-->>Client: স্ক্রিনে সাথে সাথে শব্দ রেন্ডারিং শুরু
LLM-->>SDK: শেষ টোকেন + Usage মেট্রিক্স প্রদান
SDK->>SA: ট্রিগার হলো onFinish({ text, usage })
SA-->>Client: স্ট্রিম ক্লোজ ও চূড়ান্ত AgentStepTrace প্রদান

`

আর্কিটেকচার ফ্লো সামারি (Architecture Flow Summary): ক্লায়েন্ট থেকে চ্যাট হিস্ট্রি ও মডেল আইডি Next.js ব্যাকএন্ডে গেলে তা Vercel AI SDK-এর streamText ফাংশনকে কল করে। OpenAI বা Anthropic থেকে প্রথম টোকেন তৈরি হওয়া মাত্র (~৩০০ মিলিসেকেন্ডে) SDK সেটি স্ট্রিমের মাধ্যমে সরাসরি ব্রাউজারে পাঠাতে থাকে এবং পুরো লেখা শেষ হলে onFinish কলব্যাক ফায়ার করে টোকেন সংখ্যা ও ল্যাটেন্সি AgentStepTrace-এ সেভ করে কানেকশনটি নিরাপদে বন্ধ করে।

ধাপ ১: Vercel AI SDK এবং প্রোভাইডার প্যাকেজ ইনস্টল করা

আপনার Next.js প্রজেক্টের টার্মিনালে নিচের কমান্ডটি চালিয়ে মূল ai প্যাকেজ এবং অফিশিয়াল OpenAI ও Anthropic অ্যাডাপ্টার দুটি ইনস্টল করে নিন:

bash npm install ai @ai-sdk/openai @ai-sdk/anthropic zod server-only

আমাদের ১৪তম পর্বে .env.local ফাইলে যে OPENAI_API_KEY এবং ANTHROPIC_API_KEY সেট করেছিলাম, সেগুলোই এখানে সরাসরি কাজ করবে—নতুন করে কোনো এনভায়রনমেন্ট ভেরিয়েবল বদলানোর প্রয়োজন নেই।

ধাপ ২: Unified Streaming Engine তৈরি করা (src/lib/agent/stream-client.ts)

চলুন এবার তৈরি করি আমাদের স্ট্রিমিং ইঞ্জিন src/lib/agent/stream-client.ts। খেয়াল করুন, আমরা আগের পর্বের sanitizeConversationHistory() ফাংশনটি এখানেও ব্যবহার করছি, যাতে স্ট্রিমিং শুরুর আগেই ১০-মেসেজের স্লাইডিং উইন্ডো এবং রোলের ক্রম ঠিক থাকে:

`typescript // src/lib/agent/stream-client.ts import 'server-only'; import { streamText, type LanguageModelV1 } from 'ai'; import { createOpenAI } from '@ai-sdk/openai'; import { createAnthropic } from '@ai-sdk/anthropic'; import { type AgentConfig } from './blueprint'; import { sanitizeConversationHistory, type ChatMessage, } from './llm-client';

function resolveStreamingModel( modelId: AgentConfig['modelId'] ): LanguageModelV1 { if (modelId === 'gpt-4o') { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error('Missing OPENAI_API_KEY in environment variables.'); } const openai = createOpenAI({ apiKey }); return openai('gpt-4o'); }

const apiKey = process.env.ANTHROPIC_API_KEY; if (!apiKey) { throw new Error('Missing ANTHROPIC_API_KEY in environment variables.'); } const anthropic = createAnthropic({ apiKey }); return anthropic('claude-3-5-sonnet-latest'); }

export interface StreamTelemetry { modelUsed: string; promptTokens: number; completionTokens: number; latencyMs: number; }

export function createAgentTextStream( messages: ChatMessage[], config: AgentConfig, onTelemetryReady?: (telemetry: StreamTelemetry) => void ) { const startTime = Date.now(); const safeMessages = sanitizeConversationHistory(messages, 10); const model = resolveStreamingModel(config.modelId);

return streamText({ model, system: config.systemPrompt, messages: safeMessages, temperature: config.temperature, maxTokens: 1024, onFinish: ({ usage }) => { if (onTelemetryReady) { onTelemetryReady({ modelUsed: config.modelId, promptTokens: usage.promptTokens, completionTokens: usage.completionTokens, latencyMs: Date.now() - startTime, }); } }, }); } `

এই ছোট ফাইলটি আমাদের দুটি বড় সুবিধা দিচ্ছে:

  • অভিন্ন ইন্টারফেস (LanguageModelV1): আগের পর্বে আমাদের OpenAI এবং Claude-এর জন্য আলাদা আলাদাভাবে system প্রম্পট এবং আউটপুট পার্সিং লিখতে হয়েছিল। এখানে resolveStreamingModel যে মডেলই রিটার্ন করুক না কেন, streamText একই ফরম্যাটে কাজ করে।
  • onFinish কলব্যাক দিয়ে টোকেন হিসাব: স্ট্রিমিংয়ের সময় একটি বড় চ্যালেঞ্জ হলো—মডেল তো একটার পর একটা শব্দ পাঠাচ্ছে, তাহলে মোট কত টোকেন খরচ হলো তা কখন পাব? onFinish কলব্যাকটি ঠিক শেষ টোকেনটি জেনারেট হওয়ার পরপরই সার্ভার সাইডে ট্রিগার হয় এবং আমাদের নিখুঁত promptTokens ও completionTokens দিয়ে দেয়।

ধাপ ৩: Server Action এবং Route Handler—দুই মাধ্যমেই স্ট্রিমিং এক্সপোজ করা

Next.js App Router-এ ফ্রন্টএন্ডে স্ট্রিম পাঠানোর দুটি প্রচলিত ও কার্যকর পথ আছে:

১. Server Action (createStreamableValue): যখন আপনি টেক্সট স্ট্রিমের পাশাপাশি নিজের কাস্টম TypeScript অবজেক্ট (যেমন আমাদের AgentStepTrace) একই সাথে টাইপ-সেফভাবে পাঠাতে চান। ২. Route Handler (toDataStreamResponse): যখন আপনি /api/chat রাউট তৈরি করে পরের পর্বে (পর্ব ১৬) সরাসরি React-এর জনপ্রিয় useChat হুক ব্যবহার করতে চান।

চলুন দুটিই তৈরি করে ফেলি, যাতে আমাদের ব্যাকএন্ড যেকোনো ধরনের UI-এর জন্য শতভাগ প্রস্তুত থাকে।

প্রথমে তৈরি করি আমাদের স্ট্রিমিং Server Action (src/app/actions/stream-agent.ts):

`typescript // src/app/actions/stream-agent.ts 'use server';

import { z } from 'zod'; import { createStreamableValue } from 'ai/rsc'; import { defaultDevPulseConfig, type AgentStepTrace, } from '@/lib/agent/blueprint'; import { type ChatMessage } from '@/lib/agent/llm-client'; import { createAgentTextStream } from '@/lib/agent/stream-client';

const streamRequestSchema = z.object({ sessionId: z.string().min(8), modelId: z .enum(['gpt-4o', 'claude-3-5-sonnet-latest']) .default('claude-3-5-sonnet-latest'), messages: z .array( z.object({ role: z.enum(['user', 'assistant']), content: z.string().trim().min(1).max(4000), }) ) .min(1), });

export async function streamDevPulseAgent(rawInput: unknown) { const textStream = createStreamableValue(''); const traceStream = createStreamableValue<AgentStepTrace | null>(null);

const parsed = streamRequestSchema.safeParse(rawInput); if (!parsed.success) { textStream.error( parsed.error.issues.map((i) => i.message).join('; ') ); traceStream.done(null); return { textStream: textStream.value, traceStream: traceStream.value, }; }

const { messages, modelId } = parsed.data; const activeConfig = { ...defaultDevPulseConfig, modelId };

(async () => { try { const result = createAgentTextStream( messages as ChatMessage[], activeConfig, (telemetry) => { traceStream.done({ stepIndex: 1, thoughtSummary: Streamed via ${telemetry.modelUsed} (${telemetry.promptTokens} in / ${telemetry.completionTokens} out tokens), durationMs: telemetry.latencyMs, }); } );

  for await (const delta of result.textStream) {
    textStream.append(delta);
  }

  textStream.done();
} catch (err) {
  const message =
    err instanceof Error ? err.message : 'Streaming interrupted';
  textStream.error(message);
  traceStream.done(null);
}

})();

return { textStream: textStream.value, traceStream: traceStream.value, }; } `

এখানে একটি দারুণ কৌশল খেয়াল করুন: (async () => { ... })() ব্লকটি কিন্তু আমরা await করিনি! কারণ এটিকে await করলে পুরো স্ট্রিম শেষ হওয়ার আগে ফাংশনটি কিছুই রিটার্ন করত না। বরং এটি ব্যাকগ্রাউন্ডে চলতে থাকে এবং streamDevPulseAgent সাথে সাথেই ক্লায়েন্টকে { textStream: textStream.value, traceStream: traceStream.value } হ্যান্ডেল দুটি দিয়ে দেয়।

এবার চলুন src/app/api/chat/route.ts ফাইলটিও তৈরি করে রাখি, যা একই createAgentTextStream ফাংশন ব্যবহার করে একটি স্ট্যান্ডার্ড স্ট্রিমিং HTTP রেসপন্স (toDataStreamResponse()) দেয়—যেটি পরের পর্বে আমাদের useChat হুকের মূল ব্যাকএন্ড হিসেবে কাজ করবে:

`typescript // src/app/api/chat/route.ts import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; import { defaultDevPulseConfig } from '@/lib/agent/blueprint'; import { type ChatMessage } from '@/lib/agent/llm-client'; import { createAgentTextStream } from '@/lib/agent/stream-client';

export const maxDuration = 60;

const routePayloadSchema = z.object({ modelId: z .enum(['gpt-4o', 'claude-3-5-sonnet-latest']) .default('claude-3-5-sonnet-latest'), messages: z .array( z.object({ role: z.enum(['user', 'assistant']), content: z.string().min(1), }) ) .min(1), });

export async function POST(req: NextRequest) { try { const body = await req.json(); const parsed = routePayloadSchema.safeParse(body);

if (!parsed.success) {
  return NextResponse.json(
    { error: 'Invalid chat payload', details: parsed.error.flatten() },
    { status: 400 }
  );
}

const { messages, modelId } = parsed.data;
const result = createAgentTextStream(messages as ChatMessage[], {
  ...defaultDevPulseConfig,
  modelId,
});

return result.toDataStreamResponse();

} catch (err) { const message = err instanceof Error ? err.message : 'Internal streaming error'; return NextResponse.json({ error: message }, { status: 500 }); } } `

ধাপ ৪: readStreamableValue দিয়ে ফ্রন্টএন্ডে লাইভ স্ট্রিম পরীক্ষা করা

আগামী পর্বে (পর্ব ১৬) আমরা React-এর useChat হুক নিয়ে বিস্তারিত কাজ করব। তার আগে আজ চলুন একটি ছোট কম্পোনেন্ট (src/components/StreamTestConsole.tsx) লিখে আমাদের Server Action-এর লাইভ স্ট্রিমিং এবং ট্রেস টেলিমেট্রি টেস্ট করে দেখি:

`tsx // src/components/StreamTestConsole.tsx 'use client';

import { useState } from 'react'; import { readStreamableValue } from 'ai/rsc'; import { streamDevPulseAgent } from '@/app/actions/stream-agent'; import { type AgentStepTrace } from '@/lib/agent/blueprint';

export default function StreamTestConsole() { const [prompt, setPrompt] = useState( 'Explain how our API gateway 429 rate limiting and token bucket burst rules work.' ); const [streamedReply, setStreamedReply] = useState(''); const [trace, setTrace] = useState<AgentStepTrace | null>(null); const [isStreaming, setIsStreaming] = useState(false);

async function handleStartStream(e: React.FormEvent) { e.preventDefault(); if (!prompt.trim() || isStreaming) return;

setIsStreaming(true);
setStreamedReply('');
setTrace(null);

const { textStream, traceStream } = await streamDevPulseAgent({
  sessionId: 'stream-session-01',
  modelId: 'claude-3-5-sonnet-latest',
  messages: [{ role: 'user', content: prompt }],
});

(async () => {
  for await (const latestTrace of readStreamableValue(traceStream)) {
    if (latestTrace) setTrace(latestTrace);
  }
})();

for await (const delta of readStreamableValue(textStream)) {
  if (typeof delta === 'string') {
    setStreamedReply((prev) => prev + delta);
  }
}

setIsStreaming(false);

}

return ( <input value={prompt} onChange={(e) => setPrompt(e.target.value)} className='w-full px-3 py-2 rounded-lg bg-slate-950 border border-slate-700 text-sm' /> {isStreaming ? 'Streaming Tokens...' : 'Stream Response'}

  <div className='p-4 min-h-[140px] rounded-lg bg-slate-950 border border-slate-800 text-sm whitespace-pre-wrap'>
    {streamedReply || 'Stream output will appear here token by token...'}
  </div>

  {trace && (
    <div className='text-xs text-cyan-400 pt-2 border-t border-slate-800 flex justify-between'>
      <span>{trace.thoughtSummary}</span>
      <span>Total Stream Duration: {trace.durationMs}ms</span>
    </div>
  )}
</div>

); } `

স্ট্রিমিং বাফারিং ও .done() নিয়ে আমার নিজের প্রজেক্টের একটি শিক্ষা

আমি যখন আমার LocalDrop Pro প্রজেক্টে রিয়েল-টাইম ডেটা চাঙ্ক ট্রান্সফার নিয়ে কাজ করছিলাম এবং পরবর্তীতে Next.js-এ প্রথমবার স্ট্রিমিং এজেন্ট ব্যাকএন্ড বানাই, তখন এমন দুটি বাগের মুখোমুখি হয়েছিলাম যা লোকালহোস্টে সহজে ধরা পড়ে না কিন্তু প্রোডাকশনে গিয়ে বড় ঝামেলা তৈরি করে।

প্রথম সমস্যাটি হয়েছিল createStreamableValue ব্যবহার করার সময়। আমার প্রথম দিকের কোডে try/catch ব্লকের ভেতরে যখন কোনো কারণে LLM API এরর দিচ্ছিল, আমি catch ব্লকে শুধু console.error(err) লিখে রেখেছিলাম—কিন্তু সেখানে textStream.done() বা textStream.error() কল করতে ভুলে গিয়েছিলাম! ফলাফল যা হলো: সার্ভারে এরর ঘটে যাওয়ার পরও ব্রাউজারের for await (const delta of readStreamableValue(textStream)) লুপটি স্ট্রিম শেষ হওয়ার সংকেতের আশায় অনন্তকাল ঝুলে রইল এবং UI-এর 'Streaming Tokens...' বাটনটি স্থায়ীভাবে ডিসেবল হয়ে গেল।

দ্বিতীয় অভিজ্ঞতাটি হয়েছিল যখন আমি একটি কাস্টম স্ট্রিমিং রাউট রিভার্স প্রক্সির (Nginx) পেছনে টেস্ট করছিলাম। আমি দেখলাম কোডে স্ট্রিমিং থাকা সত্ত্বেও ব্রাউজারে শব্দগুলো একে একে না এসে আগের মতোই ৫ সেকেন্ড পর একসাথে আসছে! পরে ডিবাগ করে বুঝলাম প্রক্সি সার্ভার ছোট ছোট স্ট্রিম চাঙ্কগুলোকে বাফার (Buffer) করে আটকে রাখছিল। Vercel AI SDK-এর toDataStreamResponse() ব্যবহার করলে তা স্বয়ংক্রিয়ভাবে সঠিক স্ট্রিমিং হেডার সেট করে দেয় এবং Server Action-এর প্রতিটি catch ও finally পাথে .done() বা .error() নিশ্চিত করলে স্ট্রিম ঝুলে থাকার সমস্যা চিরতরে দূর হয়ে যায়।

Vercel AI SDK দিয়ে স্ট্রিমিং করার সময় যে ৪টি ভুল এড়িয়ে চলবেন

পরের পর্বে useChat হুক যুক্ত করার আগে নিশ্চিত হয়ে নিন আপনার কোডে যেন এই ভুলগুলো না থাকে:

১. Server Action-এ return করার আগেই for await লুপকে await করে ফেলা

আপনি যদি streamDevPulseAgent ফাংশনের ভেতরে ব্যাকগ্রাউন্ড IIFE (async () => { ... })() ব্যবহার না করে সরাসরি টপ-লেভেলে for await (const delta of result.textStream) লিখে ফেলেন, তবে পুরো লেখা জেনারেট হওয়া শেষ না হওয়া পর্যন্ত Server Action ক্লায়েন্টকে কোনো স্ট্রিম অবজেক্টই রিটার্ন করবে না! ফলে স্ট্রিমিং কোড লিখেও আপনি সেই আগের মতো ব্লকিং আচরণই পাবেন।

২. .append() এবং .update() গুলিয়ে ফেলা

createStreamableValue-তে .append(delta) শুধুমাত্র নতুন আসা শব্দটুকু পাঠায়, আর .update(fullText) পুরো টেক্সটটা বারবার প্রথম থেকে পাঠায়। বড় উত্তরের ক্ষেত্রে .update() ব্যবহার করলে নেটওয়ার্কে অযথা প্রচুর ডেটা ট্রান্সফার হয়। তাই সবসময় .append(delta) ব্যবহার করা ভালো।

৩. স্ট্রিমিং রাউটে maxDuration সেট করতে ভুলে যাওয়া

স্ট্রিমিংয়ে প্রথম শব্দটা ৩০০ মিলিসেকেন্ডে চলে এলেও, পুরো উত্তর শেষ না হওয়া পর্যন্ত সার্ভারলেস ফাংশনটি কিন্তু ব্যাকএন্ডে চালু থাকে। তাই বড় কোড বা ডকুমেন্টেশন জেনারেট করার সময় যেন ১০ সেকেন্ডে কানেকশন কেটে না যায়, সেজন্য export const maxDuration = 60; অবশ্যই যোগ করবেন।

৪. স্ট্রিমিংয়ে সুইচ করার পর টোকেন ট্র্যাকিং বাদ দিয়ে দেওয়া

অনেকেই মনে করেন স্ট্রিমিং করলে হয়তো আর টোকেন ইউসেজ পাওয়া যায় না। এটি সম্পূর্ণ ভুল ধারণা। আমাদের কোডের মতো streamText-এর ভেতরে onFinish: ({ usage }) => { ... } ব্যবহার করলে স্ট্রিম শেষ হওয়ার সাথে সাথেই আপনি শতভাগ নির্ভুল টোকেন হিসাব পেয়ে যাবেন।

প্রায়শই জিজ্ঞাসিত প্রশ্নাবলী (FAQ)

Response Streaming করলে কি LLM আসলে দ্রুত চিন্তা করে, নাকি শুধু ইউজারের কাছে দ্রুত মনে হয়?

স্ট্রিমিং করলে OpenAI বা Claude-এর জিপিইউ (GPU) সেকেন্ডে যতগুলো টোকেন তৈরি করত, তার গতি বদলায় না। কিন্তু Time to First Token (TTFT) ৬ সেকেন্ড থেকে কমে মাত্র ~৩০০ মিলিসেকেন্ডে নেমে আসে। মানুষ যেহেতু সেকেন্ডে ৫০-৬০টি শব্দের চেয়ে দ্রুত পড়তে পারে না, তাই ইউজার প্রথম লাইন পড়তে পড়তেই ব্যাকগ্রাউন্ডে পরের প্যারাগ্রাফ চলে আসে—ফলে অ্যাপটি তাৎক্ষণিক (Instant) বলে মনে হয়।

কখন Server Action (createStreamableValue) আর কখন Route Handler (toDataStreamResponse) ব্যবহার করব?

আপনি যখন পরের পর্বে (পর্ব ১৬) React-এর বিল্ট-ইন useChat হুক ব্যবহার করে খুব সহজে চ্যাটবট UI বানাতে চাইবেন, তখন /api/chat Route Handler এবং toDataStreamResponse() ব্যবহার করা সবচেয়ে সুবিধাজনক। আর যখন আপনি টেক্সট স্ট্রিমের পাশাপাশি কাস্টম ডেটা স্ট্রাকচার (যেমন আমাদের AgentStepTrace বা ডায়নামিক সার্ভার কম্পোনেন্ট) একসাথে পাঠাতে চাইবেন, তখন Server Action ও createStreamableValue সেরা সমাধান।

Vercel AI SDK-এর streamText কি আমাদের এজেন্টের Tool Calling এবং Multi-step লুপ সাপোর্ট করে?

হ্যাঁ, চমৎকারভাবে সাপোর্ট করে। streamText-এর ভেতরে সরাসরি আমাদের Zod টুলস এবং maxSteps: 5 পাস করা যায়, যা স্ট্রিমিং চলাকালীন নিজে থেকেই টুল কল করে এবং ফলাফল নিয়ে আবার স্ট্রিমিং চালিয়ে যায়—যা আমরা ঠিক পরের পর্বগুলোতেই আমাদের DevPulse এজেন্টে যুক্ত করব।

🟢 ফ্রিল্যান্স ও কনট্রাক্ট প্রজেক্টের জন্য উন্মুক্ত

কাস্টম ওয়েব অ্যাপ বা এআই অটোমেশন বানাতে চান?

আমি স্টার্টআপ ও আধুনিক ব্যবসার জন্য স্কেলেবল ফুল-স্ট্যাক ওয়েব অ্যাপ্লিকেশন (Next.js, React, Node, PostgreSQL) এবং ইন্টেলিজেন্ট এআই এজেন্ট সলিউশন তৈরি করি। আপনার প্রজেক্টের আইডিয়া নিয়ে কথা বলা যাক!

ফুল-স্ট্যাক এমভিপি (MVP) ডেভেলপমেন্ট
অটোনোমাস এআই এজেন্ট ও এলএলএম ইন্টিগ্রেশন
পারফেক্ট এসইও ও সর্বোচ্চ পারফরম্যান্স
ক্লিন কোড ও মডার্ন আর্কিটেকচার

আর্টিকেলটি কি আপনার ভালো লেগেছে?

তন্ময়ের কাজকে সাপোর্ট করতে তালি (Clap) দিয়ে উৎসাহিত করুন!

0 views
Z

জাহিদ হাসান তন্ময়

সফটওয়্যার ডেভেলপার

MERN ফুল-স্ট্যাক ডেভেলপার এবং AI এজেন্ট ডেভেলপার, ঢাকা, বাংলাদেশ। ওয়েব ডেভেলপমেন্ট, রিঅ্যাক্ট, লারাভেল এবং আমার লার্নিং জার্নি নিয়ে লিখছি।

Related Articles

Stay Updated

Subscribe to get insights on full-stack architecture, AI agent engineering, and web development.