这是一个用C++从头写的本地大模型推理引擎,不依赖Python,能读你的文档(RAG),还能帮你写代码、存文件(Agentic工具),一条Docker命令就能跑起来。

一、先搞明白:我们到底在造什么?

1.1 项目定位

你可以把它理解成一个**“纯血C++版的本地的ChatGPT+文件助手”**。但它有几个特别之处:

  • 不依赖Python:现在很多本地大模型方案(比如Ollama、llama-cpp-python)底层虽然是C++,但外层都包了一层Python或者Go。这个项目是直接用C++调用llama.cpp的C API,省掉了Python解释器的开销。
  • 自带"记忆力"(RAG):你把个人简历、公司文档、笔记往data/文件夹一扔,它启动时自动读进去,聊天时就能引用这些内容。
  • 自带"动手能力"(Agentic):它不仅能聊天,还能按照你教的格式,在电脑上创建文件、写代码、保存日志——而且被关在output/目录里,不会乱动你系统。
  • Docker一键部署:编译好的静态二进制丢进容器,映射一个文件夹就能跑,连编译环境都不需要。

1.2 为什么不用Python?

很多人问:用Python不是更简单吗?

道理很简单:Python是胶水语言,C++是发动机语言。推理大模型时,真正的计算都在C++层(矩阵乘法、采样、内存管理),Python只是负责"传话"。但如果你的场景很固定——比如就是本地跑一个模型、读几个文档、偶尔写个文件——那Python这层"传话"反而成了累赘:

  • 启动慢(要加载Python解释器)
  • 内存占用多(Python对象头就几十字节)
  • 部署麻烦(要管Python版本、依赖库、虚拟环境)

所以这个项目选择直接用C++写死循环,模型加载、分词、采样、文件I/O全部自己管,干净利落。


二、核心知识点扫盲

在讲代码之前,先快速过一遍这个项目涉及的关键概念。搞懂这些,后面的设计思路你就都能看明白了。

2.1 llama.cpp:大模型的"C++心脏"

llama.cpp是Georgi Gerganov写的一个开源项目,简单说就是把Meta的LLaMA模型用纯C++重新实现了一遍。它的核心优势:

  • 无依赖:不需要PyTorch、CUDA Toolkit这些庞然大物,一个二进制文件就能跑。
  • 量化压缩:能把模型的16位浮点数权重压到4位、甚至2位,让70B的大模型在消费级显卡甚至CPU上跑起来。
  • 跨平台:x86、ARM、Mac、树莓派都能跑,甚至能编译成WebAssembly在浏览器里跑。

这个项目就是站在llama.cpp的肩膀上,直接调用它的C API来完成推理。

2.2 GGUF:模型的"集装箱"

GGUF(Georgi Gerganov Unified Format)是llama.cpp专用的一种模型格式。你可以把它想象成模型的集装箱:把权重、分词器、超参数、元数据全部打包成一个.gguf文件。

好处很明显:

  • 一个文件就是全部,不用像PyTorch模型那样拖家带口(config.json、tokenizer.json、一堆bin文件)。
  • 支持量化,文件体积能缩小到原来的1/4甚至1/8。
  • 加载快,因为内存映射(mmap)可以直接把文件映射进内存,不用全部读到RAM里。

2.3 RAG:给AI装一个"外接硬盘"

RAG全称Retrieval-Augmented Generation(检索增强生成)。这个概念现在很火,但本质很简单:

大模型的知识只截止到训练数据,RAG就是让它能实时查"外接硬盘"里的文档,再回答你。

具体到这个项目,它的RAG实现非常"原生":启动时扫描data/目录,把所有.txt文件读出来,拼接成一段系统提示词(System Prompt),塞给模型。比如:

你是Jarvis,一个本地AI助手。以下是你的知识库:
---
[文件1: 我的简历.txt]
姓名:张三,10年C++开发经验...

[文件2: 项目文档.txt]
本项目使用CMake构建...
---
请基于以上知识回答用户问题。

这种方式虽然简单(没有向量数据库、没有语义检索),但对于个人本地场景完全够用——你往data/里扔十几个文档,总字数可能也就几万字,直接全塞进上下文窗口就行。省去了 embedding 模型、向量数据库这一整套复杂架构。

2.4 Agentic Tool Use:让AI从"动嘴"到"动手"

Agentic(代理化)是2025-2026年AI领域的大趋势。简单说就是:不仅让AI生成文字,还要让它能调用工具、操作外部环境。

这个项目的Agentic能力聚焦在文件系统操作上。它设计了一套严格的语法协议,让模型在输出中嵌入"操作指令",比如:

<tool>write_file</tool>
<path>output/hello.cpp</path>
<content>
#include <iostream>
int main() {
    std::cout << "Hello, Jarvis!" << std::endl;
    return 0;
}
</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_modelllama_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=40
llama_sampler_chain_add(sampler, llama_sampler_init_temp(0.8));     // 温度=0.8
llama_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 通用对话,中文也不错 HuggingFaceLlama-3-8B-Instruct-GGUF
Qwen2.5-7B-Instruct Q4_K_M ~4.7GB 中文更强,代码能力好 ModelScopeQwen2.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/Linux
mkdir -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:latest

Windows用户注意:把路径里的 ~ 换成完整路径,比如 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——那些项目有团队维护、功能更全面。这个项目的价值在于**“极简主义"和"教育意义”**:

  1. 它证明了不用Python也能玩转LLM:对于很多C++开发者来说,这是很好的入门范例,展示了如何直接调用llama.cpp的C API。
  2. 它展示了"够用就好"的工程哲学:RAG不用向量数据库、Agentic不用复杂协议,在本地个人场景下,这种取舍是合理的。
  3. 它提供了完整的生产部署范本:从源码到Docker镜像,从本地调试到一键部署,这条路是通的。

如果你是个想深入理解LLM底层原理的开发者,或者你需要一个完全可控、不依赖外部服务、能操作本地文件的AI助手,这个项目值得一看。

Welcome to follow WeChat official account【程序猿编码


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