[AG-UI详解-05]如何构建一个兼容AG-UI协议的客户端
在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): 但是如何理解“泰誓”记载的伐纣的理由无非是今殷王纣乃用其妇人之言自绝于天毁坏其三正离其王父母弟乃断弃其先祖之乐乃为淫声用变乱正声怡说妇人。作为战斗檄文列举的这些罪行都仅限于此后世史书记载的那些是否是欲加之罪呢 这是研究商周之际时常被讨论的问题。《尚书·泰誓》确实很值得注意其中对帝辛的指责主要集中于“乱礼”“弃祖制”“亲妇人”“坏正声”等并未出现后世最著名的炮烙、酒池肉林等极端暴政叙述。 这说明两点第一西周初年的政治宣传核心是强调纣“失德失礼”因为周人的合法性建立在“天命转移”与礼制正统上第二后世关于纣的暴虐形象很可能是在长期儒家伦理化叙事中不断累积、强化的。 不过也不能据此完全否定商末问题。对古代王朝而言“弃礼”“疏宗族”“重声色”本身就是严重政治指控意味着统治集团离心。周人能迅速灭商也说明商内部确有危机。 因此更合理的理解是早期文献中的纣主要是“失德之君”而后世则逐渐将其塑造成极端暴君两者之间存在明显的历史层累。

相关新闻

GraphRAG技术深度解析:知识图谱驱动的检索增强生成

GraphRAG技术深度解析:知识图谱驱动的检索增强生成

GraphRAG技术深度解析:知识图谱驱动的检索增强生成 随着大语言模型在企业知识管理场景的深入应用,传统向量RAG在处理需要多跳推理、全局总结和关系分析的复杂查询时逐渐力不从心。GraphRAG(图检索增强生成)通过将知识库构建为知识…

2026/7/23 18:44:38 阅读更多 →
【Rust自学】1.3. Rust Cargo

【Rust自学】1.3. Rust Cargo

1.3 Rust Cargo 1.3.0. 回顾 1.2. Rust的基本认识与打印“Hello World”文章的末尾提到了只有小型简单的Rust项目适合使用rustc来编译,大型项目需要Cargo,本篇就对Cargo进行详细的介绍。 1.3.1. 什么是Cargo Cargo是Rust的构建系统和包管理工具&…

2026/7/23 18:44:38 阅读更多 →
嵌入式系统EMIF接口深度解析:SDRAM与异步存储器的配置与调试实战

嵌入式系统EMIF接口深度解析:SDRAM与异步存储器的配置与调试实战

1. 项目概述:为什么需要深入理解EMIF?在嵌入式系统开发中,尤其是涉及图像处理、数据采集或运行复杂算法的场景,片上存储(SRAM、Flash)常常捉襟见肘。这时,扩展外部存储器就成了刚需。但直接让CP…

2026/7/23 18:44:38 阅读更多 →

最新新闻

【Rust自学】14.6. 安装二进制crate

【Rust自学】14.6. 安装二进制crate

