C# 构建 AI Agent 系统 — 我的实践笔记

📝 写在前面:这是一篇学习日记+实战笔记。我最近在研究如何用 C# 和 .NET 8 从零构建一个 AI Agent 系统。整个过程踩了不少坑,也积累了很多实战经验。于是我把整个学习过程和代码实现记录下来,分享给同样对 C# AI 开发感兴趣的同学。

💡 如果你也在用 .NET 做 AI 项目,这篇文章应该能帮你少走一些弯路。所有代码都可以直接跑起来,基于 .NET 8 和 C# 12,不依赖任何第三方 AI 框架。

📌 快速导航 · TL;DR

这篇笔记记录了从零到一用 C# 构建 AI Agent 系统的完整过程。围绕三大核心组件展开:

组件 核心内容 对应章节
状态机 状态定义、转换逻辑、守卫条件、持久化方案 第三章
规划器 LLM 驱动的目标分解、步骤排序、重试机制 第四章
工具调用 工具注册发现、OpenAI Function Calling、安全沙箱 第五章

最后,我把三个组件串起来,跑在了一个 ASP.NET Core Web API 上,从配置到日志到监控,一条龙搞定。

⚡ 所有代码都是实战级别,直接 copy 就能用。基于 .NET 8 + C# 12,纯原生,不靠第三方 AI 框架。


第一章 · 为什么要用 C# 做 AI Agent?

🤖 AI Agent 到底是什么?

简单来说,AI Agent 就是那种不仅能"回答问题",还能"自己动手干活"的 AI 系统。

以前的 AI 模型(比如 GPT-4、Claude),你问它一句它答你一句,是个被动工具。而 AI Agent 不一样------它有自己的"大脑"(状态机)、会"做计划"(规划器)、能"用工具"(工具调用引擎),可以自己拆解任务、一步步执行,最终帮你搞定一件复杂的事情。

举个例子:你告诉 Agent "帮我调研一下最近的 AI Agent 框架",它会自动规划步骤 → 搜索网络 → 整理结果 → 生成报告。整个过程不需要你插手。

从 LangChain 到 AutoGen,从 CrewAI 到 Semantic Kernel,AI Agent 框架确实百花齐放。但仔细一看------几乎全是 Python 生态

作为一个 .NET 开发者,我当时就想了:凭啥我们 C# 程序员不能用自己熟悉的语言搞 AI Agent? 这就是我做这个项目的初衷。

💪 为什么选择 C#?

做这个决定之前,我也犹豫过。毕竟 Python 在 AI 领域确实生态好。但深入了解后,我发现 C# 其实有很多被低估的优势:

为什么选择 C# 构建 AI Agent?

C# 和 .NET 生态在构建 AI Agent 系统时具有独特的优势:

1. 强大的类型系统

说实话,Python 的动态类型在写原型时确实快,但真正上生产环境,类型安全就是救命稻草。C# 的强类型让 Agent 的状态管理、工具注册、规划流程在编译期就能抓到一堆 bug------这在大项目中太重要了。

2. 成熟的异步编程模型

这一点必须吹一下。.NET 的 async/await 从 C# 5 开始就有了,经过十几年的打磨已经非常成熟。配合 ValueTaskChannelTask.WhenAll 这些利器,管理并发的 LLM 请求和工具调用简直不要太舒服。

3. 内建的依赖注入

ASP.NET Core 自带的 DI 容器开箱即用,Agent 的各个组件(状态机、规划器、工具引擎)天然就是独立的接口,注册进去、拿出来用,代码干净得不行。

4. 丰富的企业级基础设施

EF Core、Serilog、Polly 弹性库、Health Checks......这些在 Python 生态里得拼拼凑凑的东西,.NET 里都是开箱即用的。

5. 跨平台与云原生

.NET 8 跑 Linux 容器、上 Kubernetes、接 Azure OpenAI 或其他兼容 API,一气呵成。云原生部署毫无压力。

🛠️ 我的技术栈选择

最终我选了这些东西来搭这个系统:

组件 技术选型 说明
框架 .NET 8 / ASP.NET Core 宿主框架与 Web API
语言 C# 12 主语言
OpenAI SDK OpenAI NuGet 包 v2.0+(官方 .NET SDK) LLM 调用
Prometheus prometheus-net.AspNetCore 指标暴露
OpenTelemetry OpenTelemetry.Extensions.Hosting + 相关 Instrumentation 包 分布式追踪
语言 C# 12 主语言,使用最新语法特性
HTTP 客户端 HttpClient + IHttpClientFactory LLM API 调用
序列化 System.Text.Json JSON 序列化/反序列化
依赖注入 Microsoft.Extensions.DependencyInjection 组件注册
日志 Microsoft.Extensions.Logging 结构化日志
弹性策略 Polly(NuGet 包 Polly v8.x) 重试、熔断、超时
配置 Microsoft.Extensions.Configuration appsettings.json
状态存储 SQLite / 内存 状态持久化(可选)

👥 这篇笔记适合谁?

  • 有 C# 基础,想入坑 AI Agent 的 .NET 同学
  • 用 Python 搞过 LangChain,想看看 .NET 怎么做的
  • 需要在公司项目里集成 AI Agent 的架构师/技术负责人

📖 这篇笔记的结构

整个系统围绕三个核心组件展开,我按照学习和开发的顺序来写:

  1. 状态机(State Machine) --- Agent 的"大脑状态",管状态切换的
  2. 规划器(Planner) --- Agent 的"计划能力",负责拆解目标
  3. 工具调用引擎(Tool Calling Engine) --- Agent 的"动手能力",管工具的注册和执行

最后,我把三个组件串起来,跑在一个 ASP.NET Core Web API 上,从配置到日志到监控,一条龙。

⚡ 所有代码都是 .NET 8 + C# 12 纯原生,不靠第三方 AI 框架,直接 copy 就能用。

第二章 · 系统架构 --- 先把骨架搭好

2.1 整体架构长啥样?

刚开始设计这个系统的时候,我先画了个流程图。一个完整的 AI Agent 系统需要三个核心组件配合工作,缺一不可:

复制代码
┌─────────────────────────────────────────────────────────┐
│                    用户请求 (User Prompt)                 │
└────────────────────────┬────────────────────────────────┘
                         ▼
              ┌──────────────────────┐
              │     规划器 (Planner)  │
              │  目标分解 → 步骤排序   │
              └────────┬─────────────┘
                       ▼
              ┌──────────────────────┐
              │   状态机 (StateMachine)│
              │  状态管理 → 转换控制   │
              └────────┬─────────────┘
                       ▼
              ┌──────────────────────┐
              │  工具调用引擎 (Tools) │
              │  工具注册 → 调度执行   │
              └────────┬─────────────┘
                       ▼
              ┌──────────────────────┐
              │   LLM 推理引擎        │
              │  API 调用 → 结果解析   │
              └────────┬─────────────┘
                       ▼
              ┌──────────────────────┐
              │   结果聚合与输出       │
              │  格式化 → 返回用户     │
              └──────────────────────┘

🔄 它是怎么工作的?

简单说就是五步走:

  1. 用户说话 → 输入一个目标描述
  2. 规划器出马 → 把目标拆成一堆步骤
  3. 状态机管节奏 → 一步步执行,维护当前状态
  4. 工具调用 → 需要查资料、跑代码、读写文件?找工具去
  5. 结果返回 → 所有步骤跑完,结果给用户

整个过程就像一个流水线工厂,每个组件各司其职。

2.2 先把接口定义好

在 C# 里写东西,我的习惯是先定义接口,再想实现。这不仅是好代码规范,更是设计思路的过程------接口定义清楚了,实现就水到渠成了。下面是整个系统的核心接口,后面几章会逐个展开。

2.2.1 Agent 生命周期状态

csharp 复制代码
namespace CSharpAgent.Core;

/// <summary>
/// Agent 运行状态枚举
/// </summary>
public enum AgentState
{
    Idle,           // 空闲
    Planning,       // 规划中
    Executing,      // 执行中
    WaitingForTool, // 等待工具完成
    Completed,      // 已完成
    Failed          // 失败
}

2.2.2 状态机接口(详见第三章完整实现)

csharp 复制代码
/// <summary>
/// 异步事件处理器委托(.NET 标准库不内置,需自定义)
/// </summary>
public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs args);

/// <summary>
/// 状态机接口 --- 管理 Agent 状态和转换
/// </summary>
public interface IStateMachine
{
    IState CurrentState { get; }
    IState? PreviousState { get; }
    IReadOnlyDictionary<string, IState> States { get; }
    void RegisterState(IState state);
    void AddTransition(string fromStateName, string toStateName, Func<StateContext, bool>? guard = null);
    Task<TransitionResult> TransitionAsync(string targetStateName, StateContext context, CancellationToken ct = default);
    Task<StateResult> ExecuteCurrentStateAsync(StateContext context, CancellationToken ct = default);
    Task ResetAsync(string initialStateName, StateContext context, CancellationToken ct = default);
}

:第三章将展开 IStateStateContextStateResultTransitionResult 的完整定义。

2.2.3 规划器接口(详见第四章完整实现)

csharp 复制代码
/// <summary>
/// 规划步骤
/// </summary>
public class PlanStep
{
    public Guid Id { get; init; } = Guid.NewGuid();
    public string Description { get; init; } = string.Empty;
    public string? ToolName { get; init; }
    public IDictionary<string, object> Parameters { get; init; } = new Dictionary<string, object>();
    public IList<Guid> DependsOn { get; init; } = new List<Guid>();
    public StepStatus Status { get; set; } = StepStatus.Pending;
    public string? Result { get; set; }
    public string? ErrorMessage { get; set; }
}

public enum StepStatus
{
    Pending,
    Running,
    Completed,
    Failed,
    Skipped
}

/// <summary>
/// 规划器接口 --- 将目标分解为可执行步骤
/// </summary>
public interface IPlanner
{
    Task<IReadOnlyList<PlanStep>> CreatePlanAsync(string goal, CancellationToken ct = default);
    Task<PlanStep?> GetNextStepAsync(IReadOnlyList<PlanStep> plan);
}

:第四章将展开 PlanPlanExecutor 等完整定义。

2.2.4 工具接口(详见第五章完整实现)

csharp 复制代码
/// <summary>
/// 工具执行结果
/// </summary>
public class ToolResult
{
    public bool Success { get; set; }
    public string? Data { get; set; }
    public string? Error { get; set; }
    public long DurationMs { get; set; }

    public static ToolResult Ok(string? data = null, long durationMs = 0)
        => new() { Success = true, Data = data ?? string.Empty, DurationMs = durationMs };

    public static ToolResult Fail(string error, long durationMs = 0)
        => new() { Success = false, Error = error, DurationMs = durationMs };
}

/// <summary>
/// 工具接口 --- 每个可调用工具必须实现此接口
/// </summary>
public interface ITool
{
    string Name { get; }
    string Description { get; }
    string ParameterSchema { get; }
    Task<ToolResult> ExecuteAsync(string parametersJson, ToolExecutionContext context, CancellationToken ct = default);
}

/// <summary>
/// 工具执行上下文
/// </summary>
public class ToolExecutionContext
{
    public string AgentId { get; init; } = string.Empty;
    public string? SessionId { get; init; }
    public CancellationToken CancellationToken { get; init; }
}

:第五章将展开 ToolRegistryToolExecutorToolMetadata 等完整定义。

2.3 组件之间的关系

搞清楚接后,我画了个依赖关系图:

复制代码
┌─────────────────────────────────────────────────────────────┐
│                     AgentRuntime                            │
│                                                             │
│  ┌──────────┐    ┌───────────┐    ┌──────────────────┐     │
│  │ IPlanner │───▶│ IStateMachine│──▶│ IToolRegistry    │     │
│  └──────────┘    └───────────┘    └────────┬─────────┘     │
│                                            │               │
│                                   ┌────────▼─────────┐     │
│                                   │  IToolExecutor   │     │
│                                   └────────┬─────────┘     │
│                                            │               │
│                                   ┌────────▼─────────┐     │
│                                   │  ILLMClient      │     │
│                                   └──────────────────┘     │
└─────────────────────────────────────────────────────────────┘

依赖注入注册示例(Program.cs):

csharp 复制代码
var builder = WebApplication.CreateBuilder(args);

// 注册核心服务
builder.Services.AddSingleton<ILLMClient, OpenAIClient>();
builder.Services.AddSingleton<IToolRegistry, ToolRegistry>();
builder.Services.AddSingleton<IToolExecutor, ToolExecutor>();
builder.Services.AddSingleton<IStateMachine, StateMachine>();
builder.Services.AddSingleton<IPlanner, LLMPlanner>();
builder.Services.AddHostedService<AgentWorkerService>();

// 注册具体工具
builder.Services.AddSingleton<ITool, WebSearchTool>();
builder.Services.AddSingleton<ITool, CodeExecutorTool>();
builder.Services.AddSingleton<ITool, FileOperationTool>();

var app = builder.Build();

2.4 一次完整的运行流程

跑起来是这样的:

  1. 接收目标 → 用户输入一个 Goal
  2. 生成规划 → IPlanner 找 LLM 生成 Plan
  3. 状态机启动 → Idle → Executing
  4. 循环执行 → 每个步骤,需要工具就调工具,LLM 推理,更新状态
  5. 终态 → 成功或失败

简单明了。

💡 设计心得

这个架构最核心的思想就是关注点分离

  • 规划器只管"做什么"
  • 状态机只管"当前在哪"
  • 工具引擎只管"怎么做"

三个组件通过接口解耦,独立测试、独立替换,互不干扰。这就是 C# 接口驱动设计的魅力。

第三章 · 状态机设计 --- Agent 的"状态大脑"

3.0 我的思考

状态机是整个系统里我最早开始写的组件,也是最容易理解的一个。说白了就是:Agent 当前在"干嘛"?是空闲?在规划?在执行?还是出错了?状态机就是管这些状态切换的。

3.1 理论基础(简单了解一下就行)

3.1.1 有限状态机(FSM)

状态机这个概念听起来学术,但其实很好理解。有限状态机(FSM)就是:

  • 状态(State):Agent 现在在干嘛
  • 转换(Transition):什么时候可以切换到下一个状态
  • 事件(Event):触发切换的信号
  • 动作(Action):切换时要做的事

FSM 适合状态不多、逻辑简单的场景。比如你的 Agent 只有"空闲 → 规划 → 执行 → 完成"四个状态,FSM 就够了。

3.1.2 层次状态机(HSM)

如果你的 Agent 比较复杂(比如"执行中"还分成"搜索"、"计算"、"写入"等子状态),那就需要 HSM 了。HSM 支持状态嵌套,父状态包子状态,子状态可以继承父状态的转换规则。

💡 我的建议:刚开始用 FSM 就够了,后面复杂了再升级 HSM。别一上来就搞太复杂。

3.2 C# 接口设计

3.2.1 IState 接口

csharp 复制代码
namespace AIAgentSystem.States;

/// <summary>
/// 表示 Agent 状态的接口
/// </summary>
public interface IState
{
    /// <summary>
    /// 状态名称,用于标识和调试
    /// </summary>
    string Name { get; }
    
    /// <summary>
    /// 进入状态时执行的操作
    /// </summary>
    /// <param name="context">状态上下文,包含 Agent 当前信息</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task OnEnterAsync(StateContext context, CancellationToken cancellationToken = default);
    
    /// <summary>
    /// 退出状态时执行的操作
    /// </summary>
    /// <param name="context">状态上下文</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task OnExitAsync(StateContext context, CancellationToken cancellationToken = default);
    
    /// <summary>
    /// 状态执行的主要逻辑
    /// </summary>
    /// <param name="context">状态上下文</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<StateResult> ExecuteAsync(StateContext context, CancellationToken cancellationToken = default);
    
    /// <summary>
    /// 检查是否可以转换到目标状态
    /// </summary>
    /// <param name="targetState">目标状态</param>
    /// <param name="context">状态上下文</param>
    /// <returns>如果可以转换则返回 true</returns>
    bool CanTransitionTo(IState targetState, StateContext context);
}

3.2.2 IStateMachine 接口

csharp 复制代码
namespace AIAgentSystem.States;

/// <summary>
/// 状态机接口,管理状态和转换
/// </summary>
public interface IStateMachine
{
    /// <summary>
    /// 当前活动状态
    /// </summary>
    IState CurrentState { get; }
    
    /// <summary>
    /// 之前的状态(用于回退)
    /// </summary>
    IState? PreviousState { get; }
    
    /// <summary>
    /// 所有已注册的状态
    /// </summary>
    IReadOnlyDictionary<string, IState> States { get; }
    
    /// <summary>
    /// 注册状态到状态机
    /// </summary>
    /// <param name="state">要注册的状态</param>
    void RegisterState(IState state);
    
    /// <summary>
    /// 添加状态转换规则
    /// </summary>
    /// <param name="fromStateName">源状态名称</param>
    /// <param name="toStateName">目标状态名称</param>
    /// <param name="guard">守卫条件(可选)</param>
    void AddTransition(string fromStateName, string toStateName, Func<StateContext, bool>? guard = null);
    
    /// <summary>
    /// 尝试转换到目标状态
    /// </summary>
    /// <param name="targetStateName">目标状态名称</param>
    /// <param name="context">状态上下文</param>
    /// <param name="cancellationToken">取消令牌</param>
    /// <returns>转换结果</returns>
    Task<TransitionResult> TransitionAsync(string targetStateName, StateContext context, CancellationToken cancellationToken = default);
    
    /// <summary>
    /// 执行当前状态的逻辑
    /// </summary>
    /// <param name="context">状态上下文</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<StateResult> ExecuteCurrentStateAsync(StateContext context, CancellationToken cancellationToken = default);
    
