从零搭建基于DeepSeek的大模型智能体实战:英语诗歌创作智能体——八种体裁随心写,生词本一键导出
本文是"从零搭建大模型智能体实战"系列的第二篇。在上一篇中,我们构建了通用的 AI 对话窗体程序,掌握了智能体的核心架构与 LLM 客户端封装。本篇将在此基础上,打造一个专精英语诗歌创作的智能体——支持俳句、十四行诗、自由诗等八种体裁,流式诗歌创作 + 右侧生词本手动记录,一键导出 TXT 格式生词表,配合金山词霸等工具轻松背词。使用 AIGC bar 统一 API 接口,免费模型即可流畅运行。
目录
博主智算菩萨,专注于人工智能、Python编程、音视频处理及UI窗体程序设计等方向。致力于以通俗易懂的方式拆解前沿技术,从零基础入门到高阶实战,陪伴开发者共同成长。目前已开设五大技术专栏,累计发布多篇原创技术文章,深受读者好评。
📌 专栏导航
- 人工智能前沿知识(已更201篇):深度剖析Transformer架构、生成式AI、强化学习、具身智能、神经符号系统、大模型及智能体(Agent)技术,系统性解析AI核心技术体系与前沿趋势。
- Python基础小白编程(已更232篇):从零开始,以保姆式教程讲解变量、数据类型、流程控制、函数等核心语法,配有大量实战代码与避坑指南,真正做到学以致用。
- 机器学习与深度学习(125篇):系统化拆解线性模型、决策树、随机森林、梯度提升树、神经网络等算法原理与工程实践,覆盖从公式推导到代码实现的全链路内容。
- 音频、图像与视频处理理论与实战(81篇):涵盖FFmpeg多媒体处理、audio_shop开源工具、ComfyUI-WanVideoWrapper视频生成等实用技术,从基础操作到高级应用一应俱全。
- UI窗体程序设计实战(78篇):深入讲解UI设计、动态窗体生成、游戏UI框架设计等实战技巧,提供从配置到编码的完整解决方案。
智算菩萨,以代码为经,以算法为纬,在人工智能的星辰大海中,做你前行路上最可靠的导航者。本人最常用AI工具为AIGCBAR。
1 为什么需要一个诗歌创作智能体
英语诗歌学习是英语进阶过程中极具价值但门槛较高的一环。传统学习方式面临几个痛点:
- 体裁知识难以系统获取——俳句的 5-7-5 音节规则、十四行诗的押韵格式、维拉内拉的叠句结构,每种体裁都有严格的形式要求,初学者很难快速掌握。
- 创作缺少即时反馈——写了诗没人点评,不知道韵律是否正确、意象是否恰当。
- 生词学习脱离语境——背单词表枯燥乏味,而在诗歌中学到的词汇既有文化内涵又有情感温度,记忆效果远胜于机械背诵。
本篇构建的英语诗歌创作智能体正是为了解决这些问题而生:你只需选择体裁、输入主题,AI 即刻生成符合格律的英语诗歌;右侧生词本手动记录诗中的生词,一键导出 TXT 文件,配合金山词霸等工具自主查词学习。

