Spiga

Net+AI智能体进阶7:自定义Agent

2025-11-15 18:12:48

一、自定义 Agent 的实现

1. 场景分析

在大多数场景下,使用 chatClient.CreateAIAgent() 创建的标准 Agent 已经足够强大。但在某些特殊场景下,自定义 Agent 实现能带来更大的价值:

  • 场景 1:规则引擎替代 AI(成本优化)

    • 问题:客服系统每天处理数万次重复性问题("营业时间?","退货流程?"),每次调用 AI 模型都会产生成本。

    • 解决方案:自定义 Agent 使用 FAQ 知识库进行关键词匹配,只在无法匹配时才调用 AI。

    • 收益:

      • 成本降低 70-90%(高频简单问题零成本)
      • 响应速度提升 10 倍(无需等待 AI 推理)

      • 答案一致性更高(预定义标准答案)

  • 场景 2:遗留系统集成(ERP/CRM/工作流引擎)

    • 问题:企业内部有成熟的审批工作流引擎,需要将其包装为 Agent 供统一调度。

    • 解决方案:自定义 Agent 作为适配器,将工作流引擎的 API 转换为 Agent 接口。

    • 收益:

      • 无缝集成现有系统(无需重构)

      • 复用企业级规则引擎(审批、权限、流程)

      • 数据安全可控(不发送敏感数据到外部 AI)

  • 场景 3:测试模拟(Mock Agent)

    • 问题:开发和测试环境中,不希望调用真实 AI 模型(成本、稳定性、可预测性)。
    • 解决方案:自定义 Agent 返回固定或可配置的测试数据。
    • 收益:
      • 单元测试更可靠(确定性输出)
      • 开发环境零成本
      • CI/CD 管道更快(无需等待 AI 响应)
  • 场景 4:混合模式(规则 + AI)

    • 问题:希望结合规则引擎的确定性和 AI 的灵活性。
    • 解决方案:自定义 Agent 先尝试规则匹配,失败后转发给 AI Agent。
    • 收益:
      • 平衡成本与效果
      • 灵活的分流策略(按优先级、置信度)
      • 逐步优化规则库(分析 AI 处理的高频问题)

对比: 标准 Agent vs 自定义 Agent

特性 ChatClientAgent(标准) 自定义 Agent 说明
创建方式 chatClient.CreateAIAgent() 继承 AIAgent 抽象类 标准方式更简单
开发复杂度 自定义需实现所有核心方法
灵活性 受限于 IChatClient 能力 完全可控 自定义可实现任意逻辑
成本 按 Token 计费 固定成本(计算资源) 高频场景自定义更省钱
响应速度 取决于 AI 模型 毫秒级(无网络调用) 规则引擎极快
确定性 不确定(AI 输出有随机性) 完全确定 审批、计算等需要确定性
学习能力 有(通过 Prompt) 无(纯代码逻辑) AI 能处理未知场景
适用场景 复杂推理、开放式对话 规则匹配、系统集成、成本优化 各有所长
互操作性 完全兼容 MAF 生态 完全兼容 MAF 生态 都实现 AIAgent 接口

关键结论:自定义 Agent 不是替代标准 Agent,而是在特定场景下的补充方案。两者可以无缝组合使用。

2. AIAgent 抽象类核心接口详解

AIAgent 是 MAF 框架中所有 Agent 的基类,无论是标准的 ChatClientAgent 还是自定义 Agent,都必须实现此抽象类定义的核心接口。

必须实现的核心成员

public abstract class AIAgent
{
    // 1. Agent 名称(用于标识和日志)
    public abstract string? Name { get; }
    
    // 2. 创建新的对话线程
    public abstract AgentThread GetNewThread();
    
    // 3. 从序列化数据恢复线程(支持持久化)
    public abstract AgentThread DeserializeThread(
        JsonElement serializedThread, 
        JsonSerializerOptions? jsonSerializerOptions = null);
    
    // 4. 同步调用(核心执行逻辑)
    public abstract Task<AgentRunResponse> RunAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        CancellationToken cancellationToken = default);
    
    // 5. 流式调用(支持实时输出)
    public abstract IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        CancellationToken cancellationToken = default);
}

可选成员(继承自基类)

  • Id (string):Agent 唯一标识符(默认自动生成)
  • DisplayName (string):显示名称(默认使用 Name)
  • Description (string?):Agent 描述
  • NotifyThreadOfNewMessagesAsync():通知线程有新消息(关键方法)

3. 自定义 Agent 流程

sequenceDiagram
    participant User as 用户代码
    participant Agent as 自定义 Agent
    participant Thread as AgentThread
    participant Logic as 自定义逻辑
    
    User->>Agent: RunAsync(messages)
    
    alt Thread 为 null
        Agent->>Agent: GetNewThread()
        Agent-->>Thread: 创建新线程
    end
    
    Agent->>Logic: 执行自定义逻辑
    Note over Logic: 可能是:<br/>- 规则匹配<br/>- 数据库查询<br/>- API 调用<br/>- AI 推理
    
    Logic->>Agent: 生成响应消息
    
    Agent->>Agent: 设置消息属性
    Note over Agent: Role = Assistant<br/>AuthorName = Agent.Name<br/>MessageId = Guid
    
    Agent->>Thread: NotifyThreadOfNewMessagesAsync()
    Note over Thread: 保存输入+输出消息<br/>到线程历史
    
    Agent->>User: 返回 AgentRunResponse

关键要点

  • 必须创建或使用提供的 Thread
  • 响应消息必须设置正确的 Role、AuthorName、MessageId
  • 必须调用 NotifyThreadOfNewMessagesAsync() 更新线程历史
  • 返回的 AgentRunResponse 包含所有响应消息

4. 关键设计原则

  • 消息转换原则:输入的 ChatMessage 通常是用户消息 (Role = User),Agent 需要:

    • 创建新的响应消息(或克隆输入消息后修改)
    • 设置 Role = ChatRole.Assistant

    • 设置 AuthorName = this.DisplayName (标识消息来源)

    • 设置 MessageId = Guid.NewGuid().ToString("N") (唯一标识)

  • 线程通知原则:为什么必须调用 NotifyThreadOfNewMessagesAsync()?

    • 维护对话历史(多轮对话的基础)

    • 支持持久化(保存完整上下文)

    • 可观测性(追踪消息流)

    调用时机:在生成响应消息后,返回结果前。

    传入的消息:输入消息 + 输出消息(使用 messages.Concat(responseMessages))

  • 响应构造原则

// AgentRunResponse 结构
new AgentRunResponse
{
    AgentId = this.Id,                    // Agent 标识
    ResponseId = Guid.NewGuid().ToString("N"),  // 本次响应标识
    Messages = responseMessages           // 响应消息列表
};

// AgentRunResponseUpdate 结构(流式)
new AgentRunResponseUpdate
{
    AgentId = this.Id,
    ResponseId = responseId,              // 同一响应的多个 update 共享 ID
    MessageId = messageId,                // 同一消息的多个 update 共享 ID
    AuthorName = this.DisplayName,
    Role = ChatRole.Assistant,
    Contents = [new TextContent(text)]    // 增量内容
};

5. 最小化 Agent 骨架代码

internal sealed class CustomAgentThread : InMemoryAgentThread
{
    internal CustomAgentThread() { }
    internal CustomAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
        : base(serializedThreadState, jsonSerializerOptions) { }
}

/// <summary>
/// 最小化的自定义 Agent 骨架
/// 功能:简单地将用户输入原样返回(Echo Agent)
/// </summary>
public class MinimalAgent : AIAgent
{
    // 1. Agent 名称
    public override string? Name => "MinimalAgent";

    // 2. 创建新线程
    public override AgentThread GetNewThread()
        => new CustomAgentThread();  // 使用内置的内存线程

    // 3. 反序列化线程
    public override AgentThread DeserializeThread(
        JsonElement serializedThread, 
        JsonSerializerOptions? jsonSerializerOptions = null)
        => new CustomAgentThread(serializedThread, jsonSerializerOptions);

    // 4. 同步执行
    public override async Task<AgentRunResponse> RunAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        CancellationToken cancellationToken = default)
    {
        // 步骤 1: 确保有线程
        thread ??= GetNewThread();

        // 步骤 2: 提取最后一条用户消息
        var lastUserMessage = messages.LastOrDefault();
        var userText = lastUserMessage?.Text ?? "(空消息)";

        // 步骤 3: 生成响应消息
        var responseMessage = new ChatMessage(
            ChatRole.Assistant,
            $"Echo: {userText}"
        )
        {
            AuthorName = this.DisplayName,
            MessageId = Guid.NewGuid().ToString("N")
        };

        var responseMessages = new[] { responseMessage };

        // 步骤 4: 通知线程(关键!)
        await NotifyThreadOfNewMessagesAsync(
            thread, 
            messages.Concat(responseMessages), 
            cancellationToken);

        // 步骤 5: 返回响应
        return new AgentRunResponse
        {
            AgentId = this.Id,
            ResponseId = Guid.NewGuid().ToString("N"),
            Messages = responseMessages
        };
    }

    // 5. 流式执行
    public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        thread ??= GetNewThread();
        var lastUserMessage = messages.LastOrDefault();
        var userText = lastUserMessage?.Text ?? "(空消息)";
        var echoText = $"Echo: {userText}";

        var responseMessage = new ChatMessage(ChatRole.Assistant, echoText)
        {
            AuthorName = this.DisplayName,
            MessageId = Guid.NewGuid().ToString("N")
        };

        await NotifyThreadOfNewMessagesAsync(
            thread, 
            messages.Concat(new[] { responseMessage }), 
            cancellationToken);

        // 流式输出:逐字符返回
        var responseId = Guid.NewGuid().ToString("N");
        var messageId = responseMessage.MessageId;

        foreach (var character in echoText)
        {
            yield return new AgentRunResponseUpdate
            {
                AgentId = this.Id,
                ResponseId = responseId,
                MessageId = messageId,
                AuthorName = this.DisplayName,
                Role = ChatRole.Assistant,
                Contents = [new TextContent(character.ToString())]
            };

            await Task.Delay(50, cancellationToken);  // 模拟流式延迟
        }
    }
}
Console.WriteLine("MinimalAgent 定义完成");

