儲存體

儲存控制對話歷史紀錄的存放位置、載入的歷史紀錄量,以及會話恢復的可靠性。

內建儲存模式

代理框架支援兩種一般儲存模式:

模式 儲存的內容 典型用法
地方會期狀態 完整聊天紀錄位於AgentSession.state(例如透過InMemoryHistoryProvider 不需要伺服器端對話持久性的服務
服務代管儲存空間 服務中的對話狀態; AgentSession.service_session_id 指向它 具備原生持續對話支援的服務

記憶體內聊天記錄儲存

當提供者不需要伺服器端的聊天記錄時,Agent Framework 會在會話中本地保存歷史,並在每次執行時發送相關訊息。

AIAgent agent = new OpenAIClient("<your_api_key>")
    .GetChatClient(modelName)
    .AsAIAgent(instructions: "You are a helpful assistant.", name: "Assistant");

AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));

// When in-memory chat history storage is used, it's possible to access the chat history
// that is stored in the session via the provider attached to the agent.
var provider = agent.GetService<InMemoryChatHistoryProvider>();
List<ChatMessage>? messages = provider?.GetMessages(session);
from agent_framework import InMemoryHistoryProvider
from agent_framework.openai import OpenAIChatClient

agent = OpenAIChatClient().as_agent(
    name="StorageAgent",
    instructions="You are a helpful assistant.",
    context_providers=[InMemoryHistoryProvider("memory", load_messages=True)],
)

session = agent.create_session()
await agent.run("Remember that I like Italian food.", session=session)

Go 透過 agent.HistoryProvider將本機聊天記錄儲存在 agent.Session中。 如果你沒有設定歷史提供者,Agent Framework 會建立一個預設的記憶體內提供者,當你通過明確的本地會話時會使用。 當你想要穩定的來源 ID 或自訂篩選器時,可以明確設定一個。

history := agent.NewInMemoryHistoryProvider(agent.InMemoryHistoryProviderConfig{
    SourceID: "chat_history",
})

a := foundryprovider.NewAgent(endpoint, token, foundryprovider.ModelDeployment(model), foundryprovider.AgentConfig{
    Instructions: "You are a helpful assistant.",
    Config: agent.Config{
        Name:            "StorageAgent",
        HistoryProvider: history,
    },
})

session, err := a.CreateSession(ctx)
if err != nil {
    panic(err)
}

_, err = a.RunText(ctx, "Remember that I like Italian food.", agent.WithSession(session)).Collect()
_, err = a.RunText(ctx, "What kind of food do I like?", agent.WithSession(session)).Collect()

縮小記憶體歷史大小

如果歷史資料過大超出模型限制,則可使用縮減器。

AIAgent agent = new OpenAIClient("<your_api_key>")
    .GetChatClient(modelName)
    .AsAIAgent(new ChatClientAgentOptions
    {
        Name = "Assistant",
        ChatOptions = new() { Instructions = "You are a helpful assistant." },
        ChatHistoryProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
        {
            ChatReducer = new MessageCountingChatReducer(20)
        })
    });

使用 HistoryProvider 過濾器限制下一次請求載入的歷史訊息數量。 例如,只保留最近的 20 則歷史訊息:

history := agent.NewInMemoryHistoryProvider(agent.InMemoryHistoryProviderConfig{
    SourceID: "chat_history",
    ProvideOutputMessageFilter: func(_ context.Context, messages []*message.Message) ([]*message.Message, error) {
        if len(messages) <= 20 {
            return messages, nil
        }

        return messages[len(messages)-20:], nil
    },
})

對於語意或有標記的縮減,應在執行前使用壓縮策略,而非僅依賴訊息計數。

備註

縮減器配置適用於記憶體內歷史提供者。 對於由服務管理的歷史紀錄,減少行為取決於提供者或服務的具體要求。

服務代管儲存空間

當服務管理對話歷史時,會話會儲存遠端對話識別碼。

對於 OpenAI 的回應和對話,服務端 ID(例如 resp_*conv_*)是不透明的,且預設會限定在其所屬的 API 金鑰或專案範圍內。 當該金鑰或專案已經被限定為某個應用程式、使用者或租戶時,這通常就足夠了。 如果你為多個終端使用者架設代理,使用相同的後盾金鑰或專案,請將這些 ID 存放在受信任的伺服器端儲存中,並從你自己的會話 ID 對應,並在恢復對話前確認所有權。

AIAgent agent = new OpenAIClient("<your_api_key>")
    .GetOpenAIResponseClient(modelName)
    .AsAIAgent(instructions: "You are a helpful assistant.", name: "Assistant");

AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));

// In this case, since we know we are working with a ChatClientAgent, we can cast
// the AgentSession to a ChatClientAgentSession to retrieve the remote conversation
// identifier.
ChatClientAgentSession typedSession = (ChatClientAgentSession)session;
Console.WriteLine(typedSession.ConversationId);
# Rehydrate when the service already has the conversation state.
session = agent.get_session(service_session_id="<service-conversation-id>")
response = await agent.run("Continue this conversation.", session=session)

Go 將提供者專屬的對話識別碼儲存在 session.ServiceID()。 當你需要恢復服務管理歷史時,使用現有的服務對話 ID 建立一個會話:

session, err := a.CreateSession(ctx, agent.WithServiceID("<service-conversation-id>"))
if err != nil {
    panic(err)
}

_, err = a.RunText(ctx, "Continue this conversation.", agent.WithSession(session)).Collect()

當服務提供者在執行期間建立或更新遠端對話識別碼時,會話會被更新,通話結束後你可以檢查:

fmt.Println(session.ServiceID())

在服務管理會話中,已設定的本地歷史提供者會被跳過,因此該服務仍是對話歷史的來源。

每次服務呼叫的本地歷史紀錄保存

工具呼叫執行可在單一 agent.run() 模型完成前進行多個模型呼叫。 預設情況下,本地歷史提供者會在完整運行後持續存在一次。 如果你希望本地歷史能更好地反映由服務管理的對話,可以將require_per_service_call_history_persistence=True設置為讓歷史提供者在每次模型呼叫時運行。

from agent_framework import Agent, InMemoryHistoryProvider
from agent_framework.openai import OpenAIChatClient

agent = Agent(
    client=OpenAIChatClient(),
    name="StorageAgent",
    instructions="You are a helpful assistant.",
    context_providers=[InMemoryHistoryProvider("memory", load_messages=True)],
    require_per_service_call_history_persistence=True,
)

這很重要

此模式僅用於框架管理的地方歷史。 若執行已綁定至服務管理對話(例如 via session.service_session_idoptions={"conversation_id": ...}),Agent Framework 會觸發錯誤,而非混合兩種持久性模型。

此模式特別適用於中介軟體可在工具呼叫後立即終止的情況下:每次模型呼叫持續保存能保持本地歷史與服務管理對話保持一致。

Go 歷程提供者會在代理程式叫用前後執行。 沒有獨立的每個服務呼叫持續交換器;如果一個工具迴圈在同一次執行中呼叫多個提供者,請在完整執行後持續維持本地歷史,或實作自訂的提供者/中介軟體來滿足應用程式的儲存需求。

第三方/自訂儲存模式

對於資料庫/Redis/blob 備份的歷史,請實作自訂歷史提供者。

主要指引:

  • 將訊息儲存在會話範圍的密鑰下。
  • 將回傳的歷史記錄保持在模型上下文的範圍內。
  • 在會話狀態中持續保留提供者專屬識別碼。

歷史提供者的基底類別為 Microsoft.Agents.AI.ChatHistoryProvider。 歷史提供者參與代理管線,能貢獻或覆寫代理輸入訊息,並可儲存新訊息。 ChatHistoryProvider 具有多個虛擬方法,您可以覆寫從而實作自己的自訂歷史紀錄提供者。 您可以參閱下方的各種實施方案,以了解應該覆寫的部分。

