[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/10/18 10:57:52 阅读更多 →
【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/10/18 9:01:48 阅读更多 →
嵌入式系统EMIF接口深度解析:SDRAM与异步存储器的配置与调试实战

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

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

2026/10/18 10:58:52 阅读更多 →

最新新闻

Zynq裸机UART升级方案:无JTAG下的固件更新与BOOT.bin烧录

Zynq裸机UART升级方案:无JTAG下的固件更新与BOOT.bin烧录

简介:面向Zynq 7020开发板的裸机UART在线升级方案可运行源码,适合嵌入式开发者、FPGA工程师以及需要在Zynq平台实现固件远程升级的工程师参考。方案重点解决升级失败导致的板砖问题,完整覆盖Multiboot启动回退机制、升级标志位判定、CRC16数据…

2026/10/18 15:54:16 阅读更多 →
UDP组播通信实战:从原理到Python实现与避坑指南

UDP组播通信实战:从原理到Python实现与避坑指南

1. 从一个"发一份、收多份"的真实需求说起很多人第一次接触组播这个概念,是在做设备发现或者状态同步的时候。你手里有十几台设备,它们需要知道彼此的存在,或者某一台设备要同时给所有设备下发一条指令。最直觉的做法是维护一张IP列…

2026/10/18 15:54:16 阅读更多 →
Traefik v2.10+ 国内落地实战:HTTPS/QUIC/动态配置工程化指南

Traefik v2.10+ 国内落地实战:HTTPS/QUIC/动态配置工程化指南

简介:本资源是面向中国开发者的AI原生IDE——Trae CN的深度解析配套代码包,专为软件开发初学者、教育机构学员及中小企业技术团队设计,旨在帮助用户快速理解其架构逻辑、交互模式与工程集成方式。压缩包共3个文件(8KB)…

2026/10/18 15:54:16 阅读更多 →
QB64:现代Windows原生编译器,让QuickBASIC重获工程可用性

QB64:现代Windows原生编译器,让QuickBASIC重获工程可用性

简介:QB64是面向编程初学者、教育工作者及复古开发爱好者的现代化QuickBASIC实现,完美解决经典BASIC代码在现代系统中无法运行、缺乏图形音视频支持与跨平台能力等痛点。资源包共2000个文件,主体为C/C底层源码(如glew.c、freeglut…

2026/10/18 15:54:16 阅读更多 →
WPS连接Zotero:文献引用与参考文献自动生成全攻略

WPS连接Zotero:文献引用与参考文献自动生成全攻略

简介:这份资源面向需要在WPS中高效管理文献的学术写作者与论文作者,聚焦解决WPS与Zotero无法直接联动、引用插入繁琐的问题。包内共2个文件,以inscode与html为主要类型,分别承担配置脚本与说明页面的功能,压缩包整体约…

2026/10/18 15:54:16 阅读更多 →
AI视频模型横向对比2026年9月版:5大开源模型实测数据与选型指南

AI视频模型横向对比2026年9月版:5大开源模型实测数据与选型指南

AI视频模型横向对比2026年9月版:5大开源模型实测数据与选型指南 2026年9月,开源AI视频生成模型已经形成完整梯队。本文横向对比5大可本地部署模型的核心参数、显存需求和实测速度,给出不同显卡的选型建议。文末附工作流模板和模型资源网盘链…

2026/10/18 15:53:16 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →