[Loop Engineering在MAF中的实现-01]基于评估结果针对Agent的循环调用
Loop Engineering是2026年人工智能圈最火爆的核心概念,它指的是设计、运营和持续改进反馈循环(Feedback Loops)的AI工程化实践。NVIDIA 首席执行官黄仁勋将其形容为定义2026下半年AI发展的重大转变为:现在没人写提示词了,大家都在编写和管理循环(Loops)。它的核心思想是让你从一个向AI发送指令的人,变成一个设计自动运转系统,让AIAgent自主完成任务并自我修正的工程师。其实Loop Engineering没有很多说得那么玄乎,这篇文章我们会通过一个简单的例子通过几行代码实现针对Agent的循环调用。
1. Loop Engineering概述
这两年来,AI圈子没几天就会出现一个新词,加上很多人为了博取流量,与不惊人死不休,动不动就是XXX已死、YYY当立,导致很多人觉得:怎么昨天我刚听前沿技术,今天就成了明日黄花了。我前几天看了一篇文章的标题就是《Harness Engineering已死、Loop Engineering才是王道》,简直莫名其妙。不过很多人将AI工程演化分成四代:
- Prompt Engineering
- Context Engineering
- Harness Engineering
- Loop Engineering
我个人是不同意这种说法的。还记得定义Harness Engineering的如下这个公式吗?按照这个公式,Harness Engineering本质上就是在LLM基础上构建理想Agent的所有工程化手段。而LLM本身就是独立的,那岂不是Harness Engineering = Agent Engineering,我们应用在Agent上所有设计模式都是一种Harness手段,自然也包括Prompt Engineering和Context Engineering优化提示词和上下文的手段,也包括Loop Engineering根据动态评估反馈自动调用Agent的手段。
Agent = LLM + Harness
所以我的意思是,如果我们认同Harness Engineering的说法,意味着在它之前以及之后出现的所有Engineering都是它的子集,自然也包括Loop Engineering。 这种说法可以在MAF中得到印证:用于实现Loop Engineering的LoopAgent中间件已经集成在HarnessAgent之中。
在Loop Engineering之前,我们是这样使用Agent的:
- 我们采用提示词来描述推理任务,并将其作为输入调用Agent,而且一般我们会使用Session保持与Agent对话的语境;
- 单次Agent的调用一般会在一个ReAct循环中执行,并最终输出响应;
- 我们根据输出响应,再次撰写提示词作为输入调用Agent。
整个过程完全是由用户主导,一切由输入的提示词来驱动。Loop Engineering引入了一种基于评估反馈的迭代机制,我们通过注入一个评估器对每一轮调用的结果进行评估,并由评估结果决定是否继续调用Agent。如果评估器决定继续,它一般会提供评估结果(比如理由和建议)并作为下次Agent调用的输入。
2. 利用MAF的LoopAgent实现针对Agent的循环调用
Loop Engineering在MAF中是通过LoopAgent这个继承自DelegatingAgent的Agent中间件实现的,它已经被集成在HarnessAgent中,当然它也可以被单独使用。接下来我们将使用它实现如下的应用场景:我们创建一个用于宋词创作的Agent,它能根据我们提供的主题和题材创作一首宋词。我们同时创建一个评估器,它能对每一轮Agent的创作结果进行评估,并根据评估结果决定是否继续创作。整个循环调用的过程如下:
- 调用Agent进行创作;
- 评估器对创作结果进行评估,评估的结果包括一个得分和一段评语:
- 如果得到达到执行的分数,整个循环结束;
- 否则,针对反馈的评语,重新调用Agent进行创作。
如下所示的是这个评估器类型ScoreBasedEvaluator的定义。它继承自LoopEvaluator,构造函数接收一个AIAgent对象作为评估器和一个目标分数。在重写得EvaluateAsync方法中,它将当前LoopContext中的初始消息和最后一次响应消息作为输入,调用评估器进行评估。我们通过结构化输出的方式让AIAgent返回一个Review类型的对象,它包含一个整数类型的得分和一个字符串类型的评语。
class ScoreBasedEvaluator(AIAgent reviwer, int score) : LoopEvaluator
{
public override async ValueTask<LoopEvaluation> EvaluateAsync(LoopContext context, CancellationToken cancellationToken = default)
{
ChatMessage[] input =
[
..context.InitialMessages,
..context.LastResponse.Messages
];
var review = (await reviwer.RunAsync<Review>(input)).Result;
Console.WriteLine($"""
{new string('-', 30)}Iteration{context.Iteration}{new string('-', 30)}
{context.LastResponse}
得分: {review.Score}
评语:{review.Feeback}
""");
return review.Score >= score
? LoopEvaluation.Stop()
: LoopEvaluation.Continue(review.Feeback);
}
}
[Description("针对创作诗词的评估结果")]
public class Review
{
[Description("得分,采用百分制")]
public int Score { get; set; }
[Description("评语")]
public string? Feeback { get; set; }
}
在得到作为评估结果的Review对象后:
- 如果得分达到预设的阈值,
LoopEvaluator将返回通过调用LoopEvaluation.Stop()方法创建的LoopEvaluation对象,整个循环调用结束; - 如果得分未达到预设的阈值,
LoopEvaluator将评语组为反馈调用LoopEvaluation.Continue(feedback)方法,并返回该方法创建的LoopEvaluation对象,整个循环调用继续,并将评语作为下一轮Agent调用的输入。
如下三个常量分别定义了创作Agent的提示词、评审Agent的提示词和整个循环调用的初始输入。创作Agent的提示词要求它根据提供的主题和意境以词牌名相见欢创作一首词。评审Agent的提示词要求它根据四个维度对创作的这首词进行评价,并给出一个得分和一段评语。初始输入是《卫风·氓》的原文。
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 = """
基于如下这首《卫风·氓》的背景和情感基调,分别创作一首一首宋词。
原文如下:
氓之蚩蚩,抱布贸丝。匪来贸丝,来即我谋。
送子涉淇,至于顿丘。匪我愆期,子无良媒。
将子无怒,秋以为期。
乘彼垝垣,以望复关。不见复关,泣涕涟涟。
既见复关,载笑载言。尔卜尔筮,体无咎言。
以尔车来,以我贿迁。
桑之未落,其叶沃若。于嗟鸠兮,无食桑葚!
于嗟女兮,无与士耽!士之耽兮,犹可说也;
女之耽兮,不可说也。
桑之落矣,其黄而陨。自我徂尔,三岁食贫。
淇水汤汤,渐车帷裳。女也不爽,士贰其行。
士也罔极,二三其德。
三岁为妇,靡室劳矣;夙兴夜寐,靡有朝矣。
言既遂矣,至于暴怒。兄弟不知,咥其笑矣。
静言思之,躬自悼矣。
及尔偕老,老使我怨。淇则有岸,隰则有泮。
总角之宴,言笑晏晏。信誓旦旦,不思其反。
反是不思,亦已焉哉!
""";
整个程序其实很简单,首先我们创建两个AIAgent,一个是创作Agent,一个是评审Agent,后者用来创建作为评估器的ScoreBasedEvaluator对象,并指定一个分数阈值(85分)。然后我们将创作Agent和评估器封装在LoopAgent中。最后我们调用LoopAgent的RunAsync方法,并传入初始输入Prompt。整个循环调用的过程会在控制台打印出每一轮的创作结果、评审结果和得分。
var composer = CreateAgent(
name: "SongLyricsComposer",
model: "gpt-5.2-chat",
instructions: ComposerInstructions);
var reviewer = CreateAgent(
name: "SongLyricsReviwer",
model: "gpt-5.2-chat",
instructions: ReviwerInstrucctions);
var agent = new LoopAgent(composer, new ScoreBasedEvaluator(reviewer, 85));
var response = await agent.RunAsync(Prompt);
var index = 1;
foreach (var message in response.Messages)
{
Console.WriteLine($"""
{new string('-', 20)}Message{index++}{new string('-', 20)}
{message}
""");
}
AIAgent CreateAgent(string name,string instructions, string model)
{
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("PROJECT_URL")!;
return new AIProjectClient(new Uri(endpoint), new AzureCliCredential())
.AsAIAgent(
name: name,
model: model,
instructions: instructions);
}
整个输出总体分为两段,第一段是每次迭代后调用评估器的输出,包含创作结果、得分和评语。从如下的输出可以看出,整个流程经历了两次迭代,第一次评估得分为78分,评语指出了创作中存在的问题,并给出了改进建议。第二次迭代后,评估得分为86分,评语表明创作已经达到了预期的标准,整个循环调用结束。
------------------------------Iteration1------------------------------
相见欢
淇桥初见匆匆。意朦胧。
笑把秋期轻许、趁东风。
桑阴翠,流年坠,人情空。
回首当时盟誓、已成鸿。
得分: 78
评语:此作依《相见欢》小令体式,句读大体齐整,然“笑把秋期轻许、趁东风”字数稍近散文化,平仄亦未尽合谱,韵脚统一尚可。意象取“淇桥”“桑阴”等,能扣《氓》本事,前后情绪递转自然。下片“流年坠,人情空”有兴衰之感,结句“盟誓已成鸿”含怨而不滞。语言清婉流畅,但部分措辞略近今人口吻,词味尚可再凝炼;情感层次由相悦至幻灭,虽较含蓄,仍具余韵。
------------------------------Iteration2------------------------------
相见欢
淇边细雨初晴。柳烟轻。
曾约复关南路、候君行。
桑青后,花零久,两心惊。
一片当时明月、照空盟。
得分: 86
评语:此作依《相见欢》小令体制,句式大体合拍,押“轻、行、惊、盟”庚青韵,尚称稳妥。上片写淇边候约,点出《氓》旧事;下片以“桑青后,花零久”暗承桑叶兴衰,意象简洁而有层次。语言清婉自然,有宋词婉约之致。“一片当时明月、照空盟”收束含怨不露,颇有余味。惟情感推进稍快,对负心与悔悟之沉痛着墨略浅,过片亦可更曲折深婉。
第二段是LoopAgent最终响应包含的消息。一共包括两条消息,第一条为第一次创作的结果,第二条为评估器给出的反馈,第三条为第二次创作的结果。由于此次传作业已经达到了预期的标准,整个循环调用结束。
--------------------Message1--------------------
相见欢
淇桥初见匆匆。意朦胧。
笑把秋期轻许、趁东风。
桑阴翠,流年坠,人情空。
回首当时盟誓、已成鸿。
--------------------Message2--------------------
此作依《相见欢》小令体式,句读大体齐整,然“笑把秋期轻许、趁东风”字数稍近散文化,平仄亦未尽合谱,韵脚统一尚可。意象取“淇桥”“桑阴”等,能扣《氓》本事,前后情绪递转自然。下片“流年坠,人情空”有兴衰之感,结句“盟誓已成鸿”含怨而不滞。语言清婉流畅,但部分措辞略近今人口吻,词味尚可再凝炼;情感层次由相悦至幻灭,虽较含蓄,仍具余韵。
--------------------Message3--------------------
相见欢
淇边细雨初晴。柳烟轻。
曾约复关南路、候君行。
桑青后,花零久,两心惊。
一片当时明月、照空盟。
3. 直接使用HarnessAgent实现Loop Engineering
我们已经说过,LoopAgent已经被集成在HarnessAgent中,所以我们可以直接使用HarnessAgent来实现Loop Engineering。如下所示,我们创建了一个用于创作的IChatClient对象,和一个用于评估的AIAgent对象,后者被用于创建一个ScoreBasedEvaluator对象。然后我们将创作的IChatClient对象和评估器封装在HarnessAgent中。
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("OPENAI_BASE_URL")!;
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);
var chatClient = new OpenAIClient(new ApiKeyCredential(apiKey), new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
.GetChatClient(model: "gpt-5.2-chat")
.AsIChatClient();
var agent = new HarnessAgent(chatClient, new HarnessAgentOptions
{
Name = "SongLyricsComposer",
HarnessInstructions = ComposerInstructions,
LoopEvaluators = [new ScoreBasedEvaluator(reviewer,85)],
DisableWebSearch = true,
DisableAgentModeProvider = true,
DisableTodoProvider = true,
DisableFileMemory = true,
DisableFileAccess = true,
});
var response = await agent.RunAsync(Prompt);
var index = 1;
foreach (var message in response.Messages)
{
Console.WriteLine($"""
{new string('-', 20)}Message{index++}{new string('-', 20)}
{message}
""");
}
由于HarnessAgent会整和很多用于实现响应Harness手段的中间件,对此由兴趣的可以参考我的文章MAF的Harness-02:HarnessAgent究竟整合了哪些Harness手段?。为了屏蔽它们的干扰,我们在给定的HarnessAgentOptions中禁用了某些中间件。程序运行之后,也能得到我们希望的输出(这次进行了4次迭代):
------------------------------Iteration1------------------------------
《相见欢·淇上初盟》
垝垣斜日轻红,望归鸿。犹记顿丘携手、语匆匆。
桑叶碧,春水急,意相从。谁料旧盟难守、各西东。
《相见欢·桑落》
桑黄淇水东流,冷汀洲。三载辛勤空付、旧时柔。
灯半灭,人未歇,泪还收。当日旦旦深誓、总成秋。
得分: 78
评语:两首皆能扣合《氓》之情事,由初盟至决绝,层次清楚。《相见欢》体式基本合拍,句读亦稳,押“ong”“ou”韵较自然。上阕“垝垣斜日”“桑黄淇水”皆有《诗经》遗意,画面感尚佳。然词味稍浅,“旧盟难守”“空付旧时柔”近于直白,少含蓄蕴藉。过片承转尚顺,但对人物情绪的递进尚未深挖,未能尽显《氓》由痴恋、怨怼至决绝的复杂波澜。整体清婉流畅,具宋词小令风致。
------------------------------Iteration2------------------------------
《相见欢·复关》
垝垣斜倚残曛,望归人。淇水悠悠不断、暮云深。
桑未落,鬓先损,泪沾襟。当日笑言犹在、却难寻。
《相见欢·已焉》
桑阴摇雨初秋,冷汀洲。帘外淇波犹自、向东流。
灯影薄,人语寂,恨难休。誓语当年如梦、一回收。
得分: 78
评语:两首皆能扣《氓》之弃妇哀怨,意象如“淇水”“桑阴”“垝垣”承袭《诗经》本色,情绪流转自然,有宋词婉约之致。《相见欢》词牌字数与分片基本合格,押“人、深、襟、寻”“秋、洲、流、休、收”亦较稳。然平仄未尽严谨,个别句法略近小令常套。上篇“鬓先损”较直白,下篇“誓语当年如梦”稍近熟语。整体胜在情感真切,能由盼望写至决绝,保留《氓》原作由爱生怨的层次。
------------------------------Iteration3------------------------------
《相见欢·淇上》
轻寒细雨孤村,掩重门。犹忆送郎淇上、语温存。
桑渐老,水空远,暮烟昏。回首复关何处、月微痕。
《相见欢·弃盟》
秋声吹冷蘅皋,夜迢迢。独倚疏灯无语、听寒潮。
当日意,而今恨,总难消。淇岸有涯人事、竟无聊。
得分: 82
评语:两首皆能扣《氓》之情脉,由相恋而怨别,意绪连贯。《相见欢》词牌字数大体合式,句读亦稳,然“犹忆送郎淇上”“独倚疏灯无语”等处平仄未尽严谨,韵脚稍嫌单调。意象承《诗经》之“淇水”“桑”“复关”,古典气息较足,尤“月微痕”“淇岸有涯”有余味。语言清婉流畅,但个别句略近小令常套,创新稍弱。第二首结句“人事、竟无聊”情感到位,然“无聊”略近今语,减损词味。总体能得哀而不怨之旨。
------------------------------Iteration4------------------------------
《相见欢·望复关》
晚云低护荒台,雨声催。独倚残垣凝望、雁初回。
桑叶尽,人情改,镜尘堆。惟有淇流如旧、去难回。
《相见欢·决绝》
露凉风紧蘅皋,夜萧骚。旧约依稀还似、梦魂遥。
灯影暗,佩声杳,恨迢迢。淇岸纵宽终有、此情消。
得分: 86
评语:两首皆合《相见欢》小令体制,句读齐整,押平声韵较稳,格律基本无失。上篇以“残垣”“雁回”“淇流”等意象承《氓》之望复关、情渐冷,画面清婉;下篇转入决绝,蘅皋、灯影、佩声等语颇具词味。语言凝练,无明显俗句,过片自然。惟情感层次稍简,未能充分展开《氓》中由热恋至怨弃的复杂心理;“镜尘堆”“梦魂遥”等语略近常套,创新度尚可再进。
--------------------Message1--------------------
《相见欢·淇上初盟》
垝垣斜日轻红,望归鸿。犹记顿丘携手、语匆匆。
桑叶碧,春水急,意相从。谁料旧盟难守、各西东。
《相见欢·桑落》
桑黄淇水东流,冷汀洲。三载辛勤空付、旧时柔。
灯半灭,人未歇,泪还收。当日旦旦深誓、总成秋。
--------------------Message2--------------------
两首皆能扣合《氓》之情事,由初盟至决绝,层次清楚。《相见欢》体式基本合拍,句读亦稳,押“ong”“ou”韵较自然。上阕“垝垣斜日”“桑黄淇水”皆有《诗经》遗意,画面感尚佳。然词味稍浅,“旧盟难守”“空付旧时柔”近于直白,少含蓄蕴藉。过片承转尚顺,但对人物情绪的递进尚未深挖,未能尽显《氓》由痴恋、怨怼至决绝的复杂波澜。整体清婉流畅,具宋词小令风致。
--------------------Message3--------------------
《相见欢·复关》
垝垣斜倚残曛,望归人。淇水悠悠不断、暮云深。
桑未落,鬓先损,泪沾襟。当日笑言犹在、却难寻。
《相见欢·已焉》
桑阴摇雨初秋,冷汀洲。帘外淇波犹自、向东流。
灯影薄,人语寂,恨难休。誓语当年如梦、一回收。
--------------------Message4--------------------
两首皆能扣《氓》之弃妇哀怨,意象如“淇水”“桑阴”“垝垣”承袭《诗经》本色,情绪流转自然,有宋词婉约之致。《相见欢》词牌字数与分片基本合格,押“人、深、襟、寻”“秋、洲、流、休、收”亦较稳。然平仄未尽严谨,个别句法略近小令常套。上篇“鬓先损”较直白,下篇“誓语当年如梦”稍近熟语。整体胜在情感真切,能由盼望写至决绝,保留《氓》原作由爱生怨的层次。
--------------------Message5--------------------
《相见欢·淇上》
轻寒细雨孤村,掩重门。犹忆送郎淇上、语温存。
桑渐老,水空远,暮烟昏。回首复关何处、月微痕。
《相见欢·弃盟》
秋声吹冷蘅皋,夜迢迢。独倚疏灯无语、听寒潮。
当日意,而今恨,总难消。淇岸有涯人事、竟无聊。
--------------------Message6--------------------
两首皆能扣《氓》之情脉,由相恋而怨别,意绪连贯。《相见欢》词牌字数大体合式,句读亦稳,然“犹忆送郎淇上”“独倚疏灯无语”等处平仄未尽严谨,韵脚稍嫌单调。意象承《诗经》之“淇水”“桑”“复关”,古典气息较足,尤“月微痕”“淇岸有涯”有余味。语言清婉流畅,但个别句略近小令常套,创新稍弱。第二首结句“人事、竟无聊”情感到位,然“无聊”略近今语,减损词味。总体能得哀而不怨之旨。
--------------------Message7--------------------
《相见欢·望复关》
晚云低护荒台,雨声催。独倚残垣凝望、雁初回。
桑叶尽,人情改,镜尘堆。惟有淇流如旧、去难回。
《相见欢·决绝》
露凉风紧蘅皋,夜萧骚。旧约依稀还似、梦魂遥。
灯影暗,佩声杳,恨迢迢。淇岸纵宽终有、此情消。
更多推荐



所有评论(0)