2 项目架构与设计思路
2.1 与通用聊天智能体的区别
| 维度 | 通用聊天智能体 | 诗歌创作智能体 |
|---|---|---|
| 系统提示词 | 用户自定义,无默认约束 | 固定为英语诗歌创作专家,体裁可选 |
| 输入方式 | 任意自然语言 | 体裁选择器 + 主题输入 |
| 回复格式 | 自由文本 | 严格按体裁格律输出诗歌 |
| 领域功能 | 无 | 生词本手动记录 + TXT 导出 + 词汇难度分级 |
| UI 布局 | 单栏对话 | 左对话 + 右生词本双栏 |
| 词汇学习 | 无 | 手动记录 + 难度提示,导出后用外部工具查词 |
2.2 核心功能模块
PoetryAgent
├── 配置管理模块 (load_config / save_config)
│ ├── API 密钥持久化
│ └── 体裁偏好记忆
├── 诗歌创作模块
│ ├── 体裁选择器(8种体裁)
│ ├── 主题输入
│ ├── 流式诗歌生成
│ └── 诗歌格式渲染
└── 生词本模块
├── 手动输入区域(每行一个单词)
├── 导出 TXT(filedialog 选择保存路径)
└── 清空生词本
2.3 强制输出英语
为杜绝 AI 输出中文诗,user prompt 末尾加上 no Chinese 强约束:
{"role": "user", "content": f"Please compose a {genre.lower()} poem about {topic}. "
f"Write only the poem in English, no explanations, no Chinese."}
同时体裁栏增加了词汇难度选择器(CET-4/6、TEM-4/8),以提示用户当前产出诗歌的用词级别,方便在使用金山词霸等工具时有目标地学习。
3 诗歌体裁系统设计
3.1 八种英语诗歌体裁详解
俳句 (Haiku):源自日本,英语俳句遵循 5-7-5 音节模式,捕捉自然或日常生活中的一个瞬间。
十四行诗 (Sonnet):莎士比亚式十四行诗,14 行,押韵格式 ABABCDCDEFEFGG,主题多为爱情、自然或人生。
自由诗 (Free Verse):没有严格的韵律和格律约束,依靠自然语言节奏和意象传达情感。
打油诗 (Limerick):五行诗,押韵格式 AABBA,内容幽默风趣。
民谣 (Ballad):叙事诗,交替押韵(ABAB 或 ABCB),有重复的叠句。
维拉内拉 (Villanelle):19 行诗,由 5 个三行节和 1 个四行节组成,ABA 押韵格式。
藏头诗 (Acrostic):每行首字母组合成一个单词或信息。
五行诗 (Cinquain):五行诗,音节数为 2-4-6-8-2。
3.2 体裁提示词工程
每种体裁对应一个系统提示词,存储在 POETRY_GENRES 字典中。以十四行诗为例:
"Sonnet": {
"name": "十四行诗 (Sonnet)",
"prompt": "You are a master of the English sonnet. Compose a 14-line Shakespearean sonnet with the rhyme scheme ABABCDCDEFEFGG, exploring themes of love, nature, or human experience.",
}
提示词设计的关键要素:
- 角色设定:告诉 LLM 它是"大师"级别的诗人
- 形式约束:明确说明格律要求(行数、押韵格式、音节数)
- 主题引导:给出合理的主题范围
4 生词本与导出功能
右侧生词本是一个简洁实用的小工具,采用纯手动记录 + 一键导出的设计。
4.1 手动记录生词
生词本的设计理念是:AI 负责创作诗歌,用户负责发现生词。阅读诗歌时遇到不认识的单词,随手输入到右侧生词本中,每行一个。这种方式的优势是:
- 100% 准确:用户自己发现的生词,不会出现 AI 提取错误的情况
- 自主选择:每个学习者词汇量不同,自己决定哪些词值得记录
- 配合金山词霸:导出后用金山词霸等工具查询释义,学习效果更扎实
4.2 导出 TXT 纯文本词表
导出功能使用标准 filedialog 选择保存路径,使用 os 库写入纯文本文件:
def _export_vocab(self):
content = self.vocab_input.get("1.0", tk.END).strip()
if not content:
messagebox.showinfo("提示", "生词本是空的。")
return
# 默认文件名:vocab_YYYYMMDD_HHMMSS.txt
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
default_name = f"vocab_{timestamp}.txt"
file_path = filedialog.asksaveasfilename(
title="导出生词本",
defaultextension=".txt",
initialfile=default_name,
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")],
)
if not file_path:
return
# 清理空行,每行一个单词写出
lines = content.split("\n")
cleaned = [line.strip() for line in lines if line.strip()]
with open(file_path, "w", encoding="utf-8") as f:
for word in cleaned:
f.write(word + "\n")
导出的 TXT 文件格式示例(一行一个单词,无编号无多余内容):
melancholy
ethereal
luminous
tranquil
whisper
这种纯文本格式兼容性最好——金山词霸、欧路词典、Anki 等工具均可直接导入。
5 完整代码
以下是完整的 poetry_agent.py 单文件代码(约 685 行),复制保存即可直接运行:
"""
英语诗歌创作智能体 - 桌面窗体对话程序
支持多种诗歌体裁创作、生词本手动记录与导出
"""
import os
import sys
import json
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext, filedialog
from threading import Thread
from datetime import datetime
# ── 配置管理 ─────────────────────────────────────────────
CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".poetry_agent")
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
DEFAULT_CONFIG = {
"api_key": "",
"model": "deepseek-v4-flash",
"genre": "Free Verse",
"temperature": 0.8,
"max_tokens": 8000,
"top_p": 1.0,
"presence_penalty": 0.3,
"frequency_penalty": 0.1,
"difficulty": "CET-6",
}
# ── 词汇难度定义 ─────────────────────────────────────────
DIFFICULTY_LEVELS = {
"CET-4": "大学英语四级",
"CET-6": "大学英语六级",
"TEM-4": "英语专业四级",
"TEM-8": "英语专业八级",
}
# ── 诗歌体裁定义 ─────────────────────────────────────────
POETRY_GENRES = {
"Haiku": {
"name": "俳句 (Haiku)",
"prompt": "You are an expert English haiku poet. Compose a traditional haiku with a 5-7-5 syllable pattern, capturing a single moment in nature or daily life with vivid imagery.",
},
"Sonnet": {
"name": "十四行诗 (Sonnet)",
"prompt": "You are a master of the English sonnet. Compose a 14-line Shakespearean sonnet with the rhyme scheme ABABCDCDEFEFGG, exploring themes of love, nature, or human experience.",
},
"Free Verse": {
"name": "自由诗 (Free Verse)",
"prompt": "You are an accomplished free verse poet. Compose a free verse poem without strict meter or rhyme, letting imagery and natural speech rhythms carry the emotional weight.",
},
"Limerick": {
"name": "打油诗 (Limerick)",
"prompt": "You are a witty limerick writer. Compose a five-line limerick with the AABBA rhyme scheme, humorous subject matter, and the characteristic anapestic meter.",
},
"Ballad": {
"name": "民谣 (Ballad)",
"prompt": "You are a skilled ballad poet. Compose a narrative ballad with alternating rhyme (ABAB or ABCB), strong storytelling elements, and a repeating refrain.",
},
"Villanelle": {
"name": "维拉内拉 (Villanelle)",
"prompt": "You are a master of the villanelle form. Compose a 19-line villanelle with two alternating refrains following the ABA rhyme scheme, expressing deep emotion.",
},
"Acrostic": {
"name": "藏头诗 (Acrostic)",
"prompt": "You are a creative acrostic poet. Compose an acrostic poem where the first letter of each line spells out a word or message related to the given theme.",
},
"Cinquain": {
"name": "五行诗 (Cinquain)",
"prompt": "You are a concise cinquain poet. Compose a five-line poem following the 2-4-6-8-2 syllable pattern, capturing a single image or emotion.",
},
}
def load_config():
"""从本地文件加载配置"""
if not os.path.exists(CONFIG_FILE):
return dict(DEFAULT_CONFIG)
try:
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
cfg = dict(DEFAULT_CONFIG)
cfg.update(data)
return cfg
except Exception:
return dict(DEFAULT_CONFIG)
def save_config(config: dict):
"""保存配置到本地文件"""
os.makedirs(CONFIG_DIR, exist_ok=True)
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
# ── LLM 客户端 ──────────────────────────────────────────
from openai import OpenAI
class LLMClient:
"""统一的 LLM API 客户端"""
BASE_URL = "https://api.aigc.bar/v1"
MODEL_CONFIG = {
"glm-5.2": {
"name": "GLM-5.2",
"desc": "智谱 GLM 系列,付费模型,智能体能力强",
"paid": True,
"group": "OpenSource-MultiModal",
},
"deepseek-v4-flash": {
"name": "DeepSeek-V4-Flash",
"desc": "DeepSeek 系列,免费模型,速度快",
"paid": False,
"group": "OpenSource-MultiModal",
},
}
def __init__(self, model="deepseek-v4-flash", api_key=None, base_url=None):
if model not in self.MODEL_CONFIG:
raise ValueError(f"未知模型: {model},可选: {list(self.MODEL_CONFIG.keys())}")
self.model = model
self.api_key = api_key or os.environ.get("AIGC_API_KEY", "")
self.base_url = base_url or self.BASE_URL
self._client = None
def _get_client(self):
if self._client is None:
if not self.api_key:
raise ValueError("API 密钥未设置,请在界面中输入或设置环境变量 AIGC_API_KEY")
self._client = OpenAI(api_key=self.api_key, base_url=self.base_url)
return self._client
def chat_stream(self, messages, on_chunk, **kwargs):
client = self._get_client()
response = client.chat.completions.create(
model=self.model,
messages=messages,
stream=True,
stream_options={"include_usage": True},
**kwargs,
)
for chunk in response:
if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.content:
on_chunk(chunk.choices[0].delta.content)
class PoetryAgent:
"""英语诗歌创作智能体主窗口"""
def __init__(self):
self.root = tk.Tk()
self.root.title("英语诗歌创作智能体 - Poetry Agent")
self.root.geometry("1200x780")
self.root.minsize(1000, 600)
# 加载配置
self.config = load_config()
# 对话历史
self.messages = []
# 设置栏折叠状态
self.settings_visible = tk.BooleanVar(value=False)
self._build_ui()
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _build_ui(self):
"""构建界面"""
# ========== 顶部:API 密钥栏 ==========
api_frame = ttk.Frame(self.root, padding=(10, 8))
api_frame.pack(fill=tk.X)
ttk.Label(api_frame, text="API密钥:", font=("微软雅黑", 9)).pack(side=tk.LEFT)
self.api_entry = ttk.Entry(api_frame, width=40, show="*")
self.api_entry.pack(side=tk.LEFT, padx=(5, 0), fill=tk.X, expand=True)
if self.config.get("api_key"):
self.api_entry.insert(0, self.config["api_key"])
ttk.Button(
api_frame, text="获取API密钥",
command=self._open_register_url,
).pack(side=tk.LEFT, padx=(5, 0))
ttk.Button(
api_frame, text="保存配置",
command=self._save_config,
).pack(side=tk.LEFT, padx=(5, 0))
ttk.Button(
api_frame, text="展开设置", width=10,
command=self._toggle_settings,
).pack(side=tk.RIGHT, padx=(5, 0))
# ========== 可折叠的设置栏 ==========
self.settings_frame = ttk.LabelFrame(
self.root, text="参数设置", padding=(10, 5)
)
row1 = ttk.Frame(self.settings_frame)
row1.pack(fill=tk.X, pady=2)
ttk.Label(row1, text="模型:", font=("微软雅黑", 9)).pack(side=tk.LEFT)
self.model_var = tk.StringVar(value=self.config["model"])
self.model_combo = ttk.Combobox(
row1, textvariable=self.model_var,
values=list(LLMClient.MODEL_CONFIG.keys()),
state="readonly", width=22,
)
self.model_combo.pack(side=tk.LEFT, padx=(5, 15))
self.model_combo.bind("<<ComboboxSelected>>", lambda e: self._update_model_info())
self.model_info_label = ttk.Label(
row1, text="", font=("微软雅黑", 8), foreground="gray"
)
self.model_info_label.pack(side=tk.LEFT)
self._update_model_info()
row2 = ttk.Frame(self.settings_frame)
row2.pack(fill=tk.X, pady=2)
ttk.Label(row2, text="温度:", font=("微软雅黑", 9)).pack(side=tk.LEFT)
self.temp_var = tk.DoubleVar(value=self.config["temperature"])
ttk.Scale(
row2, from_=0.0, to=2.0, variable=self.temp_var,
orient=tk.HORIZONTAL, length=100,
).pack(side=tk.LEFT, padx=(5, 5))
self.temp_label = ttk.Label(
row2, text=f"{self.temp_var.get():.1f}", width=4
)
self.temp_label.pack(side=tk.LEFT)
ttk.Label(row2, text=" 最大Token:", font=("微软雅黑", 9)).pack(
side=tk.LEFT, padx=(15, 0)
)
self.max_tokens_var = tk.IntVar(value=self.config["max_tokens"])
ttk.Spinbox(
row2, from_=100, to=128000, increment=100,
textvariable=self.max_tokens_var, width=8,
).pack(side=tk.LEFT, padx=(5, 0))
row3 = ttk.Frame(self.settings_frame)
row3.pack(fill=tk.X, pady=2)
ttk.Label(row3, text="Top-P:", font=("微软雅黑", 9)).pack(side=tk.LEFT)
self.top_p_var = tk.DoubleVar(value=self.config["top_p"])
ttk.Scale(
row3, from_=0.0, to=1.0, variable=self.top_p_var,
orient=tk.HORIZONTAL, length=80,
).pack(side=tk.LEFT, padx=(5, 5))
ttk.Label(row3, text=" 存在惩罚:", font=("微软雅黑", 9)).pack(
side=tk.LEFT, padx=(15, 0)
)
self.presence_var = tk.DoubleVar(value=self.config["presence_penalty"])
ttk.Scale(
row3, from_=-2.0, to=2.0, variable=self.presence_var,
orient=tk.HORIZONTAL, length=80,
).pack(side=tk.LEFT, padx=(5, 5))
ttk.Label(row3, text=" 频率惩罚:", font=("微软雅黑", 9)).pack(
side=tk.LEFT, padx=(15, 0)
)
self.freq_var = tk.DoubleVar(value=self.config["frequency_penalty"])
ttk.Scale(
row3, from_=-2.0, to=2.0, variable=self.freq_var,
orient=tk.HORIZONTAL, length=80,
).pack(side=tk.LEFT, padx=(5, 5))
# ========== 诗歌体裁选择栏 ==========
genre_frame = ttk.Frame(self.root, padding=(10, 0, 10, 5))
genre_frame.pack(fill=tk.X)
ttk.Label(genre_frame, text="诗歌体裁:", font=("微软雅黑", 9)).pack(side=tk.LEFT)
self.genre_var = tk.StringVar(value=self.config["genre"])
genre_keys = list(POETRY_GENRES.keys())
self.genre_combo = ttk.Combobox(
genre_frame, textvariable=self.genre_var,
values=genre_keys, state="readonly", width=20,
)
self.genre_combo.pack(side=tk.LEFT, padx=(5, 0))
self.genre_display_var = tk.StringVar(
value=POETRY_GENRES.get(self.config["genre"], {}).get("name", "自由诗")
)
self.genre_name_label = ttk.Label(
genre_frame, textvariable=self.genre_display_var,
font=("微软雅黑", 9), foreground="gray",
)
self.genre_name_label.pack(side=tk.LEFT, padx=(5, 10))
self.genre_combo.bind("<<ComboboxSelected>>", self._on_genre_change)
ttk.Label(genre_frame, text="词汇难度:", font=("微软雅黑", 9)).pack(side=tk.LEFT, padx=(10, 0))
self.difficulty_var = tk.StringVar(value=self.config.get("difficulty", "CET-6"))
self.difficulty_combo = ttk.Combobox(
genre_frame, textvariable=self.difficulty_var,
values=list(DIFFICULTY_LEVELS.keys()),
state="readonly", width=8,
)
self.difficulty_combo.pack(side=tk.LEFT, padx=(3, 0))
self.difficulty_desc_label = ttk.Label(
genre_frame, text=DIFFICULTY_LEVELS.get(self.config.get("difficulty", "CET-6"), ""),
font=("微软雅黑", 8), foreground="gray",
)
self.difficulty_desc_label.pack(side=tk.LEFT, padx=(3, 0))
self.difficulty_var.trace_add("write", self._on_difficulty_change)
ttk.Label(genre_frame, text="主题:", font=("微软雅汉", 9)).pack(side=tk.LEFT, padx=(10, 0))
self.topic_entry = ttk.Entry(genre_frame, width=30)
self.topic_entry.pack(side=tk.LEFT, padx=(5, 0), fill=tk.X, expand=True)
self.topic_entry.insert(0, "nature")
ttk.Button(
genre_frame, text="创作诗歌", command=self._send_message,
).pack(side=tk.RIGHT, padx=(5, 0))
# ========== 主内容区(左侧对话 + 右侧生词本) ==========
main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
main_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 5))
# ── 左侧:对话区域 ──
left_frame = ttk.Frame(main_paned)
main_paned.add(left_frame, weight=3)
self.chat_display = scrolledtext.ScrolledText(
left_frame, wrap=tk.WORD, font=("微软雅黑", 10),
bg="#f8f9fa", relief=tk.FLAT, borderwidth=1,
)
self.chat_display.pack(fill=tk.BOTH, expand=True)
self.chat_display.tag_config(
"user", foreground="#1a73e8", font=("微软雅黑", 10, "bold")
)
self.chat_display.tag_config(
"assistant", foreground="#8B4513", font=("微软雅黑", 10, "bold")
)
self.chat_display.tag_config(
"system_msg", foreground="#999999", font=("微软雅黑", 9)
)
self.chat_display.tag_config(
"error", foreground="#d93025", font=("微软雅黑", 10)
)
self.chat_display.tag_config("content", font=("微软雅黑", 10))
self.chat_display.tag_config(
"poem", foreground="#4A148C", font=("Georgia", 11, "italic")
)
self.chat_display.config(state=tk.DISABLED)
# ── 右侧:生词本(手动记录) ──
right_frame = ttk.LabelFrame(main_paned, text="生词本", padding=(8, 5))
main_paned.add(right_frame, weight=1)
# 提示文字
ttk.Label(
right_frame,
text="手动输入生词,每行一个单词:",
font=("微软雅黑", 8),
).pack(anchor=tk.W, pady=(0, 3))
# 生词输入区域
self.vocab_input = scrolledtext.ScrolledText(
right_frame, wrap=tk.WORD, font=("Consolas", 10),
bg="#FFFDE7", relief=tk.FLAT, borderwidth=1,
height=18,
)
self.vocab_input.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
# 提示示例
self.vocab_input.insert("1.0", "melancholy\nethereal\nluminous\ntranquil\nwhisper")
# 按钮行
btn_row = ttk.Frame(right_frame)
btn_row.pack(fill=tk.X)
ttk.Button(
btn_row, text="导出 TXT",
command=self._export_vocab,
).pack(side=tk.LEFT, padx=(0, 3), fill=tk.X, expand=True)
ttk.Button(
btn_row, text="清空",
command=self._clear_vocab,
).pack(side=tk.RIGHT, fill=tk.X, expand=True)
# ========== 底部输入区域 ==========
input_frame = ttk.Frame(self.root, padding=(10, 0, 10, 10))
input_frame.pack(fill=tk.X)
self.input_text = scrolledtext.ScrolledText(
input_frame, height=3, wrap=tk.WORD,
font=("微软雅黑", 10), relief=tk.FLAT, borderwidth=1,
)
self.input_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 8))
self.input_text.insert("1.0", "请创作一首关于...的诗歌...")
self.send_btn = ttk.Button(
input_frame, text="创作诗歌", width=12, command=self._send_message
)
self.send_btn.pack(side=tk.RIGHT, expand=True)
self.input_text.bind("<Control-Return>", lambda e: self._send_message())
# ========== 底部状态栏 ==========
self.status_bar = ttk.Label(
self.root, text='就绪 | 选择体裁并输入主题,点击"创作诗歌"', relief=tk.SUNKEN,
anchor=tk.W, font=("微软雅黑", 9),
)
self.status_bar.pack(fill=tk.X, side=tk.BOTTOM)
# 显示欢迎信息
self._show_welcome()
def _show_welcome(self):
"""显示欢迎信息"""
self._append_text("=" * 60 + "\n", "system_msg")
self._append_text(" 英语诗歌创作智能体 - Poetry Agent\n", "assistant")
self._append_text("=" * 60 + "\n", "system_msg")
self._append_text(
"欢迎使用英语诗歌创作智能体!\n\n"
"使用方法:\n"
"1. 在上方选择诗歌体裁(俳句、十四行诗、自由诗等)\n"
"2. 输入诗歌主题(如 nature, love, seasons)\n"
"3. 点击「创作诗歌」按钮即可生成\n"
"4. 在右侧「生词本」手动记录诗中的生词\n"
"5. 点击「导出 TXT」保存为纯文本文件\n\n"
"支持的体裁:\n",
"content",
)
for key in POETRY_GENRES:
self._append_text(f" - {POETRY_GENRES[key]['name']}\n", "content")
self._append_text("-" * 60 + "\n", "system_msg")
# ── 界面方法 ──
def _on_genre_change(self, event=None):
"""体裁选择变更"""
genre = self.genre_var.get()
info = POETRY_GENRES.get(genre, {})
self.genre_display_var.set(info.get("name", ""))
def _on_difficulty_change(self, *args):
"""难度选择变更"""
diff = self.difficulty_var.get()
desc = DIFFICULTY_LEVELS.get(diff, "")
self.difficulty_desc_label.config(text=desc)
def _toggle_settings(self):
"""切换设置栏显示状态"""
if self.settings_visible.get():
self.settings_frame.pack_forget()
self.settings_visible.set(False)
else:
self.settings_frame.pack(
fill=tk.X, padx=10, pady=(0, 5)
)
self.settings_visible.set(True)
def _update_model_info(self):
model_id = self.model_var.get()
cfg = LLMClient.MODEL_CONFIG.get(model_id)
if cfg:
paid = "付费" if cfg["paid"] else "免费"
self.model_info_label.config(text=f"{cfg['name']} | {paid}")
def _open_register_url(self):
"""打开 AIGC bar 注册页面"""
import webbrowser
webbrowser.open("https://api.aigc.bar/register?aff=UP4F")
messagebox.showinfo(
"获取API密钥",
"已打开注册页面。注册后在控制台获取 API 密钥。",
)
def _save_config(self):
"""保存所有配置到本地"""
api_key = self.api_entry.get().strip()
if not api_key:
messagebox.showwarning("提示", "请输入 API 密钥")
return
self.config["api_key"] = api_key
self.config["model"] = self.model_var.get()
self.config["genre"] = self.genre_var.get()
self.config["difficulty"] = self.difficulty_var.get()
self.config["temperature"] = round(self.temp_var.get(), 1)
self.config["max_tokens"] = self.max_tokens_var.get()
self.config["top_p"] = round(self.top_p_var.get(), 1)
self.config["presence_penalty"] = round(self.presence_var.get(), 1)
self.config["frequency_penalty"] = round(self.freq_var.get(), 1)
save_config(self.config)
messagebox.showinfo("成功", "配置已保存,下次启动自动加载。")
def _clear_chat(self):
"""清空对话历史"""
self.messages = []
self.chat_display.config(state=tk.NORMAL)
self.chat_display.delete("1.0", tk.END)
self.chat_display.config(state=tk.DISABLED)
self._show_welcome()
self.status_bar.config(text="对话已清空")
def _append_text(self, text: str, tag: str = "content"):
"""在对话区域追加文本(线程安全)"""
self.chat_display.config(state=tk.NORMAL)
self.chat_display.insert(tk.END, text, tag)
self.chat_display.see(tk.END)
self.chat_display.config(state=tk.DISABLED)
def _set_input_state(self, enabled: bool):
"""设置输入区域状态"""
state = tk.NORMAL if enabled else tk.DISABLED
self.input_text.config(state=state)
self.send_btn.config(state=state)
if enabled:
self.input_text.focus_set()
# ── 生词本(手动记录 + 导出 TXT) ──
def _export_vocab(self):
"""将生词本内容导出为 TXT 文件"""
content = self.vocab_input.get("1.0", tk.END).strip()
if not content:
messagebox.showinfo("提示", "生词本是空的,请先输入一些单词。")
return
# 默认文件名:vocab_YYYYMMDD_HHMMSS.txt
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
default_name = f"vocab_{timestamp}.txt"
file_path = filedialog.asksaveasfilename(
title="导出生词本",
defaultextension=".txt",
initialfile=default_name,
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")],
)
if not file_path:
return # 用户取消
try:
# 逐行清理:去空行、去两端空格
lines = content.split("\n")
cleaned_lines = []
for line in lines:
line = line.strip()
if line:
cleaned_lines.append(line)
with open(file_path, "w", encoding="utf-8") as f:
for word in cleaned_lines:
f.write(word + "\n")
self.status_bar.config(text=f"生词本已导出: {os.path.basename(file_path)} ({len(cleaned_lines)} 个单词)")
messagebox.showinfo(
"导出成功",
f"已导出 {len(cleaned_lines)} 个单词到:\n{file_path}\n\n"
f"可以用金山词霸、欧路词典等工具导入学习。",
)
except Exception as e:
messagebox.showerror("导出失败", f"无法保存文件:{str(e)}")
def _clear_vocab(self):
"""清空生词本"""
if messagebox.askyesno("确认清空", "确定要清空生词本中的所有内容吗?"):
self.vocab_input.delete("1.0", tk.END)
self.status_bar.config(text="生词本已清空")
# ── 诗歌创作核心方法 ──
def _send_message(self):
"""发送创作请求"""
topic = self.topic_entry.get().strip()
if not topic:
topic = "nature"
self.topic_entry.insert(0, "nature")
api_key = self.api_entry.get().strip()
if not api_key:
messagebox.showwarning("提示", "请先输入 API 密钥")
return
genre = self.genre_var.get()
genre_info = POETRY_GENRES.get(genre, POETRY_GENRES["Free Verse"])
system_prompt = genre_info["prompt"]
self._append_text(f"\n{'='*60}\n", "system_msg")
self._append_text(f"📝 创作请求\n", "user")
self._append_text(f"体裁: {genre_info['name']} 主题: {topic}\n", "content")
self._append_text(f"{'='*60}\n", "system_msg")
self.messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Please compose a {genre.lower()} poem about {topic}. Write only the poem in English, no explanations, no Chinese."},
]
self._append_text(f"\n✨ 诗歌创作中...\n", "assistant")
self._set_input_state(False)
model_id = self.model_var.get()
self.status_bar.config(text=f"AI 创作中...(体裁: {genre_info['name']} | 模型: {model_id})")
params = {
"temperature": self.temp_var.get(),
"max_tokens": self.max_tokens_var.get(),
"top_p": self.top_p_var.get(),
"presence_penalty": self.presence_var.get(),
"frequency_penalty": self.freq_var.get(),
}
Thread(
target=self._call_api_stream,
args=(api_key, model_id, params),
daemon=True,
).start()
def _call_api_stream(self, api_key: str, model_id: str, params: dict):
"""后台线程:流式调用 API"""
full_reply = ""
def on_chunk(text: str):
nonlocal full_reply
full_reply += text
self.root.after(0, self._append_text, text, "poem")
try:
client = LLMClient(model=model_id, api_key=api_key)
client.chat_stream(
self.messages,
on_chunk=on_chunk,
temperature=params["temperature"],
max_tokens=params["max_tokens"],
top_p=params["top_p"],
presence_penalty=params["presence_penalty"],
frequency_penalty=params["frequency_penalty"],
)
self.messages.append({"role": "assistant", "content": full_reply})
self.root.after(0, self._on_stream_done, full_reply)
except Exception as e:
self.root.after(0, self._on_error, str(e))
def _on_stream_done(self, reply_text: str):
"""流式完成处理"""
self._append_text("\n", "system_msg")
self._append_text("-" * 60 + "\n", "system_msg")
self.status_bar.config(
text=f"创作完成 | 输出长度: {len(reply_text)} 字符 | 在右侧生词本中手动记录生词"
)
self._set_input_state(True)
def _on_error(self, error_msg: str):
"""错误处理"""
self._append_text(f"\n[错误] {error_msg}\n", "error")
self._append_text("-" * 60 + "\n", "system_msg")
self.status_bar.config(text=f"错误: {error_msg}")
self._set_input_state(True)
def _on_close(self):
"""关闭窗口时保存配置"""
cfg_fields = [
("model", self.model_var.get()),
("genre", self.genre_var.get()),
("difficulty", self.difficulty_var.get()),
("temperature", round(self.temp_var.get(), 1)),
("max_tokens", self.max_tokens_var.get()),
("top_p", round(self.top_p_var.get(), 1)),
("presence_penalty", round(self.presence_var.get(), 1)),
("frequency_penalty", round(self.freq_var.get(), 1)),
]
for key, val in cfg_fields:
self.config[key] = val
api_key = self.api_entry.get().strip()
if api_key:
self.config["api_key"] = api_key
save_config(self.config)
self.root.destroy()
def run(self):
"""启动应用"""
self.root.mainloop()
if __name__ == "__main__":
app = PoetryAgent()
app.run()
完整代码文件:
poetry_agent.py包含了上述所有功能的完整实现,共计约 685行。从上方的体裁选择下拉框、主题输入框,到流式诗歌生成的回调处理,再到右侧生词本的手动记录与导出功能,所有代码均在一个文件中,复制粘贴即可运行。
6 运行与使用指南
6.1 环境准备
- Python 3.8+
- 安装依赖:
pip install openai - 获取 API 密钥:注册 AIGC bar 后在控制台获取
6.2 使用流程
- 启动程序:
python poetry_agent.py - 配置 API 密钥:粘贴密钥到顶部输入框,点击"保存配置"
- 选择诗歌体裁:从下拉框中选择俳句、十四行诗、自由诗等
- 设置词汇难度:选择 CET-4 / CET-6 / TEM-4 / TEM-8,提示当前诗歌用词级别
- 输入主题:在主题输入框填写主题词(如 love, nature, ocean)
- 创作诗歌:点击"创作诗歌"按钮或按
Ctrl+Enter - 记录生词:阅读诗歌时,在右侧生词本中手动输入生词,每行一个
- 导出 TXT:点击"导出 TXT",选择保存路径,用金山词霸等工具打开学习
6.3 参数说明
| 参数 | 范围 | 说明 |
|---|---|---|
| 温度(Temperature) | 0.0 - 2.0 | 诗歌创作推荐 0.8-1.0,越高越有创造性 |
| 最大输出Token | 100 - 128000 | 俳句等短诗 2000 足矣,民谣/维拉内拉需要 8000+ |
| Top-P | 0.0 - 1.0 | 核采样参数,与温度配合,通常保持 1.0 |
| 词汇难度 | CET-4/CET-6/TEM-4/TEM-8 | 标记当前诗歌的预期词汇水平,辅助用户有目标地学习 |
7 扩展与思考
体裁扩展:你可以在 POETRY_GENRES 字典中添加更多体裁,如日本短歌(Tanka)、斯宾塞体十四行诗(Spenserian Sonnet)、颂歌(Ode)等。
生词本增强:可以扩展为生词本自动保存到本地文件,下次启动时自动加载历史记录。
韵律检查:可以添加一个"韵律检查"功能,让 AI 在生成后自动检查诗歌是否符合格律要求。
双语对照:可以扩展为在诗歌右侧同步显示中文翻译。
智能体技术正在重塑人与 AI 的交互方式。从通用对话到专精创作,从被动回答到主动学习辅助,每一步扩展都在拓展 AI 的能力边界。希望这个英语诗歌创作智能体能成为你学习英语和探索 AI 应用的有趣工具。
文章涉及的代码和概念如有疑问,欢迎在评论区交流讨论。
更多推荐
所有评论(0)