1. 项目概述:一个为“共生”而生的AI智能体框架
如果你和我一样,在过去的几年里尝试过各种AI智能体框架,从LangChain到AutoGen,再到LlamaIndex,你可能会发现一个共同点:它们大多是为“单用户、单任务、理想环境”而设计的。它们在一个干净的沙箱里运行得很好,但一旦你把它们丢进一个真实、混乱、多人在线的协作环境——比如一个活跃的Slack频道、一个Telegram群组,或者一个内部开发者的Discord服务器——它们就开始“水土不服”了。上下文管理混乱、会话状态难以隔离、不同渠道的适配代码重复……这些问题会迅速暴露出来。
这就是Bub诞生的背景。它不是一个实验室里的玩具,而是一个在真实的“群聊战场”中成长起来的框架。它的核心设计哲学是“共生”——让AI智能体能够像一个真正的队友一样,与人类和其他智能体在同一个“嘈杂”的对话环境中协同工作。Bub的Slogan “Bub it. Build it.” 非常贴切,它鼓励你直接用它来构建,因为它本身就是一个为构建而生、高度可塑的“通用形态”。
简单来说,Bub是一个基于Python的、插件化的AI智能体框架。它的核心极其精简(约200行代码),所有功能,包括你认为的“内置”功能,都只是默认插件,可以被你完全替换或覆盖。它不假设任何特定的工作流,而是通过一套精心设计的“钩子”系统,让你能够精确控制智能体处理每一次交互的完整生命周期。无论是通过命令行的一次性指令,还是在Telegram群聊里的持续对话,都走同一套处理管道,确保了行为的一致性。
2. 核心设计哲学与架构拆解
2.1 从“会话积累”到“按需装配”:基于Tape的上下文管理
大多数智能体框架管理上下文的方式是“会话历史积累”。用户和模型的对话被不断追加到一个列表中,随着轮次增加,这个列表会越来越长。为了解决token限制问题,常见的做法是进行“摘要压缩”,但这不可避免地会导致信息损失,智能体可能会忘记一些关键的细节。
Bub彻底摒弃了这种方式,转而采用来自 Tape 的理念。你可以把Tape想象成一个只追加、不可变的事实记录带。每一次交互、每一个工具调用结果、每一个系统事件,都被作为一个独立的“事实”记录在这条带上。智能体需要上下文时,不是去翻阅一个冗长的聊天记录,而是根据当前任务的需要,从这条“磁带”中按需选取、组装相关的“事实片段”。
为什么这种设计更优?
- 避免信息稀释:传统的摘要会丢失细节,而Tape保留了所有原始事实。智能体在需要时可以回溯到最精确的信息。
- 支持复杂场景:在多人、多智能体的群聊中,对话是交错并发的。基于会话历史的线性模型很难处理这种交错性。Tape的“事实”模型天然支持从多个并行会话线中提取相关信息。
- 状态清晰:通过“锚点”标记会话的不同阶段(例如,一个复杂任务开始、一个子任务完成),智能体可以更清晰地理解当前所处的“上下文段落”,而不是面对一锅粥的历史记录。
2.2 彻底的插件化:一切皆钩子
这是Bub架构中最激进也最迷人的部分。整个智能体处理单次消息(称为一个“轮次”)的生命周期,被抽象为一个清晰的管道,而管道的每一个阶段,都是一个“钩子”。
解析会话 → 加载状态 → 构建提示词 → 运行模型 ↓ 派发输出 ← 渲染输出 ← 保存状态这个管道不是硬编码在框架核心里的。框架核心只定义了这些钩子接口(在hookspecs.py中)和调用它们的顺序(在framework.py中)。具体每个钩子做什么,完全由插件来实现。所谓的“内置功能”,只不过是几个默认注册的插件而已。
这种设计带来的颠覆性优势:
- 无特权代码:框架核心不包含任何业务逻辑。如果你想改变提示词构建方式,你不需要去修改框架源码,只需要写一个插件,实现
build_prompt钩子,并注册它。你的插件会覆盖默认的插件。 - 极致的可定制性:你可以替换模型调用逻辑(
run_model)、改变输出格式(render_outbound)、甚至重写整个状态管理机制(load_state/save_state)。Bub提供了一个坚实的骨架,而血肉完全由你定义。 - 清晰的关注点分离:每个插件只关心一个特定的钩子,代码更内聚,更容易测试和维护。
2.3 技能即文档:降低创作与使用门槛
在许多框架中,给智能体添加新能力(技能)需要编写Python类,使用装饰器注册,并确保导入路径正确。这对开发者很友好,但对领域专家或想快速共享能力的人来说,门槛较高。
Bub提出了“技能即文档”的概念。一个技能就是一个Markdown文件(例如SUMMARIZE.md),文件顶部用YAML格式的Frontmatter声明技能的名称、描述、参数等信息,下面则是给AI模型看的自然语言描述和使用示例。
这种做法的好处:
- 易于创建和分享:任何人只要会写Markdown,就能创建一个技能。你可以把技能文件放在Git仓库里,Bub能自动发现它们。
- 对AI更友好:技能描述是用自然语言写的,这本身就是给大模型的最佳说明书,比结构化的代码注释更易于理解。
- 版本化管理:技能文件可以和项目代码一起进行版本控制,变更历史清晰可见。
当Bub启动时,它会扫描工作区(以及配置的技能路径),自动发现所有SKILL.md文件,解析它们的Frontmatter,并将这些技能作为工具提供给AI模型调用。
3. 从零开始:环境搭建与基础使用
3.1 安装与初始化
Bub推荐使用uv作为Python包管理器和运行器,这能保证依赖环境的隔离和一致性。当然,用传统的pip也完全没问题。
使用 uv (推荐):
# 安装 uv (如果尚未安装) curl -LsSf https://astral.sh/uv/install.sh | sh # 克隆仓库并进入目录 git clone https://github.com/bubbuild/bub.git cd bub # 同步依赖(uv会自动创建虚拟环境) uv sync # 验证安装 uv run bub --help使用 pip:
# 直接从PyPI安装 pip install bub # 验证安装 bub --help安装完成后,你需要配置AI模型。Bub默认使用OpenRouter上的qwen/qwen3-coder-next模型,这是一个在代码任务上表现优秀的模型。你需要设置API密钥。
# 方法一:设置环境变量(适用于OpenAI、OpenRouter等) export OPENAI_API_KEY='your-api-key-here' # 如果使用OpenAI模型 # 或者 export OPENROUTER_API_KEY='your-openrouter-key-here' # 如果使用默认模型 # 方法二:使用Bub的登录命令(目前主要支持OpenAI Codex的OAuth流程) uv run bub login openai # 这会打开浏览器引导你完成OAuth授权,之后密钥会安全地存储在本地。注意:
BUB_API_KEY环境变量是通用后备选项。Bub的模型客户端会按照OPENAI_API_KEY->OPENROUTER_API_KEY->BUB_API_KEY的顺序查找密钥。建议根据你实际使用的模型提供商来设置对应的环境变量。
3.2 三种核心运行模式体验
Bub提供了三种交互模式,对应不同的使用场景。
1. 交互式聊天模式这类似于一个增强版的Python REPL,智能体在此模式下可以持续对话并调用技能。
uv run bub chat启动后,你会看到一个提示符>。你可以直接输入问题,例如“列出当前目录下的文件”。如果要执行内部命令(如管理技能、操作文件系统),需要在命令前加一个逗号,,例如,help查看所有内部命令。
2. 单次任务模式适合自动化脚本或快速执行一次性命令。
uv run bub run "请总结当前Git仓库的最近三次提交"Bub会处理这条消息,调用必要的技能(如读取Git日志),生成结果并输出到终端,然后退出。
3. 网关监听模式这是让Bub“活”在聊天软件中的关键。启动后,Bub会作为一个服务,监听特定渠道(如Telegram)的消息。
uv run bub gateway要使用此模式,你需要先配置通道适配器。以Telegram为例,需要设置TELEGRAM_BOT_TOKEN和TELEGRAM_CHAT_ID环境变量。当配置好后,在对应的Telegram群组中@你的机器人,它就会用Bub的核心管道来处理消息。
3.3 初探技能系统
Bub的强大之处在于其技能。让我们先看看内置技能和如何发现它们。
在bub chat交互模式下,输入内部命令查看技能:
,skill list这会列出所有已发现的技能,包括内置的和在工作区AGENTS.md或skills/目录下找到的。
尝试调用一个简单的内置技能,比如读取文件:
,fs.read path=README.md你会看到Bub执行了文件读取操作并返回了内容。注意,这个命令是由Bub的内部命令系统处理的,而不是AI模型。AI模型调用技能则是另一种方式。
要让AI使用技能,你只需要在聊天中提出需求。例如,在bub chat中:
> 请帮我读取README.md文件并总结其核心内容。AI模型会自己决定调用fs.read技能获取文件内容,然后进行分析和总结。你不需要显式地告诉它用什么技能。
4. 深度定制:编写你的第一个插件
理解了Bub的插件化架构后,最好的学习方式就是动手写一个插件。我们将创建一个简单的“回声”插件,它不会调用AI模型,而是直接将用户输入原样返回,并加上一个前缀。
4.1 创建插件项目结构
首先,我们创建一个独立的目录来管理我们的插件,这有利于复用和分发。
mkdir bub-echo-plugin cd bub-echo-plugin创建一个pyproject.toml文件来定义我们的插件包:
[project] name = "bub-echo-plugin" version = "0.1.0" description = "A simple echo plugin for Bub" authors = [{name = "Your Name", email = "you@example.com"}] readme = "README.md" requires-python = ">=3.9" dependencies = [ "bub>=0.1.0", ] [project.entry-points."bub"] echo = "bub_echo_plugin.plugin:EchoPlugin" [build-system] requires = ["hatchling"] build-backend = "hatchling.build"关键部分是[project.entry-points."bub"]。这行告诉Bub:在“bub”这个入口点组下,有一个名为“echo”的插件,其实现位于bub_echo_plugin.plugin模块的EchoPlugin类中。
4.2 实现插件逻辑
接下来,创建插件的主要代码文件。首先创建包目录和__init__.py:
mkdir -p bub_echo_plugin touch bub_echo_plugin/__init__.py然后创建插件实现文件bub_echo_plugin/plugin.py:
""" 一个简单的Bub回声插件。 这个插件拦截了提示词构建和模型运行两个钩子,实现了直接回声的功能。 """ from typing import Any, Dict from bub import hookimpl class EchoPlugin: """回声插件实现类。""" @hookimpl def build_prompt(self, message: Dict[str, Any], session_id: str, state: Dict[str, Any]) -> str: """ 构建提示词的钩子实现。 这里我们完全忽略原有的提示词构建逻辑,直接返回一个加工后的用户消息。 参数: message: 原始入站消息字典,通常包含 'content' 等字段。 session_id: 当前会话的唯一标识符。 state: 当前轮次的状态字典,可以在钩子间传递信息。 返回: 一个字符串,将作为“提示词”传递给 `run_model` 钩子。 """ user_content = message.get('content', '') # 我们给内容加上一个回声标记 echoed_prompt = f"[ECHO PLUGIN ACTIVATED] User said: {user_content}" # 我们可以修改state,供后续钩子使用(虽然这个简单例子用不到) state['processed_by_echo'] = True return echoed_prompt @hookimpl async def run_model(self, prompt: str, session_id: str, state: Dict[str, Any]) -> str: """ 运行模型的钩子实现。 这里我们完全绕过任何AI模型调用,直接返回接收到的prompt。 参数: prompt: 由 `build_prompt` 钩子产生的提示词字符串。 session_id: 当前会话的唯一标识符。 state: 当前轮次的状态字典。 返回: 一个字符串,将作为模型生成的“响应”传递给后续钩子。 """ # 简单地将prompt作为响应返回,实现“回声” # 在实际插件中,这里可能会调用OpenAI API、本地模型等。 model_response = f"<Echo Response> {prompt}" return model_response # 注意:我们没有实现 `render_outbound` 钩子,所以会使用Bub默认的或其它插件的渲染逻辑。 # 默认渲染器可能会直接输出我们的 `model_response` 字符串。代码解读:
- 导入与装饰器:我们从
bub导入hookimpl装饰器。任何插件方法都需要用这个装饰器标记,Bub才能识别它。 build_prompt方法:这个钩子负责将原始用户消息转换为给模型的提示词。我们这里做了最简单的处理:提取用户内容,加上一个前缀。我们还修改了state字典,展示了如何在钩子间传递信息。run_model方法:这个钩子通常负责调用大语言模型。我们这里“劫持”了这个过程,直接返回一个加工后的提示词字符串,模拟了模型响应。注意这个方法是async的,因为真实的模型调用是异步操作。- 选择性实现:一个插件不需要实现所有钩子。我们只实现了两个,这意味着对于其他钩子(如
load_state,save_state,render_outbound),Bub会使用其他插件(包括内置插件)的实现。
4.3 安装并测试插件
在插件项目目录下,使用uv或pip以可编辑模式安装:
# 使用 uv uv pip install -e . # 或使用 pip pip install -e .现在,进入一个包含Bub项目的工作区(或者任何目录),运行bub chat。由于我们的插件通过entry-points注册,Bub会自动加载它。
在bub chat中,输入:
> Hello, Bub!你期望看到的可能是一个AI生成的回复,但实际输出会是:
<Echo Response> [ECHO PLUGIN ACTIVATED] User said: Hello, Bub!这说明我们的插件已经成功覆盖了默认的提示词构建和模型运行逻辑!Bub的内置AI模型插件被我们“静默”了。
如何确认插件被加载?使用Bub的命令查看所有钩子与插件的绑定关系:
uv run bub hooks在输出列表中,你应该能看到build_prompt和run_model钩子下,列出了我们的EchoPlugin实现。
4.4 插件优先级与覆盖规则
Bub的插件系统遵循“后来者居上”的原则。插件的加载顺序决定了它们的优先级。后加载的插件中实现的钩子,会覆盖先加载的插件中实现的同一个钩子。
我们的pyproject.toml中定义的入口点,通常是在Bub启动时较晚被加载的(取决于Python的包加载机制)。如果你想控制更精确的覆盖,或者有多个插件需要协调,你可能需要研究更高级的插件管理方式,或者考虑将你的定制逻辑打包到Bub项目本身的插件目录中。
实操心得:插件开发的调试技巧开发插件时,一个常见的困惑是:“我的插件真的被加载了吗?” 除了
bub hooks命令,一个更直接的方法是在插件代码的__init__或某个钩子方法开始时加入一个打印语句。但注意,在生产中要移除这些调试输出。更好的做法是利用Bub的日志系统。你可以在插件中获取Bub的logger:import logging; logger = logging.getLogger(__name__),然后使用logger.debug(“Plugin loaded”)来输出信息。通过设置环境变量BUB_LOG_LEVEL=DEBUG来查看详细日志。
5. 技能开发实战:创建“天气查询”技能
插件用于定制框架行为,而技能则是赋予AI模型新的能力。现在我们来创建一个实用的“天气查询”技能。
5.1 技能文件结构与规范
在Bub的工作区(通常是你的项目根目录,或者skills/子目录下),创建一个名为GET_WEATHER.md的文件。
技能文件遵循严格的格式:
- Frontmatter:文件开头用
---分隔的YAML区域,用于声明技能的元数据。 - 描述与示例:YAML之后的内容是Markdown格式,用于向AI模型描述这个技能怎么用。
以下是GET_WEATHER.md的完整内容:
--- name: get_weather description: 获取指定城市的当前天气信息。 parameters: - name: city type: string description: 城市名称,例如“北京”、“San Francisco”。支持中文和英文城市名。 required: true - name: unit type: string description: 温度单位。`c` 表示摄氏度,`f` 表示华氏度。默认为 `c`。 required: false default: c enum: [c, f] returns: type: string description: 格式化的天气信息字符串,包含温度、天气状况、湿度等。 --- # get_weather 技能 此技能允许智能体查询实时天气信息。 ## 功能说明 调用此技能将连接到外部天气API,获取指定城市的当前天气数据,并以人类可读的格式返回。 ## 参数详解 * `city` (字符串,必需): 要查询天气的城市名称。请尽量提供完整的城市名,如“上海市”、“New York”。对于有歧义的名字,可能无法获得准确结果。 * `unit` (字符串,可选): 温度单位。默认为 `c`(摄氏度)。如果用户要求使用华氏度,请将此参数设置为 `f`。 ## 调用示例 **用户请求**: “今天北京天气怎么样?” **智能体应调用**: `get_weather(city="北京")` **用户请求**: “What's the temperature in London in Fahrenheit?” **智能体应调用**: `get_weather(city="London", unit="f")` ## 返回格式 技能将返回一个格式化的字符串,例如: “北京市:晴,当前温度 22°C,体感温度 24°C,湿度 65%,西北风 2级。” 如果城市不存在或API出错,将返回错误信息,例如:“无法获取‘某某市’的天气信息,请检查城市名称是否正确。” ## 注意事项 1. 此技能需要网络连接。 2. 调用频率可能受外部API限制。 3. 返回的天气信息可能有轻微延迟(通常不超过15分钟)。5.2 实现技能的后端逻辑
技能文件只是“说明书”,我们还需要实现真正的逻辑。这通常通过一个插件来完成,该插件实现get_tools钩子,返回一个与技能名对应的函数。
我们在之前插件项目的基础上,创建一个新的插件类。编辑bub_echo_plugin/plugin.py,添加以下内容:
import aiohttp import os from typing import List, Any, Dict from bub import hookimpl # ... 之前已有的 EchoPlugin 类 ... class WeatherSkillPlugin: """为Bub提供天气查询技能的插件。""" @hookimpl def get_tools(self) -> List[Dict[str, Any]]: """ 返回此插件提供的工具列表。 每个工具是一个字典,必须包含 `name` 和 `function`。 `name` 必须与技能文件中的 `name` 一致。 """ return [ { "name": "get_weather", "function": self._get_weather_impl, # 指向实际的实现函数 "description": "获取指定城市的当前天气信息。", # 可与技能描述一致 } ] async def _get_weather_impl(self, city: str, unit: str = "c") -> str: """ 天气查询技能的实际实现函数。 参数名和类型必须与技能文件中定义的 `parameters` 完全匹配。 参数: city: 城市名 unit: 温度单位,'c' 或 'f' 返回: 格式化的天气字符串或错误信息。 """ # 在实际项目中,你应该使用一个可靠的天气API,并妥善保管API Key # 例如:OpenWeatherMap, WeatherAPI, 和风天气等 # 这里我们使用一个模拟的API作为示例 api_key = os.getenv("WEATHER_API_KEY") if not api_key: return "错误:未配置天气API密钥。请设置 WEATHER_API_KEY 环境变量。" # 构建请求URL (这里以和风天气的免费API为例,需要注册获取key) # 实际URL和参数请查阅对应API文档 url = f"https://devapi.qweather.com/v7/weather/now" params = { "location": city, "key": api_key, "lang": "zh", # 根据需求调整语言 "unit": unit, # 注意:不同API的参数名可能不同,这里是示例 } try: async with aiohttp.ClientSession() as session: async with session.get(url, params=params, timeout=10) as resp: if resp.status == 200: data = await resp.json() # 解析API返回的JSON数据,这里需要根据实际API响应结构调整 # 示例解析: temp = data.get('now', {}).get('temp', 'N/A') text = data.get('now', {}).get('text', '未知') humidity = data.get('now', {}).get('humidity', 'N/A') wind_scale = data.get('now', {}).get('windScale', 'N/A') unit_symbol = "°C" if unit == "c" else "°F" return f"{city}:{text},当前温度 {temp}{unit_symbol},湿度 {humidity}%,风力 {wind_scale}级。" else: return f"错误:天气API请求失败,状态码 {resp.status}。" except aiohttp.ClientError as e: return f"错误:网络请求失败 - {str(e)}" except Exception as e: return f"错误:处理天气数据时发生意外 - {str(e)}" # 重要:我们需要更新入口点,让Bub加载这个新的插件类 # 修改 pyproject.toml 中的 entry-points,或者创建一个新的入口点。 # 为了简单,我们可以修改 EchoPlugin,让它也提供工具,或者创建一个综合插件类。 # 这里我们创建一个新的综合插件类来包含所有功能。 class MyCustomPlugin(EchoPlugin, WeatherSkillPlugin): """集成了回声和天气功能的综合插件。""" pass # 然后,在 pyproject.toml 中将入口点指向 MyCustomPlugin # [project.entry-points."bub"] # myplugin = "bub_echo_plugin.plugin:MyCustomPlugin"关键点解析:
get_tools钩子:这个钩子返回一个工具字典列表。name字段必须与技能Markdown文件Frontmatter中的name严格一致。Bub通过这个名称将技能描述和工具实现关联起来。- 实现函数:
_get_weather_impl是实际执行逻辑的函数。它的参数 (city,unit) 必须与技能文件中定义的parameters在名称和类型上兼容。Bub会利用Pydantic等工具进行参数验证和绑定。 - 异步支持:由于涉及网络请求,实现函数是异步的 (
async def)。Bub的框架能正确处理异步工具。 - 错误处理:在工具实现中,必须做好错误处理,并以清晰的字符串形式返回错误信息。AI模型会接收到这个字符串,并可能决定如何向用户报告。
5.3 注册与测试新技能
首先,更新pyproject.toml,将入口点指向我们新的综合插件类:
[project.entry-points."bub"] myplugin = "bub_echo_plugin.plugin:MyCustomPlugin" # 改为 MyCustomPlugin然后,重新安装插件(因为入口点发生了变化):
pip install -e . --force-reinstall # 或 uv pip install -e . --force-reinstall接下来,将写好的GET_WEATHER.md技能文件放到Bub的工作目录下(例如,项目根目录)。同时,你需要获取一个真实的天气API Key并设置环境变量:
export WEATHER_API_KEY="your_actual_weather_api_key_here"现在,启动bub chat:
uv run bub chat首先,确认技能已被发现:
,skill list你应该能在列表中看到get_weather。
然后,让AI使用这个技能:
> 查询一下上海现在的天气。如果一切配置正确,Bub的AI模型会理解你的指令,自动调用get_weather工具,插件会执行网络请求,并将结果返回给AI模型,最终AI会生成一个包含天气信息的自然语言回复给你。
注意事项:技能与工具的匹配最常见的错误是技能文件的
name和插件get_tools返回的name不匹配,或者参数定义不一致。务必仔细检查。使用bub hooks命令可以查看已注册的工具,但调试技能调用逻辑更有效的方法是开启Bub的调试日志:BUB_LOG_LEVEL=DEBUG uv run bub chat,观察AI模型决定调用工具时的日志输出。
6. 高级配置与生产环境部署
6.1 环境变量详解
Bub的配置主要通过环境变量完成,这使得它在各种部署环境中都非常灵活。
| 变量名 | 默认值 | 描述与使用技巧 |
|---|---|---|
BUB_MODEL | openrouter:qwen/qwen3-coder-next | 模型标识符。格式为provider:model-name。例如:openai:gpt-4o、anthropic:claude-3-5-sonnet、openrouter:meta-llama/llama-3-70b-instruct。OpenRouter是一个聚合平台,可以访问多种模型。 |
BUB_API_KEY | — | 通用API密钥。如果设置了特定provider的key(如OPENAI_API_KEY),则优先使用。否则Bub会尝试使用此变量。 |
BUB_API_BASE | — | 自定义API端点。用于连接自托管的模型服务(如LocalAI, vLLM, Ollama)。例如:BUB_API_BASE=http://localhost:11434/v1用于连接本地的Ollama服务。 |
BUB_API_FORMAT | completion | API通信格式。可选completion(旧版OpenAI格式)、responses(新版OpenAI格式) 或messages(通用Chat格式)。大多数现代API使用messages。 |
BUB_CLIENT_ARGS | — | 底层客户端参数。一个JSON字符串,用于传递额外的参数给底层的模型客户端库。例如:BUB_CLIENT_ARGS='{"timeout": 60, "max_retries": 3}'。 |
BUB_MAX_STEPS | 50 | 最大工具调用步数。防止AI陷入无限循环。如果一个任务需要链式调用很多工具,可以适当调高。 |
BUB_MAX_TOKENS | 1024 | 单次模型调用的最大生成token数。根据模型能力和任务复杂度调整。复杂任务可能需要2048或4096。 |
BUB_MODEL_TIMEOUT_SECONDS | — | 模型调用超时时间。网络不稳定或模型响应慢时有用。例如设置为30。 |
BUB_LOG_LEVEL | INFO | 日志级别。调试时设置为DEBUG可以看到详细的钩子调用、工具调用和网络请求信息。 |
BUB_WORKSPACE | 当前目录 | 工作区根目录。Bub在此目录下寻找AGENTS.md和skills/文件夹。 |
配置示例:使用本地Ollama运行Llama 3模型
export BUB_MODEL="ollama:llama3.1" export BUB_API_BASE="http://localhost:11434/v1" # Ollama通常不需要API Key,但如果设置了验证,可以配置 # export BUB_API_KEY="ollama-api-key" export BUB_API_FORMAT="messages" uv run bub chat6.2 部署为长期运行的服务
要让Bub作为一个真正的“群聊队友”运行,你需要将其部署为一个长期运行的服务。
使用 Systemd (Linux)这是最经典的方式。创建一个服务文件/etc/systemd/system/bub-gateway.service:
[Unit] Description=Bub AI Agent Gateway After=network.target [Service] Type=simple User=your_username WorkingDirectory=/path/to/your/bub/workspace Environment="PATH=/home/your_username/.local/bin:/usr/local/bin:/usr/bin:/bin" Environment="TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN" Environment="TELEGRAM_CHAT_ID=YOUR_CHAT_ID" Environment="OPENAI_API_KEY=YOUR_OPENAI_KEY" # 其他环境变量... ExecStart=/home/your_username/.local/bin/uv run bub gateway Restart=always RestartSec=10 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target然后启用并启动服务:
sudo systemctl daemon-reload sudo systemctl enable bub-gateway.service sudo systemctl start bub-gateway.service # 查看日志 sudo journalctl -u bub-gateway.service -f使用 DockerDocker提供了更好的隔离性和可移植性。首先创建一个Dockerfile:
FROM python:3.11-slim WORKDIR /app # 安装 uv 用于高效的依赖管理 RUN pip install --no-cache-dir uv # 复制项目文件 COPY . . # 使用 uv 同步依赖 RUN uv sync --frozen # 设置环境变量(敏感信息建议通过运行时传入) ENV BUB_WORKSPACE=/app ENV PYTHONPATH=/app/src # 运行网关 CMD ["uv", "run", "bub", "gateway"]构建并运行:
docker build -t bub-gateway . docker run -d \ --name bub-agent \ -v $(pwd)/skills:/app/skills \ # 挂载技能目录 -v $(pwd)/.bub_data:/app/.bub_data \ # 挂载数据目录(如果需要持久化状态) -e TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} \ -e OPENAI_API_KEY=${OPENAI_API_KEY} \ -e BUB_MODEL="openai:gpt-4" \ bub-gateway6.3 性能优化与监控
- 连接池:如果技能插件频繁进行网络请求(如天气插件),考虑在插件初始化时创建
aiohttp.ClientSession并复用,而不是为每个请求新建。 - 状态缓存:对于
load_state钩子,如果从数据库或远程存储加载状态较慢,可以考虑添加内存缓存,但要注意会话隔离和缓存失效。 - 异步优化:确保所有I/O操作(网络、磁盘、数据库)都使用异步库(如
aiohttp,aiofiles,asyncpg),避免阻塞事件循环。 - 日志聚合:在生产环境中,将Bub的日志(通过Python
logging输出)接入到像ELK、Loki或Sentry这样的日志聚合系统中,便于监控和排查问题。 - 健康检查:可以为Bub网关添加一个简单的HTTP健康检查端点(通过一个自定义插件实现
@hookimpl挂接到某个生命周期事件,或者启动一个简单的副线程HTTP服务器),方便Kubernetes或负载均衡器检查服务状态。
7. 故障排除与常见问题
在实际使用和开发Bub插件、技能的过程中,你可能会遇到一些典型问题。以下是一个快速排查指南。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
运行bub chat或bub run立即报错ModuleNotFoundError或ImportError。 | 1. Bub未正确安装。 2. 虚拟环境未激活或路径不对。 3. 自定义插件依赖未安装。 | 1. 使用 `pip list |
自定义插件已安装,但功能未生效(如bub hooks中未列出)。 | 1. 插件入口点未在pyproject.toml中正确声明。2. 插件包未安装或安装失败。 3. 插件类未使用 @hookimpl装饰器。 | 1. 检查pyproject.toml中[project.entry-points."bub"]部分格式是否正确。2. 重新安装插件并检查是否有错误信息: pip install -e . --force-reinstall。3. 确保插件类中的方法都正确使用了 from bub import hookimpl和@hookimpl。 |
技能文件已创建,但,skill list未显示或AI无法调用。 | 1. 技能文件不在Bub的工作区目录下。 2. 技能文件命名不正确(应为 SKILL_NAME.md)。3. 技能文件的Frontmatter YAML格式错误。 4. 插件中 get_tools返回的name与技能文件中的name不匹配。 | 1. 将技能文件放在项目根目录或skills/子目录下。检查BUB_WORKSPACE环境变量。2. 确保文件名是大写下划线风格,如 GET_WEATHER.md。3. 使用YAML校验器检查 ---之间的内容,确保缩进、冒号后空格正确。4. 仔细核对插件代码和技能文件中的 name字段,必须完全一致(包括大小写)。 |
| AI模型不调用技能,而是尝试用文字回答。 | 1. 模型能力不足,无法理解工具调用格式。 2. 系统提示词中未充分描述可用技能。 3. 用户请求的描述不够清晰,模型无法匹配到技能。 | 1. 尝试更换更强大的模型(如GPT-4)。 2. 检查工作区是否有 AGENTS.md文件,其中应清晰描述智能体的职责和可用技能。Bub会自动将其附加到系统提示词。3. 在技能文件的Markdown描述部分,提供更丰富、更贴近自然语言的示例。 |
| 技能调用失败,返回权限错误或网络错误。 | 1. 技能插件实现中访问了需要权限的资源(如文件、网络)。 2. API密钥未设置或错误。 3. 网络防火墙或代理阻止了请求。 | 1. 确保Bub进程有足够的权限读取/写入所需文件。 2. 使用 echo $API_KEY_VAR确认环境变量已正确设置并导出。3. 在技能插件代码中添加更详细的错误日志,或设置 BUB_LOG_LEVEL=DEBUG查看网络请求详情。 |
bub gateway启动后收不到Telegram消息。 | 1.TELEGRAM_BOT_TOKEN或TELEGRAM_CHAT_ID环境变量未设置或错误。2. 机器人未添加到群组,或未在群组中授予发送消息权限。 3. 服务器网络无法访问Telegram API。 | 1. 通过 `env |
| 处理速度慢,响应延迟高。 | 1. 模型API调用慢(如GPT-4)。 2. 技能插件中有同步阻塞操作(如未使用异步库进行网络请求)。 3. 状态加载/保存钩子(如连接慢速数据库)。 | 1. 考虑使用更快的模型(如GPT-3.5-Turbo),或调整BUB_MODEL_TIMEOUT_SECONDS。2. 将插件中的所有I/O操作改为异步(使用 async/await和aiohttp等库)。3. 优化 load_state/save_state插件,添加缓存或使用更快的存储后端。 |
调试心法:日志是你的朋友当遇到任何诡异的问题时,第一反应应该是打开调试日志:
BUB_LOG_LEVEL=DEBUG uv run bub chat 2>&1 | less仔细查看日志输出,你会看到:
- 插件加载顺序。
- 每个钩子被哪个插件实现调用。
- 模型接收到的原始提示词和返回的响应。
- 工具调用的参数和返回值。
- 网络请求的详细信息。 这些信息是定位问题最直接的依据。
Bub的设计哲学是简洁和模块化,这意味着大部分问题都出现在模块的边界上:插件注册、技能定义、环境配置。按照上述表格系统性排查,大部分问题都能迎刃而解。