1. 前端转大模型应用层,先想清楚提示词工程到底解决什么问题
前端程序员在大模型时代最该焦虑的,其实不是“AI 会不会写页面”,而是“我能不能把 AI 的输出变成可观测、可复现、可治理的工程链路”。提示词工程这个词被说烂了,但落到前端手里,它不是一个玄学调参游戏,而是一套可以像接口联调一样被拆解、被记录、被回归测试的工程动作。你能用 React DevTools 看组件树,就能用同样的思路去看一次大模型请求的输入、输出、耗时和 token 消耗。
我试过把提示词调试当成“改文案”,结果就是每次换模型、换参数、换上下文,效果全凭感觉。后来把它当成“接口契约”来对待,事情就清晰了:系统提示词是接口文档,用户输入是请求参数,模型返回是响应体,温度、top_p、max_tokens 是查询参数。前端最擅长的就是把这些东西可视化、可对比、可回放。这篇文章要做的,就是带你从零搭一个可复用的提示词调试与性能观测小工具,用 TaoToken 作为统一的 Key 和 API 通道,把模型调用这件事从前端视角管起来。
适合谁看?如果你写过 fetch、用过 Vite、能看懂 TypeScript 类型,那就够了。不需要你懂模型训练,也不需要你懂 Python 后端。我们要做的是一个跑在浏览器里的调试面板,能发请求、能计时、能对比不同提示词的效果,还能把配置片段复制出来直接用在项目里。核心检索词就是“前端 大模型 提示词工程 性能治理”,这四个词会贯穿整个实践过程。
先说清楚一个认知:大模型应用层的前端,价值不在于“会调 API”,而在于“能把不确定的模型输出变成确定的用户体验”。模型可能返回 JSON 里带 markdown 代码块,可能超时,可能限流,可能返回一半就断了。这些边界情况,后端不一定帮你兜,产品经理也不一定想得到,但用户会直接感受到。前端离用户最近,所以前端来做提示词调试和性能观测,天然有优势。
这个工具的目标很具体:输入一段系统提示词和用户提示词,选择模型,点发送,看到响应内容、首 token 延迟、总耗时、token 用量,并且能把这次请求的完整配置导出成可复制的片段。做完之后,你手里就有了一条可验证的通道,以及一个能持续迭代提示词的实验台。下面从环境准备开始。
2. TaoToken 前置准备:统一 Key 与 Base URL 的接入通道
在动手写代码之前,先把通道打通。前端直接调模型 API 最容易踩的坑,一是 Key 暴露在前端代码里,二是不同模型厂商的 Base URL 和鉴权方式不一样,三是跨域和流式响应处理起来琐碎。TaoToken 在这里的角色是统一入口:一个 API Key,一个 Base URL,兼容 OpenAI 风格的接口协议,前端用标准的 fetch 或 OpenAI SDK 就能接。
你需要先拿到 API Key。打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册登录后进入控制台,在 API Keys 页面创建一个新的 Key。创建时建议命名成“prompt-debug-tool”这类能识别用途的名字,方便后面轮换和吊销。Key 只在创建时完整显示一次,复制后先存到本地密码管理器里,不要直接写进前端仓库。
拿到 Key 之后,记下两个地址。API 基础地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数。模型对话的调用路径是 /v1/chat/completions,和 OpenAI 的路径保持一致。也就是说,你最终请求的完整 URL 是 https://taotoken.net/api/v1/chat/completions 。这个设计的好处是,你现有的 OpenAI SDK 代码只需要改 baseURL 和 apiKey 两个地方就能迁移过来。
关于模型 ID,控制台里会有可用的模型列表。不同模型的上下文长度、价格、擅长任务不一样。做提示词调试时,建议先固定一个模型,把提示词打磨稳定后再换模型做对比。模型 ID 一般形如厂商名/模型名,具体以控制台展示为准。不要凭记忆猜模型 ID,写错了会直接返回模型不存在的错误。
这里要强调一个安全实践:前端项目里绝对不要把 Key 硬编码进源码然后推到公开仓库。正确的做法是本地开发用 .env.local,构建时通过环境变量注入,生产环境走你自己的后端代理。我们这个调试工具是本地跑的小工具,可以用 Vite 的环境变量机制,把 Key 放在 .env.local 里,并且把 .env.local 加进 .gitignore。下面给出具体的环境变量配置片段。
在项目根目录创建 .env.local 文件,内容如下:
VITE_TAOTOKEN_API_KEY=sk-你的实际Key VITE_TAOTOKEN_BASE_URL=https://taotoken.net/api VITE_TAOTOKEN_MODEL=你的模型ID注意 Vite 只暴露以 VITE_ 开头的变量给客户端代码,这是它的约定。如果你用的是其他构建工具,前缀规则不同,比如 Create React App 用 REACT_APP_,Next.js 用 NEXT_PUBLIC_。原理一样,都是把配置从代码里抽出来。写完之后在 .gitignore 里确认有 .env.local 这一行。这一步看起来简单,但很多人的 Key 泄露就是从“先跑通再说”开始的。
如果你更习惯用 OpenAI 官方 SDK,也可以。安装 openai 包之后,初始化客户端时传入 baseURL 和 apiKey:
import OpenAI from 'openai'; const client = new OpenAI({ apiKey: import.meta.env.VITE_TAOTOKEN_API_KEY, baseURL: import.meta.env.VITE_TAOTOKEN_BASE_URL, dangerouslyAllowBrowser: true, });dangerouslyAllowBrowser 这个参数名字就是在提醒你,浏览器里直接放 Key 是有风险的。本地调试工具可以接受,但上线产品必须换成后端代理。这一点心里要有数。通道准备好之后,进入下一步,写可复制的配置和请求代码。
3. 可复制配置:从环境变量到请求体的完整片段
这一节把配置写全,你照着复制就能跑。先明确三件套:Base URL、API Key、Model ID。Base URL 是 https://taotoken.net/api ,API Key 从控制台拿,Model ID 从控制台模型列表选。这三个值分别对应环境变量里的 VITE_TAOTOKEN_BASE_URL、VITE_TAOTOKEN_API_KEY、VITE_TAOTOKEN_MODEL。
接下来是请求体的构造。OpenAI 兼容接口的请求体核心字段有 model、messages、temperature、max_tokens、stream。messages 是一个数组,每个元素有 role 和 content,role 可以是 system、user、assistant。系统提示词放在 system 角色里,用户输入放在 user 角色里。下面是一个完整的请求函数,包含计时逻辑:
export interface ChatRequestOptions { systemPrompt: string; userPrompt: string; temperature?: number; maxTokens?: number; stream?: boolean; } export interface ChatResult { content: string; firstTokenLatency: number | null; totalLatency: number; usage: { promptTokens: number; completionTokens: number; totalTokens: number } | null; } export async function chatWithTiming(options: ChatRequestOptions): Promise<ChatResult> { const baseUrl = import.meta.env.VITE_TAOTOKEN_BASE_URL; const apiKey = import.meta.env.VITE_TAOTOKEN_API_KEY; const model = import.meta.env.VITE_TAOTOKEN_MODEL; const start = performance.now(); let firstTokenAt: number | null = null; const response = await fetch(`${baseUrl}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ model, messages: [ { role: 'system', content: options.systemPrompt }, { role: 'user', content: options.userPrompt }, ], temperature: options.temperature ?? 0.7, max_tokens: options.maxTokens ?? 1024, stream: options.stream ?? false, }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`请求失败 ${response.status}: ${errorText}`); } const data = await response.json(); const totalLatency = performance.now() - start; return { content: data.choices?.[0]?.message?.content ?? '', firstTokenLatency: firstTokenAt, totalLatency, usage: data.usage ? { promptTokens: data.usage.prompt_tokens, completionTokens: data.usage.completion_tokens, totalTokens: data.usage.total_tokens, } : null, }; }这段代码里,performance.now() 是浏览器原生高精度计时器,比 Date.now() 更适合测耗时。totalLatency 覆盖了从发请求到拿到完整响应的全过程。firstTokenLatency 在非流式模式下拿不到,因为响应是一次性返回的,所以先留空。如果你要测首 token 延迟,需要开 stream 模式,逐块读取响应体,第一块到达时记录时间。流式读取的代码稍微复杂一点,但前端处理 ReadableStream 是基本功。
流式模式下,响应体是一个 ReadableStream,你需要用 getReader() 逐块读取,然后按 SSE 格式解析。每一行以 data: 开头,内容是 JSON,最后以 data: [DONE] 结束。解析时要注意跨 chunk 的拼接,因为一个 JSON 对象可能被切在两个 chunk 里。这个坑很常见,处理方式是维护一个缓冲区,按换行符切分,不完整的部分留到下一轮。
配置片段除了代码,还要有可复制的 JSON 形式,方便你在 Postman 或 curl 里验证。下面这个 JSON 就是请求体的完整结构:
{ "model": "你的模型ID", "messages": [ { "role": "system", "content": "你是一个严谨的前端代码审查助手,只输出问题点和修改建议。" }, { "role": "user", "content": "审查这段 React 代码的竞态问题:..." } ], "temperature": 0.3, "max_tokens": 1024, "stream": false }对应的 curl 命令如下,把 Key 和模型 ID 替换成你自己的:
curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的Key" \ -d '{ "model": "你的模型ID", "messages": [ { "role": "system", "content": "你是一个前端代码审查助手。" }, { "role": "user", "content": "这段代码有什么风险?" } ], "temperature": 0.3, "max_tokens": 512 }'curl 验证的好处是排除前端代码的干扰,先确认通道本身是通的。如果 curl 能返回正常结果,说明 Base URL、Key、Model ID 三件套没问题,再去排查前端代码。这个顺序能帮你省很多时间。配置写完之后,下一步是实际发一次请求,看结果和耗时。
4. 验证请求链路:一次调用看响应内容与耗时
现在把上面的代码接进一个最小的 Vite 页面,跑一次真实请求。先建项目,用 Vite 的 React + TypeScript 模板:
npm create vite@latest prompt-debug-tool -- --template react-ts cd prompt-debug-tool npm install然后把前面的 chatWithTiming 函数放到 src/lib/chat.ts 里,在 App.tsx 里写一个简单的表单:两个 textarea 分别输入系统提示词和用户提示词,一个按钮触发请求,下方展示响应内容和耗时。核心逻辑如下:
import { useState } from 'react'; import { chatWithTiming, type ChatResult } from './lib/chat'; function App() { const [systemPrompt, setSystemPrompt] = useState('你是一个前端性能优化顾问。'); const [userPrompt, setUserPrompt] = useState('一个列表页每次滚动都重新请求接口,怎么优化?'); const [result, setResult] = useState<ChatResult | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const handleSend = async () => { setLoading(true); setError(null); setResult(null); try { const res = await chatWithTiming({ systemPrompt, userPrompt, temperature: 0.5 }); setResult(res); } catch (e) { setError(e instanceof Error ? e.message : '未知错误'); } finally { setLoading(false); } }; return ( <div style={{ padding: 24, fontFamily: 'sans-serif' }}> <h2>提示词调试台</h2> <textarea value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} rows={3} style={{ width: '100%' }} /> <textarea value={userPrompt} onChange={(e) => setUserPrompt(e.target.value)} rows={5} style={{ width: '100%', marginTop: 8 }} /> <button onClick={handleSend} disabled={loading} style={{ marginTop: 8 }}> {loading ? '请求中...' : '发送'} </button> {error && <p style={{ color: 'red' }}>{error}</p>} {result && ( <div style={{ marginTop: 16 }}> <p>总耗时:{result.totalLatency.toFixed(0)} ms</p> <p>Token 用量:{result.usage ? `${result.usage.totalTokens}(输入 ${result.usage.promptTokens} / 输出 ${result.usage.completionTokens})` : '未返回'}</p> <pre style={{ whiteSpace: 'pre-wrap', background: '#f5f5f5', padding: 12 }}>{result.content}</pre> </div> )} </div> ); } export default App;启动开发服务器:
npm run dev打开浏览器,填入提示词,点发送。如果一切正常,你会看到响应内容、总耗时和 token 用量。实测下来,一次几百 token 的请求,总耗时通常在 1 到 3 秒之间,具体取决于模型和网络。这个数字就是你的基线,后面优化提示词、换模型、调参数,都拿它做对比。
验证成功的标志有三个:响应内容非空且语义合理,totalLatency 有正常数值,usage 字段返回了 token 统计。如果 usage 是 null,说明接口没返回用量信息,不影响功能,但你就没法做成本观测了。如果响应内容是空字符串,先检查 choices 数组结构,不同兼容实现的字段可能略有差异,打印完整 data 看一眼就知道。
这一步做完,你手里就有了一条可验证的通道和一个能看耗时的调试台。接下来把常见错误过一遍,这些是我踩过的坑,提前知道能省不少时间。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
第一个高频错误是 401 Unauthorized。报错信息通常是{"error":{"message":"Invalid API key","type":"invalid_request_error"}}。原因无非三种:Key 复制时带了空格或换行,Key 已经被吊销,或者 Authorization 头格式写错了。正确格式是Bearer sk-xxx,Bearer 和 Key 之间一个空格,Key 本身不要加引号。排查方法是用 curl 单独测一次,排除前端代码干扰。如果 curl 也 401,那就是 Key 本身的问题,去控制台重新创建一个。
第二个错误是 local proxy failed 或类似的网络层报错。这个通常出现在你本地配了开发代理,但代理目标写错或代理没启动。Vite 的 proxy 配置在 vite.config.ts 里,如果你把 /api 代理到了错误的地址,请求根本到不了 TaoToken。排查方法是打开浏览器开发者工具的 Network 面板,看请求的实际 URL 是什么。如果 URL 是 localhost 开头而不是 taotoken.net,说明代理规则拦截了。临时方案是直接请求完整地址,不走代理。另外检查一下 Base URL 有没有多写或少写 /api,正确值是 https://taotoken.net/api ,请求路径再拼 /v1/chat/completions。
第三个错误是 reading choices 相关的报错,典型信息是Cannot read properties of undefined (reading 'choices')或者Cannot read properties of undefined (reading '0')。这说明你拿到的响应体结构和你预期的不一样。可能原因:请求根本没成功,返回的是错误对象而不是正常响应;或者你忘了 await response.json(),拿到的还是 Response 对象;或者流式模式下你把 SSE 的 chunk 当成了完整 JSON 解析。排查方法是先把原始响应文本打印出来,看看到底返回了什么。非流式模式下,正常响应一定有 choices 数组,choices[0].message.content 才是正文。
第四个错误和 OAuth 有关。如果你在用某些 CLI 工具或 IDE 插件,可能会遇到 OAuth 相关的鉴权失败。这类工具有的走 OAuth 流程而不是 API Key,配置方式不一样。如果你只是用 API Key 调接口,不会碰到 OAuth。但如果你在配置 Claude Code 这类工具,它可能要求你设置 ANTHROPIC_BASE_URL 和 ANTHROPIC_API_KEY 环境变量,这时候 Base URL 要填 https://taotoken.net/api ,Key 填你的 TaoToken Key,模型 ID 按工具要求填。三件套缺一不可,少一个就会鉴权失败或模型找不到。
还有一个容易被忽略的错误是跨域。浏览器直接请求第三方 API 时,如果对方没返回正确的 CORS 头,请求会被浏览器拦截,控制台报has been blocked by CORS policy。TaoToken 的接口是支持跨域的,但如果你在本地用了代理又配置不当,可能引入额外的跨域问题。排查时看 Network 面板里请求是否真的发出去了,如果状态是 (failed) 且没有响应,多半是跨域或网络层问题。
把这些错误对照着排查一遍,基本能覆盖 90% 的接入问题。剩下的 10% 通常是模型 ID 写错、参数超范围、或者账户余额不足。模型 ID 写错会返回 model not found,参数超范围比如 temperature 填了 3 会返回参数校验错误,余额不足会返回 quota 相关提示。遇到报错先看响应体里的 message 字段,它通常会说清楚原因。
6. 把调试台变成长期能力:从提示词实验到性能观测
工具跑通之后,别让它停在“能发请求”这一步。前端做提示词工程,真正的价值在于把一次性的调试变成可积累的资产。你可以给调试台加几个能力:请求历史记录,把每次的提示词、参数、响应、耗时存到 localStorage,支持回放和对比;提示词版本管理,给每个提示词打标签,比如“v1 严格模式”“v2 宽松模式”,切换时能看到效果差异;耗时趋势图,把同一提示词多次请求的耗时画成折线,观察模型响应的稳定性。
这些能力不需要后端,纯前端就能做。localStorage 存历史,IndexedDB 存大文本,Chart.js 或轻量的 SVG 画趋势。做完之后,你就有了一套自己的提示词实验基础设施。换模型时,跑一遍回归用例,看哪些提示词失效了;调温度时,对比不同温度下的输出稳定性;优化系统提示词时,用耗时和 token 用量衡量成本变化。这就是前端视角的性能治理,和治理页面加载性能是同一套思路。
长期来看,这套能力可以沉淀成团队内部的提示词规范。比如规定系统提示词必须包含角色定义、输出格式约束、边界情况处理三部分;规定所有模型调用必须记录耗时和 token 用量;规定提示词变更必须跑回归用例。这些规范听起来像后端的事,但前端来做最合适,因为前端最清楚用户实际看到的是什么。
如果你要把这套东西用在正式项目里,记得把 Key 从浏览器挪到后端代理。前端只调你自己的后端,后端再转发到 TaoToken。这样 Key 不暴露,还能在后端做限流、缓存、审计。前端负责交互和观测,后端负责安全和稳定,分工清晰。
最后给一个可以直接用的验证动作:打开你的调试台,系统提示词填“你是一个前端代码审查助手,只输出问题点和修改建议,不输出完整代码”,用户提示词贴一段有竞态问题的 useEffect 代码,点发送。看响应是否只列问题不贴代码,看总耗时是否在合理范围,看 token 用量是否和输入长度匹配。如果三点都符合预期,说明通道、提示词、观测三件事都到位了。这个动作可以固化成你的冒烟测试,每次改配置后跑一遍。
通道和工具都齐了之后,下一步就是把它用起来。你可以从模型对话页面快速验证不同模型的表现,也可以把配置片段接进现有项目。API Keys 和接入文档在控制台和文档页都能找到,长期做编码和 Agent 的话,Coding Plan 会更划算。工具是死的,提示词是活的,真正拉开差距的是你用它解决了多少真实问题。