返回博客

如何给自己搓个 AI 私人助理(入门教程)

作者 约 8 分钟读完

用一个下午、零成本,从零搭建一个属于自己的 AI 私人助理:随手记录碎碎念,自动分类成 todo / mood / note,每晚生成复盘、次日邮件推送,装成手机 App。全程 Vibe Coding,附完整步骤。


如何给自己搓个 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,把整个产品的骨架写清楚:

// 原始输入
export const 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 抽出来的结构化条目
export const 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"),
});

// 每日复盘
export const 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 用)
export const users = pgTable("users", { /* id, email, timezone, ... */ });

设计思路很简单:原始的放一张表,加工过的放另一张表,永远别把 AI 的输出盖在用户的原话上。AI 可能会出错,用户的话不能丢,这是底线。

morning_sent_at 这个字段是我踩过坑之后加的——定时任务偶尔会被 Vercel 重试,没有这个字段用户会收到两封一模一样的早报。

写完以后把 schema 推到数据库(这一步要等下面 Neon 建好才能跑):

pnpm drizzle-kit push

个人项目做登录,最坑的方案是"账号 + 密码 + 验证码 + 忘记密码 + ...",做完想吐。

最省事的方案是 magic link:用户输入邮箱 → 收到一封带登录链接的邮件 → 点一下就登录进去了。没有密码要记。

用 Auth.js v5(next-auth@beta)配一下:

// auth.config.ts
import Resend from "next-auth/providers/resend";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@/lib/db";

export default {
  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 新账号只有一个沙盒地址 [email protected],这个地址只能发给你注册 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 是历史原因,实际模型后面换过):

import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.NVIDIA_API_KEY!,
  baseURL: "https://integrate.api.nvidia.com/v1",   // 就换这一行
});

const EXTRACT_MODEL = "meta/llama-3.3-70b-instruct";

export async function extract(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 逐行校验:

const ItemSchema = 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 里。代码里对上就行:

export async function GET(req: Request) {
  const auth = req.headers.get("authorization");
  if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response("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. 项目顶部 Storage tab → 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