[MAF预定义ChatClient中间件-02]FunctionInvokingChatClient:ReAct循环和工具审批实现者

在众多预定义的ChatClient中间件中,FunctionInvokingChatClient无疑是最重要的一个,以至于没有它整个Agent就无法工作了。原因在于驱动Agent执行的核心机制的ReAct循环就是通过这个中间件实现的,没有它工具函数就没法被调用。对于相对敏感的工具函数,我们还需要通过人机交互引入审批流程,这也是通过FunctionInvokingChatClient来实现的。

1. 利用FunctionInvokingChatClient实现ReAct循环

ReAct是一种结合了推理工具使用的LLM工作流模式。 它通过交替进行推理(Reasoning)行动(Acting),让Agent能够像人类一样,一边分析问题一边寻找外部信息,从而解决复杂的任务。一次ReAct循环通常由如下三个核心步骤组成,针对Agent的调用不断重复此循环,直到得出最终答案:

  • Thought(思考):模型分析当前状态,决定下一步该做什么;
  • Action(行动):模型选择并调用外部工具(如搜索引擎、数据库、计算器);
  • Observation(观察):模型读取工具返回的结果,并将其作为新的上下文。

比如我最常用的根据某地天气提供着装建议的场景,ReAct循环的执行流程如下:

  • Thought(思考):模型分析当前状态,发现缺少天气信息,决定需要调用工具来获取天气信息;
  • Action(行动):模型调用工具(如天气API)来获取天气信息;
  • Observation(观察):模型读取工具返回的天气信息,并将其作为新的上下文来分析天气情况,最终得出着装建议。

下面这个演示程序直接利用FunctionInvokingChatClient将上述的ReAct循环落地。如代码所示,我们创建了一个基于OpenAIClientIChatClient对象,并在调用AsBuilder扩展方法将ChatClientBuilder构建出来后,通过调用UseFunctionInvocation方法注册FunctionInvokingChatClient中间件。由于我们在调用GetResponseAsync方法的时候传入了一个工具函数,所以在执行过程中会触发ReAct循环。LLM会先分析当前状态,发现缺少天气信息,它会调用注册的工具函数来获取天气信息,最后根据获取到的天气信息来分析天气情况并得出着装建议。

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返回的所有工具调用视为一个类似于事务的整体,如果所有工具都不需要审批,那么它会直接调用这些工具。如果其中有任何一个工具需要审批,它会认为所有工具调用都需要审批。这也很好理解,因为所有的工具都是为了同一个任务服务的,如果其中一个工具需要审批,那么整个任务就需要审批。以本例来说,虽然Log工具本身并不敏感,但由于它和敏感的Transfer工具是同一个任务的一部分,所以它也需要审批。如果Transfer工具被拒绝,而Log工具无条件执行,那么就会出现Log工具记录了一个实际上并没有发生的转账操作的情况,这显然是我们不希望看到的。

3. FunctionInvokingChatClient

FunctionInvokingChatClient实现ReAct循环和人机交互的审批流程的逻辑相对复杂一些。我们先来了解一下定义在这个类型中用于控制ReAct循环行为的几个重要属性。

public class FunctionInvokingChatClient : DelegatingChatClient
{
    public bool IncludeDetailedErrors { get; set; }
    public bool AllowConcurrentInvocation { get; set; }
    public int MaximumIterationsPerRequest{ get; set; } = 40;
    public int MaximumConsecutiveErrorsPerRequest{ get; set; } = 3;
    public IList<AITool>? AdditionalTools { get; set; }
    public bool TerminateOnUnknownCalls { get; set; }
    public Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>>? FunctionInvoker { get; set; }
}

