将原单体 MAF1 拆成 MAF1(控制台工作流)和 MAF1.Web(可视化编排)。 抽城市失败时暂停:网页弹确认、CLI 控制台询问,超时采用默认结束查询; 也可改填城市后继续。共享决策模型放在 MAF1.Core。
16 KiB
MAF1 — 多智能体工作流编排
MAF1 是一个基于 .NET 10 和 Microsoft Agents AI 的多智能体演示仓库。同一条「读文件抽城市 → 查天气」链路拆成两个可执行项目:
- MAF1:命令行工作流。用 Microsoft Agents AI Workflows 对比「条件写在节点里」和「条件写在边上」。
- MAF1.Web:可视化编排器。浏览器里拖节点、连线、配条件,然后一键运行。
内置 Agent 在 MAF1.Core 里;网页还可以把同一套能力以 独立进程插件 的形式画到画布上。LLM 的 endpoint / API Key / 模型由各宿主注入,不会画成节点端口,也不会写进流程图 JSON。
网页打开:http://127.0.0.1:5288(先启动 MAF1.Web)
功能概览
- 可视化工作流:左侧节点面板(系统节点 + 扫描到的插件)、画布连线、右侧检查器、底部运行日志。
- 系统节点:
fileCity-system(读文本抽城市)、weather-system(按城市查天气并汇总)。 - 进程外插件:扫描输出目录
plugins/*/plugin.json,按launch启动子进程,stdin / stdout 走 JSON。 - 凭据隔离:宿主配置 LLM;节点只声明需要哪种凭据(默认
llm-default)。浏览器拿不到原始 Key。 - 边条件:连线可设
hasValidCities/!hasValidCities,不满足则跳过下游节点。 - 人机决策:抽城市失败时暂停等待确认;必须有默认方案(结束查询)。网页弹出确认框,CLI 在控制台询问。超时未确认则自动采用默认方案,也可以改填其它城市继续查天气。
- CLI 对照:
node模式把「有没有城市」写在 Gate 节点里;edge模式把这条判断写在边上。两种模式缺城市时都会进入同一套确认。 - 天气数据源:默认 wttr.in,可改为 OpenWeatherMap。
技术栈
| 部分 | 说明 |
|---|---|
| 运行时 | .NET 10(net10.0) |
| 控制台 | MAF1:普通控制台,Microsoft Agents AI Workflows |
| 网页 | MAF1.Web:ASP.NET Core Minimal API + wwwroot |
| LLM | Azure.AI.OpenAI + Microsoft.Agents.AI.OpenAI(兼容 OpenAI / Azure OpenAI / DeepSeek 等) |
| 前端 | MAF1.Web/wwwroot 下原生 HTML / CSS / JS,无 npm 依赖 |
| 插件 | 独立控制台进程,协议见 MAF1.Core/PluginContract,由网页宿主扫描 |
解决方案文件:MAF1.slnx(MAF1、MAF1.Web、MAF1.Core、两个插件工程)。
仓库结构
仓库根目录/
├── MAF1.slnx
├── MAF1/ # 控制台项目
│ ├── MAF1.csproj
│ ├── Program.cs / CliHost.cs
│ ├── Workflows/ # Microsoft Agents AI Workflow
│ ├── Orchestration/ # 控制台事件流 + 确认提示
│ ├── Data/
│ └── appsettings.json
├── MAF1.Web/ # 网页编排项目
│ ├── MAF1.Web.csproj
│ ├── Program.cs # Minimal API
│ ├── Web/ # 图执行、运行时组装
│ ├── PluginHost/ # 扫描插件、起进程、注入凭据
│ ├── wwwroot/ # 可视化界面
│ ├── Data/
│ └── appsettings.json
├── MAF1.Core/ # 共享:Agent、Tool、LLM、插件协议
└── plugins/
├── file-city/
└── weather/
构建 MAF1.Web 时会编译两个插件,并把输出复制到网页宿主 bin/.../plugins/。运行时扫描的是网页 exe 旁边的 plugins/,不是源码树。 控制台项目不扫描插件。
环境要求
- .NET 10 SDK
- 一台 OpenAI 兼容的 Chat Completions 服务(默认配置指向 DeepSeek)
- 访问天气接口的网络(wttr.in 或 OpenWeatherMap)
快速开始
1. 还原并编译
cd 仓库根目录
dotnet restore
dotnet build
2. 配置模型
不要把 API Key 提交进 Git。任选一种方式:
方式 A:环境变量(推荐)
$env:OPENAI_API_KEY = "你的密钥"
$env:OPENAI_ENDPOINT = "https://api.deepseek.com"
$env:OPENAI_CHAT_MODEL = "deepseek-v4-flash"
也识别:
| 变量 | 含义 |
|---|---|
OPENAI_API_KEY / AZURE_OPENAI_API_KEY |
密钥 |
OPENAI_ENDPOINT / OPENAI_BASE_URL / AZURE_OPENAI_ENDPOINT |
服务地址 |
OPENAI_CHAT_MODEL / AZURE_OPENAI_DEPLOYMENT_NAME |
模型名或 Azure 部署名 |
配置优先级:环境变量会覆盖 appsettings.json 里空的或未填的对应项(见 AgentFactory.Load)。
方式 B:appsettings.json 的 Llm 节点
"Llm": {
"ApiKey": "",
"Endpoint": "https://api.deepseek.com",
"Model": "deepseek-v4-flash"
}
Azure OpenAI:把 Endpoint 设成 Azure 资源地址,模型字段填 部署名。程序会按 URL 判断是否走 Azure 客户端。
Visual Studio / dotnet run --project MAF1.Web --launch-profile designer 会读 MAF1.Web/Properties/launchSettings.json 里的环境变量。该文件容易带上真实密钥,请勿把含 Key 的版本推到远程。
控制台 profile 在 MAF1/Properties/launchSettings.json(node / edge)。
3. 启动可视化编排
dotnet run --project MAF1.Web
浏览器打开 http://127.0.0.1:5288。控制台会打印:可视化编排: http://127.0.0.1:5288。
界面操作建议:
- 点 示例图,生成「抽城市 → 查天气」的演示图(含边条件)。
- 选中节点,在右侧填
filePath(例如Data/cities.txt),或检查端口映射。 - 点 运行,底部查看每步输入 / 输出。
- 改完
plugins/后点 刷新节点 重新加载目录。
4. 命令行工作流
dotnet run --project MAF1 -- node Data/cities.txt # 条件在 Gate 节点内部
dotnet run --project MAF1 -- edge Data/cities.txt # 条件在边上分流
dotnet run --project MAF1 -- --help
无参数时默认 node Data/cities.txt。无有效城市时可用 Data/not-cities.txt。工作流会在控制台询问如何继续(默认结束查询,也可输入其它城市);超时走默认方案。
dotnet run --project MAF1 -- node Data/not-cities.txt
dotnet run --project MAF1 -- edge Data/not-cities.txt
控制台会出现:
[需要确认]
没有读到有效城市。…
1) 结束查询(默认) ← 默认
2) 改查其它城市
输入序号,或直接回车采用默认:
输入 1 或回车结束;输入 2 后再填城市名继续查天气;也可以直接输入 成都。等待秒数与网页相同,来自 Workflow:DecisionTimeoutSeconds。
启动配置:
| 项目 | Profile | 作用 |
|---|---|---|
MAF1.Web |
designer |
网页编排器 |
MAF1 |
node |
CLI,节点内判断 |
MAF1 |
edge |
CLI,边上判断 |
配置说明
MAF1/appsettings.json 与 MAF1.Web/appsettings.json 会分别复制到各自输出目录。网页项目额外包含 Plugins / Credentials。主要段落:
Llm
宿主内置 Agent 与默认凭据 llm-default 使用这一段(可再被环境变量覆盖)。
Plugins
| 字段 | 含义 |
|---|---|
Directory |
相对宿主内容根的插件目录,默认 plugins |
DefaultTimeoutSeconds |
插件进程默认超时(秒),清单里可单独覆盖 |
插件进程会收到 MAF1_CONTENT_ROOT,用于定位宿主的 appsettings.json(天气插件读天气配置)。
Credentials
节点只声明凭据类型。llm-default 的 source: llm-section 表示从 Llm 段生成,不在 JSON 里再写一份 Key。
Weather
| 字段 | 含义 |
|---|---|
Provider |
Wttr(默认)或 OpenWeather |
Language |
语言,默认 zh |
Wttr.UrlTemplate |
{location}、{lang} 占位 |
OpenWeather.UrlTemplate / ApiKey |
OpenWeatherMap;需自行申请 Key |
Workflow
| 字段 | 含义 |
|---|---|
DecisionTimeoutSeconds |
需要用户确认时的等待秒数,默认 30。超时后采用默认方案。 |
节点 config 可覆盖:decisionTimeoutSeconds(秒)、askOnEmptyCities(false 时抽不到城市不询问,直接按边条件跳过)。
内置 Agent
两条链路语义相同,只是执行位置不同。
FileCity(抽城市)
- 输入:
filePath(本地文本路径,相对宿主工作目录即可) - 输出:
hasValidCities、cities、reason、raw - 行为:LLM + 读文件工具,从文本里抽出有效城市名
- 若没有有效城市且后面还有节点:工作流会 暂停等待确认。默认方案是结束后续查询;可选方案是手动输入城市继续。超时走默认。
系统节点类型:fileCity-system
插件 id:fileCity(plugins/file-city)
Weather(查天气)
- 输入:
cities(字符串数组) - 输出:
summary - 行为:LLM + HTTP 天气工具,汇总各城市天气
系统节点类型:weather-system
插件 id:weather(plugins/weather)
画布上可以混用:例如系统 FileCity 接到插件 Weather,只要端口名对得上。
可视化图模型
运行请求体大致为:
{
"nodes": [
{
"id": "n1",
"type": "fileCity-system",
"title": "抽城市",
"x": 80,
"y": 120,
"config": { "filePath": "Data/cities.txt" }
}
],
"edges": [
{
"id": "e1",
"from": "n1",
"to": "n2",
"fromPort": "cities",
"toPort": "cities",
"when": "hasValidCities"
}
]
}
- 拓扑:按依赖排序执行;未满足入边条件的节点会被标记为跳过。
- 人机决策:节点输出
hasValidCities = false时,若后面还有节点,则返回status: needsDecision,不立刻跑完。 - 端口:
fromPort→toPort把上游输出接到下游输入;未接线的必填项可写在config。 - 边条件
when(当前实现):- 空:始终通过
hasValidCities:上游输出该布尔为真!hasValidCities:为假- 其它字符串:视为通过
HTTP API
端口固定:http://127.0.0.1:5288(MAF1.Web/Program.cs)。
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/api/catalog |
系统节点 + 插件节点、扫描问题、pluginsRoot |
GET |
/api/credentials |
对外凭据列表(不含原始 Key) |
GET |
/api/status |
当前天气 Provider、插件根路径 |
POST |
/api/run |
Body 为工作流图 JSON。可能直接跑完,也可能返回 needsDecision |
GET |
/api/run/{runId} |
查询暂停中或已结束的一次运行 |
POST |
/api/run/{runId}/decide |
Body:{ "optionId": "stop" } 或 { "optionId": "query-cities", "text": "成都" } |
POST /api/run 在需要确认时返回:
{
"ok": true,
"status": "needsDecision",
"runId": "…",
"decision": {
"prompt": "没有读到有效城市…",
"defaultOptionId": "stop",
"timeoutSeconds": 30,
"deadline": "2026-08-27T09:22:00+00:00",
"options": [
{ "id": "stop", "label": "结束查询(默认)", "isDefault": true },
{ "id": "query-cities", "label": "改查其它城市", "requiresText": true }
]
},
"steps": []
}
默认方案必须存在。服务端到期会自动按 defaultOptionId 继续;客户端倒计时结束后可再 GET 一次拿最终结果。
JSON 使用 camelCase。静态站点来自 wwwroot。
插件机制
约定详见 plugins/README.md。每个子目录一份插件。
必备文件
plugin.json:id、启动命令、凭据声明、inputs、outputsREADME.md:给人看的说明- 可执行文件:由
launch.command+launch.args原样启动,宿主不替你拼路径
示例(FileCity):
"launch": {
"command": "dotnet",
"args": ["FileCityPlugin.dll"]
}
工作目录是该插件文件夹;dotnet FileCityPlugin.dll 能在该目录下找到 dll。
进程协议
stdin(一次请求):
{
"inputs": { "filePath": "Data/cities.txt" },
"credentials": {
"llm": {
"id": "llm-default",
"type": "openai-compatible",
"endpoint": "...",
"apiKey": "...",
"model": "..."
}
}
}
stdout:单个 JSON 对象,字段名与 outputs 一致。
stderr:日志。不要把日志打到 stdout,否则会破坏解析。
超时:清单 timeoutSeconds,否则用宿主 Plugins:DefaultTimeoutSeconds(默认 180)。
凭据注入
与 n8n / Dify 类似:Key 只在宿主。运行插件时会:
- 写入环境变量
OPENAI_ENDPOINT/OPENAI_API_KEY/OPENAI_CHAT_MODEL - 在 stdin JSON 的
credentials里再传一份
插件目录可选 .env 作为第三方自带模型的默认环境;节点选中的宿主凭据会覆盖同名变量。不要把 .env 提交进仓库。
更细的输入输出说明:
自己加插件
- 新建
plugins/你的插件/,写plugin.json和启动程序。 - 若要随网页宿主一起编译,可仿照
MAF1.Web.csproj增加ProjectReference(ReferenceOutputAssembly=false)和PublishPluginFolders复制规则。 id不要与系统节点类型冲突;若同名,目录会标记覆盖关系。- 重启宿主或点「刷新节点」,确认
/api/catalog的issues为空。
架构
浏览器 MAF1.Web/wwwroot
│ REST
▼
MAF1.Web Program.cs(Minimal API)
│
▼
AgentRuntime
├── NodeCatalogService 合并系统节点 + PluginScanner
├── CredentialStore llm-default 等
├── ConfigurableWorkflowRunner 拓扑、端口、when、逐步日志
├── 系统 Handler FileCity / Weather(进程内)
└── PluginProcessRunner 子进程 stdin/stdout
控制台项目不走画布:
MAF1 → CliHost → CityWeatherWorkflow(node / edge)→ WorkflowOrchestration。缺城市时走 RequestPort,与网页同一套默认方案 + 超时。
MAF1.Core 被控制台、网页与插件共用,避免两套 Agent 逻辑分叉。
示例数据
Data/cities.txt:
成都
阿姆斯特丹
北京
Data/not-cities.txt:不含有效城市,用于验证「无城市则询问后默认不查天气」。
这些文件会随构建复制到各项目输出目录。CLI 传入相对路径时,请在仓库根目录用 --project MAF1 运行,或在已复制 Data 的输出目录下运行。
常见问题
启动报「还没有配置模型」
Llm:ApiKey 和环境变量都为空。按「快速开始」配置后重启。
画布上看不到插件
插件必须出现在 MAF1.Web.exe 旁边 的 plugins/。先 dotnet build MAF1.Web/MAF1.Web.csproj,确认 MAF1.Web/bin/Debug/net10.0/plugins/file-city/plugin.json 存在。源码目录里的 plugins/ 不会被运行时直接扫描。
插件超时或卡住
加大 timeoutSeconds;确认 LLM 与天气 HTTP 可访问;日志看 stderr。
天气失败
默认走 wttr.in。若被墙或限流,把 Weather:Provider 改为 OpenWeather 并填写 Key。
Windows 控制台中文乱码
项目带 app.manifest 并在入口启用 UTF-8。若仍乱码,把终端代码页设为 UTF-8。
端口被占用
当前 URL 写死为 127.0.0.1:5288。关掉占用进程,或临时改 MAF1.Web/Program.cs / MAF1.Web/Properties/launchSettings.json。
没有自动化测试 / Docker
仓库目前没有测试项目和容器文件。验证方式:UI 示例图 + CLI node / edge。
安全注意
- LLM Key、OpenWeather Key 只放在本机配置或环境变量,不要写进
plugin.json、流程图或前端。 /api/credentials只返回公开元数据。- 若 Key 曾经出现在
launchSettings.json并被提交或分享,请到服务商控制台轮换密钥。
许可与定位
本仓库是多智能体编排与进程外插件的可运行演示,便于对照系统节点与插件、对照 CLI 的 node / edge 两种条件写法。按你自己的许可证要求补充版权声明即可。