简介:一份用于快速生成AI对话网站的PHP源码包,主要面向具备基础PHP知识的站长、博主或开发者,解决临时需要搭建轻量AI对话页并嵌入博客引流的问题。系统采用新拟态设计风格,界面简洁现代;支持自定义网站名称、AI默认开场白、AI头像昵称、引流网站地址等,生成后的网页全部保存在用户自己的服务器上,便于随时修改和二次开发。
压缩包体积仅7KB,共包含6个文件,以PHP入口文件、CSS样式文件、TXT搭建说明和URL快捷方式为主,整体结构非常紧凑,适合作为PHP与AI接口调用、前后端简单交互的学习样例。搭建时只需查找index.php中第112行的域名或网站目录配置,替换为自己的地址即可,部署流程很短。
该资源已有176人学习下载。对于希望低成本给博客或工具箱增加AI对话入口、同时想研究轻量级PHP实现原理的开发者来说,这套源码提供了可直接运行的代码框架和清晰的修改思路,也方便在此基础上扩展更多个性化功能。
1. AI对话网站一键生成的价值锚点:做生成器而不是聊天页
这个标题容易让人误以为核心是“AI 对话页面”,真正难做的其实是“生成”这两个字。你面对的是一类批量建站需求:给客户、给不同业务线、给只会填问卷的运营人员,快速产出一个带标题、欢迎语、模型配置、访问权限的独立对话站点。如果说聊天页面是 500 行代码的事,那“一键生成”需要的是把它变成模板、配置和脚本的组合体。本文要讲清楚的就是这套生成器怎么做:从配置驱动的前端拼装,到后端模型网关与限流,再到部署验证和参数回写。适合正在做 AI 应用开发、Saas 化产品封装或内部工具链的工程师,看完能直接落地一套可复现方案。
2. 把“AI对话网站”拆成本地可复现的产物结构
2.1 为什么是“配置驱动”:一键生成的第一性原理
一键生成的本质不是写代码,而是把“可变的东西”和“不变的东西”分离。一个 AI 对话网站里,不变的骨架包括:聊天输入框、消息列表、流式接收逻辑、Markdown 渲染、移动端适配。会变的是名字、logo、欢迎语、模型型号、头像、主题色、是否需要登录。
如果每次都用复制粘贴改代码的方式交付,改错一处 JS 变量就得排查半天;如果走配置驱动,把可变项全部收敛到一个 JSON 文件里,前端模板只认配置不认业务,就能用脚本批量产出几十个互不干扰的站点。
我在实际项目里一般把配置结构设计成这样:
{ "site": { "name": "示例AI助手", "welcome": "你好,我是基于大模型的文档助手", "theme": "indigo", "logo": "/logo.png" }, "chat": { "model": "qwen-plus", "temperature": 0.7, "maxTokens": 1024, "stream": true }, "access": { "requireLogin": false, "allowedDomains": ["example.com"] } }参数说明:chat.model决定对话走哪个模型服务;temperature是采样温度,数值越高回复越发散,做客服场景建议 0.3 到 0.5,做创意写作再拉高;access.requireLogin这个开关值得单独说——很多“AI 无禁词聊天网页版不用登录”的诉求本质上就是把这个值设为 false,配合网关层的访问密钥做保护。要不要让用户免登录直达,属于产品决策,部署方需要自行承担合规责任。
这套设计的好处是:生成器本身根本不关心对话网站长什么样,它只负责把 JSON 和模板合成最终可运行代码。后面要改任何站点属性,只动配置,不动逻辑。
2.2 用一条命令从模板生成站点:最小脚本与参数表
配置驱动还需要一个执行入口,也就是“一键”按钮背后的东西。在本地开发环境,我用 Node.js 写一个不到 40 行的生成脚本,它做的事情很简单:读取配置文件,递归扫描模板目录,把文件列表渲染后输出到目标目录。
// generate.js —— 最小可运行的站点生成器 const fs = require('fs'); const path = require('path'); const config = JSON.parse(fs.readFileSync('site.json', 'utf8')); const srcDir = path.resolve(__dirname, 'templates'); const outDir = path.resolve(__dirname, 'dist', config.site.name); function renderFile(file, relPath) { let content = fs.readFileSync(file, 'utf8'); // 用简单的变量插值替换模板占位符 content = content.replace(/\{\{(\w+)\}\}/g, (_, key) => { const val = key.split('.').reduce((obj, k) => obj?.[k], config); return val ?? ''; }); const outFile = path.join(outDir, relPath); fs.mkdirSync(path.dirname(outFile), { recursive: true }); fs.writeFileSync(outFile, content); } function walk(dir) { // 用 fs.readdirSync 列出模板目录下所有文件,构建待渲染清单 fs.readdirSync(dir, { withFileTypes: true }).forEach(entry => { const full = path.join(dir, entry.name); if (entry.isDirectory()) return walk(full); const rel = path.relative(srcDir, full); renderFile(full, rel); console.log(`generated: ${rel}`); }); } walk(srcDir); console.log(`站点已生成到 ${outDir}`);逻辑说明:脚本先用readdirSync配合withFileTypes拿到模板目录的文件清单,遇到子目录递归进入,这就是“一键获取文件名并生成列表”的典型实现。每个模板文件里的{{site.name}}这类占位符,会被reduce逐级解析成对应配置值,最后原样保持目录结构输出。
两个值得注意的参数设计:withFileTypes: true让我们不需要额外调用statSync判断文件类型,IO 少一半;输出目录用site.name做隔离,这样跑一次脚本就能生成多个互不覆盖的站点目录。渲染引擎我用的是正则替换,如果模板里需要循环或条件判断,直接换 Nunjucks 或 EJS,接口不用动。
2.3 对话页面最小实现:SSE 流式接收与 Markdown 渲染
生成器产出的前端页面,核心交互就是“用户发一条消息,AI 流式吐字”。这一块要给出稳定的最小实现,否则生成的站点换个浏览器就出问题。
SSE 接收用原生的fetch就能完成,不引额外依赖:
// chat.js —— 流式对话核心逻辑 async function sendMessage(text) { const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text, sessionId: getSessionId() }) }); const reader = res.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // 按换行切分 SSE 数据块,处理可能被拆包的半行 let lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data:')) continue; const payload = JSON.parse(line.slice(5)); appendToMessage(payload.content); } } }这版实现里藏着两个最容易踩的坑。第一个是decoder.decode(value, { stream: true }),多字节 UTF-8 字符在流式传输中可能被 TCP 拆到两个 chunk,不处理就会在拼接处出现乱码,这也是流式输出里“中文断字”的直接原因。第二个是按\n切行后要把最后的半行pop回 buffer,因为 SSE 是按事件边界切分的,读取位置未必刚好停在事件末尾。sessionId用来在后端做多轮对话的上下文关联,前端只传标识,不存历史。
3. 生成器的后端底座:模型网关、限流与配置下发
3.1 模型网关:把各家 Chat 接口收敛成一个路由
对话页面的前端只请求自己站点下的/api/chat,那谁来保证这个接口在不同模型服务商之间可以切换?答案是一层薄的模型网关。它的作用是:接收前端消息,补充系统提示词,调用统一格式的模型 API,把流式响应原样转发给前端。
用一个 Express 中间件就能起一个可用的网关:
// gateway.js —— 统一模型网关 const SYSTEM_PROMPT = process.env.SYSTEM_PROMPT || '你是一个乐于助人的AI助手。'; app.post('/api/chat', async (req, res) => { const { message, sessionId } = req.body; const model = getModelForSite(req.headers['x-site-id']); res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); try { const stream = await callModel({ model, messages: [ { role: 'system', content: SYSTEM_PROMPT }, ...(await loadHistory(sessionId)), { role: 'user', content: message } ], stream: true }); for await (const chunk of stream) { const content = chunk.choices?.[0]?.delta?.content || ''; if (content) res.write(`data: ${JSON.stringify({ content })}\n\n`); } res.write('data: [DONE]\n\n'); res.end(); } catch (err) { res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`); res.end(); } });参数说明:x-site-id请求头是关键,它是多站点隔离的钥匙,后端凭它决定用哪个模型、哪组密钥、哪份系统提示词。SYSTEM_PROMPT用环境变量注入,这样“内容策略”就不会写死在代码里。loadHistory控制多轮上下文长度,一般只回放最近 10 条,会话太长时先做截断,否则超出模型上下文窗口会直接报错。
如果你打算用 Java 重写这一层,Spring AI 的ChatClient抽象能对上这套设计,接口语义几乎一一对应,可以作为网关的另一套实现选项。
3.2 限流与密钥隔离:多站点共用后端的底线
一键生成系统的商业模式天然是多租户:一个后端服务 N 个 AI 对话站,每个站的流量特征和付费能力完全不同。不做限流的后果很直接——某个站点的用户刷爆你的模型额度,其它站点全部跟着失败。
我给每个站点分配独立apiKey,限流用 Redis 的滑动窗口实现:
// rateLimit.js —— 基于 Redis 的滑窗限流 const redis = require('redis'); const client = redis.createClient({ url: process.env.REDIS_URL }); async function checkRateLimit(siteId, userId, limit, windowSec) { const key = `rl:${siteId}:${userId || 'anon'}`; const now = Date.now(); const pipeline = client.multi(); pipeline.zRemRangeByScore(key, 0, now - windowSec * 1000); pipeline.zAdd(key, { score: now, value: `${now}-${Math.random()}` }); pipeline.zCard(key); pipeline.expire(key, windowSec); const results = await pipeline.exec(); return { allowed: results[2] <= limit, current: results[2] }; }逻辑说明:每个请求把自己的时间戳写进有序集合,同时清掉窗口外的旧记录,集合大小就是窗口内请求数。pipeline把四个操作合并成一次往返,Redis 开销非常低。siteId + userId组合能精确区分“这个站限流”和“这个人限流”。
建议的默认参数可以这样设:
| 维度 | 限流值 | 窗口 | 适用场景 |
|---|---|---|---|
| 单用户请求数 | 20 次 | 1 分钟 | 普通聊天站,防止脚本刷接口 |
| 单站点并发数 | 5 路 | 同时 | 小流量站点,模型 API 并发有限 |
| 单站每日总量 | 500 次 | 24 小时 | 免费体验站,控制成本 |
| 充值用户 | 200 次 | 1 分钟 | 按套餐调整,走单独配置 |
限流触发时不要只返回 429 状态码,要把剩余配额用响应头透出,前端拿到后主动禁用发送按钮,比报错提示友好得多。
3.3 配置下发与站点隔离:一个后端服务 N 个 AI 对话站
生成器负责生产站点文件,后端网关负责服务对话接口,两者之间需要一个配置下发的通道。最简单可靠的做法是:生成器把每个站点的最小配置写进数据库或 Redis,网关用x-site-id实时读取。
-- 站点配置表结构 CREATE TABLE ai_site_config ( site_id VARCHAR(64) PRIMARY KEY, site_name VARCHAR(255) NOT NULL, model VARCHAR(128) NOT NULL, api_key_enc TEXT NOT NULL, system_prompt TEXT, require_login TINYINT(1) DEFAULT 0, rate_limit INT DEFAULT 20, daily_limit INT DEFAULT 500, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );解释一下关键字段:api_key_enc存放的是加密后的模型服务商密钥,不同站点可以使用不同服务商,网关层不需要统一密钥;system_prompt让每个站点的 AI 扮演不同角色,这是“无违禁词 AI 聊天”类场景的工程化答案——不在代码层硬编码任何内容限制,而是把内容策略做成可配置的提示词,由部署方根据合规要求自行定义。require_login与前端模板里的登录页联动,开启后未登录请求直接重定向。
站点隔离的第二个层面是文件隔离。生成器输出的静态文件放在独立子目录,Web 服务器按域名解析到对应目录;后端接口则靠site_id绑定配置。这样前端是静的、后端是配置的,新增一个站点不需要重新构建服务。
4. 一键生成系统源码的部署与运行验证
4.1 docker-compose 一键拉起整套系统
部署环节最容易出问题的是环境差异。前端站点是静态文件,随便扔到 Nginx 就能跑;后端网关依赖 Node 运行时、Redis、可能的数据库。用 docker-compose 把整个系统定义成一份清单,是唯一让我觉得“靠谱”的方案。
version: "3.9" services: gateway: build: ./gateway ports: - "8080:8080" environment: REDIS_URL: redis://redis:6379 SYSTEM_PROMPT: "你是一个可靠的AI助手。" depends_on: - redis restart: unless-stopped redis: image: redis:7-alpine volumes: - redis-data:/data command: redis-server --appendonly yes web: image: nginx:alpine ports: - "80:80" volumes: - ./dist:/usr/share/nginx/html:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro depends_on: - gateway restart: unless-stopped volumes: redis-data:部署后要做的事只有两步:第一步把生成的站点目录复制到服务器上的dist文件夹,第二步在服务器上执行docker compose up -d。restart: unless-stopped保证服务器重启后服务自动拉起。这里有个容易忽略的小坑:./dist里的站点文件是在生成机上渲染出来的,如果生成机是 Windows,文件换行符是 CRLF,轻则日志乱码,重则 shell 脚本执行报错。我一般会在生成器里加一步自动把输出文件统一转成 LF。
4.2 部署后必做的四个验证动作
容器起来了不代表系统真的可用。网络层、流式传输、限流策略、跨域配置,每一层都可能让前端白屏或接口静默失败。我部署完成后的检查顺序是固定的:
# 1. 验证静态站点是否正常响应 curl -I http://your-domain/ # 2. 验证模型网关是否连通(非流式探活) curl -X POST http://your-domain/api/chat \ -H "Content-Type: application/json" \ -H "x-site-id: demo-site" \ -d '{"message":"你好","sessionId":"probe"}' \ --max-time 10 # 3. 验证 SSE 流式输出是否分块到达 curl -N -X POST http://your-domain/api/chat \ -H "Content-Type: application/json" \ -H "x-site-id: demo-site" \ -d '{"message":"从1数到5","sessionId":"probe2"}' | head -20 # 4. 验证限流是否生效 for i in $(seq 1 25); do curl -s -o /dev/null -w "%{http_code}\n" \ -X POST http://your-domain/api/chat \ -H "Content-Type: application/json" \ -H "x-site-id: demo-site" \ -d '{"message":"ping","sessionId":"ratelimit"}' done | sort | uniq -c第 3 个命令是排查“流式失效”最直接的手段。如果head -20等到很久才一次性输出所有内容,说明响应在网关层被缓冲了,此时要去关掉负载均衡层的响应缓冲,保留 chunked 传输。第 4 个命令会看到前面 20 次返回 200,后面的返回 429,数量与配置的限流值严格一致才算通过。
4.3 本地联调与线上排错:五个容易翻车的参数位置
上线后真正折磨人的不是逻辑错误,而是几个位置的隐性参数。这里列出我踩过最多次的五个,可以直接作为排查清单:
| 位置 | 症状 | 后果 | 修正方式 |
|---|---|---|---|
| 负载均衡层响应缓冲 | 前端长时间无输出,最后一次性出现全文 | 流式体验完全丢失 | 关闭响应缓冲,开启chunked_transfer_encoding |
| gzip 压缩与 SSE 冲突 | 流式内容被压缩成整块 | 前端解析失败或卡顿 | 对 text/event-stream 关闭 gzip |
| 网关读超时 | 回复稍长就中断 | 对话后半段内容丢失 | 监听层读取超时调到 5 分钟以上 |
| 上下文窗口溢出 | 多轮对话后接口报 400 | 用户无法继续聊天 | 接入模型前按 token 截断历史 |
| 浏览器同域并发限制 | 同一页面多个请求排队 | 前端首屏渲染变慢 | 静态资源和 API 分离部署到不同域名 |
这些问题的共性在于它们都不会在本地 curl 测试时暴露,只有真实浏览器环境和长会话下才会显现。所以部署验证阶段一定不要只测“能通”,要测“流式通畅”和“长对话稳定”。
5. 把“一键生成”做厚:参数回写、扩展点与压测
5.1 让一键生成具备回写能力
初版生成器往往是单向的:改配置,重新生成,重新部署。这会导致运营人员每次调品牌色都要找研发。我建议在生成脚本里加一个--watch模式,监听配置文件的变更,自动重新渲染并热更新到静态目录。实现思路是用fs.watch监听 JSON 文件,变更后直接调用 render 函数,把“一键生成”变成“保存即生效”。后端配置同理,在网关层给配置接口加个版本号,前端页面轮询到新版本后提示刷新。
5.2 三个值得内置的扩展点
第一个是“多模型路由”,同一个站点可以按问题类型分流到不同模型,代码题走代码能力强的模型,通用对话走成本更低的模型。第二个是内置 Agent 插件机制,把“AI Agent”能力做成可选项——插件接收已渲染的对话历史,执行工具调用后再把结果拼回上下文。第三个是“内容策略开关”,把系统提示词拆成基础人设和策略注入两层,策略层做成可视化配置,这是面向运营需求最划算的投入。
5.3 用数字验收你的生成系统
最后给出量化的验收标准。用 ab 或 wrk 打压测,前端网关的并发目标设为单站点 20 QPS 不失败,P95 响应时间低于 2 秒。流式场景下,首 token 到达时间(TTFT)应小于 1.5 秒,这是用户感知“快不快”的核心指标。生成器的构建速度也有标准:100 个模板文件以内的站点,从执行命令到输出完整目录,耗时不应超过 5 秒。如果超过这个值,优先检查是否在循环里重复执行了耗时的 IO 操作而非渲染本身。
# 网关压测参考命令 wrk -t4 -c20 -d30s \ -H "x-site-id: demo-site" \ -s benchmark.lua \ http://your-domain/api/chat-- benchmark.lua —— wrk 压测脚本 wrk.method = "POST" wrk.headers["Content-Type"] = "application/json" wrk.headers["x-site-id"] = "demo-site" wrk.body = '{"message":"你好","sessionId":"bench"}'压测结果出来后,把限流参数、模型 RT、网关内存占用三组数字记录到项目 README 里,后续每次改动配置都拿它做回归基准。这套“配置生成站点、网关统一收敛、脚本验证验收”的链路跑通后,整个系统才算真正做到了题目里说的“一键生成”。
本文还有配套的精品资源,点击获取