【零基础AI应用开发】第10章:风格文库与文档导入(RAG篇)
·
📦 项目教程仓库:https://github.com/ZIQI-a/AI_Agent_study 🚀 成品项目地址:https://github.com/ZIQI-a/huamiao_Agent
本章目标
要做的事:实现文章导入、风格分析、风格库管理功能
学到的知识:
- 文件上传处理
- 用 LLM 分析文章风格(结构化输出)
- 风格标签系统
10.1 安装依赖
# 已经安装了 react-markdown 等,这里不需要额外安装
10.2 文件上传 API
创建 src/app/api/styles/upload/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { styles } from "@/lib/db/schema";
export async function POST(req: NextRequest) {
try {
const formData = await req.formData();
const file = formData.get("file") as File;
const name = formData.get("name") as string;
if (!file) {
return NextResponse.json({ error: "请上传文件" }, { status: 400 });
}
// 读取文件内容
const content = await file.text();
if (content.length < 50) {
return NextResponse.json(
{ error: "文件内容太短,至少需要50字" },
{ status: 400 }
);
}
// 保存到数据库
const result = await db
.insert(styles)
.values({
name: name || file.name,
content,
})
.returning();
return NextResponse.json({ success: true, style: result[0] });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
10.3 风格分析 API
创建 src/app/api/styles/analyze/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { createOpenAI } from "@ai-sdk/openai";
import { generateObject } from "ai";
import { z } from "zod";
import { db } from "@/lib/db";
import { styles } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
const deepseek = createOpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: "https://api.deepseek.com",
});
// 风格分析的 Zod schema
const styleSchema = z.object({
tone: z.enum(["正式", "轻松", "文艺", "幽默", "严肃", "客观"]),
vocabulary: z.enum(["简单", "适中", "复杂", "学术"]),
sentenceLength: z.enum(["短句为主", "长短结合", "长句为主"]),
features: z.array(z.string()).describe("3-5个写作特点"),
summary: z.string().describe("一句话总结风格"),
});
export async function POST(req: NextRequest) {
try {
const { id } = await req.json();
// 从数据库读取文章
const result = await db.select().from(styles).where(eq(styles.id, id));
const style = result[0];
if (!style) {
return NextResponse.json({ error: "未找到文章" }, { status: 404 });
}
// 用 LLM 分析风格
const { object } = await generateObject({
model: deepseek("deepseek-chat"),
schema: styleSchema,
prompt: `分析以下文章的写作风格,用 JSON 格式输出:
文章内容:
${style.content.slice(0, 3000)} // 截取前3000字分析
`,
});
// 更新数据库
await db
.update(styles)
.set({ analysis: JSON.stringify(object, null, 2) })
.where(eq(styles.id, id));
return NextResponse.json({ success: true, analysis: object });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
知识点:
generateObjectvsstreamText
streamText:流式输出文本,适合长内容生成generateObject:输出结构化 JSON,适合数据提取和分析generateObject使用 Zod schema 确保输出格式正确
10.4 风格库管理 API
// src/app/api/styles/route.ts
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { styles } from "@/lib/db/schema";
import { desc } from "drizzle-orm";
export async function GET() {
const allStyles = await db
.select()
.from(styles)
.orderBy(desc(styles.createdAt));
return NextResponse.json(allStyles);
}
// src/app/api/styles/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { styles } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
export async function DELETE(
req: NextRequest,
{ params }: { params: { id: string } }
) {
await db.delete(styles).where(eq(styles.id, Number(params.id)));
return NextResponse.json({ success: true });
}
10.5 风格库页面
安装文件上传组件:
pnpm dlx shadcn@latest add badge
创建 src/app/styles/page.tsx:
"use client";
import { useState, useEffect, useRef } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { PageContainer } from "@/components/layout/page-container";
interface StyleItem {
id: number;
name: string;
content: string;
analysis: string | null;
createdAt: string;
}
export default function StylesPage() {
const [styles, setStyles] = useState<StyleItem[]>([]);
const [loading, setLoading] = useState(true);
const [uploading, setUploading] = useState(false);
const [analyzing, setAnalyzing] = useState<number | null>(null);
const fileRef = useRef<HTMLInputElement>(null);
const nameRef = useRef<HTMLInputElement>(null);
useEffect(() => {
fetchStyles();
}, []);
const fetchStyles = async () => {
const res = await fetch("/api/styles");
const data = await res.json();
setStyles(data);
setLoading(false);
};
const handleUpload = async () => {
const file = fileRef.current?.files?.[0];
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append("file", file);
formData.append("name", nameRef.current?.value || file.name);
try {
const res = await fetch("/api/styles/upload", {
method: "POST",
body: formData,
});
const data = await res.json();
if (data.success) {
fetchStyles();
if (fileRef.current) fileRef.current.value = "";
if (nameRef.current) nameRef.current.value = "";
} else {
alert(data.error);
}
} finally {
setUploading(false);
}
};
const handleAnalyze = async (id: number) => {
setAnalyzing(id);
try {
const res = await fetch("/api/styles/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
});
const data = await res.json();
if (data.success) {
fetchStyles();
} else {
alert(data.error);
}
} finally {
setAnalyzing(null);
}
};
const handleDelete = async (id: number) => {
if (!confirm("确定删除?")) return;
await fetch(`/api/styles/${id}`, { method: "DELETE" });
setStyles(styles.filter((s) => s.id !== id));
};
return (
<PageContainer title="风格文库" description="导入文章,分析写作风格">
{/* 上传区 */}
<Card className="mb-6">
<CardHeader>
<CardTitle>导入文章</CardTitle>
</CardHeader>
<CardContent>
<div className="flex gap-4 items-end flex-wrap">
<div className="space-y-2">
<Label>文章名称(可选)</Label>
<Input ref={nameRef} placeholder="如:鲁迅杂文风格" />
</div>
<div className="space-y-2">
<Label>选择文件</Label>
<input
ref={fileRef}
type="file"
accept=".txt,.md"
className="block text-sm file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:bg-primary file:text-primary-foreground"
/>
</div>
<Button onClick={handleUpload} disabled={uploading}>
{uploading ? "上传中..." : "导入"}
</Button>
</div>
</CardContent>
</Card>
{/* 风格列表 */}
{loading ? (
<p className="text-muted-foreground">加载中...</p>
) : styles.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<div className="text-5xl mb-4">📚</div>
<p>还没有导入任何文章</p>
<p className="text-sm">上传 .txt 或 .md 文件,AI 会分析其写作风格</p>
</div>
) : (
<div className="space-y-4">
{styles.map((item) => (
<Card key={item.id}>
<CardHeader className="flex flex-row items-start justify-between">
<div>
<CardTitle className="text-lg">{item.name}</CardTitle>
<p className="text-sm text-muted-foreground">
{item.content.length} 字 ·{" "}
{new Date(item.createdAt).toLocaleDateString("zh-CN")}
</p>
</div>
<div className="flex gap-2">
{!item.analysis && (
<Button
variant="outline"
size="sm"
onClick={() => handleAnalyze(item.id)}
disabled={analyzing === item.id}
>
{analyzing === item.id ? "分析中..." : "分析风格"}
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(item.id)}
>
删除
</Button>
</div>
</CardHeader>
<CardContent>
{/* 内容预览 */}
<p className="text-sm text-muted-foreground line-clamp-3 mb-4">
{item.content.slice(0, 300)}...
</p>
{/* 风格分析结果 */}
{item.analysis && (
<div className="bg-muted rounded-lg p-4">
<h4 className="font-medium mb-2">风格分析</h4>
<pre className="text-sm whitespace-pre-wrap">
{item.analysis}
</pre>
</div>
)}
</CardContent>
</Card>
))}
</div>
)}
</PageContainer>
);
}
10.6 测试风格文库
- 准备一篇 .txt 或 .md 文件(比如你喜欢的博客文章)
- 上传到风格文库
- 点击「分析风格」,查看 AI 的分析结果
- 尝试上传不同风格的文章,对比分析结果
本章小结
| 概念 | 说明 |
|---|---|
| 文件上传 | FormData + API Route 处理文件 |
| generateObject | Vercel AI SDK 的结构化输出函数 |
| Zod schema | 定义 JSON 结构,确保 AI 输出格式正确 |
| 风格分析 | 用 LLM 提取文章的写作特点 |
下一章预告
风格库有了,接下来实现核心的 RAG 功能 —— 把文章向量化,创作时检索相似文章作为参考,实现风格仿写。
如果这个教程对你有帮助,欢迎 ⭐ Star 支持一下!
更多推荐


所有评论(0)