Files
MAF1/README.md
T
admin777 b631acd42e feat: 将插件 stdin 协议定为 v1,并按声明注入凭据
进程外插件改为只读环境变量和自身配置,避免读宿主 appsettings;宿主按 plugin.json 声明注入 LLM / OpenWeather 等凭据。
2026-09-11 10:46:52 +08:00

18 KiB
Raw Blame History

MAF1 — 多智能体工作流编排

MAF1 是一个基于 .NET 10Microsoft Agents AI 的多智能体演示仓库。同一条「读文件抽城市 → 查天气」链路拆成两个可执行项目:

  1. MAF1:命令行工作流。用 Microsoft Agents AI Workflows 对比「条件写在节点里」和「条件写在边上」。
  2. MAF1.Route:命令行路由。方案 C 的图编译成 AgentWorkflowBuilder.CreateHandoffBuilderWith + WithHandoff;专家仍是 Core 里的 Agent。
  3. 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 10net10.0
控制台 MAF1:工作流;MAF1.Route:方案 C 路由
网页 MAF1.WebASP.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.slnxMAF1MAF1.RouteMAF1.WebMAF1.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.Route/                 # 控制台路由(方案 C)
│   ├── Graphs/                 # 白名单图 JSON(有边 / 无边)
│   ├── 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)。

方式 Bappsettings.jsonLlm 节点

"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.jsonnode / edge)。

3. 启动可视化编排

dotnet run --project MAF1.Web

浏览器打开 http://127.0.0.1:5288。控制台会打印:可视化编排: http://127.0.0.1:5288

界面操作建议:

  1. 示例图,生成「抽城市 → 查天气」的演示图(含边条件)。
  2. 选中节点,在右侧填 filePath(例如 Data/cities.txt),或检查端口映射。
  3. 运行,底部查看每步输入 / 输出。
  4. 改完 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

5. 命令行路由(方案 C

MAF1.Route 用 Microsoft Agents AI 的 WithHandoff 跑方案 C:图 JSON 是专家白名单,边是允许交接。主管是入口 Agent,框架给每条边注入 handoff_to_* 工具,控制权随对话转交。

  • 节点 = 白名单。图上没有的类型不能调。
  • 有边 = 只能沿边 WithHandoffGraphs/city-weather.json:必须先抽城市再查天气)。
  • 无边 = 主管可交接给名单内任一专家(Graphs/nodes-only.json:可以直接查「成都」天气)。

抽不到城市时,抽城市专家不交接,本轮结束(Handoff 不像工作流 CLI 那样弹确认框)。

dotnet run --project MAF1.Route
dotnet run --project MAF1.Route -- Graphs/city-weather.json
dotnet run --project MAF1.Route -- Graphs/city-weather.json Data/not-cities.txt
dotnet run --project MAF1.Route -- Graphs/nodes-only.json 请查询成都的天气

启动配置:

项目 Profile 作用
MAF1.Web designer 网页编排器
MAF1 node CLI,节点内判断
MAF1 edge CLI,边上判断
MAF1.Route route CLI 路由,有边白名单
MAF1.Route nodes-only CLI 路由,无边自选顺序

配置说明

MAF1/appsettings.jsonMAF1.Route/appsettings.jsonMAF1.Web/appsettings.json 会分别复制到各自输出目录。网页项目额外包含 Plugins / Credentials。主要段落:

Llm

宿主内置 Agent 与默认凭据 llm-default 使用这一段(可再被环境变量覆盖)。

Plugins

字段 含义
Directory 相对宿主内容根的插件目录,默认 plugins
DefaultTimeoutSeconds 插件进程默认超时(秒),清单里可单独覆盖

插件进程会收到 MAF1_CONTENT_ROOT,用于定位宿主的 appsettings.json(天气插件读天气配置)。

Credentials

节点只声明凭据类型。llm-defaultsource: 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(秒)、askOnEmptyCitiesfalse 时抽不到城市不询问,直接按边条件跳过)。


内置 Agent

两条链路语义相同,只是执行位置不同。

