.net 调用 llama.cpp

llama.cpp 中的执行器

StatelessExecutor 无状态 每次请求都是独立的,不保留任何记忆。下一次请求会清空缓存,重新从第一个 Token 开始解码。 单次文本分类、数据提取、情感分析、批量独立任务(如你之前的 JSON 提取)。
优点:绝对不会发生上下文污染。
缺点:长文本重复请求时,重复解码系统提示词导致性能较低。

InteractiveExecutor 有状态 连续对话模式(Chat)。会自动把模型的回复和用户的新输入追加到缓存中,像聊天软件一样越聊记忆越长。 智能助手、角色扮演、需要多轮连续对话的场景。
优点:速度快,完美保留对话上下文。
缺点:上下文达到上限(ContextSize)时需要做截断或滑窗处理。

InstructExecutor 有状态 指令/问答模式(Q&A)。每次输入都是一个独立的 Instruction(指令),它会在保留 System Prompt 的基础上,每次重置 User 的输入。 问答机器人、单次指令执行(但需要高频调用且固定系统提示词)。
优点:比无状态快(因为复用了系统提示词缓存)。
缺点:不适合多轮连续聊天。

StatefulExecutorBase 抽象基类 它是所有有状态执行器的“父亲”,定义了如何保存、恢复、滚动管理本地上下文缓存的底层逻辑。 无法直接实例化。如果你要自定义特殊的上下文管理策略,需要继承它。

返回 json 结构

这是一个从一段文本中提取数据返回 json 结构化数据的例子
使用了 qwen3.5 0.8 的模型,没有进行微调

  1. 使用 GBNF 语法约束了 llm 的输出格式
  2. 指定了 template=0 及其它一些参数,控制生成结果
  3. 使用 <|im_start|> 和 <|im_end|> 格式的提示词, qwen 预训练及微调使用这种格式
  4. GpuLayerCount 指定导入的 gpu 层数,0 表示全部使用 cpu 推理,如果 Mac 配置为 99 则全部使用 gpu 推理
  5. 使用 StatelessExecutor ,无状态的执行器, 多轮对话使用 InteractiveExecutor
  6. 要迸发处理时, context 与 executor 需要在线程中创建,不能共用

        var logger = this.CreateLogger();

        Assert.True(File.Exists(ModelPath), $"模型文件不存在: {ModelPath}");
        logger.LogInformation("模型文件已找到: {ModelPath}", ModelPath);

        var parameters = new ModelParams(ModelPath)
        {
            ContextSize = 2048,
            GpuLayerCount = 0, // CPU 推理, 将多少层加载到 gpu 中, 如果为0 ,则全部使用 cpu 推理,如果 Mac 设置的大一些(比如 99)
        };

        // 加载模型
        using var weights = LLamaWeights.LoadFromFile(parameters);
        using var context = new LLamaContext(weights, parameters);

        // 提示词
        var instructions = $"You are a structured data extractor. DO NOT use thinking mode. DO NOT output <thought> tags. Respond directly with JSON.\n" +
                           new ShopResultAiAnalyzerOptions().Instructions;

        // 输出约束语法(返回的 json 结构)
        const string strictShopGbnf = """
                                      root ::= "{" ws name-kv "," ws rating-kv "," ws review-kv "," ws address-kv "," ws phone-kv "," ws category-kv "," ws lat-kv "," ws lon-kv "," ws id-kv "}" ws

                                      # 🎯 各个字段的严格键值对定义(已全量修正为大写首字母)
                                      name-kv     ::= "\"Name\"" ws ":" ws string
                                      rating-kv   ::= "\"Rating\"" ws ":" ws (number | "null")
                                      review-kv   ::= "\"ReviewCount\"" ws ":" ws (number | "null")
                                      address-kv  ::= "\"Address\"" ws ":" ws (string | "null")
                                      phone-kv    ::= "\"Phone\"" ws ":" ws (string | "null")
                                      category-kv ::= "\"Category\"" ws ":" ws (string | "null")
                                      lat-kv      ::= "\"Latitude\"" ws ":" ws (number | "null")
                                      lon-kv      ::= "\"Longitude\"" ws ":" ws (number | "null")
                                      id-kv       ::= "\"PlaceId\"" ws ":" ws (string | "null")

                                      # 🎯 核心:无敌 Unicode 多语言兼容字符串规则
                                      # 完美放行标准的 ASCII 以及多字节字符(彻底解决阿拉伯语、中文、日语等中途截断的 Bug)
                                      string      ::= "\"" (async-char)* "\""
                                      async-char  ::= [^"\\\x00-\x1F] | "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) | [#x80-#x10FFFF]

                                      # 基础标记定义
                                      number      ::= "-"? [0-9]+ ("." [0-9]+)?
                                      ws          ::= [ \t\n\r]*
                                      """;

        var inferenceParams = new InferenceParams
        {
            MaxTokens = 256,
            AntiPrompts = new List<string> { "<|im_end|>", "<|im_start|>", "User:", "User:\n", "\nUser", },

            SamplingPipeline = new DefaultSamplingPipeline
            {
                // 1. 绝对零度,消除随机性
                Temperature = 0f,

                // 2. 关闭重复惩罚!让 GBNF 来保证结构,避免模型因惩罚过高而生成乱码
                PresencePenalty = 0.0f,
                FrequencyPenalty = 0.0f,

                // 3. 限制候选词,配合 Temp=0 效果极佳
                TopK = 20,
                TopP = 0.1f,

                // 4.  GBNF 语法, 约束 llm 的输出格式 
                Grammar = new Grammar(strictShopGbnf, "root"),
            },
        };

        // 无状态上下文请求
        var executor = new StatelessExecutor(weights, parameters);

        foreach (var item in items)
        {
            // 预处理文本中的一些特殊字符
            var shopText = SanitizeShopText(item.OrginalText);

            // 拼接提示词与输入文本, qwen 使用如下格式预训练 |im_start|> 和 <|im_end|> , 最后 <|im_start|>assistant 表示该 llm 回复了
            var message = $@"""
<|im_start|>system
{instructions}
<|im_end|>
<|im_start|>user
请解析下面的内容
{shopText}
<|im_end|>
<|im_start|>assistant
""";

            // 与 llm 对话,拼接返回结果
            var texts = executor.InferAsync(message, inferenceParams);
            var json = string.Join(string.Empty, await texts.ToListAsync());

        }

