一句话说清核心收获

这篇文章会让你学会:用LLM从任意人生履历文本中抽取事件并自动渲染成互动时间线页面。看完就能跑出自己的Demo,不限于讣告,传记、履历、新闻时间线都可套用。

效果:输出一个能左右滑动的时间线

输入一段讣告原文(比如下面这位Edith的),系统自动解析出:出生、求学、成家、信仰转变、戒酒、临终等关键节点,并在地图上标注地点(如果有),点击每个事件可以展开详情。最终页面长这样:

obituary timeline web app interactive
交互式时间线实机截图,从左到右依次展开,每个节点可展开详情。

技术选型:三件套搞定

  • 解析层:OpenAI GPT-4o-mini(便宜,解析精度够)
  • 前端:Next.js 14 App Router + Tailwind CSS + react-vertical-timeline-component
  • 部署:Vercel (Serverless + Edge)

为什么不直接手写正则?因为讣告/传记的写法千奇百怪,LLM泛化能力远超规则。一次API调用成本约0.001美元,性价比极高。

核心代码实现(关键片段)

1. 定义事件结构

typescript
1 2 3 4 5 6 7 8
// types.ts
export interface LifeEvent {
  date: string;        // 年份或具体日期,例如 "December 12, 1945"
  title: string;       // 事件标题,例如 "出生"
  description: string; // 从原文提取的1-2句描述
  location?: string;   // 地点,可选,例如 "Augusta & Wrens"
  type: 'birth' | 'achievement' | 'struggle' | 'faith' | 'death';
}

2. 用OpenAI API提取事件

typescript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// lib/parseObituary.ts
import OpenAI from 'openai';

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

export async function parseLifeEvents(text: string): Promise<LifeEvent[]> {
  const prompt = `
你是一个传记事件提取器。从以下讣告中提取所有关键人生事件,按时间顺序排列。
每个事件包含:date(尽量转换为年或年月),title(简短概括,如"戒酒"),description(原文1-2句),location(如果提到),type(枚举:birth|achievement|struggle|faith|death)。
只输出合法的JSON数组,不要其他文字。

原文:
"""
${text}
"""
`;

  const response = await client.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    response_format: { type: 'json_object' },
  });

  const raw = response.choices[0].message.content;
  const parsed = JSON.parse(raw);
  return parsed.events || parsed; // 兼容不同输出
}

3. 前端渲染(App Router下的API路由 + 页面组件)

typescript
1 2 3 4 5 6 7 8 9 10 11 12
// app/api/parse/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { parseLifeEvents } from '@/lib/parseObituary';

export async function POST(req: NextRequest) {
  const { text } = await req.json();
  if (!text || text.length < 20) {
    return NextResponse.json({ error: '文本太短' }, { status: 400 });
  }
  const events = await parseLifeEvents(text);
  return NextResponse.json(events);
}
tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
// components/Timeline.tsx
'use client';
import { VerticalTimeline, VerticalTimelineElement } from 'react-vertical-timeline-component';
import 'react-vertical-timeline-component/style.min.css';

interface Props {
  events: LifeEvent[];
}

export default function Timeline({ events }: Props) {
  return (
    <VerticalTimeline>
      {events.map((ev, i) => (
        <VerticalTimelineElement
          key={i}
          date={ev.date}
          iconStyle={iconColorMap[ev.type]}
        >
          <h3>{ev.title}</h3>
          <p>{ev.description}</p>
          {ev.location && <span className="text-gray-400">📍 {ev.location}</span>}
        </VerticalTimelineElement>
      ))}
    </VerticalTimeline>
  );
}

项目结构和配置

text
1 2 3 4 5 6 7 8 9 10 11 12 13
my-memorial-timeline/
├── app/
│   ├── api/parse/route.ts   # 解析API
│   ├── page.tsx              # 输入匡 + Timeline组件
│   └── layout.tsx
├── components/
│   └── Timeline.tsx
├── lib/
│   ├── parseObituary.ts      # LLM解析函数
│   └── types.ts
├── .env.local                # OPENAI_API_KEY=sk-xxxx
├── package.json
└── vercel.json               # 可无

关键注意:在Vercel部署时,OpenAI API请求可能有冷启动延迟(约1-2秒)。建议将解析结果缓存到KV Store(Vercel KV),同一条文本避免反复调用。另外要设置Edge Runtime超时60秒(默认10秒可能不够)。

上线要避的坑

  1. API Key泄露:千万不要在客户端调用OpenAI,必须走自己的API路由(服务端)。
  2. 文本长度限制:GPT-4o-mini上下文128K,但一次解析超过5000字的讣告容易丢信息。建议截断到前2000字,或分段解析后合并。
  3. 事件顺序乱序:LLM有时会打乱顺序,返回结果后要在前端按date重新排序(用moment.js解析日期)。
  4. 地理位置标注:如果地点是城市名,可额外调用Mapbox Geocoding API添加地图标记,但免费额度有限,初期建议只展示文字地点。
  5. 样式适配移动端:react-vertical-timeline-component在手机上宽度溢出,要加 overflow-x: auto

我的个人观点:为什么这个Demo值得做?

现在很多纪念网站(如Legacy.com)仍然靠人工编辑或简单模板。用AI自动解析并生成互动页面,可以将成本降到近乎零,还能一键生成个性化纪念页。未来任何有文本记述的人生(简历、回忆录、新闻人物)都可以通过这套流水线转化成可视化时间线。开发者现在应该关注的是结构化抽取 + 极简前端的组合,而不是重复造轮子去写规则解析。

你只需要一个OpenAI Key + 一个Vercel账号,2小时内就能跑通。