ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于Cloudflare Worker与大模型API构建智能故事创作聊天机器人

基于Cloudflare Worker与大模型API构建智能故事创作聊天机器人 1. 项目缘起一个“技术宅”的温柔承诺事情是这样的我媳妇儿最近迷上了用 Fable 5 来写一些她自己的小故事。Fable 5 是个挺有意思的 AI 叙事工具能根据你的想法生成连贯的、有画面感的文字甚至能帮你构建世界观。但她不是技术背景出身每次打开 Fable 5 的界面面对那些参数滑块、模型选择、提示词输入框总有点发怵。她跟我说“我就想简单点像聊天一样告诉它我想要个什么故事它就能给我写出来行不行”这句话点醒了我。对啊为什么一定要让她去适应工具的复杂界面呢工具的本质是服务于人而不是让人去学习工具。既然现在大语言模型LLM的 API 如此强大我完全可以用一个更自然的方式——聊天机器人Chatbot——来封装 Fable 5 的核心功能。让她通过最熟悉的对话方式无痛地享受创作的乐趣。这不仅仅是一个技术项目更是一个关于“如何让技术温柔地融入生活”的实践。我的核心思路很清晰构建一个 Web 聊天界面用户我媳妇儿在里面用自然语言描述她的故事想法比如“写一个关于未来都市中一个失去记忆的仿生人寻找自我身份的故事带点赛博朋克和悬疑色彩”。然后后端服务会理解她的意图将其转化为 Fable 5 能够高效执行的、结构化的提示词Prompt调用 Fable 5 的 API 生成故事最后把生成好的、带格式的文本流式地返回给聊天界面。整个过程她只需要“说话”剩下的交给机器人。技术选型上我立刻想到了几个关键词Cloudflare Worker、Anthropic API和DeepSeek。Cloudflare Worker 作为无服务器边缘计算平台部署简单、全球访问速度快且免费额度对于个人项目绰绰有余是承载这个 Chatbot 后端的绝佳选择。而大模型方面虽然项目标题和热搜词里提到了Fable 5但经过我的调研和对比Anthropic 的 Claude 系列和DeepSeek模型在长文本生成、逻辑连贯性和对中文指令的理解上可能比某些专用叙事模型的基础能力更通用、更强大且成本可控。尤其是 DeepSeek近期因其极高的性价比和出色的代码与文本能力备受关注。我的计划是用 Claude 或 DeepSeek 作为“大脑”来理解用户的自然语言并转化为优质提示词或者直接进行故事生成这比直接硬编码提示词模板要灵活和智能得多。2. 技术架构选型为什么是 Cloudflare Worker 大模型 API确定了“聊天机器人”这个形式后接下来就要搭建它的“身体”和“大脑”。身体需要轻量、快速、易部署大脑则需要聪明、听话、成本低。经过一番对比我选择了Cloudflare Workers作为后端运行时并采用Anthropic Claude API或DeepSeek API作为核心的 LLM 服务。这里我详细解释一下为什么这么选以及它们如何协同工作。2.1 后端基石为什么是 Cloudflare Worker对于这样一个个人级、希望全球快速访问且不想维护服务器的项目Cloudflare Worker 几乎是首选。它是一个无服务器Serverless函数计算平台有以下几个无法抗拒的优点极致的简单与快速你只需要写一个 JavaScript/TypeScript 脚本通过wranglerCLI 工具或在线编辑器一键部署。没有需要配置的服务器没有需要管理的运行时环境。部署后你的代码就在 Cloudflare 全球边缘网络上运行这意味着无论用户在哪里请求都能被最近的节点处理延迟极低。对我媳妇儿来说打开网页就能用体验流畅。慷慨的免费额度 Workers 的免费计划每天提供 10 万次请求这对于一个家庭内部使用的 Chatbot 来说根本用不完。这完全消除了项目的运营成本顾虑。完美的 Web 场景支持Worker 原生支持处理 HTTP/HTTPS 请求非常适合构建 RESTful API 或像本项目这样的 WebSocket 服务用于流式输出。它还能轻松处理 CORS 头方便前端页面调用。内置的 KV 存储虽然本项目暂时不需要复杂的状态持久化聊天历史可以考虑用前端本地存储但 Worker 配套的 KV键值存储为未来可能的扩展比如多用户、故事存档提供了简单易用的解决方案。相比之下自己租用 VPS 需要配置环境、考虑安全、承担 24/7 的运行成本对于这个轻量级项目来说属于“杀鸡用牛刀”。而其他 Serverless 平台如 Vercel Serverless Functions 或 AWS Lambda 虽然也不错但 Cloudflare Worker 在边缘计算性能和免费策略上更贴合这个项目的需求。2.2 智能核心Anthropic Claude vs. DeepSeek 的权衡这是项目的灵魂所在。我需要一个能够理解复杂、模糊的人类自然语言指令并转化为高质量叙事文本的“大脑”。Fable 5 本身可能是一个集成了特定模型的平台但为了更灵活、更低成本和更强的可控性我决定直接调用大模型的 API。Anthropic Claude APIClaude 系列模型特别是 Claude 3 Opus/Sonnet在长文本理解、创造性写作和遵循复杂指令方面口碑极佳。它的 API 稳定文档清晰而且提供了非常优秀的系统提示词System Prompt功能可以很好地设定 AI 的角色和行为准则。例如我可以将它设定为“一位擅长科幻、奇幻题材的资深故事创作助手”。缺点是它的 API 调用成本相对较高尤其是对于长故事生成。DeepSeek API这是近期 AI 领域的一匹“黑马”。DeepSeek 模型以其极高的性价比价格远低于 OpenAI GPT-4 和 Claude 3和强大的综合能力特别是代码和中文迅速走红。对于个人项目来说成本是至关重要的考量因素。如果 DeepSeek 在创造性文本生成上的效果能达到可接受的水平那么它无疑是更具吸引力的选择。此外DeepSeek 也支持了联网搜索需要单独申请和长上下文这对于需要汲取最新信息或编写长篇故事很有帮助。我的实际策略是“先用 DeepSeek 跑通必要时切换或降级到 Claude”。在 Worker 的后端代码中我将模型 API 的 endpoint 和 API Key 设计为可配置的环境变量。这样我可以在不修改代码的情况下通过修改环境变量轻松地在 DeepSeek、Claude 甚至未来其他模型之间切换进行效果和成本的 A/B 测试。例如我可以为复杂的、需要极高文学性的请求配置使用 Claude而为简单的、日常的故事脑暴使用 DeepSeek。注意API 密钥的安全是重中之重。绝对不要将 API Key 硬编码在客户端前端代码中。所有对模型 API 的调用都必须通过你的 Cloudflare Worker 后端进行中转。Worker 中通过env.API_KEY的方式读取环境变量确保密钥不会暴露给浏览器。3. 从零搭建Cloudflare Worker 聊天后端实战理论说完了我们开始动手。这部分我会详细拆解如何从零创建一个 Cloudflare Worker 项目并实现一个能够处理聊天请求、调用大模型 API 并流式返回响应的后端服务。3.1 环境准备与项目初始化首先确保你的开发环境已经安装了 Node.js 和 npm。然后我们需要 Cloudflare 的命令行工具wrangler。# 全局安装 wrangler npm install -g wrangler # 登录到你的 Cloudflare 账户 wrangler login # 创建一个新的 Worker 项目我命名为 fable-chatbot-worker wrangler init fable-chatbot-worker cd fable-chatbot-worker在初始化过程中wrangler会询问项目类型选择“Hello World” worker即可。这会生成一个基本的src/index.ts文件如果你选择 TypeScript或src/index.js文件以及一个wrangler.toml配置文件。3.2 核心逻辑编写处理聊天请求我们的 Worker 需要做以下几件事接收前端发送的 POST 请求请求体中包含用户的消息message和可选的对话历史history。构造一个适合故事创作的、强大的系统提示词System Prompt。将用户消息、历史如果有和系统提示词组合发送给选定的 LLM API如 DeepSeek。以流式Streaming的方式接收模型的响应并同样以流式的方式返回给前端让用户能实时看到故事一个字一个字地“写”出来。以下是src/index.ts的一个核心实现示例。这里我以 DeepSeek API 为例因为它不需要额外的 SDK直接使用fetch即可。// src/index.ts export interface Env { // 在 wrangler.toml 或 Cloudflare 仪表板中配置的环境变量 DEEPSEEK_API_KEY: string; // 未来可以添加 ANTHROPIC_API_KEY 等 } // 定义请求和响应的数据结构 interface ChatRequest { message: string; history?: Array{ role: user | assistant; content: string }; } export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): PromiseResponse { // 1. 处理 CORS 预检请求 (OPTIONS) if (request.method OPTIONS) { return new Response(null, { headers: { Access-Control-Allow-Origin: *, // 生产环境应替换为具体域名 Access-Control-Allow-Methods: POST, OPTIONS, Access-Control-Allow-Headers: Content-Type, }, }); } // 2. 只处理 POST 请求 if (request.method ! POST) { return new Response(Method Not Allowed, { status: 405 }); } try { const { message, history [] } (await request.json()) as ChatRequest; if (!message || typeof message ! string) { return new Response(JSON.stringify({ error: Invalid message }), { status: 400, headers: { Content-Type: application/json }, }); } // 3. 构建一个强大的、针对故事创作的系统提示词 const systemPrompt 你是一位才华横溢的故事创作助手专门帮助用户将模糊的想法转化为生动、结构完整的故事。请遵循以下准则 1. **沉浸式体验**用户将以自然语言描述想法如“写一个武侠故事主角是个厌倦厮杀的杀手”。你需要理解其核心要素题材、主角、基调、冲突。 2. **结构化输出**生成的故事应包含清晰的起承转合。可以自然地包含章节标题、场景分隔等但不要使用 Markdown 格式。 3. **风格适应**根据用户描述的题材科幻、奇幻、言情、悬疑等自动调整叙事语言风格。 4. **细节丰富**在关键情节处进行合理的细节描写塑造人物形象营造氛围。 5. **长度控制**根据用户想法的复杂程度生成约 800-1500 字的故事。如果用户有明确长度要求请遵循。 6. **直接开始**不要解释你的创作过程不要问“您是否满意”直接开始讲述故事。; // 4. 构建发送给 DeepSeek API 的消息数组 const messages [ { role: system, content: systemPrompt }, ...history, // 传入历史对话实现多轮上下文 { role: user, content: message }, ]; // 5. 调用 DeepSeek API (以 DeepSeek Chat 模型为例) const deepseekResponse await fetch(https://api.deepseek.com/chat/completions, { method: POST, headers: { Authorization: Bearer ${env.DEEPSEEK_API_KEY}, Content-Type: application/json, }, body: JSON.stringify({ model: deepseek-chat, // 根据实际情况选择模型如 deepseek-coder messages: messages, stream: true, // 关键启用流式输出 max_tokens: 2000, temperature: 0.8, // 创造性温度可调 }), }); if (!deepseekResponse.ok || !deepseekResponse.body) { const errorText await deepseekResponse.text(); console.error(DeepSeek API error:, deepseekResponse.status, errorText); throw new Error(API request failed: ${deepseekResponse.status}); } // 6. 创建可读流和可写流用于将 API 的流响应转发给客户端 const { readable, writable } new TransformStream(); const writer writable.getWriter(); const encoder new TextEncoder(); // 7. 处理流式响应 (async () { const reader deepseekResponse.body!.getReader(); const decoder new TextDecoder(); try { while (true) { const { done, value } await reader.read(); if (done) break; const chunk decoder.decode(value, { stream: true }); // DeepSeek 流式响应格式为 Server-Sent Events (SSE)每行以 data: 开头 const lines chunk.split(\n); for (const line of lines) { if (line.startsWith(data: ) line.trim() ! data: [DONE]) { try { const data JSON.parse(line.slice(6)); // 去掉 data: 前缀 const content data.choices?.[0]?.delta?.content; if (content) { // 将内容增量写入流 await writer.write(encoder.encode(data: ${JSON.stringify({ content })}\n\n)); } } catch (e) { // 忽略单行解析错误继续处理下一个数据块 } } } } } catch (error) { console.error(Stream processing error:, error); await writer.write(encoder.encode(data: ${JSON.stringify({ error: Stream interrupted })}\n\n)); } finally { await writer.write(encoder.encode(data: [DONE]\n\n)); // 发送结束信号 writer.close(); reader.releaseLock(); } })(); // 8. 返回一个流式响应给前端 return new Response(readable, { headers: { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, Access-Control-Allow-Origin: *, }, }); } catch (error) { console.error(Worker error:, error); return new Response(JSON.stringify({ error: Internal Server Error }), { status: 500, headers: { Content-Type: application/json }, }); } }, };3.3 配置与部署接下来我们需要配置环境变量和部署。配置wrangler.tomlname fable-chatbot-worker main src/index.ts compatibility_date 2024-07-24 [vars] # 环境变量可以在这里定义但敏感信息建议通过命令行或仪表板设置 # DEEPSEEK_API_KEY your-key-here [[r2_buckets]] binding MY_BUCKET bucket_name my-bucket # 可选如果需要存储生成的故事设置环境变量API Key切勿将 API Key 提交到代码仓库使用以下命令在本地或生产环境设置# 在本地开发环境设置 wrangler secret put DEEPSEEK_API_KEY # 然后根据提示输入你的 API Key # 或者在 Cloudflare 仪表板的 Worker 设置页面中直接添加本地开发与测试wrangler dev这会在本地启动一个开发服务器。你可以使用curl或 Postman 向http://localhost:8787发送 POST 请求进行测试。部署到生产环境wrangler deploy部署成功后你会得到一个类似https://fable-chatbot-worker.your-subdomain.workers.dev的 URL这就是你的 Chatbot 后端 API 地址。4. 前端界面打造一个极简的聊天窗口后端 API 准备好了我们需要一个简单、好看的前端界面让我媳妇儿使用。目标是极简一个聊天窗口一个输入框一个发送按钮。为了快速实现我选择了纯 HTML/CSS/JavaScript不引入复杂的框架确保加载速度和易用性。4.1 HTML 结构创建一个index.html文件结构如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleFable 故事聊天助手/title link relstylesheet hrefstyle.css link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css /head body div classcontainer header h1i classfas fa-robot/i Fable 故事聊天助手/h1 p classsubtitle像聊天一样告诉我你的故事灵感。/p /header main div idchat-container !-- 聊天消息会动态添加到这里 -- div classmessage system div classavatari classfas fa-magic/i/div div classbubble你好我是你的故事创作助手。请用自然语言描述你想要的故事比如题材、人物、情节开头我会为你创作。/div /div /div div classinput-area div classinput-wrapper textarea idmessage-input placeholder输入你的故事想法例如写一个关于在火星上发现古老文明的科幻冒险故事... rows3/textarea button idsend-button title发送 (CtrlEnter) i classfas fa-paper-plane/i /button /div div classhint i classfas fa-lightbulb/i 提示描述越具体故事越精彩。可以尝试指定风格、长度或关键情节。 /div /div /main footer pPowered by Cloudflare Worker DeepSeek/Claude • 专为无痛创作设计/p /footer /div script srcscript.js/script /body /html4.2 CSS 样式 (style.css)为了让界面看起来舒适我添加了一些简单的样式/* style.css */ * { margin: 0; padding: 0; box-sizing: border-box; font-family: Segoe UI, Microsoft YaHei, sans-serif; } body { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 20px; } .container { width: 100%; max-width: 900px; background-color: rgba(255, 255, 255, 0.95); border-radius: 20px; box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2); overflow: hidden; display: flex; flex-direction: column; height: 90vh; } header { background: linear-gradient(to right, #4f46e5, #7c3aed); color: white; padding: 25px 30px; text-align: center; } header h1 { font-size: 2.2rem; margin-bottom: 8px; display: flex; align-items: center; justify-content: center; gap: 15px; } header .subtitle { font-size: 1rem; opacity: 0.9; } main { flex: 1; display: flex; flex-direction: column; padding: 0; } #chat-container { flex: 1; padding: 25px 30px; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; background-color: #f8fafc; } .message { display: flex; gap: 15px; max-width: 85%; animation: fadeIn 0.3s ease-out; } keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .message.user { align-self: flex-end; flex-direction: row-reverse; } .message.assistant { align-self: flex-start; } .message.system { align-self: center; max-width: 95%; background-color: #e0e7ff; border-radius: 18px; padding: 15px; } .avatar { width: 40px; height: 40px; border-radius: 50%; background: linear-gradient(135deg, #667eea, #764ba2); color: white; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 1.2rem; } .user .avatar { background: linear-gradient(135deg, #10b981, #059669); } .system .avatar { background: linear-gradient(135deg, #f59e0b, #d97706); } .bubble { padding: 15px 20px; border-radius: 18px; line-height: 1.6; font-size: 1rem; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08); } .user .bubble { background-color: #3b82f6; color: white; border-bottom-right-radius: 5px; } .assistant .bubble { background-color: white; color: #334155; border: 1px solid #e2e8f0; border-bottom-left-radius: 5px; } .system .bubble { background-color: transparent; box-shadow: none; text-align: center; color: #4b5563; font-style: italic; } .bubble .thinking { color: #94a3b8; font-style: italic; } .input-area { border-top: 1px solid #e2e8f0; padding: 25px 30px; background-color: white; } .input-wrapper { display: flex; gap: 15px; margin-bottom: 12px; } #message-input { flex: 1; padding: 18px; border: 2px solid #cbd5e1; border-radius: 12px; font-size: 1rem; resize: none; transition: border-color 0.3s; } #message-input:focus { outline: none; border-color: #3b82f6; } #send-button { padding: 0 30px; background: linear-gradient(to right, #3b82f6, #1d4ed8); color: white; border: none; border-radius: 12px; font-size: 1.1rem; cursor: pointer; transition: all 0.3s; display: flex; align-items: center; justify-content: center; } #send-button:hover:not(:disabled) { background: linear-gradient(to right, #2563eb, #1e40af); transform: translateY(-2px); } #send-button:disabled { background: #94a3b8; cursor: not-allowed; } .hint { font-size: 0.85rem; color: #64748b; display: flex; align-items: center; gap: 8px; } footer { padding: 18px; text-align: center; font-size: 0.85rem; color: #64748b; background-color: #f1f5f9; border-top: 1px solid #e2e8f0; }4.3 JavaScript 逻辑 (script.js)这是前端的核心负责处理用户输入、与后端 Worker API 通信并动态更新聊天界面。// script.js document.addEventListener(DOMContentLoaded, function() { const chatContainer document.getElementById(chat-container); const messageInput document.getElementById(message-input); const sendButton document.getElementById(send-button); // 替换为你的 Cloudflare Worker 部署地址 const API_URL https://fable-chatbot-worker.your-subdomain.workers.dev; // 本地测试可以用 const API_URL http://localhost:8787; let conversationHistory []; // 存储对话历史用于实现上下文 // 添加消息到聊天窗口 function addMessage(role, content, isStreaming false) { const messageDiv document.createElement(div); messageDiv.className message ${role}; const avatarIcon role user ? fas fa-user : (role system ? fas fa-magic : fas fa-robot); messageDiv.innerHTML div classavatari class${avatarIcon}/i/div div classbubble${isStreaming ? span classthinking${content}/span : content}/div ; chatContainer.appendChild(messageDiv); chatContainer.scrollTop chatContainer.scrollHeight; // 滚动到底部 return messageDiv.querySelector(.bubble); // 返回气泡元素用于更新流式内容 } // 更新流式消息的内容 function updateStreamingMessage(bubbleElement, newContent) { const thinkingSpan bubbleElement.querySelector(.thinking); if (thinkingSpan) { thinkingSpan.textContent newContent; } else { bubbleElement.innerHTML newContent; } chatContainer.scrollTop chatContainer.scrollHeight; } // 发送消息到后端 async function sendMessage() { const userMessage messageInput.value.trim(); if (!userMessage || sendButton.disabled) return; // 禁用输入和按钮 messageInput.disabled true; sendButton.disabled true; const originalButtonHTML sendButton.innerHTML; sendButton.innerHTML i classfas fa-spinner fa-spin/i; // 1. 在界面上显示用户消息 addMessage(user, userMessage); messageInput.value ; // 2. 在界面上显示“助手正在思考”的占位符 const assistantBubble addMessage(assistant, 正在构思你的故事..., true); // 3. 准备请求数据 const requestData { message: userMessage, history: conversationHistory // 发送历史上下文 }; try { const response await fetch(API_URL, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify(requestData), }); if (!response.ok || !response.body) { throw new Error(网络请求失败: ${response.status}); } // 4. 处理流式响应 const reader response.body.getReader(); const decoder new TextDecoder(); let fullResponse ; while (true) { const { done, value } await reader.read(); if (done) break; const chunk decoder.decode(value); const lines chunk.split(\n); for (const line of lines) { if (line.startsWith(data: ) line.trim() ! data: [DONE]) { try { const data JSON.parse(line.slice(6)); if (data.content) { fullResponse data.content; updateStreamingMessage(assistantBubble, fullResponse); } else if (data.error) { updateStreamingMessage(assistantBubble, 抱歉生成故事时出错了: ${data.error}); break; } } catch (e) { console.warn(解析流数据时出错:, e, line); } } } } // 5. 流式接收完成更新对话历史 conversationHistory.push({ role: user, content: userMessage }); conversationHistory.push({ role: assistant, content: fullResponse }); // 可选限制历史记录长度避免上下文过长 if (conversationHistory.length 20) { conversationHistory conversationHistory.slice(-20); } } catch (error) { console.error(发送消息失败:, error); updateStreamingMessage(assistantBubble, 连接出错请检查网络或稍后重试。错误信息: ${error.message}); } finally { // 恢复输入和按钮 messageInput.disabled false; sendButton.disabled false; sendButton.innerHTML originalButtonHTML; messageInput.focus(); } } // 事件监听 sendButton.addEventListener(click, sendMessage); messageInput.addEventListener(keydown, function(e) { if (e.key Enter e.ctrlKey) { e.preventDefault(); sendMessage(); } }); // 自动调整输入框高度 messageInput.addEventListener(input, function() { this.style.height auto; this.style.height (this.scrollHeight) px; }); });将这三个文件index.html,style.css,script.js放在同一个目录下。你甚至可以直接用浏览器打开index.html文件需要确保 API_URL 配置正确且后端 Worker 已部署并允许跨域一个功能完整的 Chatbot 前端就运行起来了。5. 效果优化与进阶思考让 Chatbot 更“懂”创作基础功能实现后我媳妇儿已经可以愉快地使用了。但作为一个有追求的技术宅我思考了如何让这个工具更智能、更好用。这里分享几个优化方向和进阶思考。5.1 提示词工程从“翻译官”到“创意伙伴”最初版本的系统提示词已经不错但还可以更精细。我们可以根据用户输入的第一句话动态调整系统提示词。例如在 Worker 后端收到用户消息后先调用一次 LLM 进行“意图识别”// 伪代码意图识别 const intentPrompt 分析用户以下输入判断其故事创作意图 用户输入${userMessage} 请从以下类别中选择最匹配的可多选[科幻 奇幻 武侠 言情 悬疑 恐怖 喜剧 现实主义 历史 童话]。 同时判断用户是否提供了明确的主角、场景、冲突或特殊要求如“用文言文写”、“模仿海明威风格”。 请用JSON格式回复包含 categories, hasMainCharacter, hasSetting, hasConflict, specialInstructions 字段。; // 调用一次快速、便宜的模型如 DeepSeek 的快速版本进行识别 const intent await callLLM(intentPrompt, { useFastModel: true }); // 根据识别结果动态组装更精准的系统提示词 let dynamicSystemPrompt baseSystemPrompt; if (intent.categories.includes(科幻)) { dynamicSystemPrompt \n特别注意这是一个科幻故事请确保包含合理的科技设定、未来感和社会思考。; } if (intent.specialInstructions.includes(文言文)) { dynamicSystemPrompt \n请使用文言文进行创作。; } // ... 然后将 dynamicSystemPrompt 用于最终的故事生成请求这样Chatbot 就从简单的“指令翻译官”进化成了能理解创作风格的“创意伙伴”。5.2 记忆与持久化记住“上次说到哪了”目前的实现利用conversationHistory数组在单次会话中保持了上下文。但一旦刷新页面历史就消失了。为了更好的体验可以考虑前端本地存储使用localStorage或IndexedDB将conversationHistory保存在用户的浏览器中。这是最简单的方法数据完全在用户本地。// 页面加载时读取 const savedHistory localStorage.getItem(fableChatHistory); if (savedHistory) conversationHistory JSON.parse(savedHistory); // 每次更新历史后保存 localStorage.setItem(fableChatHistory, JSON.stringify(conversationHistory));后端持久化如果需要跨设备同步可以将历史记录存储在后端。Cloudflare Worker 可以搭配D1SQLite 数据库或KV键值存储来实现。每个用户需要一个唯一标识例如一个简单的匿名 UUID存储在浏览器 cookie 中然后将历史记录以该 UUID 为键存入 KV。5.3 成本控制与模型降级策略如果使用量增大API 成本是需要考虑的。除了选择性价比高的模型如 DeepSeek还可以实施一些策略缓存对于常见、通用的故事请求例如“写一个王子救公主的童话”其生成结果在一定时间内是相同的。可以在 Worker 中使用Cache API将结果缓存起来设置合适的 TTL下次遇到高度相似的请求时直接返回缓存避免重复调用昂贵的模型 API。// 伪代码缓存示例 const cacheKey story:${hash(userMessage)}; const cached await caches.default.match(cacheKey); if (cached) { return new Response(cached.body, { headers: { X-Cache: HIT } }); } // ... 正常调用 API ... // 存储到缓存 const responseToCache new Response(streamingBody, { headers: { Cache-Control: public, max-age3600 } }); ctx.waitUntil(caches.default.put(cacheKey, responseToCache.clone()));模型降级在 Worker 中配置多个模型的 API Key。对于简单的、描述性的请求如“帮我写一个故事大纲”使用更便宜、更快的模型如 DeepSeek 的deepseek-chat-fast或claude-3-haiku对于复杂的、需要文学性的完整故事生成再使用能力更强的模型如claude-3-sonnet或deepseek-chat。5.4 安全与滥用防范虽然是个个人项目但一些基本的安全考虑是必要的速率限制使用 Cloudflare Worker 的限流功能防止 API 被意外或恶意高频调用保护你的 API Key 预算。可以在 Worker 逻辑开头加入简单的计数器利用 Worker 的全局变量或 KV 来记录 IP 地址的请求频率。内容过滤虽然 LLM 本身有安全策略但可以在 Worker 端对用户输入和模型输出进行二次检查过滤明显不当的内容确保生成的故事符合家庭使用环境。经过这些优化这个最初只为“让媳妇儿无痛用上 Fable 5”而做的小工具已经成长为一个功能相对完整、体验流畅、具备一定智能的个性化故事创作助手。它剥离了复杂界面的干扰回归了“讲故事”和“听故事”最本质的交流方式。技术不再是门槛而是隐于幕后的、温暖的赋能者。看到媳妇儿现在能轻松地通过聊天框迸发创意、获得灵感甚至和我一起讨论 AI 生成的情节我觉得这个项目所有的折腾都值了。
返回列表