এখন পর্যন্ত যা বানানো হয়েছে তাতে Tool Calling যোগ করা — Agent-কে 'কাজ করার' ক্ষমতা দেওয়া

এখন পর্যন্ত যা বানানো হয়েছে তাতে Tool Calling যোগ করা — Agent-কে 'কাজ করার' ক্ষমতা দেওয়া

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

এখন পর্যন্ত যা বানানো হয়েছে তাতে Tool Calling যোগ করা — Agent-কে 'কাজ করার' ক্ষমতা দেওয়া
এখন পর্যন্ত যা বানানো হয়েছে তাতে Tool Calling যোগ করা — Agent-কে 'কাজ করার' ক্ষমতা দেওয়া

আমাদের আগের পর্বে আমরা যখন React-এ useChat Hook দিয়ে স্ট্রিমিং রেসপন্স UI-তে দেখানো নিয়ে কাজ করেছিলাম, তখন স্মার্ট অটো-স্ক্রলিং, স্টপ বাটন এবং লাইভ টোকেন টেলিমেট্রি ব্যাজসহ আমাদের প্রজেক্টের প্রথম ধাপ (Phase 1) সম্পন্ন করেছিলাম। কিন্তু একটু খেয়াল করলে দেখবেন, আমাদের DevPulse এজেন্টটি এখন খুব দ্রুত কথা বলতে পারলেও সে কিন্তু এখনো অন্ধ! আপনি যদি তাকে জিজ্ঞেস করেন—'আমাদের API-তে 429 রেট লিমিট নিয়ে কি কোনো ওপেন বাগ টিকেট আছে?'—সে শুধু অনুমানের ওপর ভিত্তি করে উত্তর দেবে, কারণ ডকুমেন্টেশন খোঁজা বা ইস্যু ট্র্যাকার চেক করার ক্ষমতা এখনো তার লাইভ স্ট্রিমে যুক্ত করা হয়নি।

আমাদের স্ট্রিমিং Next.js এজেন্টকে সত্যিকারের 'কাজ করার' ক্ষমতা দেওয়ার জন্য Vercel AI SDK-এর tool() হেল্পার ব্যবহার করে আমাদের Zod স্কিমা ও সার্ভার ফাংশনগুলোকে বাইন্ড করতে হবে, এরপর streamText()-এর ভেতরে tools ও maxSteps পাস করতে হবে এবং ফ্রন্টএন্ডের useChat হুকে message.toolInvocations রেন্ডার করতে হবে। এর ফলে এজেন্ট স্ট্রিমিং চলাকালীন নিজে থেকেই প্রয়োজনীয় TypeScript ফাংশন রান করবে, ডেটাবেস বা ডকুমেন্টেশন থেকে আসল তথ্য তুলে আনবে এবং সেই তথ্যের ওপর ভিত্তি করে ইউজারকে চূড়ান্ত উত্তর স্ট্রিম করে দেখাবে।

আজকের এই ১৭তম পর্বের মাধ্যমে আমরা আমাদের ইমপ্লিমেন্টেশন রোডম্যাপের দ্বিতীয় ধাপে (Phase 2: Tool Calling & Multi-Step Reasoning, পর্ব ১৭–২১) প্রবেশ করছি। মনে করে দেখুন, ১২ ও ১৩তম পর্বে আমরা blueprint.ts এবং tools.ts ফাইলে তিনটি টুল (searchKnowledgeBase, checkIssueTracker, এবং draftIssueTicket) তৈরি করে রেখেছিলাম। আজ আমরা সেই টুলগুলোকে সরাসরি আমাদের লাইভ OpenAI ও Claude স্ট্রিমিং ইঞ্জিনের সাথে যুক্ত করব।

টুল ছাড়া শুধুমাত্র স্ট্রিমিং চ্যাটবট কেন প্রোডাকশনে অচল?

একটি সাধারণ LLM—সেটি সেকেন্ডে যত দ্রুতই শব্দ স্ট্রিম করুক না কেন—আসলে পরীক্ষার হলে বই ছাড়া বসে থাকা একজন ছাত্রের মতো। তার প্রি-ট্রেইনিং ডেটাসেটে যা ছিল এবং আপনি প্রম্পটে যতটুকু টেক্সট দিচ্ছেন, এর বাইরে সে আপনার প্রজেক্টের ভেতরের কোনো খবরই জানে না।

