MAF快速入门(2)Agent的花样玩法

大家好,我是Edison。

上一篇,我们学习了如何使用MAF创建一个简单的Agent,这一篇我们学习下MAF对于单个Agent的花样玩法

将Agent当Function Tool调用

在MAF中,我们可以很方便地将创建好的某个Agent当做一个Function Tool在另一个Agent中直接调用。例如,下面的代码示例中的weatherAgent就被当做Function Tool在mainAgent中直接调用。 

// Step1. Create an AI agent that uses a weather service plugin
var weatherAgent = new OpenAIClient(new ApiKeyCredential(openAIProvider.ApiKey),new OpenAIClientOptions { Endpoint = new Uri(openAIProvider.Endpoint) }).GetChatClient(openAIProvider.ModelId).CreateAIAgent(instructions: "You answer questions about the weather.",name: "WeatherAgent",description: "An agent that answers questions about the weather.",tools: [AIFunctionFactory.Create(WeatherServicePlugin.GetWeatherAsync)]);
// Step2. Create another AI agent that uses the weather agent as a function tool
var mainAgent = new OpenAIClient(new ApiKeyCredential(openAIProvider.ApiKey),new OpenAIClientOptions { Endpoint = new Uri(openAIProvider.Endpoint) }).GetChatClient(openAIProvider.ModelId).CreateAIAgent(instructions: "You are a helpful assistant who responds message in Chinese.", tools: [weatherAgent.AsAIFunction()]);
// Step3. Test the portal agent
Console.WriteLine(await mainAgent.RunAsync("What is the weather like in Chengdu?"));

执行结果如下图所示:

image

由此可见,万物皆可tools。

将Agent暴露为MCP Tool

在MAF中,还可以将某个创建好的Agent快速地暴露为一个MCP Tool供其他Agent通过MCP协议调用,简直不要太方便:

var jokerAgent = new OpenAIClient(new ApiKeyCredential(openAIProvider.ApiKey),new OpenAIClientOptions { Endpoint = new Uri(openAIProvider.Endpoint) }).GetChatClient(openAIProvider.ModelId).CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Expose the agent as a MCP tool
var jokerMcpTool = McpServerTool.Create(jokerAgent.AsAIFunction());

然后,你可以创建一个MCP Server然后将这个MCP Tool注册进去:

// Create a MCP server and register the tool
// Register the MCP server with StdIO transport and expose the tool via the server.
var builder = Host.CreateEmptyApplicationBuilder(settings: null);
builder.Services.AddMcpServer().WithStdioServerTransport().WithTools([jokerMcpTool]);
await builder.Build().RunAsync();

将这个应用程序启动起来,Ta就可以对外提供MCP服务了。

持久化Agent中的对话

假设用户在与某个Agent对话还未结束时离开了,当他再次回来时是希望能保持会话的上下文的,那么我们完全可以将这个对话AgentThread进行持久化,等用户回来时从存储中加载出来上下文,便可以保证用户体验。

// Step1. Create an AI agent
var jokerAgent = new OpenAIClient(new ApiKeyCredential(openAIProvider.ApiKey),new OpenAIClientOptions { Endpoint = new Uri(openAIProvider.Endpoint) }).GetChatClient(openAIProvider.ModelId).CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Step2. Start a new thread for the agent conversation
var thread = jokerAgent.GetNewThread();
// Step3. Run the agent with a new thread
Console.WriteLine(await jokerAgent.RunAsync("Tell me a joke about a pirate.", thread));
Console.WriteLine("==> Now user leaves the chat, system save the conversation to local storage.");
// Step4. Serialize the thread state to a JsonElement, so that it can be persisted for later use
var serializedThread = thread.Serialize();
// Step5. Save the serialized thread to a file (for demonstration purposes)
var tempFilePath = Path.GetTempFileName();
await File.WriteAllTextAsync(tempFilePath, JsonSerializer.Serialize(serializedThread));
// Step6. Deserialize the thread state after loading from storage.
Console.WriteLine("==> Now user join the chat again, system starting to load last conversation.");
var reoladedSerializedThread = JsonSerializer.Deserialize<JsonElement>(await File.ReadAllTextAsync(tempFilePath));
var resumedThread = jokerAgent.DeserializeThread(reoladedSerializedThread);
// Step7. Run the agent with the resumed thread
Console.WriteLine(await jokerAgent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));

