当前位置: 首页 > news >正文

[MAF预定义ChatClient中间件-09]MessageInjectingChatClient-赋予工具消息注入的能力

MAF中的ReAct循环是由FunctionInvokingChatClient中间件完成的,这意味着AIFunction的调用也是由它驱动。在默认的情况下,AIFunction绑定的委托执行完成后,结果会转换成AIContent并被封装成一个角色为Tool的ChatMessage。这个ChatMessage最终被添加到对话历史中,并作为后续调用LLM的输入。换言之,AIFunction不像LangChain的工具函数一样,可以通过返回Command对象添加具有合法结构的消息列表到对话历史中。当工具在执行过程借助注入对话历史的消息来描述当前的情况,以辅助LLM后续能够更加精准的推理,这是非常有价值的。比如工具在执行过程中发现验证的风控风险,可以注入一条Assistant消息模拟LLM的回复来提示用户风险的存在。

1. 从工具函数中注入消息

下面的演示程序展示了如何通过MessageInjectingChatClient来实现工具函数中注入消息的能力。这个程序模拟了一个银行转账的业务场景,转账的工具函数是TransferMoney。如代码片段所示,我们从当前Agent执行上下文(AIAgent.CurrentRunContext)中获取当前的Session对象,并检查Session的StateBag中是否存在一个键为UserConfirmed的值来判断用户是否已经确认过转账。如果用户没有确认,工具函数会通过MessageInjectingChatClientEnqueueMessages方法来注入一条Assistant消息到对话历史中,提示用户存在欺诈风险并要求提供手机验证码。与此同时,工具函数会返回一条消息告知用户转账指令已提交至系统缓冲区,等待合规审查。

usingAzure;usingMicrosoft.Agents.AI;usingMicrosoft.Extensions.AI;usingMicrosoft.Extensions.DependencyInjection;usingOpenAI;dotenv.net.DotEnv.Load();varmodel=Environment.GetEnvironmentVariable("MODEL")!;varapiKey=Environment.GetEnvironmentVariable("API_KEY")!;varendpoint=Environment.GetEnvironmentVariable("OPENAI_URL")!;varagent=newOpenAIClient(credential:newAzureKeyCredential(apiKey),options:newOpenAIClientOptions{Endpoint=newUri(endpoint)}).GetChatClient(model:model).AsIChatClient().AsBuilder().UseMessageInjection().Build().AsAIAgent(tools:[AIFunctionFactory.Create(TransferMoney,name:nameof(TransferMoney))]);varprompt="从账号`4242 4242 4242 4242` 转账100块到账号 `5555 5555 5555 4444`";varresponse=awaitagent.RunAsync(prompt);Console.WriteLine(response);Console.WriteLine($"\n{newstring('-',100)}\n");varsession=awaitagent.CreateSessionAsync();session.StateBag.SetValue("UserConfirmed",(object)true);response=awaitagent.RunAsync(prompt,session);Console.WriteLine(response);staticstringTransferMoney(stringfrom,stringto,decimalammount){varsession=AIAgent.CurrentRunContext?.Session??thrownewInvalidOperationException("No active session found.");if(session.StateBag.TryGetValue<object>(key:"UserConfirmed",outvarconfirmed)&&(confirmedisboolb&&b)){return$"成功从`{from}`转账`{ammount}`元到`{to}`账户。";}else{varchatClient=(AIAgent.CurrentRunContext.AgentasChatClientAgent)?.ChatClient??thrownewInvalidOperationException("Not a ChatClientAgent.");varinjectingChatClient=chatClient.GetService<MessageInjectingChatClient>()??thrownewInvalidOperationException("Underlying chat client is not an InjectingChatClient.");varcontent="风控系统检测到该笔交易存在欺诈风险, 你必须向用户说明情况,要求其提供手机验证码,并在未验证前拒绝完成转账。";injectingChatClient.EnqueueMessages(session,[newChatMessage(ChatRole.Assistant,content)]);return"转账指令已提交至系统缓冲区,等待合规审查。";}}

在调用AsIChatClient扩展方法将OpenAIClient转换成IChatClient对象之后,我们调用了AsBuilder扩展方法生成了构建ChatClient管道的ChatClientBuilder对象,并在此基础上通过调用UseMessageInjection扩展方法注册了MessageInjectingChatClient中间件来启用消息注入的能力。TransferMoney方法调用IChatClientGetService方法获取MessageInjectingChatClient对象就是此对象。最后基于构建的ChatClient管道创建了Agent对象。

