news 2026/9/12 3:52:30

AIRI Telegram 机器人:基于 Velin 模板的群聊消息读取提示词设计与实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AIRI Telegram 机器人:基于 Velin 模板的群聊消息读取提示词设计与实现解析

AIRI Telegram 机器人:基于 Velin 模板的群聊消息读取提示词设计与实现解析

【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi

导读:本文以 AIRI 仓库中 Telegram 机器人集成的提示词模板 action-read-messages.velin.md 为核心,讲解该模板如何在 LLM Agent 的循环中注入群聊上下文(最近消息、未读消息、向量检索到的相关消息),并通过三种结构化 JSON 输出协议驱动机器人做出「忽略 / 参与发言 / 回复指定消息」的决策。读完本文,你将掌握该提示词的完整语义、它与源码执行链路的对应关系,以及如何配置环境让这条链路真正跑起来。

一、模板定位:Agent 循环中的「读消息」动作

AIRI 的 Telegram 机器人(integrations/telegram-bot包)并不是简单的「消息 → 回复」规则机器人,而是一个自主 Agent:它由 LLM 反复「想象下一个动作」,再执行动作、把结果写回上下文,形成循环。

在 类型定义 中,读消息被建模为一个动作:

export interface ReadUnreadMessagesAction { action: 'read_unread_messages' chatId: string }

当 LLM 在imagineAnAction(见 actions.ts)中决定执行read_unread_messages后,主循环 index.ts 的dispatchAction会调用readMessage(...),把读到的内容作为动作结果推入chatCtx.actions,随后handleLoopStep继续循环,让 LLM 基于新读到的内容决定是否发言。

action-read-messages.velin.md正是readMessage在拿到数据后、喂给 LLM 的那一段「现场还原式」指令——它让模型代入「正在用手机 Telegram 刷群聊」的第一人称视角来消化消息。

二、Velin 模板机制:SFC + props 的动态渲染

这个文件的后缀是.velin.md,它不是普通 Markdown,而是一个 Velin 模板(SFC + Markdown 混合)。文件开头是标准 Vue SFC 的<script setup>块,声明了三个可选 props:

<script setup lang="ts"> const props = defineProps<{ lastMessages?: string unreadHistoryMessages?: string relevantChatMessages?: string }>() </script>

这三个 props 通过{{ props.xxx }}插值注入正文。渲染入口在 prompts/index.ts:

export async function actionReadMessages(props: { lastMessages?: string, unreadHistoryMessages?: string, relevantChatMessages?: string }) { return await (velin<{ lastMessages?: string, unreadHistoryMessages?: string, relevantChatMessages?: string }>('action-read-messages.velin.md', import.meta.url))(props) }

而 utils/velin.ts 中的velin()封装了@velin-dev/corerenderMarkdownString,读取模板文件、传入数据并渲染出最终的字符串提示词。也就是说:模板只负责「教模型怎么看待和使用数据」,真正的数据装配发生在readMessage实现里,两者通过 props 契约解耦。

模板在开头还用一句话做了角色设定:

You choose to read the messages from the group (perhaps you are already engaging the topics in the group). Imaging you are using Telegram app on the mobile phone, and you are reading the messages from the group chat.

即「你选择读取群消息,想象你在手机 Telegram App 里刷这个群」,引导模型以实时聊天的阅读节奏来理解下面的三块数据。

三、注入的三块上下文数据及其源码来源

模板正文依次呈现三块数据,每一块都对应readMessage(read-message.ts)中的具体装配逻辑。三者缺一不可,分别回答「刚才在聊什么」「哪些还没看」「哪些历史消息和当前话题相关」。

1. lastMessages:最近 30 条历史消息

模板中对应的段落是:

Previous 30 messages (including what you said): {{ props.lastMessages || 'No messages' }}

源码通过 findLastNMessages 从chat_messages表按created_at倒序取最近 30 条再反转成正序,随后逐条用chatMessageToOneLine压缩成单行文本(含Message ID、发送时间、发送者、是否为回复以及回复对象等字段,见 common.ts),最后以换行符拼接。这一段给模型提供了「最近的对话脉络」,注意其中也包含机器人自己说过的话(Yourself标识),让模型知道自己的立场。

