我用华为云码道,把甲骨文交给了大模型——「汉字演变实验室」开发手记
开发方式:华为云码道(CodeArts 代码智能体)人机结对开发
技术栈:单文件 HTML/CSS/JS + Node 本地代理 + DeepSeek(deepseek-chat)+ 微软 Edge 神经语音 TTS
开源仓库:https://atomgit.com/chenge1/lab
一句话:输入任意汉字,AI 画出它从图画期到楷书的三千年演变,还能讲给孩子听。
先看一个字。
「日」。三千年前它是个圆圈,中间点一点;刻上龟甲后圆圈变硬了;小篆把它拉成长方形;到了楷书,它彻底住进方块里,中间那个点,变成了一横。
几乎每个科普视频都这么讲。但讲完之后呢?孩子记住的是一段文案,不是一场演变。
所以我想做的不是"汉字科普 App",而是一个实验室:字要在孩子眼前逐笔重演它自己的三千年——先民画它、刻刀刻它、毛笔写它。而这件事最疯狂的版本是:除了 16 个手工打磨的内置字,其他任何字都交给 DeepSeek 现场画出来。
这篇文章不写产品功能清单,只复盘这条路上真正难啃的五个技术问题,以及我和华为云码道(CodeArts 代码智能体)结对闯关的过程。
一、笔画即数据:为什么是 SVG path,而不是图片
让 AI 展示字形演变,最省事的做法是喂图:文生图模型出一套"甲骨文风格"的图片,排成时间轴就完事。但沿着这个方向论证下去,会发现它撑不起这个产品。我把两条路线的推演整理成一张表:
| 维度 | 文生图方案 | SVG path 方案(本项目采用) |
|---|---|---|
| 字形可控性 | 风格随机,"甲骨文"多半是艺术创作,教错字是原则性事故 | 坐标级精确,笔画与演变脉络可人工校订 |
| 演示形态 | 整张图片切换,像 PPT | 可逐笔描画、可点睛,像现场书写 |
| 体积成本 | 16 字 × 4 阶段 = 64 张图 | 16 个内置字约 240 行结构化数据,随 HTML 一次加载 |
| AI 扩展性 | 需文生图模型,输出不可校验 | 大模型直接生成文本,正则即可验证 |
| 离线可用 | 图片可离线,但新字不可造 | 内置字零依赖;新字仅需一次 AI 调用后永久本地化 |
结论很清晰:每个字形就是一组 SVG path 字符串。内置字的「日」是这样存的:
ri:{zi:"日",py:"rì",mean:"太阳",stages:[{d:["M60,60 m-38,0 a38,38 0 1,0 76,0 a38,38 0 1,0 -76,0"],dots:[[60,60,6]],note:"先民抬头看天:太阳就是一个圆,中间有个小黑点。"},{d:["M26,60 a34,28 0 1,0 68,0 a34,28 0 1,0 -68,0"],dots:[[60,60,5]],note:"刻在龟甲上,圆滚滚的太阳线条变硬了。"},{d:["M34,26 h52 v68 h-52 z","M46,60 h28"],note:"笔画拉得匀匀的,圆圈站直了身子。"},{kai:true,note:"横平竖直,太阳住进了方块字。"}]},看到这份结构,一个推论自然浮出水面:既然字形是结构化文本,那大模型完全可以直接生成它。这就是后面所有故事的起点——AI 不需要"画"出图片,它只需要"写"出坐标。
二、教大模型画画:一份会画字的 Prompt
难点在于,DeepSeek 没受过书法训练。你不能对它说"画个甲骨文的马"——它可能会给你一段美术课式的描述,也可能给你一串根本渲染不出来的坐标。
我的解法是把 prompt 写成一份甲方需求书,把画布约束死:
constusr=`为汉字「${zi}」设计演变学习内容,返回JSON: {"zi":"${zi}","py":"拼音带声调","mean":"2-4字本义",..., "stages":[ {"d":["SVG path"],"dots":[[60,60,5]],"note":"25字内讲解"}, {"d":["SVG path"],"note":"25字内讲解"}, {"d":["SVG path"],"note":"25字内讲解"}, {"kai":true,"note":"25字内讲解"}], ...} SVG 规则:viewBox 0 0 120 120 居中构图,stroke-width 5 圆头, 只用 M/L/Q/A/C 命令。stages 前3项依次为:图画期(像儿童简笔画)、 甲骨文(线条硬直简练)、小篆(线条圆转弯曲),要能看出同一个字逐渐演变。 dots 是朱红圆点数组,用于点睛,可省略。...`;几个被反复验证有效的细节:
- 限定命令白名单(M/L/Q/A/C):把复杂命令留给内置字,AI 只用最朴素的五个,出错率大幅下降;
- 给每个阶段定义"画风":图画期=儿童简笔画、甲骨文=硬直简练、小篆=圆转弯曲。这比"参考真实文物"有效得多——模型理解的是风格词,不是考古学;
- 要求"能看出同一个字逐渐演变":这句是灵魂。没有它,AI 经常给出四个毫不相干的图形;有了它,四个阶段之间会出现明显的继承关系——比如「山」的三个尖峰,从图画期的曲线到甲骨文的三根竖线再到楷书,主线不丢。
实测「龙」「鱼」「牛」这类复杂字,DeepSeek 一次成图的质量超出预期:笔画少而传神,恰好是儿童简笔画的气质。
三、AI 是个不可靠的画师:三道防线
指望大模型每次都返回完美 JSON?天真了。真实世界里的返回包括:markdown 代码块包着 JSON、开头带一句"好的,以下是…"、path 字符串里混进中文逗号、甚至直接缺一个阶段。
所以拿到响应之后,还有一整套"验画师"流程:
constm=txt.match(/\{[\s\S]*\}/);// ① 从杂质里抠出 JSONconstj=JSON.parse(m[0]);[0,1,2].forEach(i=>{if(!validStage(j.stages[i]))// ② 逐阶段验图形thrownewError('第'+(i+1)+'阶段图形无效,请重试')});functionvalidStage(s){returns&&Array.isArray(s.d)&&s.d.length>0&&s.d.every(p=>typeofp==='string'&&/^[-MZmLlHhVvCcSsQqTtAa0-9,.\s]+$/.test(p)&&// 只许合法字符/[Mm]/.test(p[0]));// 必须以 M 开头(先落笔)}这个正则验证器看似简单,实际拦下了绝大多数畸形输出。最妙的是那条/[Mm]/检查——SVG path 必须从 M(moveTo)开始,一个从没落笔的 path 是画不出任何东西的。
第三道防线在故事层:内置 16 个字每个都带全套手写故事(fb字段),AI 生故事失败时静默降级:
catch(e){toast('AI 生成失败,已用内置故事:'+(e.message||''));fillStory({...g.fb,src:'内置故事(AI 降级)'});}对用户来说,故事永远不缺席——这和功能多不多无关,这是可用性的底线。
顺带一提一个隐蔽的坑:项目早期接的是华为云 MaaS,后来切到 DeepSeek 官方 API。已存了旧配置的用户怎么办?我在启动时做了一次静默迁移:
if(/modelarts|deepseek-v4/.test(cfgLLM.base+' '+cfgLLM.model)){cfgLLM.base='https://api.deepseek.com';cfgLLM.model='deepseek-chat';store.set('hl_llm',cfgLLM)}API Key 原样保留,用户无感切换。凡是动过配置结构的迭代,都欠用户一次自动迁移——这是这次学到的。
四、让字自己写自己:描画动画的秘密
字形有了,下一个问题是怎么呈现。直接切换?那就是 PPT。这个项目的核心体验——“每个字都是一幅画了几千年的画”——要求字要被画出来。
浏览器其实自带这个能力:stroke-dashoffset。原理一句话:把虚线偏移量设为路径总长,描边就"消失"了;再把偏移量动画回 0,描边就会从起点一路"生长"到终点。
functionanimateGlyph(el){el.querySelectorAll('svg path').forEach((p,i)=>{constL=p.getTotalLength();p.style.strokeDasharray=L+' '+L;p.style.strokeDashoffset=L;p.animate([{strokeDashoffset:L},{strokeDashoffset:0}],{duration:Math.min(1100,300+L*2.2),// 路径越长画得越久,封顶 1.1sdelay:120+i*140,// 笔画依次落笔easing:'cubic-bezier(.45,.05,.55,.95)',fill:'both'});});el.querySelectorAll('svg circle').forEach((c,i)=>{// 朱红点睛,弹出来c.animate([{opacity:0,transform:'scale(0)'},{opacity:1,transform:'scale(1)'}],{duration:340,delay:480+i*120,easing:'cubic-bezier(.34,1.56,.64,1)',fill:'both'});});}三个调了很久的参数:
300+L*2.2:短笔画快、长笔画慢,模拟真实的书写节奏;封顶 1100ms 防止一根超长曲线拖垮整体节奏;delay:120+i*140:笔画之间留 140ms 空隙——这是"运笔"的时间,没有它笔画会同时涌出,像打印机而不是毛笔;- 圆点用回弹缓动:
cubic-bezier(.34,1.56,.64,1)的 overshoot 让"点睛"有落章的顿感。
内置字的 stage 数据里藏着一个小伏笔:dots数组专门用来表示"朱砂点睛"——太阳的黑点、马的眼睛、羊的眼珠。描画动画里它们最后弹出,恰好复现了"画龙点睛"这个成语的动作本身。
五、声音追着笔画跑:朗读与舞台的时序同步
光有画面还不够。这个 App 的正确打开方式是孩子点「听演变故事」,然后朗读讲到哪个朝代,舞台就演到哪个朝代。
实现上就是一个 token 校验的顺序循环:
asyncfunctionspeakAll(){stopSpeak();stopAuto();constmyToken=speakToken;for(leti=0;i<paras.length;i++){if(speakToken!==myToken)break;// 用户随时打断,后续全部作废paras.forEach(p=>p.classList.toggle('reading',p===paras[i]));showStage(i);// 舞台切到第 i 朝代awaitspeak(txt);// 等这段读完}}speakToken是整个音频系统的"改朝换代"计数器:任何打断(切字、暂停、点段落单读)都会speakToken++,循环里的旧 token 立刻失效。用整数 token 做异步竞态的守门员,比任何标志位都可靠。
朗读本身是三层容错:Edge 神经语音(8 音色)→ 系统speechSynthesis→ 静默。有个容易被忽略的细节:speechSynthesis.cancel()在部分浏览器上会触发上一个 utterance 的onerror,如果那里挂了 resolve 之外的逻辑就会出现幽灵回调。把onend/onerror都只指向res(),是最省心的防诈尸写法。
六、实测数据:这个"实验室"到底跑多快
光说不练假把式。以下为本机(Windows 11,node tts-proxy.js,浏览器访问http://localhost:8788/)的真实实测。
① 页面加载(单文件 App,含全部 16 个内置字数据与样式,连续 3 次):
| 次数 | 耗时 | 传输体积 |
|---|---|---|
| 第 1 次 | 0.218s | 63.4 KB |
| 第 2 次 | 0.209s | 63.4 KB |
| 第 3 次 | 0.210s | 63.4 KB |
平均0.21s首屏可用——单文件零构建架构的收益直接体现在这:无需打包器、无需请求合并,一个 HTTP 往返带走整个应用。
② 神经语音合成延迟(文本"太阳就是一个圆,中间有个小黑点",16 字,音色"晓晓",语速 85%,连续 3 次):
| 次数 | 耗时 | 产出音频 |
|---|---|---|
| 第 1 次 | 2.23s | 21,024 字节 MP3 |
| 第 2 次 | 2.13s | 21,024 字节 MP3 |
| 第 3 次 | 2.68s | 21,024 字节 MP3 |
平均2.35s拿到完整神经语音。实际体验中按段落合成、读完一段再取下一段,等待被朗读节奏消化,孩子无感知。合成失败时自动回退系统语音,故事流程不中断。
③ AI 造字链路:输入一个新汉字,DeepSeek 一次性返回拼音、本义、三阶段 SVG、故事与测验的完整 JSON(max_tokens 1800),前端校验通过即永久写入 localStorage——同一个字第二次打开是零网络依赖的,这也是"AI 生成一次、本地受用永远"架构的核心收益。
七、十六个字到无限:把造字权交出去
内置 16 个象形字是手工打磨的,质量稳定,但字库永远是死的。真正的分水岭是第二节那套"AI 画字" prompt 成熟之后——「创造新字」功能几乎是白送的:
开启 AI 后,识字页顶部出现一张卡片,输入任意汉字,DeepSeek 一次性吐出全套数据。前端校验通过后,这个字就永久住进 localStorage 的自定义字库,和内置字平起平坐——有描画动画、有朗读、答对测验照样盖"记住了"的朱砂大印。
constG=k=>GLYPHS[k]||CUSTOM[k];// 内置与 AI 字同权constallGlyphs=()=>({...GLYPHS,...CUSTOM});代码层面只需要一个统一的取字函数,UI 层完全不用区分字的出身。先设计好数据结构,功能就只是往里灌数据——这条朴素规律在这类项目里反复应验。
唯一加的专属逻辑是删除:AI 造的字在实验室右上角有个 ✕,删掉时同步清理学习记录。孩子造了一堆奇怪的字?没问题,造字的快乐本来就包含拆掉重来。
八、和华为云码道结对:模糊需求怎么变成可调参数
最后必须说说这个项目的开发方式:它不是我一个人闷头写的,也不是 AI 一键生成的,而是**"我提需求 + 码道写码 + 我验收"的结对模式**。两个印象最深的片段:
- “写字要有运笔感”——这种模糊反馈最难传给机器。码道把它翻译成了第四节那组公式:路径时长
300+L*2.2、笔间空隙i*140、点睛回弹曲线。我试听两轮、各微调一次就收敛了。人负责"舒服不舒服"的判断,机器负责把"舒服"参数化,各干各的擅长事。 - 一次改动解决三个问题——中途要从 MaaS 切到 DeepSeek,我以为要大改:结果码道给出的方案是前端默认值 + 启动时正则匹配旧配置静默迁移 + 代理端兼容两套环境变量,一次提交同时搞定了切换、老用户兼容、密钥安全。
这轮结对验证了我对 AI 辅助编程的一个判断:正确的用法不是让 AI 写完所有代码,而是人守住产品判断和验收,把实现密度交给它。SVG prompt 的多轮调优尤其如此——我负责说"这批画得不像小孩画的",它负责改需求书里的措辞。
九、美学不是装饰
再记两处非功能的细节,这是整个 App 气质的来源。方向是"宣纸水墨":暖米色纸面、衬线宋体、朱砂红印章。
纸的质感用一段 SVG 噪声内联进 CSS,零图片开销:
--paperbg:var(--paper)url("data:image/svg+xml,%3Csvg ... %3E %3CfeTurbulence type='fractalNoise'baseFrequency='.72'.../%3E %3CfeColorMatrix values='0 0 0 0 .28 ... 0 0 0 .05 0'/%3E ...");feColorMatrix把噪声的 alpha 压到 0.05——质感的第一要义是克制,噪点能被感觉到、但不能被看见。
印章的仪式感:测验答对后,一枚双线圆章"记住了"带旋转砸下来(rotate(-12deg) scale(2.2)→ 回弹到 1),配合触觉般的动画曲线,这是整个 App 里孩子最期待的 0.55 秒。传统文化 App 的奖励,不应该是星星和糖果,应该是盖章。