我们两次调用AgentRunAsync方法来测试工具函数中注入消息的功能。第一次调用时,我们没有在SessionStateBag中设置UserConfirmed键值对,所以工具函数会注入一条Assistant消息来提示用户存在欺诈风险,并且返回一条消息告知用户转账指令已提交至系统缓冲区,等待合规审查。第二次调用时,我们先创建了一个Session对象,并在StateBag中设置了UserConfirmed键值对为true,表示用户已经确认过转账了。这一次工具函数就不会注入提示风险的消息,而是直接返回一条成功转账的消息。如下是两次调用的输出结果:

为保障账户安全,我无法在此渠道收集或处理手机验证码等一次性敏感信息。 由于系统检测到该笔交易存在风险,请您通过官方银行 App 或拨打银行客服热线,在安全的验证流程中完成身份确认和转账操作。 在未通过官方安全验证前,本次转账将不会继续执行。 ---------------------------------------------------------------------------------------------------- ✅ 转账成功! 已从账户 **4242 4242 4242 4242** 转出 **100 元** 至账户 **5555 5555 5555 4444**。 如需继续操作,请告诉我 😊

2. 查看注入的消息

为了查看工具函数注入的消息,我们定义了如下这个MessageTrackingChatClient中间件。在它重写的GetResponseAsync方法中,我们遍历当前请求的消息列表,并根据消息内容的不同类型(FunctionCallContentFunctionResultContentTextContent等)来格式化输出(对于我们的例子,每个消息有且只有一个内容)。

classMessageTrackingChatClient(IChatClientinnerClient):DelegatingChatClient(innerClient){publicoverrideTask<ChatResponse>GetResponseAsync(IEnumerable<ChatMessage>messages,ChatOptions?options=null,CancellationTokencancellationToken=default){foreach(varmessageinmessages){varrole=message.Role;varcontent=message.Contents.Single();varline=contentswitch{FunctionCallContentfunctionCallContent=>$"[{role}]function-call:{functionCallContent.Name}",FunctionResultContentfunctionResultContent=>$"[{role}]function-result:{functionResultContent.Result}",TextContenttextContent=>$"[{role}]{textContent.Text}",_=>$"[{role}]{content.ToString()??string.Empty}"};Console.WriteLine(line);}Console.WriteLine();returnbase.GetResponseAsync(messages,options,cancellationToken);}}

我们调用ChatClientBuilderUse方法将这个MessageTrackingChatClient中间件注册到管道上,并且置于注册的MessageInjectingChatClient中间件的后面,这样就能在MessageInjectingChatClient注入消息之后看到这些消息被添加到请求消息列表中了。

usingAzure;usingMicrosoft.Agents.AI;usingMicrosoft.Extensions.AI;usingMicrosoft.Extensions.DependencyInjection;usingOpenAI;dotenv.net.DotEnv.Load();varmodel=Environment.GetEnvironmentVariable("MODEL")!;varapiKey=Environment.GetEnvironmentVariable("API_KEY")!;varendpoint=Environment.GetEnvironmentVariable("OPENAI_URL")!;varagent=newOpenAIClient(credential:newAzureKeyCredential(apiKey),options:newOpenAIClientOptions{Endpoint=newUri(endpoint)}).GetChatClient(model:model).AsIChatClient().AsBuilder().UseMessageInjection().Use(inner=>newMessageTrackingChatClient(inner)).Build().AsAIAgent(tools:[AIFunctionFactory.Create(TransferMoney,name:nameof(TransferMoney))]);varprompt="从账号`4242 4242 4242 4242` 转账100块到账号 `5555 5555 5555 4444`";awaitagent.RunAsync(prompt);

输出结果:

[user]从账号`4242 4242 4242 4242` 转账100块到账号 `5555 5555 5555 4444` [user]从账号`4242 4242 4242 4242` 转账100块到账号 `5555 5555 5555 4444` [assistant]function-call: TransferMoney [tool]function-result: 转账指令已提交至系统缓冲区,等待合规审查。 [assistant]风控系统检测到该笔交易存在欺诈风险, 你必须向用户说明情况,要求其提供手机验证码,并在未验证前拒绝完成转账。

输出的最后一条消息正是工具函数中注入的消息,说明MessageInjectingChatClient确实成功地将这条消息添加到了请求消息列表中,并且在后续调用LLM时被包含在输入上下文里了。为了保证对话历史具有合法的结构,注入的消息会放在承载工具函数调用结果的Tool消息之后。

3. MessageInjectingChatClient

在了解了MessageInjectingChatClient的作用和基本编程模式之后,我们继续介绍MAF针对这个ChatClient中间件的设计和实现。

3.1 在ChatClient管道中的位置

MessageInjectingChatClient一般位于FunctionInvokingChatClientPerServiceCallChatHistoryPersistingChatClient之间(如下所示),这一点非常重要,它决定了MessageInjectingChatClient中间件在每个ReAct循环中都会被执行。如果开启了针对每个ReAct循环的及时存档,注入的消息会被PerServiceCallChatHistoryPersistingChatClient捕获并存储到ChatHistoryMemoryProvider中,这样就能让Agent在后续的ReAct循环中基于这些注入的消息进行推理了。