属性说明如下:

  • IncludeDetailedErrors:这个属性控制在ReAct循环过程中是否包含详细的错误信息。如果设置为true,当模型调用工具函数时发生错误,响应中将包含详细的错误信息;如果设置为false,则只会包含一个简单的错误提示;
  • AllowConcurrentInvocation:这个属性控制是否允许并发调用工具函数。如果设置为true,模型在ReAct循环过程中可以同时调用多个工具函数;如果设置为false,则模型在调用一个工具函数时必须等待其完成后才能调用下一个工具函数;
  • MaximumIterationsPerRequest:这个属性控制每个请求中ReAct循环的最大迭代次数。如果模型在一个请求中执行的ReAct循环迭代次数超过这个值,循环将被强制终止,以防止无限循环的情况发生。
  • MaximumConsecutiveErrorsPerRequest:这个属性控制每个请求中允许的连续错误的最大次数。如果模型在一个请求中连续发生的错误次数超过这个值,循环将被强制终止,以防止模型在遇到错误时不断重试的情况发生;
  • AdditionalTools:这个属性允许我们为模型提供一些额外的工具函数,模型在ReAct循环过程中可以调用这些工具函数来获取更多的信息或执行更多的操作;
  • TerminateOnUnknownCalls:这个属性控制当模型调用了一个未知的工具函数时是否终止ReAct循环。如果设置为true,当模型调用了一个未知的工具函数时,循环将被强制终止;如果设置为false,则模型在调用一个未知的工具函数时会收到一个错误提示,但循环不会被终止;
  • FunctionInvoker:这个属性允许我们自定义工具函数的调用逻辑。如果我们不设置这个属性,FunctionInvokingChatClient将使用默认的工具函数调用逻辑来调用工具函数;如果我们设置了这个属性,FunctionInvokingChatClient将使用我们提供的自定义逻辑来调用工具函数。

3.1 ReAct循环的实现逻辑

ReAct循环在FunctionInvokingChatClientGetResponseAsyncGetStreamingResponseAsync方法中实现相对繁琐,所以我不打算详细解说ReAct循环在此中间件中的完整实现逻辑,而是利用如下这个FunctionInvokingChatClientSimulator类型的GetResponseAsync来模拟ReAct循环的实现。如下面的代码片段所示,在重写的GetResponseAsync方法中,我们首先调用InnerClientGetResponseAsync方法来获取LLM的响应,然后利用一个while循环来模拟ReAct循环。

class FunctionInvokingChatClientSimulator(IChatClient innerClient) : DelegatingChatClient(innerClient)
{
    public override async Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages, 
        ChatOptions? options = null, 
        CancellationToken cancellationToken = default)
    {
        var response = await InnerClient.GetResponseAsync(messages, options, cancellationToken);
        IEnumerable<AIFunction>? tools = options?.Tools?.OfType<AIFunction>();
        if (!(tools?.Any() ?? false))
        {
            return response;
        }

        var messages4ChatClient = messages.ToList();
        messages4ChatClient.AddRange(response.Messages);

        while (true)
        {
            var functionCalls = response.Messages.Last().Contents.OfType<FunctionCallContent>();
            if (!functionCalls.Any())
            { 
                return response;
            }
            var pairs = from functionCall in functionCalls
                           let tool = tools!.FirstOrDefault(t => string.Equals(t.Name, functionCall.Name, StringComparison.Ordinal))
                           where tool is not null
                           select new { Tool = tool, FunctionCall= functionCall };
            
            var results = await Task.WhenAll(pairs.Select(it=> InvokeFunctionAsync(it.Tool, it.FunctionCall, cancellationToken)));
            var toolMessages = results.Select(content => new ChatMessage(ChatRole.Tool, [content]));
            messages4ChatClient.AddRange(toolMessages);
            response = await InnerClient.GetResponseAsync(messages4ChatClient, options, cancellationToken);
        }
    }
    private static async Task<FunctionResultContent> InvokeFunctionAsync(AIFunction tool, FunctionCallContent functionCall, CancellationToken cancellationToken)
    { 
        var arguments = new AIFunctionArguments(functionCall.Arguments);
        var result = await tool.InvokeAsync(arguments, cancellationToken);
        return new FunctionResultContent(functionCall.CallId, result);
    }
}

对于每次迭代:

  • 首先检查LLM的响应中是否包含工具调用的意图。如果没有,就直接返回这个响应,结束ReAct循环;
  • 如果包含工具调用的意图,就提取出工具调用的相关信息,并找到对应的工具对象,然后并发调用这些工具来获取工具调用的结果;
  • 工具函数执行的结果封装成角色为ToolChatMessage对象,并将它们添加到当前的消息列表中,作为下一轮LLM调用的输入。为了保证对话历史的完整性,之前响应的消息也被添加到了这个输入消息列表中。

3.2 人机交互的审批流程的实现逻辑

