詩之前,先把小語法樹做對)
1024 想做一個輕一點的禮物輸入一小段表達(dá)式得到十四行中文短詩。這個點子可以交給 AI 擴(kuò)寫但“看起來像解析器”的代碼很容易把人帶偏。字符串里出現(xiàn)for不代表程序真的有循環(huán)數(shù)出幾個關(guān)鍵詞也得不到抽象語法樹。我更愿意先做一個很小、能解釋的核心。它只接受變量數(shù)字加法例如leaf234。輸出確實是一棵樹卻不冒充 JavaScript 編譯器。沒有函數(shù)、注釋、負(fù)數(shù)和括號遇到這些輸入就報錯而不是猜一個結(jié)果。讓結(jié)構(gòu)可檢查再談創(chuàng)意10 月 22 日Anthropic 宣布把 Agent Skills 移交給 AAIF??蓴y帶的流程說明讓創(chuàng)作工具更容易組合但說明寫得完整不等于生成代碼實現(xiàn)了它。對這個小玩具而言最便宜的檢查就是打印樹并數(shù)清輸出的非空行。下面先掃描詞元再按assignment : name number ( number)*解析。加法從左向右結(jié)合所以leaf234的右子樹是(23)4。解析完成后還要確認(rèn)沒有殘余詞元避免把leaf23 garbage的前半段當(dāng)成成功。詩句只使用樹里的變量名和數(shù)字個數(shù)做模板映射。這是確定性文本生成不調(diào)用模型不保證押韻也不宣稱符合英語商籟體的音步。中文的十四行只是這里選擇的一種排版約束。若后面接模型潤色應(yīng)該把原樹、模板和潤色結(jié)果分別保存讓讀者知道哪里發(fā)生了自由改寫。為什么寧可少支持一點邊界小失敗就容易看見。leaf23應(yīng)通過漏掉等號、加入乘號、把關(guān)鍵詞塞進(jìn)字符串都應(yīng)拒絕。生產(chǎn)環(huán)境若要處理真實 JavaScript需要完整語法解析器與資源限制不該繼續(xù)往這段玩具代碼里堆正則。將代碼保存為sonnet.js后運(yùn)行node sonnet.js。本地驗證覆蓋結(jié)構(gòu)、非法輸入和十四行數(shù)量沒有運(yùn)行瀏覽器界面或真實模型。最后的文字仍需要人讀一遍行數(shù)是程序能保證的詩意不是??瑟?dú)立運(yùn)行的核心例子function parse(source) { if (source.length 200) throw Error(輸入過長); const tokens []; const scan /\s*([A-Za-z_][A-Za-z_0-9]*|\d|[])/y; let p 0; while (p source.length) { if (!source.slice(p).trim()) break; scan.lastIndex p; const m scan.exec(source); if (!m) throw Error(不支持的位置 p); tokens.push(m[1]); p scan.lastIndex; } let i 0; const take () tokens[i]; const name take(); if (!/^[A-Za-z_]\w*$/.test(name || ) || take() ! ) throw Error(需要變量表達(dá)式); function number() { const t take(); if (!/^\d$/.test(t || )) throw Error(需要非負(fù)整數(shù)); const value Number(t); if (!Number.isSafeInteger(value)) throw Error(整數(shù)超界); return {type:Number, value}; } let value number(); while (tokens[i] ) { take(); value {type:Add, left:value, right:number()}; } if (i ! tokens.length) throw Error(多余詞元); return {type:Assign, name, value}; } function countNumbers(node) { if(node.typeNumber) return 1; if(node.typeAdd) return countNumbers(node.left)countNumbers(node.right); if(node.typeAssign) return countNumbers(node.value); throw Error(未知節(jié)點); } function poem(tree) { return [ ${tree.name} 是紙上的一枚落葉, 等號替它留下一處位置, ${countNumbers(tree)}個數(shù)字靠近尚未驚動夜色, 加號把兩盞燈接在一起, 先讀左邊的枝條, 再看右邊的新芽, 樹沒有許諾整片森林, 只解釋這一個句子, 我把能驗證的留下, 把不能保證的寫在頁邊, 窗口亮著貓沒有回答, 鍵盤停在下一行之前, 十四行到這里合上, 剩下的空白交給讀者 ]; } const tree parse(leaf234); if (tree.value.left.type ! Add || countNumbers(tree)!3 || poem(tree).length ! 14) throw Error(結(jié)構(gòu)測試失敗); for (const bad of [x2*3,x2,x 2,x2 garbage]) { let rejectedfalse; try { parse(bad); } catch { rejectedtrue; } if (!rejected) throw Error(應(yīng)當(dāng)拒絕bad); } console.log(JSON.stringify(tree)); console.log(poem(tree).join(\n));