دانيلا (⁦Dayfing⁩)
العودة إلى المقالات
2,412 كلمة14 د

AI SDK 6 وTypeScript: حلقة الأدوات، والمخرجات المهيكلة، والموافقة البشرية

يضع AI SDK 6 لتطبيق TypeScript حدّاً واضحاً بين قرارات النموذج وصلاحيات التطبيق. يستطيع النموذج اختيار أداة ذات أنواع، واستلام نتيجتها، ومتابعة المحادثة. لكن كودك يظل مسؤولاً عن التحقق من المدخلات، وتطبيق الصلاحيات، وتحديد ما إذا كان الأثر الجانبي يحتاج إلى شخص، وتسجيل ما حدث. هذا الفصل هو مبدأ التصميم المفيد للوكيل في الإنتاج. تستخدم هذه المقالة واجهات AI SDK 6 المستقرة، لا واجهة الموافقات الأحدث التي ظهرت في AI SDK 7. ثبّت ai@6 مع حزمة موفّر متوافقة مع فرع 6.x، مثل @ai-sdk/openai@3، وثبّت الإصدارات في ملف القفل.

الحلقة التي تبنيها

يمكن لاستدعاء واحد للنموذج أن يعيد نصاً أو استدعاء أداة. تضيف حلقة الأدوات استدعاءً جديداً للنموذج بعد انتهاء تنفيذ الأداة، لكي يفسّر النموذج النتيجة ويقرر ما إذا كانت هناك أداة أخرى مطلوبة. كل توليد للنموذج يُعد خطوة. تنتهي الحلقة عندما يتوقف النموذج عن طلب الأدوات، أو عندما لا تملك الأداة المستدعاة دالة execute، أو عند الحاجة إلى موافقة، أو عند تحقق شرط stopWhen. تعرض النتيجة النص النهائي، واستدعاءات الأدوات ونتائجها، ورسائل الاستجابة، والاستخدام، والمصفوفة steps. لذلك تظل الحلقة قابلة للفحص بدلاً من أن تكون سلوكاً غامضاً.

يغلّف ToolLoopAgent هذا السلوك في كائن قابل لإعادة الاستخدام. يتطلب المُنشئ LanguageModel ويقبل instructions وtools وstopWhen وoutput وprepareStep وmaxRetries والمهلات الزمنية وعمليات الاستدعاء الراجعة. في AI SDK 6 يكون شرط الإيقاف الافتراضي هو stepCountIs(20). عيّن قيمة أصغر لسير عمل محدود. هذا الحد يتحكم في التكلفة والتوافر، لكنه لا يحل محل نظام الصلاحيات.

ToolLoopAgent مع أنواع واضحة

يستنتج المساعد tool نوع معاملات execute من inputSchema. تُرسل المخططة إلى الموفّر وتُستخدم للتحقق من المعاملات التي يرسلها النموذج. لكن ذلك لا يجعل النموذج موثوقاً أمنياً. فقد تحتوي بنية صحيحة على حساب مستخدم آخر، أو مسار محظور، أو عنوان URL خطير، أو مبلغ غير مسموح به.

npm install [email protected] @ai-sdk/[email protected] @ai-sdk/[email protected] zod
import { openai } from '@ai-sdk/openai';
import { ToolLoopAgent, stepCountIs, tool } from 'ai';
import { z } from 'zod';

const getWeather = tool({
  description: 'Get the current weather for a city',
  inputSchema: z.object({
    city: z.string().min(1).max(80),
  }),
  execute: async ({ city }) => ({
    city,
    temperatureC: 18,
    condition: 'cloudy',
  }),
});

const agent = new ToolLoopAgent({
  model: openai('gpt-4o-mini'),
  instructions: 'Use getWeather for current weather. Never invent a tool result.',
  tools: { getWeather },
  stopWhen: stepCountIs(4),
  maxRetries: 2,
});

const result = await agent.generate({
  prompt: 'What is the weather in Kyiv?',
});

console.log(result.text);
console.log(result.steps.length);

