llama-cli 多半是你第一个真正跑起来的 llama.cpp 程序——一句 llama-cli -m model.gguf -p "从前有座山",模型就开始往下续写。它简单到几乎不用解释,可正因为简单,它是看清"一条命令如何变成满屏文字"的最佳样本。
这一课我们钻进它内部,把"解析命令行 + 一个生成循环"这套骨架拆开看;也借它看清 llama.cpp 的工具到底是怎么搭在 common(L26)和推理引擎之上的——工具课的"读法",从这一课开始定调。
还有一点要先剧透:现代的 llama-cli 已经不是一份独立的裸 llama_decode 主循环了,它直接复用了 llama-server 的那台引擎。所以这一课既是"上手第一课",也悄悄替下一课的 server 埋好了伏笔。
程序入口在 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 收进了一行。
common_params_parse 把 argv 按 LLAMA_EXAMPLE_CLI 填进 common_params。
common_init_from_params 一口气加载 model、建好 context、配好 sampler。
把提示词 tokenize 成 token 序列,喂进上下文当作"已生成"的开头。
decode -> 采样 -> 接回上下文,一圈圈转,直到撞上停止条件。
每选定一个 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",看它如何生成下一个词、并把它流式吐到屏幕上。
现在揭开开头那个剧透。如果你翻开 tools/cli/cli.cpp 的头部,会看到它直接 #include 了 server 的几个头文件——server-common.h、server-context.h、server-task.h;tools/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),区别只在"怎么把请求喂进去、怎么把结果递出来"。
从 argv / stdin 读 prompt,把生成的 token 流式打到 stdout;适合上手、脚本、交互对话。
从 HTTP 请求收 prompt,把结果按 OpenAI 兼容格式返回;适合多用户、做服务。
历史上 cli 曾是一份独立的 main.cpp 生成循环,后来才统一到这台共享引擎上——好处是少维护一份几乎重复的代码,引擎里修一个 bug,cli 和 server 两边都跟着受益。也正因为如此,下一课 server 的不少概念(slot、连续批处理)其实你在 cli 里已经"隔着壳"用上了,只是没察觉而已。
给命令行加上 -i,cli 就从"一次性续写"变成"来回对话":它会在你按回车后,把你的输入编码进上下文,再继续生成,如此一问一答。打断生成靠反向提示(antiprompt / reverse prompt)——你设一个字符串(比如 "User:"),模型一旦要生成到它,就停下来、把话筒交还给你。终端的着色、退格、中文输入,则由 L26 的 console 在背后撑着。
几个最常打交道的旗标值得对着主循环记一下:-n 限制最多生成多少 token(就是 n_predict,管循环转几圈),-c 设上下文窗口多大(n_ctx,呼应 L17/L19),--temp 调采样温度(L21,管挑词那一步),-i 进交互。把每个旗标和循环里的某一步对上号,你就能预测它到底改变了什么。
最后用两个折叠,补两个常被追问的点:cli 复用 server 引擎的来龙去脉,以及"到底什么时候算生成结束"。前者关乎"架构为什么这么演化",后者关乎一个你每次跑都会遇到、却未必说得清的细节。这两个问题看似琐碎,却分别对应着读源码时最常冒出的两种困惑:一种是"这段代码为什么长这样"(历史与权衡),一种是"它到底什么时候停"(运行时行为)。把它们说清楚,你再去翻 cli 的真实源码就不会被绕晕。
早期的 llama.cpp 里,cli(当时叫 main)和 server 各有一份生成循环:各自调 llama_decode、各自管 KV、各自处理停止条件。两份代码做的事高度重叠,却要分别维护——改一处采样逻辑,得记得两边都改,很容易漏。后来项目把引擎抽成共享的 server_context(slot/task 那套),让 cli 也站上去:cli 退化成"开一个 slot、喂一条序列、流式取回"的瘦客户端。好处是单一事实源——生成逻辑只剩一份实现,bug 修一处、特性加一处,cli 与 server 同时受益。
所以当你在 cli 里看到 server_task、server_slot 这些名字时不必奇怪:它们不是"server 专用",而是"引擎的词汇"。这也解释了为什么把 cli 放在 server(L28)前面讲——先在简单的命令行场景里见过这台引擎,下一课再看它如何同时服务多个 HTTP 请求,就顺理成章了。
循环停下来有三种情形,触发者各不相同。其一是长度到顶:你用 -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 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.
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.
common_params_parse fills argv into common_params per LLAMA_EXAMPLE_CLI.
common_init_from_params loads the model, builds the context, configures the sampler in one go.
Tokenize the prompt into a token sequence, fed in as the "already generated" opening.
decode -> sample -> append back, turning round and round until a stop condition hits.
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.
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.
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".
reads the prompt from argv / stdin, streams generated tokens to stdout; great for getting started, scripts, interactive chat.
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.
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.
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.
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.
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.