王君涵 發表於 2025-10-14 11:23:00

多智能体微服务实战(3/4):Aspire 打造本地 K8s 开发环境

<h1 id="net-aspire如何模拟企业k8s环境---实战演练">.NET Aspire如何模拟企业K8s环境 - 实战演练</h1>
<h2 id="引言">引言</h2>
<p>在前两篇文章中,我们讨论了为什么需要多智能体协作系统,以及如何使用MCP、A2A和Agent Framework实现智能体间的标准化通信。但有一个实际问题还没有解决:</p>
<p><strong>开发一个由5个微服务+前端组成的系统,本地怎么调试?</strong></p>
<p>想象一下传统的开发场景:</p>
<pre><code class="language-powershell"># 终端窗口1
cd Finance
dotnet run --urls "https://localhost:7043"

# 终端窗口2
cd Tech
dotnet run --urls "https://localhost:7063"

# 终端窗口3
cd HumanResource
dotnet run --urls "https://localhost:7202"

# 终端窗口4
cd QA
dotnet run --urls "https://localhost:7153"

# 终端窗口5
cd PMO
dotnet run --urls "https://localhost:7125"

# 终端窗口6
cd AgentFrameworkAspire.Web
dotnet run --urls "https://localhost:7223"
</code></pre>
<p><strong>6个终端窗口、6组命令、需要记住6个端口号、配置6次OpenAI API Key...</strong></p>
<p>更糟糕的是:</p>
<ul>
<li>如何确保服务按正确顺序启动?</li>
<li>如何让Web前端自动发现其他服务的URL?</li>
<li>如何统一查看所有服务的日志?</li>
<li>如何追踪跨服务的请求链路?</li>
</ul>
<p><strong>这就是.NET Aspire要解决的问题。</strong></p>
<hr>
<h2 id="一什么是net-aspire">一、什么是.NET Aspire?</h2>
<h3 id="11-官方定义">1.1 官方定义</h3>
<p>.NET Aspire是一个<strong>云原生应用开发栈</strong>,专为.NET设计,旨在简化分布式应用的开发、测试和部署。</p>
<p>但这个定义太官方了。用人话说:</p>
<blockquote>
<p>Aspire = Docker Compose + 服务发现 + 配置管理 + 可视化Dashboard + 一键部署</p>
</blockquote>
<h3 id="12-核心价值">1.2 核心价值</h3>
<table>
<thead>
<tr>
<th>痛点</th>
<th>传统方案</th>
<th>Aspire解决方案</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>启动多服务</strong></td>
<td>打开多个终端,逐个启动</td>
<td>一行命令启动所有</td>
</tr>
<tr>
<td><strong>端口管理</strong></td>
<td>手动指定端口,冲突频发</td>
<td>自动分配动态端口</td>
</tr>
<tr>
<td><strong>配置共享</strong></td>
<td>每个服务都要配置一遍</td>
<td>参数定义一次,自动注入</td>
</tr>
<tr>
<td><strong>服务发现</strong></td>
<td>硬编码URL或手动配置</td>
<td>自动服务发现</td>
</tr>
<tr>
<td><strong>日志查看</strong></td>
<td>切换窗口查看不同服务</td>
<td>Dashboard统一查看</td>
</tr>
<tr>
<td><strong>分布式追踪</strong></td>
<td>需要额外配置APM工具</td>
<td>内置OpenTelemetry</td>
</tr>
<tr>
<td><strong>部署到云</strong></td>
<td>写Dockerfile、K8s manifest</td>
<td>一行命令部署到ACA</td>
</tr>
</tbody>
</table>
<h3 id="13-aspire-vs-kubernetes架构对比">1.3 Aspire vs Kubernetes架构对比</h3>
<div class="mermaid">graph TB
    subgraph Local ["本地开发环境"]
      direction LR
      subgraph Aspire ["Aspire方案 - 轻量级"]
            AspireDash
            AspireHost
            
            AspireHost --&gt; |动态分配端口| AspireFinance
            AspireHost --&gt; |动态分配端口| AspireTech
            AspireHost --&gt; |动态分配端口| AspireWeb
            
            AspireDash -.监控.- AspireFinance
            AspireDash -.监控.- AspireTech
            AspireDash -.监控.- AspireWeb
            
            AspireFinance -.服务发现.- AspireWeb
            AspireTech -.服务发现.- AspireWeb
      end
      
      subgraph K8sLocal ["Kubernetes方案 - 重量级"]
            K8sDash
            DockerDesktop
            
            DockerDesktop --&gt; |YAML配置| K8sFinance
            DockerDesktop --&gt; |YAML配置| K8sTech
            DockerDesktop --&gt; |YAML配置| K8sWeb
            
            DockerDesktop --&gt; |Service| K8sSvc1
            DockerDesktop --&gt; |Service| K8sSvc2
            
            K8sFinance --&gt; K8sSvc1
            K8sTech --&gt; K8sSvc2
            K8sSvc1 --&gt; K8sWeb
            K8sSvc2 --&gt; K8sWeb
      end
    end
   
    subgraph Prod ["生产环境"]
      subgraph ACA ["Azure Container Apps&lt;br/&gt;(Aspire推荐)"]
            ACADeploy
            ACADeploy --&gt; ACAFinance
            ACADeploy --&gt; ACATech
            ACADeploy --&gt; ACAWeb
      end
      
      subgraph K8sProd ["Kubernetes&lt;br/&gt;(通用方案)"]
            K8sProdDeploy
            K8sProdDeploy --&gt; K8sProdFinance
            K8sProdDeploy --&gt; K8sProdTech
            K8sProdDeploy --&gt; K8sProdWeb
      end
    end
   
    Aspire -.一键部署.- ACA
    K8sLocal -.迁移.- K8sProd
   
    style AspireDash fill:#48c774,stroke:#333,color:#fff
    style AspireHost fill:#3273dc,stroke:#333,color:#fff
    style K8sDash fill:#ff6b6b,stroke:#333,color:#fff
    style DockerDesktop fill:#ff9800,stroke:#333,color:#fff
    style ACADeploy fill:#9b59b6,stroke:#333,color:#fff
    style K8sProdDeploy fill:#9b59b6,stroke:#333,color:#fff
    style Aspire fill:#e8f5e9,stroke:#4caf50,stroke-width:3px
    style K8sLocal fill:#ffebee,stroke:#f44336,stroke-width:3px
    style ACA fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
    style K8sProd fill:#e3f2fd,stroke:#2196f3,stroke-width:2px
