- 📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!
- 📢本文作者:由webmote 原创
- 📢作者格言:新的征程,用爱发电,去丈量人心,是否能达到人机合一?
序言
书接上回,自从重金 购买了学习AI的设备后,总得找点事情干! 这不,就有个小需求,需要训练私域数据,那么我们就从Anything LLM
的API封装做起。在当今快速发展的人工智能领域,大型语言模型(LLM)已成为企业应用的重要组成部分。Anything LLM作为一款功能强大的开源LLM管理工具,提供了丰富的API接口,使开发者能够轻松集成AI能力到自己的应用中。本文将详细介绍如何使用最新的.NET 9框架对Anything LLM API进行封装,构建一个高效、可靠的SDK,以及避坑指南。
1. Anything LLM简介

Anything LLM是一个全功能的LLM管理平台,它允许开发者:
- 管理和部署多种大型语言模型;
- 创建和管理知识库 ;
- 构建对话式AI应用;
- 实现文档检索和问答系统
我们这里就以其为训练的基础,开启.net 9编写LLM接口的辉煌。
主要的封装依据来自Anything的API文档: http://localhost:3001/api/docs/#/
,如图所示,
其分为:
- 授权
- 管理接口
- 文档接口,重要的资料,私域数据等等
- 工作空间,可以按使用用户隔离出来的私有空间
- 系统配置,配置参数等操作
- 空间线程,类似于可以在一个空间启动多个聊天窗口
- 用户管理
- 兼容OpenAI的接口
- 嵌入式文档接口,同文档接口,也是兼容接口之一。
2. SDK架构设计
2.1 核心类结构
csharp
public class AnythingLLMClient
{
// HTTP客户端和配置
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly JsonSerializerOptions _jsonDeserializerOptions;
private readonly ILogger<AnythingLLMClient> _logger;
// 服务模块,暂时实现这么多,后续可以扩展实现
public AuthenticationService Authentication { get; }
public AdminService Admin { get; }
public DocumentsService Documents { get; }
public WorkspacesService Workspaces { get; }
}
2.2 模块化设计
这里将API功能划分为几个服务模块:
- AuthenticationService:处理认证相关操作
- AdminService:管理系统设置和配置
- DocumentsService:管理文档和知识库
- WorkspacesService:管理工作空间和对话
按照这种模块化设计提高了代码的可维护性和可扩展性,使用起来手感也略有提升。
3. 核心实现详解
3.1 初始化配置
ini
public AnythingLLMClient(string baseUrl, string apiKey, ILogger<AnythingLLMClient> logger, HttpClient httpClient = null)
{
this._logger = logger;
_httpClient = httpClient ?? new HttpClient();
_httpClient.BaseAddress = new Uri(baseUrl);
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("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 = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = false,
WriteIndented = false
};
_jsonDeserializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = false,
WriteIndented = false
};
// 初始化服务模块
Authentication = new AuthenticationService(this);
Admin = new AdminService(this);
Documents = new DocumentsService(this);
Workspaces = new WorkspacesService(this);
// 注册编码提供程序
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
3.2 HTTP请求处理
我们实现了四种基本的HTTP方法:
csharp
internal async Task<T> GetAsync<T>(string endpoint)
{
var response = await _httpClient.GetAsync(endpoint);
return await HandleResponse<T>(response);
}
internal async Task<T> PostAsync<T>(string endpoint, object data = null)
{
var content = data != null
? JsonContent.Create(data, options: _jsonSerializerOptions)
: null;
var response = await _httpClient.PostAsync(endpoint, content);
return await HandleResponse<T>(response);
}
internal async Task<T> PostMultipartAsync<T>(string endpoint, MultipartFormDataContent content)
{
var response = await _httpClient.PostAsync(endpoint, content);
return await HandleResponse<T>(response);
}
internal async Task<T> DeleteAsync<T>(string endpoint)
{
var response = await _httpClient.DeleteAsync(endpoint);
return await HandleResponse<T>(response);
}
3.3 响应处理
统一的响应处理机制确保了错误的一致性和可追踪性:
csharp
private async Task<T> HandleResponse<T>(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
this._logger.LogError($"LLM: 访问 {response.RequestMessage.ToString()}失败 {response.StatusCode}, {error}");
throw new AnythingLLMApiException(
$"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<T>(info, _jsonDeserializerOptions);
}
4.服务模块实现
4.1 认证服务(AuthenticationService)
csharp
public class AuthenticationService
{
private readonly AnythingLLMClient _client;
public AuthenticationService(AnythingLLMClient client)
{
_client = client;
}
public async Task<AuthResult> Login(string username, string password)
{
var request = new { username, password };
return await _client.PostAsync<AuthResult>("api/auth/login", request);
}
public async Task<bool> Logout()
{
return await _client.DeleteAsync("api/auth/logout");
}
}
4.2 文档服务(DocumentsService)
csharp
public class DocumentsService
{
private readonly AnythingLLMClient _client;
public DocumentsService(AnythingLLMClient client)
{
_client = client;
}
public async Task<Document> UploadDocument(string filePath, string workspaceId)
{
if (!File.Exists(filePath))
throw new FileNotFoundException("文件不存在", filePath);
using var content = new MultipartFormDataContent();
using var fileStream = File.OpenRead(filePath);
using var streamContent = new StreamContent(fileStream);
string fileName = Path.GetFileName(filePath);
var contentDisposition = new ContentDispositionHeaderValue("form-data")
{
Name = ""file"",
FileName = """ + fileName + """,
FileNameStar = fileName
};
streamContent.Headers.ContentDisposition = contentDisposition;
content.Add(streamContent, "file");
content.Add(new StringContent(workspaceId), "workspaceId");
return await _client.PostMultipartAsync<Document>("api/documents", content);
}
public async Task<List<Document>> GetDocuments(string workspaceId)
{
return await _client.GetAsync<List<Document>>($"api/documents?workspaceId={workspaceId}");
}
}
5. 中文文件名乱码
尝试了多种编码,Anything LLM
的API就是不给力,一直返回错误 Invalid file upload. NOENT: no such file or directory, open 'C:\Users\Administrator\AppData\Roaming\anythingllm-desktop\storage\hotdir???\'
,经过仔细查阅github,发现这个问题在早期版本已经解决,但是.net就是无法正常传递中文文件名。
经过查阅源码,发现如下代码:
javascript
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(null, 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(null, 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 变成乱码。
因此,我们进行手工编码,如下:
ini
using MultipartFormDataContent content = new MultipartFormDataContent();
var addToWorkspacesContent = new ByteArrayContent(
Encoding.UTF8.GetBytes(request.AddToWorkspaces ?? "") // 关键点:使用ASCII编码避免UTF-8标记
);
addToWorkspacesContent.Headers.Remove("Content-Type"); ; // 不设置Content-Type,避免影响多部分表单数据的处理
content.Add(addToWorkspacesContent, "addToWorkspaces");
using var fileStream = new FileStream(request.FilePath, FileMode.Open, FileAccess.Read);
using var streamContent = new StreamContent(fileStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue(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);
return await _client.PostMultipartAsync<UploadResp>(
BuildEndpoint("v1", "document","upload"), content);
这样处理后,终于可以愉快的支持中文了。
6. 单元测试
使用Moq模拟HttpClient
ini
[Fact]
public async Task GetDocuments_ShouldReturnDocuments()
{
// 准备
var mockHttp = new Mock<HttpMessageHandler>();
var expectedDocuments = new List<Document> { new Document { Id = "1", Name = "Test" } };
mockHttp.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(JsonSerializer.Serialize(expectedDocuments))
});
var client = new HttpClient(mockHttp.Object);
var llmClient = new AnythingLLMClient("http://test.com", "api-key", Mock.Of<ILogger<AnythingLLMClient>>(), client);
// 执行
var result = await llmClient.Documents.GetDocuments("workspace1");
// 断言
Assert.Single(result);
Assert.Equal("Test", result[0].Name);
}

总结
有压力就有动力,一旦出手了,就得努力进行学习和沉淀。
目前这个SDK提供了对Anything LLM API的完整封装,后续就可以利用该SDK进行二次开发,充分利用大型语言模型的强大能力。
------ 风已起,你是否准备好迎接这场AI革命? 🌪️🚀
你学废了吗?
👓都看到这了,还在乎点个赞吗?
👓都点赞了,还在乎一个收藏吗?
👓都收藏了,还在乎一个评论吗?