Net+AI智能体12:AG-UI用户代理交互协议
2025-12-25 20:33:28一、AG-UI 协议概览
1. 为什么需要 AG-UI
随着 AI Agent 技术的发展,传统的 Web 应用交互模式面临新的挑战。AI Agent 的响应具有以下特点:
flowchart LR
subgraph 传统API
A1[请求] --> B1[处理] --> C1[响应]
end
subgraph AI Agent
A2[请求] --> B2[思考...]
B2 --> C2[工具调用]
C2 --> D2[中间结果]
D2 --> E2[继续思考...]
E2 --> F2[流式输出]
F2 --> G2[最终响应]
end
style A1 fill:#e1f5ff
style C1 fill:#e1f5ff
style A2 fill:#ffe1e1
style G2 fill:#ffe1e1
在构建 AI Agent 应用界面时,传统 API 模式面临诸多挑战:
| 问题 | 传统 API | AI Agent 需求 | 影响 |
|---|---|---|---|
| 响应模式 | 请求/响应(阻塞等待) | 流式实时响应 | 用户体验差,等待时间长 |
| 中间过程 | 无法展示 | 需要可视化思考/工具调用 | 用户无法理解 Agent 行为 |
| 状态同步 | 单次请求完成 | 需要持续状态同步 | 复杂 UI 状态管理困难 |
| 交互模式 | 确定性 UI | 非确定性(Agent 决定 UI) | 难以提前规划界面 |
| 长时间任务 | 超时问题 | 需要进度反馈 | 无法处理复杂任务 |
AG-UI (Agent User Interaction Protocol) 专为 AI Agent 与用户界面的交互而设计:
flowchart TB
subgraph AG-UI协议
direction LR
Streaming[流式响应]
Events[事件系统]
State[状态同步]
Tools[工具可视化]
end
UI[用户界面] <-->|AG-UI 协议| AG-UI协议
AG-UI协议 <-->|实时交互| Agent[AI Agent]
style AG-UI协议 fill:#fff4e1
style UI fill:#e1f5ff
style Agent fill:#e1ffe1
AG-UI 核心价值:
- 实时流式响应:即时展示 Agent 输出,无需等待
- 事件驱动架构:细粒度的交互事件,精确控制 UI
- 状态同步机制:Snapshot/Delta 模式,保持 UI 与 Agent 状态一致
- 工具调用可视化:透明展示 Agent 的思考和行动过程
2. 什么是 AG-UI 协议
AG-UI (Agent User Interaction Protocol) 是由 CopilotKit 团队发起并开源的一个协议,专注于标准化 AI Agent 与用户应用程序之间的实时通信。
AG-UI 是一个开放的、基于事件的协议,用于标准化 AI Agent 与用户应用程序之间的通信。它提供实时流式响应、事件驱动的 UI 更新、状态同步和工具调用可视化,让 AI Agent 能够与用户界面进行丰富的交互。
核心特性
| 特性 | 描述 |
|---|---|
| 开放标准 | 开源协议,任何人都可以实现和使用 |
| 事件驱动 | 基于事件流的通信,支持细粒度 UI 控制 |
| 实时流式 | 通过 SSE 实现实时推送,即时响应 |
| 状态管理 | 内置 Snapshot/Delta 状态同步机制 |
| 工具透明 | 工具调用过程可视化展示 |
| 人机协作 | 支持 Human-in-the-Loop 审批流程 |
AG-UI 在 Agent 生态中的位置
flowchart TB
User[用户] <-->|AG-UI| Frontend[前端应用]
Frontend <-->|HTTP + SSE| Backend[后端服务]
Backend --> Agent[AI Agent]
Agent <-->|MCP| Tools[外部工具]
Agent <-->|A2A| OtherAgents[其他 Agents]
style User fill:#fff4e1
style Frontend fill:#e1f5ff
style Backend fill:#e1ffe1
style Agent fill:#ffe1f5
协议信息
- 官方网站:docs.ag-ui.com
- GitHub:github.com/ag-ui-protocol/ag-ui
- 演示平台:AG-UI Dojo
- .NET 集成:Microsoft Agent Framework (MAF) 提供原生支持
- 三大 Agent 协议对比,在 AI Agent 生态中,有三个重要的协议:AG-UI、MCP 和 A2A。它们各有分工,共同构成完整的 Agent 通信体系。
flowchart TB
subgraph 用户层
User[用户]
end
subgraph UI层 ["AG-UI 协议 - Agent ↔️ 用户"]
direction LR
UI[用户界面]
end
subgraph Agent层
Agent[AI Agent]
end
subgraph 工具层 ["MCP 协议 - Agent ↔️ 工具"]
direction LR
Tools[工具/资源]
end
subgraph 协作层 ["A2A 协议 - Agent ↔️ Agent"]
direction LR
OtherAgent[其他 Agent]
end
User <--> UI
UI <-->|AG-UI| Agent
Agent <-->|MCP| Tools
Agent <-->|A2A| OtherAgent
style UI层 fill:#e1f5ff
style Agent层 fill:#fff4e1
style 工具层 fill:#e1ffe1
style 协作层 fill:#ffe1f5
详细对比
| 维度 | AG-UI | MCP | A2A |
|---|---|---|---|
| 核心定位 | Agent ↔️ User | Agent ↔️ Tools | Agent ↔️ Agent |
| 通信方向 | 双向流式 | Client-Server | 对等通信 |
| 主要目的 | 用户界面交互 | 工具能力扩展 | 多 Agent 协作 |
| 传输机制 | HTTP + SSE | stdio/HTTP + SSE | HTTP + JSON-RPC |
| 状态管理 | Snapshot/Delta | 无状态 | Task 生命周期 |
| 发现机制 | N/A | Capabilities | Agent Card |
| 典型场景 | Chat UI、实时预览 | 搜索、计算、API 调用 | 任务分发、协作编排 |
AG-UI 像是"客服窗口":
用户与 Agent 之间的交互界面
实时展示 Agent 的工作状态
支持用户输入和反馈
MCP 像是"工具箱":
Agent 调用外部工具完成任务
工具是被动的,等待调用
扩展 Agent 的能力边界
A2A 像是"同事协作":
多个 Agent 之间的任务分发
每个 Agent 都是自主的
可以互相委托和协作
在实际企业场景中,三大协议通常协同使用:
flowchart TB
User[用户] <-->|AG-UI| MainAgent[主 Agent]
subgraph 主Agent能力
MainAgent --> MCP1[MCP: 数据库]
MainAgent --> MCP2[MCP: 搜索引擎]
end
MainAgent <-->|A2A| SpecialistAgent[专家 Agent]
subgraph 专家Agent能力
SpecialistAgent --> MCP3[MCP: 分析工具]
end
style User fill:#fff4e1
style MainAgent fill:#e1f5ff
style SpecialistAgent fill:#e1ffe1
- AG-UI:用户通过界面与主 Agent 交互
- MCP:Agent 内部使用 MCP 调用工具
- A2A:复杂任务委托给专家 Agent 处理
3. AG-UI 核心架构
AG-UI 协议定义了清晰的架构组件,包括 Server、Client 和 Agent。
flowchart TB
subgraph 客户端应用
UI[Chat UI 组件]
Client[AGUIChatClient]
UI --> Client
end
subgraph 服务端
Endpoint[MapAGUI 端点]
Agent[AIAgent]
ChatClient[IChatClient]
Endpoint --> Agent
Agent --> ChatClient
end
Client -->|HTTP POST| Endpoint
Endpoint -->|SSE 事件流| Client
ChatClient -->|API 调用| LLM[LLM 服务]
style 客户端应用 fill:#e1f5ff
style 服务端 fill:#e1ffe1
style LLM fill:#fff4e1
核心组件说明
| 组件 | 职责 | .NET 类型 |
|---|---|---|
| AGUIChatClient | 客户端,发送请求、接收事件流 | AGUIChatClient |
| MapAGUI | 服务端端点映射 | app.MapAGUI() |
| AIAgent | Agent 封装,处理业务逻辑 | AIAgent / ChatClientAgent |
| IChatClient | AI 客户端抽象 | IChatClient |
.NET 包依赖,AG-UI 在 .NET 中通过 Microsoft Agent Framework 提供原生支持:
| NuGet 包 | 用途 |
|---|---|
| Microsoft.Agents.AI.Hosting.AGUI | AG-UI 核心功能 |
| Microsoft.Agents.AI.Hosting.AGUI.AspNetCore | ASP.NET Core 集成 |
| Microsoft.Extensions.AI | AI 客户端抽象 |
核心概念:
- AIAgent(Agent 封装),AIAgent 是 AG-UI 中的核心组件,封装了 AI 能力和业务逻辑:
┌────────────────────────────────────┐
│ AIAgent │
├────────────────────────────────────┤
│ Name: "AssistantAgent" │
│ Instructions: "You are a..." │
│ Tools: [tool1, tool2, ...] │
├────────────────────────────────────┤
│ 能力: │
│ ・流式响应 │
│ ・工具调用 │
│ ・状态管理 │
└─────────────────────────────────────┘
- Run(运行会话),Run 代表一次 Agent 运行会话,包含完整的交互生命周期:
┌─────────────────────────────────────┐
│ Run │
├─────────────────────────────────────┤
│ RunId: "run-001" │
│ ThreadId: "thread-001" │
│ Status: Running / Completed │
├─────────────────────────────────────┤
│ Events: [事件流...] │
│ ・RunStarted │
│ ・TextMessageContent │
│ ・ToolCallStart │
│ ・RunFinished │
└─────────────────────────────────────┘
- Message(消息),Message 是 AG-UI 中的消息单元:
┌─────────────────────────────────────┐
│ Message │
├─────────────────────────────────────┤
│ MessageId: "msg-001" │
│ Role: User / Assistant │
│ Content: "你好..." │
├─────────────────────────────────────┤
│ 附加内容: │
│ ・ToolCalls │
│ ・ToolResults │
└─────────────────────────────────────┘
4. AG-UI 事件系统
AG-UI 采用事件驱动架构,通过丰富的事件类型实现细粒度的 UI 控制。
AG-UI 事件分为以下几个类别:
flowchart TB
subgraph 生命周期事件
RS[RunStarted]
RF[RunFinished]
RE[RunError]
SS[StepStarted]
SF[StepFinished]
end
subgraph 文本消息事件
TMS[TextMessageStart]
TMC[TextMessageContent]
TME[TextMessageEnd]
end
subgraph 工具调用事件
TCS[ToolCallStart]
TCA[ToolCallArgs]
TCE[ToolCallEnd]
TCR[ToolCallResult]
end
subgraph 状态管理事件
StateS[StateSnapshot]
StateD[StateDelta]
MsgS[MessagesSnapshot]
end
style 生命周期事件 fill:#e1f5ff
style 文本消息事件 fill:#e1ffe1
style 工具调用事件 fill:#ffe1e1
style 状态管理事件 fill:#fff4e1
事件类型详解:
- 生命周期事件
| 事件 | 描述 | 触发时机 |
|---|---|---|
| RunStarted | Agent 运行开始 | 收到用户请求时 |
| RunFinished | Agent 运行完成 | 响应完成时 |
| RunError | 运行错误 | 发生异常时 |
| StepStarted | 步骤开始 | 每个处理步骤开始 |
| StepFinished | 步骤完成 | 每个处理步骤结束 |
- 文本消息事件
| 事件 | 描述 | 内容 |
|---|---|---|
| TextMessageStart | 消息开始 | 消息 ID、角色 |
| TextMessageContent | 消息内容片段 | 文本片段(流式) |
| TextMessageEnd | 消息结束 | 完成标识 |
- 工具调用事件
| 事件 | 描述 | 内容 |
|---|---|---|
| ToolCallStart | 工具调用开始 | 工具名称、调用 ID |
| ToolCallArgs | 工具参数 | 参数片段(流式) |
| ToolCallEnd | 工具调用结束 | 完成标识 |
| ToolCallResult | 工具执行结果 | 返回值 |
- 状态管理事件
| 事件 | 描述 | 用途 |
|---|---|---|
| StateSnapshot | 完整状态快照 | 初始化或重同步 |
| StateDelta | 增量状态更新 | 实时状态变更(JSON Patch) |
| MessagesSnapshot | 消息历史快照 | 恢复会话历史 |
下图展示了一个典型的 AG-UI 事件流:
sequenceDiagram
participant C as Client
participant S as AG-UI Server
participant A as AIAgent
participant L as LLM
C->>S: HTTP POST (用户消息)
S->>A: 处理请求
S-->>C: RunStarted
S-->>C: StepStarted
A->>L: 调用 LLM
S-->>C: TextMessageStart
loop 流式输出
L-->>A: Token
S-->>C: TextMessageContent
end
S-->>C: TextMessageEnd
S-->>C: StepFinished
S-->>C: RunFinished
Note over C,L: SSE 事件流
5. AG-UI 传输机制
AG-UI 使用 HTTP POST + SSE (Server-Sent Events) 作为传输机制,这是一种高效的单向推送协议。
SSE 是 HTML5 标准的一部分,允许服务器向客户端推送事件:
| 特性 | 描述 |
|---|---|
| 单向推送 | 服务器 → 客户端 |
| 持久连接 | 一次连接,持续接收事件 |
| 自动重连 | 断开后自动尝试重连 |
| 文本协议 | 基于 HTTP,易于调试 |
| 浏览器原生 | 无需额外库支持 |
请求/响应格式:
- 请求(HTTP POST):
POST /ag-ui HTTP/1.1
Content-Type: application/json
{
"messages": [
{
"role": "user",
"content": "你好"
}
],
"threadId": "thread-001"
}
- 响应(SSE 事件流):
event: RunStarted
data: {"runId":"run-001","threadId":"thread-001"}
event: TextMessageStart
data: {"messageId":"msg-001","role":"assistant"}
event: TextMessageContent
data: {"delta":"你好"}
event: TextMessageContent
data: {"delta":"!有什么"}
event: TextMessageContent
data: {"delta":"可以帮您的?"}
event: TextMessageEnd
data: {}
event: RunFinished
data: {}
vs WebSocket,AG-UI 选择 SSE 而非 WebSocket 的原因:
| 维度 | SSE | WebSocket |
|---|---|---|
| 方向 | 单向(服务器→客户端) | 双向 |
| 协议 | HTTP | 独立协议 |
| 复杂度 | 简单 | 较复杂 |
| 代理友好 | 是 | 需要特殊配置 |
| 重连 | 自动 | 需手动实现 |
| 适用场景 | 服务器推送 | 双向实时通信 |
对于 AI Agent 应用,SSE 足以满足需求:
- 用户输入通过 HTTP POST 发送(上行)
- Agent 响应通过 SSE 流式推送(下行)
6. AG-UI 集成生态
AG-UI 协议得到了多个框架和平台的支持。
Microsoft Agent Framework 提供了 AG-UI 的原生 .NET 实现:
| 组件 | 说明 |
|---|---|
| AGUIChatClient | AG-UI 客户端实现 |
| MapAGUI() | ASP.NET Core 端点映射 |
| AIAgent | Agent 封装类 |
| ChatClientAgent | 基于 IChatClient 的 Agent |
AG-UI 设计为前端框架无关,可与多种框架集成:
| 前端框架 | 集成方式 |
|---|---|
| Blazor | 原生 .NET 集成 |
| React | CopilotKit SDK |
| Vue | 通用 SSE 客户端 |
| Angular | 通用 SSE 客户端 |
AG-UI 可以与多种 Agent 框架配合使用:
flowchart LR
subgraph AG-UI协议
AGUI[AG-UI Server]
end
MAF[Microsoft Agent Framework] --> AGUI
SK[Semantic Kernel] --> AGUI
LC[LangChain] --> AGUI
AG2[AutoGen] --> AGUI
style AG-UI协议 fill:#fff4e1
二、AG-UI 事件系统详解
1. 为什么需要事件系统
在传统的 REST/GraphQL API 中,响应是一次性返回的:
客户端 ────请求────► 服务器
客户端 ◄───完整响应───服务器
但 AI Agent 的响应是渐进式的,需要实时反馈:
sequenceDiagram
participant C as 客户端
participant S as Agent 服务端
participant L as LLM
C->>S: 用户提问
S->>L: 调用 LLM
Note over C,L: 传统 API:等待完整响应
L-->>S: Token 1
L-->>S: Token 2
L-->>S: Token 3
L-->>S: ...
L-->>S: 完成
S-->>C: 完整响应(延迟高)
AG-UI 通过事件系统实现实时流式反馈:
sequenceDiagram
participant C as 客户端
participant S as AG-UI 服务端
participant L as LLM
C->>S: 用户提问
S-->>C: 🟢 RunStarted
S-->>C: 🟢 TextMessageStart
S->>L: 调用 LLM
L-->>S: Token 1
S-->>C: 💬 TextMessageContent
L-->>S: Token 2
S-->>C: 💬 TextMessageContent
L-->>S: Token 3
S-->>C: 💬 TextMessageContent
S-->>C: 🟢 TextMessageEnd
S-->>C: 🟢 RunFinished
Note over C,L: 实时流式反馈,用户体验极佳
事件系统的核心价值
| 价值 | 描述 |
|---|---|
| 实时反馈 | 用户立即看到 Agent 的输出,无需等待 |
| 过程透明 | 工具调用、思考过程可视化展示 |
| 细粒度控制 | 每种交互都有对应的事件类型 |
| 状态同步 | UI 与 Agent 状态保持一致 |
| 错误处理 | 错误事件及时通知,便于处理 |
| 2. AG-UI 事件系统概述 |
AG-UI 定义了四大类事件,每一类事件负责不同的交互场景:
flowchart TB
subgraph Events["AG-UI 事件系统"]
subgraph Lifecycle["生命周期事件<br/>Lifecycle Events"]
RS[RunStarted]
RF[RunFinished]
RE[RunError]
SS[StepStarted]
SF[StepFinished]
end
subgraph Text["文本消息事件<br/>Text Message Events"]
TMS[TextMessageStart]
TMC[TextMessageContent]
TME[TextMessageEnd]
end
subgraph Tool["工具调用事件<br/>Tool Call Events"]
TCS[ToolCallStart]
TCA[ToolCallArgs]
TCE[ToolCallEnd]
TCR[ToolCallResult]
end
subgraph State["状态管理事件<br/>State Management Events"]
StateS[StateSnapshot]
StateD[StateDelta]
MsgS[MessagesSnapshot]
end
end
style Lifecycle fill:#e1f5ff
style Text fill:#e1ffe1
style Tool fill:#ffe1e1
style State fill:#fff4e1
事件类型速查表
| 类别 | 事件类型 | 事件名称 | 用途 |
|---|---|---|---|
| 生命周期 | RUN_STARTED | RunStarted | Agent 运行开始 |
| 生命周期 | RUN_FINISHED | RunFinished | Agent 运行完成 |
| 生命周期 | RUN_ERROR | RunError | 运行错误 |
| 生命周期 | STEP_STARTED | StepStarted | 步骤开始 |
| 生命周期 | STEP_FINISHED | StepFinished | 步骤完成 |
| 文本消息 | TEXT_MESSAGE_START | TextMessageStart | 消息开始 |
| 文本消息 | TEXT_MESSAGE_CONTENT | TextMessageContent | 消息内容片段 |
| 文本消息 | TEXT_MESSAGE_END | TextMessageEnd | 消息结束 |
| 工具调用 | TOOL_CALL_START | ToolCallStart | 工具调用开始 |
| 工具调用 | TOOL_CALL_ARGS | ToolCallArgs | 工具参数片段 |
| 工具调用 | TOOL_CALL_END | ToolCallEnd | 工具调用结束 |
| 工具调用 | TOOL_CALL_RESULT | ToolCallResult | 工具执行结果 |
| 状态管理 | STATE_SNAPSHOT | StateSnapshot | 完整状态快照 |
| 状态管理 | STATE_DELTA | StateDelta | 增量状态更新 |
| 状态管理 | MESSAGES_SNAPSHOT | MessagesSnapshot | 消息历史快照 |
命名规范:AG-UI 事件类型使用 UPPER_SNAKE_CASE(如 RUN_STARTED),字段名使用 camelCase(如 threadId、runId)。 3. 生命周期事件
生命周期事件用于管理 Agent 运行的开始与结束,是所有事件流的框架。
事件列表
| 事件 | 类型 | 描述 | 触发时机 |
|---|---|---|---|
| RunStarted | RUN_STARTED | Agent 运行开始 | 收到用户请求时 |
| RunFinished | RUN_FINISHED | Agent 运行完成 | 响应完成时 |
| RunError | RUN_ERROR | 运行错误 | 发生异常时 |
| StepStarted | STEP_STARTED | 步骤开始 | 每个处理步骤开始 |
| StepFinished | STEP_FINISHED | 步骤完成 | 每个处理步骤结束 |
事件作用
flowchart LR
subgraph Run["Agent 运行生命周期"]
RS[RunStarted] --> Steps
subgraph Steps["处理步骤"]
SS1[StepStarted] --> Process1[处理...] --> SF1[StepFinished]
SS2[StepStarted] --> Process2[处理...] --> SF2[StepFinished]
end
Steps --> RF[RunFinished]
end
Error[RunError] -.-> |发生异常时| Steps
style RS fill:#d4edda
style RF fill:#d4edda
style Error fill:#f8d7da
RunStarted 事件
- 用途:标识 Agent 开始处理用户请求
- SSE 数据格式:
event: RUN_STARTED
data: {
"type": "RUN_STARTED",
"runId": "run-001",
"threadId": "thread-001"
}
| 字段 | 类型 | 描述 |
|---|---|---|
| type | string | 事件类型,固定为 RUN_STARTED |
| runId | string | 本次运行的唯一标识 |
| threadId | string | 会话线程标识 |
客户端处理
显示加载动画
准备接收后续事件
记录 runId 用于追踪
RunFinished 事件
用途:标识 Agent 完成处理
SSE 数据格式:
event: RUN_FINISHED
data: {
"type": "RUN_FINISHED",
"runId": "run-001"
}
客户端处理:
关闭加载动画
结束事件流监听
启用用户输入
RunError 事件
用途:通知客户端发生错误
SSE 数据格式:
event: RUN_ERROR
data: {
"type": "RUN_ERROR",
"runId": "run-001",
"message": "An error occurred while processing your request",
"code": "INTERNAL_ERROR"
}
| 字段 | 类型 | 描述 |
|---|---|---|
| message | string | 错误描述信息 |
| code | string | 错误代码(可选) |
客户端处理:
显示错误信息
提供重试选项
记录错误日志
StepStarted / StepFinished 事件
用途:标识处理过程中的各个步骤
SSE 数据格式:
event: STEP_STARTED
data: {
"type": "STEP_STARTED",
"stepId": "step-001",
"runId": "run-001"
}
event: STEP_FINISHED
data: {
"type": "STEP_FINISHED",
"stepId": "step-001"
}
典型步骤:
LLM 调用步骤
工具执行步骤
状态更新步骤
提示:步骤事件适合用于显示进度条或"正在思考..."等中间状态。 4. 文本消息事件
文本消息事件用于流式传输 Agent 的文本响应,是用户最常见的交互事件。
事件列表
| 事件 | 类型 | 描述 | 内容 |
|---|---|---|---|
| TextMessageStart | TEXT_MESSAGE_START | 消息开始 | 消息 ID、角色 |
| TextMessageContent | TEXT_MESSAGE_CONTENT | 消息内容片段 | 文本片段(delta) |
| TextMessageEnd | TEXT_MESSAGE_END | 消息结束 | 完成标识 |
Start-Content-End 模式
这是 AG-UI 最重要的事件模式之一,遵循 开始 → 内容(多次)→ 结束 的流程:
sequenceDiagram
participant S as AG-UI Server
participant C as Client
S-->>C: TEXT_MESSAGE_START<br/>messageId: "msg-001"<br/>role: "assistant"
S-->>C: TEXT_MESSAGE_CONTENT<br/>delta: "你好"
S-->>C: TEXT_MESSAGE_CONTENT<br/>delta: "!"
S-->>C: TEXT_MESSAGE_CONTENT<br/>delta: "有什么"
S-->>C: TEXT_MESSAGE_CONTENT<br/>delta: "可以帮您"
S-->>C: TEXT_MESSAGE_CONTENT<br/>delta: "的吗?"
S-->>C: TEXT_MESSAGE_END
Note over C: 拼接结果: "你好!有什么可以帮您的吗?"
TextMessageStart 事件
用途:标识一条新消息的开始
SSE 数据格式:
event: TEXT_MESSAGE_START
data: {
"type": "TEXT_MESSAGE_START",
"messageId": "msg-001",
"role": "assistant"
}
| 字段 | 类型 | 描述 |
|---|---|---|
| messageId | string | 消息唯一标识 |
| role | string | 角色:assistant(Agent)或 user(用户) |
客户端处理:
创建新的消息气泡
准备接收内容片段
显示打字动画
TextMessageContent 事件
用途:传输消息的文本片段
SSE 数据格式:
event: TEXT_MESSAGE_CONTENT
data: {
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "msg-001",
"delta": "你好"
}
| 字段 | 类型 | 描述 |
|---|---|---|
| messageId | string | 关联的消息 ID |
| delta | string | 文本增量片段 |
注意:delta 是增量内容,不是完整文本。客户端需要将多个 delta 拼接成完整消息。
- 客户端处理:
// 伪代码:拼接文本内容
messageContent += delta;
UpdateMessageBubble(messageId, messageContent);
TextMessageEnd 事件
用途:标识消息传输完成
SSE 数据格式:
event: TEXT_MESSAGE_END
data: {
"type": "TEXT_MESSAGE_END",
"messageId": "msg-001"
}
客户端处理:
停止打字动画
完成消息渲染
保存消息到历史
完整事件流示例
以下是一个完整的 SSE 事件流示例:
event: RUN_STARTED
data: {"type":"RUN_STARTED","runId":"run-001","threadId":"thread-001"}
event: TEXT_MESSAGE_START
data: {"type":"TEXT_MESSAGE_START","messageId":"msg-001","role":"assistant"}
event: TEXT_MESSAGE_CONTENT
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg-001","delta":"你好"}
event: TEXT_MESSAGE_CONTENT
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg-001","delta":"!"}
event: TEXT_MESSAGE_CONTENT
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg-001","delta":"有什么可以帮您的?"}
event: TEXT_MESSAGE_END
data: {"type":"TEXT_MESSAGE_END","messageId":"msg-001"}
event: RUN_FINISHED
data: {"type":"RUN_FINISHED","runId":"run-001"}
5. 工具调用事件
工具调用事件用于展示 Agent 调用工具的过程,让用户能够看到 Agent 的"思考"和"行动"。
事件列表
| 事件 | 类型 | 描述 | 内容 |
|---|---|---|---|
| ToolCallStart | TOOL_CALL_START | 工具调用开始 | 工具名称、调用 ID |
| ToolCallArgs | TOOL_CALL_ARGS | 工具参数片段 | 参数 JSON 片段 |
| ToolCallEnd | TOOL_CALL_END | 工具调用结束 | 完成标识 |
| ToolCallResult | TOOL_CALL_RESULT | 工具执行结果 | 返回值 |
工具调用同样遵循 Start-Content-End 模式:
sequenceDiagram
participant A as Agent
participant S as AG-UI Server
participant C as Client
participant T as Tool
Note over A,C: Agent 决定调用工具
S-->>C: TOOL_CALL_START<br/>name: "search_weather"<br/>toolCallId: "call-001"
S-->>C: TOOL_CALL_ARGS<br/>delta: '{"city":'
S-->>C: TOOL_CALL_ARGS<br/>delta: '"北京"}'
S-->>C: TOOL_CALL_END
Note over S,T: 服务端执行工具
A->>T: 调用 search_weather({"city":"北京"})
T-->>A: {"temperature":25,"weather":"晴"}
S-->>C: TOOL_CALL_RESULT<br/>result: {"temperature":25,"weather":"晴"}
Note over C: 客户端展示工具调用结果
ToolCallStart 事件
用途:标识 Agent 开始调用工具
SSE 数据格式:
event: TOOL_CALL_START
data: {
"type": "TOOL_CALL_START",
"toolCallId": "call-001",
"name": "search_weather"
}
| 字段 | 类型 | 描述 |
|---|---|---|
| toolCallId | string | 工具调用唯一标识 |
| name | string | 工具名称 |
客户端处理:
显示"正在调用 search_weather..."
展示工具图标或卡片
准备接收参数
ToolCallArgs 事件
用途:流式传输工具调用的参数
SSE 数据格式:
event: TOOL_CALL_ARGS
data: {
"type": "TOOL_CALL_ARGS",
"toolCallId": "call-001",
"delta": "{\"city\":"
}
| 字段 | 类型 | 描述 |
|---|---|---|
| toolCallId | string | 关联的工具调用 ID |
| delta | string | JSON 参数片段 |
为什么流式传输参数?
LLM 生成工具参数也是流式的,通过 ToolCallArgs 事件,客户端可以实时展示 Agent 正在构造的参数,增强透明度。
ToolCallEnd 事件
用途:标识工具调用参数传输完成
SSE 数据格式:
event: TOOL_CALL_END
data: {
"type": "TOOL_CALL_END",
"toolCallId": "call-001"
}
客户端处理:
拼接完整参数 JSON
显示"正在执行..."
等待执行结果
ToolCallResult 事件
用途:返回工具执行结果
SSE 数据格式:
event: TOOL_CALL_RESULT
data: {
"type": "TOOL_CALL_RESULT",
"toolCallId": "call-001",
"result": "{\"temperature\":25,\"weather\":\"晴\"}"
}
| 字段 | 类型 | 描述 |
|---|---|---|
| toolCallId | string | 关联的工具调用 ID |
| result | string | 工具执行结果(JSON 字符串) |
客户端处理:
解析并展示结果
更新工具调用卡片状态
可选:渲染自定义 UI(如天气卡片)
工具调用可视化示例
客户端可以根据工具调用事件渲染丰富的 UI:
┌─────────────────────────────────────────────┐
│ 调用工具: search_weather │
├─────────────────────────────────────────────┤
│ 参数: │
│ city: "北京" │
├─────────────────────────────────────────────┤
│ 结果: │
│ 温度: 25°C │
│ 天气: 晴 │
└─────────────────────────────────────────────┘
多工具调用场景,Agent 可能在一次响应中调用多个工具,每个工具有独立的 toolCallId:
flowchart LR
subgraph Tool1["工具调用 1: search_weather"]
S1[Start] --> A1[Args] --> E1[End] --> R1[Result]
end
subgraph Tool2["工具调用 2: get_news"]
S2[Start] --> A2[Args] --> E2[End] --> R2[Result]
end
Tool1 --> Tool2
style Tool1 fill:#e1f5ff
style Tool2 fill:#e1ffe1
6. 状态管理事件
状态管理事件用于同步 Agent 与客户端之间的状态,确保 UI 与 Agent 的内部状态保持一致。
事件列表
| 事件 | 类型 | 描述 | 用途 |
|---|---|---|---|
| StateSnapshot | STATE_SNAPSHOT | 完整状态快照 | 初始化或重同步 |
| StateDelta | STATE_DELTA | 增量状态更新 | 实时状态变更 |
| MessagesSnapshot | MESSAGES_SNAPSHOT | 消息历史快照 | 恢复会话历史 |
状态管理采用快照 + 增量的模式,兼顾效率和准确性:
flowchart LR
subgraph 初始化
Snapshot["StateSnapshot<br/>完整状态"]
end
subgraph 实时更新
Delta1["StateDelta<br/>变更1"]
Delta2["StateDelta<br/>变更2"]
Delta3["StateDelta<br/>变更3"]
end
subgraph 重同步
Resync["StateSnapshot<br/>完整状态"]
end
Snapshot --> Delta1 --> Delta2 --> Delta3
Delta3 -.-> |需要重同步时| Resync
style 初始化 fill:#e1f5ff
style 实时更新 fill:#e1ffe1
style 重同步 fill:#fff4e1
StateSnapshot 事件
用途:发送完整的状态快照
SSE 数据格式:
event: STATE_SNAPSHOT
data: {
"type": "STATE_SNAPSHOT",
"snapshot": {
"cart": {
"items": [
{"id": "item-1", "name": "iPhone 15", "quantity": 1}
],
"total": 7999
},
"user": {
"name": "张三",
"isLoggedIn": true
}
}
}
| 字段 | 类型 | 描述 |
|---|---|---|
| snapshot | object | 完整的状态对象 |
使用场景:
客户端首次连接
状态不同步需要重置
会话恢复
StateDelta 事件
用途:发送增量状态变更
SSE 数据格式(使用 JSON Patch 格式):
event: STATE_DELTA
data: {
"type": "STATE_DELTA",
"delta": [
{"op": "replace", "path": "/cart/total", "value": 15998},
{"op": "add", "path": "/cart/items/1", "value": {"id": "item-2", "name": "AirPods Pro", "quantity": 1}}
]
}
| 字段 | 类型 | 描述 |
|---|---|---|
| delta | array | JSON Patch 操作数组 |
- JSON Patch 操作:
| 操作 | 描述 | 示例 |
|---|---|---|
| add | 添加字段或数组元素 | {"op":"add","path":"/items/0","value":} |
| remove | 删除字段或元素 | {"op":"remove","path":"/items/0"} |
| replace | 替换值 | {"op":"replace","path":"/total","value":100} |
| move | 移动元素 | {"op":"move","from":"/a","path":"/b"} |
| copy | 复制元素 | {"op":"copy","from":"/a","path":"/b"} |
为什么使用 JSON Patch?
JSON Patch(RFC 6902)是一个标准化的增量更新格式,相比发送完整状态,可以大幅减少传输数据量。
MessagesSnapshot 事件
用途:发送会话消息历史
SSE 数据格式:
event: MESSAGES_SNAPSHOT
data: {
"type": "MESSAGES_SNAPSHOT",
"messages": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么可以帮您的?"},
{"role": "user", "content": "查询北京天气"},
{"role": "assistant", "content": "北京今天天气晴,气温25°C。"}
]
}
| 字段 | 类型 | 描述 |
|---|---|---|
| messages | array | 消息历史数组 |
使用场景:
页面刷新后恢复聊天历史
切换设备继续会话
会话状态恢复
状态同步可视化
sequenceDiagram
participant C as 客户端
participant S as AG-UI Server
Note over C,S: 客户端首次连接
S-->>C: STATE_SNAPSHOT (完整购物车状态)
Note over C,S: 用户添加商品
C->>S: 添加商品请求
S-->>C: STATE_DELTA (add /cart/items/1)
S-->>C: STATE_DELTA (replace /cart/total)
Note over C,S: 用户修改数量
C->>S: 修改数量请求
S-->>C: STATE_DELTA (replace /cart/items/0/quantity)
S-->>C: STATE_DELTA (replace /cart/total)
Note over C,S: 客户端状态与服务端保持同步
7. 特殊事件
除了上述四大类事件外,AG-UI 还定义了一些特殊用途的事件。
特殊事件列表
| 事件 | 类型 | 描述 | 用途 |
|---|---|---|---|
| Custom | CUSTOM | 自定义事件 | 扩展协议能力 |
| Raw | RAW | 原始事件 | 透传底层数据 |
Custom 事件
用途:发送自定义事件,用于扩展 AG-UI 协议
SSE 数据格式:
event: CUSTOM
data: {
"type": "CUSTOM",
"name": "approval_request",
"value": {
"approvalId": "approval-001",
"action": "delete_file",
"description": "是否确认删除文件 report.pdf?"
}
}
| 字段 | 类型 | 描述 |
|---|---|---|
| name | string | 自定义事件名称 |
| value | object | 自定义事件数据 |
使用场景:
Human-in-the-Loop 审批请求
自定义 UI 组件渲染
业务特定通知
Human-in-the-Loop 审批事件
AG-UI 支持 Human-in-the-Loop 工作流,让用户在关键操作前进行审批:
sequenceDiagram
participant U as 用户
participant C as 客户端
participant S as AG-UI Server
participant A as Agent
U->>C: "帮我删除 report.pdf"
C->>S: 发送请求
A->>S: 调用 delete_file 工具
Note over S: 工具需要审批
S-->>C: CUSTOM (approval_request)
C->>U: 显示审批对话框
alt 用户批准
U->>C: 点击"确认"
C->>S: 发送审批响应 (approved: true)
S->>A: 执行工具
S-->>C: TOOL_CALL_RESULT
S-->>C: TEXT_MESSAGE_CONTENT ("文件已删除")
else 用户拒绝
U->>C: 点击"取消"
C->>S: 发送审批响应 (approved: false)
S-->>C: TEXT_MESSAGE_CONTENT ("操作已取消")
end
- 审批请求事件:
event: CUSTOM
data: {
"type": "CUSTOM",
"name": "function_approval_request",
"value": {
"approvalId": "approval-001",
"toolCallId": "call-001",
"function": {
"name": "delete_file",
"arguments": "{\"filename\":\"report.pdf\"}"
}
}
}
- 审批响应(客户端 → 服务端):
{
"type": "function_approval_response",
"approvalId": "approval-001",
"approved": true
}
Raw 事件
用途:透传底层原始数据
SSE 数据格式:
event: RAW
data: {
"type": "RAW",
"content": "..."
}
使用场景:
- 调试和诊断
- 特殊协议扩展
- 第三方集成
8. 事件流模式总结
AG-UI 事件系统遵循两种核心模式:Start-Content-End* 和 Snapshot-Delta。
模式一:Start-Content-End
适用于流式内容传输,如文本消息和工具调用。
flowchart LR
Start[Start<br/>开始标识] --> Content[Content<br/>内容片段 x N]
Content --> End[End<br/>结束标识]
style Start fill:#d4edda
style Content fill:#fff3cd
style End fill:#f8d7da
- 应用场景:
| 场景 | Start 事件 | Content 事件 | End 事件 |
|---|---|---|---|
| 文本消息 | TextMessageStart | TextMessageContent | TextMessageEnd |
| 工具调用 | ToolCallStart | ToolCallArgs | ToolCallEnd |
特点:
支持流式渲染
实时展示内容
可追踪关联(通过 ID)
模式二:Snapshot-Delta
适用于状态同步,确保客户端与服务端状态一致。
flowchart LR
Snapshot[Snapshot<br/>完整快照] --> Delta1[Delta 1]
Delta1 --> Delta2[Delta 2]
Delta2 --> Delta3[Delta 3]
Delta3 -.-> Resync[Snapshot<br/>重同步]
style Snapshot fill:#d4edda
style Delta1 fill:#fff3cd
style Delta2 fill:#fff3cd
style Delta3 fill:#fff3cd
style Resync fill:#d4edda
- 应用场景:
| 场景 | Snapshot 事件 | Delta 事件 |
|---|---|---|
| 应用状态 | StateSnapshot | StateDelta |
| 消息历史 | MessagesSnapshot | N/A |
特点:
减少数据传输量
支持增量更新
可重同步恢复
完整事件流示例
下面是一个包含工具调用的完整事件流示例:
sequenceDiagram
participant C as 客户端
participant S as AG-UI Server
Note over C,S: 用户: "查询北京天气"
S-->>C: RUN_STARTED
S-->>C: STEP_STARTED
Note over C,S: Agent 调用天气工具
S-->>C: TOOL_CALL_START (search_weather)
S-->>C: TOOL_CALL_ARGS ({"city":"北京"})
S-->>C: TOOL_CALL_END
S-->>C: TOOL_CALL_RESULT ({"temp":25,"weather":"晴"})
S-->>C: STEP_FINISHED
Note over C,S: Agent 生成回复
S-->>C: STEP_STARTED
S-->>C: TEXT_MESSAGE_START
S-->>C: TEXT_MESSAGE_CONTENT ("北京今天")
S-->>C: TEXT_MESSAGE_CONTENT ("天气晴,")
S-->>C: TEXT_MESSAGE_CONTENT ("气温25°C。")
S-->>C: TEXT_MESSAGE_END
S-->>C: STEP_FINISHED
S-->>C: RUN_FINISHED
事件流层次结构,AG-UI 事件流具有清晰的层次结构:
Run(运行)
├── Step(步骤)1
│ ├── ToolCallStart
│ ├── ToolCallArgs
│ ├── ToolCallEnd
│ └── ToolCallResult
├── Step(步骤)2
│ ├── TextMessageStart
│ ├── TextMessageContent (多次)
│ └── TextMessageEnd
└── 状态事件(贯穿整个 Run)
├── StateSnapshot
└── StateDelta (多次)
事件处理最佳实践
| 实践 | 说明 |
|---|---|
| 按 ID 关联 | 使用 messageId、toolCallId 等 ID 关联相关事件 |
| 缓冲拼接 | delta 内容需要缓冲拼接成完整内容 |
| 错误处理 | 监听 RunError 事件,优雅处理错误 |
| 超时机制 | 设置合理的超时时间,避免无限等待 |
| 状态回滚 | StateDelta 应用失败时,请求 StateSnapshot 重同步 |
| .NET 中的事件处理 |
在 Microsoft Agent Framework 中,AG-UI 事件通过 AgentRunResponseUpdate 类型暴露给开发者。
相关类型
| 类型 | 描述 |
|---|---|
| AgentRunResponseUpdate | AG-UI 响应更新 |
| TextContent | 文本内容 |
| UsageContent | Token 使用量 |
| FunctionApprovalRequestContent | 函数审批请求 |
| ErrorContent | 错误内容 |
客户端事件处理示例
// 使用 AGUIChatClient 处理事件流
await foreach (var update in aguiClient.GetStreamingResponseAsync(userInput, options))
{
foreach (var content in update.Contents)
{
switch (content)
{
case TextContent textContent:
// 处理文本消息内容
Console.Write(textContent.Text);
break;
case FunctionCallContent toolCall:
// 处理工具调用
Console.WriteLine($"调用工具: {toolCall.Name}");
Console.WriteLine($" 参数: {toolCall.Arguments}");
break;
case UsageContent usage:
// 处理 Token 使用量
Console.WriteLine($"Token: {usage.Details.TotalTokenCount}");
break;
case FunctionApprovalRequestContent approval:
// 处理审批请求
Console.WriteLine($"需要审批: {approval.Function.Name}");
// 显示审批 UI...
break;
case ErrorContent error:
// 处理错误
Console.WriteLine($"错误: {error.Message}");
break;
}
}
// 获取会话 ID 用于后续请求
conversationId ??= update.ConversationId;
}
三、AG-UI 快速开始
1. AG-UI Server 架构
在开始编码之前,让我们先理解 AG-UI Server 的整体架构:
flowchart TB
subgraph Client["客户端"]
AGUIClient[AGUIChatClient]
end
subgraph Server["AG-UI Server"]
subgraph ASP["ASP.NET Core"]
MapAGUI["MapAGUI('/')"]
AddAGUI["AddAGUI()"]
end
subgraph Agent["AI Agent 层"]
AIAgent[AIAgent]
ChatClientAgent[ChatClientAgent]
end
subgraph AI["AI 客户端层"]
IChatClient[IChatClient]
AzureOpenAI[Azure OpenAI]
end
end
AGUIClient <-->|HTTP POST + SSE| MapAGUI
MapAGUI --> AIAgent
AIAgent --> ChatClientAgent
ChatClientAgent --> IChatClient
IChatClient --> AzureOpenAI
style Client fill:#e1f5ff
style ASP fill:#fff4e1
style Agent fill:#e1ffe1
style AI fill:#ffe1f5
核心组件说明
| 组件 | 命名空间 | 作用 |
|---|---|---|
| AddAGUI() | Microsoft.Agents.AI.Hosting.AGUI.AspNetCore | 注册 AG-UI 服务到 DI 容器 |
| MapAGUI() | Microsoft.Agents.AI.Hosting.AGUI.AspNetCore | 映射 AG-UI 端点,处理 HTTP 请求 |
| AIAgent | Microsoft.Agents.AI | Agent 的抽象接口 |
| CreateAIAgent() | Microsoft.Agents.AI | IChatClient 扩展方法,创建 Agent |
| AGUIChatClient | Microsoft.Agents.AI.AGUI | 客户端 AG-UI 通信组件 |
| 2. 准备环境 |
- 核心依赖
| 包名 | 版本 | 用途 |
|---|---|---|
| Microsoft.Agents.AI.Hosting.AGUI.AspNetCore | 1.0.0-preview.251110.2 | AG-UI ASP.NET Core 集成 |
| Microsoft.Agents.AI.OpenAI | 1.0.0-preview.251110.2 | MAF OpenAI 集成 |
| Azure.AI.OpenAI | 2.7.0-beta.2 | Azure OpenAI SDK |
- 客户端依赖
| 包名 | 版本 | 用途 |
|---|---|---|
| Microsoft.Agents.AI.Hosting.AGUI | 1.0.0-preview.251110.2 | AG-UI 客户端支持 |
| Microsoft.Agents.AI | 1.0.0-preview.251110.2 | MAF 核心库 |
- 使用 File-Based Apps,File-Based Apps 是 .NET 9 引入的新特性,允许直接运行 .cs 文件而无需创建完整项目。
#!/usr/bin/dotnet run
#:sdk Microsoft.NET.Sdk.Web // 指定 SDK
#:package PackageName@Version // 添加 NuGet 包
#:property PropertyName=Value // 设置项目属性
#:property Imports=path/to/file.cs // 导入外部 C# 文件
3. 创建 AG-UI Server
现在让我们创建第一个 AG-UI Server(agui-server-basic.cs),代码如下:
#!/usr/bin/dotnet run
#:property Imports=../../helper/Keys.cs
#:sdk Microsoft.NET.Sdk.Web
#:package Azure.AI.OpenAI@2.7.0-beta.2
#:package Microsoft.Agents.AI.OpenAI@1.0.0-preview.251110.2
#:package Microsoft.Agents.AI.Hosting.AGUI.AspNetCore@1.0.0-preview.251110.2
#:property PublishAot=false
using Azure.AI.OpenAI;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
using System.ClientModel;
// 1. 创建 Web 应用
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
// 2. 注册 AG-UI 服务
builder.Services.AddAGUI();
WebApplication app = builder.Build();
// 3. 配置 Azure OpenAI
string endpoint = Keys.AzureOpenAIEndpoint;
string deploymentName = "gpt-4o"; // 请替换为你的部署名称
string apiKey = Keys.AzureOpenAIApiKey;
Console.WriteLine($"Azure OpenAI Endpoint: {endpoint}");
Console.WriteLine($"部署名称: {deploymentName}");
// 4. 创建 AI 客户端
ChatClient chatClient = new AzureOpenAIClient(
new Uri(endpoint), new ApiKeyCredential(apiKey))
.GetChatClient(deploymentName);
// 5. 创建 AI Agent
AIAgent agent = chatClient.AsIChatClient().CreateAIAgent(
name: "AGUIAssistant",
instructions: "你是一个友好的 AI 助手,使用中文回答用户的问题。");
Console.WriteLine("AI Agent 创建成功");
// 6. 映射 AG-UI 端点
app.MapAGUI("/", agent);
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("AG-UI Server 已启动");
Console.WriteLine("端点地址: http://localhost:8888/");
Console.WriteLine("使用 Ctrl+C 停止服务");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
await app.RunAsync("http://localhost:8888");
代码解析
| 步骤 | 代码 | 说明 |
|---|---|---|
| 1 | #:property Imports=../../helper/Keys.cs | 导入密钥配置文件 |
| 2 | builder.Services.AddAGUI() | 注册 AG-UI 所需的服务到 DI 容器 |
| 3 | Keys.AzureOpenAIEndpoint | 从 Keys 类读取 Azure OpenAI 配置 |
| 4 | new AzureOpenAIClient(..., new ApiKeyCredential(...)) | 使用 API Key 创建 Azure OpenAI 客户端 |
| 5 | chatClient.AsIChatClient().CreateAIAgent(...) | 将 ChatClient 转换为 AIAgent |
| 6 | app.MapAGUI("/", agent) | 映射 AG-UI 端点,自动处理 SSE 流式响应 |
| 4. 创建 AG-UI Client |
Client 代码(agui-client-basic.cs)
#!/usr/bin/dotnet run
#:sdk Microsoft.NET.Sdk
#:package Microsoft.Agents.AI@1.0.0-preview.251110.2
#:package Microsoft.Agents.AI.AGUI@1.0.0-preview.251110.2
#:property PublishAot=false
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AGUI;
using Microsoft.Extensions.AI;
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL")
?? "http://localhost:8888";
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("AG-UI 客户端已启动");
Console.WriteLine($"服务端地址: {serverUrl}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 1. 创建 HTTP 客户端
using HttpClient httpClient = new()
{
Timeout = TimeSpan.FromSeconds(60)
};
// 2. 创建 AG-UI 客户端
AGUIChatClient chatClient = new(httpClient, serverUrl);
// 3. 创建 Agent
AIAgent agent = chatClient.CreateAIAgent(
name: "agui-client",
description: "AG-UI 客户端 Agent");
// 4. 创建会话线程
AgentThread thread = agent.GetNewThread();
// 5. 初始化消息列表
List<ChatMessage> messages =
[
new(ChatRole.System, "你是一个友好的 AI 助手,使用中文回答用户的问题。")
];
Console.WriteLine("开始对话(输入 :q 或 quit 退出)\n");
while (true)
{
Console.Write("用户: ");
string? message = Console.ReadLine();
if (string.IsNullOrWhiteSpace(message)) continue;
if (message is ":q" or "quit") break;
// 添加用户消息
messages.Add(new ChatMessage(ChatRole.User, message));
// 流式接收响应
Console.Write("助手: ");
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread))
{
foreach (AIContent content in update.Contents)
{
if (content is TextContent textContent)
{
Console.Write(textContent.Text);
}
else if (content is UsageContent usageContent)
{
Console.WriteLine($"\n [Tokens: {usageContent.Details.TotalTokenCount}]");
}
}
}
Console.WriteLine("\n");
}
Console.WriteLine("再见!");
客户端核心组件
| 组件 | 说明 |
|---|---|
| AGUIChatClient | AG-UI 客户端,负责与服务端通信 |
| AIAgent | 从 ChatClient 创建的 Agent 抽象 |
| AgentThread | 会话线程,维护对话上下文 |
| RunStreamingAsync | 流式发送请求并接收响应 |
| AgentRunResponseUpdate | 响应更新事件,包含各类 AIContent |
| 5. 运行测试 |
- 步骤 1:启动 Server,打开终端,运行以下命令启动 AG-UI Server:
cd file-based-apps
dotnet run agui-basic-server.cs
- 骤 2:启动 Client,在另一个终端中启动客户端:
cd file-based-apps
dotnet run agui-basic-client.cs
- 运行效果预览
Server 终端输出:
Azure OpenAI Endpoint: https://your-resource.openai.azure.com/
部署名称: gpt-4o
AI Agent 创建成功
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AG-UI Server 已启动
端点地址: http://localhost:8888/
使用 Ctrl+C 停止服务
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Client 终端交互:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AG-UI 客户端已启动
服务端地址: http://localhost:8888
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
开始对话(输入 :q 或 quit 退出)
用户: 你好,请介绍一下你自己
助手: [Thread: a1b2c3d4...]
你好!我是一个 AI 助手,由 Azure OpenAI 提供支持。
我可以帮助你回答问题、提供信息、进行对话等。
有什么我可以帮助你的吗?
[Tokens: 156]
用户: :q
再见!
6. Server 代码解析
让我们深入理解 AG-UI Server 的关键代码:
AddAGUI() - 服务注册,AddAGUI() 扩展方法将以下服务注册到 DI 容器
功能:
- 注册 SSE (Server-Sent Events) 响应处理器
- 配置 JSON 序列化选项(适用于 AG-UI 事件)
- 注册事件转换和处理组件
builder.Services.AddAGUI();
flowchart LR
AddAGUI[AddAGUI] --> SSE[SSE 处理器]
AddAGUI --> Serializer[JSON 序列化器]
AddAGUI --> EventHandler[事件处理器]
style AddAGUI fill:#e1f5ff
- 使用 Keys 类管理配置
#:property Imports=../../helper/Keys.cs
string endpoint = Keys.AzureOpenAIEndpoint;
string apiKey = Keys.AzureOpenAIApiKey;
通过 File-Based Apps 的 #:property Imports 指令导入 Keys.cs 文件,集中管理敏感配置:
| 配置项 | 说明 |
|---|---|
| Keys.AzureOpenAIEndpoint | Azure OpenAI 服务端点 |
| Keys.AzureOpenAIApiKey | Azure OpenAI API 密钥 |
安全提示:Keys.cs 文件应添加到 .gitignore,避免将密钥提交到版本控制。
- CreateAIAgent() - Agent 创建
ChatClient chatClient = new AzureOpenAIClient(
new Uri(endpoint), new ApiKeyCredential(apiKey))
.GetChatClient(deploymentName);
AIAgent agent = chatClient.AsIChatClient().CreateAIAgent(
name: "AGUIAssistant",
instructions: "你是一个友好的 AI 助手。");
CreateAIAgent() 是 IChatClient 的扩展方法,创建流程如下:
flowchart LR
ChatClient[ChatClient] -->|AsIChatClient| IChatClient
IChatClient -->|CreateAIAgent| ChatClientAgent
ChatClientAgent -->|as| AIAgent
style ChatClient fill:#fff4e1
style IChatClient fill:#e1ffe1
style ChatClientAgent fill:#ffe1f5
style AIAgent fill:#e1f5ff
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
| name | string | Agent 名称,用于标识 |
| instructions | string | 系统指令,定义 Agent 行为 |
| description | string? | Agent 描述(可选) |
| tools | AITool[]? | 可用工具列表(可选) |
MapAGUI() - 端点映射
自动处理:
- 请求解析和验证
- Agent 调用和流式处理
- 响应转换为 SSE 事件流
- 正确的 Content-Type 设置 (text/event-stream)
app.MapAGUI("/", agent);
MapAGUI() 在指定路径上创建 AG-UI 端点:
flowchart TB
Request[HTTP POST 请求] --> MapAGUI
subgraph MapAGUI["MapAGUI 处理流程"]
Parse[解析请求消息] --> Agent[调用 AIAgent]
Agent --> Stream[生成流式响应]
Stream --> SSE[转换为 SSE 事件]
end
SSE --> Response[SSE 流式响应]
style Request fill:#e1f5ff
style MapAGUI fill:#fff4e1
style Response fill:#e1ffe1
7 .Client 代码解析
- AGUIChatClient - 客户端创建
AGUIChatClient chatClient = new(httpClient, serverUrl);
AGUIChatClient 实现了 IChatClient 接口,负责:
flowchart LR
AGUIChatClient --> HTTP[HTTP 请求发送]
AGUIChatClient --> SSE[SSE 事件解析]
AGUIChatClient --> Convert[转换为 AIContent]
style AGUIChatClient fill:#e1f5ff
- RunStreamingAsync - 流式响应
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread))
{
// 处理每个更新
}
RunStreamingAsync 返回 IAsyncEnumerable
| 属性 | 类型 | 说明 |
|---|---|---|
| Contents | IReadOnlyList |
内容列表(文本、工具调用等) |
| ConversationId | string? | 会话 ID(ThreadId) |
| ResponseId | string? | 响应 ID(RunId) |
- AIContent 类型处理
foreach (AIContent content in update.Contents)
{
if (content is TextContent textContent)
{
Console.Write(textContent.Text);
}
else if (content is UsageContent usageContent)
{
Console.WriteLine($"Tokens: {usageContent.Details.TotalTokenCount}");
}
}
常见的 AIContent 子类型:
| 类型 | 说明 |
|---|---|
| TextContent | 文本内容片段 |
| FunctionCallContent | 工具调用信息 |
| FunctionResultContent | 工具执行结果 |
| UsageContent | Token 使用统计 |
| ErrorContent | 错误信息 |
| 8. AG-UI 通信流程图解 |
完整的请求-响应流程:
sequenceDiagram
participant C as Client
participant S as AG-UI Server
participant A as AIAgent
participant L as LLM (Azure OpenAI)
C->>S: HTTP POST / (messages)
S->>S: MapAGUI 解析请求
S->>A: RunStreamingAsync(messages)
A->>L: 调用 LLM
Note over S,C: SSE 流式响应开始
S-->>C: event: RUN_STARTED
S-->>C: event: TEXT_MESSAGE_START
loop LLM 流式输出
L-->>A: Token
A-->>S: TextContent
S-->>C: event: TEXT_MESSAGE_CONTENT
end
S-->>C: event: TEXT_MESSAGE_END
S-->>C: event: RUN_FINISHED
Note over S,C: SSE 连接关闭
四、AG-UI Tools(前后端工具)
1. AG-UI 工具系统概览
AG-UI 支持两种工具模式,它们协同工作,为 Agent 提供完整的工具能力:
flowchart TB
subgraph Client["🖥️ 客户端"]
AGUIClient[AGUIChatClient]
FrontendTools["📱 Frontend Tools<br/>• GetUserLocation<br/>• GetClipboard<br/>• ReadSensor"]
end
subgraph Server["⚙️ 服务端"]
AGUIServer[MapAGUI Server]
BackendTools["🔧 Backend Tools<br/>• SearchDatabase<br/>• CallExternalAPI<br/>• ProcessData"]
AI[AI Model]
end
AGUIClient <-->|SSE| AGUIServer
AGUIServer --> AI
AI -->|调用| BackendTools
AI -->|请求调用| FrontendTools
FrontendTools -->|本地执行| AGUIClient
BackendTools -->|服务端执行| AGUIServer
style Client fill:#e1f5ff
style Server fill:#fff4e1
Frontend vs Backend Tools 对比:
| 特性 | Backend Tools(后端工具) | Frontend Tools(前端工具) |
|---|---|---|
| 执行位置 | 服务端 | 客户端 |
| 定义位置 | Server 端代码 | Client 端代码 |
| 典型用途 | 数据库查询、API 调用、敏感操作 | GPS 定位、剪贴板、设备传感器 |
| 安全性 | 高(服务端控制) | 需客户端验证 |
| 延迟 | 取决于服务端处理 | 本地执行,较快 |
| 资源访问 | 服务端资源 | 客户端设备资源 |
| 示例 | SearchRestaurants, GetWeather | GetUserLocation, GetBatteryLevel |
选择工具类型的原则:
使用 Backend Tools 当:
涉及敏感数据或 API 密钥
需要访问数据库或后端服务
计算密集型任务
需要服务端验证和审计
使用 Frontend Tools 当:
- 需要访问设备功能(GPS、相机等)
- 需要读取本地资源(剪贴板、文件)
- 涉及用户隐私数据
- 需要快速响应的本地操作
2. Backend Tools(后端工具)
Backend Tools 在服务端定义和执行,适合需要访问服务端资源的操作。
后端工具架构
sequenceDiagram
participant U as 用户
participant C as 客户端
participant S as AG-UI Server
participant AI as AI Model
participant T as Backend Tool
U->>C: "北京有什么川菜餐厅?"
C->>S: HTTP POST + SSE
S->>AI: 发送请求
AI->>S: 决定调用 SearchRestaurants
rect rgb(255, 248, 220)
Note over C,S: 工具调用事件流
S-->>C: ToolCallStart
S-->>C: ToolCallArgs (参数)
S-->>C: ToolCallEnd
end
S->>T: 执行 SearchRestaurants
T->>S: 返回餐厅列表
S-->>C: ToolCallResult
S->>AI: 继续生成回答
AI->>S: 返回推荐文本
S-->>C: TextMessageContent (流式)
C->>U: 显示推荐结果
Server 端:定义后端工具
- 步骤 1:定义工具函数,使用 [Description] 属性描述工具和参数,AI 通过这些描述理解工具用途:
// 定义工具函数(在服务端执行)
[Description("根据位置和菜系搜索餐厅。Search for restaurants by location and cuisine.")]
static RestaurantSearchResponse SearchRestaurants(
[Description("餐厅搜索请求,包含位置和菜系")] RestaurantSearchRequest request)
{
Console.WriteLine($"[后端工具调用] SearchRestaurants");
Console.WriteLine($" 位置: {request.Location}");
Console.WriteLine($" 菜系: {request.Cuisine}");
// 模拟数据库查询或 API 调用
return new RestaurantSearchResponse
{
Location = request.Location,
Cuisine = request.Cuisine,
Results = [
new RestaurantInfo { Name = "金筷子中餐厅", Rating = 4.8 },
new RestaurantInfo { Name = "川味轩", Rating = 4.7 }
]
};
}
- 步骤 2:定义强类型模型,使用 C# 类定义请求和响应类型,确保类型安全:
// 请求模型
internal sealed class RestaurantSearchRequest
{
public string Location { get; set; } = string.Empty;
public string Cuisine { get; set; } = "any";
}
// 响应模型
internal sealed class RestaurantSearchResponse
{
public string Location { get; set; } = string.Empty;
public string Cuisine { get; set; } = string.Empty;
public RestaurantInfo[] Results { get; set; } = [];
}
// 餐厅信息
internal sealed class RestaurantInfo
{
public string Name { get; set; } = string.Empty;
public string Cuisine { get; set; } = string.Empty;
public double Rating { get; set; }
public string Address { get; set; } = string.Empty;
}
- 步骤 3:配置 JSON 序列化上下文(AOT 友好),使用源生成器确保在 Native AOT 环境下也能正常序列化:
// JSON 序列化上下文(AOT 源生成器)
[JsonSerializable(typeof(RestaurantSearchRequest))]
[JsonSerializable(typeof(RestaurantSearchResponse))]
internal sealed partial class RestaurantJsonContext : JsonSerializerContext;
// 在服务配置中注册
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Add(RestaurantJsonContext.Default));
- 步骤 4:注册工具并创建 Agent
// 获取 JSON 序列化选项
var jsonOptions = app.Services
.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>().Value;
// 创建工具数组
AITool[] tools =
[
AIFunctionFactory.Create(SearchRestaurants, serializerOptions: jsonOptions.SerializerOptions),
AIFunctionFactory.Create(GetWeather, serializerOptions: jsonOptions.SerializerOptions)
];
// 创建带工具的 Agent
ChatClientAgent agent = chatClient.AsIChatClient().CreateAIAgent(
name: "RestaurantAssistant",
instructions: "你是一个餐厅推荐助手。",
tools: tools);
app.MapAGUI("/", agent);
Client 端:处理后端工具事件
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread))
{
foreach (AIContent content in update.Contents)
{
switch (content)
{
// 文本内容 - AI 的回答
case TextContent textContent:
Console.Write(textContent.Text);
break;
// 函数调用 - 后端工具被调用
case FunctionCallContent functionCall:
Console.WriteLine($"[后端工具调用]");
Console.WriteLine($" 工具名称: {functionCall.Name}");
Console.WriteLine($" 调用ID: {functionCall.CallId}");
// 显示参数
if (functionCall.Arguments != null)
{
Console.WriteLine(" 参数:");
foreach (var kvp in functionCall.Arguments)
{
Console.WriteLine($" • {kvp.Key}: {kvp.Value}");
}
}
break;
// 函数结果 - 后端工具执行完成
case FunctionResultContent functionResult:
Console.WriteLine($"[工具执行结果]");
Console.WriteLine($" 结果: {functionResult.Result}");
break;
}
}
}
事件内容类型总结
| 内容类型 | 说明 | 触发时机 |
|---|---|---|
| TextContent | AI 生成的文本 | 流式输出文本时 |
| FunctionCallContent | 工具调用请求 | AI 决定调用工具时 |
| FunctionResultContent | 工具执行结果 | 工具执行完成后 |
| ErrorContent | 错误信息 | 发生错误时 |
| 运行 Backend Tools 示例 |
打开两个终端分别运行服务端和客户端:
- 终端 1 - 启动服务端:
cd file-based-apps
dotnet run agui-backend-tools-server.cs
- 终端 2 - 启动客户端:
cd file-based-apps
dotnet run agui-backend-tools-client.cs
- 测试对话:
用户: 北京有什么好吃的川菜餐厅?
[Run Started - Thread: abc123...]
[后端工具调用 #1]
工具名称: SearchRestaurants
调用ID: call_xyz
参数:
• Location: 北京
• Cuisine: 川菜
[工具执行结果]
调用ID: call_xyz
结果: {"Location":"北京","Cuisine":"川菜","Results":[...]}
根据您的需求,我为您找到了以下北京的川菜餐厅:
1. 金筷子中餐厅 - 4.8 分
2. 川味轩 - 4.7 分
...
3. Frontend Tools(前端工具)
Frontend Tools 在客户端定义和执行,适合访问客户端专属资源。
前端工具架构
sequenceDiagram
participant U as 用户
participant C as 客户端
participant FT as Frontend Tool
participant S as AG-UI Server
participant AI as AI Model
U->>C: "我现在在哪里?"
C->>S: HTTP POST + SSE
S->>AI: 发送请求
AI->>S: 决定调用 GetUserLocation
rect rgb(220, 248, 255)
Note over C: 前端工具执行
S-->>C: ToolCallStart (GetUserLocation)
C->>FT: 执行本地工具
FT->>C: 返回 "北京市朝阳区..."
C->>S: ToolCallResult
end
S->>AI: 继续生成回答
AI->>S: 返回位置描述
S-->>C: TextMessageContent
C->>U: "您当前位于北京市朝阳区..."
关键差异:前端工具在客户端本地执行,结果发送回服务端继续对话。
Client 端:定义前端工具
- 步骤 1:定义前端工具函数,在客户端代码中定义工具,访问设备特有功能:
using System.ComponentModel;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AGUI;
using Microsoft.Extensions.AI;
// 获取用户位置(模拟 GPS 访问)
[Description("获取用户当前的地理位置信息。Get the user's current location from GPS.")]
static string GetUserLocation()
{
Console.WriteLine("[前端工具执行] GetUserLocation");
Console.WriteLine(" 正在访问设备 GPS...");
// 模拟访问客户端 GPS
// 在真实应用中,这里会调用设备的地理位置 API
Thread.Sleep(500); // 模拟延迟
return "北京市朝阳区三里屯 (39.93°N, 116.45°E)";
}
// 获取剪贴板内容
[Description("获取用户剪贴板中的文本内容。Get text content from the user's clipboard.")]
static string GetClipboardContent()
{
Console.WriteLine("[前端工具执行] GetClipboardContent");
// 模拟读取剪贴板
return "这是剪贴板中的示例文本:AI Agent 开发真有趣!";
}
// 获取设备电量
[Description("获取设备当前的电池电量。Get the current battery level of the device.")]
static string GetBatteryLevel()
{
Console.WriteLine("[前端工具执行] GetBatteryLevel");
// 模拟读取电量
Random random = new();
int battery = random.Next(60, 100);
return $"电池电量: {battery}%,状态: 良好";
}
- 步骤 2:创建工具数组并注册到 Agent
// 创建前端工具数组
AITool[] frontendTools =
[
AIFunctionFactory.Create(GetUserLocation),
AIFunctionFactory.Create(GetClipboardContent),
AIFunctionFactory.Create(GetBatteryLevel)
];
// 创建 AG-UI 客户端(注册前端工具)
AGUIChatClient chatClient = new(httpClient, serverUrl);
AIAgent agent = chatClient.CreateAIAgent(
name: "agui-client",
description: "带前端工具的 AG-UI 客户端",
tools: frontendTools); // 关键:注册前端工具
注意:前端工具通过 CreateAIAgent 的 tools 参数注册,Agent 会自动识别并在需要时调用。 Server 端:基础 Agent(无需修改)
服务端只需提供基础 Agent,前端工具的调用完全由客户端处理:
// 服务端代码保持简洁 - 无需定义任何工具
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.Extensions.AI;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
builder.Services.AddAGUI();
var app = builder.Build();
// 创建基础 Agent(无后端工具)
ChatClient chatClient = new AzureOpenAIClient(
new Uri(endpoint), new DefaultAzureCredential())
.GetChatClient(deploymentName);
AIAgent agent = chatClient.AsIChatClient().CreateAIAgent(
name: "AGUIAssistant",
instructions: "You are a helpful assistant.");
app.MapAGUI("/", agent);
await app.RunAsync();
关键点:
- 服务端不需要知道前端工具的存在
- 客户端注册的工具信息会在请求时发送到服务端
- Agent 会将这些工具作为可用选项
前端工具执行流程:
flowchart LR
subgraph Client["🖥️ 客户端"]
C1[用户提问] --> C2[发送请求]
C5[收到工具调用] --> C6[执行本地工具]
C6 --> C7[发送结果]
C9[收到回答] --> C10[显示给用户]
end
subgraph Server["⚙️ 服务端"]
S1[收到请求] --> S2[AI 处理]
S2 --> S3{需要工具?}
S3 -->|是| S4[发送工具调用]
S4 --> S5[收到结果]
S5 --> S6[AI 继续处理]
S6 --> S7[发送回答]
S3 -->|否| S7
end
C2 --> S1
S4 --> C5
C7 --> S5
S7 --> C9
style Client fill:#e1f5ff
style Server fill:#fff4e1
运行 Frontend Tools 示例
- 终端 1 - 启动服务端(使用基础版本):
cd file-based-apps
dotnet run agui-frontend-tools-server.cs
- 终端 2 - 启动客户端:
cd file-based-apps
dotnet run agui-frontend-tools-client.cs
- 测试对话:
用户: 我现在在哪里?
[Run Started - Thread: abc123...]
[前端工具执行] GetUserLocation
正在访问设备 GPS...
获取成功: 北京市朝阳区三里屯 (39.93°N, 116.45°E)
根据您设备的 GPS 信息,您当前位于北京市朝阳区三里屯,
坐标为北纬 39.93°、东经 116.45°。这是北京著名的商业和娱乐区域...
4. 混合工具使用
真实场景中,前后端工具经常需要协同工作。例如用户询问"附近有什么餐厅"时:
- Frontend Tool (GetUserLocation) → 获取用户当前位置
- Backend Tool (SearchRestaurants) → 根据位置搜索附近餐厅
- AI 整合结果 → 返回个性化推荐
混合工具协作流程
sequenceDiagram
participant U as 用户
participant C as 客户端
participant FT as 前端工具<br/>GetUserLocation
participant S as AG-UI Server
participant BT as 后端工具<br/>SearchRestaurants
participant AI as AI Model
U->>C: "附近有什么好吃的餐厅?"
C->>S: 发送请求
S->>AI: 分析请求
rect rgb(220, 248, 255)
Note over C,FT: 步骤1:获取位置(前端)
AI->>S: 调用 GetUserLocation
S-->>C: ToolCallStart
C->>FT: 执行
FT->>C: "北京市朝阳区"
C->>S: 返回结果
end
rect rgb(255, 248, 220)
Note over S,BT: 步骤2:搜索餐厅(后端)
AI->>S: 调用 SearchRestaurants
S->>BT: 执行
BT->>S: [餐厅列表]
S-->>C: 工具结果
end
Note over AI: 步骤3:整合结果
AI->>S: 生成推荐
S-->>C: 流式回答
C->>U: 显示推荐
实现混合工具
- 客户端代码要点
// 定义前端工具
[Description("获取用户当前的地理位置信息。")]
static string GetUserLocation()
{
// 模拟 GPS 定位
string[] locations =
[
"北京市朝阳区三里屯",
"上海市浦东新区陆家嘴",
"广州市天河区珠江新城"
];
return locations[new Random().Next(locations.Length)];
}
[Description("获取用户保存的餐饮偏好设置。")]
static string GetUserPreferences()
{
return "偏好菜系: 川菜、粤菜; 价位: 中等; 忌口: 无";
}
// 创建带前端工具的 Agent
AITool[] frontendTools =
[
AIFunctionFactory.Create(GetUserLocation),
AIFunctionFactory.Create(GetUserPreferences)
];
AIAgent agent = chatClient.CreateAIAgent(
name: "agui-client",
tools: frontendTools); // 注册前端工具
- 服务端代码要点
// 后端工具:根据位置和偏好搜索餐厅
[Description("根据位置和用户偏好搜索附近的餐厅。")]
static RestaurantSearchResponse SearchNearbyRestaurants(
[Description("用户位置")] string location,
[Description("用户偏好(可选)")] string? preferences = null)
{
Console.WriteLine($"🔍 [后端工具] SearchNearbyRestaurants");
Console.WriteLine($" 📍 位置: {location}");
Console.WriteLine($" ⚙️ 偏好: {preferences ?? "无"}");
// 模拟数据库查询
return new RestaurantSearchResponse { ... };
}
// 创建带后端工具的 Agent
AITool[] backendTools =
[
AIFunctionFactory.Create(SearchNearbyRestaurants),
AIFunctionFactory.Create(GetRestaurantDetail)
];
ChatClientAgent agent = chatClient.AsIChatClient().CreateAIAgent(
name: "RestaurantAssistant",
instructions: "你是一个智能餐厅推荐助手...",
tools: backendTools);
运行混合工具示例
- 终端 1 - 启动服务端:
cd file-based-apps
dotnet run agui-mixed-tools-server.cs
- 终端 2 - 启动客户端:
cd file-based-apps
dotnet run agui-mixed-tools-client.cs
- 测试对话:
开始对话(输入 :q 或 quit 退出)
推荐试试以下问题体验前后端工具协作:
• "附近有什么好吃的餐厅?" → 先获取位置,再搜索
• "根据我的偏好推荐餐厅" → 获取偏好+位置+搜索
用户: 附近有什么好吃的餐厅?
━━━━━━━━━━━━━━ 开始处理 ━━━━━━━━━━━━━━━
[Run Started - Thread: abc123...]
[前端工具] GetUserLocation
正在访问设备 GPS...
GPS 定位成功: 北京市朝阳区三里屯
[后端工具调用 #1]
工具名称: SearchNearbyRestaurants
参数:
• location: 北京市朝阳区三里屯
[工具执行结果]
找到 3 家餐厅
根据您在北京市朝阳区三里屯的位置,我为您推荐以下餐厅:
1. **川香阁** 4.8
三里屯北路88号
人均 ¥128
2. **粤府私房菜** 4.6
工体北路12号
人均 ¥188
...
━━━━━━━━━━━━━━ 工具调用统计 ━━━━━━━━━━━━━━
前端工具: 1 次
后端工具: 1 次
调用链: GetUserLocation → SearchNearbyRestaurants
5. 最佳实践
- 工具设计原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 单一职责 | 每个工具只做一件事 | ✅ GetLocation ❌ GetLocationAndWeather |
| 清晰描述 | Description 要详细准确 | "获取用户当前 GPS 位置,返回城市区县" |
| 强类型 | 使用明确的参数类型 | 用 class 而非 Dictionary |
| 错误处理 | 返回有意义的错误信息 | 捕获异常并返回用户友好的消息 |
- 安全考虑
// 不安全:直接暴露敏感操作
[Description("删除用户数据")]
static void DeleteUserData(string userId) { ... }
// 安全:添加验证和审批
[Description("请求删除用户数据(需要审批)")]
static ApprovalRequest RequestDeleteUserData(
[Description("用户ID")] string userId,
[Description("删除原因")] string reason)
{
// 生成审批请求,不直接执行
return new ApprovalRequest { ... };
}
性能优化
使用 JSON 源生成器:配置 JsonSerializerContext 提高序列化性能
缓存常用数据:对于变化不频繁的数据使用缓存
异步操作:耗时操作使用异步方法
超时控制:设置合理的超时时间
五、人机协作审批
1. 为什么需要 Human-in-the-Loop
AI Agent 能够自主决策和执行操作,但某些敏感操作必须经过人类确认才能执行。这是企业级应用的基本安全要求。
没有审批机制的风险
| 场景 | 风险 | 潜在损失 |
|---|---|---|
| 财务操作 | Agent 自动批准大额报销 | 💰 财务损失、审计问题 |
| 数据删除 | 误解用户意图删除重要数据 | 📊 数据丢失、业务中断 |
| 外部通信 | 自动发送不当邮件给客户 | 🤝 客户关系受损 |
| 权限变更 | 未经授权修改用户权限 | 🔒 安全漏洞 |
Human-in-the-Loop 的价值
flowchart LR
subgraph Without["无审批机制"]
A1[用户请求] --> A2[Agent 执行] --> A3[操作完成]
style A2 fill:#ffcccc
end
subgraph With["有审批机制"]
B1[用户请求] --> B2[Agent 决策] --> B3{敏感操作?}
B3 -->|是| B4[请求审批]
B4 --> B5{用户确认}
B5 -->|批准| B6[执行操作]
B5 -->|拒绝| B7[取消并通知]
B3 -->|否| B6
style B4 fill:#ffffcc
style B5 fill:#ccffcc
end
典型应用场景
| 领域 | 需审批的操作 | 审批要点 |
|---|---|---|
| 财务 | 费用报销、付款审批 | 金额、收款方、用途 |
| 人力资源 | 请假审批、权限变更 | 时间、影响范围 |
| 客户服务 | 退款处理、合同修改 | 金额、原因、影响 |
| IT 运维 | 系统配置、数据删除 | 影响范围、回滚方案 |
| 营销 | 活动发布、邮件群发 | 内容、受众、时间 |
| 完整审批流程 |
sequenceDiagram
participant U as 👤 用户
participant C as 📱 客户端
participant CA as 🔧 ClientApprovalAgent
participant S as ⚙️ AG-UI Server
participant SA as 🔐 ServerApprovalAgent
participant AI as 🤖 AI Model
participant T as 🔧 敏感工具
U->>C: "帮我审批张三的1500元报销"
C->>CA: 发送请求
CA->>S: HTTP POST + SSE
S->>SA: 转发请求
SA->>AI: 发送给 AI
AI->>SA: 决定调用 ApproveExpense
rect rgb(255, 248, 220)
Note over SA: 检测到 ApprovalRequired 工具
SA->>SA: 生成 FunctionApprovalRequestContent
end
SA-->>CA: request_approval 工具调用
CA->>CA: 转换为 FunctionApprovalRequestContent
CA-->>C: 审批请求事件
C->>U: 显示审批确认 UI
U->>C: 点击"批准"(yes)
C->>CA: FunctionApprovalResponseContent(approved=true)
CA->>S: 审批响应
S->>SA: 转发响应
SA->>AI: 继续执行
AI->>T: 调用 ApproveExpense
T->>AI: 返回执行结果
AI->>SA: 生成回答
SA-->>C: 流式文本响应
C->>U: "费用报销已批准"
核心组件
| 组件 | 位置 | 职责 |
|---|---|---|
| ApprovalRequiredAIFunction | 服务端 | 将工具标记为"需要审批" |
| ServerFunctionApprovalAgent | 服务端 | 拦截审批请求,转换为 request_approval 工具调用 |
| ServerFunctionApprovalClientAgent | 客户端 | 将 request_approval 转换回 FunctionApprovalRequestContent |
| FunctionApprovalRequestContent | 消息类型 | 表示审批请求(函数名、参数) |
| FunctionApprovalResponseContent | 消息类型 | 表示审批响应(approved: true/false) |
消息转换流程
flowchart LR
subgraph Server["服务端"]
FAR[FunctionApprovalRequestContent]
SA[ServerApprovalAgent]
RA[request_approval 工具调用]
FAR --> SA --> RA
end
RA -->|SSE| Network
Network -->|SSE| RAC
subgraph Client["客户端"]
RAC[request_approval 工具调用]
CA[ClientApprovalAgent]
FARC[FunctionApprovalRequestContent]
RAC --> CA --> FARC
end
style FAR fill:#ffffcc
style FARC fill:#ffffcc
2. 服务端实现
服务端需要完成三个关键任务:
- 步骤 1:定义敏感工具函数,敏感工具的定义与普通工具相同,只是在注册时需要特殊处理:
// 费用报销审批工具 - 这是一个敏感操作!
[Description("批准费用报销申请。Approve an expense report.")]
static ExpenseApprovalResult ApproveExpenseReport(
[Description("费用报销单ID")] string expenseReportId,
[Description("报销金额")] decimal amount,
[Description("报销人姓名")] string employeeName)
{
Console.WriteLine($"[服务端] 费用报销已批准!");
Console.WriteLine($" 报销单号: {expenseReportId}");
Console.WriteLine($" 报销金额: ¥{amount:N2}");
Console.WriteLine($" 报销人员: {employeeName}");
return new ExpenseApprovalResult
{
ExpenseReportId = expenseReportId,
Amount = amount,
EmployeeName = employeeName,
Status = "已批准",
ApprovedAt = DateTime.Now,
Message = $"费用报销 {expenseReportId} 已成功批准,金额 ¥{amount:N2} 将于3个工作日内打款。"
};
}
// 数据删除工具 - 另一个敏感操作
[Description("删除用户数据。Delete user data permanently.")]
static DataDeletionResult DeleteUserData(
[Description("用户ID")] string userId,
[Description("删除原因")] string reason)
{
Console.WriteLine($"[服务端] 用户数据已删除!");
Console.WriteLine($" 用户ID: {userId}");
Console.WriteLine($" 删除原因: {reason}");
return new DataDeletionResult
{
UserId = userId,
Reason = reason,
DeletedAt = DateTime.Now,
Message = $"用户 {userId} 的数据已永久删除。"
};
}
- 不走 2:使用 ApprovalRequiredAIFunction 包装,ApprovalRequiredAIFunction 是一个包装器,将普通工具标记为"需要审批":
var jsonOptions = app.Services.GetRequiredService<IOptions<JsonOptions>>().Value;
// 关键:用 ApprovalRequiredAIFunction 包装敏感工具
// 这会将工具标记为"需要审批",Agent 调用时会先请求用户确认
#pragma warning disable MEAI001
AITool[] tools =
[
// 需要审批的工具
new ApprovalRequiredAIFunction(
AIFunctionFactory.Create(ApproveExpenseReport, serializerOptions: jsonOptions.SerializerOptions)),
new ApprovalRequiredAIFunction(
AIFunctionFactory.Create(DeleteUserData, serializerOptions: jsonOptions.SerializerOptions)),
// 普通工具(不需要审批)
AIFunctionFactory.Create(GetExpenseReport, serializerOptions: jsonOptions.SerializerOptions)
];
#pragma warning restore MEAI001
注意:ApprovalRequiredAIFunction 目前是预览版 API(MEAI001 警告)。
步骤 3:使用 ServerFunctionApprovalAgent,ServerFunctionApprovalAgent 是一个 DelegatingAIAgent,负责:
拦截 FunctionApprovalRequestContent
转换为 request_approval 工具调用发送给客户端
接收客户端的审批响应
将响应转换回 FunctionApprovalResponseContent
// 创建基础 Agent(带审批工具)
ChatClientAgent baseAgent = openAIChatClient.AsIChatClient().CreateAIAgent(
name: "ExpenseApprovalAssistant",
instructions: """
你是一个企业运营助手,负责处理以下敏感操作:
1. 费用报销审批 - 帮助员工提交和批准费用报销
2. 数据删除请求 - 处理用户数据删除请求(GDPR 合规)
注意:这些都是敏感操作,系统会在执行前请求用户确认。
""",
tools: tools);
// 使用 ServerFunctionApprovalAgent 包装
var agent = new ServerFunctionApprovalAgent(baseAgent, jsonOptions.SerializerOptions);
// 映射 AG-UI 端点
app.MapAGUI("/", agent);
ServerFunctionApprovalAgent 工作原理
flowchart TB
subgraph Incoming["入站处理(客户端→服务端)"]
I1[request_approval 工具调用] --> I2[解析 ApprovalRequest]
I2 --> I3[创建 FunctionApprovalRequestContent]
I3 --> I4[传递给 InnerAgent]
end
subgraph Outgoing["出站处理(服务端→客户端)"]
O1[InnerAgent 返回] --> O2{包含 FunctionApprovalRequestContent?}
O2 -->|是| O3[创建 ApprovalRequest]
O3 --> O4[转换为 request_approval 工具调用]
O2 -->|否| O5[直接传递]
end
I4 --> InnerAgent[InnerAgent]
InnerAgent --> O1
style Incoming fill:#e1ffe1
style Outgoing fill:#ffe1e1
3. 客户端实现
客户端需要完成三个关键任务:
步骤 1:使用 ServerFunctionApprovalClientAgent,客户端同样需要一个中间件来处理审批消息的转换
ClientAgent 的职责:
- 入站:将 request_approval 工具调用 → FunctionApprovalRequestContent
- 出站:将 FunctionApprovalResponseContent → 工具调用结果
// 创建基础客户端 Agent
AGUIChatClient chatClient = new(httpClient, serverUrl);
ChatClientAgent baseAgent = chatClient.CreateAIAgent(
name: "ExpenseApprovalClient",
instructions: "你是一个企业运营助手客户端。");
// 使用 ServerFunctionApprovalClientAgent 包装
JsonSerializerOptions jsonSerializerOptions = JsonSerializerOptions.Default;
ServerFunctionApprovalClientAgent agent = new(baseAgent, jsonSerializerOptions);
- 步骤 2:处理审批请求,当收到 FunctionApprovalRequestContent 时,需要显示审批 UI:
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread))
{
foreach (AIContent content in update.Contents)
{
switch (content)
{
// 处理审批请求
case FunctionApprovalRequestContent approvalRequest:
// 1. 显示审批 UI
DisplayApprovalRequest(approvalRequest);
// 2. 等待用户确认
Console.Write("是否批准此操作?(yes/no): ");
string? userInput = Console.ReadLine();
bool approved = userInput?.Trim().ToUpperInvariant() is "YES" or "Y";
// 3. 创建审批响应
FunctionApprovalResponseContent response = approvalRequest.CreateResponse(approved);
// 4. 收集审批响应
approvalResponses.Add(response);
break;
case TextContent textContent:
Console.Write(textContent.Text);
break;
// ... 处理其他内容类型
}
}
}
审批 UI 设计,审批 UI 应清晰展示操作内容,让用户了解将要执行的操作:
static void DisplayApprovalRequest(FunctionApprovalRequestContent approvalRequest)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("╔══════════════════════════════════════════════════════════╗");
Console.WriteLine("║ 需要您的审批确认 ║");
Console.WriteLine("╠══════════════════════════════════════════════════════════╣");
Console.ResetColor();
Console.WriteLine($"║ 操作名称: {approvalRequest.FunctionCall.Name}");
if (approvalRequest.FunctionCall.Arguments != null)
{
Console.WriteLine("║ 操作参数:");
foreach (var arg in approvalRequest.FunctionCall.Arguments)
{
Console.WriteLine($"║ • {arg.Key}: {arg.Value}");
}
}
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("╠═════════════════════════════════════════════════════════╣");
Console.WriteLine("║ 此操作需要您的明确授权才能执行 ║");
Console.WriteLine("╚═════════════════════════════════════════════════════════╝");
Console.ResetColor();
}
输出示例:
╔══════════════════════════════════════════════════════════════╗
║ 需要您的审批确认 ║
╠══════════════════════════════════════════════════════════════╣
║ 操作名称: ApproveExpenseReport ║
║ 操作参数: ║
║ • expenseReportId: EXP-2024-001 ║
║ • amount: 1500 ║
║ • employeeName: 张三 ║
╠══════════════════════════════════════════════════════════════╣
║ 此操作需要您的明确授权才能执行 ║
╚══════════════════════════════════════════════════════════════╝
是否批准此操作?(yes/no):
步骤 3:审批循环机制,审批后需要重新发送请求,直到没有更多审批请求:
为什么需要循环?
- 一次请求可能触发多个审批(如:先删除数据,再发送通知邮件)
- 审批后 Agent 可能需要继续执行后续操作
- 循环确保所有审批都得到处理
List<AIContent> approvalResponses = [];
// 审批循环 - 关键机制
do
{
approvalResponses.Clear();
// 1. 发送请求并处理响应
await foreach (var update in agent.RunStreamingAsync(messages, thread))
{
foreach (AIContent content in update.Contents)
{
if (content is FunctionApprovalRequestContent request)
{
// 显示审批 UI 并收集响应
DisplayApprovalRequest(request);
bool approved = GetUserApproval();
approvalResponses.Add(request.CreateResponse(approved));
}
// ... 处理其他内容
}
}
// 2. 将 Agent 响应添加到消息历史
AgentRunResponse response = chatResponseUpdates.ToAgentRunResponse();
messages.AddRange(response.Messages);
// 3. 将审批响应作为 Tool 消息添加
foreach (AIContent approvalResponse in approvalResponses)
{
messages.Add(new ChatMessage(ChatRole.Tool, [approvalResponse]));
}
}
while (approvalResponses.Count > 0); // 继续直到没有更多审批请求
审批循环流程图
flowchart TB
Start([开始]) --> Send[发送请求]
Send --> Stream[接收流式响应]
Stream --> Check{包含审批请求?}
Check -->|是| Display[显示审批 UI]
Display --> Wait[等待用户输入]
Wait --> Collect[收集审批响应]
Collect --> Stream
Check -->|否| Update[更新消息历史]
Update --> HasApproval{有审批响应?}
HasApproval -->|是| AddResponse[添加审批响应到消息]
AddResponse --> Send
HasApproval -->|否| End([结束])
style Display fill:#ffffcc
style Wait fill:#ccffcc
style HasApproval fill:#ffcccc
4. 运行测试
- 启动服务端:
cd dotnet/9.\ agui/file-based-apps
dotnet run agui-human-in-loop-server.cs
- 启动客户端(新终端):
cd dotnet/9.\ agui/file-based-apps
dotnet run agui-human-in-loop-client.cs
测试场景:
- 场景 1:费用报销审批
用户: 帮我审批一笔1500元的差旅费报销,报销人是张三,单号EXP-2024-001
╔══════════════════════════════════════════════════════════════╗
║ 需要您的审批确认 ║
╠══════════════════════════════════════════════════════════════╣
║ 操作名称: ApproveExpenseReport ║
║ 操作参数: ║
║ • expenseReportId: EXP-2024-001 ║
║ • amount: 1500 ║
║ • employeeName: 张三 ║
╚══════════════════════════════════════════════════════════════╝
是否批准此操作?(yes/no): yes
已批准,正在执行...
助手: 费用报销 EXP-2024-001 已成功批准!金额 ¥1,500.00 将于3个工作日内打款给张三。
- 场景 2:数据删除请求(拒绝)
用户: 删除用户 U12345 的所有数据
╔══════════════════════════════════════════════════════════════╗
║ 需要您的审批确认 ║
╠══════════════════════════════════════════════════════════════╣
║ 操作名称: DeleteUserData ║
║ 操作参数: ║
║ • userId: U12345 ║
║ • reason: 用户请求删除 ║
╚══════════════════════════════════════════════════════════════╝
是否批准此操作?(yes/no): no
已拒绝,操作已取消。
助手: 好的,数据删除操作已取消。如果您需要删除用户数据,请确认后再次尝试。
5. 审批协议模型
了解审批协议的数据结构有助于调试和扩展
- ApprovalRequest(审批请求)
public sealed class ApprovalRequest
{
[JsonPropertyName("approval_id")]
public required string ApprovalId { get; init; }
[JsonPropertyName("function_name")]
public required string FunctionName { get; init; }
[JsonPropertyName("function_arguments")]
public IDictionary<string, object?>? FunctionArguments { get; init; }
[JsonPropertyName("message")]
public string? Message { get; init; }
}
- ApprovalResponse(审批响应)
public sealed class ApprovalResponse
{
[JsonPropertyName("approval_id")]
public required string ApprovalId { get; init; }
[JsonPropertyName("approved")]
public required bool Approved { get; init; }
}
- JSON 序列化上下文(AOT 友好)
[JsonSerializable(typeof(ApprovalRequest))]
[JsonSerializable(typeof(ApprovalResponse))]
[JsonSerializable(typeof(Dictionary<string, object?>))]
public sealed partial class ApprovalJsonContext : JsonSerializerContext;
6. 安全最佳实践
- 推荐做法
| 实践 | 说明 |
|---|---|
| 明确显示操作内容 | 审批 UI 应完整展示函数名和所有参数 |
| 必须用户输入 | 不要自动批准,始终等待用户明确确认 |
| 记录审批日志 | 保存审批记录(谁、何时、批准/拒绝) |
| 设置超时 | 审批请求应有超时机制,避免无限等待 |
| 权限验证 | 确保只有授权用户可以批准特定操作 |
- 避免的做法
| 反模式 | 风险 |
|---|---|
| 自动批准所有请求 | 失去安全控制意义 |
| 不显示参数详情 | 用户无法做出知情决策 |
| 忽略拒绝后的处理 | Agent 可能陷入循环 |
| 不记录审批历史 | 无法审计和追溯 |
- 企业级扩展建议
- 多级审批:大额操作需要多人审批
- 审批规则引擎:根据金额、类型自动路由
- 移动端推送:紧急审批推送到手机
- 审批委托:支持临时委托他人审批
- 批量审批:支持批量处理类似请求
六、State Management(状态管理)
1. 为什么需要状态管理
传统的 AI 聊天应用只返回文本流,但现代 AI Agent 应用需要管理结构化数据。AG-UI 的状态管理解决了这一核心挑战。
典型应用场景
| 场景 | 状态内容 | 用户需求 |
|---|---|---|
| 食谱助手 | 菜名、食材、步骤、时间 | 实时预览食谱卡片 |
| 代码生成 | 文件结构、代码片段、依赖 | 实时展示项目结构 |
| 行程规划 | 目的地、日期、活动列表 | 可视化行程表 |
| 表单填写 | 表单字段、验证状态 | 自动填充表单 |
传统方式的问题
flowchart LR
subgraph Old["传统方式"]
A1[Agent] -->|"纯文本"| A2["解析困难<br/>结构不明确"]
A2 --> A3["UI 难以渲染"]
end
subgraph New["AG-UI 状态管理"]
B1[Agent] -->|"结构化状态"| B2["StateSnapshot<br/>JSON Schema"]
B2 --> B3["直接绑定 UI"]
end
AG-UI 状态管理的优势
| 优势 | 说明 |
|---|---|
| 类型安全 | 使用 C# 类型定义状态结构 |
| 实时同步 | Agent 更新状态后立即同步到客户端 |
| UI 解耦 | 状态与 UI 分离,易于维护 |
| AOT 友好 | 支持 JsonSerializerContext 源生成 |
| 状态同步机制,AG-UI 提供两种状态同步方式,适用于不同场景: |
| 模式 | 说明 | 适用场景 |
|---|---|---|
| StateSnapshot | 完整状态快照 | 初始化、重置、小型状态 |
| StateDelta | 增量更新 (JSON Patch) | 大型状态、频繁更新 |
我们后面的内容将使用 StateSnapshot 模式,这是最常见的实现方式:
sequenceDiagram
participant Client as Client
participant Server as Server
participant Agent as AI Agent
Client->>Server: 1. "帮我设计一道意大利菜"
Server->>Agent: 2. 请求 + 当前状态
Agent->>Agent: 3. 生成结构化状态<br/>(JSON Schema)
Agent->>Server: 4. AgentState JSON
Server->>Client: 5. StateSnapshot<br/>(DataContent)
Server->>Client: 6. 摘要文本<br/>(TextContent)
Client->>Client: 7. 更新 UI
状态数据流向,理解状态在各组件之间的流动是掌握状态管理的关键:
flowchart TB
subgraph Client["Client"]
C1[StatefulAgent<TState>]
C2[State 属性]
C3[UI 渲染]
C1 --> C2
C2 --> C3
end
subgraph Transport["AG-UI 传输"]
T1["状态请求<br/>(ag_ui_state)"]
T2["状态响应<br/>(DataContent)"]
end
subgraph Server["Server"]
S1[SharedStateAgent]
S2["JSON Schema<br/>ResponseFormat"]
S3[AI Agent]
S1 --> S2
S2 --> S3
end
C1 -->|发送| T1
T1 --> S1
S3 --> T2
T2 -->|接收| C1
关键组件
| 组件 | 位置 | 职责 |
|---|---|---|
| StatefulAgent |
Client | 管理状态、附加到请求 |
| SharedStateAgent | Server | 处理状态请求、配置 JSON Schema |
| DataContent | 传输 | 承载序列化的状态数据 |
| 2. 服务端实现 |
接下来我们将分步骤实现一个食谱助手(Recipe Assistant)的状态管理功能。
- 步骤 1: 定义状态 Schema,首先定义状态的数据结构。我们使用 C# 类来表示食谱状态:
// 顶层状态包装器
internal sealed class AgentState
{
[JsonPropertyName("recipe")]
public RecipeState Recipe { get; set; } = new();
}
// 食谱状态模型
internal sealed class RecipeState
{
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("cuisine")]
public string Cuisine { get; set; } = string.Empty;
[JsonPropertyName("ingredients")]
public List<string> Ingredients { get; set; } = [];
[JsonPropertyName("steps")]
public List<string> Steps { get; set; } = [];
[JsonPropertyName("prep_time_minutes")]
public int PrepTimeMinutes { get; set; }
[JsonPropertyName("cook_time_minutes")]
public int CookTimeMinutes { get; set; }
[JsonPropertyName("skill_level")]
public string SkillLevel { get; set; } = string.Empty;
}
// JSON 序列化上下文(AOT 源生成)
[JsonSerializable(typeof(AgentState))]
[JsonSerializable(typeof(RecipeState))]
[JsonSerializable(typeof(System.Text.Json.JsonElement))]
internal sealed partial class RecipeSerializerContext : JsonSerializerContext;
设计要点
| 要点 | 说明 |
|---|---|
| JsonPropertyName | 显式指定 JSON 属性名,确保前后端一致 |
| 顶层包装器 | AgentState 包含 Recipe,便于扩展 |
| JsonSerializerContext | AOT 源生成,提高性能和兼容性 |
- 步骤 2: SharedStateAgent 中间件,SharedStateAgent 是服务端状态管理的核心,它:
- 检查客户端请求中是否包含状态(ag_ui_state)
- 配置 AI Agent 使用 JSON Schema 格式输出
- 将状态快照通过 DataContent 返回给客户端
- 生成用户友好的摘要文本
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
internal sealed class SharedStateAgent : DelegatingAIAgent
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// 检查客户端是否发送了状态
if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions ||
!properties.TryGetValue("ag_ui_state", out object? stateObj) ||
stateObj is not JsonElement state ||
state.ValueKind != JsonValueKind.Object)
{
// 无状态管理请求,直接透传
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken))
{
yield return update;
}
yield break;
}
// 配置 JSON Schema 响应格式
var firstRunOptions = new ChatClientAgentRunOptions { /* ... */ };
firstRunOptions.ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema<AgentState>(
schemaName: "AgentState",
schemaDescription: "包含食谱信息的响应,包括菜名、难度、烹饪时间、食材和步骤");
// 执行并收集状态响应
var allUpdates = new List<AgentRunResponseUpdate>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(/* ... */))
{
allUpdates.Add(update);
}
// 解析并返回状态快照
var response = allUpdates.ToAgentRunResponse();
if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
{
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
stateSnapshot,
this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
yield return new AgentRunResponseUpdate
{
Contents = [new DataContent(stateBytes, "application/json")]
};
}
// 第二轮:生成摘要
await foreach (var update in this.InnerAgent.RunStreamingAsync(/* summary request */))
{
yield return update;
}
}
}
关键技术点
| 技术点 | 说明 |
|---|---|
| ag_ui_state | AG-UI 协议定义的状态属性名 |
| ChatResponseFormat.ForJsonSchema |
MEAI 结构化输出 |
| DataContent | 二进制 JSON 内容容器 |
| 两轮对话 | 第一轮生成状态,第二轮生成摘要 |
- Step 3: 服务端主程序,将所有组件组装到 ASP.NET Core 应用中:
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Options;
using OpenAI.Chat;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
// 1. 配置 JSON 序列化上下文
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Add(RecipeSerializerContext.Default));
builder.Services.AddAGUI();
// 配置端口
builder.WebHost.UseUrls("http://localhost:8888");
var app = builder.Build();
// 2. 获取 JsonSerializerOptions
var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>().Value;
// 3. 创建基础 Agent
ChatClient chatClient = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName);
AIAgent baseAgent = chatClient.AsIChatClient().CreateAIAgent(
name: "RecipeAgent",
instructions: """
你是一个专业的食谱助手。当用户要求你创建或推荐食谱时,
请返回一个完整的 AgentState JSON 对象,包含以下字段:
- recipe.title: 菜名
- recipe.cuisine: 菜系类型(如:意大利菜、墨西哥菜、日本料理、中国菜)
- recipe.ingredients: 食材数组,包含名称和用量
- recipe.steps: 烹饪步骤数组
- recipe.prep_time_minutes: 准备时间(分钟)
- recipe.cook_time_minutes: 烹饪时间(分钟)
- recipe.skill_level: 难度等级,可选值:"beginner"(初级)、"intermediate"(中级)、"advanced"(高级)
请确保返回所有字段。发挥创意,提供实用的食谱。
修改现有食谱时,除非用户明确要求更改,否则保留原有字段值。
""");
// 4. 包装为状态管理 Agent
AIAgent agent = new SharedStateAgent(baseAgent, jsonOptions.SerializerOptions);
// 5. 映射 AG-UI 端点
app.MapAGUI("/", agent);
await app.RunAsync();
Instructions 设计技巧,注意 instructions 中明确指定了状态的结构,这是让 AI 正确生成 JSON Schema 格式输出的关键。
3. 客户端实现
客户端需要管理本地状态,并将状态附加到每次请求中。
- 步骤 1: StatefulAgent 包装器,StatefulAgent
是客户端状态管理的核心: - 维护本地状态(State 属性)
- 自动将状态附加到请求消息
- 解析响应中的状态快照并更新本地状态
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
/// <summary>
/// 管理客户端状态并自动附加到请求的代理包装器
/// </summary>
internal sealed class StatefulAgent<TState> : DelegatingAIAgent
where TState : class, new()
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
/// <summary>
/// 获取或设置当前状态
/// </summary>
public TState State { get; set; }
public StatefulAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions, TState? initialState = null)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
this.State = initialState ?? new TState();
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// 将状态序列化并添加到消息
List<ChatMessage> messagesWithState = [.. messages];
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
this.State,
this._jsonSerializerOptions.GetTypeInfo(typeof(TState)));
DataContent stateContent = new(stateBytes, "application/json");
ChatMessage stateMessage = new(ChatRole.System, [stateContent]);
messagesWithState.Add(stateMessage);
// 流式处理响应,接收到状态快照时更新本地状态
await foreach (AgentRunResponseUpdate update in this.InnerAgent.RunStreamingAsync(messagesWithState, thread, options, cancellationToken))
{
foreach (AIContent content in update.Contents)
{
if (content is DataContent dataContent && dataContent.MediaType == "application/json")
{
// 反序列化并更新状态
TState? newState = JsonSerializer.Deserialize(
dataContent.Data.Span,
this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) as TState;
if (newState != null)
{
this.State = newState; // 状态已更新
}
}
}
yield return update;
}
}
}
关键点
| 关键点 | 说明 |
|---|---|
| 泛型 TState | 类型安全的状态管理 |
| class, new() 约束 | 确保状态类型可实例化 |
| DataContent 检测 | 识别状态快照消息 |
| 自动更新 | 解析后立即更新 State 属性 |
- 步骤 2: 客户端主程序,完整的客户端实现,包含状态展示功能:
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AGUI;
using Microsoft.Extensions.AI;
string serverUrl = "http://localhost:8888";
Console.WriteLine($"正在连接 AG-UI 服务端: {serverUrl}\n");
// 1. 创建 AG-UI 客户端
using HttpClient httpClient = new() { Timeout = TimeSpan.FromSeconds(60) };
AGUIChatClient chatClient = new(httpClient, serverUrl);
AIAgent baseAgent = chatClient.CreateAIAgent(
name: "recipe-client",
description: "AG-UI 食谱客户端 Agent(支持状态管理)");
// 2. 包装为状态管理 Agent
JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web)
{
TypeInfoResolver = RecipeSerializerContext.Default
};
StatefulAgent<AgentState> agent = new(baseAgent, jsonOptions, new AgentState());
AgentThread thread = agent.GetNewThread();
List<ChatMessage> messages = [
new(ChatRole.System, "你是一个专业的食谱助手,可以创建和修改食谱。")
];
// 3. 交互循环
while (true)
{
Console.Write("\nYou: ");
string? message = Console.ReadLine();
if (message is ":q" or "quit") break;
if (message?.Equals(":state", StringComparison.OrdinalIgnoreCase) == true)
{
DisplayState(agent.State.Recipe); // 显示当前状态
continue;
}
messages.Add(new ChatMessage(ChatRole.User, message!));
bool stateReceived = false;
// 4. 流式接收响应
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread))
{
foreach (AIContent content in update.Contents)
{
switch (content)
{
case TextContent textContent:
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write(textContent.Text);
Console.ResetColor();
break;
case DataContent dataContent when dataContent.MediaType == "application/json":
stateReceived = true;
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine("\n[收到状态快照 - 正在处理...]");
Console.ResetColor();
break;
}
}
}
// 5. 显示更新后的状态
if (stateReceived)
{
DisplayState(agent.State.Recipe);
}
}
- 步骤 3: 状态 UI 渲染,将结构化状态渲染为用户友好的格式:
static void DisplayState(RecipeState? state)
{
if (state == null || string.IsNullOrEmpty(state.Title))
{
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine("\n[暂无食谱状态]");
Console.ResetColor();
return;
}
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine("\n" + new string('═', 60));
Console.WriteLine("当前食谱状态");
Console.WriteLine(new string('═', 60));
Console.ResetColor();
Console.WriteLine($"\n{state.Title}");
if (!string.IsNullOrEmpty(state.Cuisine))
{
Console.WriteLine($" 菜系: {state.Cuisine}");
}
if (!string.IsNullOrEmpty(state.SkillLevel))
{
string skillText = state.SkillLevel.ToLower() switch
{
"beginner" => "初级",
"intermediate" => "中级",
"advanced" => "高级",
_ => state.SkillLevel
};
Console.WriteLine($" 难度: {skillText}");
}
if (state.PrepTimeMinutes > 0)
{
Console.WriteLine($" 准备时间: {state.PrepTimeMinutes} 分钟");
}
if (state.CookTimeMinutes > 0)
{
Console.WriteLine($" 烹饪时间: {state.CookTimeMinutes} 分钟");
}
int totalTime = state.PrepTimeMinutes + state.CookTimeMinutes;
if (totalTime > 0)
{
Console.WriteLine($" 总时间: {totalTime} 分钟");
}
if (state.Ingredients.Count > 0)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("\n 食材:");
Console.ResetColor();
foreach (var ingredient in state.Ingredients)
{
Console.WriteLine($" • {ingredient}");
}
}
if (state.Steps.Count > 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("\n 步骤:");
Console.ResetColor();
for (int i = 0; i < state.Steps.Count; i++)
{
Console.WriteLine($" {i + 1}. {state.Steps[i]}");
}
}
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine("\n" + new string('═', 60));
Console.ResetColor();
}
渲染效果示例
════════════════════════════════════════════════════════════
当前食谱状态
════════════════════════════════════════════════════════════
经典玛格丽特披萨
菜系: 意大利菜
难度: 初级
准备时间: 20 分钟
烹饪时间: 15 分钟
总时间: 35 分钟
食材:
• 中筋面粉 2 杯
• 温水 1 杯
• 干酵母 1 茶匙
• 新鲜马苏里拉奶酪 200g
• 新鲜罗勒叶
• 圣马扎诺番茄
步骤:
1. 将面粉、水和酵母混合成面团
2. 让面团发酵 1 小时
3. 将面团擀成圆形
4. 添加番茄酱和配料
5. 在 230°C 烤箱中烤 12-15 分钟
════════════════════════════════════════════════════════════
4. 运行测试
现在让我们运行完整的食谱助手应用
- 运行服务端,在第一个终端中启动服务器:
cd file-based-apps
dotnet run agui-state-management-server.cs
预期输出:
AG-UI State Management Server - Recipe Assistant
============================================================
Azure OpenAI Endpoint: https://your-resource.openai.azure.com/
Deployment: gpt-4o
SharedStateAgent 创建成功
============================================================
AG-UI State Management Server 已启动
端点地址: http://localhost:8888/
功能: 食谱状态管理 (JSON Schema)
启动客户端开始交互
按 Ctrl+C 停止服务
- 运行客户端,在第二个终端中启动客户端:
cd file-based-apps
dotnet run agui-state-management-client.cs
预期输出:
AG-UI State Management Client - Recipe Assistant
============================================================
正在连接 AG-UI 服务端: http://localhost:8888
客户端已连接就绪!
------------------------------------------------------------
可用命令:
:state - 显示当前食谱状态
:clear - 清除对话和状态
:q - 退出
------------------------------------------------------------
试试输入: "帮我设计一道适合新手的意大利面食谱"
You:
测试场景:
场景 1:创建新食谱
预期:
- 收到 [收到状态快照 - 正在处理...] 消息
- 状态自动更新并显示完整食谱
- AI 返回摘要文本
You: 帮我设计一道适合新手的意大利面食谱
场景 2:查看当前状态
预期:
- 显示结构化的食谱信息
You: :state
场景 3:修改现有食谱
预期:
- 收到新的状态快照
- 难度变为中级
- 食材列表包含培根
- 步骤可能更新
You: 请把难度改成中级,并添加一些培根作为食材
场景 4:清除状态
预期:
- 对话和状态被清除
- 重新开始新的食谱创建
You: :clear
状态流转可视化,完整的交互流程:
sequenceDiagram
participant User as 用户
participant Client as 客户端<br/>(StatefulAgent)
participant Server as 服务端<br/>(SharedStateAgent)
participant AI as AI 模型
User->>Client: 1. "帮我设计一道意大利面食谱"
Note over Client: State: {}
Client->>Server: 2. 消息 + 空状态
Server->>AI: 3. 请求 (JSON Schema 格式)
AI->>Server: 4. AgentState JSON
Server->>Client: 5. DataContent (状态快照)
Note over Client: 解析并更新 State
Server->>Client: 6. TextContent (摘要)
Client->>User: 7. 显示摘要和状态
Note over Client: State: { recipe: {...} }
User->>Client: 8. "添加培根"
Client->>Server: 9. 消息 + 当前状态
Server->>AI: 10. 请求 (含当前状态)
AI->>Server: 11. 更新后的 AgentState
Server->>Client: 12. 新状态快照
Client->>User: 13. 显示更新
5. 高级主题
- StateDelta(增量更新),对于大型状态对象,可以使用 StateDelta 模式减少传输量。StateDelta 使用 JSON Patch (RFC 6902) 格式:
[
{ "op": "replace", "path": "/recipe/title", "value": "New Title" },
{ "op": "add", "path": "/recipe/ingredients/-", "value": "Bacon" },
{ "op": "remove", "path": "/recipe/steps/2" }
]
- Snapshot vs Delta 对比
| 特性 | Snapshot | Delta |
|---|---|---|
| 传输量 | 完整状态 | 仅变更部分 |
| 实现复杂度 | 简单 | 需要 Patch 库 |
| 适用场景 | 小型状态 | 大型状态、频繁更新 |
| 一致性 | 始终一致 | 需要顺序保证 |
| 6. 最佳实践 |
- 状态设计原则
| 原则 | 说明 |
|---|---|
| 扁平化 | 避免过深的嵌套,便于 Patch |
| 可序列化 | 所有属性都支持 JSON 序列化 |
| 默认值 | 提供合理的默认值 |
| 版本兼容 | 考虑前后端版本差异 |
- Instructions 编写技巧
好的 Instructions:
- 明确列出所有字段和期望格式
- 提供示例值
- 说明约束条件(如 skill_level 的可选值)
避免:
- 模糊的字段描述
- 缺少必填字段说明
- 格式不一致
- 错误处理
// 处理反序列化失败
try
{
TState? newState = JsonSerializer.Deserialize<TState>(...)
}
catch (JsonException ex)
{
Console.WriteLine($"State deserialization failed: {ex.Message}");
// 保持原有状态不变
}
