在MAF中,Workflow的功能节点是一个ExecutorBinding对象,ExecutorBinding可以利用封装的ExecutorFactory来创建一个Executor实例,并利用后者执行具体的操作。由于ExecutorFactory是一个委托类型,所以我们可以通过不同的方式来实现这个委托,从而实现不同的功能节点。MAF提供了很多原生的ExecutorBinding实现,我们就来看看最为常用的几个。
1. 绑定一个Executor实例
ExecutorInstanceBinding是最为直接的一种实现方式,它直接将一个Executor实例绑定到ExecutorBinding上。每次执行这个功能节点时,都会使用这个绑定的Executor实例来执行操作。如下所示:
public record ExecutorInstanceBinding(Executor ExecutorInstance)
: ExecutorBinding(ExecutorInstance.Id, _ => new(ExecutorInstance), ExecutorInstance.GetType(), ExecutorInstance))
{
public override bool SupportsConcurrentSharedExecution
=> ExecutorInstance.IsCrossRunShareable;
public override bool SupportsResetting
=> ExecutorInstance is IResettableExecutor;
protected override async ValueTask<bool> ResetCoreAsync()
{
if (ExecutorInstance is IResettableExecutor resettable)
{
await resettable.ResetAsync().ConfigureAwait(false);
return true;
}
return false;
}
}
除了调用构造函数根据指定的Executor实例来创建一个ExecutorInstanceBinding对象之外,我们还可以通过一个扩展方法来简化这个调用。ExecutorBinding还提供针对Executor类型的隐式转换操作符,这就是我们为什么可以直接将一个Executor作为一个ExecutorBinding来使用的原因,这也是造成很多人只知道Executor而不知道ExecutorBinding的原因所在。
public static ExecutorBinding BindExecutor(this Executor executor)
=>new ExecutorInstanceBinding(executor);
public abstract record ExecutorBinding
{
public static implicit operator ExecutorBinding(Executor executor)=>executor.BindExecutor();
}
2. 在初步设计的Workflow中占一个坑
顾名思义,ExecutorPlaceholder是一个占位符类型的ExecutorBinding,它并不直接绑定一个Executor实例,而是通过一个工厂函数来创建一个Executor实例。Worflow的设计和一般的软件设计流程一样,不能一开始就是关注细节的实现,应该先从整体的设计入手,先把总体的流程绘制出来。此时我们就可以先使用ExecutorPlaceholder来占位,等到后续的设计和实现过程中再来替换成具体的Executor实例。
以如下这个简单的演示程序为例,在Workflow设计的初始阶段,我们只知道整个流程由三个按顺序执行的功能节点组成,但还不清楚每个功能节点的具体实现细节,所以我们先使用ExecutorPlaceholder来占位,来设计一个简单的Workflow流程。待整个流程定型之后,我们才关注每个功能节点的具体实现细节,并创建真正的ExecutorBinding,通过调用WorkflowBuilder的BindExecutor方法根据ID将用来占位的ExecutorPlaceholder替换掉。
using Microsoft.Agents.AI.Workflows;
using System.Diagnostics;
ExecutorBinding foo = new ExecutorPlaceholder("Foo");
ExecutorBinding bar = new ExecutorPlaceholder("Bar");
ExecutorBinding baz = new ExecutorPlaceholder("Baz");
var builder = new WorkflowBuilder(foo)
.AddEdge(source: foo, target: bar)
.AddEdge(source: bar, target: baz)
.WithOutputFrom(baz);
var workflow = builder
.BindExecutor(CreateExecutor("Foo"))
.BindExecutor(CreateExecutor("Bar"))
.BindExecutor(CreateExecutor("Baz"))
.Build();
var run = await InProcessExecution.Default.RunAsync(workflow, new List<string>());
var output = (List<string>)run.NewEvents.OfType<WorkflowOutputEvent>().Single().Data!;
Debug.Assert(output.SequenceEqual(["Foo","Bar","Baz"]));
static ExecutorBinding CreateExecutor(string id)
{
Func<List<string>, List<string>> func = input =>
{
return input.Append(id).ToList();
};
return func.BindAsExecutor(id);
}
ExecutorPlaceholder的定义非常简单。如下面的定义所示,ExecutorBinding只有Id这一个唯一有意义的属性成员。由于它没有用于创建Executor实例的工厂函数,所以它的IsPlaceholder属性返回true。
public record ExecutorPlaceholder(string Id) : ExecutorBinding(Id, null, typeof(Executor), Id)
{
public override bool SupportsConcurrentSharedExecution => false;
public override bool SupportsResetting => false;
public override bool IsSharedInstance => false;
}
public abstract record ExecutorBinding
{
public static implicit operator ExecutorBinding(string id)=>new ExecutorPlaceholder(id);
}
ExecutorBinding提供了针对string类型的隐式转换操作符,最终转换生成的就是一个ExecutorPlaceholder对象,所以创建ExecutorPlaceholder的方式可以更简短,比如上面演示程序中的三个ExecutorPlaceholder可以通过如下的方式来创建:
ExecutorBinding foo = "Foo";
ExecutorBinding bar = "Bar";
ExecutorBinding baz = "Baz";
3. 以Lazy Loading的方式提供Executor实例
由于ExecutorInstanceBinding需要根据现有的Executor来创建,更好的方式是采用Lazy Loading的方式来提供实例,这样可以避免创建永远不会被使用的Executor实例所带来的资源浪费。ConfiguredExecutorBinding是对一个Configured<Executor>对象的封装,泛型类型Configured<TSubject>定义如下。
internal class Configured<TSubject>(Func<ExecutorConfig, string, ValueTask<TSubject>> factoryAsync, string id, object? raw = null)
{
public object? Raw => raw;
public string Id => id;
public Func<ExecutorConfig, string, ValueTask<TSubject>> FactoryAsync => factoryAsync;
public ExecutorConfig Configuration => new ExecutorConfig(Id);
internal Func<string, ValueTask<TSubject>> BoundFactoryAsync
=> (string sessionId) => FactoryAsync(Configuration, sessionId);
}
ExecutorInstanceBinding利用Configured<Executor>对象的BoundFactoryAsync属性返回的委托作为创建Executor的工厂,此委托通过调用构建Configured<Executor>对象时指定的Func<ExecutorConfig, string, ValueTask<Executor>>委托来完成Executor实例的创建,第二个字符串参数表示SessionId。代表Executor配置的ExecutorConfig类型定义如下,它只有一个表示唯一标识的Id属性。ExecutorConfig<TOptions>是ExecutorConfig的一个泛型子类,它在ExecutorConfig的基础上增加了一个Options属性来表示Executor的配置选项。
public class ExecutorConfig(string id)
{
public string Id => id;
}
public class ExecutorConfig<TOptions>(string id, TOptions? options = default(TOptions?)) : ExecutorConfig(id)
{
public TOptions? Options => options;
}
借助于如下这个静态工厂方法FromInstance<TSubject>,我们可以根据一个Executor实例来创建对应的Configured<Executor>对象。
internal static class Configured
{
public static Configured<TSubject> FromInstance<TSubject>(TSubject subject, string? id = null, object? raw = null);
}
ConfiguredExecutorBinding是一个继承自ExecutorBinding的内部类型,下面给出了它的完整定义。从定义中我们可以看到,ConfiguredExecutorBinding用来创建Executor的工厂来源于指定Configured<Executor>对象的BoundFactoryAsync属性。
internal record ConfiguredExecutorBinding(Configured<Executor> ConfiguredExecutor, Type ExecutorType)
: ExecutorBinding(ConfiguredExecutor.Id,
ConfiguredExecutor.BoundFactoryAsync,
ExecutorType,
ConfiguredExecutor.Raw)
public override bool IsSharedInstance { get; } = ConfiguredExecutor.Raw is Executor;
protected override async ValueTask<bool> ResetCoreAsync()
{
if (this.ConfiguredExecutor.Raw is IResettableExecutor resettable)
{
await resettable.ResetAsync().ConfigureAwait(false);
}
return false;
}
public override bool SupportsConcurrentSharedExecution => true;
public override bool SupportsResetting => false;
}
如有创建的Executor具有一个对应的配置选项类型TOptions,那么我们还可以通过如下的扩展方法根据指定的Func<ExecutorConfig<TOptions>, string, ValueTask<TExecutor>>委托来创建一个ConfiguredExecutorBinding对象,其中第二个字符串类型的参数表示SessionId。
public static ExecutorBinding BindExecutor<TExecutor, TOptions>(
this Func<ExecutorConfig<TOptions>, string, ValueTask<TExecutor>> factoryAsync,
string id, TOptions? options = null)
where TExecutor : Executor where TOptions : ExecutorOptions
如下三个重载的扩展函数会帮助我们将指定的Func<string, string, ValueTask<TExecutor>>和Func<ExecutorConfig<TOptions>, string, ValueTask<TExecutor>>委托转换为ConfiguredExecutorBinding对象,前者的两个字符串参数分别表示Executor的ID和SessionId,后者的第一个参数表示Executor的配置选项,第二个参数表示SessionId。
public static ExecutorBinding BindExecutor<TExecutor>(
this Func<string, string, ValueTask<TExecutor>> factoryAsync) where TExecutor : Executor
=> BindExecutor((ExecutorConfig<ExecutorOptions> config, string sessionId) => factoryAsync(config.Id, sessionId), typeof(TExecutor).Name);
public static ExecutorBinding BindExecutor<TExecutor>(
this Func<string, string, ValueTask<TExecutor>> factoryAsync, string id) where TExecutor : Executor
=>BindExecutor((ExecutorConfig<ExecutorOptions> _, string sessionId) => factoryAsync(id, sessionId), id);
public static ExecutorBinding ConfigureFactory<TExecutor, TOptions>(
this Func<ExecutorConfig<TOptions>, string, ValueTask<TExecutor>> factoryAsync, string id, TOptions? options = null) where TExecutor : Executor where TOptions : ExecutorOptions
=> factoryAsync.BindExecutor(id, options);
在下面的演示实例中,我们定义了根据ExecutorId和SessionId来创建Executor实例的工厂函数CreateExecutor,并针对此函数对应委托对象的BindExecutor扩展方法来创建一个ConfiguredExecutorBinding对象。
using Microsoft.Agents.AI.Workflows;
using System.Diagnostics;
Func<string, string, ValueTask<Executor>> executorFactory = CreateExecutor;
var executorBinding = executorFactory.BindExecutor();
var workflow = new WorkflowBuilder(executorBinding)
.WithOutputFrom(executorBinding)
.Build();
var run = await InProcessExecution.Default.RunAsync(workflow: workflow, input: "Hello world!", sessionId: "thread_001");
var output = run.NewEvents.OfType<WorkflowOutputEvent>().Single().Data!;
Debug.Assert(output.Equals("[thread_001]Hello world!"));
static ValueTask<Executor> CreateExecutor(string id, string sessionId)
{
var executor = new FunctionExecutor<string, string>(id,
(input, _,_) => ValueTask.FromResult($"[{sessionId}]{input}"));
return ValueTask.FromResult<Executor>(executor);
}
4. 将AIAgent纳入Workflow
AIAgentBinding绑定的Executor类型为AIAgentHostExecutor,后者是对一个AIAgent对象的封装。也就是说,AIAgentBinding将一个AIAgent作为Workflow的一个功能节点。在正式介绍AIAgentHostExecutor之前,我们先来它对应的配置选项类型AIAgentHostExecutorOptions。
4.1 AIAgentHostExecutorOptions
AIAgentHostExecutorOptions定义了AIAgentHostExecutor的配置选项。这个配置类是典型的AI编排框架核心配置。在使用Workflow对应MAF时,它决定了Agent到底是单打独斗(实时输出),还是受控于工作流(高度拦截、角色重构)。
public sealed class AIAgentHostOptions
{
public bool? EmitAgentUpdateEvents { get; set; }
public bool EmitAgentResponseEvents { get; set; }
public bool InterceptUserInputRequests { get; set; }
public bool InterceptUnterminatedFunctionCalls { get; set; }
public bool ReassignOtherAgentsAsUsers { get; set; } = true;
public bool ForwardIncomingMessages { get; set; } = true;
}
各配置选项属性说明如下:
- EmitAgentUpdateEvents:是否在Agent执行过程中实时发射**流式更新(Streaming)**数据;
- EmitAgentResponseEvents:是否在Agent执行完成后,发射一个最终聚合好的完整响应事件;
- InterceptUserInputRequests:当Agent触发需要人工审批的工具(如
ToolApprovalRequestContent)时,不直接向用户抛出阻塞请求,而是将该请求打包成一条普通消息发回给工作流。由工作流在后台进行逻辑处理或路由; - InterceptUnterminatedFunctionCalls:当 Agent发出了函数调用(
FunctionCallContent)但还没有收到执行结果(FunctionResultContent)时,拦截该状态。同样将其转换为消息交给工作流托管,而不是挂起等待; - ReassignOtherAgentsAsUsers:当有多个Agent共同参与对话时,当前Agent会把其他Agent发出的消息的角色设置为User。这是为了确保当前Agent能够正确理解上下文,把其他Agent吐出的内容当成外部(User)输入来对答;
- ForwardIncomingMessages:在当前Agent的回合开始并生成新消息之前,是否先自动把接收到的前置传入消息向前转发或合并。这是为了保证对话历史的连贯性,确保下游节点能拿到完整的消息链;
4.2 AIAgentHostExecutor
我们在调用构造函数创建AIAgentHostExecutor实例时需要提供一个AIAgent对象和一个AIAgentHostOptions对象。由于AIAgentHostExecutor继承自ChatProtocolExecutor,所以默认注册了收集从上游节点传递的ChatMessage列表的能力。当接收到TurnToken时,重写的TakeTurnAsync方法会将收集的ChatMessage列表作为输入调用AIAgent对象。
internal class AIAgentHostExecutor : ChatProtocolExecutor
{
public AIAgentHostExecutor(AIAgent agent, AIAgentHostOptions options);
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder);
protected override ValueTask TakeTurnAsync(
List<ChatMessage> messages,
IWorkflowContext context,
bool? emitEvents, CancellationToken cancellationToken = default)
}
重写的TakeTurnAsync方法大体的执行流程如下:
- 如果配置选项
ForwardIncomingMessages为true,会调用IWorkflowContext的SendMessageAsync方法将接收到的消息列表转发出去; - 如果
ReassignOtherAgentsAsUsers为true,会将由其他Agent生成的Assistant消息的角色改为User; - 将
ChatMessage列表作为输入调用AIAgent对象,并得到响应消息列表;- 如果
InterceptUserInputRequests为true,并且响应消息列表中包含ToolApprovalRequestContent类型的消息内容,那么就将这些消息内容转换成普通消息并发送给下游节点,而不是直接抛出阻塞请求; - 如果
InterceptUnterminatedFunctionCalls为true,并且响应消息列表中包含FunctionCallContent类型的消息内容但不包含对应的FunctionResultContent类型的消息内容,那么同样将这些消息内容转换成普通消息发给下游节点,而不是挂起等待;
- 如果
- 对响应消息携带的内容进行规范化过滤,只保留如下这些类型的内容:
- TextContent
- DataContent
- UriContent
- FunctionCallContent
- FunctionResultContent
- ToolApprovalRequestContent
- ToolApprovalResponseContent
- HostedFileContent
- ErrorContent
- 调用
IWorkflowContext的SendMessageAsync方法将AgentResponse中的消息列表转发出去; - 如果不包含如下两种未决请求的消息内容,会调用
IWorkflowContext的SendMessageAsync方法发送一个TurnToken移交控制权;
对于针对AIAgent的调用,如果emitEvents参数为true,或者AIAgentHostOptions的EmitAgentUpdateEvents属性为true,那么最终调用的是AIAgent的RunStreamingAsync方法来获取流式更新数据,并会调用IWorkflowContext的YieldOutputAsync方法实时输出产生的AgentResponseUpdate对象。然后组合所有的AgentResponseUpdate生成一个AgentResponse对象。否则直接调用AIAgent的RunAsync方法来获取最终的AgentResponse对象;
4.3 AIAgentBinding
AIAgentBinding可以视为针对AIAgentHostExecutor的ExecutorInstanceBinding。它提供了两个构造函数重载,前者接受一个AIAgent对象和一个可选的AIAgentHostOptions对象,后者接受一个AIAgent对象和一个布尔值来指示是否发射AgentUpdateEvents事件。AIAgentBinding的IsSharedInstance属性返回false,表示每次执行这个功能节点时都会创建一个新的AIAgentHostExecutor实例。
public record AIAgentBinding(AIAgent Agent, AIAgentHostOptions? Options = null)
: ExecutorBinding(
Agent.GetDescriptiveId(),
_ => new(new AIAgentHostExecutor(Agent, Options ?? new())),
typeof(AIAgentHostExecutor),
Agent)
{
public AIAgentBinding(AIAgent agent, bool emitEvents = false)
: this(agent, new AIAgentHostOptions { EmitAgentUpdateEvents = emitEvents })
{ }
public override bool IsSharedInstance => false;
public override bool SupportsConcurrentSharedExecution => true;
public override bool SupportsResetting => false;
}
AIAgentBinding会调用AIAgent的GetDescriptiveId方法来获取绑定的ID,这个ID的格式规则如下:
- 如果
AIAgent的Name属性存在且不为空,那么ID的格式为{AgentName}{AgentId}; - 否则直接将
AgentId作为ID
MAF为AIAgent提供了如下两个扩展方法BindAsExecutor来简化创建AIAgentBinding的调用。ExecutorBinding也提供了针对AIAgent类型的隐式转换操作符,这就是我们为什么可以直接将一个AIAgent作为一个ExecutorBinding来使用的原因。
public static ExecutorBinding BindAsExecutor(this AIAgent agent, AIAgentHostOptions? options = null)
=> new AIAgentBinding(agent, options);
public static ExecutorBinding BindAsExecutor(this AIAgent agent, bool emitEvents)
=> new AIAgentBinding(agent, emitEvents);
public abstract record ExecutorBinding
{
public static implicit operator ExecutorBinding(AIAgent agent)
=> agent.BindAsExecutor();
}
在如下的演示程序中,我们构建了一个由两个节点组成的Workflow,第一个节点是一个AIAgentHostExecutor节点,绑定的Agent会根据输入消息生成一个响应消息列表;第二个节点是一个FunctionExecutor节点,用来输出第一个节点生成的响应消息列表。我们直接将一个AIAgent对象作为AIAgentHostExecutor节点的输入来创建了一个AIAgentBinding对象。
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ClientModel;
dotenv.net.DotEnv.Load();
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;
var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var agent = new OpenAIClient(
new ApiKeyCredential(apiKey),
new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
.GetChatClient(model)
.AsIChatClient()
.AsAIAgent();
var outputMessages = new FunctionExecutor<List<ChatMessage>>(
id: "OutputMessages",
outputTypes: [typeof(List<ChatMessage>)],
handlerAsync: async (input, context, cancellationToken) =>
{
await context.YieldOutputAsync(input);
});
ExecutorBinding executorBinding = agent;
var workflow = new WorkflowBuilder(executorBinding)
.AddEdge(agent, outputMessages)
.WithOutputFrom(outputMessages)
.Build();
var run = await InProcessExecution.Default.RunAsync(
workflow,
new ChatMessage(ChatRole.User, "为战国四大名将排个序"));
var messages = run.NewEvents.OfType<WorkflowOutputEvent>().Last().Data as List<ChatMessage>;
Console.WriteLine(messages?.Last());
输出:
关于战国四大名将(白起、王翦、廉颇、李牧)的排序,历来见仁见智。若综合**战绩杀伤力、战略格局、结局境遇及历史影响力**,以下提供一种有逻辑的排名体系供你参考:
### 第一位:白起(杀神·兵家巅峰)
**定位:歼灭战的鼻祖,冷兵器时代杀伤力的天花板**
* **理由:** 如果将“名将”定义为对战场胜负和地缘格局改变力的极致,白起是独一档的存在。他开创了大规模野战歼灭敌有生力量的先河,不以攻城略地为目标,而以彻底摧毁敌军主力为手段。
* **硬指标:** 伊阙之战斩首24万,鄢郢之战水淹楚国郢都,长平之战坑杀40余万赵国精锐。整个战国战死军人约200万,过半与白起有关。
* **评价:** 如果说其他三位是战术家或战略家,白起是那个时代唯一的“战略威慑武器”。他排在四大名将之首,实至名归。
### 第二位:李牧(边防战神·赵国最后的磐石)
**定位:以步制骑的先驱,天生的防守反击大师**
* **理由:** 李牧的可怕之处在于“全能”。在北方能大破匈奴,解除了数百年的边患(一战斩匈奴十余万骑);在西部能屡次以劣势兵力挫败秦军绝对主力(如肥之战、番吾之战),让王翦都讨不到便宜。
* **境遇含金量:** 李牧接手时的赵国,长平之殇未愈,国力日衰。他是在绝对的逆风局中打出神级操作,若非郭开谗言,秦灭赵至少延后数年。
* **排位逻辑:** 他是唯一能让白起之后的秦国主力屡屡受挫的人,且战法以少胜多,以弱制强,军事艺术含量极高。
### 第三位:王翦(灭国专业户·政治情商极高的统帅)
**定位:稳健的大兵团作战专家,善始善终的智慧型将领**
* **理由:** 王翦不是最天才的,但他是最“稳”的。他的战绩是实打实的灭国:灭赵、灭燕、灭楚,其子王贲灭魏、灭齐,秦国统一大业王翦父子完成大半。
* **特点:** 灭楚之战需兵六十万,体现了他对敌我实力精准到极致的计算。他深谙帝王心术,贪求田宅以自污保命,是四大名将中唯一功高震主却全身而退的。
* **排位逻辑:** 虽然战术华丽度不如白起李牧,但作为灭国总设计师的执行力无出其右。排在李牧之后,是因为其对手多为亡国之军,而李牧对抗的是秦之铁骑。
### 第四位:廉颇(信平君·老成持重的防守堡垒)
**定位:善始却不能善终的宿将,长于攻坚守阵**
* **理由:** 廉颇名气极大,但与前三位的实绩相比稍显“逊色”。其巅峰之战是伐齐破燕,但最著名的却是长平之战的“固垒坚守”。
* **局限:** 面对年轻的白起,他采取守势被赵括替换(非战之罪);相比李牧在内忧外患中逆天翻盘,廉颇的硬仗斩获较少,且晚年受排挤后辗转魏楚,终未再建灭国之功。
* **评价:** 他是顶级的名将,但放在这四人中,更多展现了“老成谋国”的风骨,缺乏那种令人窒息的天才操作。
---
### 总结排序表:
1. **白起**(战神,纯粹的军事毁灭力第一)
2. **李牧**(军神,逆境中的完美防御与反击,含金量最高)
3. **王翦**(军圣,大巧不工的政治型灭国统帅)
4. **廉颇**(军勇,忠勇可嘉的国之干城)
**一句话概括差异:** 白起是**歼灭**,李牧是**奇迹**,王翦是**吞并**,廉颇是**守卫**。如果要论“谁最不能惹”,杀神白起;论“谁最难打败”,战神李牧。
顺便提一下,上面这个例子之所以能在没有显式发送TurnToken的情况下也能向LLM提交输入并得到响应,是因为InProcessExecutionEnvironment的RunAsync能检测到当前调用是否为Chat模式,如果是的话会自动发送一个空的TurnToken来触发Agent节点的执行。至于Chat模式的检测逻辑也非常简单,就是通过ProtocolDescriptor的Accepts属性是否满足如下两个条件之一:
- 接受所有类型的输入;
- 同时接受IEnumerable类型和TurnToken类型的输入

被折叠的 条评论
为什么被折叠?



