乐于分享
好东西不私藏

AI 宝可梦攻略助手搭建指南

AI 宝可梦攻略助手搭建指南

最近怀旧重玩宝可梦这个游戏,有时遇到迷宫、关卡或属性相关的问题,都会去搜索引擎查找攻略,但搜索起来比较麻烦,于是有了做一个能用任何语言提问的宝可梦攻略网站的想法。你可以问它属性克制、进化条件、迷宫走法,它会实时查数据并用你的语言回答。这篇是从空目录到上线的完整过程,照着步骤走就行。

AI 攻略助手网址: www.pokedata.site

先看清楚你要做什么

成品是一个聊天页面。用户问「喷火龙怕什么属性」,AI 不凭记忆回答,它自己去查数据库,拿到「火/飞行、岩石 4 倍」这些确定的数据,再组织成中文回答。

这个"让 AI 自己去查"的能力叫工具调用,整个项目就靠它,第 3 步细讲。

你需要准备

东西
说明
Node.js 20 以上
命令行敲 node -v能看到版本号就行
一个代码编辑器
VS Code 之类
一个大模型 API key
下面详细说,有免费的
会用命令行
只需要会 cd和复制粘贴命令

不需要:数据库、服务器、机器学习知识。宝可梦数据用免费的公开接口 PokeAPI(https://pokeapi.co),不用注册也不用 key。

先算一笔账,别做到一半才发现

一次提问不等于一次 API 请求。这条放最前面,因为它最容易踩。

用户问「伊布的 8 种进化怎么选」,AI 会先查进化链,再逐个查 8 只宝可梦的数据,每查一次都要重新请求模型一次让它继续。一个问题打出去 10 次请求。

STEP 1 · 搭一个能跑的空架子

打开命令行,创建项目。全部选默认即可:

命令行

npx create-next-app@latest pokemon-ai --typescript --tailwind --app --src-dir --yescd pokemon-ainpm run dev

**怎么确认成功:**浏览器打开 http://localhost:3000,能看到 Next.js 的默认欢迎页。

接着装 AI 相关的包(先按 Ctrl+C停掉刚才的 dev):

命令行

npm i ai zodnpm i @ai-sdk/google        # 用 Gemininpm i @ai-sdk/react         # 前端聊天界面用

ai是 Vercel 的 AI SDK,负责跟大模型打交道;zod用来描述工具的参数长什么样;@ai-sdk/google是 Gemini 的适配层。

STEP 2 · 拿到 key,让它能说第一句话

拿 key

去 Google AI Studio(https://aistudio.google.com/apikey) 创建一个 API key(免费)。在项目根目录建一个文件 .env.local

.env.local

GOOGLE_GENERATIVE_AI_API_KEY=你的key粘贴到这里GOOGLE_MODEL=gemini-2.5-flash

⚠️

两件事关于 key:

① .env.local千万不要提交到 Git。Next.js 生成的 .gitignore默认已经忽略它了,别手动改动这条规则。

② **模型名会过期。**别照抄教程里的名字(包括这篇)。用这条命令查当前真实可用的模型:

curl -s https://generativelanguage.googleapis.com/v1beta/models \  -H "x-goog-api-key: 你的key" | grep '"name"'

写后端接口

新建 src/app/api/chat/route.ts。这个文件的作用是:接收前端发来的对话,转发给模型,把模型的回答一个字一个字地流回前端。

src/app/api/chat/route.ts

import { ToolLoopAgent, createAgentUIStreamResponse } from"ai";import { createGoogleGenerativeAI } from"@ai-sdk/google";const google = createGoogleGenerativeAI({apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,});exportasyncfunctionPOST(reqRequest) {const { messages } = await req.json();const agent = newToolLoopAgent({modelgoogle(process.env.GOOGLE_MODEL!),instructions"你是一位宝可梦攻略助手,用中文回答。",  });returncreateAgentUIStreamResponse({ agent, uiMessages: messages });}

**为什么要"流"回去?**模型生成一段长回答要十几秒。如果等全部生成完再返回,用户会盯着空白页面等。流式输出是边生成边显示,体验完全不同。这部分 SDK 已经处理好了,你只要用 createAgentUIStreamResponse

写前端页面

把 src/app/page.tsx整个替换成:

src/app/page.tsx

"use client";import { useChat } from"@ai-sdk/react";import { DefaultChatTransport } from"ai";import { useState } from"react";exportdefaultfunctionPage() {const { messages, sendMessage, status } = useChat({transportnewDefaultChatTransport({ api"/api/chat" }),  });const [input, setInput] = useState("");return (<divclassName="mx-auto max-w-2xl p-6">      {messages.map((m) => (<divkey={m.id}className="my-3"><b>{m.role === "user" ? "你" : "助手"}:</b>          {m.parts.map((p, i) =>            p.type === "text" ? <spankey={i}>{p.text}</span> : null          )}</div>      ))}<formonSubmit={(e) => {        e.preventDefault();        if (input.trim()) { sendMessage({ text: input }); setInput(""); }      }}><inputclassName="w-full rounded border p-2"value={input}onChange={(e) => setInput(e.target.value)}          disabled={status !== "ready"}          placeholder="问点什么…"        /></form></div>  );}

"use client"这行必须在最顶上,它告诉 Next.js 这个组件要在浏览器里跑,因为要处理输入和点击。

怎么确认成功:npm run dev,打开页面,随便问一句「你好」。应该看到回答一个字一个字冒出来,而不是等一会儿整段出现。

看到回答了就说明 key 有效、模型通了、流式输出正常。

STEP 3 · 工具调用,让它自己去查

现在的助手能聊天,但它报的数据不可靠。你问「喷火龙种族值多少」,它可能凭印象编一个。

先理解工具调用

你问朋友「今天上海几度」,他有两种答法:

  • • 凭记忆:「大概二十几度吧」(可能错)
  • • 查一下:掏手机打开天气 App,看到 23℃,告诉你「23 度」(准确)

工具调用就是给 AI 那个"手机"。你提供一个函数,告诉它「这个函数能查宝可梦数据,什么时候该用它」。AI 自己决定要不要调用、传什么参数,拿到结果后再组织成人话。

整个过程 SDK 会自动循环:AI 说"我要查喷火龙" → SDK 执行你的函数 → 把结果给 AI → AI 说"我还要查克制关系" → 再执行…… 直到它给出最终回答。这也是为什么一个问题会产生十几次请求。

写第一个工具

新建 src/lib/tools.ts

src/lib/tools.ts

import { tool } from"ai";import { z } from"zod";exportconst pokemonTools = {getPokemontool({// description 是给 AI 看的说明书,写清楚"什么时候用"最重要description:"查询一只宝可梦的属性、种族值、特性、图鉴说明。" +"回答任何涉及具体数值的问题前都必须调用它,不要凭记忆作答。",// inputSchema 描述参数。AI 会照这个格式传值inputSchema: z.object({name: z        .string()        .describe("英文小写名,如 pikachu、charizard。中文名要先翻译成英文"),    }),// execute 是真正干活的函数executeasync ({ name }) => {const res = awaitfetch(`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`      );if (!res.ok) {// 关键:报错要写成"指令",AI 看懂后会自己改正重试return {errortrue,hint"没查到。请确认用的是英文小写名,比如 pikachu、mr-mime。",        };      }const d = await res.json();return {name: d.name,types: d.types.map((t: { type: { name: string } }) => t.type.name),statsObject.fromEntries(          d.stats.map((s: { stat: { name: string }; base_stat: number }) => [            s.stat.name,            s.base_stat,          ])        ),      };    },  }),};

然后在接口里挂上它 —— 只加一行:

src/app/api/chat/route.ts

import { pokemonTools } from"@/lib/tools";const agent = newToolLoopAgent({modelgoogle(process.env.GOOGLE_MODEL!),instructions"你是一位宝可梦攻略助手,用中文回答。",tools: pokemonTools,          // ← 加这一行});

**怎么确认成功:**问「喷火龙的属性和种族值」。回答里应该出现准确的数字:火/飞行,种族值 HP 78、攻击 84、特攻 109、速度 100。

如果数字对得上,说明 AI 真的去查了而不是编的。

再加几个工具

照同样的模式,把这些接口包成工具,助手的能力就完整了:

工具
PokeAPI 接口
能回答什么
getEvolutionChain/evolution-chain/{id}
进化条件(等级、道具、亲密度)
getPokemonMatchup/type/{name}
弱点、几倍伤害
getMove/move/{name}
招式威力、命中、附加效果
getItem/item/{name}
道具效果
getEncounters/pokemon/{id}/encounters
野生出现地点

⚠️

**属性克制要自己算。**PokeAPI 只给「火被水克制」这种单属性关系。喷火龙是火 +飞行双属性,被岩石打是 2×2 = 4 倍,这个乘法得你自己写,把两个属性的倍率相乘。用户最容易在这里发现你算错。

STEP 4 · 多语言

模型本身就会多语言,这步几乎不用做什么,在指令里说清规则就行:

指令片段

用简体中文回答。但如果用户的提问明显使用了另一种语言,就跟随用户提问所用的语言 ——用户的实际用语优先于界面设置。宝可梦、招式、道具都用该语言的官方译名。

PokeAPI 本身也自带各语言官方译名。它的 names字段长这样:

PokeAPI 返回的多语言名

[{"language":{"name":"zh-hans"},"name":"皮卡丘"},{"language":{"name":"ja"},"name":"ピカチュウ"},{"language":{"name":"en"},"name":"Pikachu"}]

所以更好的做法是让工具直接返回目标语言的名字,不必让模型自己翻译。用工厂函数把语言"包进"工具里:

src/lib/tools.ts

// 改成函数,构造时就把语言固定下来exportfunctioncreatePokemonTools(langstring) {return {getPokemontool({// …execute({ name }) =>getPokemon(name, lang),   // lang 被闭包捕获    }),  };}

好处是模型不需要自己传语言参数,也就不会漏传。前端把当前语言随请求发过来,接口用它构造工具就行。

STEP 5 · 教它承认不知道

用户最想问的其实是「月见山的迷宫怎么走」。但 PokeAPI 没有迷宫路线数据,它只有地点名称,没有"往左走、下楼梯"这种信息。

这时候如果什么都不管,模型会编。它会给你一段看起来非常具体的路线,带着自信的方位和步数,但可能是错的,而用户没法分辨。

解决办法是在指令里把两类问题分开定规则:

指令片段(关键部分)

## 数据准确性任何涉及具体数值的问题,都必须先调用工具查询,绝不能凭记忆回答:- 种族值、属性、特性、捕获率 → getPokemon- 进化条件 → getEvolutionChain- 属性克制、弱点 → getPokemonMatchup## 迷宫与剧情攻略工具里没有迷宫内部路线数据。回答这类问题时依据你自己的游戏知识作答,并遵守:1. 先确认版本。同一个迷宫在不同版本里结构不同。如果用户没说版本,   要明确标注你假设的是哪个版本。2. 分步骤写,每步说清方向、标志物、要用的招式或道具。3. 诚实标注不确定的地方。如果某段路线你记得不牢,就直接说明这一段   建议对照图文攻略确认,不要编造具体的转向步数。   编造的路线比承认不确定有害得多。

怎么确认成功:问「月见山的迷宫怎么走」。回答开头应该主动出现版本声明,类似「本攻略以《火红/叶绿》的地图结构为基准,若你在玩《金/银》,二周目的月见山内部已大幅缩水」。

看到它主动交代前提,就说明这条指令生效了。

STEP 6 · 部署上线之前

准备部署上线用之前先想清楚一件事:你的 /api/chat是一个按次花钱的公开接口,默认没有任何限制。任何人拿到网址,写个循环脚本就能把你的额度或钱刷光。

最少要加三层:

防护
作用
怎么做
机器人验证
挡掉脚本和爬虫
部署在 Vercel 可用 BotID,几行配置
限流
挡单个来源刷量
按 IP 限每分钟次数,再加一个全站总量限制
输入长度上限
防止一次塞进巨量内容
限制消息条数和总字数

⚠️

**限流有个反直觉的地方:**按分钟限流保护不了日额度。全站限 20 次/分钟,一天仍然能放行 28800 次,而日额度只有 20 次。

分钟限流防的是突发流量。守日额度只能靠付费层。

Vible Coding 的几条经验

这个项目本身就是 Vible Coding 做出来的。几条能明显提速的经验:

别信它对库 API 的记忆

AI SDK 这类库更新很快,助手记住的往往是旧版本的写法,写出来跑不通再来回改,很浪费时间。

直接让它去读你项目里装好的那份文档。很多库会把文档随包发布,就在 node_modules/**包名**/docs/下面。让 AI 先读那里,再动手写。

模型名让它去查,不要让它猜

同理。模型名过期得很快,猜错会得到 404。直接让它调 provider 的模型列表接口。

让验证不花钱

额度紧张的时候,验证方式本身要设计。几个实用技巧:

  • • **用假 key 验证整条链路。**填一个格式对但无效的 key,如果拿到「key 无效」的报错,说明请求确实发出去了,除了 key 之外全通。这一步不花额度。
  • • **把限流阈值临时设成 0 来测限流。**请求在调模型之前就被拦下,所以完全不消耗额度。
  • • **强制触发边界情况。**要测截断提示,把输出上限临时设成 400,一次就能触发,不用等它自然发生。

让它说清哪些没验证

类型检查通过、构建成功,只能证明代码语法对、能打包,证明不了点下去真的能用。剪贴板、滚动、本地存储这类浏览器行为,得有人在浏览器里点一下。

所以让它把「验证到什么程度」和「没验证什么」分开说,别接受笼统一句「已完成」。

最后

有了 AI 之后,检索最好的办法就是让 AI 给你实现一个私人助手,它可以量身定制,比现在网上很多攻略网站要更好用。后续还可以加入记忆功能,让 AI 越来越懂你,搜索起来更准确。

【作者介绍】

老金,曾在大厂工作10年,见证了移动互联网的跌宕起伏,现在专注于个人独立开发,追求技术自由。致力于学习和传播 AI 、软件工程和工程管理方面的知识。如果能引起你的共鸣,请关注我的公众号:

【往期热门文章】

如何入门 Agent Skills

Claude 官方给出的常用工作流

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

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

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