前天嘗試通過(guò) one-api
+ dashscope(阿里云靈積)
+ qwen(通義千問(wèn))
運(yùn)行 Semantic Kernel 插件(Plugin) ,結(jié)果嘗試失敗,詳見(jiàn)前天的博文。
今天換一種方式嘗試,選擇了一個(gè)旁門(mén)左道走走看,看能不能在不使用大模型的情況下讓 Semantic Kernel 插件運(yùn)行起來(lái),這個(gè)旁門(mén)左道就是從 Stephen Toub 那偷學(xué)到的一招 —— 借助 DelegatingHandler(new HttpClientHandler())
攔截 HttpClient 請(qǐng)求,直接以模擬數(shù)據(jù)進(jìn)行響應(yīng)。
先創(chuàng)建一個(gè) .NET 控制臺(tái)項(xiàng)目
dotnet new console
dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.Extensions.Http
參照 Semantic Kernel 源碼中的示例代碼創(chuàng)建一個(gè)非常簡(jiǎn)單的插件 LightPlugin
public class LightPlugin
{
public bool IsOn { get; set; } = false;
[KernelFunction]
[Description("幫看一下燈是開(kāi)是關(guān)")]
public string GetState() => IsOn ? "on" : "off";
[KernelFunction]
[Description("開(kāi)燈或者關(guān)燈")]
public string ChangeState(bool newState)
{
IsOn = newState;
var state = GetState();
Console.WriteLine(state == "on" ? $"[開(kāi)燈啦]" : "[關(guān)燈咯]");
return state;
}
}
接著創(chuàng)建旁門(mén)左道 BackdoorHandler
,先實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的功能,打印 HttpClient 請(qǐng)求內(nèi)容
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
創(chuàng)建 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();
再然后,發(fā)送攜帶 prompt 的請(qǐng)求并獲取響應(yīng)內(nèi)容
var history = new ChatHistory();
history.AddUserMessage("請(qǐng)開(kāi)燈");
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);
運(yùn)行控制臺(tái)程序,BypassHandler
就會(huì)在控制臺(tái)輸出請(qǐng)求的 json 內(nèi)容(為了閱讀方便對(duì)json進(jìn)行了格式化):
點(diǎn)擊查看 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"
}
為了能反序列化這個(gè) json ,我們需要定義一個(gè)類型 ChatCompletionRequest
,Sermantic Kernel 中沒(méi)有現(xiàn)成可以使用的,實(shí)現(xiàn)代碼如下:
點(diǎn)擊查看 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
}
}
}
有了這個(gè)類,我們就可以從請(qǐng)求中獲取對(duì)應(yīng) Plugin 的 function 信息,比如下面的代碼:
var function = chatCompletionRequest?.Tools.FirstOrDefault(x => x.Function.Description.Contains("開(kāi)燈"))?.Function;
var functionName = function.Name;
var parameterName = function.Parameters.Properties.FirstOrDefault(x => x.Value.Type == PropertyType.Boolean).Key;
接下來(lái)就是旁門(mén)左道的關(guān)鍵,直接在 BypassHandler
中響應(yīng) Semantic Kernel 通過(guò) OpenAI.ClientCore
發(fā)出的 http 請(qǐng)求。
首先創(chuàng)建用于 json 序列化的類 ChatCompletionResponse
:
點(diǎn)擊查看 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;
}
先試試不執(zhí)行 function calling ,直接以 assistant
角色回復(fù)一句話
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 = "自己動(dòng)手,豐衣足食",
Role = "assistant"
},
FinishReason = "stop"
}
]
};
var json = JsonSerializer.Serialize(chatCompletion, GetJsonSerializerOptions());
return new HttpResponseMessage
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
}
}
運(yùn)行控制臺(tái)程序,輸出如下:
User > 請(qǐng)開(kāi)燈
Assistant > 自己動(dòng)手,豐衣足食
成功響應(yīng),到此,旁門(mén)左道成功了一半。
接下來(lái)在之前創(chuàng)建的 chatCompletion
基礎(chǔ)上添加針對(duì) function calling 的 ToolCall
部分。
先準(zhǔn)備好 ChangeState(bool newState)
的參數(shù)值
Dictionary<string, bool> arguments = new()
{
{ parameterName, true }
};
并將回復(fù)內(nèi)容由 "自己動(dòng)手,豐衣足食"
改為 "客官,燈已開(kāi)"
Message = new ResponseMessage
{
Content = "客官,燈已開(kāi)",
Role = "assistant"
}
然后為 chatCompletion
創(chuàng)建 ToolCalls
實(shí)例用于響應(yīng) function calling
var messages = chatCompletionRequest.Messages;
if (messages.First(x => x.Role == "user").Content.Contains("開(kāi)燈") == 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())
}
}
};
}
運(yùn)行控制臺(tái)程序看看效果
User > 請(qǐng)開(kāi)燈
[開(kāi)燈啦]
[開(kāi)燈啦]
[開(kāi)燈啦]
[開(kāi)燈啦]
[開(kāi)燈啦]
Assistant > 客官,燈已開(kāi)
耶!成功開(kāi)燈!但是,竟然開(kāi)了5次,差點(diǎn)把燈給開(kāi)爆了。
在 BypassHandler
中打印一下請(qǐng)求內(nèi)容看看哪里出了問(wèn)題
var json = await request.Content!.ReadAsStringAsync();
Console.WriteLine(json);
原來(lái)分別請(qǐng)求/響應(yīng)了5次,第2次請(qǐng)求開(kāi)始,json 中 messages
部分多了 tool_calls
與 tool_call_id
內(nèi)容
{
"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"
}
]
}
這時(shí)恍然大悟,之前 AI assistant 對(duì) function calling 的響應(yīng)只是讓 Plugin 執(zhí)行對(duì)應(yīng)的 function,assistant 還需要根據(jù)執(zhí)行的結(jié)果決定下一下做什么,第2次請(qǐng)求中的 tool_calls
與 tool_call_id
就是為了告訴 assistant 執(zhí)行的結(jié)果,所以,還需要針對(duì)這個(gè)請(qǐng)求進(jìn)行專門(mén)的響應(yīng)。
到了旁門(mén)左道最后100米沖刺的時(shí)刻!
給 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
中響應(yīng)時(shí)判斷一下 ToolCallId
,如果是針對(duì) Plugin 的 function 執(zhí)行結(jié)果的請(qǐng)求,只返回 Message.Content
,不進(jìn)行 function calling 響應(yīng)
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 = "客官,燈已開(kāi)";
}
else if (messages.First(x => x.Role == "user").Content.Contains("開(kāi)燈") == true)
{
chatCompletion.Choices[0].Message.Content = "";
//..
}
改進(jìn)代碼完成,到了最后10米沖刺的時(shí)刻,再次運(yùn)行控制臺(tái)程序
User > 請(qǐng)開(kāi)燈
[開(kāi)燈啦]
Assistant > 客官,燈已開(kāi)
只有一次開(kāi)燈,沖刺成功,旁門(mén)左道走通,用這種方式體驗(yàn)一下 Semantic Kernel Plugin,也別有一番風(fēng)味。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-827648.html
完整示例代碼已上傳到 github https://github.com/cnblogs-dudu/sk-plugin-sample-101文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-827648.html
到了這里,關(guān)于旁門(mén)左道:借助 HttpClientHandler 攔截請(qǐng)求,體驗(yàn) Semantic Kernel 插件的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!