测试

var minimalAgent = new MinimalAgent();
// 测试 1: 同步调用
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 1: 同步调用");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

var response1 = await minimalAgent.RunAsync("你好,世界!");
new {
    AgentId = response1.AgentId,
    ResponseId = response1.ResponseId,
    消息数量 = response1.Messages.Count,
    响应内容 = response1.Messages.First().Text
}.Display();

// 测试 2: 流式调用
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 2: 流式调用");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.Write("响应: ");

await foreach (var update in minimalAgent.RunStreamingAsync("流式测试"))
{
    var text = update.Contents.OfType<TextContent>().FirstOrDefault()?.Text;
    Console.Write(text);
}
Console.WriteLine("\n\n测试完成");

// 测试 3: 多轮对话(验证线程管理)
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 3: 多轮对话");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

var thread = minimalAgent.GetNewThread();
// 第 1 轮
var response3_1 = await minimalAgent.RunAsync("第一条消息", thread);
Console.WriteLine($"第 1 轮: {response3_1.Messages.First().Text}");
// 第 2 轮
var response3_2 = await minimalAgent.RunAsync("第二条消息", thread);
Console.WriteLine($"第 2 轮: {response3_2.Messages.First().Text}");
var msgStore = thread.GetService<ChatMessageStore>();
var msgs = await msgStore.GetMessagesAsync();
// 检查线程中的消息历史
Console.WriteLine($"\n线程中的消息总数: {msgs.Count()}");
Console.WriteLine("\n消息历史:");
foreach (var msg in msgs)
{
    Console.WriteLine($"[{msg.Role}] {msg.AuthorName}: {msg.Text}");
}
Console.WriteLine("\n多轮对话测试完成");

6. 核心要点总结

通过 MinimalAgent 示例,我们学到了:

  • 必须实现的 5 个核心方法
    • Name:返回 Agent 名称
    • GetNewThread():创建新线程(通常使用 InMemoryAgentThread)
    • DeserializeThread():从序列化数据恢复线程
    • RunAsync():同步执行逻辑
    • RunStreamingAsync():流式执行逻辑
  • RunAsync 实现的 5 个步骤
flowchart LR
    A[1. 确保线程存在] --> B[2. 提取用户消息]
    B --> C[3. 生成响应消息]
    C --> D[4. 设置消息属性]
    D --> E[5. 通知线程]
    E --> F[6. 返回响应]
    
    style A fill:#e3f2fd
    style E fill:#fff3e0
    style F fill:#e8f5e9
  • 消息属性检查清单

    • Role = ChatRole.Assistant
    • AuthorName = this.DisplayName
    • MessageId = Guid.NewGuid().ToString("N")
    • 调用 NotifyThreadOfNewMessagesAsync()
  • 流式调用要点

    • 使用 yield return 逐步返回 AgentRunResponseUpdate

    • 同一响应的所有 update 共享同一个 ResponseId

    • 同一消息的所有 update 共享同一个 MessageId

    • 支持 [EnumeratorCancellation] 特性处理取消

二、实战:FAQ 自动回复 Agent

1. 场景描述

企业客服系统每天收到数万次重复性咨询:

  • "营业时间是什么?"
  • "如何退货?"
  • "配送需要多久?"
  • "支持哪些支付方式?"

这些高频问题每次都调用 AI 模型不仅成本高昂,而且响应速度慢。我们的解决方案:使用自定义 Agent 实现 FAQ 知识库匹配,零成本、毫秒级响应!

实现目标

  • 定义 FAQ 知识库(字典结构)
  • 实现关键词匹配逻辑(简单但实用)
  • 支持同步和流式调用
  • 支持多轮对话
  • 匹配失败时返回友好提示

2. 定义 FAQ 知识库

/// <summary>
/// FAQ 自动回复 Agent
/// 功能:通过关键词匹配返回预定义答案,零成本快速响应
/// </summary>
public class FaqAgent : AIAgent
{
    // FAQ 知识库
    private readonly Dictionary<string, string> _faqDatabase = new()
    {
        // 营业相关
        ["营业时间"] = "我们的营业时间是:\n- 周一至周五: 9:00-18:00\n- 周六日: 10:00-17:00\n- 节假日: 休息",
        ["工作时间"] = "我们的营业时间是:\n- 周一至周五: 9:00-18:00\n- 周六日: 10:00-17:00\n- 节假日: 休息",
        
        // 退货相关
        ["退货"] = "退货流程:\n1. 登录账户,进入订单详情\n2. 点击\"申请退货\"\n3. 选择退货原因并上传照片\n4. 等待审核通过\n5. 打包商品并寄回\n6. 收到商品后 3-5 个工作日退款",
        ["退款"] = "退货流程:\n1. 登录账户,进入订单详情\n2. 点击\"申请退货\"\n3. 选择退货原因并上传照片\n4. 等待审核通过\n5. 打包商品并寄回\n6. 收到商品后 3-5 个工作日退款",
        
        // 配送相关
        ["配送"] = "配送时效:\n- 同城: 24 小时内送达\n- 省内: 2-3 个工作日\n- 跨省: 3-5 个工作日\n- 偏远地区: 5-7 个工作日",
        ["快递"] = "配送时效:\n- 同城: 24 小时内送达\n- 省内: 2-3 个工作日\n- 跨省: 3-5 个工作日\n- 偏远地区: 5-7 个工作日",
        ["物流"] = "配送时效:\n- 同城: 24 小时内送达\n- 省内: 2-3 个工作日\n- 跨省: 3-5 个工作日\n- 偏远地区: 5-7 个工作日",
        
        // 支付相关
        ["支付"] = "支持的支付方式:\n 微信支付\n 支付宝\n 银行卡\n 信用卡\n 货到付款(部分地区)",
        ["付款"] = "支持的支付方式:\n 微信支付\n 支付宝\n 银行卡\n 信用卡\n 货到付款(部分地区)",
        
        // 联系方式
        ["联系"] = "联系我们:\n客服热线: 400-123-4567\n邮箱: service@example.com\n在线客服: 网站右下角聊天窗口",
        ["电话"] = "联系我们:\n客服热线: 400-123-4567\n邮箱: service@example.com\n💬 在线客服: 网站右下角聊天窗口",
        ["客服"] = "联系我们:\n客服热线: 400-123-4567\n邮箱: service@example.com\n💬 在线客服: 网站右下角聊天窗口"
    };

    public override string? Name => "FaqAgent";

    public override AgentThread GetNewThread()
        => new CustomAgentThread();

    public override AgentThread DeserializeThread(
        JsonElement serializedThread, 
        JsonSerializerOptions? jsonSerializerOptions = null)
        => new CustomAgentThread(serializedThread, jsonSerializerOptions);

    // 关键词匹配逻辑
    private string? MatchFaq(string userInput)
    {
        // 遍历 FAQ 知识库,查找包含关键词的问题
        foreach (var kvp in _faqDatabase)
        {
            if (userInput.Contains(kvp.Key, StringComparison.OrdinalIgnoreCase))
            {
                return kvp.Value;
            }
        }
        return null;  // 未匹配到
    }

    public override async Task<AgentRunResponse> RunAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        CancellationToken cancellationToken = default)
    {
        thread ??= GetNewThread();
        
        // 提取最后一条用户消息
        var lastUserMessage = messages.LastOrDefault();
        var userText = lastUserMessage?.Text ?? "";

        // 尝试匹配 FAQ
        var answer = MatchFaq(userText);
        
        // 构造响应内容
        var responseText = answer ?? 
            "抱歉,我没有找到相关的 FAQ。\n\n您可以:\n" +
            "1.换个方式描述您的问题\n" +
            "2.拨打客服热线: 400-123-4567\n" +
            "3.发送邮件: service@example.com";

        // 创建响应消息
        var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
        {
            AuthorName = this.DisplayName,
            MessageId = Guid.NewGuid().ToString("N")
        };

        var responseMessages = new[] { responseMessage };

        // 通知线程
        await NotifyThreadOfNewMessagesAsync(
            thread, 
            messages.Concat(responseMessages), 
            cancellationToken);

        return new AgentRunResponse
        {
            AgentId = this.Id,
            ResponseId = Guid.NewGuid().ToString("N"),
            Messages = responseMessages
        };
    }

    public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        thread ??= GetNewThread();
        var lastUserMessage = messages.LastOrDefault();
        var userText = lastUserMessage?.Text ?? "";

        var answer = MatchFaq(userText);
        var responseText = answer ?? 
            "抱歉,我没有找到相关的 FAQ。\n\n您可以:\n" +
            "1.换个方式描述您的问题\n" +
            "2.拨打客服热线: 400-123-4567\n" +
            "3.发送邮件: service@example.com";

        var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
        {
            AuthorName = this.DisplayName,
            MessageId = Guid.NewGuid().ToString("N")
        };

        await NotifyThreadOfNewMessagesAsync(
            thread, 
            messages.Concat(new[] { responseMessage }), 
            cancellationToken);

        // 流式输出:逐字返回(模拟打字效果)
        var responseId = Guid.NewGuid().ToString("N");
        var messageId = responseMessage.MessageId;

        foreach (var character in responseText)
        {
            yield return new AgentRunResponseUpdate
            {
                AgentId = this.Id,
                ResponseId = responseId,
                MessageId = messageId,
                AuthorName = this.DisplayName,
                Role = ChatRole.Assistant,
                Contents = [new TextContent(character.ToString())]
            };
            await Task.Delay(20, cancellationToken);  // 快速流式输出
        }
    }
}
Console.WriteLine("FaqAgent 定义完成");