    /// <summary>
    /// 重置状态机到初始状态
    /// </summary>
    /// <param name="initialStateName">初始状态名称</param>
    /// <param name="context">状态上下文</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task ResetAsync(string initialStateName, StateContext context, CancellationToken cancellationToken = default);
}

3.2.3 核心数据结构

csharp 复制代码
namespace AIAgentSystem.States;

/// <summary>
/// 状态上下文,传递状态执行所需的所有信息
/// </summary>
public class StateContext
{
    /// <summary>
    /// Agent 的唯一标识
    /// </summary>
    public required string AgentId { get; init; }
    
    /// <summary>
    /// 用户输入或触发事件
    /// </summary>
    public string? Input { get; set; }
    
    /// <summary>
    /// 当前对话或任务的历史记录
    /// </summary>
    public IList<MessageRecord> History { get; init; } = new List<MessageRecord>();
    
    /// <summary>
    /// 状态机共享数据存储
    /// </summary>
    public IDictionary<string, object> Data { get; init; } = new Dictionary<string, object>();
    
    /// <summary>
    /// 用户信息
    /// </summary>
    public UserInfo? User { get; set; }
    
    /// <summary>
    /// 会话信息
    /// </summary>
    public SessionInfo? Session { get; set; }
}

/// <summary>
/// 对话消息记录(补充定义,第三章 3.2.3 引用)
/// </summary>
public class MessageRecord
{
    public string Role { get; init; } = string.Empty; // "user", "assistant", "system"
    public string Content { get; init; } = string.Empty;
    public DateTime Timestamp { get; init; } = DateTime.UtcNow;
}

/// <summary>
/// 用户信息(补充定义)
/// </summary>
public class UserInfo
{
    public string UserId { get; init; } = string.Empty;
    public string? UserName { get; init; }
}

/// <summary>
/// 会话信息(补充定义)
/// </summary>
public class SessionInfo
{
    public string SessionId { get; init; } = string.Empty;
    public DateTime CreatedAt { get; init; }
}

/// <summary>
/// LLM 服务接口(补充定义,供 ConversationState 使用)
/// </summary>
public interface ILLMService
{
    Task<string> GenerateResponseAsync(
        string input,
        IEnumerable<MessageRecord> history,
        CancellationToken cancellationToken = default);
}

/// <summary>
/// 状态执行结果
/// </summary>
public class StateResult
{
    /// <summary>
    /// 执行是否成功
    /// </summary>
    public bool IsSuccess { get; init; }
    
    /// <summary>
    /// 结果消息或错误信息
    /// </summary>
    public string? Message { get; init; }
    
    /// <summary>
    /// 建议的下一个状态(如果需要转换)
    /// </summary>
    public string? NextState { get; init; }
    
    /// <summary>
    /// 输出数据
    /// </summary>
    public object? Output { get; init; }
    
    /// <summary>
    /// 是否需要等待外部输入
    /// </summary>
    public bool RequiresInput { get; init; }
    
    public static StateResult Success(string? message = null, string? nextState = null) 
        => new() { IsSuccess = true, Message = message, NextState = nextState };
    
    public static StateResult Failure(string message) 
        => new() { IsSuccess = false, Message = message };
    
    public static StateResult Waiting(string? message = null) 
        => new() { IsSuccess = true, Message = message, RequiresInput = true };
}

/// <summary>
/// 状态转换结果
/// </summary>
public class TransitionResult
{
    public bool Success { get; init; }
    public string? ErrorMessage { get; init; }
    public IState? FromState { get; init; }
    public IState? ToState { get; init; }
    
    public static TransitionResult Succeeded(IState from, IState to) 
        => new() { Success = true, FromState = from, ToState = to };
    
    public static TransitionResult Failed(string error, IState? from = null) 
        => new() { Success = false, ErrorMessage = error, FromState = from };
}

3.3 状态转换逻辑与守卫条件

3.3.1 转换规则定义

csharp 复制代码
namespace AIAgentSystem.States;

/// <summary>
/// 状态转换规则
/// </summary>
public class TransitionRule
{
    public string FromStateName { get; init; }
    public string ToStateName { get; init; }
    public Func<StateContext, bool>? Guard { get; init; }
    public string? Description { get; init; }
    
    public TransitionRule(string fromStateName, string toStateName, 
        Func<StateContext, bool>? guard = null, string? description = null)
    {
        FromStateName = fromStateName;
        ToStateName = toStateName;
        Guard = guard;
        Description = description;
    }
    
    /// <summary>
    /// 检查守卫条件是否满足
    /// </summary>
    public bool Evaluate(StateContext context)
    {
        if (Guard is null) return true;
        
        try
        {
            return Guard(context);
        }
        catch (Exception ex)
        {
            // 守卫条件执行失败时记录日志,默认不允许转换
            Console.WriteLine($"Guard evaluation failed: {ex.Message}");
            return false;
        }
    }
}

3.3.2 状态机实现

csharp 复制代码
namespace AIAgentSystem.States;

/// <summary>
/// 状态机的默认实现
/// </summary>
public class StateMachine : IStateMachine
{
    private readonly Dictionary<string, IState> _states = new();
    private readonly List<TransitionRule> _transitions = new();
    private IState _currentState = null!;
    private IState? _previousState;
    
    public IState CurrentState => _currentState;
    public IState? PreviousState => _previousState;
    public IReadOnlyDictionary<string, IState> States => _states;
    
    public void RegisterState(IState state)
    {
        ArgumentNullException.ThrowIfNull(state);
        
        if (_states.ContainsKey(state.Name))
        {
            throw new InvalidOperationException($"State '{state.Name}' is already registered.");
        }
        
        _states[state.Name] = state;
    }
    
    public void AddTransition(string fromStateName, string toStateName, 
        Func<StateContext, bool>? guard = null)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(fromStateName);
        ArgumentException.ThrowIfNullOrWhiteSpace(toStateName);
        
        _transitions.Add(new TransitionRule(fromStateName, toStateName, guard));
    }
    
    public async Task<TransitionResult> TransitionAsync(string targetStateName, 
        StateContext context, CancellationToken cancellationToken = default)
    {
        // 检查目标状态是否存在
        if (!_states.TryGetValue(targetStateName, out var targetState))
        {
            return TransitionResult.Failed($"Target state '{targetStateName}' not found.");
        }
        
        // 检查转换规则是否存在
        var transition = _transitions.FirstOrDefault(t => 
            t.FromStateName == _currentState.Name && 
            t.ToStateName == targetStateName);
        
        if (transition is null)
        {
            return TransitionResult.Failed(
                $"No transition rule from '{_currentState.Name}' to '{targetStateName}'.", 
                _currentState);
        }
        
        // 检查守卫条件
        if (!transition.Evaluate(context))
        {
            return TransitionResult.Failed(
                $"Guard condition for transition from '{_currentState.Name}' to '{targetStateName}' not satisfied.", 
                _currentState);
        }
        
        // 检查目标状态是否允许进入
        if (!targetState.CanTransitionTo(targetState, context))
        {
            return TransitionResult.Failed(
                $"Target state '{targetStateName}' rejected the transition.", 
                _currentState);
        }
        
        // 执行状态转换
        try
        {
            // 退出当前状态
            await _currentState.OnExitAsync(context, cancellationToken);
            
            // 更新状态
            _previousState = _currentState;
            _currentState = targetState;
            
            // 进入新状态
            await _currentState.OnEnterAsync(context, cancellationToken);
            
            return TransitionResult.Succeeded(_previousState, _currentState);
        }
        catch (Exception ex)
        {
            return TransitionResult.Failed(
                $"Transition failed with error: {ex.Message}", 
                _previousState ?? _currentState);
        }
    }
    
    public async Task<StateResult> ExecuteCurrentStateAsync(StateContext context, 
        CancellationToken cancellationToken = default)
    {
        return await _currentState.ExecuteAsync(context, cancellationToken);
    }
    
    public async Task ResetAsync(string initialStateName, StateContext context, 
        CancellationToken cancellationToken = default)
    {
        if (!_states.TryGetValue(initialStateName, out var initialState))
        {
            throw new InvalidOperationException($"Initial state '{initialStateName}' not found.");
        }
        
        _previousState = null;
        _currentState = initialState;
        await _currentState.OnEnterAsync(context, cancellationToken);
    }
}

3.4 代码示例:对话状态、任务状态、错误状态

3.4.1 对话状态

csharp 复制代码
namespace AIAgentSystem.States.Conversation;

/// <summary>
/// 对话状态:处理用户交互
/// </summary>
public class ConversationState : IState
{
    private readonly ILLMService _llmService;
    private readonly ILogger<ConversationState> _logger;
    
    public string Name => "Conversation";
    
    public ConversationState(ILLMService llmService, ILogger<ConversationState> logger)
    {
        _llmService = llmService ?? throw new ArgumentNullException(nameof(llmService));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }
    
    public async Task OnEnterAsync(StateContext context, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Entering Conversation state for agent {AgentId}", context.AgentId);
        
        // 初始化对话上下文
        if (!context.Data.ContainsKey("conversation_start"))
        {
            context.Data["conversation_start"] = DateTime.UtcNow;
        }
    }
    
    public async Task OnExitAsync(StateContext context, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Exiting Conversation state for agent {AgentId}", context.AgentId);
        
        // 保存对话摘要
        context.Data["last_conversation_end"] = DateTime.UtcNow;
    }
    
    public async Task<StateResult> ExecuteAsync(StateContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            // 检查是否有输入
            if (string.IsNullOrWhiteSpace(context.Input))
            {
                return StateResult.Waiting("Waiting for user input.");
            }
            
            // 调用 LLM 服务生成回复
            var response = await _llmService.GenerateResponseAsync(
                context.Input, 
                context.History, 
                cancellationToken);
            
            // 添加到历史记录
            context.History.Add(new MessageRecord
            {
                Role = "user",
                Content = context.Input,
                Timestamp = DateTime.UtcNow
            });
            context.History.Add(new MessageRecord
            {
                Role = "assistant",
                Content = response,
                Timestamp = DateTime.UtcNow
            });
            
            // 清空输入,等待下次
            context.Input = null;
            
            // 检查是否需要切换到任务状态
            if (response.Contains("[TASK:"))
            {
                return StateResult.Success(response, "TaskExecution");
            }
            
            return StateResult.Success(response);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in Conversation state");
            return StateResult.Failure($"Conversation error: {ex.Message}");
        }
    }
    
    public bool CanTransitionTo(IState targetState, StateContext context)
    {
        // 对话状态可以转换到任何状态
        return true;
    }
}

3.4.2 任务状态

csharp 复制代码
namespace AIAgentSystem.States.Tasks;

/// <summary>
/// 任务执行状态:处理复杂任务
/// </summary>
public class TaskExecutionState : IState
{
    private readonly IPlanner _planner;
    private readonly IToolExecutor _toolExecutor;
    private readonly ILogger<TaskExecutionState> _logger;
    
    public string Name => "TaskExecution";
    
    public TaskExecutionState(IPlanner planner, IToolExecutor toolExecutor, 
        ILogger<TaskExecutionState> logger)
    {
        _planner = planner ?? throw new ArgumentNullException(nameof(planner));
        _toolExecutor = toolExecutor ?? throw new ArgumentNullException(nameof(toolExecutor));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }
    
    public async Task OnEnterAsync(StateContext context, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Entering TaskExecution state for agent {AgentId}", context.AgentId);
        
        // 创建任务计划
        var plan = await _planner.CreatePlanAsync(context.Input ?? string.Empty, cancellationToken);
        context.Data["current_plan"] = plan;
        context.Data["plan_step_index"] = 0;
    }
    
    public async Task OnExitAsync(StateContext context, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Exiting TaskExecution state for agent {AgentId}", context.AgentId);
        
        // 清理任务数据
        context.Data.Remove("current_plan");
        context.Data.Remove("plan_step_index");
    }
    
    public async Task<StateResult> ExecuteAsync(StateContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            var plan = (Plan?)context.Data.GetValueOrDefault("current_plan");
            if (plan is null)
            {
                return StateResult.Failure("No active plan found.", "Conversation");
            }
            
            var stepIndex = (int)context.Data.GetValueOrDefault("plan_step_index")!;
            if (stepIndex >= plan.Steps.Count)
            {
                // 计划完成
                return StateResult.Success("Task completed successfully.", "Conversation");
            }
            
            var currentStep = plan.Steps[stepIndex];
            
            // 执行当前步骤
            var result = await _toolExecutor.ExecuteAsync(currentStep, cancellationToken);
            
            if (!result.IsSuccess)
            {
                // 重新规划或失败
                context.Data["error"] = result.Error;
                return StateResult.Failure($"Step failed: {result.Error}", "Error");
            }
            
            // 更新步骤索引
            context.Data["plan_step_index"] = stepIndex + 1;
            
            // 检查是否还有更多步骤
            if (stepIndex + 1 >= plan.Steps.Count)
            {
                return StateResult.Success("Task completed.", "Conversation");
            }
            
            return StateResult.Success($"Step {stepIndex + 1} completed.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in TaskExecution state");
            return StateResult.Failure($"Task execution error: {ex.Message}", "Error");
        }
    }
    
    public bool CanTransitionTo(IState targetState, StateContext context)
    {
        // 任务状态可以转换到对话状态或错误状态
        return targetState.Name is "Conversation" or "Error";
    }
}

3.4.3 错误状态

csharp 复制代码
namespace AIAgentSystem.States.Error;

/// <summary>
/// 错误状态:处理异常和恢复
/// </summary>
public class ErrorState : IState
{
    private readonly ILogger<ErrorState> _logger;
    private readonly int _maxRetryCount;
    
    public string Name => "Error";
    
    public ErrorState(ILogger<ErrorState> logger, int maxRetryCount = 3)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _maxRetryCount = maxRetryCount;
    }
    
    public async Task OnEnterAsync(StateContext context, CancellationToken cancellationToken = default)
    {
        _logger.LogWarning("Entering Error state for agent {AgentId}", context.AgentId);
        
        // 初始化错误计数
        var errorCount = (int)context.Data.GetValueOrDefault("error_count", 0) + 1;
        context.Data["error_count"] = errorCount;
        context.Data["error_time"] = DateTime.UtcNow;
    }
    
    public async Task OnExitAsync(StateContext context, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Exiting Error state for agent {AgentId}", context.AgentId);
        
        // 清除当前错误信息
        context.Data.Remove("error");
    }
    
    public async Task<StateResult> ExecuteAsync(StateContext context, CancellationToken cancellationToken = default)
    {
        var errorCount = (int)context.Data.GetValueOrDefault("error_count", 0)!;
        var lastError = context.Data.GetValueOrDefault("error")?.ToString() ?? "Unknown error";
        
        _logger.LogError("Error state execution: {Error}, count: {Count}", lastError, errorCount);
        
        // 检查重试次数
        if (errorCount >= _maxRetryCount)
        {
            // 超过最大重试次数,重置并返回对话状态
            context.Data["error_count"] = 0;
            return StateResult.Failure(
                "Maximum retry attempts reached. Returning to conversation.", 
                "Conversation");
        }
        
        // 等待一段时间后重试
        await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, errorCount)), cancellationToken);
        
        // 返回之前的状态重试
        var previousState = context.Data.GetValueOrDefault("previous_state")?.ToString();
        if (!string.IsNullOrEmpty(previousState))
        {
            return StateResult.Success("Attempting recovery.", previousState);
        }
        
        // 默认返回对话状态
        return StateResult.Success("Returning to conversation.", "Conversation");
    }
    
    public bool CanTransitionTo(IState targetState, StateContext context)
    {
        // 错误状态可以转换到任何状态(用于恢复)
        return true;
    }
}

3.5 状态持久化方案

3.5.1 状态序列化

csharp 复制代码
namespace AIAgentSystem.States.Persistence;

/// <summary>
/// 状态快照,用于持久化
/// </summary>
public class StateSnapshot
{
    public required string StateMachineId { get; init; }
    public required string CurrentStateName { get; init; }
    public string? PreviousStateName { get; init; }
    public Dictionary<string, object> Data { get; init; } = new();
    public DateTime Timestamp { get; init; } = DateTime.UtcNow;
}

/// <summary>
/// 状态持久化接口
/// </summary>
public interface IStatePersistence
{
    Task SaveAsync(StateSnapshot snapshot, CancellationToken cancellationToken = default);
    Task<StateSnapshot?> LoadAsync(string stateMachineId, CancellationToken cancellationToken = default);
    Task DeleteAsync(string stateMachineId, CancellationToken cancellationToken = default);
}

/// <summary>
/// 基于 JSON 文件的持久化实现
/// </summary>
public class FileStatePersistence : IStatePersistence
{
    private readonly string _baseDirectory;
    private readonly JsonSerializerOptions _jsonOptions;
    
