Breaking News
القائمة

دليل Spring AI Advisors: شرح المستشارين المدمجين والمخصصين

دليل Spring AI Advisors: شرح المستشارين المدمجين والمخصصين
100%

تضيف أدوات المستشارين سلوكيات مخصصة حول طلبات ChatClient، مثل استرداد المستندات، أو إضافة سياق المحادثة، أو تسجيل الاستخدام، أو إرجاع استجابة دون استدعاء النموذج. يستعرض دليل Spring AI Advisors هذا كيفية عمل سلسلة المستشارين، وكيفية إعداد الخيارات المدمجة، وطريقة كتابة مستشار استدعاء مخصص باستخدام تطبيق OrderDesk التجريبي.

Advertisement

فهم سلسلة المستشارين وترتيب التنفيذ

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

ChatClientResponse response = chain.nextCall(request);   // the rest of the chain

في الاستدعاءات المتزامنة، يعمل المنطق البرمجي الموجود قبل nextCall() قبل المعالجة اللاحقة، بينما يعمل المنطق الموجود بعده عند عودة الاستجابة. يرى المستشار الأول الطلب أولاً والاستجابة النهائية أخيراً، ما لم توقف خطوة سابقة التنفيذ. يمر استدعاء ChatClient عبر المستشارين من قيمة الترتيب الأقل إلى الأعلى، وتعود الاستجابة عبرهم بترتيب عكسي.

يحدد إطار العمل الترتيب باستخدام طريقة getOrder(). ينفذ المستشارون واجهة Ordered الخاصة بإطار العمل، ويعمل المستشار ذو القيمة الأقل أولاً. يمثل Ordered.HIGHEST_PRECEDENCE أصغر عدد صحيح، لذا يعمل المستشار الذي يحمله أولاً. يشارك CallAdvisor في الاستدعاءات المتزامنة، بينما يشارك StreamAdvisor في استدعاءات البث المباشر، ويمكن لتطبيق واحد دعم كليهما. تشمل قائمة المستشارين المدمجين ما يلي:

المستشارالوحدةالوظيفةالترتيب الافتراضي
SafeGuardAdvisorspring-ai-client-chatيحظر الطلب الذي يحتوي على كلمة مدرجة0
MessageChatMemoryAdvisorspring-ai-client-chatيضيف الرسائل السابقة للمحادثة، ويحفظ الرسائل الجديدةHIGHEST_PRECEDENCE + 200
ToolCallingAdvisorspring-ai-client-chatيشغّل حلقة استدعاء الأدوات؛ يُضاف تلقائياًHIGHEST_PRECEDENCE + 300
QuestionAnswerAdvisorspring-ai-vector-store-advisorيضيف المستندات المطابقة من قاعدة البيانات المتجهية (RAG)0
RetrievalAugmentationAdvisorspring-ai-ragنظام RAG تركيبي: إعادة كتابة الاستعلام، الاسترداد، التعزيز0
SimpleLoggerAdvisorspring-ai-client-chatيسجل الطلب والاستجابة بمستوى DEBUG0

إعداد التطبيق التجريبي

يتطلب تطبيق OrderDesk التجريبي تبعيات وخصائص محددة. أضف هذه التبعيات إلى ملف pom.xml لتفعيل ذاكرة الدردشة وقاعدة البيانات المتجهية:

<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<dependency>
  <groupId>org.hsqldb</groupId>
  <artifactId>hsqldb</artifactId>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-vector-store-advisor</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-markdown-document-reader</artifactId>
</dependency>

تحفظ تبعية spring-ai-starter-model-chat-memory-repository-jdbc رسائل الدردشة في جدول قاعدة بيانات، بينما توفر hsqldb قاعدة البيانات المحفوظة في ملف. تتضمن spring-ai-vector-store-advisor مستشار QuestionAnswerAdvisor وتجلب SimpleVectorStore، وهي قاعدة بيانات متجهية تحتفظ بالمتجهات في الذاكرة. وأخيراً، تقرأ spring-ai-markdown-document-reader ملفات السياسات وتدرجها في قاعدة البيانات المتجهية.

أضف هذه الأسطر إلى ملف application.properties:

# Chat memory is saved in a file database, so it survives a restart
spring.datasource.url=jdbc:hsqldb:file:./data/orderdesk
spring.ai.chat.memory.repository.jdbc.initialize-schema=always

# Embedding model for RAG (ollama pull nomic-embed-text)
spring.ai.ollama.embedding.options.model=nomic-embed-text

