多智能体微服务实战(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 --> |动态分配端口| AspireFinance
AspireHost --> |动态分配端口| AspireTech
AspireHost --> |动态分配端口| AspireWeb
AspireDash -.监控.- AspireFinance
AspireDash -.监控.- AspireTech
AspireDash -.监控.- AspireWeb
AspireFinance -.服务发现.- AspireWeb
AspireTech -.服务发现.- AspireWeb
end
subgraph K8sLocal ["Kubernetes方案 - 重量级"]
K8sDash
DockerDesktop
DockerDesktop --> |YAML配置| K8sFinance
DockerDesktop --> |YAML配置| K8sTech
DockerDesktop --> |YAML配置| K8sWeb
DockerDesktop --> |Service| K8sSvc1
DockerDesktop --> |Service| K8sSvc2
K8sFinance --> K8sSvc1
K8sTech --> K8sSvc2
K8sSvc1 --> K8sWeb
K8sSvc2 --> K8sWeb
end
end
subgraph Prod ["生产环境"]
subgraph ACA ["Azure Container Apps<br/>(Aspire推荐)"]
ACADeploy
ACADeploy --> ACAFinance
ACADeploy --> ACATech
ACADeploy --> ACAWeb
end
subgraph K8sProd ["Kubernetes<br/>(通用方案)"]
K8sProdDeploy
K8sProdDeploy --> K8sProdFinance
K8sProdDeploy --> K8sProdTech
K8sProdDeploy --> 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<Projects.Finance>("finance")
.WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
.WithEnvironment("OpenAI__ApiKey", openAiApiKey)
.WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName);
// HR 服务
var hrService = builder.AddProject<Projects.HumanResource>("humanresource")
.WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
.WithEnvironment("OpenAI__ApiKey", openAiApiKey)
.WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName);
// QA 服务
var qaService = builder.AddProject<Projects.QA>("qa")
.WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
.WithEnvironment("OpenAI__ApiKey", openAiApiKey)
.WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName);
// Tech 服务
var techService = builder.AddProject<Projects.Tech>("tech")
.WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
.WithEnvironment("OpenAI__ApiKey", openAiApiKey)
.WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName);
// PMO 服务
var pmoService = builder.AddProject<Projects.PMO>("pmo")
.WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
.WithEnvironment("OpenAI__ApiKey", openAiApiKey)
.WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName);
// ============================================================
// 第三部分:Web前端配置(Web Frontend Configuration)
// ============================================================
builder.AddProject<Projects.AgentFrameworkAspire_Web>("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<T>()</code> - 添加服务</h4>
<pre><code class="language-csharp">var service = builder.AddProject<Projects.Finance>("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 --> FinanceProj
Program --> TechProj
Program --> WebProj
FinanceProj --> |动态分配端口| FinancePort
TechProj --> |动态分配端口| TechPort
WebProj --> |WithReference| RefFinance[注入Finance引用]
WebProj --> |WithReference| RefTech[注入Tech引用]
end
subgraph WebService
WebEnv[环境变量]
WebEnv --> |自动注入| EnvFinance["services__finance__https__0<br/>=https://localhost:7222"]
WebEnv --> |自动注入| EnvTech["services__tech__https__0<br/>=https://localhost:7063"]
WebConfig
EnvFinance --> WebConfig
EnvTech --> WebConfig
ServiceDiscovery
WebConfig --> ServiceDiscovery
HttpClient
ServiceDiscovery --> HttpClient
HttpClient --> |解析服务名| ResolveFinance["http://finance"]
HttpClient --> |解析服务名| ResolveTech["http://tech"]
ResolveFinance --> |实际请求| ActualFinance["https://localhost:7222"]
ResolveTech --> |实际请求| ActualTech["https://localhost:7063"]
end
subgraph FinanceService
FinanceAPI
end
subgraph TechService
TechAPI
end
ActualFinance -.-> FinanceAPI
ActualTech -.-> 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__<服务名>__<协议>__<索引></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<Projects.Finance>("finance");
var webfrontend = builder.AddProject<Projects.AgentFrameworkAspire_Web>("webfrontend")
.WithReference(financeService);// 这里触发服务发现配置注入
// Web/Program.cs - 消费层
builder.Services.AddHttpClient("FinanceClient", client =>
{
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 =>
{
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<Projects.AgentFrameworkAspire_Web>("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 => kv.Key != null && 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<Projects.Legal>("legal")
.WithEnvironment("OpenAI__Endpoint", openAiEndpoint)
.WithEnvironment("OpenAI__ApiKey", openAiApiKey)
.WithEnvironment("OpenAI__DeploymentName", openAiDeploymentName);
builder.AddProject<Projects.AgentFrameworkAspire_Web>("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<Projects.Tech>("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工具,非常快(<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<Projects.TestService>("testservice");
}
</code></pre>
<h3 id="93-自定义健康检查">9.3 自定义健康检查</h3>
<pre><code class="language-csharp">// Finance/Program.cs
builder.Services.AddHealthChecks()
.AddCheck("openai", () =>
{
// 检查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]