ChatHistoryProvider 狀態

一個 ChatHistoryProvider 實例會附加到代理,所有會話都會使用同一個實例。 這表示 在 ChatHistoryProvider 提供者實例中不應儲存任何特定會話狀態。 欄位 ChatHistoryProvider 裡可能有資料庫用戶端的參照,但在欄位中不應該有用於聊天紀錄的資料庫金鑰。

相反地,ChatHistoryProvider 可能會儲存任何會話特定的值,例如資料庫鍵、訊息或其他與 AgentSession 本身相關的內容。 虛擬 ChatHistoryProvider 方法皆會傳遞一個參考至當前 AIAgentAgentSession

為了方便地在AgentSession中儲存型別狀態,提供了一個工具類別:

// First define a type containing the properties to store in state
internal class MyCustomState
{
    public string? DbKey { get; set; }
}

// Create the helper
var sessionStateHelper = new ProviderSessionState<MyCustomState>(
    // stateInitializer is called when there is no state in the session for this ChatHistoryProvider yet
    stateInitializer: currentSession => new MyCustomState() { DbKey = Guid.NewGuid().ToString() },
    // The key under which to store state in the session for this provider. Make sure it does not clash with the keys of other providers.
    stateKey: this.GetType().Name,
    // An optional jsonSerializerOptions to control the serialization/deserialization of the custom state object
    jsonSerializerOptions: myJsonSerializerOptions);

// Using the helper you can read state:
MyCustomState state = sessionStateHelper.GetOrInitializeState(session);
Console.WriteLine(state.DbKey);

// And write state:
sessionStateHelper.SaveState(session, state);

簡單 ChatHistoryProvider 實作

ChatHistoryProvider 簡單的實作通常會覆蓋兩種方法:

  • ChatHistoryProvider.ProvideChatHistoryAsync - 載入相關的聊天紀錄並回傳載入的訊息。
  • ChatHistoryProvider.StoreChatHistoryAsync - 儲存請求與回應訊息,這些訊息都應該是新的。

這裡有一個 ChatHistoryProvider 簡單範例,直接將聊天歷史儲存在會話狀態中。

public sealed class SimpleInMemoryChatHistoryProvider : ChatHistoryProvider
{
    private readonly ProviderSessionState<State> _sessionState;

    public SimpleInMemoryChatHistoryProvider(
        Func<AgentSession?, State>? stateInitializer = null,
        string? stateKey = null)
    {
        this._sessionState = new ProviderSessionState<State>(
            stateInitializer ?? (_ => new State()),
            stateKey ?? this.GetType().Name);
    }

    public override string StateKey => this._sessionState.StateKey;

    protected override ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default) =>
        // return all messages in the session state
        new(this._sessionState.GetOrInitializeState(context.Session).Messages);

    protected override ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
    {
        var state = this._sessionState.GetOrInitializeState(context.Session);

        // Add both request and response messages to the session state.
        var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
        state.Messages.AddRange(allNewMessages);

        this._sessionState.SaveState(context.Session, state);

        return default;
    }

    public sealed class State
    {
        [JsonPropertyName("messages")]
        public List<ChatMessage> Messages { get; set; } = [];
    }
}

進階 ChatHistoryProvider 實作

更進階的實作可以選擇覆寫以下方法:

  • ChatHistoryProvider.InvokingCoreAsync - 在代理呼叫 LLM 並允許修改請求訊息清單之前呼叫。
  • ChatHistoryProvider.InvokedCoreAsync - 在代理呼叫 LLM 後呼叫,允許存取所有請求與回應訊息。

ChatHistoryProvider提供InvokingCoreAsyncInvokedCoreAsync的基礎實作。

