Loading

自定义工具与中间件开发

自定义工具与中间件开发

前言

在前几篇文章中,我们学习了如何使用Agent Framework内置的工具,以及如何通过工作流编排来协调多个任务。但是在实际业务中,内置工具往往不能满足所有需求。我们经常需要创建自定义工具来对接企业系统、调用外部API、操作数据库等。

本文将深入探讨如何在Agent Framework中开发自定义工具和中间件,包括工具的定义、注册、参数验证、错误处理,以及中间件的开发和使用。通过本文的学习,你将能够创建功能强大且安全的自定义工具,构建可扩展的Agent系统。

一、自定义工具基础

1.1 工具接口定义

Agent Framework中的工具需要实现特定的接口:

// ITool.cs - 工具基础接口
public interface ITool
{
    // 工具唯一标识
    string Id { get; }
    
    // 工具名称
    string Name { get; }
    
    // 工具描述
    string Description { get; }
    
    // 工具参数模式(用于LLM理解如何调用)
    ToolParameterSchema GetParameterSchema();
    
    // 执行工具
    Task<ToolExecutionResult> ExecuteAsync(Dictionary<string, object> parameters);
}

// 工具参数模式
public class ToolParameterSchema
{
    public string Type { get; set; } = "object";
    public Dictionary<string, ToolParameterProperty> Properties { get; set; } = new();
    public List<string> Required { get; set; } = new();
}

public class ToolParameterProperty
{
    public string Type { get; set; } = "string";
    public string Description { get; set; } = string.Empty;
    public bool Required { get; set; }
    public object? Default { get; set; }
    public List<string>? Enum { get; set; }
}

// 工具执行结果
public class ToolExecutionResult
{
    public bool Success { get; set; }
    public string? Output { get; set; }
    public string? Error { get; set; }
    public Dictionary<string, object> Metadata { get; set; } = new();
}

1.2 抽象基类实现

为了简化工具开发,我们创建一个抽象基类:

// BaseTool.cs
public abstract class BaseTool : ITool
{
    public abstract string Id { get; }
    public abstract string Name { get; }
    public abstract string Description { get; }
    
    protected abstract Task<ToolExecutionResult> ExecuteInternalAsync(
        Dictionary<string, object> parameters);
    
    public virtual ToolParameterSchema GetParameterSchema()
    {
        return new ToolParameterSchema();
    }
    
    public async Task<ToolExecutionResult> ExecuteAsync(Dictionary<string, object> parameters)
    {
        try
        {
            // 参数验证
            var validationResult = ValidateParameters(parameters);
            if (!validationResult.IsValid)
            {
                return new ToolExecutionResult
                {
                    Success = false,
                    Error = validationResult.ErrorMessage
                };
            }
            
            // 执行工具逻辑
            var result = await ExecuteInternalAsync(parameters);
            
            return result;
        }
        catch (Exception ex)
        {
            return new ToolExecutionResult
            {
                Success = false,
                Error = $"工具执行错误: {ex.Message}"
            };
        }
    }
    
    protected virtual ParameterValidationResult ValidateParameters(
        Dictionary<string, object> parameters)
    {
        var schema = GetParameterSchema();
        
        // 检查必需参数
        foreach (var requiredParam in schema.Required)
        {
            if (!parameters.ContainsKey(requiredParam) || 
                parameters[requiredParam] == null)
            {
                return new ParameterValidationResult
                {
                    IsValid = false,
                    ErrorMessage = $"缺少必需参数: {requiredParam}"
                };
            }
        }
        
        return new ParameterValidationResult { IsValid = true };
    }
}

public class ParameterValidationResult
{
    public bool IsValid { get; set; }
    public string? ErrorMessage { get; set; }
}

二、实用工具开发

2.1 数据库查询工具

首先,让我们创建一个可以执行SQL查询的工具:

// DatabaseQueryTool.cs
public class DatabaseQueryTool : BaseTool
{
    private readonly string _connectionString;
    private readonly ILogger<DatabaseQueryTool> _logger;
    private readonly int _maxRows;
    
    public override string Id => "db_query";
    public override string Name => "数据库查询";
    public override string Description => "执行SQL查询并返回结果。适用于查询数据、统计信息等。只支持SELECT语句。";
    
    public DatabaseQueryTool(
        string connectionString,
        ILogger<DatabaseQueryTool> logger,
        int maxRows = 100)
    {
        _connectionString = connectionString;
        _logger = logger;
        _maxRows = maxRows;
    }
    
    public override ToolParameterSchema GetParameterSchema()
    {
        return new ToolParameterSchema
        {
            Type = "object",
            Properties = new Dictionary<string, ToolParameterProperty>
            {
                ["sql"] = new ToolParameterProperty
                {
                    Type = "string",
                    Description = "要执行的SQL查询语句。只支持SELECT语句,不支持UPDATE/INSERT/DELETE。",
                    Required = true
                },
                ["parameters"] = new ToolParameterProperty
                {
                    Type = "object",
                    Description = "查询参数,用于参数化查询",
                    Required = false
                }
            },
            Required = new List<string> { "sql" }
        };
    }
    
    protected override async Task<ToolExecutionResult> ExecuteInternalAsync(
        Dictionary<string, object> parameters)
    {
        var sql = parameters["sql"]?.ToString();
        
        if (string.IsNullOrWhiteSpace(sql))
        {
            return new ToolExecutionResult
            {
                Success = false,
                Error = "SQL语句不能为空"
            };
        }
        
        // 安全检查:只允许SELECT语句
        var trimmedSql = sql.Trim().ToUpperInvariant();
        if (!trimmedSql.StartsWith("SELECT"))
        {
            return new ToolExecutionResult
            {
                Success = false,
                Error = "只允许执行SELECT查询语句"
            };
        }
        
        _logger.LogInformation("执行查询: {Sql}", sql);
        
        try
        {
            using var connection = new SqlConnection(_connectionString);
            await connection.OpenAsync();
            
            // 使用参数化查询防止SQL注入
            using var command = new SqlCommand(sql, connection);
            
            // 添加查询参数
            if (parameters.ContainsKey("parameters") && 
                parameters["parameters"] is Dictionary<string, object> queryParams)
            {
                foreach (var kvp in queryParams)
                {
                    command.Parameters.AddWithValue(kvp.Key, kvp.Value ?? DBNull.Value);
                }
            }
            
            // 限制返回行数
            command.CommandText += $" LIMIT {_maxRows}";
            
            using var reader = await command.ExecuteReaderAsync();
            
            var results = new List<Dictionary<string, object?>>();
            var columns = new List<string>();
            
            // 获取列名
            for (int i = 0; i < reader.FieldCount; i++)
            {
                columns.Add(reader.GetName(i));
            }
            
            // 读取数据
            while (await reader.ReadAsync())
            {
                var row = new Dictionary<string, object?>();
                for (int i = 0; i < columns.Count; i++)
                {
                    row[columns[i]] = reader.IsDBNull(i) ? null : reader.GetValue(i);
                }
                results.Add(row);
            }
            
            _logger.LogInformation("查询返回 {Count} 行", results.Count);
            
            return new ToolExecutionResult
            {
                Success = true,
                Output = JsonSerializer.Serialize(new
                {
                    columns,
                    rows = results,
                    totalCount = results.Count
                }),
                Metadata = new Dictionary<string, object>
                {
                    { "rowCount", results.Count },
                    { "executionTime", DateTime.UtcNow }
                }
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "查询执行失败");
            
            return new ToolExecutionResult
            {
                Success = false,
                Error = $"查询执行失败: {ex.Message}"
            };
        }
    }
}

2.2 HTTP请求工具

创建一个通用的HTTP请求工具:

