vibecoding.site
← back to blog
约 10 分钟3 标签

Mini Demo:CSV → 自动选图表,BYOK 模式的工程实践

Bring Your Own Key 的好处与陷阱:LLM 选图表 / 写 spec / 校验数据维度,以及为什么不要让模型自己执行 SQL。


title: "Mini Demo:CSV → 自动选图表,BYOK 模式的工程实践" slug: csv-smart-charts publishedAt: 2026-04-30 excerpt: Bring Your Own Key 的好处与陷阱:LLM 选图表 / 写 spec / 校验数据维度,以及为什么不要让模型自己执行 SQL。 tags: [frontend, llm, byok] readingMin: 10 wordCount: 1520 draft: false

BYOK 是给开发者准备的礼物

/works/csv-charts 这个 demo 让用户上传 CSV,LLM 自动判断该用什么图表(bar/line/pie),生成 chart spec,然后用 recharts 渲染。模型用 claude-haiku-4-5,够用且便宜。

但这个 demo 最特别的地方不是"AI 选图表",而是它跑在 BYOK(Bring Your Own Key)模式下 —— 用户把自己的 Anthropic API key 填到一个输入框里,Key 只在浏览器 localStorage,前端直接调 Anthropic API,没有任何后端中转

BYOK 在 demo 站里几乎是唯一靠谱的方式:

  • 不烧钱:每个访问者用自己的 key,作品集主理人不付费
  • 不存储凭证:Key 不进任何后端,不存在数据泄露责任
  • 开发者友好:目标用户(招聘官 / 同行)大概率自己有 key

代价是工程上要处理几个非显然的问题。这篇把整个 demo 的 BYOK 链路展开。

第一坑:浏览器直接调 Anthropic API

/v1/messages 默认不允许浏览器直接调用(CORS)。一个普通的 fetch() 会被浏览器挡掉。

Anthropic 提供了一个开关让你显式同意"浏览器直连":

  1. Anthropic Console 的 API key 设置里勾上 Allow browser usage
  2. 在请求头加 anthropic-dangerous-direct-browser-access: true

请求长这样:

const res = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "x-api-key": userKey,
    "anthropic-version": "2023-06-01",
    "anthropic-dangerous-direct-browser-access": "true",
    "content-type": "application/json",
  },
  body: JSON.stringify({
    model: "claude-haiku-4-5",
    max_tokens: 1024,
    messages: [
      { role: "user", content: prompt },
    ],
  }),
});

dangerous 这个词不是吓人的,它就是字面意思:浏览器请求会带 user-agent + referer + 用户 IP,Anthropic 想确保你明白这一点。对 BYOK demo 来说完全合理,因为用户用的就是自己的 key、自己的浏览器、自己的 IP。

给用户的预警提示

这条 CORS 开关用户不开就整个 demo 无法工作。我们在页面上方加了一个 callout:

<div role="note" className="rounded-md border border-border bg-card/60 p-3 text-xs">
  <AlertTriangle className="inline h-3.5 w-3.5" />
  首次使用前请在
  <a href="https://console.anthropic.com/" target="_blank" rel="noopener noreferrer">
    Anthropic Console
  </a>
  打开 <code>Allow browser usage</code>。不填 key 也能用,会走启发式 fallback。
</div>

**"不填 key 也能用"**这点接下来讲。

第二步:启发式 fallback

LLM 选图表当然好,但万一 key 没填、key 填错、调用失败,demo 不应该死给用户看。所以我们写了一个 ~30 行的启发式 fallback,根据 CSV 列的数据类型猜图表:

function heuristicChart(columns: ColumnInfo[]): ChartSpec {
  const num = columns.filter((c) => c.type === "number");
  const cat = columns.filter((c) => c.type === "category");
  const date = columns.find((c) => c.type === "date");
 
  // 至少 2 个数值列 + 1 个分类列 → bar / line(有 date 优先 line)
  if (num.length >= 2 && cat.length >= 1) {
    return date
      ? { kind: "line", xField: date.name, yFields: num.map((c) => c.name) }
      : { kind: "bar", xField: cat[0].name, yFields: num.map((c) => c.name) };
  }
  // 1 个数值列 + 1 个分类列 → pie
  if (num.length === 1 && cat.length >= 1) {
    return { kind: "pie", categoryField: cat[0].name, valueField: num[0].name };
  }
  // 默认 bar
  return { kind: "bar", xField: columns[0].name, yFields: [columns[1]?.name] };
}

规则简陋但够用:bar / line / pie 三选一,只看列数量与类型。LLM 的存在让规则可以更聪明,但不应让 fallback 不存在

