如何给自己搓个 AI 私人助理(入门教程)
如何给自己搓个 AI 私人助理(入门教程)
以前做同样一件事要花一整个月 + 至少一千块服务器费。现在只用一个下午、一分钱没花就做好了。这篇记录的是我从零开始搭建的完整过程——会写一点代码但没碰过 Next.js / Postgres / LLM API 的朋友,照着做就能复刻。
先说做出来是什么
TinyPA 是我给自己做的一个”碎碎念收纳器”:
-
• 打开像浏览器。随手丢一句”下班买猫粮 / 老板说 Q2 聚焦增长 / 今天有点累”进去。 -
• 1-2 秒后,消息下方冒出几张小卡片:AI 已经把这句话拆成了 todo / followup / mood / note四类之一。 -
• 每晚 22:07 自动生成当日复盘;次日 08:03 邮件推送昨日复盘 + 今日 top 3。 -
• 在 Safari / Chrome 里点一下”添加到主屏幕”,就有一个独立图标,打开没有浏览器地址栏,跟原生 app 一样。
一个下午搓出来,一分钱没花。
平台吃掉了 95% 的脏活,留给你的只剩”想清楚要做什么”。这不是技术进步,这是一个人能独立造产品的时代第一次真的到来了。
这篇文章我按动手顺序写——就是我那天坐下来从空文件夹开始敲键盘的顺序。中间每个”为什么选它 / 踩过什么坑”都不跳过。
第 0 步:先把账号注册齐
这一步不涉及代码,但一次性搞定比写一半回头再来省事得多。一共五个免费服务:
-
• GitHub:用途为代码托管,提供免费额度。 -
• Vercel:用途为前后端托管 + 定时任务,Hobby 档免费。 -
• Neon:提供 Postgres 数据库,拥有 0.5GB 免费额度。 -
• Resend:用于发邮件(包含登录 + 早报),享有 3000 封/月免费额度。 -
• NVIDIA NIM:提供大模型 API,包含个人免费额度。
Neon 有个反直觉的点:别直接去 neon.tech 建项目。要等下一步从 Vercel 里建,这样 Vercel 会自动把 DATABASE_URL注到项目环境变量,省得手动复制。
NVIDIA NIM 是这篇里最可能让你眼前一亮的东西——它把 Llama、Qwen、Gemma 这些开源大模型托管起来对外提供 API,接口完全兼容 OpenAI 协议。去 build.nvidia.com登录之后点右上 Get API Key,拿到一个 nvapi-xxx的 key 就完事。
账号都注册完了,再打开编辑器。
第 1 步:起一个 Next.js 15 项目
pnpm create next-app tinypa \
--typescript --tailwind --app --src-dir=false \
--import-alias "@/*" --use-pnpm
cd tinypa
为什么选 Next.js App Router?
-
• 前端后端一个仓库一套代码。API 路由直接写在 app/api/*下,不用单独起一个 backend。 -
• Vercel 亲儿子。push 到 GitHub 自动部署,HTTPS、preview 环境、环境变量、定时任务全在同一个后台。 -
• React Server Components免去很多”从 API 拉数据 → useEffect → 渲染”的样板。
起完以后立刻装几个核心依赖:
pnpm add next-auth@beta @auth/drizzle-adapter
pnpm add drizzle-orm postgres
pnpm add openai zod resend
pnpm add -D drizzle-kit
每一个都有明确的活干:
-
• next-auth做邮箱 magic link 登录 -
• drizzle-orm+postgres做数据层 -
• openai调 NVIDIA NIM(对,就是官方 SDK,因为协议兼容) -
• zod校验 LLM 的 JSON 输出 -
• resend发邮件
第 2 步:设计数据表,四张就够
打开新建的 lib/db/schema.ts,把整个产品的骨架写清楚:
// 原始输入
exportconst messages = pgTable("messages", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").notNull(),
rawText: text("raw_text").notNull(),
createdAt: timestamp("created_at").defaultNow(),
processedAt: timestamp("processed_at"), // LLM 抽取完成时间,null 表示还没跑
});
// AI 抽出来的结构化条目
exportconst items = pgTable("items", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").notNull(),
messageId: uuid("message_id"), // 来自哪条 message
type: text("type").notNull(), // todo | followup | mood | note
content: text("content").notNull(),
dueAt: timestamp("due_at"),
priority: integer("priority"),
status: text("status").default("open"),
});
// 每日复盘
exportconst digests = pgTable("digests", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").notNull(),
date: date("date").notNull(),
summaryMd: text("summary_md"),
topTodoIds: text("top_todo_ids"), // JSON string
morningSentAt: timestamp("morning_sent_at"), // 防重,发过就不再发
});
// 用户(timezone 字段给以后换精细 cron 用)
exportconst users = pgTable("users", { /* id, email, timezone, ... */ });
设计思路很简单:原始的放一张表,加工过的放另一张表,永远别把 AI 的输出盖在用户的原话上。AI 可能会出错,用户的话不能丢,这是底线。
morning_sent_at这个字段是我踩过坑之后加的——定时任务偶尔会被 Vercel 重试,没有这个字段用户会收到两封一模一样的早报。
写完以后把 schema 推到数据库(这一步要等下面 Neon 建好才能跑):
pnpm drizzle-kit push
第 3 步:登录——邮箱 magic link 最省事
个人项目做登录,最坑的方案是”账号 + 密码 + 验证码 + 忘记密码 + …”,做完想吐。
最省事的方案是 magic link:用户输入邮箱 → 收到一封带登录链接的邮件 → 点一下就登录进去了。没有密码要记。
用 Auth.js v5(next-auth@beta)配一下:
// auth.config.ts
importResendfrom"next-auth/providers/resend";
import { DrizzleAdapter } from"@auth/drizzle-adapter";
import { db } from"@/lib/db";
exportdefault {
adapter: DrizzleAdapter(db),
providers: [
Resend({
apiKey: process.env.RESEND_API_KEY!,
from: process.env.MAIL_FROM!,
}),
],
pages: { signIn: "/login" },
};
就这么几行。Auth.js 会自动建 accounts / sessions / users / verification_tokens四张表,你只要在 schema 里 import 它给的 schema 就行。
小坑:Resend 新账号只有一个沙盒地址
onboarding@resend.dev,这个地址只能发给你注册 Resend 时用的那个邮箱。验证自己够用,给别人用就得加自己的域名(Resend Dashboard → Domains,加 3 条 DNS 记录几分钟就过)。
第 4 步:聊天页 + AI 抽取——项目最硬核的地方
整个产品的魔法就在这一步。用户发一条消息,后端做两件事:
-
1. 立即把原文插入 messages表,立即返回给前端(保证”话不丢”)。 -
2. 后台调 LLM 把这条消息拆成若干条 items,insert 进库。
顺序很重要:先落库再调 LLM。如果反过来,LLM 超时/报错就会把用户的话吃掉。
4.1 调 NVIDIA NIM
大白话:NIM 就是”开源大模型的 Uber”——它给你一个和 OpenAI 一模一样的方向盘,底下挂着 Llama、Qwen、Mistral 这些开源车。你连司机都不用换,换个 URL 就开上了另一辆。
lib/llm/gemma.ts里(名字叫 gemma 是历史原因,实际模型后面换过):
importOpenAIfrom"openai";
const client = newOpenAI({
apiKey: process.env.NVIDIA_API_KEY!,
baseURL: "https://integrate.api.nvidia.com/v1", // 就换这一行
});
constEXTRACT_MODEL = "meta/llama-3.3-70b-instruct";
exportasyncfunctionextract(text: string, now: Date, tz: string) {
const res = await client.chat.completions.create({
model: EXTRACT_MODEL,
messages: [
{ role: "system", content: EXTRACT_SYSTEM },
{ role: "user", content: `现在是 ${now.toISOString()}(${tz})\n用户输入:${text}` },
],
max_tokens: 1024,
temperature: 0.2,
stream: true, // NDJSON 流式返回,下面讲为什么
});
// ... 解析
}
为什么选 Llama 3.3 70B 不是更小/更便宜的?我最开始用的是 8B 的模型,速度快但输出经常抽风——JSON 格式不对、内容编造。换到 gemma-4-31b-it质量完美但首 token 要 28 秒,直接把 Vercel Serverless 函数 60s 超时打爆。最后落在 70B:首 token 1-2 秒,四条消息四条 NDJSON 全对。
结论:免费 LLM 之间差距很大,先把场景跑通再按质量反选模型,别一开始就纠结哪个最强。
4.2 让 LLM 输出严格结构
prompt 里写清楚:
你是一个整理助手。用户会发一段话,你要把它拆成若干条 item。
每条 item 一行 JSON(NDJSON 格式),字段:
- type: "todo" | "followup" | "mood" | "note"
- content: 简短的一句话
- due_at: ISO 时间(todo 类才有)
如果用户只是闲聊没有实质内容,返回空。
最后一句很重要:让模型知道”什么都不抽”也是合法输出,比硬凑几个无意义 item 强。
输出拿到后用 zod 逐行校验:
constItemSchema = z.object({
type: z.enum(["todo", "followup", "mood", "note"]),
content: z.string().min(1).max(200),
due_at: z.string().datetime().optional(),
});
for (const line of text.split("\n")) {
const parsed = ItemSchema.safeParse(JSON.parse(line));
if (parsed.success) await db.insert(items).values({ ... });
// 解析失败就丢掉这一行,不让坏数据污染库
}
流式 NDJSON 的好处:每解析出一行就可以立刻 insert 到 items表 + push 到前端。用户在聊天页看到的”卡片一张一张冒出来”就是这么来的。如果等整个 response 结束再解析,用户要盯着加载动画看 3-5 秒,体验差一截。
4.3 前端聊天页
React 那边就是一个普通的聊天列表 + 输入框。唯一要注意的是:滚动和自动加载的手感。
我踩的坑:
-
• 初次加载要瞬间跳到底部,别用 smooth 动画(否则用户会看见一个”从顶部滑到底部”的诡异过场)。 -
• 聊天历史分页拉,初始只加载 5 条,往上滚到离顶部 400px 就自动加载更多。
这些细节在 git log 里能看到我改了五六次才搞定。手感这东西,写代码的时候觉得”差不多就行”,做完用一天就觉得”非改不可”。
第 5 步:每日复盘 + 早报——定时任务怎么跑
vercel.json里配两条 cron:
{
"crons":[
{"path":"/api/cron/digest","schedule":"7 14 * * *"},
{"path":"/api/cron/morning","schedule":"3 0 * * *"}
]
}
-
• UTC 14:07(北京 22:07)跑 digest:把当天所有 items扔给 LLM 生成一份总结 -
• UTC 00:03(北京 08:03)跑 morning:把昨天的 digest + 今日 top 3 todo 发邮件
坑 1:Hobby 档 cron 每天只能跑一次。我本来设计的是每小时跑一次,在任务里按每个用户的时区判断”现在是不是他的 22:00″。部署时 Vercel 直接报错:
Hobby accounts are limited to daily cron jobs.
多时区精细投递是 Pro 才有的权利。只好妥协:所有用户都走北京时间,海外朋友体验会偏一点。独立项目要学会这种”先能跑,细节以后说”的取舍。
坑 2:cron 怎么鉴权?
Vercel 有一个神仙设定:只要你设了 CRON_SECRET环境变量,它就会自动把 Authorization: Bearer $CRON_SECRET加到定时请求的 header 里。代码里对上就行:
exportasyncfunctionGET(req: Request) {
const auth = req.headers.get("authorization");
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
returnnewResponse("Unauthorized", { status: 401 });
}
// ... 跑任务
}
零配置鉴权。这个设计是真的优雅。
坑 3:digest 用 LLM 生成的”语气”
Prompt 里千万别写”请给用户一份温暖鼓励的总结”——出来的东西会像你那种”今天真棒棒呀”的微信阅读公众号。我最后定的 prompt 只有两句:
风格:温和、具体、不煽情、不说教。
200-400 字。
第 6 步:PWA——不上架、不审核、就是 app
**大白话:**PWA 就是让网页”穿上一层 app 的皮”。桌面图标、全屏打开、离线可用——用户看起来和原生 app 没区别,但你不用交 99 美元给苹果、不用写 Swift、不用等审核两周。
很多人第一次听说 PWA 觉得是黑魔法,其实就两个文件:
public/manifest.json:
{
"name":"TinyPA",
"short_name":"TinyPA",
"start_url":"/",
"display":"standalone",
"background_color":"#0a0a0a",
"theme_color":"#0a0a0a",
"icons":[
{"src":"/icon-192.png","sizes":"192x192","type":"image/png"},
{"src":"/icon-512.png","sizes":"512x512","type":"image/png"}
]
}
public/sw.js(一个最简单的 service worker 壳):
self.addEventListener("install", (e) => self.skipWaiting());
self.addEventListener("activate", (e) => self.clients.claim());
然后在 app/layout.tsx里加一句 <link rel="manifest" href="/manifest.json" />,再写个小组件 SwRegister注册 service worker。
完事。
-
• iOS Safari:分享按钮 → “添加到主屏幕” -
• Android Chrome:菜单 → “安装应用”
从桌面图标打开,地址栏消失,跟原生 app 一模一样。不用 App Store 审核、不用证书、不用 Xcode。
第 7 步:部署到 Vercel
这一步按顺序来最快,次序错了会互相卡。
-
1. 代码先 push 到 GitHub。 -
2. Vercel → Add New Project→ 导入仓库 → Framework 自动识别 Next.js。 -
3. 这时候先填 5 个环境变量( AUTH_SECRET、RESEND_API_KEY、MAIL_FROM、NVIDIA_API_KEY、CRON_SECRET),DATABASE_URL先别填。点 Deploy。第一次部署构建成功但 API 会 500,正常。 -
4. 项目顶部 Storagetab → Create Database→ 选 Neon→ region 选 Washington D.C. (iad1)。建完自动把DATABASE_URL注入项目环境变量——这就是为什么前面要从 Vercel 建 Neon 而不是反过来。 -
5. 本地 npx vercel link+npx vercel env pull .env.production.local,把生产环境变量拉到本地,然后pnpm db:push把表推上去。 -
6. 再补两个 URL 类变量: AUTH_URL和NEXT_PUBLIC_APP_URL,都填 Vercel 给你的xxx.vercel.app域名。 -
7. Deployments → 最新一次 → Redeploy(不要勾 “Use existing build cache”)。
这套顺序是我踩过几次坑之后定下来的。最常见的错误是”先在 neon.tech 自己建了库再导入 Vercel”——Neon 通过 Vercel 集成创建的账号,Neon Dashboard 的 Create Project按钮是灰的,会让你崩溃半小时。
完整 check list 我写在了 DEPLOY.md里,每一步点哪个按钮都讲清楚。
写在最后:这个项目真正让我意识到的事
做完 TinyPA 我反而没在”AI 写复盘写得多好”上兴奋。让我最有感觉的是另一件事:
一个人做一个端到端的 AI 产品,这一年的门槛被压到了一个荒唐的水平。
以前做同样一个东西你要:买服务器、部署 nginx、装 Postgres、配 SSL 证书、自建邮件服务器绕过垃圾箱、训练或微调一个你其实用不起的模型。这些事情每一件都能耗掉你一整个周末。
今天这些全都被平台吃掉了。Vercel 吃掉部署和 CDN,Neon 吃掉数据库运维,Resend 吃掉邮件投递,NVIDIA NIM 吃掉大模型推理。你需要做的事情被压缩到只剩两件:想清楚产品是什么,把代码写对。
所以现在这个时代,卡住一个人做 AI 产品的,不再是技术门槛,是他的想法。
如果你挑一个下午,照这篇文章做一遍就行。做完哪怕产品不好用,那七个服务你都亲手接过一遍,下一个想法做起来就是纯手感了。
最后留个钩子:你会拿它来做什么
我自己把 TinyPA 当”碎碎念收纳器”,但这套技术栈能做的远不止这个。留一个互动:
如果今天下午你也开一个空文件夹,你最想搓一个什么 AI 小应用?
A. 给自己的记账本接一个”随手拍发票→自动分类”
B. 把X收藏的乱七八糟链接扔进去→每周自动生成主题摘要
C. 读书笔记管家:拍一页书→沉淀成可搜的卡片
D. 其他(评论区告诉我,挑几个有意思的下一篇动手做)
选好打在评论区,我会挨个回。如果超过 50 个人选同一个,下一篇我就按那个做。
项目仓库:https://github.com/freeman5860/TinyPA
老金,曾在大厂工作10年,见证了移动互联网的跌宕起伏,现在专注于个人独立开发,追求技术自由。致力于学习和传播 AI 、软件工程和工程管理方面的知识。如果能引起你的共鸣,请关注我的公众号:
【往期热门文章】
告别 AI 单兵作战:Claude Code Agent Teams 实战详解
AI时代,人人都可以是动漫创作者 —— 老金的零门槛实操指南

夜雨聆风