博客右下角那个可以聊天的悬浮框,背后其实是一套”前端悬浮组件 + 后端反向代理 + 大模型API”的完整链路。这一篇讲清楚它的实现原理和设计考量。

一、为什么不能直接在前端调用大模型 API

最简单粗暴的想法是:直接在浏览器里的 JS 代码里写上 API Key,调用大模型接口。这是绝对不能做的,原因很简单——网页的源代码对任何人都是公开可见的,只要打开浏览器”查看源代码”,就能直接看到你的密钥,别人拿着你的 Key 随便调用,账单算在你头上。

所以必须有一个”中间层”:前端只负责把用户的问题发给这个中间层,中间层拿着密钥去调用大模型,再把结果转发回前端。这个中间层可以用 Serverless 云函数来实现,不需要自己买服务器。

二、整体架构图

1
2
3
4
5
6
7
8
9
访客浏览器(悬浮聊天框)
↓ 发送消息(无密钥)
云函数(藏着API Key + 人设Prompt)
↓ 携带密钥调用
智谱AI大模型接口
↓ 返回AI回复
云函数转发给浏览器

悬浮聊天框展示回复

三、前端:悬浮聊天框

前端部分是一个纯原生 JavaScript 组件(没有用任何框架,减少依赖),核心逻辑:

  • document.createElement 动态生成聊天框 DOM,插入到 <body>
  • sessionStorage 保存对话历史,刷新页面不丢失上下文
  • 点击发送后用 fetch() 把消息 POST 给后端云函数地址
  • 收到回复后渲染到聊天气泡里

关键代码片段:

1
2
3
4
5
6
7
8
9
fetch(API_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: history })
})
.then(res => res.json())
.then(data => {
appendMessage('assistant', data.reply)
})

四、后端:给AI一个”人设”

这是整个方案最关键的部分——大模型本身不知道”你是谁”,需要通过 System Prompt(系统提示词) 给它注入一个完整的人设,包括:

  • 姓名、教育背景、工作经历
  • 求职意向和联系方式
  • 说话的语气风格
  • 遇到不确定信息时应该怎么回应(避免编造虚假信息)

一份精心设计的人设 Prompt,大概是这样的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
你是"XXX"的数字分身助手,代表她本人与访客对话。

## 身份设定
- 姓名、学校、专业...
- 教育与证书...
- 工作经历(按时间列出,含具体成果)...

## 你的任务
1. 用第一人称回答,语气自然真诚
2. 主动介绍教育背景、经历、求职意向
3. 如果是HR,主动告知联系方式
4. 遇到不确定信息,如实说明需要联系本人,不要编造
5. 保持回答简洁

现在,请自然地和访客聊天吧。

这个 Prompt 会在每次对话请求时,作为 system 角色的消息,和用户的问题一起发给大模型。

五、为什么选智谱 AI 而不是其他大模型

调研了国内几家大模型 API 后,选择了 智谱 AI 的 GLM-4-Flash 模型,核心原因:它是目前国内唯一一个长期免费、不限量的商用级大模型 API。对于博客聊天机器人这种轻量场景,完全不用担心账单问题,可以放心长期使用。

六、云函数怎么选

一开始尝试用 Cloudflare Workers 做这个中间层,但国内访问 *.workers.dev 域名经常出现连接不稳定的问题(下一篇会详细讲这个踩坑过程)。最终换成了腾讯云函数(SCF)+ 函数URL方案,同一账号体系,国内节点访问稳定。

云函数的核心逻辑:

1
2
3
4
5
6
7
8
9
exports.main_handler = async (event) => {
const { messages } = JSON.parse(event.body)
const upstream = await callZhipuAI(messages) // 携带密钥调用智谱AI
return {
statusCode: 200,
headers: corsHeaders, // 处理跨域
body: JSON.stringify({ reply: upstream.reply })
}
}

密钥通过云函数的环境变量注入(而不是硬编码在代码里),这样即使代码开源在 GitHub 上,密钥依然不会泄露。

下一篇会详细记录从 Cloudflare Workers 切换到腾讯云函数的完整踩坑过程,包括 API 网关下线、函数URL的坑等实操细节。