旁门左道:借助 HttpClientHandler 拦截请求,体验 Semantic Kernel 插件
前天尝试通过 one-api + dashscope(阿里云灵积) + qwen(通义千问)运行 Semantic Kernel 插件(Plugin) ,结果尝试失败,详见前天的博文。
今天换一种方式尝试,选择了一个旁门左道走走看,看能不能在不使用大模型的情况下让 Semantic Kernel 插件运行起来,这个旁门左道就是从 Stephen Toub 那偷学到的一招 —— 借助 DelegatingHandler(new HttpClientHandler()) 拦截 HttpClient 请求,直接以模拟数据进行响应。
先创建一个 .NET 控制台项目
dotnet new console
dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.Extensions.Http
参照 Semantic Kernel 源码中的示例代码创建一个非常简单的插件 LightPlugin
public class LightPlugin
{
public bool IsOn { get; set; } = false;
[KernelFunction]
[Description("帮看一下灯是开是关")]
public string GetState() => IsOn ? "on" : "off";
[KernelFunction]
[Description("开灯或者关灯")]
public string ChangeState(bool newState)
{
IsOn = newState;
var state = GetState();
Console.WriteLine(state == "on" ? $"[开灯啦]" : "[关灯咯]");
return state;
}
}
接着创建旁门左道 BackdoorHandler,先实现一个最简单的功能,打印 HttpClient 请求内容
public class BypassHandler() : DelegatingHandler(new HttpClientHandler())
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.WriteLine(await request.Content!.ReadAsStringAsync());
// return await base.SendAsync(request, cancellationToken);
return new HttpResponseMessage(HttpStatusCode.OK);
}
}
然后携 LightPlugin 与 BypassHandler 创建 Semantic Kernel 的 Kernel
var builder = Kernel.CreateBuilder();
builder.Services.AddOpenAIChatCompletion("qwen-max", "sk-xxxxxx");
builder.Services.ConfigureHttpClientDefaults(b =>
b.ConfigurePrimaryHttpMessageHandler(() => new BypassHandler()));
builder.Plugins.AddFromType<LightPlugin>();
Kernel kernel = builder.Build();
再然后,发送携带 prompt 的请求并获取响应内容
var history = new ChatHistory();
history.AddUserMessage("请开灯");
Console.WriteLine("User > " + history[0].Content);
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
// Enable auto function calling
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
};
var result = await chatCompletionService.GetChatMessageContentAsync(
history,
executionSettings: openAIPromptExecutionSettings,
kernel: kernel);
Console.WriteLine("Assistant > " + result);
运行控制台程序,BypassHandler 就会在控制台输出请求的 json 内容(为了阅读方便对json进行了格式化):
点击查看 json
{
"messages": [
{
"content": "Assistant is a large language model.",
"role": "system"
},
{
"content": "\u8BF7\u5F00\u706F",
"role": "user"
}
],
"temperature": 1,
"top_p": 1,
"n": 1,
"presence_penalty": 0,
"frequency_penalty": 0,
"model": "qwen-max",
"tools": [
{
"function": {
"name": "LightPlugin-GetState",
"description": "\u5E2E\u770B\u4E00\u4E0B\u706F\u662F\u5F00\u662F\u5173",
"parameters": {
"type": "object",
"required": [],
"properties": {}
}
},
"type": "function"
},
{
"function": {
"name": "LightPlugin-ChangeState",
"description": "\u5F00\u706F\u6216\u8005\u5173\u706F",
"parameters": {
"type": "object",
"required": [
"newState"
],
"properties": {
"newState": {
"type": "boolean"
}
}
}
},
"type": "function"
}
],
"tool_choice": "auto"
}
为了能反序列化这个 json ,我们需要定义一个类型 ChatCompletionRequest,Sermantic Kernel 中没有现成可以使用的,实现代码如下:
点击查看 ChatCompletionRequest
public class ChatCompletionRequest
{
[JsonPropertyName("messages")]
public IReadOnlyList<RequestMessage>? Messages { get; set; }
[JsonPropertyName("temperature")]
public double Temperature { get; set; } = 1;
[JsonPropertyName("top_p")]
public double TopP { get; set; } = 1;
[JsonPropertyName("n")]
public int? N { get; set; } = 1;
[JsonPropertyName("presence_penalty")]
public double PresencePenalty { get; set; } = 0;
[JsonPropertyName("frequency_penalty")]
public double FrequencyPenalty { get; set; } = 0;
[JsonPropertyName("model")]
public required string Model { get; set; }
[JsonPropertyName("tools")]
public IReadOnlyList<Tool>? Tools { get; set; }
[JsonPropertyName("tool_choice")]
public string? ToolChoice { get; set; }
}
public class RequestMessage
{
[JsonPropertyName("role")]
public string? Role { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("content")]
public string? Content { get; set; }
}
public class Tool
{
[JsonPropertyName("function")]
public FunctionDefinition? Function { get; set; }
[JsonPropertyName("type")]
public string? Type { get; set; }
}
public class FunctionDefinition
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("parameters")]
public ParameterDefinition Parameters { get; set; }
public struct ParameterDefinition
{
[JsonPropertyName("type")]
public required string Type { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("required")]
public string[]? Required { get; set; }
[JsonPropertyName("properties")]
public Dictionary<string, PropertyDefinition>? Properties { get; set; }
public struct PropertyDefinition
{
[JsonPropertyName("type")]
public required PropertyType Type { get; set; }
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum PropertyType
{
Number,
String,
Boolean
}
}
}
有了这个类,我们就可以从请求中获取对应 Plugin 的 function 信息,比如下面的代码:
var function = chatCompletionRequest?.Tools.FirstOrDefault(x => x.Function.Description.Contains("开灯"))?.Function;
var functionName = function.Name;
var parameterName = function.Parameters.Properties.FirstOrDefault(x => x.Value.Type == PropertyType.Boolean).Key;
接下来就是旁门左道的关键,直接在 BypassHandler 中响应 Semantic Kernel 通过 OpenAI.ClientCore 发出的 http 请求。
首先创建用于 json 序列化的类 ChatCompletionResponse:
点击查看 ChatCompletionResponse
public class ChatCompletionResponse
{
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("object")]
public string? Object { get; set; }
[JsonPropertyName("created")]
public long Created { get; set; }
[JsonPropertyName("model")]
public string? Model { get; set; }
[JsonPropertyName("usage"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Usage? Usage { get; set; }
[JsonPropertyName("choices")]
public List<Choice>? Choices { get; set; }
}
public class Choice
{
[JsonPropertyName("message")]
public ResponseMessage? Message { get; set; }
/// <summary>
/// The message in this response (when streaming a response).
/// </summary>
[JsonPropertyName("delta"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ResponseMessage? Delta { get; set; }
[JsonPropertyName("finish_reason")]
public string? FinishReason { get; set; }
/// <summary>
/// The index of this response in the array of choices.
/// </summary>
[JsonPropertyName("index")]
public int Index { get; set; }
}
public class ResponseMessage
{
[JsonPropertyName("role")]
public string? Role { get; set; }
[JsonPropertyName("name"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Name { get; set; }
[JsonPropertyName("content"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Content { get; set; }
[JsonPropertyName("tool_calls")]
public IReadOnlyList<ToolCall>? ToolCalls { get; set; }
}
public class ToolCall
{
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("function")]
public FunctionCall? Function { get; set; }
[JsonPropertyName("type")]
public string? Type { get; set; }
}
public class Usage
{
[JsonPropertyName("prompt_tokens")]
public int PromptTokens { get; set; }
[JsonPropertyName("completion_tokens")]
public int CompletionTokens { get; set; }
[JsonPropertyName("total_tokens")]
public int TotalTokens { get; set; }
}
public class FunctionCall
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("arguments")]
public string Arguments { get; set; } = string.Empty;
}
先试试不执行 function calling ,直接以 assistant 角色回复一句话
public class BypassHandler() : DelegatingHandler(new HttpClientHandler())
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var chatCompletion = new ChatCompletionResponse
{
Id = Guid.NewGuid().ToString(),
Model = "fake-mode",
Object = "chat.completion",
Created = DateTimeOffset.Now.ToUnixTimeSeconds(),
Choices =
[
new()
{
Message = new ResponseMessage
{
Content = "自己动手,丰衣足食",
Role = "assistant"
},
FinishReason = "stop"
}
]
};
var json = JsonSerializer.Serialize(chatCompletion, GetJsonSerializerOptions());
return new HttpResponseMessage
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
}
}
运行控制台程序,输出如下:
User > 请开灯
Assistant > 自己动手,丰衣足食
成功响应,到此,旁门左道成功了一半。
接下来在之前创建的 chatCompletion 基础上添加针对 function calling 的 ToolCall 部分。
先准备好 ChangeState(bool newState) 的参数值
Dictionary<string, bool> arguments = new()
{
{ parameterName, true }
};
并将回复内容由 "自己动手,丰衣足食" 改为 "客官,灯已开"
Message = new ResponseMessage
{
Content = "客官,灯已开",
Role = "assistant"
}
然后为 chatCompletion 创建 ToolCalls 实例用于响应 function calling
var messages = chatCompletionRequest.Messages;
if (messages.First(x => x.Role == "user").Content.Contains("开灯") == true)
{
chatCompletion.Choices[0].Message.ToolCalls = new List<ToolCall>()
{
new ToolCall
{
Id = Guid.NewGuid().ToString(),
Type = "function",
Function = new FunctionCall
{
Name = function.Name,
Arguments = JsonSerializer.Serialize(arguments, GetJsonSerializerOptions())
}
}
};
}
运行控制台程序看看效果
User > 请开灯
[开灯啦]
[开灯啦]
[开灯啦]
[开灯啦]
[开灯啦]
Assistant > 客官,灯已开
耶!成功开灯!但是,竟然开了5次,差点把灯给开爆了。
在 BypassHandler 中打印一下请求内容看看哪里出了问题
var json = await request.Content!.ReadAsStringAsync();
Console.WriteLine(json);
原来分别请求/响应了5次,第2次请求开始,json 中 messages 部分多了 tool_calls 与 tool_call_id 内容
{
"messages": [
{
"content": "\u5BA2\u5B98\uFF0C\u706F\u5DF2\u5F00",
"tool_calls": [
{
"function": {
"name": "LightPlugin-ChangeState",
"arguments": "{\u0022newState\u0022:true}"
},
"type": "function",
"id": "76f8dead-b5ad-4e6d-b343-7f78d68fac8e"
}
],
"role": "assistant"
},
{
"content": "on",
"tool_call_id": "76f8dead-b5ad-4e6d-b343-7f78d68fac8e",
"role": "tool"
}
]
}
这时恍然大悟,之前 AI assistant 对 function calling 的响应只是让 Plugin 执行对应的 function,assistant 还需要根据执行的结果决定下一下做什么,第2次请求中的 tool_calls 与 tool_call_id 就是为了告诉 assistant 执行的结果,所以,还需要针对这个请求进行专门的响应。
到了旁门左道最后100米冲刺的时刻!
给 RequestMessage 添加 ToolCallId 属性
public class RequestMessage
{
[JsonPropertyName("role")]
public string? Role { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("content")]
public string? Content { get; set; }
[JsonPropertyName("tool_call_id")]
public string? ToolCallId { get; set; }
}
在 BypassHandler 中响应时判断一下 ToolCallId,如果是针对 Plugin 的 function 执行结果的请求,只返回 Message.Content,不进行 function calling 响应
var messages = chatCompletionRequest.Messages;
var toolCallId = "76f8dead- b5ad-4e6d-b343-7f78d68fac8e";
var toolCallIdMessage = messages.FirstOrDefault(x => x.Role == "tool" && x.ToolCallId == toolCallId);
if (toolCallIdMessage != null && toolCallIdMessage.Content == "on")
{
chatCompletion.Choices[0].Message.Content = "客官,灯已开";
}
else if (messages.First(x => x.Role == "user").Content.Contains("开灯") == true)
{
chatCompletion.Choices[0].Message.Content = "";
//..
}
改进代码完成,到了最后10米冲刺的时刻,再次运行控制台程序
User > 请开灯
[开灯啦]
Assistant > 客官,灯已开
只有一次开灯,冲刺成功,旁门左道走通,用这种方式体验一下 Semantic Kernel Plugin,也别有一番风味。
完整示例代码已上传到 github https://github.com/cnblogs-dudu/sk-plugin-sample-101
旁门左道:借助 HttpClientHandler 拦截请求,体验 Semantic Kernel 插件的更多相关文章
- Semantic Kernel 知多少 | 开启面向AI编程新篇章
引言 在ChatGPT 火热的当下, 即使没有上手亲自体验,想必也对ChatGPT的强大略有耳闻.当一些人在对ChatGPT犹犹豫豫之时,一些敏锐的企业主和开发者们已经急不可耐的开展基于ChatGPT ...
- Semantic Kernel 入门系列:💾Native Function
语义的归语义,语法的归语法. 基础定义 最基本的Native Function定义只需要在方法上添加 SKFunction 的特性即可. using Microsoft.SemanticKernel. ...
- Semantic Kernel 入门系列:📅 Planner 计划管理
Semantic Kernel 的一个核心能力就是实现"目标导向"的AI应用. 目标导向 "目标导向"听起来是一个比较高大的词,但是却是实际生活中我们处理问题的 ...
- LangChain vs Semantic Kernel
每当向他人介绍 Semantic Kernel, 会得到的第一个问题就是 Semantic Kernel 类似于LangChain吗,或者是c# 版本的LangChain吗? 为了全面而不想重复的回答 ...
- webapi拦截请求
[AttributeUsage(AttributeTargets.Method)] public class WebApiSensitive : ActionFilterAttribute { pub ...
- Asp.Net 拦截请求自定义处理
需求: 在Aps.Net 应用中,对于浏览器请求的部分url的地址自定义处理,不交给路由系统或页面. 解决方案: 在全局文件Global.asax中 ,提供Application_BeginReque ...
- .net core MVC 通过 Filters 过滤器拦截请求及响应内容
前提: 需要nuget Microsoft.Extensions.Logging.Log4Net.AspNetCore 2.2.6: Swashbuckle.AspNetCore 我暂时用的是 ...
- Charles拦截请求
一.通过Charles抓包,可拦截请求并篡改交互信息 1.可篡改客户端向服务器发起的请求信息(服务器收到的是假消息) 2.可篡改服务器返回给客户端的响应结果(客户端看到的是假消息) 二.篡改用户请求 ...
- springmvc拦截请求
springmvc.xml <!--拦截请求 --> <mvc:interceptors> <mvc:interceptor> <!-- 要拦截的请求类型 / ...
- 使用框架时,在web.xml中配置servlet时,拦截请求/和/*的区别。
关于servlet的拦截设置,之前看了好多,说的都不太清除,明白. 最近明白了一些,总的来说就是保证拦截所有用户请求的同时,放行静态资源. 现整理如下: 一.我们都知道在基于Spring的Applic ...
随机推荐
- Autowired注入Service变成了biaomidou的Mapper代理
问题概述 一个Springboot工程,使用Mybatis-plus作为数据层框架 使用@MapperScan注解扫描Mapper接口 @MapperScan("org.net5ijy.cl ...
- linux-网络状态-netstat
- [转帖]技术分享 | 国产麒麟 arm 上编译安装 xtrabackup8
原创 发布于 2022-07-19 13:29:29 3220 举报 作者:王向 爱可生 DBA 团队成员,负责公司 DMP 产品的运维和客户 MySQL 问题的处理.擅长数据库故障处理.对数据库技术 ...
- RPM安装的Oracle19c 修改init.ora进行修复以及最简单开机启动Oracle的方法
RPM安装的Oracle19c 修改init.ora进行修复以及最简单开机启动Oracle的方法 背景 今天开始使用自己心的ThinkBook14 的笔记本 因为已经没有了 Linux测试环境供我使用 ...
- [转帖]MySQL InnoDB存储引擎大观
https://baijiahao.baidu.com/s?id=1709263187856706948&wfr=spider&for=pc MySQL InnoDB 引擎现在广为 ...
- [转帖]人人都应该知道的CPU缓存运行效率
https://zhuanlan.zhihu.com/p/628017496 提到CPU性能,大部分同学想到的都是CPU利用率,这个指标确实应该首先被关注.但是除了利用率之外,还有很容易被人忽视的指标 ...
- 真·Android Zxing 扫码中文乱码解决
Zxing3.2.1 之前遇到过Android Zxing扫码乱码的问题,网上搜了下解决了下. 今天又遇到了问题. 依然会乱码. 研究总结如下: Zxing 可以在Hints中添加默认编码格式.这个编 ...
- 往返回来的数据数组Array中添加一个字段的最优写法
在工作中我们经常会对后端返回来的数据进行添加一个字段: 最优的写法是 直接在 res.data[i].xx=aa 这样的方式去添加: 添加好了之后美酒 可以去赋值了: 让表格去渲染数据 this.$a ...
- el-popover 点击取消按钮,弹窗仍然无法关闭
<el-popover placement="bottom" width="200" :ref="aa" :visible.sync= ...
- 【Spring】Bean装配类注解
默认注入类型 多个同类型Bean 注入失败 依赖 使用目标(@Target) @AutoWired 通过type 默认首先安装属性名称进行注入 结合@Qualifier注解(value属性)设置注入的 ...