tva
← Insights

将 Telegram AI 助手从个人使用扩展到团队协作

你已经为自己搭建了一个项目专属的 Telegram AI 助手——将 Claude、GPT 或类似的大语言模型封装成 Telegram bot,运行在自己的服务器上。单人用起来很顺手。现在你想把它分享给团队:开发者、顾问或其他需要随时调用同一个助手的成员。从一个用户扩展到小团队,涉及的环节比文档通常提到的要多得多。

本文覆盖每一个步骤:收集团队成员的 Telegram ID、以能在 Docker restart 与 recreate 区别中幸存下来的方式更新 allow-list、创建合适的群组、配置触发检测避免 bot 刷屏,以及解决大多数团队猝不及防的隐私问题。

准备工作

  • 一个已经能为至少一位用户(你自己)处理消息的 Telegram bot
  • 运行 bot container 的服务器的 SSH 或 shell 访问权限
  • 你想添加的团队成员的 Telegram 用户名
  • BotFather 访问权限(最初创建该 bot 的同一个 Telegram 账号)
  • 大约二十分钟完成技术操作,加上与团队成员的异步沟通时间

本文解决的问题

  • 团队成员无法触发你的 bot——"我这里好的,他们那里不行"
  • allow-list 改动看起来已经生效,但 bot 重启后仍然忽略新用户
  • bot 在群聊中刷屏——对每条消息都回复,而不只回复相关内容
  • 触发逻辑混乱——bot 什么时候该回复?只在 @提及 时?关键词?回复消息?
  • 没有预料到的跨用户记忆泄露引发的隐私顾虑

第一步:收集每位团队成员的 Telegram 数字用户 ID

你的 Telegram AI 助手的 allow-list 使用数字用户 ID 作为键,而不是用户名或显示名称。用户名可以更改,数字 ID 在账号生命周期内是固定的。你需要为每位想要添加的团队成员获取这个 ID。

最简单的方法:让每位团队成员向 @userinfobot(一个公开的工具 bot)发起对话。它回复的第一条消息就包含他们的数字 ID,格式类似 100000001。让他们把 ID 复制下来,通过私信发给你。

如果 @userinfobot 在你们所在地区被屏蔽或无法使用,可以用以下替代方法:

  • 查看你自己 bot 的日志。在 bot 的 allow-list 中间件里临时加一行"记录所有请求"的日志,让团队成员向你的 bot 发一条消息,然后从 container 日志里读取用户 ID。收集到 ID 后删掉那行日志代码。
  • 从群消息中读取。如果某个用户已经在你的 bot 所在的群里发过消息,该消息的 from.id 字段就包含他们的数字 ID——通过 bot 的 getUpdates 端点可以读取。

把收集到的 ID 保存在安全的地方——备忘录、密码管理器,或者直接写进你的 .env 文件。把它们当作邮件地址对待:它们标识具体的个人,可以与公开的 Telegram 主页交叉对照。

第二步:扩展 bot 的 allow-list

主流的 Telegram bot 框架——aiogram、python-telegram-bot、Telegraf、grammY——都将 allow-list 检查实现为中间件。每条传入的 update 在触发任何 handler 之前都会按发送者 ID 过滤。不在 allow-list 里的发送者会被静默丢弃:你的 bot 会在内部接收到他们的消息,但永远不会回复。

allow-list 本身通常存放在环境变量里,通过 docker-compose.yml 中的 env_file: 注入到 container:

BOT_ALLOWED_USERS=100000001,100000002,100000003
BOT_OPERATOR_ID=100000001

常见的两种格式:

  • CSV 列表:BOT_ALLOWED_USERS=111,222,333。由 bot 的配置代码解析为 list[int]
  • JSON 数组:BOT_ALLOWED_USERS=[111,222,333]。当配置解析器对复杂类型期望 JSON 格式时使用。

