惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cisco Blogs
Scott Helme
Scott Helme
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
C
Cyber Attacks, Cyber Crime and Cyber Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Schneier on Security
I
Intezer
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
V
Vulnerabilities – Threatpost
Cloudbric
Cloudbric
V2EX - 技术
V2EX - 技术
Google Online Security Blog
Google Online Security Blog
L
Lohrmann on Cybersecurity
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
LINUX DO - 热门话题
S
Secure Thoughts
T
The Exploit Database - CXSecurity.com
博客园 - 【当耐特】
Recent Announcements
Recent Announcements
Security Archives - TechRepublic
Security Archives - TechRepublic
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
K
Kaspersky official blog
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
Latest news
Latest news
B
Blog
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗
L
LangChain Blog
GbyAI
GbyAI
Last Week in AI
Last Week in AI
S
Security Affairs
Apple Machine Learning Research
Apple Machine Learning Research
N
Netflix TechBlog - Medium
Security Latest
Security Latest
Vercel News
Vercel News
Y
Y Combinator Blog
G
GRAHAM CLULEY
S
Securelist
T
Troy Hunt's Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
雷峰网
雷峰网

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
[MAF预定义ChatClient中间件-02]FunctionInvokingChatClient——实现ReAct循环和人机交互的大功臣
Artech · 2026-06-04 · via 博客园_首页

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

1. 利用FunctionInvokingChatClient实现ReAct循环

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

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

比如我最常用的“根据某地天气提供着装建议”的场景,ReAct循环的执行流程如下。这是一个简单的只涉及单次迭代的ReAct循环,实际的ReAct循环可能会涉及多次迭代,模型在每次迭代中都会根据新的上下文来分析下一步该做什么。

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

下面这个演示程序直接利用FunctionInvokingChatClient将上述的ReAct循环落地。如代码所示,我们创建了一个基于OpenAIClientIChatClient对象,并在调用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返回的所有工具调用视为一个类似于事务的整体,如果所有工具都不需要审批,那么它会采用直接调用这些工具。如果其中有任何一个工具需要审批,它会任务所有工具调用都需要审批。这也很好理解,因为所有的工具都是为了同一个任务服务的,如果其中一个工具需要审批,那么整个任务就需要审批。以本例来说,虽然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方法中实现相对繁琐,所以我不打算详细解说FunctionInvokingChatClient中ReAct循环的实现逻辑,而是利用如下这个FunctionInvokingChatClientSimulator类型的GetResponseAsync来模拟ReAct循环的实现。如下面的代码片段所示,在重写的GetResponseAsync方法中,我们首先调用InnerClient.GetResponseAsync方法来获取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);
}