智能聊天机器人 #

项目概述 #

本节将构建一个完整的智能聊天机器人,具备以下功能:

  • 多轮对话管理
  • 上下文记忆
  • 流式输出
  • 用户偏好学习
  • 函数调用能力

架构设计 #

text
┌─────────────────────────────────────────────────────────────┐
│                    聊天机器人架构                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐   │
│  │   前端 UI   │────▶│  Web API    │────▶│   Kernel    │   │
│  └─────────────┘     └─────────────┘     └──────┬──────┘   │
│                                                  │           │
│         ┌────────────────────────────────────────┤           │
│         │                    │                   │           │
│         ▼                    ▼                   ▼           │
│  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐    │
│  │   Plugins   │     │   Memory    │     │   History   │    │
│  │  (工具插件)  │     │  (记忆系统)  │     │  (对话历史)  │    │
│  └─────────────┘     └─────────────┘     └─────────────┘    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

项目结构 #

text
ChatBot/
├── Controllers/
│   └── ChatController.cs
├── Services/
│   ├── IChatService.cs
│   ├── ChatService.cs
│   ├── IHistoryService.cs
│   └── HistoryService.cs
├── Plugins/
│   ├── WeatherPlugin.cs
│   ├── CalculatorPlugin.cs
│   └── SearchPlugin.cs
├── Models/
│   ├── ChatRequest.cs
│   ├── ChatResponse.cs
│   └── ChatMessage.cs
├── Hubs/
│   └── ChatHub.cs
└── Program.cs

核心代码实现 #

1. 服务接口 #

csharp
public interface IChatService
{
    Task<ChatResponse> ChatAsync(string userId, string message);
    IAsyncEnumerable<string> ChatStreamAsync(string userId, string message);
    Task ClearHistoryAsync(string userId);
}

2. 聊天服务实现 #

csharp
public class ChatService : IChatService
{
    private readonly Kernel _kernel;
    private readonly IChatCompletionService _chatService;
    private readonly IHistoryService _historyService;
    private readonly SemanticTextMemory _memory;
    private readonly ILogger<ChatService> _logger;

    public ChatService(
        Kernel kernel,
        IHistoryService historyService,
        SemanticTextMemory memory,
        ILogger<ChatService> logger)
    {
        _kernel = kernel;
        _chatService = kernel.GetRequiredService<IChatCompletionService>();
        _historyService = historyService;
        _memory = memory;
        _logger = logger;
    }

    public async Task<ChatResponse> ChatAsync(string userId, string message)
    {
        var chatHistory = await BuildChatHistoryAsync(userId, message);
        
        var settings = new OpenAIPromptExecutionSettings
        {
            FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
        };

        var response = await _chatService.GetChatMessageContentAsync(
            chatHistory,
            settings,
            _kernel
        );

        await SaveConversationAsync(userId, message, response.Content!);
        await LearnUserPreferencesAsync(userId, message, response.Content!);

        return new ChatResponse
        {
            Message = response.Content!,
            Timestamp = DateTime.UtcNow
        };
    }

    public async IAsyncEnumerable<string> ChatStreamAsync(
        string userId,
        string message)
    {
        var chatHistory = await BuildChatHistoryAsync(userId, message);
        
        var settings = new OpenAIPromptExecutionSettings
        {
            FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
        };

        var fullResponse = new StringBuilder();

        await foreach (var chunk in _chatService.GetStreamingChatMessageContentsAsync(
            chatHistory, settings, _kernel))
        {
            if (chunk.Content != null)
            {
                fullResponse.Append(chunk.Content);
                yield return chunk.Content;
            }
        }

        await SaveConversationAsync(userId, message, fullResponse.ToString());
    }

    private async Task<ChatHistory> BuildChatHistoryAsync(string userId, string message)
    {
        var chatHistory = new ChatHistory();
        
        chatHistory.AddSystemMessage(await BuildSystemPromptAsync(userId));
        
        var history = await _historyService.GetHistoryAsync(userId, limit: 10);
        foreach (var msg in history)
        {
            chatHistory.AddUserMessage(msg.UserMessage);
            chatHistory.AddAssistantMessage(msg.AssistantMessage);
        }
        
        var relevantMemories = _memory.SearchAsync(
            "user-preferences",
            message,
            limit: 3
        );

        var contextBuilder = new StringBuilder();
        await foreach (var memory in relevantMemories)
        {
            contextBuilder.AppendLine($"- {memory.Metadata.Text}");
        }

        if (contextBuilder.Length > 0)
        {
            chatHistory.AddSystemMessage($"用户偏好信息:\n{contextBuilder}");
        }

        chatHistory.AddUserMessage(message);
        
        return chatHistory;
    }

