Поделиться через


Добавление памяти в агенте

В этом руководстве показано, как добавить память в агент путем реализации AIContextProvider и присоединения его к агенту.

Это важно

Не все типы агентов поддерживают AIContextProvider. Этот шаг использует ChatClientAgent, который поддерживает AIContextProvider.

Предпосылки

Предварительные требования и установка пакетов NuGet см. в разделе "Создание и запуск простого агента " в этом руководстве.

Создайте AIContextProvider

AIContextProvider — это абстрактный класс, от которого можно наследовать и который может быть связан с AgentThread для ChatClientAgent. Это позволяет:

  1. Запустите пользовательскую логику до и после вызова базовой службы вывода.
  2. Предоставьте дополнительный контекст агенту перед вызовом базовой службы вывода.
  3. Проверьте все сообщения, предоставленные агентом и созданные агентом.

События предварительного и последующего вызова

Класс AIContextProvider имеет два метода, которые можно переопределить для выполнения пользовательской логики до и после вызова базовой службы вывода:

  • InvokingAsync — вызывается агентом перед вызовом базовой службы вывода заключений. Вы можете предоставить дополнительный контекст агенту, возвращая AIContext объект. Этот контекст будет объединен с существующим контекстом агента перед вызовом базовой службы. Инструкции, инструменты и сообщения можно добавить в запрос.
  • InvokedAsync — вызывается после того, как агент получил ответ от базовой службы вывода. Вы можете проверить сообщения запроса и ответа и обновить состояние поставщика контекста.

Сериализация

AIContextProvider экземпляры создаются и присоединяются к AgentThread при создании потока и при его возобновлении из сериализованного состояния.

Экземпляр AIContextProvider может иметь собственное состояние, которое необходимо сохранить между вызовами агента. Например, компонент памяти, запоминающий сведения о пользователе, может иметь воспоминания в рамках своего состояния.

Чтобы обеспечить сохранение потоков, необходимо реализовать метод SerializeAsync класса AIContextProvider. Также необходимо предоставить конструктор, принимающий JsonElement параметр, который можно использовать для десериализации состояния при возобновлении потока.

Пример реализации AIContextProvider

Следующий пример пользовательского компонента памяти запоминает имя и возраст пользователя и предоставляет его агенту перед каждым вызовом.

Сначала создайте класс модели для хранения воспоминаний.

internal sealed class UserInfo
{
    public string? UserName { get; set; }
    public int? UserAge { get; set; }
}

Затем вы можете реализовать AIContextProvider для управления памятью. Приведенный UserInfoMemory ниже класс содержит следующее поведение:

  1. Он использует IChatClient для поиска имени и возраста пользователя в сообщениях, когда новые сообщения добавляются в поток в конце каждого запуска.
  2. Он предоставляет все текущие воспоминания агенту перед каждым вызовом.
  3. Если нет доступных воспоминаний, он предписывает агенту запрашивать у пользователя недостающую информацию, а не отвечать на какие-либо вопросы до тех пор, пока не будет предоставлена информация.
  4. Он также реализует сериализацию, чтобы разрешить сохранение памяти в рамках состояния потока.
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

internal sealed class UserInfoMemory : AIContextProvider
{
    private readonly IChatClient _chatClient;
    public UserInfoMemory(IChatClient chatClient, UserInfo? userInfo = null)
    {
        this._chatClient = chatClient;
        this.UserInfo = userInfo ?? new UserInfo();
    }

    public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
    {
        this._chatClient = chatClient;
        this.UserInfo = serializedState.ValueKind == JsonValueKind.Object ?
            serializedState.Deserialize<UserInfo>(jsonSerializerOptions)! :
            new UserInfo();
    }

    public UserInfo UserInfo { get; set; }