কিন্তু বাস্তব সফটওয়্যার টিমে ডেভেলপাররা এজেন্টের কাছে সাধারণ সংজ্ঞা জানতে আসেন না; তারা এমন প্রশ্ন করেন যার উত্তর দিতে হলে সিস্টেমে অ্যাকশন নিতে হয়:

  • 'আমাদের ডকুমেন্টেশন চেক করে বলো 429 এরর হলে Retry-After হেডার কীভাবে কাজ করে, আর এই নিয়ে কি কোনো বাগ রিপোর্ট খোলা আছে?'
  • 'অথেনটিকেশন গেটওয়ে ক্র্যাশ নিয়ে একটি High-severity বাগ টিকেট ড্রাফট করে দাও।'

টুল কলিং না থাকলে আপনাকে প্রতিবার চ্যাটের শুরুতে পুরো ডকুমেন্টেশন আর সব ইস্যু ডেটাবেস থেকে তুলে সিস্টেম প্রম্পটে গুঁজে দিতে হতো, যাতে সামান্য 'হ্যালো' বললেও হাজার হাজার টোকেন খরচ হয়ে যেত। কিন্তু streamText-এ টুল কলিং যুক্ত করলে এজেন্ট নিজেই বুদ্ধি খাটিয়ে সিদ্ধান্ত নেয় ঠিক কখন কোন টুলটি কল করা দরকার।

লাইভ স্ট্রিমিং চলাকালীন এআই কোর থেকে তিনটি সার্ভার টুল কল হয়ে স্ট্রাকচার্ড ডেটা আবার মূল স্ট্রিমে ফিরে আসার থ্রিডি ভিজ্যুয়ালাইজেশন
লাইভ স্ট্রিমিং চলাকালীন এআই কোর থেকে তিনটি সার্ভার টুল কল হয়ে স্ট্রাকচার্ড ডেটা আবার মূল স্ট্রিমে ফিরে আসার থ্রিডি ভিজ্যুয়ালাইজেশন

লাইভ স্ট্রিমের ভেতরে Tool Calling কীভাবে কাজ করে?

সাধারণ ব্লকিং এপিআই-তে টুল কলিং বোঝা সহজ—সার্ভার সব টুল রান শেষ করে একসাথে উত্তর পাঠায়। কিন্তু স্ট্রিমিং কানেকশনে বিষয়টি অনেক বেশি চমৎকার, কারণ এখানে একই HTTP স্ট্রিমের ভেতর দিয়ে প্রথমে Tool Call ইভেন্ট, এরপর Tool Result JSON, এবং সবশেষে Final Text Tokens ক্রমান্বয়ে ব্রাউজারে আসতে থাকে।

নিচের সিকোয়েন্স ডায়াগ্রামে দেখুন ইউজার একটি প্রশ্ন করার পর useChat থেকে শুরু করে সার্ভারের টুল এক্সিকিউশন এবং পুনরায় LLM-এ ডেটা ফেরত যাওয়ার ফ্লো কীভাবে কাজ করে:

`mermaid sequenceDiagram participant UI as React useChat UI participant Route as Next.js /api/chat (streamText) participant LLM as Claude 3.5 Sonnet / GPT-4o participant Tools as Server Tool Registry (vercel-tools.ts)

UI->>Route: POST messages[] + modelId
Route->>LLM: ধাপ ১: প্রম্পট + Zod টুল ডেফিনিশন প্রেরণ
LLM-->>Route: টুল কলের সিদ্ধান্ত (searchKnowledgeBase)
Route-->>UI: স্ট্রিমে toolInvocation পাঠানো (state = 'call')
Route->>Tools: সার্ভারে searchKnowledgeBase(args) রান করা
Tools-->>Route: ডকুমেন্টেশন চাঙ্ক JSON রিটার্ন
Route-->>UI: স্ট্রিমে toolInvocation আপডেট (state = 'result')
Route->>LLM: ধাপ ২: টুলের রেজাল্ট পুনরায় LLM-এ পাঠানো (maxSteps = 5)
LLM-->>Route: ফলাফল বিশ্লেষণ করে চূড়ান্ত টেক্সট স্ট্রিম
Route-->>UI: লাইভ টেক্সট ডেল্টা + টেলিমেট্রি ব্যাজ প্রদর্শন

`