14.6 安装二进制 crate 14.6.1 从 crates.io 安装二进制 crate 通过 cargo install 命令可以从 crates.io 安装二进制 crate。这并不是为了替换系统包,它应该是 Rust 开发人员安装其他人共享工具的便捷方式。 它的限制是只能安装具有二进制目标(binar…

2026/7/23 18:58:44 阅读更多 →
【Rust自学】15.1. 使用Box<T>智能指针来指向堆内存上的数据

【Rust自学】15.1. 使用Box<T>智能指针来指向堆内存上的数据

15.1 使用Box T 智能指针来指向堆内存上的数据 15.1.1 Box<T> Box<T> 可以被简单地理解为一个箱子。它是最简单的智能指针&#xff0c;允许你把数据存储在堆上&#xff0c;而不是栈上。 具体实现是&#xff1a;Box<T> 在栈上有一小块内存&#xff0c;存放指向…

2026/7/23 18:58:44 阅读更多 →
【Rust自学】15.3. Deref trait Pt.2:隐式解引用转化与可变性

【Rust自学】15.3. Deref trait Pt.2:隐式解引用转化与可变性

15.3 Deref trait Pt.2&#xff1a;隐式解引用转化与可变性 15.3.1 函数和方法的隐式解引用转化 隐式解引用转化&#xff08;deref coercion&#xff09;是为函数和方法提供的一种便捷特性。 它的原理是&#xff1a;假如类型 T 实现了 Deref trait&#xff0c;那么解引用转化…

2026/7/23 18:58:44 阅读更多 →
【Rust自学】15.4. Drop trait:告别手动清理,释放即安全

【Rust自学】15.4. Drop trait:告别手动清理,释放即安全

15.4 Drop trait&#xff1a;告别手动清理&#xff0c;释放即安全 15.4.1 Drop trait 的意义 如果某个类型实现了 Drop trait&#xff0c;就可以让程序员自定义值离开作用域时发生的操作。例如释放文件或网络资源。 在某些语言中&#xff08;比如 C/C&#xff09;&#xff0c;对…

2026/7/23 18:58:44 阅读更多 →
SQL2025 数据库无法编辑

SQL2025 数据库无法编辑

目录 1.问题 2.查找到原因 3.安装最新的SQL管理工具 1.问题 安装了最近的SQL2025&#xff0c;可以查询等操作&#xff0c;但是无法进行编辑 2.查找到原因 SSMS 19.3 官方最高仅支持 SQL Server 2022&#xff0c;不支持 SQL Server 2025 现象&#xff1a;可以正常连接数据…

2026/7/23 18:58:44 阅读更多 →
矩阵账号视频同质化严重,AI 怎么做出差异化内容?

矩阵账号视频同质化严重,AI 怎么做出差异化内容?

一、发现问题&#xff1a;矩阵账号内容同质化导致流量衰退多数电商、MCN运营会搭建多平台、多账号矩阵体系&#xff0c;通过批量发布短视频扩大流量覆盖面。但矩阵运营过程中&#xff0c;普遍存在视频内容高度同质化的问题&#xff0c;多账号发布的视频镜头顺序、内容结构、文案…

2026/7/23 18:57:44 阅读更多 →

日新闻

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;从单点好评到指数级传播&#xff1a;AI副业主理人必须掌握的4层口碑渗透模型&#xff08;含ROI测算表&#xff09; 当AI副业主理人不再仅满足于单次服务交付&#xff0c;而是主动构建可复用、可裂变、可…

2026/7/23 0:00:25 阅读更多 →
AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析

AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析

更多请点击&#xff1a; https://codechina.net 第一章&#xff1a;AI写作开头钩子设计&#xff1a;为什么你的AI文案完读率不足18%&#xff1f;——基于2,346篇A/B测试报告的归因分析 在对2,346篇跨行业AI生成文案的A/B测试数据进行聚类分析后&#xff0c;我们发现&#xff1…

2026/7/23 0:01:26 阅读更多 →
Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南&#xff1a;免费开源的终极点对点安全聊天工具 【免费下载链接】chitchatter Secure peer-to-peer chat that is serverless, decentralized, and ephemeral 项目地址: https://gitcode.com/gh_mirrors/ch/chitchatter Chitchatter是一款革命性的安…

2026/7/23 0:01:26 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中&#xff0c;我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源&#xff0c;还是配置文件、证书等&#xff0c;都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下&#xff0c;但这…

2026/7/22 8:58:19 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP&#xff08;轻量级目录访问协议&#xff09;作为企业级身份认证的黄金标准&#xff0c;已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时&#xff0c;发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/22 19:43:43 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”&#xff0c;而是以可解释、可审计、可迭代的方式&#xff0c;赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/23 17:49:47 阅读更多 →

月新闻