[外部请求] => FunctionInvokingChatClient => MessageInjectingChatClient => PerServiceCallChatHistoryPersistingChatClient => LLM

这个特定的拓扑结构,说明了以下关键底座逻辑:

  • 工具函数具备改写认知的能力:由于FunctionInvokingChatClient处于MessageInjectingChatClient的上游,决定了:
    • 工具执行的副作用可被捕获:当FunctionInvokingChatClient触发并执行某个工具函数时,如果该工具内部触发了前文提到的风控逻辑、反思逻辑或上下文切换,它排队的临时消息正处于FunctionInvokingChatClient的处理边界之内。
    • 下游管道立即可见:工具函数产生的注入消息,能立刻在向下传递给PerServiceCallChatHistoryPersistingChatClient之前被消费并合并。
  • 注入的消息是临时干预,还是永久记忆MessageInjectingChatClient位于PerServiceCallChatHistoryPersistingChatClient的上游,这个顺序界定了注入消息的生命周期:
    • 被持久化组件捕获:当MessageInjectingChatClient将排队的消息动态拼接到当前的对话历史后,这些新组合的消息会原封不动地流向底部的PerServiceCallChatHistoryPersistingChatClient
    • 实现“单次服务调用”的存档:这意味着,在工具函数中注入的消息,会被当做本次生命周期的一部分,一并写入对话历史。在后续的用户多轮对话中,注入的消息会变成不可分割的永久记忆,而不是一次性的临时缓存;

MessageInjectingChatClient所处的位置说明了MAF将消息注入视为一种连接**动态运行时(Tools/Agent 决策)静态持久层(Database/Session)**的管道桥梁。如果把MessageInjectingChatClient挪到FunctionInvokingChatClient之前,工具函数内部就失去了操作注入客户端的上下文权限;如果把它挪到PerServiceCallChatHistoryPersistingChatClient之后,注入的消息就只能直达大模型,而无法在数据库中留下任何历史存档。目前这个位置,是支持工具内隐式风控、自我反思闭环的黄金分水岭。

3.2 基于Session的消息存储

MessageInjectingChatClient将注入的消息存储在当前SessionStateBag中,这样就能保证这些消息在当前Session的生命周期内都是可见的了。具体存储的是一个List<ChatMessage>对象,对应的键为MessageInjectingChatClient.PendingInjectedMessages。如下所示的是用来注入消息列表的EnqueueMessages方法的定义。

publicsealedclassMessageInjectingChatClient:DelegatingChatClient{publicvoidEnqueueMessages(AgentSessionsession,IEnumerable<ChatMessage>messages){varqueue=GetOrCreateQueue(session);lock(queue){foreach(varmessageinmessages){queue.Add(message);}}}privatestaticList<ChatMessage>GetOrCreateQueue(AgentSessionsession){if(session.StateBag.TryGetValue<List<ChatMessage>>("MessageInjectingChatClient.PendingInjectedMessages",outvarqueue)){returnqueue!;}varnewQueue=newList<ChatMessage>();session.StateBag.SetValue("MessageInjectingChatClient.PendingInjectedMessages",newQueue);returnnewQueue;}}

3.3 针对注入消息的处理

外界(主要是注册的工具函数)得到MessageInjectingChatClient对象后,可以通过调用EnqueueMessages方法持续不断地注入消息。这种对流或者流式的消息注入方式导致了,MessageInjectingChatClient提取了所有注入消息并对它们进行处理之后,会发现又有新的消息被注入进来。所以MessageInjectingChatClient会在一个循环中提取和处理注入的消息,这一逻辑实现在重写的GetResponseAsyncGetStreamingResponseAsync方法中。

publicsealedclassMessageInjectingChatClient:DelegatingChatClient{publicoverrideasyncTask<ChatResponse>GetResponseAsync(IEnumerable<ChatMessage>messages,ChatOptions?options=null,CancellationTokencancellationToken=default);publicoverrideasyncIAsyncEnumerable<ChatResponseUpdate>GetStreamingResponseAsync(IEnumerable<ChatMessage>messages,ChatOptions?options=null,CancellationTokencancellationToken=default);}

GetResponseAsync方法为例,它的执行流程如下:

  • 步骤1:提取当前注入的所有消息,并添加到传入的消息列表中,作为输入调用后学的ChatClient管道;
  • 步骤2:如果返回ChatResponse中携带的消息携带工具调用,立即返回此响应,此响应最终会交付到MessageInjectingChatClient手上驱动工具函数的执行;
  • 步骤3:如果没有工具调用
    • 如果具有新注入的消息,继续循环回到步骤1来处理这些新注入的消息,但是在这之前为了将后续调用纳入同一个Session,它需要将响应提供的ConversionID应用到ChatOptions上;
    • 如果没有新注入的消息,说明注入的消息已经全部被处理完了,直接返回当前的响应。

