(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.OfType<ToolApprovalRequestContent>(); 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(it=>it.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返回的所有工具调用视为一个类似于事务的整体,如果所有工具都不需要审批,那么它会采用直接调用这些工具。如果其中有任何一个工具需要审批,它会任务所有工具调用都需要审批。这也很好理解,因为所有的工具都是为了同一个任务服务的,如果其中一个工具需要审批,那么整个任务就需要审批。以本例来说,虽然