





















和LangChain万法归一的设计哲学不同,MAF在设计上采用了多态的设计哲学,提供了一个Agent基类,通过继承这个基类来创建不同类型的Agent。虽然MAF的Agent类型多种多样,但最重要的莫过于ChatClientAgent,MAF语境下的Agent基本上指的就是这个对象。ChatClientAgent采用管道式设计,它利用一些列可扩展的组件构建了Agent和LLM消息交换的通道,还实现ReAct循环。这个管道之于MAF的重要性,可能比中间件管道对于ASP.NET Core的还要重要。MAF为Agent定义了一个名为AIAgent的基类,由于它涉及到一系列基础类型,所以我们先来看看这些基础类型以及它们在Agent管道中的作用。
与Agent相关的基础类型主要有AgentRunContext、AgentRunOptions、AgentSession和ChatMessage等。这些类型在Agent的运行过程中扮演着重要的角色,承载了运行时的上下文信息、运行选项、会话信息以及消息内容等。理解这些基础类型对于我们深入理解MAF中的Agent设计与实现是非常有帮助的。
AgentRunContext是一个类,包含了当前Agent的运行上下文信息,如下所示:
public sealed class AgentRunContext
{
public AIAgent Agent { get; }
public AgentSession? Session { get; }
public IReadOnlyCollection<ChatMessage> RequestMessages { get; }
public AgentRunOptions? RunOptions { get; }
}
属性成员说明如下:
AgentRunOptions包含了用于控制Agent的运行行为的配置选项:
public class AgentRunOptions
{
public ResponseContinuationToken? ContinuationToken { get; set; }
public bool? AllowBackgroundResponses { get; set; }
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
public ChatResponseFormat? ResponseFormat { get; set; }
}
public class ResponseContinuationToken
{
public static ResponseContinuationToken FromBytes(ReadOnlyMemory<byte> bytes);
public virtual ReadOnlyMemory<byte> ToBytes();
}
属性成员说明如下:
ContinuationToken重新传入,让Agent从中断点继续生成,而不是从头开始;RunAsync来检查任务是否完成并获取最终结果;在MAF中,ContinuationToken是一个十分常见的类型。正如它的名字所体现的那样,如果服务端返回的内容包括这样一个ContinuationToken,以为着还有后续(Continuation),对于一个流来说,这个ContinuationToken是对这个流的某个中间位置的描述,客户端得到这个ContinuationToken后,不仅不知道流尚未结束,还能利用它从正确的位置继续获取数据。对于一个非流式的后台任务来说,这个ContinuationToken用于确定任务尚未结束,还需等待。
ResponseFormat属性类型ChatResponseFormat定义如下。如果我们指定ChatResponseFormat.Text或者ChatResponseFormat.Json,Agent会强制要求LLM返回纯文本(Markdown)或者JSON格式的内容。如果我们调用ChatResponseFormat.ForJsonSchema方法创建一个ChatResponseFormatJson对象作为AgentRunOptions的ResponseFormat属性,Agent会强制要求模型返回符合该JSON Schema的JSON。结构化输出在很多场景下都是非常有用的,比如当我们需要将Agent的输出直接对接到自动化工作流或者结构化数据库时,结构化的输出可以让我们更方便地进行后续的处理和分析。
public class ChatResponseFormat
{
public static ChatResponseFormatText Text { get; } = new ChatResponseFormatText();
public static ChatResponseFormatJson Json { get; } = new ChatResponseFormatJson(null);
public static ChatResponseFormatJson ForJsonSchema(
JsonElement schema,
string? schemaName = null,
string? schemaDescription = null) ;
public static ChatResponseFormatJson ForJsonSchema<T>(
JsonSerializerOptions? serializerOptions = null,
string? schemaName = null,
string? schemaDescription = null) ;
public static ChatResponseFormatJson ForJsonSchema(
Type schemaType,
JsonSerializerOptions? serializerOptions = null,
string? schemaName = null,
string? schemaDescription = null);
}
如下所示的ChatResponseFormatText和ChatResponseFormatJson分别是ChatResponseFormat的两个子类,分别用于表示纯文本格式和JSON格式的响应格式。只有ChatResponseFormatJson与结构化输出有关,ChatResponseFormatText仅仅并不定义文本的结构,单纯地要求采用自然语言输出。
public sealed class ChatResponseFormatText : ChatResponseFormat
{
public override bool Equals(object? obj)
public override int GetHashCode()
}
public sealed class ChatResponseFormatJson : ChatResponseFormat
{
public JsonElement? Schema { get; }
public string? SchemaName { get; }
public string? SchemaDescription { get; }
public ChatResponseFormatJson(JsonElement? schema, string? schemaName = null, string? schemaDescription = null)
}
AgentSession是一个抽象类,代表了一个Agent的会话对象。如果希望多次针对Agent的调用在同一个Session中进行,只需要传递相同的AgentSession即可。其核心成员是AgentSessionStateBag类型的属性StateBag。我们将在很多类型中看到GetService和GetService<TService>看到这两个方法,它来源于宿主程序作为Dependecy Injection容器的IServiceProvider对象。我们会在很多类型中看到这两个方法的身影,之后我们就不再赘述了。
public abstract class AgentSession
{
public AgentSessionStateBag StateBag { get; protected set; }
private string DebuggerDisplay => $"StateBag Count = {StateBag.Count}";
public virtual object? GetService(Type serviceType, object? serviceKey = null) ;
public TService? GetService<TService>(object? serviceKey = null) ;
}
AgentSessionStateBag代表了一个Agent会话状态的容器,它提供了TryGetValue、GetValue、SetValue和TryRemoveValue方法来操作状态数据。它还提供了Serialize和Deserialize方法来实现状态的序列化和反序列化,以便于在分布式环境中进行状态的持久化和恢复。
public class AgentSessionStateBag
{
public int Count {get;}
public bool TryGetValue<T>(
string key,
out T? value,
JsonSerializerOptions? jsonSerializerOptions = null) where T : class
public T? GetValue<T>(
string key,
JsonSerializerOptions? jsonSerializerOptions = null) where T : class
public void SetValue<T>(
string key,
T? value,
JsonSerializerOptions? jsonSerializerOptions = null) where T : class
public bool TryRemoveValue(string key) ;
public JsonElement Serialize();
public static AgentSessionStateBag Deserialize(JsonElement jsonElement);
}
所有的基于对话的Agent都采用消息进行交互,消息不仅承载了多模态的内容,还绑定了一个在作为消息发送发的角色。MAF中的消息通过如下这个ChatMessage类来表示。
public class ChatMessage
{
public string? AuthorName{ get; set; }
public DateTimeOffset? CreatedAt { get; set; }
public ChatRole Role { get; set; }
public string Text => Contents.ConcatText();
public IList<AIContent> Contents{ get; set; }
public string? MessageId { get; set; }
public object? RawRepresentation { get; set; }
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
}
ChatMessage的属性成员说明如下:
Contents中所有内容的文本进行拼接后的结果;AIContent对象,AIContent是一个抽象类,代表了消息内容的基类,可以有不同类型的内容,如文本、图片、文件等;出于可扩展的考虑,Role并不是一个简单的枚举类型,而是一个具有更丰富功能的结构体。它预定义了System、Assistant、User和Tool四个角色,并且允许用户自定义角色。ChatMessage中的Role属性就是利用这个ChatRole结构体来表示消息发送者的角色。
public readonly struct ChatRole : IEquatable<ChatRole>
{
public static ChatRole System { get; } = new ChatRole("system");
public static ChatRole Assistant { get; } = new ChatRole("assistant");
public static ChatRole User { get; } = new ChatRole("user");
public static ChatRole Tool { get; } = new ChatRole("tool");
public string Value { get; }
}
AIContent是MAF 框架中定义一切交互内容的原子基类。它采用高度多态的设计,将AI与用户之间的对话拆解为多种专业化的内容块。在传统的AI开发中,消息通常只有Text。而AIContent将对话模型化为一个多模态、多状态的流。通过JsonDerivedTypeAttribute声明,它支持了19种(且在增加)子类型,涵盖了现代Agent交互的方方面面。
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(DataContent), typeDiscriminator: "data")]
[JsonDerivedType(typeof(ErrorContent), typeDiscriminator: "error")]
[JsonDerivedType(typeof(FunctionCallContent), typeDiscriminator: "functionCall")]
[JsonDerivedType(typeof(FunctionResultContent), typeDiscriminator: "functionResult")]
[JsonDerivedType(typeof(HostedFileContent), typeDiscriminator: "hostedFile")]
[JsonDerivedType(typeof(HostedVectorStoreContent), typeDiscriminator: "hostedVectorStore")]
[JsonDerivedType(typeof(TextContent), typeDiscriminator: "text")]
[JsonDerivedType(typeof(TextReasoningContent), typeDiscriminator: "reasoning")]
[JsonDerivedType(typeof(UriContent), typeDiscriminator: "uri")]
[JsonDerivedType(typeof(UsageContent), typeDiscriminator: "usage")]
[JsonDerivedType(typeof(ToolCallContent), typeDiscriminator: "toolCall")]
[JsonDerivedType(typeof(ToolResultContent), typeDiscriminator: "toolResult")]
[JsonDerivedType(typeof(InputRequestContent), typeDiscriminator: "inputRequest")]
[JsonDerivedType(typeof(InputResponseContent), typeDiscriminator: "inputResponse")]
[JsonDerivedType(typeof(ToolApprovalRequestContent), typeDiscriminator: "toolApprovalRequest")]
[JsonDerivedType(typeof(ToolApprovalResponseContent), typeDiscriminator: "toolApprovalResponse")]
[JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")]
[JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")]
public class AIContent
{
public IList<AIAnnotation>? Annotations { get; set; }
[JsonIgnore]
public object? RawRepresentation { get; set; }
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
}
AIContent的Annotations返回一个AIAnnotation的列表,AIAnnotation是一个注解类,用于为内容提供一些额外的信息或者标记。AIAnnotation也是一个多态类型,通过JsonDerivedTypeAttribute声明了不同的子类型,如CitationAnnotation等。
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(CitationAnnotation), typeDiscriminator: "citation")]
public class AIAnnotation
{
public IList<AnnotatedRegion>? AnnotatedRegions { get; set; }
[JsonIgnore]
public object? RawRepresentation { get; set; }
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
}
public class CitationAnnotation : AIAnnotation
{
public string? Title { get; set; }
public Uri? Url { get; set; }
public string? FileId { get; set; }
public string? ToolName { get; set; }
public string? Snippet { get; set; }
}
CitationAnnotation是AIAnnotation的一个子类,用于表示引用注解,它具有如下的属性成员:
AnnotatedRegion是AIAnnotation中的一个属性成员,它代表了被注解的内容区域,可以是文本区域、图像区域等。AnnotatedRegion同样是一个多态类型,通过JsonDerivedTypeAttribute声明了不同的子类型,如TextSpanAnnotatedRegion等。
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(TextSpanAnnotatedRegion), "textSpan")]
public class AnnotatedRegion
{
}
public sealed class TextSpanAnnotatedRegion : AnnotatedRegion
{
[JsonPropertyName("start")]
public int? StartIndex { get; set; }
[JsonPropertyName("end")]
public int? EndIndex { get; set; }
}
Agent具有两种执行方式,一种是阻塞式调用,它会等到整个执行完全结束后才会得到结果;另一种是流式调用,它会在执行过程中不断地返回结果更新。两者的响应类型分别是AgentResponse和AgentResponseUpdate。如下所示的是AgentResponse的定义,它封装了AI的回答、消耗统计、任务状态以及上下文标识等。
public class ChatResponse
{
public IList<ChatMessage> Messages{ get; set; }
public string Text { get; }
public string? ResponseId { get; set; }
public string? ConversationId { get; set; }
public string? ModelId { get; set; }
public DateTimeOffset? CreatedAt { get; set; }
public ChatFinishReason? FinishReason { get; set; }
public UsageDetails? Usage { get; set; }
public ResponseContinuationToken? ContinuationToken { get; set; }
public object? RawRepresentation { get; set; }
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
public ChatResponseUpdate[] ToChatResponseUpdates();
}
属性成员说明如下:
ChatMessage的列表;Messages中所有消息的文本内容进行拼接后的结果;AllowBackgroundResponses,ChatResponse可能会先返回。此时ContinuationToken不为空。你需要利用这个令牌传进行轮询,直到任务真正完成并拿到最终结果;ChatResponse 是开发者直接打交道最多的类。它不仅告诉你AI说了什么(Messages/Text),还告诉你怎么继续聊 (ConversationId);任务完没完 (ContinuationToken);花了多少钱 (Usage);模型跑得怎么样 (FinishReason)。
和ChatRole一样,表示响应的结束原因的ChatFinishReason也是一个具有更丰富功能的结构体,而不是一个简单的枚举类型。它预定义了Stop、Length、ToolCalls和ContentFilter四个结束原因,并且允许用户自定义结束原因。
public readonly struct ChatFinishReason(string value) : IEquatable<ChatFinishReason>
{
public string Value {get;} ;
public static ChatFinishReason Stop { get; } = new ChatFinishReason("stop");
public static ChatFinishReason Length { get; } = new ChatFinishReason("length");
public static ChatFinishReason ToolCalls { get; } = new ChatFinishReason("tool_calls");
public static ChatFinishReason ContentFilter { get; } = new ChatFinishReason("content_filter");
}
Usage属性返回的UsageDetails相当于一本账单,详细记录了模型调用的各种花费。我们知道Token就是AI领域的货币,所以这本账单使用Token数量来记账,它记录了模型调用过程中的各种Token数量,包括输入输出的Token数量、是否命中缓存、推理过程中使用的Token数量等。这些信息对于我们了解模型的使用情况、优化提示词和控制成本都非常有帮助。
比如,如果我们发现输入Token数量过多,可能需要优化提示词来减少不必要的上下文;如果输出Token数量过多,可能需要调整模型的生成策略来控制输出的长度;如果命中缓存的Token数量较多,说明模型在重复使用之前的结果,可能需要调整提示词来引导模型生成更多新的内容;如果推理过程中使用的Token数量较多,可能需要优化模型的推理过程来减少不必要的计算。总之,UsageDetails提供了一个全面的视角来审视模型的使用情况,帮助我们更好地管理和优化AI的使用。
public class UsageDetails
{
public long? InputTokenCount { get; set; }
public long? OutputTokenCount { get; set; }
public long? TotalTokenCount { get; set; }
public long? CachedInputTokenCount { get; set; }
public long? ReasoningTokenCount { get; set; }
public AdditionalPropertiesDictionary<long>? AdditionalCounts { get; set; }
public void Add(UsageDetails usage)
}
针对流式调用的每次迭代,响应的内容都会封装成一个ChatResponseUpdate对象,ChatResponse的ToChatResponseUpdates属性返回这些对象的数组。ChatResponseUpdate与ChatResponse的属性基本相同,唯一的区别是它多了一个IsFinal属性来表示当前响应是否是最终的响应。当IsFinal为true时,意味着当前响应是整个Agent执行过程中的最后一个响应,此时ChatResponseUpdate中的内容就是最终的结果;当IsFinal为false时,意味着当前响应只是整个Agent执行过程中的一个中间更新,此时ChatResponseUpdate中的内容可能只是部分结果或者一些临时的状态信息。
public class ChatResponseUpdate
{
public string? AuthorName{ get; set; }
public ChatRole? Role { get; set; }
public string Text{ get;}
public IList<AIContent> Contents{ get; set; }
public object? RawRepresentation { get; set; }
public string? ResponseId { get; set; }
public string? MessageId { get; set; }
public string? ConversationId { get; set; }
public DateTimeOffset? CreatedAt { get; set; }
public ChatFinishReason? FinishReason { get; set; }
public string? ModelId { get; set; }
public ResponseContinuationToken? ContinuationToken{ get; set; }
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
}
很多LLM都支持结构化输出。我们在调用LLM的通过提供描述输出的JSON Schema,LLM会按照这个JSON Schema来生成符合要求的JSON格式的输出。为了让开发者更方便地处理这种结构化的输出,MAF提供了ChatResponse<T>这个泛型类,它继承自ChatResponse,并添加了一个Result属性来表示解析后的结果对象。
public class ChatResponse<T> : ChatResponse
{
public T Result {get;}
public ChatResponse(ChatResponse response, JsonSerializerOptions serializerOptions);
public bool TryGetResult([NotNullWhen(true)] out T? result);
}
AIAgent是MAF中所有Agent的基类,定义了所有Agent共享的基本属性成员和操作方式。它提供了Agent运行的核心方法,如RunAsync和RunStreamingAsync等,以及与AgentSession相关的方法,如CreateSessionAsync、SerializeSessionAsync和DeserializeSessionAsync等。通过继承AIAgent,我们可以创建不同类型的Agent来满足不同的需求。
AIAgent是MAF中所有Agent的基类,定义了所有Agent共享的基本属性成员和操作方式。
public abstract class AIAgent
{
public string Id { get; }
protected virtual string? IdCore => null;
public virtual string? Name { get; }
public virtual string? Description { get; }
public static AgentRunContext? CurrentRunContext { get; }
}
属性成员说明如下:
Id:Agent的唯一标识符,通常由IdCore属性生成;AIAgent定义了三个与AgentSession相关的方法,分别是CreateSessionAsync、SerializeSessionAsync和DeserializeSessionAsync方法。分别实现了针对AgentSession的创建、序列化和反序列化功能。它们分别调用了对应的CreateSessionCoreAsync、SerializeSessionCoreAsync和DeserializeSessionCoreAsync方法来实现具体的功能,这些方法是抽象方法,需要在子类中进行实现。
public abstract class AIAgent
{
public ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default);
public ValueTask<JsonElement> SerializeSessionAsync(
AgentSession session,
JsonSerializerOptions? jsonSerializerOptions = null,
CancellationToken cancellationToken = default);
public ValueTask<AgentSession> DeserializeSessionAsync(
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null,
CancellationToken cancellationToken = default);
protected abstract ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default);
protected abstract ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session,
JsonSerializerOptions? jsonSerializerOptions = null,
CancellationToken cancellationToken = default);
protected abstract ValueTask<AgentSession> DeserializeSessionCoreAsync(
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null,
CancellationToken cancellationToken = default);
}
针对AIAgent的调用分两种,一种是阻塞式调用,另一种是异步流式调用。阻塞式调用通过如下这些重载的RunCoreAsync方法来完成,返回的AgentResponse的作为响应内容。我们可以在调用它们时指定绑定的AgentSession将当前调用纳入一个会话中。还可以指定AgentRunOptions来设置一些运行时的参数,如是否启用流式输出、是否启用工具调用等,如果没有指定将会采用默认的运行选项。四个重载体现了不同的输入形式,分别是不指定输入以及将字符串、ChatMessage和ChatMessage的集合。这四个重载的RunAsync方法最终都会调用同一个RunCoreAsync方法来完成具体的执行逻辑,而这个RunCoreAsync方法是一个抽象方法,需要在子类中进行实现。
public abstract class AIAgent
{
public Task<AgentResponse> RunAsync(
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
=> RunAsync([], session, options, cancellationToken);
public Task<AgentResponse> RunAsync(
string message,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
=> RunAsync(new ChatMessage(ChatRole.User, message), session, options, cancellationToken);
public Task<AgentResponse> RunAsync(
ChatMessage message,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
=> RunAsync([message], session, options, cancellationToken);
public Task<AgentResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
CurrentRunContext = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
return this.RunCoreAsync(messages, session, options, cancellationToken);
}
protected abstract Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
}
AIAgent提供的RunStreamingAsync方法来进行流式调用。与阻塞式调用不同,流式调用会在执行过程中不断地产生结果,而不是等到整个执行完全结束后才返回结果。RunStreamingAsync方法返回一个IAsyncEnumerable<AgentResponseUpdate>对象,AgentResponseUpdate代表了Agent在执行过程中的一个更新,可以是一个新的响应、一个工具调用的结果或者一个状态更新等。
与RunAsync方法类似,RunStreamingAsync方法也提供了四个重载,分别是不指定输入以及将字符串、ChatMessage和ChatMessage的集合。这些重载的RunStreamingAsync方法最终都会调用同一个RunCoreStreamingAsync方法来完成具体的执行逻辑,而这个RunCoreStreamingAsync方法是一个抽象方法,需要在子类中进行实现。
public abstract class AIAgent
{
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) ;
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
string message,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
ChatMessage message,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
public async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
protected abstract IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default);
}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。