הקדשנו יום אחד לבניית AutoPilot Agentic — צוות סוכני Google ADK שרץ על Gemini 3.5 מעל Vertex AI ומותקן ב-Cloud Run — כהגשה לתחרות All Things Agentic של גוגל. רוב הדרך עברה כמצופה. שתי תקלות אכלו את רוב הזמן, והתברר שיש להן מבנה משותף ששווה לכתוב:
שתיהן היו שגיאות 404 שנקראות כמו בעיית הרשאות. לאף אחת מהן לא היה קשר להרשאות.
המבנה הזה יקר בגלל מה שהוא עושה לשעה הבאה שלך. שגיאת 404 שאומרת "לא נמצא או שלפרויקט שלך אין גישה אליו" שולחת אותך לבדוק IAM, לחפש הרשאות, לקרוא מחדש דפי מכסות ולפתוח כרטיס תמיכה. התשובה לא נמצאת באף אחד מהמקומות האלה — וכל אותו זמן הדבר שחיפשת עובד מצוין במקום שלא הסתכלת בו.
1. משפחת Gemini 3.x עונה בנקודת הקצה הגלובלית, לא באזורית
הקריאה הראשונה שלנו ל-gemini-3.5-flash דרך Vertex AI חזרה כך:
404 Publisher model projects/<project>/locations/us-central1/publishers/google/models/gemini-3.5-flash
was not found or your project does not have access to it.
קראי את המשפט הזה כמפתחת שרק עכשיו קיבלה משפחת מודלים חדשה. "לפרויקט שלך אין גישה אליו" עושה שם עבודה כבדה מאוד. זה הסבר סביר ושלם: המודל חדש, אולי הוא ברשימת היתר, אולי חשבון החיוב שלנו לא רשום, אולי היה טופס preview שלא מילאנו.
במקביל, gcloud ai model-garden models list הראה את המודל יושב בדיוק שם בקטלוג של אותו פרויקט — מה שרק הגביר את הבלבול. הקטלוג אומר שיש לנו; נקודת הקצה אומרת שאין.
התשובה האמיתית היא שמשפחת Gemini 3.x מוגשת מנקודת הקצה global. אותה קריאה בדיוק, אותו פרויקט, אותם פרטי הזדהות, עם location="global" במקום us-central1 — ענתה בניסיון הראשון:
client = genai.Client(vertexai=True, project=PROJECT, location="global")
client.models.generate_content(model="gemini-3.5-flash", contents="…") # 200
שני דברים מתכללים מכאן, והשני חשוב מהראשון.
נוכחות בקטלוג איננה זמינות בנקודת קצה. מודל שמופיע ב-Model Garden עבור הפרויקט שלך מעיד שהוא קיים בקטלוג. הוא לא מעיד אילו נקודות קצה מגישות אותו. רק קריאה לנקודת קצה מסוימת עונה על השאלה הזו, ושתי העובדות עצמאיות מספיק כדי שאמון בראשונה יעלה לך בחצי יום.
ולכן — אל תקבעי את שם המודל בקוד. בדקי אותו. הפסקנו להתייחס לשם המודל כקבוע והפכנו אותו לערך נפתר: סולם מועמדים, בדיקה אחת בעליית השירות, והראשון שבאמת עונה מנצח.
def resolve_model(ladder, location, probe):
tried = []
for candidate in ladder:
tried.append(candidate)
try:
if probe(candidate):
return Resolution(model=candidate, location=location,
probed=tuple(tried),
fallback_used=candidate != ladder[0])
except Exception:
continue # 404 כאן הוא מידע, לא קריסה
raise RuntimeError(
f"no model in {list(ladder)} answered at location {location!r}; "
"the Gemini 3.x family is served from the global endpoint — check "
"the location before assuming an entitlement problem")
שימי לב להודעת השגיאה. כשהסולם מתמצה, החריגה מצביעה על נקודת הקצה כחשודה המרכזית — כי זה הדבר שהאדם הבא לא יחשוב עליו. הודעת שגיאה היא מקום להשאיר בו פתק למי שייתקל בזה בשתיים בלילה, ובדרך כלל האדם הזה הוא את.
המודל שנפתר, נקודת הקצה שענתה, והאם נעשה שימוש בשלב נסיגה — כולם מדווחים בנקודת הבריאות של השירות. ההבחנה הזו — המודל שהמופע הזה פתר מול המודל שהוגדר לו — מתבררת כחשובה, כי הם לא תמיד זהים, ורק אחד מהם הוא עובדה על התהליך הרץ.
2. הנתיב /healthz לא מגיע בכלל לקונטיינר ב-Cloud Run
השנייה מוזרה יותר, והאמנו לה רק אחרי שבנינו בקרה.
השירות שלנו חשף GET /healthz. צינור ההפצה תשאל אותו אחרי כל גרסה וסירב לעבור לירוק עד שיענה. הוא מעולם לא ענה. כל בקשה החזירה את דף ה-404 הממותג של גוגל עצמה — לא ה-404 בפורמט JSON של האפליקציה שלנו, אלא דף ה-HTML עם הלוגו של גוגל.
כל הפרשנויות המתבקשות שגויות. השירות היה תקין. באותה דקה, על אותה רוויזיה:
GET / → 200 (ה-HTML של הקונסולה)
GET /api/runs → 200 (JSON אמיתי, מגובה ב-Firestore)
GET /openapi.json → 200 — ובלוק ה-paths שלו מציג את /healthz
GET /healthz → 404 דף השגיאה של גוגל
כלומר הנתיב היה קיים, ה-framework ידע עליו, האפליקציה הגישה כל דבר אחר — ובדיקת הבריאות אמרה "מת".
המדידה שמכריעה נמצאת ביומן הבקשות. בשם מארח מסוג *.run.app, בקשה ל-/healthz אינה מופיעה באף רשומת יומן של Cloud Run. היא לא מגיעה לקונטיינר.
אבל זו לבדה אינה ראיה — היעדר ביומן יכול באותה מידה לנבוע מכך שהתיעוד כבוי, ואפס שלא נמדד אינו אפס. לכן, לפני שהאמנו, היינו צריכים ידוע-חיובי: משהו שאמור להפיק 404 מהאפליקציה, כדי להוכיח שהמכשיר מסוגל לראות אחד כזה. הבקשה GET /zzz על אותו שירות, באותה דקה, כן תועדה כ-404 מהאפליקציה. התיעוד עבד. המכשיר עבד. לכן ההיעדר של /healthz היה היעדר אמיתי.
(פרט נלווה שהקשה על הזיהוי מבחוץ: ה-frontend של גוגל מחליף את גוף השגיאה בדף משלו, כך ש-/zzz ו-/healthz נראו זהים לחלוטין דרך curl — בעוד שמתחת לפני השטח אחד הוגש על ידי האפליקציה שלנו והשני מעולם לא הועבר אליה.)
הפתרון הוא שינוי שם. נקודת הבריאות שלנו היא כעת /api/health, תחת קידומת נתיב שבבעלותנו. /healthz נשמר ככינוי — והוא עדיין עובד כשבודקים את הקונטיינר ישירות, מקומית או ב-Docker. וזו בדיוק המלכודת: הוא עובד בכל מקום שבו את בודקת, ונכשל רק בשם המארח שהמשתמשים ושער ההפצה שלך באמת משתמשים בו.
הכלל המעשי: אל תקראי לנקודת בריאות ב-Cloud Run בשם /healthz. ואם בדיקת בריאות מחזירה 404 בזמן שהשירות נראה תקין — הריצי curl לשורש ולנתיב אמיתי נוסף לפני שאת יוצאת לחפש תקלה. שתי תשובות 200 ו-404 בנתיב הבריאות הן התקלה הזו, לא שירות שבור.
ההרגל ששתיהן טוענות בעדו
תעדי את קוד הסטטוס, לא את המסקנה.
פתק שאומר "404 מנקודת הקצה האזורית, 30.08.2026" נשאר נכון לנצח וכל אחת יכולה לבדוק אותו מחדש. פתק שאומר "לפרויקט שלנו אין גישה ל-Gemini 3" מעולם לא היה נכון, הוא כעת מטעה לצמיתות — וזה החלק היקר: אף אחד לא בודק מחדש חומה. מסקנה שנכתבת ל-README או לכרטיס הופכת לעובדה שהצוות עוקף במשך חודשים. המדידה שממנה היא נולדה הייתה מופרכת תוך שלושים שניות.
ראינו בדיוק את הכישלון הזה מתרחש בתיעוד שלנו: יכולת שתועדה כבלתי אפשרית, שהוכללה מסירוב בודד, ושכל הזמן הייתה במרחק שורת הגדרה אחת. קוד הסטטוס זול לרשום והוא לא מתיישן.
מה בנינו
AutoPilot Agentic לוקח את הכרטיס הבא מלוח המשימות, קורא את הריפוזיטורי, כותב את השינוי בענף משלו ופותח פול-ריקווסט אחד לכל כרטיס — ואז נעצר בשער שאין לו דרך לפתוח. ארבעה סוכני ADK (מיון ← תכנון ← מימוש ← ביקורת) עושים את החשיבה; מעקות הבטיחות, שער האישור וכל פעולת כתיבה חיים בקוד פייתון רגיל שהסוכנים אינם יכולים להגיע אליו. המודל מציע; המתזמר מחליט.
אפשר לראות ריצה מלאה — כולל המעקה שפוסל שינוי שסוכן הביקורת כבר אישר — בסרטון ההדגמה בן שלוש הדקות, או לקרוא את התיאור המלא ב-Devpost.
אם את בונה סוכנים שנוגעים בריפוזיטוריז אמיתיים ורוצה זוג עיניים נוסף על מעקות הבטיחות — דברי איתנו. זה רוב מה שאנחנו עושים.