    public FileStatePersistence(string baseDirectory)
    {
        _baseDirectory = baseDirectory ?? throw new ArgumentNullException(nameof(baseDirectory));
        _jsonOptions = new JsonSerializerOptions
        {
            WriteIndented = true,
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        
        Directory.CreateDirectory(_baseDirectory);
    }
    
    public async Task SaveAsync(StateSnapshot snapshot, CancellationToken cancellationToken = default)
    {
        var filePath = Path.Combine(_baseDirectory, $"{snapshot.StateMachineId}.json");
        var json = JsonSerializer.Serialize(snapshot, _jsonOptions);
        await File.WriteAllTextAsync(filePath, json, cancellationToken);
    }
    
    public async Task<StateSnapshot?> LoadAsync(string stateMachineId, CancellationToken cancellationToken = default)
    {
        var filePath = Path.Combine(_baseDirectory, $"{stateMachineId}.json");
        
        if (!File.Exists(filePath))
        {
            return null;
        }
        
        var json = await File.ReadAllTextAsync(filePath, cancellationToken);
        return JsonSerializer.Deserialize<StateSnapshot>(json, _jsonOptions);
    }
    
    public Task DeleteAsync(string stateMachineId, CancellationToken cancellationToken = default)
    {
        var filePath = Path.Combine(_baseDirectory, $"{stateMachineId}.json");
        
        if (File.Exists(filePath))
        {
            File.Delete(filePath);
        }
        
        return Task.CompletedTask;
    }
}

3.5.2 状态机扩展方法

csharp 复制代码
namespace AIAgentSystem.States.Persistence;

/// <summary>
/// 状态机持久化扩展方法
/// </summary>
public static class StateMachinePersistenceExtensions
{
    /// <summary>
    /// 保存状态机当前状态
    /// </summary>
    public static async Task SaveStateAsync(this IStateMachine stateMachine, 
        IStatePersistence persistence, string stateMachineId, StateContext context,
        CancellationToken cancellationToken = default)
    {
        var snapshot = new StateSnapshot
        {
            StateMachineId = stateMachineId,
            CurrentStateName = stateMachine.CurrentState.Name,
            PreviousStateName = stateMachine.PreviousState?.Name,
            Data = new Dictionary<string, object>(context.Data),
            Timestamp = DateTime.UtcNow
        };
        
        await persistence.SaveAsync(snapshot, cancellationToken);
    }
    
    /// <summary>
    /// 从持久化存储恢复状态机状态
    /// </summary>
    public static async Task<bool> RestoreStateAsync(this IStateMachine stateMachine,
        IStatePersistence persistence, string stateMachineId, StateContext context,
        CancellationToken cancellationToken = default)
    {
        var snapshot = await persistence.LoadAsync(stateMachineId, cancellationToken);
        
        if (snapshot is null)
        {
            return false;
        }
        
        // 恢复数据
        foreach (var (key, value) in snapshot.Data)
        {
            context.Data[key] = value;
        }
        
        // 恢复状态
        if (stateMachine.States.TryGetValue(snapshot.CurrentStateName, out var state))
        {
            await stateMachine.ResetAsync(snapshot.CurrentStateName, context, cancellationToken);
            return true;
        }
        
        return false;
    }
}

3.6 小结

本章介绍了 AI Agent 系统中状态机的设计与实现。通过 IStateIStateMachine 接口,我们定义了清晰的状态管理契约。状态转换通过守卫条件进行控制,确保了状态转换的合法性。我们实现了对话状态、任务状态和错误状态三个典型示例,并提供了基于 JSON 文件的状态持久化方案。

第四章 规划器实现

规划器(Planner)是 AI Agent 系统的"大脑",负责将复杂目标分解为可执行的步骤,并协调这些步骤的执行顺序。本章将详细介绍规划器的设计与实现。

4.1 规划器的职责

规划器在 Agent 系统中承担三个核心职责:

4.1.1 目标分解

将用户的高级目标分解为具体的、可执行的原子任务。例如:

  • 用户目标:"帮我安排明天的会议"
  • 分解结果
    1. 查询日历,找出空闲时段
    2. 发送会议邀请给参会者
    3. 预订会议室
    4. 设置提醒

4.1.2 步骤排序

确定任务步骤的执行顺序,考虑:

  • 依赖关系:步骤 B 必须在步骤 A 完成后执行
  • 并行机会:无依赖的步骤可以并行执行
  • 优先级:关键路径上的步骤优先执行

4.1.3 依赖管理

管理步骤之间的数据流和依赖关系:

  • 数据依赖:步骤 B 需要步骤 A 的输出作为输入
  • 资源依赖:多个步骤竞争同一资源
  • 条件依赖:某些步骤仅在特定条件下执行

4.2 C# 类设计

4.2.1 IPlanner 接口

csharp 复制代码
namespace AIAgentSystem.Planning;

/// <summary>
/// 规划器接口,负责目标分解和计划生成
/// </summary>
public interface IPlanner
{
    /// <summary>
    /// 根据用户目标创建执行计划
    /// </summary>
    /// <param name="goal">用户目标描述</param>
    /// <param name="cancellationToken">取消令牌</param>
    /// <returns>生成的执行计划</returns>
    Task<Plan> CreatePlanAsync(string goal, CancellationToken cancellationToken = default);
    
    /// <summary>
    /// 根据上下文创建计划
    /// </summary>
    /// <param name="goal">用户目标</param>
    /// <param name="context">规划上下文,包含历史、可用工具等信息</param>
    /// <param name="cancellationToken">取消令牌</param>
    /// <returns>生成的执行计划</returns>
    Task<Plan> CreatePlanAsync(string goal, PlanContext context, CancellationToken cancellationToken = default);
    
    /// <summary>
    /// 重新规划:根据执行情况调整计划
    /// </summary>
    /// <param name="originalPlan">原始计划</param>
    /// <param name="failedStep">失败的步骤</param>
    /// <param name="error">错误信息</param>
    /// <param name="cancellationToken">取消令牌</param>
    /// <returns>调整后的新计划</returns>
    Task<Plan> ReplanAsync(Plan originalPlan, PlanStep failedStep, string error, 
        CancellationToken cancellationToken = default);
    
    /// <summary>
    /// 验证计划的可行性
    /// </summary>
    /// <param name="plan">要验证的计划</param>
    /// <param name="cancellationToken">取消令牌</param>
    /// <returns>验证结果</returns>
    Task<PlanValidationResult> ValidatePlanAsync(Plan plan, CancellationToken cancellationToken = default);
}

4.2.2 Plan 类

csharp 复制代码
namespace AIAgentSystem.Planning;

/// <summary>
/// 执行计划,包含一系列有序的步骤
/// </summary>
public class Plan
{
    /// <summary>
    /// 计划的唯一标识
    /// </summary>
    public Guid Id { get; init; } = Guid.NewGuid();
    
    /// <summary>
    /// 原始目标描述
    /// </summary>
    public required string Goal { get; init; }
    
    /// <summary>
    /// 计划的步骤列表(按执行顺序)
    /// </summary>
    public IList<PlanStep> Steps { get; init; } = new List<PlanStep>();
    
    /// <summary>
    /// 计划创建时间
    /// </summary>
    public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
    
    /// <summary>
    /// 计划状态
    /// </summary>
    public PlanStatus Status { get; set; } = PlanStatus.Pending;
    
    /// <summary>
    /// 计划元数据(可存储额外信息)
    /// </summary>
    public IDictionary<string, object> Metadata { get; init; } = new Dictionary<string, object>();
    
    /// <summary>
    /// 获取指定步骤的依赖步骤
    /// </summary>
    public IEnumerable<PlanStep> GetDependencies(PlanStep step)
    {
        return step.DependsOn
            .Select(depId => Steps.FirstOrDefault(s => s.Id == depId))
            .Where(s => s is not null)!;
    }
    
    /// <summary>
    /// 获取当前可执行的步骤(依赖已满足且未完成)
    /// </summary>
    public IEnumerable<PlanStep> GetExecutableSteps()
    {
        return Steps.Where(step => 
            step.Status == StepStatus.Pending &&
            step.DependsOn.All(depId => 
                Steps.FirstOrDefault(s => s.Id == depId)?.Status == StepStatus.Completed));
    }
    
    /// <summary>
    /// 计算计划完成百分比
    /// </summary>
    public double GetProgressPercentage()
    {
        if (Steps.Count == 0) return 0;
        
        var completedCount = Steps.Count(s => s.Status == StepStatus.Completed);
        return (double)completedCount / Steps.Count * 100;
    }
}

/// <summary>
/// 计划状态
/// </summary>
public enum PlanStatus
{
    Pending,
    InProgress,
    Completed,
    Failed,
    Cancelled
}

4.2.3 PlanStep 类

csharp 复制代码
namespace AIAgentSystem.Planning;

/// <summary>
/// 计划中的单个步骤
/// </summary>
public class PlanStep
{
    /// <summary>
    /// 步骤的唯一标识
    /// </summary>
    public Guid Id { get; init; } = Guid.NewGuid();
    
    /// <summary>
    /// 步骤描述
    /// </summary>
    public required string Description { get; init; }
    
    /// <summary>
    /// 要执行的工具或动作名称
    /// </summary>
    public string? ToolName { get; init; }
    
    /// <summary>
    /// 工具参数(JSON 格式)
    /// </summary>
    public IDictionary<string, object> Parameters { get; init; } = new Dictionary<string, object>();
    
    /// <summary>
    /// 依赖的步骤 ID 列表
    /// </summary>
    public IList<Guid> DependsOn { get; init; } = new List<Guid>();
    
    /// <summary>
    /// 步骤状态
    /// </summary>
    public StepStatus Status { get; set; } = StepStatus.Pending;
    
    /// <summary>
    /// 步骤执行结果
    /// </summary>
    public StepResult? Result { get; set; }
    
    /// <summary>
    /// 步骤执行的开始时间
    /// </summary>
    public DateTime? StartedAt { get; set; }
    
    /// <summary>
    /// 步骤执行的结束时间
    /// </summary>
    public DateTime? CompletedAt { get; set; }
    
    /// <summary>
    /// 预估执行时间(秒)
    /// </summary>
    public int EstimatedDurationSeconds { get; init; }
    
    /// <summary>
    /// 重试次数
    /// </summary>
    public int RetryCount { get; set; }
    
    /// <summary>
    /// 最大重试次数
    /// </summary>
    public int MaxRetries { get; init; } = 3;
    
    /// <summary>
    /// 是否可以重试
    /// </summary>
    public bool CanRetry => RetryCount < MaxRetries && Status == StepStatus.Failed;
    
    /// <summary>
    /// 步骤优先级(数值越大优先级越高)
    /// </summary>
    public int Priority { get; init; }
}

/// <summary>
/// 步骤状态
/// </summary>
public enum StepStatus
{
    Pending,
    InProgress,
    Completed,
    Failed,
    Skipped
}

/// <summary>
/// 步骤执行结果
/// </summary>
public class StepResult
{
    public bool IsSuccess { get; init; }
    public object? Output { get; init; }
    public string? Error { get; init; }
    public IDictionary<string, object> Metadata { get; init; } = new Dictionary<string, object>();
    
    public static StepResult Success(object? output = null) 
        => new() { IsSuccess = true, Output = output };
    
    public static StepResult Failure(string error) 
        => new() { IsSuccess = false, Error = error };
}

4.2.4 辅助类型

csharp 复制代码
namespace AIAgentSystem.Planning;

/// <summary>
/// 规划上下文
/// </summary>
public class PlanContext
{
    /// <summary>
    /// 可用工具列表
    /// </summary>
    public IList<ToolDefinition> AvailableTools { get; init; } = new List<ToolDefinition>();
    
    /// <summary>
    /// 历史执行记录
    /// </summary>
    public IList<ExecutionHistory> History { get; init; } = new List<ExecutionHistory>();
    
    /// <summary>
    /// 用户偏好
    /// </summary>
    public IDictionary<string, object> UserPreferences { get; init; } = new Dictionary<string, object>();
    
    /// <summary>
    /// 当前环境状态
    /// </summary>
    public IDictionary<string, object> EnvironmentState { get; init; } = new Dictionary<string, object>();
    
    /// <summary>
    /// 约束条件
    /// </summary>
    public IList<string> Constraints { get; init; } = new List<string>();
}

/// <summary>
/// 工具定义
/// </summary>
public class ToolDefinition
{
    public required string Name { get; init; }
    public required string Description { get; init; }
    public IDictionary<string, ParameterDefinition> Parameters { get; init; } = new Dictionary<string, ParameterDefinition>();
    public bool IsDestructive { get; init; }
}

/// <summary>
/// 参数定义
/// </summary>
public class ParameterDefinition
{
    public required string Type { get; init; }
    public string? Description { get; init; }
    public bool IsRequired { get; init; }
    public object? DefaultValue { get; init; }
}

/// <summary>
/// 执行历史记录
/// </summary>
public class ExecutionHistory
{
    public Guid StepId { get; init; }
    public string ToolName { get; init; } = string.Empty;
    public bool Success { get; init; }
    public DateTime ExecutedAt { get; init; }
    public TimeSpan Duration { get; init; }
}

/// <summary>
/// 计划验证结果
/// </summary>
public class PlanValidationResult
{
    public bool IsValid { get; init; }
    public IList<string> Errors { get; init; } = new List<string>();
    public IList<string> Warnings { get; init; } = new List<string>();
    
    public static PlanValidationResult Valid() => new() { IsValid = true };
    
    public static PlanValidationResult Invalid(params string[] errors) 
        => new() { IsValid = false, Errors = errors.ToList() };
}

4.3 LLM 驱动的智能规划

4.3.1 LLM 规划器实现

csharp 复制代码
namespace AIAgentSystem.Planning;

/// <summary>
/// 基于 LLM 的智能规划器
/// </summary>
public class LLMPlanner : IPlanner
{
    private readonly ILLMService _llmService;
    private readonly ILogger<LLMPlanner> _logger;
    private readonly IToolRegistry _toolRegistry;
    
    public LLMPlanner(ILLMService llmService, IToolRegistry toolRegistry, ILogger<LLMPlanner> logger)
    {
        _llmService = llmService ?? throw new ArgumentNullException(nameof(llmService));
        _toolRegistry = toolRegistry ?? throw new ArgumentNullException(nameof(toolRegistry));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }
    
    public async Task<Plan> CreatePlanAsync(string goal, CancellationToken cancellationToken = default)
    {
        var context = new PlanContext
        {
            AvailableTools = _toolRegistry.GetAllTools().ToList()
        };
        
        return await CreatePlanAsync(goal, context, cancellationToken);
    }
    
    public async Task<Plan> CreatePlanAsync(string goal, PlanContext context, 
        CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Creating plan for goal: {Goal}", goal);
        
        // 构建规划提示词
        var prompt = BuildPlanningPrompt(goal, context);
        
        // 调用 LLM 生成计划
        var response = await _llmService.GenerateAsync(prompt, cancellationToken);
        
        // 解析 LLM 响应为结构化计划
        var plan = ParsePlanResponse(response, goal);
        
        // 验证计划
        var validationResult = await ValidatePlanAsync(plan, cancellationToken);
        if (!validationResult.IsValid)
        {
            throw new PlanningException($"Invalid plan generated: {string.Join(", ", validationResult.Errors)}");
        }
        
        _logger.LogInformation("Plan created with {Count} steps", plan.Steps.Count);
        
        return plan;
    }
    
    public async Task<Plan> ReplanAsync(Plan originalPlan, PlanStep failedStep, string error, 
        CancellationToken cancellationToken = default)
    {
        _logger.LogWarning("Replanning due to failed step {StepId}: {Error}", failedStep.Id, error);
        
        // 标记失败步骤
        failedStep.Status = StepStatus.Failed;
        failedStep.Result = StepResult.Failure(error);
        
        // 构建重新规划提示词
        var prompt = BuildReplanningPrompt(originalPlan, failedStep, error);
        
        // 调用 LLM 重新规划
        var response = await _llmService.GenerateAsync(prompt, cancellationToken);
        
        // 解析新计划
        var newPlan = ParsePlanResponse(response, originalPlan.Goal);
        
        // 保留已完成步骤的状态
        foreach (var step in originalPlan.Steps.Where(s => s.Status == StepStatus.Completed))
        {
            var newStep = newPlan.Steps.FirstOrDefault(s => s.Description == step.Description);
            if (newStep is not null)
            {
                newStep.Status = StepStatus.Completed;
                newStep.Result = step.Result;
            }
        }
        
        return newPlan;
    }
    
    public async Task<PlanValidationResult> ValidatePlanAsync(Plan plan, CancellationToken cancellationToken = default)
    {
        var errors = new List<string>();
        var warnings = new List<string>();
        
        // 检查步骤依赖是否存在
        var stepIds = plan.Steps.Select(s => s.Id).ToHashSet();
        foreach (var step in plan.Steps)
        {
            foreach (var depId in step.DependsOn)
            {
                if (!stepIds.Contains(depId))
                {
                    errors.Add($"Step '{step.Description}' depends on non-existent step {depId}");
                }
            }
        }
        
        // 检查循环依赖
        if (HasCircularDependency(plan))
        {
            errors.Add("Plan contains circular dependencies");
        }
        
        // 检查孤立步骤(无依赖且不被依赖)
        var isolatedSteps = plan.Steps.Where(s => 
            !s.DependsOn.Any() && 
            !plan.Steps.Any(other => other.DependsOn.Contains(s.Id))).ToList();
        
        if (isolatedSteps.Count > 1)
        {
            warnings.Add($"Found {isolatedSteps.Count} isolated steps that could be parallelized");
        }
        
        // 检查工具可用性
        var availableTools = _toolRegistry.GetAllTools().Select(t => t.Name).ToHashSet();
        foreach (var step in plan.Steps.Where(s => !string.IsNullOrEmpty(s.ToolName)))
        {
            if (!availableTools.Contains(step.ToolName!))
            {
                errors.Add($"Step '{step.Description}' uses unavailable tool '{step.ToolName}'");
            }
        }
        
        return errors.Any() 
            ? PlanValidationResult.Invalid(errors.ToArray())
            : new PlanValidationResult { IsValid = true, Warnings = warnings };
    }
    
    /// <summary>
    /// 构建规划提示词
    /// </summary>
    private string BuildPlanningPrompt(string goal, PlanContext context)
    {
        var toolsDescription = string.Join("\n", context.AvailableTools.Select(t => 
            $"- {t.Name}: {t.Description}" + 
            (t.Parameters.Any() ? $"\n  Parameters: {string.Join(", ", t.Parameters.Keys)}" : "")));
        
        var constraintsText = context.Constraints.Any() 
            ? $"\n\nConstraints:\n{string.Join("\n", context.Constraints.Select(c => $"- {c}"))}" 
            : "";
        
        return $@"You are a planning agent. Create a detailed execution plan for the following goal.

Goal: {goal}

Available Tools:
{toolsDescription}
{constraintsText}

Instructions:
1. Break down the goal into atomic, executable steps
2. Define dependencies between steps (a step can only run after its dependencies complete)
3. Each step should use exactly one tool
4. Provide clear parameters for each tool call

Output the plan in the following JSON format:
{{
  ""steps"": [
    {{
      ""description"": ""Description of the step"",
      ""tool"": ""ToolName"",
      ""parameters"": {{ ""param1"": ""value1"" }},
      ""depends_on"": [0],
      ""priority"": 1
    }}
  ]
}}

Note: depends_on uses 0-based indices referring to previous steps in the array.
Only output valid JSON, no additional text.";
    }
    
