旁门左道:借助 HttpClientHandler 拦截请求,体验 Semantic Kernel 插件

这篇具有很好参考价值的文章主要介绍了旁门左道:借助 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);
    }
}

然后携 LightPluginBypassHandler 创建 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_callstool_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_callstool_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文章来源地址https://www.toymoban.com/news/detail-827648.html

到了这里,关于旁门左道:借助 HttpClientHandler 拦截请求,体验 Semantic Kernel 插件的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • VUE3 请求拦截器 响应拦截器

    1,导入axios  (使用axios进行接口的请求,页面发送http请求,很多情况我们要对请求和其响应进行特定的处理,如:判断token,设置请求头。如果请求数非常多,单独对每一个请求进行处理会变得非常麻烦,程序的优雅性也会大打折扣。所以axios为开发者提供了这样一个API:拦

    2024年02月16日
    浏览(47)
  • vue请求拦截统一给所有请求加loading

    需求:在项目开发过程中通常会给请求加loading,每次发请求单独加loading费事费力。 思路:用h5做手机端项目,业务简单,统一封装loading,由于vue组件化开发,通常是用ajax封装后的axios插件进行,在请求拦截里面可以加loading,接口返回拦截器里面关闭loading。 具体实现过程

    2024年01月23日
    浏览(30)
  • XMLHttpRequest拦截请求和响应

    环境: angular 实现: 拦截请求 向请求信息增加字段             拦截响应 过滤返回值 响应拦截: 根据angular使用的XMLHttpRequest 将对原本的请求转移到另一个将监听返回事件挂载到另一个世纪发送请求的xml上 使用get set 将客户端获取的responseText和response按照自己的意愿返

    2024年02月07日
    浏览(33)
  • Taro+VantUI请求拦截处理

    请求拦截处理 备注: CONF.baseUrl 需要在config-index.js文件中进行配置 代码如下:

    2024年02月12日
    浏览(26)
  • axios 请求和响应拦截器

    1. 创建实例 使用 axios.create() 使用自定义配置创建一个 axios 实例。 2. 拦截器 在请求或响应被 then 或者 catch 处理前拦截他们,拦截分为请求拦截和响应拦截。 2.1 request 拦截器,全局添加市场信息 removeMarketCode 是否移除市场信息,默认不移除; 根据上述代码可以看到,市场信

    2024年02月09日
    浏览(39)
  • Feign请求及响应拦截器

    feign请求拦截,处理head、param、body参数,附加解密定制化处理,也可以使用原生解码器; feign响应拦截,处理head、param、body参数,附加解密定制化处理,也可以使用原生解码器; 附件加解密工具(支持格式化rn、rt)等格式化代码加解密,五年验证品质保证

    2024年02月11日
    浏览(45)
  • Burp Suite如何拦截站点请求

    Burp Suite是一款强大的Web渗透测试工具,可以用于拦截、修改和分析Web应用程序的请求和响应。要使用Burp Suite拦截站点请求有两个方案。我会倾向选用方案二,因为它不会影响本地电脑代理配置。 安装Burp Suite:首先,您需要在您的计算机上安装Burp Suite社区版本,可以访问h

    2024年01月18日
    浏览(33)
  • 使用Postman拦截浏览器请求

    项目上线之后,难免会有BUG。在出现问题的时候,我们可能需要获取前端页面发送请求的数据,然后在测试环境发送相同的数据将问题复现。手动构建数据是挺麻烦的一件事,所以我们可以借助Postman在浏览器上的插件帮助拦截请求,获取发送的数据。 既然是基于Postman进行操

    2024年02月15日
    浏览(33)
  • 登录用户信息获取 网关+拦截器+feign请求添加请求头

    给所有请求添加用户身份 网关已经给所有请求添加了用户身份,也就是authorization头信息。   创建ThreadLocal工具类 : 创建拦截器:  将拦截器注册到SpringMvc,让它生效:  将以上代码(拦截器,config,utils) 放到哪个微服务中,哪个微服务/**路径就会有拦截功能 没有用户信息的请求将会

    2024年02月09日
    浏览(44)
  • mitmproxy 抓包神器-4.拦截请求实现篡改请求和返回数据

    fiddler 工具有个打断点功能非常实用,可以实现拦截请求,篡改请求和返回的数据。 mitmproxy 可以用python代码写插件的方式实现拦截请求,篡改请求和返回数据。 before response:这个是打在request请求的时候,未到达服务器之前 after response:也就是服务器响应之后,在Fiddler将响应

    2024年02月11日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包