MAF · Microsoft.Agents.AI 1.10.0
MAF 在项目中的实现只用 Agent,不用 Workflow
Microsoft Agent Framework 提供了 Agent 与 Workflow 两套编排能力。本项目只采用前者。这不是偷懒,而是因为知识感知的任务分解要求执行路径在运行中生成,而 DAG 要求拓扑在运行前确定——两者根本不兼容。
10
Agent 定义 · 1 编排 + 9 worker
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 挂在报告撰写员名下,但不会全部塞进提示词——只占菜单位置,用到哪章才加载哪章。
章节 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
拒绝 ..
任一路径段等于 .. 直接抛异常,防目录穿越
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 工具里的 run 与 sandbox_run_command 确实会执行命令,但跑在独立沙箱进程里,不是宿主 shell。用的是 bubblewrap(Flatpak 同款),非自研逻辑隔离。
命名空间隔离
--unshare-net
默认断网,需显式 allowInternetAccess 才放行
--unshare-ipc / uts / cgroup
进程间通信、主机名、资源组均隔离
--new-session
独立会话,防 TTY 劫持
--die-with-parent
父进程退出则必然终止,不留孤儿进程
文件系统视图
/usr /bin /lib /etc
--ro-bind 只读,内核级拒绝写入
知识库挂载
经 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 定义里逐个显式声明,没有「默认全部可用」这一档。这是比沙箱本身更根本的一层控制:不需要的能力从一开始就不存在,而非依赖运行时拦截。
部署时需确认的两个开关
AllowUserNamespace 与 EnableNetworkNamespace 是可配置项。若部署环境(如受限容器)不支持 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 本体
引用项目
仅 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,不可替代的只有三块:ChatClientAgent、DelegatingAIAgent/AgentSession、compaction。改动集中在 Run/Standard/、Run/Common/AgentChatClientAssemblyBuilder.cs、Run/Loop/LoopAgent.cs 三处,量级为数百行。结论:MAF 在此是一层可替换的类型契约,不构成锁定。
已知待修 · 影响官方管线
PoolRoutingChatClient.cs:130 的 GetService 无条件返回 null,导致官方管线(含 UseFunctionInvocation 与 compaction)拿不到底层 client 的能力与元数据。不影响当前功能,但会限制后续复用官方中间件。建议改为继承 DelegatingChatClient 或转发至 inner client。