让 Telegram 机器人安全执行数据库操作
文章将 LLM 的职责限定为意图识别与参数提取,通过函数调用把请求交给 Node.js 服务验证并执行。该设计针对输出格式漂移、虚构数据库标识符和提示词注入等问题,强调凭据与执行权限由服务端掌控。
AI 深度解读
让 Telegram 机器人回答一句话很容易,让它可靠地查订单、创建预订,就得把模型接到业务系统上。原文给出的方案是:用 OpenAI function calling 识别意图、提取参数,由 Node.js 校验并执行,最后让模型组织回复。
这套分工值得借鉴,但示例还不能直接当成生产模板。工具 Schema 不等于业务校验,能查到订单不等于有权查看,而文中的预订成功也没有真正落库。理解这些边界,比照搬调用代码更有用。
模型提出操作请求,Node.js 决定能不能执行
原文先展示了一种常见写法:把用户消息交给 gpt-4o-mini,要求它只返回 JSON,再用 JSON.parse 读取 action 和 order_id,随后查询数据库。
问题出在这条链路把自然语言回复当成了接口协议。模型多写一句介绍、加上 Markdown 代码围栏,或者改错字段名,解析就可能失败。自定义 /book_123 之类的命令字符串,也需要自行维护格式约定。
更危险的是,反例把 process.env.TELEGRAM_BOT_TOKEN 插进了 system prompt。密钥进入模型上下文后,攻击者就有机会通过提示注入(Prompt Injection)诱导泄露。原文提醒的方向正确,但它没有提供攻击实验,不能把文中的描述当成成功率结论。
作者建议把链路拆开:模型返回工具名称和参数,Node.js 根据固定映射调用本地处理函数。模型能请求 get_order,实际执行哪个函数、查哪些数据,都由服务端控制。
工具调用应该被当作一份待审核的操作请求。
这也解释了为什么接入 function calling 后仍然会出现 JSON.parse:工具参数依旧需要解析。改变的是解析对象从整段聊天回复变成了专门的参数字段,业务执行入口也变得明确。
Schema 管结构,业务规则还得自己检查
原文定义了两个工具。
get_order 接收整数 order_id;create_booking 接收字符串 customer_name、日期字符串 date 和整数 guests。日期说明要求采用 YYYY-MM-DD,所有字段都列入 required,两个对象都设置了 additionalProperties: false,用于限制额外属性。
这些约束能把接口说清楚,但示例有两处不能照单全收。
首先,作者把它们称为严格 Schema,实际定义里没有出现 strict: true。不能仅凭字段类型和 additionalProperties: false,就认定所有返回参数都已获得严格保证。
其次,字段描述不等于可执行校验。date 的格式要求写在描述里;guests 的描述说至少为 1,却没有在 Schema 中设置数值下界。它们还需要服务端兜底。
处理函数确实做了部分检查:订单号先经过 Number(args.order_id) 转换,再判断是否为正整数;预订人数必须是 1 到 20 的整数。成功与失败统一返回带有 success: true|false 的对象,让模型根据实际结果回复。
不过,Number(...) 会接受部分可转换输入,例如数字字符串,因此它采取的是「转换后校验」,不是严格拒绝非数字类型。名字检查也只覆盖了非空和字符串类型,空白字符串仍可能通过。日期则没有得到进一步验证。
更关键的是,数据库只是 Map,包含订单 1042 和 1043。executeCreateBooking 用 Math.random() 生成 BK- 加四位数字的编号,然后返回 Confirmed,没有保存预订。
读者应当把它理解为业务处理函数的接口示意。它展示了返回结果的形状,没有证明预订已经成立。
执行循环怎样让回复对应真实结果
原文最有价值的部分,是模型与工具之间的往返流程:先提交消息和工具定义,收到工具调用后执行本地函数,把执行结果放回上下文,再请求模型继续回答。
下面这段调用让模型自行选择是否使用工具,保留原样:
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages,
tools: botTools,
tool_choice: 'auto'
});
要注意 tool_choice: 'auto':它允许模型选择工具,也允许直接回复,业务端仍要处理没有工具调用的情况。
执行完成后,这段代码把结果交回模型:
// 将工具执行结果追加到消息上下文
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(resultPayload)
});
关键是 tool_call_id,它把当前执行结果关联到对应请求。查不到订单时,模型拿到的是失败对象,才有依据告诉用户记录不存在。
原文用 toolDispatcher 将工具名映射到处理函数,未知名称返回错误。参数解析失败时则退回 {},让后续校验处理。这个做法能让流程继续,但也混合了「参数根本解析失败」和「缺少必填字段」两类错误,排查时不够清楚。
循环还有 MAX_LOOPS = 5,用于限制连续请求,避免模型反复调用工具造成费用增长。不过,代码达到上限后没有给用户发送收尾消息,而且每轮可以包含多个工具调用,5 轮并不等于最多执行 5 次业务操作。
原文文字描述通过 finish_reason === 'tool_calls' 和 finish_reason === 'stop' 判断阶段,实际代码检查的是 responseMessage.tool_calls 是否存在。阅读时要分清流程说明和具体实现。
另一个容易漏掉的问题是会话保存:messages 在每条 Telegram 消息的回调里重新创建,只包含 system prompt 和当前输入。它保留了这一轮工具执行的上下文,却没有跨消息记忆。用户先说要预订,下一条再补人数,这份代码不能自动接起来。
我的判断
这篇文章最值得采纳的,是把意图识别和业务执行分开。相比正则提取命令、解析整段模型回复,function calling 提供了更明确的工具名称、参数入口和结果回传方式,Node.js 也更容易集中处理错误。
但作者没有提供成功率、延迟、成本或对照测试。这是一篇架构经验和示例代码说明,不能据此判断它比其他实现稳定多少。
参数合法,不代表操作获得授权。
示例只按订单号查询,没有说明如何将 Telegram 用户身份映射到订单所有者。即使 order_id 类型正确,也不能据此开放订单内容。创建预订同样缺少持久化、容量检查和重复提交处理;连续调用可能产生多次写操作,循环次数限制无法解决这个问题。
输出端也有一个未完成的提醒。代码使用 parse_mode: 'HTML' 发送模型回复,原文指出未转义的 <、> 可能造成 Telegram Bot API 返回 400 Bad Request,但示例没有实现转义。
密钥隔离值得遵守:TELEGRAM_BOT_TOKEN 和 OPENAI_API_KEY 应留在服务端,不应进入提示词、工具描述或工具结果。不过,密钥留在服务端之后,提示注入仍可能诱导错误操作,权限判断不能交给模型。
能拿来做什么
- 做 Node.js 客服机器人时,可以沿用「工具定义、固定分发、业务处理、结果回传」的分工,把订单权限、参数校验和数据库访问放进服务端处理函数。
- 从查订单开始接入更容易验证完整链路。至少检查非法编号、订单不存在、用户无权访问和工具执行失败时,最终回复是否准确。
- 接入
create_booking前,应补齐日期校验、真实存储、容量判断与重复提交处理,并在执行轮次耗尽时明确告知用户处理未完成。 - 如果机器人需要分多条消息收集预订信息,就按会话保存上下文;发送回复时,先采用纯文本,或在启用 HTML 后完成必要的转义处理。