不阻塞、不等待:让agent 像后台服务一样持续运行
目录序言1. 核心痛点无状态 Web vs 长任务 AI2. 开启“后台模式”3. 轮询与“存盘”The Loop4. 状态持久化 (Persistence)5. 后台运行下的工具调用6. 请求链路变化7. microsoft/agent-framework框架中OpenAI 集成8. 什么是 OpenAI 协议OpenAI Protocols什么时候使用哪种协议使用 Responses API 的场景推荐使用 Chat Completions API 的场景Chat Completions API9. 框架背后是如何处理的Responses API 调用链Chat Completions API 调用链总结序言在开发 GenAI 应用时我们经常会遇到一个很现实、也很尴尬的场景。用户发来一个复杂指令比如“写一本关于火星殖民的长篇小说”“分析这 50 份 PDF 文档给我总结结论”然后前端就开始 loading。 如果这个任务要跑一两分钟普通的 HTTP 请求基本已经超时用户也很可能已经关掉页面走人了。这个问题在 Demo 里并不明显但一旦进入真实业务场景几乎是绕不开的。1. 核心痛点无状态 Web vs 长任务 AIWeb 服务通常是无状态的。如果 AI 正在写小说写到一半这时服务重启或者遇到其他不可抗拒的因素上下文就会直接丢失。同样地如果 AI 正在执行任务的过程中客户端断开连接当前的执行状态也无法继续保留。而 GenAI 恰恰最常见的需求是一次任务持续很久。这就和无状态 Web 的执行模型产生了天然的冲突。那么有没有一种方法能够在不依赖长连接的情况下维持 AI 任务的运行状态答案是有的我们继续往下看。2. 开启“后台模式”在开始之前我们仍然需要引入如下包dotnet add package Azure.AI.OpenAI --version 2.8.0-beta.1 dotnet add package Azure.Identity --version 1.17.1 dotnet add package Microsoft.Agents.AI.Hosting.OpenAI --version 1.0.0-alpha.251219.1 dotnet add package Microsoft.Agents.AI.OpenAI --version 1.0.0-preview.251219.1针对这种场景Agent Framework 提供了相对便捷的处理方式。在初始化 Agent 运行时这里需要稍微注意一点我们使用的是GetResponsesClient()方法后面会单独解释同时需要将AllowBackgroundResponses设置为true以允许 Agent 在后台运行。AIAgent agent new AzureOpenAIClient( new Uri(endpoint), new AzureCliCredential()) .GetResponsesClient(deploymentName) .CreateAIAgent( name: SpaceNovelWriter, instructions: 你是一名太空题材小说作家。 在写作之前始终先研究相关的真实背景资料并为主要角色生成角色设定。 写作时直接完成完整章节不要请求批准或反馈。 不要向用户询问语气、风格、节奏或格式偏好——只需根据请求直接创作小说。, tools: [ AIFunctionFactory.Create(ResearchSpaceFactsAsync), AIFunctionFactory.Create(GenerateCharacterProfilesAsync) ]); // 允许 Agent 在后台运行 AgentRunOptions options new() { AllowBackgroundResponses true };3. 轮询与“存盘”The Loop这是整个 Demo 中最关键的部分。它不再是简单地await agent.RunAsync()然后一直等待结果而是通过一个循环把一次长任务拆解成多次可恢复的执行过程// 发起任务 AgentRunResponse response await agent.RunAsync(写一本超长的太空小说..., thread, options); // 只要还有 ContinuationToken说明任务没结束 while (response.ContinuationToken is not null) { // 1. 存盘把当前线程状态和令牌存起来比如存到数据库或 Redis PersistAgentState(thread, response.ContinuationToken); // 2. 休息这里模拟客户端断开连接或者 Serverless 函数释放资源 await Task.Delay(TimeSpan.FromSeconds(10)); // 3. 读盘重新恢复 Agent 状态 RestoreAgentState(agent, out thread, out ResponseContinuationToken? continuationToken); // 4. 继续带着令牌去问 AI 你写完了吗 options.ContinuationToken continuationToken; response await agent.RunAsync(thread, options); // 继续运行 }4. 状态持久化 (Persistence)注意看PersistAgentState和RestoreAgentState。在这个 Demo 里它用了一个 Dictionary 模拟数据库这就把一个长连接任务拆成了多次极短的无状态请求。5. 后台运行下的工具调用即使 Agent 在后台运行依然可以正常触发工具调用。在这个 Demo 中Agent 在写小说之前会自动调用ResearchSpaceFactsAsync查资料GenerateCharacterProfilesAsync生成角色设定这些操作本身可能就比较耗时示例中模拟了 10 秒延迟。但由于我们引入了“存盘 / 读盘”机制即使中途网络断开Agent 在恢复之后依然能够记得自己已经完成了哪些步骤而不需要从头再来。运行效果如下视频演示地址6. 请求链路变化细心的朋友可能发现我们这里创建 Agent 的方式和之前不太一样。GetResponsesClient(deploymentName).CreateAIAgent(...)而不是GetChatClient(deploymentName).CreateAIAgent(...)那么这是为什么呢? 我们就抛开迷雾看本质7. microsoft/agent-framework框架中OpenAI 集成microsoft/agent-framework 框架允许你通过兼容 OpenAI 协议的 HTTP 接口来暴露 AI Agent同时支持 Chat Completions API 和 Responses API。这使你可以将你的 Agent 与任何兼容 OpenAI 协议的客户端或工具进行集成。8. 什么是 OpenAI 协议OpenAI Protocolsmicrosoft/agent-framework 支持两种 OpenAI 协议Chat Completions API标准的、无状态的请求 / 响应格式用于聊天交互Responses API更高级的格式支持对话管理、流式输出以及长时间运行的 Agent 过程什么时候使用哪种协议根据 OpenAI 官方文档Responses API 已成为默认且推荐的方式。它提供了更完整、功能更丰富的接口适合构建现代 AI 应用内置会话管理流式输出长时间运行任务支持使用 Responses API 的场景推荐构建新应用默认推荐需要服务端对话管理但不是强制的Responses API 也可以以无状态方式使用需要持久化的对话历史构建长时间运行的 Agent需要更高级的流式能力包含详细事件类型需要跟踪和管理单个 Response例如通过 ID 获取某次响应、检查状态、取消正在运行的响应使用 Chat Completions API 的场景迁移依赖 Chat Completions 格式的旧系统只需要简单、无状态的请求 / 响应状态管理完全由客户端负责集成只支持 Chat Completions 的现有工具需要最大程度兼容遗留系统Chat Completions APIChat Completions API 提供了一个简单、无状态的接口使用标准 OpenAI Chat 格式与 Agent 交互。9. 框架背后是如何处理的Responses API 调用链下面是部分源代码我们调用了GetResponsesClient方法来实际返回ResponsesClient。下面代码位于 Azure.AI.OpenAI 包中的AzureOpenAIClient.cs类中例子中我们只拿出了一个方法其它方法省略public partial class AzureOpenAIClient : OpenAIClient { public override ResponsesClient GetResponsesClient(string deploymentName) { Argument.AssertNotNullOrEmpty(deploymentName, nameof(deploymentName)); return new AzureResponsesClient(Pipeline, deploymentName, _endpoint, _options); } } // 而 AzureOpenAIClient 的父类 OpenAIClient 类位于 OpenAI NuGet 包中下面代码是AzureResponsesClient的源码位于 Azure.AI.OpenAI NuGet 包中的AzureResponsesClient.cs类中internal partialclassAzureResponsesClient : ResponsesClient { privatereadonly Uri _aoaiEndpoint; privatereadonlystring _deploymentName; privatereadonlystring _apiVersion; internal AzureResponsesClient( ClientPipeline pipeline, string deploymentName, Uri endpoint, AzureOpenAIClientOptions options) : base( pipeline, model: deploymentName, new OpenAIClientOptions() { Endpoint endpoint }) { Argument.AssertNotNull(pipeline, nameof(pipeline)); Argument.AssertNotNull(endpoint, nameof(endpoint)); options ?? new(); _aoaiEndpoint endpoint; _deploymentName deploymentName; _apiVersion options.GetRawServiceApiValueForClient(this); } } // 而 ResponsesClient 类位于 OpenAI NuGet 包中的 ResponsesClient.cs 类中接下来我们继续看OpenAIResponseClientExtensions.cs类。该类是ResponsesClient的扩展类内部定义了两个重载的CreateAIAgent方法其中包含client.AsIChatClient()方法。该方法返回一个IChatClient接口OpenAIResponsesChatClient实现了该接口。/// OpenAIResponseClientExtensions 源码 publicstaticclassOpenAIResponseClientExtensions { public static ChatClientAgent CreateAIAgent( this ResponsesClient client, string? instructions null, string? name null, string? description null, IListAITool? tools null, FuncIChatClient, IChatClient? clientFactory null, ILoggerFactory? loggerFactory null, IServiceProvider? services null) { Throw.IfNull(client); return client.CreateAIAgent( new ChatClientAgentOptions() { Name name, Description description, ChatOptions tools isnull string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions() { Instructions instructions, Tools tools, } }, clientFactory, loggerFactory, services); } public static ChatClientAgent CreateAIAgent( this ResponsesClient client, ChatClientAgentOptions options, FuncIChatClient, IChatClient? clientFactory null, ILoggerFactory? loggerFactory null, IServiceProvider? services null) { Throw.IfNull(client); Throw.IfNull(options); var chatClient client.AsIChatClient(); if (clientFactory is not null) { chatClient clientFactory(chatClient); } returnnew ChatClientAgent(chatClient, options, loggerFactory, services); } }下面代码是 Microsoft.Extensions.AI NuGet 包中OpenAIClientExtensions.cs类的AsIChatClient方法源码public static IChatClient AsIChatClient( this ResponsesClient responseClient) new OpenAIResponsesChatClient(responseClient);下面类位于 Microsoft.Extensions.AI.OpenAI 包中是OpenAIResponsesChatClient的具体实现namespace Microsoft.Extensions.AI; /// summary /// Represents an see crefIChatClient/ for an see crefResponsesClient/. /// /summary internal sealed class OpenAIResponsesChatClient : IChatClient { ........ } /// IChatClient 接口位于 Microsoft.Extensions.AI.Abstractions 包中Chat Completions API 调用链下面是部分源代码我们调用了GetChatClient方法来实际返回AzureChatClient下面代码位于 Azure.AI.OpenAI 包中的AzureOpenAIClient.cs类中例子中我们只拿出了一个方法别的方法省略public partial class AzureOpenAIClient : OpenAIClient { public override ChatClient GetChatClient(string deploymentName) new AzureChatClient(Pipeline, deploymentName, _endpoint, _options); } // 而 AzureOpenAIClient 的父类 OpenAIClient 类位于 OpenAI NuGet 包中下面代码是AzureChatClient的源码位于 Azure.AI.OpenAI NuGet 包中的AzureChatClient.cs类中internal partialclassAzureChatClient : ChatClient { privatereadonlystring _deploymentName; privatereadonly Uri _endpoint; privatereadonlystring _apiVersion; internal AzureChatClient( ClientPipeline pipeline, string deploymentName, Uri endpoint, AzureOpenAIClientOptions options) : base( pipeline, model: deploymentName, new OpenAIClientOptions() { Endpoint endpoint }) { Argument.AssertNotNull(pipeline, nameof(pipeline)); Argument.AssertNotNullOrEmpty(deploymentName, nameof(deploymentName)); Argument.AssertNotNull(endpoint, nameof(endpoint)); options ?? new(); _deploymentName deploymentName; _endpoint endpoint; _apiVersion options.Version; } .................... } // 而 ChatClient 类位于 OpenAI NuGet 包中 ChatClient.cs 类中接下来我们继续看OpenAIChatClientExtensions类。该类是ChatClient的扩展类内部同样定义了两个重载的CreateAIAgent方法其中包含client.AsIChatClient()方法。下面类位于 Microsoft.Agents.AI.OpenAI 包中OpenAIChatClientExtensions.cs源码如下public staticclassOpenAIChatClientExtensions { public static ChatClientAgent CreateAIAgent( this ChatClient client, string? instructions null, string? name null, string? description null, IListAITool? tools null, FuncIChatClient, IChatClient? clientFactory null, ILoggerFactory? loggerFactory null, IServiceProvider? services null) client.CreateAIAgent( new ChatClientAgentOptions() { Name name, Description description, ChatOptions tools isnull string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions() { Instructions instructions, Tools tools, } }, clientFactory, loggerFactory, services); public static ChatClientAgent CreateAIAgent( this ChatClient client, ChatClientAgentOptions options, FuncIChatClient, IChatClient? clientFactory null, ILoggerFactory? loggerFactory null, IServiceProvider? services null) { Throw.IfNull(client); Throw.IfNull(options); var chatClient client.AsIChatClient(); if (clientFactory is not null) { chatClient clientFactory(chatClient); } returnnew ChatClientAgent(chatClient, options, loggerFactory, services); } }下面代码位于 Microsoft.Extensions.AI.OpenAI 包中是OpenAIClientExtensions类的AsIChatClient方法源码其它方法省略public static class OpenAIClientExtensions { public static IChatClient AsIChatClient( this ChatClient chatClient) new OpenAIChatClient(chatClient); } // Microsoft.Extensions.AI.OpenAI 包中上面方法返回一个OpenAIChatClient类下面是该类的源码可以看到它实现了IChatClient接口internal sealed partial class OpenAIChatClient : IChatClient { }到这里我们已经理清了两种不同的 OpenAI 协议调用链路以及它们是如何在 microsoft/agent-framework 框架中被封装和使用的。如果你还不理解请看下图总结这篇文章从一个非常具体的工程问题出发当 GenAI 任务变成长时间运行时传统的 HTTP 请求模型开始失效。在前面的内容中我们逐步拆解了这个问题问题本身并不在 AI而在 Web 模型Web 服务天然是无状态、短生命周期的而 GenAI 的典型任务却往往需要持续执行。这种模型上的不匹配是超时、断连、上下文丢失的根本原因。解决思路不是“把请求拉长”而是“把执行拆开”通过 ContinuationToken将一次长任务拆分为多次可恢复的执行过程使每一次请求都保持短生命周期、无状态这是整个方案成立的关键。后台运行 状态持久化是工程落地的核心AllowBackgroundResponses让 Agent 不再绑定某一次请求Persist / Restore机制则保证了任务可以在任意中断点继续执行而不是从头再来。Responses API 并不是“另一个聊天接口”而是运行模型的差异从调用链和源码可以看出Responses API 关注的是一次Response / Run 的生命周期而不是单次消息的返回结果。ContinuationToken 正是这个运行模型中的续接点这也是为什么在本文的场景下必须使用GetResponsesClient()而不是GetChatClient()。回到最初的问题如何在不依赖长连接、不引入复杂队列系统的情况下支撑长时间运行的 AI 任务本文给出的答案是借助 Responses API 提供的运行模型用 ContinuationToken 将一次长执行拆解为多次可恢复的短执行。这并不是一个“技巧”而是一种更贴合 GenAI 特性的执行方式。如果你的应用已经开始遇到长任务、后台执行、状态恢复等问题那么这套模式值得认真理解和采用。源代码地址https://github.com/bingbing-gui/aspnetcore-developer/tree/master/src/09-AI-Agent/Agent-Framework/13-Backgroud-Response-With-Tool-And-Persistence引入地址