如果你的 bot 使用 Python 和 pydantic-settings 进行配置,JSON 数组格式是更安全的选择——即使只有一个用户,也优先使用 BOT_ALLOWED_USERS=[100000001] 而不是 BOT_ALLOWED_USERS=100000001。原因在下方的 pydantic 小节中有详细说明,简单来说是 JSON 格式可以绕过一个会让你的 container 在启动时崩溃的解析歧义。

第三步:用正确的方式重启你的 container

大多数 allow-list 更新都在这一步悄悄出了问题。你编辑了 .env,运行 docker compose restart your-bot,看着 container 重新启动——新用户仍然无法触发 bot。改动"没有生效"。

原因是:docker compose restart 只是停止并启动现有的 container,不会重新创建它。环境变量——包括来自 env_file: 的所有内容——是在 container 创建时注入的,而不是启动时。重启会保留原有的环境变量快照,你编辑过的 .env 文件对已重启的 container 毫无影响。

正确的命令:

docker compose up -d --force-recreate --no-deps your-bot-service-name

各参数的作用:

  • --force-recreate:停止旧 container,删除它,然后按当前的 Compose 配置创建一个全新的 container——包含最新编辑过的 env_file: 内容。
  • --no-deps:防止 Compose 同时重新创建 bot 所依赖的其他服务(数据库、消息队列等)。如果你的 bot 没有 depends_on,这个参数是空操作,但加上无害。
  • -d:以后台模式运行重新创建的 container,命令行立即返回。

通过检查 container 的运行时间来确认重建是否成功:

docker ps --filter name=your-bot --format "{{.Status}}"
# 预期结果:"Up 10 seconds"(而不是"Up 4 hours")

如果状态显示的运行时间和之前一样长,说明重建没有发生——检查服务名称是否有拼写错误,或者你是否在正确的目录下执行了命令。

这个规律适用于所有配置存放在 env-file 中的 Docker Compose 服务:API 网关、worker、爬虫、监控 agent。每次都会踩到同一个坑。我们在Docker 化基础设施的日常健康检查指南中记录了管理大量此类 container 的更广泛模式。

pydantic-settings:一个值得提前了解的解析陷阱

如果你的 bot 使用 pydantic-settings——Pydantic v2 的标准配置库——来处理配置层,并且将 allow-list 声明为 list[int] 类型,你会遇到一个值得在踩坑前就了解的解析问题。

pydantic-settings 默认将复杂类型(listdicttuple)视为 JSON 编码。当它从 env-file 读取 BOT_ALLOWED_USERS=111,222 时,会先尝试 json.loads("111,222")。这会因为 JSONDecodeError: Extra data 失败,因为纯粹的 CSV 不是合法的 JSON。你的 container 在启动时会崩溃,并抛出 SettingsError: error parsing value for field

如果你在字段上附加了自定义的 BeforeValidator,并且它知道如何解析 CSV,你可能会以为它会先运行,在 JSON 解码尝试之前拦截原始字符串。但事实并非如此。对于复杂类型,pydantic-settings 会在任何字段级验证器之前执行 JSON 解码步骤。

有两种解决方案:

快速修复——在 env-file 中使用 JSON 数组语法:

BOT_ALLOWED_USERS=[100000001,100000002,100000003]

这是合法的 JSON。pydantic-settings 会直接将其解码为整数列表,无需验证器。代价纯粹是形式上的:列表外面多了一对方括号。

根本修复——为字段加上 NoDecode 注解:

from pydantic_settings import NoDecode
from pydantic import BeforeValidator
from typing import Annotated

def parse_csv(v):
    if isinstance(v, str):
        return [int(x.strip()) for x in v.split(",")]
    return v

class Settings(BaseSettings):
    bot_allowed_users: Annotated[list[int], NoDecode, BeforeValidator(parse_csv)]

NoDecode 完全禁止 JSON 解码步骤。你的 BeforeValidator 接收到原始字符串,并将其解析为 CSV。如果你能控制配置代码,这是更清晰的修复方案。