3. 关键设计点解析

  • FAQ 知识库 (Dictionary)

    • 使用 Dictionary<string, string> 存储关键词和答案

    • 一个答案可以对应多个关键词(如"退货"和"退款")

    • 易于维护和扩展

  • 匹配逻辑 (MatchFaq)

    userInput.Contains(kvp.Key, StringComparison.OrdinalIgnoreCase)
    
    • 简单的关键词包含匹配
    • 不区分大小写
    • 返回第一个匹配结果
  • 友好的失败提示

    • 匹配失败时不返回空响应

    • 提供替代联系方式

    • 引导用户换个方式提问

  • 流式输出优化

    • Task.Delay(20) 快速流式输出

    • 模拟真实打字效果

    • 提升用户体验

4. 测试 FaqAgent

var faqAgent = new FaqAgent();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 1: 匹配成功 - 营业时间");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var response1 = await faqAgent.RunAsync("请问你们的营业时间是什么?");
Console.WriteLine(response1.Messages.First().Text);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 2: 匹配成功 - 退货流程");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var response2 = await faqAgent.RunAsync("我想退货,怎么操作?");
Console.WriteLine(response2.Messages.First().Text);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 3: 匹配失败 - 未知问题");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var response3 = await faqAgent.RunAsync("你们有宠物用品吗?");
Console.WriteLine(response3.Messages.First().Text);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 4: 流式输出");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.Write("响应: ");
await foreach (var update in faqAgent.RunStreamingAsync("快递需要多久?"))
{
    var text = update.Contents.OfType<TextContent>().FirstOrDefault()?.Text;
    Console.Write(text);
}
Console.WriteLine("\n\n所有测试完成");

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 5: 多轮对话");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var thread = faqAgent.GetNewThread();
// 第 1 轮:咨询支付
var r1 = await faqAgent.RunAsync("支持哪些支付方式?", thread);
Console.WriteLine($"第 1 轮:\nQ: 支持哪些支付方式?\nA: {r1.Messages.First().Text}\n");
// 第 2 轮:咨询配送
var r2 = await faqAgent.RunAsync("配送要多久?", thread);
Console.WriteLine($"第 2 轮:\nQ: 配送要多久?\nA: {r2.Messages.First().Text}\n");
// 第 3 轮:咨询联系方式
var r3 = await faqAgent.RunAsync("怎么联系客服?", thread);
Console.WriteLine($"第 3 轮:\nQ: 怎么联系客服?\nA: {r3.Messages.First().Text}\n");
var msgStore = thread.GetService<ChatMessageStore>();
var msgs = await msgStore.GetMessagesAsync();

Console.WriteLine($"线程中的消息总数: {msgs.Count()}");
Console.WriteLine("多轮对话测试完成");

5. FaqAgent 价值分析

让我们对比传统 AI Agent 和 FaqAgent 的成本与性能:

指标 传统 AI Agent FaqAgent 改进幅度
响应时间 1-3 秒 < 50 毫秒 快 60 倍
单次成本 ¥0.001-0.01 ¥0 节省 100%
日处理 10 万次成本 ¥100-1000 ¥0 节省 100%
答案一致性 80-95%(有随机性) 100%(完全确定) 提升 5-20%
高并发能力 受限于 API 限流 仅受服务器性能限制 提升 10+ 倍
  • 实际场景计算

    • 假设客服系统每天 10 万次咨询

    • 其中 60% (6 万次) 是 FAQ 能解决的

    • 使用 FaqAgent:

      • 成本节省: 6 万 × ¥0.005 = ¥300/天 = ¥10 万/年
      • 响应速度提升: 从 2 秒 → 0.05 秒
      • 用户体验显著改善

6. 优化建议

  • 更智能的匹配算法
// 当前:简单的包含匹配
userInput.Contains(keyword)

// 优化:模糊匹配 + 相似度计算
- 使用 Levenshtein 距离计算相似度
- 支持同义词("购买" = "买" = "下单")
- 支持拼音匹配(容错)
  • 缓存热门问题统计
// 记录每个 FAQ 的访问次数
// 优先匹配高频问题
// 定期分析未匹配问题,扩充知识库
  • 多轮对话引导
// 当匹配模糊时,提供选项
"您是想问以下哪个问题?
1.退货流程
2.退款到账时间
3.退货运费"
  • 与 AI Agent 混合
// FaqAgent 匹配失败时
// 自动转发给 AI Agent 处理

三、实战:审批工作流 Agent

1. 场景描述

企业审批系统需要处理各种类型的审批申请:

  • 请假申请:根据天数和职级自动审批或转人工
  • 报销申请:根据金额和部门自动审批或转人工
  • 采购申请:根据金额和物品类型判断审批流程

这些审批流程有明确的规则引擎,不需要 AI 推理,但需要:

  • 多轮对话收集信息
  • 状态管理(记住已收集的信息)
  • 规则匹配(自动判断审批结果)
  • 状态持久化(支持中断恢复)

使用自定义 Agent 包装现有规则引擎,实现智能审批助手!

实现目标

  • 定义审批规则引擎
  • 实现多轮对话状态机
  • 自定义 Thread 存储对话状态
  • 支持序列化/反序列化(中断恢复)
  • 规则匹配并返回审批结果

2. 目标实现

  • 步骤1:定义审批规则和状态
/// <summary>
/// 审批类型
/// </summary>
public enum ApprovalType
{
    未知,
    请假,
    报销,
    采购
}

/// <summary>
/// 审批状态
/// </summary>
public enum ApprovalStatus
{
    收集信息中,
    自动通过,
    需要主管审批,
    需要总监审批,
    自动拒绝
}

/// <summary>
/// 审批规则
/// </summary>
public class ApprovalRule
{
    public ApprovalType Type { get; set; }
    public decimal MaxAmount { get; set; }  // 最大金额(报销/采购)
    public int MaxDays { get; set; }        // 最大天数(请假)
    public ApprovalStatus Result { get; set; }
    public string Reason { get; set; } = "";
}

/// <summary>
/// 对话状态(存储在 Thread 中)
/// </summary>
public class ApprovalConversationState
{
    public ApprovalType Type { get; set; } = ApprovalType.未知;
    public decimal Amount { get; set; }     // 金额
    public int Days { get; set; }           // 天数
    public string Description { get; set; } = "";  // 申请描述
    public bool HasType { get; set; }
    public bool HasAmount { get; set; }
    public bool HasDays { get; set; }
    public bool HasDescription { get; set; }
}
  • 步骤2:实现自定义 Thread 存储状态
/// <summary>
/// 审批 Agent 的自定义 Thread
/// 扩展 InMemoryAgentThread,添加对话状态存储
/// </summary>
public class ApprovalAgentThread : InMemoryAgentThread
{
    // 对话状态
    public ApprovalConversationState State { get; set; } = new();
    
    public ApprovalAgentThread() : base() { }

    // 反序列化构造函数
    public ApprovalAgentThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
        : base(serializedThread, jsonSerializerOptions)
    {
        // 从序列化数据中恢复状态
        if (serializedThread.TryGetProperty("state", out var stateElement))
        {
            State = JsonSerializer.Deserialize<ApprovalConversationState>(
                stateElement.GetRawText(), 
                jsonSerializerOptions) ?? new();
        }
    }

    // 重写序列化方法,包含状态
    public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
    {
        var baseJson = base.Serialize(jsonSerializerOptions);
        var dict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(baseJson.GetRawText()) 
            ?? new Dictionary<string, JsonElement>();

        // 添加状态到序列化结果
        var stateJson = JsonSerializer.SerializeToElement(State, jsonSerializerOptions);
        dict["state"] = stateJson;
        return JsonSerializer.SerializeToElement(dict, jsonSerializerOptions);
    }
}
  • 步骤3:实现审批 Agent 核心逻辑
/// <summary>
/// 审批工作流 Agent
/// 功能:多轮对话收集信息,基于规则引擎自动审批
/// </summary>
public class ApprovalAgent : AIAgent
{
    // 审批规则库
    private readonly List<ApprovalRule> _rules = new()
    {
        // 请假规则
        new() { Type = ApprovalType.请假, MaxDays = 3, Result = ApprovalStatus.自动通过, Reason = "3天以内请假自动通过" },
        new() { Type = ApprovalType.请假, MaxDays = 7, Result = ApprovalStatus.需要主管审批, Reason = "3-7天请假需要主管审批" },
        new() { Type = ApprovalType.请假, MaxDays = int.MaxValue, Result = ApprovalStatus.需要总监审批, Reason = "超过7天请假需要总监审批" },
        // 报销规则
        new() { Type = ApprovalType.报销, MaxAmount = 1000, Result = ApprovalStatus.自动通过, Reason = "1000元以内报销自动通过" },
        new() { Type = ApprovalType.报销, MaxAmount = 5000, Result = ApprovalStatus.需要主管审批, Reason = "1000-5000元报销需要主管审批" },
        new() { Type = ApprovalType.报销, MaxAmount = decimal.MaxValue, Result = ApprovalStatus.需要总监审批, Reason = "超过5000元报销需要总监审批" },
        // 采购规则
        new() { Type = ApprovalType.采购, MaxAmount = 3000, Result = ApprovalStatus.自动通过, Reason = "3000元以内采购自动通过" },
        new() { Type = ApprovalType.采购, MaxAmount = 10000, Result = ApprovalStatus.需要主管审批, Reason = "3000-10000元采购需要主管审批" },
        new() { Type = ApprovalType.采购, MaxAmount = decimal.MaxValue, Result = ApprovalStatus.需要总监审批, Reason = "超过10000元采购需要总监审批" }
    };

    public override string? Name => "ApprovalAgent";

    public override AgentThread GetNewThread() => new ApprovalAgentThread();

    public override AgentThread DeserializeThread(
        JsonElement serializedThread, 
        JsonSerializerOptions? jsonSerializerOptions = null)
        => new ApprovalAgentThread(serializedThread, jsonSerializerOptions);

