【译】使用 .NET 可组合 AI 技术栈构建 AI 驱动的会议应用
原文链接https://devblogs.microsoft.com/dotnet/building-ai-conference-app-dotnet-composable-stack/在 .NET 应用中构建 AI 功能往往意味着要把来自不同生态系统的模型、向量数据库、数据摄入管道和智能体框架拼接在一起。每个组件都有各自的模式、各自的客户端库以及下一个版本发布时各自的破坏性变更。我们一直在研发一套可组合、可扩展的构建模块为所有这些关注点提供稳定的抽象层。我们很高兴向您介绍我们是如何将它们组合在一起使用的。为了在 MVP 峰会上的一场分享我们构建了一个名为ConferencePulse的交互式会议助手。它能实时发起投票、即时回答观众问题、从互动数据中生成洞察并在会议结束时生成总结。我们使用的正是我们在现场演示的那套技术Microsoft.Extensions.AI、Microsoft.Extensions.DataIngestion、Microsoft.Extensions.VectorData、模型上下文协议MCP以及 Microsoft Agent Framework。本文将带您了解这个应用并展示每个构建模块是如何各司其职的。我们构建了什么ConferencePulse 是一个用于现场会议的 Blazor Server 应用。与会者扫描二维码加入会议通过投票和问答与演讲者互动。在后端AI 驱动了多项功能•实时投票AI 根据会议内容生成投票题目与会者投票后结果实时展示。•观众问答AI 通过 RAG 管道回答问题知识来源包括会议知识库、Microsoft Learn 文档和 GitHub Wiki 内容。•自动生成洞察随着投票结果和观众问题不断涌入系统自动挖掘其中的规律。•会议总结演讲者结束会议时触发。多个 AI 智能体并发分析投票、问题和洞察再将各自的发现合并汇总。ConferencePulse 演讲者仪表板展示实时投票结果、观众问题和 AI 生成的洞察我们想要的是一场互动体验而不是一摞幻灯片。我们想要投票和观众洞察。我们还想自动化准备工作只需把应用指向一个 GitHub 仓库它就会下载 Markdown 文件通过管道处理构建出可检索的知识库。投票题目、讨论要点和问答答案都以该内容为基础。应用基于 .NET 10、Blazor Server 和 Aspire 运行。五个项目覆盖整个技术栈src/ ├── ConferenceAssistant.Web/ ← Blazor ServerUI 编排 ├── ConferenceAssistant.Core/ ← 模型、接口、会话状态 ├── ConferenceAssistant.Ingestion/ ← 数据摄入管道 向量检索 ├── ConferenceAssistant.Agents/ ← AI 智能体、工作流、工具 ├── ConferenceAssistant.Mcp/ ← MCP 服务端工具 MCP 客户端 └── ConferenceAssistant.AppHost/ ← .NET AspireQdrant、PostgreSQL、Azure OpenAI下面我们逐一介绍各个构建模块。Microsoft.Extensions.AI一个接口支持任意提供商Microsoft.Extensions.AI为您提供了IChatClient——一个统一的抽象支持 OpenAI、Azure OpenAI、Ollama、Foundry Local 及其他提供商。ConferencePulse 中的每一次 AI 调用都经过同一个中间件管道。var openaiBuilder builder.AddAzureOpenAIClient(openai); openaiBuilder.AddChatClient(chat) .UseFunctionInvocation() .UseOpenTelemetry() .UseLogging(); openaiBuilder.AddEmbeddingGenerator(embedding);就这些六行代码。如果您熟悉 ASP.NET Core 中间件这个模式会让您感到亲切。每个.Use*()调用都在内层客户端之上包裹一层额外的行为UseFunctionInvocation()处理工具调用循环UseOpenTelemetry()追踪每次调用UseLogging()捕获请求/响应对。想把 Azure OpenAI 换成 Ollama更换内层客户端即可中间件保持不变。这一点至关重要因为IChatClient贯穿整个应用投票生成、问答、洞察、摄入数据富化以及多智能体工作流全都共享这条管道。注册一次处处可用。DataIngestion VectorData知识层AI 模型需要上下文才能给出有用的回答。Microsoft.Extensions.DataIngestion提供了一条将文档处理成可检索片段的管道Microsoft.Extensions.VectorData则提供了跨向量存储后端的提供商无关抽象。ConferencePulse 从 GitHub 仓库导入内容时会将文件送入摄入管道IngestionDocumentReader reader new MarkdownReader(); var tokenizer TiktokenTokenizer.CreateForModel(gpt-4o); var chunkerOptions new IngestionChunkerOptions(tokenizer) { MaxTokensPerChunk 500, OverlapTokens 50 }; IngestionChunkerstring chunker new HeaderChunker(chunkerOptions); var enricherOptions new EnricherOptions(_chatClient) { LoggerFactory _loggerFactory }; using var writer new VectorStoreWriterstring( _searchService.VectorStore, dimensionCount: 1536, new VectorStoreWriterOptions { CollectionName conference_knowledge, IncrementalIngestion true }); using IngestionPipelinestring pipeline new( reader, chunker, writer, new IngestionPipelineOptions(), _loggerFactory) { ChunkProcessors { new SummaryEnricher(enricherOptions), new KeywordEnricher(enricherOptions, ReadOnlySpanstring.Empty), frontMatterProcessor } };管道读取 Markdown按标题切块用 AI 生成的摘要和关键词对每个块进行富化然后向量化并存储到 Qdrant。每个步骤都是可插拔的组件可以把MarkdownReader换成 PDF 读取器把HeaderChunker换成固定大小切分器或者把 Qdrant 换成 Azure AI Search——管道组合方式保持不变。注意SummaryEnricher和KeywordEnricher都接受EnricherOptions(_chatClient)使用的正是上一节中同一个IChatClient。AI 在为自身的上下文做富化摘要富化器为每个块生成简洁描述关键词富化器提取可检索的词条二者共同提升后续的检索质量。在查询侧Microsoft.Extensions.VectorData提供了VectorStoreCollection可对任意后端进行语义检索var results collection.SearchAsync(query, topK); await foreach (var result in results) { var content result.Record[content] as string; // 使用检索到的内容... }正如在 EF Core 中可以切换数据库提供商一样这里同样可以切换向量存储提供商。今天用 Qdrant明天用 Azure AI SearchAPI 保持不变。ConferencePulse 还会随着会议进行实时摄入数据。投票回复、观众问题、问答对以及 AI 生成的洞察都会进入知识库public async Taskint IngestResponseAsync( string pollId, string topicId, string question, Dictionarystring, int results, Liststring? otherResponses null) { var sb new StringBuilder(); sb.AppendLine($Poll: {question}); sb.AppendLine(Results:); var total results.Values.Sum(); foreach (var (option, count) in results) { var percentage total 0 ? (count * 100.0 / total).ToString(F1) : 0; sb.AppendLine($ - {option}: {count} votes ({percentage}%)); } await _searchService.UpsertAsync(sb.ToString(), source: response, documentId: $response-{pollId}); return 1; }到一场会议结束时知识库中已包含最初导入的内容、每条投票结果、每个观众问题以及每条 AI 生成的洞察。AI 助手从投票结果和观众问题中实时生成洞察IChatClient 与工具选择合适的复杂度我们遵循的设计原则之一使用能完成任务的最简方案。IChatClient加工具能处理大量场景不必动用专门的智能体框架。但当编排逻辑变得复杂时框架的价值就体现出来了。关键在于选择合适的工具。ConferencePulse 有三个 AI 驱动的功能复杂程度各不相同但都使用同一个IChatClient。洞察生成单次调用投票关闭后ConferencePulse 生成一条洞察。实现方式是单次GetResponseAsync调用var response await chatClient.GetResponseAsync( [ new(ChatRole.System, You are a conference analytics assistant generating real-time insights from audience data.), new(ChatRole.User, prompt) // prompt 包含投票结果 ]); var content response.Text?.Trim(); if (!string.IsNullOrWhiteSpace(content)) { ctx.AddInsight(new Insight { TopicId poll.TopicId, PollId pollId, Content content, Type InsightType.PollAnalysis }); }无需工具无需框架。一条以投票结果为上下文的提示中间件管道负责遥测和日志记录。投票生成IChatClient 加工具生成投票需要更多上下文。AI 需要检查当前议题、回顾已涵盖的内容再创作出有针对性的题目。这就需要工具了public class PollGenerationWorkflow(IChatClient chatClient, AgentTools tools) { public async Taskstring ExecuteAsync(string topicId) { var options new ChatOptions { Tools [tools.GetCurrentTopic, tools.SearchKnowledge, tools.GetAudienceQuestions, tools.GetAllPollResults, tools.GetAllInsights, tools.CreatePoll] }; var messages new ListChatMessage { new(ChatRole.System, AgentDefinitions.SurveyArchitectInstructions), new(ChatRole.User, $Generate an engaging poll for topic: {topicId}...) }; var response await chatClient.GetResponseAsync(messages, options); return response.Text ?? Unable to generate poll.; } }每个工具都是通过 C# 方法创建的强类型AITool属性public class AgentTools { public AITool SearchKnowledge { get; } public AITool GetCurrentTopic { get; } public AITool CreatePoll { get; } // ... public AgentTools(IPollService pollService, ISemanticSearchService searchService, ...) { SearchKnowledge AIFunctionFactory.Create(SearchKnowledgeCore, new AIFunctionFactoryOptions { Name nameof(SearchKnowledge), Description Search the session knowledge base for content related to the query }); // ... } }模型判断需要上下文便调用GetCurrentTopic和SearchKnowledge再生成投票最后调用CreatePoll保存结果。UseFunctionInvocation()中间件自动处理工具调用循环。AI 助手在会议室视图中发起投票问答回答跨多来源的 RAG问答服务将多个构建模块融合在一起。当观众提问时应用会检索本地知识库、通过 MCP 查询 Microsoft Learn 文档并通过 MCP 向 DeepWiki 询问相关 GitHub 仓库最后综合生成答案// 1. 检索本地知识库 var searchResults await searchService.SearchAsync(questionText, topK: 5); var localContext string.Join(\n\n---\n\n, searchResults.Select(r r.Content).Where(c !string.IsNullOrWhiteSpace(c))); // 2. 通过 MCP 检索 Microsoft Learn 文档上下文 var docsContext await mcpClient.SearchDocsAsync(questionText); // 3. 通过 MCP 向 DeepWiki 查询相关 .NET 仓库 var deepWikiContext await mcpClient.AskDeepWikiAsync(dotnet/extensions, questionText);VectorData 负责本地检索MCP 负责外部上下文IChatClient负责生成答案。AI 助手问答界面接下来看看 MCP 是如何工作的。MCP消费与提供上下文模型上下文协议MCP是一套让 AI 应用发现并使用外部工具和上下文的标准。就像 HTTP 让任意客户端能与任意服务器通信一样MCP 让任意 AI 应用能用同一套协议连接任意上下文提供商。ConferencePulse 在两个方向上都使用了 MCP。作为消费方McpContentClient在启动时连接两个 MCP 服务器Microsoft Learn 和 DeepWiki。public async Task InitializeAsync(CancellationToken ct default) { var learnTransport new HttpClientTransport(new HttpClientTransportOptions { Endpoint new Uri(https://learn.microsoft.com/api/mcp), TransportMode HttpTransportMode.StreamableHttp }, loggerFactory); _learnClient await McpClient.CreateAsync(learnTransport, null, loggerFactory, ct); var deepWikiTransport new HttpClientTransport(new HttpClientTransportOptions { Endpoint new Uri(https://mcp.deepwiki.com/mcp), TransportMode HttpTransportMode.StreamableHttp }, loggerFactory); _deepWikiClient await McpClient.CreateAsync(deepWikiTransport, null, loggerFactory, ct); }连接建立后调用任意 MCP 服务器上的工具都遵循同一模式var result await _learnClient.CallToolAsync( microsoft_docs_search, new Dictionarystring, object? { [query] query }, cancellationToken: ct);任何支持 MCP 的服务器都能与这段客户端代码配合使用。作为提供方ConferencePulse 同时也是一个 MCP 服务器。任何兼容 MCP 的客户端GitHub Copilot、Claude、自定义工具都可以连接并查询会议数据。[McpServerToolType] public class ConferenceTools { [McpServerTool(Name get_session_status, ReadOnly true), Description(Returns the current conference session status.)] public static string GetSessionStatus(ISessionService sessionService) { var session sessionService.CurrentSession; if (session is null) return No active conference session.; // ... 构建状态字符串 } [McpServerTool(Name search_session_knowledge, ReadOnly true), Description(Searches the session knowledge base for relevant content.)] public static async Taskstring SearchSessionKnowledge( ISemanticSearchService searchService, [Description(The search query.)] string query, [Description(Max results. Defaults to 5.)] int maxResults 5) { var results await searchService.SearchAsync(query, maxResults); // ... 格式化结果 } }在Program.cs中只需几行代码即可完成注册builder.Services .AddMcpServer(options { options.ServerInfo new() { Name ConferencePulse, Version 1.0.0 }; }) .WithToolsFromAssembly(typeof(ConferenceTools).Assembly) .WithHttpTransport(); app.MapMcp(/mcp);应用消费外部知识来回答问题同时也向外部工具提供自身数据。两个方向同一套协议。Microsoft Agent Framework多智能体编排对于 ConferencePulse 的大多数功能IChatClient加工具已是正确选择。但会议总结需要的不止于此三个专业智能体并发运行各自拥有作用域内的工具将结果汇入一个综合步骤。这正是 Microsoft Agent Framework 的用武之地。public class SessionSummaryWorkflow(IChatClient chatClient, AgentTools tools) { public async Taskstring ExecuteAsync() { ChatClientAgent pollAnalyst new(chatClient, name: PollAnalyst, description: Analyzes poll results and trends, instructions: You are a poll analyst. Use GetAllPollResults to retrieve every poll..., tools: [tools.GetAllPollResults]); ChatClientAgent questionAnalyst new(chatClient, name: QuestionAnalyst, description: Analyzes audience questions and themes, instructions: You are an audience question analyst..., tools: [tools.GetAudienceQuestions]); ChatClientAgent insightAnalyst new(chatClient, name: InsightAnalyst, description: Analyzes generated insights and knowledge patterns, instructions: You are an insight analyst..., tools: [tools.GetAllInsights, tools.SearchKnowledge]);每个ChatClientAgent都封装了同一个IChatClient。各智能体拥有各自作用域的工具PollAnalyst 只能看到投票数据QuestionAnalyst 只能看到问题和专属指令。编排层使用AgentWorkflowBuilder.BuildConcurrent进行扇出再用WorkflowBuilder组合完整的管道// 扇出三个分析智能体并发运行 var analysisWorkflow AgentWorkflowBuilder.BuildConcurrent( [pollAnalyst, questionAnalyst, insightAnalyst], MergeAgentOutputs); // 扇入综合智能体合并所有发现 ChatClientAgent synthesizer new(chatClient, name: Synthesizer, instructions: Synthesize the analyses into one cohesive session summary...); // 组合并发分析 → 串行综合 var analysisExec new SubworkflowBinding(analysisWorkflow, Analysis); ExecutorBinding synthExec synthesizer; var composedWorkflow new WorkflowBuilder(analysisExec) .WithName(SessionSummaryPipeline) .BindExecutor(synthExec) .AddEdge(analysisExec, synthExec) .WithOutputFrom([synthExec]) .Build(); var run await InProcessExecution.Default.RunAsync( composedWorkflow, Analyze the conference session data and provide your specialized findings.);对比之前的投票生成工作流使用IChatClient加工具约 10 行代码会议总结大约需要 40 行因为它确实需要并发智能体、作用域工具和综合步骤。在 ConferencePulse 中Agent Framework 仅为一个工作流而存在。其他所有功能直接使用IChatClient就运作良好。两种方式共用同一个底层抽象。各构建模块如何协同Aspire 仪表板展示 ConferencePulse 的各项服务Web 应用、Qdrant、PostgreSQL 和 Azure OpenAI在 MVP 峰会的分享过程中与会者体验了由技术栈不同层次驱动的各项功能功能技术支撑投票IChatClient 工具MEAI知识接地IngestionPipelineVectorStoreWriter问答回答VectorDataIChatClient MCP自动生成洞察IChatClient单次调用会议总结Microsoft Agent Framework扇出/扇入可观测性UseOpenTelemetry() Aspire 仪表板基础设施AspireQdrant PostgreSQL Azure OpenAI每个构建模块只负责一个关注点并与其他模块组合使用。IChatClient出现在摄入数据富化器内部、智能体工具内部、MCP 增强的问答内部以及 Agent Framework 的ChatClientAgent内部。学一次处处可用。提供商会更换模型会演进。这套构建模块为您提供了稳定的基础层底层实现随时可换无需重写应用代码。开始上手我们迫不及待想看到您用这些构建模块打造出什么。•体验 ConferencePulse源代码已发布至 GitHub[1]。克隆项目运行aspire run即可亲身体验完整的技术栈。•深入了解各个库• Microsoft.Extensions.AI[2]• Microsoft.Extensions.VectorData[3]• Microsoft.Extensions.DataIngestion[4]• .NET 中的模型上下文协议[5]• Microsoft Agent Framework[6]•反馈给我们在任一仓库提交 Issue或在 .NET 社区站会[7]与我们交流。现在您已经了解了这些构建模块的组合方式不妨亲自试试告诉我们您的想法。引用链接[1]源代码已发布至 GitHub:https://github.com/luisquintanilla/dotnet-ai-conference-assistant[2]Microsoft.Extensions.AI:https://learn.microsoft.com/dotnet/ai/ai-extensions[3]Microsoft.Extensions.VectorData:https://learn.microsoft.com/dotnet/ai/vector-stores/overview[4]Microsoft.Extensions.DataIngestion:https://learn.microsoft.com/dotnet/ai/conceptual/data-ingestion[5].NET 中的模型上下文协议:https://learn.microsoft.com/dotnet/ai/get-started-mcp[6]Microsoft Agent Framework:https://github.com/microsoft/agent-framework[7].NET 社区站会:https://dotnet.microsoft.com/live