ينبغي أن تنفذ الأداة عملية واحدة وأن تعيد نتيجة صغيرة قابلة للتسلسل. اجعل الوصف دقيقاً بشأن الوحدات، والصلاحيات، وحداثة البيانات، وسلوك الفشل. استخدم strict: true عندما يدعم الموفّر استدعاءات الأدوات الصارمة وتكون المخططة متوافقة. يزيد الوضع الصارم الاعتمادية لكنه لا يمثل تفويضاً. تسمح toolChoice: 'auto' للنموذج بالاختيار، بينما تفرض required استخدام أداة متاحة، وتعطّل none الأدوات، ويختار { type: 'tool', toolName: 'getWeather' } أداة مسماة. ويمكن لـ activeTools تضييق الأدوات المتاحة في خطوة بعينها.

الاستدعاءات الصريحة وحدود الحلقة

استخدم generateText لاستدعاء واحد أو عندما تحتاج إلى التحكم المباشر في سجل الرسائل. أضف stopWhen إذا كان يجب إرسال نتائج الأدوات مرة أخرى إلى النموذج. بدون هذا الخيار ينفذ generateText توليداً واحداً ويعيد استدعاء الأداة من دون متابعة تلقائية.

import { openai } from '@ai-sdk/openai';
import { generateText, stepCountIs, tool } from 'ai';
import { z } from 'zod';

const lookupOrder = tool({
  description: 'Look up an order by its public order number',
  inputSchema: z.object({ orderNumber: z.string().regex(/^ORD-[0-9]{6}$/) }),
  execute: async ({ orderNumber }) => ({
    orderNumber,
    status: 'shipped',
  }),
});

const result = await generateText({
  model: openai('gpt-4o-mini'),
  tools: { lookupOrder },
  stopWhen: stepCountIs(3),
  prompt: 'Check order ORD-104209 and explain its status.',
});

console.log(result.text);
console.log(result.steps.flatMap(step => step.toolCalls));

الشروط المضمّنة هي stepCountIs(count) وhasToolCall(toolName) وisLoopFinished(). توقف المصفوفة الحلقة عند تحقق أي شرط. لا يملك isLoopFinished() حداً أقصى، لذلك استخدمه فقط مع ميزانية خارجية، ومهلة، وإشارة إلغاء، وحصة الموفّر. يستقبل StopCondition مخصّص القيمة { steps } ويمكنه التوقف عند حالة أعمال أو ميزانية رموز مقاسة. يجري تقييم الشروط عندما تحتوي الخطوة الأخيرة على نتائج أدوات. وعند جمع استدعاء الأدوات مع إخراج مهيكل، احجز خطوة إضافية لتوليد الإخراج.

تعيد maxRetries استدعاءات النموذج الفاشلة. لكنها لا تجعل execute عملية idempotent. يجب أن تحمل الأداة التي ترسل بريداً أو تخصم من بطاقة أو تنشئ سجلاً مفتاحاً لمنع التكرار وأن تتحقق من التكرار بنفسها. في عميل MCP البعيد تكون إعادة المحاولة معطّلة افتراضياً وتُضبط بواسطة maxRetries في createMCPClient. أعد المحاولة لأخطاء الشبكة أو تحديد المعدل فقط، ولا تعِد عمياناً إرسال طلب tools/call غير قابل للتكرار.

مخرجات مهيكلة يمكن لـ TypeScript استخدامها

يعلن AI SDK 6 أن generateObject وstreamObject قديمان، ويوصي بـ generateText وstreamText مع خيار output. يقبل Output.object مخطط Zod أو Valibot أو JSON Schema. تُحلل الاستجابة الكاملة وتُتحقق قبل أن تصبح result.output جاهزة. تصلح الكائنات الجزئية لواجهة المستخدم، لكنها ليست قرار التحقق النهائي.

import { openai } from '@ai-sdk/openai';
import { generateText, Output } from 'ai';
import { z } from 'zod';

const reportSchema = z.object({
  sentiment: z.enum(['positive', 'neutral', 'negative']),
  score: z.number().min(0).max(1),
  keyPoints: z.array(z.string().min(1)).max(8),
});