    // 从用户输入中提取审批类型
    private ApprovalType ExtractType(string input)
    {
        if (input.Contains("请假", StringComparison.OrdinalIgnoreCase)) return ApprovalType.请假;
        if (input.Contains("报销", StringComparison.OrdinalIgnoreCase)) return ApprovalType.报销;
        if (input.Contains("采购", StringComparison.OrdinalIgnoreCase)) return ApprovalType.采购;
        return ApprovalType.未知;
    }

    // 从用户输入中提取数字(金额或天数)
    private decimal ExtractNumber(string input)
    {
        var match = System.Text.RegularExpressions.Regex.Match(input, @"\d+(\.\d+)?");
        return match.Success ? decimal.Parse(match.Value) : 0;
    }

    // 根据规则匹配审批结果
    private ApprovalRule? MatchRule(ApprovalType type, decimal amount, int days)
    {
        var applicableRules = _rules.Where(r => r.Type == type).OrderBy(r => 
            type == ApprovalType.请假 ? r.MaxDays : r.MaxAmount);

        foreach (var rule in applicableRules)
        {
            if (type == ApprovalType.请假 && days <= rule.MaxDays)
                return rule;
            if (type != ApprovalType.请假 && amount <= rule.MaxAmount)
                return rule;
        }
        return null;
    }

    // 处理对话逻辑
    private string ProcessConversation(ApprovalAgentThread thread, string userInput)
    {
        var state = thread.State;

        // 步骤 1: 识别审批类型
        if (!state.HasType)
        {
            var type = ExtractType(userInput);
            if (type != ApprovalType.未知)
            {
                state.Type = type;
                state.HasType = true;

                // 根据类型询问相应信息
                return type switch
                {
                    ApprovalType.请假 => "好的,请问您要请假几天?",
                    ApprovalType.报销 => "好的,请问报销金额是多少元?",
                    ApprovalType.采购 => "好的,请问采购金额是多少元?",
                    _ => "抱歉,我没理解您的申请类型。请说明是【请假】、【报销】还是【采购】?"
                };
            }
            else
            {
                return "您好!我是审批助手。\n\n请告诉我您要申请什么类型的审批:\n请假\n报销\n采购";
            }
        }

        // 步骤 2: 收集金额或天数
        if (state.Type == ApprovalType.请假 && !state.HasDays)
        {
            var days = (int)ExtractNumber(userInput);
            if (days > 0)
            {
                state.Days = days;
                state.HasDays = true;
                return "请简单描述一下请假事由:";
            }
            else
            {
                return "请输入请假天数(例如:3天)";
            }
        }

        if ((state.Type == ApprovalType.报销 || state.Type == ApprovalType.采购) && !state.HasAmount)
        {
            var amount = ExtractNumber(userInput);
            if (amount > 0)
            {
                state.Amount = amount;
                state.HasAmount = true;
                return $"请简单描述一下{(state.Type == ApprovalType.报销 ? "报销" : "采购")}事由:";
            }
            else
            {
                return "请输入金额(例如:500元 或 500)";
            }
        }

        // 步骤 3: 收集描述
        if (!state.HasDescription)
        {
            state.Description = userInput;
            state.HasDescription = true;
            // 继续到审批逻辑
        }

        // 步骤 4: 执行审批规则匹配
        var rule = MatchRule(state.Type, state.Amount, state.Days);
        if (rule == null)
        {
            return "审批规则匹配失败,请联系管理员。";
        }

        // 构造审批结果
        var result = $"**审批申请摘要**\n\n";
        result += $"- 类型: {state.Type}\n";
        if (state.Type == ApprovalType.请假)
            result += $"- 天数: {state.Days} 天\n";
        else
            result += $"- 金额: ¥{state.Amount:N2}\n";
        result += $"- 事由: {state.Description}\n\n";

        result += $"**审批结果**: {rule.Result}\n";
        result += $"**理由**: {rule.Reason}\n\n";

        result += rule.Result switch
        {
            ApprovalStatus.自动通过 => "您的申请已自动通过!",
            ApprovalStatus.需要主管审批 => "您的申请已提交,等待主管审批...",
            ApprovalStatus.需要总监审批 => "您的申请已提交,等待总监审批...",
            _ => "您的申请未通过。"
        };
        return result;
    }

    public override async Task<AgentRunResponse> RunAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        CancellationToken cancellationToken = default)
    {
        var approvalThread = (thread as ApprovalAgentThread) ?? (ApprovalAgentThread)GetNewThread();

        var lastUserMessage = messages.LastOrDefault();
        var userText = lastUserMessage?.Text ?? "";

        // 处理对话
        var responseText = ProcessConversation(approvalThread, userText);

        var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
        {
            AuthorName = this.DisplayName,
            MessageId = Guid.NewGuid().ToString("N")
        };

        var responseMessages = new[] { responseMessage };

        await NotifyThreadOfNewMessagesAsync(
            approvalThread, 
            messages.Concat(responseMessages), 
            cancellationToken);

        return new AgentRunResponse
        {
            AgentId = this.Id,
            ResponseId = Guid.NewGuid().ToString("N"),
            Messages = responseMessages
        };
    }

    public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        var approvalThread = (thread as ApprovalAgentThread) ?? (ApprovalAgentThread)GetNewThread();
        var lastUserMessage = messages.LastOrDefault();
        var userText = lastUserMessage?.Text ?? "";

        var responseText = ProcessConversation(approvalThread, userText);

        var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
        {
            AuthorName = this.DisplayName,
            MessageId = Guid.NewGuid().ToString("N")
        };

        await NotifyThreadOfNewMessagesAsync(
            approvalThread, 
            messages.Concat(new[] { responseMessage }), 
            cancellationToken);

        var responseId = Guid.NewGuid().ToString("N");
        var messageId = responseMessage.MessageId;

        foreach (var character in responseText)
        {
            yield return new AgentRunResponseUpdate
            {
                AgentId = this.Id,
                ResponseId = responseId,
                MessageId = messageId,
                AuthorName = this.DisplayName,
                Role = ChatRole.Assistant,
                Contents = [new TextContent(character.ToString())]
            };
            await Task.Delay(15, cancellationToken);
        }
    }
}

3. 关键设计点解析

  • 自定义 Thread 存储状态

    public class ApprovalAgentThread : InMemoryAgentThread
    {
        public ApprovalConversationState State { get; set; } = new();
        // 重写 Serialize() 包含状态
    }
    
    • 扩展 InMemoryAgentThread 添加自定义状态

    • 实现序列化/反序列化支持持久化

  • 多轮对话状态机

    未知类型 → 识别类型 → 收集数字 → 收集描述 → 规则匹配 → 返回结果
    
    • 使用布尔标志跟踪每个步骤完成情况

    • 根据当前状态决定下一步询问什么

  • 规则引擎

    private ApprovalRule? MatchRule(ApprovalType type, decimal amount, int days)
    
    • 预定义规则列表

    • 按金额/天数排序后匹配第一个符合条件的规则

    • 企业可以动态加载规则(从数据库、配置文件等)

  • 信息提取

    ExtractType()   // 关键词匹配
    ExtractNumber() // 正则表达式提取数字
    
    • 简单但实用的 NLU(自然语言理解)
    • 生产环境可以使用更复杂的 NLP 库

4. 测试 ApprovalAgent

var approvalAgent = new ApprovalAgent();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 1: 请假申请(2天 - 自动通过)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

var thread1 = approvalAgent.GetNewThread();
// 第 1 轮:说明申请类型
var r1_1 = await approvalAgent.RunAsync("我要请假", thread1);
Console.WriteLine($"Agent: {r1_1.Messages.First().Text}\n");

// 第 2 轮:提供天数
var r1_2 = await approvalAgent.RunAsync("2天", thread1);
Console.WriteLine($"Agent: {r1_2.Messages.First().Text}\n");