// HttpRequestTool.cs
public class HttpRequestTool : BaseTool
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<HttpRequestTool> _logger;
    private readonly Dictionary<string, string> _allowedHeaders;
    
    public override string Id => "http_request";
    public override string Name => "HTTP请求";
    public override string Description => "发送HTTP请求,支持GET、POST、PUT、DELETE等方法。可以用于调用外部API。";
    
    public HttpRequestTool(
        HttpClient httpClient,
        ILogger<HttpRequestTool> logger,
        Dictionary<string, string>? defaultHeaders = null)
    {
        _httpClient = httpClient;
        _logger = logger;
        _allowedHeaders = defaultHeaders ?? new Dictionary<string, string>();
    }
    
    public override ToolParameterSchema GetParameterSchema()
    {
        return new ToolParameterSchema
        {
            Type = "object",
            Properties = new Dictionary<string, ToolParameterProperty>
            {
                ["url"] = new ToolParameterProperty
                {
                    Type = "string",
                    Description = "请求的URL地址",
                    Required = true
                },
                ["method"] = new ToolParameterProperty
                {
                    Type = "string",
                    Description = "HTTP方法:GET、POST、PUT、DELETE、PATCH",
                    Required = false,
                    Default = "GET",
                    Enum = new List<string> { "GET", "POST", "PUT", "DELETE", "PATCH" }
                },
                ["headers"] = new ToolParameterProperty
                {
                    Type = "object",
                    Description = "请求头",
                    Required = false
                },
                ["body"] = new ToolParameterProperty
                {
                    Type = "object",
                    Description = "请求体(JSON对象)",
                    Required = false
                },
                ["timeout"] = new ToolParameterProperty
                {
                    Type = "number",
                    Description = "请求超时时间(秒)",
                    Required = false,
                    Default = 30
                }
            },
            Required = new List<string> { "url" }
        };
    }
    
    protected override async Task<ToolExecutionResult> ExecuteInternalAsync(
        Dictionary<string, object> parameters)
    {
        var url = parameters["url"]?.ToString();
        var method = parameters.GetValueOrDefault("method")?.ToString() ?? "GET";
        var timeout = parameters.GetValueOrDefault("timeout") is int t ? t : 30;
        
        if (string.IsNullOrWhiteSpace(url))
        {
            return new ToolExecutionResult
            {
                Success = false,
                Error = "URL不能为空"
            };
        }
        
        _logger.LogInformation("发送HTTP请求: {Method} {Url}", method, url);
        
        try
        {
            var request = new HttpRequestMessage(
                new HttpMethod(method), 
                url);
            
            // 添加默认请求头
            foreach (var header in _allowedHeaders)
            {
                request.Headers.TryAddWithoutValidation(header.Key, header.Value);
            }
            
            // 添加自定义请求头
            if (parameters.ContainsKey("headers") && 
                parameters["headers"] is Dictionary<string, object> customHeaders)
            {
                foreach (var header in customHeaders)
                {
                    request.Headers.TryAddWithoutValidation(header.Key, header.Value?.ToString());
                }
            }
            
            // 添加请求体
            if (parameters.ContainsKey("body") && 
                method != "GET")
            {
                var body = parameters["body"];
                var json = JsonSerializer.Serialize(body);
                request.Content = new StringContent(json, Encoding.UTF8, "application/json");
            }
            
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout));
            var response = await _httpClient.SendAsync(request, cts.Token);
            
            var content = await response.Content.ReadAsStringAsync();
            
            _logger.LogInformation("HTTP响应: {StatusCode}, Content-Length={Length}",
                response.StatusCode, content.Length);
            
            return new ToolExecutionResult
            {
                Success = response.IsSuccessStatusCode,
                Output = content,
                Metadata = new Dictionary<string, object>
                {
                    { "statusCode", (int)response.StatusCode },
                    { "reasonPhrase", response.ReasonPhrase ?? "" }
                }
            };
        }
        catch (TaskCanceledException)
        {
            return new ToolExecutionResult
            {
                Success = false,
                Error = $"请求超时({timeout}秒)"
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "HTTP请求失败");
            
            return new ToolExecutionResult
            {
                Success = false,
                Error = $"请求失败: {ex.Message}"
            };
        }
    }
}

2.3 文件操作工具

创建用于读取和写入文件的工具:

// FileOperationTool.cs
public class FileOperationTool : BaseTool
{
    private readonly string _baseDirectory;
    private readonly ILogger<FileOperationTool> _logger;
    private static readonly string[] AllowedExtensions = { ".txt", ".json", ".xml", ".csv", ".md" };
    
    public override string Id => "file_operation";
    public override string Name => "文件操作";
    public override string Description => "读取或写入文件。支持读取文本文件内容,或将内容写入文件。";
    
    public FileOperationTool(
        string baseDirectory,
        ILogger<FileOperationTool> logger)
    {
        _baseDirectory = baseDirectory;
        _logger = logger;
        
        // 确保目录存在
        Directory.CreateDirectory(_baseDirectory);
    }
    
    public override ToolParameterSchema GetParameterSchema()
    {
        return new ToolParameterSchema
        {
            Type = "object",
            Properties = new Dictionary<string, ToolParameterProperty>
            {
                ["operation"] = new ToolParameterProperty
                {
                    Type = "string",
                    Description = "操作类型:read(读取)或 write(写入)",
                    Required = true,
                    Enum = new List<string> { "read", "write" }
                },
                ["path"] = new ToolParameterProperty
                {
                    Type = "string",
                    Description = "文件路径(相对于基础目录)",
                    Required = true
                },
                ["content"] = new ToolParameterProperty
                {
                    Type = "string",
                    Description = "写入文件的内容(write操作时必需)",
                    Required = false
                }
            },
            Required = new List<string> { "operation", "path" }
        };
    }
    
    protected override Task<ToolExecutionResult> ExecuteInternalAsync(
        Dictionary<string, object> parameters)
    {
        var operation = parameters["operation"]?.ToString()?.ToLower();
        var path = parameters["path"]?.ToString();
        
        if (string.IsNullOrWhiteSpace(path))
        {
            return Task.FromResult(new ToolExecutionResult
            {
                Success = false,
                Error = "文件路径不能为空"
            });
        }
        
        // 安全检查:防止路径遍历攻击
        var fullPath = GetFullPath(path);
        if (fullPath == null)
        {
            return Task.FromResult(new ToolExecutionResult
            {
                Success = false,
                Error = "无效的文件路径"
            });
        }
        
        return operation switch
        {
            "read" => ReadFileAsync(fullPath),
            "write" => WriteFileAsync(fullPath, parameters),
            _ => Task.FromResult(new ToolExecutionResult
            {
                Success = false,
                Error = $"不支持的操作: {operation}"
            })
        };
    }
    
