MAF · Microsoft.Agents.AI 1.10.0

MAF 在项目中的实现只用 Agent,不用 Workflow

Microsoft Agent Framework 提供了 Agent 与 Workflow 两套编排能力。本项目只采用前者。这不是偷懒,而是因为知识感知的任务分解要求执行路径在运行中生成,而 DAG 要求拓扑在运行前确定——两者根本不兼容。

10
Agent 定义 · 1 编排 + 9 worker
47
Skill 定义 · 含 16 章专属
21
报告撰写员可用工具
0
Agent 侧 Workflow 引用
01 — 选型判断

为什么放弃 Workflow

MAF 的 Workflow 适合步骤固定、分支明确的流程。但 MVR 报告撰写的实际形态不是这样。

Workflow DAG 的前提

  • 执行拓扑在运行前已知并固定
  • 节点间依赖关系可静态声明
  • 分支条件有限且可枚举
  • 适合确定性的多阶段批处理

MVR 撰写的真实形态

  • 下一步查什么,取决于这一步查到了什么
  • 某章证据不足时需回头补查,路径不可预知
  • 不同 Study 的数据集结构不同,字段须实时发现
  • 判定结果影响后续小节是否执行
代码层面的印证

Pike.Agentic.Runtime 模块中没有任何 Workflow 引用。全项目的工作流引擎(Elsa DAG)只出现在 Pike.Knowledge.Pipelines——也就是知识入库管线。那里步骤确实固定:落湖、解析、入仓、向量化、建索引,用 DAG 恰如其分。

02 — 编排结构

一个主编排器,九个 worker

编排不靠图,靠 Agent 之间的委派。主编排器把 worker 当作工具调用,worker 完成任务后回传结果——递归而非拓扑。

mvr-pharma-loop  // 主编排器 · 统一入口 · 意图分流
│
├─ 【闲聊】直接回应,不调用任何工具
│
├─ 【ASK 问答】理解问题 → 按需委派 → 汇总作答
│   ├─ general-knowledge-evidence      知识取证(内含 AI judge 把关)
│   ├─ general-document-locator        文档定位
│   ├─ general-dataset-analyst         数据集分析
│   ├─ general-excel-analyst           Excel 分析
│   └─ general-terminology-translator  术语翻译
│
└─ 【报告撰写】委派专用撰写员
    ├─ mvr-report-writer               MVR 监查访视报告
    ├─ general-report-writer           其余通用报告
    ├─ mvr-kb-extractor                知识库抽取
    ├─ mvr-document-analyst            文档分析
    └─ mvr-action-item-checker         待办核查
  
主编排器的关键设计
不做重试循环
主编排器 maxIterations: 1,答案只生成一次。证据是否充分由 worker 内部的 AI judge 把关,主编排器读取回传结果后一次性作答,不反复重写——避免多层循环叠加导致的 token 消耗失控。
意图分流前置
每轮先判断闲聊 / 问答 / 报告三类意图。闲聊路径禁止调用任何检索工具,避免为一句「你好」触发全链路检索。
规划按需触发
仅当任务需拆成两个以上子任务时才调用规划工具,单跳问题直接执行。
引用后处理
挂载 citation 后处理器,统一将 worker 回传的 chunkId 渲染为可溯源的 [N] 引用标记。
03 — 十六章

一章一 Skill,按需懒加载

MVR 报告的十六个章节,每一章都有独立的 Skill 定义,封装该章的固定问题与判定规则。这些 Skill 挂在报告撰写员名下,但不会全部塞进提示词——只占菜单位置,用到哪章才加载哪章。

CH 01
中心人员
CH 02
伦理批件
CH 03
不良事件
CH 04
知情同意
CH 05
CRF / SDV / SDR
CH 06
方案偏离
CH 07
研究药物
CH 08
紧急揭盲
CH 09
研究测量
CH 10
生物样本
CH 11
WBDC 系统
CH 12
研究物资
CH 13
研究者文件夹
CH 14
ePRO
CH 15
第三方数据
CH 16
其他