// 第 3 轮:提供事由
var r1_3 = await approvalAgent.RunAsync("家里有事", thread1);
Console.WriteLine($"Agent:\n{r1_3.Messages.First().Text}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 2: 报销申请(3500元 - 需要主管审批)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

var thread2 = approvalAgent.GetNewThread();
var r2_1 = await approvalAgent.RunAsync("申请报销", thread2);
Console.WriteLine($"Agent: {r2_1.Messages.First().Text}\n");
var r2_2 = await approvalAgent.RunAsync("3500元", thread2);
Console.WriteLine($"Agent: {r2_2.Messages.First().Text}\n");
var r2_3 = await approvalAgent.RunAsync("出差住宿费", thread2);
Console.WriteLine($"Agent:\n{r2_3.Messages.First().Text}\n");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 3: 采购申请(15000元 - 需要总监审批)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

var thread3 = approvalAgent.GetNewThread();
var r3_1 = await approvalAgent.RunAsync("我要采购", thread3);
Console.WriteLine($"Agent: {r3_1.Messages.First().Text}\n");
var r3_2 = await approvalAgent.RunAsync("15000", thread3);
Console.WriteLine($"Agent: {r3_2.Messages.First().Text}\n");
var r3_3 = await approvalAgent.RunAsync("办公设备升级", thread3);
Console.WriteLine($"Agent:\n{r3_3.Messages.First().Text}\n");

测试序列化与恢复

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 4: 序列化与恢复");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

// 开始新对话
var thread4 = approvalAgent.GetNewThread();
var r4_1 = await approvalAgent.RunAsync("我要报销", thread4);
Console.WriteLine($"第 1 轮 - Agent: {r4_1.Messages.First().Text}\n");
var r4_2 = await approvalAgent.RunAsync("800元", thread4);
Console.WriteLine($"第 2 轮 - Agent: {r4_2.Messages.First().Text}\n");

// 模拟中断:序列化状态
Console.WriteLine("模拟对话中断,序列化状态...");
var serialized = thread4.Serialize();
Console.WriteLine($"序列化数据大小: {serialized.GetRawText().Length} 字符\n");

// 模拟恢复:反序列化
Console.WriteLine("恢复对话...");
var restoredThread = approvalAgent.DeserializeThread(serialized) as ApprovalAgentThread;
Console.WriteLine($"恢复的状态:");
new {
    类型 = restoredThread!.State.Type,
    金额 = restoredThread.State.Amount,
    已收集类型 = restoredThread.State.HasType,
    已收集金额 = restoredThread.State.HasAmount
}.Display();

// 继续对话
var r4_3 = await approvalAgent.RunAsync("团队聚餐", restoredThread);
Console.WriteLine($"\n第 3 轮(恢复后)- Agent:\n{r4_3.Messages.First().Text}\n");
Console.WriteLine("序列化与恢复测试成功!");

5. ApprovalAgent 技术要点总结

核心技术点

  • 自定义 Thread 扩展
    • 在 Thread 中存储自定义业务状态
    • 支持序列化/反序列化实现对话恢复
    • 无需外部状态存储(简化架构)
public class ApprovalAgentThread : InMemoryAgentThread
{
    public ApprovalConversationState State { get; set; }
    public override JsonElement Serialize() { /* 包含状态 */ }
}
  • 多轮对话状态机
    • 结构化的对话流程
    • 可扩展到更复杂的工作流
    • 每个状态明确的输入/输出
stateDiagram-v2
    [*] --> 询问类型
    询问类型 --> 询问数字: 识别到类型
    询问数字 --> 询问事由: 提取到数字
    询问事由 --> 规则匹配: 收到描述
    规则匹配 --> [*]: 返回结果
  • 规则引擎集成
    • 确定性的审批结果(无 AI 随机性)
    • 规则可外部化(数据库、配置文件)
    • 支持复杂的企业审批逻辑
private readonly List<ApprovalRule> _rules = new() { /* 规则列表 */ };
private ApprovalRule? MatchRule(ApprovalType type, decimal amount, int days)
  • 序列化与持久化
    • 支持对话中断恢复
    • 可保存到数据库、Redis、文件等
    • 无状态服务架构的基础
var serialized = thread.Serialize();
var restored = agent.DeserializeThread(serialized);

对比: 传统工作流 vs Agent 工作流

特性 传统工作流系统 Agent 工作流 优势
交互方式 表单填写 自然对话 用户体验更好
集成难度 需要 UI 集成 只需接入对话接口 开发成本低
灵活性 固定流程 可中断/恢复 更人性化
扩展性 修改表单和逻辑 调整对话流程 更易维护
多渠道 需要多套 UI 统一对话接口 跨平台一致性

生产环境优化建议

  • 规则外部化
// 从数据库加载规则
_rules = await LoadRulesFromDatabase();

// 从配置文件加载
_rules = LoadRulesFromConfig("approval-rules.json");
  • 更智能的 NLU
// 使用 NLP 库提取实体
var (type, amount, days) = await NlpService.ExtractEntities(userInput);

// 支持更自然的表达
"我想请3天假" → 一次性提取类型和天数
  • 审批流程追踪
// 记录审批历史
await AuditLog.RecordApproval(userId, type, amount, result);

// 通知相关人员
await NotificationService.NotifyApprover(approverEmail, request);
  • 与现有系统集成
// 调用 ERP API 提交审批
var approvalId = await ErpClient.SubmitApproval(request);

// 轮询审批状态
var status = await ErpClient.GetApprovalStatus(approvalId);

四、实战3 - 数据查询 Agent

1. 场景描述

企业数据报表系统需要响应各种数据查询请求:

  • 销售数据: "今天的销售额是多少?"
  • 库存数据: "还有多少库存?"
  • 用户数据: "当前有多少活跃用户?"
  • 财务数据: "本月收入是多少?"

传统做法是让 AI 生成 SQL,但这存在风险:

  • SQL 注入风险
  • 生成的 SQL 可能有误
  • 每次查询都消耗 AI Token
  • 响应速度慢

使用自定义 Agent 预定义安全的查询命令,零成本、高安全!

2. 实现目标

  • 步骤 1: 定义数据源和查询命令
/// <summary>
/// 企业数据源模拟(生产环境中会连接真实数据库)
/// </summary>
public class EnterpriseDataSource
{
    // 销售数据
    private readonly List<(DateTime Date, decimal Amount)> _salesData = new()
    {
        (DateTime.Today, 125680.50m),
        (DateTime.Today.AddDays(-1), 98500.30m),
        (DateTime.Today.AddDays(-2), 110200.00m)
    };

    // 库存数据
    private readonly Dictionary<string, int> _inventoryData = new()
    {
        ["手机"] = 350,
        ["笔记本电脑"] = 120,
        ["平板"] = 200,
        ["耳机"] = 850,
        ["充电器"] = 1200
    };

    // 用户数据
    private int _totalUsers = 125680;
    private int _activeUsers = 45230;
    private int _newUsersToday = 1250;

    // 财务数据
    private decimal _monthlyRevenue = 3850000.00m;
    private decimal _monthlyExpense = 1520000.00m;

    // 查询方法
    public decimal GetTodaySales() => _salesData.First(s => s.Date == DateTime.Today).Amount;
    public decimal GetYesterdaySales() => _salesData.First(s => s.Date == DateTime.Today.AddDays(-1)).Amount;
    public decimal GetWeeklySales() => _salesData.Sum(s => s.Amount);
    
    public int GetInventory(string product) 
        => _inventoryData.TryGetValue(product, out var count) ? count : 0;
    public Dictionary<string, int> GetAllInventory() => _inventoryData;
    
    public int GetTotalUsers() => _totalUsers;
    public int GetActiveUsers() => _activeUsers;
    public int GetNewUsersToday() => _newUsersToday;
    
    public decimal GetMonthlyRevenue() => _monthlyRevenue;
    public decimal GetMonthlyExpense() => _monthlyExpense;
    public decimal GetMonthlyProfit() => _monthlyRevenue - _monthlyExpense;
}
  • 步骤 2: 实现数据查询 Agent
/// <summary>
/// 数据查询 Agent
/// 功能:将自然语言查询映射到预定义的安全查询命令
/// </summary>
public class DataQueryAgent : AIAgent
{
    private readonly EnterpriseDataSource _dataSource = new();

    // 查询命令映射(关键词 → 查询方法)
    private readonly Dictionary<string, Func<string>> _queryCommands;

    public DataQueryAgent()
    {
        _queryCommands = new()
        {
            // 销售相关
            ["今天销售"] = () => FormatSalesResult("今日销售额", _dataSource.GetTodaySales()),
            ["今日销售"] = () => FormatSalesResult("今日销售额", _dataSource.GetTodaySales()),
            ["昨天销售"] = () => FormatSalesResult("昨日销售额", _dataSource.GetYesterdaySales()),
            ["本周销售"] = () => FormatSalesResult("本周销售额", _dataSource.GetWeeklySales()),
            
            // 库存相关
            ["库存"] = () => FormatInventoryResult(_dataSource.GetAllInventory()),
            
            // 用户相关
            ["总用户"] = () => FormatUserResult("总用户数", _dataSource.GetTotalUsers()),
            ["用户总数"] = () => FormatUserResult("总用户数", _dataSource.GetTotalUsers()),
            ["活跃用户"] = () => FormatUserResult("活跃用户数", _dataSource.GetActiveUsers()),
            ["新增用户"] = () => FormatUserResult("今日新增用户", _dataSource.GetNewUsersToday()),
            
            // 财务相关
            ["本月收入"] = () => FormatFinanceResult("本月收入", _dataSource.GetMonthlyRevenue()),
            ["本月支出"] = () => FormatFinanceResult("本月支出", _dataSource.GetMonthlyExpense()),
            ["本月利润"] = () => FormatFinanceResult("本月利润", _dataSource.GetMonthlyProfit())
        };
    }

    public override string? Name => "DataQueryAgent";

    public override AgentThread GetNewThread() => new CustomAgentThread();

    public override AgentThread DeserializeThread(
        JsonElement serializedThread, 
        JsonSerializerOptions? jsonSerializerOptions = null)
        => new CustomAgentThread(serializedThread, jsonSerializerOptions);

    // 意图识别:匹配查询命令
    private string? MatchQuery(string userInput)
    {
        foreach (var kvp in _queryCommands)
        {
            if (userInput.Contains(kvp.Key, StringComparison.OrdinalIgnoreCase))
            {
                return kvp.Value();
            }
        }
        return null;
    }

    // 格式化销售结果
    private string FormatSalesResult(string label, decimal amount)
    {
        return $"**{label}**\n\n" +
               $"金额: ¥{amount:N2}\n" +
               $"趋势: {(amount > 100000 ? "增长" : "下降")}\n" +
               $"查询时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
    }

    // 格式化库存结果
    private string FormatInventoryResult(Dictionary<string, int> inventory)
    {
        var result = "**当前库存情况**\n\n";
        foreach (var item in inventory.OrderByDescending(i => i.Value))
        {
            var status = item.Value > 500 ? "充足" : 
                        item.Value > 200 ? "正常" : "偏低";
            result += $"- {item.Key}: {item.Value} 件 {status}\n";
        }
        result += $"\n查询时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
        return result;
    }

    // 格式化用户结果
    private string FormatUserResult(string label, int count)
    {
        return $"**{label}**\n\n" +
               $"数量: {count:N0}\n" +
               $"查询时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
    }

    // 格式化财务结果
    private string FormatFinanceResult(string label, decimal amount)
    {
        var emoji = label.Contains("利润") ? "💵" : 
                   label.Contains("收入") ? "💰" : "💸";
        return $"{emoji} **{label}**\n\n" +
               $"金额: ¥{amount:N2}\n" +
               $"查询时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
    }

    public override async Task<AgentRunResponse> RunAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        CancellationToken cancellationToken = default)
    {
        thread ??= GetNewThread();
        
        var lastUserMessage = messages.LastOrDefault();
        var userText = lastUserMessage?.Text ?? "";

        // 尝试匹配查询
        var result = MatchQuery(userText);
        
        var responseText = result ?? 
            "未识别的查询命令。\n\n" +
            "我支持以下查询:\n\n" +
            "**销售数据**\n" +
            "- 今天/昨天/本周销售额\n\n" +
            "**库存数据**\n" +
            "- 库存情况\n\n" +
            "**用户数据**\n" +
            "- 总用户数\n" +
            "- 活跃用户\n" +
            "- 新增用户\n\n" +
            "**财务数据**\n" +
            "- 本月收入/支出/利润";

        var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
        {
            AuthorName = this.DisplayName,
            MessageId = Guid.NewGuid().ToString("N")
        };

        var responseMessages = new[] { responseMessage };

        await NotifyThreadOfNewMessagesAsync(
            thread, 
            messages.Concat(responseMessages), 
            cancellationToken);

        return new AgentRunResponse
        {
            AgentId = this.Id,
            ResponseId = Guid.NewGuid().ToString("N"),
            Messages = responseMessages
        };
    }

    public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        thread ??= GetNewThread();
        var lastUserMessage = messages.LastOrDefault();
        var userText = lastUserMessage?.Text ?? "";

        var result = MatchQuery(userText);
        var responseText = result ?? 
            "未识别的查询命令。\n\n请尝试查询:销售额、库存、用户数、财务数据等。";

        var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
        {
            AuthorName = this.DisplayName,
            MessageId = Guid.NewGuid().ToString("N")
        };

        await NotifyThreadOfNewMessagesAsync(
            thread, 
            messages.Concat(new[] { responseMessage }), 
            cancellationToken);

        var responseId = Guid.NewGuid().ToString("N");
        var messageId = responseMessage.MessageId;

        foreach (var character in responseText)
        {
            yield return new AgentRunResponseUpdate
            {
                AgentId = this.Id,
                ResponseId = responseId,
                MessageId = messageId,
                AuthorName = this.DisplayName,
                Role = ChatRole.Assistant,
                Contents = [new TextContent(character.ToString())]
            };

            await Task.Delay(10, cancellationToken);
        }
    }
}

