🦙 llama.cpp 图解教程llama.cpp Visual Guide 第五部分 · 公共 API 与工具Part 5 · Public API & tools 27 / 40
第五部分 · 公共 API 与工具Part 5 · Public API & tools

llama-clillama-cli

llama-cli 多半是你第一个真正跑起来的 llama.cpp 程序——一句 llama-cli -m model.gguf -p "从前有座山",模型就开始往下续写。它简单到几乎不用解释,可正因为简单,它是看清"一条命令如何变成满屏文字"的最佳样本。

这一课我们钻进它内部,把"解析命令行 + 一个生成循环"这套骨架拆开看;也借它看清 llama.cpp 的工具到底是怎么搭在 common(L26)和推理引擎之上的——工具课的"读法",从这一课开始定调。

还有一点要先剧透:现代的 llama-cli 已经不是一份独立的裸 llama_decode 主循环了,它直接复用了 llama-server 的那台引擎。所以这一课既是"上手第一课",也悄悄替下一课的 server 埋好了伏笔。

🌍 宏观理解
一句话给 cli 定位:它是给共享推理引擎套上的一层"命令行外壳"。引擎负责真正的重活——加载模型、维护 KV、前向、采样;cli 只负责"把人的意图喂进去、把模型的输出递出来":从命令行和 stdin 读到你的 prompt,驱动引擎一轮轮生成,再把每个新词即时打到屏幕上。看懂这层"壳与引擎"的分工,你就明白为什么 cli 的源码出奇地短——重活早被引擎和 common 包圆了,cli 自己要写的,只剩"读输入、转一圈、流式输出"这点活。换个角度说,cli 的"短"恰恰是整套分层设计交出的成绩单:底层 C API(L25)足够稳,中间的 common(L26)足够厚,到了工具这一层自然就能薄。所以读 cli 的源码,与其说是在读"一个程序",不如说是在读"前面那几层到底替它省下了多少重复劳动"。这也是我们把它排在第五部分靠前的原因——它像一面镜子,照见 C API 与 common 这些铺垫的价值。你越熟悉前四部分讲过的内部机件——加载、KV、批处理、采样——就越会惊讶于 cli 能把它们用得这么轻:几乎所有重活都发生在别处,它只负责把人和引擎接起来。
🔌 生活类比
把 cli 想成一台自动售货机的面板:你按下按钮(敲命令行 / 输入文字),机器内部(共享引擎)一件件出货(生成 token),面板只管把货一件件递出来(流式打印),不必关心里头是怎么造的。下一课的 server 则是同一台机器换了个"网络下单"的面板:货还是那批货,引擎还是那台引擎,只是接单与出货的方式从"按钮"变成了"HTTP 请求"。面板可以有很多种,机器只有一台——这正是 cli 与 server 的关系。这个类比还能再往前推一步:同一台机器,将来完全可以再接上第三块、第四块面板——比如一个 gRPC 服务、一个桌面 GUI——而机器内部的配方、火候、工序,一行都不必改。这正是"面板可换、机器唯一"的威力:你想支持一种新的接入方式,只要再写一层薄薄的壳,把请求翻译成引擎认得的格式就行,完全不用重写推理逻辑。记住这幅画面,下一课看 server 时你就会明白,它无非是换上了一块"能同时接很多张订单"的面板,灶台后面那台机器,还是你在 cli 里见过的那一台。

一条命令的旅程

程序入口在 tools/cli/main.cpp,但它薄得几乎只有一行——把活儿立刻转交给 tools/cli/cli.cpp。cli.cpp 一上来先调 common_init() 起好日志系统,再用 common_params_parse(argc, argv, params, LLAMA_EXAMPLE_CLI) 把命令行灌进 common_params(正是 L26 那套机制)。留意第 4 个参数 LLAMA_EXAMPLE_CLI:它告诉解析器"按 cli 这套选项集来认参数",于是 cli 专属的旗标和共享旗标都能被正确识别。

// 入口: 薄薄的 main 转入 cli.cpp (简化自 tools/cli)
common_init();                                  // 全局初始化: 起日志
if (!common_params_parse(argc, argv, params, /*ex=*/ LLAMA_EXAMPLE_CLI))
     return 1;                                   // argv -> common_params
auto res = common_init_from_params(params);     // 概念上拿到 model+ctx+sampler (cli 实经引擎)

参数就位后,剩下的事几乎可以排成一条直线:按 common_params 载入模型、建好上下文,把 prompt 编码成 token 喂进去,进入生成循环,一边采样一边把新词流式打印。下图把这条主线画成五步——前两步你应该眼熟,正是 L25 那条手写序列,如今被 common_init_from_params 收进了一行。

1

解析参数

common_params_parse 把 argv 按 LLAMA_EXAMPLE_CLI 填进 common_params。

