【零基础AI应用开发】第08章:古诗词生成器(创作篇)

📦 项目教程仓库:https://github.com/ZIQI-a/AI_Agent_study 🚀 成品项目地址:https://github.com/ZIQI-a/huamiao_Agent

本章目标

要做的事:构建古诗词生成功能 ------ 输入名词生成诗词,带注释赏析

学到的知识

  • 领域特定的 Prompt 设计
  • Few-shot 示例在诗词中的应用
  • 多步骤生成(先写诗,再注释,再赏析)
  • 古风 UI 设计

8.1 诗词 Prompt 设计

创建 src/lib/prompts/poem.ts

typescript 复制代码
export interface PoemOptions {
  noun: string;
  type: string;
  dynasty: string;
}

export function getPoemSystemPrompt(options: PoemOptions) {
  const { type, dynasty } = options;

  return `你是一位精通古典文学的诗人,深谙${dynasty}代诗词的格律和意境。

## 创作要求
- 诗词类型:${type}
- 风格参考:${dynasty}代诗风
- 根据用户提供的名词或主题创作

## 格律要求
${getTypeRules(type)}

## 创作原则
1. 意境优先:诗要有画面感,让读者"看见"诗中的场景
2. 用典自然:可以用典故,但要自然不生硬
3. 情景交融:景中有情,情中有景
4. 语言凝练:每个字都要有用,不多余

## 输出格式
请严格按以下格式输出,不要添加额外内容:

[诗词标题]

[诗词正文,每句一行]

---

[注释]
- 词语1:解释
- 词语2:解释

[赏析]
(对诗词的意境、手法、情感进行赏析,80-120字)

## 约束
- 不要写现代白话诗
- 不要在诗词正文中加括号注释
- 不要使用过于生僻的字
- 诗词标题要雅致,不要直接用输入的名词`;
}

function getTypeRules(type: string): string {
  const rules: Record<string, string> = {
    五言绝句: "五言绝句,四句,每句五字。注意平仄和押韵。",
    七言绝句: "七言绝句,四句,每句七字。注意平仄和押韵。",
    五言律诗: "五言律诗,八句,每句五字。中间两联(颔联、颈联)要求对仗。",
    七言律诗: "七言律诗,八句,每句七字。中间两联(颔联、颈联)要求对仗。",
    宋词: "宋词,按词牌填写。请注明使用的词牌名,如《浣溪沙》《蝶恋花》等。",
  };
  return rules[type] || rules["七言绝句"];
}

export const POEM_TYPES = [
  { value: "五言绝句", label: "五言绝句" },
  { value: "七言绝句", label: "七言绝句" },
  { value: "五言律诗", label: "五言律诗" },
  { value: "七言律诗", label: "七言律诗" },
  { value: "宋词", label: "宋词" },
];

export const DYNASTIES = [
  { value: "唐", label: "唐诗" },
  { value: "宋", label: "宋词" },
  { value: "魏晋", label: "魏晋风骨" },
];

8.2 创建 API Route

创建 src/app/api/poems/generate/route.ts

