مشاركة عبر


Quickstart: الذكاء الاصطناعي Agent مع البحث المتجه في Azure DocumentDB باستخدام Go

ابن وكيل الذكاء الاصطناعي ذكي باستخدام Go و Azure DocumentDB. تظهر هذه البداية السريعة بنية وكيلتين تقوم بالبحث الدلالي في الفنادق وتولد توصيات مخصصة.

هام

هذه العينة هي تطبيق مرجعي يوضح أنماط الوكلاء في Go. يستخدم بنية وكيل مصممة خصيصا بدلا من إطار عمل وكيل، وهو النهج الموصى به لتطبيقات الوكلاء الإنتاجية.

المتطلبات الأساسية

يمكنك استخدام واجهة برمجة Azure Developer لإنشاء موارد Azure المطلوبة عن طريق تشغيل الأوامر azd في مستودع العينة. لمزيد من المعلومات، راجع نشر البنية التحتية مع Azure Developer CLI.

موارد Azure

  • مورد Azure OpenAI مع نشر النماذج التالية في Microsoft Foundry:

    • gpt-4oالنشر (وكيل السينثسيزر) - موصى به: سعة 50,000 رمز في الدقيقة (TPM)
    • gpt-4o-miniالنشر (وكيل المخطط) - موصى به: سعة 30,000 رمز في الدقيقة (TPM)
    • text-embedding-3-smallالنشر (التضمينات) - موصى به: سعة 10,000 رمز في الدقيقة (TPM)
    • حصص الرموز: قم بتكوين عدد كاف من TPM لكل عملية نشر لتجنب تحديد المعدل
      • انظر إدارة حصص Azure OpenAI لإدارة الحصص
      • إذا واجهت خطأ 429، زد حصة TPM الخاصة بك أو قلل من تكرار الطلب
  • مجموعة Azure DocumentDB (مع توافق MongoDB) مع دعم البحث المتجه:

    • متطلبات مستوى العنقود بناء على خوارزمية مؤشر المتجهات:
      • IVF (مؤشر الملفات المقلوب): M10 أو أعلى (خوارزمية افتراضية)
      • HNSW (العالم الصغير الهرمي القابل للملاحة): M30 أو أعلى (يعتمد على الرسوم البيانية)
      • قرص ANN: M40 أو أعلى (محسن للنطاق الكبير)
    • تكوين جدار الحماية: مطلوب بدون تكوين جدار حماية مناسب، تفشل محاولات الاتصال
    • للمصادقة بدون كلمة مرور، تم تفعيل التحكم في الوصول القائم على الأدوار (RBAC)

أدوات التطوير

بناء الأنظمة

تستخدم العينة بنية من وكلين حيث لكل وكيل دور محدد.

مخطط معماري يوضح سير عمل الوكلاء مع وكيل المخطط، أداة البحث المتجه، ووكيل التوليف (synthesizer).

يستخدم هذا العينة تنفيذا مخصصا مع حزمة تطوير البرمجيات OpenAI مباشرة، دون الاعتماد على إطار عمل وكيل. يستفيد من وظيفة OpenAI التي تطالب بتكامل الأدوات ويتبع سير عمل خطي بين الوكلاء وأداة البحث. التنفيذ بدون حالة ولا يوجد سجل محادثة، مما يجعله مناسبا لسيناريوهات الاستعلام والرد في دور واحد.

الحصول على التعليمات البرمجية للعينة

  1. قم باستنساخ أو تحميل مستودع Azure DocumentDB Samples إلى جهازك المحلي لمتابعة البدء السريع.

  2. انتقل إلى دليل المشروع:

    cd ai/vector-search-agent-go
    

تكوين متغيرات البيئة

أنشئ ملفا .env في جذر مشروعك لتكوين متغيرات البيئة. يمكنك إنشاء نسخة من الملف .env.sample من المستودع.

قم بتعديل الملف .env واستبدل هذه القيم المؤقتة:

يستخدم هذا البدء السريع بنية من وكلين (مخطط + مركب صوتي) مع ثلاث عمليات نشر للنماذج (نموذجان للدردشة + التضمينات). يتم تكوين متغيرات البيئة لكل نشر نموذج.

  • AZURE_OPENAI_PLANNER_DEPLOYMENT: اسم نشر gpt-4o-mini الخاص بك
  • AZURE_OPENAI_SYNTH_DEPLOYMENT: اسم نشر gpt-4o الخاص بك
  • AZURE_OPENAI_EMBEDDING_DEPLOYMENT: اسم نشر نص-3-صغير

يمكنك الاختيار بين طريقتي مصادقة: المصادقة بدون كلمة مرور باستخدام Azure Identity (موصى به) أو سلسلة اتصال تقليدية ومفتاح واجهة برمجة التطبيقات.

الخيار 1: المصادقة بدون كلمة مرور

استخدم المصادقة بدون كلمة مرور مع كل من Azure OpenAI و Azure DocumentDB. اضبط USE_PASSWORDLESS=true، AZURE_OPENAI_ENDPOINT، و AZURE_DOCUMENTDB_CLUSTER.

# Enable passwordless authentication
USE_PASSWORDLESS=true

# Azure OpenAI Configuration (passwordless)
AZURE_OPENAI_ENDPOINT=your-openai-endpoint

# Azure DocumentDB (passwordless)
AZURE_DOCUMENTDB_CLUSTER=your-mongo-cluster-name
AZURE_DOCUMENTDB_DATABASENAME=Hotels
AZURE_DOCUMENTDB_COLLECTION=hotel_data
AZURE_DOCUMENTDB_INDEX_NAME=vectorIndex

المتطلبات الأساسية للمصادقة بدون كلمة مرور:

  • تأكد من تسجيل دخولك إلى Azure: az login

  • امنح هويتك الأدوار التالية:

    • Cognitive Services OpenAI User على مورد Azure OpenAI
    • DocumentDB Account Contributor وعلى Cosmos DB Account Reader Role مورد Azure DocumentDB

    لمزيد من المعلومات حول تعيين الأدوار، راجع تعيين أدوار Azure باستخدام بوابة Azure.

الخيار الثاني: سلسلة الاتصال ومصادقة مفاتيح API

استخدم المصادقة القائمة على المفاتيح عن طريق تعيينها USE_PASSWORDLESS=false (أو حذفها) وتوفير AZURE_OPENAI_API_KEY قيم وقيم AZURE_DOCUMENTDB_CONNECTION_STRING في ملفك .env .

# Disable passwordless authentication
USE_PASSWORDLESS=false

# Azure OpenAI Configuration (API key)
AZURE_OPENAI_ENDPOINT=your-openai-endpoint
AZURE_OPENAI_API_KEY=your-azure-openai-api-key

# Azure DocumentDB (connection string)
AZURE_DOCUMENTDB_CONNECTION_STRING=mongodb+srv://username:password@cluster.mongocluster.cosmos.azure.com/
AZURE_DOCUMENTDB_DATABASENAME=Hotels
AZURE_DOCUMENTDB_COLLECTION=hotel_data
AZURE_DOCUMENTDB_INDEX_NAME=vectorIndex

بنية المشروع

يتبع المشروع تخطيط مشروع Go القياسي. يجب أن يبدو هيكل الدليل على النحو التالي:

mongo-vcore-agent-go/
├── cmd/
│   ├── agent/          # Main agent application
│   │   └── main.go
│   ├── upload/         # Data upload utility
│   │   └── main.go
│   └── cleanup/        # Database cleanup utility
│       └── main.go
├── internal/
│   ├── agents/         # Agent and tool implementations
│   │   ├── agents.go   # Planner and synthesizer agents
│   │   └── tools.go    # Vector search tool
│   ├── clients/        # Azure OpenAI client
│   │   └── openai.go
│   ├── models/         # Hotel data models
│   │   └── hotel.go
│   ├── prompts/        # System prompts and tool definitions
│   │   └── prompts.go
│   └── vectorstore/    # Azure DocumentDB vector store operations
│       └── store.go
├── .env                # Environment variable configuration
├── go.mod              # Go module file
└── go.sum              # Go module checksum file