    /// <summary>
    /// 构建重新规划提示词
    /// </summary>
    private string BuildReplanningPrompt(Plan originalPlan, PlanStep failedStep, string error)
    {
        var completedSteps = originalPlan.Steps
            .Where(s => s.Status == StepStatus.Completed)
            .Select(s => $"- {s.Description}")
            .ToList();
        
        var remainingSteps = originalPlan.Steps
            .Where(s => s.Status == StepStatus.Pending)
            .Select(s => $"- {s.Description}")
            .ToList();
        
        return $@"The following plan failed during execution. Please create a new plan to achieve the goal.

Original Goal: {originalPlan.Goal}

Completed Steps:
{(completedSteps.Any() ? string.Join("\n", completedSteps) : "None")}

Failed Step: {failedStep.Description}
Error: {error}

Remaining Steps from Original Plan:
{(remainingSteps.Any() ? string.Join("\n", remainingSteps) : "None")}

Create a revised plan that:
1. Accounts for the completed steps (don't repeat them)
2. Handles the failure appropriately
3. Achieves the original goal

Output the plan in JSON format as before.";
    }
    
    /// <summary>
    /// 解析 LLM 响应为计划对象
    /// </summary>
    private Plan ParsePlanResponse(string response, string goal)
    {
        try
        {
            // 提取 JSON 部分
            var jsonStart = response.IndexOf('{');
            var jsonEnd = response.LastIndexOf('}');
            
            if (jsonStart < 0 || jsonEnd < 0)
            {
                throw new PlanningException("No valid JSON found in LLM response");
            }
            
            var json = response.Substring(jsonStart, jsonEnd - jsonStart + 1);
            
            // 解析 JSON
            var planResponse = JsonSerializer.Deserialize<PlanResponse>(json, new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true
            });
            
            if (planResponse is null || planResponse.Steps.Count == 0)
            {
                throw new PlanningException("Empty plan received from LLM");
            }
            
            // 转换为 Plan 对象
            var steps = new List<PlanStep>();
            var stepIdMap = new Dictionary<int, Guid>();
            
            for (var i = 0; i < planResponse.Steps.Count; i++)
            {
                var stepData = planResponse.Steps[i];
                var stepId = Guid.NewGuid();
                stepIdMap[i] = stepId;
                
                var step = new PlanStep
                {
                    Id = stepId,
                    Description = stepData.Description,
                    ToolName = stepData.Tool,
                    Parameters = stepData.Parameters ?? new Dictionary<string, object>(),
                    Priority = stepData.Priority
                };
                
                steps.Add(step);
            }
            
            // 设置依赖关系
            for (var i = 0; i < planResponse.Steps.Count; i++)
            {
                var stepData = planResponse.Steps[i];
                var step = steps[i];
                
                foreach (var depIndex in stepData.DependsOn)
                {
                    if (depIndex >= 0 && depIndex < steps.Count && depIndex != i)
                    {
                        step.DependsOn.Add(stepIdMap[depIndex]);
                    }
                }
            }
            
            return new Plan
            {
                Goal = goal,
                Steps = steps,
                Status = PlanStatus.Pending
            };
        }
        catch (JsonException ex)
        {
            _logger.LogError(ex, "Failed to parse LLM response as plan");
            throw new PlanningException($"Failed to parse plan: {ex.Message}", ex);
        }
    }
    
    /// <summary>
    /// 检测循环依赖
    /// </summary>
    private bool HasCircularDependency(Plan plan)
    {
        var visited = new HashSet<Guid>();
        var recursionStack = new HashSet<Guid>();
        
        foreach (var step in plan.Steps)
        {
            if (HasCycle(step, plan, visited, recursionStack))
            {
                return true;
            }
        }
        
        return false;
    }
    
    private bool HasCycle(PlanStep step, Plan plan, HashSet<Guid> visited, HashSet<Guid> recursionStack)
    {
        if (recursionStack.Contains(step.Id))
        {
            return true;
        }
        
        if (visited.Contains(step.Id))
        {
            return false;
        }
        
        visited.Add(step.Id);
        recursionStack.Add(step.Id);
        
        foreach (var depId in step.DependsOn)
        {
            var depStep = plan.Steps.FirstOrDefault(s => s.Id == depId);
            if (depStep is not null && HasCycle(depStep, plan, visited, recursionStack))
            {
                return true;
            }
        }
        
        recursionStack.Remove(step.Id);
        return false;
    }
    
    /// <summary>
    /// JSON 响应模型
    /// </summary>
    private class PlanResponse
    {
        public List<PlanStepResponse> Steps { get; init; } = new();
    }
    
    private class PlanStepResponse
    {
        public string Description { get; init; } = string.Empty;
        public string? Tool { get; init; }
        public Dictionary<string, object>? Parameters { get; init; }
        public List<int> DependsOn { get; init; } = new();
        public int Priority { get; init; }
    }
}

/// <summary>
/// 规划异常
/// </summary>
public class PlanningException : Exception
{
    public PlanningException(string message) : base(message) { }
    public PlanningException(string message, Exception innerException) : base(message, innerException) { }
}

4.4 代码示例:多步任务规划流程

4.4.1 计划执行器

csharp 复制代码
namespace AIAgentSystem.Planning;

/// <summary>
/// 计划执行器接口
/// </summary>
public interface IPlanExecutor
{
    /// <summary>
    /// 执行计划
    /// </summary>
    Task<PlanExecutionResult> ExecuteAsync(Plan plan, CancellationToken cancellationToken = default);
}

/// <summary>
/// 计划执行结果
/// </summary>
public class PlanExecutionResult
{
    public bool IsSuccess { get; init; }
    public Plan Plan { get; init; } = null!;
    public string? ErrorMessage { get; init; }
    public int CompletedSteps { get; init; }
    public int TotalSteps { get; init; }
}

/// <summary>
/// 计划执行器实现
/// </summary>
public class PlanExecutor : IPlanExecutor
{
    private readonly IToolExecutor _toolExecutor;
    private readonly IPlanner _planner;
    private readonly ILogger<PlanExecutor> _logger;
    private readonly int _maxReplanAttempts;
    
    public PlanExecutor(IToolExecutor toolExecutor, IPlanner planner, 
        ILogger<PlanExecutor> logger, int maxReplanAttempts = 2)
    {
        _toolExecutor = toolExecutor ?? throw new ArgumentNullException(nameof(toolExecutor));
        _planner = planner ?? throw new ArgumentNullException(nameof(planner));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _maxReplanAttempts = maxReplanAttempts;
    }
    
    public async Task<PlanExecutionResult> ExecuteAsync(Plan plan, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Executing plan {PlanId} for goal: {Goal}", plan.Id, plan.Goal);
        
        plan.Status = PlanStatus.InProgress;
        var replanAttempts = 0;
        
        while (true)
        {
            // 获取可执行的步骤
            var executableSteps = plan.GetExecutableSteps().ToList();
            
            if (!executableSteps.Any())
            {
                // 检查是否全部完成
                if (plan.Steps.All(s => s.Status == StepStatus.Completed))
                {
                    plan.Status = PlanStatus.Completed;
                    return new PlanExecutionResult
                    {
                        IsSuccess = true,
                        Plan = plan,
                        CompletedSteps = plan.Steps.Count(s => s.Status == StepStatus.Completed),
                        TotalSteps = plan.Steps.Count
                    };
                }
                
                // 检查是否有失败的步骤
                var failedSteps = plan.Steps.Where(s => s.Status == StepStatus.Failed).ToList();
                if (failedSteps.Any())
                {
                    plan.Status = PlanStatus.Failed;
                    return new PlanExecutionResult
                    {
                        IsSuccess = false,
                        Plan = plan,
                        ErrorMessage = $"Plan failed at steps: {string.Join(", ", failedSteps.Select(s => s.Description))}",
                        CompletedSteps = plan.Steps.Count(s => s.Status == StepStatus.Completed),
                        TotalSteps = plan.Steps.Count
                    };
                }
                
                // 没有可执行步骤但未全部完成 - 可能存在依赖问题
                break;
            }
            
            // 执行步骤(按优先级排序)
            foreach (var step in executableSteps.OrderByDescending(s => s.Priority))
            {
                cancellationToken.ThrowIfCancellationRequested();
                
                var result = await ExecuteStepAsync(step, plan, cancellationToken);
                
                if (!result.IsSuccess)
                {
                    _logger.LogWarning("Step {StepId} failed: {Error}", step.Id, result.Error);
                    
                    // 检查步骤是否可重试
                    if (step.CanRetry)
                    {
                        step.RetryCount++;
                        step.Status = StepStatus.Pending;
                        _logger.LogInformation("Retrying step {StepId} (attempt {Attempt}/{Max})", 
                            step.Id, step.RetryCount, step.MaxRetries);
                        continue;
                    }
                    
                    // 尝试重新规划
                    if (replanAttempts < _maxReplanAttempts)
                    {
                        replanAttempts++;
                        _logger.LogInformation("Attempting replan ({Attempt}/{Max})", 
                            replanAttempts, _maxReplanAttempts);
                        
                        try
                        {
                            plan = await _planner.ReplanAsync(plan, step, result.Error ?? "Unknown error", cancellationToken);
                            plan.Status = PlanStatus.InProgress;
                            break; // 重新开始执行循环
                        }
                        catch (PlanningException ex)
                        {
                            _logger.LogError(ex, "Replanning failed");
                        }
                    }
                    
                    step.Status = StepStatus.Failed;
                    step.Result = result;
                }
            }
        }
        
        plan.Status = PlanStatus.Failed;
        return new PlanExecutionResult
        {
            IsSuccess = false,
            Plan = plan,
            ErrorMessage = "Execution stopped with pending steps",
            CompletedSteps = plan.Steps.Count(s => s.Status == StepStatus.Completed),
            TotalSteps = plan.Steps.Count
        };
    }
    
    private async Task<StepResult> ExecuteStepAsync(PlanStep step, Plan plan, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Executing step {StepId}: {Description}", step.Id, step.Description);
        
        step.Status = StepStatus.InProgress;
        step.StartedAt = DateTime.UtcNow;
        
        try
        {
            // 合并来自依赖步骤的输出
            var parameters = MergeDependencyOutputs(step, plan);
            
            // 执行工具
            var result = await _toolExecutor.ExecuteAsync(step.ToolName!, parameters, cancellationToken);
            
            step.CompletedAt = DateTime.UtcNow;
            step.Status = result.IsSuccess ? StepStatus.Completed : StepStatus.Failed;
            step.Result = result;
            
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Exception during step execution");
            step.Status = StepStatus.Failed;
            step.CompletedAt = DateTime.UtcNow;
            return StepResult.Failure(ex.Message);
        }
    }
    
    /// <summary>
    /// 合并依赖步骤的输出作为当前步骤的输入
    /// </summary>
    private IDictionary<string, object> MergeDependencyOutputs(PlanStep step, Plan plan)
    {
        var mergedParams = new Dictionary<string, object>(step.Parameters);
        
        foreach (var depId in step.DependsOn)
        {
            var depStep = plan.Steps.FirstOrDefault(s => s.Id == depId);
            if (depStep?.Result?.Output is IDictionary<string, object> output)
            {
                foreach (var (key, value) in output)
                {
                    if (!mergedParams.ContainsKey(key))
                    {
                        mergedParams[key] = value;
                    }
                }
            }
        }
        
        return mergedParams;
    }
}

4.4.2 工具执行器接口

csharp 复制代码
namespace AIAgentSystem.Tools;

/// <summary>
/// 工具执行器接口
/// </summary>
public interface IToolExecutor
{
    /// <summary>
    /// 执行指定工具
    /// </summary>
    Task<StepResult> ExecuteAsync(string toolName, IDictionary<string, object> parameters, 
        CancellationToken cancellationToken = default);
    
    /// <summary>
    /// 执行计划步骤
    /// </summary>
    Task<StepResult> ExecuteAsync(PlanStep step, CancellationToken cancellationToken = default);
}

/// <summary>
/// 工具注册表接口
/// </summary>
public interface IToolRegistry
{
    /// <summary>
    /// 获取所有已注册的工具
    /// </summary>
    IEnumerable<ToolDefinition> GetAllTools();
    
    /// <summary>
    /// 根据名称获取工具
    /// </summary>
    ToolDefinition? GetTool(string name);
    
    /// <summary>
    /// 注册工具
    /// </summary>
    void Register(ToolDefinition tool);
}

4.4.3 使用示例

csharp 复制代码
namespace AIAgentSystem.Examples;

/// <summary>
/// 规划器使用示例
/// </summary>
public class PlannerExample
{
    private readonly IPlanner _planner;
    private readonly IPlanExecutor _executor;
    
    public PlannerExample(IPlanner planner, IPlanExecutor executor)
    {
        _planner = planner;
        _executor = executor;
    }
    
    /// <summary>
    /// 执行复杂任务
    /// </summary>
    public async Task RunAsync(string goal, CancellationToken cancellationToken = default)
    {
        // 1. 创建计划
        Console.WriteLine($"Creating plan for: {goal}");
        var plan = await _planner.CreatePlanAsync(goal, cancellationToken);
        
        Console.WriteLine($"\nPlan created with {plan.Steps.Count} steps:");
        for (var i = 0; i < plan.Steps.Count; i++)
        {
            var step = plan.Steps[i];
            var dependencies = step.DependsOn.Any() 
                ? $" (depends on: {string.Join(", ", step.DependsOn)})"
                : "";
            Console.WriteLine($"  {i + 1}. {step.Description}{dependencies}");
        }
        
        // 2. 验证计划
        var validation = await _planner.ValidatePlanAsync(plan, cancellationToken);
        if (!validation.IsValid)
        {
            Console.WriteLine($"\nPlan validation failed: {string.Join(", ", validation.Errors)}");
            return;
        }
        
        if (validation.Warnings.Any())
        {
            Console.WriteLine($"\nWarnings: {string.Join(", ", validation.Warnings)}");
        }
        
        // 3. 执行计划
        Console.WriteLine("\nExecuting plan...");
        var result = await _executor.ExecuteAsync(plan, cancellationToken);
        
        // 4. 输出结果
        Console.WriteLine($"\nExecution completed:");
        Console.WriteLine($"  Success: {result.IsSuccess}");
        Console.WriteLine($"  Steps completed: {result.CompletedSteps}/{result.TotalSteps}");
        Console.WriteLine($"  Progress: {plan.GetProgressPercentage():F1}%");
        
        if (!result.IsSuccess)
        {
            Console.WriteLine($"  Error: {result.ErrorMessage}");
        }
        
        // 5. 输出详细步骤结果
        Console.WriteLine("\nStep details:");
        foreach (var step in plan.Steps)
        {
            var status = step.Status.ToString();
            var duration = step.StartedAt.HasValue && step.CompletedAt.HasValue
                ? $" ({(step.CompletedAt.Value - step.StartedAt.Value).TotalSeconds:F2}s)"
                : "";
            Console.WriteLine($"  - {step.Description}: {status}{duration}");
        }
    }
}

4.5 规划失败与重试机制

4.5.1 重试策略

csharp 复制代码
namespace AIAgentSystem.Planning;

/// <summary>
/// 步骤重试策略
/// </summary>
public class RetryStrategy
{
    /// <summary>
    /// 最大重试次数
    /// </summary>
    public int MaxRetries { get; init; } = 3;
    
    /// <summary>
    /// 初始延迟(毫秒)
    /// </summary>
    public int InitialDelayMs { get; init; } = 1000;
    
    /// <summary>
    /// 最大延迟(毫秒)
    /// </summary>
    public int MaxDelayMs { get; init; } = 30000;
    
    /// <summary>
    /// 退避倍数
    /// </summary>
    public double BackoffMultiplier { get; init; } = 2.0;
    
    /// <summary>
    /// 可重试的错误类型
    /// </summary>
    public IList<string> RetryableErrors { get; init; } = new List<string>
    {
        "timeout",
        "rate_limit",
        "temporary_failure",
        "service_unavailable"
    };
    
    /// <summary>
    /// 计算重试延迟
    /// </summary>
    public TimeSpan GetRetryDelay(int retryCount)
    {
        var delay = (int)(InitialDelayMs * Math.Pow(BackoffMultiplier, retryCount));
        return TimeSpan.FromMilliseconds(Math.Min(delay, MaxDelayMs));
    }
    
    /// <summary>
    /// 判断错误是否可重试
    /// </summary>
    public bool IsRetryable(string error)
    {
        return RetryableErrors.Any(retryable => 
            error.Contains(retryable, StringComparison.OrdinalIgnoreCase));
    }
}

/// <summary>
/// 支持重试的步骤执行器
/// </summary>
public class RetryingStepExecutor
{
    private readonly IToolExecutor _toolExecutor;
    private readonly ILogger<RetryingStepExecutor> _logger;
    private readonly RetryStrategy _strategy;
    
    public RetryingStepExecutor(IToolExecutor toolExecutor, ILogger<RetryingStepExecutor> logger, 
        RetryStrategy? strategy = null)
    {
        _toolExecutor = toolExecutor ?? throw new ArgumentNullException(nameof(toolExecutor));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _strategy = strategy ?? new RetryStrategy();
    }
    
