নিজের একটা Custom Tool লিখে Agent-এর সাথে যুক্ত করা

নিজের একটা Custom Tool লিখে Agent-এর সাথে যুক্ত করা

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

নিজের একটা Custom Tool লিখে Agent-এর সাথে যুক্ত করা
নিজের একটা Custom Tool লিখে Agent-এর সাথে যুক্ত করা

আমাদের আগের পর্বে আমরা যখন LangChain.js দিয়ে একাধিক ধাপে চিন্তা করতে পারা (Multi-step) Agent তৈরি করেছিলাম, তখন agent_scratchpad ব্যবহার করে এজেন্টকে শিখিয়েছিলাম কীভাবে ১ নম্বর ধাপের ফলাফল বিশ্লেষণ করে ২ ও ৩ নম্বর ধাপে সিদ্ধান্ত নিতে হয়। কিন্তু খেয়াল করে দেখুন—আমাদের আগের tools.ts ফাইলের টুলগুলো কিন্তু এখনো আমাদেরই লিখে রাখা কিছু স্ট্যাটিক ডেটা রিটার্ন করছিল। বাস্তবে যখন আপনি কোনো ক্লায়েন্ট বা নিজের প্রোডাক্টের জন্য AI Agent বানাবেন, তখন আপনাকে এমন কাস্টম টুল (Custom Tool) লিখতে হবে যা সরাসরি লাইভ থার্ড-পার্টি API, গিটহাব রিপোজিটরি, পেমেন্ট গেটওয়ে বা ডেটাবেসের সাথে কথা বলতে পারে।

Next.js প্রজেক্টে নিজের একটি প্রোডাকশন-গ্রেড Custom Tool লিখে AI Agent-এর সাথে যুক্ত করার মূল কৌশল হলো—আপনার এক্সটার্নাল API বা বিজনেস লজিককে ৪টি নিরাপত্তা স্তরের ভেতরে রেখে একটি ফ্রেমওয়ার্ক-নিরপেক্ষ সার্ভার ফাংশন তৈরি করা: ১) Zod Input Allowlist, ২) AbortSignal.timeout() সার্কিট ব্রেকার, ৩) DTO Payload Shaper (টোকেন বাঁচাতে অপ্রয়োজনীয় ডেটা ছাঁটাই), এবং ৪) Structured Error Envelope (agentInstruction-সহ)। এরপর এই একটি ফাংশনকেই আপনি একই সাথে Vercel AI SDK এবং LangChain.js উভয় ইঞ্জিনে যুক্ত করতে পারবেন।

অনেক ডেভেলপার প্রথমবার কাস্টম টুল লেখার সময় ভাবেন—এ আর এমন কী, টুলের execute ফাংশনের ভেতরে একটা সাধারণ fetch() কল করে await res.json() রিটার্ন করে দিলেই তো হলো! কিন্তু প্রোডাকশনে এই ছোট্ট ভুলটিই আপনার এজেন্টকে ধীরগতির, ব্যয়বহুল এবং মারাত্মক নিরাপত্তা ঝুঁকির (যেমন SSRF অ্যাটাক) মুখে ফেলে দেয়।

একটি প্রোডাকশন-রেডি Custom Tool-এর ৪টি প্রধান স্তম্ভ

সাধারণ ওয়েব অ্যাপে ফ্রন্টএন্ড যখন কোনো API কল করে, তখন JSON রেসপন্সে ৫০টি অতিরিক্ত ফিল্ড থাকলে ব্রাউজারের কিছুই যায় আসে না। কিন্তু একজন AI Agent যখন লুপের ভেতরে কোনো কাস্টম টুল কল করে, তখন সেই টুলের রিটার্ন করা প্রতিটি অক্ষর সরাসরি LLM-এর ইনপুট প্রম্পটে ঢুকে যায়!