3. 关键设计点解析

  • 预定义查询命令
    • 使用委托存储查询逻辑
    • 关键词映射到具体查询方法
    • 完全可控,无 SQL 注入风险
private readonly Dictionary<string, Func<string>> _queryCommands;
  • 数据源抽象
    • 封装数据访问逻辑
    • 易于替换为真实数据库连接
    • 支持缓存、连接池等优化
public class EnterpriseDataSource
{
    // 模拟数据(生产中连接真实数据库)
}
  • 结果格式化
    • 统一的数据展示格式
    • 包含 Emoji 和结构化信息
    • 提升用户体验
FormatSalesResult()
FormatInventoryResult()
FormatUserResult()
FormatFinanceResult()
  • 安全性保障

    • 无 AI 生成 SQL(避免注入)
    • 预定义白名单查询
    • 无法执行危险操作(UPDATE/DELETE)
    • 数据访问权限可控

4. 测试 DataQueryAgent

var dataQueryAgent = new DataQueryAgent();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 1: 查询今日销售额");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response1 = await dataQueryAgent.RunAsync("今天销售额是多少?");
Console.WriteLine(response1.Messages.First().Text);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 2: 查询库存情况");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response2 = await dataQueryAgent.RunAsync("库存情况如何?");
Console.WriteLine(response2.Messages.First().Text);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 3: 查询活跃用户");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response3 = await dataQueryAgent.RunAsync("当前有多少活跃用户?");
Console.WriteLine(response3.Messages.First().Text);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 4: 查询本月利润");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response4 = await dataQueryAgent.RunAsync("本月利润是多少?");
Console.WriteLine(response4.Messages.First().Text);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 5: 未识别的查询");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response5 = await dataQueryAgent.RunAsync("今天天气怎么样?");
Console.WriteLine(response5.Messages.First().Text);
Console.WriteLine("\n所有测试完成");

5. DataQueryAgent vs AI 生成 SQL

让我们对比两种方案的优劣:

特性 AI 生成 SQL DataQueryAgent 说明
安全性 有 SQL 注入风险 完全安全 预定义查询无注入风险
准确性 可能生成错误 SQL 100% 准确 预测试的查询逻辑
响应速度 2-5 秒 < 100 毫秒 无需 AI 推理
成本 每次查询消耗 Token 零成本 仅计算资源
灵活性 可处理任意查询 仅限预定义查询 AI 更灵活
可控性 难以审计 完全可控 明确的权限管理
复杂查询 支持复杂 JOIN 需预先实现 AI 能生成复杂 SQL
数据权限 可能越权查询 严格权限控制 角色级别控制

最佳实践:

  • 高频简单查询: 使用 DataQueryAgent(成本、安全、速度)
  • 低频复杂查询: 使用 AI 生成 SQL(灵活性)
  • 混合模式: 简单查询用 Agent,复杂查询转 AI(Part 6 会讲解)

6. 生产环境扩展建议

  • 连接真实数据库
public class EnterpriseDataSource
{
    private readonly SqlConnection _connection;
    
    public decimal GetTodaySales()
    {
        using var cmd = new SqlCommand(
            "SELECT SUM(Amount) FROM Sales WHERE Date = @Date", 
            _connection);
        cmd.Parameters.AddWithValue("@Date", DateTime.Today);
        return (decimal)cmd.ExecuteScalar();
    }
}
  • 添加缓存机制
private readonly MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());

public decimal GetTodaySales()
{
    return _cache.GetOrCreate("TodaySales", entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
        return _dataSource.GetTodaySales();
    });
}
  • 支持参数化查询
// 用户: "查询手机库存"
// 提取产品名 "手机"
var product = ExtractProduct(userInput);
var count = _dataSource.GetInventory(product);
  • 权限控制
public class SecureDataQueryAgent : DataQueryAgent
{
    private readonly IAuthorizationService _authService;
    
    public override async Task<AgentRunResponse> RunAsync(...)
    {
        // 检查用户权限
        if (!await _authService.CanAccessData(userId, queryType))
        {
            return new AgentRunResponse { 
                Messages = new[] { 
                    new ChatMessage(ChatRole.Assistant, "无权限查询此数据") 
                }
            };
        }
        
        return await base.RunAsync(messages, thread, options, cancellationToken);
    }
}
  • 审计日志
private async Task LogQuery(string userId, string query, string result)
{
    await _auditLog.LogAsync(new AuditEntry
    {
        UserId = userId,
        Action = "DataQuery",
        Query = query,
        Result = result,
        Timestamp = DateTime.UtcNow
    });
}

五、实战4 - 混合模式 Agent(规则 + AI)

1. 场景描述

前面我们实现了三种专用 Agent:

  • FaqAgent:快速响应常见问题(零成本)
  • DataQueryAgent:安全的数据查询(高安全)
  • ApprovalAgent:规则驱动的审批(确定性)

但企业实际场景更复杂:

  • 60% 的问题可以用规则解决
  • 30% 的问题需要数据查询
  • 10% 的问题需要 AI 推理

传统做法:所有问题都发给 AI → 成本高、速度慢

智能做法:根据问题类型智能分流 → 成本低、速度快、体验好

构建混合模式 Agent,实现智能分流和降级策略!

2. 架构设计

混合模式 Agent 的分流逻辑:

flowchart TD
    A[用户输入] --> B{意图识别}
    
    B -->|FAQ 类问题| C[FaqAgent]
    B -->|数据查询| D[DataQueryAgent]
    B -->|复杂推理| E[AI Agent]
    
    C -->|匹配成功| F[返回结果]
    C -->|匹配失败| G{降级策略}
    
    D -->|查询成功| F
    D -->|查询失败| G
    
    G -->|转发到 AI| E
    E --> F
    
    style C fill:#fff3e0
    style D fill:#e3f2fd
    style E fill:#f3e5f5
    style F fill:#e8f5e9

关键设计点:

  • 优先级:FAQ → 数据查询 → AI(成本递增)
  • 降级策略:规则失败时自动转 AI
  • 统计分析:记录各 Agent 使用率
  • 可配置:灵活调整分流策略

3. 代码实现

/// <summary>
/// 混合模式 Agent
/// 功能:智能路由到不同的 Agent,实现成本优化和性能提升
/// </summary>
public class HybridAgent : AIAgent
{
    private readonly FaqAgent _faqAgent = new();
    private readonly DataQueryAgent _dataQueryAgent = new();
    private readonly IChatClient _aiChatClient;

    // 统计信息
    public int FaqHits { get; private set; } = 0;
    public int DataQueryHits { get; private set; } = 0;
    public int AIHits { get; private set; } = 0;

    public HybridAgent(IChatClient aiChatClient)
    {
        _aiChatClient = aiChatClient;
    }

    public override string? Name => "HybridAgent";

    public override AgentThread GetNewThread() => new CustomAgentThread();

    public override AgentThread DeserializeThread(
        JsonElement serializedThread, 
        JsonSerializerOptions? jsonSerializerOptions = null)
        => new CustomAgentThread(serializedThread, jsonSerializerOptions);

    // 意图识别:判断问题类型
    private string ClassifyIntent(string userInput)
    {
        // 检查是否是 FAQ 类问题
        var faqKeywords = new[] { "营业时间", "工作时间", "退货", "退款", "配送", "快递", "物流", "支付", "付款", "联系", "电话", "客服" };
        if (faqKeywords.Any(k => userInput.Contains(k, StringComparison.OrdinalIgnoreCase)))
            return "faq";

        // 检查是否是数据查询问题
        var dataKeywords = new[] { "销售", "库存", "用户", "活跃", "收入", "支出", "利润", "财务", "数据" };
        if (dataKeywords.Any(k => userInput.Contains(k, StringComparison.OrdinalIgnoreCase)))
            return "data";

        // 默认:复杂问题,需要 AI
        return "ai";
    }

