422 lines
14 KiB
Markdown
422 lines
14 KiB
Markdown
# MAF1 — 多智能体工作流编排
|
||
|
||
MAF1 是一个基于 **.NET 10** 和 **Microsoft Agents AI** 的多智能体(Multi-Agent)演示项目。它把「读文件抽城市 → 查天气」这条业务链路做成两种用法:
|
||
|
||
1. **可视化编排器**:浏览器里拖节点、连线、配条件,然后一键运行。
|
||
2. **命令行工作流**:用 Microsoft Agents AI Workflows 对比「条件写在节点里」和「条件写在边上」两种编排方式。
|
||
|
||
内置 Agent 在进程内执行;同一套能力也可以以 **独立进程插件** 的形式出现在画布上。LLM 的 endpoint / API Key / 模型由宿主注入,**不会**画成节点端口,也不会写进流程图 JSON。
|
||
|
||
默认打开可视化界面:<http://127.0.0.1:5288>
|
||
|
||
---
|
||
|
||
## 功能概览
|
||
|
||
- **可视化工作流**:左侧节点面板(系统节点 + 扫描到的插件)、画布连线、右侧检查器、底部运行日志。
|
||
- **系统节点**:`fileCity-system`(读文本抽城市)、`weather-system`(按城市查天气并汇总)。
|
||
- **进程外插件**:扫描输出目录 `plugins/*/plugin.json`,按 `launch` 启动子进程,stdin / stdout 走 JSON。
|
||
- **凭据隔离**:宿主配置 LLM;节点只声明需要哪种凭据(默认 `llm-default`)。浏览器拿不到原始 Key。
|
||
- **边条件**:连线可设 `hasValidCities` / `!hasValidCities`,不满足则跳过下游节点。
|
||
- **CLI 对照**:`node` 模式把判断放在 Gate 节点内;`edge` 模式把判断写在 Workflow 边上。
|
||
- **天气数据源**:默认 [wttr.in](https://wttr.in),可改为 OpenWeatherMap。
|
||
|
||
---
|
||
|
||
## 技术栈
|
||
|
||
| 部分 | 说明 |
|
||
|------|------|
|
||
| 运行时 | .NET 10(`net10.0`) |
|
||
| 宿主 | ASP.NET Core Web SDK,Minimal API + 静态文件 |
|
||
| 编排库 | `Microsoft.Agents.AI.Workflows` 1.19.0 |
|
||
| LLM | `Azure.AI.OpenAI` + `Microsoft.Agents.AI.OpenAI`(兼容 OpenAI / Azure OpenAI / DeepSeek 等) |
|
||
| 前端 | `wwwroot` 下原生 HTML / CSS / JS,无 npm 依赖 |
|
||
| 插件 | 独立控制台进程,协议见 `MAF1.Core/PluginContract` |
|
||
|
||
解决方案文件:`MAF1.slnx`(包含宿主、`MAF1.Core`、两个插件工程)。
|
||
|
||
---
|
||
|
||
## 仓库结构
|
||
|
||
```
|
||
MAF1/
|
||
├── MAF1.csproj # 宿主:Web UI + CLI
|
||
├── MAF1.slnx
|
||
├── Program.cs # 有 CLI 参数则走命令行,否则启动 Web
|
||
├── CliHost.cs
|
||
├── appsettings.json # LLM / 插件 / 天气配置
|
||
├── app.manifest # Windows 控制台 UTF-8
|
||
├── Properties/launchSettings.json
|
||
├── Data/ # 示例文本
|
||
│ ├── cities.txt
|
||
│ └── not-cities.txt
|
||
├── Web/ # 目录、图执行、运行时组装
|
||
├── PluginHost/ # 扫描插件、起进程、注入凭据
|
||
├── Workflows/ # CLI 用的 Microsoft Agents AI Workflow
|
||
├── Orchestration/ # CLI 事件流式输出
|
||
├── wwwroot/ # 可视化编排界面
|
||
├── MAF1.Core/ # 共享:Agent、Tool、LLM、插件协议
|
||
│ ├── Agents/FileCity/
|
||
│ ├── Agents/Weather/
|
||
│ ├── Tools/
|
||
│ ├── Utils/AgentFactory.cs
|
||
│ └── PluginContract/
|
||
└── plugins/
|
||
├── README.md # 插件目录约定
|
||
├── file-city/ # FileCity 独立进程
|
||
└── weather/ # Weather 独立进程
|
||
```
|
||
|
||
构建宿主时会编译两个插件,并把输出复制到宿主 `bin/.../plugins/file-city` 与 `plugins/weather`。**运行时扫描的是可执行文件旁边的 `plugins/`,不是源码树。**
|
||
|
||
---
|
||
|
||
## 环境要求
|
||
|
||
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
|
||
- 一台 OpenAI 兼容的 Chat Completions 服务(默认配置指向 DeepSeek)
|
||
- 访问天气接口的网络(wttr.in 或 OpenWeatherMap)
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
### 1. 还原并编译
|
||
|
||
```bash
|
||
cd 仓库根目录
|
||
dotnet restore
|
||
dotnet build
|
||
```
|
||
|
||
### 2. 配置模型
|
||
|
||
不要把 API Key 提交进 Git。任选一种方式:
|
||
|
||
**方式 A:环境变量(推荐)**
|
||
|
||
```powershell
|
||
$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` 节点**
|
||
|
||
```json
|
||
"Llm": {
|
||
"ApiKey": "",
|
||
"Endpoint": "https://api.deepseek.com",
|
||
"Model": "deepseek-v4-flash"
|
||
}
|
||
```
|
||
|
||
Azure OpenAI:把 Endpoint 设成 Azure 资源地址,模型字段填 **部署名**。程序会按 URL 判断是否走 Azure 客户端。
|
||
|
||
Visual Studio / `dotnet run --launch-profile designer` 会读 `Properties/launchSettings.json` 里的环境变量。该文件容易带上真实密钥,**请勿把含 Key 的版本推到远程**。
|
||
|
||
### 3. 启动可视化编排(默认)
|
||
|
||
```bash
|
||
dotnet run
|
||
```
|
||
|
||
浏览器打开 <http://127.0.0.1:5288>。控制台会打印:`可视化编排: http://127.0.0.1:5288`。
|
||
|
||
界面操作建议:
|
||
|
||
1. 点 **示例图**,生成「抽城市 → 查天气」的演示图(含边条件)。
|
||
2. 选中节点,在右侧填 `filePath`(例如 `Data/cities.txt`),或检查端口映射。
|
||
3. 点 **运行**,底部查看每步输入 / 输出。
|
||
4. 改完 `plugins/` 后点 **刷新节点** 重新加载目录。
|
||
|
||
### 4. 命令行工作流
|
||
|
||
```bash
|
||
dotnet run -- node Data/cities.txt # 条件在 Gate 节点内部
|
||
dotnet run -- edge Data/cities.txt # 条件在边上分流
|
||
dotnet run -- --help
|
||
```
|
||
|
||
无有效城市时可用 `Data/not-cities.txt` 看跳过天气查询的路径。
|
||
|
||
启动配置(`launchSettings.json`):
|
||
|
||
| Profile | 作用 |
|
||
|---------|------|
|
||
| `designer` | Web 编排器(默认) |
|
||
| `node` | CLI,节点内判断 |
|
||
| `edge` | CLI,边上判断 |
|
||
|
||
---
|
||
|
||
## 配置说明
|
||
|
||
`appsettings.json` 会复制到输出目录。主要段落:
|
||
|
||
### 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 |
|
||
|
||
---
|
||
|
||
## 内置 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,只要端口名对得上。
|
||
|
||
---
|
||
|
||
## 可视化图模型
|
||
|
||
运行请求体大致为:
|
||
|
||
```json
|
||
{
|
||
"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"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
- **拓扑**:按依赖排序执行;未满足入边条件的节点会被标记为跳过。
|
||
- **端口**:`fromPort` → `toPort` 把上游输出接到下游输入;未接线的必填项可写在 `config`。
|
||
- **边条件 `when`**(当前实现):
|
||
- 空:始终通过
|
||
- `hasValidCities`:上游输出该布尔为真
|
||
- `!hasValidCities`:为假
|
||
- 其它字符串:视为通过
|
||
|
||
---
|
||
|
||
## HTTP API
|
||
|
||
端口固定:`http://127.0.0.1:5288`(`Program.cs`)。
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| `GET` | `/api/catalog` | 系统节点 + 插件节点、扫描问题、`pluginsRoot` |
|
||
| `GET` | `/api/credentials` | 对外凭据列表(不含原始 Key) |
|
||
| `GET` | `/api/status` | 当前天气 Provider、插件根路径 |
|
||
| `POST` | `/api/run` | Body 为工作流图 JSON,返回逐步 `steps` |
|
||
|
||
JSON 使用 camelCase。静态站点来自 `wwwroot`。
|
||
|
||
---
|
||
|
||
## 插件机制
|
||
|
||
约定详见 [`plugins/README.md`](plugins/README.md)。每个子目录一份插件。
|
||
|
||
### 必备文件
|
||
|
||
- `plugin.json`:id、启动命令、凭据声明、inputs、outputs
|
||
- `README.md`:给人看的说明
|
||
- 可执行文件:由 `launch.command` + `launch.args` **原样**启动,宿主不替你拼路径
|
||
|
||
示例(FileCity):
|
||
|
||
```json
|
||
"launch": {
|
||
"command": "dotnet",
|
||
"args": ["FileCityPlugin.dll"]
|
||
}
|
||
```
|
||
|
||
工作目录是该插件文件夹;`dotnet FileCityPlugin.dll` 能在该目录下找到 dll。
|
||
|
||
### 进程协议
|
||
|
||
**stdin**(一次请求):
|
||
|
||
```json
|
||
{
|
||
"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 只在宿主。运行插件时会:
|
||
|
||
1. 写入环境变量 `OPENAI_ENDPOINT` / `OPENAI_API_KEY` / `OPENAI_CHAT_MODEL`
|
||
2. 在 stdin JSON 的 `credentials` 里再传一份
|
||
|
||
插件目录可选 `.env` 作为第三方自带模型的默认环境;节点选中的宿主凭据会覆盖同名变量。**不要把 `.env` 提交进仓库。**
|
||
|
||
更细的输入输出说明:
|
||
|
||
- [`plugins/file-city/README.md`](plugins/file-city/README.md)
|
||
- [`plugins/weather/README.md`](plugins/weather/README.md)
|
||
|
||
### 自己加插件
|
||
|
||
1. 新建 `plugins/你的插件/`,写 `plugin.json` 和启动程序。
|
||
2. 若要随宿主一起编译,可仿照 `MAF1.csproj` 增加 `ProjectReference`(`ReferenceOutputAssembly=false`)和 `PublishPluginFolders` 复制规则。
|
||
3. `id` 不要与系统节点类型冲突;若同名,目录会标记覆盖关系。
|
||
4. 重启宿主或点「刷新节点」,确认 `/api/catalog` 的 `issues` 为空。
|
||
|
||
---
|
||
|
||
## 架构
|
||
|
||
```
|
||
浏览器 wwwroot
|
||
│ REST
|
||
▼
|
||
ASP.NET Minimal API (Program.cs)
|
||
│
|
||
▼
|
||
AgentRuntime
|
||
├── NodeCatalogService 合并系统节点 + PluginScanner
|
||
├── CredentialStore llm-default 等
|
||
├── ConfigurableWorkflowRunner 拓扑、端口、when、逐步日志
|
||
├── 系统 Handler FileCity / Weather(进程内)
|
||
└── PluginProcessRunner 子进程 stdin/stdout
|
||
```
|
||
|
||
CLI 不走画布,直接:
|
||
|
||
`CliHost` → `CityWeatherWorkflow`(node / edge)→ `WorkflowOrchestration` 把 Workflow 事件打到控制台。
|
||
|
||
`MAF1.Core` 被宿主与插件共用,避免两套 Agent 逻辑分叉。
|
||
|
||
---
|
||
|
||
## 示例数据
|
||
|
||
`Data/cities.txt`:
|
||
|
||
```
|
||
成都
|
||
阿姆斯特丹
|
||
北京
|
||
```
|
||
|
||
`Data/not-cities.txt`:不含有效城市,用于验证「无城市则不查天气」。
|
||
|
||
这些文件会随构建复制到输出目录。CLI 传入相对路径时,请在仓库根目录(或已复制 Data 的输出目录)下运行。
|
||
|
||
---
|
||
|
||
## 常见问题
|
||
|
||
**启动报「还没有配置模型」**
|
||
`Llm:ApiKey` 和环境变量都为空。按「快速开始」配置后重启。
|
||
|
||
**画布上看不到插件**
|
||
插件必须出现在 **exe 旁边** 的 `plugins/`。先 `dotnet build`,确认 `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`。关掉占用进程,或临时改 `Program.cs` / `launchSettings.json`。
|
||
|
||
**没有自动化测试 / Docker**
|
||
仓库目前没有测试项目和容器文件。验证方式:UI 示例图 + CLI `node` / `edge`。
|
||
|
||
---
|
||
|
||
## 安全注意
|
||
|
||
- LLM Key、OpenWeather Key 只放在本机配置或环境变量,不要写进 `plugin.json`、流程图或前端。
|
||
- `/api/credentials` 只返回公开元数据。
|
||
- 若 Key 曾经出现在 `launchSettings.json` 并被提交或分享,请到服务商控制台轮换密钥。
|
||
|
||
---
|
||
|
||
## 许可与定位
|
||
|
||
本仓库是多智能体编排与进程外插件的**可运行演示**,便于对照系统节点与插件、对照 CLI 的 node / edge 两种条件写法。按你自己的许可证要求补充版权声明即可。
|