AI 应用开发实战(1):用 FastAPI + TaoToken 统一 Key 从零搭建第一个 LLM API 应用
1. 为什么零基础要先搭一个“能跑起来”的 LLM API 应用很多人学 AI 应用开发第一步就卡在“调 API”这件事上文档看了一堆Key 申请了好几个代码复制过来却跑不通报错信息还看不懂。问题往往不在模型本身而在于缺少一个最小可运行骨架——一个能把请求发出去、把结果收回来、出错时知道去哪查的完整闭环。FastAPI 就是搭这个骨架最省事的工具。它用 Python 类型注解做参数校验自带交互式接口文档异步支持好几十行代码就能起一个 HTTP 服务。配合一个统一的 LLM API 入口你不需要为每个模型厂商写一套调用逻辑换模型只改配置不改业务代码。这篇文章面向的是完全没接触过 LLM API 的读者会一点 Python 基础装过 pip但没写过 Web 服务也没关系。我们会从目录结构开始一步步写出可复制的路由、配置和错误处理最后用 curl 和浏览器把接口跑通。你最终得到的是一个能对话、能流式输出、出错有明确提示的 AI 应用骨架后续加前端、加记忆、加工具调用都在这上面长。我试过把调用逻辑直接写在路由里结果换模型时改了七八处还漏了一处导致线上报错。所以这篇的重点之一就是把 LLM 调用收敛到一个文件里用统一 Key 和统一 Base URL 管理这也是后面所有实战的地基。2. TaoToken 前置准备统一 Key 与 Base URL 怎么拿TaoToken 在这里扮演的角色是统一的模型接入层你只需要一个 API Key 和一个 Base URL就能用 OpenAI 兼容的 SDK 调用不同模型。对零基础读者来说好处是不用分别注册多个厂商、不用记多套鉴权方式代码里的client只初始化一次。第一步打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录。登录后进入控制台找到 API Keys 页面deep linkhttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite创建一个新 Key。Key 只在创建时完整显示一次复制后立刻存到本地密码管理器或临时文本里不要贴在聊天窗口或提交到 Git。第二步确认 Base URL。TaoToken 的 API 入口是 https://taotoken.net/api这个地址不加 UTM 参数直接作为 SDK 的base_url使用。注意末尾不要自己补/v1SDK 会按 OpenAI 兼容规范拼接路径多写一段反而会 404。第三步确认你要用的 Model ID。在模型对话页面deep linkhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite可以看到当前可用的模型列表把模型名称原样记下来比如gpt-4o-mini这类字符串后面写进.env的LLM_MODEL。这里有个容易踩的坑Key、Base URL、Model ID 是三件套缺一不可而且必须来自同一个入口。有人把 A 平台的 Key 配 B 平台的 Base URL请求会直接返回 401有人 Model ID 拼错一个字母报错是model_not_found。所以建议你在.env里把三项写在一起注释清楚来源换环境时整段替换。安全提醒.env必须写进.gitignore。生产环境不要把 Key 硬编码进镜像用环境变量或密钥管理服务注入。TaoToken 是正规的模型接入服务Key 泄露同样有被滥用的风险这一点和任何 API 服务一样。3. 可复制配置项目结构、依赖与 FastAPI 路由先建目录。结构尽量扁平零基础读者一眼能看懂每个文件干什么ai-chat-app/ ├── main.py # FastAPI 入口与路由 ├── llm.py # LLM 调用封装唯一改模型的地方 ├── requirements.txt ├── .env # 本地配置不提交 Git ├── .gitignore └── static/ └── index.html # 可选的最小前端requirements.txt固定版本避免今天能跑明天依赖升级挂掉fastapi0.115.0 uvicorn[standard]0.30.0 python-dotenv1.0.0 openai1.50.0安装命令python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt.env三件套加站点名路径与项目根目录一致LLM_API_KEYsk-你的TaoToken密钥 LLM_BASE_URLhttps://taotoken.net/api LLM_MODEL你的模型ID SITE_NAMEAI Chat.gitignore至少包含.env venv/ __pycache__/接下来是核心的llm.py。所有模型调用都收在这里业务代码只调函数不碰 SDKimport os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL), ) MODEL os.getenv(LLM_MODEL) def chat_sync(messages, temperature0.7, max_tokens1024): resp client.chat.completions.create( modelMODEL, messagesmessages, temperaturetemperature, max_tokensmax_tokens, ) return resp.choices[0].message.content def chat_stream(messages, temperature0.7, max_tokens1024): resp client.chat.completions.create( modelMODEL, messagesmessages, temperaturetemperature, max_tokensmax_tokens, streamTrue, ) for chunk in resp: if chunk.choices and chunk.choices[0].delta.content: yield chunk.choices[0].delta.contentmain.py提供两个路由非流式/api/chat和流式/api/chat/stream外加一个健康检查/health方便验证服务是否起来import os from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, StreamingResponse from dotenv import load_dotenv from llm import chat_sync, chat_stream load_dotenv() app FastAPI(titleos.getenv(SITE_NAME, AI Chat)) app.get(/health) async def health(): return {status: ok, model: os.getenv(LLM_MODEL)} app.post(/api/chat) async def chat(request: Request): body await request.json() message (body.get(message) or ).strip() if not message: return JSONResponse({error: message 不能为空}, status_code400) try: reply chat_sync([{role: user, content: message}]) return {reply: reply} except Exception as e: return JSONResponse({error: str(e)}, status_code502) app.post(/api/chat/stream) async def chat_stream_endpoint(request: Request): body await request.json() message (body.get(message) or ).strip() if not message: return JSONResponse({error: message 不能为空}, status_code400) def generate(): try: for piece in chat_stream([{role: user, content: message}]): yield fdata: {piece}\n\n except Exception as e: yield fdata: [ERROR] {e}\n\n yield data: [DONE]\n\n return StreamingResponse( generate(), media_typetext/event-stream, headers{Cache-Control: no-cache, X-Accel-Buffering: no}, )启动命令uvicorn main:app --host 0.0.0.0 --port 8000 --reload这里的设计取舍值得说清楚。把client放在模块级只初始化一次避免每个请求都新建连接chat_sync和chat_stream分开是因为批量任务和对话场景对延迟的容忍度完全不同路由层只做参数校验和异常包装不写任何模型细节这样以后加会话历史、加系统提示词都改在调用层而不是散落在路由里。流式接口用 SSE 而不是 WebSocket是因为它基于普通 HTTP、单向推送正好匹配“服务端持续吐字”的需求前端用fetch读流即可不需要额外协议。4. 验证请求从 curl 到浏览器把接口跑通服务起来后先打健康检查确认进程和配置都加载了curl http://127.0.0.1:8000/health正常返回类似{status:ok,model:你的模型ID}。如果这里就报错说明.env没被读到或依赖没装全先解决再往下走。接着测非流式对话curl -X POST http://127.0.0.1:8000/api/chat \ -H Content-Type: application/json \ -d {message:用一句话解释什么是 FastAPI}成功时返回{reply:...}内容是一段完整回答。这一步能通说明 Key、Base URL、Model ID 三件套全部正确SDK 调用链路没问题。再测流式接口用-N关闭 curl 缓冲能肉眼看到逐段输出curl -N -X POST http://127.0.0.1:8000/api/chat/stream \ -H Content-Type: application/json \ -d {message:数一下 1 到 5}你会看到多行data: ...最后以data: [DONE]结束。如果所有内容一次性涌出来通常是中间有代理或 Nginx 开了缓冲本地直连一般不会。浏览器验证更直观打开 http://127.0.0.1:8000/docsFastAPI 自动生成的 Swagger 文档里可以直接点 “Try it out” 填参数发请求返回体和状态码一目了然。对零基础读者这个页面是最好的调试入口比手写 curl 少踩引号和转义的坑。想加最小前端的话在static/index.html里用fetch读流即可核心逻辑是resp.body.getReader()循环解码按\n\n切分事件、取data:后面的内容追加到页面。这一步不是必须的接口通了就说明骨架成立前端只是展示层。5. 本篇常见错排查401、连接失败与 choices 报错401 Unauthorized / invalid_api_key九成是 Key 问题。检查.env里LLM_API_KEY有没有多余空格或引号确认 Key 没有过期或被删除确认它和LLM_BASE_URL来自同一个平台。改完.env要重启服务load_dotenv()只在启动时读一次。Connection error / local proxy failed这类报错说明请求根本没到服务端。先确认本机网络能正常访问外网再检查环境变量里有没有残留的代理设置干扰 SDK。代码层面确认base_url写的是https://taotoken.net/api没有多写/v1或少了协议头。如果是在容器里跑确认容器网络能出网。IndexError: list index out of range 或 reading choices说明返回体结构和预期不符resp.choices是空的。常见原因是 Model ID 写错导致返回了错误对象或者请求被拦截返回了非标准 JSON。把resp整个打印出来看原始结构比盯着报错猜快得多。另外max_tokens设得过小、触发内容过滤时也可能出现空 choices。OAuth / 鉴权方式不匹配如果你用的是某些需要额外鉴权头的客户端注意 TaoToken 走的是标准 Bearer TokenOpenAI(api_key...)会自动加Authorization头不要手动再塞一遍导致重复。Claude Code、Cline 这类工具接入时Base URL、Key、Model ID 三件套要填全缺一项就会在鉴权阶段失败。流式输出卡住不动先看服务端日志有没有异常再确认响应头带了X-Accel-Buffering: no。本地直连还卡的话检查chat_stream里是否真的传了streamTrue以及循环里对delta.content为None的情况做了判断——有些 chunk 只带角色信息不带内容直接拼接会报错。排查顺序建议固定为健康检查 → 非流式 → 流式 → 前端。每一层单独验证出错时范围立刻缩小一半比一上来就调前端高效得多。6. 下一步把这套骨架用起来骨架跑通后最值得先做的两件事一是把llm.py里的模型切换做成配置项用环境变量控制这样同一套代码能在不同模型间对比效果二是给/api/chat加上简单的会话历史用字典按session_id存最近几轮消息注意按 token 数截断别等超限了才处理。需要继续拿 Key 或查接入细节走 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite和接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite想先验证模型效果直接去模型对话https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite试几句如果你打算长期做编码类或 Agent 类应用Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite更适合按量规划成本。最后留一个实操建议把本文的curl命令存成一个smoke-test.sh每次改完配置先跑一遍三秒钟就能确认链路是否健康。这个习惯比任何调试技巧都省时间。