资讯动态

用nanobot搭建本地私人助手:从零配置到定时推送的完整实践

发布时间:2026/10/22 18:49:05 来源:尧图企业网站定制
事情要从我桌面上那十几个碎片化工具说起。日历一个程序提醒一个脚本查资料要开浏览器记笔记又得换一个窗口每天来回切换真正静下来干活的时间没剩多少。后来我给自己定了个目标用一个统一的入口把杂事都收敛起来让它变成一个24小时在线的私人助手。正好在技术社区里看到有人聊 nanobot 这个轻量级助手框架核心思路是用一套 YAML 配置加几个插件就能把本地模型变成真正能干活的助手。我照着这个思路从零搭了一版从空目录跑到了稳定的定时推送实测下来很稳。这篇文章就把完整过程拆给你看包括需求拆解、配置思路、插件写法以及我踩过的五个坑给同样想为自己做一个私人助手的读者一条能直接上手的路径。1. 动手前的需求拆解这个私人助手到底要干什么很多人一上来就想着我要做一个全能的AI助手结果装了一堆插件最后发现最常用的还是聊天。我这次反着来先冷静列了一下自己每天在电子设备上重复做的事情再决定助手只做哪几件。1.1 把日常杂事收敛成四个核心能力我连续记了一周自己的使用习惯最后划分出四个必须满足的需求项需求具体场景我想要的交互方式对话检索查天气、问概念、整理一段文字直接在聊天窗口说一句话资料抓取与记录把一篇网页内容摘要后存成笔记告诉助手网址它自动存成Markdown定时提醒每天早上推送天气、待办和会议提醒不用我操作到点主动发消息本地优先数据尽量留在自己机器上不依赖某个云端平台断网时基础对话和记录功能仍能用这一步非常重要。你会发现私人助手这个词其实特别宽泛不拆解的话很容易做成一个四不像。我最后砍掉了语音控制、复杂日程协同、多用户权限这些听起来很酷但短期用不上的功能只保留上面四件事。1.2 为什么选 nanobot 而不是自己从零写我看过不少从头写助手的案例大多数人卡在了同一个地方模型接口调用、会话上下文管理、工具调用的参数解析、插件热加载这些看起来是小问题真做起来每一项都要花不少时间。自己写没有十天半个月很难稳定而且写出来的代码往往只适配自己的使用习惯。nanobot 的价值在于它提供了一套最小骨架消息进来之后该走什么流程、模型返回的工具调用 JSON 该怎么解析、插件放在哪个目录才会被自动加载这些框架都帮你处理好了。我需要做的只有三件事改配置文件、写几个插件、设定时任务。用盖房子的类比来说nanobot 是把地基和框架搭好了我只负责往里面砌墙、装门窗而不是从烧砖开始。当然框架也有它的限制如果某个功能它的插件机制表达不了你可能会被迫改框架源码。但对我来说能用一个下午跑通核心链路这个性价比已经很高了。2. nanobot 的核心工作方式先搞懂一条消息怎么变成一件事在动手配置之前我建议你先花十分钟理解 nanobot 的运行逻辑。这个理解到位了后面写插件、排错都会很顺。2.1 从收到一句话到完成一件事的处理链路nanobot 的一个会话请求在内部是这样一个循环过程用户消息进入会话框架从本地数据库里取出该会话最近的聊天记录。框架把聊天记录、系统提示词、插件描述信息拼成一次模型请求。模型返回两类内容之一要么是纯文本回复要么是一个工具调用指令JSON 格式。如果是纯文本直接返回给用户流程结束。如果是工具调用框架解析 JSON找到对应的插件并执行。插件返回结果框架把结果作为一条新的消息追加到本轮对话。框架带着插件结果再次请求模型。循环第 3 步直到模型给出最终文本回复或者达到最大轮数上限。这个设计本质上就是思维链加工具调用的 Agent 架构。你不需要理解太深只需要记住一个关键结论模型不是一次性回答你的它可能想了好几步。这意味着模型质量差或者插件返回的内容太乱都会导致要多跑好几轮响应时间变长还容易出错。我在本机上跑起来之后的直观感受是简单的问候基本一秒内返回一旦涉及查资料通常要走工具调用-搜索结果-再生成三轮耗时在五秒到十几秒之间完全可接受。2.2 插件、技能和工具调用三者的关系nanobot 里有三个容易混淆的概念我拿自己的配置来拆解。插件是实际执行任务的代码文件。我写在skills目录里一个文件对应一个功能比如web_search.py、save_note.py。技能描述是让模型知道什么情况该调用哪个插件的元信息。nanobot 规定插件文件里必须包含一段对功能和适用场景的文字描述框架会把这些描述发给模型。模型看到用户问天气就会根据描述匹配到天气插件。工具调用是模型在回复内容里输出的特殊 JSON 指令。nanobot 支持一套很简洁的格式插件函数签名里的参数会被框架自动识别成工具参数。比如我写一个带query和max_results参数的函数模型在决定搜索时就会自动填上这两个字段。我在实际的配置里给每个插件都写了类似这样的技能描述# skills/web_search.py 技能网络搜索。当用户需要查询最新信息、查找近期资料或者需要在多个网页中做筛选时使用。 参数说明 - query: 用户想搜索的关键词尽量保留原始语义。 - max_results: 返回结果数量上限默认5。 此处分行的文档字符串是给框架看的相当于向模型自我介绍我能干什么、什么场景用我。如果你写得太含糊模型就可能该调用时不调用不该调用时乱调用所以描述要具体。3. 搭建最小可用版本从空目录到第一次对话我的目标是用最快的速度跑通一个能正常聊天的 nanobot然后再考虑加插件。这个最小可用版本的思想很管用它让你先拥有一套可运行的基线之后任何新功能坏了你都知道问题出在新功能上。3.1 初始化配置数据目录、模型接口、系统提示词nanobot 的程序结构很干净我把发布包解压到~/nanobot目录之后只关心三个地方~/nanobot ├── nanobot.yaml # 全局配置 ├── skills/ # 插件目录每个插件一个py文件 └── data/ # 数据库和会话记录存这里我贴一份最简配置你照着改就行# nanobot.yaml server: host: 0.0.0.0 port: 8080 llm: base_url: http://127.0.0.1:8000/v1 model: local-assistant-7b api_key: empty temperature: 0.3 memory: type: file max_turns: 20 summary_every: 10 skills: auto_register: true schedules: - name: morning_report cron: 0 8 * * * message: 给用户发送今天的待办提醒和天气简报几个需要解释的配置项llm.base_url我填的是一个兼容开放接口协议的自建模型网关地址。你本地如果搭建了模型服务一般把端口换一下就能用。我这里特意把api_key留空因为我跑的是内网模型。如果你接入的是云厂商接口这里通常会填一串密钥。memory.max_turns控制上下文保留轮数。20 轮是我试下来比较平衡的值太少了助手记不住刚才聊了什么太多了响应时间和花费都会涨。skills.auto_register打开后我扔一个新的.py文件进skills目录重启服务就能自动加载不用改注册表。3.2 启动服务并完成首次对话启动命令非常简单cd ~/nanobot ./bin/nanobot serve --config nanobot.yaml我第一次启动时犯了个低级错误忘了先确认模型网关是活的结果服务起来了一对话就报连接拒绝。现在我的习惯是启动前先用一条命令探活模型接口curl http://127.0.0.1:8000/v1/models模型接口返回正常后再启动 nanobot。服务起来后我用下面的方式发消息curl -X POST http://127.0.0.1:8080/chat \ -H Content-Type: application/json \ -d {session:test,message:你好简单介绍一下你自己}我收到的返回是这样的{ reply: 你好我是你的私人助手。我可以帮你查资料、记录待办、设置提醒也能定时整理信息。如果你有具体需要直接告诉我就行。 }到这里最小可用版本就算跑通了。整个过程比我预想顺利最大的门槛反而是模型接口的连通性。所以如果你卡在哪一步先别怀疑 nanobot先确认模型服务本身能不能正常返回内容。3.3 给助手定一个人设系统提示词决定行为边界nanobot 的默认系统提示词是按通用助手写的我在试用的第二天就改成了自己的版本。因为默认提示词太容易被带跑偏经常会用很长的篇幅解释一些简单问题。我目前的系统提示词写了这几条原则回复优先简短直接能三句话说清就不要写三段。涉及事实性信息时优先调用搜索插件不要凭记忆编造。需要保存内容时先向用户确认是存成笔记还是待办。拿不准用户意图时直接问不要猜着做。系统提示词的效果立竿见影。同一个问题修改前的回复是长篇大论修改后就是已为你查到明日天气晴18到27度适合出行。这和模型本身的水平关系不大更多是指引它按你的偏好输出。你要是有明确的个人风格偏好这里值得花点时间反复打磨。4. 给助手装上手工具调用插件的设计与踩坑聊天只是第一步。我搭私人助手的核心目的是让它能帮我查资料、存笔记、做判断这必须靠插件撑起来。4.1 接入搜索插件让模型会查一下再说我最先写的插件是网络搜索。很多涉及时效性的问题直接让模型硬答很容易翻车不如让它先搜再答。这一步相当于是给模型配了一双眼睛。插件代码并不复杂核心是把搜索接口的返回整理成文本给模型看# skills/web_search.py 技能网络搜索。当用户需要查询最新信息、近期资料、不确定的事实或需要多个来源对比时使用。 参数说明 - query: 搜索关键词尽量保留用户原始意图。 - max_results: 返回结果条数默认5最大10。 import json from urllib.parse import urlencode from urllib.request import urlopen def run(query: str, max_results: int 5) - str: endpoint http://127.0.0.1:8001/search params urlencode({q: query, n: max_results}) with urlopen(f{endpoint}?{params}, timeout10) as resp: data json.loads(resp.read().decode(utf-8)) results [] for item in data.get(items, []): results.append(f- {item[title]}{item[url]}) if not results: return 没有搜索到相关结果。 return \n.join(results[:max_results])这个插件假设本地有一个搜索服务跑在 8001 端口。实际使用中你可以把它换成任何搜索服务只要返回结构统一就行。这里的关键点是输出格式插件返回给模型的是纯文本不是结构化 JSON。因为模型读纯文本最自然你把一堆层级复杂的 JSON 丢给它它反而容易蒙。写完插件重启服务后我用一句话测试最近有哪些轻量级笔记工具值得关注nanobot 的反应是模型判断需要搜资料调用web_search拿到几条结果之后再生成一段推荐回复。整个过程看起来就像它在给我转述搜索结果但事实上它已经做了一层筛选和归纳。4.2 写文件插件让助手学会记下来搜索是读信息我更需要的是写信息。我写了一个save_note插件让助手把网页摘要、临时想法保存成 Markdown 文件。# skills/save_note.py 技能保存笔记。当用户要求把内容存成文件、记录笔记、收藏某段文字时使用。 参数说明 - content: 要保存的正文内容使用Markdown格式。 - filename: 文件名不需要包含路径框架会自动加上白名单目录前缀。 import re from pathlib import Path # 只允许写到 notes 白名单目录防止任意路径写入 ALLOWED_DIR Path(__file__).parent.parent / notes def run(content: str, filename: str) - str: safe_name re.sub(r[^a-zA-Z0-9_\-\u4e00-\u9fa5], _, filename) if not safe_name.endswith(.md): safe_name .md ALLOWED_DIR.mkdir(parentsTrue, exist_okTrue) target ALLOWED_DIR / safe_name target.write_text(content, encodingutf-8) return f笔记已保存到 {target.name}全文共{len(content)}字。这个插件里有两点我想单独强调。第一路径白名单。框架虽然做了基础隔离但如果你在插件里直接允许用户指定任意路径风险极大。我在代码里做了两层保护文件名强制规范化把特殊字符替换成下划线保存路径强制拼接在notes目录之下。这样模型再怎么乱填参数也写不到系统目录。第二让插件返回一个易于确认的结果。插件最后一句全文共多少字是给模型看的它拿到这个结果后可以用自己的话向用户复述保存情况。我发现如果插件只返回成功两个字模型有时候会发挥过头编一些没发生的细节。返回内容具体一点模型的总结就更稳。4.3 工具调用最常见的翻车点返回内容太脏我原本以为插件写好就没事了结果第一个周末就遇到问题。搜索插件返回了十几条结果其中夹杂着阅读量、日期等无关信息模型为了从里面找答案连续两三次调用同一个搜索绕来绕去最后给出的内容还是错的。后来我把插件返回格式改成了尽量干净的原则只留下模型做判断真正需要的标题、链接和一句摘要其他信息一律丢弃。这一步改动之后助手的回答准确率明显提升。很多插件翻车不是模型的问题而是你给模型看的材料本身太乱。这个经验在我后来写所有插件时都适用。5. 让助手学会主动定时推送与通知链路只会被动回答问题还配不上私人助手这个名字。我最想要的功能其实是每天早上我打开电脑它已经把今天要做的几件事放在眼前。这就是定时任务要解决的场景。5.1 定时任务其实是虚拟消息注入nanobot 的定时任务机制比我预想的简单它不直接在后台跑你的插件而是在指定时间向指定会话注入一条消息。注入之后整个处理流程和用户正常发消息一模一样。以我的晨间播报为例。我在配置里写了这一段schedules: - name: morning_report cron: 0 8 * * * session: main message: 现在请生成今天的晨间简报先查看今天的待办事项再查询一次天气最后汇总成三条以内的短消息。到了每天 8 点整nanobot 会把这段 message 当成一个用户消息塞进 main 会话。模型收到后会根据内容决定要不要调用天气和待办相关插件。整个过程不需要我输入任何指令。这里有个容易被忽略的细节session必须指定为提前存在的会话。如果它在系统里找不到这个会话这条定时消息就会无人接单直接丢弃。我第一版配置里没有预先创建会话结果定时任务静默失败我查了两天才发现是这个原因。5.2 把通知送到眼前桌面通知与手机推送定时消息生成之后怎么通知到人nanobot 内置了一个通知回调机制每次模型生成最终回复框架会把回复内容发送给配置的notifier。我配置了两个通道。桌面通知我用了一条简单的系统命令在 Linux 桌面环境中可以用notifier: type: command command: notify-send 私人助手 {content}{content}会被替换成模型生成的最终回复内容。我在自家桌面上跑的效果是早上 8 点右上角弹出一条包含天气和待办的本地通知不需要打开浏览器或者任何聊天界面。手机推送更直接我用的是一个通用 webhook 转发服务配置如下watchtower: target: https://push-gateway.example.com/send title: 私人助手只要是支持 JSON POST 请求的推送网关都能用。实话说这一块本身没什么技术含量但它带来的体验提升极大。助手从你问它才答变成了到点就提醒你这才是私人助手和聊天机器人之间的根本差异。5.3 日程管理和待办尽快接入这才是日常用的核心天气播报只是开胃菜。真正让我觉得这套系统值回票价的是待办接入。我把待办也做成了插件底层存一个结构化文件简单可靠不需要额外的服务。用法是提醒我周五下午提交周报 —— 助手解析时间写入待办文件。把明天上午的会取消改成线上 —— 助手调用更新插件。每天早上播报时读取待办列表按照截止时间排序输出。这个组合用顺手之后我已经不太依赖独立的待办应用了。因为一切都在一个聊天入口里完成问一句我今天还有什么没做助手把清单拉出来再基于清单安排优先级非常自然。这也是我建议你优先做插件的方向搜资料的频率其实远低于记录待办和日程。6. 从搭建到稳定运行我记录下来的五类实战问题任何工具跑起来之后都会有一堆文档里没写的坑。我在使用 nanobot 的前两周里前后遇到了五类比较典型的问题逐个记录一下排查链路和解决办法。6.1 工具调用死循环模型反复搜索停不下来我遇到第一个毛病是模型在一个问题上反复调用搜索插件。日志里出现了连续四次web_search调用每次参数来源都一样看起来就像它陷入了思考短路。排查链路是这样的我先确认模型输出发现第一次搜索的结果已经返回了但是摘要内容太简单模型觉得信息不够于是又搜了一次第二次搜到的还是同一个来源它又开始追问同一个问题。真正的原因是搜索插件的摘要字段太短模型无法从结果里提取足够信息只好重复尝试。解决办法是双向调整搜索插件的摘要从 50 字扩展到 200 字同时在配置里给工具调用设置轮数上限。agent: max_tool_rounds: 3超过三次工具调用后框架会强制模型必须直接回复用户。这个兜底配置我认为是必加的它把一个可能无限消耗时间的隐患直接封死。有些框架把这种问题叫Agent 死循环本质上是模型在不确定状态下反复确认给模型更充足的材料和设一道上限双管齐下最有效。6.2 中文文件名乱码一次编码问题的完整定位过程国产环境使用这类框架中文乱码几乎避不开。我的save_note插件在保存中文文件名内容时第一次写出来的文件打开全是乱码文件名也变得不可读。我先用file命令查看文件类型发现落盘文件是 UTF-8 编码但同时发现了异常文件内容头部多了一段类似转义后的结果。跟踪后发现是插件在接收参数时模型网关传回的内容被错误地按拉丁编码解码了一次导致中文变成了乱码字符串。这类问题的排查思路有几个固定动作在插件入口处打印参数信息确认框架传给插件的内容本身就是乱的还是插件内部处理乱的。我最终的处理方式是框架配置里显式声明所有文本按 UTF-8 解析。在插件代码里对来自模型的参数做一次强制转换def _safe_str(value): if isinstance(value, bytes): return value.decode(utf-8, errorsreplace) if isinstance(value, str): return value.encode(utf-8, errorsreplace).decode(utf-8) return str(value)所有文件读写操作都显式传encodingutf-8不依赖系统默认编码。这里必须提醒一句跨平台开发时永远不要依赖系统默认编码这个隐含假设。你在自己的机器上跑得好好的换个环境可能立刻翻车。统一显式指定 UTF-8 是一种性价比最高的防御姿势。6.3 低配机器上模型响应慢从 15 秒压到 5 秒我最初是在一台老笔记本上跑的经常出现用户发完消息要等十几秒才回复。我一度以为是模型服务负载太高最后看了系统监控才发现CPU 并没有打满瓶颈在对话轮数太多和系统提示词太长。排查记录如下第一系统提示词被我写得太长包含了大量示例对话和规则说明。模型每次请求都要处理这一大段文本尤其在没有流式输出时必须等模型完整生成完才显示体感非常慢。第二工具描述太啰嗦光是一个搜索插件的技能描述就写了 200 字模型每次都要和其他插件描述一起编进系统提示词。第三没有开启流式输出导致用户等待时间被拉满。我做的优化有三个优化项改动效果精简系统提示词从 600 字压到 150 字首字响应时间减少约 30%精简技能描述每个插件描述控制在 80 字以内模型误调用减少请求轮数下降开启流式输出配置stream: true用户看到逐字输出体感大幅改善关于流式输出多说一句它不减少总耗时但用户体验差异巨大。等 15 秒然后一次性看到全文和 2 秒开始从头看到字往外蹦后者会让人感觉快多了。私人助手是自己用的这一点感知可能没那么重要但对于想分享给朋友的场景流式输出几乎是必需品。6.4 上下文越聊越胀会话变笨且变贵的根源nanobot 默认会把整个会话历史都发给模型。我的 main 会话每天早晨播报一次加上偶尔的对话聊了一周之后单条消息的响应时间越来越长。我看了调试日志发现一次请求往模型发送的 tokens 已经超过了一万其中大部分是陈旧的闲聊记录。解法是靠配置里的上下文管理策略memory: max_turns: 20 summary_every: 10max_turns: 20表示只保留最近 20 轮对话超过的部分会被截断。summary_every: 10可以在会话超过 10 轮时让框架自动生成一段摘要用摘要代替被截掉的早期内容。这两个参数组合起来的逻辑是最近的具体对话保留原样更早的内容只留一段概括性记忆。对私人助手这种日常使用场景我认为 20 轮的上限是合适的。你要是做深度长文档分析可能需要调大但代价是响应时间变长。调优思路是记录自己最长的真实对话轮数然后乘以 1.5 设置上限。别照着别人的建议拍脑袋你的使用习惯和别人不一样。6.5 并发消息导致状态错乱一个容易被忽略的坑我的助手接入了消息网关之后可以同时从桌面端和手机端发消息。没两天我就发现一个诡异现象桌面端问了一个问题手机端却在等一个不该出现的回复再问两句助手开始把两个会话的内容混着说。排查到最后问题出在我自己写的并发处理上。nanobot 本身是支持多会话的但我在绑定消息入口时用了同一个会话 ID两条消息同时到达框架内部为这个会话创建了多个上下文对象导致互相覆盖。解决方式有两个层面。第一不同消息源用不同会话 ID这个最简单也最推荐。手机端和桌面端各保留一个独立会话互不干扰。第二如果你确实需要多个入口共用同一个上下文那么并发消息需要进入队列逐条处理。我在配置里加了server: concurrency: 1这行配置意味着同一时间只处理一条消息。代价是并发时消息会排队但对于个人使用场景这个代价几乎可以忽略。相反它换来的是上下文不再错乱。如果未来我有多人使用的需求再把这行配置调大也不迟。7. 最后分享一个小经验让助手做减法而不是加法整套系统跑通到现在我最常被问到的问题是你装了哪些插件。说实话我现在启用的插件只有五六个远没有最开始设想的多。最初我一口气写了十几个插件后来一个一个关掉了因为很多功能听起来有用实际用上两回就再也不碰了。我现在的原则是只有当某个事情连续两周重复出现三次以上我才会考虑为它专门写一个插件。如果你也想复刻这套私人助手我强烈建议你从一条最窄、最完整的链路开始。不要急着追求大而全先把说一句话-助手搜索-返回答案这一条链路跑到稳定再逐步加入待办、定时推送、文件保存这些能力。每一步都保持在一个可以随时回滚的状态最后形成的系统才会是你真正每天都愿意用的东西而不是一个只能在周末折腾的玩具。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