我们知道Workflow本质上是一个有向有环图,天生适合表达这种条件循环。所以在这篇文章中,我们将试着将LoopAgent中间件的循环策略采用Workflow来实现。为此我们定义了为Workflow定义了两种类型的Executor,AgentExecutor用来调用指定的AIAgentEvaludationExecutor用来对每次迭代的输出进行评估,并决定循环是否继续。如果决定继续,还需要提供反馈已服务于后续的迭代。

1. 采用Workflow来构建诗词创作Agent

我们现在利用自定义的AgentExecutorEvaludationExecutor来构建一个Workflow。我们会创建分别用来创作和评估的两个AIAgent对象,如下三个字符串常量分别表示它们的系统指令和最终调用Workflow使用的提示词。

const string ComposerInstructions = "你是一个精通宋词创作的智能体,负责根据提供的主题和意境以词牌名**相见欢**创作一首词";
const string ReviwerInstrucctions = """
    你是一个深谙古典文化和诗词的文化大家,请按照如下的标准对创作的这首词进行评价。评语保持200字以内,尽可能简洁。
    
    1. 格律规范 (Metrical Accuracy) —— 30分
    
    检测AI生成的词是否真正符合该词牌的“说明书”。
    
    - 词牌结构(20分):总字数、分片、每句的字数长短必须与词牌名保持一致。
    - 平仄规范(5分):关键位置的平仄(平声、仄声)必须严格符合词牌要求。
    - 押韵规则(5分):检查是否在规定位置押韵,是否混淆了平仄韵,有无出韵。
    
    2. 意境创设 (Imagery & Conception) —— 30分
    
    评估词作是否具备古典美感,是否能画出**画面感**- 意象选用(15分):是否恰当使用了符合古典美学的意象。意象之间是否逻辑自洽,没有现代感违和物。
    - 流派风格(15分):整体意境的深远程度。
    
    3. 语言艺术 (Linguistic Artistry) —— 20分
    
    评估遣词造句的功底和文字的流畅度。
    
    - 用词典雅(10分):遣词造句需有**词味**,杜绝使用现代大白话或过于生硬的拼凑词。
    - 对仗与过片(10分):若词牌要求对仗,需检查是否工整;重点评估**过片**(上下片过渡)是否自然流转,有无断层。
    
    4. 情感寄托与创新 (Emotional Depth & Innovation) —— 20分
    
    评估诗词的灵魂,拒绝纯粹的字词堆砌。
    
    - 情感共鸣(10分):词中所表达的悲欢离合、家国情怀或羁旅之思是否真挚饱满。
    - 陈词翻新(10分):是否只会堆砌********等陈词滥调。优秀的词作应当在传统框架下有独特的视角或新颖的构思。
    """;
const string Prompt = """
    基于如下这首《卫风·氓》的背景和情感基调,创作**一首**宋词。
    
    原文如下:

    氓之蚩蚩,抱布贸丝。匪来贸丝,来即我谋。
    送子涉淇,至于顿丘。匪我愆期,子无良媒。
    将子无怒,秋以为期。
    
    乘彼垝垣,以望复关。不见复关,泣涕涟涟。
    既见复关,载笑载言。尔卜尔筮,体无咎言。
    以尔车来,以我贿迁。
    
    桑之未落,其叶沃若。于嗟鸠兮,无食桑葚!
    于嗟女兮,无与士耽!士之耽兮,犹可说也;
    女之耽兮,不可说也。
    
    桑之落矣,其黄而陨。自我徂尔,三岁食贫。
    淇水汤汤,渐车帷裳。女也不爽,士贰其行。
    士也罔极,二三其德。
    
    三岁为妇,靡室劳矣;夙兴夜寐,靡有朝矣。
    言既遂矣,至于暴怒。兄弟不知,咥其笑矣。
    静言思之,躬自悼矣。
    
    及尔偕老,老使我怨。淇则有岸,隰则有泮。
    总角之宴,言笑晏晏。信誓旦旦,不思其反。
    反是不思,亦已焉哉!
    """;

如下所示的composer代表我们创建的用来根据指定要求进行宋词创建的Agent,reviewer是一个通过调用LLM对作品实施评估的Agent。

var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("OPENAI_BASE_URL")!;