这个底层问题在 pydantic-settings issue #157 中有跟踪记录,相关讨论也见于 #184 和 #570。该行为在所有当前已发布的 pydantic-settings 版本(v2.x)中保持一致。如果你无法控制配置代码——比如使用第三方 bot 框架——就用 JSON 数组语法的临时方案。

第四步:创建包含你的 bot 和团队的 Telegram 群组

针对这种用途,Telegram 有两种群组类型:

  • 普通群组:最多 200 人,简单的管理员模型,没有高级功能。适合小团队。
  • 超级群组:最多 200,000 人,细粒度的管理员权限、话题讨论、消息历史持久化。如果团队规模增长,之后可以从普通群组升级。

对于十几人以内的团队工作流,普通群组已经足够。操作步骤:

  • 在 Telegram 中点击"新建群组",从联系人中选择你的团队成员
  • 给群组起一个描述性的名称——比如"项目 X — AI 助手""工程 Bot 工作区"
  • 群组创建后,打开群组设置,点击"添加成员",搜索你的 bot 用户名(@your_bot_name),将其添加进来
  • 只有当 bot 需要管理员操作(删除消息、置顶消息)时才提升为管理员。对于纯粹的问答用途,普通成员权限就够了

如果你在共享基础设施上管理多个项目专属的 bot(我们自己就维护着好几个),我们使用的多租户模式记录在多租户 Docker 开发栈指南中。

第五步:配置触发检测

默认情况下,Telegram bot 在群组中只接收明确 @提及 它(@your_bot_name)、回复它的消息,或使用斜杠命令的消息。Telegram 称之为"Privacy Mode",默认开启——这是一个合理的默认设置,防止 bot 意外刷屏。

但对于一个应该回答自然问题的 Telegram AI 助手(比如"Hey bot,部署状态怎么样了?"而不需要显式 @提及),Privacy Mode 限制太多。你有两条路:

方案 A:保持 Privacy Mode 开启,训练团队使用 @提及。简单,无需改动配置。bot 只看到它应该回复的内容。缺点:有摩擦。团队成员忘记加 @,bot 就保持沉默。

方案 B:关闭 Privacy Mode,自己实现触发逻辑。打开 BotFather,发送 /setprivacy,选择你的 bot,设置为 Disable。bot 现在能接收每一条群消息。你自己实现"要不要回复"的判断。

我们在生产环境中使用的一套实用触发规则:

  • 私信:始终回复——你在和 bot 一对一对话
  • 群组中的 @提及:始终回复——明确的调用
  • 回复 bot 的某条消息:始终回复——延续 bot 发起的话题
  • 群消息中包含 bot 触发词:回复。触发词通常是 bot 的昵称或项目名称,用词边界正则匹配,避免"advisor"意外触发"advisory"这类情况
  • 其他所有消息:静默记录到会话文件,不回复

静默记录这一点很重要。即使 bot 不回复,它仍然看到了群组的对话流。将每条消息记录到按聊天分类的文件中,给 bot 提供了未来的上下文——当有人最终 @提及 bot 并提问"我们之前决定了什么?"时,bot 可以将近期的对话作为推理上下文来使用。

具体实现取决于你的框架。在 aiogram 中,一个消息 handler 在决定是否调用 LLM 并回复之前,会执行所有五项检查。在 Telegraf 或 grammY 中,模式完全相同——一个 bot.on('message') handler,在作出反应之前明确地进行过滤。

第六步:解决隐私权衡

大多数团队在这个问题变成麻烦之前都不会认真想它:你的 bot 是为每位用户、每个群组单独维护记忆,还是跨所有会话共享一个全局记忆?

常见的三种模式:

  • 按聊天记忆:bot 为每个聊天新建一个独立会话。与用户 A 的私信独立于与用户 B 的私信,两者都独立于群组 X。隐私最大化。缺点:bot 不会跨会话记住上下文,限制了它作为"了解我们项目的助手"的有用性
  • 按用户记忆:bot 为每个用户维护独立的记忆线程,但跨同一用户的私信和群组 @提及 共享。适中的折中方案
  • 全局记忆:bot 只有一个会话,所有对话都向其贡献。上下文共享最大化——私信和群组对话都建立同一份长期记忆。缺点:隐私泄露。某位团队成员在私信中告诉 bot 的机密内容,可能会出现在对另一位成员问题的群组回复中

