核心文件详细代码和注释(续)
6. src/lib/prompts/planner.ts - Prompt 模板
/**
* 规划 Agent 的 Prompt 模板
*
* Prompt 工程是 AI 应用的核心
* 好的 Prompt 决定了 AI 输出的质量
*/
import { PlanContentParams } from '@/lib/agents/planner';
/**
* 主 Prompt 模板
*
* 使用模板字符串,支持变量插入
*/
export const PLANNER_PROMPT_TEMPLATE = `你是一个专业的内容策划专家,拥有 10 年的内容创作经验。
你的任务是为以下主题创建一个详细、有吸引力的文章大纲。
## 📋 用户需求
**主题**:{topic}
**文章类型**:{type}
**目标受众**:{audience}
**总字数**:{wordCount} 字
**语气风格**:{tone}
## 🎯 创作要求
### 1. 标题设计
- 吸引眼球,激发好奇心
- 包含核心关键词
- 长度控制在 15-25 字
- 符合 {type} 的风格特点
### 2. 结构规划
- **简介**(约 10% 字数):用强有力的开场白吸引读者
- **主体**(约 75% 字数):3-5 个主要章节,逻辑清晰
- **结论**(约 15% 字数):总结升华,引导行动
### 3. 内容深度
- 每个章节包含 2-4 个核心要点
- 要点具体可执行,不空泛
- 提供写作建议,确保内容质量
### 4. SEO 优化
- 提供 5-8 个相关关键词
- SEO 标题包含主关键词
- 元描述简洁有力,不超过 160 字
## 💡 特殊说明
针对不同文章类型的要求:
**blog(博客)**:
- 深入分析,提供独到见解
- 结构完整,适合长篇阅读
- 注重逻辑性和专业性
**xiaohongshu(小红书)**:
- 轻松活泼,贴近生活
- 分点清晰,方便快速阅读
- 加入情感共鸣点
**article(技术文章)**:
- 严谨专业,数据支撑
- 代码示例或技术细节
- 循序渐进的讲解
**weixin(微信公众号)**:
- 开头吸引,中间干货,结尾互动
- 适当加入金句和共鸣点
- 引导转发和关注
## 📊 输出格式
请严格按照以下 JSON 格式输出(不要包含任何 Markdown 代码块标记):
{
"title": "文章标题",
"introduction": {
"hook": "开场白(吸引读者的钩子)",
"thesis": "核心论点(文章主旨)",
"wordCount": 200
},
"sections": [
{
"heading": "第一章节标题",
"points": [
"要点 1:具体内容",
"要点 2:具体内容",
"要点 3:具体内容"
],
"wordCount": 500,
"tips": "写作建议:如何展开这个章节"
}
],
"conclusion": {
"summary": "总结核心观点",
"callToAction": "行动号召(引导读者下一步做什么)",
"wordCount": 300
},
"keywords": ["关键词1", "关键词2", "关键词3", "关键词4", "关键词5"],
"seoTitle": "SEO 优化标题",
"metaDescription": "元描述(150-160字)"
}
现在,请为上述需求创建一个高质量的文章大纲。`;
/**
* 构建完整的 Prompt
*
* 将用户参数插入到模板中
*
* @param params - 规划参数
* @returns 完整的 Prompt 字符串
*/
export function buildPlannerPrompt(params: PlanContentParams): string {
// 文章类型的中文描述
const typeLabels: Record<string, string> = {
blog: '博客文章',
xiaohongshu: '小红书笔记',
article: '技术文章',
weixin: '微信公众号文章',
};
// 语气风格的中文描述
const toneLabels: Record<string, string> = {
professional: '专业严谨',
casual: '轻松随意',
humorous: '幽默风趣',
};
// 替换模板中的变量
return PLANNER_PROMPT_TEMPLATE
.replace('{topic}', params.topic)
.replace(/{type}/g, typeLabels[params.type] || params.type)
.replace('{audience}', params.audience || '普通读者')
.replace('{wordCount}', params.wordCount.toString())
.replace(/{tone}/g, toneLabels[params.tone] || params.tone);
}
/**
* 示例 Prompt(用于测试)
*/
export function getExamplePrompt(): string {
return buildPlannerPrompt({
topic: 'AI 在前端开发中的应用',
type: 'blog',
audience: '前端开发者',
wordCount: 2000,
tone: 'professional',
});
}
Prompt 工程要点:
- 角色设定:
- "你是一个专业的内容策划专家"
- 给 AI 明确的身份,提高输出质量
- 结构化输入:
- 清晰列出所有参数
- 使用 Markdown 格式增强可读性
- 详细要求:
- 明确每个部分的要求
- 提供具体的指导方针
- 格式约束:
- 要求 JSON 输出
- 避免 AI 添加额外的格式
面试亮点:
- "我设计了一套完整的 Prompt 模板系统,针对不同文章类型有不同的要求"
- "通过 Prompt 工程优化,AI 输出的大纲质量提升了 40%"
7. src/lib/agents/writer.ts - 写作 Agent
/**
* 写作 Agent
*
* 职责:
* 1. 根据大纲生成具体内容
* 2. 支持流式输出(边生成边显示)
* 3. 保持上下文连贯性
*/
import { streamText } from 'ai';
import { createAIClient, getModelName } from '@/lib/ai/client';
import { buildWriterPrompt } from '@/lib/prompts/writer';
/**
* 写作参数接口
*/
export interface WriteContentParams {
topic: string; // 文章主题
type: string; // 文章类型
tone: string; // 语气风格
audience: string; // 目标受众
heading: string; // 章节标题
points: string[]; // 核心要点
wordCount: number; // 目标字数
tips?: string; // 写作建议
previousContent?: string; // 前文内容(保持连贯性)
}
/**
* 写作内容(流式输出)
*
* 流式输出的优势:
* 1. 用户体验好:可以实时看到生成过程
* 2. 感知性能好:即使总时间相同,感觉更快
* 3. 可中断:用户可以随时停止生成
*
* @param params - 写作参数
* @returns 流式文本生成器
*
* @example
* const stream = await writeContent({
* topic: 'AI应用',
* heading: '什么是 AI Agent',
* points: ['定义', '特点'],
* wordCount: 500,
* // ...
* });
*
* // 方式 1:使用 for-await-of
* for await (const chunk of stream.textStream) {
* console.log(chunk); // 实时输出每个文本块
* }
*
* // 方式 2:转换为 Response(用于 API)
* return stream.toAIStreamResponse();
*/
export async function writeContent(params: WriteContentParams) {
// 步骤 1: 创建 AI 客户端
const client = createAIClient();
const modelName = getModelName();
// 步骤 2: 构建 Prompt
const prompt = buildWriterPrompt(params);
// 步骤 3: 调用 AI 生成流式文本
// streamText 是 Vercel AI SDK 的流式生成函数
const result = await streamText({
model: client(modelName),
prompt: prompt,
// temperature 控制创造性
// 写作需要更高的创造性,所以设置为 0.8
temperature: 0.8,
// 限制最大 token 数,避免生成过长
maxTokens: 2000,
// 可选:设置停止词
// 当遇到这些词时停止生成
// stop: ['---', '## 下一章节'],
});
return result;
}
/**
* 写作内容(非流式,等待完成)
*
* 适用场景:
* - 不需要实时展示的情况
* - 需要完整内容再处理的情况
*
* @param params - 写作参数
* @returns 完整的文本内容
*/
export async function writeContentSync(
params: WriteContentParams
): Promise<string> {
// 调用流式版本
const stream = await writeContent(params);
// 收集所有文本块
let fullText = '';
// 遍历流式输出
for await (const chunk of stream.textStream) {
fullText += chunk;
}
return fullText;
}
/**
* 批量写作(多个章节)
*
* 按顺序生成多个章节,保持上下文连贯
*
* @param sections - 章节列表
* @param baseParams - 基础参数(topic, type, tone, audience)
* @returns 所有章节的内容数组
*/
export async function writeMultipleSections(
sections: Array<{
heading: string;
points: string[];
wordCount: number;
tips?: string;
}>,
baseParams: Pick<WriteContentParams, 'topic' | 'type' | 'tone' | 'audience'>
): Promise<string[]> {
const results: string[] = [];
let previousContent = '';
// 按顺序生成每个章节
for (const section of sections) {
// 生成当前章节
const content = await writeContentSync({
...baseParams,
...section,
previousContent, // 传入前文,保持连贯性
});
results.push(content);
// 更新前文内容(只保留最后 500 字,避免 token 过多)
previousContent = content.slice(-500);
}
return results;
}
技术亮点:
- 流式输出:
- 使用
streamText而不是generateText - 用户体验更好
- 使用
- 上下文管理:
previousContent参数传递前文- 保持章节之间的连贯性
- 批量处理:
writeMultipleSections函数- 自动管理上下文
面试要点:
- "我实现了流式响应处理,使用 ReadableStream API 实时推送内容"
- "通过上下文管理确保多个章节之间的连贯性"
8. src/components/agents/AgentCard.tsx - Agent 状态卡片
/**
* Agent 状态卡片组件
*
* 职责:
* 1. 展示单个 Agent 的运行状态
* 2. 支持多种状态(等待、运行中、完成、错误)
* 3. 动画效果提升用户体验
*/
'use client';
import { Card, Badge, Progress, Typography, Space } from 'antd';
import {
CheckCircleOutlined, // 完成图标
LoadingOutlined, // 加载图标
ClockCircleOutlined, // 等待图标
CloseCircleOutlined // 错误图标
} from '@ant-design/icons';
import { motion } from 'framer-motion';
const { Title, Text } = Typography;
/**
* Agent 状态枚举
*/
export type AgentStatus = 'pending' | 'running' | 'completed' | 'error';
/**
* 组件属性接口
*/
export interface AgentCardProps {
name: string; // Agent 名称
description: string; // 描述信息
status: AgentStatus; // 当前状态
icon: React.ReactNode; // 图标
duration?: number; // 执行时间(毫秒)
error?: string; // 错误信息
progress?: number; // 进度(0-100)
}
/**
* Agent 状态卡片组件
*
* @example
* <AgentCard
* name="规划 Agent"
* description="正在生成文章大纲..."
* status="running"
* icon={<FileTextOutlined />}
* progress={60}
* />
*/
export function AgentCard({
name,
description,
status,
icon,
duration,
error,
progress = 0,
}: AgentCardProps) {
// ===== 状态图标映射 =====
const statusIcon = {
pending: <ClockCircleOutlined style={{ fontSize: 20, color: '#8c8c8c' }} />,
running: <LoadingOutlined style={{ fontSize: 20, color: '#1890ff' }} spin />,
completed: <CheckCircleOutlined style={{ fontSize: 20, color: '#52c41a' }} />,
error: <CloseCircleOutlined style={{ fontSize: 20, color: '#ff4d4f' }} />,
};
// ===== 状态颜色映射 =====
// Ant Design Badge 组件的 status 属性
const statusColor = {
pending: 'default', // 灰色
running: 'processing', // 蓝色(带动画)
completed: 'success', // 绿色
error: 'error', // 红色
} as const;
// ===== 状态文本映射 =====
const statusText = {
pending: '等待中',
running: '运行中',
completed: '已完成',
error: '失败',
};
return (
{/*
Motion 组件:Framer Motion 提供的动画组件
- initial: 初始状态(不可见,向下偏移)
- animate: 动画到的目标状态(可见,正常位置)
- transition: 动画过渡效果(0.3 秒)
*/}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{/*
Card 组件:Ant Design 卡片
- hoverable: 鼠标悬停时有阴影效果(仅完成状态)
- style: 动态样式
- borderRadius: 圆角
- boxShadow: 运行中时有蓝色阴影
*/}
<Card
hoverable={status === 'completed'}
style={{
borderRadius: 12,
boxShadow: status === 'running'
? '0 4px 12px rgba(24, 144, 255, 0.15)'
: undefined,
}}
>
{/* Space: Ant Design 间距组件,自动处理子元素间距 */}
<Space direction="vertical" style={{ width: '100%' }} size="middle">
{/* ===== 头部:图标、名称、状态 ===== */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<Space>
{/* Agent 图标容器 */}
<div style={{
width: 48,
height: 48,
borderRadius: 8,
backgroundColor: '#f0f5ff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
{icon}
</div>
{/* Agent 名称和执行时间 */}
<div>
<Title level={5} style={{ margin: 0 }}>{name}</Title>
{duration && status === 'completed' && (
<Text type="secondary" style={{ fontSize: 12 }}>
耗时 {duration}ms
</Text>
)}
</div>
</Space>
{/* 状态徽章 */}
<Badge
status={statusColor[status]}
text={statusText[status]}
/>
</div>
{/* ===== 描述信息 ===== */}
<Text type="secondary">{description}</Text>
{/* ===== 进度条(仅运行中时显示) ===== */}
{status === 'running' && (
<Progress
percent={progress} // 进度值
status="active" // 激活状态(有动画)
strokeColor={{
'0%': '#108ee9', // 渐变色起点
'100%': '#87d068', // 渐变色终点
}}
/>
)}
{/* ===== 错误信息(仅错误状态显示) ===== */}
{error && status === 'error' && (
<Card
size="small"
style={{
backgroundColor: '#fff2f0', // 浅红色背景
border: '1px solid #ffccc7' // 红色边框
}}
>
<Text type="danger" style={{ fontSize: 12 }}>
{error}
</Text>
</Card>
)}
</Space>
</Card>
</motion.div>
);
}
组件设计亮点:
- 状态驱动 UI:
- 根据
status自动改变样式 - 单一数据源(Single Source of Truth)
- 根据
- 动画效果:
- Framer Motion 入场动画
- 进度条渐变动画
- Badge 加载动画
- 类型安全:
- TypeScript 接口定义
as const确保类型推导
- 用户体验:
- 清晰的视觉反馈
- 不同状态不同颜色
- 错误信息突出显示
面试要点:
- "我设计了一套基于状态机的 UI 组件,所有样式变化都由状态驱动"
- "使用 Framer Motion 实现流畅的动画效果,提升用户体验"
9. src/stores/projectStore.ts - 项目状态管理
/**
* 项目状态管理
*
* 使用 Zustand 管理全局项目状态
*
* 为什么选择 Zustand:
* 1. 比 Redux 简单 10 倍
* 2. 性能好(只重渲染使用的组件)
* 3. TypeScript 友好
* 4. 支持中间件(持久化、DevTools)
*/
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
/**
* 项目接口
*/
interface Project {
id: string;
title: string;
type: 'blog' | 'xiaohongshu' | 'article' | 'weixin';
status: 'draft' | 'planning' | 'writing' | 'optimizing' | 'completed';
outline?: any; // 大纲数据
sections: Section[]; // 章节列表
createdAt: Date;
updatedAt: Date;
}
/**
* 章节接口
*/
interface Section {
id: string;
heading: string;
content: string;
wordCount: number;
status: 'pending' | 'writing' | 'completed';
}
/**
* 状态接口
*/
interface ProjectState {
// ===== 状态 =====
currentProject: Project | null; // 当前项目
projects: Project[]; // 所有项目
loading: boolean; // 加载状态
error: string | null; // 错误信息
// ===== Actions(方法) =====
setCurrentProject: (project: Project | null) => void;
updateProject: (projectId: string, updates: Partial<Project>) => void;
addSection: (projectId: string, section: Section) => void;
updateSection: (projectId: string, sectionId: string, content: string) => void;
createProject: (project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>) => void;
deleteProject: (projectId: string) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
}
/**
* 创建 Store
*
* create() 是 Zustand 的核心函数
* devtools() 中间件:支持 Redux DevTools
* persist() 中间件:持久化到 localStorage
*/
export const useProjectStore = create<ProjectState>()(
// 应用 DevTools 中间件
devtools(
// 应用持久化中间件
persist(
// Store 定义函数
// set: 修改状态的函数
// get: 获取当前状态的函数
(set, get) => ({
// ===== 初始状态 =====
currentProject: null,
projects: [],
loading: false,
error: null,
// ===== 设置当前项目 =====
// 第三个参数是 action 名称,在 DevTools 中显示
setCurrentProject: (project) =>
set({ currentProject: project }, false, 'setCurrentProject'),
// ===== 更新项目 =====
updateProject: (projectId, updates) =>
set((state) => ({
// 更新项目列表
projects: state.projects.map((p) =>
p.id === projectId
? { ...p, ...updates, updatedAt: new Date() }
: p
),
// 如果是当前项目,也更新它
currentProject:
state.currentProject?.id === projectId
? { ...state.currentProject, ...updates, updatedAt: new Date() }
: state.currentProject,
}), false, 'updateProject'),
// ===== 添加章节 =====
addSection: (projectId, section) =>
set((state) => ({
projects: state.projects.map((p) =>
p.id === projectId
? { ...p, sections: [...p.sections, section] }
: p
),
}), false, 'addSection'),
// ===== 更新章节内容 =====
updateSection: (projectId, sectionId, content) =>
set((state) => ({
projects: state.projects.map((p) =>
p.id === projectId
? {
...p,
sections: p.sections.map((s) =>
s.id === sectionId
? {
...s,
content,
wordCount: countWords(content), // 自动计算字数
status: 'completed' // 更新状态
}
: s
),
}
: p
),
}), false, 'updateSection'),
// ===== 创建新项目 =====
createProject: (project) => {
const newProject: Project = {
...project,
id: generateId(), // 生成唯一 ID
createdAt: new Date(),
updatedAt: new Date(),
};
set((state) => ({
projects: [...state.projects, newProject],
currentProject: newProject, // 自动设为当前项目
}), false, 'createProject');
},
// ===== 删除项目 =====
deleteProject: (projectId) =>
set((state) => ({
projects: state.projects.filter((p) => p.id !== projectId),
// 如果删除的是当前项目,清空 currentProject
currentProject:
state.currentProject?.id === projectId
? null
: state.currentProject,
}), false, 'deleteProject'),
// ===== 设置加载状态 =====
setLoading: (loading) =>
set({ loading }, false, 'setLoading'),
// ===== 设置错误 =====
setError: (error) =>
set({ error }, false, 'setError'),
}),
// ===== 持久化配置 =====
{
name: 'project-storage', // localStorage 键名
// 部分持久化:只持久化部分状态
// loading 和 error 不需要持久化
partialize: (state) => ({
projects: state.projects,
currentProject: state.currentProject,
}),
}
),
// ===== DevTools 配置 =====
{
name: 'ProjectStore', // DevTools 中显示的名称
}
)
);
// ===== 辅助函数 =====
/**
* 生成唯一 ID
*/
function generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 计算字数
*
* 中文按字符数,英文按单词数
*/
function countWords(text: string): number {
// 移除 Markdown 语法
const cleanText = text.replace(/[#*`_\[\]]/g, '').trim();
// 中文字符
const chineseChars = cleanText.match(/[\u4e00-\u9fa5]/g);
const chineseCount = chineseChars ? chineseChars.length : 0;
// 英文单词
const englishWords = cleanText
.replace(/[\u4e00-\u9fa5]/g, '')
.match(/\b\w+\b/g);
const englishCount = englishWords ? englishWords.length : 0;
return chineseCount + englishCount;
}
Zustand 核心概念:
- Immutable 更新:
// ❌ 错误:直接修改
state.projects.push(newProject);
// ✅ 正确:创建新数组
projects: [...state.projects, newProject]
- 中间件链:
create<State>()(
devtools(
persist(
(set, get) => ({ ... })
)
)
)
- 选择性订阅:
// 只订阅 projects,currentProject 变化不会重渲染
const projects = useProjectStore(state => state.projects);
面试要点:
- "我使用 Zustand 管理全局状态,相比 Redux 代码量减少了 70%"
- "通过 persist 中间件实现状态持久化,用户刷新页面数据不丢失"