</div><p><strong>关键对比</strong>:</p>
<table>
<thead>
<tr>
<th>维度</th>
<th>Aspire本地开发</th>
<th>Kubernetes本地开发</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>启动复杂度</strong></td>
<td>✅ 一行命令:<code>dotnet run</code></td>
<td>❌ 多步骤:构建镜像→编写YAML→kubectl apply</td>
</tr>
<tr>
<td><strong>运行环境</strong></td>
<td>✅ 直接运行.NET进程</td>
<td>❌ 需要Docker + K8s集群</td>
</tr>
<tr>
<td><strong>配置管理</strong></td>
<td>✅ C#代码配置(类型安全)</td>
<td>❌ YAML配置(易出错)</td>
</tr>
<tr>
<td><strong>服务发现</strong></td>
<td>✅ 自动注入环境变量</td>
<td>⚠️ 需要配置Service和DNS</td>
</tr>
<tr>
<td><strong>可视化</strong></td>
<td>✅ 内置Dashboard</td>
<td>⚠️ 需要安装额外工具</td>
</tr>
<tr>
<td><strong>学习成本</strong></td>
<td>✅ .NET开发者友好</td>
<td>❌ 需要学习容器和K8s概念</td>
</tr>
<tr>
<td><strong>调试体验</strong></td>
<td>✅ 原生.NET调试器</td>
<td>⚠️ 远程调试或日志调试</td>
</tr>
</tbody>
</table>
<p><strong>生产部署路径</strong>:</p>
<ol>
<li>
<p><strong>Aspire路径</strong>:</p>
<ul>
<li>本地:Aspire AppHost</li>
<li>生产:<code>azd deploy</code> → Azure Container Apps(自动生成容器镜像、配置、网络)</li>
</ul>
</li>
<li>
<p><strong>Kubernetes路径</strong>:</p>
<ul>
<li>本地:Minikube/Docker Desktop + YAML</li>
<li>生产:<code>kubectl apply</code> → Kubernetes集群(手动编写所有配置)</li>
</ul>
</li>
</ol>
<p><strong>结论</strong>:</p>
<ul>
<li>Aspire适合 <strong>.NET团队快速开发和Azure生态</strong></li>
<li>Kubernetes适合<strong>多语言团队和混合云环境</strong></li>
<li><strong>两者不是非此即彼</strong>:Aspire也可以生成Kubernetes manifest</li>
</ul>
<h3 id="14-aspire的核心价值">1.4 Aspire的核心价值</h3>
<table>
<thead>
<tr>
<th>痛点</th>
<th>传统方案</th>
<th>Aspire解决方案</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>启动多服务</strong></td>
<td>打开多个终端,逐个启动</td>
<td>一行命令启动所有</td>
</tr>
<tr>
<td><strong>端口管理</strong></td>
<td>手动指定端口,冲突频发</td>
<td>自动分配动态端口</td>
</tr>
<tr>
<td><strong>配置共享</strong></td>
<td>每个服务都要配置一遍</td>
<td>参数定义一次,自动注入</td>
</tr>
<tr>
<td><strong>服务发现</strong></td>
<td>硬编码URL或手动配置</td>
<td>自动服务发现</td>
</tr>
<tr>
<td><strong>日志查看</strong></td>
<td>切换窗口查看不同服务</td>
<td>Dashboard统一查看</td>
</tr>
<tr>
<td><strong>分布式追踪</strong></td>
<td>需要额外配置APM工具</td>
<td>内置OpenTelemetry</td>
</tr>
<tr>
<td><strong>部署到云</strong></td>
<td>写Dockerfile、K8s manifest</td>
<td>一行命令部署到ACA</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="二agentframeworkaspire中的aspire配置">二、AgentFrameworkAspire中的Aspire配置</h2>
<h3 id="21-项目结构">2.1 项目结构</h3>
<pre><code>AgentFrameworkAspire/
├── AgentFrameworkAspire.AppHost/   ← Aspire编排器
│   ├── Program.cs                     ← 核心配置文件
│   └── appsettings.json
├── AgentFrameworkAspire.ServiceDefaults/ ← 共享配置
│   ├── Extensions.cs
│   └── AgentHelpers.cs
├── Finance/                           ← 微服务1
├── Tech/                              ← 微服务2
├── HumanResource/                     ← 微服务3
├── QA/                              ← 微服务4
├── PMO/                               ← 微服务5
└── AgentFrameworkAspire.Web/         ← Web前端
</code></pre>
<p><strong>关键角色</strong>:</p>
<ul>
<li><strong>AppHost</strong>:指挥官,负责编排所有服务</li>
<li><strong>ServiceDefaults</strong>:后勤部,提供共享配置和辅助方法</li>
<li><strong>各微服务</strong>:士兵,实际执行任务</li>
</ul>
<h3 id="22-apphost的核心配置">2.2 AppHost的核心配置</h3>
<p>让我们深入<code>AgentFrameworkAspire.AppHost/Program.cs</code>:</p>
<pre><code class="language-csharp">var builder = DistributedApplication.CreateBuilder(args);

// ============================================================
// 第一部分:参数定义(Parameter Definition)
// ============================================================