章节 Skill 的内部结构

以第三章「不良事件」为例,它并非一段笼统的提示词,而是把该章拆成九个固定小节,每个小节配一份独立的执行说明资源。

逐节执行
严格按 3.1 → 3.9 顺序推进,禁止一次读完所有资源后整章凭记忆生成。开始某小节前必须先读取同题号资源。
Schema 实时发现
每个小节先用 data_mart_discover 找数据集、data_mart_get_schema 确认字段,再查询。字段名一律以实时 schema 为准,不得硬编码——因为不同 Study 的数据集结构不同。
四判定纪律
Yes / No 须证据充分才给;N/A 表示场景不适用;证据不足一律写「无法判断」并标注缺什么证据。绝不猜 Yes/No,也不得用 N/A 掩盖证据不足
增量写入
完成一节即写入工作区,再读下一节资源。避免长上下文累积导致的注意力衰减。
阻塞标记
若本章多项「无法判断」,章节状态记为 blocked,关键缺口标注【待补充】,供人工介入。
04 — Agent 加工具

能力来自工具组合

Agent 本身不含业务逻辑,能力边界由挂载的工具决定。报告撰写员共挂载 21 个工具,分四类。

Workspace · 10 个
文件工作区
list_files read_file write_file edit_file find grep run import_kb_document extract_document analyze_image — 报告以文件形式增量构建,Agent 像开发者操作代码库一样操作报告。这些「文件」不落宿主磁盘,安全边界见 §05。
Knowledge · 6 个
知识库取证
kb_search kb_read kb_discover_documents kb_get_outline kb_check_versions kb_lookup_term — 文字性依据从这里取,支持版本核对与术语查询。
Data Mart · 3 个
结构化取数
data_mart_discover data_mart_get_schema data_mart_query — 所有数值走这条链路。查询工具带默认条数与超时限制,防止失控查询。
分工红线

结构化数值必须经 data_mart 工具查询取得,大模型不得自行计算或臆测。所有写入报告的数值都要落到证据附录,标明来源。这条约束与 HGR 模块「LLM 只做发现与叙述、确定性代码做计算」是同一条原则的两处落地。

05 — 安全边界

Workspace 不是本地文件系统

§04 里那 10 个 Workspace 工具看起来像在读写服务器文件——read_file、write_file、run,名字都很危险。实际不是:这套东西既不碰宿主文件系统,也碰不到知识库原文,且能力按 Agent 显式授予而非默认全开。

文件是数据库记录,不是磁盘文件

STEP 01
Agent 调用
workspace_write_file("report/ch3.md")
STEP 02
路径规范化
WorkspaceFileService.NormalizePath 校验并归一,不是文件系统路径
STEP 03
索引落库
DB 表 AgentWorkspaceFiles 记一行:path / size / version / source
STEP 04
内容入对象存储
key=workspace/{workspaceKey}/{path},内容在 S3

所谓「路径」只是数据库里的一个字符串字段。整条链路没有任何一处调用宿主文件系统 API,因此「Agent 误删系统文件」这类风险在架构上不成立——它连入口都没有。

路径校验规则 · NormalizePath
拒绝 ..
任一路径段等于 .. 直接抛异常,防目录穿越
去前导 /
杜绝绝对路径写法
折叠重复 /
正则归一,防用 // 绕过校验
长度上限
1024 字符
key 消毒
workspaceKey 经 [^A-Za-z0-9._-] 替换后才拼进存储 key

Workspace 与知识库是两套东西

Workspace · Agent 的草稿纸

  • 存报告十六章草稿、中间统计结果
  • 对象存储 workspace/ 前缀
  • 可读可写
  • 会话结束即为一次性产物

知识库 · SharePoint 同步的临床文档

  • Milvus 向量 + 对象存储原文
  • 经 Elsa 入库 pipeline 写入
  • Agent 只能检索,不能修改
  • 受 §05 权限与范围约束(见检索篇)