তাই নিজের কাস্টম টুল লেখার সময় সবসময় নিচের ৪টি ইঞ্জিনিয়ারিং নিয়ম মেনে চলতে হবে:

১. Input Allowlisting (SSRF ও ইনজেকশন রোধ): কখনোই এজেন্টকে এমন কোনো টুল দেবেন না যার প্যারামিটারে যেকোনো ওপেন URL (url: z.string()) পাঠানো যায়। প্রম্পট ইনজেকশনের মাধ্যমে কেউ যদি মডেলকে দিয়ে http://localhost:3000/internal বা ক্লাউড মেটাডেটা আইপি কল করিয়ে নেয়, তবে আপনার সার্ভার নিজের গোপন তথ্য নিজেই ফাঁস করে দেবে। তাই ইনপুট সবসময় নির্দিষ্ট z.enum() বা ভ্যালিডেটেড আইডির মধ্যে সীমাবদ্ধ রাখুন। ২. কঠোর টাইমআউট (AbortSignal.timeout): বাইরের যেকোনো API যেকোনো মুহূর্তে ঝুলে যেতে পারে। আপনার কাস্টম টুল যদি একটি ধীরগতির API-এর জন্য ২৫ সেকেন্ড অপেক্ষা করে, তবে পুরো Next.js Server Action টাইমআউট হয়ে যাবে। তাই প্রতিটি এক্সটার্নাল কলে ৪ থেকে ৫ সেকেন্ডের (5000ms) হার্ড টাইমআউট বসানো বাধ্যতামূলক। ৩. ডেটা ছাঁটাই ও DTO Shaping: ধরুন আপনি GitHub API থেকে একটি ইস্যুর তথ্য আনছেন। গিটহাবের র-রেসপন্সে ইউজারের avatar_url, followers_url, node_id সহ প্রায় ৬০টির বেশি ফিল্ড এবং বিশাল লম্বা স্ট্যাক ট্রেস থাকে। পুরো অবজেক্টটি রিটার্ন করলে এক টুল কলেই ৫,০০০ টোকেন শেষ হয়ে যাবে! তাই শুধুমাত্র দরকারি ৬-৮টি ফিল্ড ছেঁকে (DTO) এবং বড় টেক্সট ৬০০ ক্যারেক্টারে কেটে রিটার্ন করতে হবে। ৪. agentInstruction-সহ এরর হ্যান্ডলিং: কোনো ইস্যু খুঁজে না পেলে (404 Not Found) সরাসরি এরর থ্রো না করে { ok: false, retryable: false, agentInstruction: '...' } রিটার্ন করুন, যাতে এজেন্ট বুঝতে পারে এই আইডি দিয়ে বারবার চেষ্টা করে লাভ নেই।

এক্সটার্নাল এপিআই থেকে আসা বিশাল ডেটা পেলোডকে চার ধাপের ফিল্টার দিয়ে ছেঁটে ছোট ও নিরাপদ ডেটা কিউবে রূপান্তর করার থ্রিডি ভিজ্যুয়ালাইজেশন
এক্সটার্নাল এপিআই থেকে আসা বিশাল ডেটা পেলোডকে চার ধাপের ফিল্টার দিয়ে ছেঁটে ছোট ও নিরাপদ ডেটা কিউবে রূপান্তর করার থ্রিডি ভিজ্যুয়ালাইজেশন

আমাদের কাস্টম টুল ডিজাইন: inspectLiveGitHubIssue

আমাদের DevPulse এজেন্টকে আরও শক্তিশালী করার জন্য আজ আমরা তৈরি করব একটি লাইভ কাস্টম টুল: inspectLiveGitHubIssue। ধরুন, কোনো ডেভেলপার জানতে চাইলেন—'Next.js বা Vercel AI SDK-এর অফিশিয়াল গিটহাব রিপোজিটরিতে অমুক নম্বরের ইস্যুটির বর্তমান অবস্থা কী এবং সেখানে কী আলোচনা হচ্ছে?'