// openai-endpoint:可选参数,默认为官方API
var openAiEndpoint = builder.AddParameter(
    "openai-endpoint",
    "https://api.vveai.com/v1"// 默认值,可以留空表示使用OpenAI官方
);

// openai-apikey:密钥参数,不会明文显示
var openAiApiKey = builder.AddParameter(
    "openai-apikey",
    secret: true// 标记为敏感信息
);

// openai-deployment:模型名称,默认gpt-4o-mini
var openAiDeploymentName = builder.AddParameter(
    "openai-deployment",
    "gpt-4o-mini"
);

// ============================================================
// 第二部分:微服务定义(Microservices Configuration)
// ============================================================

// Finance 服务
var financeService = builder.AddProject&lt;Projects.Finance&gt;("finance")
    .WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
    .WithEnvironment("OpenAI__ApiKey", openAiApiKey)
    .WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName);

// HR 服务
var hrService = builder.AddProject&lt;Projects.HumanResource&gt;("humanresource")
    .WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
    .WithEnvironment("OpenAI__ApiKey", openAiApiKey)
    .WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName);

// QA 服务
var qaService = builder.AddProject&lt;Projects.QA&gt;("qa")
    .WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
    .WithEnvironment("OpenAI__ApiKey", openAiApiKey)
    .WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName);

// Tech 服务
var techService = builder.AddProject&lt;Projects.Tech&gt;("tech")
    .WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
    .WithEnvironment("OpenAI__ApiKey", openAiApiKey)
    .WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName);

// PMO 服务
var pmoService = builder.AddProject&lt;Projects.PMO&gt;("pmo")
    .WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
    .WithEnvironment("OpenAI__ApiKey", openAiApiKey)
    .WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName);

// ============================================================
// 第三部分:Web前端配置(Web Frontend Configuration)
// ============================================================

builder.AddProject&lt;Projects.AgentFrameworkAspire_Web&gt;("webfrontend")
    .WithExternalHttpEndpoints()// 允许外部访问
    .WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
    .WithEnvironment("OpenAI__ApiKey", openAiApiKey)
    .WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName)
    // 引用所有specialist服务(实现服务发现)
    .WithReference(techService)
    .WithReference(hrService)
    .WithReference(financeService)
    .WithReference(qaService)
    .WithReference(pmoService)
    // 等待所有服务就绪后再启动
    .WaitFor(techService)
    .WaitFor(hrService)
    .WaitFor(financeService)
    .WaitFor(qaService)
    .WaitFor(pmoService);

builder.Build().Run();
</code></pre>
<p><strong>关键API解析</strong>:</p>
<h4 id="addparameter---参数定义"><code>AddParameter()</code> - 参数定义</h4>
<pre><code class="language-csharp">var param = builder.AddParameter(
    name: "openai-apikey",// 参数名(会提示用户输入或从配置读取)
    secret: true            // 是否是密钥(敏感信息)
);
</code></pre>
<p><strong>效果</strong>:</p>
<ul>
<li>如果没有在User Secrets中配置,启动时会提示输入</li>
<li><code>secret: true</code> 的参数输入时不显示明文</li>
<li>参数可以传递给任何服务</li>
</ul>
<h4 id="addprojectt---添加服务"><code>AddProject&lt;T&gt;()</code> - 添加服务</h4>
<pre><code class="language-csharp">var service = builder.AddProject&lt;Projects.Finance&gt;("finance");
</code></pre>
<p><strong>效果</strong>:</p>
<ul>
<li>Aspire会自动发现<code>Finance.csproj</code></li>
<li>自动编译和启动该项目</li>
<li>分配动态端口(避免冲突)</li>
</ul>
<h4 id="withenvironment---注入环境变量"><code>WithEnvironment()</code> - 注入环境变量</h4>
<pre><code class="language-csharp">service.WithEnvironment("OpenAI__ApiKey", openAiApiKey);
</code></pre>
<p><strong>效果</strong>:</p>
<ul>
<li>将参数作为环境变量注入到服务进程</li>
<li>服务中通过<code>IConfiguration</code>自动读取</li>
<li>使用<code>__</code>(双下划线)表示层级结构(<code>OpenAI:ApiKey</code>)</li>
</ul>
<h4 id="withreference---服务引用"><code>WithReference()</code> - 服务引用</h4>
<pre><code class="language-csharp">webfrontend.WithReference(financeService);
</code></pre>
<p><strong>效果</strong>:</p>
<ul>
<li>自动注入环境变量:<code>services__finance__http__0=http://localhost:xxxx</code></li>
<li>Web前端可以通过配置自动发现Finance服务的URL</li>
<li>支持HTTP和HTTPS端点</li>
</ul>
<h4 id="waitfor---启动依赖"><code>WaitFor()</code> - 启动依赖</h4>
<pre><code class="language-csharp">webfrontend.WaitFor(financeService);
</code></pre>
<p><strong>效果</strong>:</p>
<ul>
<li>Web前端会等待Finance服务启动完成</li>
<li>确保服务按正确顺序启动</li>
<li>避免"服务未就绪"错误</li>
</ul>
<h3 id="23-aspire服务发现机制图解">2.3 Aspire服务发现机制图解</h3>
<p>下面展示Aspire如何实现服务间的自动发现和通信:</p>
<div class="mermaid">graph TB
    subgraph AppHost
      Program
      Program --&gt; FinanceProj
      Program --&gt; TechProj
      Program --&gt; WebProj
      
      FinanceProj --&gt; |动态分配端口| FinancePort
      TechProj --&gt; |动态分配端口| TechPort
      
      WebProj --&gt; |WithReference| RefFinance[注入Finance引用]
      WebProj --&gt; |WithReference| RefTech[注入Tech引用]
    end
   
    subgraph WebService
      WebEnv[环境变量]
      WebEnv --&gt; |自动注入| EnvFinance["services__finance__https__0&lt;br/&gt;=https://localhost:7222"]
      WebEnv --&gt; |自动注入| EnvTech["services__tech__https__0&lt;br/&gt;=https://localhost:7063"]
      
      WebConfig
      EnvFinance --&gt; WebConfig
      EnvTech --&gt; WebConfig
      
      ServiceDiscovery
      WebConfig --&gt; ServiceDiscovery
      
      HttpClient
      ServiceDiscovery --&gt; HttpClient
      
      HttpClient --&gt; |解析服务名| ResolveFinance["http://finance"]
      HttpClient --&gt; |解析服务名| ResolveTech["http://tech"]
      
      ResolveFinance --&gt; |实际请求| ActualFinance["https://localhost:7222"]
      ResolveTech --&gt; |实际请求| ActualTech["https://localhost:7063"]
    end
   
    subgraph FinanceService
      FinanceAPI
    end
   
    subgraph TechService
      TechAPI
    end
   
    ActualFinance -.-&gt; FinanceAPI
    ActualTech -.-&gt; TechAPI
   
    style Program fill:#3273dc,stroke:#333,color:#fff
    style ServiceDiscovery fill:#9b59b6,stroke:#333,color:#fff
    style HttpClient fill:#48c774,stroke:#333,color:#fff
    style FinanceAPI fill:#ffe082,stroke:#333
    style TechAPI fill:#ffe082,stroke:#333
    style AppHost fill:#f0f0f0,stroke:#999,stroke-width:2px
    style WebService fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style FinanceService fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style TechService fill:#fff3e0,stroke:#ff9800,stroke-width:2px