    public async Task<StepResult> ExecuteWithRetryAsync(PlanStep step, CancellationToken cancellationToken = default)
    {
        var attempts = 0;
        var lastError = string.Empty;
        
        while (attempts <= _strategy.MaxRetries)
        {
            if (attempts > 0)
            {
                var delay = _strategy.GetRetryDelay(attempts - 1);
                _logger.LogInformation("Waiting {Delay}ms before retry {Attempt}/{Max}", 
                    delay.TotalMilliseconds, attempts, _strategy.MaxRetries);
                await Task.Delay(delay, cancellationToken);
            }
            
            var result = await _toolExecutor.ExecuteAsync(step, cancellationToken);
            
            if (result.IsSuccess)
            {
                return result;
            }
            
            lastError = result.Error ?? "Unknown error";
            
            if (!_strategy.IsRetryable(lastError))
            {
                _logger.LogWarning("Non-retryable error: {Error}", lastError);
                break;
            }
            
            attempts++;
        }
        
        return StepResult.Failure($"Max retries ({_strategy.MaxRetries}) exceeded. Last error: {lastError}");
    }
}

4.5.2 降级策略

csharp 复制代码
namespace AIAgentSystem.Planning;

/// <summary>
/// 降级策略接口
/// </summary>
public interface IFallbackStrategy
{
    /// <summary>
    /// 为失败的步骤提供替代方案
    /// </summary>
    Task<PlanStep?> GetFallbackAsync(PlanStep failedStep, string error, CancellationToken cancellationToken = default);
}

/// <summary>
/// 简单降级策略:使用备用工具
/// </summary>
public class AlternativeToolFallback : IFallbackStrategy
{
    private readonly ILogger<AlternativeToolFallback> _logger;
    private readonly Dictionary<string, string> _toolAlternatives;
    
    public AlternativeToolFallback(ILogger<AlternativeToolFallback> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        
        // 定义工具的替代方案
        _toolAlternatives = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            { "search_google", "search_bing" },
            { "send_email", "send_notification" },
            { "book_meeting", "suggest_times" }
        };
    }
    
    public async Task<PlanStep?> GetFallbackAsync(PlanStep failedStep, string error, 
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrEmpty(failedStep.ToolName))
        {
            return null;
        }
        
        if (!_toolAlternatives.TryGetValue(failedStep.ToolName, out var alternativeTool))
        {
            _logger.LogDebug("No alternative tool for {Tool}", failedStep.ToolName);
            return null;
        }
        
        _logger.LogInformation("Using fallback tool {Fallback} for {Original}", 
            alternativeTool, failedStep.ToolName);
        
        // 创建使用替代工具的新步骤
        return new PlanStep
        {
            Id = Guid.NewGuid(),
            Description = $"[Fallback] {failedStep.Description}",
            ToolName = alternativeTool,
            Parameters = new Dictionary<string, object>(failedStep.Parameters),
            DependsOn = new List<Guid>(failedStep.DependsOn),
            Priority = failedStep.Priority,
            MaxRetries = 1 // 降级步骤只尝试一次
        };
    }
}

4.6 小结

本章详细介绍了 AI Agent 系统中规划器的设计与实现。通过 IPlannerPlanPlanStep 等核心类型,我们定义了清晰的规划契约。LLM 驱动的智能规划器能够根据用户目标自动生成执行计划,并通过验证机制确保计划的可行性。我们还实现了完整的重试和降级机制,确保系统在遇到异常时能够优雅地恢复或降级。

第五章 工具调用引擎

工具调用(Tool Calling)是 AI Agent 系统的核心能力之一,它让模型能够执行实际操作------搜索网络、读写文件、调用 API、执行代码等。本章将深入讲解如何用 C# 构建一个安全、可扩展的工具调用引擎。

5.1 工具注册与发现机制

设计原则

工具系统需要满足以下要求:

  1. 统一接口:所有工具实现相同的接口,便于统一管理
  2. 动态注册:支持运行时注册和卸载工具
  3. 自动发现:通过反射或约定自动发现可用工具
  4. 类型安全:参数验证和返回值类型检查
  5. 安全隔离:工具执行在受控环境中运行

核心数据模型

csharp 复制代码
namespace AgentSystem.Tools;

/// <summary>
/// 工具参数定义,用于生成 JSON Schema
/// </summary>
public class ToolParameterDefinition
{
    /// <summary>参数名称</summary>
    public required string Name { get; init; }
    
    /// <summary>参数类型(JSON Schema 类型)</summary>
    public required string Type { get; init; }
    
    /// <summary>参数描述,供模型理解用途</summary>
    public string? Description { get; init; }
    
    /// <summary>是否必须提供</summary>
    public bool Required { get; init; }
    
    /// <summary>枚举值列表(如果类型为枚举)</summary>
    public List<string>? EnumValues { get; init; }
}

/// <summary>
/// 工具元数据,描述工具的名称、描述和参数
/// </summary>
public class ToolMetadata
{
    /// <summary>工具唯一标识符</summary>
    public required string Name { get; init; }
    
    /// <summary>工具功能描述,供模型决策是否调用</summary>
    public required string Description { get; init; }
    
    /// <summary>参数定义列表</summary>
    public List<ToolParameterDefinition> Parameters { get; init; } = [];
    
    /// <summary>工具类别(用于分组和权限管理)</summary>
    public string Category { get; init; } = "general";
    
    /// <summary>风险等级(用于安全审计)</summary>
    public RiskLevel RiskLevel { get; init; } = RiskLevel.Low;
}

/// <summary>
/// 工具执行结果
/// </summary>
public class ToolResult
{
    /// <summary>执行是否成功</summary>
    public bool Success { get; init; }
    
    /// <summary>返回数据(JSON 格式字符串或纯文本)</summary>
    public string? Data { get; init; }
    
    /// <summary>错误信息(如果失败)</summary>
    public string? Error { get; init; }
    
    /// <summary>执行耗时(毫秒)</summary>
    public long DurationMs { get; set; }
    
    public static ToolResult Ok(string data, long durationMs = 0) => 
        new() { Success = true, Data = data, DurationMs = durationMs };
    
    public static ToolResult Fail(string error, long durationMs = 0) => 
        new() { Success = false, Error = error, DurationMs = durationMs };
}

/// <summary>
/// 风险等级枚举
/// </summary>
public enum RiskLevel
{
    Low,      // 只读操作,如查询
    Medium,   // 可逆修改,如创建临时文件
    High      // 不可逆操作,如删除文件、执行代码
}

5.2 C# 核心接口与实现

ITool 接口

csharp 复制代码
namespace AgentSystem.Tools.Abstractions;

/// <summary>
/// 工具接口 - 所有可调用工具必须实现此接口
/// </summary>
public interface ITool
{
    /// <summary>获取工具元数据(名称、描述、参数定义)</summary>
    ToolMetadata GetMetadata();
    
    /// <summary>
    /// 执行工具
    /// </summary>
    /// <param name="parameters">JSON 格式的参数对象</param>
    /// <param name="context">执行上下文(包含用户信息、权限等)</param>
    /// <param name="cancellationToken">取消令牌</param>
    /// <returns>工具执行结果</returns>
    Task<ToolResult> ExecuteAsync(
        JsonElement parameters, 
        ToolExecutionContext context,
        CancellationToken cancellationToken = default);
}

/// <summary>
/// 工具执行上下文,包含安全相关信息
/// </summary>
public class ToolExecutionContext
{
    /// <summary>会话 ID,用于追踪</summary>
    public string SessionId { get; init; } = Guid.NewGuid().ToString();
    
    /// <summary>用户标识</summary>
    public string? UserId { get; init; }
    
    /// <summary>请求来源</summary>
    public string? Source { get; init; }
    
    /// <summary>最大执行时间(毫秒)</summary>
    public int TimeoutMs { get; init; } = 30000;
    
    /// <summary>允许的工具类别(为空则允许所有)</summary>
    public HashSet<string>? AllowedCategories { get; init; }
    
    /// <summary>最大风险等级(不允许执行超过此等级的工具)</summary>
    public RiskLevel MaxRiskLevel { get; init; } = RiskLevel.Medium;
}

ToolRegistry 工具注册表

csharp 复制代码
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;

namespace AgentSystem.Tools;

/// <summary>
/// 工具注册表 - 管理所有可用工具的注册、发现和查询
/// </summary>
public class ToolRegistry
{
    private readonly ConcurrentDictionary<string, ITool> _tools = new();
    private readonly ILogger<ToolRegistry> _logger;

    public ToolRegistry(ILogger<ToolRegistry> logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// 注册单个工具
    /// </summary>
    public bool Register(ITool tool)
    {
        var metadata = tool.GetMetadata();
        var name = metadata.Name;
        
        if (_tools.TryAdd(name, tool))
        {
            _logger.LogInformation("已注册工具: {ToolName}, 类别: {Category}, 风险等级: {RiskLevel}",
                name, metadata.Category, metadata.RiskLevel);
            return true;
        }
        
        _logger.LogWarning("工具注册失败,名称已存在: {ToolName}", name);
        return false;
    }

    /// <summary>
    /// 批量注册工具
    /// </summary>
    public void RegisterAll(IEnumerable<ITool> tools)
    {
        foreach (var tool in tools)
        {
            Register(tool);
        }
    }

    /// <summary>
    /// 注销工具
    /// </summary>
    public bool Unregister(string name)
    {
        if (_tools.TryRemove(name, out _))
        {
            _logger.LogInformation("已注销工具: {ToolName}", name);
            return true;
        }
        return false;
    }

    /// <summary>
    /// 获取工具
    /// </summary>
    public ITool? Get(string name) => _tools.TryGetValue(name, out var tool) ? tool : null;

    /// <summary>
    /// 获取所有已注册工具
    /// </summary>
    public IReadOnlyDictionary<string, ITool> GetAll() => _tools;

    /// <summary>
    /// 按类别获取工具
    /// </summary>
    public IEnumerable<ITool> GetByCategory(string category) =>
        _tools.Values.Where(t => t.GetMetadata().Category == category);

    /// <summary>
    /// 获取所有工具的元数据(用于生成 Function Calling schema)
    /// </summary>
    public List<ToolMetadata> GetAllMetadata() =>
        _tools.Values.Select(t => t.GetMetadata()).ToList();
}

ToolExecutor 工具执行器

csharp 复制代码
using System.Diagnostics;
using System.Text.Json;
using Microsoft.Extensions.Logging;

namespace AgentSystem.Tools;

/// <summary>
/// 工具执行器 - 负责工具调用的安全验证、执行和监控
/// </summary>
public class ToolExecutor
{
    private readonly ToolRegistry _registry;
    private readonly ILogger<ToolExecutor> _logger;
    private readonly IToolSandbox? _sandbox;

    public ToolExecutor(
        ToolRegistry registry, 
        ILogger<ToolExecutor> logger,
        IToolSandbox? sandbox = null)
    {
        _registry = registry;
        _logger = logger;
        _sandbox = sandbox;
    }

    /// <summary>
    /// 执行工具调用
    /// </summary>
    public async Task<ToolResult> ExecuteAsync(
        string toolName,
        JsonElement parameters,
        ToolExecutionContext context,
        CancellationToken cancellationToken = default)
    {
        var stopwatch = Stopwatch.StartNew();
        
        // 1. 查找工具
        var tool = _registry.Get(toolName);
        if (tool is null)
        {
            _logger.LogWarning("工具未找到: {ToolName}", toolName);
            return ToolResult.Fail($"工具 '{toolName}' 未注册", stopwatch.ElapsedMilliseconds);
        }

        var metadata = tool.GetMetadata();

        // 2. 权限检查
        if (!CheckPermission(metadata, context))
        {
            _logger.LogWarning("权限拒绝: 工具 {ToolName}, 用户 {UserId}, 原因: {Reason}",
                toolName, context.UserId ?? "anonymous", "权限不足");
            return ToolResult.Fail("权限不足,无法执行此工具", stopwatch.ElapsedMilliseconds);
        }

        // 3. 参数验证
        var validationResult = ValidateParameters(metadata, parameters);
        if (!validationResult.IsValid)
        {
            _logger.LogWarning("参数验证失败: {Errors}", validationResult.ErrorMessage);
            return ToolResult.Fail($"参数验证失败: {validationResult.ErrorMessage}", stopwatch.ElapsedMilliseconds);
        }

        // 4. 在沙箱中执行(如果配置了沙箱)
        try
        {
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            cts.CancelAfter(TimeSpan.FromMilliseconds(context.TimeoutMs));

            ToolResult result;
            
            if (_sandbox is not null)
            {
                result = await _sandbox.ExecuteInSandboxAsync(
                    tool, parameters, context, cts.Token);
            }
            else
            {
                result = await tool.ExecuteAsync(parameters, context, cts.Token);
            }

            stopwatch.Stop();
            
            _logger.LogInformation(
                "工具执行完成: {ToolName}, 成功: {Success}, 耗时: {Duration}ms",
                toolName, result.Success, stopwatch.ElapsedMilliseconds);

            // 注意:ToolResult 是 class,不使用 with 表达式(仅 record 支持)
            result.DurationMs = stopwatch.ElapsedMilliseconds;
            return result;
        }
        catch (OperationCanceledException)
        {
            _logger.LogWarning("工具执行超时: {ToolName}", toolName);
            return ToolResult.Fail($"工具执行超时({context.TimeoutMs}ms)", stopwatch.ElapsedMilliseconds);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "工具执行异常: {ToolName}", toolName);
            return ToolResult.Fail($"执行异常: {ex.Message}", stopwatch.ElapsedMilliseconds);
        }
    }

    /// <summary>
    /// 检查执行权限
    /// </summary>
    private bool CheckPermission(ToolMetadata metadata, ToolExecutionContext context)
    {
        // 检查类别限制
        if (context.AllowedCategories is not null &&
            !context.AllowedCategories.Contains(metadata.Category))
        {
            return false;
        }

        // 检查风险等级
        if ((int)metadata.RiskLevel > (int)context.MaxRiskLevel)
        {
            return false;
        }

        return true;
    }

    /// <summary>
    /// 验证参数
    /// </summary>
    private (bool IsValid, string? ErrorMessage) ValidateParameters(
        ToolMetadata metadata, JsonElement parameters)
    {
        // 检查必需参数
        foreach (var param in metadata.Parameters.Where(p => p.Required))
        {
            if (parameters.ValueKind == JsonValueKind.Object &&
                !parameters.TryGetProperty(param.Name, out _))
            {
                return (false, $"缺少必需参数: {param.Name}");
            }
        }

        // 类型检查(简化版,实际应使用 JSON Schema 验证库)
        // 可集成 JsonSchema.Net 或 System.Text.Json.Schema
        
        return (true, null);
    }
}

/// <summary>
/// 工具沙箱接口 - 用于隔离执行高风险工具
/// </summary>
public interface IToolSandbox
{
    Task<ToolResult> ExecuteInSandboxAsync(
        ITool tool,
        JsonElement parameters,
        ToolExecutionContext context,
        CancellationToken cancellationToken);
}

5.3 OpenAI Function Calling 对接

OpenAI 的 Function Calling 允许模型在对话中调用预定义的函数。以下是完整对接代码:

生成 Function Calling Schema

csharp 复制代码
using System.Text.Json;
using System.Text.Json.Nodes;

namespace AgentSystem.Tools.OpenAI;

/// <summary>
/// 将工具元数据转换为 OpenAI Function Calling 格式
/// </summary>
public static class OpenAIFunctionSchemaGenerator
{
    /// <summary>
    /// 生成 OpenAI 函数定义
    /// </summary>
    public static object GenerateFunctionDefinition(ToolMetadata metadata)
    {
        return new
        {
            type = "function",
            function = new
            {
                name = metadata.Name,
                description = metadata.Description,
                parameters = GenerateParametersSchema(metadata.Parameters)
            }
        };
    }

    /// <summary>
    /// 生成所有工具的 Function 定义
    /// </summary>
    public static List<object> GenerateAllFunctions(ToolRegistry registry)
    {
        return registry.GetAllMetadata()
            .Select(GenerateFunctionDefinition)
            .ToList();
    }

    private static object GenerateParametersSchema(List<ToolParameterDefinition> parameters)
    {
        var properties = new Dictionary<string, object>();
        var required = new List<string>();

        foreach (var param in parameters)
        {
            var propSchema = new Dictionary<string, object>
            {
                ["type"] = param.Type,
            };

            if (!string.IsNullOrEmpty(param.Description))
            {
                propSchema["description"] = param.Description;
            }

            if (param.EnumValues is not null && param.EnumValues.Count > 0)
            {
                propSchema["enum"] = param.EnumValues;
            }

            properties[param.Name] = propSchema;

            if (param.Required)
            {
                required.Add(param.Name);
            }
        }

        return new
        {
            type = "object",
            properties = properties,
            required = required
        };
    }
}

处理 Function Call 响应

csharp 复制代码
using System.Text.Json;
using OpenAI.Chat;

namespace AgentSystem.Tools.OpenAI;

/// <summary>
/// 处理 OpenAI 返回的函数调用
/// </summary>
public class OpenAIFunctionHandler
{
    private readonly ToolExecutor _executor;
    private readonly ILogger<OpenAIFunctionHandler> _logger;

    public OpenAIFunctionHandler(
        ToolExecutor executor,
        ILogger<OpenAIFunctionHandler> logger)
    {
        _executor = executor;
        _logger = logger;
    }