将这个示例代码跑起来,我们可以看到它生成了一个tmp文件,里面保存了对话记录:

image

然后,我们可以设置一个端点看看加载出来后的AgentThread,完整保留了对话记录:

image

最后我们可以看到执行结果:

image

实际上,针对这个场景,我们完全可以借助Redis或关系型数据库等存储介质来实现这个对话的持久化操作

使用第三方存储保存聊天记录

这里我们就来演示下如何将对话存储在第三方存储服务中,这里我们使用 InMemoryVectorStore 来实现这个这个目的。

首先,你需要安装下面这个包:

Microsoft.SemanticKernel.Connectors.InMemory

然后,我们需要创建一个自定义的ChatMessageStore来提供添加和查询聊天记录。这是因为我们需要实现抽象父类 ChatMessageStore 要求的两个重要方法:

  • AddMessageAsync : 向存储区添加新的聊天记录

  • GetMessageAsync : 从存储区获取已有的聊天记录

下面是这个自定义ChatMessageStore类的实现:

public sealed class VectorChatMessageStore : ChatMessageStore
{private readonly VectorStore _vectorStore;public VectorChatMessageStore(VectorStore vectorStore,JsonElement serializedStoreState,JsonSerializerOptions? jsonSerializerOptions = null){this._vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore));if (serializedStoreState.ValueKind is JsonValueKind.String){this.ThreadDbKey = serializedStoreState.Deserialize<string>();}}public string? ThreadDbKey { get; private set; }public override async Task AddMessagesAsync(IEnumerable<ChatMessage> messages,CancellationToken cancellationToken){this.ThreadDbKey ??= Guid.NewGuid().ToString("N");var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");await collection.EnsureCollectionExistsAsync(cancellationToken);await collection.UpsertAsync(messages.Select(x => new ChatHistoryItem(){Key = this.ThreadDbKey + x.MessageId,Timestamp = DateTimeOffset.UtcNow,ThreadId = this.ThreadDbKey,SerializedMessage = JsonSerializer.Serialize(x),MessageText = x.Text}), cancellationToken);}public override async Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken){var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");await collection.EnsureCollectionExistsAsync(cancellationToken);var records = collection.GetAsync(x => x.ThreadId == this.ThreadDbKey, 10,new() { OrderBy = x => x.Descending(y => y.Timestamp) },cancellationToken);List<ChatMessage> messages = [];await foreach (var record in records){messages.Add(JsonSerializer.Deserialize<ChatMessage>(record.SerializedMessage!)!);}messages.Reverse();return messages;}public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) =>// We have to serialize the thread id, so that on deserialization you can retrieve the messages using the same thread id.JsonSerializer.SerializeToElement(this.ThreadDbKey);private sealed class ChatHistoryItem{[VectorStoreKey]public string? Key { get; set; }[VectorStoreData]public string? ThreadId { get; set; }[VectorStoreData]public DateTimeOffset? Timestamp { get; set; }[VectorStoreData]public string? SerializedMessage { get; set; }[VectorStoreData]public string? MessageText { get; set; }}
}

这里需要重点关注的是:当收到第一条消息时,该存储会为该线程生成一个唯一ID Key,用于表示该聊天记录便于后续从该存储中获取。而这个唯一ID Key存储在ThreadDbKey属性(ChatMessageStore类中的定义)中该属性通过SerializeStateAsync方法和接受JsonElement的构造函数进行序列化和反序列化。

现在我们可以来看看如何来使用它,我直接给出完整的示例:

// Create a shared in-memory vector store to store the chat messages.
var vectorStore = new InMemoryVectorStore();
// Create an AI agent that uses the vector store to persist its conversations.
var jokerAgent = new OpenAIClient(new ApiKeyCredential(openAIProvider.ApiKey),new OpenAIClientOptions { Endpoint = new Uri(openAIProvider.Endpoint) }).GetChatClient(openAIProvider.ModelId).CreateAIAgent(new ChatClientAgentOptions{Name = "Joker",Instructions = "You are good at telling jokes.",ChatMessageStoreFactory = ctx =>{// Create a new chat message store for this agent that stores the messages in a vector store.// Each thread must get its own copy of the VectorChatMessageStore, since the store// also contains the id that the thread is stored under.return new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions);}});
// Start a new thread for the agent conversation.
var thread = jokerAgent.GetNewThread();
// Run the agent with a new thread.
var userMessage = "Tell me a joke about a pirate.";
Console.WriteLine($"User> {userMessage}");
Console.WriteLine($"Agent> " + await jokerAgent.RunAsync(userMessage, thread));
// Assume user leaves the chat, system saves the conversation to vector storage.
Console.WriteLine("\n[DEBUG] Now user leaves the chat, system save the conversation to vector storage.");
var serializedThread = thread.Serialize();
Console.WriteLine("[DEBUG] Serialized thread ---\n");
Console.WriteLine(JsonSerializer.Serialize(serializedThread, new JsonSerializerOptions { WriteIndented = true }));
// Assume user joins the chat again, system starts to load last conversation.
Console.WriteLine("\n[DEBUG] Now user join the chat again, system starting to load last conversation.\n");
var resumedThread = jokerAgent.DeserializeThread(serializedThread);
// Run the agent with the resumed thread.
userMessage = "Now tell the same joke in the voice of a pirate, and add some emojis to the joke.";
Console.WriteLine($"User> {userMessage}");
Console.WriteLine($"Agent> " + await jokerAgent.RunAsync(userMessage, resumedThread));
// Check the thread is stored in the vector store.
var messageStore = resumedThread.GetService<VectorChatMessageStore>()!;
Console.WriteLine($"\n[DEBUG] Thread is stored in vector store under key: {messageStore.ThreadDbKey}");

执行结果如下图所示:

image

可以看到,我们模拟用户中途离开然后恢复会话,由于之前的会话记录已经被存入了InMemoryVectorStore,所以当会话恢复时,通过ThreadDbKey从中获取来原来的对话记录并继续对话,给了用户持续的体验。

给Agent添加Middleware中间

在ASP.NET开发中,我们喜欢用中间件来拦截和增强代理通信,增强日志记录和安全性。那么在Agent开发中,MAF也允许我们创建自己的中间件来实现同样的目的。

假设,我们创建一个通用的函数调用中间件,它可以在每个函数工具被调用时触发,它顺便帮我们记录下每个被调用的函数和记录函数调用的结果以便于审计。

async ValueTask<object?> CustomFunctionCallingMiddleware(AIAgent agent,FunctionInvocationContext context,Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next,CancellationToken cancellationToken)
{Console.WriteLine($"[LOG] Function Name: {context!.Function.Name}");var result = await next(context, cancellationToken);Console.WriteLine($"[LOG] Function Call Result: {result}");return result;
}

下面是如何使用这个中间件的示例代码:

(1)首先,创建一个Agent

var baseAgent = new OpenAIClient(new ApiKeyCredential(openAIProvider.ApiKey),new OpenAIClientOptions { Endpoint = new Uri(openAIProvider.Endpoint) }).GetChatClient(openAIProvider.ModelId).CreateAIAgent(instructions: "You are an AI assistant that helps people find information.",tools: [AIFunctionFactory.Create(DateTimePlugin.GetDateTime)]);

(2)将中间件添加到Agent

var middlewareEnabledAgent = baseAgent.AsBuilder().Use(CustomFunctionCallingMiddleware).Build();

(3)测试一下

var userMessage = "Hi, what's the current time?";
Console.WriteLine($"User> {userMessage}");
var agentResponse = await middlewareEnabledAgent.RunAsync(userMessage);
Console.WriteLine($"Agent> {agentResponse}");

执行结果如下图所示:可以看到我们增强的日志记录

image

给Agent添加可观测性

提到可观测行,就不得不提 OpenTelemetry,它是一个开源的可观测性框架,用于收集和分析应用程序的性能数据(例如 追踪、指标 和 日志),帮助实现系统监控和故障排查。

MAF支持为Agent启用可观测性支持,实现起来是很快速的。这里我们实现一个例子,在Agent应用中启用OpenTelemetry,并将追踪信息导出到控制台中显示。

(1)添加下列Nuget包

dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.Console

(2)创建一个自定义的追踪器来将追踪信息导出到控制台

// Create a TracerProvider that exports to the console
using var tracerProvider = Sdk.CreateTracerProviderBuilder().AddSource("agent-telemetry-source").AddConsoleExporter().Build();

这里的source name我们暂且叫它agent-telemetry-source。

(3)创建Agent并启用OpenTelemetry

// Create the agent and enable OpenTelemetry instrumentation
var agent = new OpenAIClient(new ApiKeyCredential(openAIProvider.ApiKey),new OpenAIClientOptions { Endpoint = new Uri(openAIProvider.Endpoint) }).GetChatClient(openAIProvider.ModelId).CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker").AsBuilder().UseOpenTelemetry(sourceName: "agent-telemetry-source").Build();

这里启用OpenTemetry时需要指定source name,需和我们刚刚创建的保持一致!

(4)测试一下

// Run the agent and generate telemetry
var userMessage = "Tell me a joke about a pirate.";
Console.WriteLine($"User> {userMessage}");
Console.WriteLine($"Agent> {await agent.RunAsync(userMessage)}");

执行结果如下图所示:可以看到,由于启用了OpenTelemetry,它现将调用的追踪信息发到了控制台,然后才输出了Agent的响应内容

image

实际上,我们完全可以将这些trace,metric 和 log 发到已有的IT监控系统中,如Prometheus, Elastic等等。

小结

本文介绍了MAF在Agent创建中的一些花样玩法,这些玩法可以极大地扩展我们开发Agent的模式和用途。

下一篇,我们将继续MAF的学习。

示例源码

GitHub: https://github.com/EdisonTalk/MAFD

参考资料

Microsoft Learn,《Agent Framework Tutorials》

推荐学习

圣杰,《.NET + AI 智能体开发进阶》

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/975174.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

效率与安全的双引擎:聚焦合同管理中的印章文识别技术

在合同管理的数字化进程中,我们往往追逐于复杂的技术幻象,却忽略了最本质的数据价值。一枚印章,其最核心的信息并非它的轮廓与色彩,而是它所承载的文字内容。剥离防伪鉴定的复杂外衣,聚焦于将印章图像精准转化为结…

re笔记1

polar 简单re-shell 打开后查壳,用upx -d 脱壳 找到主函数(其实不是主函数,没加载完导致跳转错了) // attributes: thunk int __stdcall sub_45CC60(int a1, int a2, int a3, int a4, int a5) { return sub_4795…

海外求职必备:多语言AI简历工具如何助力求职外企和跨国公司

全球人才流动的趋势日益显著,海外求职已成为越来越多职场人实现职业突破的关键一步。然而,跨越国界寻找工作,除了应对语言障碍,更要面对不同国家和地区在简历格式、文化偏好乃至职业表达上的巨大差异。 一份未能精…

MATLAB/Simulink水箱水位控制系统实现

一、系统建模与参数设定 1.1 水箱动力学模型 质量守恒方程: \(A\frac{dh}{dt}=Q_{in}−Q_{out}\) 其中:\(A\):水箱横截面积(m) \(h\):水位高度(m) \(Q_{in}\):进水流量(m/s) \(Q_{out}\):出水流量(m/s)阀…

AI语言大模型支持下的:SCI论文从设计到发表的全流技巧(选题、文献调研、实验设计、数据分析、论文结构及语言规范) - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Ai元人文:前言

Ai元人文:前言 AI元人文”不是一个企图统治世界的“哲学帝国”,而是一个旨在促进人机之间、学科之间良性互动的“协作生态”。它谦卑地承认权重的决定权属于全人类,并明智地将自己的雄心限定在解决“人机文明”这一…

Oracle ASM存储维护实践与规范指南

Oracle ASM存储维护实践与规范指南在现代融IT架构中,Oracle Automatic Storage Management (ASM) 已成为数据库存储管理的首选方案,尤其在RAC(Real Application Clusters)环境中,它提供了高效、可靠且易于管理的共…

新露谷物语-新手指南:

星露谷物语天行建--------君子以自强不惜

从 runC 到 runD:SAE 如何用 “装甲级” 隔离,化解运维安全焦虑!

阿里云 Serverless 应用引擎 SAE 是面向 AI 时代的一站式容器化应用托管平台,以“托底传统应用、加速 AI 创新”为核心理念。它简化运维、保障稳定、闲置特性降低 75% 成本,并通过 AI 智能助手提升运维效率。作者:张…

实用指南:SAP MM 采购申请转采购订单功能分享

实用指南:SAP MM 采购申请转采购订单功能分享2025-11-24 18:24 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: …

ddddocr: 滑块验证码的一个例子

一,代码: from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import Byfrom sel…

恢复Windows图片查看器

将下面的内容保存为 .reg 文件,双击运行即可。 Windows Registry Editor Version 5.00[HKEY_CLASSES_ROOT\Applications\photoviewer.dll][HKEY_CLASSES_ROOT\Applications\photoviewer.dll\shell][HKEY_CLASSES_ROOT…

没有root权限在linux安装python库

没有root权限在linux安装python库 # 安装兼容版本(绕过外部管理限制) pip3 install virtualenv==20.16.6 --user --break-system-packages ~/.local/bin/virtualenv ~/my_env# 激活虚拟环境(不变) source ~/my_e…

2025白酒品牌推荐:聚会必备气氛担当,7 款让酒桌升温的纯粮好酒

2025白酒品牌推荐:聚会必备气氛担当,7 款让酒桌升温的纯粮好酒周末约上三五好友撸串,冰啤酒喝着不过瘾,总想整两杯白酒助助兴;中秋全家团圆,老爸拿出珍藏的“好酒”,喝着却辣喉上头,怀疑是勾兑酒;商务宴请要撑…

linux之基于信号解决僵尸进程的写法

#include <signal.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h>void sigchld_handler(int signo) {// 回收所有退出的子进程while (waitpid(-1, NULL, WNOHANG) > 0)…

《ESP32-S3使用指南—IDF版 V1.6》第五十章 WiFi热点实验

第五十章 WiFi热点实验 1)实验平台:正点原子DNESP32S3开发板 2)章节摘自【正点原子】ESP32-S3使用指南—IDF版 V1.6 3)购买链接:https://detail.tmall.com/item.htm?&id=768499342659 4)全套实验源码+手册+…

各位大哥好

各位大哥好zq.zhaopin.Com/moment/83559851 zq.zhaopin.Com/moment/83559865 zq.zhaopin.Com/moment/83559869 zq.zhaopin.Com/moment/83559845 zq.zhaopin.Com/moment/83559846 zq.zhaopin.Com/moment/83559847 zq.zh…

【无标题】HIT-ICS2025计统大作业——程序人生 - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

mapvthree Engine 设计分析——二三维一体化的架构设计

深入分析 mapvthree Engine 的架构设计理念,探讨其如何融合地图引擎的 LBS GIS 能力与 3D 通用渲染引擎的设计思想,实现二三维一体化的创新架构。mapvthree Engine 作为二三维一体化渲染引擎的核心,其设计理念既不同…

eMMC, UFS,SATA,PCIe/NVMe

四者都是现代计算设备中常见的存储解决方案,但它们处于不同的层级和应用场景中。eMMC 和 UFS 是面向嵌入式设备(如手机、平板、低端笔记本、物联网设备)的存储芯片。SATA 和 PCIe/NVMe 是面向电脑、服务器和高性能设…