2

载入模型 + 建上下文

common_init_from_params 一口气加载 model、建好 context、配好 sampler。

3

编码 prompt

把提示词 tokenize 成 token 序列,喂进上下文当作"已生成"的开头。

4

生成循环

decode -> 采样 -> 接回上下文,一圈圈转,直到撞上停止条件。

5

流式输出

每选定一个 token 就还原成文字、即时打印,不必等整段生成完。

这条主线里,前两步(解析、初始化)几乎全被 common 包圆了(L26),真正属于 cli 自己的"主戏",是第 4 步那个一圈圈转的生成循环——它把 L19 的 KV 增长、L21 的采样、L20 的 detokenize 串成了一个你能亲眼看到输出的闭环。说得直白些,前四个部分讲过的所有内部机件,到这里终于汇成了一个你能一行行看着它吐字的循环——很多人正是从 cli 开始"爱上"读 llama.cpp 源码的,因为它把抽象的推理,变成了屏幕上实实在在跳动的文字。下面就把这个循环单独拎出来看。

生成主循环

生成的本质是一个循环:每一轮让引擎前向一次(llama_decode)、拿到"下一个词的打分"(logits),用采样器(common_sampler,L26/L21)挑出一个 token,用 common_token_to_piece 把它还原成文字、即时打印,再把这个新 token 接回上下文,进入下一轮。如此往复,直到撞上三个"停"条件之一。

// 生成主循环的本质 (现位于引擎 server_context 内, 非 cli.cpp 自有)
while (n_remain != 0) {
     llama_decode(ctx, batch);                        // 前向: 末位拿 logits
     llama_token id = common_sampler_sample(smpl, ctx, -1); // 采样下一个
     common_sampler_accept(smpl, id, true);            // 反馈: 更新惩罚/语法
     if (llama_vocab_is_eog(vocab, id)) break;         // 结束符 -> 停
     fputs(common_token_to_piece(ctx, id).c_str(), stdout); // 流式打印
     batch = llama_batch_get_one(&id, 1);              // 新 token 接回去
     n_remain--;                                         // n_predict 计数
}

三个"停下来"的理由要记牢:写满了预定长度(n_predict 计数到 0)、模型自己吐出了结束符(EOG,呼应 L20/L21,用 llama_vocab_is_eog 判断)、或在交互模式下命中了你设的反向提示(antiprompt)。循环里那句 common_sampler_accept 也一步都不能省——它把刚选的 token 反馈回采样器,更新重复惩罚、推进语法状态(L23),下一轮才采得对。这一点初学时最容易忽略:很多人以为"采样"不过是挑个词那么简单,却忘了采样器其实是带记忆的——它要记住已经出过哪些词,好施加重复惩罚;要记住语法走到了哪一步,好约束下一个合法 token。一旦漏掉 accept,这些记忆就停在原地不更新,生成很快就会重复、跑偏甚至卡死。

把这一圈用一个最小例子走一遍最直观:假设上下文里已经有 "The cat",看它如何生成下一个词、并把它流式吐到屏幕上。

追踪生成主循环一轮:从已生成的 token 出发,decode 拿 logits、采样选词、还原打印,再判断是否要停(数值为示意)。
① 已生成
Thecat
上下文里的 token
decode
末位
② logits
logits[n_vocab]
下一词的打分
common_
sampler_sample
③ 选定 token
sat
采样挑出
token_to_piece
+ 打印
④ 流式输出
"The cat sat"
即时写到屏幕
回环
检查停
⑤ 停?
n_predict? EOG? antiprompt?
否则回到 ①

跑在共享引擎上

现在揭开开头那个剧透。如果你翻开 tools/cli/cli.cpp 的头部,会看到它直接 #include 了 server 的几个头文件——server-common.hserver-context.hserver-task.htools/cli/CMakeLists.txt 里也把它链接到了 server-context 这个库。换句话说,现代 llama-cli 并没有自己再写一份裸的 llama_decode 主循环,而是复用了 server 那台引擎 server_context(带 slot 与 task 的那一套,下一课细讲)。

// tools/cli/cli.cpp 顶部: 直接复用 server 的引擎
#include "server-common.h"
#include "server-context.h"   // server_context: slot / task / KV
#include "server-task.h"

# tools/cli/CMakeLists.txt: 链接 server-context 库
target_link_libraries(${TARGET} PUBLIC server-context llama-common ...)

这件事意味着什么?cli 和 server 其实共用同一台"发动机",只是套了不同的"壳":cli 的壳是命令行 + 交互终端,server 的壳是 HTTP + 多请求。引擎完全一样(加载、KV、批处理、采样都走同一套 server_context),区别只在"怎么把请求喂进去、怎么把结果递出来"。

llama-cli(命令行壳)