</div><p><strong>服务发现流程详解</strong>:</p>
<ol>
<li>
<p><strong>AppHost编排阶段</strong>:</p>
<ul>
<li>AppHost通过<code>AddProject()</code>注册各个微服务</li>
<li>Aspire为每个服务动态分配端口(避免硬编码)</li>
<li>通过<code>WithReference()</code>建立服务依赖关系</li>
</ul>
</li>
<li>
<p><strong>环境变量注入</strong>:</p>
<ul>
<li>Aspire自动生成服务发现环境变量</li>
<li>格式:<code>services__&lt;服务名&gt;__&lt;协议&gt;__&lt;索引&gt;</code> = <code>URL</code></li>
<li>Web服务启动时自动读取这些环境变量到<code>IConfiguration</code></li>
</ul>
</li>
<li>
<p><strong>Service Discovery解析</strong>:</p>
<ul>
<li>Web服务中的<code>HttpClient</code>配置了<code>AddServiceDiscovery()</code></li>
<li>当代码请求<code>http://finance</code>时,Service Discovery拦截请求</li>
<li>从配置中查找实际URL并重写请求</li>
</ul>
</li>
<li>
<p><strong>实际通信</strong>:</p>
<ul>
<li>HttpClient使用解析后的真实URL(如<code>https://localhost:7222</code>)</li>
<li>请求到达目标服务</li>
<li>支持负载均衡、重试、断路器等弹性功能</li>
</ul>
</li>
</ol>
<p><strong>代码映射</strong>:</p>
<pre><code class="language-csharp">// AppHost/Program.cs - 编排层
var financeService = builder.AddProject&lt;Projects.Finance&gt;("finance");
var webfrontend = builder.AddProject&lt;Projects.AgentFrameworkAspire_Web&gt;("webfrontend")
    .WithReference(financeService);// 这里触发服务发现配置注入

// Web/Program.cs - 消费层
builder.Services.AddHttpClient("FinanceClient", client =&gt;
{
    client.BaseAddress = new Uri("http://finance");// 使用服务名而非硬编码URL
});
</code></pre>
<h3 id="24-servicedefaults的共享配置">2.4 ServiceDefaults的共享配置</h3>
<p>每个微服务都引用了<code>AgentFrameworkAspire.ServiceDefaults</code>项目,它提供了:</p>
<pre><code class="language-csharp">// AgentFrameworkAspire.ServiceDefaults/Extensions.cs
public static class Extensions
{
    public static IHostApplicationBuilder AddServiceDefaults(
      this IHostApplicationBuilder builder)
    {
      // 配置 OpenTelemetry(分布式追踪)
      builder.ConfigureOpenTelemetry();

      // 配置健康检查
      builder.AddDefaultHealthChecks();

      // 配置服务发现
      builder.Services.AddServiceDiscovery();

      // 配置HTTP客户端(支持服务发现)
      builder.Services.ConfigureHttpClientDefaults(http =&gt;
      {
            http.AddStandardResilienceHandler();// 添加重试、断路器等
            http.AddServiceDiscovery();         // 支持通过服务名调用
      });

      return builder;
    }
}
</code></pre>
<p><strong>每个微服务只需一行代码</strong>:</p>
<pre><code class="language-csharp">// Finance/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();// ← 添加所有标准配置
</code></pre>
<hr>
<h2 id="三实战从零启动系统">三、实战:从零启动系统</h2>
<h3 id="31-前置要求检查">3.1 前置要求检查</h3>
<pre><code class="language-powershell"># 检查 .NET SDK 版本
dotnet --version
# 应该显示 9.0.x 或更高

# 检查 Aspire Workload(首次使用需要安装)
dotnet workload list
# 如果没有 aspire,执行:
dotnet workload install aspire
</code></pre>
<h3 id="32-克隆项目">3.2 克隆项目</h3>
<pre><code class="language-powershell">git clone https://github.com/microsoft/agent-framework.git
cd AgentFrameworkAspire
</code></pre>
<h3 id="33-配置openai-api密钥">3.3 配置OpenAI API密钥</h3>
<pre><code class="language-powershell">cd AgentFrameworkAspire.AppHost

# 初始化 User Secrets
dotnet user-secrets init

# 配置参数(以Azure OpenAI为例)
dotnet user-secrets set "Parameters:openai-endpoint" "https://your-resource.openai.azure.com/"
dotnet user-secrets set "Parameters:openai-apikey" "your-api-key-here"
dotnet user-secrets set "Parameters:openai-deployment" "gpt-4o"
</code></pre>
<p><strong>验证配置</strong>:</p>
<pre><code class="language-powershell">dotnet user-secrets list
</code></pre>
<p>应该显示:</p>
<pre><code>Parameters:openai-endpoint = https://your-resource.openai.azure.com/
Parameters:openai-apikey = ********************************
Parameters:openai-deployment = gpt-4o
</code></pre>
<h3 id="34-启动系统">3.4 启动系统</h3>
<p><strong>方式1:Visual Studio</strong></p>
<ol>
<li>打开<code>AgentFrameworkAspire.slnx</code></li>
<li>设置<code>AgentFrameworkAspire.AppHost</code>为启动项目</li>
<li>按<code>F5</code>启动调试</li>
<li>Dashboard会自动在浏览器打开</li>
</ol>
<p><strong>方式2:命令行</strong></p>
<pre><code class="language-powershell">cd AgentFrameworkAspire.AppHost
dotnet run
</code></pre>
<h3 id="35-访问aspire-dashboard">3.5 访问Aspire Dashboard</h3>
<p>启动成功后,浏览器会自动打开Dashboard:<code>http://localhost:xxxxx</code></p>
<p>如果没有自动打开,手动访问即可。</p>
<hr>
<h2 id="四aspire-dashboard深度导览">四、Aspire Dashboard深度导览</h2>
<p>Dashboard是Aspire最强大的功能之一,让我们逐一探索。</p>
<h3 id="41-resources资源视图">4.1 Resources(资源视图)</h3>
<p>这是默认页面,显示所有服务的状态:</p>
<p><img src="https://img2024.cnblogs.com/blog/3358435/202510/3358435-20251014101414357-1018855667.png"></p>
<p><strong>功能</strong>:</p>
<ul>
<li><strong>State(状态)</strong>:Running、Starting、Stopped、Failed</li>
<li><strong>Endpoints(端点)</strong>:点击直接访问服务</li>
<li><strong>Logs(日志)</strong>:查看实时日志</li>
<li><strong>Details(详情)</strong>:查看环境变量、配置等</li>
</ul>
<p><strong>点击服务名称</strong>,可以看到详细信息:</p>
<p><img src="https://img2024.cnblogs.com/blog/3358435/202510/3358435-20251014111749569-760556392.png"></p>
<h3 id="42-console-logs日志视图">4.2 Console Logs(日志视图)</h3>
<p>点击任一服务的"Logs"按钮,进入日志页面:</p>
<p><img src="https://img2024.cnblogs.com/blog/3358435/202510/3358435-20251014111829571-2136182755.png"></p>
<p><strong>功能</strong>:</p>
<ul>
<li>实时滚动显示日志</li>
<li>支持日志级别过滤(Info、Warning、Error)</li>
<li>支持搜索关键词</li>
<li>支持下载日志文件</li>
</ul>
<p><strong>多服务日志对比</strong>:</p>
<p>可以同时打开多个服务的日志,放在不同标签页:</p>
<pre><code>
   ↓            ↓               ↓
Finance日志    Tech日志   Web前端日志
</code></pre>
<p>这样可以对比时间戳,追踪跨服务的调用链。</p>
<h3 id="43-traces追踪视图">4.3 Traces(追踪视图)</h3>
<p>这是<strong>分布式追踪</strong>功能,展示请求在服务间的流转:</p>
<p><strong>示例场景</strong>:用户在Web UI提交一个项目规划请求</p>
<p><img src="https://img2024.cnblogs.com/blog/3358435/202510/3358435-20251014111909235-904038780.png"></p>
<p><strong>价值</strong>:</p>
<ul>
<li><strong>性能分析</strong>:找出最慢的服务(这里是QA,9.5秒)</li>
<li><strong>错误定位</strong>:如果某个服务返回500,一目了然</li>
<li><strong>并行验证</strong>:确认Stage 1的4个服务确实是并行执行的</li>
</ul>
<p><strong>点击任一Span</strong>,可以看到详细信息:</p>
<p><img src="https://img2024.cnblogs.com/blog/3358435/202510/3358435-20251014112016266-1522310013.png"></p>
<h3 id="44-metrics指标视图">4.4 Metrics(指标视图)</h3>
<p>展示服务的性能指标:</p>
<p><img src="https://img2024.cnblogs.com/blog/3358435/202510/3358435-20251014112057909-967568919.png"></p>
<hr>
<h2 id="五服务发现的魔法">五、服务发现的魔法</h2>
<h3 id="51-aspire如何注入服务url">5.1 Aspire如何注入服务URL?</h3>
<p>当你在AppHost中写:</p>
<pre><code class="language-csharp">builder.AddProject&lt;Projects.AgentFrameworkAspire_Web&gt;("webfrontend")
    .WithReference(financeService);
</code></pre>
<p><strong>Aspire自动做的事情</strong>:</p>
<ol>
<li>Finance服务启动后,Aspire获取其端点(例如<code>https://localhost:7043</code>)</li>
<li>生成环境变量:<code>services__finance__http__0=http://localhost:7043</code></li>
<li>将环境变量注入到webfrontend进程</li>
</ol>
<h3 id="52-web前端如何解析服务url">5.2 Web前端如何解析服务URL?</h3>
<p>在<code>ProjectManagerAgent.cs</code>中:</p>
<pre><code class="language-csharp">// AgentFrameworkAspire.Web/Services/ProjectManagerAgent.cs
private string ResolveServiceUrl(
    string serviceName,
    string legacyConfigKey,
    string fallbackUrl)
{
    // 调试:列出所有 services__ 开头的配置
    var allServicesKeys = _configuration.AsEnumerable()
      .Where(kv =&gt; kv.Key != null &amp;&amp; kv.Key.StartsWith("services__"))
      .ToList();
   
    if (allServicesKeys.Any())
    {
      _logger.LogDebug("Found {Count} service discovery keys", allServicesKeys.Count);
      foreach (var kv in allServicesKeys)
      {
            _logger.LogDebug("{Key} = {Value}", kv.Key, kv.Value ?? "(null)");
      }
    }
   
    // 1. 尝试Aspire服务发现格式: services__{serviceName}__https__0
    var aspireHttpsKey = $"services__{serviceName}__https__0";
    var httpsUrl = _configuration;
    if (!string.IsNullOrEmpty(httpsUrl))
    {
      _logger.LogInformation(
            "Resolved {ServiceName} via Aspire HTTPS: {Url}",
            serviceName, httpsUrl);
      
      // 从 fallbackUrl 中提取路径部分 (例如 /tech/requirement-analyst)
      var uri = new Uri(fallbackUrl);
      var path = uri.PathAndQuery;
      return $"{httpsUrl.TrimEnd('/')}{path}";
    }
   
    // 2. 尝试Aspire服务发现格式: services__{serviceName}__http__0
    var aspireHttpKey = $"services__{serviceName}__http__0";
    var httpUrl = _configuration;
    if (!string.IsNullOrEmpty(httpUrl))
    {
      _logger.LogInformation(
            "Resolved {ServiceName} via Aspire HTTP: {Url}",
            serviceName, httpUrl);
      
      var uri = new Uri(fallbackUrl);
      var path = uri.PathAndQuery;
      return $"{httpUrl.TrimEnd('/')}{path}";
    }
   
    // 3. 尝试传统配置格式
    var legacyUrl = _configuration;
    if (!string.IsNullOrEmpty(legacyUrl))
    {
      _logger.LogInformation(
            "Resolved {ServiceName} via legacy config: {Url}",
            serviceName, legacyUrl);
      return legacyUrl;
    }
   
    // 4. 使用fallback
    _logger.LogWarning(
      "Using fallback URL for {ServiceName}: {Url}",
      serviceName, fallbackUrl);
    return fallbackUrl;
}
</code></pre>
<p><strong>调用示例</strong>:</p>
<pre><code class="language-csharp">_financeAgentUrl = ResolveServiceUrl(
    "finance",                                    // Aspire服务名
    "SpecialistAgents:Finance",                   // 传统配置key
    "https://localhost:7043/finance/budget-analyzer"// Fallback URL
);
</code></pre>
<p><strong>解析过程</strong>:</p>
<pre><code>1. 尝试 services__finance__https__0
   ✅ 找到: https://localhost:7043
   → 拼接路径: https://localhost:7043/finance/budget-analyzer

2. 如果没找到HTTPS,尝试 services__finance__http__0
   
3. 如果都没找到,尝试 appsettings.json:
   SpecialistAgents:Finance
   
4. 最后使用 fallback URL
</code></pre>
<p><strong>好处</strong>:</p>
<ul>
<li><strong>Aspire环境</strong>:自动服务发现,无需配置</li>
<li><strong>本地调试</strong>:可以在appsettings.json中硬编码URL</li>
<li><strong>单元测试</strong>:使用fallback URL,不依赖真实服务</li>
</ul>
<hr>
<h2 id="六实战场景演练">六、实战场景演练</h2>
<h3 id="61-场景1添加新服务">6.1 场景1:添加新服务</h3>
<p>假设你要添加一个"Legal(法务)"服务。</p>
<p><strong>步骤1:创建项目</strong></p>
<pre><code class="language-powershell">dotnet new webapi -n Legal
cd Legal
dotnet add package A2A.AspNetCore
dotnet add package ModelContextProtocol.Server
</code></pre>
<p><strong>步骤2:在AppHost中注册</strong></p>
<pre><code class="language-csharp">// AgentFrameworkAspire.AppHost/Program.cs
var legalService = builder.AddProject&lt;Projects.Legal&gt;("legal")
    .WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
    .WithEnvironment("OpenAI__ApiKey", openAiApiKey)
    .WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName);

builder.AddProject&lt;Projects.AgentFrameworkAspire_Web&gt;("webfrontend")
    // ... 其他引用
    .WithReference(legalService)// ← 添加这行
    .WaitFor(legalService);       // ← 添加这行
</code></pre>
<p><strong>完成!</strong> 重新启动AppHost,Legal服务会自动加入编排。</p>
<h3 id="62-场景2修改openai配置">6.2 场景2:修改OpenAI配置</h3>
<p>假设你想换成本地的Ollama模型。</p>
<p><strong>步骤1:确保Ollama运行</strong></p>
<pre><code class="language-powershell">ollama serve
ollama pull llama3
</code></pre>
<p><strong>步骤2:更新User Secrets</strong></p>
<pre><code class="language-powershell">cd AgentFrameworkAspire.AppHost
dotnet user-secrets set "Parameters:openai-endpoint" "http://localhost:11434/v1/"
dotnet user-secrets set "Parameters:openai-apikey" "ollama"
dotnet user-secrets set "Parameters:openai-deployment" "llama3"
</code></pre>
<p><strong>步骤3:重启系统</strong></p>
<pre><code class="language-powershell">dotnet run
</code></pre>
<p><strong>所有6个服务自动切换到Ollama,无需修改代码!</strong></p>
<h3 id="63-场景3调试单个服务">6.3 场景3:调试单个服务</h3>
<p>有时你只想调试Finance服务,不想启动整个系统。</p>
<p><strong>选项1:临时禁用其他服务</strong></p>
<pre><code class="language-csharp">// AgentFrameworkAspire.AppHost/Program.cs
// var techService = builder.AddProject&lt;Projects.Tech&gt;("tech")// 注释掉
//   .WithEnvironment...;
</code></pre>
<p><strong>选项2:直接运行单个服务</strong></p>
<pre><code class="language-powershell">cd Finance
$env:OpenAI__Endpoint = "https://your-resource.openai.azure.com/"
$env:OpenAI__ApiKey = "your-api-key"
$env:OpenAI__DeploymentName = "gpt-4o"
dotnet run
</code></pre>
<p>然后用Postman测试A2A端点。</p>
<h3 id="64-场景4查看跨服务调用链">6.4 场景4:查看跨服务调用链</h3>
<p><strong>步骤1:在Web UI发送请求</strong></p>
<pre><code>用户输入:开发一个电商系统,预算200万,12个月
</code></pre>
<p><strong>步骤2:打开Dashboard的Traces页面</strong></p>
<p>选择最新的trace,你会看到:</p>
<pre><code>webfrontend: ExecuteWorkflowStreamAsync (28.5s)
├─ tech: ProcessMessageAsync (7.8s)
│   └─ OpenAI API call (6.5s)
├─ hr: ProcessMessageAsync (6.2s)
│   └─ OpenAI API call (5.1s)
├─ finance: ProcessMessageAsync (7.1s)
│   ├─ MCP: ValidateBudget (0.05s)
│   ├─ MCP: GetHistoricalCosts (0.03s)
│   └─ OpenAI API call (5.9s)
└─ qa: ProcessMessageAsync (9.5s)
      └─ Workflow execution (8.8s)
          ├─ RiskIdentifier (2.1s)
          ├─ ImpactAnalyzer (2.3s)
          ├─ MitigationPlanner (2.2s)
          └─ MonitoringPlanner (2.2s)
</code></pre>
<p><strong>发现</strong>:</p>
<ul>
<li>QA服务最慢(9.5秒),因为它内部有4阶段工作流</li>
<li>Finance调用了2个MCP工具,非常快(&lt;0.1秒)</li>
<li>大部分时间花在OpenAI API调用上(5-6秒每次)</li>
</ul>
<hr>
<h2 id="七最佳实践与技巧">七、最佳实践与技巧</h2>
<h3 id="71-参数命名规范">7.1 参数命名规范</h3>
<pre><code class="language-csharp">// ✅ 好的命名:小写,短横线分隔
builder.AddParameter("openai-endpoint");
builder.AddParameter("database-connection-string");

// ❌ 不好的命名:大写、下划线
builder.AddParameter("OPENAI_ENDPOINT");
builder.AddParameter("database_connection_string");
</code></pre>
<h3 id="72-敏感信息标记">7.2 敏感信息标记</h3>
<pre><code class="language-csharp">// ✅ API Key、密码等标记为secret
var apiKey = builder.AddParameter("api-key", secret: true);

// ✅ 普通配置不标记
var endpoint = builder.AddParameter("api-endpoint");
</code></pre>
<h3 id="73-服务启动顺序">7.3 服务启动顺序</h3>
<pre><code class="language-csharp">// ✅ 明确依赖关系
webfrontend
    .WithReference(financeService)// 声明依赖
    .WaitFor(financeService);       // 等待就绪

// ❌ 不声明依赖,可能出现"服务未找到"错误
webfrontend
    .WithReference(financeService);// 只声明,不等待
</code></pre>
<h3 id="74-环境变量命名">7.4 环境变量命名</h3>
<pre><code class="language-csharp">// ✅ 使用双下划线表示层级
.WithEnvironment("OpenAI__Endpoint", endpoint)
.WithEnvironment("OpenAI__ApiKey", apiKey)

// 对应的配置结构:
// {
//   "OpenAI": {
//   "Endpoint": "...",
//   "ApiKey": "..."
//   }
// }
</code></pre>
<h3 id="75-日志级别控制">7.5 日志级别控制</h3>
<pre><code class="language-json">// appsettings.Development.json
{
"Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Aspire.Hosting": "Information"
    }
}
}
</code></pre>
<hr>
<h2 id="八常见问题排查">八、常见问题排查</h2>
<h3 id="问题1服务无法启动">问题1:服务无法启动</h3>
<p><strong>症状</strong>:Dashboard显示服务状态为"Failed"</p>
<p><strong>排查步骤</strong>:</p>
<ol>
<li>点击服务名称,查看"Console Logs"</li>
<li>查找错误消息(通常是红色)</li>
<li>常见原因:
<ul>
<li>端口被占用(切换到动态端口)</li>
<li>OpenAI配置错误(检查User Secrets)</li>
<li>依赖包缺失(<code>dotnet restore</code>)</li>
</ul>
</li>
</ol>
<p><strong>示例错误</strong>:</p>
<pre><code> Failed to start application
System.InvalidOperationException: OpenAI__ApiKey is not configured
</code></pre>
<p><strong>解决</strong>:检查User Secrets配置。</p>
<h3 id="问题2服务发现失败">问题2:服务发现失败</h3>
<p><strong>症状</strong>:Web前端报错"Unable to connect to tech service"</p>
<p><strong>排查步骤</strong>:</p>
<ol>
<li>在Dashboard确认Tech服务正在运行</li>
<li>查看Web前端的环境变量(Dashboard → webfrontend → Details)</li>
<li>检查是否有<code>services__tech__http__0</code>变量</li>
<li>如果没有,检查AppHost的<code>WithReference(techService)</code></li>
</ol>
<h3 id="问题3dashboard无法访问">问题3:Dashboard无法访问</h3>
<p><strong>症状</strong>:<code>http://localhost:15000</code>无法打开</p>
<p><strong>可能原因</strong>:</p>
<ul>
<li>Dashboard端口被占用</li>
<li>AppHost启动失败</li>
</ul>
<p><strong>解决</strong>:</p>
<pre><code class="language-powershell"># 查看AppHost进程是否运行
Get-Process -Name "AgentFrameworkAspire.AppHost" -ErrorAction SilentlyContinue