2. unreadHistoryMessages:本次请求要读的全部未读消息

模板对应段落:

All the messages you requested to read: {{ props.unreadHistoryMessages || 'No messages' }}

这部分来自BotContext.unreadMessages这个运行时缓冲区。机器人每收到一条消息都会先入队、经过去重(processedIds)、记录到数据库,再追加到对应 chat 的未读数组(上限 100 条,见 index.ts)。readMessagetelegramMessageToOneLine把每条原始 Telegram 消息(含文本、被回复内容、贴纸描述、图片描述等分支,见 common.ts)转成单行描述。读取完成后state.unreadMessages[action.chatId] = []会清空该群的未读缓冲,避免重复读取。

3. relevantChatMessages:向量检索召回的相关历史消息

模板对应段落:

Relevant chat messages may help you recall the memories: {{ props.relevantChatMessages || 'No relevant messages' }}

这是整个链路中技术含量最高的一环。readMessage先对每条未读消息调用embed()生成向量(配置EMBEDDING_API_BASE_URL/EMBEDDING_API_KEY/EMBEDDING_MODEL,带 5 次重试withRetry),随后交给 findRelevantMessages:

  • 相似度计算:按EMBEDDING_DIMENSION(1536/1024/768)选择对应的向量列,用cosineDistance求相似度(1 - cosine_distance);
  • 时间衰减:引入timeRelevance = 1 - (now - created_at) / 86400 / 30,越近的消息权重越高;
  • 综合打分combined_score = 1.2 * similarity + 0.2 * time_relevance,相似度阈值> 0.5,每轮取前 3 条;
  • 上下文窗口:对每条命中消息,再取它前后各 5 条消息(contextWindowSize = 5)组成完整上下文片段;
  • 回复追溯:自动收集片段中is_replyreply_to_id,回表查出被回复的原始消息一并带上;
  • 去重:通过excludeMessageIds排除已经出现在最近 30 条和未读列表中的消息 ID。

这些召回片段按时间正序拼成多行文本,帮助模型在长期记忆中「回忆」与当前话题相关的旧对话。

注意:三个插值都做了空值兜底('No messages'/'No relevant messages'),保证即使数据源为空,提示词结构依然完整,不会出现空白占位。

四、决策协议:三种结构化 JSON 输出

模板的核心约束是要求模型以结构化的 JSON作为唯一输出形式,并且直接给出决策结果,不事先声明「我想参与」(原文without telling you willing to participate)。完整协议如下:

1. 选择忽略

{ "messages": [] }

模板原文允许模型「直接发送一个带空数组的messages键对象即可忽略」(Feel free to ignore by just sending an empty array within a object with key "messages")。

2. 选择参与发言

{ "messages": ["message content"] }

模型想发言时,直接给出要发送的消息数组。数组支持多条,且必须messages键存在但数组为空才表示忽略——这个「空数组 = 忽略」的约定是后续解析端判断的关键。

3. 选择回复某条消息

{ "messages": ["message content"], "reply_to_message_id": "1234567890" }

在数组基础上附带被回复消息的 ID 字符串,即可让机器人在 Telegram 中以「回复」形式发出第一条消息。

模板还特别叮嘱模型不要额外解释、不要预告,直接返回 JSON——这与 actions.ts 中「Respond with the action and parameters you choose in JSON only」的整体风格一致,是该项目 Agent 协议的一贯设计。

五、输出端的解析与执行:提示词与实现的闭环