typescript 复制代码
import { streamText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import { getPoemSystemPrompt } from "@/lib/prompts/poem";

const deepseek = createOpenAI({
  apiKey: process.env.DEEPSEEK_API_KEY,
  baseURL: "https://api.deepseek.com",
});

export async function POST(req: Request) {
  const { noun, type, dynasty } = await req.json();

  if (!noun) {
    return new Response("请输入名词", { status: 400 });
  }

  const system = getPoemSystemPrompt({
    noun,
    type: type || "七言绝句",
    dynasty: dynasty || "唐",
  });

  const result = streamText({
    model: deepseek("deepseek-chat"),
    system,
    prompt: `请以「${noun}」为主题,创作一首诗词。`,
    maxTokens: 1500,
    temperature: 0.9, // 诗词需要更多创意
  });

  return result.toDataStreamResponse();
}

8.3 构建古诗词页面

修改 src/app/poems/page.tsx

typescript 复制代码
"use client";

import { useState } from "react";
import { useCompletion } from "ai/react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { PageContainer } from "@/components/layout/page-container";
import { POEM_TYPES, DYNASTIES } from "@/lib/prompts/poem";

export default function Poems() {
  const [noun, setNoun] = useState("");
  const [type, setType] = useState("七言绝句");
  const [dynasty, setDynasty] = useState("唐");

  const { completion, isLoading, error, complete } = useCompletion({
    api: "/api/poems/generate",
  });

  const handleGenerate = async () => {
    if (!noun.trim()) return;
    await complete("", {
      body: { noun, type, dynasty },
    });
  };

  return (
    <PageContainer
      title="古诗词生成"
      description="输入名词,AI 创作古诗词"
    >
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        {/* 左侧:参数 */}
        <div className="lg:col-span-1">
          <Card>
            <CardHeader>
              <CardTitle>创作参数</CardTitle>
            </CardHeader>
            <CardContent className="space-y-4">
              <div className="space-y-2">
                <Label>主题名词</Label>
                <Input
                  value={noun}
                  onChange={(e) => setNoun(e.target.value)}
                  placeholder="如:月亮、故乡、春天..."
                  onKeyDown={(e) => e.key === "Enter" && handleGenerate()}
                />
              </div>

              <div className="space-y-2">
                <Label>诗词类型</Label>
                <Select value={type} onValueChange={setType}>
                  <SelectTrigger>
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    {POEM_TYPES.map((t) => (
                      <SelectItem key={t.value} value={t.value}>
                        {t.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>

              <div className="space-y-2">
                <Label>风格朝代</Label>
                <Select value={dynasty} onValueChange={setDynasty}>
                  <SelectTrigger>
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    {DYNASTIES.map((d) => (
                      <SelectItem key={d.value} value={d.value}>
                        {d.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>

              <Button
                onClick={handleGenerate}
                disabled={!noun.trim() || isLoading}
                className="w-full"
                size="lg"
              >
                {isLoading ? "诗意酝酿中..." : "创作诗词"}
              </Button>
            </CardContent>
          </Card>
        </div>

        {/* 右侧:诗词展示 */}
        <div className="lg:col-span-2">
          <Card className="min-h-[500px]">
            <CardHeader>
              <CardTitle>诗词预览</CardTitle>
            </CardHeader>
            <CardContent>
              {error && (
                <div className="text-red-500 mb-4">
                  生成失败:{error.message}
                </div>
              )}

              {!completion && !isLoading && (
                <div className="text-center text-muted-foreground py-20">
                  <div className="text-6xl mb-4">📜</div>
                  <p>输入一个名词,开始诗意之旅</p>
                </div>
              )}

              {isLoading && !completion && (
                <div className="text-center text-muted-foreground py-20">
                  <div className="text-6xl mb-4 animate-pulse">🌙</div>
                  <p>诗兴大发中...</p>
                </div>
              )}

              {completion && (
                <div className="poem-content">
                  <PoemRenderer content={completion} />
                  {isLoading && (
                    <span className="inline-block w-0.5 h-5 bg-primary animate-pulse" />
                  )}
                </div>
              )}
            </CardContent>
          </Card>
        </div>
      </div>
    </PageContainer>
  );
}

// 诗词专用渲染组件
function PoemRenderer({ content }: { content: string }) {
  // 按 --- 分割诗词和注释赏析
  const parts = content.split("---");
  const poemPart = parts[0]?.trim();
  const analysisPart = parts[1]?.trim();

  return (
    <div className="space-y-8">
      {/* 诗词正文 - 古风样式 */}
      <div className="text-center py-8">
        <div className="inline-block text-left">
          {poemPart?.split("\n").map((line, i) => {
            if (line.trim() === "") return <br key={i} />;
            // 标题行(不以标点结尾的短行)
            if (
              i === 0 &&
              !line.match(/[,。!?;]$/) &&
              line.trim().length < 10
            ) {
              return (
                <h3
                  key={i}
                  className="text-2xl font-bold text-primary mb-6 text-center"
                >
                  {line.trim()}
                </h3>
              );
            }
            return (
              <p
                key={i}
                className="text-xl leading-loose tracking-wider text-foreground/90"
              >
                {line.trim()}
              </p>
            );
          })}
        </div>
      </div>

      {/* 注释赏析 - 普通排版 */}
      {analysisPart && (
        <div className="border-t pt-6">
          {analysisPart.split("\n").map((line, i) => {
            if (line.trim() === "") return <br key={i} />;
            if (line.trim().startsWith("[") || line.trim().startsWith("【")) {
              return (
                <h4 key={i} className="font-semibold text-lg mt-4 mb-2">
                  {line.replace(/[\[\]【】]/g, "")}
                </h4>
              );
            }
            if (line.trim().startsWith("-")) {
              return (
                <p key={i} className="text-muted-foreground ml-4 mb-1">
                  {line}
                </p>
              );
            }
            return (
              <p key={i} className="text-foreground/80 leading-7 mb-2">
                {line}
              </p>
            );
          })}
        </div>
      )}
    </div>
  );
}

8.4 测试古诗词生成

访问 http://localhost:3000/poems

  1. 输入「月亮」,选七言绝句,唐诗风格
  2. 点击创作,观察诗词生成
  3. 尝试不同名词:故乡、梅花、编程、咖啡
  4. 切换不同诗词类型和朝代风格

本章小结

要点 说明
领域 Prompt 诗词需要格律约束和意境要求
Temperature 诗词创作用较高温度(0.9)增加创意
古风 UI 居中排版、大字号、行间距大
内容分割 --- 分隔诗词正文和注释赏析

下一章预告

目前所有数据都在内存中,刷新页面就丢失了。下一章我们引入 SQLite 数据库,让创作的文章和诗词持久化保存。


如果这个教程对你有帮助,欢迎 ⭐ Star 支持一下!