二者唯一的连接是 workspace_import_kb_document——把知识库文档复制一份进工作区,单向,且不回写。原始临床文档在任何情况下都不会被 Agent 改动。

会话级隔离 · LLM 无法指定目标工作区

解析方式
context["session_id"] → 反查 AgentWorkspaceSessions → 得到 workspaceKey
关键设计
workspaceKey 由服务端反查得出,不是工具参数——LLM 根本碰不到这个字段,无法通过构造参数访问他人工作区
缺省行为
不在 Workspace 会话中调用文件工具,直接报错「当前不在 Workspace 会话中,无法使用工作区文件工具」

这与检索链路中「个人库 owner 由服务端注入、LLM 不可覆盖」是同一条原则:凡是决定访问范围的标识,一律不经过模型。

代码执行 · bubblewrap 内核级沙箱

Workspace 工具里的 runsandbox_run_command 确实会执行命令,但跑在独立沙箱进程里,不是宿主 shell。用的是 bubblewrap(Flatpak 同款),非自研逻辑隔离。

命名空间隔离
--unshare-pid
看不到宿主进程
--unshare-net
默认断网,需显式 allowInternetAccess 才放行
--unshare-ipc / uts / cgroup
进程间通信、主机名、资源组均隔离
--new-session
独立会话,防 TTY 劫持
--die-with-parent
父进程退出则必然终止,不留孤儿进程
文件系统视图
/usr /bin /lib /etc
--ro-bind 只读,内核级拒绝写入
/tmp
tmpfs 内存盘,重启即失
/workspace
唯一可写目录
知识库挂载
经 FUSE 以 --ro-bind 注入,同样内核级只读
兜底
执行超时上限、并发数限制、过期沙箱后台自动回收

最关键的一层:能力按需授予

沙箱是最后一道防线。但更重要的事实是——绝大多数 Agent 压根走不到那道防线

全仓统计
1 个
挂载了沙箱执行能力的 Agent 总数——只有 general-excel-analyst。代码执行不是通用能力。
全仓统计
5 个
使用 Workspace 文件工具的 Agent:excel-analyst、report-writer、skill-mining 系列。其余 Agent 无文件能力。
MVR 生产链路
0 个
主编排器与九个 chapter worker 均未挂载沙箱执行工具。临床报告生成全程不执行任意代码。

工具在 Agent 的 YAML 定义里逐个显式声明,没有「默认全部可用」这一档。这是比沙箱本身更根本的一层控制:不需要的能力从一开始就不存在,而非依赖运行时拦截。

部署时需确认的两个开关

AllowUserNamespaceEnableNetworkNamespace 是可配置项。若部署环境(如受限容器)不支持 user namespace 而将其置为 false沙箱的 UID 隔离会相应减弱,此时应依赖容器自身的隔离作为补偿。生产部署前须核对这两项的实际取值,不要默认它们为开启状态。

06 — 运行时

三种执行目标,一个入口

AgentFactory 是 Agent / Skill / Tool 三类目标的统一执行入口,对外暴露单一 RunAsync 契约。

TARGET · AGENT
完整 Agent 运行
装配工具与技能,进入迭代循环,可递归委派 worker
TARGET · SKILL
单技能执行
用于技能调试与独立能力调用
TARGET · TOOL
工具试验场
单工具直接调用,验证参数与返回契约

LoopAgent · 自研迭代循环

改写自 MAF 上游实现(MIT 许可),承担多轮迭代与完成判定。

迭代上限
默认 10 轮,各 Agent 可按需覆写。报告撰写员设为 24 轮以支撑长章节
完成评估器
可插拔 LoopEvaluator,支持基于 todo 完成度的判定,也可设为 none 单轮直出
上下文策略
支持每轮清空重建或累积延续,按任务特性选择
流式模式
status_plus_final — 过程推送状态,最终推送完整答案,兼顾可观测与输出洁净
输出清洗
实现 IAgentOutputSanitizer,剥离完成标记等内部控制符,不泄漏到用户可见文本
07 — 资产治理

