Harness 工程
前面几章我们讲的智能体,都是基础款:一个 ChatClientAgent,配好模型、指令、几个工具,能跑起来。但真要做编码助手、自动研究、数据分析这类长链路、自主型任务,你会发现一堆事得自己从头造:
- 工具调用循环要不要限制次数?溢出了怎么办?
- 多步任务,智能体走到第 5 步忘了前面 4 步在干嘛,谁来帮它记计划?
- 上下文越滚越长,工具调个几十轮就把窗口撑爆了,谁来压缩?
- 写文件、读文件、搜网页……这些通用能力每个项目都要重写一遍?
- 危险工具(删文件、跑 shell)要不要审批?怎么"只问一次,以后别再问"?
- 任务一次跑不完,要不要让智能体自己循环到干完为止?
这些事每一件都不难,但加起来就是一整套围绕模型的运行时。Microsoft Agent Framework 把这一整套打包成了一个开箱即用的智能体,这就是 Harness。
目前 Harness 还是属于预览版,很多功能特性没有完善,也没有说明,所以这里只能简单讲解一下。
Harness 是包裹在模型外面的一个引擎。模型本身只会生成文本,要让模型变成真正能干活的智能体,能调工具、能记住多步计划、能持续跑到任务完成、能安全地被无人值守地执行,你需要一套运行时。
Harness 就是这套运行时,而且是"自带电池(batteries-included)"的:常用的东西默认全开,你只配要改的部分,其余都有合理默认值,不想要的还能单独关掉。
它本质上还是一个 ChatClientAgent,只是在它外面包了一圈管道、上下文提供器和装饰器。
这些组件你在前面的章节里基本都见过,Harness 只是把它们按一套经过验证的组合方式拧到了一起。
编码助手(比如你正在用的这类工具)和自主智能体,底层都是某种形式的 Harness。它不是什么神秘黑科技,而是"把长链路任务反复要用的那套基础设施"标准化了。
Harness 把下面这些功能打包到一个智能体里。默认全部开启(标"可选"的除外),每个都能单独关掉或自定义。
| 能力 | 说明 | 关掉它的开关 |
|---|---|---|
| 函数调用 | 自动工具调用循环,可配迭代次数上限 | MaximumIterationsPerRequest 调小 |
| 历史持久化 | 每次模型调用后都落盘一次历史,支持崩溃恢复、运行中检视 | 不可关(核心管道) |
| 压缩(Compaction) | 上下文窗口压缩,防止长工具循环撑爆窗口 | DisableCompaction = true |
| 待办事项提供器 | 持久化的 Todo 列表,跟踪多步计划 | DisableTodoProvider = true |
| 代理模式提供器 | 计划/执行模式切换,结构化智能体的工作方式 | DisableAgentModeProvider = true |
| 文件记忆提供器 | 基于文件的会话记忆,跨轮次存笔记和产物 | DisableFileMemory = true |
| 文件访问提供器 | 限定在工作目录内的读写文件工具 | DisableFileAccess = true |
| 工具审批 | "别再问我"的常设审批规则 + 启发式自动审批 | DisableToolAutoApproval = true |
| OpenTelemetry | 遵循生成式 AI 语义约定的内置可观测性 | DisableOpenTelemetry = true |
| Web 搜索 | 默认挂上的托管网页搜索工具 | DisableWebSearch = true |
| 技能提供器*(可选)* | 从文件系统发现并逐步加载 Agent 技能 | 默认开,DisableAgentSkillsProvider = true 关 |
| 后台智能体*(可选)* | 把并行工作委派给后台子智能体 | 传 BackgroundAgents 才启用 |
| Shell 环境*(可选)* | Shell 命令执行 + 操作系统/Shell/工作目录探测 | 传 ShellExecutor 才启用(仅 .NET) |
| 循环*(可选)* | 重新调起智能体,直到满足完成条件 | 传 LoopEvaluators 才启用 |
这张表基本就是 HarnessAgentOptions 的字段索引,下面会挑重点的讲怎么用。
创建一个 Harness 智能体
Harness 通过 Microsoft.Agents.AI.Harness 包里的 HarnessAgent 类暴露(命名空间 Microsoft.Agents.AI)。最省心的方式是用 IChatClient 上的 AsHarnessAgent 扩展方法:
AIAgent agent = new OpenAIClient(
credential: new ApiKeyCredential("1234"),
options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1") })
.GetChatClient("qwen/qwen3.5-9b")
.AsIChatClient()
.AsHarnessAgent();
AgentResponse response = await agent.RunAsync("帮我规划一个周末西雅图行程。");
Console.WriteLine(response.Text);
一行代码就全都装上了。也可以直接构造:
AIAgent agent = new HarnessAgent(chatClient);
想配置的话,传一个 HarnessAgentOptions。这里有个指令分层的点要先理清:
HarnessAgentOptions.HarnessInstructions:运行时级别的通用操作准则(怎么用工具、怎么组织推理),不设就用内置的HarnessAgent.DefaultInstructions。ChatOptions.Instructions:任务特定指令("你是一个学术研究助手")。
最终发给模型的指令是前者拼后者(源码里就是 "{harnessInstructions}\n\n{agentInstructions}",见 HarnessAgent.BuildInnerAgent)。
AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
Name = "research-agent",
ChatOptions = new ChatOptions
{
Instructions = "你是一个专注学术来源的研究助手。",
Tools = [AIFunctionFactory.Create(GetStockPrice)],
},
});
Harness 的设计哲学是 "默认全开,按需关闭"。每个默认开启的功能在 HarnessAgentOptions 上都有对应的 Disable* 开关,留你要的,砍你不要的:
AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
HarnessInstructions = "这里写自定义的操作准则。",
DisableTodoProvider = true, // 不要待办列表
DisableAgentModeProvider = true, // 不要计划/执行模式
DisableWebSearch = true, // 不要网页搜索工具
DisableFileMemory = true, // 不要文件式会话记忆
});
其它开关还有 DisableFileAccess、DisableAgentSkillsProvider、DisableToolAutoApproval、DisableOpenTelemetry、DisableCompaction。还可以通过 AIContextProviders 加自己的上下文提供器,通过 AgentSkillsSource 把技能发现指向自定义目录。
启用压缩
长工具调用循环最容易翻车的地方:历史越堆越多,最后把上下文窗口撑爆。压缩(Compaction)就是干这件事的——在每次调用前,按一个策略把历史"压"到预算内。
最简单的启用方式,给模型的最大上下文窗口和最大输出 token 设置值,Harness 就会用默认的 ContextWindowCompactionStrategy:
AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
MaxContextWindowTokens = 128_000,
MaxOutputTokens = 16_384,
});
默认就是这两种,但可以替换成自定义模式和说明(通过 AgentModeProviderOptions)。看一眼 SDK 源码(HarnessAgent.BuildInnerAgent)就明白这两个数怎么用了,只有两个都给了,才会构造 ContextWindowCompactionStrategy,少一个都不行:
// 源码位置:src/Microsoft.Agents.AI.Harness/HarnessAgent.cs
if (options?.DisableCompaction is not true)
{
if (options?.CompactionStrategy is CompactionStrategy customStrategy)
compactionStrategy = customStrategy; // ① 自定义策略优先
else if (options?.MaxContextWindowTokens is int maxCtx
&& options?.MaxOutputTokens is int maxOut)
compactionStrategy = new ContextWindowCompactionStrategy(maxCtx, maxOut); // ② 两数齐了用默认
}
想用自己的策略就设 CompactionStrategy;想彻底关掉就 DisableCompaction = true。没给 token 数、也没给自定义策略时,压缩是静默关闭的。
循环到完成
默认情况下,Harness 每次调用只跑一轮。但自主任务往往需要"跑到干完为止"——这时传一个或多个 LoopEvaluator,Harness 会被包进 LoopAgent,自动一遍遍重新调起智能体,直到评估器判定"完成":
AIAgent agent = new OpenAIClient(
credential: new ApiKeyCredential("1234"),
options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1") })
.GetChatClient("qwen/qwen3.5-9b")
.AsIChatClient()
.AsHarnessAgent(new HarnessAgentOptions
{
LoopEvaluators = [new CompletionMarkerLoopEvaluator("DONE")], // 出现 "DONE" 标记即结束
});
AgentResponse response = await agent.RunAsync("帮我规划一个周末西雅图行程。");
Console.WriteLine(response.Text);
关键细节(源码 HarnessAgent.BuildAgent 里能看到):循环是最外层装饰器。也就是说,每一次迭代都是一次完整的智能体运行,独立走完工具调用、工具审批、OpenTelemetry 追踪——而不是在一个循环里反复蹭同一套状态。这意味着每轮都是干净的、可追踪的、可中断的。
代理模式
两个可选能力,按需开启。
代理模式提供器(AgentModeProvider)内置两种默认模式,天然配合待办列表,构成一种两段式工作流:
- 计划模式(Plan)——交互式。智能体先问澄清问题、起草待办列表和方案,在动手干大事之前先获得批准。
- 执行模式(Execute)——自主式。智能体独立推进待办,随时汇报进度。
你也可以自定义自己的模式。
// Arrange
var options = new AgentModeProviderOptions
{
Modes =
[
new AgentModeProviderOptions.AgentMode("draft", "Drafting mode."),
new AgentModeProviderOptions.AgentMode("review", "Review mode."),
],
DefaultMode = "draft"
};
AIAgent agent = new OpenAIClient(
credential: new ApiKeyCredential("1234"),
options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1") })
.GetChatClient("qwen/qwen3.5-9b")
.AsIChatClient()
.AsHarnessAgent(new HarnessAgentOptions
{
Name = "research-agent",
HarnessInstructions = "Use tools deliberately and report verified results.",
DisableAgentModeProvider = false,
AgentModeProviderOptions = options, // 直接把上面定义好 Modes 的 options 传进来
LoopEvaluators = [new CompletionMarkerLoopEvaluator("DONE")], // 出现 "DONE" 标记即结束
});
AgentResponse response = await agent.RunAsync("帮我规划一个周末西雅图行程。");
Console.WriteLine(response.Text);
Shell 与后台智能体
Shell 执行:想让智能体能跑 shell 命令(编译代码、执行脚本、探环境),传一个 ShellExecutor。这会同时注入一个需要审批的 shell 工具,和一个把操作系统/Shell/工作目录信息塞进系统提示的提供器。
安装 Microsoft.Agents.AI.Tools.Shell 包。
using Microsoft.Agents.AI.Tools.Shell;
// 限定在工作目录内。命令默认要审批;denyList 只是 UX 层面的前置过滤,不是安全边界。
await using var shell = new LocalShellExecutor(new LocalShellExecutorOptions
{
WorkingDirectory = workingDir,
ConfineWorkingDirectory = true,
Policy = new ShellPolicy(denyList: [@"\brm\s+-rf\b", @"\bsudo\b"]),
});
AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
ShellExecutor = shell,
});
注意
denyList是"UX 前置过滤"而非"安全围栏"——真正挡住危险操作的是工具审批那一层。把 Harness 暴露给外部输入时,审批配置一定要想清楚。
后台智能体:想让智能体能把子任务并发委派出去,传一组后台子智能体(每个必须有非空的 Name,且名字不能重复):
AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
BackgroundAgents = [webSearchAgent, codeAgent],
});
设了之后会自动挂上一个 BackgroundAgentsProvider,主智能体就能启动、监控、取回后台任务的结果。每个子智能体的 Name 必须非空且不重复(否则构造时抛 ArgumentException)。