const { output } = await generateText({
  model: openai('gpt-4o-mini'),
  output: Output.object({
    schema: reportSchema,
    name: 'review_report',
    description: 'A concise, evidence-based review report',
  }),
  prompt: 'Analyze: The battery lasts all day, but the charger is bulky.',
});

console.log(output.sentiment, output.score, output.keyPoints);

يحدث التحقق على مستويين. قد يفرض الموفّر تنسيق الاستجابة، ثم يحلل AI SDK JSON العائد ويتحقق منه بالمخطط. ولا يزال على التطبيق التحقق من قواعد الأعمال، مثل انتماء المعرّف إلى المستأجر الحالي أو صلاحية درجة معينة لتفعيل استرداد المال. اجعل المخططات ضيقة ومغلقة. ضع حدوداً للسلاسل والمصفوفات والأرقام، واستخدم القيم المحصورة. ارفض الأوامر غير المعروفة بدلاً من تمرير JSON عشوائي إلى محول ذي امتيازات.

مع الأدوات يستطيع النموذج تنفيذ بحث أولاً ثم إنتاج التقرير. اضبط stopWhen: stepCountIs(4) أو ميزانية صريحة أخرى، لأن خطوة الإخراج المهيكل جزء من التدفق متعدد الخطوات نفسه. عند فشل التحليل، التقط خطأ SDK، واحتفظ بمعرّف الارتباط وبيانات الموفّر، وأعد استجابة آمنة قابلة لإعادة المحاولة. لا تطلب من نموذج آخر تفويض مخرجات تالفة.

بث النص والأحداث

يوفر streamText تدفقات غير متزامنة للنص وللإخراج المهيكل. يعيد ToolLoopAgent.stream قيمة StreamTextResult بعد إعداد الاستدعاء، لذلك انتظرها قبل قراءة textStream. يحتوي تدفق النص على النص الناتج، بينما تكشف النتيجة الكاملة وعمليات الاستدعاء الراجعة استدعاءات الأدوات ونتائجها.

import { openai } from '@ai-sdk/openai';
import { ToolLoopAgent } from 'ai';

const agent = new ToolLoopAgent({
  model: openai('gpt-4o-mini'),
  instructions: 'Answer clearly and briefly.',
});

const stream = await agent.stream({
  prompt: 'Explain why typed tool inputs matter.',
});

for await (const chunk of stream.textStream) {
  process.stdout.write(chunk);
}

لإخراج مهيكل متدفق استخدم streamText مع Output.object واقرأ partialOutputStream. تسمح onStepFinish بحفظ خطوة مكتملة، وتسجيل الاستخدام، وعرض حدث تدقيق. قد ينتهي التدفق بجزء tool-approval-request أو tool-error أو tool-output-denied، وليس بالنص فقط. قد يخفي العميل الذي يعرض النص وحده إجراءً ينتظر شخصاً.

موافقة الإنسان على الآثار الجانبية

في AI SDK 6 اضبط needsApproval للأداة على true أو على دالة غير متزامنة تعتمد على إدخال تم التحقق منه. يعيد الاستدعاء الأول لـ generateText أو streamText جزءاً من النوع tool-approval-request. لا يتوقف الخادم منتظراً المتصفح. خزّن رسائل الاستجابة، واعرض اسم الأداة والمعاملات بدقة، وأضف tool-approval-response إلى رسالة tool جديدة، ثم استدع النموذج مرة أخرى.

يستخدم مثال Node الكامل الآتي سؤالاً في الطرفية كحد بشري. أما تطبيق الويب فيخزّن messages وapprovalId وtoolCallId وهوية المراجع المصادق عليه ووقت الانتهاء في مخزن على الخادم.

import { createInterface } from 'node:readline/promises';
import { openai } from '@ai-sdk/openai';
import {
  generateText,
  tool,
  type ModelMessage,
  type ToolApprovalResponse,
} from 'ai';
import { z } from 'zod';

const records = new Map([
  ['draft-17', { ownerId: 'user-7', text: 'Quarterly notes' }],
]);

