书接上回,自从重金购买了学习AI的设备后,总得找点事情干! 这不,就有个小需求,需要训练私域数据,那么我们就从Anything LLM的API封装做起。在当今快速发展的人工智能领域,大型语言模型(LLM)已成为企业应用的重要组成部分。Anything LLM作为一款功能强大的开源LLM管理工具,提供了丰富的API接口,使开发者能够轻松集成AI能力到自己的应用中。本文将详细介绍如何使用最新的.NET 9框架对Anything LLM API进行封装,构建一个高效、可靠的SDK,以及避坑指南。摘要:书接上回,自从重金购买了学习AI的设备后,总得找点事情干! 这不,就有个小需求,需要训练私域数据,那么我们就从Anything LLM的API封装做起。在当今快速发展的人工智能领域,大型语言模型(LLM)已成为企业应用的重要组成部分。Anything LLM作
Anything LLM是一个全功能的LLM管理平台,它允许开发者:
管理和部署多种大型语言模型;
创建和管理知识库 ;
构建对话式AI应用;
实现文档检索和问答系统
我们这里就以其为训练的基础,开启.net 9编写LLM接口的辉煌。
其分为:
授权
管理接口
文档接口,重要的资料,私域数据等等
工作空间,可以按使用用户隔离出来的私有空间
系统配置,配置参数等操作
空间线程,类似于可以在一个空间启动多个聊天窗口
用户管理
兼容OpenAI的接口
嵌入式文档接口,同文档接口,也是兼容接口之一。
{
// HTTP客户端和配置
privatereadonlyHttpClient _HttpClient;
privatereadonlyJSONSerializerOptions _jsonSerializerOptions;
privatereadonlyJsonSerializerOptions _jsonDeserializerOptions;
privatereadonlyILogger_logger;
// 服务模块,暂时实现这么多,后续可以扩展实现
publicAuthenticationService Authentication {get;}
publicAdminService Admin {get;}
publicDocumentsService Documents {get;}
publicWorkspacesService Workspaces {get;}
}
2.2 模块化设计
这里将API功能划分为几个服务模块:
AuthenticationService:处理认证相关操作
AdminService:管理系统设置和配置
DocumentsService:管理文档和知识库
WorkspacesService:管理工作空间和对话
按照这种模块化设计提高了代码的可维护性和可扩展性,使用起来手感也略有提升。
3.1 初始化配置publicAnythingLLMClient(string baseUrl,string apiKey,ILoggerlogger,HttpClient httpClient =){
this._logger = logger;
_httpClient = httpClient ??newHttpClient;
_httpClient.BaseAddress =newUri(baseUrl);
_httpClient.DefaultRequestHeaders.Authorization =
newAuthenticationHeaderValue("Bearer", apiKey);
// 配置语言首选项
_httpClient.DefaultRequestHeaders.AcceptLanguage.Clear;
_httpClient.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-GB,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,en-US;q=0.6");
// JSON序列化配置
_jsonSerializerOptions =newJsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive =false
WriteIndented =false
};
_jsonDeserializerOptions =newJsonSerializerOptions
{
false
WriteIndented =false
};
// 初始化服务模块
Authentication =newAuthenticationService(this);
Admin =newAdminService(this);
Documents =newDocumentsService(this);
Workspaces =newWorkspacesService(this);
// 注册编码提供程序
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
3.2 HTTP请求处理
我们实现了四种基本的HTTP方法:
GetAsync(string endpoint){
var response =await _httpClient.GetAsync(endpoint);
returnawaitHandleResponse(response);
}
internalasyncTaskPostAsync(string endpoint,object data =)
{
var content = data !=
? JsonContent.Create(data,options: _jsonSerializerOptions)
:;
var response =await _httpClient.PostAsync(endpoint, content);
returnawaitHandleResponse(string endpoint,MultipartFormDataContent content)
{
var response =await _httpClient.PostAsync(endpoint, content);
returnawaitHandleResponseDeleteAsync(string endpoint)
{
var response =await _httpClient.DeleteAsync(endpoint);
returnawaitHandleResponse(response);
}
3.3 响应处理
统一的响应处理机制确保了错误的一致性和可追踪性:
HandleResponse(HttpResponseMessage response){
if(!response.IsSuccessStatusCode)
{
var error =await response.Content.ReadAsStringAsync;
this._logger.LogError($"LLM: 访问 {response.RequestMessage.Tostring}失败 {response.StatusCode}, {error}");
thrownewAnythingLLMApiException(
$"API request failed: {response.StatusCode} - {error}",
(int)response.StatusCode);
}
var info =await response.Content.ReadAsStringAsync;
this._logger.LogInformation($"LLM: 访问 {response.RequestMessage.ToString},返回信息 {response.StatusCode}, {info}");
return JsonSerializer.Deserialize(info, _jsonDeserializerOptions);
}
publicclassAuthenticationService
{
privatereadonlyAnythingLLMClient _client;
publicAuthenticationService(AnythingLLMClient client)
{
_client = client;
}
publicasyncTaskLogin(string username,string password)
{
var request =new{ username, password };
returnawait _client.PostAsync("api/auth/login", request);
}
publicasyncTaskLogout
{
returnawait _client.DeleteAsync("api/auth/logout");
}
}
4.2 文档服务(DocumentsService)publicclassDocumentsService
{
privatereadonlyAnythingLLMClient _client;
publicDocumentsService(AnythingLLMClient client)
{
_client = client;
}
publicasyncTaskUploadDocument(string filePath,string workspaceId)
{
if(!File.Exists(filePath))
thrownewFileNotFoundException("文件不存在", filePath);
usingvar content =newMultipartFormDataContent;
usingvar fileStream = File.OpenRead(filePath);
usingvar streamContent =newStreamContent(fileStream);
string fileName = Path.GetFileName(filePath);
var contentDisposition =newContentDispositionHeaderValue("form-data")
{
Name ="\"file\"",
FileName ="\""+ fileName +"\"",
FileNameStar = fileName
};
streamContent.Headers.ContentDisposition = contentDisposition;
content.Add(streamContent,"file");
content.Add(newStringContent(workspaceId),"workspaceId");
returnawait _client.PostMultipartAsync("api/documents", content);
}
publicasyncTask>GetDocuments(string workspaceId)
{
returnawait _client.GetAsync>($"api/documents?workspaceId={workspaceId}");
}
}
尝试了多种编码,Anything LLM的API就是不给力,一直返回错误Invalid file upload. NOENT: no such file or directory, open 'C:\Users\Administrator\AppData\Roaming\anythingllm-desktop\storage\hotdir\???\‘,经过仔细查阅github,发现这个问题在早期版本已经解决,但是.net就是无法正常传递中文文件名。
经过查阅源码,发现如下代码:
filename:function(req, file, cb){file.originalname = Buffer.from(file.originalname,"latin1").toString(
"utf8"
);
// Set origin for watching
if(
req.headers.hasOwnProperty("x-file-origin")&&
typeof req.headers["x-file-origin"]==="string"
)
file.localPath =decodeURI(req.headers["x-file-origin"]);
cb(, file.originalname);
},
针对中文文件名问题,可以看到Anything LLM官方的处理方法如下:
Buffer.from(file.originalname, "latin1").toString("utf8")作用:将上传文件名从 Latin1(ISO-8859-1)编码转换成 UTF-8。
原因:部分浏览器或客户端上传中文文件名时编码成 Latin1,在服务器上会显示乱码。
req.headers["x-file-origin"]
这是一个自定义的 HTTP 请求头(如你前端上传时手动加的),用于携带文件的来源路径(或原始目录等信息)。
file.localPath = decodeURI(...):把这个头的值(可能包含中文路径)解码回来,存入 file.localPath 字段供后续使用。
cb(, file.originalname)cb 是 Multer 内部用来异步设置文件名的回调函数。
有了以上服务器的处理方式,那么我们就可以针对这个方式进行编码了。
由于 .NET 会自动将 FileName 的值编码为符合 MIME 标准的 ASCII-safe 字符串,并且如果包含非 ASCII 字符(如中文),将被编码成 MIME encoded-word 格式; 例如:
filename="=?UTF-8?B?5paH5Lu2LnR4dA==?=" 总体而言,这是一种 Base64 编码的 UTF-8 字符串,用来避免非 ASCII 直接放在 HTTP 头中;但是,但是,许多后端框架(如 Node.js 的 Multer)不会解析这种 MIME encoded-word 格式,导致 file.originalname 变成乱码。因此,我们进行手工编码,如下:
usingMultipartFormDataContent content =newMultipartFormDataContent;var addToWorkspacesContent =newByteArrayContent(
Encoding.UTF8.GetBytes(request.AddToWorkspaces ??"")// 关键点:使用ASCII编码避免UTF-8标记
);
addToWorkspacesContent.Headers.Remove("Content-Type");;// 不设置Content-Type,避免影响多部分表单数据的处理
content.Add(addToWorkspacesContent,"addToWorkspaces");
usingvar fileStream =newFileStream(request.FilePath, FileMode.Open, FileAccess.Read);
usingvar streamContent =newStreamContent(fileStream);
streamContent.Headers.ContentType =newMediaTypeHeaderValue(GetMimeType(request.FilePath));
var fileName = Path.GetFileName(request.FilePath);
// 先将文件名编码成 UTF-8 字节
var utf8Bytes = Encoding.UTF8.GetBytes(fileName);
// 再将 UTF-8 字节强制“解读”为 Latin1 字符串(每个字节变成 Latin1 字符)
var latin1String = Encoding.GetEncoding("ISO-8859-1").GetString(utf8Bytes);
var contentDisposition =$"form-data; name=\"file\"; filename=\"{latin1String}\"";
streamContent.Headers.TryAddWithoutValidation("Content-Disposition", contentDisposition);
content.Add(streamContent);
var encodedOriginPath = Uri.EscapeUriString(request.FilePath);// 对路径进行 URI 编码
content.Headers.Add("x-file-origin", encodedOriginPath);
returnawait _client.PostMultipartAsync(
BuildEndpoint("v1","document","upload"), content);
这样处理后,终于可以愉快的支持中文了。
[Fact]publicasyncTaskGetDocuments_ShouldReturnDocuments
{
// 准备
var mockHttp =newMock;
var expectedDocuments =newList{newDocument{ Id ="1", Name ="Test"}};
mockHttp.Protected
.Setup>(
"SendAsync",
ItExpr.IsAny,
ItExpr.IsAny)
.ReturnsAsync(newHttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content =newStringContent(JsonSerializer.Serialize(expectedDocuments))
});
var client =newHttpClient(mockHttp.Object);
var llmClient =newAnythingLLMClient("http://test.com","api-key", Mock.Of>, client);
// 执行
var result =await llmClient.Documents.GetDocuments("workspace1");
// 断言
Assert.Single(result);
Assert.Equal("Test", result[0 ].Name);
}
有压力就有动力,一旦出手了,就得努力进行学习和沉淀。
目前这个SDK提供了对Anything LLM API的完整封装,后续就可以利用该SDK进行二次开发,充分利用大型语言模型的强大能力。
—— 风已起,你是否准备好迎接这场AI革命? 🌪️🚀
你学废了吗?
👓都收藏了,还在乎一个评论吗?
来源:opendotnet