    /// <summary>
    /// 处理 Chat Completion 中的工具调用
    /// </summary>
    public async Task<List<ChatToolCallResult>> HandleToolCallsAsync(
        IReadOnlyList<ChatToolCall> toolCalls,
        ToolExecutionContext context,
        CancellationToken cancellationToken = default)
    {
        var results = new List<ChatToolCallResult>();

        foreach (var toolCall in toolCalls)
        {
            _logger.LogInformation("处理函数调用: {FunctionName}", toolCall.FunctionName);

            // 解析参数
            var parameters = JsonDocument.Parse(toolCall.FunctionArguments).RootElement;

            // 执行工具
            var result = await _executor.ExecuteAsync(
                toolCall.FunctionName,
                parameters,
                context,
                cancellationToken);

            // 包装结果
            results.Add(new ChatToolCallResult
            {
                ToolCallId = toolCall.Id,
                FunctionName = toolCall.FunctionName,
                Result = result
            });

            _logger.LogDebug(
                "函数调用完成: {FunctionName}, 成功: {Success}",
                toolCall.FunctionName, result.Success);
        }

        return results;
    }

    /// <summary>
    /// 将工具结果转换为 Chat 消息
    /// </summary>
    public ChatMessage CreateToolResultMessage(ChatToolCallResult callResult)
    {
        // 使用 OpenAI SDK 的 ToolChatMessage
        var content = callResult.Result.Success
            ? callResult.Result.Data ?? "{}"
            : JsonSerializer.Serialize(new { error = callResult.Result.Error });

        return new ToolChatMessage(
            callResult.ToolCallId,
            content);
    }
}

/// <summary>
/// 工具调用结果包装
/// </summary>
public class ChatToolCallResult
{
    public string ToolCallId { get; init; } = string.Empty;
    public string FunctionName { get; init; } = string.Empty;
    public ToolResult Result { get; init; } = null!;
}

5.4 MCP 协议对接

MCP(Model Context Protocol)是 Anthropic 提出的工具调用协议,支持更丰富的交互模式:

csharp 复制代码
using System.Text.Json;
using System.Text.Json.Serialization;

namespace AgentSystem.Tools.MCP;

/// <summary>
/// MCP 工具定义(简化版)
/// </summary>
public class MCPToolDefinition
{
    [JsonPropertyName("name")]
    public string Name { get; set; } = string.Empty;

    [JsonPropertyName("description")]
    public string Description { get; set; } = string.Empty;

    [JsonPropertyName("inputSchema")]
    public JsonElement InputSchema { get; set; }
}

/// <summary>
/// MCP 协议适配器 - 将 ITool 转换为 MCP 格式
/// </summary>
public class MCPToolAdapter
{
    private readonly ToolRegistry _registry;
    private readonly ToolExecutor _executor;

    public MCPToolAdapter(ToolRegistry registry, ToolExecutor executor)
    {
        _registry = registry;
        _executor = executor;
    }

    /// <summary>
    /// 列出所有工具(MCP tools/list 响应)
    /// </summary>
    public List<MCPToolDefinition> ListTools()
    {
        return _registry.GetAllMetadata().Select(m => new MCPToolDefinition
        {
            Name = m.Name,
            Description = m.Description,
            InputSchema = GenerateInputSchema(m.Parameters)
        }).ToList();
    }

    /// <summary>
    /// 执行工具调用(MCP tools/call 处理)
    /// </summary>
    public async Task<ToolResult> CallToolAsync(
        string name,
        JsonElement arguments,
        ToolExecutionContext context,
        CancellationToken cancellationToken = default)
    {
        return await _executor.ExecuteAsync(name, arguments, context, cancellationToken);
    }

    private JsonElement GenerateInputSchema(List<ToolParameterDefinition> parameters)
    {
        var schema = new
        {
            type = "object",
            properties = parameters.ToDictionary(
                p => p.Name,
                p => new
                {
                    type = p.Type,
                    description = p.Description,
                    @enum = p.EnumValues
                }),
            required = parameters.Where(p => p.Required).Select(p => p.Name).ToList()
        };

        return JsonDocument.Parse(JsonSerializer.Serialize(schema)).RootElement;
    }
}

5.5 实用工具示例

5.5.1 搜索引擎工具

csharp 复制代码
using System.Text.Json;
using AgentSystem.Tools.Abstractions;

namespace AgentSystem.Tools.BuiltIn;

/// <summary>
/// 网络搜索工具 - 使用搜索引擎搜索信息
/// </summary>
public class WebSearchTool : ITool
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;

    public WebSearchTool(HttpClient httpClient, string apiKey)
    {
        _httpClient = httpClient;
        _apiKey = apiKey;
    }

    public ToolMetadata GetMetadata() => new()
    {
        Name = "web_search",
        Description = "在互联网上搜索信息,返回相关结果摘要。适用于查询时事、知识点、技术文档等。",
        Category = "search",
        RiskLevel = RiskLevel.Low,
        Parameters =
        [
            new ToolParameterDefinition
            {
                Name = "query",
                Type = "string",
                Description = "搜索查询关键词",
                Required = true
            },
            new ToolParameterDefinition
            {
                Name = "max_results",
                Type = "integer",
                Description = "返回结果数量,默认 5",
                Required = false
            }
        ]
    };

    public async Task<ToolResult> ExecuteAsync(
        JsonElement parameters,
        ToolExecutionContext context,
        CancellationToken cancellationToken = default)
    {
        // 提取参数
        var query = parameters.GetProperty("query").GetString() 
            ?? throw new ArgumentException("查询关键词不能为空");
        
        var maxResults = parameters.TryGetProperty("max_results", out var maxProp)
            ? maxProp.GetInt32()
            : 5;

        // 构建搜索请求(以 DuckDuckGo API 为例)
        var requestUrl = $"https://api.duckduckgo.com/?q={Uri.EscapeDataString(query)}&format=json&max_results={maxResults}";

        try
        {
            var response = await _httpClient.GetAsync(requestUrl, cancellationToken);
            response.EnsureSuccessStatusCode();
            
            var content = await response.Content.ReadAsStringAsync(cancellationToken);
            
            // 解析并格式化结果(实际应解析 JSON 并提取关键字段)
            return ToolResult.Ok(content);
        }
        catch (Exception ex)
        {
            return ToolResult.Fail($"搜索失败: {ex.Message}");
        }
    }
}

5.5.2 代码执行工具(沙箱模式)

csharp 复制代码
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using AgentSystem.Tools.Abstractions;

namespace AgentSystem.Tools.BuiltIn;

/// <summary>
/// 代码执行工具 - 在隔离环境中执行代码片段
/// 注意:这是高风险工具,必须在沙箱中运行
/// </summary>
public class CodeExecutionTool : ITool
{
    private readonly string _sandboxPath;
    private readonly TimeSpan _executionTimeout;
    
    /// <summary>
    /// Process 的异步等待扩展方法(与 DockerSandbox 共用逻辑)
    /// </summary>
    private static async Task<bool> WaitForExitAsync(Process process, CancellationToken ct)
    {
        var tcs = new TaskCompletionSource<bool>();
        process.EnableRaisingEvents = true;
        process.Exited += (s, e) => tcs.TrySetResult(true);
        if (process.HasExited)
            tcs.TrySetResult(true);
        using var registration = ct.Register(() => tcs.TrySetCanceled());
        return await tcs.Task;
    }

    public CodeExecutionTool(string sandboxPath, int timeoutSeconds = 30)
    {
        _sandboxPath = sandboxPath;
        _executionTimeout = TimeSpan.FromSeconds(timeoutSeconds);
    }

    public ToolMetadata GetMetadata() => new()
    {
        Name = "execute_code",
        Description = "在隔离环境中执行代码片段。支持 C#、Python、JavaScript。返回执行结果或错误信息。",
        Category = "code",
        RiskLevel = RiskLevel.High,  // 高风险!
        Parameters =
        [
            new ToolParameterDefinition
            {
                Name = "language",
                Type = "string",
                Description = "编程语言:csharp、python、javascript",
                Required = true,
                EnumValues = ["csharp", "python", "javascript"]
            },
            new ToolParameterDefinition
            {
                Name = "code",
                Type = "string",
                Description = "要执行的代码片段",
                Required = true
            }
        ]
    };

    public async Task<ToolResult> ExecuteAsync(
        JsonElement parameters,
        ToolExecutionContext context,
        CancellationToken cancellationToken = default)
    {
        var language = parameters.GetProperty("language").GetString()!;
        var code = parameters.GetProperty("code").GetString()!;

        // 安全检查:禁止危险操作
        if (ContainsDangerousOperations(code))
        {
            return ToolResult.Fail("代码包含危险操作,被安全策略阻止");
        }

        try
        {
            var result = language.ToLower() switch
            {
                "python" => await ExecutePythonAsync(code, cancellationToken),
                "javascript" => await ExecuteJavaScriptAsync(code, cancellationToken),
                "csharp" => await ExecuteCSharpAsync(code, cancellationToken),
                _ => ToolResult.Fail($"不支持的语言: {language}")
            };

            return result;
        }
        catch (Exception ex)
        {
            return ToolResult.Fail($"执行异常: {ex.Message}");
        }
    }

    private async Task<ToolResult> ExecutePythonAsync(string code, CancellationToken ct)
    {
        // 写入临时文件
        var scriptPath = Path.Combine(_sandboxPath, $"script_{Guid.NewGuid():N}.py");
        await File.WriteAllTextAsync(scriptPath, code, ct);

        // 使用 docker 或受限用户执行
        using var process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "python3",
                Arguments = scriptPath,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false
            }
        };

        process.Start();
        
        var output = await process.StandardOutput.ReadToEndAsync(ct);
        var error = await process.StandardError.ReadToEndAsync(ct);
        
        var completed = await WaitForExitAsync(process, ct);
        
        // 清理临时文件
        File.Delete(scriptPath);

        if (!completed)
        {
            process.Kill();
            return ToolResult.Fail("执行超时");
        }

        return process.ExitCode == 0
            ? ToolResult.Ok(output)
            : ToolResult.Fail(error);
    }

    private Task<ToolResult> ExecuteJavaScriptAsync(string code, CancellationToken ct)
    {
        // 类似实现,使用 Node.js 或 Deno
        return ExecutePythonAsync(code, ct); // 简化示例
    }

    private Task<ToolResult> ExecuteCSharpAsync(string code, CancellationToken ct)
    {
        // 使用 Roslyn 或 dotnet-script
        return ExecutePythonAsync(code, ct); // 简化示例
    }

    private static bool ContainsDangerousOperations(string code)
    {
        // 检查危险关键词
        var dangerousPatterns = new[]
        {
            "System.IO.File.Delete",
            "System.IO.Directory.Delete",
            "Process.Start",
            "Environment.Exit",
            "Assembly.Load",
            "Marshal",
            "unsafe",
            "DllImport"
        };

        return dangerousPatterns.Any(pattern => code.Contains(pattern));
    }
}

5.5.3 文件读写工具

csharp 复制代码
using System.Text.Json;
using AgentSystem.Tools.Abstractions;

namespace AgentSystem.Tools.BuiltIn;

/// <summary>
/// 文件读取工具 - 读取指定路径的文件内容
/// </summary>
public class FileReadTool : ITool
{
    private readonly HashSet<string> _allowedDirectories;
    private readonly long _maxFileSizeBytes;

    public FileReadTool(IEnumerable<string> allowedDirectories, long maxFileSizeBytes = 1024 * 1024)
    {
        _allowedDirectories = [.. allowedDirectories.Select(Path.GetFullPath)];
        _maxFileSizeBytes = maxFileSizeBytes;
    }

    public ToolMetadata GetMetadata() => new()
    {
        Name = "file_read",
        Description = "读取文件内容。只能访问预先配置的允许目录。",
        Category = "filesystem",
        RiskLevel = RiskLevel.Low,
        Parameters =
        [
            new ToolParameterDefinition
            {
                Name = "path",
                Type = "string",
                Description = "文件路径(绝对路径或相对路径)",
                Required = true
            },
            new ToolParameterDefinition
            {
                Name = "encoding",
                Type = "string",
                Description = "文件编码:utf-8、ascii、gbk",
                Required = false,
                EnumValues = ["utf-8", "ascii", "gbk"]
            }
        ]
    };

    public async Task<ToolResult> ExecuteAsync(
        JsonElement parameters,
        ToolExecutionContext context,
        CancellationToken cancellationToken = default)
    {
        var path = parameters.GetProperty("path").GetString()!;
        var encodingName = parameters.TryGetProperty("encoding", out var encProp)
            ? encProp.GetString() ?? "utf-8"
            : "utf-8";

        // 路径安全检查
        var fullPath = Path.GetFullPath(path);
        if (!IsPathAllowed(fullPath))
        {
            return ToolResult.Fail($"路径不在允许的目录范围内: {path}");
        }

        if (!File.Exists(fullPath))
        {
            return ToolResult.Fail($"文件不存在: {path}");
        }

        var fileInfo = new FileInfo(fullPath);
        if (fileInfo.Length > _maxFileSizeBytes)
        {
            return ToolResult.Fail($"文件过大({fileInfo.Length} 字节),最大允许 {_maxFileSizeBytes} 字节");
        }

        try
        {
            var encoding = GetEncoding(encodingName);
            var content = await File.ReadAllTextAsync(fullPath, encoding, cancellationToken);
            return ToolResult.Ok(content);
        }
        catch (Exception ex)
        {
            return ToolResult.Fail($"读取失败: {ex.Message}");
        }
    }

    private bool IsPathAllowed(string fullPath)
    {
        return _allowedDirectories.Any(allowed => 
            fullPath.StartsWith(allowed, StringComparison.OrdinalIgnoreCase));
    }

    private static Encoding GetEncoding(string name) => name.ToLower() switch
    {
        "utf-8" => Encoding.UTF8,
        "ascii" => Encoding.ASCII,
        "gbk" => Encoding.GetEncoding("GBK"),
        _ => Encoding.UTF8
    };
}

/// <summary>
/// 文件写入工具 - 在允许目录写入文件
/// </summary>
public class FileWriteTool : ITool
{
    private readonly HashSet<string> _allowedDirectories;

    public FileWriteTool(IEnumerable<string> allowedDirectories)
    {
        _allowedDirectories = [.. allowedDirectories.Select(Path.GetFullPath)];
    }

    public ToolMetadata GetMetadata() => new()
    {
        Name = "file_write",
        Description = "写入或追加文件内容。只能访问预先配置的允许目录。",
        Category = "filesystem",
        RiskLevel = RiskLevel.Medium,  // 写入操作风险更高
        Parameters =
        [
            new ToolParameterDefinition
            {
                Name = "path",
                Type = "string",
                Description = "文件路径",
                Required = true
            },
            new ToolParameterDefinition
            {
                Name = "content",
                Type = "string",
                Description = "要写入的内容",
                Required = true
            },
            new ToolParameterDefinition
            {
                Name = "mode",
                Type = "string",
                Description = "写入模式:overwrite(覆盖)或 append(追加)",
                Required = false,
                EnumValues = ["overwrite", "append"]
            }
        ]
    };

    public async Task<ToolResult> ExecuteAsync(
        JsonElement parameters,
        ToolExecutionContext context,
        CancellationToken cancellationToken = default)
    {
        var path = parameters.GetProperty("path").GetString()!;
        var content = parameters.GetProperty("content").GetString()!;
        var mode = parameters.TryGetProperty("mode", out var modeProp)
            ? modeProp.GetString() ?? "overwrite"
            : "overwrite";

        // 安全检查
        var fullPath = Path.GetFullPath(path);
        if (!IsPathAllowed(fullPath))
        {
            return ToolResult.Fail($"路径不在允许的目录范围内: {path}");
        }

        // 额外权限检查:用户是否允许写入
        if (context.MaxRiskLevel < RiskLevel.Medium)
        {
            return ToolResult.Fail("当前上下文不允许写入操作");
        }

        try
        {
            // 确保目录存在
            Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);

            if (mode == "append")
            {
                await File.AppendAllTextAsync(fullPath, content, cancellationToken);
            }
            else
            {
                await File.WriteAllTextAsync(fullPath, content, cancellationToken);
            }

            return ToolResult.Ok($"文件已写入: {fullPath}");
        }
        catch (Exception ex)
        {
            return ToolResult.Fail($"写入失败: {ex.Message}");
        }
    }

    private bool IsPathAllowed(string fullPath)
    {
        return _allowedDirectories.Any(allowed =>
            fullPath.StartsWith(allowed, StringComparison.OrdinalIgnoreCase));
    }
}

5.6 工具调用的安全沙箱

对于高风险工具(如代码执行、系统命令),需要在隔离环境中运行:

csharp 复制代码
using System.Diagnostics;
using System.Text.Json;

namespace AgentSystem.Tools.Sandbox;

/// <summary>
/// Docker 沙箱实现 - 在 Docker 容器中隔离执行工具
/// </summary>
public class DockerSandbox : IToolSandbox
{
    /// <summary>
    /// Process 的异步等待扩展方法(.NET 标准库不提供 WaitForExitAsync)
    /// </summary>
    private static async Task<bool> WaitForExitAsync(Process process, CancellationToken ct)
    {
        var tcs = new TaskCompletionSource<bool>();
        process.EnableRaisingEvents = true;
        process.Exited += (s, e) => tcs.TrySetResult(true);
        if (process.HasExited)
        {
            tcs.TrySetResult(true);
        }
        using var registration = ct.Register(() => tcs.TrySetCanceled());
        return await tcs.Task;
    }

    private readonly ILogger<DockerSandbox> _logger;
    private readonly DockerSandboxOptions _options;

    public DockerSandbox(ILogger<DockerSandbox> logger, DockerSandboxOptions options)
    {
        _logger = logger;
        _options = options;
    }