从 argv / stdin 读 prompt,把生成的 token 流式打到 stdout;适合上手、脚本、交互对话。

llama-server(HTTP 壳)

从 HTTP 请求收 prompt,把结果按 OpenAI 兼容格式返回;适合多用户、做服务。

历史上 cli 曾是一份独立的 main.cpp 生成循环,后来才统一到这台共享引擎上——好处是少维护一份几乎重复的代码,引擎里修一个 bug,cli 和 server 两边都跟着受益。也正因为如此,下一课 server 的不少概念(slot、连续批处理)其实你在 cli 里已经"隔着壳"用上了,只是没察觉而已。

🌍 宏观理解
"同引擎、异壳"是理解整个第五部分工具的一把钥匙。server_context 是那台发动机,cli 与 server 是两副不同的车壳:你换壳(换交互方式),但发动机不动。这种设计的价值在于单一事实源——推理逻辑只有一份实现,所有工具共享;要优化吞吐、修采样 bug、加新特性,只动引擎一处,全家受益。所以别把 cli 看成"另一套实现",它更像 server 的一个轻量前台。把这把钥匙揣好,下一课直接拆发动机本身。再补一句这套设计的代价与回报。把引擎抽成共享组件,短期看是多添了一层抽象,读代码要多绕一道弯;可长期看,它换回的是"改一次、处处生效"的巨大便利。设想一下:要是 cli 和 server 各管各的生成循环,那么每修一个采样的边界 bug、每加一种新的停止条件,你都得在两个地方分别动手,还要时时提防两边行为不一致、悄悄跑偏。共享引擎把这种"双份维护"的负担一笔勾销了。这种"宁可多一层抽象,也要消灭重复"的取舍,是成熟工程里反复出现的母题,值得你在自己的项目里也留个心眼。

交互模式

给命令行加上 -i,cli 就从"一次性续写"变成"来回对话":它会在你按回车后,把你的输入编码进上下文,再继续生成,如此一问一答。打断生成靠反向提示(antiprompt / reverse prompt)——你设一个字符串(比如 "User:"),模型一旦要生成到它,就停下来、把话筒交还给你。终端的着色、退格、中文输入,则由 L26 的 console 在背后撑着。

几个最常打交道的旗标值得对着主循环记一下:-n 限制最多生成多少 token(就是 n_predict,管循环转几圈),-c 设上下文窗口多大(n_ctx,呼应 L17/L19),--temp 调采样温度(L21,管挑词那一步),-i 进交互。把每个旗标和循环里的某一步对上号,你就能预测它到底改变了什么。

💡 动手试试
最值得记的四个旗标:-m 指模型、-p 给提示词、-n 限生成长度、-i 进交互。想体会"壳与引擎"的分别,可以同一个模型先 llama-cli -m x.gguf -p "讲个笑话" -n 64 跑一次性续写,再 llama-cli -m x.gguf -i 进交互聊几句——你会发现底下那台引擎一模一样,变的只是你和它打交道的方式。再加上 -c 调上下文、--temp 调温度,把它们和这一课的生成循环对着看,"参数 -> 循环行为"的因果就一目了然了。还可以再做个小实验加深印象:把 --temp 分别设成 0 和 1.2,各跑一次同样的 prompt,你会直观看到温度如何左右"挑词"那一步——设 0 时它几乎每次都吐一模一样的话,设 1.2 时则天马行空、花样百出。再把 -n 调得很小(比如 4),看它怎么话没说完就被硬生生截断,这就是 n_predict 这道闸门在起作用。把这些旗标一个个亲手拨动、对照生成循环看效果,远比死记每个参数的定义来得有用——你建立起来的,是"参数到行为"的肌肉记忆,将来调任何模型都用得上。

深入:复用的来龙去脉与"何时算停"

最后用两个折叠,补两个常被追问的点:cli 复用 server 引擎的来龙去脉,以及"到底什么时候算生成结束"。前者关乎"架构为什么这么演化",后者关乎一个你每次跑都会遇到、却未必说得清的细节。这两个问题看似琐碎,却分别对应着读源码时最常冒出的两种困惑:一种是"这段代码为什么长这样"(历史与权衡),一种是"它到底什么时候停"(运行时行为)。把它们说清楚,你再去翻 cli 的真实源码就不会被绕晕。

1 cli 为什么要复用 server 的引擎? 点击展开

早期的 llama.cpp 里,cli(当时叫 main)和 server 各有一份生成循环:各自调 llama_decode、各自管 KV、各自处理停止条件。两份代码做的事高度重叠,却要分别维护——改一处采样逻辑,得记得两边都改,很容易漏。后来项目把引擎抽成共享的 server_context(slot/task 那套),让 cli 也站上去:cli 退化成"开一个 slot、喂一条序列、流式取回"的瘦客户端。好处是单一事实源——生成逻辑只剩一份实现,bug 修一处、特性加一处,cli 与 server 同时受益。