    private string? GetFullPath(string relativePath)
    {
        try
        {
            var fullPath = Path.GetFullPath(Path.Combine(_baseDirectory, relativePath));
            
            // 确保路径在基础目录内
            if (!fullPath.StartsWith(_baseDirectory, StringComparison.OrdinalIgnoreCase))
            {
                return null;
            }
            
            // 检查文件扩展名
            var extension = Path.GetExtension(fullPath);
            if (!AllowedExtensions.Contains(extension.ToLower()))
            {
                return null;
            }
            
            return fullPath;
        }
        catch
        {
            return null;
        }
    }
    
    private Task<ToolExecutionResult> ReadFileAsync(string path)
    {
        try
        {
            if (!File.Exists(path))
            {
                return Task.FromResult(new ToolExecutionResult
                {
                    Success = false,
                    Error = $"文件不存在: {path}"
                });
            }
            
            var content = File.ReadAllText(path);
            
            _logger.LogInformation("读取文件: {Path}, Size={Size}", path, content.Length);
            
            return Task.FromResult(new ToolExecutionResult
            {
                Success = true,
                Output = content,
                Metadata = new Dictionary<string, object>
                {
                    { "path", path },
                    { "size", content.Length }
                }
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "读取文件失败: {Path}", path);
            
            return Task.FromResult(new ToolExecutionResult
            {
                Success = false,
                Error = $"读取文件失败: {ex.Message}"
            });
        }
    }
    
    private Task<ToolExecutionResult> WriteFileAsync(
        string path, 
        Dictionary<string, object> parameters)
    {
        var content = parameters.GetValueOrDefault("content")?.ToString();
        
        if (string.IsNullOrEmpty(content))
        {
            return Task.FromResult(new ToolExecutionResult
            {
                Success = false,
                Error = "写入内容不能为空"
            });
        }
        
        try
        {
            // 确保目录存在
            var directory = Path.GetDirectoryName(path);
            if (!string.IsNullOrEmpty(directory))
            {
                Directory.CreateDirectory(directory);
            }
            
            File.WriteAllText(path, content);
            
            _logger.LogInformation("写入文件: {Path}, Size={Size}", path, content.Length);
            
            return Task.FromResult(new ToolExecutionResult
            {
                Success = true,
                Output = $"文件已写入: {path}",
                Metadata = new Dictionary<string, object>
                {
                    { "path", path },
                    { "size", content.Length }
                }
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "写入文件失败: {Path}", path);
            
            return Task.FromResult(new ToolExecutionResult
            {
                Success = false,
                Error = $"写入文件失败: {ex.Message}"
            });
        }
    }
}

三、工具注册与管理

3.1 工具管理器

// ToolRegistry.cs
public class ToolRegistry : IToolRegistry
{
    private readonly Dictionary<string, ITool> _tools;
    private readonly ILogger<ToolRegistry> _logger;
    private readonly SemaphoreSlim _lock = new(1, 1);
    
    public ToolRegistry(ILogger<ToolRegistry> logger)
    {
        _tools = new Dictionary<string, ITool>();
        _logger = logger;
    }
    
    public void Register(ITool tool)
    {
        _lock.Wait();
        try
        {
            if (_tools.ContainsKey(tool.Id))
            {
                _logger.LogWarning("工具已存在,将被替换: {ToolId}", tool.Id);
                _tools[tool.Id] = tool;
            }
            else
            {
                _tools[tool.Id] = tool;
                _logger.LogInformation("注册工具: {ToolName} ({ToolId})", tool.Name, tool.Id);
            }
        }
        finally
        {
            _lock.Release();
        }
    }
    
    public void Unregister(string toolId)
    {
        _lock.Wait();
        try
        {
            if (_tools.Remove(toolId))
            {
                _logger.LogInformation("注销工具: {ToolId}", toolId);
            }
        }
        finally
        {
            _lock.Release();
        }
    }
    
    public ITool? Get(string toolId)
    {
        _tools.TryGetValue(toolId, out var tool);
        return tool;
    }
    
    public IEnumerable<ITool> GetAll()
    {
        return _tools.Values;
    }
    
    public IEnumerable<ITool> Search(string? keyword)
    {
        if (string.IsNullOrWhiteSpace(keyword))
        {
            return GetAll();
        }
        
        return _tools.Values
            .Where(t => t.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
                       t.Description.Contains(keyword, StringComparison.OrdinalIgnoreCase))
            .ToList();
    }
    
    public ToolList GetToolListForLLM()
    {
        var tools = _tools.Values.Select(tool => new ToolDefinition
        {
            Type = "function",
            Function = new ToolFunctionDefinition
            {
                Name = tool.Id,
                Description = tool.Description,
                Parameters = tool.GetParameterSchema()
            }
        }).ToList();
        
        return new ToolList { Tools = tools };
    }
}

public interface IToolRegistry
{
    void Register(ITool tool);
    void Unregister(string toolId);
    ITool? Get(string toolId);
    IEnumerable<ITool> GetAll();
    IEnumerable<ITool> Search(string? keyword);
    ToolList GetToolListForLLM();
}

public class ToolList
{
    public List<ToolDefinition> Tools { get; set; } = new();
}

public class ToolDefinition
{
    public string Type { get; set; } = string.Empty;
    public ToolFunctionDefinition Function { get; set; } = new();
}

public class ToolFunctionDefinition
{
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public ToolParameterSchema Parameters { get; set; } = new();
}

3.2 依赖注入集成

在ASP.NET Core中集成工具注册:

// ToolExtensions.cs
public static class ToolExtensions
{
    public static IServiceCollection AddAgentTools(this IServiceCollection services)
    {
        // 注册工具注册表
        services.AddSingleton<IToolRegistry, ToolRegistry>();
        
        // 注册内置工具
        services.AddSingleton<ITool>(sp =>
        {
            var logger = sp.GetRequiredService<ILogger<DatabaseQueryTool>>();
            var connectionString = sp.GetRequiredService<IConfiguration>()["Database:ConnectionString"];
            return new DatabaseQueryTool(connectionString!, logger);
        });
        
        services.AddSingleton<ITool>(sp =>
        {
            var logger = sp.GetRequiredService<ILogger<HttpRequestTool>>();
            var httpClient = sp.GetRequiredService<HttpClient>();
            var headers = sp.GetRequiredService<IConfiguration>()
                .GetSection("HttpClient:DefaultHeaders")
                .Get<Dictionary<string, string>>();
            return new HttpRequestTool(httpClient, logger, headers);
        });
        
        services.AddSingleton<ITool>(sp =>
        {
            var logger = sp.GetRequiredService<ILogger<FileOperationTool>>();
            var baseDirectory = sp.GetRequiredService<IConfiguration>()["FileStorage:BaseDirectory"]
                ?? Path.Combine(AppContext.BaseDirectory, "files");
            return new FileOperationTool(baseDirectory, logger);
        });
        
        // 注册自定义工具
        services.AddTransient<ICustomTool, OrderQueryTool>();
        services.AddTransient<ICustomTool, WeatherQueryTool>();
        
        return services;
    }
    
    public static IAgentHost UseTools(this IAgentHost host, IServiceProvider serviceProvider)
    {
        var registry = serviceProvider.GetRequiredService<IToolRegistry>();
        
        // 注册所有实现了ITool接口的服务
        var tools = serviceProvider.GetServices<ITool>();
        foreach (var tool in tools)
        {
            registry.Register(tool);
        }
        
        return host;
    }
}

四、中间件开发

4.1 中间件基础

中间件允许在请求处理过程中插入自定义逻辑:

// IAgentMiddleware.cs
public interface IAgentMiddleware
{
    // 异步调用下一个中间件
    Task InvokeAsync(AgentContext context, AgentDelegate next);
}

// 中间件委托
public delegate Task AgentDelegate(AgentContext context);

// 中间件管道
public class AgentMiddlewarePipeline
{
    private readonly List<IAgentMiddleware> _middlewares = new();
    private readonly ILogger<AgentMiddlewarePipeline> _logger;
    
    public AgentMiddlewarePipeline(ILogger<AgentMiddlewarePipeline> logger)
    {
        _logger = logger;
    }
    
    public void Use(IAgentMiddleware middleware)
    {
        _middlewares.Add(middleware);
        _logger.LogInformation("注册中间件: {MiddlewareType}", middleware.GetType().Name);
    }
    
    public void Use(Func<AgentContext, AgentDelegate, Task> middleware)
    {
        _middlewares.Add(new AnonymousMiddleware(middleware));
    }
    
    public async Task InvokeAsync(AgentContext context)
    {
        var index = 0;
        
        AgentDelegate next = async (ctx) =>
        {
            if (index >= _middlewares.Count)
            {
                return; // 没有更多中间件了
            }
            
            var middleware = _middlewares[index++];
            await middleware.InvokeAsync(ctx, next);
        };
        
        if (_middlewares.Count > 0)
        {
            await _middlewares[0].InvokeAsync(context, next);
        }
    }
    
    private class AnonymousMiddleware : IAgentMiddleware
    {
        private readonly Func<AgentContext, AgentDelegate, Task> _middleware;
        
        public AnonymousMiddleware(Func<AgentContext, AgentDelegate, Task> middleware)
        {
            _middleware = middleware;
        }
        
        public Task InvokeAsync(AgentContext context, AgentDelegate next)
        {
            return _middleware(context, next);
        }
    }
}

4.2 日志记录中间件

// LoggingMiddleware.cs
public class LoggingMiddleware : IAgentMiddleware
{
    private readonly ILogger<LoggingMiddleware> _logger;
    
    public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
    {
        _logger = logger;
    }
    
    public async Task InvokeAsync(AgentContext context, AgentDelegate next)
    {
        var startTime = DateTime.UtcNow;
        var requestId = Guid.NewGuid().ToString("N")[..8];
        
        context.Properties["RequestId"] = requestId;
        
        _logger.LogInformation(
            "[{RequestId}] 开始处理请求: UserId={UserId}, Message={Message}",
            requestId,
            context.UserId,
            context.Message);
        
        try
        {
            await next(context);
            
            var duration = DateTime.UtcNow - startTime;
            
            _logger.LogInformation(
                "[{RequestId}] 请求处理完成: Duration={Duration}ms, Success={Success}",
                requestId,
                duration.TotalMilliseconds,
                context.Properties.GetValueOrDefault("Success"));
        }
        catch (Exception ex)
        {
            var duration = DateTime.UtcNow - startTime;
            
            _logger.LogError(ex,
                "[{RequestId}] 请求处理失败: Duration={Duration}ms, Error={Error}",
                requestId,
                duration.TotalMilliseconds,
                ex.Message);
            
            throw;
        }
    }
}

4.3 认证中间件

// AuthenticationMiddleware.cs
public class AuthenticationMiddleware : IAgentMiddleware
{
    private readonly ITokenValidator _tokenValidator;
    private readonly ILogger<AuthenticationMiddleware> _logger;
    private readonly List<string> _excludedPaths;
    
    public AuthenticationMiddleware(
        ITokenValidator tokenValidator,
        ILogger<AuthenticationMiddleware> logger,
        List<string>? excludedPaths = null)
    {
        _tokenValidator = tokenValidator;
        _logger = logger;
        _excludedPaths = excludedPaths ?? new List<string> { "/health", "/ready" };
    }
    
    public async Task InvokeAsync(AgentContext context, AgentDelegate next)
    {
        // 检查是否需要认证
        if (ShouldSkipAuthentication(context))
        {
            await next(context);
            return;
        }
        
        // 获取token
        var token = ExtractToken(context);
        
        if (string.IsNullOrEmpty(token))
        {
            _logger.LogWarning("缺少认证令牌: Path={Path}", context.Properties.GetValueOrDefault("Path"));
            
            context.Properties["AuthError"] = "Missing authentication token";
            context.Response = new AgentResponse
            {
                Success = false,
                Error = "需要认证"
            };
            
            return;
        }
        
        // 验证token
        try
        {
            var validationResult = await _tokenValidator.ValidateAsync(token);
            
            if (!validationResult.IsValid)
            {
                _logger.LogWarning("Token验证失败: {Error}", validationResult.Error);
                
                context.Properties["AuthError"] = validationResult.Error;
                context.Response = new AgentResponse
                {
                    Success = false,
                    Error = "认证失败"
                };
                
                return;
            }
            
            // 设置用户信息
            context.UserId = validationResult.UserId;
            context.Properties["UserRoles"] = validationResult.Roles;
            
            _logger.LogDebug("认证成功: UserId={UserId}", validationResult.UserId);
            
            await next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "认证过程出错");
            
            context.Properties["AuthError"] = ex.Message;
            context.Response = new AgentResponse
            {
                Success = false,
                Error = "认证服务错误"
            };
        }
    }
    
    private bool ShouldSkipAuthentication(AgentContext context)
    {
        var path = context.Properties.GetValueOrDefault("Path")?.ToString() ?? "";
        return _excludedPaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase));
    }
    
    private string? ExtractToken(AgentContext context)
    {
        // 从Header中获取
        if (context.Properties.TryGetValue("Authorization", out var authHeader))
        {
            var header = authHeader?.ToString();
            if (header?.StartsWith("Bearer ") == true)
            {
                return header.Substring(7);
            }
        }
        
        // 从Cookie中获取
        if (context.Properties.TryGetValue("Cookie", out var cookie))
        {
            var cookieString = cookie?.ToString();
            var tokenMatch = System.Text.RegularExpressions.Regex.Match(
                cookieString ?? "", 
                @"token=([^;]+)");
            
            if (tokenMatch.Success)
            {
                return tokenMatch.Groups[1].Value;
            }
        }
        
        return null;
    }
}