# 如果没有,查看启动日志
cd AgentFrameworkAspire.AppHost
dotnet run --verbose
</code></pre>
<h3 id="问题4openai调用超时">问题4:OpenAI调用超时</h3>
<p><strong>症状</strong>:服务日志显示"Request timeout"</p>
<p><strong>排查</strong>:</p>
<ol>
<li>检查网络连接</li>
<li>检查Endpoint URL格式(应以<code>/</code>结尾)</li>
<li>检查API Key是否有效</li>
<li>尝试直接用curl测试:</li>
</ol>
<pre><code class="language-powershell">curl -X POST "https://your-resource.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-08-01-preview" `
-H "api-key: your-key" `
-H "Content-Type: application/json" `
-d '{"messages":[{"role":"user","content":"Hello"}]}'
</code></pre>
<hr>
<h2 id="九进阶技巧">九、进阶技巧</h2>
<h3 id="91-多环境配置">9.1 多环境配置</h3>
<pre><code class="language-csharp">// AgentFrameworkAspire.AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);

// 根据环境读取不同配置
var environment = builder.Environment.EnvironmentName;

if (environment == "Development")
{
    // 开发环境:使用本地Ollama
    var endpoint = "http://localhost:11434/v1/";
}
else if (environment == "Staging")
{
    // 测试环境:使用Azure OpenAI测试资源
    var endpoint = builder.Configuration["AzureOpenAI:StagingEndpoint"];
}
else if (environment == "Production")
{
    // 生产环境:使用Azure OpenAI生产资源
    var endpoint = builder.Configuration["AzureOpenAI:ProductionEndpoint"];
}
</code></pre>
<h3 id="92-条件服务注册">9.2 条件服务注册</h3>
<pre><code class="language-csharp">// 只在开发环境启动某些服务
if (builder.Environment.IsDevelopment())
{
    builder.AddProject&lt;Projects.TestService&gt;("testservice");
}
</code></pre>
<h3 id="93-自定义健康检查">9.3 自定义健康检查</h3>
<pre><code class="language-csharp">// Finance/Program.cs
builder.Services.AddHealthChecks()
    .AddCheck("openai", () =&gt;
    {
      // 检查OpenAI连接
      try
      {
            var client = AgentHelpers.CreateChatClient(builder.Configuration);
            // 简单测试
            return HealthCheckResult.Healthy();
      }
      catch
      {
            return HealthCheckResult.Unhealthy("Cannot connect to OpenAI");
      }
    });
</code></pre>
<h3 id="94-资源限制">9.4 资源限制</h3>
<pre><code class="language-csharp">// 限制服务资源使用(仅Container模式)
var financeService = builder.AddContainer("finance", "finance-image")
    .WithEnvironment(...)
    .WithResourceLimits(memory: 512, cpu: 0.5);// 512MB内存,0.5核CPU
</code></pre>
<hr>
<h2 id="十总结">十、总结</h2>
<h3 id="aspire的核心价值">Aspire的核心价值</h3>
<ol>
<li><strong>简化本地开发</strong>:一行命令启动所有服务</li>
<li><strong>统一配置管理</strong>:参数定义一次,自动注入</li>
<li><strong>自动服务发现</strong>:无需硬编码URL</li>
<li><strong>可视化监控</strong>:Dashboard实时查看状态、日志、追踪</li>
<li><strong>生产就绪</strong>:一键部署到Azure或生成K8s manifest</li>
</ol>
<h3 id="与其他方案对比">与其他方案对比</h3>
<table>
<thead>
<tr>
<th>场景</th>
<th>传统方式</th>
<th>Docker Compose</th>
<th>Aspire</th>
</tr>
</thead>
<tbody>
<tr>
<td>启动服务</td>
<td>6个终端</td>
<td>1条命令</td>
<td>1条命令</td>
</tr>
<tr>
<td>配置管理</td>
<td>重复配置</td>
<td>.env文件</td>
<td>参数系统</td>
</tr>
<tr>
<td>服务发现</td>
<td>硬编码</td>
<td>Docker网络</td>
<td>自动注入</td>
</tr>
<tr>
<td>日志查看</td>
<td>切换窗口</td>
<td>docker logs</td>
<td>Dashboard</td>
</tr>
<tr>
<td>分布式追踪</td>
<td>额外工具</td>
<td>需要配置</td>
<td>内置</td>
</tr>
<tr>
<td>.NET集成</td>
<td>一般</td>
<td>一般</td>
<td><strong>原生</strong></td>
</tr>
<tr>
<td>学习曲线</td>
<td>低</td>
<td>中</td>
<td><strong>低</strong></td>
</tr>
</tbody>
</table>
<h3 id="下一步">下一步</h3>
<p>在下一篇文章中,我们将讨论:</p>
<ul>
<li>如何将Demo系统改造为生产就绪</li>
<li>需要添加哪些功能(认证、缓存、测试)</li>
<li>如何部署到Azure Container Apps</li>
<li>性能优化和成本控制</li>
</ul>
<hr>
<h2 id="参考资料">参考资料</h2>
<ul>
<li>.NET Aspire官方文档:https://learn.microsoft.com/dotnet/aspire/</li>
<li>AgentFrameworkAspire项目:https://github.com/microsoft/agent-framework</li>
<li>Azure Container Apps:https://learn.microsoft.com/azure/container-apps/</li>
<li>OpenTelemetry:https://opentelemetry.io/</li>
</ul>


</div>
<div id="MySignature" role="contentinfo">
    <p>本文来自博客园,作者:MadLongTom,转载请注明原文链接:https://www.cnblogs.com/madtom/p/19140269</p><br><br>
来源:https://www.cnblogs.com/madtom/p/19140269
頁: [1]
查看完整版本: 多智能体微服务实战(3/4):Aspire 打造本地 K8s 开发环境