IChatClient

官方提供了扩展函数 AsChatClient 返回 IChatClient , 使用上面的例子分析字符,返回结果经常直接返回输入,无法正常工作,以后再研究

简单调用

使用 session 包装了执行器

using LLama;
using LLama.Common;
using LLama.Sampling;

// 引用 LLamaSharp, LLamaSharp.Backend.Cpu
// 这个例子中,内存使用的 4 个G

// Llava 调用的例子,新版本中被移除
// scisharp.github.io/LLamaSharp/0.25.0/QuickStart/
// https://github.com/SciSharp/LLamaSharp/issues/1194

var modelPath = @"/Users/iox/Downloads/MiniCPM-V-4_5-Q4_0.gguf"; // 引用的 model 路径
// var multiModalProj = @"/Users/iox/Downloads/mmproj-model-f16.gguf";
// var modelImage = @"/Users/iox/Downloads/截屏2025-11-11 02.24.48.png";

var parameters = new ModelParams(modelPath)
{
    ContextSize = 1024, // The longest length of chat as memory.
    GpuLayerCount = 0, // How many layers to offload to GPU. Please adjust it according to your GPU memory.
};

using var model = LLamaWeights.LoadFromFile(parameters);
using var context = model.CreateContext(parameters);

// Llava Init
// https://github.com/SciSharp/LLamaSharp/issues/1255, Llava 已经被移至 mtmd, 新的版本中还没有提供。等新版本出来再研究
// using var clipModel = LLavaWeights.LoadFromFile(multiModalProj);
// var executor = new InteractiveExecutor(context, clipModel);s
var executor = new InteractiveExecutor(context);

var system = @"你是一位名叫小瓜的通用生活 AI 助手,你的主要目标是成为用户日常生活中最值得信赖、最有趣、最全能的帮手。";

// Add chat histories as prompt to tell AI how to act.
var chatHistory = new ChatHistory();
chatHistory.AddMessage(AuthorRole.System, system);
// chatHistory.AddMessage(AuthorRole.User, "Hello, Bob.");
// chatHistory.AddMessage(AuthorRole.Assistant, "Hello. How may I help you today?");

ChatSession session = new(executor, chatHistory);

var inferenceParams = new InferenceParams
{
    MaxTokens = 256, // 最大回复数,如果指定了合适的停止词(AntiPrompts)也可以不设置该值
    AntiPrompts = new List<string> { "User:", }, // 停止词,防止模型生成无意义的内容。比如 AI 自动完成 user 的输出
    SamplingPipeline = new DefaultSamplingPipeline(),
};

Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write("The chat session has started.\nUser: ");
Console.ForegroundColor = ConsoleColor.Green;
var userInput = Console.ReadLine() ?? "";

while (userInput != "exit")
{
    // Generate the response streamingly.
    await foreach (var text in session.ChatAsync(new ChatHistory.Message(AuthorRole.User, userInput), inferenceParams))
    {
        Console.ForegroundColor = ConsoleColor.White;
        Console.Write(text);
    }

    Console.ForegroundColor = ConsoleColor.Green;
    userInput = Console.ReadLine() ?? "";
}
上一篇
下一篇