استكشاف التعليمات البرمجية

يستعرض هذا القسم المكونات الأساسية لسير عمل وكلاء الذكاء الاصطناعي. تسلط الضوء على كيفية معالجة الوكلاء للطلبات، وكيف تربط الأدوات الذكاء الاصطناعي بقاعدة البيانات، وكيف توجه التعليمات سلوك الذكاء الاصطناعي.

تطبيق الوكيل

يقوم الملف cmd/agent/main.go بتنظيم نظام توصية فندقي مدعوم الذكاء الاصطناعي.

يستخدم التطبيق خدمتين من Azure:

  • Azure OpenAI الذي يستخدم نماذج الذكاء الاصطناعي التي تفهم الاستعلامات وتولد التوصيات
  • Azure DocumentDB الذي يخزن بيانات الفنادق ويجري عمليات بحث التشابه المتجهية

مكونات الوكيل والأدوات

تعمل المكونات الثلاثة معا لمعالجة طلب البحث عن الفندق:

  • وكيل المخطط - يفسر الطلب ويقرر كيفية البحث
  • أداة البحث المتجهة - تجد فنادق مشابهة لما يصفه وكيل المخطط
  • وكيل التوليف - يكتب توصية مفيدة بناء على نتائج البحث

سير عمل التطبيق

يعالج الطلب طلب البحث عن فندق على خطوتين:

  • التخطيط: يستدعي سير العمل وكيل المخطط، الذي يحلل استعلام المستخدم (مثل "الفنادق القريبة من مسارات الجري") ويبحث في قاعدة البيانات عن فنادق مطابقة.
  • التركيب: يستدعي سير العمل وكيل السينثسيزر، الذي يراجع نتائج البحث ويكتب توصية شخصية تشرح أي الفنادق تتوافق بشكل أفضل مع الطلب.
// Run planner agent
hotelContext, err := plannerAgent.Run(ctx, query, nearestNeighbors)
if err != nil {
    log.Fatalf("Planner agent failed: %v", err)
}

if debug {
    fmt.Printf("\n--- HOTEL CONTEXT ---\n%s\n", hotelContext)
}

// Run synthesizer agent
finalAnswer, err := synthesizerAgent.Run(ctx, query, hotelContext)
if err != nil {
    log.Fatalf("Synthesizer agent failed: %v", err)
}

العاملون

internal/agents/agents.go ينفذ الملف المصدري وكلاء المخطط والسينثسيزر اللذين يعملان معا لمعالجة طلبات البحث في الفنادق.

وكيل المخطط

وكيل المخطط هو صاحب القرار الذي يحدد كيفية البحث عن الفنادق.

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

// PlannerAgent orchestrates the tool calling
type PlannerAgent struct {
    openAIClients *clients.OpenAIClients
    searchTool    *VectorSearchTool
    debug         bool
}

// NewPlannerAgent creates a new planner agent
func NewPlannerAgent(openaiClients *clients.OpenAIClients, searchTool *VectorSearchTool, debug bool) *PlannerAgent {
    return &PlannerAgent{
        openAIClients: openaiClients,
        searchTool:    searchTool,
        debug:         debug,
    }
}

