Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Deze handleiding laat zien hoe u chatgeschiedenis van agents opslaat in externe opslag door een aangepaste ChatMessageStore te implementeren en deze te gebruiken met een ChatClientAgent.
Standaard, wanneer u ChatClientAgent gebruikt, wordt de chatgeschiedenis opgeslagen in het geheugen in het AgentThread object of in de onderliggende inferentieservice, als de service dit ondersteunt.
Wanneer voor services geen chatgeschiedenis in de service hoeft te worden opgeslagen, is het mogelijk om een aangepast archief te bieden voor het behouden van de chatgeschiedenis in plaats van te vertrouwen op het standaardgedrag in het geheugen.
Vereiste voorwaarden
Zie de stap Een eenvoudige agent maken en uitvoeren in deze zelfstudie voor vereisten.
NuGet-pakketten installeren
Als u Microsoft Agent Framework wilt gebruiken met Azure OpenAI, moet u de volgende NuGet-pakketten installeren:
dotnet add package Azure.AI.OpenAI --prerelease
dotnet add package Azure.Identity
dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
Daarnaast gebruikt u het vectorarchief in het geheugen om chatberichten op te slaan.
dotnet add package Microsoft.SemanticKernel.Connectors.InMemory --prerelease
Een aangepaste ChatMessage Store maken
Als u een aangepaste klasse wilt maken, moet u de abstracte ChatMessageStoreChatMessageStore klasse implementeren en implementaties bieden voor de vereiste methoden.
Methoden voor het opslaan en ophalen van berichten
De belangrijkste methoden om te implementeren zijn:
-
InvokingAsync- aangeroepen aan het begin van agentoproep om berichten op te halen uit de opslag die als context moet worden verstrekt. -
InvokedAsync- aangeroepen aan het einde van de aanroep van de agent om nieuwe berichten toe te voegen aan de opslag.
InvokingAsync moet de berichten in oplopende chronologische volgorde retourneren (oudste eerst). Alle berichten die erdoor worden geretourneerd, worden gebruikt door de ChatClientAgent bij het aanroepen van de onderliggende IChatClient. Het is daarom belangrijk dat deze methode rekening houdt met de limieten van het onderliggende model en alleen zoveel berichten retourneert als door het model kan worden verwerkt.
Alle logica voor het verminderen van chatgeschiedenis, zoals samenvatting of bijsnijden, moet worden uitgevoerd voordat berichten worden geretourneerd.InvokingAsync
Serialization
ChatMessageStore instanties worden gemaakt en gekoppeld aan een AgentThread wanneer de thread wordt aangemaakt en wanneer een thread wordt hervat vanuit een geserialiseerde toestand.
Hoewel de werkelijke berichten die de chatgeschiedenis vormen, extern worden opgeslagen, moet het ChatMessageStore exemplaar mogelijk sleutels of een andere status opslaan om de chatgeschiedenis in de externe opslag te identificeren.
Als u persistente threads wilt toestaan, moet u de Serialize methode van de ChatMessageStore klasse implementeren. Met deze methode moet een JsonElement met de status worden geretourneerd die nodig is om de opslag later te herstellen. Wanneer het agentframework wordt gedeserialiseerd, wordt deze geserialiseerde status doorgegeven aan de ChatMessageStoreFactory, zodat u deze kunt gebruiken om de store opnieuw te maken.
Voorbeeld van chatberichtstore-implementatie
In de volgende voorbeeld-implementatie worden chatberichten opgeslagen in een vectorarchief.
InvokedAsync voegt berichten toe aan de vectoropslag met behulp van een unieke sleutel voor elk bericht. Zowel de aanvraagberichten als de antwoordberichten worden opgeslagen vanuit de aanroepcontext.
InvokingAsync haalt de berichten voor de huidige thread op uit het vectorarchief, rangschikt ze op tijdstempel en retourneert ze in oplopende chronologische volgorde (oudste eerst).
Wanneer de eerste aanroep plaatsvindt, genereert het archief een unieke sleutel voor de thread, die vervolgens wordt gebruikt om de chatgeschiedenis in het vectorarchief te identificeren voor volgende aanroepen.
De unieke sleutel wordt opgeslagen in de ThreadDbKey eigenschap, die wordt geserialiseerd met behulp van de Serialize methode en gedeserialiseerd via de constructor die een JsonElement.
Deze sleutel wordt daarom behouden als onderdeel van de AgentThread status, zodat de thread later kan worden hervat en dezelfde chatgeschiedenis blijft gebruiken.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.InMemory;
internal sealed class VectorChatMessageStore : ChatMessageStore
{
private readonly VectorStore _vectorStore;
public VectorChatMessageStore(
VectorStore vectorStore,
JsonElement serializedStoreState,
JsonSerializerOptions? jsonSerializerOptions = null)
{
this._vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore));
if (serializedStoreState.ValueKind is JsonValueKind.String)
{
this.ThreadDbKey = serializedStoreState.Deserialize<string>();
}
}
public string? ThreadDbKey { get; private set; }
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(
InvokingContext context,
CancellationToken cancellationToken = default)
{
if (this.ThreadDbKey is null)
{
// No thread key yet, so no messages to retrieve
return [];
}
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
var records = collection
.GetAsync(
x => x.ThreadId == this.ThreadDbKey,
10,
new() { OrderBy = x => x.Descending(y => y.Timestamp) },
cancellationToken);
List<ChatMessage> messages = [];
await foreach (var record in records)
{
messages.Add(JsonSerializer.Deserialize<ChatMessage>(record.SerializedMessage!)!);
}
// Reverse to return in ascending chronological order (oldest first)
messages.Reverse();
return messages;
}
public override async ValueTask InvokedAsync(
InvokedContext context,
CancellationToken cancellationToken = default)
{
// Don't store messages if the request failed.
if (context.InvokeException is not null)
{
return;
}
this.ThreadDbKey ??= Guid.NewGuid().ToString("N");
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
// Store request messages, response messages, and optionally AIContextProvider messages
var allNewMessages = context.RequestMessages
.Concat(context.AIContextProviderMessages ?? [])
.Concat(context.ResponseMessages ?? []);
await collection.UpsertAsync(allNewMessages.Select(x => new ChatHistoryItem()
{
Key = this.ThreadDbKey + x.MessageId,
Timestamp = DateTimeOffset.UtcNow,
ThreadId = this.ThreadDbKey,
SerializedMessage = JsonSerializer.Serialize(x),
MessageText = x.Text
}), cancellationToken);
}
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) =>
// We have to serialize the thread id, so that on deserialization you can retrieve the messages using the same thread id.
JsonSerializer.SerializeToElement(this.ThreadDbKey);
private sealed class ChatHistoryItem
{
[VectorStoreKey]
public string? Key { get; set; }
[VectorStoreData]
public string? ThreadId { get; set; }
[VectorStoreData]
public DateTimeOffset? Timestamp { get; set; }
[VectorStoreData]
public string? SerializedMessage { get; set; }
[VectorStoreData]
public string? MessageText { get; set; }
}
}
De aangepaste ChatMessageStore gebruiken met een ChatClientAgent
Als u de aangepaste ChatMessageStorefunctie wilt gebruiken, moet u een ChatMessageStoreFactory opgeven bij het maken van de agent. Met deze factory kan de agent een nieuw exemplaar van de gewenste ChatMessageStore voor elke thread maken.
Bij het maken van een ChatClientAgent is het mogelijk om een ChatClientAgentOptions object op te geven, waarmee naast ChatMessageStoreFactory ook alle andere agentopties kunnen worden opgegeven.
De factory is een asynchrone functie die een contextobject en een annulerings-token ontvangt, en een ValueTask<ChatMessageStore> retourneert.
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.InMemory;
// Create a vector store to store the chat messages in.
VectorStore vectorStore = new InMemoryVectorStore();
AIAgent agent = new AzureOpenAIClient(
new Uri("https://<myresource>.openai.azure.com"),
new AzureCliCredential())
.GetChatClient("gpt-4o-mini")
.AsAIAgent(new ChatClientAgentOptions
{
Name = "Joker",
ChatOptions = new() { Instructions = "You are good at telling jokes." },
ChatMessageStoreFactory = (ctx, ct) => new ValueTask<ChatMessageStore>(
// Create a new chat message store for this agent that stores the messages in a vector store.
// Each thread must get its own copy of the VectorChatMessageStore, since the store
// also contains the id that the thread is stored under.
new VectorChatMessageStore(
vectorStore,
ctx.SerializedState,
ctx.JsonSerializerOptions))
});
// Start a new thread for the agent conversation.
AgentThread thread = await agent.GetNewThreadAsync();
// Run the agent with the thread
var response = await agent.RunAsync("Tell me a joke about a pirate.", thread);
// The thread state can be serialized for storage
JsonElement serializedThread = thread.Serialize();
// Later, deserialize the thread to resume the conversation
AgentThread resumedThread = await agent.DeserializeThreadAsync(serializedThread);
Deze handleiding laat zien hoe u chatgeschiedenis van agents opslaat in externe opslag door een aangepaste ChatMessageStore te implementeren en deze te gebruiken met een ChatAgent.
Standaard, wanneer u ChatAgent gebruikt, wordt de chatgeschiedenis opgeslagen in het geheugen in het AgentThread object of in de onderliggende inferentieservice, als de service dit ondersteunt.
Wanneer services geen chatgeschiedenis vereisen of niet in staat zijn om te worden opgeslagen in de service, is het mogelijk om een aangepast archief te bieden voor het behouden van de chatgeschiedenis in plaats van te vertrouwen op het standaardgedrag in het geheugen.
Vereiste voorwaarden
Zie de stap Een eenvoudige agent maken en uitvoeren in deze zelfstudie voor vereisten.
Een aangepaste ChatMessage Store maken
Als u een aangepaste ChatMessageStoremethode wilt maken, moet u het ChatMessageStore protocol implementeren en implementaties bieden voor de vereiste methoden.
Methoden voor het opslaan en ophalen van berichten
De belangrijkste methoden om te implementeren zijn:
-
add_messages- wordt aangeroepen om nieuwe berichten toe te voegen aan de opslag. -
list_messages- aangeroepen om de berichten op te halen uit de opslag.
list_messages moet de berichten in oplopende chronologische volgorde retourneren. Alle berichten die door het worden geretourneerd, zullen worden gebruikt door de ChatAgent bij het aanroepen van de onderliggende chatclient. Het is daarom belangrijk dat deze methode rekening houdt met de limieten van het onderliggende model en alleen zoveel berichten retourneert als door het model kan worden verwerkt.
Alle logica voor het verminderen van chatgeschiedenis, zoals samenvatting of bijsnijden, moet worden uitgevoerd voordat berichten worden geretourneerd.list_messages
Serialization
ChatMessageStore instanties worden gemaakt en gekoppeld aan een AgentThread wanneer de thread wordt aangemaakt en wanneer een thread wordt hervat vanuit een geserialiseerde toestand.
Hoewel de werkelijke berichten die de chatgeschiedenis vormen, extern worden opgeslagen, moet het ChatMessageStore exemplaar mogelijk sleutels of een andere status opslaan om de chatgeschiedenis in de externe opslag te identificeren.
Als u persistente threads wilt toestaan, moet u de serialize_state en deserialize_state methoden van het ChatMessageStore protocol implementeren. Met deze methoden kan de status van de opslag worden behouden en hersteld bij het hervatten van een thread.
Voorbeeld van chatberichtstore-implementatie
In de volgende voorbeeldimplementatie worden chatberichten in Redis opgeslagen met behulp van de gegevensstructuur Redis Lists.
In add_messagesworden berichten in Redis opgeslagen met behulp van RPUSH om ze toe te voegen aan het einde van de lijst in chronologische volgorde.
list_messages haalt de berichten voor de huidige thread van Redis op met behulp van LRANGE en retourneert ze in oplopende chronologische volgorde.
Wanneer het eerste bericht wordt ontvangen, genereert de store een unieke sleutel voor de thread, die vervolgens wordt gebruikt om de chatgeschiedenis in Redis te identificeren voor volgende aanroepen.
De unieke sleutel en andere configuratie worden opgeslagen en kunnen worden geserialiseerd en gedeserialiseerd met behulp van de serialize_state en deserialize_state methoden.
Deze status wordt daarom behouden als onderdeel van de AgentThread status, zodat de thread later kan worden hervat en dezelfde chatgeschiedenis kan blijven gebruiken.
from collections.abc import Sequence
from typing import Any
from uuid import uuid4
from pydantic import BaseModel
import json
import redis.asyncio as redis
from agent_framework import ChatMessage
class RedisStoreState(BaseModel):
"""State model for serializing and deserializing Redis chat message store data."""
thread_id: str
redis_url: str | None = None
key_prefix: str = "chat_messages"
max_messages: int | None = None
class RedisChatMessageStore:
"""Redis-backed implementation of ChatMessageStore using Redis Lists."""
def __init__(
self,
redis_url: str | None = None,
thread_id: str | None = None,
key_prefix: str = "chat_messages",
max_messages: int | None = None,
) -> None:
"""Initialize the Redis chat message store.
Args:
redis_url: Redis connection URL (for example, "redis://localhost:6379").
thread_id: Unique identifier for this conversation thread.
If not provided, a UUID will be auto-generated.
key_prefix: Prefix for Redis keys to namespace different applications.
max_messages: Maximum number of messages to retain in Redis.
When exceeded, oldest messages are automatically trimmed.
"""
if redis_url is None:
raise ValueError("redis_url is required for Redis connection")
self.redis_url = redis_url
self.thread_id = thread_id or f"thread_{uuid4()}"
self.key_prefix = key_prefix
self.max_messages = max_messages
# Initialize Redis client
self._redis_client = redis.from_url(redis_url, decode_responses=True)
@property
def redis_key(self) -> str:
"""Get the Redis key for this thread's messages."""
return f"{self.key_prefix}:{self.thread_id}"
async def add_messages(self, messages: Sequence[ChatMessage]) -> None:
"""Add messages to the Redis store.
Args:
messages: Sequence of ChatMessage objects to add to the store.
"""
if not messages:
return
# Serialize messages and add to Redis list
serialized_messages = [self._serialize_message(msg) for msg in messages]
await self._redis_client.rpush(self.redis_key, *serialized_messages)
# Apply message limit if configured
if self.max_messages is not None:
current_count = await self._redis_client.llen(self.redis_key)
if current_count > self.max_messages:
# Keep only the most recent max_messages using LTRIM
await self._redis_client.ltrim(self.redis_key, -self.max_messages, -1)
async def list_messages(self) -> list[ChatMessage]:
"""Get all messages from the store in chronological order.
Returns:
List of ChatMessage objects in chronological order (oldest first).
"""
# Retrieve all messages from Redis list (oldest to newest)
redis_messages = await self._redis_client.lrange(self.redis_key, 0, -1)
messages = []
for serialized_message in redis_messages:
message = self._deserialize_message(serialized_message)
messages.append(message)
return messages
async def serialize_state(self, **kwargs: Any) -> Any:
"""Serialize the current store state for persistence.
Returns:
Dictionary containing serialized store configuration.
"""
state = RedisStoreState(
thread_id=self.thread_id,
redis_url=self.redis_url,
key_prefix=self.key_prefix,
max_messages=self.max_messages,
)
return state.model_dump(**kwargs)
async def deserialize_state(self, serialized_store_state: Any, **kwargs: Any) -> None:
"""Deserialize state data into this store instance.
Args:
serialized_store_state: Previously serialized state data.
**kwargs: Additional arguments for deserialization.
"""
if serialized_store_state:
state = RedisStoreState.model_validate(serialized_store_state, **kwargs)
self.thread_id = state.thread_id
self.key_prefix = state.key_prefix
self.max_messages = state.max_messages
# Recreate Redis client if the URL changed
if state.redis_url and state.redis_url != self.redis_url:
self.redis_url = state.redis_url
self._redis_client = redis.from_url(self.redis_url, decode_responses=True)
def _serialize_message(self, message: ChatMessage) -> str:
"""Serialize a ChatMessage to JSON string."""
message_dict = message.model_dump()
return json.dumps(message_dict, separators=(",", ":"))
def _deserialize_message(self, serialized_message: str) -> ChatMessage:
"""Deserialize a JSON string to ChatMessage."""
message_dict = json.loads(serialized_message)
return ChatMessage.model_validate(message_dict)
async def clear(self) -> None:
"""Remove all messages from the store."""
await self._redis_client.delete(self.redis_key)
async def aclose(self) -> None:
"""Close the Redis connection."""
await self._redis_client.aclose()
De aangepaste ChatMessageStore gebruiken met een ChatAgent
Als u de aangepaste ChatMessageStorefunctie wilt gebruiken, moet u een chat_message_store_factory opgeven bij het maken van de agent. Met deze factory kan de agent een nieuw exemplaar van de gewenste ChatMessageStore voor elke thread maken.
Wanneer u een ChatAgentmaakt, kunt u de chat_message_store_factory parameter opgeven naast alle andere agentopties.
from azure.identity import AzureCliCredential
from agent_framework import ChatAgent
from agent_framework.openai import AzureOpenAIChatClient
# Create the chat agent with custom message store factory
agent = ChatAgent(
chat_client=AzureOpenAIChatClient(
endpoint="https://<myresource>.openai.azure.com",
credential=AzureCliCredential(),
ai_model_id="gpt-4o-mini"
),
name="Joker",
instructions="You are good at telling jokes.",
chat_message_store_factory=lambda: RedisChatMessageStore(
redis_url="redis://localhost:6379"
)
)
# Use the agent with persistent chat history
thread = agent.get_new_thread()
response = await agent.run("Tell me a joke about pirates", thread=thread)
print(response.text)