আমাদের এই কাস্টম টুলটি সরাসরি লাইভ GitHub REST API ([https://api.github.com/repos/](https://api.github.com/repos/)...) কল করবে, ৫ সেকেন্ডের টাইমআউট গার্ড রাখবে, গিটহাবের বিশাল JSON থেকে অপ্রয়োজনীয় মেটাডেটা ফেলে দিয়ে মাত্র ১৮০ টোকেনের একটি পরিচ্ছন্ন সারাংশ তৈরি করবে এবং সেটি এজেন্টের কাছে পাঠাবে।

নিচের ফ্লোচার্টে দেখুন আমাদের কাস্টম টুলের ভেতরের ৪-স্তরের পাইপলাইন কীভাবে কাজ করে:

`mermaid flowchart TD Agent([AI Agent Orchestrator Vercel AI SDK / LangChain.js]) -->|টুল আর্গুমেন্ট| Guard[স্তর ১: Zod স্কিমা ও Repo Allowlist চেক] Guard -->|ভুল ইনপুট| Err[Non-Retryable ভ্যালিডেশন এরর রিটার্ন] Guard -->|নিরাপদ ইনপুট| Fetch[স্তর ২: AbortSignal.timeout 5000ms সহ লাইভ GitHub Fetch]

Fetch -->|HTTP 200 JSON| Shaper[স্তর ৩: DTO Shaper ও বডি ট্রিমার < 600 chars]
Fetch -->|Timeout / HTTP 4xx 5xx| Handler[স্তর ৪: এরর ক্লাসিফায়ার

retryable: true / false]

Shaper --> Output[কমপ্যাক্ট JSON Observation ~180 Tokens]
Handler --> Output
Err --> Output
Output --> Agent

`

আর্কিটেকচার ফ্লো সামারি (Architecture Flow Summary): এজেন্ট অর্কেস্ট্রেটর যখন কাস্টম টুলটি কল করে, তখন ১ম স্তরে Zod স্কিমা এবং অনুমোদিত রিপোজিটরি তালিকা (Allowlist) যাচাই করা হয়; ২য় স্তরে ৫০০০ মিলিসেকেন্ডের AbortSignal.timeout-সহ লাইভ GitHub API কল যায়; ৩য় স্তরে গিটহাবের বিশাল JSON থেকে বাড়তি মেটাডেটা ছেঁটে ফেলে একটি ছোট DTO (~১৮০ টোকেন) তৈরি হয়; এবং ৪র্থ স্তরে কোনো নেটওয়ার্ক বা ৪0৪ এরর হলে তা স্পষ্ট agentInstruction-সহ আবার এজেন্টের কাছে ফিরে যায়।

ধাপ ১: শূন্য থেকে কাস্টম টুল এক্সিকিউটর লেখা (custom-github-tool.ts)

চলুন src/lib/agent/custom-github-tool.ts ফাইলটি তৈরি করি। লক্ষ্য করুন, এই ফাইলটিতে আমরা কোনো নির্দিষ্ট AI লাইব্রেরি ইমপোর্ট করছি না—শুধুমাত্র খাঁটি TypeScript, Zod এবং Next.js-এর fetch ব্যবহার করছি, যাতে এই একই টুল যেকোনো ফ্রেমওয়ার্কে ব্যবহার করা যায়:

`typescript // src/lib/agent/custom-github-tool.ts import 'server-only'; import { z } from 'zod';

const ALLOWED_REPOSITORIES = [ 'vercel/next.js', 'vercel/ai', 'langchain-ai/langchainjs', ] as const;

export const inspectGitHubIssueSchema = z.object({ repository: z .enum(ALLOWED_REPOSITORIES) .describe( 'Allowlisted GitHub repository in owner/name format to inspect.' ), issueNumber: z .number() .int() .positive() .max(999999) .describe('The exact numeric GitHub issue or pull request ID (e.g., 402).'), });

export type InspectGitHubIssueInput = z.infer;

export interface SanitizedGitHubIssueDTO { repository: string; issueNumber: number; title: string; state: string; author: string; labels: string[]; createdAt: string; commentsCount: number; bodyExcerpt: string; htmlUrl: string; }

export interface CustomToolEnvelope { ok: boolean; toolName: string; retryable: boolean; durationMs: number; data: T | null; agentInstruction?: string; }

interface RawGitHubIssueResponse { number: number; title: string; state: string; user?: { login?: string }; labels?: Array<{ name?: string } | string>; created_at: string; comments: number; body?: string | null; html_url: string; }

function truncateCleanText(raw: string | null | undefined, maxLength = 600): string { if (!raw) return 'No description provided.'; const collapsed = raw.replace(/\r\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim(); if (collapsed.length <= maxLength) return collapsed; return ${collapsed.slice(0, maxLength)}... [Truncated to save context tokens]; }

export async function executeInspectGitHubIssue( rawInput: unknown ): Promise<CustomToolEnvelope> { const startTime = Date.now(); const parsed = inspectGitHubIssueSchema.safeParse(rawInput);

if (!parsed.success) { return { ok: false, toolName: 'inspectLiveGitHubIssue', retryable: false, durationMs: Date.now() - startTime, data: null, agentInstruction: Validation failed: ${parsed.error.issues .map((i) => i.message) .join('; ')}. Use an allowlisted repository and a positive integer., }; }

const { repository, issueNumber } = parsed.data; const endpoint = https://api.github.com/repos/${repository}/issues/${issueNumber};

const headers: Record<string, string> = { Accept: 'application/vnd.github+json', 'User-Agent': 'DevPulse-AI-Agent/1.0', };

if (process.env.GITHUB_PAT_TOKEN) { headers.Authorization = Bearer ${process.env.GITHUB_PAT_TOKEN}; }

try { const response = await fetch(endpoint, { method: 'GET', headers, signal: AbortSignal.timeout(5000), next: { revalidate: 120 }, });

if (response.status === 404) {
  return {
    ok: false,
    toolName: 'inspectLiveGitHubIssue',
    retryable: false,
    durationMs: Date.now() - startTime,
    data: null,
    agentInstruction: `Issue #${issueNumber} does not exist in ${repository}. Do not retry the same issue number.`,
  };
}