const deleteDraft = tool({
  description: 'Delete one draft owned by the authenticated user',
  inputSchema: z.object({ draftId: z.string().regex(/^draft-[0-9]+$/) }),
  needsApproval: true,
  execute: async ({ draftId }) => {
    if (!records.delete(draftId)) {
      throw new Error('Draft was not found');
    }
    return { draftId, deleted: true };
  },
});

async function requestHumanApproval(toolName: string, input: unknown) {
  const terminal = createInterface({ input: process.stdin, output: process.stdout });
  const answer = await terminal.question(`Approve ${toolName} ${JSON.stringify(input)}? [y/N] `);
  terminal.close();
  return answer.trim().toLowerCase() === 'y';
}

const messages: ModelMessage[] = [
  { role: 'user', content: 'Delete draft-17.' },
];

const first = await generateText({
  model: openai('gpt-4o-mini'),
  system: 'If an action is denied, do not retry it.',
  tools: { deleteDraft },
  messages,
});

messages.push(...first.response.messages);
const approvals: ToolApprovalResponse[] = [];

for (const part of first.content) {
  if (part.type === 'tool-approval-request') {
    approvals.push({
      type: 'tool-approval-response',
      approvalId: part.approvalId,
      approved: await requestHumanApproval(part.toolCall.toolName, part.toolCall.input),
      reason: 'Decision made by the authenticated reviewer',
    });
  }
}

if (approvals.length > 0) {
  messages.push({ role: 'tool', content: approvals });
  const final = await generateText({
    model: openai('gpt-4o-mini'),
    system: 'If an action is denied, do not retry it.',
    tools: { deleteDraft },
    messages,
  });
  console.log(final.text);
} else {
  console.log(first.text);
}

الموافقة لا تحل محل التفويض. قبل الأثر الجانبي مباشرة أعد التحقق من المستخدم، والمستأجر، والهدف، والسياسة، وإصدار المورد. اربط الموافقة المخزنة بـ approvalId وtoolCallId واسم الأداة وتجزئة الإدخال الذي تم التحقق منه. ضع لها وقت انتهاء، واسمح باستخدام واحد، وارفض الإدخال الذي تغير. يجب أن يتحول الرفض إلى نتيجة أداة واضحة وألا يعاد تلقائياً. في الإجراء عالي الأثر اعرض الهدف والهوية والمعاملات والبيانات الخارجة من النظام والأثر المتوقع، لا ملخص النموذج فقط.

الأدوات الخطرة وحدود الأمان

لا تعرض للنموذج shell عاماً، أو عميلاً HTTP غير مقيّد، أو مسار ملف اعتباطياً، أو اتصالاً مباشراً بقاعدة البيانات. فضّل عمليات ضيقة مثل deleteDraft وcreateCalendarEvent وlookupOrder. ضع التفويض والقوائم المسموح بها داخل execute أو خدمة سياسات. تحقق من مخطط URL والمضيف والمنفذ والعنوان المحلّل وإعادة التوجيه. حلّ المسارات داخل جذر مسموح وراعِ الروابط الرمزية. تحقق من ملكية المستأجر في استعلام قاعدة البيانات، لا في prompt وحده.

اعتبر أوصاف الأدوات والوثائق المسترجعة والذاكرة ونتائج الأدوات محتوى غير موثوق. قد يطلب حقن prompt من النموذج كشف سر، أو استدعاء أداة لا علاقة لها بالمهمة، أو إرسال البيانات إلى موقع يسيطر عليه مهاجم. النموذج ليس حدّاً أمنياً. تشرح مقالة حقن prompt وأمان MCP ضوابط أقل الصلاحيات، ومصدر البيانات، والخروج إلى الشبكة، والعزل، والحدود، والموافقات. ولتقسيم المخطط والمنفّذ والسياسة والتخزين راجع بنية وكيل الذكاء الاصطناعي في الإنتاج.

