折腾 astrbot

nanobot 我关心的 bug 死活不修,影响正常使用了,而且 token 耗的也快……我改成维护更久的 astrbot,这玩意儿 22 年就开始开发了,不过 LLM,主动推送等能力也是最近才开发的所以仍旧 buggy,但仍旧有如下的优点让我用它:

  1. 有一个完善的前端页面可用,可以直接拿来看日志,十分舒适
  2. Skill,MCP,定时任务等功能都有(定时任务比 nanobot 好用,但对 QQ 适配器需要做一些处理)
  3. 支持插件系统来帮助我热修改它的代码或增加功能(这让我把它某种程度上可以 openclaw 化)
  4. 对 QQ 适配器的支持好,支持流式输出,文件、图像的上传下载,主动推送(定时任务等)……
  5. 得益于轻量的系统提示词,astrbot 对 Token 的用量似乎很少

我做了一些处理把 astrbot 去 openclaw 化,这里记一下。

我是用官方的 docker compose 部署的(以安全为考虑),本来是打算同时部署 shipyard,但发现目前的版本对沙箱的配置不太好,对临时文件如上传的图像的 mount 没有处理好。我决定就走 local 了,反正是在 docker 容器里,按天备份就行了,掀不起大风浪。

工作区

执行astrbot run时,默认是把数据存在当前目录的 data 目录下,并在当前目录存一个.astrbot空文件作为标识。在 Docker 容器中,容器在/Astrbot目录下执行,因此 Docker 容器下,astrbot 自己的数据目录存在/Astrbot/data

但这个数据目录东西比较多,如果都让 AI 访问的话我觉得有些噪音了。我的选择是:

  1. 创建一个 workspace 目录,在提示词中强调 AI 以/Astrbot/data/workspace作为工作目录。提示词见下。
  2. (可选?我只是确保一致性,欣慰的是 astrbot 它能正确处理软链接)把/Astrbot/data/skills挪到工作区中,然后在/Astrbot/data中安排一个相对软链接供索引 ln -r -s workspace/skills skills

增加的人格系统提示词如下:

1
2
3
<工作区设定>
你的默认的工作区在 `/Astrbot/data/workspace`,如果你看到相对路径,以该绝对路径为基本路径。你尽量不要污染 workspace 以外的路径。
</工作区设定>

Git 备份工作区

/Astrbot/data创建一个私有仓库,创建 deploy key,然后在 config 中增加这些配置,mount 到 docker 容器中:

1
2
3
4
5
Host github-astrbot-workspace
HostName ssh.github.com
User git
IdentityFile ~/.ssh/astrbot-workspace
IdentitiesOnly yes # 只使用指定的密钥

然后远程仓库就设置成下面这样,就能直接对单独这个仓库用上这个密钥了:

1
git remote set-url origin github-astrbot-workspace:USERNAME/REPO.git

然后给 AI 一个定时任务,让她每天自己做备份,就这样。

可以配置.gitignore减少仓库大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.clawhub/