// Run executes the planner agent workflow
func (a *PlannerAgent) Run(ctx context.Context, userQuery string, nearestNeighbors int) (string, error) {
    fmt.Println("\n--- PLANNER ---")

    userMessage := fmt.Sprintf(
        `Search for hotels matching this request: "%s". Use nearestNeighbors=%d.`,
        userQuery,
        nearestNeighbors,
    )

    // Get tool definition
    toolDef := a.searchTool.GetToolDefinition()

    // Call planner with tool definitions
    resp, err := a.openAIClients.ChatCompletionWithTools(ctx, prompts.PlannerSystemPrompt, userMessage, []openai.ChatCompletionToolUnionParam{toolDef})
    if err != nil {
        return "", fmt.Errorf("planner failed: %w", err)
    }

    // Extract tool call
    toolName, argsMap, err := clients.ExtractToolCall(resp)
    if err != nil {
        return "", fmt.Errorf("failed to extract tool call: %w", err)
    }

    if toolName != prompts.ToolName {
        return "", fmt.Errorf("unexpected tool called: %s", toolName)
    }

    // Parse arguments using typed struct
    args, err := parseToolArgumentsFromMap(argsMap)
    if err != nil {
        return "", fmt.Errorf("failed to parse tool arguments: %w", err)
    }

    // Use default if nearestNeighbors not provided
    if args.NearestNeighbors == 0 {
        args.NearestNeighbors = nearestNeighbors
    }

    fmt.Printf("Tool: %s\n", toolName)
    fmt.Printf("Query: %s\n", args.Query)
    fmt.Printf("K: %d\n", args.NearestNeighbors)

    // Execute the tool
    searchResults, err := a.searchTool.Execute(ctx, args.Query, args.NearestNeighbors)
    if err != nil {
        return "", fmt.Errorf("search tool execution failed: %w", err)
    }

    return searchResults, nil
}

وكيل السينثسيزر

وكيل السينثسيزر هو الكاتب الذي يكتب توصيات مفيدة.

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

// NewSynthesizerAgent creates a new synthesizer agent
func NewSynthesizerAgent(openaiClients *clients.OpenAIClients, debug bool) *SynthesizerAgent {
    return &SynthesizerAgent{
        openAIClients: openaiClients,
        debug:         debug,
    }
}

// Run executes the synthesizer agent workflow
func (a *SynthesizerAgent) Run(ctx context.Context, userQuery, hotelContext string) (string, error) {
    fmt.Println("\n--- SYNTHESIZER ---")
    fmt.Printf("Context size: %d characters\n", len(hotelContext))

    userMessage := prompts.CreateSynthesizerUserPrompt(userQuery, hotelContext)

    // Call synthesizer (no tools)
    finalAnswer, err := a.openAIClients.ChatCompletion(ctx, prompts.SynthesizerSystemPrompt, userMessage)
    if err != nil {
        return "", fmt.Errorf("synthesizer failed: %w", err)
    }

    return finalAnswer, nil
}

أدوات العامل

internal/agents/tools.go يحدد ملف المصدر أداة البحث المتجهة التي يستخدمها وكيل المخطط.

يحدد ملف الأدوات أداة بحث يمكن لوكيل الذكاء الاصطناعي استخدامها للعثور على الفنادق. هذه الأداة هي الطريقة التي يتصل بها الوكيل بقاعدة البيانات. الذكاء الاصطناعي لا يبحث في قاعدة البيانات مباشرة. يطلب استخدام أداة البحث، وتنفذ الأداة البحث الفعلي.

تعريف الأداة

تصف الطريقة GetToolDefinition الأداة لنموذج الذكاء الاصطناعي بصيغة يفهمها. يحدد اسم الأداة، ووصفا لما تفعله الأداة، والمعاملات التي تحدد المدخلات التي تحتاجها الأداة. هذا التعريف يخبر الذكاء الاصطناعي بوجود الأداة وكيفية استخدامها بشكل صحيح.

// GetToolDefinition returns the Azure OpenAI tool definition
func (t *VectorSearchTool) GetToolDefinition() openai.ChatCompletionToolUnionParam {
    paramSchema := map[string]any{
        "type": "object",
        "properties": map[string]any{
            "query": map[string]any{
                "type":        "string",
                "description": "Natural language search query describing desired hotel characteristics",
            },
            "nearestNeighbors": map[string]any{
                "type":        "integer",
                "description": "Number of results to return (1-20)",
                "default":     5,
            },
        },
        "required": []string{"query", "nearestNeighbors"},
    }

    return openai.ChatCompletionToolUnionParam{
        OfFunction: &openai.ChatCompletionFunctionToolParam{
            Function: openai.FunctionDefinitionParam{
                Name:        prompts.ToolName,
                Description: openai.String(prompts.ToolDescription),
                Parameters:  paramSchema,
            },
        },
    }
}