# Which vector store is active: simple (default, in memory), chroma, pgvector, redis or qdrant.
# The profiles application-chroma.properties and so on switch it.
spring.ai.vectorstore.type=simple

يشير spring.datasource.url إلى ملف HSQLDB، وينشئ initialize-schema=always جدول SPRING_AI_CHAT_MEMORY عند بدء التشغيل. يحدد spring.ai.ollama.embedding.options.model النموذج الذي يحول النص إلى متجهات، ويختار spring.ai.vectorstore.type=simple قاعدة SimpleVectorStore. لرؤية مخرجات SimpleLoggerAdvisor، أضف logging.level.org.springframework.ai.chat.client.advisor=DEBUG.

المكونات التي يستخدمها المستشارون

يغلف المستشارون مكون supportClient، وهو مكون ChatClient. يحتوي الأمر النصي للنظام الخاص به على قواعد المتجر:

package com.howtodoinjava.orderdesk.chat;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatClientConfig {

    static final String SUPPORT_RULES = """
        You are OrderDesk, the support assistant of an online store.
        Answer in at most three sentences.
        Delivery: Pune, Mumbai and Delhi in 2 days; every other city in India in 5 days.
        Delivery is free above INR 499; below that it costs INR 49.
        We do not deliver outside India.
        """;

    @Bean
    ChatClient supportClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem(SUPPORT_RULES)
            .build();
    }

    @Bean
    ChatClient summaryClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem("Summarize the customer's message in one line of at most 12 words.")
            .build();
    }
}

يحتاج MessageChatMemoryAdvisor إلى مكون ChatMemory. يحتفظ تطبيق OrderDesk بآخر 10 رسائل من كل محادثة في جدول JDBC:

package com.howtodoinjava.orderdesk.memory;

import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatMemoryConfig {

    // Keep the last 10 messages of each conversation (the default is 20)
    @Bean
    ChatMemory chatMemory(JdbcChatMemoryRepository repository) {
        return MessageWindowChatMemory.builder()
            .chatMemoryRepository(repository)
            .maxMessages(10)
            .build();
    }
}

يحتاج QuestionAnswerAdvisor إلى مكون VectorStore يحتوي على سياسات المتجر. بعد إضافة الشيفرة البرمجية، حمّل السياسات مرة واحدة باستخدام الأمر curl -X POST localhost:8080/ingest.

بناء وفحص سلسلة المستشارين

الخطوة 1: كتابة مستشار مخصص

يسجل TokenUsageAdvisor المستشارين في السلسلة، وعدد الرموز، ووقت كل استدعاء:

package com.howtodoinjava.orderdesk.advisors;

import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;

import reactor.core.publisher.Flux;

// Logs the time and the tokens of every ChatClient call that it advises
public class TokenUsageAdvisor implements CallAdvisor, StreamAdvisor {

    private static final Logger log = LoggerFactory.getLogger(TokenUsageAdvisor.class);

    private final int order;

    public TokenUsageAdvisor(int order) {
        this.order = order;
    }

    @Override
    public String getName() {
        return "TokenUsageAdvisor";
    }

    @Override
    public int getOrder() {
        return order;
    }

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
        log.info("Chain: {}", chain.getCallAdvisors().stream()
            .map(a -> a.getName() + "(" + a.getOrder() + ")")
            .collect(Collectors.joining(" -> ")));
        long start = System.currentTimeMillis();

        ChatClientResponse response = chain.nextCall(request);   // the rest of the chain

        logUsage(response.chatResponse(), System.currentTimeMillis() - start);
        return response;
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest request,
                                                 StreamAdvisorChain chain) {
        long start = System.currentTimeMillis();
        return chain.nextStream(request)
            .doOnNext(response -> {
                ChatResponse chatResponse = response.chatResponse();
                // only the last piece of a stream carries the token counts
                if (chatResponse != null && chatResponse.getMetadata().getUsage() != null
                        && chatResponse.getMetadata().getUsage().getTotalTokens() != null
                        && chatResponse.getMetadata().getUsage().getTotalTokens() > 0) {
                    logUsage(chatResponse, System.currentTimeMillis() - start);
                }
            });
    }

    private void logUsage(ChatResponse chatResponse, long millis) {
        Usage usage = chatResponse.getMetadata().getUsage();
        log.info("Tokens: prompt={}, completion={}, total={}, time={} ms",
            usage.getPromptTokens(), usage.getCompletionTokens(), usage.getTotalTokens(), millis);
    }
}