    public override async ValueTask InvokedAsync(
        InvokedContext context,
        CancellationToken cancellationToken = default)
    {
        if ((this.UserInfo.UserName is null || this.UserInfo.UserAge is null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
        {
            var result = await this._chatClient.GetResponseAsync<UserInfo>(
                context.RequestMessages,
                new ChatOptions()
                {
                    Instructions = "Extract the user's name and age from the message if present. If not present return nulls."
                },
                cancellationToken: cancellationToken);
            this.UserInfo.UserName ??= result.Result.UserName;
            this.UserInfo.UserAge ??= result.Result.UserAge;
        }
    }

    public override ValueTask<AIContext> InvokingAsync(
        InvokingContext context,
        CancellationToken cancellationToken = default)
    {
        StringBuilder instructions = new();
        instructions
            .AppendLine(
                this.UserInfo.UserName is null ?
                    "Ask the user for their name and politely decline to answer any questions until they provide it." :
                    $"The user's name is {this.UserInfo.UserName}.")
            .AppendLine(
                this.UserInfo.UserAge is null ?
                    "Ask the user for their age and politely decline to answer any questions until they provide it." :
                    $"The user's age is {this.UserInfo.UserAge}.");
        return new ValueTask<AIContext>(new AIContext
        {
            Instructions = instructions.ToString()
        });
    }

    public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
    {
        return JsonSerializer.SerializeToElement(this.UserInfo, jsonSerializerOptions);
    }
}

Использование AIContextProvider с агентом

Чтобы использовать настраиваемый AIContextProvider, необходимо указать AIContextProviderFactory при создании агента. Эта фабрика позволяет агенту создавать новый экземпляр требуемого AIContextProvider для каждого потока.

При создании ChatClientAgent объекта можно предоставить ChatClientAgentOptions объект, который позволяет задать AIContextProviderFactory в дополнение к другим параметрам агента.

using System;
using Azure.AI.OpenAI;
using Azure.Identity;
using OpenAI.Chat;
using OpenAI;

ChatClient chatClient = new AzureOpenAIClient(
    new Uri("https://<myresource>.openai.azure.com"),
    new AzureCliCredential())
    .GetChatClient("gpt-4o-mini");

AIAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions()
{
    Instructions = "You are a friendly assistant. Always address the user by their name.",
    AIContextProviderFactory = ctx => new UserInfoMemory(
        chatClient.AsIChatClient(),
        ctx.SerializedState,
        ctx.JsonSerializerOptions)
});

При создании нового потока AIContextProvider он будет создан GetNewThread и присоединен к потоку. После извлечения воспоминаний можно получить доступ к компоненту памяти с помощью метода потока GetService и проверить воспоминания.

// Create a new thread for the conversation.
AgentThread thread = agent.GetNewThread();

Console.WriteLine(await agent.RunAsync("Hello, what is the square root of 9?", thread));
Console.WriteLine(await agent.RunAsync("My name is Ruaidhrí", thread));
Console.WriteLine(await agent.RunAsync("I am 20 years old", thread));

// Access the memory component via the thread's GetService method.
var userInfo = thread.GetService<UserInfoMemory>()?.UserInfo;
Console.WriteLine($"MEMORY - User Name: {userInfo?.UserName}");
Console.WriteLine($"MEMORY - User Age: {userInfo?.UserAge}");

В этом руководстве показано, как добавить память в агент путем реализации ContextProvider и присоединения его к агенту.

Это важно

Не все типы агентов поддерживают ContextProvider. Этот шаг использует ChatAgent, который поддерживает ContextProvider.

Предпосылки

Для ознакомления с предварительными требованиями и установкой пакетов см. шаг "Создание и запуск простого агента" в этом руководстве.

Создайте объект ContextProvider

ContextProvider является абстрактным классом, от которого можно наследовать, и который может быть связан с AgentThread для ChatAgent. Это позволяет:

  1. Запустите пользовательскую логику до и после вызова базовой службы вывода.
  2. Предоставьте дополнительный контекст агенту перед вызовом базовой службы вывода.
  3. Проверьте все сообщения, предоставленные агенту и созданные агентом.

События предварительного и последующего вызова

Класс ContextProvider имеет два метода, которые можно переопределить для выполнения пользовательской логики до и после вызова базовой службы вывода:

  • invoking — вызывается агентом перед вызовом базовой службы вывода заключений. Вы можете предоставить дополнительный контекст агенту, возвращая Context объект. Этот контекст будет объединен с существующим контекстом агента перед вызовом базовой службы. Инструкции, инструменты и сообщения можно добавить в запрос.
  • invoked — вызывается после того, как агент получил ответ от базовой службы вывода. Вы можете проверить сообщения запроса и ответа и обновить состояние поставщика контекста.

Сериализация

ContextProvider экземпляры создаются и присоединяются к AgentThread при создании потока и при его возобновлении из сериализованного состояния.

Экземпляр ContextProvider может иметь собственное состояние, которое необходимо сохранить между вызовами агента. Например, компонент памяти, запоминающий сведения о пользователе, может иметь воспоминания в рамках своего состояния.

Чтобы разрешить сохранение потоков, необходимо реализовать сериализацию для ContextProvider класса. Также необходимо предоставить конструктор, который может восстановить состояние из сериализованных данных при возобновлении потока.

Пример реализации ContextProvider

Следующий пример пользовательского компонента памяти запоминает имя и возраст пользователя и предоставляет его агенту перед каждым вызовом.

Сначала создайте класс модели для хранения воспоминаний.

from pydantic import BaseModel

class UserInfo(BaseModel):
    name: str | None = None
    age: int | None = None

Затем вы можете реализовать ContextProvider для управления памятью. Приведенный UserInfoMemory ниже класс содержит следующее поведение:

