全栈串联:训练、GGUF、推理服务与 RAG

全栈串联:训练、GGUF、推理服务与 RAG

「本系列第 16/17 章」,也是最后一章。请先通读第 00–15 章:00–11 分别对应 LLaMA Factory、llama.cpp / GGML 与本地服务细节,12–15 对应 LlamaIndex 0.14.23 的骨架、摄取、合成与 Agent。本章只做串联,不发明新 API,也不把某一段的默认值说成全栈唯一标准。命令能跑,仍然要以 00–15 章核对过的对象边界为准,不要在串联时“顺便”改回旧接口。

1. 为什么要先读 00–15

全栈最常见的失败不是“哪一条命令敲错”,而是把四个系统的职责叠在一起。Factory 负责权重怎么训、怎么导出成 HuggingFace 目录;llama.cpp 负责把目录变成 GGUF、量化、用 llama-server 提供 OpenAI 兼容 HTTP;LlamaIndex 负责文档怎么切、怎么检索、怎么合成。GGML 是推理后端的张量与量化基础,应用代码通常碰不到它,但你要理解:量化发生在 C++ 工具里,不发生在 Settings.llm 里。

第 00–11 章已经用各自仓库的真实入口说明了这些边界,例如 Factory 的 llamafactory-cli train / export、llama.cpp 的 convert_hf_to_gguf.pyllama-quantizellama-server。第 12–15 章则固定了 0.14.23 里不能再靠印象拼的事实:元包只带 core、OpenAI 与 nltk;Document(Node(BaseNode))Settings 是模块单例;ServiceContext 直接抛错;两条摄取路径不要叠用;UPSERTS 是 Pipeline 默认策略;stores_text 决定拓扑;ChatMode.REACT / OPENAI 已删除;记忆用 Memory.from_defaults;图用 PropertyGraphIndex;本地只走 OpenAILike / OpenAILikeEmbedding。本章把它们按一次落地的顺序接起来。

若某一跳报错,回到对应章节核对,而不是在全栈脚本上继续打补丁。训练导出对不上,查 Factory;GGUF 架构不支持,查转换脚本;HTTP 404 或模板回显,查 server 与 is_chat_model;检索来源不对,查摄取与 stores_text;答案慢,先数合成的 K 和对话是否改写追问。

2. 一条可以照着敲的路径

目标:用 Factory 做 LoRA SFT 并导出 HuggingFace 目录,转成 GGUF、量化,用两个 llama-server 分别提供 chat 与 embedding,再让 LlamaIndex 持久化索引并查询。路径里的示例文件名来自本系列已经核对过的 Factory / llama.cpp 章节,本机目录按实际替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1) SFT(在 LLaMA Factory 仓库根目录)
llamafactory-cli train examples/train_lora/qwen3_lora_sft.yaml

# 2) 合并 LoRA,导出 HF 目录(该示例真实输出 saves/qwen3_sft_merged)
llamafactory-cli export examples/merge_lora/qwen3_lora_sft.yaml

# 3) 转 GGUF(在 llama.cpp 仓库)
python convert_hf_to_gguf.py /path/to/saves/qwen3_sft_merged \
--outfile /models/qwen3-sft-f16.gguf --outtype f16

# 4) 量化
llama-quantize /models/qwen3-sft-f16.gguf /models/qwen3-sft-q4_k_m.gguf Q4_K_M

# 5a) chat::8080
llama-server -m /models/qwen3-sft-q4_k_m.gguf --host 127.0.0.1 --port 8080 -c 8192

# 5b) embedding::8081(换本机实际的 embedding GGUF)
llama-server -m /models/bge-m3.gguf --host 127.0.0.1 --port 8081 --embedding

curl http://127.0.0.1:8080/v1/models:8081/v1/models,确认模型 ID 再写入客户端。chat 与 embedding 必须分进程:一个权重既做生成又做向量,模板、批处理和 -c 都会互相干扰。然后在 Python 里只走 HTTP,不混用进程内 LlamaCPP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from llama_index.core import (
SimpleDirectoryReader, VectorStoreIndex, StorageContext, load_index_from_storage, Settings,
)
from llama_index.llms.openai_like import OpenAILike
from llama_index.embeddings.openai_like import OpenAILikeEmbedding

Settings.llm = OpenAILike(
model="Qwen2.5-7B-Instruct", # 以 /v1/models 返回值为准
api_base="http://127.0.0.1:8080/v1",
api_key="local-placeholder",
is_chat_model=True,
is_function_calling_model=False,
context_window=8192,
)
Settings.embed_model = OpenAILikeEmbedding(
model_name="bge-m3",
api_base="http://127.0.0.1:8081/v1",
api_key="local-placeholder",
)

docs = SimpleDirectoryReader("./data", recursive=True).load_data()
index = VectorStoreIndex.from_documents(docs) # 嵌入发生在 Index 内
index.storage_context.persist("./storage/local-kb")

index = load_index_from_storage(
StorageContext.from_defaults(persist_dir="./storage/local-kb"),
embed_model=Settings.embed_model,
)
print(index.as_query_engine(similarity_top_k=5).query("退货政策是什么?"))

这是一次性建库。需要增量时改走 IngestionPipeline,默认 UPSERTS,transformations 显式写成切分器加 embed model,不要再叠一次 from_documents()。需要工具时接 ReActAgent + AgentWorkflow,不要开已经删除的 ChatMode.REACT。多轮对话用 Memory.from_defaults,不要新写 ChatMemoryBuffer

