Microsoft Agent Framework — 只有三个文件,Harness 凭什么扛起一个生产级 Agent?
目录一、先纠正一个常见的认知偏差二、Harness 在 MAF 里的物理位置一个极薄的程序集三、Harness 站在哪些 MAF 抽象之上四、装配清单Harness 默认替你装了什么五、装配顺序即设计意图六、默认指令也是装配的一部分七、为什么这套实现是组合而不是封装八、装配之外每个零件还能往下挖什么总结一、先纠正一个常见的认知偏差很多人第一次看到HarnessAgent会下意识把它当成和AIAgent、ChatClientAgent、AIContextProvider平级的又一个核心概念去记。这是个误解而且会让后面越读越乱。HarnessAgent没有发明任何新能力。函数调用、上下文压缩、聊天历史持久化、待办管理、文件记忆、技能发现、工具审批、链路追踪——这些零件在 MAF 里本来就都存在散落在核心程序集的不同角落。Harness 做的事情只有一件把它们按一个经过验证的顺序拼成一条流水线再用一行扩展方法交给你。换句话说Harness 在 MAF 的版图里不是一个新模块而是一个装配层assembly layer——它的价值不在于造了什么而在于替你把散件装好、并把装配顺序里那些容易踩的坑写死成正确值。这是理解后面所有内容的总纲。二、Harness 在 MAF 里的物理位置一个极薄的程序集要验证Harness 是装配层这个判断最直接的证据是看它在源码里占了多大体积。HarnessAgent独立成一个 NuGet 包 / 程序集Microsoft.Agents.AI.Harness而这个程序集总共只有三个源文件职责各自单一HarnessAgent.cs—— 装配逻辑本体把零件按序拼成流水线对外是一个DelegatingAIAgent。HarnessAgentOptions.cs—— 装配的配置面板每个零件的开关、替换点、子选项。ChatClientHarnessExtensions.cs—— 一个扩展方法AsHarnessAgent(...)挂在IChatClient上。只有三个文件Harness 凭什么扛起一个带工具调用、长对话不爆窗口、还自带待办 / 文件 / 技能 / 审批 / 追踪的生产级 Agent答案藏在下一句里——关键事实是它真正在用的那些零件一个都不住在这个程序集里。TodoProvider、AgentModeProvider、FileMemoryProvider、FileAccessProvider、AgentSkillsProvider、BackgroundAgentsProvider、以及压缩用的ContextWindowCompactionStrategy/CompactionProvider——全部住在核心程序集Microsoft.Agents.AI的Harness/子目录下。ShellEnvironmentProvider/ShellExecutor住在Microsoft.Agents.AI.Tools.Shell而且只在 .NETnet8.0目标下才被引用。函数调用、消息注入、历史持久化、上下文压缩这几个装饰器是Microsoft.Extensions.AI层的IChatClient能力。所以依赖方向非常清楚Microsoft.Agents.AI.Harness ← 极薄的装配层3 个文件 │ 依赖 ▼ Microsoft.Agents.AI ← 真正的零件仓库各 Provider、Compaction 都在这 │ 依赖 ▼ Microsoft.Extensions.AI ← 更底层的 IChatClient / 装饰器抽象 可选仅 .NET Microsoft.Agents.AI.Harness ──→ Microsoft.Agents.AI.Tools.Shell一个有意思的细节HarnessAgent类型本身在Microsoft.Agents.AI命名空间下但创建它的扩展方法AsHarnessAgent却被刻意放进了更底层的Microsoft.Extensions.AI命名空间。这意味着只要你 using 了Microsoft.Extensions.AI任意一个IChatClient实例上就会长出这个方法——Harness 把自己伪装成了IChatClient的一个原生入口。这是它低存在感、高便利性定位的一个微观体现。三、Harness 站在哪些 MAF 抽象之上既然 Harness 自己不造零件那它就是一个纯粹的抽象组合者。它把 MAF 的几层既有抽象自下而上叠起来每一层都是 MAF 本来就提供的扩展点┌─────────────────────────────────────────────────────────┐ │ HarnessAgent : DelegatingAIAgent │ ← 顶层只是个委托壳 │ 对外就是一个普通 AIAgent调用转发给下面装好的 agent │ 不持有业务逻辑 ├─────────────────────────────────────────────────────────┤ │ Agent 装饰器层AIAgentBuilder │ ← UseToolApproval / UseOpenTelemetry │ ToolApprovalAgent → OpenTelemetryAgent │ 在Agent粒度上包横切关注点 ├─────────────────────────────────────────────────────────┤ │ ChatClientAgent由 BuildAIAgent 产出 │ ← MAF 把IChatClient 选项 │ 持有 ChatOptions / ChatHistoryProvider / │ 收敛成一个标准 Agent 的地方 │ 一组 AIContextProvider │ ├─────────────────────────────────────────────────────────┤ │ AIContextProvider 模型 │ ← 可插拔的上下文注入抽象 │ Todo / AgentMode / FileMemory / FileAccess / │ 每个 Provider 往上下文里 │ AgentSkills / Background / Shell │ 叠 prompt、挂工具 ├─────────────────────────────────────────────────────────┤ │ IChatClient 装饰链Microsoft.Extensions.AI │ ← 最底层对模型调用的 │ FunctionInvoking → MessageInjecting → │ 管道式增强 │ PerServiceCallChatHistoryPersisting → AIContextProvider│ └─────────────────────────────────────────────────────────┘逐层看Harness 用到的核心调用链就是把这几层串起来在IChatClient上.AsBuilder()依次挂UseFunctionInvocation→UseMessageInjection→UsePerServiceCallChatHistoryPersistence→UseAIContextProviders(压缩)再.BuildAIAgent(...)落成一个ChatClientAgent然后在这个 Agent 上再.AsBuilder()挂UseToolApproval→UseOpenTelemetry最后包进HarnessAgent这个DelegatingAIAgent壳里。这条链里没有一个环节是 Harness 新造的抽象——AIAgentBuilder、ChatClientAgent、AIContextProvider、IChatClient装饰器全是 MAF 通用机制。Harness 的全部工作就是替你选定零件 排好顺序 对齐开关。四、装配清单Harness 默认替你装了什么下面这张表是 Harness 的零件清单也是后面深入每个零件时的索引。它分三层内层 4 件是流水线骨架不可关中间一组 ContextProvider 是能力插件可关外层 2 件是横切装饰可关。层次零件干什么默认关闭开关内层装饰链UseFunctionInvocation自动执行模型发起的工具调用always—内层装饰链UseMessageInjection允许外部代码中途往对话里注入消息always—内层装饰链UsePerServiceCallChatHistoryPersistence函数调用循环里每次服务调用后存历史always—内层装饰链UseAIContextProviders(CompactionProvider)每次调用前做上下文窗口压缩always—ContextProviderTodoProvider待办列表管理✔DisableTodoProviderContextProviderAgentModeProviderplan / execute 模式跟踪✔DisableAgentModeProviderContextProviderFileMemoryProvider基于文件的会话记忆✔DisableFileMemoryContextProviderFileAccessProvider工作目录文件读写✔DisableFileAccessContextProviderAgentSkillsProvider技能发现与加载✔DisableAgentSkillsProviderContextProviderBackgroundAgentsProvider把任务委派给后台子 Agent仅当BackgroundAgents非空—ContextProviderShellEnvironmentProviderShell 命令执行仅 .NET仅当配了ShellExecutor—默认工具HostedWebSearchTool内置 Web 搜索✔DisableWebSearch外层装饰器ToolApprovalAgent别再问我了工具审批✔DisableToolApproval外层装饰器OpenTelemetryAgentOpenTelemetry 链路追踪✔DisableOpenTelemetry还有一处替你对齐值得单独点出当你不传自己的ChatHistoryProvider时Harness 默认给你一个InMemoryChatHistoryProvider并且把和压缩策略同源的ChatReducer挂上去——这样实时上下文和落盘历史用的是同一套裁剪规则跨会话的内存历史也不会无限增长。这正是手动装配时最容易漏对齐的一处。一点提醒表中HostedWebSearchTool是由模型服务端托管执行的工具需要你接的 Provider / 模型本身支持托管 Web 搜索才会生效若模型不支持可用DisableWebSearch关掉避免困惑。五、装配顺序即设计意图零件清单只回答装了什么真正体现实现思路的是按什么顺序装。顺序错了Agent 要么行为不对要么直接出问题。下面这张图是 Harness 的骨架真相理解它之后每个零件该落在哪一环就都有了坐标。chatClient │ ▼ 内层IChatClient 装饰链自底向上包裹最先挂的最贴近模型 ├─ UseFunctionInvocation ← 最内层先把工具调用执行掉 ├─ UseMessageInjection ← 再允许中途注入消息 ├─ UsePerServiceCallChatHistoryPersistence ← 每次服务调用后落历史 ├─ UseAIContextProviders(CompactionProvider) ← 调用前压缩避免循环爆窗口 └─ BuildAIAgent → ChatClientAgent ├─ ChatHistoryProvider 默认 InMemory 同源压缩 Reducer └─ AIContextProviders注入次序固定 1. TodoProvider 2. AgentModeProvider 3. FileMemoryProvider 4. FileAccessProvider 5. AgentSkillsProvider 6. BackgroundAgentsProvider BackgroundAgents 非空时 7. ShellEnvironmentProvider 配了 ShellExecutor 时仅 .NET │ ▼ 外层Agent 装饰器 ├─ UseToolApproval ← 先过审批 └─ UseOpenTelemetry ← 最外层包追踪覆盖全部内部活动 │ ▼ HarnessAgentDelegatingAIAgent对外就是一个普通 AIAgent几个关键的为什么每一条都解释了一个实现决策FunctionInvocation在最内层工具调用必须紧贴模型先被执行掉外面的压缩、持久化、追踪才有完整的一次工具循环可以处理。Compaction在调用前生效长函数调用循环最容易把上下文撑爆所以压缩挂在UseAIContextProviders上每次调模型前先裁剪而不是事后补救。历史 Reducer 与CompactionProvider同源两处用的是同一个ContextWindowCompactionStrategy保证实时上下文和落盘历史用同一套规则不会一边压了一边没压。OpenTelemetry在最外层只有包在最外面它的 Span 才能覆盖里面所有工具调用、模型调用、压缩动作——追踪要的就是全景。ContextProvider 注入次序固定Todo → Mode → FileMemory → FileAccess → Skills →Background→Shell。这个顺序决定了它们往系统上下文里叠 prompt的先后。此外Harness 还替你写死了两个极易漏的对齐开关让框架不再二次包裹你的IChatClient、以及强制每次服务调用落历史。它们必须和内层装饰链保持一致Harness 直接固化成正确值不给你出错的机会。六、默认指令也是装配的一部分Harness 装配的不只是零件还包括一段内置的默认 system 指令。它的核心是几条通用 agent 行为准则先想后做、把复杂任务拆步骤、用工具验证而非凭记忆、连续工具调用之间解释自己在干什么、失败了就换方法别重复同一调用。它和你自己传的业务指令是拼接关系默认指令在前、业务指令在后不是覆盖。这就解释了 Harness 官方示例里一个常见现象每个示例的业务指令只写研究助手 / 数据分析助手这类业务独有的部分完全不写先想后做拆步骤这些通用准则——因为装配时已经替它们拼进去了。七、为什么这套实现是组合而不是封装理解了上面的结构就能看懂 Harness 一个关键的实现取向它选择了组合composition而非封装encapsulation。如果走封装路线Harness 大可以把 Todo、压缩、文件记忆等逻辑都吃进自己内部对外只暴露几个粗粒度开关。但它没有——它把每个能力都保留为独立的、住在核心程序集里的AIContextProvider/ 装饰器自己只负责挑选 排序 对齐。这带来三个直接的好处每个零件都能脱离 Harness 单独使用你完全可以不要 Harness自己new TodoProvider()挂到任意ChatClientAgent上。Harness 关停了也不影响这些 Provider 的存在。装配是可改写的不是黑盒HarnessAgentOptions暴露了一整排扩展点——每个 Provider 的Disable*开关、FileMemoryStore/FileAccessStore/AgentSkillsSource这类替换点、AgentModeProviderOptions这类子选项以及一个AIContextProviders口子让你追加自己的 Provider。你既能做减法关掉不需要的也能做加法塞进自定义的。升级与替换是局部的因为零件住在核心程序集、Harness 只持有装配关系单个 Provider 的演进不需要动 Harness 这三个文件反过来Harness 调整装配顺序也不会改动任何一个 Provider 的实现。一句话Harness 是一份配方不是一个成品罐头。它把 MAF 推荐的 agent 默认值固化下来但每一味配料你都还能换、能减、能加。八、装配之外每个零件还能往下挖什么本文停在装配这一层——只讲了零件清单和拼装顺序。真正想吃透 Harness下一步是钻进单个零件看它内部怎么工作、有哪些档位可调、什么场景该开该关压缩CompactionContextWindowCompactionStrategy怎么决定裁哪些、留哪些为什么要同时驱动 Provider 和历史 Reducer。ContextProvider 家族Todo / AgentMode / FileMemory / FileAccess / Skills 各自往上下文里注入什么、暴露哪些工具。后台子 AgentBackgroundAgents父 Agent 怎么并发委派、收割、清理子任务。工具审批与可观测性ToolApproval的别再问我了规则OpenTelemetry的 Span 分层。MAF 官方提供了4 个 Harness 示例Research / BackgroundAgents / DataProcessing / CodeExecution它们是 Harness 设计意图最好的活文档。想看每个示例怎么跑、每行代码什么意思可以配合示例解析一起读示例逐行解析Microsoft Agent Framework - Harness 示例解析总结Harness 是装配层不是新概念。它在 MAF 里只占一个极薄的程序集三个文件真正的零件全部住在核心程序集它的全部工作是挑选 排序 对齐。它站在 MAF 既有抽象之上IChatClient装饰链 →AIContextProvider模型 →ChatClientAgent→AIAgentBuilder装饰器 →DelegatingAIAgent壳一层都没新造。装配顺序即设计意图FunctionInvocation 贴模型、Compaction 调用前生效、历史 Reducer 与 CompactionProvider 同源、OpenTelemetry 包最外层、ContextProvider 注入次序固定。实现取向是组合而非封装每个零件都能脱离 Harness 单独用HarnessAgentOptions把减法Disable*和加法替换点 追加口子都开放出来。应用场景要快速立起一个能用工具、长对话不爆窗口、带待办/文件/技能/审批/追踪的生产级 Agent又不想手写易错装配时Harness 是首选只需要极简 Agent、或要对装配链做深度定制时回退到手动组合即可。最佳实践默认全开起步用Disable*按需做减法如后台子 Agent 场景里把父 Agent 的 Todo/FileMemory/FileAccess 关掉、只留 BackgroundAgents而不是从零手动加法——这也是 4 个官方示例反复演示的用法。引入地址