ينفذ المستشار واجهة CallAdvisor للطلبات المتزامنة أو StreamAdvisor لطلبات stream(). تتطلب كلتا الواجهتين getName() وgetOrder(). تُرجع طريقة chain.getCallAdvisors() السلسلة بأكملها مرتبة. لتغيير الطلب، أنشئ طلباً جديداً باستخدام request.mutate() ومرره إلى chain.nextCall().

الخطوة 2: ربط المستشارين المدمجين

تُضاف أربعة مستشارين مدمجين بجوار المستشار المخصص في AdvisorChatController. تركت النسخة الأولية كل مستشار مدمج بترتيبه الافتراضي:

supportClient.prompt()
    .user(question)
    .advisors(safeGuardAdvisor, tokenUsageAdvisor, memoryAdvisor,
        questionAnswerAdvisor, new SimpleLoggerAdvisor())
    .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, customerId))
    .call()
    .content();

تحدد طريقة getOrder() أولوية التنفيذ بدلاً من ترتيب الوسائط المقدمة إلى advisors(). لا تؤسس القيم المتساوية عقداً موثوقاً للترتيب.

الخطوة 3: اكتشاف المشكلتين

ظهرت مشكلتان مع الإعداد الافتراضي. أولاً، عندما شارك عميل رقم بطاقته، حظر SafeGuardAdvisor الطلب، لكن رقم البطاقة كان قد حُفظ بالفعل لأن مستشار الذاكرة عمل قبله. ثانياً، سألت عميلة سابقة عن اسمها وسياسة الإرجاع، لكن النموذج تجاهل السجل. يوجه القالب الافتراضي لمستشار QuestionAnswerAdvisor النموذج لاستخدام السياق المقدم فقط وليس المعرفة السابقة.

الخطوة 4: ضبط الترتيب والقالب

يحدد التكوين المُعدّل أولويات صريحة ويغير قالب RAG للسماح بكل من سياق المستند وسجل المحادثة:

package com.howtodoinjava.orderdesk.advisors;

import java.util.List;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SafeGuardAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.Ordered;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Flux;

@RestController
public class AdvisorChatController {

    // The default QuestionAnswerAdvisor template says "not prior knowledge",
    // so the model ignored the chat history; this one allows both
    static final PromptTemplate POLICY_AND_HISTORY = new PromptTemplate("""
        {query}

        Answer from the conversation so far and from the policy text below.
        ---------------------
        {question_answer_context}
        ---------------------
        If neither of them has the answer, say that you don't know.
        """);

    private final ChatClient supportClient;
    private final List<Advisor> advisors;
    private final TokenUsageAdvisor tokenUsageAdvisor =
        new TokenUsageAdvisor(Ordered.HIGHEST_PRECEDENCE + 100);

    AdvisorChatController(@Qualifier("supportClient") ChatClient supportClient,
                          ChatMemory chatMemory, VectorStore vectorStore) {
        this.supportClient = supportClient;
        this.advisors = List.of(
            // 1. block sensitive input before anything else sees or stores it
            new SafeGuardAdvisor(List.of("password", "card number", "CVV"),
                "Please don't share payment details or passwords in chat.",
                Ordered.HIGHEST_PRECEDENCE),
            // 2. measure the whole call, including memory and RAG
            tokenUsageAdvisor,
            // 3. add the conversation history (default order HIGHEST_PRECEDENCE + 200)
            MessageChatMemoryAdvisor.builder(chatMemory).build(),
            // 4. add the policy sections that match the question
            QuestionAnswerAdvisor.builder(vectorStore)
                .searchRequest(SearchRequest.builder().topK(3).similarityThreshold(0.5).build())
                .promptTemplate(POLICY_AND_HISTORY)
                .order(0)
                .build(),
            // 5. log the final prompt, just before the model call
            new SimpleLoggerAdvisor(Ordered.LOWEST_PRECEDENCE - 1));
    }

    @GetMapping("/advisors/ask")
    String ask(@RequestParam String customerId, @RequestParam String question) {
        return supportClient.prompt()
            .user(question)
            .advisors(advisors)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, customerId))
            .call()
            .content();
    }

    @GetMapping("/advisors/stream")
    Flux<String> stream(@RequestParam String question) {
        return supportClient.prompt()
            .user(question)
            .advisors(tokenUsageAdvisor)
            .stream()
            .content();
    }
}

