正如穷举MAF所有可能的Harness手段所说,LangChain平台预定义的Harness手段整合在Deep Agents中,具体体现在利用create_deep_agent方法创建的Deep Agent中。MAF则将这些Harness手段整合在HarnessAgent这个Agent中间件中,HarnessAgent就是MAF中的Deep Agent。由于Agent的特性和能力决定于底层构建的Agent管道,所以可用的Harness手段是Agent管道定制手段的子集。前面一篇文章穷举了所有用于定制Agent管道的手段,主要体现在如下三个方面。
- 注册Agent中间件
- 配置
ChatClientOptions,其中包括但不限于:- 注册用于持久化对话历史的
ChatMessageHistoryProvider - 注册用于LLM输入输出增强的
AIContextProvider - 在
ChatOptions上指定系统指令 - 在
ChatOptions上注册工具
- 注册用于持久化对话历史的
- 注册
ChatClient中间件接下来我们将看看究竟有哪些手段被使用在HarnessAgent`中。
1. 被HarnessAgent代理的是个怎样的Agent?
针对上述的三个方面,我们来剖析一下使用默认配置选项创建的HarnessAgent,它注册了哪些Agent中间件和ChatClient中间件,它绑定了怎样的ChatClientOptions。为此我们定义了如下这个静态的辅助方法PrettyPrintAgent,在这个方法中我们利用反射获取了HarnessAgent中注册的Agent中间件、ChatClient中间件以及ChatClientAgentOptions的详细信息,并以较为友好的格式打印出来。
staticvoidPrettyPrintAgent(DelegatingAIAgentharnessAgent){PrintAgentMiddlewares(harnessAgent,true);var(chatClientAgent,agentOptions)=PrintAgentOptions(harnessAgent);PrintChatClientMiddlewares(chatClientAgent.ChatClient,true);Console.WriteLine($"\nInstructions: \n{agentOptions.ChatOptions?.Instructions}");}staticvoidPrintAgentMiddlewares(DelegatingAIAgent?harnessAgent,booldisplayTitle=false){if(displayTitle)Console.WriteLine("AgentMiddlewares:");if(harnessAgentisnotnull){Console.WriteLine($"{harnessAgent.GetType().Name}");PrintAgentMiddlewares(GetNonPublicPropertyFieldValue(harnessAgent,"InnerAgent")asDelegatingAIAgent);}}static(ChatClientAgent,ChatClientAgentOptions)PrintAgentOptions(DelegatingAIAgent?harnessAgent){ChatClientAgent?chatClientAgent=null;object?agent=harnessAgent;while(chatClientAgentisnull){agent=GetNonPublicPropertyFieldValue(agent!,"InnerAgent");chatClientAgent=agentasChatClientAgent;}varagentOptions=(ChatClientAgentOptions)GetNonPublicPropertyFieldValue(chatClientAgent!,"_agentOptions")!;Console.WriteLine($""" ChatClientAgentOptions:Id:{agentOptions.Id??"null"}Name:{agentOptions.Name??"null"}Description:{agentOptions.Description??"null"}UseProvidedChatClientAsIs:{agentOptions.UseProvidedChatClientAsIs}ClearOnChatHistoryProviderConflict:{agentOptions.ClearOnChatHistoryProviderConflict}WarnOnChatHistoryProviderConflict:{agentOptions.WarnOnChatHistoryProviderConflict}ThrowOnChatHistoryProviderConflict:{agentOptions.ThrowOnChatHistoryProviderConflict}RequirePerServiceCallChatHistoryPersistence:{agentOptions.RequirePerServiceCallChatHistoryPersistence}EnableMessageInjection:{agentOptions.EnableMessageInjection}ChatHistoryProvider:{agentOptions.ChatHistoryProvider?.GetType().Name}AIContextProviders:""");foreach(varaiContextProviderinchatClientAgent.AIContextProviders!){Console.WriteLine($"{aiContextProvider.GetType().Name}");}varchatOptions=agentOptions.ChatOptions!;Console.WriteLine($""" ChatOptions:Temperature:{chatOptions.Temperature?.ToString()??"null"}MaxOutputTokens:{chatOptions.MaxOutputTokens?.ToString()??"null"}TopP:{chatOptions.TopP?.ToString()??"null"}TopK:{chatOptions.TopK?.ToString()??"null"}FrequencyPenalty:{chatOptions.FrequencyPenalty?.ToString()??"null"}PresencePenalty:{chatOptions.PresencePenalty?.ToString()??"null"}Seed:{chatOptions.Seed?.ToString()??"null"}Reasoning:Effort:{chatOptions.Reasoning?.Effort?.ToString()??"null"}Output:{chatOptions.Reasoning?.Output?.ToString()??"null"}ResponseFormat:{chatOptions.ResponseFormat?.ToString()??"null"}ModelId:{chatOptions.ModelId?.ToString()??"null"}StopSequences:{string.Join(',',chatOptions.StopSequences??[])}AllowMultipleToolCalls:{chatOptions.AllowMultipleToolCalls?.ToString()??"null"}ToolMode:{chatOptions.ToolMode?.ToString()??"null"}Tools:""");foreach(vartoolinchatOptions.Tools??[]){Console.WriteLine($"{tool.GetType().Name}");}return(chatClientAgent,agentOptions);}staticvoidPrintChatClientMiddlewares(IChatClient?chatClient,booldisplayTitle=false){if(displayTitle)Console.WriteLine("\nChatClientMiddlewares:");if(chatClientisDelegatingChatClientmiddleware){Console.WriteLine($"{chatClient.GetType().Name}");PrintChatClientMiddlewares(GetNonPublicPropertyFieldValue(chatClient,"InnerClient")asIChatClient);}}staticobject?GetNonPublicPropertyFieldValue(objectobj,stringpropertyName){vartype=obj.GetType();varproperty=type.GetProperty(propertyName,System.Reflection.BindingFlags.NonPublic|System.Reflection.BindingFlags.Instance);if(propertyisnotnull){returnproperty.GetValue(obj);}varfield=type.GetField(propertyName,System.Reflection.BindingFlags.NonPublic|System.Reflection.BindingFlags.Instance);returnfield?.GetValue(obj);}在如下的演示程序中,我们将创建的OpenAIClient对象转换成IChatClient对象后,直接调用AsHarnessAgent方法创建了一个默认配置选项的HarnessAgent,并将其传入PrettyPrintAgent方法中进行剖析。
usingdotenv.net;usingMicrosoft.Agents.AI;usingMicrosoft.Extensions.AI;usingOpenAI;usingSystem.ClientModel;DotEnv.Load();varmodel=Environment.GetEnvironmentVariable("MODEL")!;varapiKey=Environment.GetEnvironmentVariable("API_KEY")!;varendpoint=Environment.GetEnvironmentVariable("OPENAI_URL")!;varharnessAgent=newOpenAIClient(newApiKeyCredential(apiKey),newOpenAIClientOptions{Endpoint=newUri(endpoint)}).GetChatClient(model:model).AsIChatClient().AsHarnessAgent(maxContextWindowTokens:128000,maxOutputTokens:16000);PrettyPrintAgent(harnessAgent);输出:
AgentMiddlewares: HarnessAgent ToolApprovalAgent OpenTelemetryAgent ChatClientAgentOptions: Id: null Name: null Description: null UseProvidedChatClientAsIs: True ClearOnChatHistoryProviderConflict: True WarnOnChatHistoryProviderConflict: False ThrowOnChatHistoryProviderConflict: False RequirePerServiceCallChatHistoryPersistence: True EnableMessageInjection: False ChatHistoryProvider: InMemoryChatHistoryProvider AIContextProviders: TodoProvider AgentModeProvider FileMemoryProvider FileAccessProvider AgentSkillsProvider ChatOptions: Temperature: null MaxOutputTokens: 16000 TopP: null TopK: null FrequencyPenalty: null PresencePenalty: null Seed: null Reasoning: Effort: null Output: null ResponseFormat:null ModelId: null StopSequences: AllowMultipleToolCalls: null ToolMode: null Tools: HostedWebSearchTool ChatClientMiddlewares: FunctionInvokingChatClient MessageInjectingChatClient PerServiceCallChatHistoryPersistingChatClient AIContextProviderChatClient Instructions: You are a helpful AI assistant that uses tools to complete tasks. ## General guidelines - Think through the task before acting. Break complex work into clear steps. - Use the tools available to you to gather information, perform actions, and verify results. - Explain your reasoning and thought process as you work through tasks. - Explain what you learned and what you are going to do next between tool calls, so the user can follow along with your thought process. - Avoid making more than 4 tool calls in a row without explaining what you are doing. - If a tool call fails or returns unexpected results, adapt your approach rather than repeating the same call. - When you have completed the task, present a clear and concise summary of what you did and what you found.2. 剖析默认的HarnessAgent
我们通过上面演示程序的输出可用看出它注册的Agent中间件和ChatClient中间件,绑定了怎样的ChatClientAgentOptions,以及默认指定的系统指令。具体来说,它默认注册了如下的Agent中间件:
- ToolApprovalAgent:实现了dont ask again模式的工具调用审批,详细内容可参考我的文章TodoProvider:用TodoList驱动Agent的任务执行
- OpenTelemetryAgent:实现了针对Agent调用的调用链跟踪和针对LLM调用和Token使用的指标收集,详细内容可参考我的文章OpenTelemetryAgent:基于Agent的调用链跟踪和性能监控
默认注册了InMemoryChatHistoryProvider,实现了基于内存存储的对话历史持久化。由于默认注册了HostedWebSearchTool,所以Agent可以使用网络搜索。注册的AIContextProvider包括:
- TodoProvider:提供了基于TodoList的推理和任务解决方法,详细内容可参考我的文章TodoProvider:用TodoList驱动Agent的任务执行
- AgentModeProvider:实现在根据当前推理任务自由切换适合的行为模式,详细内容可参考我的文章AgentModeProvider:自由切换行为模式
- FileMemoryProvider:在Session范围内提供了基于文件存储的短期记忆,详细内容可参考我的文章FileMemoryProvider:为Agent提供可解释、可回溯的记忆能力
- FileAccessProvider:为Agent提供了一个文件系统,详细内容可参考我的文章FileAccessProvider:为Agent提供文件读写能力
- AgentSkillsProvider:将Agent Skill引入MAF,详细内容可参考我的文章AgentSkillsProvider:将Skills引入MAF
除了我们用于连接指定LLM的IChatClient对象,ChatClient管道还包含如下的中间件:
- FunctionInvokingChatClient:实现最为核心的ReAct循环和基于人机交互的工具调用审批功能,详细内容可参考我的文章FunctionInvokingChatClient:ReAct循环和工具审批实现者
- MessageInjectingChatClient:赋予了工具函数动态注入消息的能力,详细内容可参考我的文章MessageInjectingChatClient:赋予工具消息注入的能力
- PerServiceCallChatHistoryPersistingChatClient:实现基于ReAct循环的一步一存档,详细内容可参考我的文章PerServiceCallChatHistoryPersistingChatClient:一步一存档
- AIContextProviderChatClient:利用注册的AIContextProvider来对请求消息进行再加工,详细内容可参考我的文章动态修改对话配置的两种解决方案
3. 系统指令解读
我们可以进一步解读一下HarnessAgent默认提供的系统指令,完整的指令文本如下:
You are a helpful AI assistant that uses tools to complete tasks. ## General guidelines - Think through the task before acting. Break complex work into clear steps. - Use the tools available to you to gather information, perform actions, and verify results. - Explain your reasoning and thought process as you work through tasks. - Explain what you learned and what you are going to do next between tool calls, so the user can follow along with your thought process. - Avoid making more than 4 tool calls in a row without explaining what you are doing. - If a tool call fails or returns unexpected results, adapt your approach rather than repeating the same call. - When you have completed the task, present a clear and concise summary of what you did and what you found.这段系统指令的核心目的,是为了抑制LLM的鲁莽与幻觉,将其塑造成一个高可靠性、有条理、可审计的交付型专家:
3.1 任务前置思考与拆解设
强制开启思维链(CoT:Chain of Thought)机制。LLM在直接生成代码或执行命令时容易管中窥豹。通过这条指令,要求 Agent 在调用任何工具前,必须先在内部生成一个全局的任务规划。将长周期、复杂的模糊任务拆解为确定性的SOP(标准作业程序),能呈数倍地提升最终任务的成功率。
Think through the task before acting. Break complex work into clear steps.3.2 闭环执行与结果验证
构建信息获取=>执行动作=>结果验证的闭环思维。传统LLM往往自信且盲目,生成结果后便认为任务已结束。此指令的核心在于verify results(验证结果)。它要求Agent在执行完动作(如:写入文件、调用 API、修改配置)后,必须主动调用检查工具(如:读取文件、运行测试、查看状态)来确认动作是否真的生效,属于典型的**防呆(Poka-yoke)**设计。
Use the tools available to you to gather information, perform actions, and verify results.3.3 白盒化推理过程设计意图
提高Agent的可解释性与可观测性。将LLM的隐式推理转变为显式文本。一方面,这方便人类用户在中途或事后进行审计,了解Agent为什么这么做;另一方面,LLM把思考过程写出来本身就能作为其下一步行动的上下文,降低后续决策的出错率。
Explain your reasoning and thought process as you work through tasks.3.4 动态上下文桥接设计意图
保持用户与Agent的信息同步,建立信任感。在连续的ReAct迭代中,LLM容易陷入无声的底层逻辑中。这条指令强制Agent在两次工具调用中间充当解说员——既是对上一步工具返回结果的总结提炼,又是对下一步行动的预告。这种走一步、看一步、说一步的节奏,是多步复杂Agent的标准控制范式。
Explain what you learned and what you are going to do next between tool calls, so the user can follow along with your thought process.3.5 熔断机制
硬性限制,防止Agent陷入幻觉死循环或工具滥用。在自动化运维、代码生成中,Agent极易因为某个长尾Bug导致疯狂调用工具(例如死循环地ls或尝试编译)。限制连续调用不得超过4次,强制其必须停下来向人类汇报解释,起到了安全熔断器的作用,不仅保护了用户的系统,也节省了Token资费。
Avoid making more than 4 tool calls in a row without explaining what you are doing.3.6 容错与动态自适应
打破LLM的惯性思维。原生LLM在遇到报错时,本能反应常常是复读机式地用完全相同的参数重新尝试,这会导致无意义的重试。指令强制要求其在失败时必须调整策略,例如换一个工具、修改调用参数、或者退回上一步重新规划。
If a tool call fails or returns unexpected results, adapt your approach rather than repeating the same call.3.7 结构化交付
提供结构化的最终交付物。复杂任务执行完后,底层的日志和工具返回数据可能成千上万行。人类用户不需要看冗长的过程,只需要看结论。这条指令要求Agent在收尾时进行信息提炼,提供做了什么(过程摘要)和发现了什么/产出了什么(结果交付)的高干预度报告。
When you have completed the task, present a clear and concise summary of what you did and what you found.MAF的这套Harness指令,是典型的高约束、高透明、注重安全与闭环的工业级Agent提示词模板。它将LLM从一个纯聊天的AI,规范成了一个步步有回应、件件有着落、出错懂绕行、事毕有归档的靠谱虚拟员工。
4. HarnessAgent的进一步定制
到目前为止,我们已经指导采用默认配置选项创建的HarnessAgent,其背后的Agent管道大体长什么样子。接下来我们将看看如何通过使用HarnessAgentOptions对这个管道做出进一步的定制。从上面的输出苦于看出,对于默认的HarnessAgent,其Id、Name和Description都是null,这些都可以通过HarnessAgentOptions如下所示的三个属性来指定。
publicsealedclassHarnessAgentOptions{publicstring?Id{get;set;}publicstring?Name{get;set;}publicstring?Description{get;set;}}如何我们觉得默认的系统指令不够好,可以通过HarnessInstructions属性指定新的指令。由于默认使用InMemoryChatHistoryProvider来持久化对话历史,再生成环境一般会利用ChatHistoryProvider属性设置一个支持分布式部署环境的ChatHistoryProvider。通过AIContextProviders属性提供的AIContextProvider会添加(不是覆盖)到Agent的AIContextProvider列表中。我们也可以对ChatOptions做进一步设置,比如注册工具以及其他与LLM交互相关的参数设置。
publicsealedclassHarnessAgentOptions{publicChatOptions?ChatOptions{get;set;}publicstring?HarnessInstructions{get;set;}publicChatHistoryProvider?ChatHistoryProvider{get;set;}publicIEnumerable<AIContextProvider>?AIContextProviders{get;set;}HarnessAgentOptions提供了如下这些布尔类型的属性,它们会作为开关开启或者关闭某些特性。
publicsealedclassHarnessAgentOptions{publicboolDisableToolApproval{get;set;}publicboolDisableFileMemory{get;set;}publicboolDisableFileAccess{get;set;}publicboolDisableWebSearch{get;set;}publicboolDisableTodoProvider{get;set;}publicboolDisableAgentModeProvider{get;set;}publicboolDisableAgentSkillsProvider{get;set;}publicboolDisableOpenTelemetry{get;set;}}这些开关说明如下:
- DisableToolApproval:关闭基于don’t ask again模式的工具调用审批功能,也就是不再注册
ToolApprovalAgent中间件; - DisableFileMemory:关闭基于文件存储的短期记忆功能,也就是不再注册
FileMemoryProvider这个AIContextProvider。 - DisableFileAccess:关闭文件系统访问功能,也就是不再注册
FileAccessProvider这个AIContextProvider。 - DisableWebSearch:关闭网络搜索功能,也就是不再注册
HostedWebSearchTool这个服务端执行的工具。 - DisableTodoProvider:关闭基于TodoList的推理和任务解决方法,也就是不再注册
TodoProvider这个AIContextProvider。 - DisableAgentModeProvider:关闭Agent行为模式提供功能,也就是不再注册
AgentModeProvider这个AIContextProvider。 - DisableAgentSkillsProvider:关闭Agent Skills提供功能,也就是不再注册
AgentSkillsProvider这个AIContextProvider。 - DisableOpenTelemetry:关闭OpenTelemetry功能,也就是不再注册
OpenTelemetryAgent中间件。 - OpenTelemetrySourceName:指定OpenTelemetry的SourceName,只有在
DisableOpenTelemetry为false时才会生效。
HarnessAgentOptions还提供了如下这些属性:
publicsealedclassHarnessAgentOptions{publicint?MaximumIterationsPerRequest{get;set;}publicAgentFileStore?FileMemoryStore{get;set;}publicAgentFileStore?FileAccessStore{get;set;}publicAgentModeProviderOptions?AgentModeProviderOptions{get;set;}publicAgentSkillsSource?AgentSkillsSource{get;set;}publicstring?OpenTelemetrySourceName{get;set;}publicIEnumerable<AIAgent>?BackgroundAgents{get;set;}publicBackgroundAgentsProviderOptions?BackgroundAgentsProviderOptions{get;set;}publicShellEnvironmentProviderOptions?ShellEnvironmentProviderOptions{get;set;}publicShellExecutor?ShellExecutor{get;set;}}这些属性说明如下:
MaximumIterationsPerRequest:限制每次请求的最大迭代次数,迭代指的是ReAct循环中的工具调用次数。当达到这个限制时,Agent会停止继续调用工具并返回当前的结果。FileMemoryStore:指定用于FileMemoryProvider的AgentFileStore实例;FileAccessStore:指定用于FileAccessProvider的AgentFileStore实例;AgentModeProviderOptions:指定AgentModeProvider的配置选项;AgentSkillsSource:指定AgentSkillsProvider的技能来源;OpenTelemetrySourceName:指定OpenTelemetry的SourceName;BackgroundAgents:指定一组会在HarnessAgent内部作为后台Agent运行的AIAgent实例,如果设置,将会据此注册一个BackgroundAgentsProvider实现异步执行这组SubAgent;BackgroundAgentsProviderOptions:为BackgroundAgentsProvider指定的配置选项;ShellEnvironmentProviderOptions:如果设置,将会据此注册一个ShellEnvironmentProvider向LLM提供Shell环境信息;ShellExecutor:为ShellEnvironmentProvider指定的ShellExecutor实例用来探测Shell执行环境信息;