导出阶段记住 Factory 的硬约束:合并 adapter 时不要带 quantization_bit;量化模型上不能再合并 adapter。量化留给 llama-quantizeconvert_hf_to_gguf.py 读的是导出目录,不是 adapter 文件夹本身。

3. 每一跳在守什么

把命令看成契约,而不是脚本彩排:

步骤 守住的不变量
trainexport 得到可被转换脚本识别的 HF 目录;示例输出在 saves/qwen3_sft_merged
F16 GGUF → llama-quantize 推理侧的体积与精度在这一跳决定,LlamaIndex 看不见 GGUF
两个 llama-server :8080 聊天、:8081 向量;-c 与客户端 context_window 对齐
from_documents / persist 文档向量已写入 simple store,加载不再向 :8081 重算;查询仍要嵌入问题
is_function_calling_model=False 本地默认走 ReAct,而不是假想中的原生 tools

换 embedding 模型或维数,必须新建 persist 目录。stores_text 为假的外部库不能走 from_vector_store()。合成模式按第 14 章的 K 估算调用次数:compact 在 repack 后仍可能多次 refine;condense_plus_context 有历史时还多一次改写。把这些次数加在 llama-server 的并发槽位上,才能解释“为什么一个用户问题打满了 GPU”。

健康检查失败时,先看端口和 /v1 前缀,再看模型 ID,最后才怀疑 LlamaIndex。输出像 prompt 回显,优先查 is_chat_model 和 instruct 模板。上下文溢出要同时核对 server -c、客户端窗口、chunk、top-k、历史和 max_tokens。客户端把 context_window 写成更大的数字,不会扩大服务端 KV。

4. 如何选路径

不必每次都走完整链条。按你缺的那一层裁剪,这是本系列作为课本而不是清单的原因:

  • 只验证数据格式、模板和损失是否有限: Factory 的短跑 trainchat 或 Board 即可,不必转 GGUF。
  • 只验证本地吞吐、槽位和 chat template: llama-server + curl,不必上 LlamaIndex。
  • 已有云端模型和托管向量库、只要 RAG: 跳过上面 1–5 步,直接安装 core 与对应集成;仍然遵守第 12–13 章的对象和摄取边界。
  • 本机闭环、要引用私有文档: 走本章命令,查询用 compact,多轮用 Memory.from_defaults
  • 要工具、增量更新或属性图: 第 15 章的 AgentWorkflowPropertyGraphIndex,摄取用 Pipeline 的 UPSERTS,不要开已废弃的 KnowledgeGraphIndex
  • 要多租户服务: 不要在请求里改 Settings;按租户注入 engine 与 Memory。这是第 12、15 章的工程结论,与是否本地推理无关。

选型时回到具体章节核对事实,而不是凭印象拼接 0.10 时代的教程:ServiceContext 已抛错,ChatMemoryBuffer 已 deprecated,ChatMode.REACT 已删除,pip install llama-index 不会装齐约 620 个集成。版本数字变了,先重读对应章的“会抛错 / 已删除 / 默认值”,再改自己的仓库。

若你带团队分工,也可以按章节切开责任:训练同学读 00–11 的 Factory 与导出约束;推理同学读 llama-server 与量化;应用同学读 12–15,只把 :8080 / :8081 当成两个 OpenAI 兼容端点。全栈负责人的工作是守住本章这张表,防止有人在应用进程里再拉起一份进程内 LlamaCPP,或在 Pipeline 之后又 from_documents 一次。

系列结束

第 00–16 章合在一起,是一条可复查的全栈课本:训练导出、GGUF 量化、HTTP 推理、RAG 编排。从哪一章切入,取决于你缺的是权重、服务还是检索质量。选定一条路径后,用该章已经出现过的命令和对象边界做最小验证,再向外扩存储、权限与 Agent。不要同时换模型、换切分、换合成模式和换 Agent,否则你无法判断是哪一跳坏了。

复习时不必按文件名从 00 扫到 16。可以倒着问三个问题:用户问题经过了几次 LLM 调用、文档向量是在哪一次写入的、生成请求打到了哪一个端口。答得出,全栈就接上了;答不出,就回到第 14 章数 K、第 13 章看摄取、第 12 章看 Settings,或回到 00–11 查导出与 server。正向阅读建立地图,反向提问用来验收。

落地验收建议只做三件事,且一次只动一件。第一,固定提示问 llama-server 的 chat 端口,确认模板与中文指令遵循。第二,对同一批文档建索引后立刻 persist、重启进程再 load_index_from_storage,确认查询不再重算文档向量、但问题向量仍会打到 :8081。第三,把同一句带代词的追问分别交给 as_query_enginecondense_plus_context,数清 LLM 调用次数是否符合第 14 章。三件事都稳定,再打开 Agent 或外部向量库。

本系列到此结束。若只记住一句话:先固定“权重在哪推理、向量在哪存储、嵌入发生在哪一步”,再谈 Prompt 与工具。三者未定,后面的技巧都是噪音。选路时宁肯裁掉整段链条,也不要同时开两条互相嵌入的摄取路径,或同时开 HTTP 与进程内两套本地推理。读完 00–15 再改生产配置,比对着搜索引擎拼旧参数更安全。旧博客里的类名可以当索引,不能当本版本的行为说明。

文章互动

阅读 --

留言

0 条留言

正在加载留言…