    // 检查 Agent 响应是否成功
    private bool IsSuccessfulResponse(string response)
    {
        // 如果响应包含错误提示,说明匹配失败
        return !response.Contains("抱歉") && 
               !response.Contains("未识别") && 
               !response.Contains("没有找到");
    }

    public override async Task<AgentRunResponse> RunAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        CancellationToken cancellationToken = default)
    {
        thread ??= GetNewThread();
        
        var lastUserMessage = messages.LastOrDefault();
        var userText = lastUserMessage?.Text ?? "";

        string responseText;
        string source;

        // 步骤 1: 意图识别
        var intent = ClassifyIntent(userText);

        // 步骤 2: 根据意图路由到相应 Agent
        if (intent == "faq")
        {
            // 尝试 FaqAgent
            var faqResponse = await _faqAgent.RunAsync(userText);
            var faqText = faqResponse.Messages.First().Text;

            if (IsSuccessfulResponse(faqText))
            {
                responseText = faqText;
                source = "FaqAgent";
                FaqHits++;
            }
            else
            {
                // 降级到 AI
                responseText = await CallAI(userText);
                source = "AI (降级)";
                AIHits++;
            }
        }
        else if (intent == "data")
        {
            // 尝试 DataQueryAgent
            var dataResponse = await _dataQueryAgent.RunAsync(userText);
            var dataText = dataResponse.Messages.First().Text;

            if (IsSuccessfulResponse(dataText))
            {
                responseText = dataText;
                source = "DataQueryAgent";
                DataQueryHits++;
            }
            else
            {
                // 降级到 AI
                responseText = await CallAI(userText);
                source = "AI (降级)";
                AIHits++;
            }
        }
        else
        {
            // 直接使用 AI
            responseText = await CallAI(userText);
            source = "AI";
            AIHits++;
        }

        // 添加来源标识(调试用)
        responseText += $"\n\n---\n*处理方式: {source}*";

        var responseMessage = new ChatMessage(ChatRole.Assistant, responseText)
        {
            AuthorName = this.DisplayName,
            MessageId = Guid.NewGuid().ToString("N")
        };

        var responseMessages = new[] { responseMessage };

        await NotifyThreadOfNewMessagesAsync(
            thread, 
            messages.Concat(responseMessages), 
            cancellationToken);

        return new AgentRunResponse
        {
            AgentId = this.Id,
            ResponseId = Guid.NewGuid().ToString("N"),
            Messages = responseMessages
        };
    }

    // 调用 AI Agent
    private async Task<string> CallAI(string userInput)
    {
        try
        {
            var response = await _aiChatClient.GetResponseAsync(userInput);
            return response.Text ?? "AI 未返回有效响应";
        }
        catch (Exception ex)
        {
            return $"AI 调用失败: {ex.Message}";
        }
    }

    public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
        IEnumerable<ChatMessage> messages, 
        AgentThread? thread = null, 
        AgentRunOptions? options = null, 
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        // 简化版:直接调用 RunAsync 再流式输出
        var response = await RunAsync(messages, thread, options, cancellationToken);
        var responseText = response.Messages.First().Text;

        var responseId = Guid.NewGuid().ToString("N");
        var messageId = Guid.NewGuid().ToString("N");

        foreach (var character in responseText)
        {
            yield return new AgentRunResponseUpdate
            {
                AgentId = this.Id,
                ResponseId = responseId,
                MessageId = messageId,
                AuthorName = this.DisplayName,
                Role = ChatRole.Assistant,
                Contents = [new TextContent(character.ToString())]
            };

            await Task.Delay(10, cancellationToken);
        }
    }

    // 获取统计信息
    public string GetStatistics()
    {
        var total = FaqHits + DataQueryHits + AIHits;
        if (total == 0) return "暂无统计数据";

        return $"**使用统计**\n\n" +
               $"- FaqAgent: {FaqHits} 次 ({FaqHits * 100.0 / total:F1}%)\n" +
               $"- DataQueryAgent: {DataQueryHits} 次 ({DataQueryHits * 100.0 / total:F1}%)\n" +
               $"- AI Agent: {AIHits} 次 ({AIHits * 100.0 / total:F1}%)\n" +
               $"- 总计: {total} 次\n\n" +
               $"**成本优化**: {(FaqHits + DataQueryHits) * 100.0 / total:F1}% 的请求避免了 AI 调用";
    }
}

4. 关键设计点解析

  • 意图识别(ClassifyIntent)
    • 简单但有效的分类逻辑
    • 可扩展为更复杂的 NLP 模型
private string ClassifyIntent(string userInput)
{
    // 关键词匹配识别问题类型
    if (faqKeywords.Any(...)) return "faq";
    if (dataKeywords.Any(...)) return "data";
    return "ai";  // 默认:复杂问题
}
  • 智能路由(RunAsync)
flowchart LR
    A[意图识别] --> B{问题类型}
    B -->|FAQ| C[FaqAgent]
    B -->|Data| D[DataQueryAgent]
    B -->|AI| E[AI Agent]
    C -->|失败| E
    D -->|失败| E
    style E fill:#f3e5f5
  • 降级策略(IsSuccessfulResponse)
    • 规则 Agent 失败时自动降级到 AI
    • 保证用户体验(总能得到答案)
private bool IsSuccessfulResponse(string response)
{
    // 检查响应是否包含错误提示
    return !response.Contains("抱歉") && !response.Contains("未识别");
}
  • 统计分析(GetStatistics)

    • 记录各 Agent 使用次数
    • 计算成本优化比例
    • 支持性能监控和优化决策

5. 测试 HybridAgent

// 创建 HybridAgent(需要 AI ChatClient)
var chatClient = AIClientHelper.GetDefaultChatClient();
var hybridAgent = new HybridAgent(chatClient);
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 1: FAQ 类问题(路由到 FaqAgent)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response1 = await hybridAgent.RunAsync("请问你们的营业时间?");
Console.WriteLine(response1.Messages.First().Text);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 2: 数据查询问题(路由到 DataQueryAgent)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response2 = await hybridAgent.RunAsync("今天销售额是多少?");
Console.WriteLine(response2.Messages.First().Text);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 3: 复杂问题(路由到 AI Agent)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
var response3 = await hybridAgent.RunAsync("如何提升客户满意度?");
Console.WriteLine(response3.Messages.First().Text);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("测试 4: 降级场景(规则失败 → AI)");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// 这个问题会被识别为数据查询,但 DataQueryAgent 无法处理,会降级到 AI
var response4 = await hybridAgent.RunAsync("去年的销售数据趋势如何?");
Console.WriteLine(response4.Messages.First().Text);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("查看使用统计");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
Console.WriteLine(hybridAgent.GetStatistics());
Console.WriteLine("\n所有测试完成");

6. 混合模式的价值分析

成本对比

假设企业客服系统每天处理 10万次 对话:

模式 成本计算 每日成本 每年成本 节省
纯 AI 模式 10万 × ¥0.005 ¥500 ¥18万 -
混合模式 1万(AI) × ¥0.005 ¥50 ¥1.8万 90%

假设分流比例:

  • 60% FAQ (6万次) → ¥0

  • 30% 数据查询 (3万次) → ¥0

  • 10% AI (1万次) → ¥50/天

  • 年度节省:¥18万 - ¥1.8万 = ¥16.2万

性能对比

指标 纯 AI 模式 混合模式 提升
平均响应时间 2 秒 0.5 秒 4倍
P95 响应时间 5 秒 1 秒 5倍
并发能力 100 QPS 1000+ QPS 10倍+
可用性 99.9% 99.99%+ 更高

用户体验提升

  • 响应更快:90% 的问题 < 100ms 响应
  • 答案更准:规则 Agent 100% 准确
  • 高可用:规则 Agent 无外部依赖
  • 降级保障:AI 作为兜底,保证总能得到答案

7. 生产环境优化策略

  • 更智能的意图识别
// 使用 ML.NET 或其他 NLP 模型
public class MLIntentClassifier
{
    private readonly PredictionEngine<Input, Output> _model;
    
    public string Classify(string text)
    {
        var prediction = _model.Predict(new Input { Text = text });
        return prediction.Intent;  // "faq" / "data" / "ai"
    }
}
  • 动态分流策略
public class RoutingStrategy
{
    // 根据系统负载动态调整
    public string Route(string intent, double systemLoad)
    {
        if (systemLoad > 0.8 && intent == "ai")
        {
            // 高负载时,尽量用规则 Agent
            return "try_rule_first";
        }
        return "normal";
    }
}
  • A/B 测试支持
public class HybridAgentWithAB : HybridAgent
{
    private readonly IAbTestingService _abTest;
    
    public override async Task<AgentRunResponse> RunAsync(...)
    {
        var variant = _abTest.GetVariant(userId);
        
        if (variant == "pure_ai")
        {
            // 对照组:纯 AI
            return await CallAI(userText);
        }
        else
        {
            // 实验组:混合模式
            return await base.RunAsync(messages, thread, options, cancellationToken);
        }
    }
}
  • 智能学习与优化
// 记录用户反馈
public void RecordFeedback(string query, string source, bool isHelpful)
{
    // 存储到数据库
    _feedbackRepo.Add(new Feedback
    {
        Query = query,
        Source = source,
        IsHelpful = isHelpful,
        Timestamp = DateTime.UtcNow
    });
    
    // 分析哪些问题应该加入规则库
    if (source == "AI" && isHelpful && CountSimilarQueries(query) > 100)
    {
        SuggestAddToRuleBase(query);
    }
}
  • 监控与告警
// 监控各 Agent 使用情况
public class AgentMonitor
{
    public void Track(string agentName, TimeSpan duration, bool success)
    {
        _metrics.RecordHistogram($"agent.{agentName}.duration", duration.TotalMilliseconds);
        _metrics.IncrementCounter($"agent.{agentName}.{(success ? "success" : "failure")}");
        
        // AI 使用率过高告警
        if (CalculateAIUsageRate() > 0.5)
        {
            _alertService.SendAlert("AI 使用率超过 50%,建议优化规则库");
        }
    }
}