if (response.status === 403 || response.status === 429) {
  return {
    ok: false,
    toolName: 'inspectLiveGitHubIssue',
    retryable: false,
    durationMs: Date.now() - startTime,
    data: null,
    agentInstruction:
      'GitHub API rate limit reached. Inform the user and answer using existing documentation context.',
  };
}

if (!response.ok) {
  return {
    ok: false,
    toolName: 'inspectLiveGitHubIssue',
    retryable: true,
    durationMs: Date.now() - startTime,
    data: null,
    agentInstruction: `Upstream GitHub returned HTTP ${response.status}. You may retry once or fallback to local issue tracker.`,
  };
}

const rawJson = (await response.json()) as RawGitHubIssueResponse;

const cleanLabels = Array.isArray(rawJson.labels)
  ? rawJson.labels
      .map((lbl) => (typeof lbl === 'string' ? lbl : lbl.name ?? ''))
      .filter(Boolean)
  : [];

const shapedDto: SanitizedGitHubIssueDTO = {
  repository,
  issueNumber: rawJson.number,
  title: rawJson.title,
  state: rawJson.state,
  author: rawJson.user?.login ?? 'unknown',
  labels: cleanLabels.slice(0, 8),
  createdAt: rawJson.created_at,
  commentsCount: rawJson.comments,
  bodyExcerpt: truncateCleanText(rawJson.body, 600),
  htmlUrl: rawJson.html_url,
};

