Agent + 微服务 本地开发最佳实践 —— Aspire 引入
前言
在全栈 Agent 开发上下文中,我们往往需要多个 Agent / SubAgent 协作开发。用单测(UT)可以让 Agent 对自己写的代码负责,但如果需要多个项目同时进行联调测试,就得把上下文同步到多个 Agent 中,比较麻烦。Aspire 提供的编排能力正好能够把整体项目统一编排起来,然后只需要一个 Agent 拉起测试即可。
Aspire 是什么
Aspire 是一个统一的本地服务编排工具,通过一套特殊的 DSL 描述服务的依赖关系、环境变量、开放端口,借助 docker / podman 和本地运行时(go、nodejs 等)快速拉起所有需要的服务,并注入所需的环境变量。
我在多个项目中已经落地这套方案。相较于传统的开发环境配置,可以节省大量的本地环境配置时间:在一个由 3 个 Node.js 后端服务 + 2 个前端服务 + PostgreSQL + Redis + RabbitMQ + Kafka 组成的应用中,10 分钟就能顺利把整个项目跑起来。setup 只需要安装 .NET runtime(Aspire 依赖)、docker 以及对应的本地运行时(如 Node.js),剩下的工作全部在 Aspire 的编排脚本中完成。
因为 Aspire 掌握了服务列表、服务间依赖的上下文、所需的环境变量、暴露的接口等几乎所有的运维配置,所以它还能支持生成 docker 或 k8s 的 deploy manifest。比如 docker 生成的产物是一个 .env 文件和一个 docker-compose.yaml,在真实部署场景下只需编辑 .env 填入对应环境变量即可,十分方便。
Aspire 最近推出了 aspire-cli,配合完备的文档功能,直接打通了与 Agent 的协作通道。同时 aspire-cli 的 --isolate 参数,也能让诸如 Claude Agent Teams 这类多 Agent 在同时开发多个需求时,为不同的 worktree 独立拉起各自 worktree 对应的服务。
接入 Aspire
安装 aspire-cli
先去 Aspire 官网按照安装教程安装 aspire CLI。安装后运行 aspire agent,可以看到如下输出:
Commands:
mcp Start the MCP (Model Context Protocol) server
init Initialize agent environment configuration for detected agents
MCP 笔者个人不推荐使用,太吃上下文了。
初始化本地编排项目
先创建 infra 目录:
mkdir -p infra/local-dev
cd infra/local-dev
用 aspire init 创建默认的 Aspire 项目模板,目前支持 C# 和 TypeScript,这里选择 TypeScript。完成后它会询问是否要生成对应的 skill,选择后会自动生成 claude、github 和 opencode 的 aspire skill 和 playwright skill,生成后按需保留即可,也可以挪到项目根目录对应的文件夹中。
我个人推荐让 Agent 自己生成一个 infra Agent 来控制对 Aspire 的代码修改,在其中描述清楚项目结构、Agent 职责、项目的依赖关系,例如:
为这个项目准备一个运维 agent,并接入 aspire,相关代码放置在 infra 文件夹
当前 infra 目录结构
- infra/AGENTS.md 指示如何编辑 infra 相关项目,这个 AGENT 收集各个项目中需求的基础设施能力,如 `postgresql`,并使用 `aspire` 工具添加相关的集成(integration)到对应的 aspire 项目中(参考 aspire 的 skill)
- infra/local-dev/ - 这是用于本地开发的 aspire project
让 Agent 编写 apphost.ts
当 Agent 把 agents.md 生成好之后,让 AI 自动分析项目依赖,并编写 apphost.ts:
扫描其他项目的引用和依赖,分析项目之间的依赖关系,并根据其技术栈使用 aspire docs search 查找如何集成到 aspire 中,并修改 infra/local-dev 中的 apphost.ts 完成项目的集成。扫描项目中的环境变量,在 apphost.ts 中体现,如果是引用其他基础设施和项目,使用 aspire 的 withReference/withEnvironment 功能进行引用。如果需要帮助请使用 aspire docs search <keyword> 进行检索。
Agent 会自动根据 aspire skill 按技术栈添加引用,最终 apphost.ts 会长这样:
import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const redis = await builder.addContainer('cache', 'redis:latest');
const postgres = await builder.addPostgres('pgsql');
const db = await postgres.addDatabase('db');
const backend = await builder.addJavaScriptApp('backend', '../../backend', {
runScriptName: 'dev',
})
.withBun()
.withReference(db)
.withReference(redis)
.withHttpEndpoint({ env: 'PORT' });
const frontend = await builder.addViteApp('frontend', '../../frontend')
.withBun()
.withReference(backend)
.withEnvironment('VITE_API_BASE_URL', await backend.getEndpoint('http'));
await builder.build().run();
之后就可以让 AI 直接用 Aspire 把整个项目拉起来,或者进行集成测试 / API 测试 / e2e 测试的编写,甚至可以引入一个测试 Agent,配合 Playwright 的 skill,对指定分支进行测试。
一些技巧和问题
eslint 报错
aspire init 产出的默认项目不含 tsconfig.json,但 eslint 只认这个文件而不认 tsconfig.apphost.json。我们创建一个 tsconfig.json,直接 extends 一下即可:
{
"extends": ["tsconfig.apphost.json"]
}
构造复杂环境变量
withReference 只会把默认的环境变量注入到环境中,比如 PostgreSQL 会注入 DB_JDBCCONNECTIONSTRING、DB_URI 这种。在我们不想改代码,或现有环境变量无法直接使用的时候,可以自己引用一下,或者重新构造。
引用一个已有的环境变量
如果某个环境变量已经满足需求,只需要换个名字引用,可以使用 withEnvironmentCallback。例如已经有了 DB_URI,把它别名成 DATABASE_URL:
.withEnvironmentCallback(async (cb) => {
cb.environmentVariables.set('DATABASE_URL', await cb.environmentVariables.get('DB_URI'));
})
使用 refExpr 自定义构造
如果默认注入的环境变量都不是你想要的,可以用 refExpr 自己构造:
import { refExpr } from './.modules/aspire.js';
const pg = await builder.addPostgres('pgsql');
const db = await pg.addDatabase('db');
async function makePgsqlConnectionString() {
const user = await pg.userNameReference.get();
const password = await pg.passwordParameter.get();
const host = await pg.host.get();
const port = await pg.port.get();
const dbName = await db.databaseName.get();
return refExpr`postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=public`;
}
const pgsqlRef = await makePgsqlConnectionString();
const backend = await builder.addJavaScriptApp('backend', '../../backend', {
runScriptName: 'dev',
})
.withBun()
.withEnvironment('DATABASE_URL', pgsqlRef);
增加自定义扩展
如果我们定义了很多个自定义的扩展,可以通过函数来复用代码,例如把公共的后端服务模板抽出来(db 来自外层作用域):
import { DistributedApplicationBuilder } from './.modules/aspire.js';
async function backendProjectTemplate(builder: DistributedApplicationBuilder, folder: string, script = 'dev') {
return await builder.addJavaScriptApp(`backend-${folder}`, `../../${folder}`, {
runScriptName: script,
})
.withBun()
.withReference(db);
}
const backendA = await backendProjectTemplate(builder, 'backend-a');
const backendB = await backendProjectTemplate(builder, 'backend-b');
如果想追求类似 C# 扩展方法的体验,可以使用 monkey patch 的方式把方法注入到 DistributedApplicationBuilder 中,例如我自定义接入的 minio:
import { DistributedApplicationBuilder, ContainerResource, ParameterResource } from './.modules/aspire.js';
// 定义 MinIoResource,让外部调用时能感知到 user 和 password
export interface MinIoResource extends ContainerResource {
user: ParameterResource;
password: ParameterResource;
}
DistributedApplicationBuilder.prototype.addMinIoContainer = async function (name: string) {
// 生成 user / password 参数
const user = await this.addParameterWithValue(`${name}-rootuser`, 'minioadmin');
const password = await this.addParameterWithGeneratedValue(`${name}-rootpassword`, {});
// 使用 addContainer 构造 minio,并注入 root 账号信息
const minio = (await this
.addContainer(name, 'minio/minio:latest')
.withEnvironment('MINIO_ROOT_USER', user)
.withEnvironment('MINIO_ROOT_PASSWORD', password)) as MinIoResource;
minio.user = user;
minio.password = password;
return minio;
};
// 给 TS 类型打个补丁
declare module './.modules/aspire.js' {
interface DistributedApplicationBuilder {
addMinIoContainer: (name: string) => Promise<MinIoResource>;
}
}
在外部调用时,直接像内置 API 一样使用即可:
const builder = await createBuilder();
const minio = await builder.addMinIoContainer('minio');
总结
接入 Aspire 之后,整个微服务项目的本地开发 / 联调 / 测试环境就被统一到一个编排脚本里:Agent 只需要看一份 apphost.ts 就能理解全局拓扑,用一条命令拉起所有服务,还能自动生成部署清单,配合测试 Agent 甚至能做到”一键起环境、自动跑测试”。如果你也在用多 Agent 开发微服务,值得一试。