尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用Trae Solo+GLM-4.6做沉浸式小说创作:用户选剧情,AI写故事

用Trae Solo+GLM-4.6做沉浸式小说创作:用户选剧情,AI写故事 1. 从单向生成到分支叙事Trae Solo GLM-4.6 要解决的真实问题传统 AI 写作工具最大的毛病是“一次性输出”——你给个开头它吐一大段不满意只能重来中间没有任何介入点。写小说本来是个不断做选择的过程主角是推门进去还是转身离开是相信陌生人还是保持警惕这些选择决定了故事的走向和张力。但大多数工具把这条路堵死了用户只能当旁观者。我想要的是一种“读者即作者”的体验用户给一个故事开头系统生成三个风格不同的情节分支概览用户选一个AI 立刻续写 300 到 500 字的详细内容然后基于新内容再生成下一轮分支。如此循环故事像一棵树一样生长每个节点都是用户的选择每条路径都是独一无二的。这个模式的核心不是“AI 写得好不好”而是“用户能不能掌控叙事节奏”。Trae Solo 在这里扮演的是上下文工程调度器的角色。它不只是帮你写代码更重要的是把整个项目的上下文——产品需求、技术架构、API 调用规范、状态管理逻辑——结构化地组织起来让 AI 在正确的时机拿到正确的信息。GLM-4.6 则负责叙事生成它在中文长文本连贯性和情节延展上的表现比很多通用模型更稳。两者结合才能让“选择-续写-再选择”这个闭环跑得顺畅。这篇文章会带你从零搭一个可运行的 React 应用包含 API Key 本地配置、分支状态管理、GLM-4.6 调用封装以及一次完整的选择-续写-验证流程。代码可以直接复制配置改一下就能跑。2. 前置准备TaoToken 接入 GLM-4.6 的 API Key 与 Base URL 配置在开始写 React 代码之前先把模型调用通道打通。GLM-4.6 的 API 兼容 OpenAI 的 chat completions 格式所以你可以用任何支持自定义 Base URL 的客户端来调。这里我用 TaoToken 作为统一接入层它的好处是 Base URL 和 Key 管理集中切换模型不用改代码。首先去 TaoToken 控制台创建一个 API Key。打开 https://taotoken.net/api-keys 登录后点“创建密钥”复制生成的 Key格式类似sk-xxxxxxxx。这个 Key 只显示一次建议先存到密码管理器里。然后确认 Base URL。TaoToken 的 API 端点是https://taotoken.net/api注意不要加 UTM 参数直接写这个地址就行。GLM-4.6 的模型 ID 是glm-4.6在请求体里指定。如果你用的是 Claude Code 或者 Cline 这类工具配置方式略有不同。以 Claude Code 为例需要在~/.claude/settings.json里加一段{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的Key } }Cline 的话在 MCP 配置里写{ mcpServers: { taotoken: { command: npx, args: [-y, taotoken/mcp-server], env: { TAOTOKEN_API_KEY: sk-你的Key, TAOTOKEN_BASE_URL: https://taotoken.net/api } } } }Codex 的auth.json配置类似{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: glm-4.6 }三件套记住Base URL 是https://taotoken.net/apiKey 是sk-开头那串Model ID 是glm-4.6。这三个信息在后面的 React 代码里会用到。配置完之后先用 curl 测一下通不通curl -X POST https://taotoken.net/api/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: glm-4.6, messages: [ {role: user, content: 用一句话描述一个雨夜书店的场景} ], temperature: 0.8, max_tokens: 200 }如果返回的 JSON 里有choices[0].message.content说明通道没问题。如果报 401检查 Key 有没有复制完整如果报 model not found确认模型 ID 写的是glm-4.6而不是glm-4或其他。3. 可复制配置React 项目结构与 GLM-4.6 调用封装现在开始搭 React 应用。用 Vite 初始化项目命令如下npm create vitelatest ai-novel-platform -- --template react-ts cd ai-novel-platform npm install npm install axios项目结构建议这样组织src/ api/ glm.ts # GLM-4.6 调用封装 components/ StoryInput.tsx # 故事开头输入 BranchSelector.tsx # 分支选择卡片 StoryTree.tsx # 故事树展示 ContentDisplay.tsx # 当前情节内容 context/ StoryContext.tsx # 全局状态管理 pages/ Home.tsx Config.tsx types/ story.ts # TypeScript 类型定义 App.tsx main.tsx先写类型定义src/types/story.tsexport interface BranchOption { id: string; title: string; description: string; isSelected: boolean; order: number; } export interface StoryNode { id: string; parentNodeId?: string; content: string; level: number; isSelected: boolean; createdAt: Date; branches?: BranchOption[]; } export interface Story { id: string; title: string; initialContent: string; nodes: StoryNode[]; currentNodeId?: string; createdAt: Date; } export interface GLMResponse { choices: Array{ message: { content: string; role: string; }; finish_reason: string; }; usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number; }; }然后是 API 封装src/api/glm.ts。这里把 Base URL 和 Key 从 localStorage 读取方便用户在配置页修改import axios from axios; import type { GLMResponse } from ../types/story; const BASE_URL https://taotoken.net/api; function getApiKey(): string { const key localStorage.getItem(glm_api_key); if (!key) { throw new Error(API Key 未配置请先前往配置页面设置); } return key; } export async function generateBranches( storySoFar: string, temperature 0.8 ): Promisestring[] { const apiKey getApiKey(); const response await axios.postGLMResponse( ${BASE_URL}/chat/completions, { model: glm-4.6, messages: [ { role: system, content: 你是一个专业的小说创作助手。根据给定的故事内容生成三个不同的情节发展方向。每个方向用一句话概括格式为1. xxx 2. xxx 3. xxx。不要展开细节只给方向。, }, { role: user, content: 当前故事内容\n${storySoFar}\n\n请生成三个情节分支概览。, }, ], temperature, max_tokens: 300, stream: false, }, { headers: { Authorization: Bearer ${apiKey}, Content-Type: application/json, }, timeout: 30000, } ); const content response.data.choices[0].message.content; const branches content .split(/\d\.\s*/) .filter((s) s.trim().length 0) .slice(0, 3); return branches; } export async function generateDetail( storySoFar: string, selectedBranch: string, temperature 0.7 ): Promisestring { const apiKey getApiKey(); const response await axios.postGLMResponse( ${BASE_URL}/chat/completions, { model: glm-4.6, messages: [ { role: system, content: 你是一个专业的小说创作助手。根据给定的故事内容和选择的情节方向创作 300-500 字的详细情节。要求保持叙事连贯性注重场景描写和人物对话结尾留一个悬念或转折点。, }, { role: user, content: 故事内容\n${storySoFar}\n\n选择的情节方向${selectedBranch}\n\n请创作详细情节。, }, ], temperature, max_tokens: 800, stream: false, }, { headers: { Authorization: Bearer ${apiKey}, Content-Type: application/json, }, timeout: 60000, } ); return response.data.choices[0].message.content; }注意timeout设了 30 秒和 60 秒因为 GLM-4.6 生成 500 字左右的内容通常需要 5 到 15 秒网络波动时可能更久。如果超时太短会频繁报错。接下来是状态管理src/context/StoryContext.tsx。用 useReducer 管理故事树import React, { createContext, useReducer, useContext, useEffect } from react; import type { Story, StoryNode, BranchOption } from ../types/story; interface State { story: Story | null; isLoading: boolean; error: string | null; apiKey: string | null; } type Action | { type: SET_STORY; payload: Story } | { type: ADD_NODE; payload: StoryNode } | { type: SET_LOADING; payload: boolean } | { type: SET_ERROR; payload: string | null } | { type: SET_API_KEY; payload: string } | { type: RESET }; const initialState: State { story: null, isLoading: false, error: null, apiKey: localStorage.getItem(glm_api_key), }; function reducer(state: State, action: Action): State { switch (action.type) { case SET_STORY: return { ...state, story: action.payload }; case ADD_NODE: if (!state.story) return state; return { ...state, story: { ...state.story, nodes: [...state.story.nodes, action.payload], currentNodeId: action.payload.id, }, }; case SET_LOADING: return { ...state, isLoading: action.payload }; case SET_ERROR: return { ...state, error: action.payload }; case SET_API_KEY: localStorage.setItem(glm_api_key, action.payload); return { ...state, apiKey: action.payload }; case RESET: return { ...initialState, apiKey: state.apiKey }; default: return state; } } const StoryContext createContext{ state: State; dispatch: React.DispatchAction; }({ state: initialState, dispatch: () {} }); export function StoryProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] useReducer(reducer, initialState); useEffect(() { const saved localStorage.getItem(current_story); if (saved) { try { const story JSON.parse(saved); dispatch({ type: SET_STORY, payload: story }); } catch (e) { console.warn(恢复故事状态失败, e); } } }, []); useEffect(() { if (state.story) { localStorage.setItem(current_story, JSON.stringify(state.story)); } }, [state.story]); return ( StoryContext.Provider value{{ state, dispatch }} {children} /StoryContext.Provider ); } export function useStory() { return useContext(StoryContext); }这个 Context 做了两件事一是管理故事树的状态二是自动把故事同步到 localStorage刷新页面不丢进度。API Key 单独存一个 key不跟故事数据混在一起。4. 验证请求一次完整的选择-续写-验证流程代码写完了现在跑一次完整流程。启动开发服务器npm run dev打开浏览器访问http://localhost:5173先点“配置”页面把 TaoToken 的 API Key 粘贴进去点保存。然后回到首页输入一个故事开头比如深夜的图书馆里林晚在整理旧书时发现了一本没有书名的黑色笔记本。翻开第一页上面写着“如果你看到这行字说明你已经死了。”点“生成分支”等几秒钟页面会出现三个分支卡片。我实测下来GLM-4.6 生成的分支大概是这样的林晚继续翻看笔记本发现后面记录着一个个陌生人的死亡时间和地点而最后一个名字是她自己。林晚把笔记本合上放回原处但第二天它又出现在她的包里并且多了一行字“你逃不掉的。”林晚拿着笔记本去问图书馆管理员管理员看到后脸色大变说这本笔记本三十年前就烧毁了。选第二个分支点“生成详细内容”。等大约 10 秒页面会显示一段 400 字左右的续写。我拿到的内容是林晚把笔记本塞回书架最底层用三本厚重的辞典压住然后快步走出图书馆。夜风灌进领口她裹紧外套告诉自己只是幻觉。回到家她把包扔在玄关洗澡、热牛奶、关灯一切照常。但凌晨三点她被一阵纸张摩擦的声音惊醒。打开床头灯那个黑色笔记本正躺在她的枕头旁边封面朝上。她颤抖着翻开昨天那行字下面多了一行新的笔迹墨迹还没干透“你逃不掉的。”林晚猛地抬头看向窗户玻璃上映出她的脸但嘴角的弧度比她本人慢了一拍。这段续写保持了原文的悬疑氛围结尾留了悬念符合提示词里“结尾留转折点”的要求。然后基于这段内容系统会自动生成下一轮三个分支用户可以继续选。故事树在左侧实时更新当前节点高亮历史路径可以点击回看。验证 API 调用是否成功可以打开浏览器开发者工具的 Network 面板找到chat/completions请求看返回状态码是不是 200响应体里有没有choices字段。如果状态码是 401说明 Key 有问题如果是 429说明请求太频繁等几秒再试。5. 常见报错排查401、local proxy failed、reading choices、OAuth接入过程中最容易碰到几类报错这里对照真实错误信息给排查路径。401 Unauthorized返回体通常是{error:{message:Invalid API key,type:invalid_request_error}}。原因有三个Key 复制时带了空格或换行Key 已经过期或被删除请求头里Authorization格式写错了。正确格式是Bearer sk-xxxxBearer 和 Key 之间有一个空格。检查src/api/glm.ts里的Authorization拼接确认没有多余字符。local proxy failed / ECONNREFUSED这个报错通常出现在你本地配了代理但代理没启动或者 Base URL 写成了http://localhost:xxxx。TaoToken 的 Base URL 是https://taotoken.net/api不要加端口号也不要写成http://。如果你在用 Cline 或 Claude Code检查 MCP 配置里的TAOTOKEN_BASE_URL是不是这个地址。reading choices of undefined这个错误说明响应体里没有choices字段通常是 API 返回了错误信息但代码没做错误处理。在glm.ts里加一层判断if (!response.data.choices || response.data.choices.length 0) { throw new Error(API 返回数据格式异常 JSON.stringify(response.data)); }同时检查model字段是不是glm-4.6如果写成了glm-4或gpt-4有些网关会返回错误结构。OAuth token expired / invalid_grant如果你用的是 Claude Code 或 Codex 的 OAuth 登录方式token 过期后会报这个。解决办法是重新走一遍授权流程或者改用 API Key 方式接入。在settings.json里把ANTHROPIC_API_KEY换成 TaoToken 的 KeyBase URL 指向https://taotoken.net/api就不依赖 OAuth 了。超时 timeout of 30000ms exceededGLM-4.6 生成 500 字内容在高峰期可能需要 20 秒以上。把timeout调到 60000同时在 UI 上加一个进度条或 loading 动画避免用户以为卡死。Trae Solo 里可以直接跟它说“给生成分支加一个动态进度条”它会自动改代码。分支解析失败只返回一段文字GLM-4.6 有时候不按“1. 2. 3.”格式输出而是用换行分隔。在generateBranches里加一个兜底逻辑let branches content.split(/\d\.\s*/).filter((s) s.trim().length 0); if (branches.length 3) { branches content.split(\n).filter((s) s.trim().length 10); } return branches.slice(0, 3);这样即使格式不标准也能拿到三个分支。6. 语义一致 CTA从能跑到好用下一步做什么代码跑通之后你会发现几个可以继续优化的点。一是分支生成的速度GLM-4.6 在temperature0.8时创意性好但偶尔会跑偏可以降到 0.7 让输出更稳定。二是故事树的展示节点多了之后需要折叠和展开可以用react-flow或d3-hierarchy做可视化。三是多结局管理当用户走到某个节点不想继续时可以标记为“结局”并导出整条路径的文本。如果你想把模型调用换成其他模型只需要改src/api/glm.ts里的model字段和 Base URL。TaoToken 的接入层兼容 OpenAI 格式所以切换成本很低。API Key 的管理建议用环境变量而不是硬编码部署到 Vercel 时在项目设置里加VITE_GLM_API_KEY代码里用import.meta.env.VITE_GLM_API_KEY读取。部署命令很简单npm run build npx vercel --prodVercel 会自动识别 Vite 项目构建输出目录是dist。部署完成后在 Vercel 项目设置里加环境变量把 API Key 配进去。这样线上版本就不需要用户手动填 Key 了。整个项目从零到上线核心代码不到 500 行但覆盖了上下文工程、状态管理、API 封装、错误处理和部署全流程。你可以在这个基础上加自己的功能比如角色卡管理、世界观设定库、多语言支持。故事树的每个节点都是一次选择而每次选择都是用户和 AI 共同创作的痕迹。
返回列表