4.4 限流中间件

// RateLimitingMiddleware.cs
public class RateLimitingMiddleware : IAgentMiddleware
{
    private readonly IRateLimiter _rateLimiter;
    private readonly ILogger<RateLimitingMiddleware> _logger;
    
    public RateLimitingMiddleware(
        IRateLimiter rateLimiter,
        ILogger<RateLimitingMiddleware> logger)
    {
        _rateLimiter = rateLimiter;
        _logger = logger;
    }
    
    public async Task InvokeAsync(AgentContext context, AgentDelegate next)
    {
        // 使用用户ID或IP地址作为限流key
        var key = context.UserId ?? 
                  context.Properties.GetValueOrDefault("ClientIP")?.ToString() ?? 
                  "anonymous";
        
        var limitResult = await _rateLimiter.CheckLimitAsync(key);
        
        if (!limitResult.Allowed)
        {
            _logger.LogWarning(
                "请求被限流: Key={Key}, Limit={Limit}, Remaining={Remaining}",
                key, limitResult.Limit, limitResult.Remaining);
            
            context.Response = new AgentResponse
            {
                Success = false,
                Error = $"请求过于频繁,请{limitResult.RetryAfter}秒后再试"
            };
            
            context.Properties["RateLimited"] = true;
            context.Properties["RetryAfter"] = limitResult.RetryAfter;
            
            return;
        }
        
        _logger.LogDebug(
            "限流检查通过: Key={Key}, Remaining={Remaining}/{Limit}",
            key, limitResult.Remaining, limitResult.Limit);
        
        // 添加限流信息到响应头
        context.Properties["RateLimit-Limit"] = limitResult.Limit;
        context.Properties["RateLimit-Remaining"] = limitResult.Remaining;
        context.Properties["RateLimit-Reset"] = limitResult.ResetTime;
        
        await next(context);
    }
}

