Spiga

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

协议信息

  • 三大 Agent 协议对比,在 AI Agent 生态中,有三个重要的协议:AG-UIMCPA2A。它们各有分工,共同构成完整的 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 协议定义了清晰的架构组件,包括 ServerClientAgent

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. 混合工具使用

真实场景中,前后端工具经常需要协同工作。例如用户询问"附近有什么餐厅"时:

  1. Frontend Tool (GetUserLocation) → 获取用户当前位置
  2. Backend Tool (SearchRestaurants) → 根据位置搜索附近餐厅
  3. 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}");
    // 保持原有状态不变
}