序言
在开发 GenAI 应用时,我们经常会遇到一个很现实、也很尴尬的场景。用户发来一个复杂指令,比如:
- “写一本关于火星殖民的长篇小说”
- “分析这 50 份 PDF 文档,给我总结结论”
然后前端就开始 loading。如果这个任务要跑一两分钟,普通的 HTTP 请求基本已经超时,用户也很可能已经关掉页面走人了。
这个问题在 Demo 里并不明显,但一旦进入真实业务场景,几乎是绕不开的。
核心痛点:无状态 Web vs 长任务 AI
Web 服务通常是无状态的。
如果 AI 正在写小说写到一半,这时服务重启,或者遇到其他不可抗拒的因素,上下文就会直接丢失。
同样地,如果 AI 正在执行任务的过程中,客户端断开连接,当前的执行状态也无法继续保留。
而 GenAI 恰恰最常见的需求是:
一次任务,持续很久。
这就和无状态 Web 的执行模型产生了天然的冲突。
那么,有没有一种方法,能够在不依赖长连接的情况下,维持 AI 任务的运行状态?
答案是有的,我们继续往下看。
开启“后台模式”
在开始之前我们仍然需要引入如下包:
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 };轮询与“存盘”(The Loop)
这是整个 Demo 中最关键的部分。
它不再是简单地await agent.RunAsync()然后一直等待结果,而是通过一个循环,把一次长任务拆解成多次可恢复的执行过程:
// 发起任务 AgentRunResponse response = await agent.RunAsync("写一本超长的太空小说...", thread, options); // 只要还有 ContinuationToken,说明任务没结束 while (response.ContinuationToken isnotnull) { // 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); // 继续运行 }状态持久化(Persistence)
注意看PersistAgentState和RestoreAgentState。在这个 Demo 里它用了一个 Dictionary 模拟数据库,这就把一个长连接任务,拆成了多次极短的无状态请求。
后台运行下的工具调用
即使 Agent 在后台运行,依然可以正常触发工具调用。
在这个 Demo 中,Agent 在写小说之前,会自动调用:
- ResearchSpaceFactsAsync(查资料)
- GenerateCharacterProfilesAsync(生成角色设定)
这些操作本身可能就比较耗时(示例中模拟了 10 秒延迟)。
但由于我们引入了“存盘 / 读盘”机制,即使中途网络断开,Agent 在恢复之后,依然能够记得自己已经完成了哪些步骤,而不需要从头再来。
运行效果如下:
请求链路变化
细心的朋友可能发现我们这里创建 Agent 的方式和之前不太一样。
GetResponsesClient(deploymentName).CreateAIAgent(...)而不是
GetChatClient(deploymentName).CreateAIAgent(...)那么这是为什么呢? 我们就抛开迷雾,看本质!
microsoft/agent-framework 框架中 OpenAI 集成
microsoft/agent-framework 框架允许你通过兼容 OpenAI 协议的 HTTP 接口来暴露 AI Agent,同时支持 Chat Completions API 和 Responses API。
这使你可以将你的 Agent 与任何兼容 OpenAI 协议的客户端或工具进行集成。
什么是 OpenAI 协议(OpenAI Protocols)?
microsoft/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 API
Chat Completions API 提供了一个简单、无状态的接口,使用标准 OpenAI Chat 格式与 Agent 交互。
框架背后是如何处理的
Responses API 调用链
下面是部分源代码,我们调用了GetResponsesClient方法来实际返回ResponsesClient。
下面代码位于 Azure.AI.OpenAI 包中的AzureOpenAIClient.cs类中,例子中我们只拿出了一个方法,其它方法省略:
publicpartialclassAzureOpenAIClient : OpenAIClient { publicoverride ResponsesClient GetResponsesClient(string deploymentName) { Argument.AssertNotNullOrEmpty(deploymentName, nameof(deploymentName)); returnnew 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; internalAzureResponsesClient( 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 { publicstatic ChatClientAgent CreateAIAgent( this ResponsesClient client, string? instructions = null, string? name = null, string? description = null, IList<AITool>? tools = null, Func<IChatClient, 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); } publicstatic ChatClientAgent CreateAIAgent( this ResponsesClient client, ChatClientAgentOptions options, Func<IChatClient, IChatClient>? clientFactory = null, ILoggerFactory? loggerFactory = null, IServiceProvider? services = null) { Throw.IfNull(client); Throw.IfNull(options); var chatClient = client.AsIChatClient(); if (clientFactory isnotnull) { chatClient = clientFactory(chatClient); } returnnew ChatClientAgent(chatClient, options, loggerFactory, services); } }下面代码是 Microsoft.Extensions.AI NuGet 包中
OpenAIClientExtensions.cs类的AsIChatClient方法源码:
publicstatic IChatClient AsIChatClient( this ResponsesClient responseClient) => new OpenAIResponsesChatClient(responseClient);下面类位于 Microsoft.Extensions.AI.OpenAI 包中,是OpenAIResponsesChatClient的具体实现:
namespaceMicrosoft.Extensions.AI; ///<summary> /// Represents an <see cref="IChatClient"/> for an <see cref="ResponsesClient"/>. ///</summary> internalsealedclassOpenAIResponsesChatClient : IChatClient { ........ } /// IChatClient 接口位于 Microsoft.Extensions.AI.Abstractions 包中Chat Completions API 调用链
下面是部分源代码,我们调用了GetChatClient方法来实际返回AzureChatClient,下面代码位于 Azure.AI.OpenAI 包中的AzureOpenAIClient.cs类中,例子中我们只拿出了一个方法,别的方法省略:
publicpartialclassAzureOpenAIClient : OpenAIClient { publicoverride 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; internalAzureChatClient( 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 { publicstatic ChatClientAgent CreateAIAgent( this ChatClient client, string? instructions = null, string? name = null, string? description = null, IList<AITool>? tools = null, Func<IChatClient, 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); publicstatic ChatClientAgent CreateAIAgent( this ChatClient client, ChatClientAgentOptions options, Func<IChatClient, IChatClient>? clientFactory = null, ILoggerFactory? loggerFactory = null, IServiceProvider? services = null) { Throw.IfNull(client); Throw.IfNull(options); var chatClient = client.AsIChatClient(); if (clientFactory isnotnull) { chatClient = clientFactory(chatClient); } returnnew ChatClientAgent(chatClient, options, loggerFactory, services); } }下面代码位于 Microsoft.Extensions.AI.OpenAI 包中,
是OpenAIClientExtensions类的AsIChatClient方法源码(其它方法省略):
publicstaticclassOpenAIClientExtensions { publicstatic IChatClient AsIChatClient( this ChatClient chatClient) => new OpenAIChatClient(chatClient); } // Microsoft.Extensions.AI.OpenAI 包中上面方法返回一个OpenAIChatClient类,下面是该类的源码,可以看到它实现了IChatClient接口:
internalsealedpartialclassOpenAIChatClient : 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 任务?
如何学习大模型 AI ?
由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。
但是具体到个人,只能说是:
“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。
这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
第一阶段(10天):初阶应用
该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。
- 大模型 AI 能干什么?
- 大模型是怎样获得「智能」的?
- 用好 AI 的核心心法
- 大模型应用业务架构
- 大模型应用技术架构
- 代码示例:向 GPT-3.5 灌入新知识
- 提示工程的意义和核心思想
- Prompt 典型构成
- 指令调优方法论
- 思维链和思维树
- Prompt 攻击和防范
- …
第二阶段(30天):高阶应用
该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。
- 为什么要做 RAG
- 搭建一个简单的 ChatPDF
- 检索的基础概念
- 什么是向量表示(Embeddings)
- 向量数据库与向量检索
- 基于向量检索的 RAG
- 搭建 RAG 系统的扩展知识
- 混合检索与 RAG-Fusion 简介
- 向量模型本地部署
- …
第三阶段(30天):模型训练
恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。
到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?
- 为什么要做 RAG
- 什么是模型
- 什么是模型训练
- 求解器 & 损失函数简介
- 小实验2:手写一个简单的神经网络并训练它
- 什么是训练/预训练/微调/轻量化微调
- Transformer结构简介
- 轻量化微调
- 实验数据集的构建
- …
第四阶段(20天):商业闭环
对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。
- 硬件选型
- 带你了解全球大模型
- 使用国产大模型服务
- 搭建 OpenAI 代理
- 热身:基于阿里云 PAI 部署 Stable Diffusion
- 在本地计算机运行大模型
- 大模型的私有化部署
- 基于 vLLM 部署大模型
- 案例:如何优雅地在阿里云私有部署开源大模型
- 部署一套开源 LLM 项目
- 内容安全
- 互联网信息服务算法备案
- …
学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。
如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。