var composer = new OpenAIClient(new ApiKeyCredential(apiKey), new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetChatClient(model: "gpt-5.2-chat")
    .AsIChatClient()
    .AsAIAgent(instructions: ComposerInstructions);

var reviewer = new OpenAIClient(new ApiKeyCredential(apiKey), new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetChatClient(model: "gpt-5.2-chat")
    .AsIChatClient()
    .AsAIAgent(
    name: "SongLyricsReviwer",
    instructions: ReviwerInstrucctions);

EvaludationExecutor节点内部使用一个Func<LoopContext, ValueTask<LoopEvaluation>>委托实施评估,不过这里的LoopContextLoopEvaluation并非LoopAgent涉及的同名类型,而是我们自定义的类型。我们定义了如下的EvaluateAsync方法根据指定的AIAgent针对LoopContext上下文实施评估。我们使用了结构化输出返回一个Review对象,两个字段ScoreFeedback代表评估得分和反馈评语。循环是否继续取决评估得到是否到达80分以上。

async ValueTask<LoopEvaluation> EvaluateAsync(AIAgent agent, LoopContext context)
{
    ChatMessage[] input =
        [
            ..context.InitialMessages,
            ..context.LastResponse.Messages
        ];
    var review = (await agent.RunAsync<Review>(input)).Result;
    return review.Score >= 80
        ? new (false, null)
        : new (true, review.Feeback + $"\n得分:{review.Score}");
}

[Description("针对创作诗词的评估结果")]
public class Review
{
    [Description("得分,采用百分制")]
    public int Score { get; set; }

    [Description("评语")]
    public string Feedback { get; set; } = string.Empty;
}

Worflow的构建和调用体现如下的演示程序中。我们分别根据创作Agent和提供的Func<LoopContext, ValueTask<LoopEvaluation>>委托创建了AgentExecutorEvaluationExecutor。我们将这两个节点添加到WorkflowBuilder中,并在它们之间添加了双向边实现了调用循环。具体来说,从AgentExecutorEvaluationExecutor到之间是一条无条件的DirectEdge,反向则是一条有条件的DirectEdge,路由添加有作为路由消息的LoopSignal对象决定:如果等于LoopSignal.Reinvoke则继续调用,反之则退出。

var agentExecutor = new AgentExecutor(composer);
var evaluationExecutor = new EvaluationExecutor(context => EvaluateAsync(reviewer, context));
var workflow = new  WorkflowBuilder(agentExecutor)
    .AddEdge(agentExecutor, evaluationExecutor)
    .AddEdge<LoopSignal>(evaluationExecutor,agentExecutor, signal=>signal == LoopSignal.Reinvoke)
    .WithOutputFrom(agentExecutor)
    .WithOutputFrom(evaluationExecutor)
    .Build();

var run = await InProcessExecution.Default.RunStreamingAsync(workflow,Prompt);
string? executorId = null;
await foreach (var evt in run.WatchStreamAsync())
{
    if (evt is WorkflowOutputEvent @event)
    {
        if (@event.ExecutorId != executorId)
        {
            executorId = @event.ExecutorId;
            Console.WriteLine($"\n\n{new string('-', 20)}{executorId}{new string('-', 20)}");            
        }
        if (@event.Data is LoopEvaluation evaluation)
        {
            Console.Write(evaluation.Feedback);
        }
        else if (@event is AgentResponseUpdateEvent  updateEvent)
        {
            Console.Write(updateEvent.Data);
        }        
    }
    if (evt is WorkflowErrorEvent errorEvent)
    {
        Console.Write(errorEvent.Exception);
    }
}

我们以流的方式调用Workflow,并通过监听WorkflowOutputEvent事件捕获AgentExecutorEvaluationExecutor创作和评估的内容,具体输入如下所示:

--------------------Agent--------------------
相见欢·淇水旧盟

淇边初见春柔,柳丝稠。
笑把青丝低绾、倚层楼。

旧盟在,人空改,梦难留。
一段伤心都付、晚来秋。

--------------------Evaluator--------------------
此作依《相见欢》小令体式,句读与分片大体合拍,但“笑把青丝低绾、倚层楼”句字数略参差,格律尚欠精严。押“尤”韵较统一,音律流畅。意象取“淇边”“柳丝”“晚秋”等,能呼应《氓》之旧情与弃妇之叹,画面清婉。语言清丽自然,有词家婉约气息,但情感层次偏简,未充分展开从热恋至决绝的深重转折。结句“都付晚来秋”稍近熟语,创新不足。整体含蓄哀婉,尚具词味。
得分:72

--------------------Agent--------------------
相见欢·淇水遗情

当年淇上相逢,语春风。
误把深盟轻许、月明中。

桑阴碧,人情易,恨无穷。
一任残灯孤枕、听秋蛩。

--------------------Evaluator--------------------
此作依《相见欢》小令体制,句式大体合拍,押“风、中、穷、蛩”韵,音律尚协调,但“误把深盟轻许”句平仄略欠斟酌。意象取“淇上”“桑阴”“残灯”“秋蛩”,承《氓》之情事而不直袭原句,颇有婉约词风。上片追忆初逢,下片转入怨悔,过片自然。惟篇幅稍短,情感层次未能充分展开,对女子由痴至醒的复杂心绪表现略浅,创新处亦有限。
得分:78

--------------------Agent--------------------
相见欢·淇岸秋思

昔年携手河桥,雨初消。
脉脉柔情曾向、柳边招。

桑犹在,人空瘦,鬓先凋。
回首旧盟如梦、暮云遥。

从输出可以看出,整个流程经历了3次创作-评估迭代,前面两次评估得到分别是72和78。

2. 将对话历史和上下文写入Workflow上下文

由于针对Agent的调用需要将包括评估反馈的整个对话历史作为输入,具体的评估在当前迭代的循环上下文中进行,所以我们将两者封装在如下所示的LoopState中。我们希望整个状态对于用户是只读的,所以我们直接将LoopState定义成只读结构体。

public readonly record struct LoopState(List<ChatMessage> History, LoopContext LoopContext)
{
    public static LoopState Create(IEnumerable<ChatMessage> initialMessages)
    => new(initialMessages.ToList(), new LoopContext(initialMessages));
    public LoopState Evaluate(AgentResponse lastResponse)
    {
        History.AddRange(lastResponse.Messages);
        return new(History, LoopContext.Evaluate(lastResponse));
    }

    public LoopState Reinvoke(string feedback)
    {
        var feedbackMessage = new ChatMessage(ChatRole.User, feedback);
        feedbackMessage.AdditionalProperties ??= [];
        feedbackMessage.AdditionalProperties["OnBehalfOfMessages"] = true;
        History.Add(feedbackMessage);
        return new(History, LoopContext.Reinvoke(feedback));
    }
}

public record struct LoopContext
{
    public IReadOnlyList<ChatMessage> InitialMessages { get; } = [];
    public AgentResponse LastResponse { get; private init; } = default!;
    public IReadOnlyList<string> Feedback { get; private set; } = [];
    public int Iteration { get; private set; } = 0;
    public LoopContext() { }
    public LoopContext(IEnumerable<ChatMessage> originalMessages)
        => InitialMessages = originalMessages.ToList().AsReadOnly();
    public LoopContext Evaluate(AgentResponse lastResponse) 
        => this with {  LastResponse = lastResponse, Iteration = Iteration + 1 };
    public LoopContext Reinvoke(string feedback)
        => this with { Feedback = [.. Feedback, feedback] };
}

表示循环上下文的LoopContext包含当前迭代次数、最初消息列表、Agent最后的响应和之前的评估反馈。两个方法EvaluateReinvoke分别在AgentExecutorEvaluationExecutor执行之后被调用,前者用于指定Agent的响应,后者用于设置当前的评估反馈。LoopState也定义了对应的方法返回对应的状态对象。

我们会将LoopState放在Workflow的上下文中,所以我们针对IWorkflowContext接口定义了如下两个用来读写LoopState状态的扩展方法。

public static class WorkflowContextExtensions
{
    public static ValueTask<LoopState> ReadLoopStateAsync(this IWorkflowContext context)
        => context.ReadStateAsync<LoopState>(
         key: "LoopState",
         scopeName:"LoopWorkflow");

    public static ValueTask QueueLoopStateAsync(this IWorkflowContext context, LoopState loopState)
        => context.QueueStateUpdateAsync(
         key: "LoopState",
         value: loopState,
         scopeName: "LoopWorkflow");
}

3. 基于信号的路由消息

MAF的Workflow通过消息路由的方式实现流程在节点之间流转。由于我们将需要处理的数据封装在LoopState中,并存储在Workflow上下文,所以当AgentExecutorEvaluationExecutor执行之后需要发送一个简单的信号就可以了。为此我们定义了如下这个LoopSignal类型。

public record LoopSignal(LoopAction Action)
{
    public static readonly LoopSignal Evaluate = new(LoopAction.Evaluate);
    public static readonly LoopSignal Reinvoke = new(LoopAction.Reinvoke);
}

public enum LoopAction
{ 
    Evaluate,
    Reinvoke
}

枚举LoopAction定义的三个选项体现了三种路由场景:

  • EvaluateAgentExecutor在完成完成Agent调用后,路由到EvaluationExecutor实施评估;
  • Reinvoke: 根据评估结果,要求继续调用Agent;

4. AgentExecutor的实现

用于执行Agent的AgentExecutor直接继承自Executor。实现的两个InvokeAsync方法用于完整针对指定AIAgent的初始调用,我们可以指定单纯的字符串提示词或者消息列表。为了获得更好的用户体验,我们以流的形式调用指定的Agent,并通过AgentResponseUpdateEvent实施输出返回的结果。整个调用结束后,我们将原始输入消息和响应封装成LoopState对象,并存储到Workflow上下文中,最后发送一个LoopSignal.Evaluate信号路由到评估节点。

public partial class AgentExecutor(AIAgent agent) : Executor(id:"Agent")
{
    [MessageHandler(Send = [typeof(LoopSignal)])]
    public ValueTask InvokeAsync(string request, IWorkflowContext context)
    => InvokeAsync([new ChatMessage(ChatRole.User, request)], context);

    [MessageHandler(Send = [typeof(LoopSignal)])]
    public async ValueTask InvokeAsync(IEnumerable<ChatMessage> messages, IWorkflowContext context)
    {
        List<AgentResponseUpdate> updates = [];
        await foreach (var update in agent.RunStreamingAsync(messages))
        {
            await context.AddEventAsync(new AgentResponseUpdateEvent(Id, update));
            updates.Add(update);
        }
        var state = LoopState.Create(messages);
        await context.QueueLoopStateAsync(state.Evaluate(updates.ToAgentResponse()));
        await context.SendMessageAsync(LoopSignal.Evaluate);
    }

    [MessageHandler(Send = [typeof(LoopSignal)])]
    public async ValueTask ReinvokeAsync(LoopSignal signal, IWorkflowContext context)
    {
        var state = await context.ReadLoopStateAsync();
        List<AgentResponseUpdate> updates = [];
        await foreach (var update in agent.RunStreamingAsync(state.History))
        {
            await context.AddEventAsync(new AgentResponseUpdateEvent(Id, update));
            updates.Add(update);
        }
        await context.QueueLoopStateAsync(state.Evaluate(updates.ToAgentResponse()));
        await context.SendMessageAsync(LoopSignal.Evaluate);
    }
}

ReinvokeAsync方法用于处理从评估节点重新路由回来的循环调用请求。我们从Workflow上下文中提取出LoopState,并将其对话历史作为输入,以流的形式调用Agent,并以AgentResponseUpdateEvent的形式实时输出响应结果。整个调用结束之后,我们针对最新的响应内容根据LoopState。在完成了针对LoopState的存储后,我们发送一个LoopSignal.Evaluate信号路由到评估节点。

5. EvaluationExecutor的实现

我们简化了表示评估结果的LoopEvaluation的定义,将它定义成如下这个只读的结构体。两个属性成员ContinueFeedback分别表示循环是否继续和提供的反馈。静态字段Stop返回一个Continue属性为falseLoopEvaluation。静态方法Reinvoke根据指定的反馈文本为继续循环创建一个LoopEvaluation

public readonly record struct LoopEvaluation(bool Continue, string? Feedback = null)
{ 
    public static readonly LoopEvaluation Stop = new (false, null);
    public static LoopEvaluation Reinvoke(string feedback)=> new (true, feedback);
}

整个EvaluationExecutor定义如下。我们在构造函数中,我们指定了一个Func<LoopContext,ValueTask<LoopEvaluation>>委托用来完成真正的评估操作,另一个MaxIterations参数用于设置一个最大允许的迭代次数。

public partial class EvaluationExecutor(
    Func<LoopContext,ValueTask<LoopEvaluation>> Evaluator, 
    int MaxIterations = 5): Executor(id: "Evaluator")
{
    [MessageHandler(Send = [typeof(LoopSignal)], Yield = [typeof(IEnumerable<ChatMessage>)])]
    public async ValueTask EvaluateAsync(LoopSignal signal, IWorkflowContext context)
    {
        var state = await context.ReadLoopStateAsync();
        var evaluation = state.LoopContext.Iteration >= MaxIterations
            ? LoopEvaluation.Stop
            : await Evaluator(state.LoopContext);
        if (!evaluation.Continue)
        {
            var messages = new List<ChatMessage>();
            var history = state.History;
            for (var index = state.LoopContext.InitialMessages.Count; index < history.Count; index++)
            {
                messages.Add(history[index]);
            }
            await context.AddEventAsync(new AgentResponseEvent(Id, new AgentResponse(messages)));
        }
        else
        {
            var feedback = evaluation.Feedback ?? throw new InvalidOperationException("No feedback is provided.");
            await context.AddEventAsync(new WorkflowOutputEvent(evaluation, Id));
            await context.QueueLoopStateAsync(state.Reinvoke(feedback));
            await context.SendMessageAsync(LoopSignal.Reinvoke);
        }
    }
}

在用于完成评估的EvaluateAsync中,我们从Workflow上下文中提取出LoopState。如果没有超出迭代阈值,我们会调用评估委托针对LoopContext实施评估。

  • 如果评估结果要求立即终止,我们会从对话历史中提取Agent的响应消息和评估返回消息,并使用它们创建一个AgentResponse对象,最终以AgentResponseEvent进行输出;
  • 否则我们针对评估反馈更新并保存LoopState,发送一个LoopSignal.Reinvoke信号路由到Agent的AgentExecutor节点。
Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