// 简单的内存限流器实现
public class MemoryRateLimiter : IRateLimiter
{
    private readonly ConcurrentDictionary<string, RateLimitEntry> _entries;
    private readonly int _maxRequests;
    private readonly TimeSpan _window;
    
    public MemoryRateLimiter(int maxRequests = 100, int windowSeconds = 60)
    {
        _maxRequests = maxRequests;
        _window = TimeSpan.FromSeconds(windowSeconds);
        _entries = new ConcurrentDictionary<string, RateLimitEntry>();
    }
    
    public Task<RateLimitResult> CheckLimitAsync(string key)
    {
        var now = DateTime.UtcNow;
        var entry = _entries.GetOrAdd(key, _ => new RateLimitEntry
        {
            Count = 0,
            WindowStart = now
        });
        
        lock (entry)
        {
            // 检查是否在同一个窗口内
            if (now - entry.WindowStart > _window)
            {
                // 重置窗口
                entry.WindowStart = now;
                entry.Count = 0;
            }
            
            entry.Count++;
            
            var remaining = Math.Max(0, _maxRequests - entry.Count);
            var allowed = entry.Count <= _maxRequests;
            
            return Task.FromResult(new RateLimitResult
            {
                Allowed = allowed,
                Limit = _maxRequests,
                Remaining = remaining,
                RetryAfter = allowed ? 0 : (int)(entry.WindowStart + _window - now).TotalSeconds,
                ResetTime = entry.WindowStart + _window
            });
        }
    }
    
