DeepSeek私域数据训练之封装Anything LLM的API 【net 9】

  • 📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!
  • 📢本文作者:由webmote 原创
  • 📢作者格言:新的征程,用爱发电,去丈量人心,是否能达到人机合一?

序言

书接上回,自从重金 购买了学习AI的设备后,总得找点事情干! 这不,就有个小需求,需要训练私域数据,那么我们就从Anything LLM的API封装做起。在当今快速发展的人工智能领域,大型语言模型(LLM)已成为企业应用的重要组成部分。Anything LLM作为一款功能强大的开源LLM管理工具,提供了丰富的API接口,使开发者能够轻松集成AI能力到自己的应用中。本文将详细介绍如何使用最新的.NET 9框架对Anything LLM API进行封装,构建一个高效、可靠的SDK,以及避坑指南。

1. Anything LLM简介

Anything LLM是一个全功能的LLM管理平台,它允许开发者:

  1. 管理和部署多种大型语言模型;
  2. 创建和管理知识库 ;
  3. 构建对话式AI应用;
  4. 实现文档检索和问答系统

我们这里就以其为训练的基础,开启.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革命? 🌪️🚀

你学废了吗?

👓都看到这了,还在乎点个赞吗?

👓都点赞了,还在乎一个收藏吗?

👓都收藏了,还在乎一个评论吗?

相关推荐
bin91532 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的日历(Calendar),日历_天气预报日历示例(CalendarView01_18)
前端·javascript·vue.js·ecmascript·deepseek
InCerry12 小时前
.NET周刊【5月第4期 2025-05-25】
c#·.net·.net周刊
界面开发小八哥15 小时前
VS代码生成工具ReSharper v2025.1——支持.NET 10和C# 14预览功能
开发语言·ide·c#·.net·visual studio·resharper
橙某人16 小时前
🤝和Ollama贴贴!解锁本地大模型的「私人订制」狂欢🎉
前端·deepseek
步、步、为营19 小时前
.net jwt实现
ui·.net
步、步、为营20 小时前
.NET Core接口IServiceProvider
.net·.netcore
Kookoos20 小时前
功能管理:基于 ABP 的 Feature Management 实现动态开关
c#·.net·saas·多租户·abp vnext
奔跑吧邓邓子1 天前
DeepSeek 赋能金融衍生品:定价与风险管理的智能革命
人工智能·金融衍生品·deepseek·金融市场·定价与风险管理