تنفيذ الأدوات

عندما يستدعي الذكاء الاصطناعي الأداة، يتم تشغيل الطريقة Execute . يقوم بتوليد تضمين عن طريق تحويل استعلام النص إلى متجه رقمي باستخدام نموذج التضمين الخاص ب Azure OpenAI. ثم يبحث في قاعدة البيانات عن طريق إرسال المتجه إلى Azure DocumentDB، الذي يجد فنادق ذات متجهات متشابهة مما يعني أوصاف متشابهة. وأخيرا، يقوم بتنسيق النتائج عن طريق تحويل سجلات قاعدة البيانات إلى نص قابل للقراءة يمكن لوكيل المركب فهمه.

// Execute performs the vector search
func (t *VectorSearchTool) Execute(ctx context.Context, query string, nearestNeighbors int) (string, error) {
    // Generate embedding for query
    queryVector, err := t.openAIClients.GenerateEmbedding(ctx, query)
    if err != nil {
        return "", fmt.Errorf("failed to generate embedding: %w", err)
    }

    // Perform vector search
    results, err := t.vectorStore.VectorSearch(ctx, queryVector, nearestNeighbors)
    if err != nil {
        return "", fmt.Errorf("vector search failed: %w", err)
    }

    // Format results for synthesizer
    var formattedResults []string
    for i, result := range results {
        fmt.Printf("Hotel #%d: %s, Score: %.6f\n", i+1, result.Hotel.HotelName, result.Score)
        formattedResults = append(formattedResults, vectorstore.FormatHotelForSynthesizer(result))
    }

    return strings.Join(formattedResults, "\n\n"), nil
}

لماذا تستخدم هذا النمط؟

فصل الأداة عن الوكيل يوفر مرونة. يقرر الذكاء الاصطناعي متى يبحث وماذا يبحث، بينما يتولى الأداة كيفية البحث. يمكنك إضافة أدوات أكثر دون تغيير منطق الوكيل.

المطالبات

يحتوي ملف المصدر internal/prompts/prompts.go على تنبيهات النظام وتعريفات الأدوات للوكلاء.

يحدد ملف التعليمات والسياق المعطى لنماذج الذكاء الاصطناعي لكل من وكلاء المخطط والسينثسيزر. توجه هذه المحفزات سلوك الذكاء الاصطناعي وتضمن فهمه لدوره في سير العمل.

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

const PlannerSystemPrompt = `You are a hotel search planner. Your job is to help users find hotels by calling the search tool.

CRITICAL INSTRUCTION: You MUST call the "search_hotels_collection" tool for every request. This is the ONLY way to search the database.

When you call the tool, use these parameters:
- query: A clear, detailed natural language description of what the user is looking for. Expand vague requests (e.g., "nice hotel" → "hotel with high ratings, good reviews, and quality amenities").
- nearestNeighbors: Number of results (1-20). Use 3-5 for specific requests, 10-15 for broader searches.

EXAMPLES of how you should call the tool:
- User: "cheap hotel" → Call tool with query: "budget-friendly hotel with good value and affordable rates", nearestNeighbors: 10
- User: "hotel near downtown with parking" → Call tool with query: "hotel near downtown with good parking and wifi", nearestNeighbors: 5

IMPORTANT: Always call the tool. Do not provide answers without calling the tool first.`

const SynthesizerSystemPrompt = `You are an expert hotel recommendation assistant using vector search results.
Only use the TOP 3 results provided. Do not request additional searches or call other tools.

GOAL: Provide a concise comparative recommendation to help the user choose between the top 3 options.

REQUIREMENTS:
- Compare only the top 3 results across the most important attributes: rating, score, location, price-level (if available), and key tags (parking, wifi, pool).
- Identify the main tradeoffs in one short sentence per tradeoff.
- Give a single clear recommendation with one short justification sentence.
- Provide up to two alternative picks (one sentence each) explaining when they are preferable.

FORMAT CONSTRAINTS:
- Plain text only (no markdown).
- Keep the entire response under 220 words.
- Use simple bullets (•) or numbered lists and short sentences (preferably <25 words per sentence).
- Preserve hotel names exactly as provided in the tool summary.

Do not add extra commentary, marketing language, or follow-up questions. If information is missing and necessary to choose, state it in one sentence and still provide the best recommendation based on available data.`