模板的协议能否生效,取决于下游解析是否严格遵守。这一步由sendMessage中的 parseMayStructuredMessage 完成:

  • 先用正则^\{(("?)*.*\s*)*\}$判断响应是否整体是 JSON 对象;
  • 若是,用best-effort-json-parser容错解析,并过滤掉空白字符串消息;
  • messages是空数组 → 返回null→ 主流程直接不发送任何消息(对应模板「忽略」分支);
  • messages非空 → 逐条发送,第一条若带reply_to_message_id则用reply_parameters回复到指定消息(send-message.ts);
  • 输出不是合法 JSON 对象 → 降级为「把原文当一条消息发送」。

这些分支全部有单测覆盖,见 send-message.test.ts:包括空数组返回null、多行数组、带reply_to_message_id的对象、缺失messages键时回退原文等 9 个用例。

发送前机器人还会调用sendChatAction('typing')模拟打字、按item.length * 50毫秒模拟输入节奏,并把待发言内容交给另一个 Velin 模板 message-split-v1.velin.md 进行「人类化拆分」——控制长文本不要被机械切碎,保持 1~2 条连贯消息的自然语感。至此,「读消息 → 理解 → 决策 → 自然发言」形成完整闭环。

六、运行前提与环境配置

要让read_unread_messages链路真正工作,需要满足以下前提(均来自 telegram-bot README):

  1. 向量数据库:使用 PostgreSQL + pgvectors 扩展(sql/init.sql 开启vectors.pgvector_compatibility),chat_messages表为 1536/1024/768 三档向量列各建了 HNSW 余弦索引(schema.ts);
  2. Embedding 服务:本地ollama startollama pull nomic-embed-text,并把EMBEDDING_API_BASE_URL指向 Ollama 的 OpenAI 兼容端点,EMBEDDING_DIMENSION必须与实际模型维度一致(README 示例为768),否则recordMessagefindRelevantMessages都会因维度不匹配而抛错;
  3. LLM 服务:配置LLM_API_BASE_URL/LLM_API_KEY/LLM_MODEL/LLM_RESPONSE_LANGUAGE,可选LLM_VISION_*用于图片理解、LLM_OLLAMA_DISABLE_THINK关闭思考链;
  4. 启动方式docker compose up -d启动数据库,pnpm run -F @proj-airi/telegram-bot start启动机器人(见 package.json 的start脚本,加载.env与可选的.env.local)。

另外在 actions.ts 中,模型输出动作名时还有别名归一化处理:read_messagesget_unread_messagescheck_messagesget_messages_from_chat等都会统一映射为read_unread_messages,降低不同模型对动作命名不一致带来的失败率——这从侧面说明该动作是 Agent 高频决策路径之一。

七、总结

action-read-messages.velin.md是 AIRI Telegram 机器人「读消息」动作的提示词核心:它以 Velin SFC + props 的形态接收三块装配好的上下文(最近 30 条、未读消息、向量召回的相关消息),用第一人称「刷群」视角引导模型消化信息,并通过{"messages": []}{"messages": [...]}{"messages": [...], "reply_to_message_id": "..."}三种 JSON 协议把「忽略 / 参与 / 回复」的决策权交给模型。源码侧readMessage负责数据装配、parseMayStructuredMessage负责协议解析、findRelevantMessages负责向量召回,测试用例则锁定了协议边界的正确性。对于想为自托管 AI 角色接入 Telegram 群聊、或学习「提示词模板 + 结构化输出协议 + Agent 循环」架构的开发者,这份模板与其上下游实现构成了一个完整、可复现的参考样例。

【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 3:52:13

Agent执行时代:从LLM到ReAct与Workflow的工程落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 3:49:38

静磁场仿真中的形状优化与灵敏度分析:从概念到工程实践

1. 为什么关注静磁场仿真里的形状优化与灵敏度分析先说清楚这个东西到底是什么。静磁场仿真&#xff0c;解决的是永磁体、电流线圈、铁磁材料这些对象在稳态条件下的磁场分布问题&#xff0c;典型场景包括电机、电磁阀、磁吸盘、磁共振线圈、磁性夹具。形状优化&#xff0c;是在…

作者头像 李华
网站建设 2026/9/12 3:48:00

单卡微调7B大模型:LoRA显存优化与MindSpore实战

1. 为什么LoRA能让单卡微调大模型成为可能&#xff1a;显存账本与原理拆解先说个很多人的直觉误区&#xff1a;大模型微调动辄需要多卡集群&#xff0c;单卡只能做做推理。这个结论在“全参微调”时代基本成立&#xff0c;但LoRA出现后&#xff0c;单卡跑大模型微调的可行性已经…

作者头像 李华
网站建设 2026/9/12 3:47:05

AUTOSAR ComM状态机详解:Full Communication切换失败根因与排查方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华