<tool>write_file</tool><path>output/hello.cpp</path><content>include <iostream>int main() { std::cout << "Hello, Jarvis!" << std::endl; return 0;}</iostream></content>引擎解析到这段标记后,就会在output/目录下创建文件。为了安全,路径被严格限制在output/内,防止AI"手滑"删你系统文件。
2.5 量化(Quantization):让大模型"减肥"
原始的大模型权重通常是16位浮点数(FP16),一个7B模型就要占14GB内存。量化就是把它压缩成低精度数字:
Q4_K_M:4位量化,压缩率约1/4,质量损失很小,是最常用的"甜点"方案。
Q8_0:8位量化,压缩率1/2,质量几乎无损,适合对精度要求高的场景。
这个项目支持任何GGUF格式的模型,所以你完全可以根据你的显存大小选择不同量化级别的模型。
2.6 Docker与静态链接:部署的"终极形态"
项目用Docker做最终交付,而且采用了多阶段构建+静态链接:
Builder阶段:在一个带编译环境的容器里,用CMake+G++编译出静态链接的二进制。
Runtime阶段:用一个极简的Linux基础镜像(比如Alpine或者scratch),只把静态二进制和必要的运行时文件拷进去。
这样做的好处是最终镜像极小,而且不依赖宿主机的任何库,真正做到"一次编译,到处运行"。
---
三、设计思路:为什么要这么造?
理解了上面的知识点,现在来看看这个项目的整体架构设计。我把它总结成**"三层设计"**。
3.1 第一层:推理层(C++原生)
设计决策:直接调用llama.cpp C API,不绕Python。
原因前面说了,为了性能和部署简洁。但这一层的设计有几个关键点:
内存自己管:C++没有Python的垃圾回收,模型权重、KV缓存、上下文内存都需要手动分配和释放。项目用RAII模式(资源获取即初始化)封装了llama_model和llama_context的生命周期,防止内存泄漏。
采样策略可配置:温度(Temperature)、Top-K、Top-P这些采样参数直接影响生成文本的"随机性"。项目把这些参数暴露成配置项,让用户可以调整AI的"性格"。
流式输出:不是等模型全部生成完再显示,而是每生成一个token就输出,这样用户体验更接近ChatGPT的打字机效果。
3.2 第二层:知识层(原生RAG)
设计决策:不用向量数据库,直接文本拼接进System Prompt。
这个决策可能看起来"土",但其实是针对本地个人场景的精准取舍:
个人文档通常不大:你的简历、几篇笔记、一些代码文档,加起来可能也就几千到几万token。而现在的模型(如Llama-3-8B)上下文窗口有8K甚至128K,完全装得下。
省去Embedding和向量检索的复杂度:不用装Sentence-Transformers、不用FAISS/Chroma、不用算余弦相似度。启动时读一遍文件,拼成字符串,完事。
检索精度反而高:因为所有文档都在上下文里,模型自己就能"看到"全部内容,回答时不会漏掉关键信息(而向量检索有时会把关键段落漏掉)。
当然,这种方案的局限也很明显:如果文档超过上下文窗口,就需要更复杂的分块和检索策略。但对于个人本地使用,这是性价比最高的方案。
3.3 第三层:工具层(Agentic文件操作)
设计决策:用XML-like标记协议,让模型输出结构化指令。
为什么不用Function Calling(OpenAI风格)或者JSON Schema?因为:
简单可靠:Function Calling需要模型原生支持,而且要在分词层面做特殊处理。而用一个简单的文本标记协议(如<tool>write_file</tool>),任何指令微调过的模型都能理解。
易于解析:C++里解析XML比解析JSON稍微麻烦点,但用简单的字符串查找(find/substr)就能搞定,不需要引入第三方库。
安全可控:通过正则或路径检查,确保所有文件操作都被限制在output/目录内。
---
四、代码实现原理与流程图
好了,知识点和设计思路都讲完了,现在来看看代码到底是怎么跑的。下面的流程图和伪代码是基于项目架构和llama.cpp标准API的合理推断,帮你理解核心逻辑。
4.1 整体架构流程图
┌─────────────────────────────────────────────────────────────┐│ 启动阶段 ││ 1. 解析命令行参数(模型路径、温度、上下文长度等) ││ 2. 扫描 data/ 目录,读取所有 .txt 文件 ││ 3. 拼接 RAG 知识库文本 → 生成 System Prompt ││ 4. 调用 llama.cpp API 加载 GGUF 模型 ││ 5. 初始化采样器(Sampler)和 KV 缓存 │└─────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ 交互循环 ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ ││ │ 用户输入 │───│ 组装 Prompt │───│ Tokenize(分词) │ ││ └─────────────┘ └─────────────┘ └─────────────────┘ ││ ││ ▼│ ┌─────────────────────────────────────────────────────────┐ ││ │ 推理循环(Decode) │ ││ │ while (token != EOS) { │ ││ │ 1. llama_decode() // 前向传播,预测下一个token │ ││ │ 2. llama_sampler_sample() // 采样(Top-K/Temp) │ ││ │ 3. 输出token到屏幕 │ ││ │ 4. 检查是否包含工具调用标记 │ ││ │ } │ ││ └─────────────────────────────────────────────────────────┘ ││ ││ ▼│ ┌─────────────────────────────────────────────────────────┐ ││ │ 后处理阶段 │ ││ │ if (检测到 <tool>write_file</tool>) { │ ││ │ 解析 <path> 和 <content> │ ││ │ 安全检查:路径是否在 output/ 目录内? │ ││ │ 写入文件 → 返回成功提示给模型 │ ││ │ } │ ││ └─────────────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────┘4.2 核心模块拆解
模块A:模型加载与初始化
这是程序的"发动机启动"阶段。llama.cpp提供了一套C API,典型的初始化流程如下:
// 1. 初始化模型参数llama_model_params model_params = llama_model_default_params();model_params.n_gpu_layers = 99; // 尽可能把层放到GPU上// 2. 加载GGUF模型llama_model* model = llama_load_model_from_file("model.gguf", model_params);// 3. 初始化上下文参数llama_context_params ctx_params = llama_context_default_params();ctx_params.n_ctx = 4096; // 上下文窗口大小ctx_params.n_batch = 512; // 一次处理多少token// 4. 创建推理上下文llama_context* ctx = llama_new_context_with_model(model, ctx_params);// 5. 初始化采样器(控制输出的随机性)llama_sampler* sampler = llama_sampler_chain_init(llama_sampler_chain_default_params());llama_sampler_chain_add(sampler, llama_sampler_init_top_k(40)); // Top-K=40llama_sampler_chain_add(sampler, llama_sampler_init_temp(0.8)); // 温度=0.8llama_sampler_chain_add(sampler, llama_sampler_init_dist(1234)); // 随机种子关键点:n_gpu_layers决定多少层放到GPU上。如果显存够,设成99(实际是全部);如果显存不够,减少这个值,剩下的层在CPU上跑。
模块B:RAG知识库加载
这是项目的"记忆植入"阶段。逻辑非常简单直接:
std::string build_rag_prompt(const std::string& data_dir) { std::string knowledge; // 遍历 data/ 目录下的所有 .txt 文件 for (const auto& entry : std::filesystem::directory_iterator(data_dir)) { if (entry.path().extension() == ".txt") { std::ifstream file(entry.path()); std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); // 拼接文件名和内容 knowledge += "[文件: " + entry.path().filename().string() + "]\\n"; knowledge += content + "\\n\\n"; } } // 组装成System Prompt return "你是Jarvis,一个本地AI助手。以下是你的知识库:\\n---\\n" + knowledge + "---\\n请基于以上知识回答用户问题。";}设计亮点:没有引入任何外部依赖(如向量数据库、Embedding模型),纯C++标准库搞定。启动时一次性读取,运行时零开销。
模块C:推理主循环
这是项目的"思考过程",也是llama.cpp最核心的使用方式:
// 组装完整的对话Prompt(System + User + Assistant前缀)std::string full_prompt = system_prompt + "\\n\\nUser: " + user_input + "\\nAssistant: ";// 分词:把字符串转成token数组std::vector<llama_token> tokens;// ... 调用 llama_tokenize 进行分词 ...// 把token输入到模型llama_decode(ctx, llama_batch_get_one(tokens.data(), tokens.size()));// 生成循环std::string response;for (int i = 0; i < max_tokens; i++) { // 采样得到下一个token llama_token new_token = llama_sampler_sample(sampler, ctx, -1); // 如果生成结束标记,跳出 if (llama_token_is_eog(model, new_token)) break; // 把token转回字符串,输出 char buf[256]; int n = llama_token_to_piece(model, new_token, buf, sizeof(buf), 0, true); std::string piece(buf, n); response += piece; std::cout << piece << std::flush; // 流式输出 // 把这个新token喂回模型,继续预测下一个 llama_decode(ctx, llama_batch_get_one(&new_token, 1));}关键点:llama_decode是前向传播函数,每次调用都会更新KV缓存(Key-Value Cache),这样模型就能"记住"前面的对话内容。
模块D:Agentic工具解析与执行
这是项目的"动手能力",让AI从说话变成做事:
void process_agentic_tools(const std::string& response) { // 查找工具调用标记 size_t tool_pos = response.find("<tool>write_file</tool>"); if (tool_pos == std::string::npos) return; // 提取路径 size_t path_start = response.find("<path>", tool_pos); size_t path_end = response.find("</path>", path_start); std::string filepath = response.substr(path_start + 6, path_end - path_start - 6); // 安全检查:确保路径在 output/ 目录下 if (filepath.find("..") != std::string::npos || filepath.find("/output/") != 0) { std::cerr << "非法路径,拒绝执行!" << std::endl; return; } // 提取内容 size_t content_start = response.find("<content>", path_end); size_t content_end = response.find("</content>", content_start); std::string content = response.substr(content_start + 9, content_end - content_start - 9); // 写入文件 std::ofstream out(filepath); out << content; out.close(); std::cout << "[系统] 已创建文件: " << filepath << std::endl;}安全设计:路径检查是重中之重。通过禁止..和强制前缀/output/,确保AI只能在自己的"沙盒"里操作。
4.3 数据流全景图
为了更直观地理解,下面是用户从输入到得到输出的完整数据流:
用户输入: "帮我写个Hello World程序,保存到文件" │ ▼┌──────────────────┐│ Prompt组装器 ││ System: [RAG知识]││ User: 用户输入 ││ Assistant: │└──────────────────┘ │ ▼┌──────────────────┐│ Tokenizer分词 ││ "帮"→[token_123] ││ "我"→[token_456] ││ ... │└──────────────────┘ │ ▼┌──────────────────┐│ llama_decode ││ 前向传播×N次 ││ KV缓存更新 │└──────────────────┘ │ ▼┌──────────────────┐│ Sampler采样 ││ Top-K筛选 ││ Temperature随机 │└──────────────────┘ │ ▼流式输出: "好的,我帮你写..." │ ▼┌──────────────────┐│ Agentic解析器 ││ 检测到write_file ││ 提取路径+内容 ││ 安全检查通过 ││ 写入output/目录 │└──────────────────┘ │ ▼返回结果: "已保存到 output/hello.cpp"---
If you need the complete source code, please add the WeChat number (c17865354792)
五、Docker一键跑
第1步:准备模型文件
先去 HuggingFace 或 ModelScope 下载一个 GGUF 格式的模型。推荐这几个对新手友好的:
模型 | 大小 | 适用场景 | 下载地址 |
| Llama-3-8B-Instruct Q4_K_M | ~4.9GB | 通用对话,中文也不错 | HuggingFace 搜 Llama-3-8B-Instruct-GGUF |
| Qwen2.5-7B-Instruct Q4_K_M | ~4.7GB | 中文更强,代码能力好 | ModelScope 搜 Qwen2.5-7B-Instruct-GGUF |
| Phi-4 Q4_K_M | ~4.1GB | 小钢炮,低显存也能跑 | HuggingFace 搜 Phi-4-GGUF |
选模型的小技巧:文件名带 Q4_K_M 的是"甜点配置"——体积只有原版的1/4,但智商损失很小。显存8GB以下选这个,16GB以上可以试 Q8_0。
下载后把 .gguf 文件放到你找得到的地方,比如:
~/Downloads/llama-3-8b-instruct.Q4_K_M.gguf第2步:准备知识库文件夹
在你电脑上建两个文件夹:
# macOS/Linuxmkdir -p ~/Desktop/Jarvis_Output # AI写文件会出现在这里mkdir -p ~/Desktop/My_Knowledge # 你的文档扔这里# Windows (PowerShell)mkdir "$env:USERPROFILE\Desktop\Jarvis_Output"mkdir "$env:USERPROFILE\Desktop\My_Knowledge"往 My_Knowledge 里丢几个 .txt 文件测试,比如建一个 我的简历.txt:
姓名:张三技能:C++、Python、Docker爱好:折腾本地大模型第3步:一条命令启动
docker run -it --rm \ -e TZ="Asia/Shanghai" \ -v ~/Desktop/Jarvis_Output:/app/output \ -v ~/Desktop/My_Knowledge:/app/data \ -v ~/Downloads/llama-3-8b-instruct.Q4_K_M.gguf:/app/models/model.gguf \ hitesh917/jarvis-cpp:latestWindows用户注意:把路径里的 ~ 换成完整路径,比如 C:\Users\你的用户名\Desktop\...,并且用 PowerShell 运行,或者把反斜杠 \ 改成双反斜杠 \\。
第4步:测试对话
启动成功后,你会看到一个命令行交互界面。试试这几类问题:
测试1:基础对话
User: 你好,你是谁?→ 应该能正常自我介绍,提到它是 Jarvis 助手。
测试2:RAG知识库(关键测试)
User: 根据我的简历,我有什么技能?→ 如果 RAG 生效,它会提到"张三"、"C++"、"Python"这些你写在 我的简历.txt 里的内容。如果没提到,说明 data/ 挂载有问题。
测试3:Agentic文件操作(核心测试)
User: 帮我写一个Python的Hello World程序,保存到文件→ 观察输出,如果看到类似 <tool>write_file</tool> 的标记,然后你去 ~/Desktop/Jarvis_Output 看,应该出现了一个 .py 文件。
六、知识要点总结
看完上面的内容,我们来总结一下这个项目涉及的核心知识领域:
知识领域 | 关键要点 | 项目中的应用 |
| C++系统编程 | RAII、内存管理、文件I/O、字符串处理 | 整个引擎的骨架,手动管理模型生命周期 |
| LLM推理原理 | 自回归生成、KV缓存、采样策略(Top-K/Temp) | 调用llama.cpp API完成token-by-token生成 |
| 模型量化 | GGUF格式、Q4_K_M/Q8_0等量化方案 | 支持任意GGUF模型,让用户根据硬件选模型 |
| RAG架构 | 检索增强生成、上下文注入、向量检索(本项目未用) | 用"全文注入"方案实现轻量级RAG |
| Agentic AI | 工具调用、Function Calling、安全沙盒 | XML标记协议+路径限制实现文件操作 |
| Docker容器化 | 多阶段构建、静态链接、Volume映射 | 一键部署,隔离运行环境 |
| CMake构建系统 | 跨平台编译、依赖管理、编译优化 | 管理llama.cpp子模块和项目编译 |
---
总结
说实话,这个项目不是要做下一个Ollama或者vLLM——那些项目有团队维护、功能更全面。这个项目的价值在于**"极简主义"和"教育意义"**:
它证明了不用Python也能玩转LLM:对于很多C++开发者来说,这是很好的入门范例,展示了如何直接调用llama.cpp的C API。
它展示了"够用就好"的工程哲学:RAG不用向量数据库、Agentic不用复杂协议,在本地个人场景下,这种取舍是合理的。
它提供了完整的生产部署范本:从源码到Docker镜像,从本地调试到一键部署,这条路是通的。
如果你是个想深入理解LLM底层原理的开发者,或者你需要一个完全可控、不依赖外部服务、能操作本地文件的AI助手,这个项目值得一看。
Welcome to follow WeChat official account【程序猿编码】
---
</llama_token></content></path>
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!