在AG-UI详解-04如果构建一个兼容AG-UI协议的Agent中我们通过在一个ASP.NET Core应用中注册了一个绑定指定IChatClient的路由终结点使外界可以通过Web调用的方式以AG-UI协议调用对应的LLM。在这篇文章中我们看看AG-UI客户端又该如何开发。和前面一样我们的目的仅仅是作一个简单的演示并非开发一款真正的功能完备的AG-UI客户端所以我们的客户端建立在一个简单的假设上面比如不考虑工具调用、人机交互、多模态消息内容和中断等。1. 流式读取AG-UI事件我们创建的客户端将用于对接AG-UI详解-04如果构建一个兼容AG-UI协议的Agent创建的Agent后端。由于此后端采用SSE输出AG-UI响应的事件所以我们的客户端的核心功能就是读取这些事件并交给上层应用处理。我们创建的AG-UI客户端的实现非常简单。如下面这个AGUIClient所示唯一的InvokeAsync方法将RunAgentInput对象作为输入。我们将RunAgentInput对象序列化成JSON作为远程调用AG-UI服务地址通过构造函数指定的输入。在得到作为响应的HttpResponseMessage对象后我们利用创建的SseParser采用SSE协议将每个Chunk读出来并反序列化成BaseEvent对象。整个方法以IAsyncEnumerableBaseEvent的形式返回读取的AG-UI事件。publicclassAGUIClient(stringserver):IDisposable{privatereadonlyHttpClient_httpClientnew();publicasyncIAsyncEnumerableBaseEventInvokeAsync(RunAgentInputinput,[EnumeratorCancellation]CancellationTokencancellationToken){varresponseawait_httpClient.PostAsJsonAsync(server,input,cancellationToken);usingStreamstreamawaitresponse.Content.ReadAsStreamAsync(cancellationToken);varsseParserSseParser.Create(stream,(eventType,bytes)JsonSerializer.DeserializeBaseEvent(bytes)!);awaitforeach(SseItemBaseEventiteminsseParser.EnumerateAsync(cancellationToken)){yieldreturnitem.Data;}}publicvoidDispose()_httpClient.Dispose();}2. 利用AGUIClient构建一个聊天应用我们在一个控制台上利用上面定义的这个AGUIClient构建一个简单的聊天应用。如下面代码所示我们通过连接AG-UI详解-04如果构建一个兼容AG-UI协议的Agent构建的Agent后端创建出AGUIClient对象后开启了一个对话循环。为了保持对话上下文我们让输入的RunAgentInput对象共享同一个ThreadId。varthreadId$thread-{Guid.NewGuid()};varrunIndex0;varinputnewRunAgentInput{ThreadIdthreadId,};varisFirstDialogtrue;varsystemMessagenewAGUISystemMessage{Content 你是一个深谙中国古代历史的专家善于根据正史以公正客观的态度于人交流历史问题。 对于用户提出的问题请以简介概括性的语言予以答复字数尽量保持在200字以内。};usingvarclientnewAGUIClient(http://localhost:5566);while(true){Console.Write(\n$ (:q or quit to exit): );varmessageConsole.ReadLine();if(messageis:qorquit){break;}input.RunId$run-{runIndex};input.MessagesisFirstDialog?[systemMessage,newAGUIUserMessage{ContentnewAGUIUserContent(message!)}]:[newAGUIUserMessage{ContentnewAGUIUserContent(message!)}];isFirstDialogfalse;Console.WriteLine();awaitforeach(vareventinclient.InvokeAsync(input,CancellationToken.None)){if(eventisTextMessageContentEventcontentEvent!string.IsNullOrEmpty(contentEvent.Delta)){Console.Write(contentEvent.Delta);}}}我们将用户输入转换成一个AGUIUserMessage对象并添加到RunAgentInput的Messages列表中。对于初始调用我们还会添加一个AGUISystemMessage对象作为系统指令。我们将RunAgentInput对象作为输入调用AGUIClient的InvokeAsync方法并得到一个IAsyncEnumerableBaseEvent对象。在异步迭代中我们输出TextMessageContentEvent事件携带的实时生成的文本。运行程序后我们于AI展开如下这段关于战国国君排名的简单对话。$ (:q or quit to exit): 给战国国君排个序你觉得前四位会有谁 若以“综合国力塑造、制度影响、战略眼光、历史结果”衡量我会把战国国君前四大致排为 1. 秦孝公重用商鞅变法彻底改变秦国结构为统一奠基。 2. 齐威王整顿吏治、广纳谏言齐国一度成为东方最强国。 3. 赵武灵王推行“胡服骑射”完成军事革命使赵国跃升强国。 4. 秦昭襄王在位最长重用范雎、白起持续削弱六国统一已见雏形。 若看个人雄才燕昭王、魏文侯、楚悼王也很突出若看最终成果秦始皇当然最耀眼但严格说已属“战国末—秦朝开端”。 $ (:q or quit to exit): 对于这四位赵雍的结局令人唏嘘你觉得是什么造成的 赵武灵王赵雍的悲剧核心还是“改革成功却未处理好权力交接”。 他通过胡服骑射让赵国崛起但晚年过早让位给幼子赵何自称“主父”形成“两套权威并存”。同时他偏爱长子赵章又未彻底明确继承秩序结果引发“沙丘之乱”。赵章失败后赵成、李兑等人担心主父再起变数最终将赵雍围困沙丘宫活活饿死。 赵雍强于开创却失于收束。他敢打破传统却低估了宗室、贵族与继承问题的破坏力。战国很多名君都能“兴国”但“善终”往往比改革更难。赵武灵王正是典型例子。 $ (:q or quit to exit): 如果他能向其余三位一样处理好继承人的问题地位能否排在田婴齐前面 有可能甚至不少后世史家会倾向把赵武灵王排到齐威王之前。 赵武灵王的“胡服骑射”不仅是强兵更是文明层面的制度突破。他直接改变了中原传统战争方式使赵国从二流国家跃升为能与秦抗衡的强国对战国军事格局影响极大。相比之下齐威王的成就更多是“中兴齐国”制度创新和长期影响力略逊一筹。 但赵雍的问题在于他留下的是“强国”却不是“稳固秩序”。他死后赵国内耗加剧国力虽强却逐渐失去持续扩张能力。而齐威王给齐国留下的政治结构相对稳定还开启了稷下学宫的文化高峰。 所以若赵武灵王能平稳完成权力交接让改革成果延续两三代他的历史地位确实有机会进入“战国第一流君主”的更前列。3. 将AGUIClient进一步封装成IChatClient如果将基于AG-UI的Agent后端视为一个AI服务实际上就是我们可以定义一个连接它的IChatClient实现类型以方便上层应用的使用毕竟AG-UI的BaseEvent还偏底层。为此我们定义了如下这个AGUIChatClient类型。如代码片段所示AGUIChatClient实际上是对上面定义的AGUIClient对象的封装。由于作为AG-UI输入的RunAgentInput需要指定ThreadId对于IChatClient接口的定义来看这个ThreadId要么放在传入的消息中要么放在ChatOptions中我选择了后者。在实现的GetStreamingResponseAsync中我们从ChatOptions中取出ThreadId并将传入的ChatMessage消息列表转换成AGUIMessage消息列表两者组合生成的RunAgentInput作为调用AGUIClient对象InvokeAsync方法的输入。publicclassAGUIChatClient(stringserver):IChatClient{privatereadonlyAGUIClient_clientnew(server);privatereadonlyJsonSerializerOptions_jsonSerializerOptionsAGUIJsonSerializerContext.Default.Options;publicvoidDispose()_client.Dispose();publicasyncTaskChatResponseGetResponseAsync(IEnumerableChatMessagemessages,ChatOptions?optionsnull,CancellationTokencancellationTokendefault){ListAgentResponseUpdateupdates[];awaitforeach(varupdateinGetStreamingResponseAsync(messages,options,cancellationToken)){updates.Add(newAgentResponseUpdate(update));}returnnewChatResponse(updates.ToAgentResponse().Messages);}publicobject?GetService(TypeserviceType,object?serviceKeynull)null;publicIAsyncEnumerableChatResponseUpdateGetStreamingResponseAsync(IEnumerableChatMessagemessages,ChatOptions?optionsnull,CancellationTokencancellationTokendefault){varthreadIdoptions?.AdditionalProperties?.TryGetValuestring(agui-thread-id,outvarvalue)true?value:$thread-{Guid.NewGuid()};threadId??$thread-{Guid.NewGuid()};varrunId$run-{Guid.NewGuid()};varinputnewRunAgentInput{ThreadIdthreadId,RunIdrunId,Messagesmessages.Select(AsAGUIMessages).ToList(),};return_client.InvokeAsync(input,cancellationToken).AsChatResponseUpdatesAsync(_jsonSerializerOptions,cancellationToken)}privateAGUIMessageAsAGUIMessages(ChatMessagemessage){varcontentstring.Join(\n\n,message.Contents.OfTypeTextContent().Select(itit.Text));varrolemessage.Role;returnroleChatRole.System?newAGUISystemMessage{Contentcontent}:roleChatRole.User?newAGUIUserMessage{ContentnewAGUIUserContent(content)}:roleChatRole.Assistant?newAGUIAssistantMessage{Contentcontent}:thrownewArgumentException($ChatMessageType{message.GetType()}is not supported.);}}我们调用如下定义的扩展方法AsChatResponseUpdatesAsync将AGUIClient对象InvokeAsync方法返回的IAsyncEnumerableBaseEvent转换成最终返回的IAsyncEnumerableChatResponseUpdate对象。按照我们之前的假设我们只考虑RunStartedEvent、RunFinishedEvent、RunErrorEvent、TextMessageStartEvent和TextMessageEndEvent这五种AG-UI事件。internalstaticclassChatResponseUpdateAGUIExtensions{privatestaticreadonlyMediaTypeHeaderValue?s_jsonPatchMediaTypenew(application/json-patchjson);privatestaticreadonlyMediaTypeHeaderValue?s_jsonnew(application/json);publicstaticasyncIAsyncEnumerableChatResponseUpdateAsChatResponseUpdatesAsync(thisIAsyncEnumerableBaseEventevents,JsonSerializerOptionsjsonSerializerOptions,[EnumeratorCancellation]CancellationTokencancellationTokendefault){string?conversationIdnull;string?responseIdnull;vartextMessageBuildernewTextMessageBuilder();awaitforeach(varevtinevents.WithCancellation(cancellationToken).ConfigureAwait(false)){switch(evt){caseRunStartedEventrunStarted:conversationIdrunStarted.ThreadId;responseIdrunStarted.RunId;textMessageBuilder.SetConversationAndResponseIds(conversationId,responseId);yieldreturnnewChatResponseUpdate(ChatRole.Assistant,[]){ConversationIdrunStarted.ThreadId,ResponseIdrunStarted.RunId,CreatedAtDateTimeOffset.UtcNow};;break;caseRunFinishedEventrunFinished:yieldreturnnewChatResponseUpdate(ChatRole.Assistant,runFinished.Result?.GetRawText()){ConversationIdconversationId,ResponseIdresponseId,CreatedAtDateTimeOffset.UtcNow};break;caseRunErrorEventrunError:yieldreturnnewChatResponseUpdate(ChatRole.Assistant,[(newErrorContent(runError.Message){ErrorCoderunError.Code})]);break;caseTextMessageStartEventtextStart:textMessageBuilder.AddTextStart(textStart);break;caseTextMessageContentEventtextContent:yieldreturntextMessageBuilder.EmitTextUpdate(textContent);break;caseTextMessageEndEventtextEnd:textMessageBuilder.EndCurrentMessage(textEnd);break;}}}privatesealedclassTextMessageBuilder(){privateChatRole_currentRole;privatestring?_currentMessageId;privatestring?_conversationId;privatestring?_responseId;publicvoidSetConversationAndResponseIds(string?conversationId,string?responseId){_conversationIdconversationId;_responseIdresponseId;}publicvoidAddTextStart(TextMessageStartEventtextStart){if(_currentRole!default||this._currentMessageId!null){thrownewInvalidOperationException(Received TextMessageStartEvent while another message is being processed.);}_currentRoleAGUIChatMessageExtensions.MapChatRole(textStart.Role);_currentMessageIdtextStart.MessageId;}internalChatResponseUpdateEmitTextUpdate(TextMessageContentEventtextContent){returnnewChatResponseUpdate(_currentRole,textContent.Delta){ConversationIdthis._conversationId,ResponseIdthis._responseId,MessageIdtextContent.MessageId,CreatedAtDateTimeOffset.UtcNow};}internalvoidEndCurrentMessage(TextMessageEndEventtextEnd){_currentRoledefault;_currentMessageIdnull;}}}4. 利用AGUIChatClient构建聊天应用我们将前面聊天应用使用的AGUIClient替换成AGUIChatClient这样我们可以直接使用ChatMessage进行交流了。varthreadId$thread-{Guid.NewGuid()};varoptionsnewChatOptions{AdditionalPropertiesnewAdditionalPropertiesDictionary([newKeyValuePairstring,object?(agui-thread-id,threadId)])};varisFirstDialogtrue;varsystemMessagenewChatMessage(ChatRole.System, 你是一个深谙中国古代历史的专家善于根据正史以公正客观的态度于人交流历史问题。 对于用户提出的问题请以简介概括性的语言予以答复字数尽量保持在200字以内。);usingvarchatClientnewAGUIChatClient(http://localhost:5566);while(true){Console.Write(\n$ (:q or quit to exit): );varmessageConsole.ReadLine();if(messageis:qorquit){break;}varuserMessagenewChatMessage(ChatRole.User,message);ListChatMessagemessagesisFirstDialog?[systemMessage,userMessage]:[userMessage];isFirstDialogfalse;Console.WriteLine();awaitforeach(varupdateinchatClient.GetStreamingResponseAsync(messages,options)){foreach(AIContentcontentinupdate.Contents){if(contentisTextContenttextContent){Console.Write(textContent.Text);}}}}这是我们运行程序后进行的一段对话。$ (:q or quit to exit): 可否认为周公旦是传统中国文化的奠基人之一 可以这样认为但需加限定。周公旦并非“传统中国文化”的唯一奠基人却是早期核心塑造者之一。西周初年他制礼作乐、确立宗法与封建秩序并以“敬天保民”强化政治伦理对后世儒家所重视的礼制、德治、名分影响极深。孔子尤其推崇周公称“郁郁乎文哉吾从周”使周公在后世被视为礼乐文明的代表人物。不过中国传统文化的形成还经历了春秋战国诸子、秦汉制度化等长期发展因此更准确的说法是周公旦是中华礼乐政治与儒家传统的重要奠基者之一。 $ (:q or quit to exit): 有没有可能因为周文化的强势导致商王朝尤其是帝辛这段历史包括他本人被错误评价 有这种可能而且学界普遍承认“胜者书写历史”对商亡周兴的叙述影响很大。现存关于帝辛商纣王的主要记载多出自周人及后世儒家文献带有“以暴君证明革命合理性”的政治目的因此诸如“酒池肉林”等故事很可能存在夸张和道德化加工。 但这不等于帝辛一定是“明君”。甲骨文与考古材料显示商后期长期战争频繁、赋役沉重政治压力确实存在。周人能够联合诸侯与部分商人反商也说明商王朝内部并非毫无矛盾。 因此较稳妥的看法是帝辛形象大概率被妖魔化了但商末统治出现严重问题也并非完全虚构。 $ (:q or quit to exit): 但是如何理解“泰誓”记载的伐纣的理由无非是今殷王纣乃用其妇人之言自绝于天毁坏其三正离其王父母弟乃断弃其先祖之乐乃为淫声用变乱正声怡说妇人。作为战斗檄文列举的这些罪行都仅限于此后世史书记载的那些是否是欲加之罪呢 这是研究商周之际时常被讨论的问题。《尚书·泰誓》确实很值得注意其中对帝辛的指责主要集中于“乱礼”“弃祖制”“亲妇人”“坏正声”等并未出现后世最著名的炮烙、酒池肉林等极端暴政叙述。 这说明两点第一西周初年的政治宣传核心是强调纣“失德失礼”因为周人的合法性建立在“天命转移”与礼制正统上第二后世关于纣的暴虐形象很可能是在长期儒家伦理化叙事中不断累积、强化的。 不过也不能据此完全否定商末问题。对古代王朝而言“弃礼”“疏宗族”“重声色”本身就是严重政治指控意味着统治集团离心。周人能迅速灭商也说明商内部确有危机。 因此更合理的理解是早期文献中的纣主要是“失德之君”而后世则逐渐将其塑造成极端暴君两者之间存在明显的历史层累。