আর্কিটেকচার ফ্লো সামারি (Architecture Flow Summary): ইউজার প্রশ্ন পাঠানোর পর /api/chat রাউটের streamText ফাংশনটি চ্যাট হিস্ট্রির সাথে আমাদের Zod টুল স্কিমাগুলো LLM-এ পাঠায়। মডেল কোনো টুল কল করতে চাইলে সাথে সাথে ফ্রন্টএন্ডে state: 'call' সিগন্যাল যায়, সার্ভারে নিরাপদে টুলটি এক্সিকিউট হয়ে state: 'result' আপডেট হয় এবং maxSteps: 5 থাকার কারণে সেই JSON ফলাফল পুনরায় LLM-এর কাছে গিয়ে চূড়ান্ত উত্তরটি শব্দে শব্দে স্ক্রিনে স্ট্রিম হয়।

ধাপ ১: আমাদের Blueprint স্কিমাগুলোকে Vercel AI SDK-এর tool() দিয়ে যুক্ত করা

আমরা ১২তম পর্বে src/lib/agent/blueprint.ts-এ আমাদের তিনটি টুলের Zod স্কিমা (searchDocsSchema, checkIssuesSchema, এবং draftTicketSchema) এবং ১৩তম পর্বে src/lib/agent/tools.ts-এ executeAgentTool() ফাংশনটি লিখে রেখেছিলাম। শুরুতে আর্কিটেকচার পরিষ্কার রাখার সুফল আমরা এখন পাব—আমাদের কোনো ভ্যালিডেশন বা বিজনেস লজিক নতুন করে লিখতে হবে না।

চলুন src/lib/agent/vercel-tools.ts নামে একটি ফাইল তৈরি করি, যা ai প্যাকেজের tool() ফাংশন দিয়ে আমাদের আগের স্কিমা ও এক্সিকিউটরকে এক সুতোয় বাঁধবে:

`typescript // src/lib/agent/vercel-tools.ts import 'server-only'; import { tool } from 'ai'; import { searchDocsSchema, checkIssuesSchema, draftTicketSchema, } from './blueprint'; import { executeAgentTool } from './tools';

export const devPulseTools = { searchKnowledgeBase: tool({ description: 'Search internal technical documentation and architecture guides by semantic query. Always call this before answering product-specific API or configuration questions.', parameters: searchDocsSchema, execute: async (args) => { const result = await executeAgentTool('searchKnowledgeBase', args); return result; }, }),

checkIssueTracker: tool({ description: 'Query the project issue tracker for open or closed bug reports matching a keyword or error code.', parameters: checkIssuesSchema, execute: async (args) => { const result = await executeAgentTool('checkIssueTracker', args); return result; }, }),

draftIssueTicket: tool({ description: 'Create a structured bug ticket draft requiring Level 2 human confirmation before database insertion. Call this only when the user asks to report or log a confirmed bug.', parameters: draftTicketSchema, execute: async (args) => { const result = await executeAgentTool('draftIssueTicket', args); return result; }, }), }; `

এখানে প্রতিটি টুলের ভেতরে তিনটি অংশ কীভাবে কাজ করছে দেখুন:

  • description: মডেলকে স্পষ্টভাবে বলে দিচ্ছে ঠিক কোন পরিস্থিতিতে এই টুলটি বেছে নিতে হবে।
  • parameters: আমাদের তৈরি করা Zod স্কিমাকে সরাসরি OpenAI ও Anthropic-এর নিজস্ব JSON Schema ফরম্যাটে রূপান্তর করছে এবং মডেল যে আর্গুমেন্ট তৈরি করছে তা রানটাইমে ভ্যালিডেট করছে।
  • execute: সার্ভার সাইডে আমাদের executeAgentTool কল করে তার JSON আউটপুট স্ট্রিমে ফিরিয়ে দিচ্ছে।

ধাপ ২: stream-client.ts-এ tools এবং maxSteps সক্রিয় করা

এবার আমরা ১৫তম পর্বে তৈরি করা src/lib/agent/stream-client.ts ফাইলটি আপডেট করব। এখানে streamText()-এর ভেতরে আমরা tools: devPulseTools এবং maxSteps: config.maxSteps (5) পাস করব। একই সাথে onStepFinish হুক ব্যবহার করে কোন কোন টুল কল হলো তার তালিকাও টেলিমেট্রিতে যুক্ত করব:

`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'; import { devPulseTools } from './vercel-tools';

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; stepCount: number; toolsCalled: string[]; }

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); const toolsCalled: string[] = [];

return streamText({ model, system: config.systemPrompt, messages: safeMessages, tools: devPulseTools, maxSteps: config.maxSteps, temperature: config.temperature, maxTokens: 1024, onStepFinish: ({ toolCalls }) => { if (toolCalls && toolCalls.length > 0) { for (const call of toolCalls) { toolsCalled.push(call.toolName); } } }, onFinish: ({ usage, steps }) => { if (onTelemetryReady) { onTelemetryReady({ modelUsed: config.modelId, promptTokens: usage.promptTokens, completionTokens: usage.completionTokens, latencyMs: Date.now() - startTime, stepCount: steps.length, toolsCalled, }); } }, }); } `

আর আমাদের src/app/api/chat/route.ts ফাইলে MessageTelemetryAnnotation ইন্টারফেস এবং writeMessageAnnotation কলটিতে নতুন ফিল্ড দুটি (stepCount ও toolsCalled) যুক্ত করে দিন:

typescript // src/app/api/chat/route.ts (Updated interface) export interface MessageTelemetryAnnotation { modelUsed: string; promptTokens: number; completionTokens: number; latencyMs: number; stepCount: number; toolsCalled: string[]; }

ধাপ ৩: React useChat UI-তে লাইভ Tool Invocation এবং Approval দেখানো

ব্যাকএন্ডে যখনই কোনো টুল কল হয়, তখন ফ্রন্টএন্ডের useChat হুক প্রতিটি মেসেজের ভেতরে m.toolInvocations নামের একটি অ্যারে আপডেট করতে থাকে। টুলটি চলাকালীন এর state থাকে 'call', আর কাজ শেষ হলে state হয়ে যায় 'result'।

চলুন আমাদের src/components/DevPulseStreamChat.tsx কম্পোনেন্টটি আপডেট করি, যাতে ইউজার লাইভ দেখতে পান এজেন্ট কোন টুলটি কল করছে এবং draftIssueTicket টুল কল হলে যেন একটি Approve & Save Ticket কনফার্মেশন বাটন সামনে আসে:

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

import { useState } from 'react'; import { useChat } from 'ai/react'; import { useSmartAutoScroll } from '@/hooks/useSmartAutoScroll'; import { type MessageTelemetryAnnotation } from '@/app/api/chat/route';

export default function DevPulseStreamChat() { const [modelId, setModelId] = useState<'claude-3-5-sonnet-latest' | 'gpt-4o'>( 'claude-3-5-sonnet-latest' ); const [approvedDrafts, setApprovedDrafts] = useState<Record<string, boolean>>({});

const { messages, input, handleInputChange, handleSubmit, isLoading, stop, reload, error, } = useChat({ api: '/api/chat', id: 'devpulse-tool-session-01', });

const { containerRef, handleScroll, shouldAutoScroll, scrollToBottom } = useSmartAutoScroll(messages);

function onFormSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim()) return;

handleSubmit(e, {
  body: {
    sessionId: 'devpulse-tool-session-01',
    modelId,
  },
});

}