上面我们说过,FunctionInvokingChatClient会将LLM返回的所有工具调用视为一个整体,如果所有工具都不需要审批,那么它会直接调用这些工具。如果其中有任何一个工具需要审批,它会任务所有工具调用都需要审批。此时它会为每个代表工具调用的FunctionCallContent生成一个对应的ToolApprovalRequestContent,然后根据它们创建一个ChatMessage对象,代替LLM返回的原始响应返回给用户。

用户接收到这个包含审批请求的响应后,需要对每个ToolApprovalRequestContent生成一个对应的ToolApprovalResponseContent,其Approved属性表示用户是批准还是拒绝了这个工具调用。在根据ToolApprovalResponseContent创建ChatMessage对象并将它们添加到消息列表中后,用户需要再次调用GetResponseAsync方法。

请求被FunctionInvokingChatClient拦截后,它会使用上次LLM返回的携带FunctionCallContent消息替换掉消息列表中自己创建的那一条(这条消息是为了向用户展示审批请求而创建的占位消息,LLM不需要它)。然后遍历消息列表中的每个ToolApprovalResponseContent对象:

  • 如果Approved为true,那么FunctionInvokingChatClient就会执行对应的工具调用,将根据工具调用的结果来生成一个FunctionResultContent对象;
  • 如果Approved为false,那么FunctionInvokingChatClient同样会生成一个表示拒绝执行的FunctionResultContent对象。

这些FunctionResultContent对象同样会被封装成角色为ToolChatMessage对象,并添加到消息列表中,作为下一轮LLM调用的输入。

4. UseFunctionInvocation扩展方法

针对FunctionInvokingChatClient中间件的注册可以通过ChatClientBuilderUseFunctionInvocation扩展方法来完成。FunctionInvokingChatClient在内部也会输出相应的日志,我们可以传入用来创建ILogger对象的ILoggerFactory来控制这些日志的输出。如果没有指定,这个ILoggerFactory会从宿主程序的DI容器中提取。和其他用于注册中间件的扩展方法一样,我们也利用提供一个委托对注册的中间件进行相应的设置。

