这段时间在折腾一个叫 Pi Bot 的项目。
最初的想法很简单:我想要一个能长期待在群里的 Bot。做下去才发现,"收到消息 → 调一次大模型"是整件事里最简单的部分。
难的是别的。每句话都接、每个问题都答成小论文、聊过的事转头就忘——模型再强也没用,它就是不像这个群里的人。
所以后面大部分时间我不是在调"答得多聪明",而是在调另外三件事:什么时候该闭嘴,该记住什么,该用什么形式回。
什么时候开口
群聊和一对一是两回事。
被点名、被叫名字、被引用,那当然要回。但群里绝大多数话不是说给 Bot 听的。两个人在互相损,有人甩了张图,有人就是感叹了一句——这时候它跳出来总结一下,存在感立刻变成打扰。
所以参与判断是单独一套逻辑:点名、唤醒词、引用、自然续聊、公共提问、热聊状态,各自是不同的触发理由。另外有安静时段,半夜不会因为普通闲聊突然冒出来。群冷太久了可以试着暖场,但想不出合适的话就不说。
实现上有个我自己比较满意的点:Agent 可以显式返回 skip_reply。沉默是一个正常的、可解释的决定,不是生成失败,也不会往群里丢一个 NO_REPLY 之类的占位符。
记忆不是把聊天记录塞回去
很多 Agent 项目讲"记忆",做法就是把历史消息往上下文里堆。短期看有效,聊久了成本、噪声和错误一起涨。
Pi Bot 的状态分三层:
- transcript:真实发生过的对话;
- memory:一周之后还有价值的事实,比如成员偏好、固定称呼、长期关系;
- behavior:大家教给它的相处方式,比如"回复短一点""别每次都接话"。
三层互相独立,也都能审计和改。有人说"你记错了",它不该当场道歉然后下次接着错——它会去找到对应那条记忆,改归属、改内容,或者直接忘掉。而"你话太多了"这种属于偏好,进 behavior,不和事实记忆混在一起。
默认不记的东西比记的多:一次性请求、临时情绪、聊天总结,都不进长期记忆。
回复的形态也是内容
真实聊天里没人只发纯文字。
图片、音频、语音、视频、文件、链接、引用、@,Pi Bot 都能处理。但我更在意的是它什么时候该用——有视觉模型不代表每张图都要分析一遍,只有当前问题真的依赖画面时才调理解工具。别人发语音,它可以跟着用语音回,也可以进持续语音或者耳语模式。
表情包不是塞在菜单第三层的附加功能。Agent 有单独的表情检索和发送工具:遇到"哈哈""离谱""确实"这种短态度,表情库里要是正好有合适的,它就只发表情,不再补一句同义的话。另外还集成了 meme 生成、斗图和复读,自己传的图也能直接进表情库。
不是所有事都值得叫模型
底层用的是 Pi Agent。每次运行,Core 会重新组织当前消息、最近上下文、召回的长期记忆、已学的行为和可用工具,模型不直接面对平台原始数据。
这里有条边界我划得比较死:平台声称"能做什么",不等于模型自动获得这个能力。Adapter 也许支持发文件、解析媒体、管理成员,但只有显式注册过、带限制的 Tool Provider 才会出现在 Pi Agent 的工具列表里。外部网页、附件正文、MCP 返回值、平台 metadata 一律当不可信材料,不能反过来变成系统指令。
另一类是压根没必要叫模型的:入退群通知、拍一拍、连着两个人发同样内容之后的复读,确定性插件就够了。需要语义判断、记忆或者工具协作的时候才让 Agent 上,快,也可控。
重启不能失忆
做 demo 容易,长期跑是另一回事。
入站事件在判断要不要触发模型之前就先落库;同一个事件不会重复跑 Agent;同一会话的消息严格串行,避免两条同时生成、互相看不见;回复记录和待发送动作在同一个事务里提交。发送失败的消息留在 durable outbox 里,进程起来接着重试。
媒体不进 SQLite,也不进主提示词,只存 AssetRef,二进制交给独立的 AssetStore。
这些都比直接写麻烦一点。但项目跑上几个月之后,状态清不清楚、崩了能不能恢复,比少写几行代码重要。
核心和聊天平台是解耦的:Adapter 负责连接、事件转换和实际发送,Core 只收标准事件、出标准动作。换个聊天入口,记忆、插件和 Agent 逻辑不用重写。
现在
已经有的东西:多模型 Provider、主备降级、知识检索、MCP、视觉理解、语音转写和 TTS、定时任务、群聊总结、主动参与、表情库、meme 生成,还有一个 React 管理界面,用来配 Bot、人设、模型、插件、知识库、会话和运行状态。
但我没打算把它当成一个"做完了"的产品。我想知道的还是最开始那个问题:一个 Chatbot 要怎么才能从"会答问题的接口",变成群里一个有分寸的存在。这个我现在也没答案。
MIT License 开源:
GitHub:JimiZhou/pi-bot
在做类似东西的话欢迎来聊,issue 和 PR 都行。
若无法加载请检查网络环境。
若无法加载请检查网络环境,或切回 Disqus 稍后再试。