    private async Task<string> BuildSystemPromptAsync(string userId)
    {
        var userProfile = await _historyService.GetUserProfileAsync(userId);
        
        return $"""
            你是一个友好的 AI 助手。
            
            用户信息:
            - 用户ID: {userId}
            - 对话次数: {userProfile?.ConversationCount ?? 0}
            
            你的职责:
            1. 友好地回答用户问题
            2. 记住用户的偏好和重要信息
            3. 在需要时使用工具帮助用户
            4. 保持对话的连贯性
            """;
    }

    private async Task SaveConversationAsync(
        string userId,
        string userMessage,
        string assistantMessage)
    {
        await _historyService.SaveMessageAsync(userId, userMessage, assistantMessage);
    }

    private async Task LearnUserPreferencesAsync(
        string userId,
        string userMessage,
        string assistantMessage)
    {
        var extractPrompt = $"""
            从以下对话中提取用户的偏好或重要信息。
            如果没有重要信息,返回"无"。
            
            用户: {userMessage}
            助手: {assistantMessage}
            """;

        var info = await _kernel.InvokePromptAsync(extractPrompt);
        
        if (info.ToString() != "无")
        {
            await _memory.SaveInformationAsync(
                "user-preferences",
                text: info.ToString(),
                id: $"{userId}-{Guid.NewGuid()}"
            );
        }
    }

    public async Task ClearHistoryAsync(string userId)
    {
        await _historyService.ClearHistoryAsync(userId);
    }
}

3. 插件实现 #

csharp
public class WeatherPlugin
{
    private readonly IWeatherService _weatherService;

    public WeatherPlugin(IWeatherService weatherService)
    {
        _weatherService = weatherService;
    }

    [KernelFunction("get_weather")]
    [Description("获取指定城市的天气信息")]
    public async Task<string> GetWeatherAsync(
        [Description("城市名称")] string city)
    {
        var weather = await _weatherService.GetWeatherAsync(city);
        return $"{city}天气:{weather.Condition},气温{weather.Temperature}°C";
    }
}

public class CalculatorPlugin
{
    [KernelFunction("calculate")]
    [Description("执行数学计算")]
    public double Calculate(
        [Description("数学表达式")] string expression)
    {
        try
        {
            var result = new DataTable().Compute(expression, null);
            return Convert.ToDouble(result);
        }
        catch
        {
            throw new KernelException("无法计算该表达式");
        }
    }
}

public class SearchPlugin
{
    private readonly ISearchService _searchService;

    public SearchPlugin(ISearchService searchService)
    {
        _searchService = searchService;
    }

    [KernelFunction("web_search")]
    [Description("搜索网络信息")]
    public async Task<string> SearchAsync(
        [Description("搜索关键词")] string query)
    {
        var results = await _searchService.SearchAsync(query);
        return string.Join("\n", results.Select(r => $"- {r.Title}: {r.Snippet}"));
    }
}

4. API 控制器 #

csharp
[ApiController]
[Route("api/[controller]")]
public class ChatController : ControllerBase
{
    private readonly IChatService _chatService;

    public ChatController(IChatService chatService)
    {
        _chatService = chatService;
    }

    [HttpPost]
    public async Task<ActionResult<ChatResponse>> Chat([FromBody] ChatRequest request)
    {
        var response = await _chatService.ChatAsync(request.UserId, request.Message);
        return Ok(response);
    }

    [HttpPost("stream")]
    public async Task StreamChat([FromBody] ChatRequest request)
    {
        Response.Headers.Append("Content-Type", "text/event-stream");
        Response.Headers.Append("Cache-Control", "no-cache");

        await foreach (var chunk in _chatService.ChatStreamAsync(
            request.UserId, request.Message))
        {
            await Response.WriteAsync($"data: {chunk}\n\n");
            await Response.Body.FlushAsync();
        }

        await Response.WriteAsync("data: [DONE]\n\n");
    }

    [HttpDelete("history/{userId}")]
    public async Task<IActionResult> ClearHistory(string userId)
    {
        await _chatService.ClearHistoryAsync(userId);
        return NoContent();
    }
}

5. SignalR Hub #

csharp
public class ChatHub : Hub
{
    private readonly IChatService _chatService;

    public ChatHub(IChatService chatService)
    {
        _chatService = chatService;
    }

