# .NET Agent Framework 样例解读笔记(01–05 + A2A)
> 覆盖范围dotnet/samples/01-get-started/01_hello_agent ~ 05_first_workflowdotnet/samples/02-agents/A2A/*(AsFunctionTool / PollingForTaskCompletion / ProtocolSelection)。
> 本文为样例代码解读与知识点汇总,含关键源码位置索引。
---
## 一、样例清单与主题
| # | 样例路径 | 主题 | 关键新增概念 |
|---|---|---|---|
| 1 | 01-get-started/01_hello_agent | 最小可用 Agent | AIProjectClient.AsAIAgent()RunAsync / RunStreamingAsync |
| 2 | 01-get-started/02_add_tools | 函数工具 | AIFunctionFactory.Create[Description]tools: 与自动 Function Calling 闭环 |
| 3 | 01-get-started/03_multi_turn | 多轮对话 | AgentSessionCreateSessionAsync、session 复用即上下文复用 |
| 4 | 01-get-started/04_memory | 自定义记忆 | AIContextProviderProviderSessionState<T>AIContextProviders、会话序列化 |
| 5 | 01-get-started/05_first_workflow | 工作流基础 | Executor<TIn,TOut>WorkflowBuilderAddEdgeWithOutputFromInProcessExecution、事件流 |
| 6 | 02-agents/A2A/A2AAgent_AsFunctionTool | A2A 智能体即工具 | A2ACardResolver → AgentCard → AsAIAgent() → AsAIFunction() |
| 7 | 02-agents/A2A/A2AAgent_PollingForTaskCompletion | 长任务轮询 | AllowBackgroundResponsesResponseContinuationTokenGetTaskAsync |
| 8 | 02-agents/A2A/A2AAgent_ProtocolSelection | 协议绑定选择 | A2AClientOptions.PreferredBindingsProtocolBindingNames |
---
## 二、运行环境与依赖
### 必配环境变量
| 变量 | 用途 | 缺省 |
|---|---|---|
| FOUNDRY_PROJECT_ENDPOINT | Foundry 项目地址,缺失即抛 InvalidOperationException | 必填(样例 1–4、6) |
| FOUNDRY_MODEL | 模型部署名 | gpt-5.4-mini |
| A2A_AGENT_HOST | 远端 A2A 服务地址 | 必填(样例 6–8) |
### 工程结构共性
- <TargetFrameworks>net10.0</TargetFrameworks>Nullable / ImplicitUsings 均开启。
- 样例 1–4 使用**顶级语句**(无显式 Main);样例 5 使用显式 Program.Main。
### 依赖切换规律
| 场景 | 引用 |
|---|---|
| 单 Agent 样例(1–4、6) | Microsoft.Agents.AI.Foundry + Azure.Identity + Microsoft.Extensions.AI.Abstractions |
| 工作流样例(5) | Microsoft.Agents.AI.Workflows(纯进程内,**零外部 AI 依赖**) |
| A2A 样例(6–8) | Microsoft.Agents.AI.A2A;需要直接用 SDK 类型时(样例 8)再 PackageReference: A2A |
### 认证
统一 new DefaultAzureCredential()。源码注释反复警告:生产环境应改用 ManagedIdentityCredential,避免凭据兜底探测带来的延迟与安全风险。
---
## 三、概念分层(自下而上)
### 1. Agent 层
- AIAgent 是统一抽象:Foundry Responses、A2A 远端、工作流托管等后端都包装成它,调用面完全一致。
- 创建方式:
- **扩展方法**AIProjectClient.AsAIAgent(model, instructions, name, description, tools, ...)(散参)或 AsAIAgent(ChatClientAgentOptions)(结构化,样例 4 使用)
- **A2A**agentCard.AsAIAgent()
- 实质都落到 ChatClientAgentOptions.ChatOptionsModelId / Instructions / Tools)
- 调用形态:
- RunAsync(...) → Task<AgentResponse>(一次拿完整结果)
- RunStreamingAsync(...) → IAsyncEnumerable<AgentResponseUpdate>await foreach 增量输出)
- 重载链RunAsync() / (string) / (ChatMessage) / (IEnumerable<ChatMessage>),前者逐级委托到后者,内部有 Throw.IfNullOrWhitespace 校验;核心抽象方法 RunCoreAsync 由具体实现A2AAgentChatClientAgent 等)覆写。
- AgentRunOptions 承载运行期控制AllowBackgroundResponsesContinuationTokenAdditionalProperties。
- 安全提示(框架文档):框架**不校验/不清洗**消息内容,system 角色消息必须由开发者控制,不可混入终端用户输入。
```csharp
// dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs
public Task<AgentResponse> RunAsync(
string message,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNullOrWhitespace(message);
return this.RunAsync(new ChatMessage(ChatRole.User, message), session, options, cancellationToken);
}
```
### 2. 会话层 AgentSession
- CreateSessionAsync() 建会话;**不传 session 时框架内部也会临时建一个,但调用结束即丢弃 → 每次调用相互独立、无记忆**。
- 语义约定:输入消息与本轮响应都会写入 session,供后续轮次使用。
- 会话可**序列化 / 反序列化**SerializeSessionAsync / DeserializeSessionAsync),状态包含 StateBag(记忆组件状态也在内)。
三种 session 传递模式:
| 写法 | 行为 |
|---|---|
| RunAsync(msg) 不传 session | 框架内部临时建 session,调用完即弃 → 无记忆 |
| 共享 session 多次 RunAsync | 消息累积,支持多轮上下文关联 |
| 每次重新 CreateSessionAsync | 主动"开新话题",两组对话互不干扰 |
### 3. 工具层 AITool / AIFunction
- 定义方式一:普通 C# 方法 + [Description](方法级 = 工具用途,参数级 = 参数含义)→ AIFunctionFactory.Create(...) 反射生成 JSON Schema。
- 定义方式二:**整个 Agent 变成工具** agent.AsAIFunction(),签名为 string query → string,名称取自 agent.Name(非法字符替换为 _),描述取自 agent.Description。
- 执行闭环由框架自动完成:模型产出 function call → 本地(或远端)执行 → 结果回填 → 模型组织自然语言回答,**无需手写胶水代码**。
- 注意:带 session 的 AIFunction 是**有状态**的,不要在并发 / 并行函数调用场景共用。
```csharp
// dotnet/src/Microsoft.Agents.AI/AgentExtensions.cs
public static AIFunction AsAIFunction(this AIAgent agent, AIFunctionFactoryOptions? options = null, AgentSession? session = null)
{
[Description("Invoke an agent to retrieve some information.")]
async Task<string> InvokeAgentAsync(
[Description("Input query to invoke the agent.")] string query,
CancellationToken cancellationToken)
{
var response = await agent.RunAsync(query, session: session, options: agentRunOptions, cancellationToken: cancellationToken);
return response.Text;
}
options ??= new();
options.Name ??= SanitizeAgentName(agent.Name);
options.Description ??= agent.Description;
return AIFunctionFactory.Create(InvokeAgentAsync, options);
}
```
### 4. 记忆层 AIContextProvider
- 两个生命周期钩子:
- **跑前** ProvideAIContextAsync(InvokingContext) → 返回 AIContext { Instructions, Messages, Tools },由框架合并进本次请求
- **跑后** StoreAIContextAsync(InvokedContext) → 从消息里沉淀记忆(样例 4 用独立 IChatClient + GetResponseAsync<UserInfo> 做结构化抽取)
- ProviderSessionState<TState> 把状态存进 AgentSession.StateBag,键默认取类型名 → 天然支持序列化与跨会话复制GetUserInfo / SetUserInfo)。
- 通过 AIContextProviders = [...] 挂到 Agent,该 Agent 创建的**每个 session 各自持有一份状态**。
- 组件可用 agent.GetService<UserInfoMemory>() 取回(Agent 兼作微型服务容器)。
- 生产化方向(源码注释):按 user id 维度落库,并跨会话共享。
记忆组件调用时序:
| 时机 | 框架回调 | 组件动作 | 效果 |
|---|---|---|---|
| 每次 Run 前 | ProvideAIContextAsync | 读 StateBag,生成条件化指令 | 缺 → 让模型索要;有 → 喂给模型 |
| 每次 Run 后 | StoreAIContextAsync | 若含用户消息且信息不全,用独立 IChatClient 结构化抽取 | 记忆沉淀进 session |
| 任意时刻 | GetUserInfo / SetUserInfo | 显式读 / 写 StateBag | 程序可控、可跨会话复制 |
| 序列化 / 反序列化 | SerializeSessionAsync / DeserializeSessionAsync | 状态随 session 一起 JSON 往返 | 记忆可持久化恢复 |
### 5. 工作流层
- Executor<TInput, TOutput>:处理单元,覆写 HandleAsync(message, IWorkflowContext, ct);返回值默认产出并沿边流转。
- 造执行器两条路:继承泛型基类 / 用 Func<...>.BindAsExecutor(id) 绑定(框架提供大量按入参形态区分的重载,亦支持 AIAgent.BindAsExecutor(...)——**Agent 可作为执行器节点**)。
- WorkflowBuilder(start) 指定入口 → AddEdge(src, dst)(可带条件 / 标签做分支)→ WithOutputFrom(x) 声明出口 → Build()。
- InProcessExecution.RunAsync(workflow, input) 返回 Run,通过 run.NewEvents 读事件流,过滤 ExecutorCompletedEvent(含 ExecutorId / Data)。
样例 5 数据流"Hello, World!" → [UppercaseExecutor] → "HELLO, WORLD!" → [ReverseTextExecutor] → "!DLROW ,OLLEH"。
### 6. 分布式互操作层(A2A)
- A2ACardResolver(new Uri(host)).GetAgentCardAsync() 发现远端 Agent;卡片含名称、描述、能力、端点、协议绑定。
- agentCard.AsAIAgent() = A2AClientFactory.Create(card, httpClient, options) + AsAIAgent(name: card.Name, description: card.Description)。
- 三种组合 / 运行模式(三个样例各演示一种):
1. **Agent-as-Tool**a2aAgent.AsAIFunction() 注册进主 Agent 的 tools
2. **后台响应轮询**AllowBackgroundResponses=true → 服务端 ReturnImmediately=true 立刻返回 Task → 客户端凭 ContinuationToken 反复 GetTaskAsync(TaskId) 直至令牌为 null
3. **协议绑定选择**PreferredBindings = [ProtocolBindingNames.HttpJson](默认 HTTP+JSON 优先、JSON-RPC 兜底;可改 .JsonRpc)
样例 7 时序:
| 步骤 | 调用 | 服务端 / 框架行为 | 返回 |
|---|---|---|---|
| ① | RunAsync(长任务提问, session, AllowBackgroundResponses=true) | 发消息 + ReturnImmediately=true | AgentResponse 带 ContinuationToken(Submitted/Working) |
| ② | Task.Delay(2s) 后 RunAsync(session, ContinuationToken=token) | GetTaskAsync(TaskId) 轮询状态 | 仍在进行 → 又带 token;已完成 → token 为 null |
| ③ | 重复 ② 直到 token 为 null | — | 最终 AgentResponse |
| ④ | Console.WriteLine(response) | — | 打印完整结果 |
---
## 四、API 速查表
| API | 作用 |
|---|---|
| AIProjectClient.AsAIAgent(...) | Foundry 项目客户端 → ChatClientAgent(Responses API 后端) |
| AIFunctionFactory.Create(Method, options) | 方法 → AIFunction |
| agent.RunAsync(msg, session, options, ct) | 非流式运行 |
| agent.RunStreamingAsync(msg, session, options, ct) | 流式运行 |
| agent.CreateSessionAsync() | 新建会话 |
| agent.SerializeSessionAsync(s) / DeserializeSessionAsync(json) | 会话状态往返 |
| agent.GetService<T>() | 取回挂载的组件(如记忆组件) |
| agent.AsAIFunction(options, session) | Agent → 单个函数工具 |
| agentCard.AsAIAgent(httpClient, options, loggerFactory) | 卡片 → Agent(可指定协议偏好) |
| resolver.GetAIAgentAsync(...) | 解析卡片 + 建 Agent 一步到位 |
| Func<TIn,TOut>.BindAsExecutor(id) | 委托 → 执行器 |
| builder.AddEdge(a,b) / WithOutputFrom(b) | 连线 / 声明输出 |
| InProcessExecution.RunAsync(workflow, input) | 进程内执行工作流 |
---
## 五、关键源码位置
| 文件 | 内容 |
|---|---|
| dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs | RunAsync / RunStreamingAsync 重载链CreateSessionAsyncRunCoreAsync 抽象 |
| dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs | InvokingCoreAsync、输入消息过滤、上下文合并与来源标注 |
| dotnet/src/Microsoft.Agents.AI.Abstractions/ProviderSessionState{TState}.cs | 强类型状态读写(存 AgentSession.StateBag) |
| dotnet/src/Microsoft.Agents.AI/AgentExtensions.cs | AsAIFunction() 实现 |
| dotnet/src/Microsoft.Agents.AI.Foundry/AIProjectClientExtensions.cs | AsAIAgent 各重载(含带 model/instructions/tools 的 L183 重载) |
| dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs | 后台响应分支GetTaskAsync 轮询CreateContinuationToken |
| dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs | 卡片 → Agent,含默认协议偏好说明 |
| dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs | GetAIAgentAsync 一步式创建 |
| dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilder.cs | AddEdge / WithOutputFrom |
| dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBindingExtensions.cs | BindAsExecutor 全套重载(含 AIAgent) |
---
## 六、学习脉络(样例编排逻辑)
```
单 Agent 基础 → 能力扩展 → 状态管理 → 编排 → 分布式
①对话 ②工具 ③会话 ⑤工作流 ⑥Agent 即工具
④记忆 ⑦长任务轮询
⑧协议选择
```
- ①→②:让 Agent **能做事**(调本地函数)
- ②→③:让 Agent **记得住一轮内的上下文**
- ③→④:让 Agent **跨调用沉淀长期记忆**,且记忆可序列化 / 共享
- ⑤:跳出单 Agent,转向**多节点数据流编排**(纯本地、可验证)
- ⑥→⑧:突破进程边界,用 A2A 协议**跨服务组合** Agent,并处理长任务与传输协议
---
## 七、易错点与最佳实践
1. **忘记传 session** 是多轮失效的最常见原因——不传即无状态。
2. **轮询续接调用不能再带新消息**GetContinuationToken 发现同时有消息会抛异常;轮询应写 RunAsync(session, options: new(){ ContinuationToken = token })。
3. **轮询间隔**示例写死 2 秒,生产应改为退避策略 + CancellationToken + 超时上限。
4. *ContinuationTokenResponseContinuationToken)是实验性 API**[Experimental]),跨版本需留意变更;流式场景对应 AgentResponseUpdate.ContinuationToken,用于断流续传。
5. **记忆组件状态必须走 ProviderSessionState / StateBag**,否则无法随会话序列化,也无法跨会话复制。
6. **抽取用的 IChatClient 要独立创建**(样例 4):避免旁路调用污染主对话历史,并规避"先有 Agent 还是先有 Client"的鸡生蛋问题。
7. **生产环境凭据**:不要沿用 DefaultAzureCredential。
8. *AsAIFunction 带 session 时有状态**,禁止并发共用。
9. 工作流中 HandleAsync 返回值默认会继续下发;若某节点结果不应外流,需显式抑制产出。
10. 协议绑定若卡片未声明所选项,具体回退 / 报错行为由 A2A SDK 决定,落地前需实测目标服务端。
11. 消息内容**不做清洗**,system 角色消息必须由开发者控制,防范提示注入。