所以当你在 cli 里看到 server_taskserver_slot 这些名字时不必奇怪:它们不是"server 专用",而是"引擎的词汇"。这也解释了为什么把 cli 放在 server(L28)前面讲——先在简单的命令行场景里见过这台引擎,下一课再看它如何同时服务多个 HTTP 请求,就顺理成章了。

2 "生成结束"到底由谁说了算? 点击展开

循环停下来有三种情形,触发者各不相同。其一是长度到顶:你用 -n 设的 n_predict 计数归零,这是"你"喊停。其二是模型自己喊停:它生成了一个 EOG(end-of-generation)token,比如 </s> 或某些聊天模板里的 <|im_end|>——cli 用 llama_vocab_is_eog(vocab, id) 判断,呼应 L20 里那个"结束符集合"。其三是反向提示命中:交互模式下,模型快要生成到你设的 antiprompt 时被截停,把控制权还给你。

这三者的优先级与细节,正是"为什么有时它早早就停 / 为什么停不下来"的根源:忘了设 -n 又遇上模型不肯吐 EOG,就可能一直生成;而某些模型的 EOG token 若没被模板正确标注,也会让它"刹不住车"。理解这三个闸门,你就能对症下药地控制生成长度。

✅ 关键要点
  • llama-cli = 给共享推理引擎套的一层命令行/交互外壳:读 stdin、驱动生成、流式打印 stdout,是上手 llama.cpp 最直接的工具。
  • 入口 main.cpp(薄)-> cli.cppcommon_init + common_params_parse(..., LLAMA_EXAMPLE_CLI) 把命令行变成 common_params(L26)。
  • 生成主循环:decode 拿 logits -> common_sampler_sample 选 token -> common_sampler_accept 反馈 -> common_token_to_piece 流式打印 -> 接回上下文,循环往复。
  • 三个停止条件:n_predict 写满、EOG 结束符(llama_vocab_is_eog)、交互模式下命中反向提示(antiprompt)。
  • 现状重点:现代 cli 复用 server 的引擎——#include "server-context.h" 并链接 server-context,与 server 同引擎、异壳(cli=命令行,server=HTTP)。
💡 设计洞察
cli 这一课真正想留给你的,不是某个旗标的用法,而是"壳与引擎分离"这一架构直觉。同一台 server_context,套上命令行壳就是 cli,套上 HTTP 壳就是 server——交互方式千变万化,推理内核始终如一。这种"把稳定的核做厚、把多变的壳做薄"的思路,和 L25 的"稳定 C ABI + 自由内部"、L26 的"对外稳定 + 对内便利"是同一条线索的延续:好的系统总在努力分清"哪些该统一、哪些该各异"。把这层想透,你看第五部分剩下的工具,就不会再把它们当成一个个孤立程序,而会看见底下那台被反复复用的引擎——下一课,我们就正面把它拆开。最后留一个值得反复咀嚼的问题给你:下次自己设计系统时,该怎么判断"哪一部分做成稳定的核、哪一部分做成可换的壳"?cli 给的答案朴素而有力——把"所有接入方式都共享的那部分"(也就是推理逻辑)沉进核里,把"每种接入方式各不相同的那部分"(命令行还是 HTTP)留在壳上。这条看似简单的分界线,其实适用于绝大多数需要支持多种入口的软件:Web 框架的路由与业务、数据库的协议层与存储引擎,背后都是同一种智慧。把它内化成你自己的设计直觉,你带走的就不只是"llama-cli 怎么用",而是一种能迁移到任何项目的判断力。

🧪 自测 · 想一想为什么这么设计

1. 现代 llama-cli 内部复用了哪个组件的引擎?
  1. 它有一份完全独立的裸 llama_decode 主循环,与 server 无关
  2. server-context(server_context):cli #include server-context.h 并链接 server-context,与 server 同引擎、异壳
  3. Python 绑定提供的引擎
  4. ggml 的计算图执行器,绕过 llama 层
看答案与解析 点击展开
答案:B。现代 cli 不再自带裸主循环:cli.cpp #include 了 server-common.h / server-context.h / server-task.h,CMake 也链接 server-context,直接复用 server 的 server_context(slot/task)。cli 与 server 是“同引擎、异壳”。
2. llama-cli 的生成主循环靠什么停下来?
  1. 由操作系统的定时器中断决定
  2. 三者之一:n_predict 写满、模型吐出 EOG 结束符、或交互模式下命中反向提示(antiprompt)
  3. 只有一种:必须等模型自己吐出 EOG
  4. 固定生成 2048 个 token 后无条件停止