    public async IAsyncEnumerable<string> StreamChat(
        string userId,
        string message,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await foreach (var chunk in _chatService.ChatStreamAsync(userId, message))
        {
            cancellationToken.ThrowIfCancellationRequested();
            yield return chunk;
        }
    }
}

6. 程序配置 #

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddSignalR();

builder.Services.AddKernel()
    .AddAzureOpenAIChatCompletion(
        deploymentName: builder.Configuration["AzureOpenAI:DeploymentName"]!,
        endpoint: builder.Configuration["AzureOpenAI:Endpoint"]!,
        apiKey: builder.Configuration["AzureOpenAI:ApiKey"]!
    )
    .AddOpenAITextEmbeddingGeneration(
        modelId: "text-embedding-3-small",
        apiKey: builder.Configuration["OpenAI:ApiKey"]!
    );

builder.Services.AddSingleton<ISearchService, SearchService>();
builder.Services.AddSingleton<IWeatherService, WeatherService>();
builder.Services.AddScoped<IHistoryService, HistoryService>();
builder.Services.AddScoped<IChatService, ChatService>();

builder.Services.AddSingleton<SemanticTextMemory>(sp =>
{
    var embeddingService = sp.GetRequiredService<ITextEmbeddingGenerationService>();
    return new SemanticTextMemory(new VolatileMemoryStore(), embeddingService);
});

builder.Services.AddTransient<WeatherPlugin>();
builder.Services.AddTransient<CalculatorPlugin>();
builder.Services.AddTransient<SearchPlugin>();

var app = builder.Build();

app.MapControllers();
app.MapHub<ChatHub>("/chat");

app.Run();

前端示例 #

html
<!DOCTYPE html>
<html>
<head>
    <title>智能聊天机器人</title>
    <script src="https://cdn.jsdelivr.net/npm/microsoft/signalr"></script>
</head>
<body>
    <div id="chat-container">
        <div id="messages"></div>
        <input type="text" id="user-input" placeholder="输入消息...">
        <button onclick="sendMessage()">发送</button>
    </div>

    <script>
        const connection = new signalR.HubConnectionBuilder()
            .withUrl("/chat")
            .build();

        let currentAssistantMessage = "";

        connection.on("ReceiveMessage", (chunk) => {
            if (chunk === "[DONE]") {
                addMessage("assistant", currentAssistantMessage);
                currentAssistantMessage = "";
                return;
            }
            currentAssistantMessage += chunk;
            updateLastMessage(currentAssistantMessage);
        });

        async function sendMessage() {
            const input = document.getElementById("user-input");
            const message = input.value;
            
            if (!message) return;
            
            addMessage("user", message);
            input.value = "";

            await connection.invoke("StreamChat", "user-1", message);
        }

        function addMessage(role, content) {
            const div = document.createElement("div");
            div.className = `message ${role}`;
            div.textContent = content;
            document.getElementById("messages").appendChild(div);
        }

        function updateLastMessage(content) {
            const messages = document.getElementById("messages");
            const lastMessage = messages.lastElementChild;
            if (lastMessage && lastMessage.classList.contains("assistant")) {
                lastMessage.textContent = content;
            } else {
                addMessage("assistant", content);
            }
        }

        connection.start();
    </script>
</body>
</html>

最佳实践 #

1. 错误处理 #

csharp
public async Task<ChatResponse> ChatAsync(string userId, string message)
{
    try
    {
        // 处理逻辑
    }
    catch (HttpOperationException ex)
    {
        _logger.LogError(ex, "API 调用失败");
        return new ChatResponse
        {
            Message = "抱歉,服务暂时不可用,请稍后再试。",
            Timestamp = DateTime.UtcNow
        };
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "处理消息失败");
        throw;
    }
}

2. 限流保护 #

csharp
public class RateLimitingFilter : IFunctionFilter
{
    private readonly IMemoryCache _cache;
    private readonly int _maxRequestsPerMinute;

    public async Task OnFunctionInvocationAsync(
        FunctionInvocationContext context,
        Func<FunctionInvocationContext, Task> next)
    {
        var userId = context.Arguments["userId"]?.ToString();
        var cacheKey = $"rate_limit_{userId}";
        
        var count = _cache.GetOrCreate(cacheKey, entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
            return 0;
        });

        if (count >= _maxRequestsPerMinute)
        {
            throw new KernelException("请求过于频繁,请稍后再试");
        }

        _cache.Set(cacheKey, count + 1);
        await next(context);
    }
}

下一步 #

现在你已经掌握了聊天机器人的开发,接下来学习 文档问答系统,构建企业级知识库!

最后更新:2026-04-04