llama.cpp 中的执行器
StatelessExecutor 无状态 每次请求都是独立的,不保留任何记忆。下一次请求会清空缓存,重新从第一个 Token 开始解码。 单次文本分类、数据提取、情感分析、批量独立任务(如你之前的 JSON 提取)。
优点:绝对不会发生上下文污染。
缺点:长文本重复请求时,重复解码系统提示词导致性能较低。
InteractiveExecutor 有状态 连续对话模式(Chat)。会自动把模型的回复和用户的新输入追加到缓存中,像聊天软件一样越聊记忆越长。 智能助手、角色扮演、需要多轮连续对话的场景。
优点:速度快,完美保留对话上下文。
缺点:上下文达到上限(ContextSize)时需要做截断或滑窗处理。
InstructExecutor 有状态 指令/问答模式(Q&A)。每次输入都是一个独立的 Instruction(指令),它会在保留 System Prompt 的基础上,每次重置 User 的输入。 问答机器人、单次指令执行(但需要高频调用且固定系统提示词)。
优点:比无状态快(因为复用了系统提示词缓存)。
缺点:不适合多轮连续聊天。
StatefulExecutorBase 抽象基类 它是所有有状态执行器的“父亲”,定义了如何保存、恢复、滚动管理本地上下文缓存的底层逻辑。 无法直接实例化。如果你要自定义特殊的上下文管理策略,需要继承它。
返回 json 结构
这是一个从一段文本中提取数据返回 json 结构化数据的例子
使用了 qwen3.5 0.8 的模型,没有进行微调
- 使用 GBNF 语法约束了 llm 的输出格式
- 指定了 template=0 及其它一些参数,控制生成结果
- 使用 <|im_start|> 和 <|im_end|> 格式的提示词, qwen 预训练及微调使用这种格式
- GpuLayerCount 指定导入的 gpu 层数,0 表示全部使用 cpu 推理,如果 Mac 配置为 99 则全部使用 gpu 推理
- 使用 StatelessExecutor ,无状态的执行器, 多轮对话使用 InteractiveExecutor
- 要迸发处理时, 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() ?? "";
}