  1. Он использует клиент чата для поиска имени пользователя и возраста в сообщениях пользователей при добавлении новых сообщений в поток в конце каждого запуска.
  2. Он предоставляет все текущие воспоминания агенту перед каждым вызовом.
  3. Если нет доступных воспоминаний, он предписывает агенту запрашивать у пользователя недостающую информацию, а не отвечать на какие-либо вопросы до тех пор, пока не будет предоставлена информация.
  4. Он также реализует сериализацию, чтобы разрешить сохранение памяти в рамках состояния потока.

from agent_framework import ContextProvider, Context, InvokedContext, InvokingContext, ChatAgent, ChatClientProtocol


class UserInfoMemory(ContextProvider):
    def __init__(self, chat_client: ChatClientProtocol, user_info: UserInfo | None = None, **kwargs: Any):
        """Create the memory.

        If you pass in kwargs, they will be attempted to be used to create a UserInfo object.
        """

        self._chat_client = chat_client
        if user_info:
            self.user_info = user_info
        elif kwargs:
            self.user_info = UserInfo.model_validate(kwargs)
        else:
            self.user_info = UserInfo()

    async def invoked(
        self,
        request_messages: ChatMessage | Sequence[ChatMessage],
        response_messages: ChatMessage | Sequence[ChatMessage] | None = None,
        invoke_exception: Exception | None = None,
        **kwargs: Any,
    ) -> None:
        """Extract user information from messages after each agent call."""
        # Check if we need to extract user info from user messages
        user_messages = [msg for msg in request_messages if hasattr(msg, "role") and msg.role.value == "user"]

        if (self.user_info.name is None or self.user_info.age is None) and user_messages:
            try:
                # Use the chat client to extract structured information
                result = await self._chat_client.get_response(
                    messages=request_messages,
                    chat_options=ChatOptions(
                        instructions="Extract the user's name and age from the message if present. If not present return nulls.",
                        response_format=UserInfo,
                    ),
                )

                # Update user info with extracted data
                if result.value:
                    if self.user_info.name is None and result.value.name:
                        self.user_info.name = result.value.name
                    if self.user_info.age is None and result.value.age:
                        self.user_info.age = result.value.age

            except Exception:
                pass  # Failed to extract, continue without updating

    async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context:
        """Provide user information context before each agent call."""
        instructions: list[str] = []

        if self.user_info.name is None:
            instructions.append(
                "Ask the user for their name and politely decline to answer any questions until they provide it."
            )
        else:
            instructions.append(f"The user's name is {self.user_info.name}.")

        if self.user_info.age is None:
            instructions.append(
                "Ask the user for their age and politely decline to answer any questions until they provide it."
            )
        else:
            instructions.append(f"The user's age is {self.user_info.age}.")

        # Return context with additional instructions
        return Context(instructions=" ".join(instructions))

    def serialize(self) -> str:
        """Serialize the user info for thread persistence."""
        return self.user_info.model_dump_json()

Использование ContextProvider с агентом

Чтобы использовать настраиваемый ContextProvider, необходимо указать инициализированный экземпляр ContextProvider при создании агента.

При создании ChatAgent можно предоставить параметр context_providers для подключения компонента памяти к агенту.

import asyncio
from agent_framework import ChatAgent
from agent_framework.azure import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential

async def main():
    async with AzureCliCredential() as credential:
        chat_client = AzureAIAgentClient(async_credential=credential)

        # Create the memory provider
        memory_provider = UserInfoMemory(chat_client)

        # Create the agent with memory
        async with ChatAgent(
            chat_client=chat_client,
            instructions="You are a friendly assistant. Always address the user by their name.",
            context_providers=memory_provider,
        ) as agent:
            # Create a new thread for the conversation
            thread = agent.get_new_thread()

            print(await agent.run("Hello, what is the square root of 9?", thread=thread))
            print(await agent.run("My name is Ruaidhrí", thread=thread))
            print(await agent.run("I am 20 years old", thread=thread))

            # Access the memory component via the thread's context_providers attribute and inspect the memories
            user_info_memory = thread.context_provider.providers[0]
            if user_info_memory:
                print()
                print(f"MEMORY - User Name: {user_info_memory.user_info.name}")
                print(f"MEMORY - User Age: {user_info_memory.user_info.age}")


if __name__ == "__main__":
    asyncio.run(main())

Дальнейшие шаги