配套代码:
dotnet/samples/02-agents/A2A/A2AAgent_PollingForTaskCompletion/Program.cs
风格延续:「把另一个智能体当成工具来使」那集的讲法,继续用大白话 + 比喻
适用人群:刚接触 MAF / A2A,想知道"任务要跑很久怎么办"的同学
一、先讲人话:这集到底在干啥?
上一集我们让主智能体去"叫老王帮忙",那是一来一回很快的事。但现实里,老王的活儿可能特别重——比如:
"给我做一份量子计算在密码学里应用的全面分析报告,要包含最新突破、落地难点、未来路线,还要配图。"
这种活儿,老王可能要算 好几分钟。如果你傻站着等他干完再走,就会遇到一堆麻烦:连接超时、界面卡死、服务器以为你掉线了……
所以这集教的是**"点完单先拿号,过会儿再来问"**的模式,专业名词叫 轮询(Polling)+ 续传令牌(Continuation Token)。
一句话总结本集灵魂:任务太长别傻等,先拿个"取餐号"(token),隔几秒去问一次"好了没",好了就端走。
二、核心概念(先认脸,不背定义)
-
长任务(Long-running task)
智能体干一件要花很久的活(跑分析、生成大报告、画图)。A2A 协议专门支持这种任务,不会让调用方一直阻塞。 -
AllowBackgroundResponses = true(后台响应开关)★★★ 本集最关键的开关
默认情况下,调用RunAsync会一直阻塞到任务跑完才返回(慢任务就会卡死/超时)。把它设成true,服务端就立刻返回,只先给你一个"续传令牌"(continuation token),任务在后台继续跑。相当于点餐时选了"做好了叫号,别在柜台干等"。 -
Continuation Token(续传令牌 / 取餐号)
服务端返回的一张"小票"。它证明:"我是刚才点的那单"。你拿它再去问,服务端就知道接着给你干、或者告诉你干到哪了。令牌还在 = 活儿没干完;令牌没了(null) = 活儿干完了。 -
AgentSession(会话)
一次对话的"上下文容器"。同一个 session 里,智能体记得你前面说过啥。轮询全程都在这个 session 里进行,保证是同一笔单子。 -
轮询循环(Polling loop)
每隔一会儿(本例 2 秒)拿 token 去问一次"好了没",直到 token 消失,拿到最终结果。
三、代码逐行拆解(配人话注释)
// ① 问清楚:远程 A2A 智能体(老王)住哪个网址
var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST")
?? throw new InvalidOperationException("A2A_AGENT_HOST is not set.");
白话:还是老规矩,先确认老王的地址。设好环境变量
A2A_AGENT_HOST再跑。
// ② 去拿老王的"名片"
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
AgentCard agentCard = await agentCardResolver.GetAgentCardAsync();
// ③ 把名片翻译成本地可用的智能体对象
AIAgent agent = agentCard.AsAIAgent();
白话:这两步和上一集一模一样——
A2ACardResolver去取 Agent Card,AsAIAgent()翻译成能对话的AIAgent。到这儿,"远程老王"已经在线了。
// ④ 开一个会话(上下文容器),保证是同一笔单子
AgentSession session = await agent.CreateSessionAsync();
白话:开个"会话房间"。后面所有轮询都在这个房间里进行,老王才记得这是同一个任务。
// ⑤ 关键开关:允许后台响应(先给号、不傻等)
AgentRunOptions options = new() { AllowBackgroundResponses = true };
白话:★★★ 本集灵魂第一行。告诉服务端:"这活儿长,你别等我干完,先给我个号(token)就返回,活儿你后台慢慢跑。"如果不设这个,后面就会一直阻塞到任务结束。
// ⑥ 发起长任务,立刻拿到一个"带 token 的回执"就返回
AgentResponse response = await agent.RunAsync(
"Conduct a comprehensive analysis of quantum computing applications in cryptography, ...",
session, options: options);
白话:把那个超长的分析任务丢给老王,带上
session和刚设好的options。因为开了后台响应,这一行不会等任务跑完,它会很快返回一个response,里面大概率带着一个ContinuationToken(取餐号)。
// ⑦ 轮询:只要还有 token,就说明活儿没干完,继续问
while (response.ContinuationToken is { } token)
{
// 歇 2 秒,别把服务端问炸了
await Task.Delay(TimeSpan.FromSeconds(2));
// 拿号再去问一次,服务端返回最新进度(可能还是个新 token)
response = await agent.RunAsync(session, options: new AgentRunOptions { ContinuationToken = token });
}
白话:这是轮询循环的核心。
while (response.ContinuationToken is { } token):只要 token 还在(不是 null),就说明活儿还没干完,继续循环。Task.Delay(2 秒):先歇会儿再去问,礼貌且省资源(不会一秒钟狂问几百次)。- 下一行重新
RunAsync,这次不再传新任务,而是只带ContinuationToken = token——意思是"我是刚才那单,进度如何?"服务端会返回最新状态:还没好就给个新 token 继续循环;好了就不给 token,循环结束。
// ⑧ 循环结束 = token 没了 = 活儿干完了,打印最终结果
Console.WriteLine(response);
白话:当
ContinuationToken变成null,说明老王把大餐做好了,最终答案就在response里,直接打印。
四、一张图看懂轮询怎么流
你 ──RunAsync(长任务, AllowBackgroundResponses=true)──▶ 老王(后台开始慢慢干)
◀── 立刻返回一个 response,带 ContinuationToken(取餐号#1) ──
┌─ while (token 不为 null) ─────────────────────────┐
│ 等 2 秒 │
│ 你 ──RunAsync(只带 token#1)──▶ 老王 │
│ ◀── 返回 response(还带 token#2,说明没干完) ──────│
│ 等 2 秒 │
│ 你 ──RunAsync(只带 token#2)──▶ 老王 │
│ ◀── 返回 response(带 token#3) ───────────────────│
│ ... 反复 ... │
│ 你 ──RunAsync(只带 token#N)──▶ 老王 │
│ ◀── 返回 response(没有 token 了 = 干完了!) ──────│
└───────────────────────────────────────────────────┘
Console.WriteLine(response) ← 打印最终大餐
餐厅取餐类比:你点了个大餐 → 服务员给你取餐号 → 你每隔一会儿拿号去问"我的菜好了吗" → 直到服务员说"齐了,拿走"→ 端走开吃。
五、和上一集(A2A 当工具)对比着学
| 对比点 | 上一集:A2A 当函数工具 | 这集:A2A 轮询长任务 |
|---|---|---|
| 任务特点 | 快,一来一回 | 慢,要跑很久 |
| 是否阻塞 | 同步等结果返回 | AllowBackgroundResponses=true 先返回,后台跑 |
| 关键机制 | AsAIFunction() 伪装成工具 | ContinuationToken 续传令牌 + while 轮询 |
| 你干的事 | 调一下,拿答案 | 点单拿号,反复来问,直到号没了 |
| 共同前提 | A2ACardResolver 取卡 → AsAIAgent() | 完全相同 |
一句话记忆:上一集解决"怎么叫老王帮忙",这集解决"老王活儿太慢怎么不卡死地等"。两集前 3 步一模一样,区别全在 AllowBackgroundResponses 和那段轮询循环。
六、动手改造(建议你自己跑一遍才叫真懂)
前置条件
- 一个支持长任务 / 后台响应的远程 A2A 智能体(先起在本地,例如
http://localhost:8000)。 - 设好环境变量:
$env:A2A_AGENT_HOST = "http://localhost:8000"
然后 dotnet run,观察控制台:它会先很快返回、然后每 2 秒"问一次",最后吐出完整报告。
改造练习
- 练习 1(看令牌变化):在
while循环里加一行Console.WriteLine($"还在跑,token={token}");,亲眼看到 token 每轮都在、最后变null。理解"有 token=没干完,无 token=干完了"。 - 练习 2(调轮询节奏):把
TimeSpan.FromSeconds(2)改成5或10,感受"问得越勤越快知道结果、但也更耗资源"的取舍。 - 练习 3(加超时保护):给循环加一个计数器或
CancellationToken,比如"最多问 30 次还不行就放弃",避免任务真卡死时无限轮询。这是生产环境必加的保险。
int tries = 0;
while (response.ContinuationToken is { } token && tries++ < 30)
{
await Task.Delay(TimeSpan.FromSeconds(2));
response = await agent.RunAsync(session, options: new AgentRunOptions { ContinuationToken = token });
}
七、教学要点小结(背这 5 条就够了)
- 长任务不能傻等:阻塞会超时/卡死,A2A 协议原生支持"后台跑、稍后来取"。
AllowBackgroundResponses = true★★★ 是本集灵魂开关:让RunAsync立刻返回、任务后台继续,而不是干等到完。ContinuationToken(取餐号) 是轮询的凭据:有 token = 没干完,没 token(null) = 干完了。- 轮询循环
while (response.ContinuationToken is { } token)+Task.Delay+ 带 token 再RunAsync,是标准"问进度"姿势;记得加延时,别把服务端问炸。 AgentSession保证全程是同一笔单子,老王才记得上下文。
一句话收尾:这集给你的是"和慢智能体打交道不出事"的招——点单拿号、隔会儿来问、号没了端走,就是
AllowBackgroundResponses+ContinuationToken轮询的全部精髓。