1. 端侧 Agent 到底是个什么东西
先把概念钉死。端侧 Agent,说白了就是把一个能自主感知、决策、行动的智能体,完整跑在用户本地设备上——手机、PC、车机、IoT 模组,甚至一块开发板。它跟云端 Agent 最大的区别不在于模型大小,而在于数据不出端、推理不依赖网络、响应延迟可控。
我最早接触这个概念是在做一个离线语音助手项目的时候。当时团队第一反应是调云端 API,结果一算账:每次唤醒都要走网络,延迟 800ms 起步,用户说句话要等两秒才有反应,体验直接崩了。后来换成端侧方案,把一个小参数量的 LLM 量化后塞进设备,配合本地意图识别和工具调用,端到端延迟压到了 300ms 以内。从那以后我就认准了一个理:不是所有 Agent 都需要 GPT-4 级别的脑子,很多场景下“够用且快”比“聪明但慢”重要得多。
端侧 Agent 的核心架构可以拆成四层:感知层、认知层、决策层、执行层。感知层负责接收多模态输入(语音、图像、传感器数据),认知层用 LLM 做语义理解和上下文管理,决策层基于 ReAct 范式做推理和工具选择,执行层调用本地 API 或硬件接口完成任务。这四层不是串行流水线,而是带反馈回路的闭环系统。
适合谁来读这篇?如果你正在做端侧 AI 硬件部署、Agent 开发,或者单纯想搞清楚“LLM 怎么在资源受限设备上跑起来”,那接下来的内容应该能帮你省掉不少试错时间。我会从架构设计讲到实操细节,包括模型选型、量化策略、ReAct 循环实现、内存管理这些硬骨头。
注意:端侧 Agent 不是“把云端 Agent 缩小版搬过来”,两者的设计哲学完全不同。云端可以堆算力、堆参数,端侧必须做减法,每一个字节的内存和每一毫秒的延迟都要抠。
2. 基础架构拆解:四层模型怎么落地
2.1 感知层:多模态输入的预处理流水线
感知层是 Agent 的“五官”。端侧设备通常有麦克风阵列、摄像头、IMU、触摸屏等传感器,但原始数据不能直接喂给 LLM。你需要一条预处理流水线:
- 语音输入:VAD(语音活动检测)切分有效片段 → ASR(自动语音识别)转文本 → 文本送入认知层。端侧 ASR 推荐用 Whisper Tiny 或 Paraformer 量化版,模型大小控制在 50MB 以内。
- 图像输入:分辨率降采样到 224x224 或 448x448 → 轻量级视觉编码器(如 MobileViT、EfficientNet-Lite)提取特征 → 特征向量与文本 embedding 对齐。
- 传感器数据:IMU 数据做滑动窗口滤波 → 提取时域和频域特征 → 转成结构化文本描述(如“设备正在快速移动”)。
这里有个关键设计决策:感知层要不要做本地缓存?我的经验是必须做。端侧设备可能随时断网或休眠,感知数据如果只存在内存里,进程一杀就全丢了。建议用环形缓冲区(Ring Buffer)存最近 30 秒的原始数据,配合 SQLite 或 LevelDB 做持久化。
实操心得:VAD 的阈值不要设得太灵敏,否则环境噪声会频繁触发唤醒。我一般把能量阈值设在 -35dB 到 -30dB 之间,再叠加一个 200ms 的最短语音时长过滤。
2.2 认知层:LLM 在端侧怎么跑起来
认知层是端侧 Agent 的大脑,核心是一个本地 LLM。但“本地跑 LLM”这句话背后有一堆坑。
模型选型是第一道坎。端侧设备的内存通常 4GB 到 16GB,能分给 LLM 的也就 1GB 到 4GB。按 FP16 精度算,1GB 内存大约能放 5 亿参数;INT8 量化后翻倍到 10 亿;INT4 再翻倍到 20 亿。所以端侧 LLM 的参数量天花板大概在 1B 到 3B 之间(INT4 量化)。
目前主流选择有:
| 模型 | 参数量 | INT4 大小 | 适用场景 |
|---|---|---|---|
| Qwen2-0.5B | 0.5B | ~350MB | 简单意图识别、槽位填充 |
| Qwen2-1.5B | 1.5B | ~900MB | 多轮对话、基础工具调用 |
| Phi-3-mini | 3.8B | ~2.2GB | 复杂推理、代码生成 |
| Gemma-2B | 2B | ~1.2GB | 通用对话、文本摘要 |
选型逻辑很简单:先看任务复杂度,再看内存预算,最后看推理速度。如果只是做“打开空调”“设个闹钟”这种指令解析,0.5B 模型足够;如果要处理多步推理和工具编排,至少上 1.5B。
量化策略是第二道坎。INT4 量化能把模型压到原大小的 1/4,但精度损失不可忽视。我实测下来,Qwen2-1.5B 在 INT4 下的意图识别准确率比 FP16 掉约 3 个百分点,但推理速度提升 2.5 倍。这个 trade-off 在端侧完全值得。
量化工具推荐 llama.cpp 的quantize工具或 AutoGPTQ。以 llama.cpp 为例:
# 将 FP16 模型转为 GGUF 格式 python convert.py models/Qwen2-1.5B --outfile qwen2-1.5b-f16.gguf # INT4 量化 ./quantize qwen2-1.5b-f16.gguf qwen2-1.5b-q4_0.gguf q4_0q4_0是最基础的 4-bit 量化,还有q4_K_M这种混合量化,对关键层保留更高精度。实测q4_K_M比q4_0精度高 1-2 个百分点,大小只多 10%。
推理引擎是第三道坎。端侧推理不能用 PyTorch 原生,太吃内存。主流方案:
- llama.cpp:C++ 实现,跨平台,支持 CPU/GPU 混合推理,社区活跃。
- MLC-LLM:TVM 生态,支持 Vulkan/Metal/CUDA,编译优化做得好。
- ONNX Runtime:微软系,适合 Windows 和 Android,量化支持完善。
- MNN:阿里系,移动端优化到位,中文社区文档全。
我个人的选择顺序是:Android 优先 MNN,iOS 优先 MLC-LLM,PC 端优先 llama.cpp。原因很简单——跟着平台生态走,别跟工具链较劲。
2.3 决策层:ReAct 范式在端侧的轻量化改造
ReAct(Reasoning + Acting)是 Agent 的核心决策范式。标准 ReAct 循环是:Thought → Action → Observation → Thought → ...,直到任务完成。
但在端侧,标准 ReAct 有两个问题:一是每轮循环都要调 LLM,延迟叠加起来很恐怖;二是 LLM 输出格式不稳定,解析失败率不低。
我的改造方案是**“快慢双系统”**:
- 快系统:用规则引擎或小分类模型处理高频简单指令(如“打开蓝牙”“音量调大”),不走 LLM,直接映射到本地 API。响应时间 < 50ms。
- 慢系统:复杂任务走 ReAct 循环,但限制最大循环次数(通常 3-5 轮),超时直接降级到预设回复。
ReAct 的 Prompt 模板也要精简。云端可以用几百 token 的详细指令,端侧必须压缩到 100 token 以内。我的模板长这样:
你是一个端侧助手。可用工具:[工具列表]。 用户输入:{query} 按格式回复: Thought: 你的推理 Action: 工具名(参数) 或 Thought: 你的推理 Answer: 最终回复关键技巧:把工具描述做成短标签,比如[T1]打开应用(app_name)、[T2]查询天气(city),而不是自然语言描述。这样能省 30% 以上的 prompt token。
注意:端侧 ReAct 一定要设超时和最大轮次。我踩过的坑是模型陷入“Thought → Action → Observation → Thought”死循环,把设备电量从 80% 干到 20%。现在我的默认配置是:单轮超时 3 秒,最大 5 轮,超时返回“抱歉,我暂时无法处理这个请求”。
2.4 执行层:本地工具调用与硬件接口
执行层是 Agent 的“手脚”。端侧 Agent 能调用的工具分三类:
- 系统 API:打开应用、发短信、设闹钟、调音量、查通讯录。
- 硬件接口:摄像头拍照、麦克风录音、GPIO 控制、蓝牙配对。
- 本地服务:查本地数据库、读写文件、调用其他本地模型。
工具注册用 JSON Schema 描述,但端侧要精简字段。比如“打开应用”的工具定义:
{ "name": "open_app", "description": "打开指定应用", "parameters": { "app_name": {"type": "string", "enum": ["微信", "相机", "设置", "音乐"]} } }enum限定取值范围能大幅降低模型幻觉。实测下来,加了enum之后工具调用准确率从 78% 提升到 94%。
执行层的另一个关键是权限管理。端侧 Agent 直接操作硬件,必须做权限校验。我的做法是维护一个权限表,每个工具标注所需权限等级,调用前检查:
| 工具 | 权限等级 | 是否需要用户确认 |
|---|---|---|
| 查天气 | 低 | 否 |
| 发短信 | 中 | 是(首次) |
| 删除文件 | 高 | 是(每次) |
| 修改系统设置 | 高 | 是(每次) |
3. 实操:从零搭一个端侧 Agent 原型
3.1 环境准备与依赖安装
我以 Android 平台 + MNN 推理引擎为例,走一遍完整流程。硬件是一台骁龙 8 Gen 2 手机,12GB 内存。
第一步,装依赖:
# 克隆 MNN git clone https://github.com/alibaba/MNN.git cd MNN # 编译 Android 库 ./schema/generate.sh mkdir build_android && cd build_android cmake .. -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a \ -DANDROID_PLATFORM=android-28 \ -DMNN_BUILD_LLM=ON \ -DMNN_SUPPORT_TRANSFORMER_FUSE=ON make -j8编译产物是libMNN.so和libMNN_Express.so,塞进 Android 项目的jniLibs/arm64-v8a/。
第二步,准备模型。下载 Qwen2-1.5B-Instruct,用 MNN 的转换工具转成.mnn格式:
# 导出 ONNX python export_onnx.py --model Qwen/Qwen2-1.5B-Instruct --output qwen2.onnx # ONNX 转 MNN ./MNNConvert -f ONNX --modelFile qwen2.onnx --MNNModel qwen2.mnn --bizCode MNN # 量化 ./quantized.out qwen2.mnn qwen2_quant.mnn quant_config.jsonquant_config.json里配量化参数:
{ "quant_type": "int4", "block_size": 128, "symmetry": true, "calibration_dataset": "calib_data.txt" }校准数据集用 100-200 条真实用户 query 就行,不用多。
3.2 ReAct 循环的代码实现
核心逻辑用 Kotlin 写(Android 端),伪代码结构如下:
class EndSideAgent( private val llm: MNNLlm, private val tools: Map<String, Tool> ) { private val maxRounds = 5 private val timeoutMs = 3000L fun run(query: String): String { val context = mutableListOf<String>() context.add(buildPrompt(query)) repeat(maxRounds) { round -> val start = System.currentTimeMillis() val output = llm.generate(context.joinToString("\n"), maxTokens = 128) if (System.currentTimeMillis() - start > timeoutMs) { return "抱歉,处理超时了" } when { output.contains("Answer:") -> { return output.substringAfter("Answer:").trim() } output.contains("Action:") -> { val action = parseAction(output) val observation = executeTool(action) context.add(output) context.add("Observation: $observation") } else -> { // 格式解析失败,重试一次 context.add("请按格式回复") } } } return "任务太复杂了,我搞不定" } private fun executeTool(action: Action): String { val tool = tools[action.name] ?: return "工具不存在" return try { tool.execute(action.params) } catch (e: Exception) { "执行失败: ${e.message}" } } }几个关键点:
maxTokens = 128:端侧生成不能太长,128 token 足够 ReAct 一轮的 Thought + Action。- 超时检查:每轮生成后检查耗时,超时直接返回,避免用户干等。
- 格式解析失败重试:LLM 偶尔会输出不规范的格式,给一次重试机会,第二次还失败就放弃。
3.3 内存管理与性能调优
端侧最稀缺的资源是内存。我的实测数据:Qwen2-1.5B INT4 模型加载后占约 1.1GB,KV Cache 在 512 上下文长度下占约 200MB,加上 Android 运行时本身的开销,总共约 1.8GB。12GB 内存的手机完全扛得住,但 6GB 的设备就紧张了。
优化手段:
- KV Cache 复用:多轮对话时,如果 system prompt 不变,可以复用 KV Cache,省掉重复计算。MNN 支持
prefix_cache,实测能省 40% 的首 token 延迟。 - 动态上下文长度:简单指令用 128 上下文,复杂任务才开到 512。根据 query 长度动态调整。
- 模型懒加载:Agent 不活跃时卸载模型,释放内存。用户再次唤醒时重新加载(约 1.5 秒)。
- 线程绑定:推理线程绑到大核,别让小核拖后腿。Android 上用
Process.setThreadPriority(THREAD_PRIORITY_URGENT_AUDIO)。
实操心得:别在低电量模式下跑 LLM 推理。我测试过,电量低于 15% 时系统会限制 CPU 频率,推理速度直接掉一半。建议在代码里检测电量,低于 20% 时降级到规则引擎。
4. 踩坑记录与常见问题排查
4.1 模型加载失败与内存溢出
问题现象:App 启动时加载模型,直接 OOM 崩溃。
排查思路:先看模型文件大小和可用内存。Android 上单个进程的内存上限通常是 512MB 到 1GB(取决于设备),但可以用android:largeHeap="true"申请更大堆。不过更靠谱的做法是用mmap加载模型文件,让操作系统管理内存映射,而不是一次性读进堆里。
MNN 默认用mmap,但如果你自己写加载逻辑,记得用MNN::Express::Module的load接口,别用fread全读。
解决方案:
- 模型文件放
assets或files目录,用mmap加载。 - 开启
largeHeap。 - 如果还不行,换更小的模型或更高压缩率的量化。
4.2 ReAct 循环死锁与工具调用失败
问题现象:Agent 反复调用同一个工具,或者工具返回错误后不处理,继续循环。
排查思路:打印每轮的 Thought 和 Action,看模型是不是陷入了固定模式。常见原因是工具描述有歧义,或者 Observation 格式不符合模型预期。
解决方案:
- 工具描述加
enum限定参数范围。 - Observation 统一格式,比如
"结果: xxx"或"错误: xxx"。 - 加循环检测:如果连续两轮 Action 相同,强制中断。
4.3 推理速度慢与延迟优化
问题现象:首 token 延迟超过 2 秒,用户体验差。
排查思路:用adb shell top看 CPU 占用,用 MNN 的 profiler 看各层耗时。常见瓶颈是 attention 计算和 FFN 层。
解决方案:
- 开启
MNN_SUPPORT_TRANSFORMER_FUSE编译选项,融合算子。 - 用
q4_K_M替代q4_0,精度更高但速度差不多。 - 减少上下文长度,别动不动就 2048。
- 预热:App 启动后在后台跑一次空推理,把模型权重加载到缓存。
4.4 常见问题速查表
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 模型加载 OOM | 内存不足 | 用 mmap、开 largeHeap、换小模型 |
| 推理速度慢 | CPU 降频、算子未优化 | 绑大核、开算子融合、降上下文 |
| 工具调用失败 | 参数格式错误 | 加 enum、校验参数类型 |
| ReAct 死循环 | 工具描述歧义 | 加循环检测、精简工具描述 |
| 输出格式不稳定 | Prompt 不够明确 | 加 few-shot 示例、限制输出格式 |
| 电量消耗快 | 频繁唤醒推理 | 快慢双系统、懒加载、低电量降级 |
5. 端侧 Agent 的边界与扩展方向
端侧 Agent 不是万能的。它的能力边界由三个因素决定:模型参数量、内存带宽、功耗预算。1.5B 模型能做的事,3.8B 模型能做得更好,但功耗和延迟也上去了。我的经验是:在满足任务准确率的前提下,选最小的模型。
扩展方向有几个值得关注:
- 多 Agent 协作:端侧跑一个小模型做路由,复杂任务转发到云端大模型,形成端云协同。但要注意数据隐私,敏感数据不出端。
- 个性化微调:用本地聊天记录做 LoRA 微调,让 Agent 更懂用户习惯。LoRA 权重很小(几 MB),端侧完全能存。
- 多模态融合:端侧视觉编码器 + LLM,实现“看图说话”“拍照问答”。但视觉编码器的计算量不小,需要专门优化。
我在实际项目里发现,端侧 Agent 最实用的场景不是“全能助手”,而是垂直领域的自动化——比如车载语音控制、工业设备巡检、智能家居中控。这些场景任务边界清晰,小模型足够用,而且对延迟和隐私要求高,正好是端侧 Agent 的甜点区。
最后分享一个小技巧:端侧 Agent 的 Prompt 里一定要加“不知道就说不知道”。端侧模型幻觉比云端严重,不加这句,它会在没把握的时候瞎编。加了之后,拒答率上升,但用户信任度反而更高。