لا تضع مفاتيح API في prompts أو نتائج الأدوات. أخفِ الرموز في السجلات والتليمترية. لا تسجل المدخل الكامل إذا كان قد يحتوي بيانات شخصية. مرّر معرّف الطلب عبر استدعاءات النموذج والأدوات. حد حجم النتيجة قبل إدخالها إلى prompt التالي. استخدم بيانات اعتماد منفصلة للقراءة والمسودة والتنفيذ. شغّل وكيل المتصفح أو الملفات في worker معزول بلا أسرار غير مرتبطة وبشبكة مغلقة افتراضياً.

MCP مع الحفاظ على سلامة الأنواع

تكيّف حزمة @ai-sdk/mcp أدوات خادم MCP لتصبح أدوات AI SDK. يوصي AI SDK 6 باستخدام HTTP في الإنتاج وقصر stdio على الخوادم المحلية. عرّف المخططات صراحة عندما لا تسيطر على الخادم أو تكون الأداة حساسة. يحافظ ذلك على مجموعة ضيقة ويعطي TypeScript أنواع إدخال مفيدة. استخدم redirect: 'error' عندما لا تسمح السياسة بإعادة التوجيه، وتحقق من أصول خوادم تفويض OAuth، وأغلق العميل في finally أو onFinish.

import { openai } from '@ai-sdk/openai';
import { createMCPClient } from '@ai-sdk/mcp';
import { ToolLoopAgent, stepCountIs } from 'ai';
import { z } from 'zod';

const mcpUrl = process.env.MCP_URL;
if (!mcpUrl) throw new Error('MCP_URL is required');

const mcpClient = await createMCPClient({
  transport: {
    type: 'http',
    url: mcpUrl,
    headers: process.env.MCP_TOKEN
      ? { Authorization: `Bearer ${process.env.MCP_TOKEN}` }
      : undefined,
    redirect: 'error',
  },
  maxRetries: 2,
});

try {
  const tools = await mcpClient.tools({
    schemas: {
      'get-customer-note': {
        inputSchema: z.object({ customerId: z.string().uuid() }),
      },
    },
  });
  const agent = new ToolLoopAgent({
    model: openai('gpt-4o-mini'),
    instructions: 'Use only the customer note tool for this request.',
    tools,
    stopWhen: stepCountIs(3),
  });
  const result = await agent.generate({ prompt: 'Read the requested customer note.' });
  console.log(result.text);
} finally {
  await mcpClient.close();
}

يتميز الاكتشاف التلقائي بواسطة mcpClient.tools() بالسهولة، لكنه يعرض كل أداة يعلنها الخادم ولا يوفر أنواع إدخال وقت الترجمة. أما المخططات الصريحة فتحمّل الأدوات المسماة فقط. أعد المحاولة للأخطاء الشبكية المؤقتة وحدها. يجب تمرير أخطاء تطبيق MCP والاستجابات الناجحة التي تحمل isError: true دون إعادة أثر جانبي. يظل OAuth بحاجة إلى فحص الجمهور، وPKCE، ورموز قصيرة العمر، وعناوين redirect دقيقة، وقائمة مسموحة لخوادم التفويض المكتشفة. يشرح خادم MCP TypeScript مع OAuth جانب الخادم.

الأخطاء والاختبارات والتشغيل

ضع التوليد خلف حد يميّز بين الإدخال غير الصالح، وفشل الموفّر، والمهلة، والإلغاء، والمخرج المشوه، وفشل الأداة. يحول AI SDK استثناء execute إلى جزء tool-error لكي يرى النموذج متعدد الخطوات الفشل. أعد رسالة منقّاة من الأداة أو حوّل النتيجة قبل إرسالها إلى النموذج. لا تكشف stack trace أو الأسرار أو SQL أو المسارات المحلية أو أجسام استجابات upstream. حد كل طلب بواسطة abortSignal وtimeout. سجّل finishReason وusage وtotalUsage وعدد الخطوات وقرار سياسة الأداة.

يضم AI SDK 6 نماذج mock حتمية في ai/test. يسمح MockLanguageModelV3 بإرجاع استدعاء أداة في التوليد الأول ونص في التوليد الثاني. يثبت الاختبار التالي أن الحلقة تنفذ الأداة مرة واحدة وتحصل على النتيجة النهائية دون الاتصال بموفّر.

