树莓派本地智能体实战:Neo4j知识图谱+本地模型长期记忆 1. 为什么要在树莓派上折腾一个本地智能体把大模型塞进树莓派听起来像是给自行车装喷气发动机但真动手做完之后你会发现这件事的性价比高得离谱。我这次做的项目核心目标很明确用一块树莓派当宿主跑一个完全本地化的个人 AI 智能体代号就叫 Jeremy Adams底层用 Neo4j 做知识图谱存储让这个智能体不只是一问一答而是能记住我说过的话、理清人物和事件之间的关系并且在我断网的时候照样能用。先说清楚它到底能干什么。Jeremy Adams 这个智能体本质上是一个跑在本地的小型对话系统它接收我的文字输入经过本地推理后给出回复同时把对话里出现的关键实体人、地点、事件、偏好抽取出来写进 Neo4j 图数据库。下次我再提到相关话题它会先去图数据库里查一遍把历史上下文捞出来再拼进提示词里交给模型。这样一来它就有了长期记忆而不是每次对话都从零开始。这套东西适合谁参考我觉得有三类人值得动手试试。第一类是手里正好有一块吃灰的树莓派想找个正经项目把它用起来的开发者第二类是对知识图谱感兴趣但一直没找到轻量级落地场景的人第三类是想拥有一个完全私有、数据不出本地设备的个人助理的普通用户。哪怕你之前没碰过 Neo4j只要会基本的 Linux 命令和 Python跟着做下来问题不大。我选择树莓派而不是旧笔记本或者云服务器理由很实在。树莓派功耗低7×24 小时开着一年电费也就几十块体积小塞在书架角落完全不占地方GPIO 接口还能后续接传感器扩展空间大。至于为什么用 Neo4j 而不是简单的 SQLite 或者纯文本存对话历史是因为对话数据天然是图结构——我和某个人之间有关系某个人和某个事件之间又有关系用关系型数据库查这种多跳关联会写出一堆 JOIN而图数据库一句 Cypher 就搞定。提示这个项目的定位是个人使用不是生产级服务。别指望它能同时服务几十个用户也别拿它去跑需要高并发的场景。它的价值在于私有、可控、可折腾。2. 硬件选型与系统环境搭建2.1 树莓派型号怎么挑才不踩坑树莓派型号五花八门我先把结论摆出来跑本地智能体内存是硬指标至少 8GB。我实测过 4GB 版本的树莓派 4B加载一个 3B 参数量的量化模型之后系统就开始频繁使用交换分区响应速度肉眼可见地变慢一次对话要等十几秒。换成 8GB 版本之后同样的模型跑起来就顺畅多了首字延迟能压到两三秒。具体型号上树莓派 5 的 8GB 版本是目前最合适的选择。它的 CPU 主频更高内存带宽也比 4B 强不少而且支持 PCIe 接口后续如果想外接 NVMe 固态硬盘来加速模型加载也有路子。如果手头只有树莓派 4B也不是不能用但建议把模型参数量控制在 3B 以内并且一定要用量化版本。存储方面我强烈建议用 SSD 而不是 TF 卡。原因很简单模型文件动辄几个 GBNeo4j 的数据库文件也会随着对话增多而膨胀TF 卡的读写速度和寿命都扛不住长期高频写入。我用的是一个 USB 3.0 接口的固态硬盘盒加一块 256GB 的 NVMe 盘通过 USB 启动系统整体体验比 TF 卡流畅太多。散热也别忽视。树莓派 5 满载跑推理的时候发热量不小我一开始只贴了散热片结果连续对话二十分钟后 CPU 就降频了。后来换了一个带风扇的金属外壳温度稳定在 60 度左右降频问题就没了。这个钱不能省降频带来的性能损失远比一个散热壳贵。2.2 系统安装与基础环境配置系统我选的是 64 位的 Raspberry Pi OS Lite 版本没有桌面环境省内存。安装过程用官方镜像工具写入 SSD 就行这里不展开。装完之后第一件事是更新源和升级已有包sudo apt update sudo apt full-upgrade -y然后装 Python 环境和必要的编译工具。树莓派上很多 Python 包没有预编译的 wheel需要本地编译所以 build-essential 和相关的开发库必须提前装好sudo apt install -y python3-pip python3-venv build-essential cmake git libopenblas-dev我习惯给每个项目建独立的虚拟环境避免包版本冲突。这个项目涉及模型推理和图数据库两个方向的依赖混在一起很容易出问题python3 -m venv ~/jeremy-env source ~/jeremy-env/bin/activate虚拟环境激活后先升级 pip再装基础依赖。这里有个坑要注意树莓派上直接 pip install 某些包会尝试从源码编译耗时很长建议优先找预编译版本或者用 apt 装系统级的库。注意树莓派的默认交换分区只有 100MB 左右编译大包的时候容易内存不足导致进程被杀。可以临时把交换分区调大编辑 /etc/dphys-swapfile把 CONF_SWAPSIZE 改成 2048然后重启交换服务。2.3 Neo4j 在 ARM 架构上的安装要点Neo4j 官方对 ARM 架构的支持这几年好了很多但安装方式跟 x86 平台还是有区别。最省事的办法是用 Docker但树莓派上跑 Docker 本身也要占资源我最后选择的是直接装 Neo4j 的社区版压缩包。先去 Neo4j 官网找 ARM64 版本的压缩包下载后解压到 /opt 目录下。然后需要装 Java 运行环境Neo4j 5.x 需要 JDK 17 或 21sudo apt install -y openjdk-17-jre-headless解压之后修改 conf/neo4j.conf 里的几个关键配置。默认配置是给服务器用的内存分配很大树莓派上必须调小否则一启动就把内存吃光了server.memory.heap.initial_size512m server.memory.heap.max_size1g server.memory.pagecache.size512m dbms.default_listen_address0.0.0.0heap 给 1GBpagecache 给 512MB加起来 1.5GB留足够的内存给模型推理。如果你的树莓派是 8GB 版本可以适当调大 pagecache对图查询性能有好处。启动 Neo4j 用bin/neo4j console可以前台运行看日志确认没问题后再用bin/neo4j start后台启动。第一次启动会自动初始化数据库需要等一两分钟。启动完成后浏览器访问 7474 端口就能看到 Neo4j Browser 界面默认账号密码都是 neo4j首次登录会强制改密码。3. 智能体核心架构拆解3.1 整体数据流是怎么走的Jeremy Adams 的工作流程我用一句话概括输入进模型模型出回复回复里抽实体实体进图库图库反哺下一轮输入。听起来简单但每个环节都有讲究。具体来说当我在终端输入一句话比如我下周要去杭州出差帮我记一下系统会先做一次实体抽取。这一步我用的是一个轻量级的命名实体识别流程不依赖外部 API纯本地跑。抽出来的实体包括下周时间、杭州地点、出差事件。然后系统会拿这些实体去 Neo4j 里查看看之前有没有相关的记录比如杭州这个节点之前出现过没有跟哪些事件关联过。查到的历史上下文会被拼进提示词跟当前输入一起交给本地模型。模型生成回复之后系统再把这一轮的新实体和关系写回图数据库。整个过程是闭环的用得越久图数据库里的信息越丰富智能体的记忆就越完整。这个架构最大的好处是解耦。模型只负责生成文本记忆管理交给图数据库两边各司其职。如果哪天我想换个模型只要接口不变图数据库这边完全不用动。反过来如果我想换图数据库模型这边也不用改。3.2 为什么选 Neo4j 而不是向量数据库现在做本地智能体很多人第一反应是用向量数据库做 RAG。我一开始也考虑过这个方案但实际对比之后还是选了 Neo4j原因有三个。第一对话数据的关系性很强。向量数据库擅长的是语义相似度检索你给它一段话它找出语义相近的另一段话。但对话里更多的是结构化关系比如A 是 B 的同事、C 事件发生在 D 时间这种关系用图来存和查比向量直观得多也精确得多。第二多跳查询是图数据库的强项。假设我问上次跟我一起出差的那个人他负责的项目进展怎么样了这个问题需要先找到出差事件再找到关联的人再找到这个人关联的项目。在 Neo4j 里就是一条 Cypher 路径查询在向量数据库里就得拆成多次检索再拼接麻烦且容易丢信息。第三Neo4j 的可视化界面太好用了。Neo4j Browser 能把整个知识图谱画出来节点和关系一目了然。调试的时候我能直接看到智能体到底记住了什么、漏掉了什么这种直观性是向量数据库给不了的。当然Neo4j 也不是没缺点。它的写入速度比关系型数据库慢资源占用也更高。但在个人使用场景下这些缺点可以接受毕竟对话频率不高树莓派 8GB 内存也扛得住。3.3 本地模型的选型与量化策略模型选型是这个项目里最纠结的部分。树莓派没有独立显卡只能靠 CPU 推理所以模型不能太大。我试过好几个模型最后锁定在 3B 参数量级别的量化版本上。量化是什么意思简单说就是把模型参数的精度从 16 位浮点数降到 4 位整数模型体积缩小到原来的四分之一左右推理速度也能提升。代价是精度会损失一点但对于日常对话来说这点损失基本感觉不到。我用的推理框架是 llama.cpp 的 Python 绑定它对 ARM 架构优化得不错支持多种量化格式。模型文件我选的是 Q4_K_M 量化级别这个级别在体积和精度之间平衡得比较好。3B 模型量化后大概 2GB 左右加载到内存里占 2.5GB 上下树莓派 8GB 版本完全吃得消。推理速度方面我实测下来首字延迟在 2 到 4 秒之间后续生成速度大概每秒 5 到 8 个 token。这个速度跟云端 API 没法比但作为个人助理够用了。如果你追求更快的响应可以降到 1B 参数量级别但回复质量会明显下降经常答非所问。提示模型文件下载下来之后建议先在一台性能更好的机器上测试一下确认模型本身没问题再拷到树莓派上。树莓派上调试模型加载问题会很痛苦因为每次加载都要等好几分钟。4. 实操过程与关键环节实现4.1 图数据库 Schema 设计动手写代码之前先把图数据库的结构设计好这一步偷懒后面会付出代价。我的设计原则是节点类型少而通用关系类型具体而明确。节点类型我定义了四种Person人物、Place地点、Event事件、Topic话题。为什么不用更细的分类因为分类越细实体抽取的时候越容易分错维护成本也越高。四种类型覆盖了日常对话里绝大多数实体。关系类型我定义了六种KNOWS认识、LOCATED_AT位于、PARTICIPATES_IN参与、MENTIONS提及、HAPPENED_AT发生于、RELATES_TO相关。关系类型要具体因为查询的时候主要靠关系来导航。在 Neo4j 里创建约束和索引能显著提升查询性能CREATE CONSTRAINT person_name IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE; CREATE INDEX event_time IF NOT EXISTS FOR (e:Event) ON (e.timestamp); CREATE INDEX topic_keyword IF NOT EXISTS FOR (t:Topic) ON (t.keyword);给 Person 的 name 加唯一约束避免同一个人重复创建节点。给 Event 的 timestamp 和 Topic 的 keyword 加索引因为这两个字段是查询时最常用的过滤条件。这里有个经验不要一上来就设计得很复杂。我第一版设计了十几种节点类型结果实体抽取的准确率惨不忍睹后来砍到四种才稳定下来。图数据库的 schema 应该随着使用逐步演化而不是一开始就追求完备。4.2 实体抽取模块的实现实体抽取我用的是一套基于规则加轻量模型混合的方案。纯规则太死板纯模型在树莓派上跑太慢混合方案兼顾了速度和准确率。规则部分主要处理时间表达式和明显的专有名词。比如下周、明天、下个月这类相对时间用正则表达式匹配后转换成绝对时间戳。人名和地名则用一个预置的词典做初步匹配。模型部分我用的是一个很小的序列标注模型参数量只有几十兆专门做命名实体识别。它跑在 CPU 上单次推理不到一秒可以接受。模型输出的实体再跟规则匹配的结果做合并取并集。抽取出来的实体需要做归一化。比如杭州和杭州市应该归到同一个节点老张和张工如果指的是同一个人也应该合并。归一化我用的是简单的字符串相似度加别名表别名表可以手动维护也可以从对话历史里自动学习。def normalize_entity(entity, entity_type): entity entity.strip() if entity_type Place: entity entity.rstrip(市省区县) if entity_type Person: entity alias_table.get(entity, entity) return entity这段代码看着简单但实际效果很好。地名去掉行政级别后缀之后重复率大幅下降。人名通过别名表映射能把昵称和正式称呼统一起来。4.3 上下文注入与提示词工程模型本身没有记忆所谓的记忆全靠提示词注入。每一轮对话系统会从图数据库里查出相关上下文拼成一段文字放在用户输入前面一起交给模型。上下文注入的关键是相关性和长度控制。查出来的信息太多提示词会超长模型处理不过来查出来的信息太少又起不到记忆的作用。我的做法是限制注入的上下文不超过 500 个 token优先注入最近的和最相关的内容。查询逻辑用 Cypher 实现从当前输入抽取的实体出发向外扩展两跳按时间倒序取最近的若干条记录MATCH (n)-[r]-(m) WHERE n.name IN $entities OR n.keyword IN $entities RETURN n, r, m ORDER BY r.timestamp DESC LIMIT 10查出来的结果格式化成自然语言比如你上次提到杭州是在 3 天前当时关联的事件是出差。这种表述比直接给模型一堆结构化数据效果好得多因为模型更擅长处理自然语言。提示词的模板我改了好几版最后定下来的结构是系统角色说明 历史上下文 当前输入 回复要求。系统角色说明里明确告诉模型你是一个有记忆的个人助理请结合历史上下文回复。回复要求里限制回复长度避免模型长篇大论。注意提示词里的历史上下文一定要标注清楚时间否则模型会把很久以前的事当成刚发生的。我踩过这个坑模型把一个月前的出差和当天的安排混在一起回复闹了笑话。4.4 对话循环与数据写回整个对话循环用 Python 写主流程就是一个 while 循环读输入、抽实体、查图库、拼提示词、跑推理、写回图库、打印回复。写回图库这一步要小心不能每轮对话都无脑写入否则图数据库会迅速膨胀里面全是重复节点。我的做法是写入前先做一次 MERGE 查询节点存在就更新不存在才创建MERGE (p:Person {name: $name}) ON CREATE SET p.created_at timestamp() ON MATCH SET p.last_seen timestamp()MERGE 是 Neo4j 里非常实用的一个操作它相当于存在则匹配不存在则创建。配合 ON CREATE 和 ON MATCH 可以分别处理新建和更新两种情况。这样既能保证节点不重复又能记录节点的活跃时间。关系写入也是类似逻辑先 MERGE 两端的节点再 MERGE 关系。关系的属性里记录时间戳和对话轮次方便后续查询时排序。实测下来一轮完整对话从输入到输出耗时大概 5 到 10 秒其中模型推理占大头图数据库查询和写入加起来不到一秒。这个延迟在个人使用场景下完全可以接受毕竟不是在做实时客服。5. 常见问题与排查技巧实录5.1 模型加载失败与内存不足这是新手最容易遇到的问题。现象是程序启动后卡在加载模型那一步或者直接报 Killed 错误。根本原因通常是内存不够系统把进程杀掉了。排查思路分三步。第一步用free -h看当前内存占用确认模型加载前系统还剩多少可用内存。第二步用dmesg | grep -i kill看内核日志确认是不是 OOM Killer 干的。第三步如果确实是内存不足要么换更小的模型要么调大交换分区。调大交换分区的操作前面提过这里补充一点交换分区放在 SSD 上比放在 TF 卡上快得多但即便如此交换分区的读写速度也远不如物理内存。所以调大交换分区只是权宜之计根本解决办法还是加内存或者换小模型。我自己的经验是3B 模型 Q4 量化版在 8GB 树莓派上跑系统剩余内存保持在 1GB 以上比较稳妥。如果剩余内存低于 500MB就要警惕了随时可能触发 OOM。5.2 Neo4j 启动报错与端口冲突Neo4j 启动失败的原因有好几种最常见的是端口被占用和 Java 版本不对。端口冲突的排查很简单用ss -tlnp | grep 7474看 7474 端口被谁占了。如果是之前的 Neo4j 进程没退干净用pkill -f neo4j杀掉再重启。如果是别的服务占了端口要么改 Neo4j 的端口配置要么停掉那个服务。Java 版本问题稍微隐蔽一点。Neo4j 5.x 要求 JDK 17 或 21如果系统里装了多个 Java 版本Neo4j 可能用错了版本。可以在 neo4j 的启动脚本里显式指定 JAVA_HOME或者在系统层面用update-alternatives切换默认 Java 版本。还有一个坑是文件权限。Neo4j 的数据目录和日志目录需要写权限如果之前用 root 跑过再用普通用户跑就会报权限错误。解决办法是把相关目录的属主改成当前用户sudo chown -R $USER:$USER /opt/neo4j-community-5.x.x5.3 实体抽取准确率低的调优方法实体抽取不准智能体的记忆就会乱。我遇到过几种典型的抽取错误这里整理成表格方便对照排查。问题现象可能原因解决办法人名被拆成多个词分词粒度太细调整分词词典加入常见姓氏组合地名识别遗漏词典覆盖不全补充地名词典或用模型兜底时间解析错误相对时间规则不全补充大后天下下周等表达实体重复创建归一化不彻底加强别名映射和字符串相似度合并关系方向搞反抽取逻辑有误检查主语宾语识别加人工校验调优的核心思路是先看数据再改规则。我建议每次调优前先把最近一百轮对话的抽取结果导出来人工看一遍统计错误类型和频率然后针对最高频的错误改。盲目改规则往往事倍功半。另外抽取模块最好留一个人工纠正的入口。有时候模型抽错了我直接在终端里输入纠正命令系统就把错误的节点或关系改过来。这个功能在调试阶段特别有用。5.4 响应速度慢的优化手段响应慢是树莓派跑本地模型的通病但通过一些手段可以明显改善。第一开启模型的热加载。模型加载一次要几十秒如果每次对话都重新加载体验极差。正确做法是程序启动时加载一次之后常驻内存对话循环里直接复用。第二限制生成的最大 token 数。模型生成是逐 token 进行的生成越多越慢。个人助理场景下回复不需要太长我把最大生成长度限制在 200 个 token超出就截断。这样既加快了速度又避免了模型啰嗦。第三优化图数据库查询。前面提到的索引和约束一定要建另外查询时尽量限制返回条数不要一次性把整个图捞出来。我一开始没加 LIMIT结果查询越来越慢加上 LIMIT 10 之后速度就稳定了。第四考虑用更小的量化级别。Q4 换成 Q3 或者 Q2速度能提升不少但精度损失也更明显。这个取舍看个人需求我最后留在 Q4因为 Q3 的回复质量下降太多经常答非所问。提示优化是个循序渐进的过程不要一次性改太多参数否则出了问题不知道是哪个改动导致的。每次只改一个变量测完再改下一个。6. 后续可扩展的方向与个人体会这套东西跑通之后能扩展的方向其实挺多的。我目前正在尝试的一个方向是接入语音输入输出用本地的语音识别和合成模型让 Jeremy Adams 能听会说这样交互就更自然了。另一个方向是接 GPIO 传感器比如温湿度传感器让智能体能感知物理环境问它现在屋里多少度它能直接答上来。还有一个我觉得很有价值的方向是多智能体协作。现在是一个智能体单打独斗如果拆成几个专职智能体一个管日程、一个管知识、一个管闲聊它们之间通过图数据库共享记忆可能会产生更有意思的效果。不过这个复杂度就上去了树莓派的资源能不能扛住还得实测。我个人在实际操作中的体会是本地智能体这个方向最大的价值不在于性能多强而在于数据完全私有。所有对话记录、所有知识图谱都在我自己的设备上不经过任何第三方。这种掌控感是云端服务给不了的。性能上确实有妥协响应慢一点、模型笨一点但换来的是安心。最后再分享一个小技巧图数据库里的数据要定期备份。Neo4j 支持在线备份用neo4j-admin database dump命令就能把整个数据库导出成一个文件。我设置了一个定时任务每周日凌晨自动备份一次备份文件同步到另一块硬盘上。这样即使树莓派出问题积累的知识图谱也不会丢。毕竟这东西用得越久越有价值丢了重来成本太高。