# 如何给自己搓个 AI 私人助理（入门教程）

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

- 原文链接: https://laojin.blog/blog/20260427_tinypa_tutorial
- 作者: 老金
- 发布日期: 2026-04-27
- 标签: AI Coding, 免费, Vibe Coding, Vercel

---

![img](https://pic-1258874139.cos.ap-hongkong.myqcloud.com/laojinblog/posts/20260428/01.jpg)

以前做同样一件事要花一整个月 + 至少一千块服务器费。现在只用一个下午、一分钱没花就做好了。这篇记录的是我从零开始搭建的完整过程——会写一点代码但没碰过 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](https://build.nvidia.com/) 登录之后点右上 **Get API Key**，拿到一个 `nvapi-xxx` 的 key 就完事。

账号都注册完了，再打开编辑器。

---

## 第 1 步：起一个 Next.js 15 项目

```bash
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 → 渲染"的样板。

起完以后立刻装几个核心依赖：

```bash
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`，把整个产品的骨架写清楚：

```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 建好才能跑）：

```bash
pnpm drizzle-kit push
```

---

## 第 3 步：登录——邮箱 magic link 最省事

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

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

用 Auth.js v5（`next-auth@beta`）配一下：

```ts
// 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 新账号只有一个沙盒地址 `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 是历史原因，实际模型后面换过）：

```ts
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 逐行校验：

```ts
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：

```json
{
  "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 里。代码里对上就行：

```ts
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`**：
```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 壳）：
```js
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](./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