    public async Task<ToolResult> ExecuteInSandboxAsync(
        ITool tool,
        JsonElement parameters,
        ToolExecutionContext context,
        CancellationToken cancellationToken)
    {
        var metadata = tool.GetMetadata();
        
        _logger.LogInformation(
            "在 Docker 沙箱中执行工具: {ToolName}, 风险等级: {RiskLevel}",
            metadata.Name, metadata.RiskLevel);

        // 序列化参数
        var parametersJson = parameters.GetRawText();
        
        // 构建容器配置
        var containerName = $"agent-tool-{Guid.NewGuid():N}";
        var imageName = _options.ImageName ?? "agent-sandbox:latest";
        
        // 资源限制
        var resourceLimits = $"--memory={_options.MemoryLimitMb}m --cpus={_options.CpuLimit}";

        // 执行命令
        var command = $"run --rm --name {containerName} {resourceLimits} " +
                      $"-e TOOL_NAME={metadata.Name} " +
                      $"-e PARAMETERS='{parametersJson.Replace("'", "'\\''")}' " +
                      $"{imageName} dotnet AgentSandbox.dll";

        using var process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "docker",
                Arguments = command,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            }
        };

        var outputBuilder = new StringBuilder();
        var errorBuilder = new StringBuilder();
        
        process.OutputDataReceived += (_, e) => outputBuilder.AppendLine(e.Data);
        process.ErrorDataReceived += (_, e) => errorBuilder.AppendLine(e.Data);

        process.Start();
        process.BeginOutputReadLine();
        process.BeginErrorReadLine();

        var completed = await WaitForExitAsync(process, cancellationToken);

        if (!completed)
        {
            // 超时,强制停止容器
            var killProcess = Process.Start("docker", $"kill {containerName}");
            killProcess?.WaitForExit();
            await WaitForExitAsync(process, cancellationToken);
            return ToolResult.Fail("沙箱执行超时,已强制终止");
        }

        var output = outputBuilder.ToString();
        var error = errorBuilder.ToString();

        if (process.ExitCode != 0)
        {
            _logger.LogWarning("沙箱执行失败,退出码: {ExitCode}, 错误: {Error}", 
                process.ExitCode, error);
            return ToolResult.Fail($"执行失败 (ExitCode: {process.ExitCode}): {error}");
        }

        _logger.LogInformation("沙箱执行成功: {ToolName}", metadata.Name);
        return ToolResult.Ok(output);
    }
}

/// <summary>
/// Docker 沙箱配置选项
/// </summary>
public class DockerSandboxOptions
{
    /// <summary>Docker 镜像名称</summary>
    public string ImageName { get; set; } = "agent-sandbox:latest";
    
    /// <summary>内存限制(MB)</summary>
    public int MemoryLimitMb { get; set; } = 512;
    
    /// <summary>CPU 核心数限制</summary>
    public double CpuLimit { get; set; } = 1.0;
    
    /// <summary>网络访问(默认禁用)</summary>
    public bool AllowNetwork { get; set; } = false;
    
    /// <summary>最大执行时间(秒)</summary>
    public int TimeoutSeconds { get; set; } = 30;
}

沙箱使用示例

csharp 复制代码
// 在依赖注入中配置
services.AddSingleton<IToolSandbox>(sp => 
    new DockerSandbox(
        sp.GetRequiredService<ILogger<DockerSandbox>>(),
        new DockerSandboxOptions
        {
            ImageName = "my-agent-sandbox:latest",
            MemoryLimitMb = 256,
            CpuLimit = 0.5,
            TimeoutSeconds = 20
        }));

小结

本章构建了完整的工具调用引擎:

  1. ITool 接口:定义统一的工具抽象
  2. ToolRegistry:管理工具的注册与发现
  3. ToolExecutor:处理权限验证、参数校验和执行
  4. OpenAI Function Calling:对接 GPT 模型的函数调用
  5. MCP 协议:支持 Anthropic 的工具协议
  6. 实用工具示例:搜索、代码执行、文件读写
  7. 安全沙箱:Docker 隔离执行高风险工具

第六章 · 系统集成与实战 --- 三大组件合体

6.0 终于到了这一步!

前面三章我们分别搞定了状态机、规划器和工具调用引擎。现在,是时候把它们串起来了。这一章,我会从零搭建一个完整的 ASP.NET Core Web API,把 Agent 跑起来。

6.0.0 NuGet 包清单

先装好这些包:

bash 复制代码
# 核心框架(.NET 8 SDK 自带,无需额外安装)
# ASP.NET Core Web API 模板:
dotnet new webapi -n CSharpAgent

# OpenAI 官方 .NET SDK(v2.0+,基于 Microsoft.Extensions.AI 抽象层)
dotnet add package OpenAI --version 2.0.0

# Polly 弹性库(重试、熔断、超时)
dotnet add package Polly --version 8.3.1
dotnet add package Polly.Extensions --version 8.3.1

# Prometheus 指标暴露
dotnet add package prometheus-net.AspNetCore --version 8.2.1

# OpenTelemetry 分布式追踪
dotnet add package OpenTelemetry.Extensions.Hosting --version 1.8.1
dotnet add package OpenTelemetry.Instrumentation.AspNetCore --version 1.8.1
dotnet add package OpenTelemetry.Instrumentation.Http --version 1.8.1

# 健康检查(.NET 8 SDK 自带 Microsoft.Extensions.Diagnostics.HealthChecks)
# 无需额外安装

6.0 补充:缺失的核心组件定义

在前三章中,我们引用了一些核心组件但未给出完整实现。本节补充这些定义,确保系统可编译运行。

6.0.1 MemoryService(记忆服务)

csharp 复制代码
namespace AgentSystem.Memory;

/// <summary>
/// 对话记忆条目
/// </summary>
public class MemoryEntry
{
    public string UserMessage { get; set; } = string.Empty;
    public string AssistantResponse { get; set; } = string.Empty;
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;
    public double[]? Embedding { get; set; }
}

/// <summary>
/// 记忆服务接口
/// </summary>
public interface IMemoryService
{
    /// <summary>
    /// 记忆服务是否已初始化
    /// </summary>
    bool IsInitialized { get; }
    
    Task StoreAsync(string sessionId, MemoryEntry entry, CancellationToken ct = default);
    Task<IReadOnlyList<MemoryEntry>> SearchAsync(string sessionId, string query, int topK = 5, CancellationToken ct = default);
    Task<IReadOnlyList<MemoryEntry>> GetRecentAsync(string sessionId, int count = 10, CancellationToken ct = default);
}

/// <summary>
/// 内存版记忆服务实现(生产环境应替换为数据库或向量存储)
/// </summary>
public class MemoryService : IMemoryService
{
    /// <summary>
    /// 记忆服务是否已初始化(内存版始终为 true)
    /// </summary>
    public bool IsInitialized => true;
    
    private readonly ConcurrentDictionary<string, ConcurrentBag<MemoryEntry>> _store = new();

    public Task StoreAsync(string sessionId, MemoryEntry entry, CancellationToken ct = default)
    {
        var bag = _store.GetOrAdd(sessionId, _ => new ConcurrentBag<MemoryEntry>());
        bag.Add(entry);
        return Task.CompletedTask;
    }

    public Task<IReadOnlyList<MemoryEntry>> SearchAsync(string sessionId, string query, int topK = 5, CancellationToken ct = default)
    {
        if (_store.TryGetValue(sessionId, out var bag))
        {
            return Task.FromResult<IReadOnlyList<MemoryEntry>>(bag.Take(topK).ToList());
        }
        return Task.FromResult<IReadOnlyList<MemoryEntry>>(Array.Empty<MemoryEntry>());
    }

    public Task<IReadOnlyList<MemoryEntry>> GetRecentAsync(string sessionId, int count = 10, CancellationToken ct = default)
    {
        if (_store.TryGetValue(sessionId, out var bag))
        {
            return Task.FromResult<IReadOnlyList<MemoryEntry>>(bag.OrderByDescending(e => e.Timestamp).Take(count).ToList());
        }
        return Task.FromResult<IReadOnlyList<MemoryEntry>>(Array.Empty<MemoryEntry>());
    }
}

6.0.2 IVectorStore(向量存储接口)

csharp 复制代码
namespace AgentSystem.Memory;

/// <summary>
/// 向量存储接口(用于语义搜索)
/// </summary>
public interface IVectorStore
{
    Task UpsertAsync(string collection, string id, double[] vector, IDictionary<string, object> metadata, CancellationToken ct = default);
    Task<IReadOnlyList<(string Id, double Score, IDictionary<string, object> Metadata)>> QueryAsync(string collection, double[] vector, int topK = 5, CancellationToken ct = default);
}

/// <summary>
/// 内存版向量存储实现(开发环境使用)
/// </summary>
public class InMemoryVectorStore : IVectorStore
{
    private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, (double[] Vector, IDictionary<string, object> Metadata)>> _collections = new();

    public Task UpsertAsync(string collection, string id, double[] vector, IDictionary<string, object> metadata, CancellationToken ct = default)
    {
        var col = _collections.GetOrAdd(collection, _ => new ConcurrentDictionary<string, (double[], IDictionary<string, object>)>());
        col[id] = (vector, metadata);
        return Task.CompletedTask;
    }

    public Task<IReadOnlyList<(string Id, double Score, IDictionary<string, object> Metadata)>> QueryAsync(string collection, double[] vector, int topK = 5, CancellationToken ct = default)
    {
        if (!_collections.TryGetValue(collection, out var col))
        {
            return Task.FromResult<IReadOnlyList<(string, double, IDictionary<string, object>)>>(Array.Empty<(string, double, IDictionary<string, object>)>());
        }

        var results = col
            .Select(kvp => (kvp.Key, Score: CosineSimilarity(vector, kvp.Value.Vector), kvp.Value.Metadata))
            .OrderByDescending(x => x.Score)
            .Take(topK)
            .ToList();

        return Task.FromResult<IReadOnlyList<(string, double, IDictionary<string, object>)>>(results);
    }

    private static double CosineSimilarity(double[] a, double[] b)
    {
        if (a.Length != b.Length) return 0;
        double dot = 0, normA = 0, normB = 0;
        for (int i = 0; i < a.Length; i++)
        {
            dot += a[i] * b[i];
            normA += a[i] * a[i];
            normB += b[i] * b[i];
        }
        return normA == 0 || normB == 0 ? 0 : dot / (Math.Sqrt(normA) * Math.Sqrt(normB));
    }
}

6.1 运行流程

📌 命名空间建议

各章节用了不同的命名空间来展示独立模块。实际项目建议统一为 CSharpAgent.CoreCSharpAgent.StatesCSharpAgent.PlanningCSharpAgent.Tools 等。

复制代码
用户请求 → API Controller → AgentService
    ↓
对话引擎(处理用户输入)
    ↓
记忆系统(检索相关上下文)
    ↓
LLM 推理(决定是否调用工具)
    ↓
工具执行器(如需要)
    ↓
响应生成 → 返回用户

核心服务:AgentService

csharp 复制代码
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenAI.Chat;

namespace AgentSystem.Core;

/// <summary>
/// Agent 核心服务 - 编排对话、记忆和工具调用
/// </summary>
public class AgentService
{
    private readonly ChatClient _chatClient;
    private readonly MemoryService _memoryService;
    private readonly ToolExecutor _toolExecutor;
    private readonly ToolRegistry _toolRegistry;
    private readonly ILogger<AgentService> _logger;
    private readonly AgentOptions _options;

    public AgentService(
        ChatClient chatClient,
        MemoryService memoryService,
        ToolExecutor toolExecutor,
        ToolRegistry toolRegistry,
        IOptions<AgentOptions> options,
        ILogger<AgentService> logger)
    {
        _chatClient = chatClient;
        _memoryService = memoryService;
        _toolExecutor = toolExecutor;
        _toolRegistry = toolRegistry;
        _options = options.Value;
        _logger = logger;
    }

    /// <summary>
    /// 处理用户消息,返回 Agent 响应
    /// </summary>
    public async Task<AgentResponse> ProcessAsync(
        string userId,
        string userMessage,
        CancellationToken cancellationToken = default)
    {
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        var sessionId = Guid.NewGuid().ToString("N");

        _logger.LogInformation("[{SessionId}] 开始处理用户消息: {Message}", 
            sessionId, userMessage[..Math.Min(100, userMessage.Length)]);

        try
        {
            // 1. 检索相关记忆
            var relevantMemories = await _memoryService.SearchAsync(
                userId, userMessage, _options.MaxMemoryResults, cancellationToken);
            
            _logger.LogDebug("[{SessionId}] 检索到 {Count} 条相关记忆", 
                sessionId, relevantMemories.Count);

            // 2. 构建消息历史
            var messages = BuildMessages(userId, userMessage, relevantMemories);

            // 3. 获取可用工具定义
            var tools = OpenAIFunctionSchemaGenerator.GenerateAllFunctions(_toolRegistry);

            // 4. 调用 LLM
            var chatOptions = new ChatOptions
            {
                Tools = tools,
                ToolChoice = ChatToolChoice.Auto,
                MaxOutputTokenCount = _options.MaxOutputTokens,
                Temperature = _options.Temperature
            };

            ChatCompletion completion;
            var iterations = 0;
            var maxIterations = _options.MaxToolIterations;
            var toolCallsMade = new List<ToolCallRecord>();

            do
            {
                iterations++;
                _logger.LogDebug("[{SessionId}] LLM 调用迭代 {Iteration}", sessionId, iterations);

                completion = await _chatClient.CompleteChatAsync(messages, chatOptions, cancellationToken);

                // 检查是否需要工具调用
                if (completion.ToolCalls.Count > 0)
                {
                    _logger.LogInformation("[{SessionId}] LLM 请求调用 {Count} 个工具", 
                        sessionId, completion.ToolCalls.Count);

                    // 将助手消息添加到历史
                    messages.Add(new AssistantChatMessage(completion));

                    // 执行所有工具调用
                    var context = new ToolExecutionContext
                    {
                        SessionId = sessionId,
                        UserId = userId,
                        MaxRiskLevel = _options.MaxRiskLevel,
                        TimeoutMs = _options.ToolTimeoutMs
                    };

                    foreach (var toolCall in completion.ToolCalls)
                    {
                        var parameters = JsonDocument.Parse(toolCall.FunctionArguments).RootElement;
                        
                        var result = await _toolExecutor.ExecuteAsync(
                            toolCall.FunctionName,
                            parameters,
                            context,
                            cancellationToken);

                        // 记录工具调用
                        toolCallsMade.Add(new ToolCallRecord
                        {
                            ToolName = toolCall.FunctionName,
                            Parameters = toolCall.FunctionArguments,
                            Result = result.Data ?? result.Error,
                            Success = result.Success,
                            DurationMs = result.DurationMs
                        });

                        // 将工具结果添加到消息历史
                        messages.Add(new ToolChatMessage(
                            toolCall.Id,
                            result.Success ? result.Data ?? "{}" : JsonSerializer.Serialize(new { error = result.Error })));
                    }
                }
                else
                {
                    // 没有工具调用,返回最终响应
                    break;
                }
            } while (iterations < maxIterations);

            stopwatch.Stop();

            // 5. 提取最终响应
            var finalResponse = completion.Content[0].Text;

            // 6. 存储对话到记忆
            await _memoryService.StoreAsync(userId, new MemoryEntry
            {
                UserMessage = userMessage,
                AssistantResponse = finalResponse,
                Timestamp = DateTime.UtcNow
            }, cancellationToken);

            _logger.LogInformation("[{SessionId}] 处理完成,耗时 {Duration}ms,工具调用 {ToolCount} 次",
                sessionId, stopwatch.ElapsedMilliseconds, toolCallsMade.Count);

            return new AgentResponse
            {
                SessionId = sessionId,
                Response = finalResponse,
                ToolCalls = toolCallsMade,
                DurationMs = stopwatch.ElapsedMilliseconds,
                Iterations = iterations
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[{SessionId}] 处理失败", sessionId);
            throw new AgentException($"处理请求失败: {ex.Message}", ex);
        }
    }

    /// <summary>
    /// 构建发送给 LLM 的消息列表
    /// </summary>
    private List<ChatMessage> BuildMessages(
        string userId, 
        string userMessage, 
        List<MemoryEntry> memories)
    {
        var messages = new List<ChatMessage>();

        // 系统提示
        var systemPrompt = BuildSystemPrompt(userId, memories);
        messages.Add(new SystemChatMessage(systemPrompt));

        // 用户消息
        messages.Add(new UserChatMessage(userMessage));

        return messages;
    }

    /// <summary>
    /// 构建系统提示(包含记忆上下文)
    /// </summary>
    private string BuildSystemPrompt(string userId, List<MemoryEntry> memories)
    {
        var prompt = _options.SystemPrompt;

        if (memories.Count > 0)
        {
            prompt += "\n\n## 相关历史对话\n";
            foreach (var memory in memories)
            {
                prompt += $"\n- 用户: {memory.UserMessage}\n";
                prompt += $"  助手: {memory.AssistantResponse[..Math.Min(200, memory.AssistantResponse.Length)]}...\n";
            }
        }

        prompt += $"\n\n当前时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
        prompt += $"\n用户 ID: {userId}";

        return prompt;
    }
}

/// <summary>
/// Agent 配置选项
/// </summary>
public class AgentOptions
{
    /// <summary>系统提示词</summary>
    public string SystemPrompt { get; set; } = "你是一个有用的 AI 助手。";

    /// <summary>最大记忆检索数量</summary>
    public int MaxMemoryResults { get; set; } = 5;

    /// <summary>最大输出 Token 数</summary>
    public int MaxOutputTokens { get; set; } = 2000;

    /// <summary>生成温度</summary>
    public float Temperature { get; set; } = 0.7f;

    /// <summary>最大工具调用迭代次数</summary>
    public int MaxToolIterations { get; set; } = 5;

    /// <summary>工具调用超时(毫秒)</summary>
    public int ToolTimeoutMs { get; set; } = 30000;

    /// <summary>允许的最大风险等级</summary>
    public RiskLevel MaxRiskLevel { get; set; } = RiskLevel.Medium;
}

/// <summary>
/// Agent 响应
/// </summary>
public class AgentResponse
{
    /// <summary>会话 ID</summary>
    public string SessionId { get; init; } = string.Empty;