第三步:让 LLM 输出 chart spec,不让它执行 SQL

这是 BYOK demo 最容易踩的坑:你给 LLM 看完整的 CSV 数据,然后让它"返回结果"。模型可能给你计算好的数字,但这个数字完全无法验证 —— LLM 算错了你也不知道。

更糟的是有人会让 LLM 生成 SQL 让前端 sandbox 跑。SQL 注入 + 沙箱逃逸是个常年话题,demo 站不值得引入这个风险面。

我们的设计:LLM 只输出 chart spec(讨论用哪个图表 + 哪几列),实际聚合 / 计数 / 绘图全部在前端用 papaparse + recharts 干

// 给 LLM 的 prompt(简化版)
const prompt = `
我有一份 CSV,头部和前 10 行如下:
 
${csvSample}
 
请只返回一个 JSON,描述应该用什么图表:
{
  "kind": "bar" | "line" | "pie",
  "xField": "<列名>",     // bar/line
  "yFields": ["<列名>"],  // bar/line
  "categoryField": "...", // pie
  "valueField": "...",    // pie
  "reason": "<一句话理由>"
}
 
不要返回数据本身,只返回 spec。
`;

返回值用 zod schema 校验:

const ChartSpec = z.discriminatedUnion("kind", [
  z.object({
    kind: z.literal("bar"),
    xField: z.string(),
    yFields: z.array(z.string()).min(1),
    reason: z.string(),
  }),
  z.object({
    kind: z.literal("line"),
    xField: z.string(),
    yFields: z.array(z.string()).min(1),
    reason: z.string(),
  }),
  z.object({
    kind: z.literal("pie"),
    categoryField: z.string(),
    valueField: z.string(),
    reason: z.string(),
  }),
]);

模型返回非合法 JSON 或字段不存在 → 抛错 → 走启发式 fallback。模型不可信,zod 才可信

TypeScript 陷阱:discriminated union 在 JSX 里 narrow 失败

写到这里有个非常具体的坑:ChartSpec 是 discriminated union(bar/line/pie 各自字段不同),在 JSX 里三元表达式 narrow 经常丢类型:

// ❌ 这样写 pie 分支拿不到 categoryField
return spec.kind === "pie"
  ? <PieChart data={...} dataKey={spec.categoryField} />
  : <BarChart data={...} xKey={spec.xField} />;

TS 看到 JSX 同一个 return 里嵌套三元,narrow 不能跨表达式传播。解法是把每个分支抽出来用 switch:

function ChartByType({ spec, data }: { spec: ChartSpec; data: Row[] }) {
  switch (spec.kind) {
    case "bar":
      return <BarChart data={data} xKey={spec.xField} yKeys={spec.yFields} />;
    case "line":
      return <LineChart data={data} xKey={spec.xField} yKeys={spec.yFields} />;
    case "pie":
      return <PieChart data={data} categoryKey={spec.categoryField} valueKey={spec.valueField} />;
  }
}

switch 让 narrow 在每个 case 里完整传播,JSX 内不要嵌三元

第四步:Key 的本地存储与隐藏

Key 不进任何后端,但仍要持久化(否则刷新就丢)。用 localStorage:

const KEY = "csv-charts:anthropic-key";
 
function loadKey(): string | null {
  if (typeof window === "undefined") return null;
  return window.localStorage.getItem(KEY);
}
 
function saveKey(k: string) {
  window.localStorage.setItem(KEY, k);
}

输入框 <input type="password">,UI 上 mask 掉。前端做一个最低限度的格式校验,防止用户填错系统 key:

const KEY_REGEX = /^sk-ant-[A-Za-z0-9_-]{20,}$/;

模型 ID 也写死在前端 claude-haiku-4-5,单点改,Sprint 3 之前不打算引一个"模型下拉"。

BYOK 适合谁,不适合谁

总结一下我们的判断:

  • 适合:开发者面向的 demo、内部工具、招聘作品集
  • 不适合:面向终端用户的产品(用户不知道也不应该知道什么是 API key)
  • 不适合:有付费层级的产品(扣钱要扣自己的,不能让用户为你的服务付 Anthropic 的钱)

我们这个 demo 站本质就是作品集,BYOK 是诚实地告诉招聘官:我知道怎么让 AI 跑活,但我不让你为我的演示付费

想看代码,在 apps/web/app/works/csv-charts/page.tsx。整页含上传 / parse / 调 LLM / 渲染 ~500 行,无任何后端依赖。

评论

加载评论中…

留下评论 · 提交后由 PM 审核显示

评论无需注册,审核通过后可见。