把大模型跑在自己机器上,再用它驱动一个能调用本地工具的小智能体,是端侧 AI 里比较实用的一个闭环。这篇教程以 LFM2.5-24B-A2B 为例,走一遍「拉权重 → 量化 → 本地起服务 → 接工具 → 验证 → 排错 → 维护」的全流程。模型仓库地址、具体版本号这些会变动的信息,以官方文档和官方模型卡当前版本为准。
适用场景
适合手上有单张消费级或入门级专业显卡、或者一台大内存 Mac / 工作站,希望数据不出本机就跑智能体的开发者。典型用途是本地文档问答、内网工具调用、离线环境下的自动化脚本编排,以及给端侧应用做原型验证。如果你的诉求是超高并发在线服务,这套方案不是首选,直接上集群化的推理框架更合适。
LFM2.5-24B-A2B 这个名字里的关键信息是:总参数规模 24B,激活参数约 2B。这通常意味着它是稀疏混合专家(MoE)结构——权重需要全部装进内存,但每个 token 实际参与计算的部分很小。带来的直接好处是生成速度快、单机延迟低;代价是显存占用按总参数量算,不会因为「A2B」而省内存。具体结构、上下文长度、是否原生支持工具调用,请以官方模型卡为准。
环境与前置条件
操作系统:Linux(Ubuntu 系列较省心)或 macOS。Windows 建议走 WSL2,原生 Windows 跑 llama.cpp 也可以,但工具链踩坑概率更高。
运行时:
- Python 3.10 或更新版本(以所用推理框架官方文档为准)
- Git、CMake、C++ 编译器
- NVIDIA 环境需要可用的 CUDA Toolkit 与匹配的显卡驱动;Apple Silicon 走 Metal,无需额外安装
硬件建议(以下为按通用经验估算,实际以官方模型卡为准):
- 4bit 量化后的权重体积大致可以用「参数量 × 0.5~0.6 字节」估算,24B 量级大约落在 12~16 GB 区间,所以显存或统一内存至少准备 16 GB 起,留出 KV 缓存后建议 24 GB 以上。
- 如果要保留 bf16 / fp16 原始权重用于自行量化,按「参数量 × 2 字节」估算约 48 GB 量级,加上量化产物和中间文件,磁盘建议预留 100 GB 以上。
- CPU 也能跑,但 24B 权重的内存带宽压力大,首 token 延迟会明显上升;端侧低延迟场景优先用 GPU 或统一内存架构。
磁盘与网络:权重文件几十 GB,下载中断很常见,提前规划好目录并确认剩余空间。
分步骤部署
第 1 步:建立工作目录,锁定模型标识
先建一个干净目录,并把模型仓库标识写成变量,后续命令直接引用,避免手抄出错。
```bash
mkdir -p ~/lfm-agent && cd ~/lfm-agent
mkdir -p models gguf logs
从官方模型卡复制准确的仓库 ID,不要凭记忆拼写
export MODEL_ID="<官方模型卡上的仓库ID>"
```
这一步没有输出就是成功。后续所有命令都假定在当前目录下执行。
第 2 步:下载模型权重
用 Hugging Face 官方 CLI 拉取。部分网络环境下可以配置镜像端点,是否可用以你的实际网络为准。
```bash
pip install -U "huggingface_hub[cli]"
可选:网络受限时设置镜像端点
export HF_ENDPOINT=https://hf-mirror.com
hf download "$MODEL_ID" \
--local-dir models/raw \
--exclude "*.pth" "*.msgpack" "*.h5"
```
--exclude 用来跳过训练框架专用的冗余权重,只留 safetensors 和配置。成功标志是 models/raw 下出现 config.json、tokenizer.json、若干 .safetensors 文件,且 hf download 最终打印出本地路径。
顺带看一下 config.json 里的 num_experts、num_experts_per_tok、max_position_embeddings 三个字段,它们决定后面该给多少显存、开多大上下文。
第 3 步:编译推理引擎
以 llama.cpp 为主线,它对 MoE 模型的量化和低配硬件部署都比较友好。
```bash
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp
NVIDIA 显卡
cmake -B build -DGGML_CUDA=ON
Apple Silicon 用:cmake -B build -DGGML_METAL=ON
纯 CPU 用:cmake -B build
cmake --build build --config Release -j
```
编译成功的标志是 build/bin/ 下出现 llama-server、llama-cli、llama-quantize 等可执行文件。注意不同版本里这些二进制可能叫 server、quantize,如果找不到就 ls build/bin 看一下实际名字,具体以官方文档当前版本为准。
第 4 步:转换并量化到 4bit
如果官方或社区已经提供了与你的引擎版本兼容的量化文件,直接下载能省掉几十分钟,但要用校验和确认文件完整。自己量化则分两步:
```bash
cd ~/lfm-agent/llama.cpp
1) 转成 GGUF(bf16 中间格式)
python convert_hf_to_gguf.py ../models/raw \
--outfile ../gguf/model-bf16.gguf \
--outtype bf16
2) 量化到 4bit
./build/bin/llama-quantize \
../gguf/model-bf16.gguf \
../gguf/model-q4_k_m.gguf \
Q4_K_M
```
convert_hf_to_gguf.py 会在最后打印词表和超参摘要;llama-quantize 结束时会输出量化后文件大小和各张量的量化类型分布。Q4_K_M 是质量与体积的常规折中点,显存紧张可以退到 Q4_K_S,追求效果可以上 Q5_K_M 或 Q6_K。
如果显存实在吃紧,量化完还可以考虑对 KV 缓存做量化,这个在起服务时用参数控制,不需要重新量化权重。
第 5 步:启动本地 OpenAI 兼容服务
```bash
cd ~/lfm-agent
./llama.cpp/build/bin/llama-server \
-m gguf/model-q4_k_m.gguf \
-c 8192 \
-ngl 999 \
-ctk q8_0 -ctv q8_0 \
--flash-attn \
--jinja \
--host 127.0.0.1 --port 8080 \
--metrics \
> logs/server.log 2>&1 &
```
各参数在做什么:
-c 8192:上下文长度。端侧智能体不要一上来就开大,KV 缓存随上下文线性增长,8192 通常够用。-ngl 999:尽量把所有层放到 GPU。显存不够就往下调,比如 40、30,直到能起来。-ctk / -ctv q8_0:KV 缓存量化,能省不少显存,质量损失一般可接受。--jinja:启用模型的 chat template,这一步对工具调用至关重要,没有它模型可能只会输出一段描述文字而不是结构化的tool_calls。--metrics:暴露 Prometheus 指标,方便后面做监控。
成功标志:日志末尾出现类似 server is listening on http://127.0.0.1:8080 的行。如果启用了 GPU,还会打印 offload 到显存的层数。
如果没有 GPU 或想跑高并发服务,也可以改用 vLLM:
```bash
pip install vllm
vllm serve models/raw --port 8080 --max-model-len 8192
```
第 6 步:验证接口可用
```bash
curl -s http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "local",
"messages": [{"role": "user", "content": "用一句话说明什么是端侧智能体"}],
"temperature": 0.2,
"max_tokens": 128
}' | head -c 600
```
返回 JSON 里 choices[0].message.content 有正常中文回答,就说明服务通了。model 字段填什么都行,本地引擎一般不校验。
第 7 步:跑通工具调用闭环
新建 agent.py,让模型在需要时调用本地函数。这里的核心是工具必须真的在本机执行,而不是让模型自己编答案。
```python
import json, time, datetime
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="sk-local")
MODEL = "local"
def get_time(tz: str = "Asia/Shanghai") -> str:
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def calc(expr: str) -> str:
allowed = set("0123456789+-*/(). ")
if not set(expr) <= allowed:
return "表达式含不支持的字符"
try:
return str(eval(expr, {"__builtins__": {}}, {}))
except Exception as e:
return f"计算失败:{e}"
FUNCS = {"get_time": get_time, "calc": calc}
TOOLS = [
{"type": "function", "function": {
"name": "get_time",
"description": "获取本机当前时间",
"parameters": {"type": "object",
"properties": {"tz": {"type": "string", "description": "时区名"}},
"required": []}}},
{"type": "function", "function": {
"name": "calc",
"description": "计算四则运算表达式,例如 128*64",
"parameters": {"type": "object",
"properties": {"expr": {"type": "string"}},
"required": ["expr"]}}},
]
SYSTEM = "你是运行在本机的端侧智能体。需要当前时间或数学计算时,必须调用工具,不要凭记忆回答。"
def run_agent(question: str, max_steps: int = 5):
messages = [{"role": "system", "content": SYSTEM},
{"role": "user", "content": question}]
for _ in range(max_steps):
t0 = time.time()
resp = client.chat.completions.create(
model=MODEL, messages=messages, tools=TOOLS,
tool_choice="auto", temperature=0.2)
print(f"[耗时 {time.time()-t0:.2f}s]")
msg = resp.choices[0].message
asst = {"role": "assistant", "content": msg.content or ""}
if msg.tool_calls:
asst["tool_calls"] = [
{"id": c.id, "type": "function",
"function": {"name": c.function.name,
"arguments": c.function.arguments}}
for c in msg.tool_calls]
messages.append(asst)
if not msg.tool_calls:
return msg.content
for c in msg.tool_calls:
fn = FUNCS.get(c.function.name)
args = json.loads(c.function.arguments or "{}")
out = fn(**args) if fn else f"未知工具:{c.function.name}"
print(f" 调用 {c.function.name}({args}) -> {out}")
messages.append({"role": "tool",
"tool_call_id": c.id,
"content": str(out)})
return "达到最大步数仍未收敛"
if __name__ == "__main__":
print(run_agent("现在几点了?顺便帮我算一下 128*64 等于多少"))
```
运行:
```bash
pip install -U openai
python agent.py
```
成功的输出形态是:先打印一次耗时,再打印两行 调用 get_time(...) 和 调用 calc(...),然后第二次请求返回一句把时间和计算结果都说清楚的自然语言答案。整个链路里,时间是本机真实时间,计算是本机真实计算——这就是端侧智能体的最小闭环。
第 8 步:压延迟的几个常规动作
1. 精简 system prompt。系统提示每次都进上下文,越短首 token 越快。
2. 固定前缀,复用缓存。把不变的系统提示和工具定义放在消息列表最前面,服务端的 prompt cache 才能命中。
3. 开流式输出。stream=True,用户感知的首字延迟会明显下降,体感快很多。
4. 控制 max_tokens。工具调用场景下回答通常很短,给 256~512 就够,别让它自由发挥。
5. 调低上下文。-c 从 8192 降到 4096,KV 缓存直接减半。
验证部署是否成功
按三个层次依次确认:
第一层,服务存活。 curl -s http://127.0.0.1:8080/health 返回 200,或者直接看日志最后一行是不是监听成功。
第二层,单轮推理正常。 第 6 步的 curl 能拿到合理回答,且响应用时在秒级。
第三层,工具闭环成立。 python agent.py 输出中同时出现工具调用记录和最终自然语言答案。再补一个负向用例:
```bash
python -c "from agent import run_agent; print(run_agent('你好,介绍一下你自己'))"
```
这个用例不应该触发任何工具,如果它莫名其妙调用了 calc,说明系统提示或工具描述写得太模糊,需要收紧。
常见报错与解决
报错 1:CUDA out of memory 或 ggml_cuda_host_malloc: failed to allocate
原因:权重加 KV 缓存超过了可用显存。
解决:先降 offload 层数,再降上下文,最后才考虑换更小的量化档。
```bash
把 -ngl 从 999 降到能起的数值,例如 35
同时把 -c 从 8192 降到 4096
./llama.cpp/build/bin/llama-server -m gguf/model-q4_k_m.gguf -c 4096 -ngl 35 \
-ctk q8_0 -ctv q8_0 --no-kv-offload --port 8080
```
报错 2:error: unknown argument: --jinja
原因:llama.cpp 版本过旧,该参数在旧版不存在。
解决:更新源码后重新编译。
```bash
cd ~/lfm-agent/llama.cpp
git pull
cmake --build build --config Release -j
```
重新编译前先确认当前分支能正常构建,升级后要跑一遍第 6、7 步的回归用例。
报错 3:模型返回一大段文字,tool_calls 始终为空
原因:服务端没有正确应用 chat template,模型不知道要输出结构化调用。
解决:确认启动参数里有 --jinja;若仍无效,改用显式提示约束输出 JSON,再自行解析。
```bash
重启服务时确保带上 --jinja
若模板本身不支持工具调用,可以在 system 里要求:
"需要调用工具时,只输出 {\"tool\": \"名称\", \"args\": {...}} 这一个 JSON 对象"
```
报错 4:Connection refused 或端口被占用
原因:服务没起来,或 8080 已被别的进程占用。
解决:
```bash
ss -ltnp | grep 8080
换端口重启
./llama.cpp/build/bin/llama-server -m gguf/model-q4_k_m.gguf -c 4096 -ngl 999 --port 8081
```
客户端 base_url 记得同步改成新端口。
报错 5:hf download 中途断流或校验失败
原因:网络抖动导致分片文件不完整,或磁盘空间不足。
解决:确认剩余空间后重新执行下载命令,CLI 默认会续传已下载的分片。
```bash
df -h ~/lfm-agent
hf download "$MODEL_ID" --local-dir models/raw
```
后续维护
备份。 需要长期保留的是三样东西:量化后的 GGUF 文件、推理引擎的编译产物或版本记录、以及你的智能体代码(尤其是 system prompt 和工具定义)。权重文件大,可以只保留量化版本,原始 bf16 权重按需重新下载即可。给每个量化文件记一份 sha256sum,机器迁移后先校验再用。
升级。 推理引擎的更新频率高于模型本身,升级后一定要重跑第 6、7 步的验证,重点看工具调用是否还正常——chat template 的处理逻辑在不同版本间有过变化。模型权重升级时,先小流量并行跑一段时间,对比同一批问题的回答质量与耗时,确认无回归再切换。升级前保留一份已知可用的引擎版本和量化文件,回退成本最低。
日志。 本次配置已经把服务输出重定向到 logs/server.log。建议配合 logrotate 做切分,避免长期运行把磁盘写满。用 systemd 托管更省事:
```ini
/etc/systemd/system/lfm-agent.service
[Unit]
Description=LFM local inference server
After=network.target
[Service]
User=youruser
WorkingDirectory=/home/youruser/lfm-agent
ExecStart=/home/youruser/lfm-agent/llama.cpp/build/bin/llama-server \
-m /home/youruser/lfm-agent/gguf/model-q4_k_m.gguf \
-c 8192 -ngl 999 -ctk q8_0 -ctv q8_0 --jinja \
--host 127.0.0.1 --port 8080 --metrics
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
```
启用:sudo systemctl daemon-reload && sudo systemctl enable --now lfm-agent,之后用 journalctl -u lfm-agent -f 看日志。
监控。 启动时带了 --metrics,可以直接抓 http://127.0.0.1:8080/metrics,关注队列长度和单次请求耗时。硬件侧用 nvidia-smi dmon(NVIDIA)或 sudo powermetrics(macOS)看显存占用和功耗曲线。建议额外记录两类业务指标:工具调用成功率(有多少次 tool_calls 最终执行成功)和端到端延迟的 P95。这两个数字比单纯看 token/s 更能反映端侧智能体是否真的可用。