return {
  ok: true,
  toolName: 'inspectLiveGitHubIssue',
  retryable: false,
  durationMs: Date.now() - startTime,
  data: shapedDto,
};

} catch (err) { const isTimeout = err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError');

return {
  ok: false,
  toolName: 'inspectLiveGitHubIssue',
  retryable: false,
  durationMs: Date.now() - startTime,
  data: null,
  agentInstruction: isTimeout
    ? 'GitHub API timed out after 5000ms. Do not retry this tool in the current turn.'
    : 'Network error while reaching GitHub API. Proceed with available context.',
};

} } `

উপরের কোডে তিনটি প্রোডাকশন কৌশল বিশেষভাবে লক্ষ্য করুন:

  • রিপোজিটরি Allowlist (ALLOWED_REPOSITORIES): মডেল চাইলে যেকোনো উল্টোপাল্টা URL বা প্রাইভেট পাথ পাঠাতে পারবে না; শুধুমাত্র আমাদের নির্ধারিত রিপোজিটরিগুলোর মধ্যেই সার্চ সীমাবদ্ধ থাকবে।
  • Next.js Data Cache (next: { revalidate: 120 }): একই ইস্যু নিয়ে যদি ২ মিনিটের মধ্যে একাধিক প্রশ্ন আসে, তবে Next.js বারবার গিটহাব API কল না করে ক্যাশ থেকে ২ মিলিসেকেন্ডে উত্তর দেবে—যাতে গিটহাবের রেট-লিমিট বাঁচে।
  • truncateCleanText() ফাংশন: গিটহাবের ইস্যু বডিতে যদি ১০,০০০ লাইনের এরর লগও থাকে, আমাদের ফাংশনটি তার মূল প্রথম ৬০০ ক্যারেক্টার রেখে বাকিটা কেটে দেবে, ফলে আপনার OpenAI বা Claude-এর টোকেন খরচ একদম নিয়ন্ত্রণে থাকবে।

ধাপ ২: কাস্টম টুলটিকে Vercel AI SDK এবং LangChain.js—উভয় এজেন্টে যুক্ত করা

যেহেতু আমাদের মূল executeInspectGitHubIssue ফাংশনটি সম্পূর্ণ স্বতন্ত্র, তাই মাত্র কয়েক লাইন কোড লিখে আমরা এটিকে ১৭তম পর্বের স্ট্রিমিং এজেন্ট এবং ১৮তম পর্বের LangChain এজেন্টের সাথে যুক্ত করে ফেলতে পারি।

প্রথমে src/lib/agent/vercel-tools.ts ফাইলে কাস্টম টুলটি যোগ করুন:

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

export const devPulseTools = { searchKnowledgeBase: tool({ description: 'Search internal technical documentation and architecture guides by semantic query.', parameters: searchDocsSchema, execute: async (args) => executeAgentTool('searchKnowledgeBase', args), }),

checkIssueTracker: tool({ description: 'Query internal project issue tracker for open or closed bug reports matching a keyword.', parameters: checkIssuesSchema, execute: async (args) => executeAgentTool('checkIssueTracker', args), }),

inspectLiveGitHubIssue: tool({ description: 'Fetch live metadata, status, labels, and truncated body for a specific numeric issue ID in an allowlisted upstream GitHub repository (vercel/next.js, vercel/ai, or langchain-ai/langchainjs).', parameters: inspectGitHubIssueSchema, execute: async (args) => executeInspectGitHubIssue(args), }),

draftIssueTicket: tool({ description: 'Create a structured bug ticket draft requiring Level 2 human confirmation.', parameters: draftTicketSchema, execute: async (args) => executeAgentTool('draftIssueTicket', args), }), }; `

একইভাবে আমাদের মাল্টি-স্টেপ LangChain এজেন্টের জন্য src/lib/agent/langchain-tools.ts ফাইলেও এটি যুক্ত করে দিন:

