版本:v2.0 | 最后更新:2026-02-08
项目愿景
架构规范
开发标准
任务清单
实施计划
成功指标
RadioNowhere 是一个 AI 驱动的网络电台,目前实现了:
✅ 基础播放器 UI
✅ TTS 语音合成(Microsoft/Gemini)
✅ 音乐搜索与播放
✅ 多主持人系统
✅ ReAct Agent 内容生成
问题
表现
影响
节目同质化
都是"鸡汤文+音乐"
用户体验单调
音乐偏好固定
总是选相同歌手
缺乏新鲜感
内容深度不足
对话浅薄空洞
无真实电台感
Agent 职责不清
单一 Writer 处理所有
难以优化
┌────────────────────────────────────────────────────────┐
│ RadioNowhere v2.0 │
├────────────────────────────────────────────────────────┤
│ 🎙️ 多样化节目类型 │
│ - 新闻播报、脱口秀、历史故事、音乐专题... │
│ │
│ 🎵 智能音乐推荐 │
│ - 曲风维度驱动,避免歌手偏好 │
│ - 根据节目类型自动调整音乐配比 │
│ │
│ 🤖 专业化 Agent 系统 │
│ - 职责分离,每类节目有专属 Writer │
│ - 可扩展工具系统 │
│ │
│ 👤 个性化体验(未来) │
│ - 用户偏好设置 │
│ - 智能推荐 │
└────────────────────────────────────────────────────────┘
2.1 项目结构(Feature-Sliced Design)
src/
├── app/ # Next.js App Router
├── features/ # 业务功能模块
│ ├── agents/ # Agent 调度系统
│ │ ├── lib/
│ │ │ ├── director-agent.ts # 总导演
│ │ │ ├── director-types.ts # 类型定义
│ │ │ ├── playback-controller.ts # 播放控制
│ │ │ ├── preload-manager.ts # 预加载
│ │ │ ├── talk-executor.ts # 对话执行
│ │ │ └── music-executor.ts # 音乐执行
│ │ └── index.ts
│ │
│ ├── content/ # 内容生成模块
│ │ ├── lib/
│ │ │ ├── writer-agent.ts # 编剧 Agent
│ │ │ ├── cast-system.ts # 角色系统
│ │ │ ├── show-config.ts # 节目配置 [NEW]
│ │ │ ├── writer-tools.ts # 工具系统
│ │ │ ├── response-parser.ts # 响应解析
│ │ │ └── prompt-templates/ # Prompt 模板 [NEW]
│ │ │ ├── base.ts
│ │ │ ├── talk.ts
│ │ │ ├── news.ts
│ │ │ ├── story.ts
│ │ │ ├── music.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ │
│ ├── music-search/ # 音乐搜索模块
│ │ ├── lib/
│ │ │ ├── gd-music-service.ts # 音乐 API
│ │ │ ├── diversity-manager.ts # 多样性管理
│ │ │ └── genre-wheel.ts # 曲风轮盘 [NEW]
│ │ └── index.ts
│ │
│ ├── tts/ # TTS 模块
│ ├── feedback/ # 用户反馈模块
│ └── history-tracking/ # 历史记录模块
│
├── shared/ # 共享模块
│ ├── services/ # 基础服务
│ │ ├── audio-service/ # 音频混音器
│ │ ├── monitor-service/# 事件监控
│ │ └── storage-service/# 存储服务
│ ├── stores/ # 全局状态
│ ├── types/ # 类型定义
│ │ ├── radio-core.ts # 核心类型
│ │ └── segment.ts # 环节类型 [NEW]
│ └── utils/ # 工具函数
│
└── widgets/ # UI 组件
└── radio-player/
├── ui/
│ ├── index.tsx
│ ├── SubtitleDisplay.tsx
│ ├── TimelinePanel.tsx
│ ├── MailboxDrawer.tsx
│ └── PlayerActionBtn.tsx
├── hooks/
│ └── useRadioPlayer.ts
└── types.ts
┌─────────────────────────────────────────────────────────────┐
│ DirectorAgent │
│ (总导演:调度、决策) │
└──────────────────────────┬──────────────────────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│WriterAgent │ │ TTS Agent │ │AudioMixer │
│ (编剧) │ │ (语音合成) │ │ (混音器) │
└─────┬──────┘ └────────────┘ └────────────┘
│
│ 根据 ShowType 选择
▼
┌─────────────────────────────────────────┐
│ Prompt Templates │
├─────────┬─────────┬─────────┬───────────┤
│ talk │ news │ story │ music │
│ 脱口秀 │ 新闻 │ 故事 │ 音乐专题 │
└─────────┴─────────┴─────────┴───────────┘
│
│ 根据 ShowConfig 选择
▼
┌─────────────────────────────────────────┐
│ Tool System │
├──────────────────────────────────────────┤
│ search_music | fetch_news | search_knowledge │
│ get_lyrics | fetch_trending | submit_show │
└──────────────────────────────────────────┘
用户点击播放
│
▼
DirectorAgent.startShow()
│
├─→ CastDirector.randomShowType() // 决定节目类型
│ │
│ ▼
├─→ WriterAgent.generateTimeline()
│ │
│ ├─→ getShowConfig(type) // 获取节目配置
│ ├─→ getPromptTemplate(type) // 获取专属 Prompt
│ ├─→ getToolsForType(type) // 获取专属工具
│ └─→ AI 生成 → ShowTimeline
│
├─→ prepareBlocks() // 预加载音频
│ ├─→ TalkExecutor.prepare() // TTS 合成
│ └─→ MusicExecutor.prepare() // 音乐下载
│
└─→ executeTimeline() // 执行播放
│
├─→ radioMonitor.emit() // 发送事件
└─→ UI 更新
ShowType
对话占比
音乐占比
音乐用途
必需工具
talk
60-70%
30-40%
背景+过渡
-
entertainment
60-70%
30-40%
背景+过渡
fetch_trending
news
80-90%
10-20%
仅过渡
fetch_news
history
70-80%
20-30%
氛围+过渡
search_knowledge
science
70-80%
20-30%
氛围+过渡
search_knowledge
mystery
75-85%
15-25%
氛围烘托
-
story
65-75%
25-35%
情感渲染
-
nighttalk
65-75%
25-35%
情感渲染
search_quotes
music
30-40%
60-70%
主体内容
search_music, get_lyrics
interview
70-80%
20-30%
过渡
-
drama
85-95%
5-15%
场景+过渡
-
// ✅ 好的做法
export interface ShowConfig {
type : ShowType ;
talkRatio : [ number , number ] ; // [min, max]
musicRatio : [ number , number ] ;
musicPurpose : MusicPurpose ;
requiredTools : ToolName [ ] ;
optionalTools : ToolName [ ] ;
}
// ❌ 避免的做法
export interface Config {
t : string ; // 不清晰的命名
tr : number [ ] ; // 缺少类型约束
mp : string ; // 应使用枚举
}
类型
命名规则
示例
组件
PascalCase
SubtitleDisplay.tsx
Hook
camelCase + use前缀
useRadioPlayer.ts
工具函数
camelCase
diversity-manager.ts
类型定义
kebab-case
radio-core.ts
常量
UPPER_SNAKE_CASE
SHOW_CONFIGS
// ✅ 好的 Prompt 结构
const PROMPT_TEMPLATE = `
## 🎯 任务目标
[清晰描述要做什么]
## 📋 结构要求
[具体的结构和格式]
## ✅ 期望的表达
[正面示例]
## ❌ 禁止的内容
[负面示例]
## 🛠️ 可用工具
[工具列表和用法]
` ;
// ❌ 避免的 Prompt
const BAD_PROMPT = `
请生成一个好的节目。要有趣,要深刻。
` ; // 太模糊,缺乏具体指导
// ✅ 标准工具定义
export const TOOL_DEFINITION : ToolDefinition = {
name : 'search_knowledge' ,
description : `搜索知识/百科内容。
适用场景:
- 历史故事节目需要史实依据
- 科普节目需要科学原理
- 文化节目需要背景知识
返回格式:
- title: 条目标题
- summary: 摘要(100-200字)
- source: 来源` ,
parameters : [
{
name : 'query' ,
type : 'string' ,
description : '搜索关键词(如:三国演义、量子力学、茶道文化)' ,
required : true
} ,
{
name : 'type' ,
type : 'string' ,
description : '知识类型:history/science/culture/general' ,
required : false
}
]
} ;
// ✅ 使用 React Context + Hooks
export function useRadioPlayer ( ) : RadioPlayerState & RadioPlayerActions {
const [ state , setState ] = useState < RadioPlayerState > ( initialState ) ;
// 使用 useCallback 缓存方法
const togglePlayback = useCallback ( async ( ) => {
// ...
} , [ dependencies ] ) ;
// 使用 useEffect 订阅事件
useEffect ( ( ) => {
const cleanup = radioMonitor . on ( 'script' , handler ) ;
return cleanup ;
} , [ ] ) ;
return { ...state , togglePlayback } ;
}
// ❌ 避免的做法
// 直接在组件内定义复杂逻辑
// 不使用 useCallback 导致重复渲染
// 不清理事件监听器
// ✅ 标准错误处理
async function executeToolCall ( name : string , args : unknown ) : Promise < ToolResult > {
try {
const result = await doSomething ( args ) ;
return { success : true , data : result } ;
} catch ( error ) {
// 记录日志
radioMonitor . log ( 'WRITER' , `Tool ${ name } failed: ${ error } ` , 'error' ) ;
// 返回友好错误信息
return {
success : false ,
error : `工具执行失败:${ getErrorMessage ( error ) } `
} ;
}
}
// ❌ 避免的做法
async function badExample ( ) {
const result = await doSomething ( ) ; // 没有 try-catch
return result ; // 错误会导致整个流程崩溃
}
文件 : src/widgets/radio-player/ui/SubtitleDisplay.tsx
问题 : Talk block 展开时切换到 music block,UI 进入异常状态
修复 :
// 在第 101 行 useEffect 后添加
useEffect ( ( ) => {
if ( displayInfo . type !== 'talk' && isExpanded ) {
onExpandChange ( false ) ;
}
} , [ displayInfo . type , isExpanded , onExpandChange ] ) ;
文件 : src/widgets/radio-player/ui/MailboxDrawer.tsx
问题 : 320px 屏幕上输入框和按钮被挤压
修复 :
// 第 36 行
className = "mt-6 w-full max-w-[calc(100vw-2rem)] sm:max-w-md mx-auto"
// 第 73, 86 行按钮
className = "p-2 sm:p-2.5 rounded-xl ..."
文件 : src/features/content/lib/cast-system.ts
改动 : 修改 randomShowType() 使用加权随机
const weights : Record < ShowType , number > = {
talk : 15 , interview : 10 , news : 8 , drama : 5 ,
entertainment : 12 , story : 10 , history : 10 ,
science : 10 , mystery : 10 , nighttalk : 8 , music : 5
} ;
新目录 : src/features/content/lib/prompt-templates/
文件 :
新文件 : src/features/music-search/lib/genre-wheel.ts
内容 :
export const GENRE_DIMENSIONS = [
{ name : '流派' , options : [ '民谣/Folk' , '摇滚/Rock' , ...] } ,
{ name : '年代' , options : [ '60年代' , '70年代' , ...] } ,
{ name : '文化' , options : [ '华语' , '欧美' , '日韩' , ...] } ,
{ name : '氛围' , options : [ '治愈' , '激情' , '忧郁' , ...] }
] ;
文件 : src/features/content/lib/writer-agent.ts
改动 :
async generateTimeline ( duration : number , showType ?: ShowType ) {
const type = showType || castDirector . randomShowType ( ) ;
const config = getShowConfig ( type ) ;
const prompt = this . buildPromptForType ( type , config , duration ) ;
const tools = this . getToolsForType ( type , config ) ;
return this . executeGeneration ( prompt , tools ) ;
}
新文件 : src/shared/types/segment.ts
内容 :
export type SegmentType =
| 'opening' | 'main_topic' | 'music_break'
| 'interaction' | 'closing' ;
export interface ShowSegment {
type : SegmentType ;
durationHint : [ number , number ] ;
blocks : TimelineBlock [ ] ;
}
新文件 : src/features/user-preferences/
内容 :
interface UserPreference {
favoriteGenres : string [ ] ;
dislikedGenres : string [ ] ;
favoriteShowTypes : ShowType [ ] ;
explorationLevel : 'conservative' | 'balanced' | 'adventurous' ;
}
任务 ID
任务名称
文件
优先级
状态
BUG-001
展开状态修复
SubtitleDisplay.tsx
P0
[ ]
BUG-002
窄屏适配
MailboxDrawer.tsx
P0
[ ]
BUG-004
禁止列表持久化
diversity-manager.ts
P0
[ ]
交付物 :
任务 ID
任务名称
文件
优先级
状态
FEAT-001
启用全部节目类型
cast-system.ts
P1
[ ]
FEAT-002
节目配置系统
show-config.ts (新)
P1
[ ]
FEAT-003
专属 Prompt 模板
prompt-templates/ (新)
P1
[ ]
FEAT-004
对话模式引导
prompt-templates/talk.ts
P1
[ ]
FEAT-005
内容密度提升
prompt-templates/*.ts
P1
[ ]
BUG-003
批量 TTS 显示
SubtitleDisplay.tsx
P1
[ ]
交付物 :
新增配置文件
Prompt 模板目录
更新后的 WriterAgent
Phase 3: Agent 系统升级(2-4 周)
任务 ID
任务名称
文件
优先级
状态
ARCH-001
WriterAgent 重构
writer-agent.ts
P2
[ ]
FEAT-006
曲风轮盘系统
genre-wheel.ts (新)
P2
[ ]
FEAT-007
音乐节目曲风引导
writer-agent.ts
P2
[ ]
ARCH-002
工具系统扩展
writer-tools.ts
P2
[ ]
ARCH-003
节目环节系统
segment.ts (新)
P2
[ ]
交付物 :
重构后的 Agent 系统
新工具实现
环节系统原型
任务 ID
任务名称
文件
优先级
状态
ARCH-002.1
search_knowledge 工具
writer-tools.ts
P3
[ ]
ARCH-002.2
fetch_trending 工具
writer-tools.ts
P3
[ ]
FEAT-009
用户偏好系统
user-preferences/ (新)
P3
[ ]
FEAT-010
广播剧完整支持
多文件
P3
[ ]
交付物 :
指标
当前值
Phase 2 目标
Phase 4 目标
节目类型覆盖
3-4 种
8+ 种
11 种
每期平均台词数
15-20 句
40+ 句
50+ 句
纯对话节目占比
0%
20%
30-40%
歌手重复率(连续3期)
50%+
20%
<10%
节目配比正确率
-
80%
95%
指标
评估方法
目标
内容深度
人工评审
无"空洞鸡汤"感
对话自然度
人工评审
有来有往,不像念稿
节目多样性
连续收听10期
明显感知不同风格
音乐惊喜感
用户反馈
发现新歌手/风格
指标
当前值
目标
Prompt 长度
5000+ tokens
按类型 1500-2500
AI 调用次数/期
8-15 次
5-8 次
生成成功率
85%
95%
内存占用
无监控
<500MB
import { ShowType } from './cast-system' ;
export type MusicPurpose = 'main' | 'background' | 'transition_only' ;
export interface ShowConfig {
type : ShowType ;
talkRatio : [ number , number ] ;
musicRatio : [ number , number ] ;
musicPurpose : MusicPurpose ;
requiredTools : string [ ] ;
optionalTools : string [ ] ;
promptTemplate : string ;
}
export const SHOW_CONFIGS : Record < ShowType , ShowConfig > = {
// ...
} ;
export function getShowConfig ( type : ShowType ) : ShowConfig {
return SHOW_CONFIGS [ type ] || SHOW_CONFIGS . talk ;
}
import { RADIO } from '@shared/utils/constants' ;
export function getBasePrompt ( ) : string {
return `
## 📻 电台身份
- 电台名称:${ RADIO . NAME } (${ RADIO . SLOGAN } )
- 频率:${ RADIO . FREQUENCY }
## 📝 输出格式
严格按以下 JSON 格式输出:
{
"id": "唯一ID",
"title": "节目标题",
"estimatedDuration": 数字,
"blocks": [...]
}
` ;
}
# Phase 1 测试
npm test -- --grep " SubtitleDisplay"
npm test -- --grep " MailboxDrawer"
# Phase 2 测试
npm test -- --grep " cast-system"
npm test -- --grep " show-config"
# 手动测试
# 1. 连续生成 10 期节目,检查类型分布
# 2. 在 320px 设备上测试 MailboxDrawer
# 3. 验证音乐歌手不连续重复
文档维护者: AI Assistant
最后更新: 2026-02-08