import assert from 'node:assert/strict';
import { generateText, stepCountIs, tool } from 'ai';
import { MockLanguageModelV3 } from 'ai/test';
import { z } from 'zod';

const usage = {
  inputTokens: { total: 1, noCache: 1, cacheRead: undefined, cacheWrite: undefined },
  outputTokens: { total: 1, text: 1, reasoning: undefined },
};

let calls = 0;
let executions = 0;
const model = new MockLanguageModelV3({
  doGenerate: async () => {
    calls += 1;
    if (calls === 1) {
      return {
        content: [{ type: 'tool-call', toolCallId: 'call-1', toolName: 'add', input: '{"a":2,"b":3}' }],
        finishReason: { unified: 'tool-calls', raw: undefined },
        usage,
        warnings: [],
      };
    }
    return {
      content: [{ type: 'text', text: 'The result is 5.' }],
      finishReason: { unified: 'stop', raw: undefined },
      usage,
      warnings: [],
    };
  },
});

const add = tool({
  description: 'Add two numbers',
  inputSchema: z.object({ a: z.number(), b: z.number() }),
  execute: async ({ a, b }) => {
    executions += 1;
    return a + b;
  },
});

const result = await generateText({
  model,
  tools: { add },
  stopWhen: stepCountIs(2),
  prompt: 'Add 2 and 3.',
});

assert.equal(result.text, 'The result is 5.');
assert.equal(calls, 2);
assert.equal(executions, 1);

أضف اختبارات للمخططات المرفوضة، وعدم تطابق المستأجر، واجتياز المسارات، وعناوين SSRF، ومفاتيح منع التكرار المكررة، وانتهاء الموافقة، والرفض، وقوائم أدوات MCP، والتحقق من المخرج. اختبر أجزاء الأداة والموافقة في التدفق، لا النص الظاهر فقط. شغّل حالات عدائية بوثائق تطلب من النموذج تجاهل المهمة أو تسريب السياق. يشرح دليل تقييم وكلاء الذكاء الاصطناعي مجموعات الانحدار، وتأكيدات استدعاء الأدوات، وقياس التكلفة والزمن.

ملاحظات الترحيل من AI SDK 5

استبدل Experimental_Agent بـ ToolLoopAgent. وتصبح إعدادته system باسم instructions. يتغير شرط الإيقاف الافتراضي من stepCountIs(1) إلى stepCountIs(20)، لذلك قد يؤدي التحديث إلى استدعاءات أكثر. ضع حداً صريحاً. استبدل generateObject وstreamObject بـ generateText وstreamText مع Output.object أو Output.array أو استراتيجية أخرى. يستخدم ناتج البث partialOutputStream.

يصبح CoreMessage هو ModelMessage، وتصبح convertToModelMessages غير متزامنة في AI SDK 6. ويصبح ToolCallOptions هو ToolExecutionOptions. استبدل mock من V2 بـ MockLanguageModelV3 وبقية نماذج V3 من ai/test. أزيل خيار الموفّر structuredOutputs من نماذج الدردشة لصالح strictJsonSchema. عند تنفيذ toModelOutput فكّك المعامل إلى { output } وفق توقيع v6. شغّل codemod الخاص بـ v6، ثم راجع يدوياً محولات الموفّرين، وتحويل الرسائل، وإعادة الموافقة، واختبارات البث. يستطيع codemod إعادة تسمية الرموز، لكنه لا يقرر ما إذا كانت القيمة الافتراضية الجديدة آمنة لسير عملك.

تعتمد واجهة API الدقيقة على patch المثبت والموفّر. قبل التحديث اقرأ مرجع ToolLoopAgent، ودليل استدعاء الأدوات، ودليل البيانات المهيكلة، ودليل MCP، ودليل الاختبار، ودليل الترحيل من AI SDK 5 إلى 6. اعتبر تحذير الموفّر الذي يذكر تجاهل خيار فشل اختبار.

المصادر

مقالات أخرى