InvokingCoreAsync基礎實作的做法如下:

  • 呼叫 ProvideChatHistoryAsync 來取得應用於執行聊天記錄的訊息
  • 對由 返回的Func訊息執行可選過濾器。provideOutputMessageFilterProvideChatHistoryAsync 濾波器 Func 可由 ChatHistoryProvider 建構子提供。
  • 將由 回 ProvideChatHistoryAsync 傳的過濾訊息與呼叫者傳入代理的訊息合併,產生代理請求訊息。 聊天紀錄會加在客服輸入訊息的前面。
  • 蓋章所有已過濾的 ProvideChatHistoryAsync 訊息,並附上來源資訊,表示這些訊息來自聊天歷史。

InvokedCoreAsync底座的功能如下:

  • 檢查執行是否失敗,若失敗則返回,無需進一步處理。
  • 過濾代理請求訊息,排除由 ChatHistoryProvider產生的訊息,因為我們只想儲存新訊息,而非最初由 產生 ChatHistoryProvider 的訊息。 請注意,這個過濾器可以透過storeInputMessageFilter建構子上的ChatHistoryProvider參數來覆寫。
  • 將過濾過的請求訊息及所有回應訊息傳送至 StoreChatHistoryAsync 儲存。

你可以覆蓋這些方法以實現ChatHistoryProvider,不過,這需要實施者適當地自行設計基礎功能。 這裡有一個此類實作的範例。

public sealed class AdvancedInMemoryChatHistoryProvider : ChatHistoryProvider
{
    private readonly ProviderSessionState<State> _sessionState;

    public AdvancedInMemoryChatHistoryProvider(
        Func<AgentSession?, State>? stateInitializer = null,
        string? stateKey = null)
    {
        this._sessionState = new ProviderSessionState<State>(
            stateInitializer ?? (_ => new State()),
            stateKey ?? this.GetType().Name);
    }

    public override string StateKey => this._sessionState.StateKey;

    protected override ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
    {
        // Retrieve the chat history from the session state.
        var chatHistory = this._sessionState.GetOrInitializeState(context.Session).Messages;

        // Stamp the messages with this class as the source, so that they can be filtered out later if needed when storing the agent input/output.
        var stampedChatHistory = chatHistory.Select(message => message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, this.GetType().FullName!));

        // Merge the original input with the chat history to produce a combined agent input.
        return new(stampedChatHistory.Concat(context.RequestMessages));
    }

    protected override ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
    {
        if (context.InvokeException is not null)
        {
            return default;
        }

        // Since we are receiving all messages that were contributed earlier, including those from chat history, we need to filter out the messages that came from chat history
        // so that we don't store message we already have in storage.
        var filteredRequestMessages = context.RequestMessages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory);

        var state = this._sessionState.GetOrInitializeState(context.Session);

        // Add both request and response messages to the state.
        var allNewMessages = filteredRequestMessages.Concat(context.ResponseMessages ?? []);
        state.Messages.AddRange(allNewMessages);

        this._sessionState.SaveState(context.Session, state);

        return default;
    }

    public sealed class State
    {
        [JsonPropertyName("messages")]
        public List<ChatMessage> Messages { get; set; } = [];
    }
}
  • 在 Python 中,只有一個歷史提供者應該使用 load_messages=True
from agent_framework.openai import OpenAIChatClient

history = DatabaseHistoryProvider(db_client)
agent = OpenAIChatClient().as_agent(
    name="StorageAgent",
    instructions="You are a helpful assistant.",
    context_providers=[history],
)

session = agent.create_session()
await agent.run("Store this conversation.", session=session)

在 Go 裡,當你需要資料庫、Redis、blob 或檔案備份的歷史時,可以實現 agent.HistoryProvider 。 由 agent.NewHistoryProvider 建立的預設輔助器會在 Provide 中載入先前訊息,並在 Store 中儲存新的請求/回應訊息。 將任何儲存金鑰保留在會話中,這樣提供者實例才能在不同會話間重複使用。

import (
    "context"
    "fmt"
    "time"

    "github.com/microsoft/agent-framework-go/agent"
    "github.com/microsoft/agent-framework-go/message"
)