Agent 与 Skill 是数据,不是代码

全部 10 个 Agent 与 47 个 Skill 以 YAML 定义,由 OpsMate.Init 在启动时种入数据库。这意味着调整业务规则不需要改 C# 代码、不需要重新编译。

定义结构
Agent
模型池与参数 · 运行时形态 · 可委派 worker 列表 · 挂载工具 · 挂载技能 · 后处理器 · 指令文本
Skill
指令文本 · 资源列表(逐小节执行说明,支持懒加载)· 版本号
版本与演进
版本标识
文件名即版本(v1.agent.yaml),技能引用可指定版本(如 B1
同步机制
BuiltinSkillSyncService 负责内置技能与数据库的一致性维护
配套评测
各阶段配有评测集与运行记录,能力变更可回归验证
08 — 依赖深度

薄封装,不是深度绑定

把「用了 MAF」说成「依赖 MAF」并不准确。真正引用 MAF 本体的只有 3 个项目,大量使用的其实是更底层的 Microsoft.Extensions.AI——那是模型调用抽象,不是 Agent 框架。

Microsoft.Agents.AI · MAF 本体
版本
1.10.0(含 .OpenAI
引用项目
仅 3 个Pike.Agentic.Runtime · Pike.Agentic · Pike.Agentic.Tools
用到的类型
AIAgent(10 处) · ChatClientAgent(3) · AgentRunOptions(6) · AIFunction(4,作为基类继承)
已显式删除
Microsoft.Agents.AI.Workflows — 不是没用,是主动移除了包依赖
Microsoft.Extensions.AI · 模型抽象
版本
10.7.0(含 Abstractions / Evaluation / Quality / OpenAI)
引用模块
13 个:Knowledge · Evaluation · AiGateway · SkillMining · Workspace 等
用到的类型
ChatMessage(100 处) · ChatRole(61) · IChatClient(50) · ChatOptions(27)
性质
这才是真正的广泛依赖——但它是通用模型调用契约,与是否用 MAF 编排无关

官方能力的取舍

采用

  • ChatClientAgent — 基础 Agent 实现
  • DelegatingAIAgent / AgentSession — 委派与会话
  • 上下文 compaction(走 evaluation-only API)
  • ChatMessage / AIFunction 等类型契约

弃用

  • Workflows — 显式删包
  • Sequential / Concurrent — 官方编排模式
  • GroupChat / Handoff — 官方多 Agent 模式
  • 内置 memory · 内置 middleware

官方提供的五种编排模式一个没用,全部替换为自研的 AgentFactory + LoopAgent 递归委派。原因与 §01 一致:这些模式的拓扑都需要预先声明。

LoopAgent 为何要 fork

隐藏中间状态
上游默认暴露迭代过程,本项目改为对用户不可见
完成标记剥离
把完成标记视为内部控制信号,最终答案输出前统一剥掉(LoopAgent.cs:78-88 SanitizeAssistantAnswer + IAgentOutputSanitizer
可插拔评估器
代码已支持多 evaluator,配置层当前只开放单个
迁移成本评估

若要脱离 MAF,不可替代的只有三块:ChatClientAgentDelegatingAIAgent/AgentSession、compaction。改动集中在 Run/Standard/Run/Common/AgentChatClientAssemblyBuilder.csRun/Loop/LoopAgent.cs 三处,量级为数百行。结论:MAF 在此是一层可替换的类型契约,不构成锁定

已知待修 · 影响官方管线

PoolRoutingChatClient.cs:130GetService 无条件返回 null,导致官方管线(含 UseFunctionInvocation 与 compaction)拿不到底层 client 的能力与元数据。不影响当前功能,但会限制后续复用官方中间件。建议改为继承 DelegatingChatClient 或转发至 inner client。