六、序列化与互操作性

1. 序列化最佳实践

自定义 Agent 的状态持久化是生产环境的关键需求。我们已经在工作流实战中实现了 ApprovalAgent 的序列化,现在总结最佳实践:

序列化检查清单

  • 必须序列化的内容:
    • 消息历史(base.Serialize() 已包含)
    • 自定义状态(需显式序列化)
    • Thread ID 和元数据
    • 时间戳信息
  • 可选序列化的内容:
    • 统计信息(如 HybridAgent 的使用统计)
    • 临时缓存(可重建的数据)
    • UI 状态(特定于客户端)
// 回顾 ApprovalAgentThread 的序列化实现
public class ApprovalAgentThread : InMemoryAgentThread
{
    public ApprovalState State { get; set; } = new();
    
    public override string Serialize()
    {
        // 1. 序列化基础消息历史
        var baseJson = base.Serialize();
        var baseDoc = JsonDocument.Parse(baseJson);
        
        // 2. 添加自定义状态
        var root = JsonSerializer.SerializeToElement(new
        {
            messages = baseDoc.RootElement.GetProperty("messages"),
            customState = State // 自定义状态
        });
        
        return JsonSerializer.Serialize(root, new JsonSerializerOptions 
        { 
            WriteIndented = true,
            Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
        });
    }
}

2. 跨平台互操作性

自定义 Agent 可以通过以下方式集成到 MAF 生态系统:

  • 导出为 AI Function:任何 AIAgent 都可以通过 AsAIFunction() 方法导出为 Function Calling 工具
// 演示: 将 FaqAgent 导出为工具函数
var faqAgent = new FaqAgent();
var faqFunction = faqAgent.AsAIFunction();
Console.WriteLine(" 导出的 Function 信息:");
faqFunction.Display();
Console.WriteLine("\nAgent 导出为 Function 完成");
Console.WriteLine("现在可以在其他 Agent 或 ChatOptions 中使用此工具");
  • 嵌套使用 Agent:自定义 Agent 可以作为其他 Agent 的工具
// 演示: 在 ChatCompletionAgent 中使用自定义 Agent
var dataAgent = new DataQueryAgent();
var masterAgent = chatClient.CreateAIAgent(
    name: "MasterAgent",
    instructions : "你是一个主 Agent,可以调用多个专业工具回答用户问题",
    tools: new[]
    {
        faqAgent.AsAIFunction(),
        dataAgent.AsAIFunction()
    }
);

// 测试嵌套调用
var testThread = masterAgent.GetNewThread();
var nestedResponse = await masterAgent.RunAsync("今日销售额如何?", testThread);
Console.WriteLine("嵌套调用结果:");
Console.WriteLine(nestedResponse.Text);
Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("嵌套 Agent 测试完成");
  • MCP 集成:自定义 Agent 可以通过 MCP 协议暴露给外部系统
flowchart LR
    A[MCP Client] -->|MCP Protocol| B[MCP Server]
    B --> C[Custom Agent]
    C --> D[Agent Logic]
    D --> E[Response]
    E --> B --> A
// 在 MCP Server 中注册 Agent 工具
server.AddTool(
    name: "faq_agent",
    description: "查询常见问题知识库",
    async (string query) =>
    {
        var thread = new CustomAgentThread();
        thread.AddUserMessage(query);
        var response = await faqAgent.InvokeAsync(thread);
        return response.Text;
    }
);

//在 MCP Client 中调用
var mcpClient = new McpClient(transport);
var result = await mcpClient.CallToolAsync("faq_agent", new { query = "如何修改密码?" });

优势:自定义 Agent 可以跨语言、跨平台使用

3. 互操作性对比

集成方式 适用场景 优点 缺点
AsAIFunction() 在同一应用内嵌套 零开销、类型安全 仅限 .NET
MCP 集成 跨语言/跨进程 语言无关、松耦合 序列化开销
HTTP API 微服务架构 标准化、可扩展 网络延迟
消息队列 异步处理 高吞吐、解耦 复杂度高

七、最佳实践与总结

1. 何时使用自定义 Agent?

决策树

flowchart TD
    Start[需要创建 Agent] --> Q1{需求是否简单?}
    Q1 -->|是<br>单轮对话| ChatCompletionAgent
    Q1 -->|否| Q2{是否需要状态管理?}
    
    Q2 -->|否<br>无状态逻辑| Q3{是否需要工具调用?}
    Q3 -->|否| SimpleAgent[FaqAgent 模式]
    Q3 -->|是| ToolAgent[DataQueryAgent 模式]
    
    Q2 -->|是<br>多轮对话| Q4{状态复杂度}
    Q4 -->|简单<br>基础字段| InMemory[扩展 InMemoryAgentThread]
    Q4 -->|复杂<br>嵌套状态| Custom[ApprovalAgent 模式]
    
    Q2 --> Q5{需要智能路由?}
    Q5 -->|是| Hybrid[HybridAgent 模式]
    Q5 -->|否| Direct[直接实现 AIAgent]
    
    style ChatCompletionAgent fill:#90EE90
    style SimpleAgent fill:#87CEEB
    style ToolAgent fill:#87CEEB
    style InMemory fill:#FFD700
    style Custom fill:#FFA07A
    style Hybrid fill:#FF6B6B

2. 实现检查清单

必须实现的 5 个方法

方法 作用 实现难度 注意事项
InvokeStreamingAsync() 流式响应 ⭐⭐⭐ 必须使用 yield return 语句
InvokeAsync() 同步响应 ⭐⭐ 通常调用 Streaming 版本后聚合结果
CreateThread() 创建对话线程 返回 InMemoryAgentThread 或自定义子类
Serialize() 序列化状态 ⭐⭐⭐⭐ 需同时序列化消息历史和自定义状态
DeserializeThread() 反序列化 ⭐⭐⭐⭐ 必须还原完整状态,包括自定义字段

3. 核心设计原则

  • 单一职责原则 (SRP)

    • FaqAgent 只处理 FAQ

    • ApprovalAgent 只处理审批流程

    • DataQueryAgent 只处理数据查询

    • 避免一个 Agent 做太多事情

  • 状态管理原则

    • 能用 InMemoryAgentThread 就不要自定义

    • 自定义 Thread 时必须实现完整序列化

    • 状态类使用 record 类型(不可变性)

  • 错误处理原则

try 
{
    // Agent 逻辑
    yield return new ChatMessageContent(AgentRole.Assistant, result);
}
catch (Exception ex)
{
    // 必须返回错误消息,而不是抛出异常
    yield return new ChatMessageContent(
        AgentRole.Assistant, 
        $"处理失败: {ex.Message}"
    );
}
  • 性能优化原则

    • 使用 async/await 避免阻塞
    • 缓存可重用的数据(如 FAQ 字典)
    • 使用 IAsyncEnumerable 流式传输大响应
    • 避免在 Agent 内部做大量计算

4. 安全性考虑

  • 输入验证
public override async IAsyncEnumerable<ChatMessageContent> InvokeStreamingAsync(
    AgentThread thread)
{
    var lastMessage = thread.GetLastUserMessage();
    
    // 1. 长度检查
    if (string.IsNullOrWhiteSpace(lastMessage) || lastMessage.Length > 10000)
    {
        yield return new ChatMessageContent(
            AgentRole.Assistant, 
            "输入无效或过长"
        );
        yield break;
    }
    
    // 2. SQL 注入防护(DataQueryAgent)
    if (lastMessage.Contains(";") || lastMessage.Contains("--"))
    {
        yield return new ChatMessageContent(
            AgentRole.Assistant, 
            "检测到非法字符"
        );
        yield break;
    }
    
    // 3. 敏感信息过滤
    var sanitizedInput = RemoveSensitiveInfo(lastMessage);
    
    // 继续处理...
}
  • 权限控制
// 在 AgentThread 中存储用户权限
public class SecureAgentThread : InMemoryAgentThread
{
    public string UserId { get; set; } = "";
    public List<string> Roles { get; set; } = new();
    
    public bool HasPermission(string requiredRole) =>
        Roles.Contains(requiredRole);
}

// 在 Agent 中检查权限
if (secureThread.HasPermission("Admin"))
{
    // 执行管理员操作
}
else
{
    yield return new ChatMessageContent(
        AgentRole.Assistant, 
        "权限不足"
    );
    yield break;
}
  • 审计日志
// 记录所有 Agent 操作
protected async Task LogOperationAsync(string action, string details)
{
    await _auditLogger.LogAsync(new AuditLog
    {
        AgentName = Name,
        Action = action,
        Details = details,
        Timestamp = DateTime.UtcNow,
        UserId = GetCurrentUserId()
    });
}

5. 性能监控

关键指标

public class AgentMetrics
{
    public int TotalRequests { get; set; }
    public TimeSpan AverageLatency { get; set; }
    public int ErrorCount { get; set; }
    public Dictionary<string, int> RouteStatistics { get; set; } = new();
}

// 在 Agent 中集成监控
private readonly AgentMetrics _metrics = new();

public override async IAsyncEnumerable<ChatMessageContent> InvokeStreamingAsync(
    AgentThread thread)
{
    var sw = Stopwatch.StartNew();
    _metrics.TotalRequests++;
    
    try
    {
        // Agent 逻辑...
        yield return result;
    }
    catch (Exception ex)
    {
        _metrics.ErrorCount++;
        throw;
    }
    finally
    {
        sw.Stop();
        _metrics.AverageLatency = CalculateAverageLatency(sw.Elapsed);
    }
}

监控仪表盘数据

指标 目标值 告警阈值 说明
平均延迟 < 2s > 5s 用户体验关键指标
错误率 < 1% > 5% 稳定性指标
并发连接 < 1000 > 5000 资源使用指标
Token 使用 < 1M/天 > 10M/天 成本控制指标