type MessageStore interface {
    LoadMessages(context.Context, string) ([]*message.Message, error)
    AppendMessages(context.Context, string, []*message.Message) error
}

func NewDatabaseHistoryProvider(store MessageStore) agent.HistoryProvider {
    const stateKey = "database_history.key"

    historyKey := func(session *agent.Session) string {
        var key string
        if ok, _ := session.Get(stateKey, &key); ok && key != "" {
            return key
        }

        key = fmt.Sprintf("history-%d", time.Now().UnixNano())
        session.Set(stateKey, key)
        return key
    }

    return agent.NewHistoryProvider(agent.HistoryProviderConfig{
        SourceID: "database_history",
        Provide: func(ctx context.Context, invoking agent.InvokingContext) ([]*message.Message, error) {
            session, _ := agent.GetOption(invoking.Options, agent.WithSession)
            if session == nil {
                return nil, nil
            }

            return store.LoadMessages(ctx, historyKey(session))
        },
        Store: func(ctx context.Context, invoked agent.InvokedContext) error {
            session, _ := agent.GetOption(invoked.Options, agent.WithSession)
            if session == nil {
                return nil
            }

            allMessages := make([]*message.Message, 0, len(invoked.RequestMessages)+len(invoked.ResponseMessages))
            allMessages = append(allMessages, invoked.RequestMessages...)
            allMessages = append(allMessages, invoked.ResponseMessages...)

            return store.AppendMessages(ctx, historyKey(session), allMessages)
        },
    })
}

將客製化供應商附加到代理人上:

a := foundryprovider.NewAgent(endpoint, token, foundryprovider.ModelDeployment(model), foundryprovider.AgentConfig{
    Instructions: "You are a helpful assistant.",
    Config: agent.Config{
        Name:            "StorageAgent",
        HistoryProvider: NewDatabaseHistoryProvider(store),
    },
})

不要將已設定的本地 HistoryProvider 工作區與服務管理的會話合併。 使用本地歷史儲存或提供者遠端對話狀態來設定特定會話。

重啟後繼續維持會話

持久化整個會話物件,而不只是訊息文字。

JsonElement serialized = agent.SerializeSession(session);
// Store serialized payload in durable storage.
AgentSession resumed = await agent.DeserializeSessionAsync(serialized);
serialized = session.to_dict()
# Store serialized payload in durable storage.
resumed = AgentSession.from_dict(serialized)

會話可以透過 JSON 序列化來持久化。 儲存整個 agent.Session,而不只是訊息文字或歷史記錄鍵。

data, err := json.Marshal(session)
if err != nil {
    panic(err)
}
if err := os.WriteFile("session.json", data, 0o644); err != nil {
    panic(err)
}

loaded, err := os.ReadFile("session.json")
if err != nil {
    panic(err)
}

var resumed agent.Session
if err := json.Unmarshal(loaded, &resumed); err != nil {
    panic(err)
}

_, err = a.RunText(ctx, "Continue this conversation.", agent.WithSession(&resumed)).Collect()

對於以資料庫為後端的儲存,請將工作階段序列化為 []byte,並使用你偏好的後端加以儲存:

data, _ := json.Marshal(session)
db.Set(sessionID, data)

data, _ := db.Get(sessionID)
var resumed agent.Session
_ = json.Unmarshal(data, &resumed)

小提示

完整範例請參閱 第三方會話儲存範例

這很重要

AgentSession 視為不透明狀態物件,並使用創建它的相同代理程式/提供者設定來還原它。 將序列化的會話及任何服務端會話 ID 作為受信任的應用程式狀態儲存。 在託管或多租戶應用程式中,請將每個儲存的會話綁定給已認證的使用者或租戶,然後再允許它恢復。

小提示

使用額外的審計/評估歷史提供者(load_messages=Falsestore_context_messages=True)來捕捉豐富的上下文及輸入/輸出,而不影響主要歷史載入。

後續步驟