`typescript // src/lib/agent/langchain-tools.ts (কাস্টম গিটহাব টুল যুক্ত করা) import { tool } from '@langchain/core/tools'; import { inspectGitHubIssueSchema, executeInspectGitHubIssue, } from './custom-github-tool';

export const inspectGitHubIssueLangChainTool = tool( async (input) => { const result = await executeInspectGitHubIssue(input); return JSON.stringify(result); }, { name: 'inspectLiveGitHubIssue', description: 'Fetch live issue status, labels, and truncated description from an allowlisted upstream GitHub repository by numeric issue ID.', schema: inspectGitHubIssueSchema, } ); `

ধাপ ৩: React UI-তে কাস্টম টুলের জন্য ডেডিকেটেড কার্ড রেন্ডার করা

১৭তম পর্বে আমরা সব টুলের জন্য একই রকম সাধারণ বক্স দেখিয়েছিলাম। কিন্তু কাস্টম টুল থেকে যখন আমরা পরিচ্ছন্ন SanitizedGitHubIssueDTO ডেটা পাচ্ছি, তখন ফ্রন্টএন্ডে সেই ইস্যুর টাইটেল, স্ট্যাটাস ব্যাজ (open/closed) এবং লেবেলগুলো সুন্দর একটি কার্ডে দেখাতে পারি।

চলুন src/components/GitHubToolCard.tsx নামে একটি কম্পোনেন্ট তৈরি করি, যা সরাসরি আমাদের চ্যাট ইন্টারফেসে ব্যবহৃত হবে:

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

import { type CustomToolEnvelope, type SanitizedGitHubIssueDTO, } from '@/lib/agent/custom-github-tool';

interface GitHubToolCardProps { state: 'call' | 'partial-call' | 'result'; args: unknown; result?: CustomToolEnvelope; }

