乐于分享
好东西不私藏

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

如何给自己搓个 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-ormpostgres做数据层
  • • openai调 NVIDIA NIM(对,就是官方 SDK,因为协议兼容)
  • • zod校验 LLM 的 JSON 输出
  • • resend发邮件

第 2 步:设计数据表,四张就够

打开新建的 lib/db/schema.ts,把整个产品的骨架写清楚:

// 原始输入
exportconst messages = pgTable("messages", {
iduuid("id").defaultRandom().primaryKey(),
userIduuid("user_id").notNull(),
rawTexttext("raw_text").notNull(),
createdAttimestamp("created_at").defaultNow(),
processedAttimestamp("processed_at"),    // LLM 抽取完成时间,null 表示还没跑
});

// AI 抽出来的结构化条目
exportconst items = pgTable("items", {
iduuid("id").defaultRandom().primaryKey(),
userIduuid("user_id").notNull(),
messageIduuid("message_id"),             // 来自哪条 message
typetext("type").notNull(),              // todo | followup | mood | note
contenttext("content").notNull(),
dueAttimestamp("due_at"),
priorityinteger("priority"),
statustext("status").default("open"),
});

// 每日复盘
exportconst digests = pgTable("digests", {
iduuid("id").defaultRandom().primaryKey(),
userIduuid("user_id").notNull(),
datedate("date").notNull(),
summaryMdtext("summary_md"),
topTodoIdstext("top_todo_ids"),          // JSON string
morningSentAttimestamp("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 {
adapterDrizzleAdapter(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. 1. 立即把原文插入 messages表,立即返回给前端(保证”话不丢”)。
  2. 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(textstringnowDatetzstring) {
const res = await client.chat.completions.create({
modelEXTRACT_MODEL,
messages: [
      { role"system"contentEXTRACT_SYSTEM },
      { role"user",   content`现在是 ${now.toISOString()}${tz})\n用户输入:${text}` },
    ],
max_tokens1024,
temperature0.2,
streamtrue,  // 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.successawait 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(reqRequest) {
const auth = req.headers.get("authorization");
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
returnnewResponse("Unauthorized", { status401 });
  }
// ... 跑任务
}

零配置鉴权。这个设计是真的优雅。

坑 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. 1. 代码先 push 到 GitHub
  2. 2. Vercel → Add New Project→ 导入仓库 → Framework 自动识别 Next.js。
  3. 3. 这时候先填 5 个环境变量AUTH_SECRETRESEND_API_KEYMAIL_FROMNVIDIA_API_KEYCRON_SECRET),DATABASE_URL先别填。点 Deploy。第一次部署构建成功但 API 会 500,正常。
  4. 4. 项目顶部 Storagetab → Create Database→ 选 Neon→ region 选 Washington D.C. (iad1)。建完自动把 DATABASE_URL注入项目环境变量——这就是为什么前面要从 Vercel 建 Neon 而不是反过来。
  5. 5. 本地 npx vercel linknpx vercel env pull .env.production.local,把生产环境变量拉到本地,然后 pnpm db:push把表推上去。
  6. 6. 再补两个 URL 类变量:AUTH_URL和 NEXT_PUBLIC_APP_URL,都填 Vercel 给你的 xxx.vercel.app域名。
  7. 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 、软件工程和工程管理方面的知识。如果能引起你的共鸣,请关注我的公众号:

【往期热门文章】

如何入门 Agent Skills

Claude 官方给出的常用工作流

告别 AI 单兵作战:Claude Code Agent Teams 实战详解

OpenClaw 源码解读(1): 项目概览

AI时代,人人都可以是动漫创作者 —— 老金的零门槛实操指南