• בלוג
  • כתיבת AI Agent שמשתמש בכלים עם Vercel AI SDK

כתיבת AI Agent שמשתמש בכלים עם Vercel AI SDK

04/05/2025

בפוסט אתמול הצגתי סוכן AI מאוד פשוט שרץ כל יום ומעצב אימייל עם סיכום מידע שעניין אותי מגיטהאב. בואו ניקח את הקוד צעד קטן קדימה ונתן לסוכן שלנו גישה לכלים כך שיוכל להפעיל קוד וכך לקבל החלטות בצורה יותר עצמאית.

הפעלת כלים מתוך קוד שניגש ל LLM זה בסך הכל בקשה ל LLM להדפיס קוד מיוחד כשהוא מרגיש שצריך להפעיל כלי, זיהוי הקוד הזה מתוך הסוכן שפנה ל LLM ואז הסוכן מפעיל את הכלי ופונה שוב ל LLM עם התוצאה להמשך עבודה. כתבתי בעבר איך לממש לבד מנגנון הפעלת כלים ב AI. היום לא אבנה את המנגנון מאפס אלא אשתמש בספריה קיימת להפעלת כלים של Vercel שנקראת פשוט AI SDK. הספריה של ורסל מתפתחת מהר וכוללת הרבה יכולות ולדעתי היא הדרך הקלה ביותר לכתוב סוכני AI בטייפסקריפט.

1. מה אנחנו בונים

הסוכן היום יעזור לכם ליצור נוכחות במדיה חברתית - הוא מזהה דיונים מעניינים ברדיט על תכנות ומציע רעיונות לתגובות שאנחנו יכולים לכתוב לדיונים אלה. בנוסף הוא מציע רעיונות לפוסטים שאפשר להוסיף ב subreddits שהוא סוקר.

בשביל לעבוד הסוכן ישתמש בשני כלים:

  1. כלי שמוציא את כל הדיונים מ Subreddit.

  2. כלי שמוציא את כל ה Comments על פוסט.

זכרו ש"שימוש בכלים" זה בסך הכל הפעלת פונקציה. אנחנו מעבירים את ה"כלי", כלומר את הפונקציה, כפרמטר לפונקציה של AI SDK, והפריימוורק פונה ל LLM, מפענח את התשובה, מזהה בקשה להפעלת כלים, מפעיל את הפונקציה ופונה שוב ל LLM להמשך עבודה. אנחנו רק צריכים להגביל את מספר הפעמים שהלולאה הזו תתבצע באמצעות עוד פרמטר שנעביר לפונקציה של AI SDK.

2. הקוד

זה קוד הסוכן, בדוגמה בתור סקריפט שרק מדפיס את התוצאה למסך:

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


interface RedditComment {
  author: string;
  body: string;
  score: number;
  created_utc: number;
  replies?: RedditComment[];
}

interface RedditPost {
  title: string;
  author: string;
  url: string;
  permalink: string;
  created_utc: number;
  num_comments: number;
  score: number;
}

async function getLatestDiscussions({subreddit, limit = 10}: {subreddit: string, limit?: number}): Promise<RedditPost[]> {
  const url = `https://www.reddit.com/r/${subreddit}/new.json?limit=${limit}`;

  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Error fetching subreddit: ${response.statusText}`);
    }

    const data = await response.json();

    const posts: RedditPost[] = data.data.children.map((child: any) => ({
      title: child.data.title,
      author: child.data.author,
      url: child.data.url,
      permalink: `https://www.reddit.com${child.data.permalink}`,
      created_utc: child.data.created_utc,
      num_comments: child.data.num_comments,
      score: child.data.score,
    }));

    return posts;
  } catch (error) {
    console.error('Failed to fetch discussions:', error);
    return [];
  }
}

function parseComment(comment: any): RedditComment {
  return {
    author: comment.data.author,
    body: comment.data.body,
    score: comment.data.score,
    created_utc: comment.data.created_utc,
    replies: comment.data.replies
      ? comment.data.replies.data.children
          .filter((child: any) => child.kind === "t1")
          .map(parseComment)
      : [],
  };
}