FileCity(抽城市)

  • 输入:filePath(本地文本路径,相对宿主工作目录即可)
  • 输出:hasValidCitiescitiesreasonraw
  • 行为:LLM + 读文件工具,从文本里抽出有效城市名
  • 若没有有效城市且后面还有节点:工作流会 暂停等待确认。默认方案是结束后续查询;可选方案是手动输入城市继续。超时走默认。

系统节点类型:fileCity-system
插件 idfileCityplugins/file-city

Weather(查天气)

  • 输入:cities(字符串数组)
  • 输出:summary
  • 行为:LLM + HTTP 天气工具,汇总各城市天气

系统节点类型:weather-system
插件 idweatherplugins/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,不立刻跑完。
  • 端口fromPorttoPort 把上游输出接到下游输入;未接线的必填项可写在 config
  • 边条件 when(当前实现):
    • 空:始终通过
    • hasValidCities:上游输出该布尔为真
    • !hasValidCities:为假
    • 其它字符串:视为通过

HTTP API

端口固定:http://127.0.0.1:5288MAF1.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.jsonid、protocolVersion、启动命令、凭据声明、inputs、outputs
  • README.md:给人看的说明
  • 可执行文件:由 launch.command + launch.args 原样启动,宿主不替你拼路径

示例(FileCity):

"launch": {
  "command": "dotnet",
  "args": ["FileCityPlugin.dll"]
}

工作目录是该插件文件夹;dotnet FileCityPlugin.dll 能在该目录下找到 dll。

进程协议

stdin(一次请求):

{
  "protocolVersion": 1,
  "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 只在宿主。只注入 plugin.json 声明过的凭据。运行插件时会:

  1. 按 type 写入环境变量(LLM 为 OPENAI_*OpenWeather 为 OPENWEATHER_API_KEY
  2. 在 stdin JSON 的 credentials 里再传一份

JSON Schemadocs/plugin-protocol-v1.schema.jsonplugin.jsonprotocolVersion 缺省视为 1;大于 1 的清单会被扫描拒绝。

插件目录可选 .env 作为第三方自带模型的默认环境;节点选中的宿主凭据会覆盖同名变量。不要把 .env 提交进仓库。 非秘密配置放在插件自己的 appsettings.json,不要读宿主 exe 目录。

更细的输入输出说明:

自己加插件

  1. 新建 plugins/你的插件/,写 plugin.json 和启动程序。
  2. 若要随网页宿主一起编译,可仿照 MAF1.Web.csproj 增加 ProjectReferenceReferenceOutputAssembly=false)和 PublishPluginFolders 复制规则。
  3. id 不要与系统节点类型冲突;若同名,目录会标记覆盖关系。
  4. 重启宿主或点「刷新节点」,确认 /api/catalogissues 为空。

架构

浏览器 MAF1.Web/wwwroot
    │  REST
    ▼
MAF1.Web Program.csMinimal API
    │
    ▼
AgentRuntime
    ├── NodeCatalogService     合并系统节点 + PluginScanner
    ├── CredentialStore        llm-default 等
    ├── ConfigurableWorkflowRunner   拓扑、端口、when、逐步日志
    ├── 系统 Handler           FileCity / Weather(进程内)
    └── PluginProcessRunner    子进程 stdin/stdout

控制台项目不走画布:

MAF1CliHostCityWeatherWorkflownode / edge)→ WorkflowOrchestration。缺城市时走 RequestPort,与网页同一套默认方案 + 超时。

MAF1.RouteHandoffGraphBuilderCreateHandoffBuilderWith + WithHandoff)→ Core 里的 FileCity / Weather。图 JSON 决定白名单和允许交接;原来的 workflow CLI 仍在 MAF1 里。

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 + MAF1.Route


安全注意

  • LLM Key、OpenWeather Key 只放在本机配置或环境变量,不要写进 plugin.json、流程图或前端。
  • /api/credentials 只返回公开元数据。
  • 若 Key 曾经出现在 launchSettings.json 并被提交或分享,请到服务商控制台轮换密钥。

许可与定位

本仓库是多智能体编排与进程外插件的可运行演示,便于对照系统节点与插件、对照 CLI 的 node / edge 两种条件写法。按你自己的许可证要求补充版权声明即可。