public static class FunctionInvokingChatClientBuilderExtensions
{
    public static ChatClientBuilder UseFunctionInvocation(
        this ChatClientBuilder builder,
        ILoggerFactory? loggerFactory = null,
        Action<FunctionInvokingChatClient>? configure = null);
}
内容概要:本文研究了基于阶跃响应的V-Tiger自动增益调整PID控制器优化方法,并提供了完整的Matlab代码实现。通过深入分析PID控制的核心性能指标与V-Tiger控制器的动态特性,提出了一种融合阶跃响应特征提取与多目标协同优化的自动整定方案,设计了具备自适应迭代校正能力的优化机制,有效提升了控制系统的响应速度、稳定性抗干扰能力。文中系统阐述了整定原理、算法架构设计及性能验证流程,通过仿真实验充分验证了该方法在复杂工业控制场景下实现高精度参数自整定的可行性与优越性,为智能PID控制提供了可复现、可拓展的技术路径。; 适合人群:具备自动控制理论基础Matlab编程能力,从事控制工程、自动化、电气工程等领域研究的研发人员及高校研究生。; 使用场景及目标:①应用于需要高精度PID参数整定的工业控制系统中,如电机驱动、温度控制、电力电子变换器等;②为科研人员提供一种可复现、可扩展的智能PID整定方法,用于提升系统动态性能与鲁棒性;③作为教学案例帮助学生理解PID整定原理与现代优化算法的融合应用。; 阅读建议:建议读者结合文中的Matlab代码逐模块运行与调试,重点关注阶跃响应特征提取与增益优化策略的实现逻辑,同时可尝试将其应用于实际控制系统中进行对比验证,以深化对自动整定机制的理解。
内容概要:本文围绕一种集成DoS攻击、二次控制、下垂控制与事件触发式负荷控制的四机并联孤岛微电网系统展开研究,旨在实现微电网在遭受网络攻击时仍能维持电压与频率稳定,并完成功率的精确共享分配。通过Simulink仿真实现,系统融合了多种先进控制策略,重点构建了一个具有高容错性与强鲁棒性的分布式控制架构。该架构不仅能够有效抵御拒绝服务(DoS)等网络攻击对通信链路造成的干扰,还能借助事件触发机制显著降低通信频率与资源消耗,从而提升系统实时性与运行效率。研究深入探讨了多逆变器间的协同控制逻辑,实现了在孤岛运行模式下系统的动态响应优化与稳态性能提升。; 适合人群:具备扎实的电力电子、自动控制理论与微电网系统基础知识,熟悉Simulink/MATLAB仿真环境,从事微电网、分布式能源系统、智能电网安全防护、网络物理系统(CPS)等领域研究的研究生、科研人员及高级工程技术开发人员。; 使用场景及目标:①探究微电网在面临网络安全威胁(特别是DoS攻击)时的稳定性维持与恢复机制;②实现孤岛模式下多分布式电源(DG)并联系统的电压频率精准调控与有功/无功功率均分;③应用事件触发控制策略以减少不必要的通信负担,提高系统能效与实时响应能力;④为构建高可靠、自适应、低通信开销的下一代智能微电网控制系统提供理论依据与仿真验证范例。; 阅读建议:建议读者结合文中详细的Simulink模型与控制算法设计,逐步复现仿真过程,重点关注DoS攻击模块的建模方式、二次控制与下垂控制的协同机制、事件触发条件的设定及其对系统性能的影响,并可通过修改攻击强度、通信延迟、负载变化等参数,深入分析系统在不同工况下的鲁棒性与动态响应特性。
内容概要:本文系统研究了基于事件触发分布式策略的孤岛微电网二次频率与电压恢复控制方法,提出一种面向通信优化的分布式协同控制框架。通过引入动态事件触发机制,有效降低系统通信频次与网络负载,提升控制效率与资源利用率;结合分布式二次控制策略,实现对微电网频率电压偏差的精确补偿,保障孤岛运行模式下系统的稳定性、电能质量及功率均分性能。研究在Simulink平台构建多逆变器协同控制仿真模型,全面验证所提策略在负载突变、通信延迟等典型工况下的有效性、鲁棒性与动态响应特性,为高比例分布式电源接入场景下的微电网控制提供了理论支持与技术路径。; 适合人群:具备电力系统、自动化、新能源等相关专业背景,从事微电网控制、分布式能源系统、智能配电网等领域研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①应用于孤岛微电网实现频率与电压的快速、精准恢复;②优化通信资源消耗,适用于通信条件受限的实际工程场景;③为含多分布式电源的智能微网系统提供高效、可靠的二次控制解决方案; 阅读建议:建议结合提供的Simulink仿真模型进行实践操作,重点剖析事件触发条件的设计逻辑、分布式控制协议的实现流程及仿真结果的动态性能分析,以深入掌握控制机理与系统协同优化方法。
这个是完整源码 java实现 大数据 Spark 可视化大屏+Kafka+SpringBoot+Vue3 【大数据毕业设计】基于Spark实时交通流量分析与拥堵预测系统(Java版本+可视化大屏+Kafka+SpringBoot+Vue3) 源码+论文 完整版 数据库Mysql 随着城市化进程不断加快,机动车保有量持续上升,城市道路拥堵问题日益突出。传统交通管理系统多依赖人工巡查与事后统计,难以对海量、高速产生的交通流数据进行实时感知与趋势研判,导致调度决策滞后。为缓解上述问题,本文设计并实现了一套“基于Spark实时交通流量分析与拥堵预测系统”。系统采用前后端分离架构:前端基于Vue3、Vite、Element Plus与ECharts构建管理后台与可视化大屏;后端基于Java 17与Spring Boot 3提供REST接口,结合Spring Security与JWT完成管理员身份认证与权限控制;数据层使用MySQL 8存储路段、流量、统计与预测结果,持久层采用MyBatis-Plus;实时链路引入Kafka作为交通事件消息中间件,使用Apache Spark完成窗口聚合统计,并基于Spark ML线性回归实现车流量预测与误差评估(RMSE、MAE、MAPE)。 系统实现了管理员登录与个人中心、道路路段管理、交通流量查询、实时窗口统计、拥堵预测分析以及可视化大屏展示等功能。针对Kafka不可用场景,系统提供纯Java写库降级策略,保证演示与运行的鲁棒性。测试结果表明,系统能够稳定完成交通事件采集、实时统计分析与拥堵趋势预测,界面交互清晰,数据展示及时,满足本科毕业设计对完整性、可演示性与技术综合性的要求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值