儲存控制對話歷史紀錄的存放位置、載入的歷史紀錄量,以及會話恢復的可靠性。
內建儲存模式
代理框架支援兩種一般儲存模式:
| 模式 | 儲存的內容 | 典型用法 |
|---|---|---|
| 地方會期狀態 | 完整聊天紀錄位於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_id 或 options={"conversation_id": ...}),Agent Framework 會觸發錯誤,而非混合兩種持久性模型。
此模式特別適用於中介軟體可在工具呼叫後立即終止的情況下:每次模型呼叫持續保存能保持本地歷史與服務管理對話保持一致。
Go 歷程提供者會在代理程式叫用前後執行。 沒有獨立的每個服務呼叫持續交換器;如果一個工具迴圈在同一次執行中呼叫多個提供者,請在完整執行後持續維持本地歷史,或實作自訂的提供者/中介軟體來滿足應用程式的儲存需求。
第三方/自訂儲存模式
對於資料庫/Redis/blob 備份的歷史,請實作自訂歷史提供者。
主要指引:
- 將訊息儲存在會話範圍的密鑰下。
- 將回傳的歷史記錄保持在模型上下文的範圍內。
- 在會話狀態中持續保留提供者專屬識別碼。
歷史提供者的基底類別為 Microsoft.Agents.AI.ChatHistoryProvider。
歷史提供者參與代理管線,能貢獻或覆寫代理輸入訊息,並可儲存新訊息。
ChatHistoryProvider 具有多個虛擬方法,您可以覆寫從而實作自己的自訂歷史紀錄提供者。
您可以參閱下方的各種實施方案,以了解應該覆寫的部分。
ChatHistoryProvider 狀態
一個 ChatHistoryProvider 實例會附加到代理,所有會話都會使用同一個實例。
這表示 在 ChatHistoryProvider 提供者實例中不應儲存任何特定會話狀態。
欄位 ChatHistoryProvider 裡可能有資料庫用戶端的參照,但在欄位中不應該有用於聊天紀錄的資料庫金鑰。
相反地,ChatHistoryProvider 可能會儲存任何會話特定的值,例如資料庫鍵、訊息或其他與 AgentSession 本身相關的內容。 虛擬 ChatHistoryProvider 方法皆會傳遞一個參考至當前 AIAgent 和 AgentSession。
為了方便地在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提供InvokingCoreAsync及InvokedCoreAsync的基礎實作。
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=False、 store_context_messages=True)來捕捉豐富的上下文及輸入/輸出,而不影響主要歷史載入。