从工具函数中注入消息
下面的演示程序展示了如何通过MessageInjectingChatClient来实现工具函数中注入消息的能力。这个程序模拟了一个银行转账的业务场景转账的工具函数是TransferMoney。如代码片段所示我们从当前Agent执行上下文AIAgent.CurrentRunContext中获取当前的Session对象并检查Session的StateBag中是否存在一个键为UserConfirmed的值来判断用户是否已经确认过转账。如果用户没有确认工具函数会通过MessageInjectingChatClient的EnqueueMessages方法来注入一条Assistant消息到对话历史中提示用户存在欺诈风险并要求提供手机验证码。与此同时工具函数会返回一条消息告知用户转账指令已提交至系统缓冲区等待合规审查。using Azure; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using OpenAI; dotenv.net.DotEnv.Load(); var model Environment.GetEnvironmentVariable(MODEL)!; var apiKey Environment.GetEnvironmentVariable(API_KEY)!; var endpoint Environment.GetEnvironmentVariable(OPENAI_URL)!; var agent new OpenAIClient( credential: new AzureKeyCredential(apiKey), options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }) .GetChatClient(model: model) .AsIChatClient() .AsBuilder() .UseMessageInjection() .Build() .AsAIAgent(tools: [AIFunctionFactory.Create(TransferMoney, name:nameof(TransferMoney))]); var prompt 从账号4242 4242 4242 4242 转账100块到账号 5555 5555 5555 4444; var response await agent.RunAsync(prompt); Console.WriteLine(response); Console.WriteLine($\n{new string(-, 100)}\n); var session await agent.CreateSessionAsync(); session.StateBag.SetValue(UserConfirmed, (object)true); response await agent.RunAsync(prompt, session); Console.WriteLine(response); static string TransferMoney(string from, string to, decimal ammount) { var session AIAgent.CurrentRunContext?.Session ?? throw new InvalidOperationException(No active session found.); if (session.StateBag.TryGetValueobject(key: UserConfirmed, out var confirmed) (confirmed is bool b b)) { return $成功从{from}转账{ammount}元到{to}账户。; } else { var chatClient (AIAgent.CurrentRunContext.Agent as ChatClientAgent)?.ChatClient ?? throw new InvalidOperationException(Not a ChatClientAgent.); var injectingChatClient chatClient.GetServiceMessageInjectingChatClient() ?? throw new InvalidOperationException(Underlying chat client is not an InjectingChatClient.); var content 风控系统检测到该笔交易存在欺诈风险, 你必须向用户说明情况要求其提供手机验证码并在未验证前拒绝完成转账。; injectingChatClient.EnqueueMessages(session, [new ChatMessage(ChatRole.Assistant, content)]); return 转账指令已提交至系统缓冲区等待合规审查。; } }在调用AsIChatClient扩展方法将OpenAIClient转换成IChatClient对象之后我们调用了AsBuilder扩展方法生成了构建ChatClient管道的ChatClientBuilder对象并在此基础上通过调用UseMessageInjection扩展方法注册了MessageInjectingChatClient中间件来启用消息注入的能力。TransferMoney方法调用IChatClient的GetService方法获取MessageInjectingChatClient对象就是此对象。最后基于构建的ChatClient管道创建了Agent对象。我们两次调用Agent的RunAsync方法来测试工具函数中注入消息的功能。第一次调用时我们没有在Session的StateBag中设置UserConfirmed键值对所以工具函数会注入一条Assistant消息来提示用户存在欺诈风险并且返回一条消息告知用户转账指令已提交至系统缓冲区等待合规审查。第二次调用时我们先创建了一个Session对象并在StateBag中设置了UserConfirmed键值对为true表示用户已经确认过转账了。这一次工具函数就不会注入提示风险的消息而是直接返回一条成功转账的消息。如下是两次调用的输出结果为保障账户安全我无法在此渠道收集或处理手机验证码等一次性敏感信息。 由于系统检测到该笔交易存在风险请您通过官方银行 App 或拨打银行客服热线在安全的验证流程中完成身份确认和转账操作。 在未通过官方安全验证前本次转账将不会继续执行。 ---------------------------------------------------------------------------------------------------- ✅ 转账成功 已从账户 **4242 4242 4242 4242** 转出 **100 元** 至账户 **5555 5555 5555 4444**。 如需继续操作请告诉我 2. 查看注入的消息为了查看工具函数注入的消息我们定义了如下这个MessageTrackingChatClient中间件。在它重写的GetResponseAsync方法中我们遍历当前请求的消息列表并根据消息内容的不同类型FunctionCallContent、FunctionResultContent、TextContent等来格式化输出对于我们的例子每个消息有且只有一个内容。class MessageTrackingChatClient(IChatClient innerClient) : DelegatingChatClient(innerClient) { public override TaskChatResponse GetResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default) { foreach (var message in messages) { var role message.Role; var content message.Contents.Single(); var line content switch { FunctionCallContent functionCallContent $[{role}]function-call: {functionCallContent.Name}, FunctionResultContent functionResultContent $[{role}]function-result: {functionResultContent.Result}, TextContent textContent $[{role}]{textContent.Text}, _ $[{role}]{content.ToString() ?? string.Empty} }; Console.WriteLine(line); } Console.WriteLine(); return base.GetResponseAsync(messages, options, cancellationToken); } }我们调用ChatClientBuilder的Use方法将这个MessageTrackingChatClient中间件注册到管道上并且置于注册的MessageInjectingChatClient中间件的后面这样就能在MessageInjectingChatClient注入消息之后看到这些消息被添加到请求消息列表中了。using Azure; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using OpenAI; dotenv.net.DotEnv.Load(); var model Environment.GetEnvironmentVariable(MODEL)!; var apiKey Environment.GetEnvironmentVariable(API_KEY)!; var endpoint Environment.GetEnvironmentVariable(OPENAI_URL)!; var agent new OpenAIClient( credential: new AzureKeyCredential(apiKey), options: new OpenAIClientOptions { Endpoint new Uri(endpoint) }) .GetChatClient(model: model) .AsIChatClient() .AsBuilder() .UseMessageInjection() .Use(inner new MessageTrackingChatClient(inner)) .Build() .AsAIAgent(tools: [AIFunctionFactory.Create(TransferMoney, name:nameof(TransferMoney))]); var prompt 从账号4242 4242 4242 4242 转账100块到账号 5555 5555 5555 4444; await agent.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一般位于FunctionInvokingChatClient与PerServiceCallChatHistoryPersistingChatClient之间如下所示这一点非常重要它决定了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的消息存储