看答案与解析 点击展开
答案:B。三个停止条件:n_predict 计数归零(你设的长度上限)、llama_vocab_is_eog 判定的结束符(模型自己喊停)、交互模式下命中你设的反向提示。忘了设 -n 又遇上模型不吐 EOG,就可能一直生成。
3. llama-cli 怎么把命令行变成内部配置?
  1. 从一个 JSON 配置文件读取,命令行被忽略
  2. cli 自己手写一大堆 if-else 直接解析 argv
  3. llama_model_load_from_file 顺便解析命令行
  4. common_params_parse(argc, argv, params, LLAMA_EXAMPLE_CLI) 把 argv 填进 common_params(L26)
看答案与解析 点击展开
答案:D。cli 复用 common 的参数解析:common_params_parse 按第 4 个参数 LLAMA_EXAMPLE_CLI 选定 cli 的选项集,逐个调用 common_arg 回调把 argv 写进 common_params,再交给 common_init_from_params 产出 model+ctx+sampler。
💭 发散思考(没有标准答案,动手或动脑想想)
  • 既然 cli 和 server 复用同一台 server_context 引擎、只是外壳不同,试着说说:把“引擎”与“外壳”分开,对维护和加新特性各有什么好处?如果要再写一个 gRPC 版的 llama 服务,你会怎么搭?

llama-cli is most likely the first llama.cpp program you ever run - one line, llama-cli -m model.gguf -p "Once upon a time", and the model starts continuing the text. It is so simple it barely needs explaining, but that very simplicity makes it the best sample for seeing "how one command turns into a screen full of text".

This lesson digs inside it and takes apart the skeleton of "parse the command line + one generation loop"; it also uses cli to see how llama.cpp's tools actually sit on top of common (L26) and the inference engine - the "way to read" the tool lessons is set here.

One more spoiler up front: a modern llama-cli is no longer a standalone bare llama_decode main loop - it directly reuses llama-server's engine. So this lesson is both "your first hands-on lesson" and a quiet setup for the server lesson next.

🌍 Big picture
cli in one line: it is a "command-line shell" wrapped around the shared inference engine. The engine does the real heavy lifting - load the model, maintain the KV, run the forward pass, sample; cli only "feeds in the human's intent and hands out the model's output": read your prompt from the command line and stdin, drive the engine round after round, and print each new word to the screen immediately. Understand this "shell vs engine" split and you see why cli's source is surprisingly short - the heavy lifting was packaged up by the engine and common, leaving cli with just "read input, turn a loop, stream output". Put differently, cli's "shortness" is the report card of the whole layered design: the C API underneath (L25) is stable enough, the common layer in the middle (L26) is thick enough, so by the tool layer it can afford to be thin. Reading cli's source is thus less like reading "a program" and more like reading "how much repeated labor the layers below saved it". That is also why we place it early in Part 5 - it is a mirror reflecting the value of the C API and common groundwork. The more familiar you are with the internal machinery of the first four parts - loading, KV, batching, sampling - the more it surprises you how lightly cli uses them: almost all the heavy lifting happens elsewhere, and it only wires the human to the engine.
🔌 Analogy
Think of cli as the panel of a vending machine: you press buttons (type the command line / input text), the machine inside (the shared engine) dispenses items one at a time (generates tokens), and the panel just hands them out one by one (streams the print), never minding how they were made inside. Next lesson's server is the same machine with an "order over the network" panel instead: same goods, same engine, only the way of taking orders and dispensing changes from "buttons" to "HTTP requests". There can be many panels, but only one machine - that is exactly the cli-and-server relationship. The analogy stretches one step further: the same machine could later take a third or fourth panel - a gRPC service, a desktop GUI - while the recipe, the heat, the steps inside change not one line. That is the power of "panels swappable, machine singular": to support a new way in, you write one more thin shell that translates requests into what the engine understands, with no rewrite of the inference logic. Hold this picture, and next lesson server makes sense at once - it is merely fitted with a panel that "takes many orders at once", while the machine behind the stove is the very one you met in cli.

A command's journey

