利用FunctionInvokingChatClient实现ReAct循环 📅 2026/6/28 2:13:28 Reasoning and Acting推理与行动是一种结合了推理与工具使用的LLM工作流模式。 它通过交替进行推理Reasoning和行动Acting让AI能够像人类一样一边分析问题一边寻找外部信息从而解决复杂的、需要实时数据的任务。ReAct 的核心循环机制ReAct循环通常由三个核心步骤组成不断重复直到得出最终答案Thought思考模型分析当前状态决定下一步该做什么Action行动模型选择并调用外部工具如搜索引擎、数据库、计算器Observation观察模型读取工具返回的结果并将其作为新的上下文比如我最常用的“根据某地天气提供着装建议”的场景ReAct循环的执行流程如下。这是一个简单的只涉及单次迭代的ReAct循环实际的ReAct循环可能会涉及多次迭代模型在每次迭代中都会根据新的上下文来分析下一步该做什么。Thought模型分析当前状态发现缺少天气信息决定需要调用工具来获取天气信息Action模型调用工具如天气API来获取天气信息Observation模型读取工具返回的天气信息并将其作为新的上下文来分析天气情况最终得出着装建议下面这个演示程序直接利用FunctionInvokingChatClient将上述的ReAct循环落地。如代码所示我们创建了一个基于OpenAIClient的IChatClient对象并在调用AsBuilder扩展方法将ChatClientBuilder构建出来后通过调用UseFunctionInvocation方法来注册FunctionInvokingChatClient中间件。由于我们在调用GetResponseAsync方法的时候传入了一个工具函数所以在执行过程中会触发ReAct循环模型会先分析当前状态发现缺少天气信息然后调用工具函数来获取天气信息最后根据获取到的天气信息来分析天气情况并得出着装建议。using Azure; using dotenv.net; using Microsoft.Extensions.AI; using OpenAI; using System.ComponentModel; DotEnv.Load(); var model Environment.GetEnvironmentVariable(MODEL)!; var apiKey Environment.GetEnvironmentVariable(API_KEY)!; var endpoint Environment.GetEnvironmentVariable(OPENAI_URL)!; var tool AIFunctionFactory.Create(method: GetWeather); var client new OpenAIClient( credential: new AzureKeyCredential(apiKey), options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }) .GetChatClient(model:model) .AsIChatClient() .AsBuilder() .UseFunctionInvocation() .Build(); var response await client.GetResponseAsync( messages: [new ChatMessage(ChatRole.User,content: 根据苏州当前天气情况给出一些穿衣建议)], options: new ChatOptions { Tools [tool] }); Console.WriteLine(response.Text); [Description(获取指定城市的天气信息)] static string GetWeather(string city) ${city}今天的天气是晴天气温是25°C。;输出:苏州今天**晴天气温25°C**体感整体比较舒适稍微偏暖一些。给你一些穿衣建议 ### 上衣 - 短袖T恤、薄衬衫都很合适 - 如果长时间在空调房可以带一件**薄外套或防晒衫** ### 下装 - 牛仔裤、休闲裤、薄款长裤 - 女生也可以选择半身裙、连衣裙 ### 鞋子 - 运动鞋、休闲鞋、帆布鞋 - 不建议穿太厚重或闷脚的鞋子 ### ☀️ 其他建议 - 晴天紫外线较强外出可以做好**防晒帽子、墨镜、防晒霜** - 白天气温较暖但早晚可能稍微凉一点怕冷的话可带薄外套 如果你是要通勤、旅游或者运动我也可以帮你细化搭配 2. 利用FunctionInvokingChatClient实现人机交互的审批流程在某些场景下工具函数可能会涉及一些敏感操作比如访问用户的个人信息、执行一些可能产生副作用的操作等。对于这些敏感的工具函数我们可能需要引入一个人机交互的审批流程在模型调用工具函数之前先征求用户的同意。在如下的演示程序中我们创建了一个工具函数Transfer它模拟了一个银行转账的操作。由于这个操作比较敏感所以我们在调用UseFunctionInvocation方法注册FunctionInvokingChatClient中间件的时候并没有直接将这个工具函数传入而是通过一个包装类ApprovalRequiredAIFunction来包装这个工具函数。ApprovalRequiredAIFunction会在模型调用工具函数之前先生成一个审批请求并将其作为响应的一部分返回给用户。用户可以根据这个审批请求来决定是否批准执行这个工具函数。如果用户批准了那么模型就会继续执行这个工具函数如果用户拒绝了那么模型就会放弃执行这个工具函数。using Azure; using dotenv.net; using Microsoft.Extensions.AI; using OpenAI; using System.ComponentModel; DotEnv.Load(); var model Environment.GetEnvironmentVariable(MODEL)!; var apiKey Environment.GetEnvironmentVariable(API_KEY)!; var endpoint Environment.GetEnvironmentVariable(OPENAI_URL)!; AIFunction transfer AIFunctionFactory.Create(method: Transfer, Transfer); AIFunction logTool AIFunctionFactory.Create(method: Log, Log); transfer new ApprovalRequiredAIFunction(transfer); AITool[] tools [transfer, logTool]; var client new OpenAIClient( credential: new AzureKeyCredential(apiKey), options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }) .GetChatClient(model:model) .AsIChatClient() .AsBuilder() .UseFunctionInvocation() .Build(); var prompt new ChatMessage( role: ChatRole.User, content: 从账号4242 4242 4242 4242 转账100块到账号 5555 5555 5555 4444); var options new ChatOptions { Tools tools }; var response await client.GetResponseAsync( messages: [prompt], options: options); while (response is not null) { var lastMessage response.Messages.Last(); var approvalRequestContents lastMessage.Contents.OfTypeToolApprovalRequestContent(); if (!approvalRequestContents.Any()) { Console.WriteLine(lastMessage.Text); break; } Console.WriteLine(如下待执行工具需要你的审批); foreach (var content in approvalRequestContents) { var toolCall (FunctionCallContent)content.ToolCall; Console.WriteLine($工具 {toolCall.Name} 正在请求执行参数如下:); foreach (var (k, v) in toolCall.Arguments!) { Console.WriteLine($ - {k}: {v}); } Console.WriteLine(); } Console.Write(是否批准执行 [Y/N]: ); var input Console.ReadLine(); bool isApproved input?.Trim().ToUpper() Y; var approvalResponses approvalRequestContents.Select(itit.CreateResponse(isApproved)).ToArray(); var messages response.Messages.ToList(); messages.Add(new ChatMessage(ChatRole.User, approvalResponses)); response await client.GetResponseAsync(messages, options); } [Description(执行银行转账操作)] static string Transfer( [Description(转出银行账号)] string from, [Description(转入银行账号)] string to, [Description(转账金额)] decimal amount) $从账号 {from} 转账 {amount} 元到账号 {to} 已完成。; [Description(跟踪记录执行银行转账操作)] static void Log(string message) Console.WriteLine(message);如下的两端输出分别对应批准执行和拒绝执行的情况如下待执行工具需要你的审批 工具 Transfer 正在请求执行参数如下: - from: 4242 4242 4242 4242 - to: 5555 5555 5555 4444 - amount: 100 工具 Log 正在请求执行参数如下: - message: 从账号 4242 4242 4242 4242 转账 100 元到账号 5555 5555 5555 4444 是否批准执行 [Y/N]: Y 从账号 4242 4242 4242 4242 转账 100 元到账号 5555 5555 5555 4444 ✅ 转账已完成 - **转出账户**4242 4242 4242 4242 - **转入账户**5555 5555 5555 4444 - **转账金额**100 元 交易记录已成功保存。如需继续操作请告诉我。如下待执行工具需要你的审批 工具 Transfer 正在请求执行参数如下: - from: 4242 4242 4242 4242 - to: 5555 5555 5555 4444 - amount: 100 工具 Log 正在请求执行参数如下: - message: 从账号 4242 4242 4242 4242 转账 100 元到账号 5555 5555 5555 4444 是否批准执行 [Y/N]: N ❌ 转账失败。 原因无法确定目标账户的有效性因此银行转账操作被拒绝执行。 请核对以下信息后重新提交 - 转出账户是否正确 - 转入账户是否正确 - 账户是否为有效银行账号格式 - 是否需要提供更多身份验证信息 如需重新发起转账请提供正确的账户信息。从这里例子可以看出FunctionInvokingChatClient会将LLM返回的所有工具调用视为一个类似于事务的整体如果所有工具都不需要审批那么它会采用直接调用这些工具。如果其中有任何一个工具需要审批它会任务所有工具调用都需要审批。这也很好理解因为所有的工具都是为了同一个任务服务的如果其中一个工具需要审批那么整个任务就需要审批。以本例来说虽然