plugins/*
!plugins/astrbot_plugin_naer_*
plugins.json

dashboard.zip
dist/
site-packages/
temp/
.DS_Store

# SQLite temporary files
*.db-shm
*.db-wal
*.db-journal
__pycache__

迁移 openclaw 的记忆能力

我直接拷贝了 nanobot 的 memory 技能 到工作区中。

但这里有一个问题——Openclaw 中的长期记忆,它是直接将 MEMORY.md 的内容构造到 System Prompt 的,而 astrbot 并没有提供这样的能力,它只能自己读取 MEMORY skill 内容然后自己读 MEMORY.md

但这并不保险,即使严肃提示,AI 还是会偷懒不读 MEMORY.md,所以能做到的话还是尽量希望能动态构造系统提示词。

然而,you guess what?通过插件可以做到,参考文档 接受消息事件时,在这时直接操作系统提示词即可,我选择使用内置的string.Template进行变量插值。

/Astrbot目录下执行astrbot plug new去创建新插件,然后修改 main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import os
from pathlib import Path
from astrbot.api.event import filter, AstrMessageEvent, MessageEventResult
from astrbot.api.star import Context, Star, register
from astrbot.api import logger
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
from astrbot.api.provider import ProviderRequest
from string import Template

@register("helloworld", "YourName", "一个简单的 Hello World 插件", "1.0.0")
class MyPlugin(Star):
def __init__(self, context: Context):
super().__init__(context)

async def initialize(self):
"""可选择实现异步的插件初始化方法,当实例化该插件类之后会自动调用该方法。"""

async def terminate(self):
"""可选择实现异步的插件销毁方法,当插件被卸载/停用时会调用。"""

@filter.on_llm_request(desc='读取 workspace/memory/MEMORY.md,作为 $MEMORY 或 ${MEMORY} 插值到系统提示词')
async def my_custom_hook_1(self, event: AstrMessageEvent, req: ProviderRequest): # 请注意有三个参数
template = Template(req.system_prompt)
memory_file_path = Path(get_astrbot_data_path())/'workspace'/'memory'/'MEMORY.md'
if not memory_file_path.exists():
logger.info(f'文件 {memory_file_path} 不存在,未插值 $MEMORY')
return
result = template.safe_substitute(MEMORY=memory_file_path.read_text('utf-8'))
req.system_prompt = result
logger.debug(f'REPLACED SYSTEM_PROMPT: {req.system_prompt}')

然后,人格系统提示词中增加下面的内容:

1
2
3
4
5
6
<记忆设定>
积极地使用memory skill以记忆和回忆重要信息,保证跨会话上下文一致。但**你无需自己读取长期记忆(MEMORY.md)内容,下面的`<长期记忆>`会自动注入最新的MEMORY.md内容**。
<长期记忆>
${MEMORY}
</长期记忆>
</记忆设定>

一个 QQ 主动推送的 bug

参照我提的 issuepr,因为这个改动可能会有未预期的影响,所以一时半会估计合并不进去,我自己做处理了。

这里最整蛊的地方是,QQ官方解除了主动推送的限制,但被动回复的时间限制还在,结果导致居然移除msg_id反而就能让代码通过了,搞笑啊这。

反正我就直接用插件打猴子补丁了,我自己先解决问题,官方想怎么干就怎么干吧。

在插件初始化时执行下面的函数即可。

1
2
3
4
5
6
7
8
def override_qqofficial_post_c2c_message():
"""重写一个方法,永远地移除掉msg_id参数"""
from astrbot.core.platform.sources.qqofficial.qqofficial_message_event import QQOfficialMessageEvent
old_method = QQOfficialMessageEvent.post_c2c_message
async def new_method(self, *args, **kwargs):
kwargs.pop('msg_id', None)
return await old_method(self, *args, **kwargs)
QQOfficialMessageEvent.post_c2c_message = new_method

SubAgent超时bug

SubAgent没有走平台中配置的工具调用超时,而是默认的60秒超时,这处理deep research就非常勉强。

关于DeepResearch,我发现还是专门整一个独立的MCP服务去处理好使,我用的是 https://github.com/u14app/deep-research

这个还是打一个猴子补丁……我的issue在这里 https://github.com/AstrBotDevs/AstrBot/issues/6671

1
2
3
4
5
6
7
8
9
10
11
def override_context_tool_loop_agent():
old_tool_loop_agent = Context.tool_loop_agent
async def new_tool_loop_agent(self: Context, *args, **kwargs):
if 'tool_call_timeout' in kwargs:
return await old_tool_loop_agent(self, *args, **kwargs)
# 取默认配置(理论上应该获取当前会话对应的配置,但当前会话不好获取)
prov_settings: dict = self.get_config().get("provider_settings", {})
tool_call_timeout = int(prov_settings.get("tool_call_timeout", 60))
kwargs['tool_call_timeout'] = tool_call_timeout
return await old_tool_loop_agent(self, *args, **kwargs)
Context.tool_loop_agent = new_tool_loop_agent

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!