ARTICLE DETAIL

资讯详情

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

从零构建本地AI角色扮演工具:Ollama+FastAPI+Vue实战

从零构建本地AI角色扮演工具:Ollama+FastAPI+Vue实战 上个月有个周五晚上朋友发来一条语音语气比白天开会时精神多了“能不能帮我弄一个能自己在电脑上跑的AI角色扮演工具”我第一反应是这不是现成一堆开源项目么去GitHub上拉一个就行。结果她把需求一条一条列出来之后我意识到事情没那么简单。她要的是一个不绑大厂账号、本地优先的AI角色扮演工具对话记录不能传到别人服务器上人设必须能自由编辑最好还能离线跑。我答应先调研一周结果发现现有方案大部分要么太重、要么太“平台化”最后干脆自己动手写了一个项目叫Utopia。写完之后我把它开源了这一个月踩的坑、做的取舍、以及真正跑通的核心逻辑今天一次性写清楚给同样想做本地AI角色扮演项目的朋友一个参考。1. 这个需求的起点朋友的一通电话和一串吐槽1.1 “不绑大厂”到底在反对什么很多人可能不理解为什么一个角色扮演工具要强调“不绑大厂”。朋友的原话是我把角色设定、对话内容、甚至那个角色的“人生经历”都写进去了这些东西放在别人服务器上我自己心里不舒服。这个不舒服不是矫情。角色扮演聊天的内容天然非常私密你可能会和AI倾诉现实中不会说出口的事也会创作一个完全属于自己的虚构角色。如果这个工具绑定某个大厂账号你的对话记录、创作素材、习惯偏好全都变成平台侧的“数据资产”你不知道它被谁看到、被用来做什么也没有办法真正删除。传统云端AI产品再方便在这个场景下就是一个信任问题。我自己也有过类似的感受。平时用网页版AI工具写工作邮件、改简历没负担但让我把一段虚构的小说角色对话、人物小传放进某个商业平台的聊天记录里我会下意识抗拒。这和你不会把日记本随便交给陌生人保管是一个道理。所以从一开始“本地优先”就不是一句口号而是整个项目的底层约束模型推理必须在本地完成至少核心对话链路不依赖外部API聊天记录、角色卡、配置全部落在本地文件或本地数据库整个服务可以完全离线运行断网也能继续聊不强制注册任何账号打开就能用。这个约束直接决定了后面的技术选型方向。我需要在本地找一个能跑大语言模型的方式再围绕它搭一个足够小但完整好用的角色扮演聊天应用。1.2 现成的开源工具明明一堆为什么还要自己写调研阶段我确实认真看了几个非常成熟的开源方案比如Open WebUI、SillyTavern、Text Generation WebUI。它们都很强但我实际部署完发现一个问题它们是给“高度自定义玩家”准备的不是给“只想安静聊个角色”的普通用户准备的。SillyTavern在角色扮演圈子里几乎是标准答案角色卡、世界书、正则脚本、自定义表情功能多到能写一本书。但它打开之后藏着一大堆配置项第一次使用的人会当场懵掉。朋友的需求很直接导入一个角色设定好背景故事打开聊天界面开始对话完事。她不需要一个能无限扩展的“创作平台”需要的是一个打开就能用的“私人剧场”。另外从开发者角度看那些项目的大部分功能我根本用不上但它们的代码规模已经很复杂前端加上后端加各种依赖整体改造门槛高。我要的其实是一个“最小可用的角色扮演闭环”——一个角色配置文件、一个后端接口、一个聊天页面仅此而已。与其在那堆通用工具里做减法不如自己从头写一个干净的最小实现。这一个月的时间足够把核心链路跑通了。还有一个很现实的原因是学习成本。自己搭一遍远比看别人文档理解得更深。比如本地模型的流式输出怎么解析、上下文窗口怎么管理、人设为什么聊着聊着就崩了这些问题只有亲手处理过才知道里面水多深。后面我会逐个讲。2. 技术选型思路为什么是Ollama FastAPI Vue2.1 模型调度层Ollama到底好在哪要跑本地大模型最底层可以选llama.cpp直接编译也可以选vLLM做高性能推理服务但对一个个人项目来说Ollama是最合适的。原因很直接Ollama跨平台Windows、macOS、Linux都有安装包装完以后通过命令就能拉模型不需要手动下载权重文件再去改路径。它自带模型版本管理ollama list、ollama pull一条命令解决。更重要的是它提供了非常干净的REST API默认跑在localhost:11434前端和后端只需要向这个端口发请求就能完成一次模型调用。Ollama还支持Modelfile可以在里面自定义temperature、top_p、stop这些推理参数甚至能改系统提示词。这个特性对角色扮演特别关键因为角色温度高低直接决定回复风格。我把常用的角色参数全放在Modelfile或角色配置里方便随时调。实测下来Ollama加载Qwen2.5 7B这类模型在普通机器上能跑有独立显卡体验会好很多没有显卡用CPU也能跑中小尺寸模型。对我来说最重要的是模型权重真正落在本地硬盘上所有推理都在本机完成。这个特性完美契合“本地优先”的核心需求。当然Ollama也不是没有缺点。它的并发处理能力不强同时对多个模型请求时会排队而且对底层采样参数的暴露没有llama.cpp那么细。但这些对我的场景没有影响本地角色扮演基本是单用户低并发Ollama的性能瓶颈根本碰不到。2.2 服务端为什么选FastAPI而不是Node服务端我选了Python FastAPI。当时考虑过Node.js也考虑过直接用纯Python标准库写HTTP服务但最后FastAPI胜出主要因为三点。第一FastAPI的StreamingResponse对SSEServer-Sent Events支持非常好。角色扮演最需要的就是流式打字机效果AI回复一个字一个词地蹦出来而不是转半天圈突然弹出一整段。FastAPI可以直接把Ollama的流式响应转发给前端链路短逻辑清晰。第二FastAPI带自动生成OpenAPI文档。跑起来之后访问/docs就能直接在浏览器里调接口、看参数调试效率高很多。前端还在写的时候我后端每一个接口都能先通过文档页面验证一遍。第三Python生态对文本处理更友好。角色扮演离不开文本拼装比如把角色背景、历史对话、系统提示拼成一个消息数组偶尔做一下关键词提取、摘要压缩这些用Python处理起来最顺手。没有用LangChain这类框架原因很简单抽象层级太厚。甲方就是一个人设加一个聊天窗口我只需要拼接消息列表再调用Ollama上框架反而要学它的概念和回调机制。真正的复杂点在于角色一致性和记忆管理这些都是应用层逻辑不需要框架来“帮忙”。2.3 前端界面功能不复杂选Vue就够了前端我选了Vue 3加TailwindCSS。说实话这种项目的前端不需要重型框架核心界面就是三个区域左侧角色列表、中间聊天窗口、右侧参数面板。Vue 3的单文件组件结构非常适合这样的小项目状态管理用自带ref和reactive就够不用上Pinia更不用上Redux。为什么没选React不是React不行而是我在Vue上的开发效率更高。个人项目最大的原则是“顺手优先”技术选型不只是看社区活跃度也要看开发者自己的熟练度。只要技术栈足够主流、后期有人接手能找到资料选什么都可以。前端需要处理的核心交互只有一个接收SSE流式数据并渲染成打字机效果。这个逻辑用fetch加ReadableStream就能完成不需要额外依赖。组件拆分也很简单ChatWindow负责对话展示CharacterPanel负责角色切换SettingsPanel负责参数调整三个组件各自独立。3. 系统设计Utopia的核心模块和数据流3.1 目录结构一眼看明白这项目怎么长出来的Utopia的项目结构很直白我当时故意没有做复杂的分层因为一个人维护的项目过度的架构抽象会变成负担。utopia/ ├── backend/ │ ├── main.py # FastAPI入口路由定义 │ ├── ollama_client.py # 封装Ollama API调用 │ ├── character.py # 角色卡读取与解析 │ ├── db.py # SQLite读写聊天记录管理 │ └── history.py # 上下文整理与摘要压缩 ├── frontend/ │ ├── index.html │ ├── src/ │ │ ├── components/ │ │ │ ├── ChatWindow.vue │ │ │ ├── CharacterPanel.vue │ │ │ └── SettingsPanel.vue │ │ └── main.js │ └── package.json ├── configs/ │ └── characters/ # 角色卡YAML文件 ├── scripts/ │ ├── start.sh # 一键启动脚本 │ └── backup.sh # 数据备份脚本 └── README.mdbackend里每个文件职责非常单一。ollama_client.py只做一件事接收消息列表调用Ollama把流式内容拿到主程序里。character.py负责把YAML角色卡解析成系统提示词和用户示例。db.py负责把每轮对话写入SQLite。history.py负责处理上下文过长的问题这个后面细说。configs/characters目录存一批示例角色卡用户新增角色只需要在这个目录里放一个YAML文件不需要改代码。这个设计本质上就是“内容与程序分离”角色是数据程序是引擎两者解耦让扩展变得非常简单。3.2 一条消息从输入到输出的完整链路先聊一次完整对话流程。用户在聊天框输入“你回来啦”回车之后发生了下面这几件事前端把消息通过POST /api/chat发给后端Body里带character_id和message。后端收到请求后先读取这个角色对应的YAML配置从里面取出系统提示词、人设摘要、few-shot示例、推理参数。然后从SQLite里加载最近的聊天记录拼成一个messages数组。这个数组的结构是[ {role: system, content: 你是一个名叫林晚的角色性格冷静克制……}, {role: user, content: 好久不见你最近去哪了}, {role: assistant, content: 我在等一个会问我去哪的人。}, {role: user, content: 你回来啦}, ]这个数组通过ollama_client转发给本地Ollama服务Ollama开始流式生成回复。每生成一个小片段Ollama就把一行JSON推回来后端这一侧逐行解析内容再用StreamingResponse把这些片段推给前端浏览器。前端拿到一个片段就追加到当前气泡里于是用户看到的就是一个字一个字蹦出来的效果。整轮对话结束后后端把最终完整回复连同对应的用户消息一起写入SQLite。下次对话时这段历史就会被加载进上下文角色才会“记得”刚刚发生了什么。3.3 人设稳定性的设计不要让角色一句话就崩了角色扮演最影响体验的问题是“人设蹦了”。前两轮很有感觉第五轮开始说话像客服。要解决这个问题不能只靠一个简单的系统提示词需要设计一套组合方案。我在角色卡配置里拆了三个层次。第一层是人设的“核心价值观”包括性格、说话方式、禁忌、知识和动机这一层放进系统提示词告诉模型“你是谁”。第二层是“对话风格示例”在系统提示词之后附上几轮符合角色人设的few-shot对话让模型知道具体输出长什么样。第三层是“场景设定”如果角色处于某个特定世界观就把当前场景写进上下文避免角色聊着聊着跳出世界观去讨论现实问题。参数层面也有讲究。temperature设置太高角色容易放飞自我、语言风格漂移设置太低又显得机械。我通常把temperature设置在0.6到0.8之间top_p设置在0.9左右这样能让模型有足够的创造性又不至于脱离人设。角色卡的YAML里还单独提供了temperature_override字段允许不同角色覆盖默认参数。上下文管理同样绕不开。大模型上下文窗口再大也是有限的聊到两百轮之后早期记忆会慢慢被挤出去角色就会失去“历史感”。我用了最朴素的方案保留最近20轮完整对话更早的内容每隔10轮让模型生成一段压缩摘要存进一个叫memory_summary的字段。每次请求时把压缩摘要放在系统提示词后面再拼接最近的完整对话。这个方案不复杂但实测能有效缓解角色失忆。4. 从零跑通Utopia关键代码与配置文件拆解4.1 安装Ollama并准备本地模型先解决“跑什么模型”的问题。在Linux或macOS上安装Ollama官方脚本基本一条命令curl -fsSL https://ollama.com/install.sh | shWindows用户可以下载官方安装包它会自动处理WSL2环境。装完以后先做两件事验证服务是否启动拉一个适合本地跑的模型。ollama serve ollama pull qwen2.5:7b ollama listollama pull会把模型权重下载到本地目录。推荐用Qwen2.5 7B或Llama 3.1 8B两者在中文角色扮演和通用对话上都有不错表现。没有独立显卡的机器可以选qwen2.5:3b或phi3:mini牺牲一点智商换来能够忍受的响应速度。拉完模型后用curl快速测一下接口是否正常curl http://localhost:11434/api/chat -d { model: qwen2.5:7b, messages: [{role: user, content: 你好}] }只要这条命令能输出内容Ollama侧就通了。这个最小验证很重要之后所有问题都能在这个起点之上一层层排查。4.2 角色卡配置文件怎么写才有效角色卡的格式决定用户扩展角色的难度。Utopia用YAML作为角色卡格式原因只有一个对人类友好写注释也方便。一个最简角色卡长这样character: 林晚 greeting: 你来了。我等了你很久。 system_prompt: | 你是林晚。 性格冷静、克制、外冷内热说话简洁但精准。 语言风格短句为主偶尔带一点点讽刺从不用感叹号。 禁忌不主动讨好用户不出现“亲爱的”之类甜腻称呼。 You are not an AI assistant; you are a fictional character in a story. few_shots: - user: 你是不是一直在等我 assistant: 我只是刚好有空。 - user: 我想听听你的事。 assistant: 我的事没什么好听的一页纸都写不满。 temperature: 0.7 top_p: 0.9 max_tokens: 512注意system_prompt里我特意加了一句“你不是AI助手你是虚构角色”这是一句很朴素的防跑偏咒语。大模型约定俗成的“助手身份”非常强大如果没有明确否定聊着聊着就会回到那种礼貌、万能、没有性格的状态。few_shots的示例也不能省。模型对“你想要的具体风格”的判断力有限给两三轮高质量示范比在系统提示里写一万字形容词都管用。示范内容本身就是模型的“锚点”它会把输出往这个方向拉。4.3 后端接口组装messages并转发给Ollama后端最核心的接口就是/api/chat。这里放一段我当时实现的最简版本from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse from pydantic import BaseModel import json from ollama_client import stream_chat from character import load_character from db import load_history, save_exchange app FastAPI() app.add_middleware( CORSMiddleware, allow_origins[http://localhost:5173], allow_methods[*], allow_headers[*], ) class ChatRequest(BaseModel): character_id: str message: str app.post(/api/chat) async def chat(req: ChatRequest): char load_character(req.character_id) if char is None: raise HTTPException(status_code404, detail角色不存在) history load_history(req.character_id, limit20) messages [{role: system, content: char.system_prompt}] messages.extend(history) messages.append({role: user, content: req.message}) return StreamingResponse( stream_chat(messages, char.params), media_typetext/event-stream )stream_chat封装了Ollama的/api/chat接口用requests库的流式模式逐行读取import requests import json def stream_chat(messages, params): url http://localhost:11434/api/chat payload { model: params.get(model, qwen2.5:7b), messages: messages, stream: True, options: { temperature: params.get(temperature, 0.7), top_p: params.get(top_p, 0.9), max_tokens: params.get(max_tokens, 512), }, } with requests.post(url, jsonpayload, streamTrue) as r: for line in r.iter_lines(): if not line: continue data json.loads(line) if data.get(done): break yield data[message][content]这段代码每次拿到一个token片段就立刻yield给FastAPIFastAPI再通过流式响应转发给浏览器。链路不复杂但实测下来非常稳。db.py里的save_exchange会在流式结束后把完整对话写入SQLite因为前端只是展示最终权威数据必须以后端写入为准。4.4 前端流式渲染把逐字token变成打字机效果前端最关键的代码是用fetch读取流式响应。Vue 3里可以这样写async function sendMessage() { const resp await fetch(/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ character_id: currentCharacterId, message: inputMessage }) }); const reader resp.body.getReader(); const decoder new TextDecoder(utf-8); let fullText ; let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop(); for (const line of lines) { if (!line.trim()) continue; const token line.replace(data:, ).trim(); fullText token; currentReply.value fullText; // 触发页面更新 } } }注意我用了decoder.decode(value, { stream: true })这个参数很容易被忽略。如果只用decode(value)多字节UTF-8字符可能在边界处被截断变成乱码。用stream: true之后解码器会缓存未完成的字节等下一个分块到达再继续解码中文显示就不会出问题。前端把当前回复绑定到一个响应式变量上每当追加一个token就触发Vue视图更新最终形成打字机效果。如果发现打字机一卡一卡的可以设置一个小的节流比如每20毫秒更新一次DOM体验会平滑很多。4.5 一键启动脚本让朋友不用打开三个终端原本我总是让用户手动开三个终端分别启动Ollama、后端和前端后来反思了一下这像话吗朋友要的是一个“开箱即用”不是“命令行者”。于是我在scripts/start.sh里写了启动逻辑#!/usr/bin/env bash if ! pgrep -f ollama /dev/null; then echo 启动 Ollama... ollama serve fi echo 启动后端... cd backend uvicorn main:app --host 127.0.0.1 --port 8000 echo 启动前端... cd ../frontend npm run dev echo Open http://localhost:5173脚本里先检查Ollama是否已经运行避免重复启动后端和前端都用后台方式拉起来最后提示访问地址。这个脚本看起来简单但对使用体验的提升是决定性的。把三步变一步产品的门槛立刻降了一个数量级。生产环境部署时还可以把uvicorn和前端构建产物合并成一个静态文件服务只留一个8000端口彻底不需要Vite开发服务器。这个改造在开源前我做了部署体验又稳了一点。5. 开源上线的隐藏成本许可证、文档、CI这些事一个都不能少5.1 开源许可证怎么选才不给自己挖坑代码写完之后Open Source不是把仓库设成public就完事了。许可证选择是第一个要决定的坑。我当时在MIT、Apache-2.0、GPL-3.0之间纠结了一阵。三者的主要差别如下许可证允许商用允许闭源修改专利授权传染性适合场景MIT是是无无最宽松适合个人工具、库Apache-2.0是是有无适合需要商标和专利保护的公司项目GPL-3.0是否无强适合希望修改者也必须开源的社区项目Utopia最后选了MIT。原因很朴素这是一个工具类项目我希望任何人都能拿它做二次开发甚至闭源集成到自己的产品里都没问题。如果选GPL虽然“维护开源精神”但会挡掉一部分想嵌入项目的用户。对这个项目来说最核心的目标是“让大家用起来”MIT是阻力最小的方案。多说一句如果你把一个项目开源但里面用了AGPL依赖是要特别小心的AGPL的传染性比GPL更强。Utopia本身没有引入AGPL依赖所以MIT许可证是干净、自洽的。5.2 README写得好不好直接决定有没有人用开源项目第一眼不看代码看README。我见过太多优秀代码死在README太烂。Utopia的README我前后改了五版核心原则是十条第一屏必须有截图或者GIF展示角色聊天界面人都是视觉动物第二行必须是一句话定位Utopia是一个本地优先、离线可用、自由定义人设的AI角色扮演聊天工具接着给快速开始三步命令就能跑起来不分平台差异然后给角色卡格式说明附示例文件最后开FAQ入口把常见问题放在一个独立文档里。README不是说明书是“销售文案加教程的合体”。写不清README代码写得再好也留不住用户。5.3 GitHub CI、Release和第一波Issue开源不能只把代码扔上去。跑一个最小CI至少保证新提交不会破坏构建。我给Utopia配的GitHub Actions非常简单后端pip install后跑pytest前端npm ci后跑vite build。这个CI不测推理效果只保证基本代码健康。Release流程同样重要。我把每个稳定版本打一个tag生成一个release压缩包包里放后端、前端构建产物、启动脚本、示例角色卡。用户下载release包解压运行start.sh直接可用。不能要求每个人都去clone仓库再自己build。开源之后收到的第一个Issue是“Windows上启动失败”排查之后发现是Windows下pgrep命令不存在导致的。解决办法是把进程检查改成一个跨平台的Python脚本。这类问题在个人开发环境根本测不到只有用户多了才会暴露处理起来非常有价值。6. 一个月里的真实踩坑记录与排查思路6.1 问题速查表10个让新手崩溃的报错我把一个多月里遇到的所有典型问题整理成了速查表方便参照现象可能原因解决办法请求时报Ollama is not runningOllama服务没启动执行ollama serve确认11434端口在监听前端报CORS错误跨域配置缺失在FastAPI里加CORSMiddleware允许前端端口打字机输出乱码前端没启用stream: true解码用TextDecoder.decode(value, { stream: true })回复到一半中断上下文过长、Ollama超时缩短历史轮数开启内存摘要调高超时时间角色聊着聊着变成客服系统提示词力量不足或温度过高增加few-shot温度降到0.7以下上下文太载导致内存满历史消息全部塞进request只保留最近20轮早期内容压缩CPU上跑7B太慢模型太大硬件扛不住换成3B模型或使用量化版本显卡存在但Ollama没用GPU驱动或环境变量问题执行ollama ps确认设备排查CUDA/ROCm新增角色不生效YAML格式错误用yaml.safe_load排查语法检查字段名SQLite锁报错并发写操作使用单写连接或加上timeout参数这十类问题几乎覆盖了所有新手会踩的坑具体到某几个值得展开细说。6.2 人设崩溃与记忆问题的解决过程人设崩溃是最难排查的问题因为模型不会报错只会默默跑偏。朋友一开始反馈林晚这个角色前几轮很有个性到第五轮就开始“作为一个人工智能我无法……”了。我马上意识到是助手身份压制了角色设定。解决第一步是加强系统提示词加入“你不是AI助手你是虚构角色”这类否定句。但只加这一句不够。如果模型在几轮对话后把系统提示“忘记”了那是因为上下文中后续的用户和助手消息把模型的注意力拉走了。我加了一步在每轮请求的messages里永远把系统提示放在最前面并且在历史消息中不保留太长的角色乱入内容。第二步是降低温度。典型角色扮演最容易犯的错就是给太高的temperature模型会变得天马行空越聊越不像原来那个人。我把0.9降到0.7效果立竿见影。再配合few-shot示例两三轮示范对话能把模型拉回设定轨道。第三个更隐蔽的问题是记忆丢失。朋友的角色聊到两百多轮之后角色不再记得早期设定。我一开始傻乎乎地把两百轮历史全部拼进messages导致请求体积爆炸Ollama响应也变慢。后来设计了一个朴素的记忆机制只保留最近20轮完整对话每10轮让模型把更早的内容压缩成两百字以内的摘要这个摘要会一直留在系统提示词里。角色的“长期记忆”变成了摘要“短期记忆”是原始对话这很像人的实际记忆机制。6.3 本地模型慢到不能忍先确认你是不是在跑CPU很多人第一次跑本地模型发现慢得像播PPT第一反应是“模型太差了”。其实更可能是你根本没在用GPU推理。Ollama在配置不完整时默认落到CPU模式7B模型在CPU上的速度也就是每秒几个token体验不可能好。我建议在执行对话前跑一下ollama ps看模型是否加载在GPU上。如果显示CPU先检查显卡驱动是否安装完整Ollama在NVIDIA显卡上需要CUDA环境AMD显卡需要ROCmmacOS的Metal支持一般比较省心。如果确实没有可用显卡那就老老实实换小模型。Qwen2.5 3B量化版在普通笔记本CPU上速度可以达到勉强能聊的水平体验远好过死磕7B。项目在角色配置里支持model字段随时可以把不同模型分配给不同角色这个设计在资源受限机器上很实用。6.4 最隐蔽的一个坑前端请求直接挂了但后端日志正常这个问题调试了很久。前端发POST /api/chat迟迟没有响应页面一直转圈但后端日志里完全看不到报错。我一度以为是前端异步代码写错了反复检查fetch逻辑没问题。最后发现是Vite开发服务器的代理配置问题。前端跑在5173端口后端跑在8000端口fetch(/api/chat)发出的是相对路径浏览器会把它解析到5173端口而Vite没有把/api代理到8000请求直接打到Vite dev server自己身上当然不会有回应。解决办法是在vite.config.js里配置代理export default { server: { proxy: { /api: http://localhost:8000 } } }这个坑提醒我本地开发的前后端联调一定要先确认请求实际发到了哪个端口。逻辑看着没问题不代表数据真的到了服务端。6.5 数据备份本地优先项目的“最后一根救命线”本地优先意味着数据全在你自己手里没有云端容灾所以备份比什么都重要。我写了一个简单的backup.sh把SQLite数据库文件和整个configs/characters目录打包成一个带日期的压缩包#!/usr/bin/env bash DATE$(date %Y%m%d) tar czf utopia-backup-$DATE.tar.gz backend/utopia.db configs/characters这个脚本用cron每周跑一次。人的创作内容才是最有价值的资产模型崩了可以重拉角色卡没了就全没了。7. 如果让我重新写一遍我会先做这几件事7.1 先把角色卡标准定下来再写代码第一版角色卡我写得非常随意字段想到哪加到哪后面前端、后端、数据库全部跟着角色卡的结构走。中间改过一次字段名引发了一连串重构浪费了大概三天。回头看如果一开始就花一个晚上把角色卡的所有字段、格式、示例定义清楚后面能省下大量无关痛痒的改代码时间。数据结构是软件的根根一稳上层随便长。7.2 先验证最小闭环再补好看界面我前期花了不少时间在调界面样式上按钮圆角、阴影深浅、字体间距定了好几版。结果前端调得再好看真正聊天时人设跑偏、回复断流体验一样差。靠后我才把精力全部压回核心链路curl命令通不通、消息拼得对不对、上下文有没有正确加载、流式有没有断。我犯了典型的“先粉刷后盖房”错误下个项目一定会先用命令行验证完闭环再动手做视觉。7.3 开源不等于做完Release和文档才是真正的交付这一个月最大的认知更新是代码写完只做了百分之五十剩下的百分之五十是让人能装上、能跑起来、能看懂。Utopia开源之后真正花在README、Release、CI、Issue模板上的时间不比写代码少但这些功夫直接决定了项目会不会有人愿意用。如果只让我给想做类似项目的人一个建议我会说先把“别人拿到项目后五分钟内跑起来”当成验收标准做到这一点项目才算真正完成。我和朋友现在还在给Utopia补角色卡模板最近又加了让角色主动推进对话的功能。做一个本地优先、完全自己掌控的角色扮演工具这件事对我来说最大的收获不是代码而是重新体会了一次“从头造一个顺手的工具”到底是什么感觉。
返回列表