从俯卧撑到AI应用:最小可行习惯的力量

Logan Paul说他身体转变的起点不是什么花哨训练,而是每天坚持俯卧撑、仰卧撑和跑步。这个逻辑放在AI开发里完全一样:别一上来就想做完美产品,先跑通三个核心动作

对AI应用来说,三个核心习惯是:

  1. 模型调用(能发请求、收响应)
  2. 流式输出(让用户看到逐字生成)
  3. 状态管理(保持对话上下文)

这三个搞定了,剩下都是装饰——就像肌肉线条是俯卧撑堆出来的。


核心习惯一:模型调用——一个函数搞定

别纠结用什么框架,先写一个能调通API的函数。我用OpenAI的GPT-4o mini(成本最低),但换成Claude或本地模型原理一样。

javascript
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// lib/ai.js
import OpenAI from 'openai';

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export async function getCompletion(messages) {
  const response = await client.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: messages,
    stream: true,  // 关键:开启流式
    max_tokens: 1024,
  });
  return response;
}

developer typing code on laptop with AI chat bubble
把API调用写成一个纯函数,其他地方只传messages数组,这就是每天做的“俯卧撑”。


核心习惯二:流式输出——让用户感觉快

很多新手把流式输出想复杂了。其实后端用ReadableStream,前端用EventSourcefetch的流解析,20行内搞定。

后端(Node.js + Vercel Edge Function)

javascript
1 2 3 4 5 6 7 8 9 10
// api/chat.js
import { getCompletion } from '../lib/ai.js';

export const config = { runtime: 'edge' };

export default async function handler(req) {
  const { messages } = await req.json();
  const stream = await getCompletion(messages);
  return new Response(stream.toReadableStream());
}

前端(React)

javascript
1 2 3 4 5 6 7 8 9 10 11 12
const response = await fetch('/api/chat', {
  method: 'POST',
  body: JSON.stringify({ messages }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  setReply(prev => prev + decoder.decode(value, { stream: true }));
}

这里有个坑:Edge Function 超时限制。Vercel免费版无服务器函数超时10秒,但流式响应可以持续30秒以上?实测Edge Function支持流式长响应,但免费层有CPU时长限制。解决办法:用waitUntil扩展,或者直接部署到支持更长超时的平台(如Cloudflare Workers,付费版可以调超时)。


核心习惯三:状态管理——别重建历史

最简单的方法:在客户端存一个messages数组,每次新消息push进去。但这样刷新就丢了。要上线,必须做持久化。

我推荐用localStorage做临时方案(不涉及隐私数据的话),或者用Supabase免费层存会话。以下是localStorage版本:

javascript
1 2 3 4 5 6 7 8 9 10
const STORAGE_KEY = 'chat_messages';

function loadMessages() {
  const saved = localStorage.getItem(STORAGE_KEY);
  return saved ? JSON.parse(saved) : [{ role: 'system', content: 'You are a helpful assistant.' }];
}

function saveMessages(messages) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(messages));
}

注意:localStorage容量约5MB,对话太长记得自动截断(保留最近20轮)。


项目结构(直接复制可用)

text
1 2 3 4 5 6 7 8 9 10 11 12 13
my-ai-chat/
├── api/
│   └── chat.js          # Edge Function,流式处理
├── lib/
│   └── ai.js            # OpenAI客户端封装
├── pages/
│   ├── index.html       # 单页应用入口
│   └── app.js           # 前端逻辑(React + hooks)
├── public/
│   └── style.css        # 简陋样式,能看就行
├── package.json
├── vercel.json          # Vercel部署配置
└── .env.local           # OPENAI_API_KEY

关键配置:Vercel需要将api/*路由到Edge Runtimevercel.json

json
1 2 3 4 5 6 7
{
  "functions": {
    "api/chat.js": {
      "runtime": "edge"
    }
  }
}

上线要注意的坑

  1. API Key 泄漏:前端无法直接调OpenAI,必须走自己的后端。Vercel环境中设置环境变量,不要写死在代码里。
  2. 成本控制:每次调用都计费,给用户无限次调用等你破产。解决方案:加一个简单的速率限制(比如每IP每分钟10次请求),可以用Upstash Redis免费层。
  3. 流式中断处理:用户关掉页面再打开,对话上下文最好恢复。localStorage方案已经解决。但流过程中断会导致最后一部分丢失,需要前端做重试或容错。
  4. 长上下文token溢出:GPT-4o mini 128k上下文,但对话太长会变慢且贵。建议在getCompletion中对messages数组做滑动窗口,保留系统指令+最近用户/助理消息。

我的个人观点:不要一开始就想着做多完美,像Logan Paul一样,先坚持三个基本动作——模型调用能跑、流式输出顺滑、对话状态能恢复。然后在实际使用中一点点加功能:图像生成、搜索增强、多轮记忆……那时候你的“肌肉”已经长出来了。


代码已脱敏,可直接在本地运行。用vercel dev本地调试,部署前记得设置环境变量。