    private class RateLimitEntry
    {
        public int Count { get; set; }
        public DateTime WindowStart { get; set; }
    }
}

public interface IRateLimiter
{
    Task<RateLimitResult> CheckLimitAsync(string key);
}

public class RateLimitResult
{
    public bool Allowed { get; set; }
    public int Limit { get; set; }
    public int Remaining { get; set; }
    public int RetryAfter { get; set; }
    public DateTime ResetTime { get; set; }
}

五、工具与中间件组合使用

5.1 完整的Agent主机

// AgentHost.cs
public class AgentHost
{
    private readonly IToolRegistry _toolRegistry;
    private readonly AgentMiddlewarePipeline _pipeline;
    private readonly IAIAgent _agent;
    private readonly ILogger<AgentHost> _logger;
    
    public AgentHost(
        IToolRegistry toolRegistry,
        IAIAgent agent,
        ILogger<AgentHost> logger)
    {
        _toolRegistry = toolRegistry;
        _agent = agent;
        _logger = logger;
        _pipeline = new AgentMiddlewarePipeline(logger);
    }
    
    public AgentHost Use(IAgentMiddleware middleware)
    {
        _pipeline.Use(middleware);
        return this;
    }
    
    public AgentHost Use(Func<AgentContext, AgentDelegate, Task> middleware)
    {
        _pipeline.Use(middleware);
        return this;
    }
    
