Semantic Kernel在AI领域的应用越来越广泛。然而,在使用Semantic Kernel时,如果不注意一些细节问题,可能会导致你的模型表现异常,甚至出现“胡说八道”的情况。今天,我将分享一个关于使用Semantic Kernel的小细节,这个问题曾让我一度陷入困惑,幸好最终找到了问题的根源。
问题背景
在我的一个项目中,我遇到了一个奇怪的问题:当我使用OpenAI时,模型表现非常智能,但是一旦切换到本地模型,输出结果就变得非常“弱智”。起初,我怀疑这是模型自身的问题,但通过使用Postman进行调试,发现并非如此。
为了排查问题,我决定从请求报文入手,使用HttpClientHandler进行请求拦截。这一步非常关键,揭示了问题的所在。
抓取请求报文
首先,我们通过如下代码来创建一个OpenAI的HttpClientHandler,用以拦截和修改请求:
var handler = new OpenAIHttpClientHandler();
services.AddTransient<Kernel>((serviceProvider) =>
{
if (_kernel == null)
{
_kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: OpenAIOption.ChatModel,
apiKey: OpenAIOption.Key,
httpClient: new HttpClient(handler)
)
.Build();
}
return _kernel;
});
然后,通过自定义的OpenAIHttpClientHandler
来拦截和修改请求内容。代码如下:
public class OpenAIHttpClientHandler : HttpClientHandler
{
private string _endPoint { get; set; }
public OpenAIHttpClientHandler(string endPoint)
{
this._endPoint = endPoint;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
UriBuilder uriBuilder;
Regex regex = new Regex(@"(https?)://([^/:]+)(:\d+)?/(.*)");
Match match = regex.Match(_endPoint);
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development" && request.Content != null)
{
string requestBody = await request.Content.ReadAsStringAsync();
//便于调试查看请求prompt
Log.Information(requestBody);
}
// Modify request based on endpoint
if (match.Success)
{
string xieyi = match.Groups[1].Value;
string host = match.Groups[2].Value;
string port = match.Groups[3].Value;
port = string.IsNullOrEmpty(port) ? port : port.Substring(1);
var hostnew = string.IsNullOrEmpty(port) ? host : $"{host}:{port}";
switch (request.RequestUri.LocalPath)
{
case "/v1/chat/completions":
uriBuilder = new UriBuilder(request.RequestUri)
{
Scheme = $"{xieyi}://{hostnew}/",
Host = host,
Path = $"{route}v1/chat/completions",
};
if (port.ConvertToInt32() != 0) uriBuilder.Port = Convert.ToInt32(port);
request.RequestUri = uriBuilder.Uri;
break;
case "/v1/embeddings":
uriBuilder = new UriBuilder(request.RequestUri)
{
Scheme = $"{xieyi}://{host}/",
Host = host,
Path = $"{route}v1/embeddings",
};
if (port.ConvertToInt32() != 0) uriBuilder.Port = Convert.ToInt32(port);
request.RequestUri = uriBuilder.Uri;
break;
}
}
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
return response;
}
}
发现问题:Unicode编码
通过拦截并查看请求报文内容,我发现请求中的内容并非直接的中文,而是Unicode编码。这对OpenAI服务而言,并不会影响识别和处理,但对本地模型来说,表现出明显的智力差异。
于是,我决定对Unicode编码进行解码,通过下面的方法来实现:
public static string Unescape(this string value)
{
if (value.IsNull()) return "";
try
{
Formatting formatting = Formatting.None;
object jsonObj = JsonConvert.DeserializeObject(value);
string unescapeValue = JsonConvert.SerializeObject(jsonObj, formatting);
return unescapeValue;
}
catch (Exception ex)
{
Log.Error(ex.ToString());
return "";
}
}
接下来,我们对HttpClientHandler
进行修改,以便在发送请求前进行解码:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
UriBuilder uriBuilder;
Regex regex = new Regex(@"(https?)://([^/:]+)(:\d+)?/(.*)");
Match match = regex.Match(_endPoint);
string guid = Guid.NewGuid().ToString();
var mediaType = request.Content.Headers.ContentType.MediaType;
string requestBody = (await request.Content.ReadAsStringAsync()).Unescape();
var uncaseBody = new StringContent(requestBody, Encoding.UTF8, mediaType);
request.Content = uncaseBody;
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "Production")
{
Log.Information("{Message}", $"【模型服务接口调用-{guid},host:{_endPoint}】: {Environment.NewLine}{requestBody}");
}
// Modify request based on endpoint
if (match.Success)
{
string xieyi = match.Groups[1].Value;
string host = match.Groups[2].Value;
string port = match.Groups[3].Value;
port = string.IsNullOrEmpty(port) ? port : port.Substring(1);
var hostnew = string.IsNullOrEmpty(port) ? host : $"{host}:{port}";
switch (request.RequestUri.LocalPath)
{
case "/v1/chat/completions":
uriBuilder = new UriBuilder(request.RequestUri)
{
Scheme = $"{xieyi}://{hostnew}/",
Host = host,
Path = $"{route}v1/chat/completions",
};
if (Convert.ToInt32(port) != 0) uriBuilder.Port = Convert.ToInt32(port);
request.RequestUri = uriBuilder.Uri;
break;
case "/v1/embeddings":
uriBuilder = new UriBuilder(request.RequestUri)
{
Scheme = $"{xieyi}://{host}/",
Host = host,
Path = $"{route}v1/embeddings",
};
if (Convert.ToInt32(port) != 0) uriBuilder.Port = Convert.ToInt32(port);
request.RequestUri = uriBuilder.Uri;
break;
}
}
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "Production")
{
string responseContent = response.Content.ReadAsStringAsync().Result.Unescape();
Log.Information("{Message}", $"【模型服务接口返回-{guid},host:{_endPoint}】: {Environment.NewLine}{responseContent}");
}
return response;
}
最终测试
经过以上调整后,我们再次验证请求内容,可以发现请求已经恢复为正常的中文编码,本地模型的表现也随之恢复正常。
接下来,我们来探究一下为什么Semantic Kernel在传输过程中会使用Unicode编码。目前我的猜测是,在使用Function Call时,函数参数可能包含各种不规则的JSON数据。这些数据在进行序列化和反序列化时可能会引发问题,因此,Semantic Kernel可能为了确保数据的完整性和一致性,选择将参数进行Unicode编码后再进行解码处理。这样一来可以减少因不规则JSON导致的错误。
总结
通过修改请求内容的编码方式,我们成功解决了由于Unicode编码导致本地模型表现不佳的问题。这次的经验告诉我,编码方式在AI模型中的影响不可忽视。希望通过本文的分享,能够帮助到遇到类似问题的开发者,提升你们使用Semantic Kernel的体验。
如果你有任何问题或想法,欢迎关注我的公众号加入我们的交流群,与我和其他开发者一起探讨更多技术细节。我们下期再见!