    /// <summary>响应文本</summary>
    public string Response { get; init; } = string.Empty;

    /// <summary>工具调用记录</summary>
    public List<ToolCallRecord> ToolCalls { get; init; } = [];

    /// <summary>总耗时(毫秒)</summary>
    public long DurationMs { get; set; }

    /// <summary>LLM 调用迭代次数</summary>
    public int Iterations { get; init; }
}

/// <summary>
/// 工具调用记录
/// </summary>
public class ToolCallRecord
{
    public string ToolName { get; init; } = string.Empty;
    public string Parameters { get; init; } = string.Empty;
    public string Result { get; init; } = string.Empty;
    public bool Success { get; init; }
    public long DurationMs { get; set; }
}

/// <summary>
/// Agent 异常
/// </summary>
public class AgentException : Exception
{
    public AgentException(string message, Exception? innerException = null) 
        : base(message, innerException) { }
}

6.2 ASP.NET Core Web API 宿主

Program.cs - 应用入口

csharp 复制代码
using System.Net;
using OpenAI;
using AgentSystem.Core;
using AgentSystem.Tools;
using AgentSystem.Tools.BuiltIn;
using AgentSystem.Tools.Sandbox;
using AgentSystem.Memory;

var builder = WebApplication.CreateBuilder(args);

// ===== 配置绑定 =====
builder.Services.Configure<AgentOptions>(
    builder.Configuration.GetSection("Agent"));
builder.Services.Configure<MemoryOptions>(
    builder.Configuration.GetSection("Memory"));
builder.Services.Configure<DockerSandboxOptions>(
    builder.Configuration.GetSection("Sandbox"));

// ===== OpenAI 客户端 =====
var openAiKey = builder.Configuration["OpenAI:ApiKey"] 
    ?? throw new InvalidOperationException("OpenAI API Key 未配置");
var openAiEndpoint = builder.Configuration["OpenAI:Endpoint"]; // 支持自定义端点

builder.Services.AddSingleton<ChatClient>(sp =>
{
    var clientOptions = openAiEndpoint is not null
        ? new OpenAIClientOptions { Endpoint = new Uri(openAiEndpoint) }
        : new OpenAIClientOptions();

    var client = new OpenAIClient(openAiKey, clientOptions);
    return client.GetChatClient("gpt-4o"); // 或从配置读取
});

// ===== 记忆系统 =====
builder.Services.AddSingleton<MemoryService>();
// 选择向量存储后端
// builder.Services.AddSingleton<IVectorStore, QdrantVectorStore>();
// builder.Services.AddSingleton<IVectorStore, PineconeVectorStore>();
builder.Services.AddSingleton<IVectorStore, InMemoryVectorStore>(); // 开发环境

// ===== 工具系统 =====
builder.Services.AddSingleton<ToolRegistry>();
builder.Services.AddSingleton<ToolExecutor>();

// 注册内置工具
builder.Services.AddSingleton<ITool, WebSearchTool>(sp =>
{
    var httpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient();
    var apiKey = builder.Configuration["Tools:Search:ApiKey"] ?? "";
    return new WebSearchTool(httpClient, apiKey);
});

builder.Services.AddSingleton<ITool, FileReadTool>(sp =>
{
    var allowedDirs = builder.Configuration.GetSection("Tools:FileSystem:AllowedDirectories").Get<string[]>()
        ?? ["/tmp"];
    return new FileReadTool(allowedDirs);
});

builder.Services.AddSingleton<ITool, FileWriteTool>(sp =>
{
    var allowedDirs = builder.Configuration.GetSection("Tools:FileSystem:AllowedDirectories").Get<string[]>()
        ?? ["/tmp"];
    return new FileWriteTool(allowedDirs);
});

// 可选:代码执行沙箱
if (builder.Configuration.GetValue<bool>("Sandbox:Enabled"))
{
    builder.Services.AddSingleton<IToolSandbox, DockerSandbox>();
    builder.Services.AddSingleton<ITool, CodeExecutionTool>();
}

// 工具注册 - 在所有工具注入后执行
builder.Services.AddHostedService<ToolRegistrationService>();

// ===== 核心服务 =====
builder.Services.AddSingleton<AgentService>();

// ===== HTTP 客户端 =====
builder.Services.AddHttpClient();

// ===== 日志增强 =====
builder.Logging.AddConsole();
builder.Logging.AddJsonConsole(); // 结构化日志

// ===== 健康检查 =====
builder.Services.AddHealthChecks()
    .AddCheck<AgentHealthCheck>("agent");

// ===== API 控制器 =====
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
    {
        Title = "AI Agent API",
        Version = "v1",
        Description = "基于 C# 的 AI Agent 系统 API"
    });
});

// ===== CORS(如需跨域) =====
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.WithOrigins(builder.Configuration.GetSection("AllowedOrigins").Get<string[]>() ?? [])
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

var app = builder.Build();

// ===== 中间件管道 =====
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseCors();
app.UseHttpsRedirection();
app.UseAuthorization();

app.MapControllers();
app.MapHealthChecks("/health");

app.Run();

/// <summary>
/// 工具注册后台服务 - 启动时自动注册所有工具
/// </summary>
public class ToolRegistrationService : IHostedService
{
    private readonly ToolRegistry _registry;
    private readonly IEnumerable<ITool> _tools;
    private readonly ILogger<ToolRegistrationService> _logger;

    public ToolRegistrationService(
        ToolRegistry registry,
        IEnumerable<ITool> tools,
        ILogger<ToolRegistrationService> logger)
    {
        _registry = registry;
        _tools = tools;
        _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("开始注册工具...");
        _registry.RegisterAll(_tools);
        _logger.LogInformation("已注册 {Count} 个工具", _registry.GetAll().Count);
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

/// <summary>
/// Agent 健康检查
/// </summary>
public class AgentHealthCheck : IHealthCheck
{
    private readonly ToolRegistry _toolRegistry;
    private readonly MemoryService _memoryService;

    public AgentHealthCheck(ToolRegistry toolRegistry, MemoryService memoryService)
    {
        _toolRegistry = toolRegistry;
        _memoryService = memoryService;
    }

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var data = new Dictionary<string, object>
        {
            ["tools_registered"] = _toolRegistry.GetAll().Count,
            ["memory_initialized"] = _memoryService.IsInitialized
        };

        var healthy = _toolRegistry.GetAll().Count > 0 && _memoryService.IsInitialized;
        
        return Task.FromResult(healthy
            ? HealthCheckResult.Healthy("Agent 系统正常", data)
            : HealthCheckResult.Unhealthy("Agent 系统异常", null, data));
    }
}

API Controller

csharp 复制代码
using Microsoft.AspNetCore.Mvc;
using AgentSystem.Core;

namespace AgentSystem.Api.Controllers;

/// <summary>
/// Agent API 控制器
/// </summary>
[ApiController]
[Route("api/v1/[controller]")]
public class AgentController : ControllerBase
{
    private readonly AgentService _agentService;
    private readonly ILogger<AgentController> _logger;

    public AgentController(AgentService agentService, ILogger<AgentController> logger)
    {
        _agentService = agentService;
        _logger = logger;
    }

    /// <summary>
    /// 发送消息给 Agent
    /// </summary>
    [HttpPost("chat")]
    [ProducesResponseType(typeof(AgentResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
    public async Task<ActionResult<AgentResponse>> Chat(
        [FromBody] ChatRequest request,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(request.Message))
        {
            return BadRequest(new ProblemDetails
            {
                Title = "消息不能为空",
                Status = StatusCodes.Status400BadRequest
            });
        }

        try
        {
            var userId = request.UserId ?? HttpContext.Connection.RemoteIpAddress?.ToString() ?? "anonymous";
            
            var response = await _agentService.ProcessAsync(
                userId,
                request.Message,
                cancellationToken);

            return Ok(response);
        }
        catch (AgentException ex)
        {
            _logger.LogWarning(ex, "Agent 处理失败");
            return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails
            {
                Title = "处理请求失败",
                Detail = ex.Message,
                Status = StatusCodes.Status500InternalServerError
            });
        }
    }

    /// <summary>
    /// 流式响应(Server-Sent Events)
    /// </summary>
    [HttpPost("chat/stream")]
    public async Task ChatStream(
        [FromBody] ChatRequest request,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(request.Message))
        {
            Response.StatusCode = StatusCodes.Status400BadRequest;
            return;
        }

        Response.Headers.ContentType = "text/event-stream";
        Response.Headers.CacheControl = "no-cache";
        Response.Headers.Connection = "keep-alive";

        var userId = request.UserId ?? HttpContext.Connection.RemoteIpAddress?.ToString() ?? "anonymous";

        try
        {
            await foreach (var chunk in StreamResponseAsync(userId, request.Message, cancellationToken))
            {
                await Response.WriteAsync($"data: {chunk}\n\n", cancellationToken);
                await Response.Body.FlushAsync(cancellationToken);
            }
        }
        catch (OperationCanceledException)
        {
            // 客户端断开连接
        }
    }

    /// <summary>
    /// 获取可用工具列表
    /// </summary>
    [HttpGet("tools")]
    public IActionResult GetTools([FromServices] ToolRegistry registry)
    {
        var tools = registry.GetAllMetadata()
            .Select(m => new
            {
                m.Name,
                m.Description,
                m.Category,
                m.RiskLevel,
                Parameters = m.Parameters
            });

        return Ok(tools);
    }

    private async IAsyncEnumerable<string> StreamResponseAsync(
        string userId,
        string message,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        // 简化示例:实际应使用 OpenAI Streaming API
        var response = await _agentService.ProcessAsync(userId, message, cancellationToken);
        
        // 模拟流式输出
        var words = response.Response.Split(' ');
        foreach (var word in words)
        {
            yield return System.Text.Json.JsonSerializer.Serialize(new { word });
            await Task.Delay(50, cancellationToken);
        }
    }
}

/// <summary>
/// 聊天请求
/// </summary>
public class ChatRequest
{
    /// <summary>用户消息</summary>
    public string Message { get; set; } = string.Empty;

    /// <summary>用户 ID(可选,用于区分用户上下文)</summary>
    public string? UserId { get; set; }
}

6.3 配置管理

appsettings.json

json 复制代码
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "AgentSystem": "Debug"
    }
  },
  "AllowedHosts": "*",
  "AllowedOrigins": [
    "http://localhost:3000",
    "https://myapp.example.com"
  ],

  "OpenAI": {
    "ApiKey": "${OPENAI_API_KEY}",
    "Endpoint": null,
    "Model": "gpt-4o"
  },

  "Agent": {
    "SystemPrompt": "你是一个有用的 AI 助手,具备工具调用能力。请根据用户需求选择合适的工具完成任务。",
    "MaxMemoryResults": 5,
    "MaxOutputTokens": 2000,
    "Temperature": 0.7,
    "MaxToolIterations": 5,
    "ToolTimeoutMs": 30000,
    "MaxRiskLevel": "Medium"
  },

  "Memory": {
    "Provider": "InMemory",
    "EmbeddingModel": "text-embedding-3-small",
    "Qdrant": {
      "Host": "localhost",
      "Port": 6333,
      "Collection": "agent_memory"
    },
    "Postgres": {
      "ConnectionString": "${POSTGRES_CONNECTION_STRING}"
    }
  },

  "Tools": {
    "Search": {
      "ApiKey": "${SEARCH_API_KEY}"
    },
    "FileSystem": {
      "AllowedDirectories": [
        "/tmp",
        "/data/workspace"
      ]
    }
  },

  "Sandbox": {
    "Enabled": false,
    "ImageName": "agent-sandbox:latest",
    "MemoryLimitMb": 512,
    "CpuLimit": 1.0,
    "TimeoutSeconds": 30
  }
}

配置类与依赖注入

csharp 复制代码
// 使用 Options Pattern 注入配置
services.Configure<AgentOptions>(configuration.GetSection("Agent"));
services.Configure<MemoryOptions>(configuration.GetSection("Memory"));

// 在服务中使用
public class AgentService
{
    private readonly AgentOptions _options;
    
    public AgentService(IOptions<AgentOptions> options)
    {
        _options = options.Value;
    }
}

// 支持配置热更新
services.Configure<AgentOptions>(
    configuration.GetSection("Agent"),
    options => options.BindNonPublicProperties = true);

// 配置验证
services.AddOptions<AgentOptions>()
    .Bind(configuration.GetSection("Agent"))
    .Validate(options => 
        options.MaxToolIterations > 0 && 
        options.MaxOutputTokens > 0,
        "配置验证失败")
    .ValidateOnStart(); // 启动时验证

6.4 日志、监控与可观测性

结构化日志

csharp 复制代码
// Program.cs 已配置
builder.Logging.AddJsonConsole();

// 在服务中使用
public class AgentService
{
    private readonly ILogger<AgentService> _logger;

    public async Task<AgentResponse> ProcessAsync(...)
    {
        // 使用结构化日志
        _logger.LogInformation(
            "[{SessionId}] 开始处理消息,用户: {UserId}, 消息长度: {MessageLength}",
            sessionId, userId, userMessage.Length);

        try
        {
            // ...
            _logger.LogDebug(
                "[{SessionId}] 工具调用: {ToolName}, 耗时: {Duration}ms",
                sessionId, toolName, durationMs);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "[{SessionId}] 处理失败: {ErrorMessage}",
                sessionId, ex.Message);
            throw;
        }
    }
}

Prometheus 指标

csharp 复制代码
using Prometheus;

// Program.cs
app.MapMetrics("/metrics"); // 暴露 /metrics 端点

// 在服务中记录指标
public class AgentService
{
    private static readonly Counter RequestsTotal = Metrics
        .CreateCounter("agent_requests_total", "总请求数", "user_id");
    
    private static readonly Histogram RequestDuration = Metrics
        .CreateHistogram("agent_request_duration_seconds", "请求耗时", 
            new HistogramConfiguration { Buckets = Histogram.ExponentialBuckets(0.1, 2, 10) });
    
    private static readonly Counter ToolCallsTotal = Metrics
        .CreateCounter("agent_tool_calls_total", "工具调用次数", "tool_name", "success");

    public async Task<AgentResponse> ProcessAsync(...)
    {
        using var timer = RequestDuration.NewTimer();
        RequestsTotal.Labels(userId).Inc();
        
        // ...
        
        ToolCallsTotal.Labels(toolName, success.ToString()).Inc();
    }
}

健康检查端点

csharp 复制代码
// 已在 Program.cs 配置
app.MapHealthChecks("/health");

// 访问 /health 返回
{
  "status": "Healthy",
  "checks": [
    {
      "name": "agent",
      "status": "Healthy",
      "data": {
        "tools_registered": 5,
        "memory_initialized": true
      }
    }
  ]
}

OpenTelemetry 分布式追踪

csharp 复制代码
// 添加 OpenTelemetry
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddSource("AgentSystem.*")
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri(builder.Configuration["OpenTelemetry:Endpoint"] ?? "http://localhost:4317");
        }));

// 在服务中创建 Span
using var activity = ActivitySource.StartActivity("AgentService.ProcessAsync");
activity?.SetTag("user.id", userId);
activity?.SetTag("session.id", sessionId);

小结

本章完成了 AI Agent 系统的最终整合:

  1. AgentService:编排对话、记忆、工具调用的核心流程
  2. ASP.NET Core 宿主:完整的 Web API,支持 REST 和 SSE 流式响应
  3. 配置管理:Options Pattern + 环境变量 + 验证
  4. 可观测性:结构化日志、Prometheus 指标、健康检查、OpenTelemetry 追踪

至此,一个完整的、生产可用的 AI Agent 系统框架已经搭建完成。你可以基于此框架扩展更多工具、优化记忆检索策略、接入不同的 LLM 后端,构建属于你的智能应用。

完整代码结构:

复制代码
AgentSystem/
├── Core/
│   ├── AgentService.cs
│   ├── AgentOptions.cs
│   └── AgentResponse.cs
├── Tools/
│   ├── Abstractions/
│   │   ├── ITool.cs
│   │   └── ToolExecutionContext.cs
│   ├── ToolRegistry.cs
│   ├── ToolExecutor.cs
│   ├── BuiltIn/
│   │   ├── WebSearchTool.cs
│   │   ├── FileReadTool.cs
│   │   └── CodeExecutionTool.cs
│   ├── OpenAI/
│   │   └── OpenAIFunctionSchemaGenerator.cs
│   └── Sandbox/
│       └── DockerSandbox.cs
├── Memory/
│   ├── MemoryService.cs
│   └── IVectorStore.cs
├── Api/
│   ├── Controllers/
│   │   └── AgentController.cs
│   └── Program.cs
└── appsettings.json
相关推荐
SilentSamsara1 小时前
Pandas 工程化:多层索引、分组聚合与窗口函数的进阶用法
开发语言·python·青少年编程·pandas
甄心爱学习1 小时前
【项目实训(个人12)】
人工智能·python·算法
协享科技1 小时前
前端 SSE 流式响应处理实践:从接收、解析到渲染
前端·人工智能·程序人生·go·ai编程·sse
何以解忧,唯有..1 小时前
Python 字符串完全指南:从基础到高级操作
开发语言·python
程序大视界1 小时前
AI正在“接管“法槌?2026年法律AI全面入侵:合同审查99.2%准确率,律师该何去何从?
人工智能·ai法律
kiss strong1 小时前
自制请求工具
开发语言·python·lua
scan7241 小时前
短期记忆记忆存储在内存里,一个会话里的多轮对话
开发语言·c#
暗夜猎手-大魔王1 小时前
转载--Hermes Agent 12 | 沙箱与执行环境:六种终端后端的安全隔离
人工智能·安全
ylscode1 小时前
CISA紧急拉响警报:SolarWinds Serv-U曝高危漏洞CVE-2026-28318,零认证即可瘫痪文件传输服务
人工智能·安全