• בלוג
  • קריאה מודרכת בלולאת הסוכן של פאי

קריאה מודרכת בלולאת הסוכן של פאי

06/05/2026

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

הריפו של פאי נמצא כאן:

https://github.com/badlogic/pi-mono/

ובשביל להבין איך עובדת לולאת סוכן נסתכל על הקובץ:

https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent-loop.ts

ונחפש את הפונקציה runLoop. זה המימוש כמו שהוא - סופר קריא ומתועד:

async function runLoop(
    currentContext: AgentContext,
    newMessages: AgentMessage[],
    config: AgentLoopConfig,
    signal: AbortSignal | undefined,
    emit: AgentEventSink,
    streamFn?: StreamFn,
): Promise<void> {
    let firstTurn = true;
    // Check for steering messages at start (user may have typed while waiting)
    let pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || [];

    // Outer loop: continues when queued follow-up messages arrive after agent would stop
    while (true) {
        let hasMoreToolCalls = true;

        // Inner loop: process tool calls and steering messages
        while (hasMoreToolCalls || pendingMessages.length > 0) {
            if (!firstTurn) {
                await emit({ type: "turn_start" });
            } else {
                firstTurn = false;
            }

            // Process pending messages (inject before next assistant response)
            if (pendingMessages.length > 0) {
                for (const message of pendingMessages) {
                    await emit({ type: "message_start", message });
                    await emit({ type: "message_end", message });
                    currentContext.messages.push(message);
                    newMessages.push(message);
                }
                pendingMessages = [];
            }

            // Stream assistant response
            const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
            newMessages.push(message);

            if (message.stopReason === "error" || message.stopReason === "aborted") {
                await emit({ type: "turn_end", message, toolResults: [] });
                await emit({ type: "agent_end", messages: newMessages });
                return;
            }

            // Check for tool calls
            const toolCalls = message.content.filter((c) => c.type === "toolCall");

            const toolResults: ToolResultMessage[] = [];
            hasMoreToolCalls = false;
            if (toolCalls.length > 0) {
                const executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit);
                toolResults.push(...executedToolBatch.messages);
                hasMoreToolCalls = !executedToolBatch.terminate;

                for (const result of toolResults) {
                    currentContext.messages.push(result);
                    newMessages.push(result);
                }
            }

            await emit({ type: "turn_end", message, toolResults });

            if (
                await config.shouldStopAfterTurn?.({
                    message,
                    toolResults,
                    context: currentContext,
                    newMessages,
                })
            ) {
                await emit({ type: "agent_end", messages: newMessages });
                return;
            }

            pendingMessages = (await config.getSteeringMessages?.()) || [];
        }

        // Agent would stop here. Check for follow-up messages.
        const followUpMessages = (await config.getFollowUpMessages?.()) || [];
        if (followUpMessages.length > 0) {
            // Set as pending so inner loop processes them
            pendingMessages = followUpMessages;
            continue;
        }

        // No more messages, exit
        break;
    }

    await emit({ type: "agent_end", messages: newMessages });
}

אלה החלקים המרכזיים:

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

  2. לולאת הסוכן מורכבת מלולאה כפולה, הלולאה החיצונית מטפלת בהודעות נוספות שאנחנו כותבים אחרי שהסוכן מתחיל לעבוד. הלולאה הפנימית אחראית על מענה לפרומפט.

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

  4. נשים לב שכמעט בכל שלב בלולאה, וזה נכון באופן כללי לקוד של פאי, יש לנו קריאות ל emit עם מזהה אירוע. אלה "נקודות התחברות". בעבודה עם פאי נוכל לכתוב תוספים שיופעלו בכל נקודת התחברות שנרצה.

  5. אם יש הודעות ניווט ממתינות נוסיף אותן לרשימת ההודעות של השיחה.

עכשיו מגיעה השורה הכי חשובה של הפונקציה:

const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);

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

const toolCalls = message.content.filter((c) => c.type === "toolCall");

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

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

לולאת הסוכן היא הלב של סוכני קידוד והיא מטפלת בכל הודעה שאנחנו שולחים. בזכות פאי ראינו איך הלולאה הזו בנויה ומה היא מאפשרת. הלולאה שקראנו נקראת לולאת React שזה קיצור של Reason ו Act, וזה אומר שהקוד שולח הודעה למודל שפה, נותן לו הזדמנות להריץ כלים, שולח את התוצאה של הכלים שוב למודל וממשיך בלולאה עד שאין יותר קלים להפעיל.

תוכן עניינים

  1. שאלות למיטיבי לכת

1. שאלות למיטיבי לכת

קראו את הקוד ונסו לחשוב:

  1. באיזה מצבים נרצה לעצור את לולאת הסוכן כשהמודל רוצה להמשיך ולהפעיל עוד כלים?

  2. איזה כלים עשויים לגרום לסיום הלולאה?

  3. האם לולאה זו מתאימה לכל מצבי העבודה שאנחנו מכירים עם סוכנים חכמים? מה קורה במצב תכנון? במצב טייס אוטומטי? במצב Ask?