    public async Task<AgentResponse> ProcessAsync(AgentRequest request)
    {
        var context = new AgentContext
        {
            UserId = request.UserId,
            ConversationId = request.ConversationId,
            Message = request.Message,
            Properties = request.Properties ?? new Dictionary<string, object>(),
            Timestamp = DateTime.UtcNow
        };
        
        try
        {
            // 通过中间件管道处理
            await _pipeline.InvokeAsync(context);
            
            // 如果中间件已经设置了响应,直接返回
            if (context.Response != null)
            {
                return context.Response;
            }
            
            // 调用Agent处理
            var tools = _toolRegistry.GetAll().ToList();
            var response = await _agent.ProcessAsync(context, tools);
            
            return new AgentResponse
            {
                Success = true,
                Message = response,
                ConversationId = context.ConversationId,
                Metadata = context.Properties
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Agent处理失败");
            
            return new AgentResponse
            {
                Success = false,
                Error = $"处理失败: {ex.Message}"
            };
        }
    }
}

5.2 配置示例

// Program.cs 配置
var builder = WebApplication.CreateBuilder(args);

// 添加服务
builder.Services.AddSingleton<IToolRegistry, ToolRegistry>();
builder.Services.AddAgentTools();

// 配置中间件
builder.Services.AddSingleton<ITokenValidator, JwtTokenValidator>();
builder.Services.AddSingleton<IRateLimiter>(sp => 
    new MemoryRateLimiter(maxRequests: 100, windowSeconds: 60));

var app = builder.Build();

// 配置Agent主机
var agentHost = new AgentHost(
    app.Services.GetRequiredService<IToolRegistry>(),
    app.Services.GetRequiredService<IAIAgent>(),
    app.Services.GetRequiredService<ILogger<AgentHost>>())
    .Use(new LoggingMiddleware(app.Services.GetRequiredService<ILogger<LoggingMiddleware>>()))
    .Use(new AuthenticationMiddleware(
        app.Services.GetRequiredService<ITokenValidator>(),
        app.Services.GetRequiredService<ILogger<AuthenticationMiddleware>>()))
    .Use(new RateLimitingMiddleware(
        app.Services.GetRequiredService<IRateLimiter>(),
        app.Services.GetRequiredService<ILogger<RateLimitingMiddleware>>()));

app.MapPost("/agent", async (AgentRequest request) =>
{
    var response = await agentHost.ProcessAsync(request);
    return Results.Ok(response);
});

app.Run();

六、最佳实践

6.1 工具设计原则

在开发自定义工具时,应遵循以下原则:

单一职责:每个工具应该只完成一项具体任务。这样可以提高工具的可测试性和可维护性。

清晰的参数定义:为每个参数提供清晰的描述和验证规则。这有助于LLM正确理解何时以及如何使用工具。

完善的错误处理:工具应该捕获并处理可预见的错误,返回有意义的错误信息,而不是抛出原始异常。

安全优先:对所有输入进行验证,特别是涉及文件操作、数据库查询、HTTP请求等可能存在安全风险的场景。

幂等性:在可能的情况下,工具应该具有幂等性,即多次执行相同操作应该产生相同的结果。

6.2 中间件设计原则

短小精悍:中间件应该专注于单一功能,不要在中间件中混合多个不相关的逻辑。

不要阻塞:避免在中间件中执行耗时的同步操作,使用异步方式处理。

正确传递上下文:确保修改context时不要影响后续中间件的正确执行。

错误处理:在中间件中妥善处理异常,避免未处理的异常导致请求崩溃。

七、总结与展望

通过本文的学习,我们已经掌握了自定义工具和中间件开发的核心技术:

  1. 工具接口:理解Agent Framework的工具接口定义
  2. 工具开发:创建数据库查询、HTTP请求、文件操作等实用工具
  3. 工具管理:实现工具注册、搜索、动态管理
  4. 中间件开发:创建日志、认证、限流等中间件
  5. 组合使用:将工具和中间件组合构建完整的Agent系统
  6. 最佳实践:遵循工具和中间件的设计原则

关键收获:

自定义工具和中间件是扩展Agent Framework功能的关键手段。通过合理设计工具,我们可以让Agent与各种外部系统进行交互;通过使用中间件,我们可以在请求处理过程中添加认证、限流、日志等功能。在实际开发中,要注意工具的安全性和中间件的性能,确保系统稳定可靠运行。

下一篇文章预告:

在第八篇文章中,我们将探索监控与可观测性。我们将学习如何使用OpenTelemetry实现Agent系统的全面监控,包括性能指标、分布式追踪、日志聚合等,确保生产环境的稳定运行。


实践建议:

  1. 为每个工具编写单元测试,确保功能正确
  2. 使用依赖注入管理工具生命周期
  3. 为敏感操作添加审计日志
  4. 监控工具调用频率和性能
  5. 定期审查和更新工具权限

相关资源:


"好的工具让Agent更强大,好的中间件让系统更可靠。"

posted @ 2026-03-13 16:40  饭勺oO  阅读(961)  评论(0)    收藏  举报