تشغيل تطبيق العرض التوضيحي

  1. قبل تشغيل الوكيل، قم بتحميل بيانات الفندق مع التضمينات. يقوم الأمر cmd/upload/main.go بتحميل الفنادق من ملف JSON، ويولد تضمينات لكل فندق باستخدام text-embedding-3-small، ويدرج المستندات في Azure DocumentDB، وينشئ فهرسة متجهة.

    go run cmd/upload/main.go
    
  2. شغل وكيل توصية الفندق باستخدام cmd/agent/main.go الأمر. يقوم الوكيل باستدعاء وكيل المخطط، والبحث المتجه، ووكيل السينثسيزر. تشمل النتائج درجات التشابه، وتحليل مقارن لوكيل السينثسيزر مع التوصيات.

    go run cmd/agent/main.go
    
    Query: quintessential lodging near running trails, eateries, retail
    Nearest Neighbors: 5
    
    --- PLANNER ---
    Tool: search_hotels_collection
    Query: quintessential lodging near running trails, eateries, and retail shops with good amenities and access to outdoor activities
    K: 5
    Hotel #1: Nordick's Valley Motel, Score: 0.498665
    Hotel #2: White Mountain Lodge & Suites, Score: 0.487320
    Hotel #3: Trails End Motel, Score: 0.479854
    Hotel #4: Country Comfort Inn, Score: 0.474320
    Hotel #5: Lakefront Captain Inn, Score: 0.457873
    
    --- SYNTHESIZER ---
    Context size: 3233 characters
    
    --- FINAL ANSWER ---
    1. COMPARISON SUMMARY:  
    • Nordick's Valley Motel has the highest rating (4.5) and offers free parking, air conditioning, and continental breakfast. It is located in Washington D.C., near historic attractions and trails.
    • White Mountain Lodge & Suites is a resort with unique amenities like a pool, restaurant, and meditation gardens, but has the lowest rating (2.4). It is located in Denver, surrounded by forest trails.
    • Trails End Motel is budget-friendly with a moderate rating (3.2), free parking, free wifi, and a restaurant. It is close to downtown Scottsdale and eateries.
    
    Key tradeoffs:
    - Nordick's Valley Motel excels in rating and proximity to historic attractions but lacks a pool or free wifi.
    - White Mountain Lodge & Suites offers resort-style amenities and forest trails but has the lowest rating.
    - Trails End Motel balances affordability and essential amenities but has fewer unique features compared to the others.
    
    2. BEST OVERALL:
    Nordick's Valley Motel is the best choice for its high rating, proximity to trails and attractions, and free parking.
    
    3. ALTERNATIVE PICKS:
    • Choose White Mountain Lodge & Suites if you prioritize resort amenities and forest trails over rating.
    • Choose Trails End Motel if affordability and proximity to downtown Scottsdale are your main concerns.
    

عرض البيانات وإدارتها في Visual Studio Code

  1. اختر إضافة DocumentDB في كود Visual Studio للاتصال بحساب Azure DocumentDB الخاص بك.

  2. عرض البيانات والفهارس في قاعدة بيانات الفنادق.

    امتداد Visual Studio Code DocumentDB الذي يعرض فهرس البحث المتجه ومستندات الفندق.

تنظيف الموارد

استخدم أمر التنظيف لحذف قاعدة بيانات الاختبار عند الانتهاء. شغّل الأمر التالي:

go run cmd/cleanup/main.go

احذف مجموعة الموارد، وحساب DocumentDB، ومورد Azure OpenAI عندما لا تحتاجها لتجنب التكاليف الإضافية.