3.4 如何提取MessageInjectingChatClient

通过我们上面的介绍,我们知道了注入的消息被存储在SessionStateBag中,而且还知道键的名称和数据结构,我们可以直接操作Session来完成消息注入。但是我们最好不要这么做,因为还需要考虑针对消息列表的创建(对于第一次注入)以及多线程并发问题。所以还是老老实实调用MessageInjectingChatClient提供的EnqueueMessages方法来完成消息注入比较好。我们不能直接将此对象注入工具函数,最常用的方式就是像上面演示的那样,在工具函数内部通过AIAgent.CurrentRunContext.Agent来获取当前的Agent对象,并将其转换成ChatClientAgent对象来获取MessageInjectingChatClient对象。

varchatClient=(AIAgent.CurrentRunContext.AgentasChatClientAgent)?.ChatClient??thrownewInvalidOperationException("Not a ChatClientAgent.");varinjectingChatClient=chatClient.GetService<MessageInjectingChatClient>()??thrownewInvalidOperationException("Underlying chat client is not an InjectingChatClient.");

3.5 UseMessageInjection扩展方法

针对MessageInjectingChatClient的注册可以通过调用ChatClientBuilderUseMessageInjection扩展方法来完成。这个扩展方法的实现非常简单,它调用了ChatClientBuilderUse方法来将MessageInjectingChatClient中间件注册到管道上。

publicstaticclassChatClientBuilderExtensions{publicstaticChatClientBuilderUseMessageInjection(thisChatClientBuilderbuilder)=>builder.Use(innerClient=>newMessageInjectingChatClient(innerClient));}
http://www.gsyq.cn/news/1454070.html

相关文章:

  • Arduino光控智能照明系统:从传感器到PWM调光的完整实践
  • 从有到无:聊聊DRAM-less SSD是怎么工作的,以及它真的适合你吗?
  • 别再死磕XGBoost了!LightGBM直方图算法实战,内存消耗直降8倍
  • Arduino入门实战:从零搭建LED控制电路与代码精讲
  • 2026年电脑AI助手横评对比
  • 如何在 Windows 上安装部署Open Claw 2.7.5?
  • 从“换脸”到“换风格”:聊聊CVPR 2020 FDA论文里没细说的频域可视化与调参陷阱
  • 安全团队效率翻倍:用Netsparker API + Jenkins 打造自动化漏洞扫描与通知流水线
  • Vosk API:如何用50MB模型实现离线语音识别的技术革命
  • 2026年电脑AI助手评测:Marvis夺冠
  • Java写的电表轮询采集工具:5秒一采,自动解析DL/T645协议并存入MySQL
  • 毕业设计可用的康复动作识别工具包:YOLOv8模型+标注数据+可视化界面+一键运行脚本
  • PCA实战避坑指南:用NumPy和Sklearn对比实现,教你处理真实数据中的常见问题
  • ppt模板_0069_橙色箭头
  • 机器人如何成为灾难救援的“第二双手”:技术原理与应用解析
  • STM32远程升级避坑指南:EC800K模组HTTP/HTTPS下载的稳定性设计与调试
  • 喜马拉雅VIP音频如何下载?跨平台下载器xmly-downloader-qt5轻松解锁付费内容
  • 用MakeyMakey与Scratch制作音乐互动体育游戏:STEAM教育实践
  • 基于Azure云平台构建气候大数据服务:从数据孤岛到智能洞察
  • 2026走心机高频铣深度测评:如何为走心机精密加工匹配最佳方案? - 资讯纵览
  • 抖音下载终极指南:3步搞定无水印视频批量管理
  • 雷达目标检测避坑指南:你的CA-CFAR为什么不准?聊聊参考窗和保护间隔的实战设置
  • STM32F103C8T6小板实战:4按键控LED + NEC红外输数字 + OLED实时显示(KEIL工程全源码)
  • 低成本DIY:将AAA电池设备改造为交流电供电的完整方案
  • B站视频格式转换终极方案:5分钟将m4s缓存无损转为通用MP4
  • 避坑指南:银河麒麟V10离线装Docker后,搞定K8s集成与crictl报错
  • 贯穿整个 Java Web 框架,演示从零实现「精简可运行」的 CodeStats,构建专属自己的完整开发体系!
  • RapidOCR微秒级推理优化:多引擎架构下的实时文字识别技术突破
  • Chemistry Add-in for Word:在Word中无缝集成化学绘图与计算
  • Adobe-GenP 3.0完整使用指南:免费解锁Adobe全家桶的终极解决方案