return ( DevPulse Tool-Enabled Agent Level 2 Autonomy | Tools: Docs RAG, Issue Tracker, Ticket Drafter

    <select
      value={modelId}
      onChange={(e) =>
        setModelId(e.target.value as 'claude-3-5-sonnet-latest' | 'gpt-4o')
      }
      disabled={isLoading}
      className='bg-slate-950 border border-slate-700 rounded-lg px-3 py-1.5 text-xs text-slate-200'
    >
      <option value='claude-3-5-sonnet-latest'>Claude 3.5 Sonnet</option>
      <option value='gpt-4o'>OpenAI GPT-4o</option>
    </select>
  </div>

  <div
    ref={containerRef}
    onScroll={handleScroll}
    className='relative h-96 overflow-y-auto space-y-4 p-4 bg-slate-950 rounded-lg border border-slate-800/80'
  >
    {messages.map((m) => {
      const telemetry = (m.annotations?.[0] as unknown) as
        | MessageTelemetryAnnotation
        | undefined;

      return (
        <div
          key={m.id}
          className={`p-3.5 rounded-lg text-sm space-y-3 ${
            m.role === 'user'
              ? 'bg-cyan-950/40 border border-cyan-800/50 ml-12'
              : 'bg-slate-900 border border-slate-800 mr-12'
          }`}
        >
          <div className='flex items-center justify-between text-[10px] uppercase tracking-wider text-slate-400'>
            <span>{m.role === 'user' ? 'Developer' : 'DevPulse Agent'}</span>
            {telemetry && (
              <span className='text-cyan-400 font-mono lowercase'>
                {telemetry.modelUsed} • {telemetry.stepCount ?? 1} steps •{' '}
                {telemetry.promptTokens} in / {telemetry.completionTokens} out •{' '}
                {telemetry.latencyMs}ms
              </span>
            )}
          </div>

          {m.toolInvocations && m.toolInvocations.length > 0 && (
            <div className='space-y-2'>
              {m.toolInvocations.map((toolInvocation) => {
                const { toolCallId, toolName, state, args } = toolInvocation;

                return (
                  <div
                    key={toolCallId}
                    className='p-2.5 rounded bg-slate-950 border border-purple-900/60 text-xs space-y-1.5 font-mono'
                  >
                    <div className='flex items-center justify-between'>
                      <span className='text-purple-400 font-semibold'>
                        ⚡ Tool: {toolName}
                      </span>
                      <span className='text-[10px] px-2 py-0.5 rounded bg-slate-900 text-slate-300'>
                        {state === 'result' ? '✓ Completed' : '⏳ Executing...'}
                      </span>
                    </div>
                    <div className='text-slate-400 truncate'>
                      Args: {JSON.stringify(args)}
                    </div>

                    {state === 'result' &&
                      toolName === 'draftIssueTicket' &&
                      !approvedDrafts[toolCallId] && (
                        <div className='pt-2 flex items-center justify-between border-t border-slate-800 font-sans'>
                          <span className='text-amber-400 text-xs'>
                            Level 2 Guard: Confirm creating this issue ticket?
                          </span>
                          <button
                            type='button'
                            onClick={() =>
                              setApprovedDrafts((prev) => ({
                                ...prev,
                                [toolCallId]: true,
                              }))
                            }
                            className='px-2.5 py-1 rounded bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-medium'
                          >
                            Approve & Save Ticket
                          </button>
                        </div>
                      )}

                    {approvedDrafts[toolCallId] && (
                      <div className='text-emerald-400 text-xs font-sans pt-1'>
                        ✓ Ticket confirmed and persisted to tracker.
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          )}

          {m.content && (
            <div className='whitespace-pre-wrap leading-relaxed'>
              {m.content}
            </div>
          )}
        </div>
      );
    })}

    {!shouldAutoScroll && isLoading && (
      <button
        type='button'
        onClick={scrollToBottom}
        className='sticky bottom-2 left-1/2 -translate-x-1/2 px-3 py-1 rounded-full bg-cyan-700 text-xs font-medium shadow-lg'
      >
        Resume Auto-Scroll ↓
      </button>
    )}
  </div>

  {error && (
    <div className='p-3 rounded-lg bg-red-950/50 border border-red-800 text-xs text-red-200 flex items-center justify-between'>
      <span>Error: {error.message}</span>
      <button
        type='button'
        onClick={() => reload({ body: { modelId } })}
        className='px-2.5 py-1 rounded bg-red-800 hover:bg-red-700 text-white font-medium'
      >
        Retry
      </button>
    </div>
  )}

  <form onSubmit={onFormSubmit} className='flex gap-2'>
    <input
      value={input}
      onChange={handleInputChange}
      placeholder='Ask about 429 rate limits, open bugs, or ask to draft a ticket...'
      className='flex-1 px-3.5 py-2 rounded-lg bg-slate-950 border border-slate-700 text-sm'
    />
    {isLoading ? (
      <button
        type='button'
        onClick={stop}
        className='px-4 py-2 rounded-lg bg-amber-600 hover:bg-amber-500 text-sm font-semibold'
      >
        Stop
      </button>
    ) : (
      <button
        type='submit'
        disabled={!input.trim()}
        className='px-4 py-2 rounded-lg bg-cyan-600 hover:bg-cyan-500 disabled:opacity-50 text-sm font-semibold'
      >
        Send
      </button>
    )}
  </form>
</div>

); } `

ডার্ক ফ্রস্টেড গ্লাস চ্যাট কার্ডে স্ট্রিমিং উত্তরের উপরে দুটি বেগুনি টুল ইনভোকেশন বক্স এবং সবুজ কনফার্মেশন বাটনের ভিজ্যুয়াল রূপ
ডার্ক ফ্রস্টেড গ্লাস চ্যাট কার্ডে স্ট্রিমিং উত্তরের উপরে দুটি বেগুনি টুল ইনভোকেশন বক্স এবং সবুজ কনফার্মেশন বাটনের ভিজ্যুয়াল রূপ

আমার নিজের ডিবাগিং অভিজ্ঞতা: টুল কল হওয়ার পরও যখন চ্যাট বাবল পুরোপুরি ফাঁকা থাকত!

আমি যখন আমার GitHub Issues Tracker প্রজেক্টের ইস্যু ফিল্টারিং লজিক থেকে অনুপ্রাণিত হয়ে প্রথমবার Vercel AI SDK-এর streamText ও useChat হুকের ভেতরে টুল কলিং যুক্ত করি, তখন বেশ অদ্ভুত একটা সমস্যার মুখে পড়েছিলাম।

আমি tools অবজেক্টটি সুন্দরভাবে streamText-এ পাস করলাম এবং চ্যাটবটকে জিজ্ঞেস করলাম: '429 এরর নিয়ে কোনো ওপেন ইস্যু আছে কি না চেক করো।' আমার VS Code-এর টার্মিনালে পরিষ্কার দেখতে পেলাম checkIssueTracker ফাংশনটি কল হয়েছে এবং সঠিক JSON ডেটাও রিটার্ন করেছে। কিন্তু ব্রাউজারে তাকিয়ে দেখি অ্যাসিস্ট্যান্টের মেসেজ বাবল একদম ফাঁকা—একটি অক্ষরও লেখা আসেনি এবং লোডিং স্পিনারও থেমে গেছে!

প্রায় এক ঘণ্টা নেটওয়ার্ক ট্যাব ঘাঁটার পর আসল কারণটা ধরা পড়ল। Vercel AI SDK-তে আপনি যদি streamText()-এর ভেতরে স্পষ্টভাবে maxSteps বলে না দেন, তবে এর ডিফল্ট মান থাকে 1। ফলে ১ নম্বর ধাপে যখন মডেল কোনো টেক্সট না লিখে checkIssueTracker টুলটি কল করল এবং সার্ভার সেটির JSON রেজাল্ট বের করল, তখনই ১ ধাপ শেষ হয়ে যাওয়ায় streamText সাথে সাথে স্ট্রিম ক্লোজ করে দিচ্ছিল! অর্থাৎ, টুলের পাওয়া রেজাল্টটা যে আবার ২ নম্বর ধাপে মডেলের কাছে ফেরত পাঠিয়ে মানুষের পড়ার মতো বাংলা বা ইংরেজি বাক্য তৈরি করতে হবে—সেই দ্বিতীয় ধাপটি আর শুরুই হচ্ছিল না। আর ফ্রন্টএন্ডে তখনো m.toolInvocations রেন্ডার না করায় পুরো বাবলটি খালি দেখাচ্ছিল।

যেই মুহূর্তে আমি maxSteps: config.maxSteps (5) যোগ করলাম এবং JSX-এ m.toolInvocations কার্ড রেন্ডার করলাম, সাথে সাথেই ম্যাজিকের মতো ১ নম্বর ধাপে বেগুনি রঙের টুল কার্ড এবং ২ নম্বর ধাপে তার নিচে চূড়ান্ত টেক্সট উত্তর স্ট্রিম হওয়া শুরু করল।

স্ট্রিমিং এজেন্টে Tool Calling যোগ করার সময় যে ৪টি ভুল এড়িয়ে চলবেন

পরের পর্বে (পর্ব ১৮) মাল্টি-স্টেপ চেইনিং নিয়ে কাজ করার আগে নিশ্চিত হয়ে নিন আপনার কোডে যেন এই ৪টি ভুল না থাকে:

১. streamText()-এ maxSteps সেট করতে ভুলে যাওয়া

একটু আগেই যেমনটি বললাম, maxSteps না দিলে টুল কল হওয়ার পর এজেন্ট আর চূড়ান্ত উত্তর লিখতে পারে না। তাই সবসময় maxSteps: 5 (বা আপনার প্রয়োজন অনুযায়ী ৩-৫) সেট করে রাখবেন।

২. টুল থেকে বিশাল আকারের আনফিল্টার্ড ডেটাবেস অবজেক্ট রিটার্ন করা

আপনার টুলের execute ফাংশন যা রিটার্ন করে, পরের ধাপে সেটি হুবহু LLM-এর ইনপুট প্রম্পটে চলে যায়। আপনি যদি ডেটাবেস থেকে ৫০টি ইস্যুর পুরো বিবরণ ও লগ রিটার্ন করে দেন, তবে এক টুল কলেই ১৫,০০০ টোকেন শেষ হয়ে যাবে। তাই সবসময় টুলের ভেতর থেকে শুধুমাত্র প্রয়োজনীয় ফিল্ডগুলো (id, title, status, এবং ছোট excerpt) ফিল্টার করে রিটার্ন করবেন।

৩. টুলের description অস্পষ্ট রাখা

এজেন্ট কখন searchKnowledgeBase কল করবে আর কখন checkIssueTracker কল করবে, তা সম্পূর্ণ নির্ভর করে আপনার লেখা description-এর ওপর। তাই প্রতিটি টুলের বর্ণনায় স্পষ্টভাবে লিখে দিন কোন টুলটি ঠিক কোন ধরনের প্রশ্নের জন্য ব্যবহার করতে হবে।

৪. UI-তে শুধুমাত্র m.content রেন্ডার করা

এজেন্ট যখন পরপর দুটি টুল কল করে, তখন প্রথম ২-৩ সেকেন্ড m.content ফাঁকা থাকে এবং শুধু m.toolInvocations আপডেট হতে থাকে। আপনি যদি UI-তে টুল ইনভোকেশন কার্ড না দেখান, তবে ইউজার ভাববে চ্যাটবট কাজ করছে না। তাই সবসময় {m.content}-এর উপরে m.toolInvocations রেন্ডার করবেন।

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

streamText কীভাবে বোঝে কখন সাধারণ টেক্সট উত্তর দিতে হবে আর কখন টুল কল করতে হবে?

যখন আপনি streamText-এ tools: devPulseTools পাস করেন, তখন SDK প্রতিটি টুলের নাম, description এবং Zod স্কিমা LLM-এর কাছে পাঠায়। ইউজার যদি সাধারণ কুশল বিনিময় বা কনসেপ্ট নিয়ে প্রশ্ন করেন, মডেল সরাসরি টেক্সট স্ট্রিম করে। কিন্তু প্রশ্নটি যদি ডকুমেন্টেশন বা বাগ স্ট্যাটাস নিয়ে হয়, তবে মডেল টেক্সটের বদলে একটি বিশেষ tool_call সিগন্যাল পাঠায়, যা SDK ধরে ফেলে এবং সার্ভারে থাকা execute ফাংশনটি রান করে।

ব্রাউজারের ইউজার কি আমার টুলের ভেতরের সার্ভার কোড বা ডেটাবেস কুয়েরি দেখতে পাবে?

না, কখনোই না। আমাদের vercel-tools.ts ফাইলে import 'server-only' দেওয়া আছে এবং এর execute ফাংশনটি শুধুমাত্র সার্ভারের /api/chat রাউটের ভেতরে রান হয়। ব্রাউজারে শুধুমাত্র টুলের নাম, ইনপুট আর্গুমেন্ট এবং ফিল্টার করা ফলাফলটুকু (toolInvocation) যায় যাতে UI-তে কার্ডটি দেখানো যায়।

এজেন্ট কি একই প্রশ্নের উত্তরে একসাথে দুটি আলাদা টুল কল করতে পারবে?

হ্যাঁ, পারবে। যেহেতু আমরা maxSteps: 5 দিয়ে রেখেছি, তাই ইউজার যদি জিজ্ঞেস করেন—'429 এরর কেন আসে এবং এটা নিয়ে কোনো বাগ খোলা আছে কি না?'—তবে এজেন্ট প্রথমে searchKnowledgeBase কল করে ডকুমেন্টেশন পড়বে, এরপর checkIssueTracker কল করে ওপেন বাগ চেক করবে এবং সবশেষে দুটি তথ্য মিলিয়ে একটি সম্পূর্ণ উত্তর স্ট্রিম করবে।

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

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

আমি স্টার্টআপ ও আধুনিক ব্যবসার জন্য স্কেলেবল ফুল-স্ট্যাক ওয়েব অ্যাপ্লিকেশন (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.