☰
用nanobot搭建本地私人助手:从零配置到定时推送的完整实践
2026/10/22 18:48:45 网站建设 项目流程

事情要从我桌面上那十几个碎片化工具说起。日历一个程序,提醒一个脚本,查资料要开浏览器,记笔记又得换一个窗口,每天来回切换,真正静下来干活的时间没剩多少。后来我给自己定了个目标:用一个统一的入口把杂事都收敛起来,让它变成一个24小时在线的私人助手。正好在技术社区里看到有人聊 nanobot 这个轻量级助手框架,核心思路是用一套 YAML 配置加几个插件,就能把本地模型变成真正能干活的助手。我照着这个思路从零搭了一版,从空目录跑到了稳定的定时推送,实测下来很稳。这篇文章就把完整过程拆给你看,包括需求拆解、配置思路、插件写法,以及我踩过的五个坑,给同样想为自己做一个私人助手的读者一条能直接上手的路径。

1. 动手前的需求拆解:这个私人助手到底要干什么

很多人一上来就想着"我要做一个全能的AI助手",结果装了一堆插件,最后发现最常用的还是聊天。我这次反着来,先冷静列了一下自己每天在电子设备上重复做的事情,再决定助手只做哪几件。

1.1 把日常杂事收敛成四个核心能力

我连续记了一周自己的使用习惯,最后划分出四个必须满足的需求项:

需求具体场景我想要的交互方式
对话检索查天气、问概念、整理一段文字直接在聊天窗口说一句话
资料抓取与记录把一篇网页内容摘要后存成笔记告诉助手网址,它自动存成Markdown
定时提醒每天早上推送天气、待办和会议提醒不用我操作,到点主动发消息
本地优先数据尽量留在自己机器上,不依赖某个云端平台断网时基础对话和记录功能仍能用

这一步非常重要。你会发现"私人助手"这个词其实特别宽泛,不拆解的话很容易做成一个四不像。我最后砍掉了语音控制、复杂日程协同、多用户权限这些听起来很酷但短期用不上的功能,只保留上面四件事。

1.2 为什么选 nanobot 而不是自己从零写

我看过不少从头写助手的案例,大多数人卡在了同一个地方:模型接口调用、会话上下文管理、工具调用的参数解析、插件热加载,这些看起来是小问题,真做起来每一项都要花不少时间。自己写没有十天半个月很难稳定,而且写出来的代码往往只适配自己的使用习惯。

nanobot 的价值在于它提供了一套最小骨架:消息进来之后该走什么流程、模型返回的工具调用 JSON 该怎么解析、插件放在哪个目录才会被自动加载,这些框架都帮你处理好了。我需要做的只有三件事:改配置文件、写几个插件、设定时任务。用盖房子的类比来说,nanobot 是把地基和框架搭好了,我只负责往里面砌墙、装门窗,而不是从烧砖开始。

当然,框架也有它的限制:如果某个功能它的插件机制表达不了,你可能会被迫改框架源码。但对我来说,能用一个下午跑通核心链路,这个性价比已经很高了。

2. nanobot 的核心工作方式:先搞懂一条消息怎么变成一件事

在动手配置之前,我建议你先花十分钟理解 nanobot 的运行逻辑。这个理解到位了,后面写插件、排错都会很顺。

2.1 从"收到一句话"到"完成一件事"的处理链路

nanobot 的一个会话请求,在内部是这样一个循环过程:

  1. 用户消息进入会话,框架从本地数据库里取出该会话最近的聊天记录。
  2. 框架把聊天记录、系统提示词、插件描述信息拼成一次模型请求。
  3. 模型返回两类内容之一:要么是纯文本回复,要么是一个工具调用指令(JSON 格式)。
  4. 如果是纯文本,直接返回给用户,流程结束。
  5. 如果是工具调用,框架解析 JSON,找到对应的插件并执行。
  6. 插件返回结果,框架把结果作为一条新的消息追加到本轮对话。
  7. 框架带着插件结果,再次请求模型。
  8. 循环第 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}", timeout=10) 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(parents=True, exist_ok=True) target = ALLOWED_DIR / safe_name target.write_text(content, encoding="utf-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 编码,但同时发现了异常:文件内容头部多了一段类似转义后的结果。跟踪后发现是插件在接收参数时,模型网关传回的内容被错误地按拉丁编码解码了一次,导致中文变成了乱码字符串。

这类问题的排查思路有几个固定动作:在插件入口处打印参数信息,确认框架传给插件的内容本身就是乱的,还是插件内部处理乱的。我最终的处理方式是:

  1. 框架配置里显式声明所有文本按 UTF-8 解析。
  2. 在插件代码里,对来自模型的参数做一次强制转换:
def _safe_str(value): if isinstance(value, bytes): return value.decode("utf-8", errors="replace") if isinstance(value, str): return value.encode("utf-8", errors="replace").decode("utf-8") return str(value)
  1. 所有文件读写操作都显式传encoding="utf-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: 10

max_turns: 20表示只保留最近 20 轮对话,超过的部分会被"截断"。summary_every: 10可以在会话超过 10 轮时,让框架自动生成一段摘要,用摘要代替被截掉的早期内容。这两个参数组合起来的逻辑是:最近的具体对话保留原样,更早的内容只留一段概括性记忆。

对私人助手这种日常使用场景,我认为 20 轮的上限是合适的。你要是做深度长文档分析,可能需要调大,但代价是响应时间变长。调优思路是:记录自己最长的真实对话轮数,然后乘以 1.5 设置上限。别照着别人的建议拍脑袋,你的使用习惯和别人不一样。

6.5 并发消息导致状态错乱:一个容易被忽略的坑

我的助手接入了消息网关之后,可以同时从桌面端和手机端发消息。没两天我就发现一个诡异现象:桌面端问了一个问题,手机端却在等一个不该出现的回复,再问两句,助手开始把两个会话的内容混着说。

排查到最后,问题出在我自己写的并发处理上。nanobot 本身是支持多会话的,但我在绑定消息入口时用了同一个会话 ID,两条消息同时到达,框架内部为这个会话创建了多个上下文对象,导致互相覆盖。

解决方式有两个层面。第一,不同消息源用不同会话 ID,这个最简单,也最推荐。手机端和桌面端各保留一个独立会话,互不干扰。第二,如果你确实需要多个入口共用同一个上下文,那么并发消息需要进入队列,逐条处理。我在配置里加了:

server: concurrency: 1

这行配置意味着同一时间只处理一条消息。代价是并发时消息会排队,但对于个人使用场景,这个代价几乎可以忽略。相反,它换来的是上下文不再错乱。如果未来我有多人使用的需求,再把这行配置调大也不迟。

7. 最后分享一个小经验:让助手做减法,而不是加法

整套系统跑通到现在,我最常被问到的问题是"你装了哪些插件"。说实话,我现在启用的插件只有五六个,远没有最开始设想的多。最初我一口气写了十几个插件,后来一个一个关掉了,因为很多功能听起来有用,实际用上两回就再也不碰了。我现在的原则是:只有当某个事情连续两周重复出现三次以上,我才会考虑为它专门写一个插件。

如果你也想复刻这套私人助手,我强烈建议你从一条最窄、最完整的链路开始。不要急着追求大而全,先把"说一句话-助手搜索-返回答案"这一条链路跑到稳定,再逐步加入待办、定时推送、文件保存这些能力。每一步都保持在一个可以随时回滚的状态,最后形成的系统才会是你真正每天都愿意用的东西,而不是一个只能在周末折腾的玩具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询