每种模式都有其合理性,也各有代价——你的团队需要在上线多用户之前达成共识。

如果你选择全局记忆——对于关系紧密、上下文共享本身就是价值所在的团队,我们会这样做——在团队开始使用 bot 之前明确告知他们:"你告诉这个 bot 的任何内容都可能出现在整个群组可见的回答中。把它当作共享工作区,而不是私密的倾诉对象。"

如果你选择按聊天记忆,你放弃了跨上下文的推理能力("上周我们对 X 做了什么决定?"),但完全避免了泄露风险。

这是一个有真实社会影响的设计选择,而不是你之后随时可以改变的技术旋钮——改变它需要全团队重新对齐。我们在关于面向特定领域工作流的 AI agent 技能的深度文章中讨论了类似的权衡,共享上下文配置在我们经手的每个客户项目中都会出现。

将你的 Telegram AI 助手扩展到小团队之外

env-file allow-list 模式对大约二三十人以内的团队运转良好。超过这个规模,硬编码条目就会变得痛苦——每次新增成员都需要一次 git commit(如果你的 env 通过 SOPS 或类似密钥管理层进行了版本控制)、一次部署和一次 container 重建。

可以进一步扩展的模式:

  • 数据库驱动的 allow-list:用户存储在 SQL 表中,bot 在启动时读取并缓存列表,然后定期刷新(或通过用户变更的 webhook 触发)。添加用户只需一条 INSERT 语句,无需部署
  • 基于群组成员身份的访问控制:不再授权单个用户,而是授权某个特定 Telegram 群组(或少数几个群组)的所有成员。群组成员身份成为访问边界。Telegram 的 getChatMember API 在每次调用前确认成员身份
  • 基于频道:对于只读助手(每日摘要、告警、监控汇报),使用 Telegram 频道而非群组。频道有不同的权限模型——只有管理员可以发布,其他人只能阅读。适用于一对多广播而非双向对话的场景

对于小型团队工作流——开发者、顾问、偶尔参与的专家——env-file allow-list 模式已经足够。我们在大多数内部基础设施上都在使用它,并将数据库驱动的方案视为项目明确"长大"之后再做的重构。

上线前检查清单

在团队开始使用 bot 之前,确认以下各项:

  • 所有团队成员的 Telegram 数字 ID 已收集并添加到 allow-list
  • allow-list 格式与你的配置解析器期望的一致——如果 pydantic-settings 在栈中,使用 JSON 数组
  • container 已重建(而不只是重启),使新的环境变量加载到全新的 container 中
  • Telegram 群组已创建,bot 已作为成员添加(如需要管理员操作则提升为管理员)
  • BotFather 的 Privacy Mode 已根据你的触发策略配置——只有在你已经实现了自己的过滤逻辑时才关闭
  • bot 代码中的触发逻辑与 Privacy Mode 设置一致(不要在没有过滤的情况下关闭 Privacy Mode,否则 bot 会回复所有群消息)
  • 记忆模式(按聊天 / 按用户 / 全局)已选定并告知团队
  • 在团队开始使用 bot 之前,已明确设定隐私预期

如果你正在从零开始构建项目专属助手,想了解底层 bot 基础设施如何组合在一起,我们的姊妹文章通过 Telegram 构建项目专属 AI 助手涵盖了基础部分。对于常常伴随这类设置的邮件基础设施——通知、升级路径、审计追踪——请参阅我们关于项目专属邮箱配置(含 DKIM 和 DMARC)的文章。

如果你在为客户或自己的团队运营这类多用户 Telegram AI 助手基础设施,并希望在部署过程中获得帮助,欢迎联系我们。我们将此类设置作为项目工作的一部分进行构建和运营。


相关文章

相关文章