The entry point is tools/cli/main.cpp, but it is so thin it is almost one line - it immediately hands off to tools/cli/cli.cpp. cli.cpp first calls common_init() to start the logging system, then uses common_params_parse(argc, argv, params, LLAMA_EXAMPLE_CLI) to pour the command line into common_params (exactly L26's mechanism). Note the 4th argument LLAMA_EXAMPLE_CLI: it tells the parser "recognize parameters by cli's option set", so cli-specific flags and shared flags are both parsed correctly.

// entry: the thin main hands off to cli.cpp (simplified from tools/cli)
common_init();                                  // global init: start logging
if (!common_params_parse(argc, argv, params, /*ex=*/ LLAMA_EXAMPLE_CLI))
     return 1;                                   // argv -> common_params
auto res = common_init_from_params(params);     // conceptually model+ctx+sampler (cli does it via the engine)

With the parameters in place, the rest lines up almost straight: load the model and build the context from common_params, encode the prompt into tokens and feed it in, enter the generation loop, and stream out new words as you sample. The diagram below draws this main line as five steps - the first two should look familiar, they are exactly L25's hand-written sequence, now folded into one line of common_init_from_params.

1

Parse args

common_params_parse fills argv into common_params per LLAMA_EXAMPLE_CLI.

2

Load model + build context

common_init_from_params loads the model, builds the context, configures the sampler in one go.

3

Encode prompt

Tokenize the prompt into a token sequence, fed in as the "already generated" opening.

4

Generation loop

decode -> sample -> append back, turning round and round until a stop condition hits.

5

Stream output

Each chosen token is restored to text and printed at once, no waiting for the whole thing.

On this main line, the first two steps (parse, init) are almost entirely packaged by common (L26); the part that truly belongs to cli, its "main act", is step 4's looping generation loop - it threads L19's KV growth, L21's sampling, and L20's detokenize into a closed loop whose output you can watch live. Put plainly, all the internal machinery the first four parts covered finally converges here into a loop you can watch emit text line by line - many people first "fall for" reading llama.cpp's source from cli, because it turns abstract inference into words actually dancing on the screen. Let us pull that loop out and look at it alone.

The generation main loop

Generation is in essence a loop: each round runs one forward pass through the engine (llama_decode) to get "the next word's scores" (logits), uses the sampler (common_sampler, L26/L21) to pick a token, restores it to text with common_token_to_piece and prints it at once, then appends this new token back to the context and goes round again. So it repeats, until it hits one of three "stop" conditions.

// the essence of the generation loop (now inside the engine, server_context)
while (n_remain != 0) {
     llama_decode(ctx, batch);                        // forward: logits at last pos
     llama_token id = common_sampler_sample(smpl, ctx, -1); // sample the next one
     common_sampler_accept(smpl, id, true);            // feedback: penalties/grammar
     if (llama_vocab_is_eog(vocab, id)) break;         // end-of-gen -> stop
     fputs(common_token_to_piece(ctx, id).c_str(), stdout); // stream print
     batch = llama_batch_get_one(&id, 1);              // append new token
     n_remain--;                                         // n_predict countdown
}

Keep the three "stop" reasons in mind: the set length is reached (n_predict counts down to 0), the model itself emits an end-of-generation token (EOG, echoing L20/L21, tested with llama_vocab_is_eog), or in interactive mode it hits the reverse prompt (antiprompt) you set. That common_sampler_accept line cannot be skipped either - it feeds the just-chosen token back to the sampler, updating repetition penalties and advancing grammar state (L23), so the next round samples correctly. This is the easiest thing to overlook as a beginner: many think "sampling" is just picking a word, forgetting the sampler is actually stateful - it must remember which words already appeared to apply repetition penalties, and where the grammar has advanced to constrain the next legal token. Skip accept and that memory freezes in place, so generation soon repeats, drifts, or even stalls.

Walking one turn with a minimal example is the most vivid: suppose the context already holds "The cat", and watch how it generates the next word and streams it to the screen.

Tracing one generation step: starting from the already-generated tokens, decode for logits, sample a word, restore and print, then decide whether to stop (values are illustrative).
(1) generated
Thecat
tokens in context
decode
last pos
(2) logits
logits[n_vocab]
scores for next word
common_
sampler_sample
(3) chosen token
sat
picked by sampling
token_to_piece
+ print
(4) streamed out
"The cat sat"
written to screen now
loop
check stop
(5) stop?
n_predict? EOG? antiprompt?
else back to (1)

Running on the shared engine

Now lift the spoiler from the start. If you open the top of tools/cli/cli.cpp, you will see it directly #includes several of server's headers - server-common.h, server-context.h, server-task.h; and tools/cli/CMakeLists.txt links it against the server-context library. In other words, a modern llama-cli does not write its own bare llama_decode main loop anymore, it reuses server's engine server_context (the slot-and-task machinery, detailed next lesson).

// top of tools/cli/cli.cpp: reuse server's engine directly
#include "server-common.h"
#include "server-context.h"   // server_context: slot / task / KV
#include "server-task.h"

# tools/cli/CMakeLists.txt: link the server-context library
target_link_libraries(${TARGET} PUBLIC server-context llama-common ...)

What does this mean? cli and server actually share one "engine", just wrapped in different "shells": cli's shell is the command line plus an interactive terminal, server's shell is HTTP plus many requests. The engine is identical (loading, KV, batching, sampling all go through the same server_context); the only difference is "how requests are fed in and how results are handed out".

llama-cli (command-line shell)

reads the prompt from argv / stdin, streams generated tokens to stdout; great for getting started, scripts, interactive chat.

llama-server (HTTP shell)

takes the prompt from an HTTP request, returns results in an OpenAI-compatible shape; great for many users, running a service.

Historically cli was a standalone main.cpp generation loop, and only later was unified onto this shared engine - the gain is one less near-duplicate copy to maintain, and a bug fixed in the engine benefits cli and server alike. For the same reason, many of next lesson's server concepts (slots, continuous batching) you are in fact already using in cli "through the shell", just without noticing.

🌍 Big picture
"Same engine, different shells" is a key to the whole of Part 5's tools. server_context is that engine, and cli and server are two different car bodies: you swap the body (the way you interact), but the engine stays put. The value of this design is a single source of truth - the inference logic has exactly one implementation, shared by all tools; to optimize throughput, fix a sampling bug, or add a feature, you touch the engine in one place and the whole family benefits. So do not see cli as "another implementation", it is more like a lightweight front desk for server. Pocket this key, and next lesson we take the engine itself apart. One more word on this design's cost and reward. Extracting the engine into a shared component adds, short term, one more layer of abstraction and one more hop to follow while reading; but long term it buys the huge convenience of "fix once, effective everywhere". Imagine cli and server each minding their own generation loop: every boundary bug in sampling, every new stop condition would have to be changed in two places, while you guard against the two drifting out of sync. The shared engine wipes out that "double maintenance" entirely. This trade of "rather one more abstraction than any duplication" is a recurring motif in mature engineering, worth keeping an eye out for in your own projects.

Interactive mode

Add -i to the command line and cli turns from "one-shot continuation" into "back-and-forth conversation": after you press enter, it encodes your input into the context and keeps generating, taking turns. Interrupting generation is done with the reverse prompt (antiprompt) - you set a string (say "User:"), and the moment the model is about to generate up to it, it stops and hands the microphone back to you. The terminal's coloring, backspace, and UTF-8 input are held up behind the scenes by L26's console.

A few of the flags you deal with most are worth noting against the main loop: -n caps how many tokens to generate (that is n_predict, governing how many times the loop turns), -c sets the context window size (n_ctx, echoing L17/L19), --temp tunes the sampling temperature (L21, governing the pick-a-word step), and -i enters interactive. Match each flag to a step in the loop and you can predict exactly what it changes.

💡 Hands-on
The four flags most worth remembering: -m for the model, -p for the prompt, -n to cap length, -i for interactive. To feel the "shell vs engine" split, take one model and first run llama-cli -m x.gguf -p "tell a joke" -n 64 for a one-shot continuation, then llama-cli -m x.gguf -i to chat a few turns - you will find the engine underneath is identical, only the way you talk to it changes. Add -c for context and --temp for temperature, line them up against this lesson's generation loop, and the "param -> loop behavior" cause and effect becomes plain. Try one more small experiment to cement it: set --temp to 0 and then to 1.2, running the same prompt each time, and you will see directly how temperature swings the "pick a word" step - at 0 it spits almost the same words every time, at 1.2 it runs wild and varied. Then set -n very small (say 4) and watch it get cut off mid-sentence - that is the n_predict gate at work. Turning these flags by hand one by one and watching the effect against the generation loop beats memorizing each definition - what you build is muscle memory of "param to behavior" that carries to any model.

Deep dive: the story of reuse and "when to stop"

Finally two folds for two often-asked points: the backstory of cli reusing server's engine, and "when exactly generation counts as finished". The first is about "why the architecture evolved this way", the second about a detail you meet every run yet may not be able to explain. These two questions look trivial, yet each answers one of the two confusions that most often surface while reading source: one is "why is this code shaped this way" (history and trade-offs), the other "when exactly does it stop" (runtime behavior). Make them clear and you will not get lost when you open cli's real source.

1 Why does cli reuse server's engine? click to expand

In early llama.cpp, cli (then called main) and server each had their own generation loop: each calling llama_decode, each managing the KV, each handling stop conditions. The two bodies of code did highly overlapping things yet were maintained separately - change one piece of sampling logic and you had to remember to change both, easy to miss. Later the project extracted the engine into a shared server_context (the slot/task machinery) and let cli stand on it too: cli degenerates into a thin client that "opens one slot, feeds one sequence, streams it back". The gain is a single source of truth - generation logic has one implementation, a bug fixed once and a feature added once benefit cli and server together.

So when you see names like server_task and server_slot in cli, do not be surprised: they are not "server-only", they are "the engine's vocabulary". This also explains why cli is taught before server (L28) - meet the engine first in the simple command-line setting, and next lesson, seeing it serve many HTTP requests at once follows naturally.

2 Who decides "generation is done"? click to expand

The loop stops in three cases, each with a different trigger. First, length cap reached: the n_predict you set with -n counts to zero - "you" call stop. Second, the model calls stop itself: it generates an EOG (end-of-generation) token, such as </s> or some chat templates' <|im_end|> - cli tests it with llama_vocab_is_eog(vocab, id), echoing L20's "end-of-generation set". Third, reverse prompt hit: in interactive mode, the model is cut off just as it is about to generate up to your antiprompt, handing control back to you.

The priority and details of these three are the root of "why it sometimes stops early / why it will not stop": forget to set -n and meet a model unwilling to emit EOG, and it may generate forever; and if some model's EOG token is not correctly marked by the template, it too "cannot hit the brakes". Understand these three gates and you can control generation length to the point.

✅ Key points
  • llama-cli = a command-line/interactive shell over the shared inference engine: read stdin, drive generation, stream stdout - the most direct way to get started with llama.cpp.
  • Entry main.cpp (thin) -> cli.cpp; common_init + common_params_parse(..., LLAMA_EXAMPLE_CLI) turns the command line into common_params (L26).
  • The generation main loop: decode for logits -> common_sampler_sample picks a token -> common_sampler_accept feeds back -> common_token_to_piece streams print -> append back to context, round and round.
  • Three stop conditions: n_predict filled, EOG token (llama_vocab_is_eog), or reverse prompt (antiprompt) hit in interactive mode.
  • Current state: a modern cli reuses server's engine - #include "server-context.h" and links server-context, same engine as server with a different shell (cli=command-line, server=HTTP).
💡 Design insight
What this cli lesson really wants to leave you is not the use of some flag, but the architectural instinct of "separate the shell from the engine". The same server_context, wrapped in a command-line shell, is cli; wrapped in an HTTP shell, is server - the way you interact varies endlessly, the inference core stays one and the same. This idea of "make the stable core thick and the changing shell thin" continues the same thread as L25's "stable C ABI plus free interior" and L26's "stable outward plus convenient inward": good systems always work to tell apart "what should be unified and what should differ". Think this through, and you will stop seeing Part 5's remaining tools as isolated programs, and start seeing the one engine reused beneath them all - next lesson, we take it apart head-on. One last question worth chewing on: next time you design a system yourself, how do you decide "which part to make the stable core and which the swappable shell"? cli's answer is plain and strong - sink "the part all entry methods share" (the inference logic) into the core, and leave "the part each entry method differs in" (command line vs HTTP) on the shell. This seemingly simple dividing line fits most software that must support multiple entries: a web framework's routing vs business logic, a database's protocol layer vs storage engine - the same wisdom underneath. Internalize it as your own design instinct and you walk away with not just "how to use llama-cli", but a judgment that transfers to any project.

🧪 Self-test - think about the design

1. Which component's engine does a modern llama-cli reuse internally?
  1. it has a fully standalone bare llama_decode main loop, unrelated to server
  2. server-context (server_context): cli #includes server-context.h and links server-context, sharing server's engine with a different shell
  3. an engine provided by the Python bindings
  4. ggml's graph executor, bypassing the llama layer
Show answer & explanation click to expand
Answer: B. A modern cli no longer carries its own bare loop: cli.cpp #includes server-common.h / server-context.h / server-task.h and CMake links server-context, reusing server's server_context (slot/task). cli and server are 'same engine, different shells'.
2. What makes llama-cli's generation main loop stop?
  1. an operating-system timer interrupt decides
  2. any of three: n_predict filled, the model emits an EOG token, or a reverse prompt (antiprompt) is hit in interactive mode
  3. only one: it must wait for the model to emit EOG
  4. it always stops unconditionally after exactly 2048 tokens
Show answer & explanation click to expand
Answer: B. Three stop conditions: n_predict counting to zero (your length cap), an end-of-generation token detected by llama_vocab_is_eog (the model stopping itself), or hitting your reverse prompt in interactive mode. Forget -n and meet a model that will not emit EOG, and it may run forever.
3. How does llama-cli turn the command line into internal config?
  1. it reads a JSON config file; the command line is ignored
  2. cli hand-writes a big pile of if-else to parse argv itself
  3. llama_model_load_from_file parses the command line along the way
  4. common_params_parse(argc, argv, params, LLAMA_EXAMPLE_CLI) fills argv into common_params (L26)
Show answer & explanation click to expand
Answer: D. cli reuses common's arg parsing: common_params_parse picks cli's option set via the 4th argument LLAMA_EXAMPLE_CLI, calls each common_arg callback to write argv into common_params, then hands it to common_init_from_params to produce model+ctx+sampler.
💭 Open questions (no single right answer - just think or try)
  • Since cli and server reuse the same server_context engine and differ only in shell, explain: what does separating 'engine' from 'shell' buy you for maintenance and for adding features? If you had to write a gRPC version of a llama service, how would you structure it?