export default function GitHubToolCard({ state, args, result, }: GitHubToolCardProps) { if (state !== 'result' || !result) { return ( ⚡ Probing Live GitHub API... {JSON.stringify(args)} ); }

if (!result.ok || !result.data) { return ( ⚠ Custom Tool Notice ({result.durationMs}ms) {result.agentInstruction} ); }

const issue = result.data;

return ( {issue.repository} #{issue.issueNumber} <span className={px-2 py-0.5 rounded uppercase text-[10px] font-bold ${ issue.state === 'open' ? 'bg-emerald-950 text-emerald-300 border border-emerald-700' : 'bg-purple-950 text-purple-300 border border-purple-700' }} > {issue.state} • {result.durationMs}ms

  <div className='text-sm font-medium text-slate-100'>{issue.title}</div>

  {issue.labels.length > 0 && (
    <div className='flex flex-wrap gap-1.5'>
      {issue.labels.map((label) => (
        <span
          key={label}
          className='px-2 py-0.5 rounded bg-slate-900 border border-slate-700 text-[10px] text-slate-300'
        >
          {label}
        </span>
      ))}
    </div>
  )}

  <p className='text-slate-400 line-clamp-3 whitespace-pre-wrap'>
    {issue.bodyExcerpt}
  </p>
</div>

); } `

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

আমার নিজের প্রজেক্টের অভিজ্ঞতা: GitHub API-এর বিশাল JSON যখন এজেন্টের ১৪,০০০ টোকেন গিলে ফেলেছিল!

আমি যখন আমার GitHub Issues Tracker ওয়েব অ্যাপটি তৈরি করেছিলাম এবং পরবর্তীতে Movie Explorer প্রজেক্টে এক্সটার্নাল TVMaze API নিয়ে কাজ করছিলাম, তখন খেয়াল করেছিলাম পাবলিক REST API-গুলো একটা সিঙ্গেল অবজেক্টের ভেতরেও কত বিপুল পরিমাণ মেটাডেটা পাঠায়। সাধারণ ফ্রন্টএন্ড অ্যাপে গিটহাব API যদি ৪৫ কিলোবাইটের একটা JSON পাঠায় যার ভেতরে ইউজারের avatar_url, followers_url, subscriptions_url, node_id সহ ৭০টি প্রপার্টি থাকে, তাতে ব্রাউজারের কোনো সমস্যাই হয় না—আমরা শুধু issue.title আর issue.state রিড করে মডালে দেখিয়ে দিই।

কিন্তু আমি যখন প্রথমবার সেই একই গিটহাব ফেচ ফাংশনকে কোনো DTO ফিল্টার ছাড়া সরাসরি আমার এজেন্টের কাস্টম টুল হিসেবে যুক্ত করলাম, তখন এক ভয়াবহ কান্ড ঘটল! আমি এজেন্টকে বললাম ৩টি ইস্যুর তুলনা করে রিপোর্ট দিতে। এজেন্ট ৩ বার টুলটি কল করল এবং গিটহাবের র-JSON সরাসরি মডেলের প্রম্পটে ঢুকে গেল। মাত্র একটি প্রশ্নের উত্তরেই ১৪,০০০-এর বেশি ইনপুট টোকেন খরচ হয়ে গেল—যার ৯০ শতাংশই ছিল অকেজো ইউআরএল, ইমোজি রিঅ্যাকশন অবজেক্ট আর বিশাল স্ট্যাক ট্রেস! তার ওপর রেসপন্স আসতে সময় লাগল ১১ সেকেন্ডের বেশি।

সেদিনই আমি বুঝতে পারি: মানুষের ব্রাউজারের জন্য বানানো API রেসপন্স আর LLM-এর জন্য বানানো টুল আউটপুট এক জিনিস নয়। এরপর যখনই আমি SanitizedGitHubIssueDTO বসিয়ে বাড়তি মেটাডেটা ছেঁটে ফেললাম, bodyExcerpt ৬০০ ক্যারেক্টারে বেঁধে দিলাম এবং ৫ সেকেন্ডের AbortSignal.timeout(5000) যোগ করলাম—তখন প্রতি টুল কলের সাইজ ৪,৫০০ টোকেন থেকে কমে মাত্র ১৮০ টোকেনে নেমে এলো এবং এজেন্টের গতি প্রায় তিন গুণ বেড়ে গেল।

নিজের Custom Tool লেখার সময় যে ৪টি ভুল এড়িয়ে চলবেন

পরের পর্বে (পর্ব ২০) এজেন্টের Thinking/Loading UI স্টেট নিয়ে কাজ করার আগে নিশ্চিত হয়ে নিন আপনার কাস্টম টুলে যেন এই ৪টি ভুল না থাকে:

১. এক্সটার্নাল API-এর Raw JSON সরাসরি রিটার্ন করা

কখনোই কোনো থার্ড-পার্টি API কল করে সরাসরি return await response.json() লিখে দেবেন না। এতে অপ্রয়োজনীয় ফিল্ড গিয়ে মডেলের মনোযোগ নষ্ট করে এবং টোকেন বিল বাড়িয়ে দেয়। সবসময় আমাদের SanitizedGitHubIssueDTO-এর মতো একটি পরিষ্কার অবজেক্ট তৈরি করে শুধু দরকারি ফিল্ডগুলোই পাঠাবেন।

২. টুল প্যারামিটারে ওপেন URL বা ফাইল পাথ গ্রহণ করা

টুলের স্কিমায় কখনোই endpointUrl: z.string() রাখবেন না। এতে প্রম্পট ইনজেকশনের মাধ্যমে যে কেউ আপনার সার্ভার থেকে ইন্টারনাল নেটওয়ার্ক বা লোকালহোস্টে রিকোয়েস্ট পাঠাতে পারে। বেস URL সবসময় সার্ভারে হার্ডকোড করবেন এবং ইনপুটে শুধু z.enum() বা ভ্যালিডেটেড আইডি নেবেন।

৩. AbortSignal.timeout() ছাড়া নেটওয়ার্ক কল করা

Node.js-এর ডিফল্ট fetch কোনো সাড়া না পেলে অনেক সময় কয়েক মিনিট পর্যন্ত ঝুলে থাকতে পারে। কিন্তু আপনার সার্ভারলেস ফাংশনের আয়ু থাকে মাত্র ১০ থেকে ৬০ সেকেন্ড। তাই প্রতিটি এক্সটার্নাল কলে signal: AbortSignal.timeout(5000) ব্যবহার করবেন, যাতে বাইরের সার্ভার ডাউন থাকলে আপনার এজেন্ট ৫ সেকেন্ডের মধ্যেই তা বুঝে বিকল্প উত্তর দিতে পারে।

৪. 404 বা ভ্যালিডেশন এররে সরাসরি Exception Throw করা

ইউজারের দেওয়া কোনো ইস্যু নম্বর যদি গিটহাবে না থাকে (404 Not Found), তখন throw new Error() করলে এজেন্ট বুঝতে পারে না কেন টুলটি ফেইল করেছে এবং সে বারবার একই ভুল নম্বর দিয়ে টুলটি কল করতে থাকে। তাই সবসময় retryable: false এবং স্পষ্ট agentInstruction মেসেজ রিটার্ন করবেন।

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

একটি সিঙ্গেল AI Agent-এর সাথে সর্বোচ্চ কয়টি Custom Tool যুক্ত করা উচিত?

বাস্তব প্রোডাকশনে Claude 3.5 Sonnet বা GPT-4o ব্যবহার করলে একটি এজেন্টে ৪ থেকে ১২টি সুনির্দিষ্ট টুল সবচেয়ে নির্ভুলভাবে কাজ করে—যদি প্রতিটি টুলের বর্ণনা ও স্কিমা আলাদা হয়। আপনার প্রজেক্টে যদি ১৫-২০টির বেশি টুল প্রয়োজন হয়, তবে সব টুল একটি এজেন্টের ঘাড়ে না চাপিয়ে কাজ অনুযায়ী একাধিক ছোট এজেন্টে (Multi-Agent System) ভাগ করে দেওয়া উত্তম।

কাস্টম টুলের ভেতরে কি লগইন করা ইউজারের সেশন বা কুকি (Cookies) নিরাপদে ব্যবহার করা যাবে?

হ্যাঁ, অবশ্যই যাবে। তবে কখনোই ইউজারের userId বা অ্যাক্সেস টোকেন LLM-এর টুল আর্গুমেন্টের (parameters) মাধ্যমে পাঠাতে বলবেন না—কারণ প্রম্পট ইনজেকশনের মাধ্যমে মডেল অন্য ইউজারের আইডি বসিয়ে দিতে পারে। সবসময় টুলের সার্ভার-সাইড execute ফাংশনের ভেতরে সরাসরি Next.js-এর cookies() বা সেশন হেল্পার থেকে ইউজারের পরিচয় ভেরিফাই করবেন।

টাইমআউটের জন্য setTimeout বা Promise.race ব্যবহার না করে কেন AbortSignal.timeout(5000) ব্যবহার করা হয়েছে?

আধুনিক Node.js (v18+) এবং Edge রানটাইমে AbortSignal.timeout() বিল্ট-ইন থাকে। Promise.race ব্যবহার করলে নির্দিষ্ট সময় পর জাভাস্ক্রিপ্ট পরের লাইনে চলে গেলেও ব্যাকগ্রাউন্ডে আসল HTTP কানেকশনটি কিন্তু চালু থেকেই মেমোরি নষ্ট করতে থাকে। কিন্তু AbortSignal.timeout(5000) সরাসরি নেটওয়ার্ক সকেটটি বাতিল (abort) করে দেয়, যা সার্ভারের রিসোর্স সাথে সাথে মুক্ত করে।

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

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

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