.NET+AI | Agent | MAF 的「手动驾驶」与「自动驾驶」:深入理解 UseProvidedChatClientAsIs
目录一、先从这行代码开始二、「自动驾驶」模式WithDefaultAgentMiddleware 做了什么第一件事注入 FunctionInvokingChatClient第二件事绑定工具Tools三、「自动驾驶」的心脏FunctionInvokingChatClient 的 Loop3.1 启动前处理审批响应预处理3.2 主循环一轮一轮地推进① 快速路径Fast Path② 历史消息管理History Fixup③ 连续错误计数consecutiveErrorCount④ 迭代次数上限MaximumIterationsPerRequest⑤ 审批流转Approval Flow四、「手动驾驶」模式你全权掌控这意味着什么典型的使用场景五、两种模式的对比六、一个真实的场景七、为什么这个设计是合理的八、写在最后想象你刚拿到一辆新车。如果你是老司机技术过硬你会说把方向盘给我我自己来。 你清楚每一个弯道、每一脚油门该怎么踩。但如果你想享受旅程、专注于目的地你会说开启自动驾驶让系统来处理路途中的一切。 系统会自动识别障碍、自动调整车速、自动绕行——你只需要告诉它去哪里。Microsoft Agent FrameworkMAF里有一个不起眼却极其关键的配置项 ChatClientAgentOptions.UseProvidedChatClientAsIs 。 这个布尔值就是那把切换「手动驾驶」与「自动驾驶」的开关。理解它是真正用好 MAF 的第一步。一、先从这行代码开始逻辑清晰只有两条路true手动驾驶直接使用你传进来的IChatClient框架不加任何东西。false或未设置自动驾驶调用WithDefaultAgentMiddleware在你的 client 外面包裹一层或多层中间件组成完整的 Agentic 执行流水线。看起来很简单但这背后的差异是整个 Agent 执行模型的分水岭。二、「自动驾驶」模式WithDefaultAgentMiddleware 做了什么当UseProvidedChatClientAsIs为false时框架会调用WithDefaultAgentMiddleware方法对你的 ChatClient 进行改造这里发生了两件重要的事情第一件事注入FunctionInvokingChatClientFunctionInvokingChatClient是Microsoft.Extensions.AI提供的一个DelegatingChatClient它的职责是自动处理 Function Calling 的完整循环Agentic Loop。构建出来的 pipeline 大致是这样的由于ChatClientBuilder.Build是按注册顺序逆序应用工厂方法的所以第一个Use注册的是最外层。结果就是FunctionInvokingChatClient站在最外面像一个调度员控制整个 loop。第二件事绑定工具Tools如果你在构造 Agent 时通过ChatOptions.Tools提供了一批工具AIFunction这些工具会被绑定为FunctionInvokingChatClient.AdditionalTools在整个 Agent 生命周期内持续可用。三、「自动驾驶」的心脏FunctionInvokingChatClient 的 Loop让我们把镜头推进到最核心的位置——GetResponseAsync里的 Agentic Loop。这段代码文章开头给出的那段展示了一个精心设计的多轮对话执行流程。我们来逐步拆解它3.1 启动前处理审批响应预处理在进入主循环之前框架会先检查传入的消息中是否包含「审批响应」ToolApprovalResponseContent。这是 MAF 支持Human-in-the-Loop的关键机制某些工具被标记为需要人工审批ApprovalRequiredAIFunction在上一轮对话中框架不会直接执行它们而是把它们转换为审批请求返回给调用方。调用方可能是你的 UI 或后端让用户决定批准还是拒绝当用户做出决定把审批结果打包进下一轮消息传回来FunctionInvokingChatClient就在这里处理这些结果——执行被批准的函数或者直接终止流程。3.2 主循环一轮一轮地推进这个循环有几个精妙的设计值得特别说明① 快速路径Fast Path如果第一轮 LLM 就直接给了答案不需要调用任何工具框架会立刻返回不做任何多余的处理。这对于大多数「普通问答」场景非常重要——你不会因为套了一层中间件而付出性能代价。② 历史消息管理History Fixup每一轮迭代框架都会维护一份augmentedHistory把原始消息、LLM 响应、函数调用结果按正确顺序拼接起来传给下一轮。这保证了 LLM 在每一轮都能看到完整的上下文。③ 连续错误计数consecutiveErrorCount框架会追踪连续的函数调用错误次数。如果工具调用持续失败框架可以选择终止循环而不是无限重试避免浪费 token 和产生幻觉。④ 迭代次数上限MaximumIterationsPerRequest防止 Agent 陷入无限循环。当达到上限时框架会移除 ChatOptions 中的工具声明让 LLM 在下一轮只能给出文本回答优雅地结束循环。⑤ 审批流转Approval Flow当某个工具被标记为ApprovalRequiredAIFunction框架不会执行它而是把它包装成ToolApprovalRequestContent返回给调用方。同一轮响应中的其他工具调用也会一并暂停——这是为了安全性一旦有需要审批的操作所有操作都要等人来决定。四、「手动驾驶」模式你全权掌控当UseProvidedChatClientAsIs true时框架什么都不做——你传什么 client 进来就用什么。没有FunctionInvokingChatClient没有自动 loop没有工具绑定没有审批流转。这意味着什么你需要自己处理所有事情如果 LLM 返回了FunctionCallContent你要自己识别、自己调用、自己把结果拼回消息历史、自己决定是否继续下一轮。如果你想要 Human-in-the-Loop你要自己实现审批逻辑。如果你的 client 已经内置了 function calling比如某些 Azure AI Foundry 服务在服务端处理工具调用你就不需要客户端再做一遍——这时候手动驾驶反而是正确选择。典型的使用场景你的 LLM 服务已经在服务端处理 Function Calling比如 Azure AI Foundry 的某些托管 Agent 服务服务端已经执行了工具循环返回的就是最终答案。客户端再套一个FunctionInvokingChatClient反而会造成混乱。你想实现自定义的调度逻辑也许你的 Agentic Loop 需要特殊的错误处理、特定的重试策略或者与外部审批系统深度集成默认的 loop 不能满足你的需求。性能极致敏感的场景你知道自己的 Agent 不会用到工具那么去掉中间层可以减少一点点的调用栈开销虽然通常微不足道。测试与调试你想观察 raw IChatClient 的行为不想让中间件干扰你的观察。五、两种模式的对比维度手动驾驶true自动驾驶false/默认Function Calling Loop❌ 不处理需自行实现✅ 自动循环处理工具绑定❌ 需自行管理✅ 自动绑定 AdditionalToolsHuman-in-the-Loop❌ 需自行实现✅ 内置审批流转机制历史消息管理❌ 需自行维护✅ 自动 augmentedHistoryUsage 统计聚合❌ 仅单次✅ 多轮累加迭代次数限制❌ 无✅ MaximumIterationsPerRequest灵活性⭐⭐⭐⭐⭐ 完全掌控⭐⭐⭐ 框架约束适合场景高级定制、服务端已处理工具调用大多数 Agent 场景六、一个真实的场景假设你在构建一个企业级报销审批 Agent它需要理解员工的报销申请调用财务系统 API 查询余额如果金额超过 5000 元发送审批请求给经理等待经理决定后执行转账或拒绝用自动驾驶模式UseProvidedChatClientAsIs false这个流程几乎开箱即用步骤 2 是一个普通的AIFunctionFunctionInvokingChatClient会自动调用它步骤 3 的发送审批请求工具被标记为ApprovalRequiredAIFunction框架会自动暂停执行并把审批请求返回给你的 UI步骤 4 中经理的决定以ToolApprovalResponseContent的形式传回框架在下一次RunAsync调用时自动处理整个 Agentic Loop 的管理、消息历史的拼接、多轮 Usage 的累计都由框架负责。你只需要关注业务逻辑。如果用手动驾驶模式UseProvidedChatClientAsIs true你要手写 for 循环要手动识别FunctionCallContent要自己设计审批暂停和恢复机制要自己管理augmentedHistory……除非你有非常特殊的理由否则手动驾驶在这里只会让你重新发明一辆方形的轮子。七、为什么这个设计是合理的MAF 这个设计体现了一种叫做「约定优于配置Convention over Configuration」的哲学同时兼顾了逃生舱Escape Hatch。大多数使用场景下开发者希望框架帮他们处理复杂的状态机也就是那个 Agentic Loop。框架的默认行为自动驾驶就是为此优化的。但框架的设计者也清楚没有任何一个框架能预见所有场景。所以他们提供了UseProvidedChatClientAsIs true这个逃生舱让高级用户可以完全绕开框架的默认行为。这种「默认安全、高级可控」的设计思路在FunctionInvokingChatClient的其他地方也有体现AllowConcurrentInvocation默认false保护线程安全需要并发时可以主动开启MaximumIterationsPerRequest防止无限循环的保护机制但可以配置ApprovalRequiredAIFunction默认函数直接执行需要人工介入时才标记八、写在最后一行代码UseProvidedChatClientAsIs切换的是两种完全不同的 Agent 执行哲学自动驾驶把复杂的 Agentic Loop、工具调用管理、审批流转、历史消息维护统统交给框架。你专注于定义工具、编写业务逻辑、设计 Agent 指令。手动驾驶框架退到幕后你拿到一个干净的IChatClient一切尽在掌握代价是你需要自己实现那套复杂的状态机。对于绝大多数构建 AI Agent 的场景自动驾驶是更好的起点。它背后的FunctionInvokingChatClient是一个经过精心设计的生产级组件处理了大量你在手写时很可能遗漏的边界情况错误计数、迭代上限、快速路径优化、消息历史修复、Usage 聚合……当你真正理解了这套机制之后你才能做出明智的选择什么时候应该信任自动驾驶什么时候应该接管方向盘。这才是真正驾驭 MAF 的姿态。引入地址