يأخذ SafeGuardAdvisor الآن الكلمات المحظورة، ورسالة مخصصة، وترتيب Ordered.HIGHEST_PRECEDENCE. يحتفظ القالب بعناصر {query} و{question_answer_context}. يُرجع SearchRequest ما يصل إلى 3 أقسام من السياسة بدرجة تشابه لا تقل عن 0.5. يمنع نقل الحماية إلى الموضع الخارجي وصول الطلبات المحظورة إلى الذاكرة.

يؤكد الاختبار مع عميل جديد يُدعى Ravi هذه الإصلاحات:

curl -G localhost:8080/advisors/ask --data-urlencode "customerId=ravi" --data-urlencode "question=Hi, I am Ravi. How many days do I have to return a jacket?"
curl -G localhost:8080/advisors/ask --data-urlencode "customerId=ravi" --data-urlencode "question=What is my name, and how long did you say I have?"
curl -G localhost:8080/advisors/ask --data-urlencode "customerId=ravi" --data-urlencode "question=My card number is 4111 1111 1111 1111, can you save it?"

تأتي الإجابة الأولى من قسم السياسة الذي أضافه QuestionAnswerAdvisor. وتأتي الإجابة الثانية من السجل، حيث لم يسجل أي قسم سياسة درجة 0.5، وسمح القالب الجديد للنموذج باستخدام المحادثة. يعود الطلب الثالث في 13 مللي ثانية لأن SafeGuardAdvisor أرجع الاستجابة قبل عمل مستشار الذاكرة، تاركاً رقم البطاقة دون حفظ.

الخطوة 5: تقديم المشورة لاستدعاء البث المباشر

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

أخطاء شائعة

  • الترتيب المتساوي: لا يعمل المستشارون الذين يحملون الترتيب ذاته بترتيب القائمة. حدد ترتيباً لكل مستشار يهم موقعه.
  • الحماية المتأخرة: ضع الفحوصات قبل المعالجة التي تحميها. تحقق من عدم التقاط البيانات الحساسة بواسطة التسجيل المبكر.
  • الشيفرات البرمجية المعيقة في البث المباشر: تجنب العمل المعيق في خيوط حلقة أحداث Reactor. إذا استدعى مستشار بث مباشر مخصص قاعدة بيانات معيقة، فاستخدم مجدولاً مناسباً.
  • المستشارون الافتراضيون يعملون في كل مكان: تضيف طريقة defaultAdvisors() المستشارين إلى كل طلب يُجرى عبر ذلك العميل. أضف المستشارين لكل طلب ما لم تكن كل الطلبات بحاجة إليهم.
  • القوالب المتعارضة: يضيف كل مستشار يضيف نصاً إلى الأمر النصي تعليمات أيضاً. اقرأ الأمر النصي النهائي في سجل SimpleLoggerAdvisor عند دمج المستشارين.

مفاهيم متقدمة حول المستشارين

بينما يمكن لكل من المستشارين وجوانب Spring AOP تغليف التنفيذ، يعمل مستشارو ChatClient تحديداً مع كائنات الأمر النصي المهيكل، والاستجابة، وسياق المستشار. يمرر المستشارون البيانات لبعضهم البعض عبر سياق المستشار، وهو خريطة (Map) على الطلب والاستجابة. على سبيل المثال، يضع QuestionAnswerAdvisor المستندات المعثور عليها تحت QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS، والتي يمكن لمستشار لاحق قراءتها من response.context().

يمكن للمستشار تخطي استدعاء النموذج بالكامل. إذا أرجعت adviseCall() استجابة دون استدعاء chain.nextCall()، فلن يُستدعى النموذج أبداً، وهي الطريقة التي يعمل بها SafeGuardAdvisor. لرؤية الأمر النصي النهائي، ضع SimpleLoggerAdvisor عند Ordered.LOWEST_PRECEDENCE - 1 وفعّل تسجيل DEBUG. يُذكر أن الإصدار 1.0 من إطار العمل أعاد تسمية CallAroundAdvisor وStreamAroundAdvisor إلى CallAdvisor وStreamAdvisor، وAdvisedRequest إلى ChatClientRequest.

إدارة سلسلة المستشارين

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

كُتب هذا المقال بالذكاء الاصطناعي اعتماداً على ما نشره: howtodoinjava.com.

هل أعجبك هذا المقال؟
Advertisement

اقرأ المزيد

عمليات البحث الشائعة