async function getPostComments({permalink}: {permalink: string}): Promise<RedditComment[]> {
  const url = `https://www.reddit.com${permalink}.json`;

  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Error fetching comments: ${response.statusText}`);
    }

    const data = await response.json();
    const comments = data[1].data.children;


    const parsedComments = comments
      .filter((item: any) => item.kind === "t1")
      .map(parseComment);

    return parsedComments;
  } catch (error) {
    console.error("Failed to fetch post comments:", error);
    return [];
  }
}




const tools = {
  latestDiscussions: tool({
    description: 'Get the latest reddit discussions in a given subreddit',
    parameters: z.object({
      subreddit: z.string().describe('The subreddit to get the latest discussions from'),
      limit: z.number().optional().default(10).describe('The max number of discussions to get'),
    }),
    execute: async ({ subreddit, limit }) => {
      const discussions = await getLatestDiscussions({ subreddit, limit });
      return discussions;
    },
  }),
  postComments: tool({
    description: 'Get the comments for a given reddit post',
    parameters: z.object({
      permalink: z.string().describe('The permalink of the reddit post'),
    }),
    execute: async ({ permalink }) => {
      const comments = await getPostComments({ permalink });
      return comments;
    },  
  }),
}


// Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts
if (import.meta.main) {
  const model = openai("gpt-4o-mini");

  const result = await generateText({
    model,
    tools,
    prompt: `
    Your are a community manager for a development company.
    Help the company create social media presence by participating in relevant discussions on reddit.

    1. Find relevant subreddits related to programming and development, that have active discussions. Start with subreddits: "programming", "learnprogramming", "webdev", "reactjs", "python"
    2. For each subreddit find the discussions and comments and provide a summary of the best discussions we can participate in.
    3. Suggest comments to add or posts to create.

    Print out a report as a JSON object listing:

    {
      "postsToCreate": [
        {
          "subreddit": "subreddit_name",
          "title": "post_title",
          "text": "post_text"
        }
      ],
      "commentsToAdd": [
        {
          "subreddit": "subreddit_name",
          "commentText": "comment_text",
          "postPermalink": "post_permalink"
      ]
    }
    `,
    maxSteps: 10,
  });
  const jsonMatch = result.text.match(/```json\s*([\s\S]*?)\s*```/);
  const data = JSON.parse(jsonMatch ? jsonMatch[1].trim() : '[]');

  console.log(data);
}

דגשים בקריאת הקוד:

  1. הפונקציה generateText של ורסל עדיין לא יודעת להחזיר אוביקט. יש להם פונקציה אחרת בשם generateObject אבל היא לא יודעת להפעיל כלים. אני מאמין שעוד כמה גירסאות זה יסתדר. בינתיים פירססתי את ה JSON בעצמי.

  2. קל לעבוד עם רדיט כי לא צריך API Key, אבל גם מול APIs יותר מסובכים אפשר להשתמש באותו מבנה.

  3. הסוד היחיד שהקוד צריך בשביל לעבוד הוא משתנה סביבה בשם OPENAI_API_KEY שמכיל את מפתח ה API כדי להתחבר ל Chat GPT. אין בעיה לשנות את המודל לספק אחר או למודל אחר של OpenAI.

3. רעיונות להרחבה

רוצים לקחת את הסוכן עוד כמה צעדים קדימה? הנה כמה רעיונות לתרגול:

  1. הוסיפו פונקציה שיודעת להוסיף Comment לדיון ברדיט ותנו אותה לסוכן בתור Tool נוסף. כעת במקום להחזיר רשימה של "הצעות" לתגובות הסוכן יוכל ממש להגיב בשמכם בדיונים.

  2. הפעילו את הסוכן בצורה מחזורית פעם או פעמיים ביום (אפשר להעלות אותו ל val.town).

  3. אם הסוכן כותב בעצמו תגובות יהיה מעניין לעקוב ולהבין מה הוא כתב. דרך קלה לעשות את זה היא להוסיף בכל Tool Call שכותב הערה גם עדכון של משתנה ששומר את כל ההערות שנכתבו בהפעלה זו. בסוף הריצה שלחו לעצמכם למייל את הרשימה כדי לוודא שהסוכן לא כותב שטויות.